查找的详细说明分类和常用算法

183 阅读9分钟

Snipaste_2022-07-19_19-52-14.png

1.查找

按照某种规则在查找表中寻找与给定值相等的记录是否存在的过程称为查找。

1.1 查找表

同一类型的数据元素(或记录)构成的集合。

例如:在电话号码簿中查找 某单位”或“某人的电话号码;在字典中查阅“某个词 的读音和含义等等。 其中“电话号码簿”和“字典”都可视作是一张查找表。在各种系统软件或应用软件中,查找表也是最常见的结构之一,如编译程序中符号表、信息处理系统中信息表等等。

1.2 静态查找

只在查找表中查询与给定值相等的记录是否存在,或检索某个特定数据元素的各种属性,不进行插入与删队余操作的查找称为静态查找。

1.3 动态查找

在查找表中进行查找的过程中,查询与给定值相等的记录是否存在。如果不存在,则进行插入操作;否则,进行删除操作。

1.4 关键字

是可以标识(识别)一个数据元素(记录)的某个数据项的值。若此关键字能唯一地标识一个记录,则称为主关键字。否则,称用以识别若干记录的关键字为次关键字。

1.5 平均查找长度

为确定记录在查找表中的位置,需和给定个直进行比较的关键字个数的期望值称为查找算法在查找成功时的平均查找长度。同样,有查找不成功的平均查找长度。最好情况下的、最坏情况下的和平均情况下的平均查找长度。

2.顺序存储结构

Typedef struct {

ElemType*elem;(数据元素存储空间基址,0 号单元留空)

int length;(表长度)

}SSTable;

2.1 顺序表查找

顺序表的查找适用于顺序存储和链表存储的记录表。从表的任一端开始逐个与给定值相比较,若相等,则查找成功。返回值为该记录在表中的位置序号;若超出界限,则查找失败。返回值为零。适用于顺序存储与链表存储。

2.1.1 顺式存储的顺序查找
  • 从尾部查找
Int Search-Seq(SSTableST,keyTypekey){
ST.elem[0].key=key;

 for(i=ST.length;!EQ(ST.elem[i].key,key);--i);
return i;
 } Search-seq


  • 从头部查找
int Search_seq(SSTable ST,keytype key)

 int i= 1;
 {
 while(i <= ST.length &&ST.elem[i].key!= key)
 i++;
 if(i>ST.length) return FALSE; //没找到
 else return i; //找到了
 }
  • 从头部查找比从尾部查找效率低,建议用尾部查找法
2.1.2 链式存储的顺序查找
status getelement(link head,keytype key){
 (for(p=head;p!=NULL&&p->key!=key;p=p->next);
 return p;
 }

2.2 有序表的查找

2.2.1 折半查找

折半查找(Binary Search)的过程是: 首先计算被查表的中间位置; 然后将中间位置记录的关键字与给定值相比较:若相等则查找成功;若中间位置记录的关键字小于给定值, 则下次从后半个表继续查找; 若中间位置记录的关键字大于给定值, 则下次从前半个表继续查找。

例如:查找 21 需要比较 3 次。

  • 算法
Int Search-Bin(SSTable ST,keyType key) {
 low = 1; high = ST.length; While( low<=high ) {
 mid =(low +high)/2;
 if EQ(key,ST.elemmid].key) return mid;
 else if LTkey,ST.elem[mid].key) high= mid-1; else low =mid +1 ;
 }
 return 0;
 }

折半查找的过程可以用下面的判定树表示:

从下图可见,查找 21 的过程恰好是走过一条从根 21 到结点 4 的路径,和给定关键字进行比较的关键字个数为该路径上的结点数或结点 4 在判定树上的层数。其他结点也类似。因此,折半查找在查找成功时进行比较的关键字个数不超过树的深度,而具有 n 个结点的判定树的深度为 [log₂n] +1,所以折半查找在查找成功时和给定值进行比较的关键字个数最多为 [log₂n] +1。

3.1 分块查找

索引顺序表的查找若以索引顺序表表示静态查找表,则可用分块查找来实现。分块查找又称索引顺序查找。这种查找方法中除了表本身以外尚需建立一个索引表。

  • 构造过程

