算法导论第三版代码python实现与部分习题答案-第七章:快速排序(一)

第七章 快速排序

7.1 快速排序的描述

算法思想

快速排序(QuickSort)采用分治策略对数组进行排序,包含三个步骤:

  • 分解:将数组 $ A[p…r] $ 划分为两个子数组 $ A[p…q-1] $ 和 $ A[q+1…r] $,使得:

    • $ A[p…q-1] $ 中所有元素 ≤ 主元 $ A[q] $
    • $ A[q+1…r] $ 中所有元素 ≥ 主元 $ A[q] $
    • 主元 $ A[q] $ 处于其最终排序位置
    • 划分过程由 PARTITION 实现
  • 解决:递归地对两个子数组 $ A[p…q-1] $ 和 $ A[q+1…r] $ 进行快速排序

  • 合并:无需额外操作,排序在原数组上完成,子数组有序后整个数组即有序

算法实现

def partition(A, p, r):
    """
    将子数组 A[p..r] 按主元划分成两部分:

   - 左侧:所有元素 ≤ 主元
     - 右侧:所有元素 > 主元
       主元为 A[r],最终放置在正确位置并返回其索引。
	参数:
	A: 待划分的数组
	p: 子数组起始索引(包含)
	r: 子数组结束索引(包含)

	返回值:
	主元的最终位置(索引)
	"""
	x = A[r]        # 选择最后一个元素作为主元
	i = p - 1       # i 指向小于等于主元区域的最后一个位置

	# 遍历从 p 到 r-1 的每个元素
	for j in range(p, r):
    	# 如果当前元素小于等于主元
    	if A[j] <= x:
        	i += 1  # 扩展小于等于区域
        	A[i], A[j] = A[j], A[i]  # 将 A[j] 交换到左侧区域

	# 将主元 A[r] 放置到正确位置(i+1)
	A[i + 1], A[r] = A[r], A[i + 1]

	return i + 1  # 返回主元的索引

def quicksort(A, p, r):
    """
    快速排序主函数:对数组 A[p..r] 进行原址排序。
	参数:
	A: 待排序数组
	p: 当前排序区间的起始索引
	r: 当前排序区间的结束索引
	"""
	if p < r:  # 基本情况:当 p >= r 时,子数组长度 <= 1,无需排序
    	q = partition(A, p, r)      # 划分数组,q 为主元的最终位置
    	quicksort(A, p, q - 1)      # 递归排序左半部分 A[p..q-1]
    	quicksort(A, q + 1, r)      # 递归排序右半部分 A[q+1..r]
    

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数组A = [2, 8, 7, 1, 3, 5, 6, 4],调用partition(A, 0, 7)

  1. 初始化:主元x = A[7] = 4, i = -1, p = j = 0
    (设置主元为最后一个元素,i初始化为小于等于区域的边界)

  2. j = 0: A[0] = 2 ≤ 4,i = -1 → 0,交换A[0]和A[0] → [2, 8, 7, 1, 3, 5, 6, 4]
    (元素2小于等于主元4,扩展小于等于区域,与自身交换无变化)

  3. j = 1: A[1] = 8 > 4,i保持0 → [2, 8, 7, 1, 3, 5, 6, 4]
    (元素8大于主元4,不扩展小于等于区域,i保持不变)

  4. j = 2: A[2] = 7 > 4,i保持0 → [2, 8, 7, 1, 3, 5, 6, 4]
    (元素7大于主元4,不扩展小于等于区域,i保持不变)

  5. j = 3: A[3] = 1 ≤ 4,i = 0 → 1,交换A[1]和A[3] → [2, 1, 7, 8, 3, 5, 6, 4]
    (元素1小于等于主元4,扩展小于等于区域,交换A[1]和A[3])

  6. j = 4: A[4] = 3 ≤ 4,i = 1 → 2,交换A[2]和A[4] → [2, 1, 3, 8, 7, 5, 6, 4]
    (元素3小于等于主元4,扩展小于等于区域,交换A[2]和A[4])

  7. j = 5: A[5] = 5 > 4,i保持2 → [2, 1, 3, 8, 7, 5, 6, 4]
    (元素5大于主元4,不扩展小于等于区域,i保持不变)

  8. j = 6: A[6] = 6 > 4,i保持2 → [2, 1, 3, 8, 7, 5, 6, 4]
    (元素6大于主元4,不扩展小于等于区域,i保持不变)

  9. 最后:交换A[3]和A[7] → [2, 1, 3, 4, 7, 5, 6, 8]
    (将主元4放到正确位置,即i+1的位置)

  10. 返回:q = 3
    (返回主元的最终位置索引)

