1、这些都将在后文的各个算法说明中看到。知识准备结合算法导论和编程珠玑,下面说明循环不变式的概念与性质。循环不变式主要用来帮助理解算法的正确性。形式上很类似与数学归纳法,它是一个需要保证正确断言。对于循环不变式,必须证明它的三个性质:初始化:它在循环的第一轮迭代开始之前,应该是正确的。保持:如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。终止:循环能够终止,并且可以得到期望的结果。文章说明(1)在推导每次数组减少的长度时,mid是不能代换成(left+right)/2的。这种形式代表了非整型的运算,没有舍去小数部分,而在代码中实际的mid是会舍去小数部分的。
2、(2)代码部分的=和=意义同C语言;文字说明部分的=代表赋值,=代表等式推导或者逻辑判断,由上下文而定。(3)除了3和5外,最初的各个变体代码参考于:二分查找,你真的会吗?为了符合思路的前后连贯和说明循环不变式,做了一些修改。原文的测试很方便,读者可以自行参考。(4)根据getgoing的提示和对原始参考代码的回顾,mid = (left+right)/2本身可能导致溢出,比较保险的写法是mid = left + (right-left)/2,如果想通过移位而不是除法提升速度,可以写成mid = left + (right-left)1)。 本文最初版本没有注意到这一点,现在已经修改,这个表达
3、式和最初的比不是很直观,专门在这里说明。当然,在理论推导而不是程序运行时是不必考虑溢出的,mid = (left+right)/2和mid = left + (right-left)/2总是相等,因此(1)和全文中的推导并未对此修改。1.二分查找值为key的下标,如果不存在返回-1。循环不变式:如果key存在于原始数组0,n-1,那么它一定在left,right中。第一轮循环开始之前,处理的数组就是原始数组,这时显然成立。每次循环开始前,key存在于待处理数组arrayleft, ., right中。对于arraymidkey,arraymid, ., right均大于key,key只可能存在
4、于arrayleft,mid-1中;对于arraymid=key,查找到了key对应的下标,直接返回。在前两种情况中,数组长度每次至少减少1(实际减少的长度分别是mid-left+1和right-mid+1),直到由1(left=right)变为0(leftright),不会发生死循环。结束时,leftright,待处理数组为空,表示key不存在于所有步骤的待处理数组,再结合每一步排除的部分数组中也不可能有key,因此key不存在于原数组。cppview plaincopy1. intbinsearch(int*array,intlength,key)2. 3. if(!array)4. re
5、turn-1;5. left=0,rightlength,mid;6. while(leftright)7. 8. mid+(right-left)/2;9. if(arraymid10. 11. 1;12. else13. 14. -15. 16. mid;17. 18. 19. 2.二分查找返回key(可能有重复)第一次出现的下标x,如果不存在返回-1如果key存在于数组,那么key第一次出现的下标x一定在left,right中,且有arrayleft=key。第一轮循环开始之前,处理的数组是0,n-1,这时显然成立。每次循环开始前,如果key存在于原数组,那么x存在于待处理数组array
6、left, ., right中。key,arrayleft, ., mid均小于key,x只可能存在于arraymid+1, ., right中。数组减少的长度为mid-left+1,至少为1。否则,arraymid=key, arraymid是arraymid, ., right中第一个大于等于key的元素,后续的等于key的元素(如果有)不可能对应于下标x,舍去。此时x在left, ., mid之中。数组减少的长度为right-(mid+1)+1,即right-mid,根据while的条件,当right=mid时为0。此时right=left,循环结束。此时left=right。在每次循环
7、结束时,left总是x的第一个可能下标,arrayright总是第一个等于key或者大于key的元素。那么对应于left=right的情况,检查arrayleft即可获得key是否存在,若存在则下标为x;对于leftright的情况,其实是不用考虑的。因为left=上一次循环的mid+1,而midright,意味着mid = right,但此时必有left = right,这一轮循环从开始就不可能进入。binsearch_first(intlength,intlength-1,mid;mid+1;11. elseif(arrayleft=left;17. 3.二分查找返回key(可能有重复)最
8、后一次出现的下标x,如果不存在返回-1(模仿2的第一版)如果key存在于数组,那么key最后一次出现的下标x一定在left,right中,且有arrayleft对于arraymid=key, arraymid是arrayleft, ., mid中最后一个值为key的元素,那么x的候选只能在arraymid, . ,right中,数组减少长度为mid-left。除非left = right或left = right-1,否则数组长度至少减小1。由于while的条件,只有后一种情况可能发生,如果不进行干预会陷入死循环,加入判断分支即可解决。key, arraymid, ., right均大于key
9、,x只可能在left, ., mid-1之中。数组减少的长度为(right-mid)+1,同样至少为1。=right,right总是从数组末尾向开始的倒序中第一个候选的x,检查它的值是否符合要求即可。而left总是上一轮删掉失去x资格的元素后的第一个元素,不过这里用不到。说明:与上一种不同,这个算法不能简单地根据对称,从上一个算法直接改过来,由于整数除法总是舍弃小数,mid有时会离left更近一些。所以这种算法只是沿着上一个算法思路的改进,看上去并不是很漂亮。binsearch_last(intif(leftmid)if(arrayrightright;19. 20. 21. 22. 23.
10、24. 25. 26. 4.二分查找返回key(可能有重复)最后一次出现的下标x,如果不存在返回-1(修改版)根据3中的讨论,可以发现不能直接照搬的原因是mid=(left+right)/2的舍弃小数,在left+1=right且arrayleft=key时,如果不加以人为干预会导致死循环。既然最终需要干预,干脆把需要干预的时机设置为终止条件就行了。使用while(leftright-1)可以保证每次循环时数组长度都会至少减一,终止时数组长度可能为2(left+1=right)、1(left=mid,上一次循环时right取mid=left),但是不可能为0。(每一次循环前总有left=mid
11、right)。同3一样,right总是指向数组中候选的最后一个可能为key的下标,此时只需先检查right后检查left是否为key就能确定x的位置。这样就说明了循环不变式的保持和终止,就不再形式化地写下来了。对于两种情况的合并:arraymid = key时,mid有可能是x,不能将其排除;arraymid=left,违反则意味着x不存在。写下arraymid的比较判断分支:(1) arraymidkey, 意味着x只可能在arraymid, ., right之间,下一次循环令left = mid,数组长度减少了(mid-1)-left+1 = mid-left,这个长度减少量只有在righ
12、t-left=key,意味着x只可能在arrayleft ,. ,mid-1之间,下一次循环令right = mid-1,同样推导出数组长度至少减少了1。这样,把循环条件缩小为rightleft+1,和4一样,保证了(1)中每次循环必然使数组长度减少,而且终止时也和4的情况类似:终止时待处理数组长度只能为2或1或者空(left接着保持中的讨论,结束时,符合的x要么在最终的数组中,要么既不在最终的数组中也不在原始的数组中(因为每一次循环都是剔除不符合要求的下标)。数组长度为2时,right=left+1,此时先检查right后检查left。如果都不符合其值小于key,那么返回-1。数组长度为1时
13、,只用检查一次;数组长度为0时,这两个都是无效的,检查时仍然不符合条件。把这三种情况综合起来,可以写出通用的检查代码。反过来,根据精简的代码来理解这三种情况比正向地先给出直观方法再精简要难一些。binsearch_last_less(int1)7.二分查找返回刚好大于key的元素下标x,如果不存在返回-1和6很类似,但如果只是修改循环中下标的改变而不修改循环条件是不合适的,下面仍要进行严谨的说明和修正。如果原始数组中存在比key大的元素,那么原始数组中符合要求的元素对应下标x存在于待处理的数组。仍然先把执行while循环的条件暂时写为right=key, 意味着x只可能在arraymid+1,
14、 ., right之间,下一次循环令left = mid,数组长度减少了mid-left+1,减少量至少为1。key,意味着x只可能在arrayleft ,. ,mid之间,下一次循环令right = mid,数组长度减少了right-(mid+1)+1= right-mid,只有在right=mid时为0,此时left=right=mid。因此,循环条件必须由right=left收缩为rightleft才能避免left=right时前者会进入的死循环。由循环的终止条件,此时left类似2的分析,leftright是不可能的,只有left=right。此时检查arrayrightkey成立否就
15、可以下结论了,它是唯一的候选元素。补充说明:如果是对数组进行动态维护,返回值-1可以改为length+1,表示下一个需要填入元素的位置。binsearch_first_more(int8.总结:如何写出正确的二分查找代码?结合以上各个算法,可以找出根据需要写二分查找的规律和具体步骤,比死记硬背要强不少,万变不离其宗嘛:(1)大体框架必然是二分,那么循环的key与arraymid的比较必不可少,这是基本框架;(2)循环的条件可以先写一个粗略的,比如原始的while(left=right)就行,这个循环条件在后面可能需要修改;(3)确定每次二分的过程,要保证所求的元素必然不在被排除的元素中,换句话说,所求的元素要么在保留的其余元素中,要么可能从一开始就不存在于原始的元素中;(4)检查每次排除是否会导致保留的候选元素个数的减少?如果没有,分析这个边界条件,如果它能导致循环的结束,那么没有问题;否则,就会陷入死循环。为了避免死循环,需要修改循环条件,使这
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1