【数据结构】查找

风流意气都作罢 提交于 2020-03-11 23:15:00
  • 平均查找长度(ASL, Average Search Length):在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字比较次数的平均值,(即 ASL=\(\sum\)查找概率*比较次数)(一般为等概率1/n)
  • 静态查找表:查找表的操作无需动态地修改查找表,如 顺序查找、折半查找、散列查找等。
  • 动态查找表:需要动态地插入或删除的查找表,如 二叉排序树、二叉平衡树、B树、散列查找等。

线性结构

顺序查找

  • 适用条件:
    适用于线性表

  • 基本思想:
    从线性表的一段开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定的条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。

  • 具体实现:

typedef struct {    //查找表的数据结构
    ElemType *data;     //元素空间基址,建表时按实际长度分配,0号单元留空
    int length;     //表的长度
}List;      //Sequential Search Table

//在顺序表L中顺序查找关键字为key的元素。若找到则返回该元素在表中的位置
int Search_Seq(List L, ElemType key) {
    L.data[0] = key;        //“哨兵”
    for(i = L.length; L.data[i] != key; --i);       //从后往前找
    return i;       //如果表中不存在key,则会找到第0号(哨兵),返回0
}

引入哨兵的目的是使得函数内的循环不用每次都判断数组是否会越界,因为满足i==0时,函数一定会跳出。可以避免很多不必要的判断语句,从而提高程序效率。

缺点是当n较大时,平均查找长度较大,效率低;优点是对数据元素的存储没有需求,顺序存储或链式存储皆可;对表中记录的有序性也没有要求,无论记录是否按关键码有序,均可应用。

PS:线性的链表只能进行顺序查找

  • 性能分析:
    • 平均查找长度
      \(ASL_{成功}\)=\({n+1} \over {2}\)
      \(ASL_{失败}=n+1\) (因为0位置处还有一个哨兵需要比较,所以+1)
    • 时间复杂度O(n)
    • 优点:是对数据元素的存储没有需求,顺序存储或链式存储皆可;对表中记录的有序性也没有要求,无论记录是否按关键码有序,均可应用。
    • 缺点:是当n较大时,平均查找长度较大,效率低;
  • 特点:
    • 顺序查找下给出的查找序列可以有序,也可以无序
    • 查找算法简单,但时间效率太低时间复杂度为O(n)

折半查找

  • 适用条件:
    仅适用于有序的顺序表

  • 基本思想:
    首先将给定值key与表中中间位置元素的关键字比较,若相等,则查找成功,返回该元素存储位置,若不等,则所需查找的元素只能在中间元素以外的左半部分或右半部分,然后再缩小的范围内继续进行同样的查找,如此重复,直到找到为止;或确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。

  • 折半查找判定树:(不仅要画出圆结点,还要画出方结点,否则查找失败不好计算)

    折半查找判定树实际上是一棵二叉排序树,它的中序序列是一个有序序列。(方形结点是虚构的,代表失败,并不计入比较的次数之中)
    • 计算ASL:如 11个元素的有序表,即 一个绳子切11刀(11个成功),剩下12段(12个失败)
      每个查找成功(圆)的元素查找概率相等的情况下,即 每个圆结点概率为1/11(11个存在的成功的结点)(也可以理解成n2,即 双分支结点个数(其实也没有度为1的结点)),比较次数为层数;
      每个查找失败(方)的元素查找概率相等的情况下,即 每个方结点概率为1/12(11+1=12个不存在的失败的结点)(也可以理解成n0,即 叶子结点个数),比较次数该方结点的父结点(圆结点)的层数

      注意:方形虚构的查找失败结点,不算入比较次数中。

    • 每个根结点=(其最左结点+最右结点)/2 (向上或向下取整,要统一)

      注意:没有左结点或右结点,就取自己代替。如上图结点1没有左结点,1=(1+2)/2

    • 折半查找法在查找不成功时和给定值进行关键字的比较次数最多树的高度,即 \(⌊log_n⌋+1\)
  • 具体实现:

//在有序表L中查找关键字为key的元素,若存在则返回其位置,不存在则返回-1
int Binary_Search(SeqList L, ElemType key) {
    int low = 0, high = L.length-1, mid;
    while(low <= high) {
        mid = (low + high) / 2;         //mid取中间值
        if(key == L.data[mid]) {        //key刚好等于中间值
            return mid;
        }else if(key < L.data[mid]) {   //key小于中间值
            high = mid - 1;             //在左半边部分查找
        }else {                         //key大于中间值
            low = mid + 1;              //在右半边部分查找
        }
        return -1;      //当循环结束(low >high)说明查找失败了
    }
}
  • 性能分析:
    • 平均查找长度
      \(ASL_{成功}\)=\({n+1} \over {n}log_2(n+1)-1\)=\(log_2(n+1)-1\)
    • 时间复杂度O(logn)
    • 优点:折半查找的时间复杂度为O(logn),远远优于顺序查找的O(n)
    • 缺点:虽然二分查找的效率高,但是要求表关键字有序
  • 特点:
    • 折半查找只适用顺序存储结构,链表上无法实现二分查找
    • 折半查找适用于那种一经建立就很少改动但又经常需要查找的线性表

分块查找

分块查找又称索引顺序查找。(因为用了索引表,而索引表中又是顺序查找,所以叫索引顺序查找)

  • 适用条件:
    适用于顺序存储结构和链式存储结构(存放的数据量过大
  • 基本思想:
    将查找表分为若干个子块。块内的元素可以无序,但块之间是有序的(即 第一个块中的最大关键字小于第二个块中的所有记录的关键字,以此类推)。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列

  • 查找过程分为两步:
    • 第一步是在索引表确定待查记录所在的,可以顺序查找折半查找索引表(因为块之间有序);
    • 第二步是在块内顺序查找(因为块内可以无序)(当然,若块内有序,也可以折半查找)。
  • 具体实现:

//未完待续
  • 性能分析:
    • 平均查找长度
      ASL = \(L_I\)+\(L_S\) (Index和Sequence,索引查找和块内查找(块内顺序查找)的平均长度之和)
      长度为n的查找表均匀地分为b块每块s个记录
      • 索引表中(块间)采用顺序查找,块内采用顺序查找:
        \(ASL_{成功}\)=\(L_I\)+\(L_S\)=(b+1)/2+(s+1)/2=\({s^2+2s+n} \over {2s}\)

        注意:此时,若s=\(\sqrt{n}\),则平均查找长度取最小值\(\sqrt{n}+1\)

      • 索引表中(块间)采用折半查找,块内采用顺序查找:
        \(ASL_{成功}\)=\(L_I\)+\(L_S\)=\(log_2(b+1)+{s+1} \over {2}\)
      • 为使查找效率最高每个索引块的大小应是√n。(即 索引项*索引块=表长,等分了索引项和索引块)

  • 特点:
    • 分块查找下给定的序列应该是分块有序的,适用于顺序存储结构和链式存储结构(如果索引表不强制要求用折半查找的话)
    • 分块查找下其平均查找长度介于顺序查找和折半查找之间
    • 其平均查找长度有索引表主表两部分组成

树形结构

二叉排序树与二叉平衡树详情请看《数据结构---树》

二叉排序树

二叉排序树、平衡二叉树等虽然属于树的内容,但这里更着重与查找的关系,因此放到这一章,二叉排序树是一种动态的查找表(区分静态还是动态是看查找过程中是否可以插入删除元素)

  • 基本概念:
    二叉排序树的特性如下:左子树所有关键字均小于根关键字,而右子树所有关键字均大于根关键字,而左右子树也都是一棵二叉排序树(即左小右大)

    PS:通过中序遍历即可得出有序的序列

  • 删除:
    二叉排序树中删除一个关键字的时候,需要格外注意,为了保持这种二叉排序树的特性需要考虑以下三种情况:
    1. 待删除节点为叶子节点,删除即可
    2. 待删除节点只有一侧有子树,将该节点删除,将子树直接接在待删除节点的双亲节点之上即可(原来是左还是左,原来是右还是右)
    3. 待删除节点左右子树都存在,沿着待删除节点的左子树根节点的右指针一直往右走,找到其最右边一个节点(即 直接前驱)(其实就是左子树中最大的一个节点,比待删除节点小,比左子树中其他节点都大),将这个节点的记录覆盖到原来待删除的节点中去,因为这个节点可能还带有子树(最多只能带一个),那么删除这个节点时,就参考上面(1)(2)两种方法(这里也可以在右子树中找右子树中最小的那个,原理几乎是一样的,读者可以自己去思考,并不难)
  • 相关结论:
    • 二叉排序树进行中序遍历时,可以得到按从小到大排列的关键字序列
    • 若从根结点到某个叶子结点有一条路径,则路径左边的结点的关键字不一定小于路径上的结点的关键字的值
    • 在对二叉排序树进行插入操作时,每次都是从根结点出发查找插入位置,并把新结点作为叶子结点插入到合适的位置
    • 对于有n个关键码的集合,其关键码有n!种不同的排列,可构成的不同二叉排序树有\({{1}\over{n+1}}C_{2n}^{n}\)(卡特兰数)
    • 对二叉排序树进行对关键字的查找时,其平均查找长度和二叉排序树的形态有关,在最坏情况下,n个结点的二叉排序树是一棵深度为n的单支树(树的高度达到最大),其ASL和单链表上的顺序查找相同,为(n+1)/2最好情况下,n个结点的二叉树会得到一棵形态与折半查找的判定树相似的二叉树(树的高度达到最小),此时其ASL约为\(log_2n\)
    • 对于经常需要进行插入、删除、查找相关操作的表,建议使用二叉排序树
    • 二叉排序树的插入必为一个新的叶子结点

二叉平衡树

为避免树的高度增长过快,降低二叉排序树的性能,规定左右子树高度差不超过1。
平衡二叉树(Balanced Binary Tree)(简称平衡树,AVL)也是二叉排序树的一种,其特点在于,左右子树的高度之差的绝对值不超过1,左右子树高度之差被称为平衡因子,每次插入一个新的值的时候,都要检查二叉树的平衡,也就是平衡调整

  • 平衡调整技巧
    一般情况下,只有新插入结点的祖先结点的平衡因子受影响,即 以这些祖先结点为根的子树有可能失衡。下层的祖先结点恢复平衡将使上层的祖先结点恢复平衡,因此每次调整的使用应该先调整最下面的失衡子树。因为平衡因子为0的祖先不可能失衡,所以从新插入的结点开始向上,遇到地第一个其平衡因子不等于0的祖先结点,为第一个可能失衡的结点,如果失衡则应调整以该结点为根的子树。

  • 结论
    \(N_h\)表示的是高度为h的AVL树的最少结点数,则\(N_0\)=0(空树),\(N_1\)=1(仅包含根结点),现有结论\(N_h=N_{h-1}+N_{h-2}+1\)(h>1)

    例如:
    n0=0
    n1=1
    n2=2
    n3=4
    n4=7
    n5=12
    n6=20

B树、B+树

B树和B+树都是平衡的多叉树,都可用于文件索引结构
B树和B+树都能有效的支持随机查找,而B+树还支持顺序查找。

举个简单的例子, 比如我们要从123456中找出5这个数字:
顺序查询:先用5跟1比较,然后再跟2比较,然后再跟3比较,按数据顺序一直比下去,直到找到;
随机查询:从数据中随机抽出一个数字跟5比较,比如第一次随机抽到了4跟5比较,然后再随机抽一个3跟5比较,不断的随机抽然后比较,最终找到结果;
直接查询:这个概念较多,不过一般指的是数据分类后或者建立索引后,根据数据结构组成,有效率的去查询对比!

B树

前面所说的查找算法都是在内存中进行的,适用于组织在内存中较小的文件。而对于存放在外存上的较大的文件(如,操作系统中的文件目录存储,数据库中的文件索引结构的存储,等等)都不可能是在内存中建立查找结构而只能是在磁盘中建立这个查找结构。在磁盘中组织查找结构,从任何一个结点指向其他结点都有可能读取一次磁盘数据,再将这些数据写入内存进行比较,而频繁的进行磁盘I/O操作,其效率是很低的。所以,所有的二叉树的查找结构在磁盘中都是低效的。

  • 基本概念:
    基于此,1970年R.Bayer和E.McCreight提出了一种称为B树多路平衡查找树,适用于在磁盘等直接存储设备上组织动态的索引表,在系统运行过程中插入或删除记录是,索引结构本身也可能发生变化,以保持较好的查询性能。
    B树是多路查找树,是二叉排序树的拓展
    简单来说,B-树就是平衡二叉树的扩展,是一棵m叉树,B-树的阶就是其最大分支数,其非根节点至少有⌈m/2⌉个分支,而根节点至少有2个分支,每个节点的关键字按顺序从小到大排列。

  • 基本性质:

    m阶B树=m叉树,而不是度为m的树。这两点要分开,m叉树中可以不含有m棵子树,而度为m的树必须至少有一个结点有m棵子树,因为树的度是树中结点的最大度数,这个结点必须存在。

    1. 树中每个结点至多有m棵子树(即至多含有m-1个关键字)(两棵子树指针夹着一个关键字
    2. 根结点不是终端结点,则至少含有两棵子树(即至少含有一个关键字
    3. 除根结点外的所有非叶结点至少有⌈m/2⌉子树(即至少含有⌈m/2⌉-1关键字)(注意是向上取整,结点中包含越多关键字越好
    4. 关键字个数n的取值范围(⌈m/2⌉-1≤n≤m-1)(即根结点1≤n≤m-1
    5. 所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
    • 非根结点中关键字的个数范围(⌈m/2⌉-1 ~ m-1)
  • 高度(磁盘存取次数):

    B树中大部分操作所需的磁盘存储次数与B树的高度成正比。

    注意:B树的高度不包括最后的不带任何信息的叶结点所处的那一层。

    若n≥1,则对任意一棵包含n个关键字高度为h阶数为m的B树:

    • (最小高度)因为B树中每个结点最多有m棵子树,m-1个关键字,所以在一棵高度为h的m阶B数中关键字的个数应满足\(n≤(m-1)(1+m+m^2+...+m^{h-1})=m^h-1\),因此有\[h≥log_m(n+1)\]

    • (最大高度)若但每个结点中的关键字个数达到最少,则容纳同样多关键字的B树的高度达到最大。由B树的定义:第一层至少1个结点;第二层至少2个结点;除根结点外的每个非终端结点至少有⌈m/2⌉棵子树,则第三层至少有2⌈m/2⌉个结点……第h+1层至少有\(2(⌈m/2⌉)^{h-1}\)个结点,注意到第h+1层是不包含任何信息的叶结点。对于关键字个数为n的B树,叶结点(即 查找不成功的结点为n+1),由此有\(n+1≥2(⌈m/2⌉)^{h-1}\),即\[h≤log_{⌈m/2⌉}((n+1)/2)+1\]

    • 例如,一棵3阶B树共有8个关键字,则其高度范围为2≤h≤3.17

  • 查找:
    B树的查找操作和二叉排序树的查找操作非常类似。
    1. 在B树中找结点;(在磁盘上进行查找)
    2. 在结点内找关键字(在内存中进行查找)
      1. 先让待查找关键字key和结点中的关键字比较,如果等于其中的关键字,则查找成功。
      2. 如果和所有关键字都不相等,则看key处在哪个范围内,饭后取对应的指针所指向的子树中查找。
  • 插入:

    在二叉排序树中,仅需查找到需插入的终端结点的位置。但是,在B树中找到插入的位置后,并不能简单地将其添加到终端结点位置,因为此时可能会导致整棵树不再满足B树中定义的要求(结点至多有m-1个关键字,非根节点关键字个数至少是⌈m/2⌉-1)一旦达到m个关键字就需要进行分裂。

    1. 定位
    2. 插入:可能导致关键字个数超过上限,分裂。
    3. 分裂:取这个关键字数组中的中间关键字(⌈n/2⌉)作为新的根结点的关键字向上进位到父结点中,然后其他关键字形成两个结点作为新结点的左右孩子
  • 删除:
    • 删除的关键字在终端结点上:
      • 结点内关键字数量>⌈m/2⌉-1,此时参数不会破坏定义,直接删除
      • 结点内关键字数量=⌈m/2⌉-1
        • 其左右兄弟结点中关键字数量>⌈m/2⌉-1(兄弟够借),去兄弟中借(需要调整该结点、左(右)兄弟结点及其双亲结点(父子换位法),以达到新平衡
        • 其左右兄弟结点中关键字数量均不大于⌈m/2⌉-1(兄弟不够借),则需要进行结点合并(从上一层的结点取关键字与下一层的结点合并)
    • 删除的关键字在非终端结点上:需要先转换成在终端结点上,再按照再终端结点上的情况来分别考虑对应的方法。
      相邻关键字:对于不在终端结点上的关键字,它的相邻关键字是其左子树中值最大或者右子树中值最小的关键字。
      1. 存在关键字数量>⌈m/2⌉-1结点的左子树或者右子树,在对应子树上找到该关键字的相邻关键字,然后将相邻关键字替换待删除的关键字
      2. 左右子树的关键字数量均=⌈m/2⌉-1,则将这两个左右子树结点合并,然后删除待删除关键字。
  • 总结:
    1. 首先确定结点中关键字的个数范围(⌈m/2⌉-1 ~ m-1)
    2. 插入时,若超过范围,则将中间关键字向上进位到父节点中
    3. 删除时,若低于范围,若为分支结点,则父债子偿,将后继(前驱)结点儿子顶替父亲(若儿子无力偿还(顶替)(已到达最低关键字个数限制),则合并(与兄弟合并一起偿还));若为叶子结点,则子债父偿,父结点降位顶替儿子(就如同删除了父亲,后续操作继续进行父债子偿
    • 其实就是删除之后拿前驱(后继)一个个往前面补,直到(结点关键字个数不足以弥补,弥补过后结点中关键字个数不在范围之中)补不了合并,每个结点关键字都在个数范围之中
  • 插入步骤:
    • 当超过范围,中间关键字作为父亲担起责任。
  • 删除步骤:
    • 子债父偿
    • 父债子偿
    • 子偿还不了,与兄弟合并,一起偿还
    • 一起仍然偿还不了,向上找父亲的父亲(爷爷)
    • 上一层继续子债父偿(父亲的债爷爷偿)
    • 父债子偿
    • 子偿还不了,合并,一起偿还
    • 以此类推。。
  • 删除简化:
    • 子债父偿
    • 父债子偿
    • 子偿不了
    • 合并偿还

注意:由于这是平衡查找树,所以中序遍历为有序序列,其前驱(或 后继)为左子树下最大元素(或 右子树下最小元素

总之,先求关键字个数范围(⌈m/2⌉-1 ~ m-1),插入或删除操作一定要满足定义,不满足,可以自己看着办(不必那么复杂的记),只需调整到满足定义即可。

B+树

常用于数据库操作系统文件系统中的一种查找的数据结构。

  • 基本性质(与B树比较):
    1. 关键字与子树
      • 在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树
      • 在B树中,具有n个关键字的结点含有(n+1)棵子树,即两棵子树指针夹着一个关键字
    2. 关键字个数范围(其实与第一个一样)
      • 在B+树中,每个结点(非根内部结点)关键字个数n的范围是⌈m/2⌉≤n≤m(根结点1≤n≤m
      • 在B树中,每个结点(非根内部结点)关键字个数n的范围是⌈m/2⌉-1≤n≤m-1(根结点1≤n≤m-1
    3. 记录
      • 在B+树中,叶子结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
      • 在B树中,每个关键字对应一个记录存储地址
    4. 叶结点
      • 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中,而且叶子结点的指针指向记录
      • 在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复
    5. 在B+树中,有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个单链表

散列结构

散列表

前面的线性表和树表的查找中,记录在表中的位置与记录关键字之间不存在确定关系,这类查找方法建立在“比较”的基础上,查找的效率取决于比较的次数。

那么如果需要快速查找到需要的关键字,而关键字不方便比较怎么办?所以就引入了散列表,亦满足了动态查找

  • 散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr(这里的地址可以是数组下标、索引或内存地址等)

  • 冲突:散列函数可能会把两个或两个以上的不同关键字映射到同一地址

  • 同义词:发生碰撞的不同的关键字

  • 散列表:根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。

  • 基本概念:
    散列表的特点是根据给定的关键字计算出关键字在表中的地址,就是将每个关键字通过一个算法来得出其对应的地址位置,例如最常见的的取余计算法,对关键字进行取余计算(除数一般用关键字的数量,也就是表长),即可得出每个关键字应该放的位置,而查找的时候根据待查找得关键字进行取余计算即可得到该关键字所在的位置,若不符,则查找失败

  • 散列函数构造:
    1. 直接定址法
      取关键字的某个线性函数值为散列地址,即h(key)=a*key+b (a、b为常数)

      比如:查找年份,是一个线性关系

    2. 除留余数法
      散列函数为:h(key) mod(%) p
      • 一般p取表的大小,为了映射均匀p一般取不大于表长的素数
    3. 数字分析法
      分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址

      比如:取11位手机号码key的后四位作为地址,或者18位的身份证号码

    4. 平方取中法
      希望每一位都能影响到最后的函数结果值。

      如56793542,\({56793542}^2\)=325506412905764,取中间三位641

    5. 折叠法
      把数字分割成位数相同的几个部分,然后叠加。

      如56793542,542+793+056=1391,h(56793542)=391

  • 处理冲突:
    1. 开放定址法(Open Addressing):(换个位置)

      所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。
      发生聚集的主要原因是,解决冲突的方法选择不当
      产生堆积现象,即 产生了冲突,它对存储效率、散列函数和装填因子均不会有影响,而平均查找长度会因为堆积现象而增大。(注意这里是存储效率,而不是查找效率)

      一旦产生了冲突,就按某种规则去递增寻找另一空地址。
      hi=(H(key)+di)%m
      式中,i代表冲突次数,m为表长,di为增量序列。

      这里形象化的比喻一下:设想一下你预定了一家旅馆的房间,但后来发现这间房被人住了,于是旅馆工作人员会给你重新分配一个房间。
      这就是开放定址法,当发现有哈希冲突时,则将元素往后面的空间存放,这里就有很多种分配方式,例如

      • 线性探测法(线性探测再散列)(Linear Probing):简单说就是一个一个的往后面找,缺点是容易产生堆积(某个地方冲突越来越多)。
        \(d_i=0,1,2,...\)
      • 平方探测法(二次探测再散列):就是对当前的冲突位置+\(1^2\),-\(1^2\),+\(2^2\),-\(2^2\)……进行探查,直到找到空位置位置,这种算法不会出现堆积问题,但是不能探查到所有的空位
        \(d_i=0^2,1^2,-1^2,...\)

        有定理显示:如果散列表长度TableSize是某个4k+3(k是正整数)形式的素数时,平方探测法可以探查到整个散列表空间

      • 再哈希法(双散列法):使用两个散列函数h1、h2
        \(H_i=(H(key)+i×Hash_2(key))%m\)

        i为冲突次数,m为表长;
        其实就是在m表上,不停的\(+Hash_2(key)\),循环,直到有空位为止。
        (如 Hash2(key)=6,那么就一直+6,直到有空位)

        • 探测序列还应该保证所有的散列存储单元都应该能够被探测到,选择以下形式有良好的效果:
          h2(key) = p - (key mod p)
      • 伪随机序列法:伪随机放(di=伪随机数序列)

      注意:在开放地址散列表中,删除操作要很小心,通常只能“懒惰删除”,即 需要增加一个“删除标记(Deleted)”,而并不是真正删除它,以便查找时不会“断链”。其空间可以在下次插入时重用。

    2. 链地址法(Separate Chaining):(同一位置的冲突对象(同义词)组织在一起)

      将相应位置上冲突的所有关键字存储在同一个单链表中。

      这种方法有点像违建房,散列表本身不存储信息,而保存指针,而每个记录除了带有关键字以外,还要带一个指针域,对于某一个关键字进行计算后得出的值,与哈希表中某个值对应,就将指针指向这个关键字记录,然后,如果有另外一个关键字计算也得到这个数,那么就将前一个关键字记录的指针域指向这个关键字,形成一个链表。

    3. 建立公共溢出区:
      将哈希表分为基本表溢出表两部分,凡是和基本表发生冲突的元素一律顺着填入溢出表中。

  • 性能分析:
    散列表的性能指标可以这样分析:
    对于散列表查找成功的ASL计算,通过对所有查找成功的关键字所需的比较次数进行相加再除以关键字的数量即可得出,查找失败的ASL,则通过将从每个地址开始进行查找直到查找到空所需的地址比较次数相加除以地址数即可得出

    • 装填因子(Loading Factor):散列表的装填因子一般记为α,定义为一个表的装满程度,即
      \[α = {{表中记录数n} \over {散列表长度m}}\]

      散列表的平均查找长度依赖于散列表的装填因子α,而不直接依赖于n或m。α越大,表示装填的记录越满,发生冲突的可能性越大,反之发生冲突的可能性越小。一般0.5~0.85较为合理。

    • 平均查找长度
      • 查找成功:是针对关键字
        \[ASL_{成功}={{关键字查找次数之和}\over{关键字个数}}\]

        查找成功:比较几次找到关键字/关键字的个数(除以关键字的个数 即 查找关键字的概率相等)

      • 查找失败:是针对可以散列的位置(就是模几)(MOD p,即 可以散列的位置为0~p-1)(其实也就是说,只可能在散列这几个位置后查找失败,因为其他位置散列不到))
        \[ASL_{失败}={{每个地址的查找次数之和}\over{地址个数}}\]

        查找失败:每个0~p-1的散列地址中,比较几次才能发现没有这个关键字(即 比较几次才能找到第一个,空则表示查找失败了)(如 散列地址为0、1、2都不为空,3为空,那么发现地址0的查找失败次数为4,1为3,2为2,3为1)

      注意:线性探测法查找到空,也算一次比较(如012都不为空,那么0的查找失败次数为4)
      链地址法查找到空,不算一次比较,因为链地址法指向下一个结点的指针为空,就知道查找失败了,不算一次比较。
      我们这里说的比较,是指需要查找的元素该地址上存储的元素进行比较而链地址法中指向下一个结点的指针为空,没有与查找元素比较,所以不算一次比较

    • 关键字的比较次数取决于产生冲突的多少
      影响产生冲突多少有以下三个因素:
      • 散列函数是否均匀
      • 处理冲突的方法
      • 散列表的装填因子α
    • 散列表:

      散列地址 0 1
      关键字 -- --
      比较次数 -- --
  • 总结:
    • 选择合适的h(key),散列表的查找效率期望是常数O(1),它几乎与关键字的空间的大小n无关,也适合于关键字直接比较计算量大的问题。
    • 它是以较小的α为前提,因此,散列方法是一种空间换时间的方法。
    • 散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合范围查找,或最大最小值查找。
    • 开放地址法:
      • 散列表是一个数组,存储效率高,随机查找。
      • 散列表有“堆积”现象
      • 关键字删除需要“懒惰删除”
    • 链地址法:
      • 散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低。
      • 关键字删除不需要“懒惰删除”法,从而没有存储“垃圾”。
      • 太小的α可能导致空间浪费,大的α又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降。
  • “冲突”是不是特别讨厌?
    答:不一定!正因为有冲突,使得文件加密后无法破译(不可逆,是单向散列函数,可用于数字签名)。
    利用了哈希表性质:源文件稍稍改动,会导致哈希表变动很大。

本文参考:https://blog.csdn.net/qq_25940921/article/details/82224418

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!