划分过程的循环不变量分析

循环不变量

for 循环的每次迭代开始时,对于子数组 $ A[p…r] $ 和主元 $ x = A[r] $,以下性质成立:

  1. 若 $ p \leq k \leq i $,则 $ A[k] \leq x $
    → 区域 $ A[p…i] $ 包含所有已处理且小于等于主元的元素

  2. 若 $ i+1 \leq k \leq j-1 $,则 $ A[k] > x $
    → 区域 $ A[i+1…j-1] $ 包含所有已处理且大于主元的元素

  3. $ A[r] = x $
    → 主元始终保留在末尾,尚未参与交换

未处理区域为 $ A[j…r-1] $,其元素尚未分类。


  1. 初始化(Initialization)

在第一次迭代前:

  • $ i = p - 1 $
  • $ j = p $

此时:

  • 区域 $ A[p…i] = A[p…p-1] $ 为空 → 条件 1 成立(空命题为真)
  • 区域 $ A[i+1…j-1] = A[p…p-1] $ 为空 → 条件 2 成立
  • $ A[r] = x $ 由第 1 行 x = A[r] 赋值保证 → 条件 3 成立

因此,循环不变量在初始时成立。


  1. 保持(Maintenance)

假设在某次迭代开始前循环不变量成立。根据 A[j]x 的比较,分两种情况:

情况一:$ A[j] > x $

  • 不修改 i
  • 执行 j = j + 1
  • 原来的 $ A[j] $ 现在位于 $ A[j-1] $ 的位置
  • 该值 > x,属于区域 $ A[i+1…j-1] $
  • 区域 $ A[p…i] $ 不变
  • 主元 $ x $ 仍位于末尾

→ 所有三个条件仍满足,循环不变量保持。

情况二:$ A[j] \leq x $

  • 执行 i = i + 1,扩展左侧区域边界
  • 交换 A[i]A[j]
    • 原 $ A[j] $(≤ x)被移至 $ A[i] $,进入左侧区域
    • 原 $ A[i] $ 被移至 $ A[j] $,仍在未处理区或即将被处理
  • 执行 j = j + 1

更新后:

  • $ A[p…i] $ 仍包含所有 ≤ x 的已处理元素
  • $ A[i+1…j-1] $ 包含所有 > x 的已处理元素
  • $ A[r] = x $

→ 循环不变量在操作后继续保持。

两种情况下,循环不变量均得以维持。


  1. 终止(Termination)

for 循环结束时,$ j = r $(因为循环遍历 $ j = p $ 到 $ r-1 $)

此时:

  • 未处理区域 $ A[j…r-1] = A[r…r-1] $ 为空
  • 所有元素 $ A[p…r-1] $ 都已被划分到:
    • $ A[p…i] $:≤ x
    • $ A[i+1…r-1] $:> x
  • $ A[r] = x $

接着执行:

  • 第 7 行:exchange A[i+1] with A[r]
    将主元插入到 $ i+1 $ 位置,恰好位于两个区域之间
  • 返回 $ q = i+1 $

最终结构:

  • $ A[p…q-1] $:所有元素 ≤ $ A[q] $
  • $ A[q] = x $
  • $ A[q+1…r] $:所有元素 > $ A[q] $

更严格地说,若所有元素互异,则 $ A[q] < A[q+1…r] $

因此,PARTITION 正确实现了划分。


时间复杂度

PARTITION 过程中:

  • for 循环执行 $ r - p $ 次
  • 每次操作为常数时间
  • 因此时间复杂度为 $ \Theta(n) $,其中 $ n = r - p + 1 $

结论

通过循环不变量的三要素(初始化、保持、终止)分析,证明了 PARTITION 过程的正确性:

  • 每次迭代维护了元素的分类性质
  • 循环结束后,主元被放置在正确位置
  • 子数组被划分为满足排序要求的两部分

这为 QUICKSORT 的整体正确性提供了基础保障。

Exercise 7.1-3

题目:
证明:在规模为 $ n $ 的子数组上,PARTITION 的时间复杂度为 $ \Theta(n) $。

解答:

设子数组为 $ A[p…r] $,其长度为 $ n = r - p + 1 $。

上界分析(O(n)O(n)O(n)):

  • 第1行 x = A[r]O(1)O(1)O(1)
  • 第2行 i = p - 1O(1)O(1)O(1)
  • 第3行 for j = p to r-1:执行 n−1n-1n1 次迭代
  • 第4~6行:每次迭代 O(1)O(1)O(1),总计 O(n−1)=O(n)O(n-1) = O(n)O(n1)=O(n)
  • 第7~8行:O(1)O(1)O(1)

