【数据结构与算法】——第七章:查找


 ============================ 【说明】 ===================================================
  大家好,本专栏是 数据结构与算法该科目是计算机类专业必修课之一,比较重要也比较基础,有想从事算法研究的同学,这些内容是专/本科、甚至硕士期间较为基础的内容,适用范围较广:大学专业课学习、考研复习等。
  通过自己的理解进行整理,希望大家积极交流、探讨,多给意见。后面也会给大家更新其他一些知识。若有侵权,联系删除!共同维护网络知识权利!


1、前言

  生活中,不管是打开电脑还是手机,就会涉及到查找技术。如炒股软件中查股票信息、抖音中找你喜欢的网红等等,都要涉及到查找。当然,在互联网上查找信息就更加是家常便饭。所有这些需要被查的数据所在的集合,我们给它-一个统称叫查找表查找表(Search Table) 是由同一类型的数据元素(或记录)构成的集合。

  关键字(Key)是数据元素中某个数据项的值,又称为键值
  若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)
  对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)


2、查找定义

   查找( Searching ) 就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

  查找表按照操作方式来分有两大种静态查找表和动态查找表。区别如下:

  如何进行查找?

   · 查找的方法取决于查找表的结构。
   · 由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找。
   · 为了提高查找的效率, 需要在查找表中的元素之间人为地 附加某种确定的关系,换句话说,用另外一种结构来表示查找表。

  本节对于查找而言主要讨论的查找结构如下

  线性表:适用于静态查找,主要采用顺序查找技术、折半查找技术分块查找
  树表:适用于动态查找,主要采用二叉排序树的查找技术。
  哈希表静态查找和动态查找均适用,主要采用散列技术


3、静态查找表

3.1 顺序查找

  它的查找过程是:

  从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;
  如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

  算法分析:

  对于这种顺序查找算法来说,查找成功最好的情况就是在第一个位置就找到了,算法时间复杂度为O(1)
  最坏的情况是在最后一位置才找到,需要n次比较,时间复杂度为O(n),当查找不成功时,需要n+1次比较,时间复杂度为0(n)
  关键字在任何一位置的概率是相同的,所以平均查找次数为(n+1)/ 2,所以最终时间复杂度还是O(n)

  缺点:
  平均查找长度较大,特别是当待查找集合中元素较多时,查找效率较低。

  优点:
  算法简单而且使用面广。
    对表中记录的存储没有任何要求,顺序存储和链接存储均可;
    对表中记录的有序性也没有要求,无论记录是否按关键码有序均可。


3.2 折半(二分)查找

  折半查找,又称为二分查找它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储

  算法分析:
  1、将给定值与中间元素的关键字比较:
    若相等,则查找成功;
    若小于,则在左半区继续查找;
    若大于,则在右半区继续查找。
  2、不断重复上述查找过程,直到查找成功,或所查找的区域无数据元素,查找失败。

  例如:已知如下11个数据元素的有序表(关键字即为数据元素的值):(05,13,19,21,37,56,64,75,80,88,92)现要查找关键字为64和60的数据元素。

在这里插入图片描述

  
折半查找判定树

  判定树:折半查找的过程可以用二叉树来描述,树中的每个结点对应有序表中的一个记录,结点的值为该记录在表中的位置。通常称这个描述折半查找过程的二叉树为折半查找判定树,简称判定树

   查找成功:在表中查找任一记录的过程,即是折半查找判定树中从根结点到该记录结点的路径,和给定值的比较次数等于该记录结点在树中的层数。
  查找不成功:查找失败的过程就是走了一条从根结点到外部结点的路径,和给定值进行的关键码的比较次数等于该路径上内部结点的个数。

  算法分析:
    假设表中每个元素的查找是等概率的

    当n较大时,可以近似为:

    所以有:T(n)=O(log2n )

  不过由于折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法已经比较好了。但对于需要频繁执行插人或删除操作的数 据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

  注:每个结点的比较次数之和,即该结点所在的层次数;空指针处比较次数之和,就是该空指针的双亲结点所在的层次。


查找性能对比:


3.3 分块查找

   适用条件:将查找表分成若干个子表,子表为非有序表,但要求子表之间是存在着有序关系。

  算法思想:
  1、用给定值k在索引表中检测索引项,以确定所要进行的查找在查找表中的查找分块 (由于索引项按关键字字段有序,可用顺序查找或折半查找) ;
  2、对该分块进行顺序查找。

  示例:

  算法分析:

  分块查找的平均查找长度为索引查找Li和块内查找Ls之和。
  设n个数据元素的查找表分为m个子表,且每个子表均为t个元素,则t=n/m
  分块查找的平均查找长度为

  若块内与块间采用顺序查找

  若块内采用顺序查找块间采用折半查找

4、动态查找表

4.1 二叉排序树

(1) 相关概念

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

   1) 若左子树不空,则左子树上所有结点的值均小于根结点的值;若右子树不空,则右子树上所有结点的值均大 于根结点的值。
   2) 左右子树也都是二叉排序树

  查找过程:

  1、若根结点的关键字值等于查找的关键字,成功;
  2、若根结点的关键字值不等于查找的关键字,
    若小于根结点的关键字值,查其左子树;
    若大于根结点的关键字值,查其右子树。在左右子树上的操作类似。

  例如查找关键字:

  查找性能分析:

  从上述查找过程可见,在查找过程中,生成了一条查找路径:
  从根结点出发,沿着左分支或右分支逐层向下直至关键字等于给定值的结点; ——查找成功
  从根结点出发,沿着左分支或右分支逐层向下直至指针指向空树为止。——查找不成功

  对于每一棵特定的二叉排序树,均可按照平均查找长度的定义来求它的 ASL 值,显然,由值相同的 n 个关键字,构造所得的不同形态的各棵二叉排序树的平均查找长度的值不同,甚至可能差别很大。如下例:

  前者ASL=(1+2*2+3*2)/5=2.2;后者ASL=(1+2+3+4+5)/5=3

  
  算法分析:

  在二叉排序树中进行查找的平均查找长度和二叉树的形态有关,即:
  最坏: O(n)(单支树)
  最好: O(log2n)(形态匀称,与二分查找的判定树相似)

  
(2) 二叉排序树插入

  根据动态查找表的定义,“插入”操作在查找不成功时才进行

  若二叉排序树为空树,则新插入的结点为新的根结点;否则,新插入的结点必为一个新的叶子结点,其插入位置由查找过程得到。

  示例:在下图给定的二叉排序树中插入结点20。

  
(3) 二叉排序树构造

  示例:关键字序列为 {10, 18, 3, 8, 12, 2, 7, 6}

  
(4) 二叉排序树删除

  操作要点:
  从二叉排序树删除一个结点之后,要使得删除之后二叉树还是一棵二叉排序树;
  从二叉排序树删除一个结点,既可能是叶子结点也可能是分支结点,处理的方法不同。

  三种情况 :
  设待删结点为p,其双亲结点为f 。

   1)p结点为叶结点

    由于删去叶结点后不影响整棵树的特性,所以,只需将被删结点的双亲结点相应指针域改为空指针。

   2)p结点只有右子树pr或只有左子树pl

    p结点只有右子树pr或只有左子树pl,此时,只需将pr或pl替换f结点的p子树即可。

  删除12

  删除18

   3)p结点既有左子树pl又有右子树pr

    p结点既有左子树pl又有右子树pr ,可按中序遍历保持有序进行调整。

  方法

  删除10


4.2 平衡二叉树(AVL)

(1) 相关概念

  平衡二叉树又称AVL树,它或者是一棵空树,或者是具有下列性质的二叉树:

  1) 左、右子树是平衡二叉树
  2) 所有结点的左、右子树深度之差的绝对值≤ 1,即|左子树深度-右子树深度|≤ 1

  平衡因子balance结点左子树与右子树的高度差。
  任一结点的平衡因子只能取:-1、0 或 1否则,这棵二叉树就失去平衡,不再是AVL树

  下列这棵树为非二叉平衡树:

(2) 平衡二叉树插入

  如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。称调整平衡过程为平衡旋转。平衡旋转有如下四种:

  1) LL平衡旋转

    若在A的左子树的左子树上插入结点,使A的平衡因子从1增加至2,需要进行一次顺时针旋转。(以B为旋转轴)

  2) RR平衡旋转

    若在A的右子树的右子树上插入结点,使A的平衡因子从-1增加至-2,需要进行一次逆时针旋转。(以B为旋转轴)

  3) LR平衡旋转

    若在A的左子树的右子树上插入结点,使A的平衡因子从1增加至2,需要先进行逆时针旋转,再顺时针旋转。(以插入的结点C为旋转轴)

  4) RL平衡旋转

    若在A的右子树的左子树上插入结点,使A的平衡因子从-1增加至-2,需要先进行顺时针旋转,再逆时针旋转。(以插入的结点C为旋转轴)

  在插入过程中,采用平衡旋转技术。基本思想:在构造二叉排序树的过程中,每插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树

  通用技巧:在进行平衡旋转时,对于顺时针、逆时针这种情况容易混淆,最简单的方式就是,找到最小不平衡子树,对其进行中序遍历,例如上述 RL平衡旋转

  对其进行中序遍历得:ACB,以C为子树根,A、B分别为左右子树,即可得到最后旋转结果。

  
  案例请将下面序列构成一棵平衡二叉排序树( 13,24,37,90,53)

5、哈希表查找

  以上两种查找表的各种结构的共同特点:记录在表中的位置和它的关键字之间不存在一个确定的关系。以上两种查找表的共性是:

  1、查找的过程为给定值依次和关键字集合中各个关键字进行比较。
  2、查找的效率取决于和给定值进行比较的关键字个数。
  3、用这类方法表示的查找表,其平均查找长度都不为零。

  相反,对于频繁使用的查找表,我们更希望ASL = 0。只有一个办法:预先知道所查关键字在表中的位置,即要求:记录在表中位置和其关键字之间存在一种确定的关系

  对于动态查找表而言:
  1) 表长不确定;
  2) 在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。


5.1 相关定义

  因此在一般情况下,需在关键字与记录在表中的存储位置之间建立一个函数关系以 f(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 f(key) 为哈希函数

  1) 哈希(Hash)函数是一个映象,即:将关键字的集合映射到某个地址集合上, 它的设置很灵活,只要这个地址集合的大小不超出允许范围即可;
  2) 由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,即: key1不等于 key2,而 f(key1) = f(key2)
  3) 很难找到一个不产生冲突的哈希函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生

  因此,在构造这种特殊的“查找表” 时,除了需要选择一个“好”(尽可能少产生冲突)的哈希函数之外;还需要找到一种“处理冲突” 的方法

  根据设定的哈希函数 H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”。

  总结,需解决两个问题:
   (1)构造好的哈希函数:所选函数尽可能简单,以便提高转换速度。所选函数对关键字计算出的地址,应在哈希地址集中大致均匀分布,以减少空间浪费。
   (2)制定解决冲突的方案


5.2 常用哈希函数

  (1) 直接定址法

  Hash(key)=a·key+b (a、b为常数) 即取关键字的某个线性函数值为哈希地址
  特点:所得地址集合与关键字集合大小相等,不会发生冲突。

  适用情况?
  事先知道关键码,关键码集合不是很大且连续性较好。适合查找表较小且连续的情况,由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。

  例:关键字集合为{100,300,500,700,800,900},选取哈希函数为:Hash(key)=key/100,则所建立的哈希表如下:

