常见查找算法

1 常见查找概念

  • 查找指根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
  • 查找表是由同一类型的数据元素或记录构成的集合。
  • 关键字是数据元素中某个数据项的值,又称为键值,主关键字可以唯一地标识一个记录,次关键字可以标识多个数据元素或记录。
  • 静态查找表:只做查找操作的查找表,可用线性表结构来组织数据。
  • 动态查找表:在查找的过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。可以用二叉排序树来组织数据。
  • 从逻辑上来说,查找所基于的数据结构是集合,集合中的记录之间没有本质关系。若想获得较高的查找性能,我们可以改变数据元素之间的关系,在存储时将查找集合组织成表、树等结构。

2 顺序表查找算法

  • 是最基本的查找技术,适用于小型数据的查找。可以将容易被查找到的记录放在前面以提高效率。
  • 时间复杂度为O(n)
bool sequentialSearch1(const int* a, int n, int key, int& pos)
{
	for(int i = 0; i < n; ++i)
	{
		if(a[i] == key)
		{
			pos = i;
			return true;
		}
	}
	return false;
}

//对顺序表查找算法进行优化,已设置哨兵在a[0]
bool sequentialSearch2(const int* a, int n, int key, int& pos)
{
	int i = n;
	while(a[i] != key)
		--i;
	if(i == 0)
		return false;
	else
	{
		pos = i;
		return true;
	}
}

3 有序表查找

3.1 折半查找

  • 又称为二分查找,记录必须关键字有序并且采用顺序存储,适用于静态查找表,对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来较大的工作量,不适宜使用。
  • 时间复杂度为O(logn)
bool binarySearch(const int* a, int n, int key, int& pos)
{
	int low = 0;
	int high = n - 1;
	while(low <= high)
	{
		int mid = (low + high) / 2;
		if(key < a[mid])
			high = mid - 1;
		else if(key > a[mid])
			low = mid + 1;
		else
		{
			pos = mid;
			return true;
		}
	}
	return false;
}

3.2 插值查找

  • 基本思想是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法。
  • 对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。
bool insertSearch(const int* a, int n, int key, int& pos)
{
	int low = 0;
	int high = n - 1;
	while(low <= high)
	{
		int mid = low + (high - low) * (key - a[low]) / (a[high] - a[low]);
		if(key < a[mid])
			high = mid - 1;
		else if(key > a[mid])
			low = mid + 1;
		else
		{
			pos = mid;
			return true;
		}
	}
	return false;
}

3.3 斐波那契查找

  • 利用了黄金分割的原理实现,平均性能优于折半查找
  • 三种有序表查找本质上是分割点选择的不同,就平均性能来说,斐波那契查找优于折半查找。
bool fibonacciSearch(const int* a, int n, int key, int& pos)
{
	int low = 0;
	int high = n - 1;
	int k = 0;
	while(n > F[k] - 1)		//计算n位于斐波那契数列的位置
		++k;
	for(int i = n; i < F[k] - 1; ++i)	//将不满的数值补全
		a[i] = a[n];
}

4 二叉排序树BST

  • 二叉排序树的插入和删除效率不错,又可以比较高效率地实现查找算法,用于动态查找表中。
  • 二叉排序树又被称为二叉查找树,它或者是一颗空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左右子树也分别为二叉排序树。
  • 构造二叉排序树的目的不是为了排序,而是为了提高查找和插入删除关键字的速度。
  • 二叉排序树的查找走的就是从根节点到要查找结点的路径,比较次数等于给定值结点在二叉排序树的深度,即二叉排序树的查找性能取决于二叉排序树的形状。对于递增的数组,二叉排序树退化为单链表,使层数迅速增加,我们希望二叉排序树是平衡的,深度与完全二叉树相同,为[log2n] + 1
//二叉树的二叉链表结点结构定义
typedef struct _BiTNode
{
	int data;
	struct BiTNode* lchild;
	struct BiTNode* rchild;
} BiTNode, *BiTree;

