数据结构(十二)----查找

目录

一.查找的概念

二.查找算法

1.顺序查找

顺序查找的查找效率:

顺序查找的优化:

•有序表的优化(缩短查找失败的平均查找长度)

•被查概率不相等的表的优化(缩短查找成功的平均查找长度)

2.折半查找

折半查找的查找效率:

折半查找判定树的构造:

3.分块查找(索引顺序查找)

分块查找的查找效率:

4.散列查找

(1)拉链法

散列查找的查找效率(拉链法):

如何设计冲突更少的散列函数:

(2)开放定址法

① 线性探测法

② 平方探测法

③ 伪随机序列法

(3)再散列法


一.查找的概念

查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找

查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成。

关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是
唯一的。例如班级同学的学号就可以作为关键字

•查找常见操作:

对查找表的常见操作有:

查找符合条件的数据元素。     

② 插入,删除某个数据元素。

若只是查找符合条件的数据元素,不用改变查找表的数据,那么这样的查找表就是静态查找表。若还需要对表中的数据元素进行插入和删除,那么这样的查找表为动态查找表

•查找算法的评价指标:

查找长度:在查找运算中,需要对比关键字的次数称为查找长度。

平均查找长度(ASL,Average Search Length):所有查找过程中进行关键字的比较次数的平均值。通常认为查找任何一个元素的概率都相同。

在学习二叉排序树的时候就遇到过ASL的计算,例如左边的图

查找50只需要经过1次比较,所以1*1,查找26或66时都需要经过2次对比,所以2*2。依次类推,最后除以元素的个数,就是平均查找长度。

以上讨论的是查找成功的情况,但是在评价一个查找算法的效率时,通常考虑查找成功/查找失败两种情况的 ASL。

补充:二叉排序树查找成功和查找失败的计算问题,可以看看:

http://t.csdnimg.cn/V8s3f

二.查找算法

1.顺序查找

顺序查找又称为"线性查找",通常用于线性表(顺序表,链表)。

typedef struct{    //查找表的数据结构(顺序表)
    ElemType *elem;    //动态数组基址
    int TableLen;    //表的长度
}SSTable;

//顺序查找
int Search_Seq(SSTable ST,ElemType key){
    int i;
    for(i=0;i<ST.TableLen && ST.elem[i]!=key; ++i);
    //查找成功,则返回元素下标;查找失败,则返回-1
    return i==ST.TableLen?-1:i;
}

课本中的实现方法如下,二者本质是一样的:

typedef struct{    //查找表的数据结构(顺序表)
    ElemType *elem;    //动态数组基址
    int TableLen;    //表的长度
}SSTable;

//顺序查找
int Search_Seq(SSTable ST,ElemType key){
    ST.elem[0]=key;    //“哨兵”,将要查找的数据放到数组的0号位置
    int i;    
    for(i=ST.TableLen; ST.elem[i]!=key; --i);    //从后往前找
    return i;    //查找成功,则返回元素下标;查找失败,则返回0
}

相比于上面那段代码,这段代码的优点是,无论能否查找到目标数据,都能正常跳出for()循环,无需判断是否越界(i<ST.TableLen),效率更高。

顺序查找的查找效率:

查找成功:

若采用有"哨兵"的代码实现方式,假设查找每个元素的概率都是相同的,且是\frac{1}{n},那么查找成功的概率就是:

1*\frac{1}{n}+2*\frac{1}{n}+3*\frac{1}{n}+.....+n*\frac{1}{n}

查找成功的平均查找长度为:

查找失败:

查找失败的情况下,需要对比n+1次关键字:

无论查找成功还是查找失败,时间复杂度都为O(n)。

顺序查找的优化:
有序表的优化(缩短查找失败的平均查找长度)

若查找的表元素是递增或递减的。例如下图,当我们要查找21这个元素时,查找到比21大的第一个元素29时都没有找到,后面肯定也找不到了,停止查找。

下图是有序表的查找判定树,若有序表中有n个元素的话,那么就有n+1种查找失败的情况,因为有n+1个失败结点。在这种情况下计算查找失败的平均查找长度:

若要查找的元素落在(-∞,7),只需要对比1次关键字即可,出现这种情况的概率是\frac{1}{n+1}