因此:T(n)=O(1)+O(1)+O(n)+O(1)=O(n)T(n) = O(1) + O(1) + O(n) + O(1) = O(n)T(n)=O(1)+O(1)+O(n)+O(1)=O(n)

下界分析(Ω(n)\Omega(n)Ω(n)):
每次迭代至少执行一次比较操作,共 n−1n-1n1 次比较,
存在常数 c>0c > 0c>0 使得 T(n)≥c(n−1)=Ω(n)T(n) \geq c(n-1) = \Omega(n)T(n)c(n1)=Ω(n)

结论:
T(n)=O(n)T(n) = O(n)T(n)=O(n)T(n)=Ω(n)T(n) = \Omega(n)T(n)=Ω(n) ⇒\Rightarrow T(n)=Θ(n)T(n) = \Theta(n)T(n)=Θ(n)

Exercise 7.1-4

题目:
如何修改 QUICKSORT,使得它能够以非递增序(即降序)进行排序?

解答:

要使快速排序生成非递增序(从大到小),只需修改划分过程 PARTITION 中的比较逻辑。

修改方法:

在划分过程中,原本是将小于等于主元的元素放在左侧,大于主元的放在右侧,从而得到升序结果。

为了得到降序,应改为:

  • 大于等于主元的元素放在左侧
  • 小于主元的元素放在右侧
具体操作:

PARTITION 过程中的比较条件:

如果 $ A[j] \leq x $,则将其归入左侧区域

修改为:

如果 $ A[j] \geq x $,则将其归入左侧区域

即,将判断条件从 A[j] <= x 改为 A[j] >= x

示例代码修改:
def partition_desc(A, p, r):
    x = A[r]          # 主元
    i = p - 1         # 小于主元区域的右边界
    for j in range(p, r):
        if A[j] >= x:           # 修改:改为 >=
            i += 1
            A[i], A[j] = A[j], A[i]
    A[i + 1], A[r] = A[r], A[i + 1]
    return i + 1

7.2 快速排序的性能

快速排序的运行时间高度依赖于划分的平衡性,而划分的平衡性又取决于主元的选择。

  • 平衡划分:子问题规模大致相等 → 性能接近归并排序,为 $ O(n \log n) $
  • 不平衡划分:一个子问题大小为 $ n-1 $,另一个为 0 → 性能退化为 $ O(n^2) $,与插入排序相当

最坏情况划分

当每次划分都产生两个极不平衡的子问题:

  • 一个包含 $ n-1 $ 个元素
  • 另一个包含 0 个元素

这种情况发生在每次选取的主元都是当前子数组的最小或最大元素时,例如:

  • 输入数组已按升序排列,且主元选在末尾
  • 输入数组已按降序排列,且主元选在末尾

此时,递归调用树呈现线性结构,每层只减少一个元素。

时间复杂度递归式

设 $ T(n) $ 为对规模为 $ n $ 的数组排序所需时间,则有:
T(n)=T(n−1)+T(0)+Θ(n) T(n) = T(n-1) + T(0) + \Theta(n) T(n)=T(n1)+T(0)+Θ(n)
其中:

  • $ T(n-1) $:递归处理 $ n-1 $ 个元素的子数组
  • $ T(0) $:空子数组的处理时间,为 $ \Theta(1) $
  • $ \Theta(n) $:PARTITION 过程的时间

因此:
T(n)=T(n−1)+Θ(n) T(n) = T(n-1) + \Theta(n) T(n)=T(n1)+Θ(n)

求解递归式

展开递归:
T(n)=T(n−1)+cn=T(n−2)+c(n−1)+cn=T(n−3)+c(n−2)+c(n−1)+cn⋮=T(1)+c∑k=2nk=T(1)+c(n(n+1)2−1)=Θ(n2) \begin{align*} T(n) &= T(n-1) + cn \\ &= T(n-2) + c(n-1) + cn \\ &= T(n-3) + c(n-2) + c(n-1) + cn \\ &\quad \vdots \\ &= T(1) + c \sum_{k=2}^{n} k \\ &= T(1) + c \left( \frac{n(n+1)}{2} - 1 \right) \\ &= \Theta(n^2) \end{align*} T(n)=T(n1)+cn=T(n2)+c(n1)+cn=T(n3)+c(n2)+c(n1)+cn=T(1)+ck=2nk=T(1)+c(2n(n+1)1)=Θ(n2)

即:
T(n)=Θ(n2) T(n) = \Theta(n^2) T(n)=Θ(n2)

