这个系列是我多年前找工做时对数据结构和算法总结,其中有基础部分,也有各大公司的经典的面试题,最先发布在CSDN。现整理为一个系列给须要的朋友参考,若有错误,欢迎指正。本系列完整代码地址在 这里。git
二分查找自己是个简单的算法,可是正是由于其简单,更容易写错。甚至于在二分查找算法刚出现的时候,也是存在bug的(溢出的bug),这个bug直到几十年后才修复(见《编程珠玑》)。本文打算对二分查找算法进行总结,并对由二分查找引伸出来的问题进行分析和汇总。如有错误,请指正。本文完整代码在 这里 。github
相信你们都知道二分查找的基本算法,以下所示,这就是二分查找算法代码:面试
/**
* 基本二分查找算法
*/
int binarySearch(int a[], int n, int t)
{
int l = 0, u = n - 1;
while (l <= u) {
int m = l + (u - l) / 2; // 同(l+u)/ 2,这里是为了防溢出
if (t > a[m])
l = m + 1;
else if (t < a[m])
u = m - 1;
else
return m;
}
return -(l+1);
}
复制代码
算法的思想就是:从数组中间开始,每次排除一半的数据,时间复杂度为O(lgN)
。这依赖于数组有序这个性质。若是t存在数组中,则返回t在数组的位置;不然,不存在则返回-(l+1)
。算法
这里须要解释下为何t不存在数组中时不是返回-1
而要返回-(l+1)
。首先咱们能够观察 l
的值,若是查找不成功,则 l
的值刚好是 t 应该在数组中插入的位置。编程
举个例子,假定有序数组a={1, 3, 4, 7, 8}
, 那么若是t=0
,则显然t不在数组中,则二分查找算法最终会使得l=0 > u=-1
退出循环;若是t=9,则t也不在数组中,则最后l=5 > u=4
退出循环。若是t=5
,则最后l=3 > u=2
退出循环。所以在一些算法中,好比DHT(一致性哈希)中,就须要这个返回值来使得新加入的节点能够插入到合适的位置中,在求最长递增子序列的NlgN算法中,也用到了这一点,参见博文最长递增子序列算法。数组
还有一个小点就是之因此返回-(l+1)
而不是直接返回 -l
是由于 l
可能为0,若是直接返回 -l
就没法判断是正常返回位置0仍是查找不成功返回的0。bash
如今考虑一个稍微复杂点的问题,若是有序数组中有重复数字,好比数组a={1, 2, 3, 3, 5, 7, 8}
,须要在其中找出3第一次出现的位置。这里3第一次出现位置为2。这个问题在《编程珠玑》第九章有很好的分析,这里就直接用了。算法的精髓在于循环不变式的巧妙设计,代码以下:数据结构
/**
* 二分查找第一次出现位置
*/
int binarySearchFirst(int a[], int n, int t)
{
int l = -1, u = n;
while (l + 1 != u) {
/*循环不变式a[l]<t<=a[u] && l<u*/
int m = l + (u - l) / 2; //同(l+u)/ 2
if (t > a[m])
l = m;
else
u = m;
}
/*assert: l+1=u && a[l]<t<=a[u]*/
int p = u;
if (p>=n || a[p]!=t)
p = -1;
return p;
}
复制代码
算法分析:设定两个不存在的元素a[-1]和a[n],使得a[-1] < t <= a[n]
,可是咱们并不会去访问这两个元素,由于(l+u)/2 > l=-1
, (l+u)/2 < u=n
。循环不变式为l<u && t>a[l] && t<=a[u]
。循环退出时必然有l+1=u
, 并且a[l] < t <= a[u]
。循环退出后u的值为t可能出现的位置,其范围为[0, n]
,若是t在数组中,则第一个出现的位置p=u
,若是不在,则设置p=-1
返回。该算法的效率虽然解决了更为复杂的问题,可是其效率比初始版本的二分查找还要高,由于它在每次循环中只须要比较一次,前一程序则一般须要比较两次。数据结构和算法
举个例子:对于数组a={1, 2, 3, 3, 5, 7, 8}
,咱们若是查找t=3
,则能够获得p=u=2
,若是查找t=4,a[3]<t<=a[4]
, 因此p=u=4
,判断a[4] != t
,因此设置p=-1
。 一种例外状况是u>=n
, 好比t=9
,则u=7
,此时也是设置p=-1
.特别注意的是,l=-1,u=n
这两个值不能写成l=0,u=n-1
。虽然这两个值不会访问到,可是若是改为后面的那样,就会致使二分查找失败,那样就访问不到第一个数字。如在a={1,2,3,4,5}
中查找1,若是初始设置l=0,u=n-1
,则会致使查找失败。ui
扩展 若是要查找数字在数组中最后出现的位置呢?其实这跟上述算法是相似的,稍微改一下上面的算法就能够了,代码以下:
/**
* 二分查找最后一次出现位置
*/
int binarySearchLast(int a[], int n, int t)
{
int l = -1, u = n;
while (l + 1 != u) {
/*循环不变式, a[l] <= t < a[u]*/
int m = l + (u - l) / 2;
if (t >= a[m])
l = m;
else
u = m;
}
/*assert: l+1 = u && a[l] <= t < a[u]*/
int p = l;
if (p<=-1 || a[p]!=t)
p = -1;
return p;
}
复制代码
固然还有一种方法能够将查询数字第一次出现和最后一次出现的代码写在一个程序中,只须要对原始的二分查找稍微修改便可,代码以下:
/**
* 二分查找第一次和最后一次出现位置
*/
int binarySearchFirstAndLast(int a[], int n, int t, int firstFlag)
{
int l = 0;
int u = n - 1;
while(l <= u) {
int m = l + (u - l) / 2;
if(a[m] == t) { //找到了,判断是第一次出现仍是最后一次出现
if(firstFlag) { //查询第一次出现的位置
if(m != 0 && a[m-1] != t)
return m;
else if(m == 0)
return 0;
else
u = m - 1;
} else { //查询最后一次出现的位置
if(m != n-1 && a[m+1] != t)
return m;
else if(m == n-1)
return n-1;
else
l = m + 1;
}
}
else if(a[m] < t)
l = m + 1;
else
u = m - 1;
}
return -1;
}
复制代码
把一个有序数组最开始的若干个元素搬到数组的末尾,咱们称之为数组的旋转。例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转。如今给出旋转后的数组和一个数,旋转了多少位不知道,要求给出一个算法,算出给出的数在该数组中的下标,若是没有找到这个数,则返回-1。要求查找次数不能超过n。
由题目能够知道,旋转后的数组虽然总体无序了,可是其先后两部分是部分有序的。由此仍是可使用二分查找来解决该问题的。
首先肯定数组分割点,也就是说分割点两边的数组都有序。好比例子中的数组以位置2分割,前面部分{3,4,5}有序,后半部分{1,2}有序。而后对这两部分分别使用二分查找便可。代码以下:
/**
* 旋转数组查找-两次二分查找
*/
int binarySearchRotateTwice(int a[], int n, int t)
{
int p = findRotatePosition(a, n); //找到旋转位置
if (p == -1)
return binarySearchFirst(a, n, t); //若是原数组有序,则直接二分查找便可
int left = binarySearchFirst(a, p+1, t); //查找左半部分
if (left != -1)
return left; //左半部分找到,则直接返回
int right = binarySearchFirst(a+p+1, n-p-1, t); //左半部分没有找到,则查找右半部分
if (right == -1)
return -1;
return right+p+1; //返回位置,注意要加上p+1
}
/**
* 查找旋转位置
*/
int findRotatePosition(int a[], int n)
{
int i;
for (i = 0; i < n-1; i++) {
if (a[i+1] < a[i])
return i;
}
return -1;
}
复制代码
二分查找算法有两个关键点:1)数组有序;2)根据当前区间的中间元素与t的大小关系,肯定下次二分查找在前半段区间仍是后半段区间进行。
仔细分析该问题,能够发现,每次根据 l
和 u
求出 m
后,m
左边([l, m]
)和右边([m, u]
)至少一个是有序的。a[m]分别与a[l]和a[u]比较,肯定哪一段是有序的。
t<a[m] && t>a[l]
, 则 u=m-1
;其余状况,l =m+1
;t> a[m] && t<a[u]
则 l=m+1
;其余状况,u =m-1
; 代码以下:/**
* 旋转数组二分查找-一次二分查找
*/
int binarySearchRotateOnce(int a[], int n, int t)
{
int l = 0, u = n-1;
while (l <= u) {
int m = l + (u-l) / 2;
if (t == a[m])
return m;
if (a[m] >= a[l]) { //数组左半有序
if (t >= a[l] && t < a[m])
u = m - 1;
else
l = m + 1;
} else { //数组右半段有序
if (t > a[m] && t <= a[u])
l = m + 1;
else
u = m - 1;
}
}
return -1;
}
复制代码