若要查找的元素落在(7,13),需要对比2次关键字即可,出现这种情况的概率是\frac{1}{n+1}

 以此类推,得到查找失败的平均查找长度为:

:最后加了两个n,因为最下面的两种失败情况,都需要经过n次关键字对比。

我们还可以观察到

1.一个成功结点的查找长度=自身所在层数。例如查找19,那么他的查找长度就是自身所在的层数3(也就是要对比3次关键字)。

2.一个失败结点的查找长度=其父节点所在层数。若查找的关键字落在了(13,19),那么直到确认为查找失败,需要对比3次关键字对比(也就是19所在的层数)。

3.默认情况下,各种失败情况或成功情况都等概率发生。

•被查概率不相等的表的优化(缩短查找成功的平均查找长度)

若各个关键字被查找的概率不相等,那么可以把被查找的概率更大的关键字放到更靠前的位置。这样做能在查找成功的情况下,使得查找长度缩短。

但由于这样做又会使得表乱序,所以查找失败的情况不能得到改善。

2.折半查找

折半查找,又称“二分查找”,仅适用于有序的顺序表。

算法的实现过程如下

首先使用两个指针分别表的开头元素和末尾元素:

第一轮对比的元素是"low"和"high"指向的元素的中间的元素:

由于33>mid,那么33这个元素只可能在mid的右边。所以把low指针指向mid的右边的数。

第二轮继续检查"low"和"high"指向的元素的中间的元素:

37(mid)>33,所以33只可能出现在37的左边区域。所以将“high”指针指向37的左边的数:

第三轮继续检查"low"和"high"指向的元素的中间的元素mid,即\left \lfloor \frac{6+7}{2} \right \rfloor(向下取整)=6,将33与32进行对比,32<33,如果33存在,那么一定是在mid右边的区域中。所以让"low"指针指向mid右边的数。

继续检查"low"和"high"指向的元素的中间的元素mid。7+7/2=7,将33与该位置的元素进行对比,33=33。查找成功。

查找失败:

前面的过程相同:

mid=\frac{high+low}{2}=\frac{10+10}{2}=10,由于mid小于12,所以如果12存在,则应该在10的右边的区域。所以让low指向mid右边的数(low=mid+1)

本应该在high左边的指针,指向了high右边的数,说明查找失败。

代码如下:

typedef struct{    
    ElemType *elem;    //动态数组基址
    int TableLen;    //表的长度
}SSTable;

//折半查找(基于升序排列)
int Binary_Search(SSTable L,ElemType key){
    int low=0,high=L.TableLen-1,mid;
    while(low<=high){
        mid=(low+high)/2;
        if(L.elem[mid]==key)    //取中间位置
            return mid;    //查找成功则返回所在位:
        else if(L.elem[mid]>key)
            high=mid-1;    //从前半部分继续查找
        else    
            low=mid+1;    //从后半部分继续查找
    }    
    return -1;    //查找失败,返回-1
}

//如果是降序排列,只需要判断条件更改下即可
int Binary_Search(SSTable L,ElemType key){
    int low=0,high=L.TableLen-1,mid;
    while(low<=high){
        mid=(low+high)/2;
        if(L.elem[mid]==key)    //取中间位置
            return mid;    //查找成功则返回所在位:
        else if(L.elem[mid]>key)
            low=mid+1;    //从后半部分继续查找
        else    
            high=mid-1;    //从前半部分继续查找  
    }    
    return -1;    //查找失败,返回-1
}

所以折半查找只适用于有序的表,那为什么一定要是顺序表呢?

如果用顺序表,可以根据数组下标找到中间位置元素,而链表必须从头开始扫描才能找到中间位置元素,也就是说顺序表拥有随机访问的特性,而链表没有。

折半查找的查找效率:

如下图所示,如果要查找的元素是29,那么只需要进行一次对比即可,如果要查找13或37,则需要经过2次对比,依次类推。

可以发现,在查找成功的情况下,折半查找的查找次数为4:

平均查找长度为:

在查找失败的情况下,加入失败结点:

平均查找长度为:

折半查找判定树的构造:

如果当前low和high之间有奇数个元素

则 mid 分隔后,左右两部分元素个数相等

如果当前low和high之间有偶数个元素

则 mid 分隔后,左半部分比右半部分少一个元素