//BST的查找操作
//f指向T的双亲,初始调用值为NULL
//若查找成功,p指向该元素结点,否则p指向查找路径上访问的最后一个结点
//想要改变传入p的指向,所以这里p为二重指针
bool searchBst(const BiTree T, int key, BiTree f, BiTree* p)
{
	if(T == nullptr)	//查找不成功
	{
		*p = f;
		return false;
	}
	else if(key == T->data)	 //查找成功
	{
		*p = T
		return true;
	}
	else if(key < T->data)
		return searchBst(T->lchild, key, T, p);		//在左子树继续查找
	else
		return searchBst(T->rchild, key, T, p);		//在右子树继续查找
}

//BST的插入操作
//构造BST的过程就是排序过程
//当T中不存在key时,将其插入并返回true,否则返回false
//因为可能要改动根节点,所以T为二重指针
bool insertBst(BiTree* T, int key)
{
	BiTNode* p = nullptr;	//记录访问的最后一个结点
	if(!searchBst(*T, key, nullptr, &p))
	{
		BiTNode* s = new BiTNode;
		s->data = key;
		s->lchild = s->rchild = nullptr;
		if(p == nullptr)	
			*T = s;	//无根结点时插入s为新根结点
		else if(key < p->data)
			p->rchild = s;
		else
			p->lchild = s;
		return true;
	}
	return false;
}

//BST的删除操作
//当T中存在key时,删除该结点并返回true,否则返回false
bool deleteBst(BiTree* T, int key)
{
	if(T == nullptr || *T == nullptr)	//不存在时
		return false;
	else
	{
		if(key == (*T)->data)
			return deleteNode(T);
		else if(key < (*T)->data)
			return deleteBst(&(*T)->lchild, key);
		else
			return deleteBst(&(*T)->rchild, key);
	}
}
//从BST中删除结点p,并重接它的左右子树
bool deleteNode(BiTree* p)
{
	BiTNode* q = nullptr;
	BiTNode* s = nullptr;
	if((*p)->rchild == nullptr)	//右子树为空只需要重接它的左子树
	{
		q = *p;
		*p = (*p)->lchild;
		free(q);
	}
	else if((*p)->lchild == nullptr)    //左子树为空只需要重接它的右子树
	{
		q = *p;
		*p = (*p)->rchild;
		free(q);
	}	
	else //左右子树均不为空时,找到待删除结点p的直接前驱(或直接后继)s,用s来替换p,然后删除s
	{
		q = *p;
		s = (*p)->lchild;
		while(s->rchild)	//转左,然后向右到尽头,到达待删除结点的前驱
		{
			q = s;
			s = s->rchild;
		}
		(*p)->data = s->data;	//将前驱结点的值赋给待删除结点
		
		if(q != *p)
			q->rchild = s->lchild;	//重接q的右子树
		else
			q->lchild = s->lchild;	//重接q的左子树
		free(s);	//删除前驱节点
	}
	return true;
}

5 平衡二叉树

  • 对于动态查找来说,我们在频繁查找的同时也需要经常插入和删除结点。在构建二叉排序树时如果该树是不平衡的,那么查找效率就会很低,因此我们在构建时就让这棵二叉树是平衡的。
  • 平衡二叉树是一种特殊的二叉排序树,每一个节点的左子树和右子树的高度差至多为1。
  • 在平衡二叉树中进行查找、插入、删除的时间复杂度为O(logn)
  • 平衡因子定义为二叉树上结点左子树深度减去右子树深度的值,平衡二叉树所有结点的平衡因子只可能为-1,0,1。

5.1 AVL树

  • 最小不平衡子树定义为距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树。
  • AVL树是一种平衡二叉树,构建的基本思想是将不平衡消灭在最早时刻。每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持BST特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
typedef struct _BiTNode
{
	int data;
	int bf;		//平衡因子
	struct BiTNode* lchild, *rchild;
} BiTNode, *BiTree;

