实事求是的说二分搜索是我学习算法的时候学的最好,理解的最透彻,能够当时就写出代码的的算法。事到如今,就如我可以分分钟写出hello world一样,我可以分分钟写出一个二分搜索算法,曾经几何时,这曾经是我在大学时面对一众连hello world都不会写的同学的装高手利器,我曾以为我可以带着这份荣耀感一直到我找到下一份荣耀感,但是终有一天残酷的现实总能无声的击碎无力的意淫。
先不考虑二分搜索的各种本体形式,先从最简单的非递归版本看起吧,以下是粗略易错在我写程序的前几个月一直认为没有错并且我觉得在实际应用上一定能用的版本:
//四个参数,数组,开始点,终止点,查找值
//当然这个函数可以再包装一下成为只传数组,数组大小和查找值
int BinarySearch(const int list[],int start,int end,int key) { while(start<=end)
{
int mid=(start+end)/2;
if(list[mid]<key) start=mid;
else if(list[mid]>key) end=mid;
else return mid;
}
return -1; }
上面的代码粗一看绝对很难看出问题在哪,这也是我曾经一直以为这样是正确的原因,但是直到有一天当时一个大牛用了一组测试数据的时候立马打破了我所有的幻想。传统上,这里都要说,先不要朝下看喔,先自己想想能不能找出错误,这么多年来,每当我在书上看到这句话我都会果断往下看。
让我们从最小的冲击开始,不妨试试这样的测试数据:
const int index=10;
int testArr[index]; for(int i=0;i<index;i++) testArr[i]=i; cout<<BinarySearch(testArr,0,index-1,index-1)<<endl;
你会发现善意的小黑框并没有任何数据,在你等了十秒之后幡然领悟貌似是死循环的时候,你才会猛然惊醒查看是不是代码的哪个环节已经操蛋了,通过采用最吊丝的输出中间下标的方法查看到了在某一段时间后,mid的值不变了,这才领悟到应该把start=mid改成start=mid+1,同时我也猛然间领悟到为什么在二分搜索的递归本体中的一些细节了。这是我还在非常初级阶段时犯得错误,但是就是这个错误让我意识到任何一个程序都是那么容易做的完美的,特别是你作为一个写代码的不会知道调用代码的会是怎样的一个格式,代码得具有大爱,得具有包容性。
冲击波略微升级,万一传入的数组是一个没有排过序的怎么办?什么?你说这不可能,二分搜索就是应该传入排序好的数组,先不说这世界上本来就没有什么是应该的,就说无论是有意挑战还是无意调用,这种情况都是极端常见的,所以说在这种情况下,什么二分搜索算法什么高效率瞬间成浮云,这和智能手机再怎么吊,也怕一盆水是一个道理,所以才有所谓三防技术的出现。那怎么避免这种情况呢?我见到的有两种,一个是在真正进行搜索之前无论传入的数组有没有排序,都进行一次的排序工作,第二种是用一个循环,遍历整个数组,如果发现未排序的立马输出错误,return该return的值。这两种虽然牺牲了效率,但是可以确保二分搜索不会被一盆水弄得完全不可以工作。
接着,偶然的机会我又遇到了如下这个强大型冲击波,比如
struct StudentInfo { int grade;//分数 string name;//姓名 }SI;
对于这样一个含有这样结构的数组进行二分搜索,找到分数大于等于60分,也就是没有挂科的人的名字,这个问题很重要,我相信对于大多数人不会希望在某一个统计挂科的名册中发现自己是传说中的六十分但是因为自己的名字按某种默认的规则是排在前面的而被一个不完备的算法所漏统计吧?也许你还没有意识到我说的是什么问题,如果用不废话的版本,如果二分搜索的数组中有重复的数字,怎么处理这一情况,是返回第一个重复的数字还是返回最后一个,或者是随便返回一个。这个问题在上面那个应用背景下就很有意义了,更别说查询什么账户余额大于多少的程序里,毕竟,一旦扯上钱,任何细微的地方都是值得考虑的,如果某一个人因为默认排序而被漏掉,后果大多数情况下应该都比较难搞。
所以,真心觉得任何一个小程序想写的完美都不简单,如果真的想要写的一个完美的程序,细节才是最重要的,使用怎样的技巧证明的是你有没有成为一个优秀程序员的潜质,而能不能考虑到大部分细节绝逼是你能不能成为一个工程师,一个真的开发人员的品质啊,所以,共勉啊,同志们!