您好,欢迎来到99网。
搜索
您的当前位置:首页查找算法汇总

查找算法汇总

来源:99网

1.概述

  • 查找表:查找表是由同一类型的数据元素构成的集合。由于“集合”中的数据元素之间存在完全松散的关系,因此查找表是一种非常灵便的数据结构
  • 对查找表经常进行的操作有:
    • 查询某个“特定的”数据元素是否在查找表中
    • 检索某个“特定的”数据元素的各种属性
    • 在查找表中插入一个数据元素
    • 从查找表中删去某个数据元素
  • 如果只满足上述前两个功能,则称静态查找表,如果全部满足,则称为动态查找表
  • 查找的依据——关键字:关键字是数据元素中某个数据项的值,用它可以标识一个数据元素。唯一地标识一个记录的关键字为主关键字,标识若干记录的关键字称为次关键字
  • 查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或记录元素。
  • 查找操作的性能分析:为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值称为查找算法在查找成功得到平均查找长度ASL(Average Search Length)

2.静态查找表

(1)顺序表的查找

  • 查找过程:从表中最后一个记录开始,逐个进行记录的关键字和给定值的比较,若某某个记录的关键字和给定值比较相等,则查找成功,找到所查记录;反之,若直至第一个纪录,其关键字和给定值比较都不等,则把表明表中没有所查记录,查找不成功。
int Search_Seq(int ST[],int key)
{
	for(int i=ST.length-1;i>=0;--i)
	{
		if(ST[i] == key)
			return i;
	}
	return -1;
}
  • 顺序表查找的平均查找长度:
    Pi为查找表第i个记录的概率
    Ci为查找表第i个记录比较的次数
  • 如果加上查找不成功的平均查找长度:
  • 有时候,表中各个记录的查找概率并不相等。我们可以在每个记录附设一个访问频度域,并使顺序表中的记录始终保持按访问频度非递减有序的次序排列,使得查找频度大的元素在查找过程中不断后移。
  • 算法优缺点:
    • 优点:算法简单且适应面广
    • 缺点:平均查找长度较大

(2)有序表的查找——二分查找

  • 先确定待查记录所在范围,然后逐步缩小范围直到找到或找不到该记录为止
int Search_Bin(int ST[],int key)
{	
	int low = 0,high = ST.length-1;
	while(low<=high)
	{
		int mid = (low+high)/2;
		if(ST[mid]<key)
			high = mid - 1;
		else if(ST[mid]>key)
			low = mid +1;
		else
			return mid;
	}
	return -1;
}
  • 二分查找的性能分析:二分查找可以构造一棵判定树

(3)索引顺序表的查找

  • 若以索引顺序表表示静态查找表,则Search函数可用分块查找实现
  • 分块查找又称索引顺序查找,这是顺序查找的一种改进方法。在此查找法中,除表本身外,尚需建立一个“索引表”。索引表将原表分为几个块,记录每个块的首项位置以及该块中最大关键字,索引表按关键字有序,则表或者有序或者分块有序。分块有序指的是每块的所有关键字大于前一块的所有关键字
  • 因此,分块查找过程需分两步进行,**先确定待查记录所在块,然后在块中顺序查找。**即分块查找的算法即为两种算法的简单合成
  • 如果块中有序,则可用顺序查找,亦可用折半查找,若无序,只能是顺序查找
  • 分块查找的性能:
    • 若用都用顺序查找:
    • 若用二分查找

3.动态查找表

  • 动态查找表的特点是:表的结构本身是在查找过程中动态生成的。

(1)二叉查找树

  • 二叉查找树:或者是一颗空树;或者是具有下列性质的二叉树:
    • 若它的左子树不空,则左子树上所有节点的值均小于它的根结点的值
    • 若它的右子树不空,则右子树上所有节点的值均大于它的根结点的值
    • 它的左、右子树也分别为二叉排序树
  • 查找算法如下:
//注意没有回溯的过程
bool SearchBST(BiTree T,KeyType key,BiTree &f,Bitree& p)
{
	//如果没有找到则返回路径的最后一个叶节点
	if(!T){
		p = f;
		return false;
	}
	//如果找到了则返回该节点
	if(T->data  == key){
		p = T;
		return true;
	}
	else if(T->data > key)
		return (SearchBST(T->lchild,key,T,p));
	else
		return (SearchBST(T->rchild,key,T,p));
}
  • 插入算法