Hash(100)=100/100=1    	 Hash(700)=700/100=7
 Hash(300)=300/100=3 	 Hash(800)=800/100=8
 Hash(500)=500/100=5     Hash(900)=900/100=9

  (2) 除留余数法

  Hash(key)=key mod p (p是一个整数) 即取关键字除以p的余数作为哈希地址。选取合适的p很重要,若哈希表表长为m,则要求p≤m,且接近m或等于m。p一般选取质数,也可以是不包含小于20质因子的合数。

  为什么要对 p 加限制?

  例:给定一组关键字为: 12, 39, 18, 24, 33, 21,若取 p=9, 则他们对应的哈希函数值将为: 3, 3, 0, 6, 6, 3
  可见,若 p 中含质因子 3, 则所有含质因子 3 的关键字均映射到“3 的倍数”的地址上,从而增加了“冲突”的可能。

  (3) 乘余取整法

  Hash(key)= ⌊B*(A*key mod 1) ⌋ (A、B均为常数,且0<A<1,B为整数)

  B取什么值并不关键,但A的选择却很重要,最佳的选择依赖于关键字集合的特征。
  一般取A= 0.6180339……较为理想。

  (4) 数字分析法

  对关键字进行分析,取关键字的若干位或其组合作哈希地址

  例:有80个记录,关键字为8位十进制数,哈希地址为2位十进制数

  适用情况?
  能预先估计出全部关键码的每一位上各种数字出现的频度,不同的关键码集合需要重新分析。

  如果关键字是位数较多的数字(比如手机号),且这些数字部分存在相同规律,则可以采用抽取剩余不同规律部分作为散列地址。
   比如手机号前三位是接入号,中间四位是 HLR 识别号,只有后四位才是真正的用户号也就是说,如果手机号作为关键字,那么极有可能前 7 位是相同的此时我们选择后四位作为散列地址就是不错的选择 。
  同时,对于抽取出来的数字,还可以再进行反转右环位移,左环位移等操作
  目的:就是为了提供一个能够尽量合理地将关键字分配到散列表的各个位置的散列函数。

  数字分析法通常适合处理关键字位数比较大的情况
  如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法

  (5) 平方取中法

  取关键字平方后的中间几位为哈希地址

  例:散列地址为2位,则关键码1234的散列地址为:

  适用情况?
  事先不知道关键码的分布且关键码的位数不是很大。

  (6) 折叠法

  将关键字自左到右分成位数相等的几部分,最后一部分位数可以短些,然后将这几部分叠加求和。

  有两种叠加方法:
    1) 移位法 。
    2) 间界叠加法 。

  适用情况?
  关键码位数很多,事先不知道关键码的分布。


5.3 处理冲突方法

  (1) 开放定址法

  由关键字得到的哈希地址一旦产生了冲突,就去寻找下一个空的哈希地址。 即 Hi=(H(key)+di)MOD m,i=1,2,……k(k≤m-1)其中

    H(key)为哈希函数;
    m为哈希表表长;
    di为增量序列;

  线性探测法

di=1,2,3,……,m-1

  二次探测法

di=1²,-1²,2²,-2²,3²,……±k²(km/2)

  双哈希函数探测法

di=伪随机数序列

  例:关键字集合 { 19, 01, 23, 14, 55, 68, 11, 82, 36 },设定哈希函数 H(key) = key MOD 11 ( 表长=11 )。

    若采用线性探测再散列处理冲突

    若采用二次探测再散列处理冲突

  优点弊端

  优点:这种方法能够使得关键字不产生聚集
  弊端:当然,相应地也增加了计算的时间

  (2) 链地址法

  将所有哈希地址相同的记录都链接在同一链表中。例:同前例的关键字,哈希函数为 H(key)=key MOD 7

  优点弊端

  优点:链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障
  弊端:当然,这也就带来了查找时需要遍历单链表的性能损耗


  哈希表的查找分析

  查找过程中,关键字的比较次数,取决于产生冲突的多少 。

  影响产生冲突多少有以下三个因素
    哈希函数是否均匀;
    处理冲突的方法
    哈希表的填满因子=哈希表数据元素个数/哈希表长度

  装填因子α :是哈希表的装满程度。
  直观地看,α越小,发生冲突的可能性就越小,反之,表中装填的记录越多,发生冲突的可能就越大,查找时,给定值与关键字比较的个数也就越多。

  散列查找的查找效率取决于三个因素:散列函数、处理冲突、装填因子


  哈希查找性能比较

猜你喜欢

转载自blog.csdn.net/qq_41225961/article/details/128846688