如图所示,该表的构造过程是:把要查找的表分成长度相等的几个子表 (称为块)。对每个子表建立一个索引项,其中包括两项内容:关键字项(存放该块中最大的关键字)指针项(存放该块中第一个记录的地址)。索引表中关键字递增有序,表中记录可以有序,也可以无序。但块之间一定有序(即后一块中的最小关键字都大于前一块中的最大关键字)。

  • 查找过程

查找过程分两步:首先在索引表中确定待查记录所在的块;然后,在块中按顺序查找。由于索引表中的关键字递增有序,且是顺序存储可以用折半查找。而块中记录只能用顺序查找。因此,分块查找算法是上述两种算法的简单合成。

  • 平均查找长度

分块查找的平均查找长度为 ASL= Lь+Lw。

(其中 Lb 为确定待查记录所在块的平均查找长度,Lw 为在块中的平均查找长度。)

  • 顺序查找确定所在块,则分块查找的平均查找长度为:

ASLbs = Lь+ Lw= (b+1)/2+ (s+1)/2=(n/s+s)/2+1。

将长度为 n 的表均匀地分成 b 块,每块含有 s 个记录,即 b=n/s;又假定表中每个记录的查找概率相同,即每块查找的概率为 1/b,块中每个记录的查找概率为 1/s。

  • 折半查找确定所在块,则分块查找的平均查找长度为:

ASL≈log2 (n/s+1)+s/2

其中 n 为表的长度;s 为每块中含有记录的个数。

4.二叉排序树

二叉排序树或者是一棵空树;或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别是二叉排序树。

查找过程中,当二叉排序树不空时,首先将给定值与根结点的关键字比较,若相等查找成功;若给定值小于根结点的关键字,则在左子树上继续查找,否则,在右子树上继续查找。

下图为二叉树:

  • 二叉排序树递归算法:

BiTree SearchBST(BiTreeT, KeyTepykey){

if( (!T)EQ(key, T->data.key)) return(T);//查找结束

else if LT(key,T->data.key)

return(SearchBST(T->Ichild,key));//在左子树继续查找

else return(SearchBST(T->rchildkey));//在右子树继续查找

}// SearchBST

  • 查找不成功返回插入位置

Status SearchBSTBiTreeTKeyTepy keyBiTreefBiTree&p){/在根指针 T 所指二叉排序树中递归地查找其关键字等于 kev 的数据元素,若查找成功,则指针 p 指向该数据元素结点,并返回 TRUE,否则指针 p 指向查找路径上访问的最后一个结点并返回 FALSE,指针 f 指向 T 的双亲,其初始调用值为 NULL/

if(!T) { P=f; return FALSE;}//查找不成功

else if EQ(Key,T->data.key) {

p=T; return TRUE;

}//查找成功

else if LT(Key,T->data.key)

return SearchBST(T->lchild,key,T,p);//在左子树继续查找

else return SearchBST(T->rchild,key,T,p);//在右子树继续查找

}// SearchBST

  • 二叉排序树上插入算法