bool InsertBST(BiTree& T,ElemType e)
{
	Bitree p;
	//如果已有相同节点返回false
	if(searchBST(T,e,key,NULL,p))
		return false;
	Bitree s= (BiTree)malloc(sizeof(BiNode));
	s->data = e;
	s->lchild = s->rchild = NULL;
	if(!p) T = s;  //如果树不存在则将s作为根结点
	else if(s->data < p->data)
	 	p->lchild = s;
	else
		p->rchild = s;
	return true;
}
  • 容易看出,中序遍历二叉排序树可得到一个关键字的有序序列。这就是说,一个无序序列可以通过通过构造一棵二叉排序树而变成一个有序序列,构造数的过程即对无序序列进行排序的过程。
  • 删除的算法较为复杂,要分成三种情况,这里不失一般性,设被删节点为p,其父结点为f,并且设p为f的左结点:
    • 若p结点为叶子结点,直接删去
    • 若p结点只有左子树或者只有右子树,将其子树作为f的子树然后删去p即可
    • 若p结点既有左子树又有右子树的时候。将p的左子树作为f的左子树,将右子树插入p的左子树下面最右边的叶子结点,作为其右子树
bool DeleteBST(BiTree& T,KeyType key)
{
	BiTree f = nullptr,p = nullptr;
	if(!SearchBST(T,key,f,p))
		return false;
	//对应第一种情况,p为叶子结点
	if(f == p)
		free(p);
	//对应第三种情况,p既有左子树又有右子树
	if(p->lchild!=nullptr && p->rchild!=nullptr)
	{
		//将左子树插到父节点上面
		if(p == f->lchild){
			f->lchild = p->lchild;
		}
		else{
			f->rchild = p->lchild;
		}
		Bitree tmp = p->lchild;
		while(tmp->rchild!=nullptr)
			tmp = tmp->rchild;
		tmp->rchild = p->rchild;
		free(p);
	}
	//对应第二种情况,只有左子树或者只有右子树
	else
	{
		if(p == f->lchild){
			p->lchild == nullptr ? f->lchild = p->rchild:f->lchild = p->lchild;
			free(p);
		}
		else{
			p->lchild == nullptr ? f->rchild = p->rchild:f->lchild = p->lchild;
			free(p);
		}
	}
}
  • 二叉查找树的查找分析:和二分查找类似,与给定值比较的关键字个数等于结点所在层次数,因此比较次数不超过树的深度,但是二分查找长度为n的表的判定树是唯一的,而二叉排序树却不唯一。
  • 因此,含有n个结点的二叉排序树的平均查找长度和树的形态有关。当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树,此时AVL为(n+1)/2,与顺序查找相同,这是最好的情况,而最好的情况AVL是log2(n+1)-1,和折半查找的判定书相同
  • 经过计算,二叉排序树的平均查找长度是和log2n是等数量级的。然而,在某些情况下,尚需在构成二叉查找树的过程中进行“平衡化处理”,称为二叉平衡树

