二分法和快速排序中的边界问题

来源:互联网 发布:淘宝宝贝排名不稳定 编辑:程序博客网 时间:2024/06/07 05:14

                 提起二分法,估计大部分人都会觉得非常简单,没啥可说的,可要是让你写出一份没有错误的二分查找代码,嘿嘿,说实话,你很大可能会写错.

因为二分查找中的边界问题,非常容易写错.正好前几天逛了一下知乎,发现这个问题有不少不错的回答,其中一个深得我心,思路清晰,代码简洁,分享过来.

https://www.zhihu.com/question/36132386,这是知乎上关于二分查找问题的网址,下边是我个人最欣赏的一个回答.

不管怎么写,只要遵循一个原则,就是L是符合条件的的,R是不符合的(反过来也一样)。这个不变量将贯穿整个二分的过程。什么意思呢?举个栗子,你需要在[l, r]区间中找到符合条件的最大的x,按照上面的原则,你的初始条件就应该是L = l, R = r + 1, 注意这里的R是不符合条件的,因为它超出了边界,所以它不可能是答案。这时候,你的mid如果合法,就说明答案大于等于mid,所以应该更新L(L=mid即可),如果mid不合法,直接把R设成mid。至于如何判断合法(是大于等于还是大于),该不该加一减一,按照这个原则都可以很容易想出来。

这样在整个二分的过程中,你会发现,R在任何时候都不可能是答案,那么这个循环的停止条件也很容易判断了,就是L + 1 = R,这时候说明L已经是符合条件的最大值了,因为再加一就是非法值。
作者:匿名用户
链接:https ://www.zhihu.com/question/36132386/answer/66089177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本人补充:这里的目标可以统一为寻找最后一个符合条件的,如果目标是寻找第一个符合A条件的,可以转化为目标是寻找最后一个符合B条件的,比如"寻找第一个大于等于A的"可以转化为"寻找最后一个小于A的",然后索引+1即可.初始条件的R是对的,但是L不对,因为最左端也不一定符合条件.可以这样做:先加个判断,跟最左端元素比较一下,如果最左端元素不合法,显然,没有任何元素会合法了,特殊处理即可,如果其合法,那不用管,可以按照上边的式子初始化.或者更简单一点,直接L=l-1,仿照R=r+1.

假如我们的目标是在给定数组a[]中,寻找第一个大于等于给定值goal_value的元素的索引,那么按照以上分析,代码如下:

using namespace std;int binary_search(int * a, int len,int goal_value ){int l = -1;int r = len;int mid;while (l + 1 < r){mid = (l + r) / 2;if (a[mid] >= goal_value)  //根据以上分析,其为非法值,保证r始终是非法值.r = mid;else                   //保证l始终是合法值.l = mid;}//l必是最后的合法值索引.而我们需要的是第一个不合法的值的索引,所以+1即可.return l+1;}void main_binary_search(int * a, int len, int goal_value){int index=binary_search(a, len, goal_value);if (index <= len - 1)cout << "第一个大于等于" << goal_value << "的是a[" << index << "],其值为" << a[index];elsecout << "所有元素都小于" << goal_value;}
同样深受边界问题困扰的还有快速排序,快速排序中的交换元素到主元两侧,这个问题处理不好容易导致子问题规模没有变化或者出错.

如果你不想为边界问题烦恼,okay,推荐你直接使用快慢指针,不用担心边界问题,同时代码非常简洁.

void fast_paixu(int * a, int left, int right){if (left >= right)return;int i = left+1;int j = left+1;int pivot = a[left];for (; j <= right; j++)if (a[j] < pivot)swap_num(&a[j], &a[i++]);swap_num(&a[left], &a[i - 1]);fast_paixu(a, left, i - 2);  //i-1是主元位置,所以左区间右端点是i-2.fast_paixu(a, i, right); //同上,右区间左端点是i.}void main_fast_paixu(int * a, int len){fast_paixu(a, 0, len - 1);display(a, len);}
当然了,如果你就是想用头尾指针来解决这个问题,那么请一定要从右侧的尾指针开始搜索,这样不会出错,如果从左侧头指针开始搜索,可能会出错.假设j一开始指向尾端,i一开始指向头部.那么从右侧开始查找,可以保证最后尾指针后元素一定是大于等于pivot的.同时尾指针之前的元素和尾指针所指元素一定小于等于pivot.这时,我们只需把主元与a[j](即a[i])交换即可.但是从头指针开始查找就可能面临一种难以处理的特殊情况,理论上如果从头指针开始,那么最后主元应该与i-1的位置交换,比如6,5,4.头尾指针在4处相遇,实际上尾指针没动过,这时理论上应该交换6和4前边一个元素即5,那么变成了5,6,4.很显然这是错误的.那么为什么尾指针先行不会遇到这种问题呢,因为这种问题的发生主要是由于后行的指针压根没动过,其所指元素情况就不确定导致的.如果尾指针先行,头指针没动过,那么两者必然在第一个元素处相遇,而第一个元素的情况是确定的,就是pivot,这时交换pivot与i位置元素,即自己与自己交换,交换过后我们可以看出来,这是正确的.

所以一定要从尾部先查找开始,因为我们选取的主元是第一个元素,它的情况是清楚的.而先从头部开始的话,尾部元素情况并不确定.

代码如下:

#include <stdio.h> void quicksort(int * a,int left, int right){int i, j, t, temp;if (left>right)return;temp = a[left]; //temp中存的就是基准数 i = left;j = right;while (i != j){//顺序很重要,要先从右边开始找 while (a[j] >= temp && i<j)j--;//再找左边的 while (a[i] <= temp && i<j)i++;//交换两个数在数组中的位置 if (i<j){t = a[i];a[i] = a[j];a[j] = t;}}//最终将基准数归位 a[left] = a[i]; //插一句,由于主元小于等于自己,所以在上边的循环中,实际上主元位置根本没变.a[i] = temp;  //所以直接交换主元与a[i]即可.quicksort(a,left, i - 1);//继续处理左边的,这里是一个递归的过程 quicksort(a,i + 1, right);//继续处理右边的 ,这里是一个递归的过程 }void main_quick_sort(int * a,int n){quicksort(a,1, n); //快速排序调用 //输出排序后的结果 for (int i = 1; i <= n; i++)printf("%d ", a[i]);printf("\n");}





原创粉丝点击