折半查找的判定树中,若采用mid=\left \lfloor (low+high)/2 \right \rfloor(向下取整),则对于任何一个结点,必有:右子树结点数-左子树结点数=0或1(右子树比左子树的结点数多一个,或者右子树的结点数=左子树结点数)

 所以如果有两个元素,判定树为:

:图中数字只是一个编号,并不是关键字的值。

如果有3个元素,判定树为:

右子树的结点数最多只能比左子树的结点数多一个

如果有4个元素,判定树为:

所以可以观察到:

1.折半查找的判定树一定是平衡二叉树

2.折半查找的判定树中,只有最下面一层是不满的。因此,元素个数为n时,树高h =\left \lceil log_{2}(n+1) \right \rceil(和完全二叉树树高的计算方法一样)。树高直接反映了折半查找的时间复杂度(不含失败结点的情况,如果包含失败结点,就是h+1)。

3.判定树结点关键字:左<中<右,满足二叉排序树的定义。失败结点:n+1个(等于成功结点的空链域数量)。

假设一棵判定树的树高为h=\left \lceil log_{2}(n+1) \right \rceil,那么查找成功的的平均查找长度ASL一定<=h。

之前说过失败结点的查找长度=父结点所在的层次,所以查找失败的平均查找长度ASL也<=h。所以折半查找的时间复杂度为O(log_{2}n)。

注:

1.折半查找时间复杂度=O(log_{2}n)顺序查找的时间复杂度=O(n)。那么折半查找的速度一定比顺序查找更快?当然不是。例如下图,若采用顺序查找,那么对比一次就找到了目标元素,而采用折半查找,显然比较的次数会更多。

所以大部分情况下,折半查找比顺序查找更优秀,但不能说任何情况下都比顺序表优秀。

2.之前说的都是mid=\left \lfloor (low+high)/2 \right \rfloor,那么 mid=\left \lceil (low+high)/2 \right \rceil的情况是什么样子?

如果有偶数个元素,左右两部分元素个数相等。

如果有奇数个元素,左子树的元素个数比右子树的元素个数多一个。和向下取整的结论是相反的。

所以,若mid=\left \lceil (low+high)/2 \right \rceil,则必有左子树的结点数-右子树的结点数=0或1

3.分块查找(索引顺序查找)

如下表所示,虽然下面表中存在的元素是乱序的,但是可以把他分为一块一块的元素(块内无序,块间有序),第一块的元素<=10,第二块的元素<=20,第三块地元素<=30,依次类推。

由于这种性质存在,所以可以为查找表建立上一级的索引:

//索引表
typedef struct {
    ElemType maxValue;
    int low,high;
}Index;

//顺序表存储实际元素
ElemType List[100];

分块查找的过程如下

若想在表中查找目标关键字22,可以先查找索引表,从索引表的第一个元素往后查找:

10<22,继续查找下一个方块,20<22,查找下一个方块。30>=22,所以如果存在22,一定是在这个方块内。

确定了方块,就从方块的起始位置开始寻找。

当检索到7号位置,指向的元素和要查找的元素相等,查找成功。

若要查找29,那么出现的情况如下图所示,在30这个方块所包含的6-8号位置中都没有29号元素,也就是查找到9号元素时,说明查找失败

总结:

① 在索引表中确定待查记录所属的分块(可顺序、可折半),由于索引表中存储的元素是有序的,所以可以用折半查找。

② 在块内顺序查找,因为分块内的元素是乱序存放的。

下面举一个用折半查找索引元素的例子:

刚开始"low"和“high”分别指向开头元素和结尾元素:

由于mid=30>19,所以"high"指向mid-1,mid=(low+high)/2(向下取整),所以mid=10。

10<19,所以low=mid+1,mid=(low+high)/2(向下取整),所以mid=20。

19<20,所以high=mid-1:

由于high<low,这意味着要在low所指分块中查找

所以,若索引表中不包含目标关键字,则折半查找索引表最终停在 low>high,要在low所指分块中查找。

因为在high<low的前一步,一定是low=high=mid。在这一步时,若mid<要查找的元素,那么low+1,加1后的元素一定是比查找的元素更大的,所以就是我们要查找的分块。

若mid>要查找的元素,那么high-1,low的位置不变,low>high时,low指向的元素一定比要查找的元素大。而索引表当中保存的是分块中最大的关键字,所以我们需要在比要查找的值更大 的分块中查找,也就是在low指向的位置查找,其实多举例子就容易理解了。