(2)平衡二叉树(AVL树)

  • 平衡二叉树:它或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1
  • 平衡因子BF(Balance Factor)定义为该节点的左子树的深度减去它的右子树的深度。显然,BF只能是-1,0,1三种情况
  • 我们希望由任何初始序列构成的二叉排序树都是AVL树。因为AVL树上任何节点的左右子树的深度之差都不超过1。证明它的深度和logn是同数量级的。
  • 一般情况下,假设由于在二叉排序树上插入结点而失去平衡的最小子树根结点为a,则失去平衡后进行调整的规律可归纳为下列4种情况:
    • 单向右旋平衡处理
    • 单向左旋平衡处理
    • 双向旋转(先左后右)平衡处理
    • 双向旋转(先右后左)平衡处理
  • 无论哪种情况,在经过平衡旋转处理之后,它的深度和插入之前的深度相同,因此,当平衡的二叉查找树因插入结点而失去平衡时,仅需对最小不平衡子树进行平衡旋转处理
  • 在平衡的二叉排序树插入一个新的数据元素e的递归算法可描述如下:
    • 若为空树,则插入一个数据元素为e的新结点作为根结点,树的深度增1
    • 若e的关键字和根结点的关键字相等,则不进行插入
    • 若e的关键字小于根结点的关键字,而且在左子树中不存在和e有相同关键字的结点,则将e插入在左子树上,并且当插入之后的左子树深度+1时,分别就下列不同情况处理:
      • 根结点的平衡因子为-1:则将根结点的平衡因子改为0,整个树的深度不变
      • 根结点的平衡因子为0:则将根结点的平衡因子更改为1,BBST的深度增1
      • 根结点的平衡因子为1:
        ① 若左子树根结点的平衡因子为1,进行单向右旋平衡处理,处理后,将根结点和其右子树根结点的平衡因子更改为0,数的深度不变
        ② 若左子树根结点的平衡因子为-1,进行先左后右旋平衡处理,处理后,修改根结点和其左右子树根结点的平衡因子,数的深度不变
    • 若e的关键字大于根结点的关键字,则处理操作和上述相对称。
  • 平衡二叉树的结构定义:
typedef struct BSTNode
{
	ElemType data;
	int      bf;
	struct BSTNode* lchild,*rchild;	
}BSTNode,*BSTree;
  • 右旋操作
void R_Rotate(BSTree& p)
{
	BSTNode* tmp = p->lchild;
	p->lchild = tmp->rchild;
	tmp->rchild = p;
	p = tmp;
}
  • 左旋操作
void L_Rotate(BSTree& p)
{
	BSTNode* tmp = p->rchild;
	p->rchild = tmp->lchild;
	tmp->lchild = p;
	p = tmp;
}
  • 具体算法描述
typedef struct node {
	int val;
	struct node* left, * right;
}node,*tree;
void rotateLeft(tree& root) {
	node* t = root->right;
	root->right = t->left;
	t->left = root;
	root = t;
}
void rotateRight(tree& root) {
	node* t = root->left;
	root->left = t->right;
	t->right = root;
	root = t;
}
void rotateLeftRight(tree& root) {
	rotateLeft(root->left);
	rotateRight(root);
}
void rotateRightLeft(tree& root) {
	rotateRight(root->right);
	rotateLeft(root);
}
int getHeight(tree& root) {
	if (root == nullptr) return 0;
	return max(getHeight(root->left), getHeight(root->right)) + 1;
}
void insert(tree& root, int val) {
	if (root == nullptr) {
		root = new node();
		root->val = val;
		root->left = root->right = nullptr;
		return;
	}
	else if (val < root->val) {
		insert(root->left, val);
		if (getHeight(root->left) - getHeight(root->right) == 2)
			val < root->left->val ? rotateRight(root) : rotateLeftRight(root);
	}
	else {
		insert(root->right, val);
		if (getHeight(root->left) - getHeight(root->right) == -2)
			val > root->right->val ? rotateLeft(root) : rotateRightLeft(root);
	}
}

(3)2-3 查找树

在动态插入中保证树的完美平衡的代价太高了,所以有了为了依旧保持树高为lgN,有了2-3查找树和下面的红黑树

我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链接),而现在我们引入3- 结点,它含有两个键和三条链接

插入:
(1)向2-结点中插入新键

(2)向一棵只含有一个3-结点的树中插入新键

在考虑一般情况之前,先假设我们需要向一棵只含有一 个 3-结点的树中插人一个新键。这棵树中有两个键,所以在它唯一的结点中已经没有可插人新键的空间了。为了将新键插人,我们先临时将新键存入该结点中,使之成为一个4-结点。它很自然地扩展了以前的结点并含有3个键和4条链接。创建一个4- 结点很方便,因为很容易将它转换为一棵由3个2-结点组成的2-3树,其中一个结点(根 )含有中键,一个结点含有3个键中的最小者(和根结点的左链接相连) ,一个结点含有3个键中的最大者(和根结点的右链接相连)。这棵树既是一棵含有3个结点的二叉查找树,同时也是一棵完美平衡的2-3 树

(3)向一个父节点为2- 结点的3- 结点插入新键

