授课:张腾 tengzhang@hust.edu.cn
地点:西十二楼 N504
32 学时
考核:闭卷考试 (70%)、平时作业 (30%)
回答三个问题
要把大象装冰箱,拢共分几步?
通俗的讲,算法是完成一个任务所需的一系列步骤
上课算法
刷牙算法:挤牙膏到牙刷上,牙刷贴到牙齿上,上下移动 N 秒
阿尔·花拉子米:9 世纪波斯数学家、天文学家、地理学家
《射雕英雄传》第三十七回 从天而降
原来蒙古大军分路进军,节节获胜,再西进数百里,即是花剌子模的名城撒麻尔罕。成吉思汗哨探获悉,此城是花剌子模的新都,结集重兵十余万守御,城精粮足……
成吉思汗自进军花剌子模以来,从无如此大败,当晚在帐中悲痛爱孙之亡,怒如雷霆。郭靖回帐翻阅《武穆遗书》,要想学一个攻城之法,但那撒麻尔罕的城防与中国大异,遗书所载的战法均无用处……
郭靖正欲说出辞婚之事,忽听得远处传来……,只道城中投降了的花剌子模军民突然起事,……。成吉思汗笑道:“没事,没事。这狗城不服天威,累得我损兵折将,又害死了我爱孙,须得大大洗屠一番。大家都去瞧瞧。”
丘处机:城中常十余万户,国破以来,存者四之一
耶律楚材:寂莫河中府,声名昔日闻,城隍连畎亩,市井半邱坟
阿尔·花拉子米:9 世纪波斯数学家、天文学家、地理学家
引入欧洲
词汇演化
运行在计算设备上的各种 App (算法) 已经接管生活
良定义的计算过程,把输入转换成输出的计算步骤序列
算法是使用计算机求解问题的精确有效方法的代名词
两个没有刻度的水桶,总容量分别为 9 升和 6 升,如何量出 3 升的水?
两个没有刻度的水桶,总容量分别为 9 升和 6 升,如何量出 3 升的水?
算法:
如果总容量分别为 7 升和 5 升,如何量出 1 升的水?
两个没有刻度的水桶,总容量分别为 7 升和 5 升,如何量出 1 升的水?
算法:
两个没有刻度的水桶,总容量分别为$a$升和$b$升,是否可以量出$t$升的水?如果可以,请给出具体方案,其中$a,b,t \in \Zbb_+$
思路:装满对应$+a$或$+b$,倒掉对应$-a$或$-b$
是否可以通过若干次$+a$、$+b$、$-a$、$-b$得到$t$?即是否存在整数$x$、$y$使得$ax + by = t$?
前面的例子:$5 \cdot 3 - 7 \cdot 2 = 1$
$t$必须是$a,b$最大公约数的倍数,若$t=1$,则$a,b$必须互素
问题 1:求最大公约数 (greatest common divisor, gcd)
输入$a, b \in \Zbb_+$,输出整数$d=\gcd(a,b)$
问题 2:求待定系数
输入$a, b \in \Zbb_+$且$\gcd(a,b)=1$,输出整数$x,y$使得$ax+by=1$
从$1$遍历到$\min(a,b)$,最大能同时整除$a,b$的数就是最大公约数
def gcd(a, b): for i in range(1, min(a, b) + 1): # 最大公约数不会大于两者中的较小者 if ((a % i == 0) and (b % i == 0)): # 同时整除即为公约数 gcd = i return gcd
对$x$从$1$遍历到$b$,检测$y$是否可同时为整数
def coef(a, b): for x in range(1, b): # 0 a 2a ... (b-1)a构成一个模b的剩余系 if (1 - a * x) % b == 0: # 若y也为整数 y = int((1 - a * x) / b) return x, y
回答三个问题
记$a / b = q \cdots r$且$r < b$,于是$\gcd(a,b) \mid r$
$\gcd(a,b) = \gcd(b,r)$,不断令$(a,b) = (b,r)$直到$r = 0$即能整除
def Euclidean(a, b): # 辗转相除 while a % b: a, b = b, a % b return b def Euclidean_coef(a, b): # 辗转相除 if a % b == 0: # 递归停止条件:若b可以整除a return b, 1, 1 - int(a / b) else: d, x, y = Euclidean_coef(b, a % b) return d, y, x - int(a / b) * y
$\gcd(a,b) = \gcd(a-b,b)$,不断令$(a,b) = (a-b,b)$直到$a=b$
def gxjs(a, b): # 更相减损 while True: if a > b: a = a - b elif a < b: b = b - a else: return b def gxjs_coef(a, b): # 更相减损 if a == b: # 递归停止条件:a = b return b, 1, 0 elif a > b: d, x, y = gxjs_coef(a - b, b) return d, x, y - x else: d, x, y = gxjs_coef(a, b - a) return d, x - y, y
对$a$、$b$分奇偶性讨论
def gxjs2(a, b): # 改进的更相减损 if a == b: return a while True: if not (a & 1) and not (b & 1): # 均为偶 gcd(a,b) = 2 * gcd(a/2, b/2) return gxjs2(a >> 1, b >> 1) << 1 elif not (a & 1) and (b & 1): # a偶 b奇 gcd(a,b) = gcd(a/2, b) return gxjs2(a >> 1, b) elif (a & 1) and not (b & 1): # a奇 b偶 gcd(a,b) = gcd(a, b/2) return gxjs2(a, b >> 1) else: # 均为奇 更相减损 gcd(a,b) = gcd(a-b, b) if a > b: return gxjs2(a - b, b) else: return gxjs2(a, b - a)
迭代轮数约为$\log(\max(a, b))$,同辗转相除,但避免了取模运算
四种方法
方法 | 迭代轮数 | 每轮操作 |
---|---|---|
暴力穷举法 | $\min(a, b)$ | 2 次取模运算 |
辗转相除法 | 约$\log(\max(a,b))$ | 1 次取模运算 |
更相减损术 | 最坏$\max(a, b)$ | 无取模运算 |
改进的更相减损术 | 约$\log(\max(a,b))$ | 无取模运算 |
输入:数组$a = \langle a_1, a_2, \ldots, a_n \rangle$
输出:$a$的元素的重排列$\langle a'_1, a'_2, \ldots, a'_n \rangle$且$a'_1 \le a'_2 \le \cdots \le a'_n$
借助这个最基本的问题,我们详细展示
冒泡排序、选择排序、插入排序、归并排序、快速排序
相邻两个元素比较,如果前者大于后者,则交换
def bubble_sort(a, n): for i in range(n-1): for j in range(n-1, i, -1): if a[j] < a[j-1]: a[j], a[j-1] = a[j-1], a[j]
前两轮外层循环
| 1 | 2 | 3 | 4 | 5 | 6 | | | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(1) | 5 | 2 | 4 | 6 | 1 | 3 | | (6) | 1 | 5 | 2 | 4 | 3 | 6 |
(2) | 5 | 2 | 4 | 1 | 6 | 3 | | (7) | 1 | 5 | 2 | 3 | 4 | 6 |
(3) | 5 | 2 | 1 | 4 | 6 | 3 | | (8) | 1 | 5 | 2 | 3 | 4 | 6 |
(4) | 5 | 1 | 2 | 4 | 6 | 3 | | (9) | 1 | 2 | 5 | 3 | 4 | 6 |
(5) | 1 | 5 | 2 | 4 | 6 | 3 | | | | | | | | |
def bubble_sort(a, n): for i in range(n-1): for j in range(n-1, i, -1): if a[j] < a[j-1]: a[j], a[j-1] = a[j-1], a[j]
如何证明算法的正确性?
循环不变式 (loop-invariant):一个满足如下三条性质的命题
内层循环:每次进入循环前,子数组$a[j, \ldots, n-1]$的第一个元素最小
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素
def bubble_sort(a, n): for i in range(n-1): for j in range(n-1, i, -1): if a[j] < a[j-1]: a[j], a[j-1] = a[j-1], a[j]
内层循环:每次进入循环前,子数组$a[j, \ldots, n-1]$的第一个元素最小
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素
外层循环不变式证明依赖内层的终止情况,先证明内层
def bubble_sort(a, n): for i in range(n-1): for j in range(n-1, i, -1): if a[j] < a[j-1]: a[j], a[j-1] = a[j-1], a[j]
内层循环:每次进入循环前,子数组$a[j, \ldots, n-1]$的第一个元素最小
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素
bubble_sort(a, n): | 时间 | 次数 |
---|---|---|
for i in range(n-1): | $c_1, c'_1$ | 对$i$赋值$1$次、自增$n-1$次 |
$c''_1$ | $i$与$n-1$比较$n$次 | |
for j in range(n-1, i, -1): | $c_2, c'_2$ | 对$j$赋值$n-1$次、自减$\frac{n^2-n}{2}$次 |
$c''_2$ | $j$与$i$比较$\frac{n^2+n-2}{2}$次 | |
if a[j] < a[j-1]: | $c_3$ | 比较$\frac{n^2-n}{2}$次 |
a[j], a[j-1] = a[j-1], a[j] | $c_4$ | 交换$\sum_{i=0}^{n-2} t_i$次 |
$$ \begin{align*} \quad & T = c_1 + c'_1 (n-1) + c''_1 n + c_2 (n-1) + c'_2 \frac{n^2-n}{2} + c''_2 \frac{n^2+n-2}{2} \\ & \qquad \quad + c_3 \frac{n^2-n}{2} + c_4 \sum_{i=0}^{n-2} t_i = a n^2 + b n + c + c_4 \sum_{i=0}^{n-2} t_i \end{align*} $$
$t_i$是外层循环第$i$轮中内层循环$a[j]$和$a[j-1]$交换的次数
$$ \begin{align*} \qquad & a n^2 + b n + c \le T \le a n^2 + b n + c + c_4 \sum_{i=0}^{n-2} (n-1-i) \\ & \qquad \Longrightarrow T \in \Theta(n^2), ~ \text{最坏情况下交换次数} \in \Theta(n^2) \end{align*} $$
算法 | 最坏情况下运行时间 | 最好情况下运行时间 | 最坏情况下交换次数 |
---|---|---|---|
冒泡排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n^2)$ |
最小元与第一个元素互换,次小元与第二个元素互换,……
def selection_sort(a, n): for i in range(n-1): smallest = i for j in range(i+1, n): if a[j] < a[smallest]: smallest = j a[i], a[smallest] = a[smallest], a[i]
算法 | 最坏情况下运行时间 | 最好情况下运行时间 | 最坏情况下交换次数 |
---|---|---|---|
冒泡排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n^2)$ |
选择排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n)$ |
def selection_sort(a, n): for i in range(n-1): smallest = i for j in range(i+1, n): if a[j] < a[smallest]: smallest = j a[i], a[smallest] = a[smallest], a[i]
内层循环:每次进入循环前,$a[\text{smallest}]$是子数组$a[i, \ldots, j-1]$的最小元素
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素,与冒泡排序相同
内层循环:
def selection_sort(a, n): for i in range(n-1): smallest = i for j in range(i+1, n): if a[j] < a[smallest]: smallest = j a[i], a[smallest] = a[smallest], a[i]
内层循环:每次进入循环前,$a[\text{smallest}]$是子数组$a[i, \ldots, j-1]$的最小元素
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素,与冒泡排序相同
外层循环保持:若子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素,又$a[\text{smallest}]$是子数组$a[i, \ldots, n-1]$的最小元素,经过第 7 行的交换,$a[i]$是$a[i, \ldots, n-1]$的最小元素,因此子数组$a[0, \ldots, i]$有序排列着整个数组$a[]$的最小的$i+1$个元素
在前半部分构建有序子数组,将后半部分的未排序元素插入其中
def insertion_sort(a, n): for i in range(1, n): key = a[i] j = i - 1 while j >= 0 and key < a[j]: a[j+1] = a[j] j -= 1 a[j+1] = key
| 1 | 2 | 3 | 4 | 5 | 6 | | | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(1) | 5 | 2 | 4 | 6 | 1 | 3 | | (2) | 2 | 5 | 4 | 6 | 1 | 3 |
| | | | | | | | | | | | | | |
(3) | 2 | 4 | 5 | 6 | 1 | 3 | | (4) | 2 | 4 | 5 | 6 | 1 | 3 |
| | | | | | | | | | | | | | |
(5) | 1 | 2 | 4 | 5 | 6 | 3 | | (6) | 1 | 2 | 3 | 4 | 5 | 6 |
def insertion_sort(a, n): for i in range(1, n): key = a[i] j = i - 1 while j >= 0 and key < a[j]: a[j+1] = a[j] j -= 1 a[j+1] = key
内层循环:每次进入循环前,$\text{key} \le a[j+1]$
外层循环:每次进入循环前,子数组$a[0, \ldots, i-1]$已经排好序
insertion_sort(a, n): | 时间 | 次数 |
---|---|---|
for i in range(1, n): | $c_1, c'_1$ | 对$i$赋值$1$次、自增$n-1$次 |
$c''_1$ | $i$与$n$比较$n$次 | |
key = a[i] | $c_2$ | 对$key$赋值$n-1$次 |
j = i - 1 | $c_3$ | 对$j$赋值$n-1$次 |
while j >= 0 and key < a[j]: | $c_4,c'_4$ | 比较$2 \sum_{i=1}^{n-1} t_i + \{1,2\}$次 |
a[j+1] = a[j] | $c_5$ | 赋值$\sum_{i=1}^{n-1} t_i$次 |
j -= 1 | $c_6$ | 对$j$赋值$\sum_{i=1}^{n-1} t_i$次 |
a[j+1] = key | $c_7$ | 赋值$n-1$次 |
$$ \begin{align*} \qquad T = b n + c + d \sum_{i=1}^{n-1} t_i \end{align*} $$
$t_i$是外层 for 循环第$i$轮中内层 while 循环的执行次数
$$ \begin{align*} \qquad b n + c \le b n + c + d \sum_{i=1}^{n-1} t_i \le b n + c + d \cdot \frac{n^2-n}{2} \end{align*} $$
算法 | 最坏情况下运行时间 | 最好情况下运行时间 | 最坏情况下交换次数 |
---|---|---|---|
冒泡排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n^2)$ |
选择排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n)$ |
插入排序 | $\Theta(n^2)$ | $\Theta(n)$ | $\Theta(n^2)$ |
分治法:将原问题分解为类似原问题的子问题进行递归求解
def merge_sort(a, l, r): if l < r: m = int((l+r)/2) # 取中间点分开 merge_sort(a, l, m) merge_sort(a, m+1, r) merge(a, l, m, r)
def merge_sort(a, l, r): if l < r: m = int((l+r)/2) # 取中间点分开 merge_sort(a, l, m) merge_sort(a, m+1, r) merge(a, l, m, r)
合并:取两个子数组的最小元素做比较,并将小者取出
def merge(a, l, m, r): # 子数组的长度 n1, n2 = m - l + 1, r - m # 创建临时数组 L, R = [0] * n1, [0] * n2 # 拷贝数据到临时数组L for i in range(0, n1): L[i] = a[l + i] # 拷贝数据到临时数组R for j in range(0, n2): R[j] = a[m + 1 + j] i, j, k = 0, 0, l # 归并L和R到a[l..r] while i < n1 and j < n2: if L[i] <= R[j]: a[k] = L[i] i += 1 else: a[k] = R[j] j += 1 k += 1 # 拷贝L的剩余元素 while i < n1: a[k] = L[i] i += 1 k += 1 # 拷贝R的剩余元素 while j < n2: a[k] = R[j] j += 1 k += 1
设排序长度为$n$的数组的时间为$T(n)$,则有递推式
$$ \begin{align*} \qquad T(n) = 2 \cdot T \left( \frac{n}{2} \right) + \underbrace{\Theta(n)}_{\text{合并}} \end{align*} $$
根据主方法可得$T(n) \in \Theta(n \lg n)$
算法 | 最坏情况下运行时间 | 最好情况下运行时间 | 最坏情况下交换次数 |
---|---|---|---|
冒泡排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n^2)$ |
选择排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n)$ |
插入排序 | $\Theta(n^2)$ | $\Theta(n)$ | $\Theta(n^2)$ |
归并排序 | $\Theta(n \lg n)$ | $\Theta(n \lg n)$ | $\Theta(n \lg n)$ |
分治法:将原问题分解为类似原问题的子问题进行递归求解
def quick_sort(a, l, r): if l < r: m = partition(a, l, r) quick_sort(a, l, m-1) quick_sort(a, m+1, r)
def quick_sort(a, l, r): if l < r: m = partition(a, l, r) quick_sort(a, l, m-1) quick_sort(a, m+1, r)
def partition(a, l, r): # 最右元素作为主元 pivot = a[r] # 小于主元的元素的存放位置 初始为最左 i = l # l -> r-1 遍历其他元素 for j in range(l, r): if a[j] <= pivot: # 小于主元的元素放到主元左边 a[i], a[j] = a[j], a[i] i += 1 # 存放位置右移一位 # 所有小于主元的元素已位于主元左边 # 当前的i就是主元应该放的位置 # 当前的a[i]大于主元 a[i], a[r] = a[r], a[i] return i
设排序长度为$n$的数组的时间为$T(n)$,则有递推式
$$ \begin{align*} \quad T(n) = T(k) + T(n-1-k) + \underbrace{\Theta(n)}_{\text{与主元比较}} \end{align*} $$
如何改进?
算法 | 最坏情况下运行时间 | 最好情况下运行时间 | 最坏情况下交换次数 |
---|---|---|---|
冒泡排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n^2)$ |
选择排序 | $\Theta(n^2)$ | $\Theta(n^2)$ | $\Theta(n)$ |
插入排序 | $\Theta(n^2)$ | $\Theta(n)$ | $\Theta(n^2)$ |
归并排序 | $\Theta(n \lg n)$ | $\Theta(n \lg n)$ | $\Theta(n \lg n)$ |
快速排序 | $\Theta(n^2)$ | $\Theta(n \lg n)$ | $\Theta(n^2)$ |
除最坏/最好情况分析,还有平均情况分析
我们通常更关心算法在最坏情况下的运行时间
$\Theta(n \lg n)$和$\Theta(n^2)$的差别有多大?
设对长度一千万的数组进行排序,$n = 10^7$
CPU | 算法 | 时间复杂度 | |
---|---|---|---|
计算机 A | $10^{10}$条指令/秒 | 插入排序 | $2 \cdot n^2$ |
计算机 B | $10^7$条指令/秒 | 归并排序 | $50 \cdot n \lg n$ |
$$ \begin{align*} \qquad & \frac{2 \cdot (10^7)^2 \text{ instructions}}{10^{10} \text{ instructions/s}} = 20000 \text{ s} > 5.5 \text{ h} \\ & \frac{50 \cdot 10^7 \lg 10^7 \text{ instructions}}{10^7 \text{ instructions/s}} \approx 1163 \text{ s} < 20 \text{ m} \end{align*} $$
像对待计算机硬件一样把算法看成是一种技术
如果计算机计算存储资源无限,那么我们还需研究算法吗?
回答三个问题
课外阅读:算法导论 4th 前两章
习题:1.2-2、1.2-3、2.1-3、2.1-4