再举一个例子:

假设此次要查找54:

折半查找最后会停在high=4,low=5的位置。同样是low>high,本该在low所指的分块内查找,但是low已经超出了索引表的范围,所以查找失败。

分块查找的查找效率:
对如下的表进行分析,共有14个元素,各自被查的概率是14: 若采用顺序查找,查找的元素是7,那么需要先查1次索引表,再查分块中的元素,总共2次。10同理,需要查3次,13需要查3次。
再把被查概率和各自查找次数相加即可。
索引表采用折半查找,则找到30需要对比4次,若想找到27,需要对比4次(不是2次,需要low指针>high指针才能判断索引的分块, 因为目标关键字没有在索引表中)。

上面的表中分块是不均匀的,但是若分块相同,那么计算的方法是有规律的。

假设,长度为n的查找表被均匀地分为b块,每块s个元素。

设索引查找和块内查找的平均查找长度分别为L_{I}L_{S},则分块查找的平均查找长度为:

ASL=L_{I}+L_{S}

由于n=sb,b=n/s,将这个式子带入ASL,就能得到上述ASL计算公式。

若n=10000,则ASLmin=101:最优的分块是有100个分块,每个分块内有100个记录。

例题:

1.设顺序存储的某线性表共有 123 个元素,按分块查找的要求等分为3块。若对索引表采用顺序查找法来确定子块,且在确定的子块中也采用顺序查找法,则在等概率情况下,分块查找成功的平均查找长度为()

由题意可知:n=123,s=123/3,带入:\frac{s^2+2s+n}{2s},得到:ASL=23

2.为提高查找效率,对有 65025个元素的有序顺序表建立索引顺序结构,在最好情况下查找到表中已有元素最多需要执行( )次关键字比较。

最好情况\sqrt{65025}=255,也就是有255个索引块,每个索引块中索引项的个数为255。并采用折半查找。

最多比较次数\left \lceil log_2(n+1) \right \rceil,即折半查找树的树高(和完全二叉树的算法是一样的)

所以比较次数就是:\left \lceil log_2{(255+1)} \right \rceil(索引块)+\left \lceil log_2{(255+1)} \right \rceil(索引项)=16

也就是索引想和索引块内都采用折半查找。

补充:

若查找表是“动态查找表”,如下图所示,若要插入元素8,那么只能在第一个分块内插入,其他位置的元素都要往后移。开销较大。

所以可以采用链式存储,若要插入8这个元素,只用放到第一个分块最后即可。

4.散列查找

散列查找是基于散列表进行的,散列表(Hash Table)又称哈希表。是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关

如何建立关键字和存储地址的映射关系呢?通过“散列函数(哈希函数)”:Addr=H(key),举个例子:

例:有一堆数据元素,关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数
H(key)_key%13。通过13取余,每一个元素都能被映射到0~12这个区间内。

 映射过程如下:

各元素取余后放到相应位置:

1%13=1,但是1这一位置已经被占了,要怎么处理呢?

注:

若不同的关键字通过散列函数映射到同一个值,则称它们为“同义词”

通过散列函数确定的位置已经存放了其他元素,则称这种情况为"冲突"

处理冲突的方法:

(1)拉链法

拉链法就是把所有“同义词”存储在一个链表中。用这种方法,数组中不会存放数据元素,只会存放指向某个元素的指针。

按照这种方法,得到的结果如下(下面的结果是将新元素放到链表的链尾,当然也可以把新元素放到链表的链头):

散列查找的查找效率(拉链法):

如何基于这一数据结构进行查找操作呢?

若要查找27这个关键字,通过散列函数计算目标元素存储地址:Addr=H(Key):27%13=1,27这个数据元素如果存在一定是在1所连接的链表当中。所以在链表中依次检查各个元素的值即可。查找长度是3。

若要查找21这个关键字,Addr=H(Key):21%13=8,如果21存在一定是在8所指的链表中,但是8所指的链表头指针是空的,查找失败,查找21这个关键字的查找长度(对比关键字的次数,没有把指针的判断算到查找长度中)为0。若把指针的判断也算到查找长度中,那么查找21的查找长度=1。同理,66这个关键字的查找长度是4。

那么查找成功的平均查找长度是多少?

