二分查找
首选我们介绍标题中提到的两个名词:
二分查找
二分查找的搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则查找成功;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
循环不变量
其主要用来帮助理解算法的正确性。形式上很类似与数学归纳法,它是一个需要保证正确断言。对于循环不变式,必须证明它的三个性质:
- 初始化:它在循环的第一轮迭代开始之前,应该是正确的。
- 保持:如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。
- 终止:循环能够终止,并且可以得到期望的结果,不变式给了我们一个有用的性质,那就是获得正确的结果。
那这两个东西又有什么关联呢?上面已经提到循环不变量可以证明算法的正确性!这样的话,我们下面就运用循环不变量证明一下二分查找各种应用算法的正确性。
-
给定一个有序(升序,我们后面所有提到的数据均为升序,后面不再赘述)数组A,求任意一个i使得A[i]等于target,不存在则返回-1
这是最原始的二分查找,利用数组有序性进行折半查找,时间复杂度为O(nlogn)。设原始数据为A[0 ~ n-1]- 初始化:low = 0, high = n - 1,mid = low + (high - low)/2。如果targer存在原始数组中,其对应index一定处于[low, high], 即 A[low] <= target <= A[high]
- 保持: 对于 A[mid] <target,则index只能存在[mid + 1, high]中,且low = mid + 1;对于 A[mid] > target, 则index只能存在[low, mid - 1], 且high = mid;对于A[mid] = target,因为 low <= mid <= high, index = mid,直接返回。
- 终止:low >high(即high + 1)。待处理数组为空,表示tartget不存在此数组中。
int search(int A[], int n, int target) { int low = 0, high = n-1; while(low <= high) { // 注意:若使用(low+high)/2求中间位置容易溢出 int mid = low+((high-low)>>1); if(A[mid] == target) return mid; else if(A[mid] < target) low = mid+1; else // A[mid] > target high = mid-1; // 循环不变式: A[low - 1] < target < A[high + 1] } return -1; }
后面的案例,也都可以按照上述方法证明算法的正确性,但我们不一一赘述,但在描述算法的时候,我们会指出,循环过程中不变的性质(虽然这个性质可能在初始化的时候不一定成立),这对我们理解算法为何这么写有很大帮助!
-
给定一个有序数组A,可含有重复元素,求最小的i使得A[i]等于target,不存在则返回-1
// 两种写法 int lowerBound(int A[], int n, int target) { int low = 0, right = n - 1; while(low <= high) { /* 为什么上面一定是 low <= high? low < high 行不行? */ int mid = low + (high - low)/2; if(A[mid] < target) low = mid + 1; else high = mid - 1; } /* 循环过程中,当low大于0时,因为A[mid] < target时,low=mid+1, 所以A[low-1] < target; 当high小于n-1时,因为A[mid] >= target时,high = mid - 1, A[high + 1 >= target; 循环结束时,low 等于 high + 1,所以,如果A[low](A[high + 1])存在就等于target, 那么low(high)就是target出现的最小位置,否则target在数组中不存在。 综上所示,循环不变式为: A[low - 1] < target <= A[high + 1] */ if(low < n && A[low] == target) return low; return -1; } int searchFirstPos(int A[], int n, int target) { if(n <= 0) return -1; int low = 0, high = n-1; while(low < high) { int mid = low+((high-low)>>1); if(A[mid] < target) low = mid+1; else // A[mid] >= target high = mid; } /* 循环过程中,当low大于0时,因为A[mid] < target时,low=mid+1, 所以A[low-1] < target; 当high小于等于n-1时,因为A[mid] >= target时,high = mid, A[high] >= target; 循环结束时,low 等于 high,所以,如果A[low](A[high])就等于target, 那么low(high)就是target出现的最小位置,否则target在数组中不存在。 综上所示,循环不变式为: A[low - 1] < target <= A[high] */ if(A[low] != target) return -1; else