二分查找也称折半查找(Binary Search),是一种效率较高的查找方法。但是,二分查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
查找过程
假设表中的元素是升序排列,取表中间位置,将中间位置的关键字与所要查找的关键字进行比较,如果相等则查找成功。否则将表从中间位置分成1、2两个表,如果中间位置记录的关键字大于查找关键字,则继续查找1表;否则继续查找2表。重复以上过程,若查到则成功,若直到子表不存在,则查找失败。
算法要求
1.必须采用顺序存储结构。
2.必须按关键字大小有序排列。
算法复杂度
二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x.
时间复杂度无非就是while循环的次数!
总共有n个元素,渐渐跟下去就是n,n/2,n/4,....n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数。
由于你n/2^k取整后>=1,即令n/2^k=1,可得k=log2n,(是以2为底,n的对数)。所以时间复杂度可以表示O(h)=O(log2n)
二分查找形式
1. 迭代版
int binary_search(int a[],int target,int n)
{
int low,high,mid;
low=0,high=n;
while(low<=high){
mid=low+(high-low)/2;
if(a[mid]<target)
low=mid+1;
if(a[mid]>=target)
high=mid-1;
}
return low;
}
2. 递归版
int binary_search(int a[],int target,int low,int high)
{
if(low>high)return low;
int mid=low+(high-low)/2;
if(a[mid]<target)
return binary_search(a,target,mid+1,high);
else
return binary_search(a,target,low,mid-1);
}
二分查找的变体
题目1:在一个二维数组中,每一行都按照从左到右的递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解法:
例如下面这个数组
1 2 8 9
2 4 9 12
4 7 10 13
6 8 11 15
我们观察到最小的数字位于左上角,最大的数字位于右下角。我们首先选取右上角的数字,如果该数字等于要查找的数字,查找过程结束;如果该数字大于要查找的数字,剔除这个数字所在的列;如果数字小于要查找的元素,剔除这个数字所在的行。同样我们可以选取左下角的数字。这个方法类似于变种的二分查找法。
下面是代码:
代码1
bool findValue(vector<vector<int>>m, int target) {
int i, j;
for (i = m[0].size()-1; i >= 0; i--) {
for (j = 0; j < m.size(); j++) {
if (m[i][j] > target)
break;
else if (m[i][j] < target)
continue;
else
return true;
}
}
return false;
}
代码2
bool Find(int *matrix, int rows, int columns, int number)
{
int row, column;
bool found = false;
if (matrix != NULL && rows > 0 && columns > 0) {
row = 0, column = columns - 1;
while (row < rows && column >= 0) {
if (matrix[row*columns + column] == number) {
found = true; break;
}
else if (matrix[row*columns + column] > number) column--;
else ++row;
}
}
return found;
}
题目2:把一个数组最开始的若干个元素搬到数组的末尾,称之为数组的旋转。输入一个递增排序的数组的一个旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2} 为 {1,2,3,4,5}的一个旋转,该数组的最小值为1。
解法
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则,第一个元素应该是大于或者等于最后一个元素的(当然这有特例)。
接着我们可以找到数组中间的元素。如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于中间元素的后面。我们可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动之后的第一个指针仍然位于前面递增子数组中。
同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们可以把第二个指针指向该中间元素,这样也可以缩小寻找范围。移动之后的第二个指针仍然位于后面的递增子数组之中。
不管上述的思路,第一个指针总是指向前面递增数组的元素,而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素,而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素。这是循环结束的条件。
int Min(int *numbers, int length)
{
if (numbers == NULL || length <= 0)
throw new std::exception("Invalid parameters");
int index1 = 0;
int index2 = length - 1;
int indexMid = index1;
while (numbers[index1] >= numbers[index2])
{
if (index2 - index1 == 1) {
indexMid = index2;
break;
}
indexMid = (index1 + index2) / 2;
if (numbers[indexMid] >= numbers[index1])
index1 = indexMid;
else if (numbers[indexMid] <= numbers[index2])
index2 = indexMid;
}
return numbers[indexMid];
}
如果数组中有重复数字,即两个指针指向的数字重复,同时它们中间的数字也相同时。前面的方法就不行了。如下面两个数组
1 0 1 1 1 和 1 1 1 0 1
在这两种情况下,第一个指针和第二个指针指向的数字都是1,并且两个指针中间的数字也是1,这三个数字相同。在第一种情况中,中间数字(下标为2)位于后面的子数组;在第二种情况中,中间数字(下标为2)位于前面的子数组中。因此,当两个指针指向的数字及他们中间的数字三者相同的时候,我们无法判断中间的数字是位于前面的子数组还是还是后面的子数组,也就无法移动两个指针来缩小查找范围。此时我们不得不采用顺序查找法。
int MinInOrder(int *numbers, int index1, int index2)
{
int result = numbers[index1];
for (int i = index1 + 1; i <= index2; ++i) {
if (result > numbers[i])
result = numbers[i];
}
return result;
}
int Min(int *numbers, int length)
{
if (numbers == NULL || length <= 0)
throw new std::exception("Invalid parameters");
int index1 = 0;
int index2 = length - 1;
int indexMid = index1;
while (numbers[index1] >= numbers[index2])
{
if (index2 - index1 == 1) {
indexMid = index2;
break;
}
indexMid = (index1 + index2) / 2;
if (numbers[index1] == numbers[index2] && numbers[indexMid] == numbers[index1])
return MinInOrder(numbers,index1,index2);
if (numbers[indexMid] >= numbers[index1])
index1 = indexMid;
else if (numbers[indexMid] <= numbers[index2])
index2 = indexMid;
}
return numbers[indexMid];
}