,假设未命中的查找结束于一个3-结点,而它的父结点是一个2-结点。在这 种情况下我们需要在维持树的完美平衡的前提下为新键腾出空间。我们先像刚才一样构造一个临时的4-结点并将其分解,但此时我们不会为中键创建一个新结点,而是将其移动至原来的父结点中。你可以将这次转换看成将指向原3- 结点的一条链接替换为新父结点中的原中键左右两边的两条链 接,并分别指向两个新的2-结点。根据我们的假设,父结点中是有空间的:父结点是一个2-结点(一 个键两条链接),插人之后变为了一个3-结点(两个键3 条链接)。另外,这次转换也并不影响(完 美平衡的)2-3树的主要性质。树仍然是有序的,因为中键被移动到父结点中去了;树仍然是完美平衡的,插入后所有的空链接到根结点的距离仍然相同

(4)向一个父节点为3- 结点的3- 结点中插入新键

现在假设未命中的查找结束于一个父结点为3- 结点的结点。我们再次和刚才一样构造一个 临时的4-结点并分解它,然后将它的中键插入它的父结点中。但父结点也是一个3-结点,因此 我们再用这个中键构造一个新的临时4-结点,然后在这个结点上进行相同的变换,即分解这个 父结点并将它的中键插入到它的父结点中去。推广到一般情况,我们就这样一直向上不断分解临 时的4-结点并将中键插入更高层的父结点,直至遇到一个2- 结点并将它替换为一个不需要继续 分解的3-结点,或者是到达3-结点的根
如果从插入结点到根结点的路径上全都是3-结点,我们的根结点最终变成一个临时的4-结点。
此时我们可以按照向一棵只有一个3-结点的树中插入新键的方法处理这个问题。我们将临时的4- 结点分解为3个 2-结点,使得树高加1 , 如图3.3.7所示。请注意,这次最后的变换仍然保持了树 的完美平衡性,因为它变换的是根结点

(5)红黑树