图中有12个数据元素,连接在第一行的元素有6个,第二行的有4个,所以查找成功的平均查找长度为:

另一种查找成功的平均查找长度如下,若要查找关键字14,那么查找长度为1,若要查找的关键字是1,查找长度为2。以此类推:

上面大于1的关键字反映的是和第一行的关键字存在“冲突”,并且数值越大,冲突越多,查找效率就越低。

更理想的情况如下,也就是所有的关键字都没有同义词

这样散列查找的时间复杂度可到达O(1)。

查找失败的平均查找长度怎么计算?

对于散列函数,H(key)=key%13,无论key等于多少,余13后,任何一个只有可能落在0~12这个区间中。假设查找失败的关键字映射到任何位置的概率相同,也就是1/13。

若映射到0,不需要对比任何关键字,所以查找长度为0,映射到1,需要将链表中所有4个关键字都对比一遍才能确定查找失败,以此类推,得到查找失败的平均查找长度为:

可以观察到,分子刚好是表中数据元素的个数,分母为散列表的长度。

这样的计算方式和装填因子\alpha的计算方式是相同的。

装填因子\alpha=表中记录数/散列表长度,所以装填因子会直接影响散列表的查找失败的查找效率。其实也可以间接反映查找成功的查找效率,因为装填因子越大,表中存储的数据元素就越多,发生冲突的可能性就越大,在查找成功的情况下平均查找长度也会越大。

所以,散列表的平均查找长度与装填因子\alpha直接相关,表的查找效率不直接依赖于表中已有表项个数(n)或表长(m)。若表中存放的记录全是某个地址的同义词,则平均查找长度为O(n)

如何设计冲突更少的散列函数:

除留余数法----H(key)= key % p
散列表表长为m,取一个不大于m但最接近或等于m的质数p(质数又称素数。指除了1和此整数自身外,不能被其他自然数整除的数)

例如,散列表的表长是13,那么散列函数 H(key)=key%13(不大于13的最大质数是13)。又例如,散列表表长15,散列函数 H(key)=key%13(不大于15的质数也是13)。这会导致所有的数据元素只会被映射到0~12位置,13,14这两个位置是没有用的。

为什么一定要取质数呢

因为设计冲突更少的散列函数的目标是让不同关键字的冲突尽可能少,而取质数就可以达到这一目的。

若关键字都为偶数,若散列函数设计为让关键字对8取余,各关键字的散列地址会集中在0,2,4,6位置,而对质数7取余,即使牺牲了某个位置,但是数据的分布会更均匀,冲突更少

虽然可能在某些情况下,对其他数取余更好,但是在多数情况下,特别是在关键字呈现某些规律的情况下,取质数能使关键字的分布更加均匀。

直接定址法----H(key)= key 或 H(key)= a*key +b

其中,a和b是常数。这种方法计算最简单,且不会产生冲突(因为各个关键字的值是不同的,进行映射时数据元素的位置也是不同的)。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

数字分析法----选取数码分布较为均匀的若干位作为散列地址。

例如电话号码,总共有13位数,这13位数在各个位置出现的频率时不相同的,例如开头可能都是138,199等等,而某些位数据分布是比较均匀的,即没有规律。 

可以这样设计散列表,将手机号后4位作为散列地址,将后四位是0000的手机号连接在第一个位置的链表的。

平方取中法----取关键字的平方值的中间几位作为散列地址。

具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。

例如,要存储整个学校的学生信息,以“身份证号”作为关键字设计散列函数,由于身份证号各个位置数据的分布是不均匀的,像“年,月,日”可能都是“199几”开头。

这样的情况,我们可以将身份证号平方,取中间的5个数作为散列地址,这样能使散列地址分布更加均匀。

若散列表长度为100..00(18个0),则可以直接用身份证号作为散列地址,且不可能有冲突,查找时间复杂度为O(1)。

所以,散列查找是典型的“用空间换时间”的算法,只要散列函数设计的合理,则散列表越长,冲突的概率越低。

补充:拉链法的优化

若可以保持关键字升序或降序排列,那么在顺序查找链表时可以提高查找效率,主要是提高查找失败时的查找效率(在顺序查找时讲过)。

(2)开放定址法

上面讲的是处理冲突的方法是拉链法,就是把所有"同义词"存储在一个链表中。这里来讲另一种处理冲突的方法:开放定址法,就是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:

