算法的不变性与单调性证明举例(二分查找)
//在有序向量中查找[lo, hi)区间内元素e的下标
//若有多个e,返回最大下标
//若e不存在,返回不大于e的最大值的下标
int binSearch(int * a, int e, int lo, int hi)
{
while(lo < hi)
{
int mi = (lo+hi)>>1;
if( e<a[mi])
hi = mi;
else
lo = mi+1;
}
return --lo;
}
int search(int *a, int e, int len)
{
return binSearch(a, e, 0, len);
}
单调性:
while(lo < hi)
{
int mi = (lo+hi)>>1;
if( e<a[mi])
hi = mi;
else
lo = mi+1;
}
由以上代码可知,[lo, hi)区间确实会不断缩减,最终缩减为空集,即lo==hi。
不变性:
若要满足以下语义
在有序向量中查找[lo, hi)区间内元素e的下标
若有多个e,返回最大下标
若e不存在,返回不大于e的最大值的下标
需要找到一个分界线,满足分界线左边的元素都<=e,分界线右边的元素都>e。该分界线左边的元素即为满足语义的待求元素。
由于单调性,lo、hi最终会相等,相等时,lo=hi 即为分界线。
证明:
若要 lo=hi 为分界线
需要满足 条件fj: [0, lo)内的元素都<=e, (hi, len)即[hi+1, len)内的元素都>e.
初始时,lo=0, hi=len,[0, lo)与(hi, len)都为空集,显然满足 条件fj 。
while(lo < hi)
{
int mi = (lo+hi)>>1;
if( e<a[mi])
hi = mi;
else //隐含的 a[mi]<=e
lo = mi+1;
}
每次while循环,有两种情况可能发生
情况1: e<a[mi]
情况2: a[mi]<=e (代码中的else分支)
当 情况1 时,hi=mi,[0,lo)区间不变,(hi, len)区间扩展,只要证明扩展后(hi, len)内的元素仍然都>e就好了。
因为是用mi更新的hi, a[mi]>e, 所以,mi右边的元素也都>e。所以,扩展后(hi, len)内的元素仍然都>e。
当 情况2 时,lo=mi+1, (hi, len)区间不变,[0,lo)区间扩展,只要证明扩展后[0,lo)内的元素仍然都<=e就好了。
因为用 mi+1 更新的lo,a[mi]<=e, 所以mi+1左边的元素也都<=e。所以,扩展后[0,lo)内的元素仍然都<=e。
综上所述,每次循环,无论 情况1 情况2 ,循环后, [0, lo)内的元素都<=e, (hi, len)内的元素都>e.
即每次循环后,都仍然满足 条件fj 。
综上,根据该算法的单调性与不变性,while执行完后,lo==hi,lo=hi 左侧的元素都<=e, lo=hi右侧的元素都>e,lo-1即为满足 语义 的结果。算法正确。
此算法的难点在于 不是查找点,而是查找一个空区间[lo,lo)作为分界线,使 [0, lo)和[lo, len)内的元素满足要求