题目描述程序员
给定一个有序的数组,查找某个数是否在数组中,请编程实现。算法
分析与解法编程
一看到数组自己已经有序,我想你可能反应出了要用二分查找,毕竟二分查找的适用条件就是有序的。那什么是二分查找呢?小程序
二分查找能够解决(预排序数组的查找)问题:只要数组中包含T(即要查找的值),那么经过不断缩小包含T的范围,最终就能够找到它。其算法流程以下:数组
一开始,范围覆盖整个数组。
将数组的中间项与T进行比较,若是T比数组的中间项要小,则到数组的前半部分继续查找,反之,则到数组的后半部分继续查找。
如此,每次查找能够排除一半元素,范围缩小一半。就这样反复比较,反复缩小范围,最终就会在数组中找到T,或者肯定原觉得T所在的范围实际为空。
对于包含N个元素的表,整个查找过程大约要通过log(2)N次比较。编辑器
此时,可能有很多读者内心嘀咕,不就二分查找么,太简单了。ide
然《编程珠玑》的做者Jon Bentley曾在贝尔实验室作过一个实验,即给一些专业的程序员几个小时的时间,用任何一种语言编写二分查找程序(写出高级伪代码也能够),结果参与编写的一百多人中:90%的程序员写的程序中有bug(我并不认为没有bug的代码就正确)。设计
也就是说:在足够的时间内,只有大约10%的专业程序员能够把这个小程序写对。但写不对这个小程序的还不止这些人:并且高德纳在《计算机程序设计的艺术 第3卷 排序和查找》第6.2.1节的“历史与参考文献”部分指出,虽然早在1946年就有人将二分查找的方法公诸于世,但直到1962年才有人写出没有bug的二分查找程序。code
你能正确无误的写出二分查找代码么?不妨一试,关闭全部网页,窗口,打开记事本,或者编辑器,或者直接在本文评论下,不参考上面我写的或其余任何人的程序,给本身十分钟到N个小时不等的时间,当即编写一个二分查找程序。排序
二分查找法主要是解决在“一堆数中找出指定的数”这类问题。
而想要应用二分查找法,这“一堆数”必须有一下特征:(1)存储在数组中 (2) 有序排列
因此若是是用链表存储的,就没法在其上应用二分查找法了。
至因而顺序递增排列仍是递减排列,数组中是否存在相同的元素都没关系。不过通常状况,咱们仍是但愿并假设数组是递增排列,数组中的元素互不相同。
二分查找法在算法家族大类中属于“分治法”,分治法基本均可以用递归来实现的,二分查找法的递归JS实现以下:
function bsearch(array,low,high,target) { if (low > high) return -1; var mid = Math.floor((low + high)/2); if (array[mid]> target){ return bsearch(array, low, mid -1, target); } else if (array[mid]< target){ return bsearch(array, mid+1, high, target); }ese{return mid;} }
不过全部的递归均可以自行定义stack来解递归,因此二分查找法也能够不用递归实现,并且它的非递归实现甚至能够不用栈,由于二分的递归实际上是尾递归,它不关心递归前的全部信息。
function bsearchWithoutRecursion(array,low,high,target) { while(low <= high) { var mid = Math.floor((low + high)/2); if (array[mid] > target){ high = mid - 1; }else if (array[mid] < target){ low = mid + 1; }else{ return mid; } } return -1; }
以前的都是在数组中找到一个数要与目标相等,若是不存在则返回-1。咱们也能够用二分查找法找寻边界值,也就是说在有序数组中找到“正好大于(小于)目标数”的那个数。
用数学的表述方式就是:
在集合中找到一个大于(小于)目标数t的数x,使得集合中的任意数要么大于(小于)等于x,要么小于(大于)等于t。
举例来讲:
给予数组和目标数
var array = {2, 3, 5, 7, 11, 13, 17};
var target = 7;
那么上界值应该是11,由于它“刚恰好”大于7;下届值则是5,由于它“刚恰好”小于7。
用二分查找法找寻上界
function BSearchUpperBound(array,low,high,target) { if(low > high || target >= array[high]) return -1; var mid = (low + high) / 2; while (high > low) { if (array[mid] > target){ high = mid; } else{ low = mid + 1; } mid = (low + high) / 2; } return mid; }
与精确查找不一样之处在于,精确查找分红三类:大于,小于,等于(目标数)。而界限查找则分红了两类:大于和不大于。
若是当前找到的数大于目标数时,它可能就是咱们要找的数,因此须要保留这个索引,也所以if (array[mid] > target)时 high=mid; 而没有减1。
用二分查找法找寻下界
function BSearchLowerBound(array,low,high,target) { if(high < low || target <= array[low]) return -1; var mid = (low + high + 1) / 2; //make mid lean to large side while (low < high) { if (array[mid] < target){ low = mid; }else{ high = mid - 1; } mid = (low + high + 1) / 2; } return mid; }
下届寻找基本与上届相同,须要注意的是在取中间索引时,使用了向上取整。若同以前同样使用向下取整,那么当low == high-1,而array[low] 又小于 target时就会造成死循环。由于low没法往上爬超过high。
这两个实现都是找严格界限,也就是要大于或者小于。若是要找松散界限,也就是找到大于等于或者小于等于的值(即包含自身),只要对代码稍做修改就行了:
去掉判断数组边界的等号:
target >= array[high]改成 target > array[high]
在与中间值的比较中加上等号:
array[mid] > target改成array[mid] >= target
用二分查找法找寻区域
以前咱们使用二分查找法时,都是基于数组中的元素各不相同。假如存在重复数据,而数组依然有序,那么咱们仍是能够用二分查找法判别目标数是否存在。不过,返回的index就只能是随机的重复数据中的某一个。
此时,咱们会但愿知道有多少个目标数存在。或者说咱们但愿数组的区域。
结合前面的界限查找,咱们只要找到目标数的严格上届和严格下届,那么界限之间(不包括界限)的数据就是目标数的区域了。
//return type: pair<int, int> //the fisrt value indicate the begining of range, //the second value indicate the end of range. //If target is not find, (-1,-1) will be returned pair<int, int> SearchRange(int A[], int n, int target) { pair<int, int> r(-1, -1); if (n <= 0) return r; int lower = BSearchLowerBound(A, 0, n-1, target); lower = lower + 1; //move to next element if(A[lower] == target) r.first = lower; else //target is not in the array return r; int upper = BSearchUpperBound(A, 0, n-1, target); upper = upper < 0? (n-1):(upper - 1); //move to previous element //since in previous search we had check whether the target is //in the array or not, we do not need to check it here again r.second = upper; return r; }
它的时间复杂度是两次二分查找所用时间的和,也就是O(log n) + O(log n),最后仍是O(log n)。