二分查找很快,因为他每比较一次就能将查找范围减半,所以时间复杂度是logN。很多计算机科学家对二分查找算法进行了改进,这种改进主要是基于代码调优层次的,虽然其时间复杂度任然是logN,但总的比较次数进一步减少。
最简单的二分查找:
int binarySearch_v1(int array[], int low, int high, int value)
{
while(low <= high)
{
int mid = low + (high - low) / 2;
if(array[mid] == value)
{
return mid;
}
else if(array[mid] < value)
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
对于初次接触到二分查找的童靴而言,可能会比较容易地写出上面的二分查找程序,下面我们再看另一个版本的二分查找
改进版本的二分查找:
int binarySearch_v2(int array[], int low, int high, int value)
{
int mid;
while((high - low) > 1)
{
mid = low + (high - low) >> 1;
if(array[mid] <= value)
{
low = mid;
}
else
{
high = mid;
}
}
if(array[low] == value)
{
return low;
}
else
{
return -1;
}
}
第二个版本相比较于第一个版本的改进在于循环中的比较次数减少了一次,在内置类型的数据比较,如整形数,这或许并不能为我们节约多少时间;然而在实际的应用中,数据的比较往往都是很复杂的数据结构对象之间的比较,所以第二个算法的优势是很明显的。
第二个二分查找的不变式是:array[low] <= value && array[high] > value,在保证程序的输入满足该不变式的情况下,我们可以通过算法断言来证明,每一个循环都满足该不变式,当然退出循环时也满足。
注意:为保持不变式,输入整个数组,输入的high值应该指向数组最后一个元素的后一个位置。否则,若需要查找的元素刚好是数组的最后一个元素的话,则会查找失败。因为初始的输入条件不满足不变式。
二分查找的应用:
查找两个长度相等的已排序数组的中位数,如:
A:1,2,3 B:4,5,6 A与B的中位数是3
A:1,6,7 B:2,3,4 A与B的中位数是3
对于查找类的问题,如果最朴素的方式能够在O(N)的时间复杂度内求出问题解的话,那么根据问题本身的数据是否有某种特征(如有序)可以进一步的改进算法,对于有序序列的查找类问题,我们第一反应就应该想到二分查找。
对于这个问题,最朴素的算法就是对A,B两个序列扫描一遍,然后找到中位数,这种算法的时间复杂度为O(N),代码如下:
int midOfTwoArray(int arr1[], int arr2[], int length)
{
int i = 0;
int j = 0;
int pos = 0;
for(;i < length && j < length && pos < length; pos++)
{
if(arr1[i] < arr2[j])
{
i++;
}
else
{
j++;
}
}
return (arr1[i] < arr2[j] ? arr1[i] : arr2[j]);
}
使用二分查找来解决问题:
我们从最简单的问题规模开始考虑,如果A与B各自只有一个元素,那么直接返回A与B中值较小的那个元素;如果A与B的长度大于1该怎么办呢?根据二分查找的特点,不断地减半查找范围,并且要保证我们要查找的元素不在被我们舍弃掉的范围中,直到A与B的长度为1。
两个指导性原则:
1.减半查找范围
2.保证数据在被保留下来的范围中
按照这两个原则来设计算法,首先定位到A,B序列各自的中点A[mid]与B[mid],然后比较值,然后分情况考虑
case1:A[mid] == B[mid];循环结束,返回两个值中的任意一个
case2:A[mid] > B[mid];舍弃掉A的后半段,B的前半段,不舍弃中间点
case3:A[mid] < B[mid];舍弃掉B的后半段,A的前半段,不舍弃中间点
同样,根据不变式,我们可以将case1融入到case2中,或者融入到case3中,使得循环中的比较次数只有一次
case1,case2,case3的证明比较简单,这里不再叙述,不清楚的读者可以自己列举两个数组来观察并验证。
int midOfTwoArray_v3(int arr1[], int arr2[], int length)
{
int mid;
while(length > 1)
{
mid = (length - 1) >> 1;
if(arr1[mid] <= arr2[mid])
{
arr1 += length >> 1;
}
else
{
arr2 += length >> 1;
}
length = mid + 1;
}
return MIN(arr1[0], arr2[0]);
}
查找两个长度不一定相等的已排序序列的中位数,如:
A:1,2,5 B:3,4 中位数是3
A:1,3,4 B:2,5 中位数是3
要解决这个问题同样可以用最朴素的方法在O(N)的时间复杂度里得出结果,我们直接考虑如何用二分查找来高效地解决这个问题。
还是先考虑最简单的问题,一个序列的长度为1,另一个序列的长度为1,返回较小者。当一个序列的长度为1,另一个序列的长度大于1时,又该如何?设序列A的长度为1,序列B的长度大于1,对序列B进行二分查找,确定A中的那个元素在B中的什么位置;如果B中小于等于A中元素的元素个数为lengthB / 2,则返回A中那个元素;如果B中小于等于A中元素的元素个数大于lengthB / 2,则返回B[lengthB / 2];如果B中小于等于A中元素的个数小于lengthB / 2,则返回B中B[lengthB / 2 - 1]那个元素。
上面考虑的两种情况是属于最简单的情况,下面考虑一般的情况,当两个序列A与B的长度都大于1的情况。这个时候的解决办法与长度相等的时候的解决办法相同,只是化为最简单状态时,有可能是A与B序列长度均为1,也有可能只有一个长度为1。这取决于A与B的初始长度是否相等,先上代码:
15 class Solution
16 {
17 public:
18 int findMedianSortedArrays(int IA[], int m, int IB[], int n)
19 {
20 int LA = m;
21 int LB = n;
22 int *A = IA;
23 int *B = IB;
24 while(LA > 1 && LB > 1)
25 {
26 int MA = (LA - 1) >> 1;
27 int MB = (LB - 1) >> 1;
28 if(A[MA] <= B[MB])
29 {
30 A += (LA >> 1);
31 }
32 else
33 {
34 B += (LB >> 1);
35 }
36 LA = MA + 1;
37 LB = MB + 1;
38 }
39 if(LA == LB)
40 {
41 return (A[0] < B[0] ? A[0] : B[0]);
42 }
43 int prefixLength = (A - IA) + (B - IB);
44 if(LA == 1)
45 {
46 int mid = (m + n - 1) >> 1;
47 int index = mid - prefixLength;
48 int pos = BinarySearch(B, LB, A[0]);
49 if(index < pos)
50 {
51 return B[index];
52 }
53 else if(index == pos)
54 {
55 return A[0];
56 }
57 else
58 {
59 return B[index - 1];
60 }
61
62 }
63 else if(LB == 1)
64 {
65 int mid = (m + n - 1) >> 1;
66 int index = mid - prefixLength;
67 int pos = BinarySearch(A, LA, B[0]);
68 if(index < pos)
69 {
70 return A[index];
71 }
72 else if(index == pos)
73 {
74 return B[0];
75 }
76 else
77 {
78 return A[index - 1];
79 }
80 }
81 }
82 /*Loop invariant£º
83 * A[low] <= val && A[high] > val
84 * input also should feet this condition
85 * */
86 int BinarySearch(int A[], int n, int val)
87 {
88 int low = 0;
89 int high = n - 1;
90 while((high - low) > 1)
91 {
92 int mid = low + ((high - low) >> 1);
93 if(A[mid] <= val)
94 {
95 low = mid;
96 }
97 else
98 {
99 high = mid;
100 }
101 }
102 return high;
103 }
104 };
简单的讲一下代码的实现原理,第24-38行对应我们前面所说的一般情况,即A与B的长度都大于1。
当循环退出时(或者没进入循环)我们可以确定,此时已经化为最简单情况,至少一个序列的长度为1;
如39-42行所述,若两个序列长度都为1,则返回较小值。
其他的代码是对应求解只有一个序列的长度为1的情况。如44-62对应的是A中的元素在B中的位置,因为我们此时要确定A中仅剩的元素在最初的A与B的合并序列(逻辑)中的位置,所以要求出43行的prefixLength变量,他表示的是已知的小于等于A中仅剩的一个元素的元素个数,并且也是已知的小于B中剩余的最小元素的元素个数;所以我们孩的求出A中仅剩的哪个元素在B中仅剩的若干个元素中的哪个位置;再通过与prefixLength相加,从而确定在整个A与B的合并序列中的位置。然后用前面讲过的对应于一个序列长度为1,一个序列长度大于1时的解决方案。
直接上个例子吧:
初始序列:A:1 2 3 4 7; B:5 6
出了24-38的循环后:A:3 4 7 B:5
求出prefixLength为2,由求得B中元素在A中的位置(A中小于等于B的元素个数)为2,则B中仅剩的元素在IA,IB的合并序列中的位置为4,又由于4>(lengthA + lengthB - 1) / 2,所以取出IB中的IB[(lengthA + lengthB - 1) / 2]作为返回值。