七大查找常见算法(下)

一、线性索引查找
1.1 简介
  前面讲的几种比较高效的查找方法是基于有序的基础之上的(详见七大查找常见算法(上)),而事实上,数据集可能增长非常快,例如,某些微博网站或大型论坛的帖子和回复总数每天都是成百万上千万条,或者一些服务器的日志信息记录也可能是海量数据,要保证记录全部是按照当中的某个关键字有序,其时间代价是非常高昂的,所以这种数据都是按先后顺序存储的。
  对于这样的查找表,我们如何能够快速查找到需要的数据呢?常常使用的方法就是—-索引。索引是为了加快查找速度而设计的一种数据结构。它是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。
  索引按照结构可以分为线性索引、树形索引和多级索引。我们这里就只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三个线性索引:稠密索引、分块索引、和倒排索引。
1.2 稠密索引
  稠密索引如下图所示,它是指在线性索引中,将数据集中的每个记录对应一个索引项。

这里写图片描述

  上图中,左边的图像为索引序列,它是是按照关键码有序排列的。索引项有序也就意味着,我们要查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高效率。比如查找上表中的18。如果不用索引表,需要6次。而用左侧的索引表,折半两次就可以找到18对应的指针。
  这显然是稠密索引优点,但是如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
1.3 分块索引
  稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大。为了减少索引的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
  分块有序,是把数据集的记录分成若干块,并且这些块需要满足两个条件:
  (1)块内无序,即每一块内的记录不要求有序。当然,你如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间代价,因此通常我们不要求块内有序
  (2)块间有序,例如要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字….因为只有块间有序,才有可能在查找时带来效率。
  对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。我们定义的分块索引项由三个数据项组成,如下图所示:

这里写图片描述

  这三个数据项分别为最大关键码(它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中最小关键字也能比这一块最大的关键字要大)、存储了块中的记录个数(以便于循环时使用)和指向块首数据元素的指针(便于开始对这一块中的记录进行遍历)。
  由上面的分析我们可以大概明白分块索引的步骤:
  (1)在分块索引表中查找要查关键字所在的块。由于分块索引表是块间有序的,因此很容易利用折半、插值等算法得到结果。
  (2) 根据块首指针找到相应的块,并在块中顺序查找关键码。因为块中可以是无序的,因此只能顺序查获。
1.4 倒排索引
  不知道你对搜索引擎好奇过没,无论你查找什么样的信息,它都可以在极短的时间内给你一些结果,是什么算法技术达到这样的高效查找呢?这里介绍一种最基础的搜索技术—-倒排索引。
  我们来看一个例子,假设有以下两篇文章:
  (1) Books and friends should be few but good .
  (2) A good book is a good friend.
  假设我们忽略掉如“books”,“friends”中的复数”s”以及如“A”这样的大小写差异。我们可以整理出这样一张单词表,如下图所示,并将单词做了排序,也就是表格显示了每个不同的单词分别出现在哪篇文章中,比如“good”它在两篇文章中都有出现,而is只有在文章2中才有。

这里写图片描述

  在这里这张单词表就是索引表,索引项的通用结构是次关键码和记录号表。 其中记录号表存储具有相同次字关键字的所有记录的记录号(可以指向记录的指针或者是该记录的主关键字)。因为这种查找方法是通过属性值来确定记录的位置,而不是通过记录来确定属性值,所以我们称其为倒排索引。