Status Insert BST(BiTree &TElemTypee){

//当二叉排序树 T 中不存在关键字等于 ekey 的数据元素时,插入 e 并返回 TRUE.否则返回 FALSE

if(!SearchBST(Te.key,NULL,p){(查找不成功)

s=(BiTree)malloc(sizeof(BiTNode));(申请新的空间构成新结点)

s->data=e;

s->lchild= s->rchild = NULL;

if(!p)T=s;(若为空树,新结点为根结点)

else if LT(e.key,p->data.key)p->lchild=s;(新结点值小于当前值,插入点*s 为左孩子)

else p->rchild=s;(新结点值大于当前值,插入点*s 为右孩子)

return TRUE;

else return FALSE;//树中已有和关键字相同的结点,不再插入

}// Insert BST

  • 二叉排序树上的删除

假设被删结点 P 为二叉排序树上任一结点,F 为 P 的双亲结点。为简单起见,设 P 是 F 的左孩子结点。

下面分三种情况进行讨论:

(1)若*P 为叶子结点,则,只需修改 P 的双亲结点 F 的相应孩子域为空。例如, P=83,只需把结点 82 的右孩子域置空即可。

(2)若 P 结点只有一个左孩子结点或一个右孩子结点。此时只要把左孩子结点或右孩子结点作为其双亲 F 的左或右孩子结点即可。(替换原来的 P 即可)例如,P=75,它只有一个左孩子 74,所以,删除 75,只要用 74 带替 75 的位置,即将 74 作为 73 的右孩子,而把 75 释放就行了。

(3)若 P 结点既有左孩子结点又有右孩子结点。例如,P 为 80 号结点,此时,既不能用其左孩子结点带代,也不能用右孩子结点代替。而只能用 P 的中序遍历的直接前驱或中序遍历的直接后继代替 P。即可以用 75 来代替 80,而把 75 的左孩子 74 变为 73 的右孩子;或 82 来代替 80,而把 82 的右孩子 83 变为 85 的左孩子。

或左子树直接替代,而将原来该节点的右子树接在原来左子树的最右端。如图 70 接到 80 上,85 接到 75 的右子树上。

  • 二叉排序树删除递归算法

Status DeleteBST(BiTree&T,ElemTypekey){

if(! T) return FALSE;(不存在要查找的关键字)

else{

if EQ(key,T->data.key) {

return Delete(T);

}(查找成功)

else if LT(key,P->data.key) return DeleteBST(T->Ichild,key);

else return DeleteBST(T->rchild,key);

}

}// DeleteBST

  • 实际过程算法

Status Delete(BiTree&p){

(从二叉排序树中删除结点 P,并连接好它的左或右子树)

if(!P->rchild){(被删除结点 P 没有右子树,只需连接左子树)

q=p;p=p->lchild;

free(q);(用 P 的左孩子带替被删除结点 P)

}

else if(!P->lchild){(被删除结点 P 没有左子树,只需连接右子树)

q=p;

p=p->rchild;

free(q);(用 P 的右孩子带替被删除结点 P)

}

else{(结点 P 左右子树都存在,包括左孩子有与没有右孩子两种情况)

q=p; s=p->lchild;(q 作为遍历前驱的双亲结点)

while(s->rchild){

q=s;s=s->rchild

}(沿右分支查找 S)

p->data=s->data;(用 P 的遍历前驱 S 的数据覆盖 P 的数据)

if (q!= p)q->rchild=s->lchild;(左孩子有右孩子,修改右子树)

else q->lchild=s->lchild;(左孩子没有右孩子,修改左子树)

delete s;

}(删除结点 S)

return TRUE;

}Delete

5.平衡二叉树

(1)平衡二叉树又称为 AVL 树。在平衡二叉树中每个结点都有一个平衡因子域,用来存放该结点的平衡因子值;例如假设用 TL 代表结点 T 的左子树的最大深度;TR 代表结点 T 的右子树的最大深度;那么,AVL 树每个结点的平衡因子值 T->BF=TL-TR。

  • 若 TL-TR=0(即该结点平衡因子值为 0),则称其为平衡结点。
  • 若 TL-TR=1(即该结点平衡因子值为 1),则称其为左重结点。
  • 若 TL-TR=-1,则称其为右重结点。

在一棵二叉排序树中,如果每个结点的平衡因子值都是-1、0、1 三者之一,那么,称这棵树为平衡二叉排序树简称平衡二叉树。树中结点的平衡因子的绝对值大于等于 2,该树不平衡。

(2)观察二叉排序树构造过程找出造成不平衡的原因。新结点插在平衡因子值为 0 的结点左或右都不会造成不平衡。新结点插在平衡因子值为 1(或-1)的结点的右(或左)分支,该结点也不会造成不平衡。新结点插在平衡因子值为 1(或-1)的结点的左(或右)分支上,此时,该结点的平衡因子的绝对值≧2,造成二叉排序树不平衡。下图 10.7 的左图中在 70 插入前,50 的平衡因子值为-1,而右图中 20 插入前,50 的平衡因子值为 1。

  • 二叉排序树的类型定义

typedef structBSTNode{

ElemType data;

int bf;(结点的平衡因子域)

struct BSTNode *lchild,*rchild;

} BSTNode,*BSTree;

Void R-Rotate(BSTree &p){(作右旋转处理,)

lc=p->lchild;(LC 为*P 的左子树根结点)

p->lchild=lc->rchild;(LC 的右子树作为*P 的左子树)

lc->rchild=p; p=lc;(P 指向新的根结,即 B 带替 A)

}// R- Rotate

Void L-Rotate(BSTree &p){(作左旋转处理)

rc=p->rchild; (RC 为*P 的右子树根结点)

p->rchild=lc->lchild;(RC 的左子树作为*P 的右子树)

lc->lchild=p;p=rc;(P 指向新的根结,即 B 带替 A)

}// L- Rotate

6.B-树

什么是 B-树?

B-树是一种平衡的多路排序查找树。

满足条件:

一棵 m 阶的非空 B-树,应当满足下面几个条件:

(1)树中每个结点最多有 m 棵子树。

(2)如果,根结点不是叶子,那么至少有两棵子树;

(3)除根以外的所有非终端结点至少有[m/2 棵子树;

(4)所有非终端结点中包含下列信息数据

(n,Ao,K1,A1,K2,A2,..,Kn,An)

其中:n([m/2]-1≤n≤m-1)为结点中关键字的个数;Ki(i=1,23,…,n)为关键字,且 Ki<Ki+1(i=1,2,3,...,n-1) ;A(i=0,1,2,…,n)为指向子树根结点的指针;且指针 Ai 所指子树中所有结点的关键字均小于 Ki(i=1,2,3,..,n)。 An 子树上所有结点的关键字均大于 Kn。

(5)所有的叶子结点都出现在同一层上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

B-树的查找过程

(1)将给定值与根结点关键字相比较, 如果相等,那么查找成功。

(2)如果小于 Ki,那么下次从以 Ai-1 为根的子树中查找。否则下次从以 Ai+1 为根的子树中查找。

(3)如果查找到叶子结点,那么查找失败。

B-树的插入

(1)从根结点开始按照查找的过程确定给定值应插入的结点位置 P。

(2)如果该结点上的关键字个数少于 m-1 个,贝到将给定值直接插到该结点上。

(3)如果应插入的结点上,已有 m-1 个关键字, 那么,必须把该结点分裂成两

个结点;(P、P')。

例:在图中插入 65 后的 3 阶 B-树

B-树的删除

(1)被删除结点上关键字个数不少于 [m/2],那么只需从该结点上删除该关键字 Ki 和相应的指针 Ai。

(2)如果被删除结点上关键字个数等于 m/2-1,而与其相邻的右兄弟结点(或左兄弟结点)中关键字的个数大于 m/2 -1,则需将其兄弟结点总数最小(或最大)的关键字上移至双亲结点中,而将双亲结点中小于(或大于),且紧靠该上移关键字的关键字移至被删关键字所在结点中。下图为删除 50 前、后的 3 阶 B-树。

(3)被删除关键字所在结点和其相邻的右兄弟结点(或左兄弟)结点中关键字的个数均等于[m/2]-1,假设该结点有右兄弟,且其右兄弟结点地址由双亲结点中的指针 Ai 所指,则在删去关键字之后,它所在结点中剩余的关键字和指针,加上双亲结点中的关键字 Ki 一起,合并到 AI 所指兄弟结点中(若没有右兄弟,则合并到左兄弟结点中)。

7.B+树

是应文件系统的需要而构造的一种 B-树的变形树。

与 B-差异

一棵 m 阶的 B+树和 m 阶的 B-树的差异在于:

(1)有 n 棵子树的结点中含有 n 个关键字。

(2)所有的叶子结点中包含全部关键字字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序连接。

(3)所有的非终端结点可以看成是索引部分,结点中含有其子树中最大(或最小)关键字。

B+树查找有两种方式:
  • 一种是从 sqt 开始按关键字从小到大顺序查找;
  • 另一种是从根结点 Root 开始,进行随机查找。在 B+树上进行随机查找、插入、删除的过程与 B-树类似。只是在查找时,若非终端结点上的关键字等于给定值,并不终止,而是继续向下直到叶子结点。在 B+树上插入仅在叶子结点上进行,当结点中的关键字个数大于 m 时要分裂成两个结点,它们所含关键字个数都为【m+1 /2】。并且,它们的双亲结点中应同时包含这两个结点中最大关键字。在 B+树上删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时,其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中关键字的个数少于【m/2】时,其和兄弟结点的合并过程和 B-树类似。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