结论

  • 在最坏情况下,快速排序的时间复杂度为 $ \Theta(n^2) $
  • 此时其性能不优于插入排序
  • 特别地,当输入数组已完全有序(升序或降序)且主元取末尾元素时,就会出现最坏情况
  • 而插入排序在已排序输入下时间复杂度为 $ O(n) $,反而更优

因此,原始快速排序在有序输入下表现较差,这是其主要缺点之一。

最好情况划分

在最平衡的划分情况下,PARTITION 将数组划分为两个规模尽可能相等的子问题。

具体而言:

  • 一个子数组的规模为 $ \lfloor n/2 \rfloor $
  • 另一个子数组的规模为 $ \lceil n/2 \rceil - 1 $,即 $ \lfloor (n-1)/2 \rfloor $
  • 两者都不超过 $ n/2 $

例如,当主元是当前子数组的中位数时,就能实现这种平衡划分。

此时,快速排序的递归结构类似于归并排序。

时间复杂度递归式

设 $ T(n) $ 为对规模为 $ n $ 的数组进行快速排序所需的时间,则有:
T(n)=T(⌊n2⌋)+T(⌈n2⌉−1)+Θ(n) T(n) = T\left(\left\lfloor \frac{n}{2} \right\rfloor\right) + T\left(\left\lceil \frac{n}{2} \right\rceil - 1\right) + \Theta(n) T(n)=T(2n)+T(2n1)+Θ(n)

为简化分析,忽略向下取整、向上取整和减 1 等细节,近似为:
T(n)=2T(n2)+Θ(n) T(n) = 2T\left(\frac{n}{2}\right) + \Theta(n) T(n)=2T(2n)+Θ(n)

求解递归式

根据主定理(Master Theorem,定理 4.1)情况 2

  • $ a = 2 $, $ b = 2 $, $ f(n) = \Theta(n) $
  • $ n^{\log_b a} = n^{\log_2 2} = n^1 = n $
  • $ f(n) = \Theta(n) = \Theta(n^{\log_b a}) $

满足主定理情况 2,因此:
T(n)=Θ(nlog⁡n) T(n) = \Theta(n \log n) T(n)=Θ(nlogn)

结论

当每次划分都尽可能平衡时,快速排序的运行时间为 $ \Theta(n \log n) $。

这与归并排序的渐近性能相同,是一种高效的运行情况

通过在每一层递归中实现平衡划分,快速排序能够达到渐近最优的时间复杂度,展现出其在理想情况下的优越性能。

平衡的划分

快速排序的平均运行时间更接近于其最好情况 $ \Theta(n \log n) $,而非最坏情况 $ \Theta(n^2) $。理解这一点的关键在于:即使划分看似不平衡,只要每层递归产生的子问题规模按常数比例分割,总时间复杂度仍为 $ O(n \log n) $

示例:9:1 划分分析

假设 PARTITION 总是将数组划分为比例为 9:1 的两个子数组:

  • 一个子问题规模为 $ \frac{9n}{10} $
  • 另一个子问题规模为 $ \frac{n}{10} $

此时,快速排序的运行时间满足以下递归式:
T(n)=T(9n10)+T(n10)+cn T(n) = T\left(\frac{9n}{10}\right) + T\left(\frac{n}{10}\right) + cn T(n)=T(109n)+T(10n)+cn
其中 $ c $ 是 PARTITION 过程中每层的常数因子(即 $ \Theta(n) $ 中的隐含常数)。

递归树分析(如图 7-4)

  • 每一层的总代价
    尽管子问题规模不同,但所有子问题的 PARTITION 操作总代价之和为 $ cn $,即每层总代价为 $ O(n) $。

  • 递归树的深度

    • 最短路径:沿规模为 $ \frac{n}{10} $ 的子问题下降,每层乘以 $ \frac{1}{10} $,达到常数规模所需深度为:
      log⁡10n=Θ(log⁡n) \log_{10} n = \Theta(\log n) log10n=Θ(logn)

    • 最长路径:沿规模为 $ \frac{9n}{10} $ 的子问题下降,每层乘以 $ \frac{9}{10} $,达到常数规模所需深度为:
      log⁡10/9n=ln⁡nln⁡(10/9)=Θ(log⁡n) \log_{10/9} n = \frac{\ln n}{\ln(10/9)} = \Theta(\log n) log10/9n=ln(10/9)lnn=Θ(logn)

    虽然左右子树深度不同,但由于底数 $ 10 $ 和 $ 10/9 $ 都是大于 1 的常数,因此 所有叶子节点都出现在 $ \Theta(\log n) $ 的深度范围内

  • 总代价
    每层总代价为 $ O(n) $,递归树高度为 $ O(\log n) $,因此总运行时间为:
    T(n)=O(n)×O(log⁡n)=O(nlog⁡n) T(n) = O(n) \times O(\log n) = O(n \log n) T(n)=O(n)×O(logn)=O(nlogn)