当发生第 i 次冲突的时候,会将哈希函数算得的地址作为起始,加上增量d_{i}(H(key)+d_{i}),再对哈希表的表长取模。

线性增量如何设计?

① 线性探测法

d_{i}=0,1,2,3...m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空。

:有一堆数据元素,关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数H(key)=key%13。  

依次存放19,14,23后:

存放1时,发现存放1的位置已经被占了,那么就是用线性探测法:

H0=(1+d0)%16=1(d0=0),1想存放在1号位置,1号位置被占,继续探测;

H1=(1+d1)%16=2,(d1=1),1想存放在2号位置,由于2号位置空闲,所以1可以放在2号位置。

当想存放84这个关键字时,冲突又发生了,所以用线性探测法,依次探测当前发生冲突的位置的后续位置,哪一个是空闲的。

H0=(6+0)%16=6,6号位置被占,继续探测;

H1=(6+1)%16=7,7号位置被占,继续探测;

H2=8,8号位置空闲,所以84这个关键字可以放在8号位置。

所以这个过程探测了3次,发生冲突2次。

依次类推,得到下表:

这里一定要厘清过程:

首先是对13取模,因为题目规定H(key)=key%13哈希函数的值域是[0,12]。

若产生冲突,则用线性探测处理冲突:H(H(key)+1)%16(表长),冲突处理函数值域[0,15]。

例如,想存放25这个关键字,有哈希函数算地25应该存放在12号位置,由于12号被占,所以经过线性探测法,25关键字就被存放在13这个位置。

线性探测法下的查找操作:

若想在表中查找27这个元素,用哈希函数计算得到27应该存放的位置是1,但是1号位置已经被占了,并且关键字不是17:

所以接下来会按线性探测法继续查找接下来的位置:

冲突次数为3,对比了4次关键字,所以27的查找长度=4。

可以观察到,上图中14,1与27是同义词,也就是取模都为1,但是68与27并不是同义词,所以在开放定址法中,既有可能与同义词发生冲突,也有可能与非同义词发生冲突。

同理,若要查找元素21,那么就需要从起始位置开始一直对比,一直冲突,直到查找到13号位置(13号位置没有元素了),才确定查找失败。21的查找长度=6(对空位置的判断也要算作一次比较)。

:在这里,要将空位置的判断算作一次比较,但是在拉链法中,我们并没有将空指针的判断算作一次比较。

可以这样理解,拉链法中数组中存放的是一个个指针,如下图所示。而在开放定址法中,数组中存放的是一个个元素,所以要把与空位置的对比算作一次比较。

所以如果使用线性探测法,并且数据元素是连续存放的,那么在查找失败的情况下,要找到空闲的位置才能确定查找失败。

若表中有空缺的位置,那么查找的效率会提升,例如找21这个关键字,21%13=8,但是8号位置被占,所以继续往后寻找,找到10号位置时,发现10号位置空闲,那么就能确定查找失败(因为按照线性探测法存储是不会出现空位的)。

线性探测法下的删除操作:

:有一堆数据元素,关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数H(key)=key%13

在这个表中删除数据元素1:

若要查找27这个关键字,27%13=1,但1号位置被占,所以继续往后探测,在和2号位置进行比较时,发现2号位置是空的,按照上面的说法应该查找失败才对。但是27号元素是真实存在的。

所以,采用“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除(没有真正将这个位置置空)。

如下图所示,若要查找的目标元素是79,79%13=1,那么依然需要从头往后探测,直到找到79这个元素才可以,也就是查找过程产生了8次冲突,进行了9次对比。表看起来很满,实际都是空的,这就是用开放定址法解决冲突的弊端。

线性探测法的查找效率:

要查找的关键字分别是{19,14,23,1,68,20,84,27,55,11,10,79}

查找成功:

当查找19时,19%13=6,并且第6个位置就是19,所以只需要对比一次,查找27时,27%13=1,但是1号位置没有存放27,所以继续往后探测,直到查找到27,需要对比的次数为4。以此类推,将查找的次数/所有的数据元素=2.5

查找失败

初次探测的地址 H0 只有可能在[0,12],也就是13种情况。