红黑二叉查找树背后的基本思想是用标准的二叉查找树(完全有2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树

我们将树中的链接分为两种类型:红链接将两个2- 结点连 接起来构成一个3-结点,黑链接则是2-3树中的普通链接。确 切地说,我们将3-结点表示为由一条左斜的红色链接(两个2- 结点其中之一是另一个的左子结点)相连的两个2-结点,如图 3.3.12所示。这种表示法的一个优点是,我们无需修改就可以直 接使用标准二叉查找树的g e t()方法。对于任意的2-3树,只要对结点进行转换,我们都可以立即派生出一棵对应的二叉查找树

红黑树的另一种定义是含有红黑链接并满足下列条件的二叉查找树:

  • 红链接均为左链接
  • 没有任何一个结点同时和两条红链接相连
  • 该数是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同

如果我们将一棵红黑树的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如下图所示:

如果我们将会由红链接相连的结点合并,得到的就是一棵2-3树。无论我们选择用何种方式去定义它们,红黑树都既是二叉查找树,也是2-3树。因此,如果我们能够在保持一一对应关系的基础上实现2-3的插入算法,那么我们就能够将两个算法的优点结合起来:二叉查找树中简洁高效的查找方法和2-3树中高效的平衡插入算法

为了方便起见,因为每个结点都只会有一条指向自己的链接,我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中。如果指向它的链接是红色的,那么该变量为true,黑色则为false。

#define RED true
#define BLACK false
typedef int Key;
typedef struct
{
	Key key; //键
	Node* left, *right;
	bool color;
}Node,*RBTree;

在我们实现的某些操作中可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被小心地旋转并修复旋转操作会改变红链接的指向

假设我们有一条红色的右链接需要被转化为左链接。这个操作叫做左旋转
左旋前:

左旋后:

void RotateLeft(RBTree& h)
{
	Node* tmp = h->right;
	h->right = tmp->left;
	tmp->color = h->color;
	h->color = RED;
	tmp->left = h;
	h = tmp;
}

实现将一个红色左链接转换为一个红色右链接的一个右旋 转的代码完全相同,只需要将left换成right即可

void RotateRight(RBTree& h)
{
	Node* tmp = h->left;
	h->left = tmp->right;
	tmp->color = h->color;
	h->color = RED;
	tmp->right = h;
	h = tmp;
}

无论左旋转还是右旋转,旋转操作都会返回一条链接。我们总是会用rotateRight( ) 或 rotateLeft()的返回值重置父结点(或是根结点)中相应的链接。返回的链接可能是左链接也可能是右链接,但是我们总会将它赋予父结点中的链接。这个链接可能是红色也可能是黑色—— rotateLeft() 和 rotateRight() 都通过将x.color设为h.color保留它原来的颜色。这可能会产生两条连续的红链接,但我们的算法会继续用旋转操作修正这种情况。
插入:
(1)向根结点插入新键

一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键之后,我们马上就需要将它们旋转。如果新键小于老键,我们只需要新增一个红色的结点即可,新的红黑树和单个3- 结点完全等价。如果新键大于老键,那么新增的红色结点将会产生一条红色的右链接。我们需要使用root = RotateLeft(root);来将其旋转为红色左链接并修正根结点的链接,插入操作才算完成

(2)向树底部的2- 结点插入新键

用和二叉查找树相同的方式向一棵红黑树中插入一个新键会在树的底部新增一个结点(为了保 证有序性),但总是用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么 刚才讨论的两种处理方法仍然适用。如果指向新结点的是父结点的左链接,那么父结点就直接成为 了一个3-结点;如果指佝新结点的是父结点的右链接,这就是一个错误的3-结点,但一次左旋转 就能够修正它

(3)向一棵双键树(即一个3- 结点)中插入新键

①新键大于原树中的两个键
②新键小于原树中的两个键
③新键介于两者之间

在上述步骤的颜色变换,不仅要将子结点变黑,还要将父结点变红,于是我们单独封装一个函数:

void flipColors(RBTree& h)
{
	h->color = RED;
	h->left->color = BLACK;
	h->right->color = BLACK;
}

注意:在上述情况中,颜色转换会让根结点变红,严格地说,红色的根结点说明根结点是一个3- 结点的一部分,但实际情况并不是这样。因此我们在每次插入后都会将根结点设为黒色。注意,每当根结点由红变红黑时树的黑链接高度就会加1

红黑树的递归思想

2-3树中的插人算法需要我们分解3-结点,
将中间键插入父结点,如此这般直到遇到一个2- 结点或是根结点。我们所考虑过的所有情况都正是为了达成这个目标:每次必要的旋转之后我们都会进行颜色转换,这使得中结点变红。在父结点看来,处理这样一个红色结点的方式和处理一个新插入的红色结点完全相同,即继续把红链接转移到中结点上去。

实现代码:

bool Insert(RBTree h, Key key)
{
	//如果树为空
	if (h == nullptr) {
		h = (Node*)malloc(sizeof(h));
		h->color = RED;
		h->key = key;
		h->left = h->right = nullptr;
		return;
	}
	if (key < h->key)  Insert(h->left, key);
	else if (key > h->key)  Insert(h->right, key);
	else return false;
	if (h->left->color == RED && h->right->color == RED) flipColors(h);
	if (h->left->left->color == RED && h->left->color == RED) RotateRight(h);
	if (h->right->color == RED && h->left->color == BLACK) RotateLeft(h);
}

void InsertRBTree(RBTree root,Key key)
{
	Insert(root, key);
	root->color = BLACK;
}

删除:
首先我们要回到2-3树。和插入操作一样,我们也可以定义一系列局部变换来删除一个结点的同时保持树的完美平衡性。这个过程比插入一个结点更加复杂,因为我们不仅要在(为了删除一个结点而)构造临时4-结点时沿着查找路径向下进行变换,还要再分解遗留的4-结点时沿着查找路径向上进行变换(同插入操作)
(1)删除最小值

在第二轮热身中我们要学习2-3树中删除最小键的操作。我们注意到从树底部的3- 结点中删除 键是很简单的,但 2-结点则不然。从 2-结点中删除一个键会留下一个空结点,一般我们会将它替 换为一个空链接,但这样会破坏树的完美平衡性。所以我们需要这样做:为了保证我们不会删除一 个 2-结点,我们沿着左链接向下进行变换,确保当前结点不是2-结 点 (可能是3-结点,也可能是 临时的4-结点)。首先,根结点可能有两种情况。如果根是2-结点且它的两个子结点都是2-结点,
我们可以直接将这三个结点变成一个4-结点;否则我们需要保证根结点的左子结点不是2-结点,如有必要可以从它右侧的兄弟结点“借” 一个键来。以上情况如图3.3.26所示。在沿着左链接向下 的过程中,保证以下情况之一成立:

  • 如果当前结点的左子结点不是2-结点,完成
  • 如果当前结点的左子结点是2-结点而它的亲兄弟结点不是2-结点,将左子结点的兄弟结点 中的一个键移动到左子结点中
  • 如果当前结点的左子结点和它的亲兄弟结点都 是 2-结点,将左子结点、父结点中的最小键 和左子结点最近的兄弟结点合并为一个4- 结 点,使父结点由3-结点变为2-结点或者由4- 结点变为3-结点。

在遍历的过程中执行这个过程,最后能够得到一 个含有最小键的3-结点或者4-结点,然后我们就可 以直接从中将其删除,将 3-结点变为2-结点,或者 将 4-结点变为3-结点。然后我们再回头向上分解所有临时的4-结点。
删除最小值代码:

void moveRedLeft(RBTree& root)
{
	flipColors(root);
	if (isRed(root->right->left)) {
		RotateRight(root->right);
		RotateLeft(root);
		flipColors(root);
	}
}
void balance(RBTree& h)
{
	if (isRed(h->right) && !isRed(h->left)) RotateLeft(h);
	if (isRed(h->left->left) && isRed(h->left)) RotateRight(h);
	if (isRed(h->left) && isRed(h->right)) flipColors(h);
}
void deleteMin(RBTree& h)
{
	if (h->left == nullptr) {
		delete h;
		return;
	}
	if (!isRed(h->left) && !isRed(h->left->left))
		moveRedLeft(h);
	deleteMin(h->left);
	balance(h);
}
void DeleteMin(RBTree& root)
{
	if (!isRed(root->left) && !isRed(root->right))
		root->color = RED;
	deleteMin(root);
	if (root != nullptr)
		root->color = BLACK;
}

(2)删除最大值:
基本与删除最小值对称

void moveRedRight(RBTree& root)
{
	flipColors(root);
	if (isRed(root->left->left)) {
		//此时只需要做一次旋转
		RotateRight(root->left);
		flipColors(root);
	}
}
void deleteMax(RBTree& h)
{
	if(isRed(h->left)) 
		RotateRight(h);
	if (h->right == nullptr) {
		delete h;
		return;
	}
	if (!isRed(h->rightt) && !isRed(h->right->left))
		moveRedLeft(h);
	deleteMin(h->right);
	balance(h);
}

void DeleteMax(RBTree& root)
{
	if (!isRed(root->right) && !isRed(root->left))
		root->color = RED;
	deleteMax(root);
	if (root != nullptr)
		root->color = BLACK;
}

请注意:在deleteMax中不是直接判断是不是已经到了最大的结点,而是加了一个判断语句,因为如果最大结点是根结点的情况,如果直接删除会丢失掉左子树,所以要进行一次右旋转
那么为什么不会出现最小结点是根结点的情况呢,这就跟红黑树的性质有关了:

红黑树只会出现右图的情况,而不会出现左图的情况
总删除代码:

void Delete(RBTree& root,Key key)
{
	if (!isRed(root->left) && !isRed(root->right))
		root->color = RED;
	delete(root,key);
	if (root != nullptr)
		root->color = BLACK;
}
void delete(RBTree& h,Key key)
{
	if(key<h->key){
		if (!isRed(h->left) && !isRed(h->left->left))
			moveRedLeft(h);
		delete(h->left,key);
	}
	else{
		if(isRed(h->left)) 
			RotateRight(h);
		if(key == h.key && h->right ==nullptr)
			delete h;
		if (!isRed(h->right) && !isRed(h->right->left))
			moveRedLeft(h);
		if(key == h.key){
			Node* x = min(root->right);
			h->key = x->key;
			deleteMin(h->right);
		}
		delete(h->right,key);
	}
	balance(h);
}

(4)B-和B+树

  • B-树:B-树是一种平衡的多路查找树,他在文件系统中很有用。
  • 一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
    • 树中每个结点至多有m棵子树
    • 若各结点不是叶子结点,则至少有两棵子树
    • 除根之外的所有非终端结点至少有 ⌈m/2⌉棵子树
    • 所有的非终端结点中包含下列信息数据:
      (n,A0,K1,A1,K2,A2,…,An,Kn)
      其中Ki为关键字,且Ki<Ki+1;Ai为指向子树根结点的指针;n为关键字的个数
    • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)
  • B-树的结构:
#define m 3 
typedef struct BTNode
{
	int keynum; //结点关键字个数
	struct BTNode* parent;
	KeyType key[m+1];  //关键字向量,0留空
	struct BTNode *ptr[m+1]; //子树指针向量
}BTNode,*BTree;
typdef struct
{
	BTNode* pt;  //指向周到的结点
	int     i;   //在结点中的关键字序号
	int     tag; // 1:查找成功 0:查找失败
}Result;
  • B-树的查找操作
Result SearchBTree(BTree T,KeyType K)
{
	BTNode* q = nullptr,*p = T;
	int i = 0;
	while(p)
	{
		i = Search(p,K);
		if(i > 0 && p->key[i] == K)
			return Result(p,i.1);
		else{
			q = p;
			p = p->ptr[i];
		}
	}
	return Result(q,i,0);  //查找不成功,返回k的插入位置信息
}
int Search(BTree T,KeyType key)
{	
	int low = 1,high = T.keynum;
	bool little = false;
	while(low<=high)
	{
		int mid = (low+high)/2;
		if(T->key[mid]<key)
			high = mid - 1;
		else if(T->key[mid]>key)
			low = mid +1;
		else
			return mid;
	}
	return high;
}
  • B-树的生成:B- 树的生成也是从空树起,逐个插入关键字而得。但由于B-树结点中的关键字个数必须>= ⌈m/2⌉-1,因此,每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最底层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1,则插入完成,否则要产生结点的“”
  • 的具体思想:
  • 插入算法实现:
bool InsertBTree(BTree& T,KeyType K,BTree q,int i)
{
	while(q)
	{
		Insert(q,i,x);
		if(q->keynum<m)
			break;
		int s =  ceil(m/2);
		split(q,s);
		x = q->keys[s];
		q = q->parent;
		if(q)
			i = Search(q,x);
	}
	if(!q)
		NewRoot(T,q,x);
	return true;
}
  • B+树:B+树是应文件系统所需而出的一种B-树的变形树。一棵m阶的B+树和m阶的B-树的差异在于:
    • 有n棵子树的结点中包含n个关键字
    • 所有的叶子结点包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
    • 所有的非终端结点可以看成是索引部分,结点中仅含有其子树中的最大(最小)关键字

(5)键树

  • 键树:又称数字查找树。它是一棵度>=2的树,树中的每个结点中不是包含一个或几个关键字,而是只含有组成关键字的符号。例如,若关键字是数值,则接待您中只包含一个数位;若关键字是单词,则结点中只包含一个字母字符。
  • 例如有如下16个关键字的集合

    它生成如下的键树
  • 为了查找和插入方便,我们约定键树是有序树,即同一层中兄弟结点之间依所含符号自左至右有序,并约定结束符$小于任何字符。
  • 主要用处:文件索引

4.哈希表

  • 哈希表:在前面讨论的各种结构中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。理想情况是希望不经过任何比较,因此存取便能得到所查记录,那就必须在记录位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。在此,我们称这个对应关系f为哈希(hash)函数
  • 哈希函数是一个映像,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许范围之内即可
  • 对不同的关键字可能得到同一哈希地址,即key1≠key2,而f(key1)=f(key2),这种现象称冲突。在一般情况下,冲突只能尽可能地少,而不能完全避免

