第七章 查找
查找的基本概念
- 查找表
查找表是由同一类型的数据元素(或记录)构成的集合。 - 关键字
关键字是数据元素(或记录)中某个数据项的值,用它可以标识一个数据元素(或记录)。若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(对不同的记录,其主关键字均不同)。反之,称用以识别若干记录的关键字为次关键字。 - 查找
查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。若表中存在这样的一个记录,则称查找成功,此时查找的结果可给出整个记录的信息,或指示该记录在查找表中的位置;若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。 - 动态查找表和静态查找表
若在查找的同时对表执行修改操作(如插入和删除),则称相应的表为动态查找表,否则称之为静态查找表。 - 平均查找长度
为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度(Average Search Length,ASL)。
线性表的查找
顺序查找
顺序查找(Sequential Search)的查找过程为:从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功;反之,若查找整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
顺序查找方法既适用于线性表的顺序存储结构,又适用于线性表的链式存储结构。
- 顺序查找
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 100
typedef struct {
int* R;
int length;
}SSTable;
bool InitSSTable(SSTable &s) {
s.R= new int[MAXSIZE];
if (!s.R)
{
exit(-2);
return false;
}
s.length = 0;
return true;
}
int Search_Seq(SSTable ST,KeyType key)
{//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为该元素
在表中的位置,否则为0
for(i=ST.length;i>=1;--i)
if(ST.R[i].key==key) return i; //从后往前查找
return 0;
}
- 设置监视哨的顺序查找
int Search_Seq(SSTable ST,KeyType key)
{//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置,否则为0
ST.R[0].key=key; //“监视哨”
for(i=ST.length;ST.R[i].key!=key;--i); //从后往前找
return i;
}
时间复杂度为O(n)
顺序查找的优点是:算法简单,对表结构无任何要求,既适用于顺序结构,也适用于链式结构,无论记录是否按关键字有序均可应用。
缺点是:平均查找长度较大,查找效率较低,所以当n很大时,不宜采用顺序查找。
折半查找
折半查找(Binary Search)也称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
【算法步骤】
① 置查找区间初值,low为1,high为表长。
② 当low<=high时,循环执行以下操作:
mid取low和high的中间值;
将给定值key与中间位置记录的关键字进行比较,若相等则查找成功,返回中间位置mid;
若不相等则利用中间位置记录将表对分成前、后两个子表。如果key比中间位置记录的关键字小,则high取为mid−1,否则low取为mid+1。
③ 循环结束,说明查找区间为空,则查找失败,返回0。
int Search_Bin(SSTable ST,KeyType key)
{//在有序表ST中折半查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置,否则为0
low=1;high=ST.length; //置查找区间初值
while(low<=high)
{
mid=(low+high)/2;
if(key==ST.R[mid].key) return mid; //找到待查元素
else if(key<ST.R[mid].key) high=mid-1; //继续在前一子表进行查找
else low=mid+1; //继续在后一子表进行查找
} //while
return 0; //表中不存在待查元素
}
折半查找过程可用二叉树来描述。树中每一节点对应表中一个记录,但节点值不是记录的关键字,而是记录在表中的位置序号。把当前查找区间的中间位置作为根,把左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的决策树。
折半查找的时间复杂度为O(log2n )
折半查找的效率比顺序查找的高,但折半查找只适用于有序表,且限于顺序存储结构。.
折半查找的优点是:比较次数少,查找效率高。
其缺点是:对表结构要求高,只能用于顺序存储的有序表。采用折半查找前元素需要排序,而排序本身是一种费时的运算。同时为了保持顺序表的有序性,对有序表进行插入和删除时,平均比较和移动表中一半元素,这也是一种费时的运算。因此,折半查找不适用于数据元素经常变动的线性表。
分块查找
分块查找(Blocking Search)又称索引顺序查找,这是一种性能介于顺序查找和折半查找之间的查找方法。
分块查找过程需分两步进行。
先确定待查记录所在的块(子表),然后在块中顺序查找。
假设给定值key = 38,则先将key依次和索引表中各分块的最大关键字进行比较,因为22<key<48,则关键字为38的记录若存在,必定在第二个子表中。由于同一索引项中的指针指示第二个子表中的第一个记录是表中第7个记录,则自第7个记录起进行顺序查找,直到ST.elem[10].key = key为止。假如此子表中没有关键字等于key的记录(例如,key = 29时自第7个记录起至第12个记录的关键字和key都不等),则查找不成功。
分块查找的优点是:在表中插入和删除数据元素时,只要找到该元素对应的块,就可以在该块内进行插入和删除运算。由于块内是无序的,故插入和删除比较容易,无须进行大量移动。如果线性表既经常动态变化,又需对其进行快速查找,则可采用分块查找。
其缺点是:要增加一个索引表的存储空间并对初始索引表进行排序运算。
树表的查找
二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。
- 二叉排序树的定义
(1)若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
(2)若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
(3)它的左、右子树也分别为二叉排序树。
二叉排序树是递归定义的。由定义可以得出二叉排序树的一个重要性质:中序遍历一棵二叉树时可以得到一个节点值递增的有序序列。
//- - - - -二叉排序树的二叉链表存储表示- - - - -
typedef struct
{
KeyType key; //关键字项
InfoType otherinfo; //其他数据项
}ElemType; //每个节点的数据域的类型
typedef struct BSTNode
{
ElemType data; //每个节点的数据域包括关键字项和其他数据项
struct BSTNode *lchild,*rchild; //左右孩子指针
}BSTNode,*BSTree;
- 二叉排序树的查找
二叉排序树的递归查找
【算法步骤】
① 若二叉排序树为空,则查找失败,返回空指针。
② 若二叉排序树非空,将给定值key与根节点的关键字T−>data.key进行比较:
若key等于T−>data.key,则查找成功,返回根节点地址;
若key小于T−>data.key,则递归查找左子树;
若key大于T−>data.key,则递归查找右子树。
BSTree SearchBST(BSTree T,KeyType key)
{//在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素
//若查找成功,则返回指向该数据元素节点的指针,否则返回空指针
if( (!T) || key == T->data.key) return T; //查找结束
else if(key < T->data.key) return SearchBST(T->lchild,key); //在左子树中继续查找
else return SearchBST(T->rchild,key); //在右子树中继续查找
}
含有n个节点的二叉排序树的平均查找长度和树的形态有关。
二叉排序树的平均查找长度仍然和log2n是同数量级的。
对于需要经常进行插入、删除和查找运算的表,采用二叉排序树比较好。
- 二叉排序树的插入
【算法步骤】
① 若二叉排序树为空,则将待插入节点S作为根节点插入空树。
② 若二叉排序树非空,则将key与根节点的关键字T−>data.key进行比较:
若key小于T−>data.key,则将S插入左子树;
若key大于T−>data.key,则将*S插入右子树。
void InsertBST(BSTree &T,ElemType e)
{//当二叉排序树T中不存在关键字等于e.key的数据元素时,则插入该元素
if(!T)
{ //找到插入位置,递归结束
S=new BSTNode; //生成新节点*S
S->data=e; //新节点*S的数据域置为e
S->lchild=S->rchild=NULL; //新节点*S作为叶子节点
T=S; //把新节点*S链接到已找到的插入位置
}
else if(e.key < T->data.key)
InsertBST(T->lchild,e); //将*S插入左子树
else if(e.key > T->data.key)
InsertBST(T->rchild,e); //将*S插入右子树
}
二叉排序树插入的基本过程是查找,所以时间复杂度同查找一样,是O(log2n)。
- 二叉排序树的创建
【算法步骤】
① 将二叉排序树T初始化为空树。
② 读入一个关键字为key的节点。
③ 如果读入的关键字key不是输入结束标志,则循环执行以下操作:
将此节点插入二叉排序树T中;
读入一个关键字为key的节点。
void CreatBST(BSTree &T)
{//依次读入关键字为key的节点,将相应节点插入二叉排序树T中
T=NULL; //将二叉排序树T初始化为空树
cin>>e;
while(e.key!=ENDFLAG) //ENDFLAG为自定义常量,作为输入结束标志
{
InsertBST(T,e); //将此节点插入二叉排序树T中
cin>>e;
}
}
假设有n个节点,则需要n次插入操作,而插入一个节点的算法时间复杂度为O(log2n),所以创建二叉排序树算法的时间复杂度为O(nlog2n)。
- 二叉排序树的删除
void DeleteBST(BSTree &T,KeyType key)
{//从二叉排序树T中删除关键字等于key的节点
p=T;f=NULL; //初始化
/*------------下面的while循环从根开始查找关键字等于key的节点*p---------------*/
while(p)
{
if(p->data.key==key) break; //找到关键字等于key的节点*p,结束循环
f=p; //*f为*p的双亲节点
if(p->data.key>key) p=p->lchild; //在*p的左子树中继续查找
else p=p->rchild; //在*p的右子树中继续查找
} //while
if(!p) return; //找不到被删节点则返回
/*----考虑3种情况实现p所指子树内部的处理 :*p左右子树均不空、无右子树、无左子树---*/
q=p;
if((p->lchild)&&(p->rchild)) //被删节点*p左右子树均不空
{ s=p->lchild;
while (s->rchild) //在*p的左子树中继续查找其前驱节点,即最右下节点
{
q=s; s=s->rchild; //向右到尽头
}
p->data=s->data; //s指向被删节点的“前驱”
if(q!=p) q->rchild=s->lchild; //重接*q的右子树
else q->lchild=s->lchild; //重接*q的左子树
delete s;
return;
} //if
else if(!p->rchild) //被删节点*p无右子树,只需重接其左子树
{
p=p->lchild;
} //else if
else if(!p->lchild) //被删节点*p无左子树,只需重接其右子树
{
p=p->rchild;
} //else if
/*-----------------将p所指的子树挂接到其双亲节点*f相应的位置----------------*/
if(!f) T=p; //被删节点为根节点
else if(q==f->lchild) f->lchild=p; //挂接到*f的左子树位置
else f->rchild=p; //挂接到*f的右子树位置
delete q;
}
同二叉排序树插入一样,二叉排序树删除的基本过程也是查找,所以时间复杂度仍是O(log2n)。
平衡二叉树
- 平衡二叉树的定义
希望二叉树的高度尽可能小称为平衡二叉树(Balanced Binary Tree或Height-Balanced Tree),因由苏联数学家阿德尔森-维尔斯基(Adelson-Velskii)和兰迪斯(Landis)提出,所以又称AVL树。
平衡二叉树或者是空树,或者是具有如下特征的二叉排序树:
(1)左子树和右子树的深度之差的绝对值不超过1;
(2)左子树和右子树也是平衡二叉树。
时间复杂度是O(log2n)。
-
平衡二叉树的平衡调整方法
调整方法是:找到离插入节点最近且平衡因子绝对值超过1的祖先节点,以该节点为根的子树称为最小不平衡子树,可将重新平衡的范围局限于这棵子树。
一般情况下,假设最小不平衡子树的根节点为A,则失去平衡后进行调整的规律可归纳为下列4种情况。-
LL型
-
RR型
-
LR型
-
RL型
-
-
平衡二叉搜索树的插入
在平衡二叉搜索树(Balanced Binary Search Tree,BBST)上插入一个新的数据元素e的递归算法可描述如下:
手算平衡二叉树的插入
B-树
- B- 树的定义
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
(1)树中每个节点至多有m棵子树;
(2)若根节点不是叶子节点,则至少有两棵子树;
(3)除根之外的所有非终端节点至少有⌈m/2⌉棵子树;
(4)所有的叶子节点都出现在同一层次上,并且不带信息,通常称为失败节点(失败节点并不存在,指向这些节点的指针为空。引入失败节点是为了便于分析B−树的查找性能);
(5)所有的非终端节点最多有m − 1个关键字。 - B- 树的查找
- B- 树的插入
- B- 树的删除
B+树
-
B+ 树和 B- 树的差异
-
B+ 树的查找、插入和删除
散列表的查找
散列表的基本概念
如果能在元素的存储位置和其关键字之间建立某种直接关系,那么在进行查找时,就无须作比较或只需作很少的比较,按照这种关系直接由关键字找到相应的记录。这就是散列查找法(Hash Search)的思想,它通过对元素的关键字值进行某种运算,直接求出元素的地址,即使用关键字到地址的直接转换方法,而不需要反复比较。因此,散列查找法又叫杂凑法或散列法。
散列法中常用的几个术语:
- 散列函数和散列地址
在记录的存储位置p和其关键字key之间建立一个确定的对应关系H,使p = H(key),称这个对应关系H为散列函数,p为散列地址。 - 散列表
一个有限连续的地址空间,用以存储按散列函数计算得到相应散列地址的数据记录。通常散列表的存储空间是一个一维数组,散列地址是数组的下标。 - 冲突和同义词
对不同的关键字可能得到同一散列地址,即key1 ≠ key2,而H(key1) =H(key2),这种现象称为冲突。
具有相同函数值的关键字对该散列函数来说称作同义词,key1与key2互为同义词。
散列函数的构造方法
-
数字分析法
-
平方取中法
-
折叠法
-
除留余数法
处理冲突的方法
处理冲突的方法与散列表本身的组织形式有关。按组织形式的不同,处理冲突的方法通常分两大类:开放地址法和链地址法。
- 开放地址法
上述3种处理方法各有优缺点。
线性探测法的优点是:只要散列表未填满,总能找到一个不发生冲突的地址。
缺点是:会产生“二次聚集”现象。
而二次探测法和伪随机探测法的优点是:可以避免“二次聚集”现象。其缺点也很显然:不能保证一定找到不发生冲突的地址。
- 链地址法
散列表的查找
//- - - - -开放地址法散列表的存储表示- - - - -
#define m 20 //散列表的表长
typedef struct{
KeyType key; //关键字项
InfoType otherinfo; //其他数据项
}HashTable[m];
算法 散列表的查找
【算法步骤】
① 给定待查找的关键字key,根据创建表时设定的散列函数计算H0 = H(key)。
② 若单元H0为空,则所查元素不存在。
③ 若单元H0中元素的关键字为key,则查找成功。
④ 否则重复下述解决冲突的过程:
按处理冲突的方法,计算下一个散列地址Hi;
若单元Hi为空,则所查元素不存在;
若单元Hi中元素的关键字为key,则查找成功。
#define NULLKEY 0 //单元为空的标记
int SearchHash(HashTable HT,KeyType key)
{//在散列表HT中查找关键字为key的元素,若查找成功,返回散列表的单元标号,否则返回-1
H0=H(key); //根据散列函数H(key)计算散列地址
if(HT[H0].key==NULLKEY) return -1; //若单元H0为空,则所查元素不存在
else if(HT[H0].key==key) return H0; //若单元H0中元素的关键字为key,则查找成功
else
{
for(i=1;i<m;++i)
{
Hi=(H0+i)%m; //按照线性探测法计算下一个散列地址Hi
if(HT[Hi].key==NULLKEY) return -1; //若单元Hi为空,则所查元素不存在
else if(HT[Hi].key==key) return Hi; //若单元Hi中元素的关键字为key,则查找成功
} //for
return -1;
} //else
}
小结