查找

 转自https://www.cnblogs.com/feixuelove1009/p/6148357.html

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

  查找算法分类:
  1)静态查找和动态查找;
    注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
  2)无序查找和有序查找。
    无序查找:被查找数列有序无序均可;
    有序查找:被查找数列必须为有序数列。
  平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
  对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
  Pi:查找表中第i个数据元素的概率。
  Ci:找到第i个数据元素时已经比较过的次数。

1. 顺序查找

  说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
  基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
  复杂度分析: 
  查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
  当查找不成功时,需要n+1次比较,时间复杂度为O(n);
  所以,顺序查找的时间复杂度为O(n)。
  C++实现源码:

2. 二分查找

  说明:元素必须是有序的,如果是无序的则要先进行排序操作。

  基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。

  复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);

 

2. 插值查找

二分查找法虽然已经很不错了,但还有可以优化的地方。
有的时候,对半过滤还不够狠,要是每次都排除十分之九的数据岂不是更好?选择这个值就是关键问题,插值的意义就是:以更快的速度进行缩减。

插值的核心就是使用公式:
value = (key - list[low])/(list[high] - list[low])

用这个value来代替二分查找中的1/2。

 

插值算法的总体时间复杂度仍然属于O(log(n))级别的。其优点是,对于表内数据量较大,且关键字分布比较均匀的查找表,使用插值算法的平均性能比二分查找要好得多。反之,对于分布极端不均匀的数据,则不适合使用插值算法。

3. 斐波那契查找

由插值算法带来的启发,发明了斐波那契算法。其核心也是如何优化那个缩减速率,使得查找次数尽量降低。
使用这种算法,前提是已经有一个包含斐波那契数据的列表
F = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,...]

算法分析:斐波那契查找的整体时间复杂度也为O(log(n))。但就平均性能,要优于二分查找。但是在最坏情况下,比如这里如果key为1,则始终处于左侧半区查找,此时其效率要低于二分查找。

总结:二分查找的mid运算是加法与除法,插值查找则是复杂的四则运算,而斐波那契查找只是最简单的加减运算。在海量数据的查找中,这种细微的差别可能会影响最终的查找效率。因此,三种有序表的查找方法本质上是分割点的选择不同,各有优劣,应根据实际情况进行选择。

 

四、线性索引查找

对于海量的无序数据,为了提高查找速度,一般会为其构造索引表。
索引就是把一个关键字与它相对应的记录进行关联的过程。
一个索引由若干个索引项构成,每个索引项至少包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为:线性索引、树形索引和多级索引。
线性索引:将索引项的集合通过线性结构来组织,也叫索引表。
线性索引可分为:稠密索引、分块索引和倒排索引

 

  1. 稠密索引

稠密索引指的是在线性索引中,为数据集合中的每个记录都建立一个索引项。
image_1b2cl8r0dk1v1u0ssf0rmk8o29.png-157.4kB

这其实就相当于给无序的集合,建立了一张有序的线性表。其索引项一定是按照关键码进行有序的排列。
这也相当于把查找过程中需要的排序工作给提前做了。

  1. 分块索引

给大量的无序数据集合进行分块处理,使得块内无序,块与块之间有序。
这其实是有序查找和无序查找的一种中间状态或者说妥协状态。因为数据量过大,建立完整的稠密索引耗时耗力,占用资源过多;但如果不做任何排序或者索引,那么遍历的查找也无法接受,只能折中,做一定程度的排序或索引。
image_1b2clkecf3mt1j7a8hn3v5vbrm.png-136.6kB

分块索引的效率比遍历查找的O(n)要高一些,但与二分查找的O(logn)还是要差不少。

  1. 倒排索引

不是由记录来确定属性值,而是由属性值来确定记录的位置,这种被称为倒排索引。其中记录号表存储具有相同次关键字的所有记录的地址或引用(可以是指向记录的指针或该记录的主关键字)。

倒排索引是最基础的搜索引擎索引技术。

 

五、二叉排序树

二叉排序树又称为二叉查找树。它或者是一颗空树,或者是具有下列性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值均小于它的根结构的值;
  • 若它的右子树不为空,则右子树上所有节点的值均大于它的根结构的值;
  • 它的左、右子树也分别为二叉排序树。
    image_1b2cm8v3mi50141m1vi31v658dh13.png-61.7kB

构造一颗二叉排序树的目的,往往不是为了排序,而是为了提高查找和插入删除关键字的速度。

二叉排序树的操作:

    1. 查找:对比节点的值和关键字,相等则表明找到了;小了则往节点的左子树去找,大了则往右子树去找,这么递归下去,最后返回布尔值或找到的节点。
    2. 插入:从根节点开始逐个与关键字进行对比,小了去左边,大了去右边,碰到子树为空的情况就将新的节点链接。
    3. 删除:如果要删除的节点是叶子,直接删;如果只有左子树或只有右子树,则删除节点后,将子树链接到父节点即可;如果同时有左右子树,则可以将二叉排序树进行中序遍历,取将要被删除的节点的前驱或者后继节点替代这个被删除的节点的位置。

 

七、多路查找树(B树)

多路查找树(muitl-way search tree):其每一个节点的孩子可以多于两个,且每一个结点处可以存储多个元素。
对于多路查找树,每个节点可以存储多少个元素,以及它的孩子数的多少是关键,常用的有这4种形式:2-3树、2-3-4树、B树和B+树。

2-3树

2-3树:每个结点都具有2个孩子,或者3个孩子,或者没有孩子。

