查找算法
查找 (Searching ) 就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
- 对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
- Pi:查找表中第i个数据元素的概率。
- Ci:找到第i个数据元素时已经比较过的次数。
一、顺序查找。
顺序查找 (Sequential Search) 又叫线性查找,是最基本的查找技术, 它的查找过程是:从表中第一个(或最后一个)记录开始 , 逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功 , 找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功 。
说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
复杂度:时间复杂度为O(n)
java实现:
1 private static int search(int[] a, int key) { 2 int i; 3 for(i = 1; i <= a.length; i++){ 4 if(a[i] == key) 5 return i; 6 } 7 return 0; 8 }
优化:
上面程序第3行每次循环都要 判断是否越界,事实上,还可以有更好一点的办法,设置一个哨兵,不用这个判断。
1 private static int search(int[] a, int key) { 2 int i = a.length-1; 3 a[0] = key; //设置哨兵 4 while(a[i] != key){ 5 i--; 6 } 7 return i;//返回0表明查找失败 8 }
二、有序表查找
2.1二分查找
折半查找 (Binary Search) 技术,又称为二分查找。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域元记录,查找失败为止。
说明:二分查找前提是线性表中的记录必须是关键码有序(通常从小到大有序) ,线性表必须采用顺序存储。
复杂度:时间复杂度为O(log2n)
java实现:
1 public static int binarySearch(int[] a, int key){ 2 int left = 0; 3 int right = a.length-1; 4 while(left <= right){ //注意这里有等号 5 int mid = (left + right) / 2; 6 if(a[mid] == key){ 7 return mid; 8 }else if(a[mid] < key){ 9 left = mid + 1; 10 }else{ 11 right = mid - 1; 12 } 13 } 14 return -1; 15 }
注意:代码中的判断条件必须是while (left <= right),否则判断不完整。
例:array[3] = {1, 3, 5},待查找的键为5,此时在(low < high)条件下就会找不到,因为low和high相等时,指向元素5,但是此时条件不成立,没有进入while()中。
二分查找的变种
数组之中的数据可以重复:
(1)查找第一个与key相等的元素;
1 public static int binarySearch2(int[] a, int key){ 2 int left = 0; 3 int right = a.length-1; 4 while(left <= right){ //注意这里有等号 5 int mid = (left + right) / 2; 6 if(a[mid] >= key){ 7 right = mid - 1;//因为要找第一个,所以等于的时候肯定得向左移 8 }else { 9 left = mid + 1; 10 } 11 } 12 if(left < a.length && a[left] == key){ 13 return left; 14 } 15 return -1; 16 }
(2)查找最后一个与key相等的元素;
1 public static int binarySearch3(int[] a, int key){ 2 int left = 0; 3 int right = a.length-1; 4 while(left <= right){ //注意这里有等号 5 int mid = (left + right) / 2; 6 if(a[mid] <= key){ 7 left = mid + 1; //因为要找最后个,所以等于的时候肯定得向右移 8 }else { 9 right = mid - 1; 10 } 11 } 12 if(right > 0 && a[right] == key){ 13 return right; 14 } 15 return -1; 16 }
(3)查找最后一个等于或者小于key的元素;
(4)查找第一个等于或者大于key的元素
等等,基本上都是修改第6行的判断条件的符号(if(a[mid] <= key)),模版都是上面一样的。
我们需要注意:
1)判断返回left还是right
最后跳出while (left <= right)循环条件是right < left,即right = left - 1。举例试一下最稳。
2)if(a[mid] <= key)的判断符号。
2.2、插值查找
打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
这其实就是对二分查找的优化
算法法科学家们将这个 1/2 进行改进 :mid=low+(key-a[low])/(a[high]-a[low])*(high-low),即key如果离low近,则把mid选的靠近low。
java实现
1 public static int InsertionSearch(int[] a, int key){ 2 int left = 0; 3 int right = a.length-1; 4 while(left <= right){ 5 int mid = left+(key-a[left])/(a[right]-a[left])*(right-left);//对比二分查找只是改了这里 6 if(a[mid] == key){ 7 return mid; 8 }else if(a[mid] < key){ 9 left = mid + 1; 10 }else{ 11 right = mid - 1; 12 } 13 } 14 return -1; 15 }
2.3、斐波那契查找
也是二分查找的优化,它是利用了黄金分割原理来实现的 。
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况:
1)相等,mid位置的元素即为所求
2)>,low=mid+1;
3)<,high=mid-1。
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=F(k)-1;
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种
1)相等,mid位置的元素即为所求
2)>,low=mid+1,k-=2;
说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。
3)<,high=mid-1,k-=1。
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。
复杂度:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。
三、线性索引查找
有序查找是基于数据已经排好序的基础上,可是当有大量数据时,可能排序的代价非常高。
那么对于这样的查找呢,我们如何能够快速查找到需要的数据呢?办法就是一一索引。
索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术。
索引按照结构可以分为线性索引、树形索引和多级索引。我们这里就只介绍线性索引技术。 所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三种线性索引;稠密索引、 分块索引和倒排索引。
3.1稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。
稠密索引要应对的可能是成千上万的数据,因此对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。我们要查找关键字时,可以用到折半、插值、斐被那契等有序查找算法,大大提高了效率 。
但是如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
3.2分块索引
分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:
块内无序,即每一块内的记录宋要求有序。当然 ,你如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常我们不要求块内有序 。
块间有序,例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字……因为只有块间有序,才有可能在查找时带来放率。
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。
我们定义的分块索引的索引项结构分三个数据项 :
- 最大关键码,它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字要大;
- 存储了块中的记录个数,以便于循环时使用;
- 用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。
在分块索引表中查找,就是分两步进行:
由于分块索引表是块间有序的,因此很容易利用折半、插值等算法得到结果;
因为块中可以是无序的,因此只能顺序查找。
普遍被用于数据库表查找等技术的应用当中。
3.3倒排索引
最基础的搜索(引擎)技术——倒排索引
举个例子:
现在有两篇极短的英文"文章"——其实只能算是句子,我们暂认为它是文章,编号分别是 1 和 2 。
- Books and friends should be few but good (读书如交友,应求少而精。)
- A good book is a good friend (好书如挚友。)
假设我们忽略掉如 "books" 、"什iends" 中的复数 "s" 以及如 "A" 这样的大小写差异。我们可以整理出这样一张单词表并将单词做了排序,也就是表格显示了每个不同的单词分别出现在哪篇文章中,比如"good"它在两篇文章中都有出现,而“is”只是在文章 2 中才有。
有了这样一张单词衰,我们要搜索文章,就非常方便了。如果你在搜索框中填写" book" 关键字.系统就先在这张单词表中有序查找"book",找到后将它对应的文章编号1和2的文章地址(通常在搜索引擎中就是网页的标题和链接)返回,并告诉你,查找到两条记录,用时 0.0001 秒。由于单词表是有序的,查找效率很高,返回的又只是文章的编号,所以整体速度都非常快。
在这里这张单词表就是索引衰,索引项的通用结构是:
- 次关键码.例如上面的"英文单词" ;
- 记录号衰,例如上面的"文章编号"。
其中记录号表存储具有相同次关键字的所有记录的记录号 (可以是指向记录的指针或者是该记录的主关键字) 。 这样的索引方法就是倒排索引 (invened index) 。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。
当然,现实中的搜索技术非常复杂,比如我们不仅要知道某篇文章有要搜索的关键字,还想知道这个关键字在文章中的哪些地方出现,这就需要我们对记录号表做一些改良。再比如,文章编号上亿,如果都用长数字也没必要,可以进行压缩,比如三篇文章的编号是 "112,115,119" ,我们可以记录成 "112 , +3 , +4" ,即只记录差值,这样每个关键字就只占用一两个字节。
**************************************************************************************************
如果查找的数据集是有序线性表,并且是顺序存储的,查找可以用折半、插值、斐波那契等查找算法来实现,可惜,因为有序,在插入和删除操作上,就需要耗费大量的时间。有没有一种即可以使得插入和删除效率不错,又可以比较高效率地实现查找的算法呢?还真有。(把这种需要在查找时插入或删除的查找表称为动态查找表)
**************************************************************************************************
四、树表查找
4.1最简单的树表查找算法——二叉树查找算法。
基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3)任意节点的左、右子树也分别为二叉查找树。
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
有关二叉查找树的查找、插入、删除等操作的详细讲解,请移步浅谈算法和数据结构: 七 二叉查找树。
复杂度分析:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
下图为二叉树查找和顺序查找以及二分查找性能的对比图:
基于二叉查找树进行优化,进而可以得到其他的树表查找算法,如平衡树、红黑树等高效算法。
4.2平衡查找树之2-3查找树(2-3 Tree)
2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
1)要么为空,要么:
2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
2-3查找树的性质:
1)如果中序遍历2-3查找树,就可以得到排好序的序列;
2)在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中“平衡”一词的概念,根节点到叶节点的最长距离对应于查找算法的最坏情况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。)
复杂度分析:
2-3树的查找效率与树的高度是息息相关的。
- 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
- 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN
距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。
对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:
4.3平衡查找树之红黑树(Red-Black Tree)
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的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。
红黑树的定义:
红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:
- 红色节点向左倾斜
- 一个节点不可能有两个红色链接
- 整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。
下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了。
红黑树的性质:整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同(2-3树的第2)性质,从根节点到叶子节点的距离都相等)。
复杂度分析:最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。
下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的2倍:
红黑树的平均高度大约为logn。
下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
红黑树这种数据结构应用十分广泛,在多种编程语言中被用作符号表的实现,如:
- Java中的java.util.TreeMap,java.util.TreeSet;
- C++ STL中的:map,multimap,multiset;
- NET中的:SortedDictionary,SortedSet 等。
4.4平衡查找树之B树和B+树(B Tree/B+ Tree)
平衡查找树中的2-3树以及其实现红黑树。2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。
维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。
B树定义:
B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
-
根节点至少有两个子节点
-
每个节点有M-1个key,并且以升序排列
-
位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
-
其它节点至少有M/2个子节点
下图是一个M=4 阶的B树:
可以看到B树是2-3树的一种扩展,他允许一个节点有多于2个的元素。B树的插入及平衡化操作和2-3树很相似,这里就不介绍了。下面是往B树中依次插入
6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4
B+树定义:
B+树是对B树的一种变形树,它与B树的差异在于:
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
如下图,是一个B+树:
下图是B+树的插入动画:
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
- 由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
- B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
下面是B 树和B+树的区别图:
B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。它广泛用于文件系统及数据库中,如:
- Windows:HPFS文件系统;
- Mac:HFS,HFS+文件系统;
- Linux:ResiserFS,XFS,Ext3FS,JFS文件系统;
- 数据库:ORACLE,MYSQL,SQLSERVER等中。
有关B/B+树在数据库索引中的应用,请看张洋的MySQL索引背后的数据结构及算法原理这篇文章,这篇文章对MySQL中的如何使用B+树进行索引有比较详细的介绍,推荐阅读。
树表查找总结:
二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。
五、哈希查找
什么是哈希表(Hash)?
什么是哈希函数?
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
算法流程:
复杂度分析:单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?
1:《大话数据结构》
2:http://www.cnblogs.com/maybe2030/p/4715035.html#_label4