若关键字刚开始是被映射到了0号位置,由于0号位置为空,所以只需要对比0次就知道查找失败了。若关键字刚开始被映射到了1号位置,那么需要对比1号位置以后的所有位置才知道查找失败,对比次数为13,依次类推,查找失败的平均查找长度为7。

所以:线性探测法很容易造成同义词,非同义词的“聚集(堆积)”现象,严重影响查找效率。因为采用线性探测法,产生冲突后,都是依次往后寻找空闲的位置,所以数据元素一定是放在某个连续的位置。可以用平方探测法解决这一问题。

例题:

1.现有长度为11且初始为空的散列表HT,散列函数是H(key)=key%7,采用线性探查(线性探测再散列)法解决冲突。将关键字序列 87,40,30,6,11,22,98,20依次插入HT后,HT查找失败的平均查找长度是()

解答:

每个关键字的存放情况如下:

由于 H(key)=0~6,查找失败时可能对应的地址有7个,对于计算出地址为0的关键字key0,只有比较完0~8号地址后才能确定该关键字不在表中,比较次数为9;对于计算出地址为1的关键字 key1,只有比较完1~8号地址后才能确定该关键字不在表中,比较次数为8;以此类推。

需要特别注意的是,散列函数不可能计算出地址7,因此有

ASL失败=(9+8+7+6+5+4+3)/7=6

2.现有长度为 5、初始为空的散列表 HT,散列函数H(k)=(k+4)%5,用线性探查再散列法解决冲突、若将关键字序列 2022,12,25 依次插入 HT,然后删除关键字25,则HT 中查找失败的平均查找长度为()

解答:

当采用开放定址法时,不能随便物理删除表中的已有元素,因为若删除元素,则可能截断其他具有相同散列地址的元素的查找地址。因此,当要删除一个元素时,可给它做一个删除标记。依次将 2022,12,25 插入散列表,然后删除25,得到的散列表如下:

散列表如下图所示:

当查找位置是删除标记时,应继续往后查找

查找失败的平均査找长度为(1+3+2+1+2)/5=1.8

② 平方探测法

当di=0^2,1^2,-1^2,2^2,-2^2....k^2,-k^2时,称为平方探测法,又称二次探测法,其中k<=m/2。用这一方法能够很好地解决"聚集(堆积)"问题。

例如下图,这些元素原本都应该存放在6号位置,但是6后续的元素因为发生了冲突,所以不能存放到6号位置。

假设下一个想放到6号位置的元素是19,发生了一次冲突,所以:

H1=(6+d1)%27=7,由于7号位置为空,所以将19放到7中。

接下来要存放的元素是32,H0=6(被占),H1=7(被占):
H2=(6-1)%27=5,5号位置空闲,所以将32号元素放到5号位置。

其余依次类推:

当存放84这个数据元素时:

H6=(6+d6)%27=(6-9)%27,也就是6往左移动9位,即24号位置。

所以使用平方探测法,比起线性探测法来说,查找同义词(取模相同的数)更不容易产生"聚集(堆积)"问题,这是由增量的特性决定的。

采用平方探测法查找元素的过程:

若此时要查找的数据元素是71,那么会从6号位置开始查找,6号位置不是71号元素,所以会到H1=(6+1)%27的位置找,也就是7号位置,7号位置的元素不是71,所以会到H3=5号位置找,5号位置也不是,直到找到H5=15号位置,才找到71号元素。

补充:若采用平方探测法,散列表长度m必须是一个可以表示成4j+3的素数(质数),才能探测到所有位置。如图所示,举下面这个例子,当表长是7(满足4j+3),那么H0~H6可以把表中所有的位置都探测一遍。当表长是8(满足4j+3),那么H0~H6并没有把表中所有的位置都探测一遍。

③ 伪随机序列法

其中,di是一个伪随机序列,如di=0,5,24,11,.....,例如下图:

若想存放的数据元素是目标19,由于6号位置被占(H0),所以继续寻找下一个位置:H1=(6+5)%27= 11。

依次类推,32将放在6号位置往后移动24的位置,而45会放在6号位置往后移动11的位置。

总结

以上学习的都是开放定址法,其中细分的3种方法只是对增量di的设定不同而已。

注意:采用“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除

(3)再散列法

再散列法(再哈希法):除了原始的散列函数 H(key)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止:

散列查找的总结如下,如果哪一点不太清楚可以再看看笔记:

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值