二、树表查找
2.1 二叉树查找算法(最简单的树表查找算法)
  如果要查找的数据集是有序线性表,并且是顺序存储的,查找可以用折半、插值、斐波那契等查找算法来实现,可惜的是,因为有序,在插入和删除操作上就需要耗费大量的时间。有没有一种既可以使得插入和删除效率不错,又可以比较高效的实现查找的算法?这是有的,二叉树查找算法就可以实现这样的功能。
  它的基本思想为:二叉查找树是先对待查找的数据生成其对应的树,其中树的左分支的值小于右分支的值,然后在所查数据和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
  它的性质为:(1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;(2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;(3)任意节点的左、右子树也分别为二叉查找树。同时,对二叉查找树进行中序遍历,即可得到有序的数列。
  不同形态的二叉查找树如下图所示:

这里写图片描述

  对它的时间复杂度进行分析:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。所以可以发现二叉查找树对于大多数情况下的查找和插入在效率上来说是没有问题的,但是它在最差的情况下效率比较低。而我们追求的是在最坏的情况下仍然有较好的时间复杂度,所以普通的二叉树查找算法还没有达到目的,这也就是为何设计平衡查找树的原因。
2.2 平衡查找树之2-3查找树
  和二叉树不一样,2-3树中每个节点保存1个或者两个key值。对于普通的2节点(2-node),它保存1个key和左右两个孩子(或没有孩子)。对应3节点(3-node),保存两个Key和三个孩子(或没有孩子),2-3查找树的定义和性质如下:
  (1)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key有效,有节点也是一个2-3节点,所有的值比key要大。
  (2)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
  (3)2-3树中所有的叶子都在同一层次上。
  同样,如果中序遍历2-3查找树,就可以得到排好序的序列。2-3查找树可如下图所示:

这里写图片描述

复杂度分析:
  2-3树的查找效率与树的高度是息息相关的。
  (1)在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN。
  (2)在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN。
2.3 平衡查找树之红黑树
  2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。红黑树比一般的二叉查找树具有更好的平衡,所以查找起来更快。
  基本思想:红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。
  红黑树的定义:红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:(1)红色节点向左倾斜;(2)一个节点不可能有两个红色链接;(3)整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。
  下图可以看到红黑树其实是2-3树的另外一种表现形式,如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了。

这里写图片描述

  红黑树的性质:整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同(2-3树的第2)性质,从根节点到叶子节点的距离都相等)。
  复杂度分析:最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。红黑树的平均高度大约为logn。红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
2.4 B树和B+树
  平衡查找树中的2-3树以及其实现红黑树。2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。
  B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。定义如下:
  (1)根节点至少有两个子节点;
  (2)每个节点有M-1个key,并且以升序排列;
  (3)位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间;
  (4)其它节点至少有M/2个子节点。
  下图是一个M=4 阶的B树:

这里写图片描述

  B+树是对B树的一种变形树,它与B树的差异在于:
  (1)有k个子结点的结点必然有k个关键码;
  (2)非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中;
  (3)树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
  如下图,是一个B+树:

这里写图片描述

  B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。所以B+树特别适合带有范围的查找。它的插入和删除跟B树类似,只不过插入和删除的元素都是在叶子节点上进行而已。
  B+ 树的优点在于:
  (1)由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
  (2)B+树的叶子结点都是相链的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
  但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。下面是B 树和B+树的区别图:

这里写图片描述

  B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。

三、哈希查找
3.1 简介
  哈希查找也称为散列查找。O(1)的查找,即所谓的秒查。所谓的哈希其实就是在记录的存储位置和记录的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。哈希技术既是一种存储方法,也是一种查找方法。
3.2 哈希查找的操作步骤
  (1)用给定的哈希函数构造哈希表;
  (2)根据选择的冲突处理方法解决地址冲突;
  (3)在哈希表的基础上执行哈希查找。
3.3 哈希函数的构造方法
(1)直接定址法
  函数公式:f(key) = a * key + b(a,b为常数)
  这种方法的优点是:简单、均匀,不会产生冲突。但是需要事先知道关键字的分布情况,适合查找表较小并且连续的情况。
(2)数字分析法
  也就是取出关键字中的若干位组成哈希地址。比如我们的11位手机号是“187****1234”,其中前三位是接入号,一般对应不同的电信公司。中间四位表示归属地。最后四位才表示真正的用户号。
  如果现在要存储某个部门的员工的手机号,使用手机号码作为关键字,那么很有可能前面7位都是相同的,所以我们选择后面的四位作为哈希地址就不错。
(3)平方取中法
  取关键字平方后的中间几位作为哈希地址。由于一个数的平方的中间几位与这个数的每一位都有关,所以平方取中法产生冲突的机会相对较小。平方取中法所取的位数由表长决定。
  如:K=456,K^2=207936,如果哈希表的长度为100,则可以取79(中间两位)作为哈希函数值。
(4)折叠法
  折叠法是将关键字从左到右分割成位数相等的几个部分(最后一部分位数不够可以短),然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址。当关键字位数很多,而且关键字中每一位上数字分布大致均匀时,可以使用折叠法。
  如:我们的关键字是9876543210,哈希表表长三位,我们可以分为四组:987 | 654 | 321 | 0,然后将他们叠加求和:987+654+321+0 = 1962,再取后三位就可以得到哈希地址为962.
(5)除留余数法
  选择一个适当的正整数p(p<=表长),用关键字除以p,所得的余数可以作为哈希地址。即:H(key) = key % p(p<=表长),除留余数法的关键是选取适当的p,一般选p为小于或等于哈希表的长度(m)的某个素数。
  如:m = 8,p=7
    m = 16,p = 13
    m = 32,p = 31
(6)随机数法
  函数公式:f(key) = random(key). 这里的random是随机函数,当关键字的长度不等时,采用这种方式比较合适。
  总之,哈希函数的规则就是:通过某种转换关系,使关键字适度的分散到指定大小的顺序结构中。越分散,查找的时间复杂度就越小,空间复杂度就越高。哈希查找明显是一种以空间换时间的算法。但是在构建映射关系的时候往往存在的最多的问题就是冲突,即把不同的关键字分在了相同的位置上去。此时我们需要解决这个冲突问题。
3.4 解决哈希列表冲突的方法
(1)开放地址法(线性探测法)  
  如果两个数据元素的哈希值相同,则在哈希表中为后插入的数据元素另外选择一个表项。当程序查找哈希表时,如果没有在第一个对应的哈希表项中找到符合查找要求的数据元素,程序就会继续往后查找,直到找到一个符合查找要求的数据元素,或者遇到一个空的表项。
(2)链地址法(拉链法)
  将哈希值相同的数据元素存放在一个链表中,在查找哈希表的过程中,当查找到这个链表时,必须采用线性查找方法。 
(3)公共溢出法
  它的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。在查找时,对给定值通过哈希函数计算出哈希地址后,先于基本表的相应位置进行对比,如果相等则查找成功;如果不相等,则到溢出表中去进行顺序查找。
3.5 总结
  哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
  复杂度分析:单纯论查找复杂度,对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。可以发现查找数据的效率非常高,但是我们实现快速的查找付出了什么代价?
  Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
  各种查找算法的最坏和平均条件下各种操作的时间复杂度如下图所示:

这里写图片描述

  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值