查找的基本概念
查找表:由同一类型的数据元素(或记录)构成的集合
静态查找表:查找的同时对查找表不做修改操作(如插入和删除)
动态查找表:查找的同时对查找表具有修改操作
关键字:数据元素(或记录)中某个数据项的值,可用来标识一个数据元素(或记录)
主关键字:唯一标识数据元素
次关键字:可以标识若干个数据元素
查找:也叫检索,是根据给定的某个值,在表中确定一个关键字等于给定值的记录或数据元素
关键字的平均比较次数,也称平均查找长度ASL。
n:记录的个数
pi:查找第i个记录的概率(通常认为pi=1/n)
ci:找到第i个记录所需的比较次数
线性表的查找
顺序查找(线性查找)
应用范围:顺序表或线性链表表示的静态查找表,表内元素之间无序
顺序表的表示:
typdef struct
{
KeyType key;//关键字域
InfoType otherinfo;//其他域
}ElemType;
typedef struct
{
ElemType *R;//表基址
int length;//表长
}SSTable;
顺序查找的过程:从表的一端开始逐个进行记录的关键字和给定值的比较
/*不用监视哨法,在顺序表中查找关键字等于key的元素*/
int SeqSearch ( sSTable ST,KeyType key)
{
i=ST.length;
while (i>=1&&ST.R[i].key!=key) i--;
return i;
}
把待查关键字key存入表头或表尾(俗称“哨兵”)这样可以加快执行速度。若将待查找的特定值key存入顺序表的首部(如0号单元),则顺序查找的实现方案为从后向前逐个比较。
int Search_Seq(SSTable ST,KeyType key)
{
ST.R[0].key=key;
for(i=ST.length;ST.R[i].key!=key;--i);
return i;
}
顺序查找的性能分析:
空间复杂度:一个辅助空间。
时间复杂度:查找成功时的平均查找长度设表中各记录查找概率相等ASL=(1+2+...+n)/n =(n+1)/2
,时间复杂度为O(n)。
顺序查找算法特点:
优点:算法简单,对表结构无任何要求(顺序和链式均可),无论记录是否按关键字有序均可应用
缺点:平均查找长度较大,n很大时查找效率较低
折半查找(二分查找、对分查找)
查找过程:每次将待查记录所在区间缩小一半直到查找成功或不成功为止
适用条件:采用顺序存储结构的有序表
算法实现:
先给数据排序(例如按升序排好),形成有序表,折半查找时,先求位于查找区间正中的对象的下标mid,用其关键码与给定值x比较:
ST.R[mid].key==x,查找成功;
ST.R[mid].key>x,把查找区间缩小到表的前半部分,继续;
ST.R[mid].key<x,把查找区间缩小到表的后半部分,继续。
如果查找区间已缩小到一个对象,仍未找到要查找对象,则查找失败
int Search_Bin(SSTable ST,KeyType 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;//后一子表查找
}
return 0;//表中不存在待查元素
}
int Search_Bin (SSTable ST,keyType key,int low,int high)//递归算法
{
if(low>high) return 0; //查找不到时返回0
mid=(low+high)/2;
if(key=-ST.elem[mid].key) return mid;//查找成功
else if(key<ST.elem[mid].key) return Search_Bin (ST,key,low,mid-1);//递归
else return Search_ Bin(ST,key,mid+1,high);//递归
}
折半查找的性能分析——判定树
若所有结点的空指针域设置为一个指向一个方形结点的指针,称方形结点为判定树的外部结点。对应的,圆形结点为内部结点。
查找成功时比较次数:为该结点在判定树上的层次数,不超过树的深度d=⌊⌋+1。
查找不成功的过程就是走了一条从根结点到外部结点的路径,其中外部结点不进行比较。比较次数仍然不超过⌊⌋+1。
折半查找的性能分析:
查找过程:每次将待查记录所在区间缩小一半,比顺序查找效率高,时间复杂度O()。
适用条件:采用顺序存储结构的有序表,不宜用于链式结构。
树的查找
动态查找表:
特点:表结构在查找过程中动态生成。
操作:检索、插入和删除。
二叉排序树(二叉查找树、检索树)
若其左子树非空,则左子树上所有结点的值均小于根结点的值。若其右子树非空,则右子树上所有结点的值均大于根结点的值。其左右子树本身又各是一棵二叉排序树。
中序遍历二叉排序树得到一个关键字的递增有序序列。
二叉排序树的操作:
[查找]
BSTree SearchBST(BSTree T,KeyType 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);//在右子树中继续查找
}
[插入]
void InsertBST (BSTree &T,ElemType e)
{
if(!T)
{
S= (BSTNode *)malloc(sizeof(BSTNode));
S->data=e;
S->lchild=S->rchild=NULL;
T=S;
}
else if(e.key < T->data.key) InsertBST(T->lchild,e);
else if(e.key > T->data.key) InsertBST(T->rchild,e);
}
[生成]
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树。
中序遍历二叉排序树可得到一个关键字的有序序列。这就是说,一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。
每次插入的新结点都是二叉排序树上新的叶子结点。则在进行插入操作时,不必移动其它结点,仅需改动某个结点的指针。这就相当于在一个有序序列上插入一个记录而不需要移动其它记录。
二叉排序树既拥有类似于折半查找的特性,又采用了链表作存储结构,因此是动态查找表的一种适宜表示。
[删除]
原则:在二叉排序树上删除一个结点后依旧要保持二叉排序树的特性。
假设:*p表示被删结点,PL和PR分别表示的左、右孩子指针,*f表示*的双亲结点。假定*p是*f的左孩子。
*p为叶子:删除此结点时,直接修改*f指针域即可。
*p只有一棵子树(或左或右):令PL或PR为*f的左子树即可。
*p有两棵子树:
二叉排序树查找的性能分析:
二叉排序树上查找某关键字等于结点值的过程,其实就是走了一条从根到该结点的路径。
比较的关键字次数=此结点的层次数
最多的比较次数=树的深度(或高度)
第i层结点需比较i次。在等概率的前提下,上述两图的平均查找长度为:
左图:
右图:
平均查找长度和二叉树的形态有关,即:
最好:与成正比(形态匀称,与二分查找的判定树相似)
最坏:(n+1)/2(单支树)
平衡二叉树(AVL树)
提高二叉排序树的查找效率:尽量让二叉树的形状均衡——平衡二叉树(AVL树)
定义:n=0,一棵空树。n>0,左子树和右子树的深度之差的绝对值不超过1,且左右子树都是平衡二叉树。
平衡因子(BF):某结点的BF为其左子树和右子树的深度之差。平衡二叉树中所有结点的BF值只取-1,0,1。
对于一棵有n个结点的AVL树,其高度保持在O()数量级,ASL也保持在O(
)量级。
如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转:LL平衡旋转、RR平衡旋转、LR平衡旋转、RL平衡旋转。
哈希(散列/杂凑)表的查找
哈希表可不经过任何比较,一次便能得到所查记录。它既是一种查找方法,又是一种存贮方法。
基本思想:记录的存储位置与关键字之间存在对应关系,Loc(i)=H(keyi) ,即哈希函数。
优点:查找速度极快O(1),查找效率与元素个数n无关。
对不同的关键字可能得到同一哈希地址,即key1!=key2,而f(key1)=f(key2),这种现象称冲突。具有相同函数值的关键字对该哈希函数来说,称做同义词。
冲突是不可能避免的,只能尽量减少。构造哈希表必须解决以下两个问题:构造好的哈希
函数(简单、哈希地址分布均匀)、制定一个好的解决冲突方案。
哈希表的构造
哈希函数的构造方法:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。
[直接定址法] Hash(key)=a·key+b(a、b为常数)
优点:以关键码key的某个线性函数值为哈希地址,不会产生冲突。
缺点:要占用连续地址空间,地址集合与关键字集合大小相等,空间效率低,适用范围窄。
[除留余数法] Hash(key)=key mod p(p是一个整数)
关键:如何选取合适的p
技巧:若设计的哈希表长为m,则一般取p为小于等于表长m的最大质数(也可以是不包含小于20质因子的合数)
处理冲突的方法:
1.开放定址法:有冲突时就去寻找下一个空的哈希地址,直至哈希地址不发生冲突为止。
线性探测法
=(Hash(key)+
) mod m(1≤i<m)
其中:Hash(key)为哈希函数,m为哈希表长度,为增量序列1,2,...,m-1,且
=i。
例:
关键码集为{47,7,29,11,16,92,22,8,3},设:哈希表表长为m=11。
哈希函数为Hash(key)=key mod 11。
①47、7(以及11、16、92)均是由哈希函数得到的没有冲突的哈希地址、
②Hash(29)=7,哈希地址有冲突,需寻找下一个空的哈希地址:由=(Hash(29)+1) mod 11=8,哈希地址8为空,因此将29存入。
③另外,22、8、3同样在哈希地址上有冲突,也是由找到空的哈希地址的。
其中3还连续移动了三次(二次聚集)
优点:只要哈希表未被填满,保证能找到一个空地址单元存放有冲突的元素。
缺点:可能使第i个哈希地址的同义词存入第i+1个地址,这样本应存入第i+1个哈希地址的元素变成了第i+2个哈希地址的同义词(二次聚集),因此,可能出现很多元素在相邻的哈希地址上“堆积”起来,大大降低了查找效率。
解决方案:二次探测法或伪随机探测法
二次探测法
=(Hash(key)+
) mod m
其中:为增量序列1²,-1²,2²,-2²,...,q²或1²,2²,...,q²
注:只有3这个关键码的冲突处理与上例不同,Hash(3)=3,哈希地址上冲突,由=(Hash(3)+1²) mod 11=4,仍然冲突,
=(Hash(3)-1²) mod 11=2,找到空的哈希地址,存入。
伪随机探测法
二次探测法和伪随机探测法的特点:
优点:可以缓解“二次聚集”现象。
缺点:在哈希表没有填满的情况下,不能保证一定找到不发生冲突的地址。
2.链地址法:相同哈希地址的记录链成一单链表,m个哈希地址就设m个单链表,然后用用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。
例:设{47,7,29,11,16,92,22,8,3,50,37,89}的哈希函数为:Hash(key)=key mod 11,
用链地址法处理冲突,则建表如下图所示。
注:有冲突的元素可以插在表尾,也可以插在表头。
特点:
1.非同义词不会冲突,无“二次聚集”
2.现象链表上结点空间动态申请,更适合于表长不确定的情况
3.附加指针域,存储效率较低
哈希表的查找
哈希查找的速度不是为真正的O(1) 。由于冲突的产生,使得哈希表的查找过程仍然要进
行比较,仍然要以平均查找长度ASL来衡量。在哈希表上进行查找的过程和哈希造表的过程基本一致。给定待查找关键字K值,根据造表时设定的哈希函数求得哈希地址;若表中此位置上为空,则查找不成功;若等于K值,则查找成功;否则根据造表时设定的处理冲突的方法找“下一地址”,直至哈希表中某个位置为“空”或者表中所填记录的关键字等于给定值时为止。
#define NULLKEY 0
typedef struct
{
int key;
}HashTable[m];
int SearchHash(HashTable HT, int key)
{
H0=H(key);//H(key)为哈希函数
if(HT[H0].key==NULLKEY) return -1;
else if(HT[H0].key==key) return H0;
else
{
for(int i=1;i<m;++i)
{
Hi=(H0+i)%m;
if(HT[Hi].key==NULLKEY) return -1;
else if(HT[Hi].key==key) return Hi ;
}
return -1;
}
}
效率分析:使用平均查找长度ASL来衡量查找算法,ASL取决于:哈希函数、处理冲突的方法、哈希表的装填因子。α越大,表中记录数越多,说明表装得越满,发生冲突的可能性就越大,查找时比较次数就越多。