//插入在左边时BF>0,故要右旋
//对以p为根的BST作右旋处理,处理后p指向新的树根结点,即旋转之前的左子树根节点
void rightRotate(BiTree* p)
{
	BiTNode* L;
	L = (*p)->lchild;
	(*P)->lchild = L->rchild;	//L的右子树为P的前驱,故L的右子树挂接为P的左子树
	L->rchild = *p;
	*p = L;		//p指向新的根结点
}
//插入在右边时BF<0,故要左旋
//对以p为根的BST作左旋处理,处理后p指向新的树根结点,即旋转之前的右子树根节点
void leftRotate(BiTree* p)
{
	BiTNode* R;
	R = (*p)->rchild;
	(*p)->rchild = R->lchild;	//R的左子树为P的后继,故R的左子树挂接为P的右子树
	R->lchild = *p;
	*p = R;		//p指向新的根节点
}

//左平衡旋转处理,新结点插入在左边时
//对以指针T所指结点为根的二叉树作左平衡旋转处理,处理后T指向新的根节点
#define LH   1   //左高
#define RH   -1  //右高
#define EH   0   //等高
void leftBalance(BiTree* T)
{
	BiTNode* L = (*T)->lchild;	 //L指向T的左子树根结点
	BiTNode* Lr;
	switch(L->bf)  //检查T的左子树的平衡度,并作相应平衡处理
	{
		case LH:  //新结点插入在T左孩子的左子树上,同号,要作单右旋处理
			(*T)->bf = L->bf = EH;
			rightRotate(T);
			break;
		case RH:  //新结点插入在T左孩子的右子树上,反号,要作先右旋后左旋处理
			Lr = L->rchild;		
			switch(Lr->bf)	//修改T及其左孩子的平衡因子
			{
				case LH:
					(*T)->bf = RH;
					L->bf = EH;
					break;
				case EH:
					(*T)->bf = L->bf = EH;
					break;
				case RH:
					(*T)->bf = EH;
					L->bf = LH;
					break;
			}
			Lr->bf = EH;
			leftRotate((*T)->lchild);	//对T的左子树作左平衡处理
			rightRotate(T);		//对T作右旋处理
	}
}
//右平衡旋转处理,新结点插入在右边时
//对以指针T所指结点为根的二叉树作右平衡旋转处理,处理后T指向新的根节点
void rightBalance(BiTree* T)
{
	BiTNode* R = (*T)->rchild;	 //L指向T的右子树根结点
	BiTNode* Rl;
	switch(R->bf)  //检查T的右子树的平衡度,并作相应平衡处理
	{
	    case RH:	//新节点插入在T右孩子的右子树上,同号,要做单左旋处理
			(*T)->bf = (*R)->bf = EH;
			leftRotate(T);
			break;
		case LH:	//新节点插入在T右孩子的左子树上,反号,要做先右旋后左旋处理
			Rl = R->lchild;
			switch(Rl->bf)  //修改T及其右孩子的平衡因子
			{
				case LH:
					(*T)->bf = RH;
					R->bf = EH;
					break;
				case EH:
					(*T)->bf = R->bf = EH;
					break;
				case RH:
					(*T)->bf = EH;
					R->bf = LH;
					break;
			}
			Rl->bf = EH;
			rightRotate(&(*T)->rchild);		//对T的右子树作右平衡处理
			leftRotate(T);		//对T作左平衡处理
	}
}

//平衡二叉树的插入算法
//若在平衡二叉树T中不存在和e有相同关键字的结点,则插入一个数据元素为e的结点并返回true
//若因插入而使二叉树失去平衡,则作平衡旋转处理,taller反映T长高与否
bool insertAVL(BiTree* T, int e, bool* taller)
{
	if(*T == nullptr)
	{
		*T = (BiTNode*)malloc(sizeof(BiTNode));
		(*T)->data = e;
		(*T)-lchild = (*T)->rchild = nullptr;
		(*T)->bf = EH;
		*taller = true;
	}
	else
	{
		if(e == (*T)->data)	//树中已存在和e有相同关键字的结点则不再插入
		{
			*taller = true;
			return false;
		}
		if(e < (*T)->data)   //在T的左子树继续进行搜索
		{
			if(!insertAVL(&(*T)->lchild, e, taller))
				return false;
			if(*taller)		//已插入到T的左子树,且左子树长高
			{
				switch((*T)->bf)
				{
					case LH:	//原本左子树比右子树高,需要左平衡处理,左右子树等高
						leftBalance(T);
						*taller = false;
						break;
					case EH:	//原本左子树与右子树等高,现因左子树增高而树增高
						(*T)->bf = LH;
						*taller = true;
						break;
					case RH:	//原本右子树比左子树高,插入后左右子树等高
						(*T)->bf = EH;
						*taller = false;
						break;
				}
			}
		}
		else	//在T的右子树继续进行搜索
		{
			if(!insertAVL(&(*T)->rchild, e, taller))
				return false;
			if(*taller)		//已插入到T的右子树,且右子树长高
			{
				switch((*T)->bf)
				{
					case LH:	//原本左子树比右子树高,插入后左右子树等高
						(*T)->bf = EH;
						*taller = false;
						break;
					case EH:	//原本左右子树等高,现因右子树增高而树增高
						(*T)->bf = RH;
						*taller = true;
						break;
					case RH:	//原本右子树比左子树高,需要右平衡旋转处理,左右子树等高
						rightBalance(T);
						*taller = false;
						break;
				}
			}	
		}
	}
	return true;
}

