浅谈二分查找
来源:互联网 发布:办公无线键盘鼠标 知乎 编辑:程序博客网 时间:2024/05/19 10:39
如果你学习数据结构已经有一段时间了,那么我相信对于二分查找,你一定不会陌生,但是如果你还没听说过斐波那契查找,那么我想这篇文章值得你一读~接下来我们就从最基本的二分查找说起,来聊一聊关于二分查找的各种变化版本。
一、二分查找
为了把重点放在查找算法的讨论上,我们这里统统使用整型数组作为查找目标的容器;其他类型的容器,只要其元素之间可以比较大小,那么就都可以使用基于比较树模型的查找算法,关于比较树模型,可以参考另一篇博客(暂未完成)。
那么对于一个给定的数组,如果数组中的元素是无序的,我们要在这个数组中查找某个元素的话,只能从第一个元素开始一一比对,直到找到我们要找的元素,这样的话需要的时间复杂度为O(n);但是如果这个数组中的元素是按序排列的,那么我们是不是还需要从第一个元素开始依次比对呢?答案是否定的,数组这种循秩访问(也就是按照索引访问元素)的特点加上有序性,使得我们可以将“减而治之”(这里是指搜索的范围不断减小,所以是减治)的策略运用于有序数组的查找,思路如下:
假设我们需要再数组S的[lo, hi)区间中查找元素t:
以任一元素S[mid]为界,数组S可以被分为三个部分:S[lo, mid),S[mid],S(mid, hi);
且根据数组的有序性可知:S[lo, mid) <= S[mid] <= S(mid, hi)
那么我们只需要将S[mid]与目标元素t对比,就可以根据比较结果分三种情况进一步处理:
- S[mid] < t,则t若存在,一定位于右侧子区间S(mid, hi);
- S[mid] > t,则t若存在,一定位于左侧子区间S[lo, mid);
- S[mid] = t,则s[mid]就是我们要找的元素!
那么我们怎样实现这样的一种算法呢?我们先来看一下版本A:
//查找范围[lo, hi)int binSearch(int *arr, const int e, int lo, int hi){ while(lo < hi) { int mid = (lo + hi) >> 1; if(e < arr[mid]) hi = mid; if(arr[mid] < e) lo = mid + 1; //search successed else return mid; } //if search failed, return -1 return -1;}
- 每次迭代的判断次数不平衡,有可能只需判断一次,也有可能要判断两次;
- 有多个元素命中时,不能保证返回秩最大(或者最小)者;
- 查找失败时,不能指示失败的位置;
接下来我们以长度为7的向量为例,分析其成功查找长度和失败查找长度:(涉及的具体证明可以查阅原书)
每一个成功查找所对应的查找长度,仅取决于向量的长度n以及目标元素所对应的秩,而与元素的具体数值无关,因此我们可以得出,n = 7时,各元素所对应的成功查找长度和失败查找长度分别为:
{4, 3, 5, 2, 5, 4, 6} 平均为4.14;
{3, 4, 4, 5, 4, 5, 5, 6} 平均为4.50;
我们可以看出,迭代次数相同的情况下对应的查找长度不尽相同!这就是所谓的不平衡,那么对于更加一般的情况(n = 2^k - 1),我们采用递推分析法:
递推式:C(k) = [C(k-1) + (2^(k-1) - 1)] + 2 + [C(k-1) + 2 * (2^(k-1) - 1)]
C(k)是查找长度;
[C(k-1) + (2^(k-1) - 1)] 代表经过一次成功的比较,查找范围深入到左侧部分;
2 代表经过两次失败的比较,命中要查找的元素;
[C(k-1) + 2 * (2^(k-1) - 1)] 代表经过一次失败的比较和一次成功的比较,查找范围深入到右侧部分;
最终我们可以得出平均查找长度为O(1.5logn);对于失败查找长度同样可以得出为O(1.5logn);我们说这个常系数还有改进的余地,当查找更加平衡时,常系数是可以减小的!
二、斐波那契查找
为了优化算法的时间复杂度,我们不妨从上述那个递推式入手:
递推式:C(k) = [C(k-1) + (2^(k-1) - 1)] + 2 + [C(k-1) + 2 * (2^(k-1) - 1)]
我们仔细观察这两项:C(k-1) + (2^(k-1) - 1) 和 C(k-1) + 2 * (2^(k-1) - 1),其中(2^(k-1) - 1)为子向量的宽度,而系数1和2则是算法深入左右子向量所需要做的比较次数,我们说该算法的不平衡正是源于此处!如果我们能够将系数为1的子向量长度适当加长,系数为2的子向量长度适当减小,那么算法将变得平衡。这正是斐波那契查找的思路来源。
我们在分割向量为左右两部分时,切分点不一定非得是中间点,事实上切分点的选取是任意的!那么我们根据上述改进思路,不妨根据斐波那契数来选择切分点mi,现假设向量长度n = fib(k) - 1。
那么fibSearch(e, 0, n)就可以以mi = fib(k - 1) - 1作为左右子向量的切分点:
左长度:fib(k - 1) - 1;
右长度:(fib(k) - 1) - (fib(k - 1) - 1) - 1
所以无论往那个方向深入,新向量的长度依然是某个斐波那契数减1,因此可以反复迭代,直到命中目标或者长度收缩至0而终止,这就是斐波那契查找。
我们依然以n = fib(6) - 1 = 7为例,成功查找长度和失败查找长度分别为:
{5,4,3,5,2,5,4} 平均为4.00;
{4,5,4,4,5,4,5,4} 平均为4.38;
较之上一个版本确实有所提高!根据递推方程不难证明平均查找长度为O(1.44logn),比之前略有提高。
三、二分查找一个比较好的实现版本
其实我们大可不必使用斐波那契查找这种麻烦的方法,而采用另外一种更直接的方法来平衡二分查找的最好和最坏时间复杂度,就是我们可以直接将三分支改为两分支!我们在每个切分点S[mid]处仅做一次比较,若目标元素小于S[mid]则深入S[lo, mi),否则,深入S(mid,hi),乍一看我们可能会漏掉目标元素从而使本应成功的一次查找以失败告终,然而真的是这样嘛?我们先来看一下代码:
int binSearch(int *arr, const int e, int lo, int hi){ while(lo < hi) { int mid = (lo + hi) >> 1; arr[mid] <= e ? lo = mid + 1 : hi = mid; } return --lo;}
- 每次迭代的判断次数不平衡,有可能只需判断一次,也有可能要判断两次;
- 有多个元素命中时,不能保证返回秩最大(或者最小)者;
- 查找失败时,不能指示失败的位置;
第一个就不用说了,我们将三分支转变成二分支,判断次数非常平衡!接下来我们主要证明后面的两个。
首先,这个版本只有当有效区间宽度缩短至0的时候才会终止,而每一次迭代我们都可以证明这样的一个不变性:
- arr[0, lo)中的元素皆不大于e;arr[hi, n)中的元素皆大于e;
在进入某次循环是,无非两种情况:
- arr[mid] <= e,这时 lo = mid + 1,arr[0, lo)中的元素皆不大于e;
- arr[mid] > e,这时 hi = mid,arr[hi, n)中的元素肯定也都大于e;
- 浅谈二分查找法
- 浅谈二分查找
- 浅谈二分查找
- 浅谈二分查找算法
- 浅谈二分查找
- 浅谈二分查找
- 浅谈二分查找
- 浅谈数据的查找(二分查找)
- 浅谈PHP第三弹---使用二分查找法查找数组中的元素位置
- 【捷哥浅谈PHP】第三弹---使用二分查找法查找数组中的元素位置
- 二分查找
- 二分查找
- 二分查找
- 二分查找
- 二分查找
- 二分查找
- 二分查找
- 二分查找
- python的学习 二维码的使用
- [牛客网]数组中的逆序对
- SSH HTTP HTTPS
- 日本传统色彩大全
- system()路径中含有 空格与 冒号及斜杠 ( \\与 “”)问题
- 浅谈二分查找
- java初学--读入输出
- Windows 7/8.1 下 双版本Python2.7/Python3.5 安装 GPU版的tensorflow
- RGB颜色查询对照表
- tensorflow深度学习原理
- mysql使用问题录
- Python中for复合语句的使用
- bzoj1715 [Usaco2006 Dec]Wormholes 虫洞 spfa
- HCNA