推广:任意常数比例划分

设划分比例为 $ \alpha : (1 - \alpha) $,其中 $ 0 < \alpha \leq 1/2 $ 为常数(如 99:1、3:1 等),则:

  • 较大子问题规模为 $ (1 - \alpha)n $
  • 较小子问题规模为 $ \alpha n $
  • 递归树的最大深度由较小子问题决定:$ \log_{1/\alpha} n = \Theta(\log n) $
  • 每层所有子问题的 PARTITION 总代价为 $ O(n) $

因此,总时间复杂度为:
T(n)=O(nlog⁡n) T(n) = O(n \log n) T(n)=O(nlogn)

结论

  • 即使划分看似不平衡(如 9:1 或 99:1),只要每层划分的比例是固定的常数,递归树的深度就是 $ \Theta(\log n) $
  • 每层的处理代价为 $ \Theta(n) $
  • 因此,总运行时间为 $ \Theta(n \log n) $

Exercise 7.2-1

题目:
用代入法证明:递归式 $ T(n) = T(n-1) + \Theta(n) $ 的解为 $ T(n) = \Theta(n^2) $。

解答:

我们使用代入法(Substitution Method)来证明。

由于 $ \Theta(n) $ 表示一个介于 $ c_1 n $ 和 $ c_2 n $ 之间的函数,存在常数 $ c_1, c_2 > 0 $ 和 $ n_0 $,使得对所有 $ n \geq n_0 $:
T(n)=T(n−1)+f(n),其中 c1n≤f(n)≤c2n T(n) = T(n-1) + f(n), \quad \text{其中 } c_1 n \leq f(n) \leq c_2 n T(n)=T(n1)+f(n),其中 c1nf(n)c2n

我们猜测解的形式为:
T(n)=Θ(n2) T(n) = \Theta(n^2) T(n)=Θ(n2)
即存在正常数 $ a_1, a_2 $ 和 $ n_0 $,使得对所有 $ n \geq n_0 $,有:
a1n2≤T(n)≤a2n2 a_1 n^2 \leq T(n) \leq a_2 n^2 a1n2T(n)a2n2

上界证明(T(n)=O(n2)T(n) = O(n^2)T(n)=O(n2)):

归纳假设: 对所有 $ m < n $,有 $ T(m) \leq a_2 m^2 $

归纳步骤:
T(n)=T(n−1)+f(n)≤a2(n−1)2+c2n T(n) = T(n-1) + f(n) \leq a_2(n-1)^2 + c_2 n T(n)=T(n1)+f(n)a2(n1)2+c2n

展开右边:
a2(n−1)2+c2n=a2(n2−2n+1)+c2n=a2n2−2a2n+a2+c2n a_2(n-1)^2 + c_2 n = a_2(n^2 - 2n + 1) + c_2 n = a_2 n^2 - 2a_2 n + a_2 + c_2 n a2(n1)2+c2n=a2(n22n+1)+c2n=a2n22a2n+a2+c2n

=a2n2+(c2−2a2)n+a2 = a_2 n^2 + (c_2 - 2a_2)n + a_2 =a2n2+(c22a2)n+a2

要证明 $ T(n) \leq a_2 n^2 $,需要:
(c2−2a2)n+a2≤0 (c_2 - 2a_2)n + a_2 \leq 0 (c22a2)n+a20

选择 $ a_2 \geq c_2 $,则 $ c_2 - 2a_2 \leq -a_2 < 0 $,所以当 $ n \geq \frac{a_2}{2a_2 - c_2} $ 时,不等式成立。

下界证明(T(n)=Ω(n2)T(n) = \Omega(n^2)T(n)=Ω(n2)):

归纳假设: 对所有 $ m < n $,有 $ T(m) \geq a_1 m^2 $

归纳步骤:
T(n)=T(n−1)+f(n)≥a1(n−1)2+c1n T(n) = T(n-1) + f(n) \geq a_1(n-1)^2 + c_1 n T(n)=T(n1)+f(n)a1(n1)2+c1n

展开右边:
a1(n−1)2+c1n=a1(n2−2n+1)+c1n=a1n2−2a1n+a1+c1n a_1(n-1)^2 + c_1 n = a_1(n^2 - 2n + 1) + c_1 n = a_1 n^2 - 2a_1 n + a_1 + c_1 n a1(n1)2+c1n=a1(n22n+1)+c1n=a1n22a1n+a1+c1n

=a1n2+(c1−2a1)n+a1 = a_1 n^2 + (c_1 - 2a_1)n + a_1 =a1n2+(c12a1)n+a1

