二分查找
有序数组无重复元素的查找Key的下标
// 二分查找的迭代版本
/*
注:如果数组索引使用整数求mid时,l+h可能发生溢出*/
int binarySearch(int a[], int n, int key)
{
int l = 0;
int h = n-1;
int mid;
while (l <= h) {
mid = (l + h)/2; // l+h可能发生溢出
if (a[mid] > key) {
h = mid-1;
} else if (a[mid] < key) {
l = mid+1;
} else {
return mid;
}
}
return -1;
}
// 二分查找的递归版本
// 与上面的存在相同的问题
int binarySearch2(int a[], int l, int h, int key)
{
if (l <= h) {
int mid = (l+h)/2;
if (a[mid] > key)
return binarySearch2(a, l, mid-1, key);
else if (a[mid] < key)
return binarySearch2(a, mid+1, h, key);
else
return mid;
}
return -1;
}
// 以上两个版本都有可能在求mid,即l+h计算式发生溢出情况
int binarySearch3(int a[], int n; int key)
{
int h = n;
int l = 0;
int mid;
while (l <= h) {
// 求解mid,使用 l+ (h-l)/2防止以上版本溢出情况
mid = l + (h-l)/2;
if (a[mid] > key)
h = mid - 1;
else if (a[mid] < key)
l = mid + 1;
else
return mid;
}
return -1;
}
二分查找返回key(可能有重复)第一次出现的下标
// 自己优先想到的版本
// 直接在binarySearch3上改动的版本,直觉能直接想到的。没有一通科学的分析,总是缺少点什么。
int binarySearch_fist(int a[], int n; int key)
{
int h = n;
int l = 0;
int mid;
while (l <= h) {
mid = l + (h-l)/2;
if (a[mid] > key)
h = mid - 1;
else if (a[mid] < key)
l = mid + 1;
else {
mid--;
while (mid >= 0) {
if (a[mid] == key)
mid--;
else
break;
}
return mid + 1;
}
}
return -1;
}
其他版本
通过循环不变式理论分析
循环不变式:
如果key存在于数组,那么key第一次出现的下标x一定在[left,right]中,且array[left] < key, array[right] >=key。
初始化:
第一轮循环开始之前,处理的数组是[0,n-1],这时显然成立。
保持:
每次循环开始前,如果key存在于原数组,那么x存在于待处理数组array[left,…right]中。
对于array[mid] < key,array[left,…,right]均小于key,x只可能存在于array[mid+1,…,right]中。数组减少的长度为mid-left+1,至少为1。
否则,array[mid] >= key,array[mid]是array[mid,…,right]中第一个大于等于key的元素,后续的等于key的元素(如果有)不可能对应于下标x,舍去。此时x在[left,…,mid]之后。数组减少的长度为right-(mid+1)+1,即right-mid,根据while的条件,当right==mid时为0。此时right==left,循环结束。
终止:
此时left>=right。在每次循环结束时,left总是x的第一个可能下标,array[right]总是第一个等于key或者大于key的元素。
那么对应于left==right的情况,检查array[left]即可获得key是否存在,若存在则下标为x;
对于left>right的情况,其实是不用考虑的。因为left==上一次循环的mid+1,而mid <= right。若mi+1>right,意味着mid == right,但此时必有left == right,这一轮循环从开始就不可能进入。
int binarySearch_first(int array[], int length, int key)
{
if (!array)
return -1;
int left = 0, right = lenght -1, mid;
while (left < right) {
mid = left + (right - left)/2;
if (array[mid] < key)
left = mid + 1;
else
right = mid;
}
if (array[left] == key)
return left;
return -1;
}
二分查找的思想
利用循环不变式,通过每轮循环都能够缩小(或找到)关键字key的范围,直到找到或循环结束(即查找失败)。
循环不变式
循环不变式主要是用来帮助理解算法的正确性。形式上类似于数学归纳法,它是一个需要保证正确的断言。
循环不变式的三个性质:
初始化:他在循环的第一轮迭代开始之前,应该是正确的。
保持:如果在循环的某一次迭代开始之前它是正确的,那么下一次迭代开始之前,它也应该保持正确。
终止:循环能够终止,并且可以得到期望的结果。
理解
- 利用循环不变式的思想指导二分查找中每一次迭代开始时,必须保证排除了不可能存在key的范围,确定key可能存在的范围[left, right]。
- 确定循环终止的条件,防止死循环的发生。
怎么确定循环终止条件?
举例:
分析二分查找返回key(可能有重复)第一次出现的下标中循环终止条件
若array[mid] < key 时,第一个等于Key的元素可能存在的范围为[mid+1, right],那么原数组减少的长度为 mid - left + 1,即数组减少至少为1(mid=left时)。
若array[mid] >= key 时,可知array[mid]可能是array[mid, right]范围内第一个大于等于key的值,因此第一个等于key值可能存在arry[left, mid]范围,那么原数组减少的长度为right - (mid + 1) +1 = right -mid,当right = mid时,left=right,此时key可能的取值为array[left]。
因此,循环终止条件为left < right。当left=right,循环在array[mid]>=key时,left=right=mid,发生死循环。
循环终止时:left=right
若循环结束前,上一次循环是array[mid]<key,那么循环结束时,left=mid+1=right。若循环结束前,上一次循环是array[mid]>=key,那么循环结束时,left=mid=right。
因此,只需判断array[left]是否等于key即可。