一个2结点包含一个元素和两个孩子(或者没有孩子,不能只有一个孩子)。与二叉排序树类似,其左子树包含的元素都小于该元素,右子树包含的元素都大于该元素。
一个3结点包含两个元素和三个孩子(或者没有孩子,不能只有一个或两个孩子)。

2-3树中所有的叶子都必须在同一层次上。

image_1b3f9opsn55815u31cme12fp1qjh4b.png-96.7kB

 

B树

B树是一种平衡的多路查找树。节点最大的孩子数目称为B树的阶(order)。2-3树是3阶B树,2-3-4是4阶B树。
B树的数据结构主要用在内存和外部存储器的数据交互中。
image_1b3fajra41dobmdl1jbn1gk9461ar.png-159.6kB
image_1b3fahcg450l11lo19eor1i1gv2ae.png-30.7kB
image_1b3fal5pn1m8119551bh0nuf1oqgb8.png-176.4kB

B+树

为了解决B树的所有元素遍历等基本问题,在原有的结构基础上,加入新的元素组织方式后,形成了B+树。

B+树是应文件系统所需而出现的一种B树的变形树,严格意义上将,它已经不是最基本的树了。

B+树中,出现在分支节点中的元素会被当做他们在该分支节点位置的中序后继者(叶子节点)中再次列出。另外,每一个叶子节点都会保存一个指向后一叶子节点的指针。
image_1b3fav2fa7661pl21usc1g331vq8bl.png-29.7kB

所有的叶子节点包含全部的关键字的信息,及相关指针,叶子节点本身依关键字的大小自小到大顺序链接

B+树的结构特别适合带有范围的查找。比如查找年龄在20~30岁之间的人。

八、散列表(哈希表)

散列表:所有的元素之间没有任何关系。元素的存储位置,是利用元素的关键字通过某个函数直接计算出来的。这个一一对应的关系函数称为散列函数或Hash函数。
采用散列技术将记录存储在一块连续的存储空间中,称为散列表或哈希表(Hash Table)。关键字对应的存储位置,称为散列地址。

散列表是一种面向查找的存储结构。它最适合求解的问题是查找与给定值相等的记录。但是对于某个关键字能对应很多记录的情况就不适用,比如查找所有的“男”性。也不适合范围查找,比如查找年龄20~30之间的人。排序、最大、最小等也不合适。

因此,散列表通常用于关键字不重复的数据结构。比如python的字典数据类型。

设计出一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。
但是,一般散列函数都面临着冲突的问题。
冲突:两个不同的关键字,通过散列函数计算后结果却相同的现象。collision。

8.1 散列函数的构造方法

好的散列函数:计算简单、散列地址分布均匀

  1. 直接定址法
    例如取关键字的某个线性函数为散列函数:
    f(key) = a*key + b (a,b为常数)
  2. 数字分析法
    抽取关键字里的数字,根据数字的特点进行地址分配
  3. 平方取中法
    将关键字的数字求平方,再截取部分
  4. 折叠法
    将关键字的数字分割后分别计算,再合并计算,一种玩弄数字的手段。
  5. 除留余数法
    最为常见的方法之一。
    对于表长为m的数据集合,散列公式为:
    f(key) = key mod p (p<=m)
    mod:取模(求余数)
    该方法最关键的是p的选择,而且数据量较大的时候,冲突是必然的。一般会选择接近m的质数。
  6. 随机数法
    选择一个随机数,取关键字的随机函数值为它的散列地址。
    f(key) = random(key)

总结,实际情况下根据不同的数据特性采用不同的散列方法,考虑下面一些主要问题:

  • 计算散列地址所需的时间
  • 关键字的长度
  • 散列表的大小
  • 关键字的分布情况
  • 记录查找的频率

8.2 处理散列冲突

  • 开放定址法

就是一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

公式是:
image_1b3gi1p0u6qpdqukj3c961kp99.png-29.8kB
这种简单的冲突解决办法被称为线性探测,无非就是自家的坑被占了,就逐个拜访后面的坑,有空的就进,也不管这个坑是不是后面有人预定了的。
线性探测带来的最大问题就是冲突的堆积,你把别人预定的坑占了,别人也就要像你一样去找坑。

改进的办法有二次方探测法和随机数探测法。

  • 再散列函数法
    发生冲突时就换一个散列函数计算,总会有一个可以把冲突解决掉,它能够使得关键字不产生聚集,但相应地增加了计算的时间。

  • 链接地址法
    碰到冲突时,不更换地址,而是将所有关键字为同义词的记录存储在一个链表里,在散列表中只存储同义词子表的头指针,如下图:
    image_1b3gig3eu1uh3rujcvuli1qspm.png-59.3kB

这样的好处是,不怕冲突多;缺点是降低了散列结构的随机存储性能。本质是用单链表结构辅助散列结构的不足。

    • 公共溢出区法
      其实就是为所有的冲突,额外开辟一块存储空间。如果相对基本表而言,冲突的数据很少的时候,使用这种方法比较合适。
      image_1b3gim8dp1m4hd0015su1jvm15mg13.png-56.8kB

8.4 散列表查找性能分析

如果没发生冲突,则其查找时间复杂度为O(1),属于最极端的好了。
但是,现实中冲突可不可避免的,下面三个方面对查找性能影响较大:

  • 散列函数是否均匀
  • 处理冲突的办法
  • 散列表的装填因子(表内数据装满的程度)

 

 

posted @ 2018-09-16 19:57  静静的yu  阅读(411)  评论(0编辑  收藏  举报