二分查找

刚学完一些基础的数据结构与算法,想借此机会与大家分享一些学习所得。第一次写博客,言语不当,请海涵,如有错误,请指正。
那下面我就来总结一下二分查找的基础算法与改进思路:

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 的元素,这再完美不过。
就这样子。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值