算法设计与分析


绪论

计算机学院    张腾

tengzhang@hust.edu.cn

课程概况

授课:张腾 tengzhang@hust.edu.cn

地点:西十二楼 N504

32 学时

  • 第 9 ~ 12 周:周一 7 ~ 8 节课、周三 7 ~ 8 节课
  • 第 13 ~ 16 周:周一 3 ~ 4 节课、周三 5 ~ 6 节课

考核:闭卷考试 (70%)、平时作业 (30%)

主页:https://avanti1980.github.io/course-alg/

参考书目

本讲目标

回答三个问题

是什么 what

为什么 why

怎么样 how

什么是算法

什么是算法

要把大象装冰箱,拢共分几步?

  1. 把冰箱门打开
  2. 把大象装进去
  3. 把冰箱门带上

通俗的讲,算法是完成一个任务所需的一系列步骤

上课算法

  1. 锁门离开宿舍房间
  2. 骑自行车到西十二
  3. 到教室找位置坐下

刷牙算法:挤牙膏到牙刷上,牙刷贴到牙齿上,上下移动 N 秒

Algorithm一词的由来

阿尔·花拉子米:9 世纪波斯数学家、天文学家、地理学家

  • 穆罕默德·伊本·穆萨·阿尔·花拉子米
  • Muḥammad ibn Mūsā al-Khwārizmī
  • محمد بن موسی خوارزمی
Algorithm一词的由来

《射雕英雄传》第三十七回 从天而降

原来蒙古大军分路进军,节节获胜,再西进数百里,即是花剌子模的名城撒麻尔罕。成吉思汗哨探获悉,此城是花剌子模的新都,结集重兵十余万守御,城精粮足……

成吉思汗自进军花剌子模以来,从无如此大败,当晚在帐中悲痛爱孙之亡,怒如雷霆。郭靖回帐翻阅《武穆遗书》,要想学一个攻城之法,但那撒麻尔罕的城防与中国大异,遗书所载的战法均无用处……

郭靖正欲说出辞婚之事,忽听得远处传来……,只道城中投降了的花剌子模军民突然起事,……。成吉思汗笑道:“没事,没事。这狗城不服天威,累得我损兵折将,又害死了我爱孙,须得大大洗屠一番。大家都去瞧瞧。”

丘处机:城中常十余万户,国破以来,存者四之一

耶律楚材:寂莫河中府,声名昔日闻,城隍连畎亩,市井半邱坟

Algorithm一词的由来

阿尔·花拉子米:9 世纪波斯数学家、天文学家、地理学家

  • 早年游学印度
  • 于公元 825 年写成《印度数字算术》:十进制记数法、基础算术

引入欧洲

  • 意大利数学家斐波那契前往阿拉伯地区学习,约于 1200 年回国
  • 名字和算术均被翻译成拉丁语 Algorismus,数字误解为阿拉伯数字

词汇演化

  • 拉丁语算术 Algorismus => 英语算术 Algorism [ˈælɡəˌrɪzəm]
  • 英语算术 Algorism 被 Arithmetic [əˈrɪθmətɪk] 替代
  • Algorism 的异体词 Algorithm [ˈælɡərɪðəm] => 计算机专业术语“算法”
计算机算法

运行在计算设备上的各种 App (算法) 已经接管生活

计算机算法

良定义的计算过程,把输入转换成输出的计算步骤序列

g 输入 输入 算法 算法 输入->算法 输出 输出 算法->输出

  • 步骤序列是有穷的
  • 输入输出由待求解的问题决定

算法是使用计算机求解问题的精确有效方法的代名词

量水问题

两个没有刻度的水桶,总容量分别为 9 升和 6 升,如何量出 3 升的水?

量水问题

两个没有刻度的水桶,总容量分别为 9 升和 6 升,如何量出 3 升的水?

算法:

  • 将 9 升的桶装满水
  • 向 6 升的桶倒水,直到满
  • 9 升桶中剩下的即为 3 升

如果总容量分别为 7 升和 5 升,如何量出 1 升的水?

量水问题

两个没有刻度的水桶,总容量分别为 7 升和 5 升,如何量出 1 升的水?

算法:

  • 将 5 升的桶装满水,5/5、0/7
  • 将 5 升的桶中的 5 升水全部倒入 7 升的桶中,0/5、5/7
  • 将 5 升的桶装满水,5/5、5/7
  • 向 7 升的桶倒水,直到满,此时 5 升的桶中还剩 3 升水,3/5、7/7
  • 将 7 升的桶中的水倒掉,3/5、0/7
  • 将 5 升的桶中的 3 升水倒入 7 升的桶中,0/5、3/7
  • 将 5 升的桶装满水,5/5、3/7
  • 向 7 升的桶倒水,直到满,此时 5 升的桶中还剩 1 升水,1/5、7/7
