一.你能准确写出二分查找吗?
先看看定义
二分查找的搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则查找成功;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
思路很简单以至于大多数人都能讲出来,但是有多少人能一次写出bug-free的代码?《编程之美》第2.16节的最长递增子序列算法,如果想实现O(n2)到O(nlogn)的时间复杂度下降,必须借助于二分算法的变形。其实很多算法都是这样,如果出现了在有序序列中元素的查找,使用二分查找总能提升原先使用线性查找的算法的性能。
很多人觉得二分算法简单,但随手一写会发现诸如死循环,边界条件无法确定的问题,提供充足的时间,仅有约10%的专业程序员能够完成一个正确的二分查找,看来,二分查找并不是我们想象的那么简单。网络上的资料大多对边界值的确定避而不谈,即使进行讲解,思路也有些凌乱并且不适合我这种初学者,在参考多篇文章之后,我对二分算法有了一个新的认识。
————==写出这篇博文献给和我一样对二分查找的思路毫无头绪但又不甘于死记硬背的人们==。
二.利用循环不变式来证明边界取值的正确性
note:
1. 本文中,文字说明部分的’=’究竟是赋值还是逻辑判断请根据上下文推断。
2. 算法推导过程中的mid的计算采用(low+high)/2的方式,因为只是推导,所以无需考虑溢出的问题,但是代码中全部采用low+(high-low)/2,推导过程中可认为两者值相等。
3. 在理论推导每次数组减少的长度时,mid是不能代换成left + (right - left)/2的。这种形式代表了非整型的运算,没有舍去小数部分,而在实际的运行过程中mid是会舍去小数部分的。
循环不变式主要用来帮助理解算法的正确性。形式上很类似与数学归纳法,它是一个需要保证正确断言。对于循环不变式,必须证明它的三个性质:
- 初始化:它在循环的第一轮迭代开始之前,应该是正确的。
- 保持:如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。
- 终止:循环能够终止,并且可以得到期望的结果。
例1:找出数组中值为v的元素,不存在时返回-1
初始化:初始区间为[0,n-1],low初始值为0,high初始值为n-1,若v存在于数组中,则其必然存在于区间[low,high中],正确。
保持:
- 若Array[mid]< v,则v应该存在于[mid+1,high]区间内,舍弃区间[low,mid],数组减小长度为(mid-low+1),则数组每次至少减少1个单位,并且low应该指向mid+1的位置;
- 若Array[mid]==v,则说明找到,返回mid;
- 若Array[mid]>v,则v应该存在于[low,mid-1]区间内,舍弃区间[mid,high],数组减小长度为(high-mid+1),则数组每次至少减少1个单位,并且high指针应该指向mid-1位置;