一、简要介绍二分查找算法

二分查找算法(Binary Search)是一种非常经典且有用的查找算法,它适用于有序数组的元素查找,并且思路简单,能够用非常简洁的代码来将其实现,经典版本的二分查找算法最优情况下的时间复杂度为O(1),最坏情况下的复杂度为O(lgN),平均时间复杂度也仅为O(lgN)。

在时间复杂度上更加优秀的算法不是太多,如hash table通过空间换时间的得到O(1)的时间复杂度,以及在二分法思想上进行改进的插值查找算法得到的O(lg(lgN))的时间复杂度(对数据的分布有一定的要求,不够稳定)。

二、二分查找算法的实现

2.1二分法的算法思想

1)若数组不为空(beg < end),获取位于数组中间的元素(mid),如果中间元素正好是要查找的元素,则搜索过程结束,否则进入下一步;

2)判断关键值是否大于还是小于中间元素,以中间元素为界,若关键值小于vec[mid],则进入前半段[beg,mid),否则进入后半段[mid+1, end)。

3)若数组为空,则数组中不存在等于该关键字的元素,返回-1,意味着不存在这样的下标。

 2.2版本A

版本A时二分法算法思想的直接体现。在实现时,需要注意避免溢出的风险

int mid = (beg + end) >> 1//规避溢出风险并且理论上移位比除法快一些(写/2也没关系,编译器会帮你做出优化的)
#include<vector>
using namespace std;
int BinarySearchA(vector<int> vec, int beg, int end, int val) {int ret = -1;//如果查找不到所需要的值后,返回-1while (beg < end) {//规避溢出的风险, int mid = (beg + end) >> 1,在 beg + end的时候可能溢出。int mid = beg + (end - beg) >> 1;if (val < vec[mid])end = mid;else if (vec[mid] < val)beg = mid + 1;else {ret = mid;break;}}return ret;
}

这个 版本在最好的情况时间复杂度为O(1), 即第一次就命中要查找的元素。但是为了得到O(1)这种结果,我们的循环中有两个判断语句。让我们看一下在平均状况下会发生些什么。

1)当val < vec[mid],我们只需进行一次判断      2)当vec[mid] <= val时 我们需要进行两次判断。

也就是说但val在左半区间和在右半区间的所计算的次数是不一样的,这会让平均情况下的复杂度变坏。

假设val在左半区间和右半区间的概率是相同的,那么我们 估算下我们平均情况下的时间复杂度,则为 1/2 * lgn + 1/2 * 2 * lgn = 1.5lgn(这是由最坏情况估算出来的,所以实际系数可能要稍微再小一些 但是仍>1)。

如果你觉得我们的运气没有足够好,以至于不能够经常一次命中的话。我们不妨牺牲掉最好的情况(不再有提前命中了),从而降改进下lgn的系数。从而可以得到版本B。

2.3版本B

因为循环的结束条件只剩下了一个 (end - beg) > 1,该版本已经无法因为提前命中而退出了,但是向左折半和向右折半的代价却都变成1次判断了。

版本的改变关键在于对不变式的设计:(val < vec[mid]) ? end = mid : beg = mid;

下边我们就来论证每一次迭代[beg, end)的长度均会缩小。mid = beg + (end - beg)/2 或是 mid = end - (end-beg)/2, 所以当循环条件(end - beg) > 1为真时,beg < mid < end. 所以(end - mid) 和 (mid - beg)的值总是小于(end - beg)的。所以论证结束。所以循环迟早会结束。循环结束以后beg是不小于val的元素中秩最高的元素的秩,如果它不等于val,则没有元素等于val了,此时返回-1,若等于则返回beg。

int BinarySearchB(vector<int> vec, int beg, int end, int val) {int ret = -1;//如果查找不到所需要的值后,返回-1while (1 < end - beg) {//规避溢出的风险, int mid = (beg + end) >> 1,在 beg + end的时候可能溢出。int mid = beg + (end - beg) >> 1;(val < vec[mid]) ? end = mid : beg = mid;}if (val == vec[beg])ret = beg;return ret;
}

