首先顺序查找的很简单理解的,就是按顺序一个个从后到前查找(书中约定返回命中的多个相同元素的最大的秩)。
template <typename T>
Rank Vector :: find(T const& e, Rank lo, Rank hi) const{
while ((lo < hi--) && (e != _elem[hi]));
return hi;
}
容易得出该算法的时间复杂度为:O(n)
。
以下总结二分查找算法。
二分查找算法(版本A)利用的是此前学习的减而治之的思想策略。因为总是可以将它分解为两个规模减少的子问题和一个平凡的子问题。
二分查找(版本A)的思路不再进行赘述,这篇博客主要讲的是如何更细微地改进查找算法。
二分查找(版本A)
//在有序向量空间[lo,hi)内查找元素e,0 <= lo <= hi <= _size
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi){
while(lo < hi){
Rank mid = (lo + hi) >> 1;
if(e < A[mi]) hi = mid;
else if(e > A[mi]) lo = mid + 1;
else return mid
}
return -1;
}
该算法的时间复杂度可以很快计算出来为:O(1.5*logn)
。
继续对二分查找进行改进。
Feibonacci查找
# include "..\fibonacci\Fib.h" //这里调用自己构造的斐波那契数列的.h文件
template <typename T> static Rank fibSearch(T* A, T const& e, Rank lo, Rank hi){
Fib fib(hi - lo); //首先构建fib数列,这里fib(hi - lo)应该是整个向量的长度,而向量的实际规模为 (hi - lo)
while (lo < hi){
while (hi - lo < fib.get())
fib.prev(); //获取fib的前一个数字
Rank mi = lo + (fib.get() - 1);
if (e < A[mi]) hi = mid;
else if (A[mi] < e) lo = mid + 1;
else return mid;
}
return -1;
}
该算法的时间复杂度可以很快计算出来为:O(1.44*logn)
。
可以很明显地看出,二分查找(版本A)与Feibonacci查找的区别在于轴点mi
的区别,这个区别在于,**二分查找(版本A)**在进行分支的时候,左侧进行一次操作,右侧则要进行两次操作(先向左(1次),再向中间比较(1次))。如图所示:
那么可以看出来,二分查找时左右两侧查找的操作次数不一样,但是查找的长度是一样的。所以,将查找长度变一下(即改变轴点mi
的大小)。滚局计算出来的时间复杂度,可以看出,在小规模数据情况下,其实两种查找方法区别并不大,甚至,后面的斐波那契数列查找算法因为要构造斐波那契数列反而稍显麻烦,但是,在数据规模增大以后,这样的区别会相对明显,那么,斐波那契数列算法确实更加好一点。
既然看出来是介于轴点mi
的大小不一样导致的查找长度不一样,那么,改变轴点mi
的大小,而不仅仅局限于二分,黄金分割(斐波那契数),可以根据数据的具体情况分割出更合适的轴点。
既然我们已经看出来查找的时间与查找的长度和查找的擦做次数有关,那么,可以针对这两个方面做更精细的算法。
二分查找(版本B)
不再是三个分支,而是改做两个分支。
template <typename T> static Rank fibSearch(T* A, T const& e, Rank lo, Rank hi){
while (1 < hi - lo){ //区别在此处
Rank mi = (hi + lo) >> 1;
(e < A[mi])? hi = mi : lo = mid; //区别在此处,查找时:[lo,mi)和[mi,hi)
}//出口时仅有A[lo]一个元素
return (e == A[mi])? lo : -1;
}
很明显,变化在于:(e < A[mi])? hi = mi : lo = mid;
,缩减为两个分支,将轴点mi
纳入右侧一并查找。这样查找最后只有当查找元素的空间宽度缩减为1时算法才会终止。坏处是,即使一开始在轴点mi
命中,也要等整个算法终止才可以停下返回数值;好处是,这样做确实减少了操作次数,时间复杂度还是O(logn)
,但明显会比版本A要高效一点。
二分查找(版本C)
template <typename T> static Rank fibSearch(T* A, T const& e, Rank lo, Rank hi){
while (lo < hi){ //区别在此处
Rank mi = (hi + lo) >> 1;
(e < A[mi])? hi = mi : lo = mid + 1; //区别在此处,查找时:[lo,mi)和(mi,hi)
}//
return --lo;
}
可以看到:(e < A[mi])? hi = mi : lo = mid + 1;
这里似乎是舍弃了A[mi]
,会不会导致查找失败呢?
来分析下,版本C中,具有以下不变性:A[0,lo)中的元素皆不大于e;A[hi,n)中的元素皆大于e
。
首次迭代时,lo = 0
且hi = n
,A[0,lo)
和A[hi,n)
均为空,不变性成立。
进入循环后呢?如图:无论如何,最后得到的总会是lo-1
的那个秩才是最后要查找的秩所代表的元素。如何失败,那就是-1
,否则,返回的是A[lo-1]
。自此,可以验证算法是正确的。
总和比较以上五种查找算法,可以很轻易地放弃顺序查找,毕竟它相对比较慢。
其次,我们最熟悉的版本A的二分查找,要非常熟练,至于Feibonacci查找相对了解即可,毕竟,重点在于能不能找到更好地轴点mi
,如果能找到的话,未必需要构造斐波那契数列。
第三,版本B和版本C确实值得以后再想到二分查找的时候优先选择这两种方法,它相对版本A确实有改进,并且,使我们理解的范围内能够使用到的。既然已经了解了,那么久大胆地去使用。
自此,完成了对二分查找的总结。
再次回顾下,几个要点:
1.二分查找的指导思想:减而治之。
2.操作次数和查找长度的优化可以更加优化算法。
3.优化代码靠的是对数据结构的分析,对程序执行过程的分析,在此基础上,找到他们的共同点,进行优化。
(一些小知识:i++和++i的区别
。最通俗易懂的i++和++i详解)