5.2 红黑树

  • 红黑树是一种自平衡的二叉查找树,它除了符合二叉查找树的基本特性外,还具有下列的附加特性。
    (1)节点是红色或黑色。
    (2)根节点是黑色。
    (3)每个叶子节点都是黑色的空节点(NIL节点)。
    (4)每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
    (5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
  • 红黑树从根到叶子的最长路径不超过最短路径的两倍。
    在这里插入图片描述

6 多路查找树

  • 多路查找树结点的每个孩子数可以多于两个,且每个结点处可以存储多个元素,是一种平衡的查找树。
  • 一般在大数据量且考虑外存的情况下使用。

6.1 2-3树

  • 2-3树是一种多路查找树,其中每一个结点都有两个孩子(称为2结点)或者三个孩子(称为3结点),树中所有叶子都在同一层次上。
  • 一个2结点包含一个元素和两个孩子(或者没有孩子)。左子树包含的元素小于该元素,右子树包含的元素大于该元素。
  • 一个3结点包含一大一小两个元素和三个孩子(或者没有孩子)。左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。
  • 同理,2-3-4树类似于2-3树的定义。

6.3 B树

  • B树是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶。
  • 2-3树和2-3-4树都是B树的特例。
  • 一个m阶的B树具有如下属性:
    (1)如果根节点不是叶节点,则其至少有两颗子树。
    (2)所有叶子节点都位于同一层次。
    (3)每一个非根的分支节点都有k-1个元素和k个孩子,其中[m/2]<=k<=m,每一个叶子结点n都有k-1个元素。
    (4)所有分支节点包含下列信息数据(n,A0,K1,A1,K2,A2,…,Kn,An),其中K为关键字,Ki<Ki+1,Ai为指向子树根节点的指针。n为关键字的个数。
  • 在查找的过程中通过使用B树这种数据结构,减少了必须访问结点和数据块的数量,B树这种数据结构就是为了内外存间的数据交换而准备的。

6.4 B+树

  • 在B树中遍历时,我们可能需要往返于每个结点之间,如果结点大小与硬盘页面大小近似,这也意味着我们必须在硬盘页面之间进行多次访问。
  • B+树是应文件系统所需提出的一种B树的变形树,严格意义上讲它已不是树了。
  • B+树中出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者,而在叶子结点中再次列出,并且所有叶子结点都链接在一起。
  • B+树的结构特别适合带有范围的查找。
  • 一颗m阶的B+树和m阶的B树的差异在于:
    (1)所有的叶子结点包含全部关键字的信息,叶子结点本身依关键字的大小自小而大顺序链接。
    (2)所有分支结点可以看成是索引,结点中只含有其子树中的最大(最小)关键字。

7 哈希查找(散列查找)

  • 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个key对应一个存储位置f(key)。存储位置 = f(关键字)
  • 这个f称为散列函数,又称为哈希函数。
  • 采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为哈希表。
  • 散列技术适合一对一的查找。
  • 散列函数的构造方法:直接定址法、平方取中法、折叠法、除留余数法、随机数法
  • 处理散列冲突的方法:开放定址法、随机探测法、再散列函数法、链地址法、公共溢出区法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值