2.3版本C

为了使BinarySearch能够作为其他数据结构的基本模块,我们需要对它进行语义的约定。让它返回的是有序数组中不小于等于val的最后一个元素的秩。与版本B相比,1.循环为真的条件改变了。2.不变式中的beg = mid 改为了 beg = mid + 1。

可能有人会问 版本B返回的不也是小于且等于e的最后一个元素的秩吗? 那么版本C的优势在哪里。

我们可以看一下以下这两种情况,当val小于或大于初始区间的所有元素,版本B(这里版本B不再返回-1,直接返回beg)

对于前者返回0,对于后者返回end - 1。版本C则返回的是 -1 和 end -1。

现在我们将二分搜索法用于元素的插入中去:vec.insert(BinarySearch(vec, 0, vec.size()) + 1, val)。

此时若val足够小,则我们插入的位置是 -1+1 = 0。 若val足够大,则插入的位置为 end-1 + 1 = end,都是正确的位置。    对于版本B的插入位置,前者为1, 后者为end。前者的插入位置不正确,需要额外进行处理。所以版本C更符和语义约定的要求,可以更好的做为其他结构或算法的基本模块而被使用。

int BinarySearchC(vector<int> vec, int beg, int end, int val) {while (beg < end) {//规避溢出的风险, int mid = (beg + end) >> 1,在 beg + end的时候可能溢出。int mid = beg + (end - beg) >> 1;(val < vec[mid]) ? end = mid : beg = mid + 1;}return --beg;
}

三、改进算法- Fibonacci 搜索

二分查找将数组从中间折半,是一种非常自然的想法,但是通过数学的推导,借助Fibonacci数列来为数组进行分割是一种更好的分割方式,能够略微降低logn的常系数。不过既然Fibonacci数列不是一种自然的想法,它的实现比起二分查找可能就略显复杂:你得计算Fibonacci数列,以及需要额外的循环找到合适的Fibonacci数,在部分实现中还需要对数组进行扩增。

理想的Fibonacci Search算法对应的数组长度应该真好是某个Fib(n),分割点则为beg + Fib(n-1),网上有许多五花八门的实现大家感兴趣的可以自己参考。

//参考学堂在线邓俊辉老师的《数据结构(上)》的代码
template<typename T>
int fibSearch(vector<T> *v, const T &e, int beg, int end) {//Fib类的细节忽略Fib fib(end - beg);while (beg < end) {//找到刚好小于end - beg的Fibnacci数(额外引入的循环)while ((end - beg) < fib.get()) fib.prev();int mid = beg + fib.get();if (e < v[mid]) end = mid;else if (v[mid] < e) beg = mid + 1;elsereturn mid;}return -1;
}

既然实现Fibnacci Search用到了相对复杂的代码,这些代码中的操作又增加了程序运行的时间成本,那么Fibnacci Search在理论上的常系数的优化真的能够在实际意义上更快吗? 这是有可能的,

原因主要有以下两点:

一、当关键字的比较是一些复杂的类型(本代码用模板函数实现就是为了强调这一点)时,复杂类型的比较操作将耗费更多的时间,这时更少的关键字比较次数所节约的时间就可能超过那些额外的操作带来的时间成本(这些操作无非是int类型的运算或比较)。

二、当数组长度很大时,内存中一下不能读取那么多元素,采用折半查找两次元素读取下标的间距相对于Fibonacci分割的间距而言更大,更不易命中,当不命中现象发生时,需要从硬盘中读取数据到内存,这也是非常耗时的,所以Fibonacci Search在这点上较有优势。

但是一般情况下,还是更推荐使用二分搜索。

二分查找法和Fibonacci查找相关推荐

  1. 十九、顺序查找法和折半查找法

    顺序查找法 概述:从表的一端开始,逐个进行记录的关键字和给定值的比较,若找到一个记录的关键字与给定值相等,则查找成功:若整个表中的记录均比较过,仍未找到关键字等于给定值的记录,则查找失败.顺序查找的方 ...

  2. 详解【C语言】中的二分查找法和折半查找法(例题解答)

    目录 问题 思路 详解 代码 问题 在一个有序数组中查找具体的某个数字n 比如我买了一双鞋,你好奇问我多少钱,我说不超过300元.你还是好奇,你想知道到底多少,我就让你猜,你会怎么猜? 答案:你每次猜 ...

  3. 顺序查找法和二分查找法

    顺序查找法 方法1 def shunxu(f_list,temp):for index,i in enumerate(f_list):if i==temp:return indexreturn Non ...

  4. 数据结构三大查找算法(二分查找、插值查找、斐波那契数列查找)C语言实现

    文章目录 查找 二分查找(折半查找) 插值查找 斐波拉契查找 总结: 查找 查找是在大量的信息里面寻找一个特定的信息元素 (1)静态查找和动态查找: 静态或者动态都是针对查找表而言的.动态表指查找表中 ...

  5. C++实现查找 - 顺序、二分和哈希查找

    数据结构与算法专栏 -- C++实现 写在前面: 前面我们其实已经涉及到了查找算法,比如二叉排序树和平衡二叉树等.这一讲我们来补充一下其它常见的查找算法,下面我会依次讲解并实现顺序查找.二分查找和哈希 ...

  6. 数据结构与算法(8-2)有序表查找(折半查找(二分查找)、插值查找)

    目录 一.折半查找(二分查找) 二.插值查找 总代码 一.折半查找(二分查找) 原理:一次次折半,不断向着查找值的位置靠近 . 适用场景:有序(必须) 流程:开始时,min标志首,max标志尾,med ...

  7. 数据结构之二分查找(折半查找)

    数据结构之二分查找(折半查找) 二分查找又称折半查找,优点是次数比较少,查找速度快,平均性能好,其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.首先 ...

  8. python实现二分查找(折半查找)算法

    python实现二分查找算法 二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法.但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列. 查找又称折半 ...

  9. 查找算法:二分查找、顺序查找

    08年9月入学,12年7月毕业,结束了我在软件学院愉快丰富的大学生活.此系列是对四年专业课程学习的回顾,索引参见:http://blog.csdn.net/xiaowei_cqu/article/de ...

最新文章

  1. Mac m1 ocr 工具
  2. AI技术已达如此高度:去码、上色6到飞起
  3. C++ share_prt 简单设计和实现
  4. Java:使用Fork / Join框架的Mergesort
  5. sw工程图导出bom_SolidWorks材料明细表自动调用钣金展开尺寸,轻松导出BOM表
  6. KDD Cup 2020多模态召回比赛亚军方案与搜索推荐业务的业务应用
  7. 微软、苹果、谷歌、三星……这些区块链中的科技巨头原来已经做了这么多事!...
  8. CISCO 路由器的E1模块配置指南
  9. VS开发中,常见编译问题解决方案
  10. 工地人员定位管理系统,如何有效做到安全生产双预防?
  11. 虚拟机Linux共享主机Windows文件夹
  12. Mybatis---简单缓存了解
  13. python中assert的用法(简洁明了)
  14. 金蝶K3 SQL报表系列-委外未勾稽明细表
  15. 解决火狐浏览器提示连接不安全或证书错误的问题
  16. mt6355功率设计注意事项 [仅为mt 6758资料汇总]
  17. dell 2950 LED报W1228 ROMB Batt 24hr 警告处理
  18. 手游SDK是什么意思,可以解决哪些问题?
  19. Alientek I.MX6UL Linux-第九章 I.MX6U的启动方式
  20. 阿里云TSDB时空数据库实战(一):数据入库与导出

热门文章

  1. sql server 创建动态交叉表
  2. Poj 1655 【树的重心】
  3. bootstrap-fileinput踩坑-选择文件类型
  4. whm修改tmp目录空间大小为4096M
  5. 抖音很火的动态表白源码
  6. excel快捷键附录笔记
  7. iOS开发者证书介绍与总结
  8. 无心剑中译伊丽莎白·毕肖普《一门技艺》
  9. 华三交换机如何进入配置_H3C交换机恢复出厂和各种基本配置方法
  10. Office2010 把多个excel合并成一个