刚学完一些基础的数据结构与算法,想借此机会与大家分享一些学习所得。第一次写博客,言语不当,请海涵,如有错误,请指正。
那下面我就来总结一下二分查找的基础算法与改进思路:
1,基础算法
二分查找是基于有序向量的一种简单的查找算法法。其基本思想是减而治之,也就是将一段向量的查找,分成两个子段的查找,二分查找中的二分也就是这么来的。
核心代码如下:
while(lo<hi){//在秩 lo ,hi之间,查找元素 e
Rank mi=lo+(hi-lo)>>1; //防止lo+hi越界
if(e<a[mi]) hi=mi; //转入前半段查找
else if(e>a[mi]) lo=mi++; //转入后半段查找
else return mi; //命中e
}
复杂度分析:
每次循环的结果,不外乎以下三种 :
1) 转入左半段查找 , 比较次数 1
2) 转入右半段查找 , 比较次数 2
3)命中 , 比较次数 2
也就是说,没经过至多2次比较,或者能够命中,或者能将问题规模减半。
最好情况:
第一次即命中,O(1)
最坏情况:
T(n)=T(n/2)+o(1)=O(log(n))
如果精确到常数项,大约为 O(1.5log(n))
2、改进思路:
1)从前面的复杂度分析,我们知道,每次循环后左右转向,关键码的比较次数是不一样的。转入左半段,只需一次比较,而转入右半段,需要两次比较。
那我们可不可以,更多的进行左侧的查找?这样整体的比较次数会下降,也就提高了算法的性能。
一个很直接的想法就是,我们在划分左右子段的时候,左段比右段多划分一些元素,那么转向左段查找的概率也就想要的增大。那么问题来了,该如何划分才能使得查找次数最优呢?我把这个问题抽象成一个数学问题:
由于一些很复杂的数学公式,用Markdown表示,我不会。所以我直接说出结论,当划分取做 0.6180339时,平均查找长度最优。这也就是大名鼎鼎的黄金分割点。我们知道基础版本的二分查找的划分是0.5,那有没有一种查找算法的划分是0.6180339呢?
答案是肯定的,fibonacci查找就是了。一看这个名字,就知道这种查找算法是基于fibonacci数列的。其代码如下:
template
fibsearch(T*A,T const &e,Rank lo,Rank hi)
{
Fib fib(hi-lo);
while(lo < hi )
{
while( hi-lo < fib.get()) fib.prev();
Rank mi=lo+fib.get()-1; //按黄金分割切分
f(e < a[mi]) hi=mi; //转入前半段查找
else if(e>a[mi]) lo=mi++; //转入后半段查找
else return mi; //命中e
}
}
其实这已经不是二分查找了
2)上面的做法是,利用更多左侧查找来对转向成本的不均衡进行补偿。如果换种思路我们可不可以直接一劳永逸的解决这种不均衡呢?
二分查找版本B:
方法:
(i)e < x:则 e 如果存在,必在左侧的子区间 [lo,mi)
(ii)否则, e 如果存在,必在左侧的子区间 [mi,hi) 。请注意这里区间是包括mi这个点的
只有当子区间元素数目 hi-lo=1时,才能判断该元素是否命中
while(1 < hi-lo){//只有当查找区间为1 的时候算法才终止
Rank mi=lo+(hi-lo)>>1;
(e < A[mi])?hi=mi:lo=mi;
return (e==A[lo])?lo:-1;
}
我们知道,基础版本的算法,最好情况是一次命中,最坏情况下 O(1.5log(n))。相比较而言,这种算法整体性能比较稳定。
二分查找版本C:
while(lo < hi){
mi=lo+(hi-lo)>>1;
(e < A[mi])?hi=mi:lo=mi++;
return lo--;
}
版本C与版本B很像,我们来比较一下它们的差异:
1)循环退出的条件不同。一个要求 hi-lo=1 ,一个要求hi-lo=0,也就是待查找区间长度为0而非1是算法才结束。
2)转入左侧子区间时,左侧边界取做 mi+1 而非mi 。那A[mi]会被遗漏吗?
3)return lo–是啥意思。
我们来简单说明一下版本C:
程序主要由循环构成
循环的过程,就是在不断的压缩搜索区间,直至最终区间长度为0。此处应用图形,奈何Markdown不熟。我就简单描述一下:
不妨假设每次均转入左半段
第一次循环,查找区间为:[ lo , hi )
第二次循环,查找区间为:[ lo , hi 1 )此处hi1 为(hi+mi)/2 查找空间被压缩了一半
第三次循环,查找区间为:[ lo , hi 2 )此处hi2 为(hi1+mi)/2 查找空间又被压缩了一半
。
。
。
那么对于一个长度为n的向量,经过log 以2为底的n 次后,被压缩到0。
在循环的过程中,一个非常非常重要不变性始终保持:
A[0 , lo ) <= e < A[ hi , n)
最终:lo==hi
上面的不变性变成了: A[0 , lo ) <= e < A[lo , n)
也就是说整个向量A 被lo分成了两个部分,在左侧严格 <=e而在右侧严格>e
那么return lo–,也就是返回 秩最大 的那个 不大于 e 的元素
我们知道 c++中vector的search接口的语义约定就是,返回 秩最大 的那个 不大于 e 的元素,这再完美不过。
就这样子。