要证明 $ T(n) \geq a_1 n^2 $,需要:
(c1−2a1)n+a1≥0 (c_1 - 2a_1)n + a_1 \geq 0 (c12a1)n+a10

选择 $ a_1 \leq \frac{c_1}{2} $,则 $ c_1 - 2a_1 \geq 0 $,所以不等式对所有 $ n \geq 1 $ 成立。

结论:

通过选择合适的常数 $ a_1 $ 和 $ a_2 $,我们可以证明:
a1n2≤T(n)≤a2n2 a_1 n^2 \leq T(n) \leq a_2 n^2 a1n2T(n)a2n2

因此,$ T(n) = \Theta(n^2) $。

证毕。

Exercise 7.2-2

题目:
当数组 $ A $ 的所有元素都具有相同值时,QUICKSORT 的时间复杂度是什么?

解答:

当所有元素相等时,设主元为末尾元素xxx,在PARTITION过程中:

  • 由于所有元素都等于主元,条件 A[j] <= x 对所有jjj都恒成立
  • 因此所有元素都会被交换到左侧区域
  • 最终主元归位到位置n−1n-1n1(0-based索引)或位置nnn(1-based索引)
  • 左子数组包含前n−1n-1n1个元素,右子数组为空

具体来说:

  • 左子数组:QUICKSORT(A, 0, n-2) (包含n−1n-1n1个元素)
  • 右子数组:QUICKSORT(A, n, n-1) (空数组,因为范围无效)

因此,每层递归产生一个大小为 $ n-1 $ 的子问题,划分极度不平衡。

递归式为:
T(n)=T(n−1)+T(0)+Θ(n)=T(n−1)+Θ(n) T(n) = T(n-1) + T(0) + \Theta(n) = T(n-1) + \Theta(n) T(n)=T(n1)+T(0)+Θ(n)=T(n1)+Θ(n)

由Exercise 7.2-1知其解为 $ T(n) = \Theta(n^2) $

所以,当所有元素都相等时,QUICKSORT 的时间复杂度为 $ \Theta(n^2) $。

Exercise 7.2-3

题目:
证明:当数组 $ A $ 包含的元素互不相同,并且是按降序排列时,QUICKSORT 的时间复杂度为 $ \Theta(n^2) $。

解答:

设数组为降序:$ A = [n, n-1, \dots, 1] $,主元选在末尾(标准 PARTITION)。

PARTITION(A, p, r) 为例分析:

  • 主元 $ x = A[r] $(当前子数组中的最小元素)
  • 对于所有 $ j $ 从 $ p $ 到 $ r-1 $,都有 $ A[j] > A[r] = x $
  • 因此条件 A[j] <= x 永不成立
  • 循环中无交换发生,i 保持为 $ p-1 $
  • 循环结束后,交换 A[i+1] = A[p]A[r]
  • 这将主元(当前最小元素)放到位置 $ p $
  • 返回 q = p
  • 左子数组:A[p..p-1](空数组)
  • 右子数组:A[p+1..r](包含 $ r-p $ 个元素)

具体例子:数组 $ A = [5, 4, 3, 2, 1] $

第一次调用 PARTITION(A, 0, 4)

  • 主元 $ x = A[4] = 1 $
  • 所有元素 $ 5, 4, 3, 2 > 1 $,所以都不满足 A[j] <= 1
  • i 保持为 $ -1 $
  • 交换 A[0]A[4],数组变为 $ [1, 4, 3, 2, 5] $
  • 返回 q = 0
  • 左子数组:A[0..(-1)](空)
  • 右子数组:A[1..4](包含4个元素)

因此,每次划分都产生一个空的左子数组和一个大小为 $ n-1 $ 的右子数组,划分极度不平衡。

递归式为:
T(n)=T(0)+T(n−1)+Θ(n)=T(n−1)+Θ(n) T(n) = T(0) + T(n-1) + \Theta(n) = T(n-1) + \Theta(n) T(n)=T(0)+T(n1)+Θ(n)=T(n1)+Θ(n)

由Exercise 7.2-1知其解为 $ T(n) = \Theta(n^2) $

得证。

Exercise 7.2-4

题目:
银行对账单问题:输入几乎有序(每个元素离其正确位置至多 $ c $ 位),证明 INSERTION-SORT 性能优于 QUICKSORT

解答:

假设"几乎有序"指:每个元素 $ A[i] $ 距其在有序数组中的正确位置不超过常数 $ c $。

INSERTION-SORT 的性能分析:

在插入排序中,当处理第 $ i $ 个元素时:

  • 该元素在其最终位置的 $ c $ 位范围内
  • 在内层while循环中,需要将该元素向左移动,直到找到正确位置
  • 由于元素最多偏离其正确位置 $ c $ 位,所以最多需要移动 $ c $ 次
  • 每次移动是常数时间操作

因此:

  • 外层循环执行 $ n $ 次
  • 每次内层循环最多执行 $ c $ 次移动
  • 每次移动耗时 $ O(1) $
  • 总时间:$ O(n \times c) = O(n) $(因为 $ c $ 是常数)
QUICKSORT 的性能分析:

考虑标准快速排序实现,主元选在子数组末尾:

当数组几乎有序时:

  • 主元通常是接近当前子数组最大值的元素(但不一定是最大值)
  • 在PARTITION过程中,大部分元素都小于等于主元
  • 因此大部分元素会被归入左侧区域
  • 这导致划分不平衡,左子数组接近完整大小,右子数组很小或为空

具体分析:

  • 如果每次都选择接近最大值的元素作为主元
  • 划分结果会是:左子数组大小约为 $ n-1 $,右子数组很小
  • 这种不平衡划分导致递归深度接近 $ n $
  • 每层的PARTITION操作耗时 $ \Theta(n) $
  • 总时间:$ \Theta(n^2) $
结论:

在几乎有序输入下:

  • INSERTION-SORT 的时间复杂度为 $ O(n) $
  • QUICKSORT 的时间复杂度为 $ \Theta(n^2) $

因此,INSERTION-SORT 的性能远优于 QUICKSORT,这解释了为什么银行等机构在处理几乎有序的数据时更倾向于使用插入排序。

Exercise 7.2-5

题目:
假设快速排序每层划分比例为 $ (1-\alpha):\alpha $,其中 $ 0 < \alpha \leq 1/2 $ 为常数。证明:递归树中叶节点的最小深度约为 $ -\frac{\lg n}{\lg(1-\alpha)} $,最大深度约为 $ -\frac{\lg n}{\lg \alpha} $。

解答:

在递归树中,每层划分产生两个子问题:

  • 较大子问题规模为 $ (1-\alpha)n $
  • 较小子问题规模为 $ \alpha n $
最小深度(沿较大子问题下降):
  • 每层乘以 $ 1-\alpha $

  • 设 $ k $ 层后规模 ≤ 1:
    (1−α)kn≤1⇒(1−α)k≤1n (1-\alpha)^k n \leq 1 \Rightarrow (1-\alpha)^k \leq \frac{1}{n} (1α)kn1(1α)kn1
    取对数:
    klg⁡(1−α)≤−lg⁡n k \lg(1-\alpha) \leq -\lg n klg(1α)lgn
    由于 $ 0 < 1-\alpha < 1 $,有 $ \lg(1-\alpha) < 0 $,不等号方向反转:
    k≥−lg⁡nlg⁡(1−α)=−lg⁡nlg⁡(1−α) k \geq \frac{-\lg n}{\lg(1-\alpha)} = -\frac{\lg n}{\lg(1-\alpha)} klg(1α)lgn=lg(1α)lgn
    所以最小深度 ≈ $ -\frac{\lg n}{\lg(1-\alpha)} $

最大深度(沿较小子问题下降):
  • 每层乘以 $ \alpha $

  • 设 $ k $ 层后规模 ≤ 1:
    αkn≤1⇒αk≤1n \alpha^k n \leq 1 \Rightarrow \alpha^k \leq \frac{1}{n} αkn1αkn1
    取对数:
    klg⁡α≤−lg⁡n k \lg \alpha \leq -\lg n klgαlgn
    由于 $ 0 < \alpha < 1 $,有 $ \lg \alpha < 0 $,不等号方向反转:
    k≥−lg⁡nlg⁡α=−lg⁡nlg⁡α k \geq \frac{-\lg n}{\lg \alpha} = -\frac{\lg n}{\lg \alpha} klgαlgn=lgαlgn
    所以最大深度 ≈ $ -\frac{\lg n}{\lg \alpha} $

得证。

Exercise 7.2-6

题目:
证明:在随机输入数组上,对于任意常数 $ 0 < \alpha \leq 1/2 $,PARTITION 产生比 $ 1-\alpha : \alpha $ 更平衡划分的概率约为 $ 1 - 2\alpha $。

解答:

假设数组元素互异,且为 $ 1 $ 到 $ n $ 的随机排列。主元为末尾元素 $ A[r] $。

设主元的秩为 $ k $(即它是第 $ k $ 小的元素,$ 1 \leq k \leq n $)。
划分后:

  • 左子数组大小为 $ k-1 $
  • 右子数组大小为 $ n-k $

