CMU 15122:Deliberate Programming
本篇总结CMU 15122第一单元Deliberate Programming的全部内容(除了Contracts部分)。这门课的整体脉络还是较为清晰的,虽然大部分东西并不陌生,但从safety和correctness的角度重新审视一遍也是不错的。
Integers
涉及进制转换的内容略过不提。
Two’s Complement
Two’s Complement是C0中表示数字的一种方法。C0中的整数一共占据32个比特位,其中最高的比特位是该比特位代表的负的2的31次方,也就是说,C0中的最大正整数可以用0x7FFFFFFF表示,正数的范围是0x00000000-0x7FFFFFFF()。当最大正整数int_max()再继续做加法的时候整数就会溢出到负数的范围内,也就是0x80000000-0xFFFFFFFF。其中0x80000000是int_min(),其顺序与正数表示相反。由此,c0能够表示的整个整数范围是。
对于int的操作类型有:Addition, multiplication, division, subtraction, comparisons, mod;
对于Tow’s complement表示方法来说,有以下成立:
- -x = ~x + 1
- int_min() = -int_min()
对于以下操作会产生runtime error:
- int_min() (/ or %) - 1
- x / 0
Bit Pattern
Int不是数字,是0和1组成的字符串。因此,bitwise operation可以通过对每一比特位进行操作来改变数字大小。其中包括是掩码以及各种比特位操作。& 0xFF000000 可以将另外一个32位数的后24位全部置为零,这就是为什么其被称作mask(掩码)。另外我们可以通过| 0xFF000000 将32位数的前8位全部置为1。左移<<就是将比特数左移多少位,右边补零。右移>>会根据最高位进行补齐。
Arrays
数组是相同类型数据的集合,例如 int[] 表示整型数组。数组可以包含任何类型的元素,包括其他数组。
局部变量存储在局部内存中,函数的变量在各自的帧(frame)中。函数调用时,每个被调用的函数都有自己的帧,且只能操作自己帧中的变量。函数返回后,其帧被撤销。
使用 alloc_array(type, n) 创建数组,返回一个类型为 type[] 的数组,包含 n 个元素。数组的元素存储在分配的内存中,而数组变量本身存储在局部内存中,包含指向分配内存的地址。通过 A[i] 访问数组 A 的第 i 个元素,索引从0开始。分配的内存使用默认值初始化,对于整型数组,默认值为0。越界访问(out-of-bound access)是不允许的,有效索引是0到数组长度减一。数组操作需要满足前提条件(preconditions),例如 alloc_array(type, n) 需要 n ≥ 0。
C0中Aarray变量名存储的是数列的地址,因此,假如int[] B = A,A是某个已经存在的数列,B只会复制A的地址。后续对B的操作也实际上是对于A的操作。一旦某个数列没有指针指向它,C0的Garbage Collector就会消除掉该数列。整数与整数之间不涉及到地址的问题。C0无法返回某个数组的长度,但是用Contract中的\length()可以。
Searching and Sorting
In-place: allocates constant amount of memory.
Stable: relative position of duplicates is preserved.
Linear Search
Linear Search本身没什么可以探讨的,主要是关于contracts方面的问题。
Implementation
1 | bool is_in(int x, int[] A, int lo, int hi) |
Safety and Correctness
确保搜索过程中数组索引 i 在有效范围内,即 0 <= i < n,其中 n 是数组 A 的长度。使用contract来定义函数的前提条件(@requires)和后置条件(@ensures)。前提条件:确保传入的数组长度 n 与数组 A 的实际长度相匹配。后置条件:如果找到元素 x,则返回其索引;否则返回 -1。
- 数组段表示数组中的一部分,用 A[lo, hi) 表示,包含从索引 lo(包含)到索引 hi(不包含)的元素。
- 定义 is_in(x, A, lo, hi) 函数,用于检查元素 x 是否在数组段 A[lo, hi) 中。
Contract Exploits
即使函数满足Contract,也可能不满足预期的行为。例如,一个函数可以满足Contract但在逻辑上是错误的。
Binary Search
Implementation
1 | int binarySearch(int x, int[] A, int n) |
使用循环,维护两个指针lo(low)和hi(high),分别代表搜索区间的下限和上限。
计算中间索引mid,并与目标值x进行比较。
- 如果A[mid]等于x,则返回mid。
- 如果x小于A[mid],则更新hi为mid。
- 如果x大于A[mid],则更新lo为mid + 1。
当lo和hi相遇时,搜索结束,返回-1表示未找到。
Loop Invariant
- 0 ≤ lo
- lo ≤ hi
- hi ≤ n
- A[0, lo) < x
- x < A[hi, n)
应确保mid的计算不会超出整数范围,避免溢出错误。循环不变式在算法的初始化(INIT)、保持(PRES)和终止(TERMINATE)阶段都是可以被证明有效的。
Mergesort
Implementation
1 | void merge(int[] A, int lo, int mid, int hi) |
Explanation
- 递归地将数组分成两半,直到分出的数组长度为1或者为0;
- 递归地将数组两两合并,合并的时候做具体的比较运算使得合并出来的数组都是有序的;
- Merge Halves -> O(n) cost.
- Levels of Merging -> O(logn) cost.
- O(nlogn) cost.
Quicksort
Implementation
1 | int partition(int[] A, int lo, int hi) |
Explanation
- 随机挑一个轴;
- 遍历整个数组,把每个数和轴比较,然后放到轴的左右边;
- 递归地向下分,分到长度为1或者0时表示整个数组排序完毕;
- Quicksort 和 Mergesort本质上很像,只不过一个在分的过程中做比较运算,一个是分完之后在合并的过程中做比较运算。
- Unlucky: Pick smallest element as pivot O(n^2), just selection sort;
- Average: Pick middle element -> O(nlogn);
Complexity
O(g(n))是一组函数,其中f(n)∈O(g(n))。当且仅当:
- 有c∈R+
- 有n0∈N
使对于所有n≥n0,f(n)≤cg(n)。
O(1) ⊂ O(log n) ⊂ O(n) ⊂ O(n log n) ⊂ O(n2) ⊂ O(2n) ⊂ O(n!)
做复杂度分析是可以直接抛掉常数,取变量最坏的情况。保留不同变量间的运算。log的底因为可以通过换底公式换成常数乘以以某数为底因此不重要。
Linear search: O(1) best case, O(n) worst case.
Binary search: O(1) best case, O(logn) worst case.
Selection sort: O(n^2)
Merge sort: O(nlogn)
Quick sort: O(nlogn) best case, O(nlogn) avg case, O(n^2) worst case.
Structs and pointers
Local Memory and Allocated Memory
- Local memory: Literals (int, bool, string, any direct value);
- Born when: declared in its scope
- Vanishes when: exit its scope
- Allocated memory: (contents of) pointers, arrays, tructs;
- Born when: we allocate them;
- Vanishes when: we can no longer access them -> garbage collector;
Pointers
多重指针,指针修改,内存图绘制,不做赘述
Default values:
- int: 0
- char: ‘\0’
- string: “”
- bool: false
- pointer: NULL
- array: empty array
Structs
统一管理多种类数据的方便手段。内存图绘制同样不做赘述。
1 | struct goose_header{ |
Interfaces vs. Implementation
我们可以通过typedef给数据起别名,这样可以增加程序的抽象程度,方便调用。调用interface进行开发的方面也不需要深入程序细节,使用提供的函数接口和数据别名就可以完成开发。因此通常情况下client使用的只是interface提供的指针,不对具体数据进行操作。specification function属于implementation contracts部分,确保程序正确性,因此client不会调用它们。因为client知道他们调用的是指针,因此为了防止指针被设置成NULL,interface中都会有NULL check,作为另一种监督程序正确性的手段。