量水问题 一般化

两个没有刻度的水桶,总容量分别为$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
本讲目标

回答三个问题

是什么 what

为什么 why

怎么样 how

欧几里得 辗转相除法

$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
  • 暴力穷举法迭代轮数为$\min(a, b)$,每轮做$2$次取模运算
  • 辗转相除法迭代轮数约为$\log(\max(a,b))$,每轮做$1$次取模运算
九章算术 更相减损术

$\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
  • 避免了低效的取模运算,只做加减法
  • 最坏情况下迭代轮数为$\max(a, b)$,例如$\gcd(10000,1)$
改进的更相减损术

$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):一个满足如下三条性质的命题

  1. 初始:在初次进入循环前成立
  2. 保持:若每次进入循环前成立,则下次进入循环前还成立
  3. 终止:循环可以终止,此时循环不变式提供一个有用的性质

内层循环:每次进入循环前,子数组$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$个元素

外层循环不变式证明依赖内层的终止情况,先证明内层

  1. 初始:$j = n-1$,子数组$a[n-1]$只有一个元素,循环不变式显然成立
  2. 保持:若$a[j]$$a[j, \ldots, n-1]$中最小的,循环中比较$a[j-1]$$a[j]$并将小者置于前,则再次进入循环前$a[j-1, \ldots, n-1]$的第一个元素最小
  3. 终止:$j = i$,此时子数组$a[i, \ldots, n-1]$第一个元素最小
冒泡排序 正确性

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$个元素

  1. 初始:$i=0$,子数组$a[0, \ldots, -1]$为空,循环不变式显然成立
  2. 保持:若子数组$a[0, \ldots, i-1]$有序排列着整个数组$a[]$的最小的$i$个元素,又内层循环终止时$a[i, \ldots, n-1]$的第一个元素最小,因此子数组$a[0, \ldots, i]$亦有序排列着整个数组$a[]$的最小的$i+1$个元素
  3. 终止:$i = n-1$,此时子数组$a[0, \ldots, n-2]$有序排列着整个数组$a[]$的最小的$n-1$个元素,故剩余的$a[n-1]$是最大元素,从而整个数组$a[]$已排好序
冒泡排序 时间分析

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]$交换的次数

  • 最好情况下(初始顺序)发生$0$
  • 最坏情况下(初始逆序)发生$n-1-i$

$$ \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*} $$

  • 顺序结构中的语句只执行$1$
  • 循环结构中的语句执行次数等于循环被执行的次数,一重循环中的语句可以产生$n$的线性项,二重循环中的语句可以产生$n$的平方项
算法 最坏情况下运行时间 最好情况下运行时间 最坏情况下交换次数
冒泡排序 $\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]
  • 第 5 行的比较操作在二重循环里,因此算法总时间$\in \Theta(n^2)$
  • 第 7 行的交换操作在一重循环里,因此最坏情况下交换次数$\in \Theta(n)$
算法 最坏情况下运行时间 最好情况下运行时间 最坏情况下交换次数
冒泡排序 $\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$个元素,与冒泡排序相同

内层循环:

  1. 初始:$j=i+1$,子数组$a[i, \ldots, j-1]$只有$a[\text{smallest}]$,循环不变式成立
  2. 保持:若$a[\text{smallest}]$是子数组$a[i, \ldots, j-1]$的最小元素,经过第 5 ~ 6 行比较后,$a[\text{smallest}]$将是子数组$a[i, \ldots, j]$的最小元素
  3. 终止:$j = n$$a[\text{smallest}]$$a[i, \ldots, n-1]$的最小元素
选择排序 正确性

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]$已经排好序

  1. 初始:子数组$a[0]$只有一个元素,已经排好序
  2. 保持:内层循环终止时,要么$j=-1$$\text{key} < a[0]$,故令$a[0] = \text{key}$;要么$\text{key} > a[j]$(注意同时有$\text{key} \le a[j+1]$),故令$a[j+1] = \text{key}$,无论哪种情况都为$a[i]$在已经排好序的子数组$a[0, \ldots, i-1]$中找到了正确的插入位置,故下次进入循环前$a[0, \ldots, i]$已经排好序
  3. 终止:$i = n$$a[0, \ldots, n-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 循环的执行次数

  • 最好情况下(初始顺序)执行$0$
  • 最坏情况下(初始逆序)执行$i$

$$ \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)$
归并排序

