最近看到July大神的二分查找帖子里的两个版本,以及Lucida大神写的算法数据结构经历以及各种面试经历帖,偶然看到编程珠玑这本书,发现里面对二分查找的证明有着与算法导论的循环不变性相似,但是更加make sense的证明思路,最近读了一下,有一些心得。大家可以尝试运用这个方法来证明下更加容易出现bug的快速排序算法,以便确定自己的代码是否正确。
代码4中,验证到第 10行时,{musbe(l,h-1)&&l>h} 得出找不到x,会漏掉l=h的情况,此时mustbe{l,h-1}也是找不到x的。这个或许不够make sense, 再往下走
验证到第 13行时,{musbe(l,h-1)&&l<=h}
m=l+(h-l)/2;
{musbe(l,h-1)&&l<=m<=h} 注意:此处之前l<h,加上m=(l+h)/2可以推导出l<=m<=h-1,而现在l<=h,加上m=(l+h)/2只可以推导出l<=m<=h
验证到第25 行时, x<a[m]:
{musbe(l,h-1)&&l<=m<=h&&x<a[m]<=a[m+1]...<=a[n-1]}
{mustbe(l,h-1)&&cannotbe(m,n-1)&&m-1<=h-1}
{mustbe(l,m-1)}
h=m-1;
{musbe(l,h)}//不能保持loop invariant {mustbe(l,h-1)}
引用自July师兄的二分查找博客http://blog.csdn.net/v_july_v/article/details/7093204#reply
二分查找算法(h=n-1版本):(代码1)
int BinarySearch(int *a, int n, int x)//
{
int l=0,h=n-1;
while(1)
{
if(l>h)
return -1;
m=l+(h-l)/2;
case:
a[m]<x: l=m+1;
x<a[m]: h=m-1;
x=a[m]: return m;
}
}
定义循环不变式 mustbe(l,m): 如果x在a[l...m]中,则mustbe(l,m) bool值为真
cannotbe(l,m): x不可能出现在a[l,m]中
二分查找算法(h=n-1版本)循环不变性证明:(代码2)
int BinarySearch(int *a, int n, int x)//
{
{mustbe(0,n-1)}
int l=0,h=n-1;
{mustbe(l,h)}
while(1)
{
{musbe(l,h)}
if(l>h)
{musbe(l,h)&&l>h}
{can not find x}
return -1;
{musbe(l,h)&&l<=h}
m=l+(h-l)/2;
{musbe(l,h)&&l<=m<=h}
case:
a[m]<x:
{musbe(l,h)&&l<=m<=h&&a[0]<=a[1]...<=a[m]<x}
{mustbe(l,h)&&cannotbe(0,m)}
{mustbe(m+1,h)}
l=m+1;
{musbe(l,h)}//loop invariant
x<a[m]:
{musbe(l,h)&&l<=m<=h&&x<a[m]<=a[m+1]...<=a[n-1]}
{mustbe(l,h)&&cannotbe(m,n-1)}
{mustbe(l,m-1)}
h=m-1;
{musbe(l,h)}//loop invariant
x=a[m]:
{musbe(l,h)&&l<=m<=h&&x==a[m]}
{mustbe(m,m)}
return m;
}
}
二分查找算法(h=n版本):(代码3)
int BinarySearch(int *a, int n, int x)//
{
int l=0,h=n;
while(1)
{
if(l>=h)
return -1;
m=l+(h-l)/2;
case:
a[m]<x: l=m+1;
x<a[m]: h=m;
x=a[m]: return m;
}
}
二分查找算法(h=n版本)循环不变性证明:(代码4)
int BinarySearch(int *a, int n, int x)//
{
{mustbe(0,n-1)}
int l=0,h=n;
{mustbe(l,h-1)}
while(1)
{
{musbe(l,h-1)}
if(l>=h)
{musbe(l,h-1)&&l>=h}
{can not find x}
return -1;
{musbe(l,h-1)&&l<h}
m=l+(h-l)/2;
{musbe(l,h-1)&&l<=m<=h-1}
case:
a[m]<x:
{musbe(l,h-1)&&l<=m<=h-1&&a[0]<=a[1]...<=a[m]<x}
{mustbe(l,h-1)&&l<=m<=h-1&&cannotbe(0,m)}
{mustbe(m+1,h-1)}
l=m+1;
{musbe(l,h-1)}//loop invariant
x<a[m]:
{musbe(l,h-1)&&l<=m<=h-1&&x<a[m]<=a[m+1]...<=a[n-1]}
{mustbe(l,h-1)&&l<=m<=h-1&&cannotbe(m,n-1)}
{mustbe(l,m-1)}
h=m;
{musbe(l,h-1)}//loop invariant
x=a[m]:
{musbe(l,h-1)&&l<=m<=h-1&&x==a[m]}
{mustbe(m,m)}
return m;
}
}
1.以下代码:(代码5)
int BinarySearch(int *a, int n, int x)//
{
int l=0,h=n-1;
while(1)
{
if(l>h)
return -1;
m=l+(h-l)/2;
case:
a[m]<x: l=m+1;
x<a[m]: h=m-1;
x=a[m]: return m;
}
}
与以下代码等价:(代码6)
int BinarySearch(int *a, int n, int x)//
{
int l=0,h=n-1;
while(l<=h)
{
m=l+(h-l)/2;
case:
a[m]<x: l=m+1;
x<a[m]: h=m-1;
x=a[m]: return m;
}
return -1;
}
同理可得第二个case的二分查找代码.
为什么在证明程序的时候,要改写一下呢?
因为证明比较方便,而且非常beautiful~~
2.正如July师兄的代码所说,为啥初始从h=n-1 -> h=n后,后面if(l>h) ->if( l>=h), x<a[m]后的h=m-1 -> h=m
代码4中,验证到第 10行时,{musbe(l,h-1)&&l>h} 得出找不到x,会漏掉l=h的情况,此时mustbe{l,h-1}也是找不到x的。这个或许不够make sense, 再往下走
验证到第 13行时,{musbe(l,h-1)&&l<=h}
m=l+(h-l)/2;
{musbe(l,h-1)&&l<=m<=h} 注意:此处之前l<h,加上m=(l+h)/2可以推导出l<=m<=h-1,而现在l<=h,加上m=(l+h)/2只可以推导出l<=m<=h
验证到第25 行时, x<a[m]:
{musbe(l,h-1)&&l<=m<=h&&x<a[m]<=a[m+1]...<=a[n-1]}
{mustbe(l,h-1)&&cannotbe(m,n-1)&&m-1<=h-1}
{mustbe(l,m-1)}
h=m-1;
{musbe(l,h)}//不能保持loop invariant {mustbe(l,h-1)}
有人会说如果用mustbe(l,h)作为循环不变性呢?
当你走到代码4第10行的时候,就会发现{mustbe(l,h)&&l>=h}这个是推不出x not found的,因为可能l=h,区间还有一个元素,可能是x
这种问题对应于程序,就是所谓bug,所以这种断言式证明其实就是找bug的一种严谨的数学手段,而测试只能发现bug,不能确定没有bug!引用自July师兄的二分查找博客http://blog.csdn.net/v_july_v/article/details/7093204#reply