(1)哈希函数的构造方法

  • 均匀的哈希函数:若对于关键字集合中的任一关键字,经哈希函数映像到地址集合中任何一个地址的概率是相等的。(就是使关键字经过哈希函数得到一个“随机地址”,以便使一组关键字的哈希地址均匀分布在整个地址区间中,从而减少冲突

① 直接定址法

  • 取关键字或关键字的某个线性函数值为哈希地址
    H(key)=key或H(key)= a*key + b
    其中a和b为常数(这种哈希函数叫做自身函数
  • 由于直接定址所得地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际中能使用这种哈希函数的情况很少

② 数字分析法

  • 假设关键字是以r为基的数,并且哈希表中可能出现的关键字都事先知道的,则可取关键字的若干数位组成哈希地址

③ 平方取中法

  • 关键字平方后的中间几位为哈希地址。这是一种较常用的构造哈希函数的方法, 通常在选定哈希函数时不一定能知道关键字的全部情况,取其中哪几位也不一定合适,而一个数的平方中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。

④ 折叠法

  • 将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为哈希地址。关键字位数很多,而且关键字中每一位上数字分布大致均匀,可以采用折叠法得到哈希地址

⑤ 除留余数法

  • 取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址,即:
    H(key) = key MOD p ,p<=m
  • 这是一种最简单、也最常用的构造哈希函数的方法,它不仅可以对关键字直接取模,也可在折叠、平凡取中等运算之后取模
  • 由众人的经验得知:一般情况下,可以选p为质数或不包含小于20的质因数的合数

⑥ 随机数法

  • 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即:
    H(key) = random(key)
    通常,当关键字长度不等时采用此法构造哈希函数较恰当

(2)处理冲突的方法

  • 假设哈希表的地址集为0~n-1,冲突是指由关键字得到的哈希地址为j的位置上已存有记录,则“处理冲突”就是为该关键字的记录找到另一个哈希地址
  • 在处理哈希地址的过程中可能得到一个地址序列Hi。即在处理哈希地址的冲突时,若得到的另一个哈希地址H1仍然发生冲突,则再求下一个地址H2,直到不发生冲突为止

① 开放定址法

  • Hi = (H(key)+di) MOD m
    其中的di为增加序列,可有下列三种取法
    (1)线性探测再散列:di = 1,2,3,…,m-1`
    (2)二次探测再散列:di = 12,-12,22,-22…,±k^^2(k<=m/2)
    (3)伪随机探测再散列:di = 伪随机数序列

② 再哈希法

  • Hi = RHi(key) i = 1,2,…,k
    RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。这种方法不易产生聚集,但增加计算的时间

③ 链地址法

④ 建立一个公共溢出区

  • 这也是处理冲突的一种方法。假设哈希函数的值域为[0,m-1],则设向量HashTable[0,m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0,…v]为已溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,填入溢出表。

(3)哈希表的查找及其分析

  • 存储结构
#define SUCCEESS 1
#define UNSUCCESS 0 
#define DUPLICATE -1

int hashsize[n] = {}; //哈希表容量递增表,一个合适的素数序列
typedef struct 
{
	ElemType *elem;
	int count;   //当前数据元素个数
	int sizeindex;  //hashsize[sizeindex]为当前容量
}HashTable;
  • 查找算法
bool SearchHash(HashTable H,KeyType K,int& p,int& c)
{
	p = Hash(k);   //求得哈希地址
	while(H.elem[p].key!=NULL && H.elem[p].key != K)
		collision(p,++c);
	if(K == H.elem[p].key)
		return SUCCESS;
	else
		return UNSUCCESS; 
}
  • 插入算法
bool InsertHash(HashTable& H,ElemType e)
{
	c = 0;
	int p;
	if(SearchHash(H,e.key,p,c))
		return DUPLICATE;
	else if(c<hashsize[H.sizeindex]/2)
	{
		H.elem[p] = e;
		++H.count;
		return truel;
	}
	else
	{
		//重建Hash表
		RecreateHashTable(H);
		return UNSUCCESS;
	}
}
  • 从哈希表的查找过程可见:
    • 虽然哈希表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的产生,使得哈希表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需以平均查找长度作为衡量哈希表的查找效率的量度
    • 查找过程需要与给定值进行比较的关键字的个数取决于下列三个因素:哈希函数,处理冲突的方法和哈希表的装填因子
      哈希表的装填因子定义为:
      - 得出各个处理冲突方法的平均查找长度:
    • 成功时
    • 不成功时

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 99spj.com 版权所有 湘ICP备2022005869号-5

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务