分治法:将原问题分解为类似原问题的子问题进行递归求解

  • 分解:将子数组$a[l,\ldots,r]$分成$a[l,\ldots,m]$$a[m+1,\ldots,r]$两部分,其中$m$$(l+r)/2$取整
  • 解决:分别对子数组$a[l,\ldots,m]$$a[m+1,\ldots,r]$进行递归排序
  • 合并:将分别排好序的$a[l,\ldots,m]$$a[m+1,\ldots,r]$合并成$a[l,\ldots,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)
  • 分解:第 3 行计算分解的中间点
  • 解决:第 4 ~ 5 行对子数组递归调用归并排序
  • 合并:第 6 行合并已排好序的两个子数组
归并排序

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)

g n1 5 2 4 6 1 3 n2 5 2 4 n1->n2 n3 6 1 3 n1->n3 n4 5 2 n2->n4 n5 4 n2->n5 n7 3 n3->n7 n6 6 1 n3->n6 n8 5 n4->n8 n9 2 n4->n9 n14 2 4 5 n5->n14 n15 1 3 6 n7->n15 n10 6 n6->n10 n11 1 n6->n11 n12 2 5 n8->n12 n9->n12 n13 1 6 n10->n13 n11->n13 n12->n14 n13->n15 n16 1 2 3 4 5 6 n14->n16 n15->n16

归并排序

合并:取两个子数组的最小元素做比较,并将小者取出

g n1 (1) 2 4 5 1 3 6 1 (2) 2 4 5 1 3 6 1 2 (3) 2 4 5 1 3 6 1 2 3 (4) 2 4 5 1 3 6 1 2 3 4 (5) 2 4 5 1 3 6 1 2 3 4 5 (6) 2 4 5 1 3 6 1 2 3 4 5 6

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)$
快速排序

分治法:将原问题分解为类似原问题的子问题进行递归求解

  • 分解:将最后一个元素作为主元并确定其在$a[]$中的正确位置$m$,将小/大于主元的元素分别挪到主元的左/右边
  • 解决:分别对子数组$a[l,\ldots,m-1]$$a[m+1,\ldots,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)
  • 分解:第 3 行计算主元的位置
  • 解决:第 4 ~ 5 行对子数组递归调用快速排序
快速排序

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)

g n1 5 2 4 6 1 3 n2 2 1 3 6 5 4 n1->n2 n3 2 1 n2->n3 n4 3 n2->n4 n5 6 5 4 n2->n5 n6 1 2 n3->n6 n7 4 5 6 n5->n7 n8 1 n6->n8 n9 2 n6->n9 n10 4 n7->n10 n11 5 6 n7->n11 n12 5 6 n11->n12 n13 5 n12->n13 n14 6 n12->n14

快速排序

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

g n1 ij 5 2 4 6 1 3 n2 i j 5 2 4 6 1 3 n1->n2 n3 ij 2 5 4 6 1 3 n2->n3 n5 i j 2 5 4 6 1 3 n3->n5 n6 i j 2 1 4 6 5 3 n5->n6 n7 i j 2 1 3 6 5 4 n6->n7

快速排序 时间分析

设排序长度为$n$的数组的时间为$T(n)$,则有递推式

$$ \begin{align*} \quad T(n) = T(k) + T(n-1-k) + \underbrace{\Theta(n)}_{\text{与主元比较}} \end{align*} $$

  • 最好情况:主元是中位数,$T(n) = 2 \cdot T (n/2) + \Theta(n)$,同归并排序
  • 最坏情况:主元是最大/小元素,$T(n) = T (n-1) + \Theta(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)$
快速排序 $\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*} $$

为什么研究算法

像对待计算机硬件一样把算法看成是一种技术

  • 选择更快的硬件可以提升总体性能
  • 运行更好的算法也可以提升总体性能,甚至可以成为胜负手

如果计算机计算存储资源无限,那么我们还需研究算法吗?

  • 硬件的计算能力虽然日新月异,但终归不可能无限
  • 大数据时代问题的规模越来越大,算法间的效率差异也越来越显著
  • 有一个好的算法基础,可以做更多的事情,同时也能做得更好
本讲目标

回答三个问题

是什么 what

为什么 why

怎么样 how

课程大纲

算法设计与分析分治法动态规划贪心法回溯法分支限界法迭代改进归并排序 快速排序最大子数组 最近点对矩阵加法乘法Strassen矩阵乘法代入法 递归树 主方法

作业

课外阅读:算法导论 4th 前两章

习题:1.2-2、1.2-3、2.1-3、2.1-4