由一道题目引出的:
题目形貌
给定一个有序的数组,查找某个数是不是在数组中,请编程完成。
剖析与解法
一看到数组本身已有序,我想你能够回响反映出了要用二分查找,毕竟二分查找的实用前提就是有序的。那什么是二分查找呢?
二分查找能够处理(预排序数组的查找)题目:只需数组中包括T(即要查找的值),那末经由历程不停减少包括T的局限,终究就能够找到它。其算法流程以下:
一开始,局限掩盖全部数组。
将数组的中心项与T举行比较,如果T比数组的中心项要小,则到数组的前半部份继承查找,反之,则到数组的后半部份继承查找。
云云,每次查找能够消除一半元素,局限减少一半。就这样反复比较,反复减少局限,终究就会在数组中找到T,或许肯定原以为T地点的局限现实为空。
关于包括N个元素的表,全部查找历程约莫要经由log(2)N次比较。
此时,能够有不少读者内心嘀咕,不就二分查找么,太简朴了。
然《编程珠玑》的作者Jon Bentley曾在贝尔试验室做过一个试验,即给一些专业的递次员几个小时的时候,用任何一种言语编写二分查找递次(写出高等伪代码也能够),效果介入编写的一百多人中:90%的递次员写的递次中有bug(我并不认为没有bug的代码就准确)。
也就是说:在充足的时候内,只要约莫10%的专业递次员能够把这个小递次写对。但写不对这个小递次的还不止这些人:而且高德纳在《计算机递次设计的艺术 第3卷 排序和查找》第6.2.1节的“汗青与参考文献”部份指出,虽然早在1946年就有人将二分查找的要领公诸于世,但直到1962年才有人写出没有bug的二分查找递次。
你能准确无误的写出二分查找代码么?不妨一试,封闭一切网页,窗口,翻开记事本,或许编辑器,或许直接在本文批评下,不参考上面我写的或其他任何人的递次,给本身十分钟到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)。