算法初步--二分法
二分法的经典问题
从一个经典问题开始:给定一个严格递增序列,要求找到当中的某个元素所在的位置。
具体点说:给定序列A[6] = {1, 2, 3, 5, 7, 9},找到 x=2所在的位置。
二分法即是为解决这类有序序列所准备的。基本原理是这样的:
在已知序列中,查找区间为[left, right] = [0, 5],注意下标从0开始。
检查中间元素A[mid]是否为所找元素,式中mid = (left + right)/2 = (0+5)/2 = 2。显然A[mid] = 3>x,则将查找区间更新为[0, 1],即right = mid-1。
再次检查中间元素A[(0+1)/2] = A[0] = 1是否为所找元素x,显然A[0]<x,则跟新区间左边界left = mid+1=1,新区间为[1,1]。
再次检查中间元素A[mid] = A[(1+1)/2] = A[1] = 2,即为所找元素x。
由此引申为C的代码如下:
//定义如下函数,用来返回所找元素x的下标
//A[]为严格递增序列,[left,right]为所查区间,x为所找元素
int binarySort(int A[], int left, int right, int x){
int mid;
while(left <= right){
mid = (left + right) / 2; //区间中间元素下标
if(A[mid] == x) return mid; // 找到元素,返回下标
else if(A[mid] > x) right = mid - 1;//所找元素在当前区间左侧,修改右边界
else left = mid + 1;
}
return -1; // 查找失败,返回-1
}
经典问题的微扩展
以上解决了严格递增序列,再思考一个问题:给定一个递增序列(非严格递增)找到元素x的所在下标,具体问题如下:
给定A[5] = {1, 2, 2, 4, 6},如要求找x = 2的下标,应给出答案为区间[1,3),之所以区间设计为左闭右开是为了解决这样一个问题:
要求找x = 3的下标,那么答案应该为[3,3)。显然,当所找元素不存在时,也应该返回一个它应该在的位置,所以区间设计为左闭右开(从这点出发,设计成左开右闭也是可以的)。
下面我们来分析上述问题:
显然,任务从经典问题的寻找一个元素下标,变为找到一个区间的左右边界,所以我们分别解决两个任务:左闭边界和右开边界:
-
任务A:寻找左闭区间下标
再次解读我们需要做的事情,将问题转化为:从左往右找到第一个大于等于x的元素下标。这样的一个问题和我们第一个经典问题类似了,区别在于条件不同。这里,我们找到一个大于等于x的还不够,当找到这样一个元素时,说明我们仍然需要将查找区间[left,right]向左移,即改变右边界,直到确定到这是第一个满足条件的元素。我们一边看C代码一边感受这里的变化:
//递增序列A,找到第一个大于等于x的元素下标
int leftBound(int A[], int left, int right, int x){
int mid;
while(left < right){
// 从 <= 变为 <
mid = (left+right) / 2;
if(A[mid] >= x) right = mid; // 右边界修改不再是 mid-1
else left = mid + 1;
}
return left; // 返回夹出来的边界,即下标
}
while(left < right)
从<=变为<,仔细思考原因:上一个经典问题中,我们需要在left=right时,进一次循环,从而能return mid,在这里我们并不需要return mid,当left=right时,并不需要再次进入循环,因为我们在while循环结束后有 return left 语句,并且left=right时的left即为所求。if(A[mid] >= x) right = mid;
这里当中间元素A[mid]≥x,说明所找x在区间左侧,则改变右边界,但是右边界并不能改为 mid-1,因为你不知道这个A[mid]是不是就是你找的那个第一个满足条件的元素,所以下次查找时,区间应该包括这个元素。- 最后是
return left;
这里返回边界,事实上返回左边界还是返回右边界都一样,因为这条语句是在while循环后执行的,而while循环结束的条件是left==right。
- 任务B:寻找右开区间下标
同样地解读任务,转化为:找到第一个大于x的元素的下标,这个问题和上一个问题极其类似,直接给出C代码,我们能更直观的感受到究竟有多类似:
//递增序列A,找到第一个大于x的元素下标
int rightBound<