1. 顺序表查找(Sequential Search)
1> 算法思想:顺序表查找应该是查找算法中最简单的了。顺序表中所有的记录都是无序的,因此在查找时,没有对查找对象范围的可能线索,唯一的方法就是沿着一个方向一直比较,直到和查找对象相等。完成查找的过程。这里一个优化点是设置一个哨兵,放在顺序表的开始或者结束,每次在搜索顺序表的时候就不必考虑索引是否越界,减少了比较的过程,性能得到提高。
2> 时间复杂度:最好的情况是第一次比较就找到了,时间复杂度是O(1),最坏情况是到最后一个记录才找到,或者根本没找到,时间复杂度是O(n)。因此平均的查找次数是(n+1)/2,时间复杂度是O(n)
3> 优缺点:顺序表的优点就是简单,插入只需要在表的末尾增加一个记录,删除也只需用末尾的记录替换待删除的记录,后将末尾记录删除即可。缺点就是查找的效率比较低,尤其是当n很大时,因此顺序表对于一些小型数据的查找是有用的。
2. 有序表查找
顺序表中的记录之间没有大小关系,造成查找时效率低,如果我们的表是有序的呢?
2.1 折半查找(Binary Search)
1> 算法思想:对有序表来说,可以每次取表的中间进行比较,如果查找记录和中间的记录不同,就可以通过比较大小知道它落在有序表中的哪一半,这样就可以过滤掉另一半的记录。这样有序表的结果实际上就是一个二叉树,效率得到提高。
2> 时间复杂度:折半查找的总次数就是二叉树的深度,对于n个结点的二叉树深度为log2n+1,因此时间复杂度为O(logn)。
3> 优缺点:折半查到的时间复杂度是O(logn),好于顺序查找的O(n),但它的缺点就在于每次都是从表的中间开始,如果对于一个很大的表,查找表的开始或结束附近的记录就需要经过很多次查找。这个缺点在下面的插值查找中得到的解决。
2.2 插值查找(Interpolation Search)
1> 算法思想:插值查找和折半查找类似,可以通过比较表中的某项记录,确定查找记录的范围。唯一不同的是,对每次查找的中间点进行了变换,使之更加接近待查找的记录,减少了查找的次数。折半查找的中间点mid = low + (high - low) / 2,插值查找对1/2进行了修正,mid = low + (key - a[low])/(a[high] - a[low]) * (high - low)
2> 时间复杂度:和折半查找相同,插值查找的时间复杂度也是O(logn)
3> 优缺点:对于分布均匀的有序表来说,插值查找减少了查找次数,因为(key - a[low])/(a[high] - a[low])更加接近于查找记录在表中的位置。但对于分布不均匀的表来说,插值查找就未必是合适的选择。
2.3 斐波那契查找(Fibonacci Search)
1> 算法思想:依然是对查找点的优化,采用Fibonacci数组,找到对于当前长度的有序表的黄金分割点,作为每次的中间值。
2> 时间复杂度:时间复杂度和其他两种有序表查找相同,都是O(logn)
3> 优缺点:对于平均性能,斐波那契查找要优于折半查找,但如果是最坏情况,查找效率低于折半查找。
小结:有序表查找是一种针对查找优化的表结构,查找的时间复杂度是O(logn)。但有序表的插入和删除性能是比较差的,插入和删除不能破坏有序表的特性。
3. 线形索引查找
之前的查找都是基于记录所在的位置,如果我们的查找对象是key-value对呢?
3.1 稠密索引
1> 算法思想:稠密索引是关键码有序的,因此可以对索引使用折半、插值、斐波那契等有序表查找算法,大大提高了效率。
2> 时间复杂度:因为对于索引的查找使用的也是有序表的查找算法,时间复杂度是O(logn)
3> 优缺点:和有序表类似的是,稠密索引必须要维护索引的有序性。另外如果数据量很大,也要同时维护一个同样规模的索引,可能就需要反复访问磁盘,降低了查找性能。
3.2 分块索引
1> 算法思想:如果对索引进行一次分级呢?对于一级索引下,可能会有多个记录,称之为一个块,块内的记录再获得一个二级的索引。这些块有一个条件,就是块内无序,块间有序。块内无序就是在一级索引内部的记录可以是无序的,只要落在索引的范围内就可以;块间有序就是下一个块所有的关键字都要大于上一个块的最大关键字。因此对于一个块结构来讲,它具有最大关键码,块中的记录个数和指向块首数据的指针。
2> 时间复杂度:分块索引在查找时,先找到查找记录所在的块,在查找在块内的为孩子。设n个记录,平均分成m个块,每个块有t个记录,这样平均查找次数就是(m+1)/2 + (t+1)/2 = (n/t + t)/2 + 1 >= logn + 1。所以分块索引的时间复杂度介于O(n)和O(logn)之间。
3> 分块索引兼顾了有序和无序的需求,平衡了插入,删除和查找的性能,普遍用于数据库查找技术等。
3.3 倒排索引
1> 算法思想:倒排索引主要应用于搜索引擎。基本思想就是将得到的key-value关系进行一个反映射,得到某个value有多少个key指向它。比如查找某个单词出现在哪些文章中,可以先访问文章中的所有单词,建立一个单词的索引,将出现该单词的文章记录到索引中。这样在搜索时直接输入单词,就能得到文章列表。
2> 优缺点:倒排索引的优点是速度快,缺点就是记录不等长,维护比较困难,插入和删除都要做相应的处理。比如删除某个文章,就可能要对所有的单词都进行考察。
4. 二叉排序树
1> 算法思想:有序表的问题就是如果插入一个较小的记录,就要把比它大的记录依次移动,腾出插入的位置。如果用二叉树来实现呢,只需要让这个较小的记录成为某个结点的左孩子就可以了。为什么是左孩子呢,和二叉排序数的定义有关,简单来说,二叉排序树的中序遍历就是一个有序表。这样插入任何一个记录都不需要改变已经建好的树。
1) 查找:查找某个记录时,从根结点开始,如果查找记录大于该结点的值,就走右子树;如果小于该结点的值,就走左子树。不断向下查找,直到找到该记录,或者到叶子结点的值和查找记录不同,未找到该记录。
2) 插入:插入和查找类似,向下找到最接近它的结点,然后把该记录作为它的左孩子或者右孩子。
3) 删除:删除相对查找和插入来讲复杂一点,主要复杂在如果处理它的子树。删除分为几种情况,如果删除结点是叶子结点,直接把它和二叉排序树断开即可,不影响树上的其他结点。如果删除结点带左子树或者右子树(不同时),那就将它左子树或者右子树的根结点代替它,连接到树上。如果删除结点同时带有左子树和右子树呢?想要对原排序树的破坏最小,最好的办法是找到该结点的前驱或者后继结点,这可以很容易的找到。假设我们这里使用删除结点的前驱结点,要先将前驱结点的值赋给删除结点,如果前驱结点就是删除结点的左孩子(删除结点及其子孙只有左孩子,斜树),就将前驱结点的左子树接到删除结点的左子树上;如果前驱结点是某个结点的右孩子(删除结点及其子孙不只有左孩子),还要将它的左子树接到它父母的右子树上。
2> 时间复杂度:如果二叉排序树是平衡的,那么查找的时间复杂度是O(logn);如果是不平衡,比如最极端的斜树,那么时间复杂度是O(n)。
3> 优缺点:二叉排序树保留了有序表查找高效的特点,最理想的情况能达到O(logn)的时间复杂度,并且解决了插入和删除记录的问题,能够保证树的整体结构不受影响。缺点就是可能在插入的过程中,二叉排序树不能保持平衡,出现了某一边的树远远大于另一边,降低了查找的效率。后面提到的平衡二叉树解决了这个问题。
5. 平衡二叉树(AVL)
1> 算法思想:平衡二叉树的出现是为了解决二叉排序树可能出现的不平衡问题。平衡二叉树的概念是树中任何结点的平衡因子只能是-1,0,1,也就左子树和右子树的深度相差最多是1。为了实现这个目的,每次插入记录后,都会检查二叉树是否处在平衡状态,如果不是的话,会进行相应的旋转操作使之平衡。平衡的过程就是从新插入的结点向上查找,如果某个结点的BF=2,就顺时针旋转。最简单的旋转就是对于斜树,直接将BF=2结点的孩子作为新的子树根结点,将BF=2连接到它的右孩子。稍复杂的旋转就是BF=2和它的左孩子有相同的旋转方向,这样将它的左孩子作为新的子数根结点,BF=2连接在新根结点的右子树上,再将新的根结点原来的右孩子连接到BF=2的左子树上。最复杂的旋转就是BF=2和它的左孩子是相反的旋转方向,就要将它的左孩子先进行一次右旋转,再对BF=2作左旋转。同样,如果找到某个结点的BF=-2,做逆时针旋转,旋转的方法和顺时针旋转类似。
2> 时间复杂度:由于平衡二叉树的特性,它的时间复杂度一直是O(logn)。
3> 优缺点:平衡二叉树的优点就在于将不平衡消灭在最初的阶段,保持了很好的查找特性。缺点?比较复杂?
6. 2-3树,2-3-4树,B树
1> 算法思想:结点的孩子可以多于2个,可以是3个,4个。
// TODO: add B-tree later
7. 散列表查找
1> 算法思想:对key-value对进行储存。散列就是把关键字和存储位置之间建立一个确定的对应关系,f(key) = location. 因此散列表的查找就是通过散列函数计算出关键字存储记录所在的位置。存储就是通过散列函数计算出该记录的关键字代表的应该存储的位置。散列表的构造方法有很多种,包括直接定址法(f(key)= a * key +b),平方取中法(对关键字算平方后取出中间的几位),折叠法(把关键字分割几次后叠加求和),除留余数法(f(key) = key mode p (p <= m),通常p为小于或等于表厂的最小质数或者不包含小于20质因子的合数),随机数法( f(key) = random(key))。如果两个不同的key计算出相同的地址,那么就出现了冲突,如何处理这种冲突呢?开放定址法(寻找下一个散列地址,fi(key) = (f(key) + di) MOD m),再散列函数法(准备多个散列函数),链地址法(每个记录是个单链表),公共溢出区法(冲突放在溢出表)