本文部分概念引用知乎
作者:南葱
链接:https://www.zhihu.com/question/19864652/answer/71204977
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
===========================正文=======================================
这章讲述了一些有趣的东西,比如契约式编程(Design by Contract ),二分查找的停机问题,循环不变式,以及对编程心理的把握,在困难的地方应用强大正规的手段,在容易的地方容易出现老问题。
这里我觉得首先要讲述一下这几个概念:
契约式编程:
就个人理解,Contract是介于静态类型系统(Statical type system)和异常(Exception/Error)之间的一种声明/检查机制,而Design by Contracts则是用这种机制来指导设计。那么什么是Contract呢?通常,我们在声明一个函数/方法的时候,对函数的输入和输出所具备的性质是有所期望和规定的。有时候这种性质会被我们明确的写出来,有时候会被我们忽略掉。这些期望和规定就是Contract。以Python为例,假如我们需要一个求和函数,对数字的序列进行求和:def
my_sum(inp):
res = 0
for x in inp:
res += x
return res
那么问题来了,这样的语法并不能很好地帮我们对输入的参数进行把关。比如:inp = [1, 2, 3, ‘a’, 4]
my_sum(inp)
然后异常被抛出Traceback (most recent call last):
File "c:/Users/Shellay/OneDrive/Code/python/clips/contracts.py", line 12, in <module>
my_sum(inp)
File "c:/Users/Shellay/OneDrive/Code/python/clips/contracts.py", line 6, in my_sum
res += x
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
即在函数执行的过程中,Runtime Error被捕捉。但是这些Runtime Error有时候并不能直接表明问题的信息。为了使得错误更加有提示性,熟悉try-catch的童鞋们可能会这样写:def my_sum(inp):
res = 0
try:
for x in inp:
res += x
return res
except TypeError as e:
raise Exception('Input {} are not summable - should be numbers.'.format(inp))
看上去这种写法应该是足够清晰了,但还不够:一方面错误是在计算过程中被发现的,maybe too late——对于不合法的输入,我们更希望程序crash early;另一方面,try-catch其实是比较丑陋的,干扰了原有代码的层次结构。若要在函数被调用、但函数体还未执行之前,就检测出输入中可能存在的错误,并给出相应的提示(从而避免不必要的计算),我们可以先声明一个Contract,再实现函数体:def my_sum(inp):
# Contract
assert all(isinstance(x, int) or isinstance(x, float) for x in inp), \
'Contract vialation: Input are not numbers. '
# Body
res = 0
for x in inp:
res += x
return res
这样的话,我们不仅区分了Contract和Runtime Error,还使得函数体最大化地保留了清晰的原貌。inp = [1, 2, 3, ‘a’, 4]
my_sum(inp)
Traceback (most recent call last):
File "c:/Users/Shellay/OneDrive/Code/python/clips/contracts.py", line 35, in <module>
my_sum(inp)`这里写代码片`
File "c:/Users/Shellay/OneDrive/Code/python/clips/contracts.py", line 26, in my_sum
'Contract vialation: Input are not numbers. '
AssertionError: Contract vialation: Input are not numbers.
这里再声明一下Wiki对契约式编程的定义
契约就是这些权利和义务的正式形式。我们可以用“三个问题”来总结DbC,并且作为设计者要经常问:
它期望的是什么?
它要保证的是什么?
它要保持的是什么?
很多编程语言都有对这种断言的支持。然而DbC认为这些契约对于软件的正确性至关重要,它们应当是设计过程的一部分。实际上,DbC提倡首先写断言。
契约的概念扩展到了方法/过程的级别。对于一个方法的契约通常包含下面这些信息
- 可接受和不可接受的值或类型,以及它们的含义
- 返回的值或类型,以及它们的含义
- 可能出现的错误以及异常情况的值和类型,以及它们的含义
- 副作用
- 先验条件
- 后验条件
- 不变条件
- (不太常见)性能上的保证,如所用的时间和空间
综上我对契约式编程目前的理解是在程序运行之前通过断言等形式判断程序的合法性,而不是在运行时判定,类似于结成契约前要满足某些条件然后才能执行(这么一想还有这么一丢丢中二气息~~~)。
二分查找的停机问题
之前刚讲过图灵机的停机问题是个悖论,然后看到这个概念的时候疙瘩一下以为二分查找也出了悖论,实际上是一些程序员(也包括我)有时会犯得一个毛病因为一些边界问题而使程序进入了死循环,好了,证明停机问题的思路是什么呢?
二分查找结束的条件是在范围中少于一个元素时停止循环,还有一种直接找到了这里就不证明,前者要满足的条件是每次循环范围至少减少1,然后我们分析每次比较二分查找的时候如果不中都会去掉那个中间元素,这就是范围减少了1,所以这个循环必然停止。
循环不变式
wiki的定义如下:
在计算机科学中,循环不变性(loop invariant,或“循环不变量”),是一组在循环体内、每次迭代均保持为真的性质,通常被用来证明程式或伪码的正确性(有时但较少情况下用以证明算法的正确性)。简单说来,“循环不变性”是指在循环开始和循环中,每一次迭代时为真的性质。这意味着,一个正确的循环体,在循环结束时“循环不变性”和“循环终止条件”必须同时成立。
这里我的理解是数学归纳法证明一公式成立时,显然公式的变量=n时也成立,循环不变量就是一个公式,在每一次循环中变量的变化始终保持着这个循环不变量的成立,然后用这个循环不变量代表这段循环程序的正确性(所谓的初始化、保持、终止是三个抽象出来的循环阶段用来证明循环不变量的成立进而证明循环程序的正确性)
4.1
QAQ又是逻辑形式证明任意x属于变量声明范围,存在一个结果使得在二分查找终止。
4.2
int bs( int a[], int l, int r, int v ){
while( l <= r ){
if( a[l] == v ) return l;
int mid = (l+r)/2;
if( a[mid] < v ) l = mid+1;
if( a[mid] == v )r = mid;
if( a[mid] > v ) r = mid-1;
}
return -1;
}
4.3
int bss( int *a, int l, int r, int v ){
if( l > r ) return -1;
if( a[l] == v ) return l;
int mid = (l+r)/2;
if( a[mid] < v ) return bss( a, mid+1, r, v );
if( a[mid] == v )return bss( a, l, mid, v );
if( a[mid] > v ) return bss( a, l, mid-1, v );
}
判定mid和循环结束部分和循环一样,不同的是范围的变化体现在了尾递归的参数上
4.4
运行两次?每次数据增长个平方观察运行时间,如果是指数的应该是两倍才是。
4.5
假设存在一个正整数不能使程序终止,则必然在if else 中来回跳转,又因为在一次跳转红x不会保持不变所以3*x+1始终在增长,存在一个值使得3*x+1满足2的x次,从而不再跳转回else,所以原假设不成立
4.6
每次执行会造成总豆子数目减少1所以循环一定次数会成立
白色是奇数个,是白,如果黑色是奇数个,是黑。
4.7
上下为两个边界,二分法查找,只要x在所在线段的范围就行。
4.8
这道题不太会,感觉用尾递归会比普通循环快一点?要看具体编译器的把….
4.9
.(1)首先应当证明每一次的操作能够得到当前一步想要的结果,然后因为i是递增的,且每次都加1,所以从0开始直到最后计算结束,n个维度都得到了相加。且i每次都递增,所以一定会结束。
(2)一开始max是数组第一个数字,在循环中,数组中第i个数字会与max比较,如果比max大就更新,另一方面0到i-1也已经比较完了。i从1开始,每次比较后i都会增加1,所以循环会结束,数组中的数字也都会与max比较,从而得到正确的最大值。
(3)循环中每次i都会自增1,所以循环一定会终止。如果i超过了范围会终止,如果找到了也会终止。因为i是从0开始顺序递增的,所以如果找到了,那么一定是第一个,程序因此是正确的。
(4)每次如果满足条件递归下去,那么递归的定义域会一直缩小。要么减一,要么除以2,而且程序中给定了边界条件。每次递归的结果都是上一层需要的,所以程序可以结束且能得到正确答案。
4.10
通过断言or放入try-catch代码块?
4.11
int binarysearch(int x[], int n, int begin, int end) {
int mid;
if (begin <= end){
mid = (begin+end)/2;
if (n == x[mid]) {
return mid;
}
if (n > x[mid]) {
return binarysearch(x,n,mid+1,end);
}
if (n < x[mid]){
return binarysearch(x,n,begin,mid-1);
}
}
return -1;
}