划分比 $ 1-\alpha : \alpha $ 更平衡,当且仅当两个子数组的大小都在 $ [\alpha n, (1-\alpha)n] $ 范围内:
αn≤k−1≤(1−α)n \alpha n \leq k-1 \leq (1-\alpha)n αnk1(1α)n
解得:
αn+1≤k≤(1−α)n+1 \alpha n + 1 \leq k \leq (1-\alpha)n + 1 αn+1k(1α)n+1

满足条件的 $ k $ 的个数为:
⌊(1−α)n+1⌋−⌈αn+1⌉+1≈(1−2α)n+1 \left\lfloor (1-\alpha)n + 1 \right\rfloor - \left\lceil \alpha n + 1 \right\rceil + 1 \approx (1-2\alpha)n + 1 (1α)n+1αn+1+1(12α)n+1

由于 $ k $ 在 $ 1 $ 到 $ n $ 上均匀分布,概率为:
(1−2α)n+1n=1−2α+1n \frac{(1-2\alpha)n + 1}{n} = 1 - 2\alpha + \frac{1}{n} n(12α)n+1=12α+n1

当 $ n \to \infty $ 时,$ \frac{1}{n} \to 0 $,因此概率约为:
1−2α 1 - 2\alpha 12α

得证。

[!NOTE]

我们通过一个具体例子来理解这道题的含义。


题目回顾

证明:在随机输入数组上,对于任意常数 $ 0 < \alpha \leq 1/2 $,PARTITION 产生比 $ 1-\alpha : \alpha $ 更平衡的划分的概率约为 $ 1 - 2\alpha $。

设:

  • 数组长度 $ n = 100 $
  • $ \alpha = 0.2 $
  • 所以 $ 1 - \alpha = 0.8 $

我们说一个划分是 “比 $ 0.8:0.2 $ 更平衡”,意思是:

  • 两个子数组的大小都更接近中点,即都不太小
  • 具体来说,较小子数组的大小 至少为 $ \alpha n = 0.2 \times 100 = 20 $

换句话说:

  • 如果划分后,左或右子数组大小 < 20,就是“不平衡”
  • 如果两个子数组都 ≥ 20,就是“更平衡”

主元的作用

PARTITION 中,主元是最后一个元素。划分后:

  • 左子数组:所有 ≤ 主元的元素 → 大小 = 主元的秩 - 1
  • 右子数组:所有 > 主元的元素 → 大小 = $ n - $ 秩

设主元是第 $ k $ 小的元素(即它的为 $ k $),则:

  • 左子数组大小 = $ k - 1 $
  • 右子数组大小 = $ 100 - k $

我们希望这个划分比 80:20 更平衡,即:

  • 左子数组 ≥ 20 → $ k - 1 \geq 20 $ → $ k \geq 21 $
  • 右子数组 ≥ 20 → $ 100 - k \geq 20 $ → $ k \leq 80 $

所以,当主元的秩 $ k $ 满足:
21≤k≤80 21 \leq k \leq 80 21k80
时,划分就是“比 80:20 更平衡”。


计算概率

  • 总共可能的秩:$ k = 1, 2, \dots, 100 $

  • 满足条件的 $ k $:从 21 到 80,共:
    80−21+1=60 个 80 - 21 + 1 = 60 \text{ 个} 8021+1=60 

  • 概率:
    60100=0.6 \frac{60}{100} = 0.6 10060=0.6

而 $ 1 - 2\alpha = 1 - 2 \times 0.2 = 0.6 $

$ \alpha $子数组最小大小秩范围满足的 $ k $ 个数概率
0.11011 ≤ k ≤ 90800.8 = 1 - 2×0.1
0.22021 ≤ k ≤ 80600.6 = 1 - 2×0.2
0.33031 ≤ k ≤ 70400.4 = 1 - 2×0.3
0.44041 ≤ k ≤ 60200.2 = 1 - 2×0.4
0.55051 ≤ k ≤ 500(不可能)0

当 $ \alpha = 0.5 $,要求两个子数组都 ≥ 50,只有完全平分才满足,概率趋近于 0。


结论

  • 如果主元的秩在中间的 $ 1 - 2\alpha $ 范围内,划分就是“更平衡”的
  • 由于输入是随机排列,主元的秩等概率出现在 $ 1 $ 到 $ n $ 中
  • 所以概率就是中间部分的比例:$ 1 - 2\alpha $

这就是为什么快速排序在平均情况下表现良好:

超过一半的划分是相对平衡的(例如 $ \alpha = 0.2 $ 时,60% 的划分比 80:20 更平衡)

这个例子清楚地展示了“平衡划分”的概率本质。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值