当前位置: 首页 > news >正文

算法导论第三版代码python实现与部分习题答案-第六章:堆排序

第六章 堆排序

6.1 堆

Exercise 6.1-1

题目:
在高度为 $ h $ 的堆中,元素个数最多和最少分别是多少?

解答:

  • 最少元素个数:$ 2^h $
  • 最多元素个数:$ 2^{h+1} - 1 $

解释:

  • 最少情况:当堆的前 $ h $ 层构成一个深度为 $ h-1 $ 的完全二叉树(共 $ 2^h - 1 $ 个节点),第 $ h+1 $ 层仅有一个最左侧的叶节点。此时总节点数为:
    (2h−1)+1=2h (2^h - 1) + 1 = 2^h (2h1)+1=2h

  • 最多情况:当堆是一棵深度为 $ h $ 的完全二叉树(即满二叉树),所有层都完全填满。此时总节点数为:
    ∑i=0h2i=2h+1−1 \sum_{i=0}^{h} 2^i = 2^{h+1} - 1 i=0h2i=2h+11

注:此处“高度”定义为从根到最远叶节点的边数(即根的高度为 0)。


Exercise 6.1-2

题目:
证明:含 $ n $ 个元素的堆的高度为 $ \lfloor \lg n \rfloor $。

解答:
设堆的高度为 $ h $。根据 Exercise 6.1-1 的结论,高度为 $ h $ 的堆满足:
2h≤n<2h+1 2^h \leq n < 2^{h+1} 2hn<2h+1
对不等式取以 2 为底的对数:
h≤lg⁡n<h+1 h \leq \lg n < h + 1 hlgn<h+1
因此:
h=⌊lg⁡n⌋ h = \lfloor \lg n \rfloor h=lgn
故含 $ n $ 个元素的堆的高度为 $ \lfloor \lg n \rfloor $。


Exercise 6.1-3

题目:
证明:在最大堆的任一子树中,该子树所包含的最大元素在该子树的根结点上。

解答:
采用反证法。假设存在某个子树,其最大元素不在根节点,而在某个非根节点 $ x $ 上。

由于 $ x $ 不是子树的根,则它在该子树中有一个父节点 $ p $。根据最大堆性质,有:
A[p]≥A[x] A[p] \geq A[x] A[p]A[x]
但 $ A[x] $ 是子树中的最大元素,故 $ A[x] > A[p] $(若相等则仍不违反,但最大值仍应在根路径上;关键在于 $ A[x] $ 严格大于其祖先中某些值)。

特别地,从 $ x $ 到子树根的路径上,每个父节点都应 ≥ 其子节点。因此,子树根的值 ≥ 路径上所有节点的值,包括 $ x $,即:
A[root]≥A[x] A[\text{root}] \geq A[x] A[root]A[x]
这与 $ A[x] $ 是子树中最大元素且不在根矛盾(除非相等,但即使相等,最大值也在根可达路径上,而堆性质要求根 ≥ 所有后代)。

更直接地说:若最大值不在根,则存在某个节点 $ x $ 满足 $ A[x] > A[\text{root}] $,但堆性质要求根 ≥ 所有后代,矛盾。

因此,子树中的最大元素必须位于该子树的根节点上。


Exercise 6.1-4

题目:
假设一个最大堆的所有元素都不相同,那么该堆的最小元素应该位于哪里?

解答:
最小元素必须位于某个叶节点上。

证明:
采用反证法。假设最小元素位于非叶节点 $ x $ 上,并设 $ y $ 是 $ x $ 的一个子节点。

根据最大堆性质,有:
A[x]≥A[y] A[x] \geq A[y] A[x]A[y]
由于所有元素互不相同,该不等式是严格的:
A[x]>A[y] A[x] > A[y] A[x]>A[y]
但这与 $ A[x] $ 是堆中最小元素矛盾(因为 $ A[y] < A[x] $)。

因此,包含最小元素的节点不可能有子节点,即必须是叶节点。


Exercise 6.1-5

题目:
一个已排好序的数组是一个最小堆吗?

解答:
是的,一个升序排列的数组是一个最小堆。

解释:
考虑使用 1-based 索引的数组 $ A[1…n] $,且 $ A[1] \leq A[2] \leq \cdots \leq A[n] $。

对于任意非叶节点 $ i $(即 $ i \leq \lfloor n/2 \rfloor $),其左子节点为 $ 2i $,右子节点为 $ 2i+1 $(若存在)。

由于 $ i < 2i < 2i+1 \leq n $,且数组升序,有:
A[i]≤A[2i],A[i]≤A[2i+1] A[i] \leq A[2i], \quad A[i] \leq A[2i+1] A[i]A[2i],A[i]A[2i+1]
因此,每个节点的值都不大于其子节点的值,满足最小堆性质。


Exercise 6.1-6

题目:
值为 $ \langle 23, 17, 14, 6, 13, 10, 1, 5, 7, 12 \rangle $ 的数组是一个最大堆吗?

解答:
不是,该数组不是一个最大堆。

解释:
使用 1-based 索引分析:

  • 元素 $ 7 $ 位于第 9 个位置($ A[9] = 7 $)
  • 其父节点位置为 $ \lfloor 9/2 \rfloor = 4 $,对应元素 $ A[4] = 6 $
  • 但 $ 7 > 6 $,即子节点值大于父节点值,违反了最大堆性质(父节点应 ≥ 子节点)

因此,该数组不满足最大堆性质。


Exercise 6.1-7

题目:
证明:当用数组表示存储 $ n $ 个元素的堆时,叶结点下标分别是 $ \lfloor n/2 \rfloor + 1, \lfloor n/2 \rfloor + 2, \ldots, n $。

解答:
只需证明:节点 $ i $ 是叶节点 当且仅当 它没有子节点,即 $ 2i > n $。

证明:

  1. 若 $ i \geq \lfloor n/2 \rfloor + 1 $,则 $ i $ 是叶节点:
    此时:
    i>n/2⇒2i>n i > n/2 \Rightarrow 2i > n i>n/22i>n
    因此左子节点 $ 2i $ 已超出数组范围,右子节点 $ 2i+1 $ 更是如此。故 $ i $ 无子节点,为叶节点。

  2. 若 $ i $ 是叶节点,则 $ i \geq \lfloor n/2 \rfloor + 1 $:
    若 $ i $ 是叶节点,则无子节点,即:
    2i>n⇒i>n/2 2i > n \Rightarrow i > n/2 2i>ni>n/2
    由于 $ i $ 是整数,有:
    i≥⌊n/2⌋+1 i \geq \lfloor n/2 \rfloor + 1 in/2+1

综上,叶节点的下标集合为:
{⌊n/2⌋+1,⌊n/2⌋+2,…,n} \{ \lfloor n/2 \rfloor + 1, \lfloor n/2 \rfloor + 2, \ldots, n \} {⌊n/2+1,n/2+2,,n}

6.2 维护堆的性质

算法实现

算法说明:
MAX-HEAPIFY 是堆排序中的核心算法,用于维护最大堆的性质。它假设节点 i 的左右子树都已经满足最大堆性质,但节点 i 可能违反了最大堆性质,需要将其"下沉"到正确位置。

# 此时提到的数组从0开始 也就是0-based
def left(i):"""计算节点i的左子节点索引(0-based索引)"""return 2 * i + 1def right(i):"""计算节点i的右子节点索引(0-based索引)"""return 2 * i + 2def max_heapify(A, i, heap_size):"""维护最大堆性质的函数参数:A: 数组(列表),表示堆结构(使用0-based索引)i: 需要维护堆性质的节点索引heap_size: 堆的有效大小(数组中前heap_size个元素构成堆)功能:确保以节点i为根的子树满足最大堆性质"""# 获取左子节点和右子节点的索引l = left(i)     r = right(i)    # 找到节点i、左子节点、右子节点中值最大的节点索引if l < heap_size and A[l] > A[i]:  largest = l                    else:largest = i                   if r < heap_size and A[r] > A[largest]: largest = r                         # 如果最大值不在节点i上,则交换并递归处理if largest != i:                         # 交换节点i和最大值节点的值A[i], A[largest] = A[largest], A[i]  # 递归维护被交换节点的子树堆性质max_heapify(A, largest, heap_size)   # 使用示例:
# arr = [16, 4, 10, 14, 7, 9, 3, 2, 8, 1]
# heap_size = len(arr)
# max_heapify(arr, 1, heap_size)  # 从索引1开始维护堆性质
       16               →                16               →                16/    \                           /    \                           /    \4      10                        14      10                       14      10/ \    /  \         swap(1,3)    / \    /  \         swap(3,8)    / \    /  \14  7  9    3        ─────────>  4   7  9    3        ─────────>  8   7  9    3/ \ /                              / \/                              / \/ 
2  8 1                             2  8 1                             2  4 1A[1]=4 < A[3]=14                    A[3]=4 < A[8]=8                    已满足堆性质需下沉 → 交换                     继续下沉 → 交换                     停止

时间复杂度分析

  1. 递归式建立

MAX-HEAPIFY 在每次调用中:

  • 执行常数时间操作(比较与交换)
  • 最多递归处理一个子树

设 $ T(n) $ 为处理 $ n $ 个节点的运行时间。

最坏情况下,递归发生在最大的子树上。
可以证明:最大子树的大小至多为 $ \frac{2n}{3} $(最坏情况:堆的最底层恰好半满,且在左子树)。

因此,递归式为:
T(n)≤T(2n3)+O(1) T(n) \leq T\left(\frac{2n}{3}\right) + O(1) T(n)T(32n)+O(1)

考虑递归式:
T(n)=T(2n3)+O(1) T(n) = T\left(\frac{2n}{3}\right) + O(1) T(n)=T(32n)+O(1)

与主定理形式 $ T(n) = aT(n/b) + f(n) $ 对比:

  • $ a = 1 $
  • $ b = \frac{3}{2} $(因为 $ n/b = 2n/3 \Rightarrow b = 3/2 $)
  • $ f(n) = O(1) $

计算 $ n^{\log_b a} = n^{\log_{3/2} 1} = n^0 = 1 $

比较 $ f(n) = O(1) $ 与 $ n^{\log_b a} = 1 $:

  • $ f(n) = \Theta(n^{\log_b a}) $,属于主定理情况 2

因此:
T(n)=Θ(log⁡n) T(n) = \Theta(\log n) T(n)=Θ(logn)

更精确地,由于每层只递归一次且工作量为常数:
T(n)=O(log⁡n) T(n) = O(\log n) T(n)=O(logn)

MAX-HEAPIFY 的最坏运行时间为:
O(log⁡n) \boxed{O(\log n)} O(logn)
即,与堆的高度成正比。若堆高为 $ h $,则 $ T(n) = O(h) = O(\lg n) $。

Exercise 6.2-2

题目:
参考过程 MAX-HEAPIFY,写出能够维护相应最小堆的 MIN-HEAPIFY(A, i) 的代码,并比较 MIN-HEAPIFYMAX-HEAPIFY 的运行时间。

解答:

MIN-HEAPIFY 代码:

def left(i):"""返回左子节点索引(0-based)"""return 2 * i + 1def right(i):"""返回右子节点索引(0-based)"""return 2 * i + 2def min_heapify(A, i, heap_size):"""维护最小堆性质:父节点 ≤ 子节点"""l = left(i)r = right(i)# 找出 i, l, r 中值最小的节点if l < heap_size and A[l] < A[i]:smallest = lelse:smallest = iif r < heap_size and A[r] < A[smallest]:smallest = r# 如果最小值不在当前节点,则交换并递归下沉if smallest != i:A[i], A[smallest] = A[smallest], A[i]min_heapify(A, smallest, heap_size)

MIN-HEAPIFY 的运行时间满足以下递归式:

T(n)=T(2n3)+O(1) T(n) = T\left(\frac{2n}{3}\right) + O(1) T(n)=T(32n)+O(1)

  • $ O(1) $:每层执行的比较和交换操作为常数时间
  • $ T\left(\frac{2n}{3}\right) $:最坏情况下递归处理最大子树,其规模至多为 $ \frac{2n}{3} $(当堆的最底层恰好半满时)

应用主定理(Master Theorem, 定理 4.1)

  • 形式:$ T(n) = aT(n/b) + f(n) $

  • 此处:$ a = 1 $, $ b = \frac{3}{2} $, $ f(n) = O(1) $

  • 计算:
    log⁡ba=log⁡3/21=0⇒nlog⁡ba=n0=1 \log_b a = \log_{3/2} 1 = 0 \quad \Rightarrow \quad n^{\log_b a} = n^0 = 1 logba=log3/21=0nlogba=n0=1

  • $ f(n) = O(1) = \Theta(n^{\log_b a}) $,属于主定理情况 2

因此,解为:
T(n)=Θ(log⁡n) \boxed{T(n) = \Theta(\log n)} T(n)=Θ(logn)

即:
O(log⁡n) \boxed{O(\log n)} O(logn)

MIN-HEAPIFY 的最坏运行时间为 $ O(\log n) $,与 MAX-HEAPIFY 完全相同,仅比较方向不同,结构与时间复杂度一致。

Exercise 6.2-3

题目:
当元素 $ A[i] $ 比其孩子的值都大时,调用 MAX-HEAPIFY(A, i) 会有什么结果?

解答:
数组保持不变。

解释:
MAX-HEAPIFY 的作用是维护最大堆性质:父节点 ≥ 子节点。
如果 $ A[i] $ 已经大于其所有子节点的值,则在比较过程中:

  • $ A[i] \geq A[l] $ 且 $ A[i] \geq A[r] $
  • 因此 largest 会被设为 i
  • 条件 if largest != i 不成立
  • 不执行交换,也不进行递归调用

所以函数直接返回,堆结构无任何变化

Exercise 6.2-4

题目:
当 $ i > \text{heap_size}/2 $ 时,调用 MAX-HEAPIFY(A, i) 会有什么结果?

解答:
不会有任何操作。

解释:
在基于 0-based 索引的堆中:

  • 非叶节点(有子节点)的最大索引为 $ \lfloor \text{heap_size}/2 \rfloor - 1 $(若使用 0-based)
  • 更清晰地:叶节点的索引从 $ \lfloor \text{heap_size}/2 \rfloor $ 开始到 $ \text{heap_size} - 1 $

因此,当 $ i > \text{heap_size}/2 $ 时(特别地,$ i \geq \lfloor \text{heap_size}/2 \rfloor $),节点 $ i $ 是叶节点,没有左子或右子节点。

MAX-HEAPIFY 中:

  • l = 2*i + 1r = 2*i + 2 都 $ \geq \text{heap_size} $
  • 所以 l < heap_sizer < heap_size 均为假
  • largest 被设为 i
  • if largest != i 为假,不交换也不递归

函数直接返回,无任何操作

Exercise 6.2-5

题目:
MAX-HEAPIFY 的代码效率较高,但递归调用可能例外,它可能使某些编译器产生低效的代码。请用循环控制结构取代递归,重写 MAX-HEAPIFY 代码。

解答:

迭代版本的 MAX-HEAPIFY(使用循环代替递归)

def left(i):"""返回左子节点索引(0-based)"""return 2 * i + 1def right(i):"""返回右子节点索引(0-based)"""return 2 * i + 2def max_heapify_loop(A, i, heap_size):"""使用循环实现的 MAX-HEAPIFY,避免递归开销"""while True:l = left(i)r = right(i)# 找出 i, l, r 中最大值的索引if l < heap_size and A[l] > A[i]:largest = lelse:largest = iif r < heap_size and A[r] > A[largest]:largest = r# 如果最大值就是当前节点,堆性质已满足,退出循环if largest == i:break# 否则交换并继续在被交换的子节点上维护堆性质A[i], A[largest] = A[largest], A[i]# 更新 i 为被交换的子节点,模拟递归下沉i = largest# 继续循环

说明:

  • 核心思想:将递归版本中“递归调用 max_heapify(A, largest, heap_size)”的过程,改为通过更新当前节点索引 i = largest 并继续循环来模拟节点在堆中逐层下沉的过程。
  • 终止条件:当 largest == i 时,表示当前节点已大于或等于其子节点,最大堆性质满足,循环结束。
  • 时间复杂度:仍为 $ O(\log n) $,因为最坏情况下需从根节点下沉至叶子节点,路径长度为树高。
  • 空间复杂度:由递归版本的 $ O(\log n) $(函数调用栈深度)降低为 $ O(1) $,仅使用常量额外空间。

Exercise 6.2-6

题目:
证明:对一个大小为 $ n $ 的堆,MAX-HEAPIFY 的最坏情况运行时间为 $ \Omega(\lg n) $。

解答:

构造最坏情况输入

我们构造一个大小为 $ n $ 的最大堆(数组表示),使得 MAX-HEAPIFY 被调用时,触发最长可能的下沉路径:

  • 设根节点 $ A[1] = 1 $(使用 1-based 索引)
  • 所有其他节点 $ A[i] = 2 $,其中 $ 2 \leq i \leq n $

即:
A=[1,2,2,2,…,2] A = [1, 2, 2, 2, \dots, 2] A=[1,2,2,2,,2]

此时:

  • 除根外,所有节点值均为 2
  • 根节点值为 1,明显小于其子节点
  • 堆的结构是一棵完全二叉树

运行过程分析

调用 MAX-HEAPIFY(A, 1, n)(从根开始维护堆性质):

  1. 由于 $ A[1] = 1 < 2 = A[2] $ 和 $ A[3] $,largest 将指向左或右子节点(值相同,任选其一)。
  2. 交换 $ A[1] $ 与 $ A[2] $(假设选左子),此时 $ A[2] = 1 $
  3. 递归调用 MAX-HEAPIFY(A, 2, n)
  4. 在节点 2 处,其子节点 $ A[4], A[5] $ 均为 2 > 1,继续交换
  5. 节点值 1 持续“下沉”,直到到达某条路径的叶节点

由于每次比较都发现当前节点小于子节点,交换持续发生,节点 1 会从根沿一条最长路径一直下沉到叶子

路径长度分析

  • 完全二叉堆的高度为 $ h = \lfloor \lg n \rfloor $
  • 从根到最远叶节点的路径长度(边数)为 $ h $
  • 因此,MAX-HEAPIFY 至少执行 $ h = \lfloor \lg n \rfloor $ 次递归调用(或循环迭代)

即运行时间至少与树高成正比。

结论

存在一种输入情况(如上述构造),使得 MAX-HEAPIFY 的运行时间达到 $ \Theta(\lg n) $。
因此,其最坏情况运行时间为
Ω(lg⁡n) \boxed{\Omega(\lg n)} Ω(lgn)

结合已知上界 $ O(\lg n) $,可得 MAX-HEAPIFY 的最坏时间复杂度为:
Θ(lg⁡n) \boxed{\Theta(\lg n)} Θ(lgn)

6.3 建堆

算法实现

算法说明:
BUILD-MAX-HEAP 算法通过从最后一个非叶节点开始,自底向上地对每个节点调用 MAX-HEAPIFY,从而将一个无序数组转换为最大堆。

def build_max_heap(A):"""将数组A构建为最大堆参数:A: 数组(列表),需要转换为最大堆算法:1. 设置堆大小等于数组长度2. 从最后一个非叶节点开始,向前对每个节点调用MAX-HEAPIFY"""heap_size = len(A)  # 第1行:A.heap-size = A.length# 最后一个非叶节点的索引是 floor(n/2) - 1(0-based索引)for i in range(heap_size // 2 - 1, -1, -1):# 第3行:MAX-HEAPIFY(A, i)max_heapify(A, i, heap_size)

时间复杂度分析

  1. 简单上界:$ O(n \log n) $
  • 每次调用 MAX-HEAPIFY 的最坏时间复杂度为 $ O(\lg n) $

  • BUILD-MAX-HEAP 调用 MAX-HEAPIFY 共 $ \lfloor n/2 \rfloor = O(n) $ 次

  • 因此总时间的简单上界为:
    O(n)×O(lg⁡n)=O(nlg⁡n) O(n) \times O(\lg n) = O(n \lg n) O(n)×O(lgn)=O(nlgn)

这个上界是正确的,但不是渐近紧确的(即过于宽松)。


  1. 精确分析:实际时间复杂度为 $ O(n) $

为了得到更紧确的界,我们利用以下关键性质:

  1. 堆的高度:含 $ n $ 个元素的堆,其高度为 $ \lfloor \lg n \rfloor $
  2. 高度为 $ h $ 的节点数:最多为 $ \left\lceil \dfrac{n}{2^{h+1}} \right\rceil $
  3. MAX-HEAPIFY 在高度 $ h $ 节点上的代价:$ O(h) $

注意:这里的“高度”指从该节点到其最远叶节点的边数(即叶节点高度为 0)。


3.总代价求和

我们将所有节点按其高度 $ h $ 分组,计算每组的总代价并求和:

∑h=0⌊lg⁡n⌋(高度为 h 的节点数)×O(h)≤∑h=0⌊lg⁡n⌋n2h+1⋅O(h)=O(n∑h=0⌊lg⁡n⌋h2h) \sum_{h=0}^{\lfloor \lg n \rfloor} \left( \text{高度为 } h \text{ 的节点数} \right) \times O(h) \leq \sum_{h=0}^{\lfloor \lg n \rfloor} \frac{n}{2^{h+1}} \cdot O(h) = O\left( n \sum_{h=0}^{\lfloor \lg n \rfloor} \frac{h}{2^h} \right) h=0lgn(高度为 h 的节点数)×O(h)h=0lgn2h+1nO(h)=Onh=0lgn2hh

由于级数 $ \sum_{h=0}^{\infty} \frac{h}{2^h} $ 收敛,我们可以将其扩展到无穷级数(上界更松但便于计算):

[!IMPORTANT]

幂级数是一类形式为 ∑n=0∞an(x−c)n\sum_{n=0}^{\infty} a_n (x - c)^nn=0an(xc)n 的无穷级数,其中 ana_nan 是系数,ccc 是展开中心。当 c=0c = 0c=0 时,称为麦克劳林级数;一般情形称为泰勒级数。

最基本的幂级数是几何级数:
∑k=0∞xk=1+x+x2+x3+⋯ \sum_{k=0}^{\infty} x^k = 1 + x + x^2 + x^3 + \cdots k=0xk=1+x+x2+x3+
∣x∣<1|x| < 1x<1 时收敛,其和为:
∑k=0∞xk=11−x \sum_{k=0}^{\infty} x^k = \frac{1}{1 - x} k=0xk=1x1
∣x∣≥1|x| \geq 1x1 时发散。

几何级数的部分和(前 nnn 项)为:
∑k=0n−1xk=1−xn1−x,x≠1 \sum_{k=0}^{n-1} x^k = \frac{1 - x^n}{1 - x}, \quad x \ne 1 k=0n1xk=1x1xn,x=1

对几何级数两边关于 xxx 求导,得到:
ddx(∑k=0∞xk)=∑k=1∞kxk−1=1(1−x)2 \frac{d}{dx} \left( \sum_{k=0}^{\infty} x^k \right) = \sum_{k=1}^{\infty} k x^{k-1} = \frac{1}{(1 - x)^2} dxd(k=0xk)=k=1kxk1=(1x)21
两边乘以 xxx,得:
∑k=1∞kxk=x(1−x)2,∣x∣<1 \sum_{k=1}^{\infty} k x^k = \frac{x}{(1 - x)^2}, \quad |x| < 1 k=1kxk=(1x)2x,x<1
这是算法分析中常见的级数,例如在堆构建时间复杂度分析中出现。

x=12x = \frac{1}{2}x=21 代入上式:
∑k=1∞k(12)k=12(1−12)2=1214=2 \sum_{k=1}^{\infty} k \left( \frac{1}{2} \right)^k = \frac{\frac{1}{2}}{\left(1 - \frac{1}{2}\right)^2} = \frac{\frac{1}{2}}{\frac{1}{4}} = 2 k=1k(21)k=(121)221=4121=2
因此:
∑k=0∞k2k=0+∑k=1∞k2k=2 \sum_{k=0}^{\infty} \frac{k}{2^k} = 0 + \sum_{k=1}^{\infty} \frac{k}{2^k} = 2 k=02kk=0+k=12kk=2

类似地,可以计算更高阶的幂级数。例如:
∑k=1∞k2xk=x(1+x)(1−x)3,∣x∣<1 \sum_{k=1}^{\infty} k^2 x^k = \frac{x(1 + x)}{(1 - x)^3}, \quad |x| < 1 k=1k2xk=(1x)3x(1+x),x<1
该结果可通过对 ∑kxk\sum k x^kkxk 再次求导并整理得到。

幂级数在 ∣x∣<1|x| < 1x<1 内具有良好的分析性质:可以逐项求导、逐项积分,并且收敛半径内的和函数是连续可导的。

在算法分析中,常利用幂级数估算求和式,尤其是当项的形式为 k⋅rkk \cdot r^kkrk0<r<10 < r < 10<r<1 时,其无穷级数收敛于一个常数倍的 nnn,从而帮助证明线性时间复杂度。

例如,在 BUILD-MAX-HEAP 的时间复杂度分析中,总代价可表示为:
∑h=0⌊lg⁡n⌋n2h+1⋅O(h)=O(n∑h=0∞h2h)=O(n⋅2)=O(n) \sum_{h=0}^{\lfloor \lg n \rfloor} \frac{n}{2^{h+1}} \cdot O(h) = O\left( n \sum_{h=0}^{\infty} \frac{h}{2^h} \right) = O(n \cdot 2) = O(n) h=0lgn2h+1nO(h)=O(nh=02hh)=O(n2)=O(n)
其中利用了 ∑h=0∞h2h=2\sum_{h=0}^{\infty} \frac{h}{2^h} = 2h=02hh=2 这一结论。

幂级数的收敛性由比值判别法或根值判别法判断。对于 ∑akxk\sum a_k x^kakxk,收敛半径 RRR 为:
R=1lim sup⁡k→∞∣ak∣1/k R = \frac{1}{\limsup_{k \to \infty} |a_k|^{1/k}} R=limsupkak1/k1
或当极限存在时:
R=lim⁡k→∞∣akak+1∣ R = \lim_{k \to \infty} \left| \frac{a_k}{a_{k+1}} \right| R=klimak+1ak

常见函数的幂级数展开包括:

  • $ \frac{1}{1 - x} = \sum_{k=0}^{\infty} x^k, \quad |x| < 1 $
  • $ e^x = \sum_{k=0}^{\infty} \frac{x^k}{k!}, \quad \text{对所有 } x $
  • $ \ln(1 - x) = -\sum_{k=1}^{\infty} \frac{x^k}{k}, \quad |x| < 1 $
  • $ \sin x = \sum_{k=0}^{\infty} (-1)^k \frac{x^{2k+1}}{(2k+1)!}, \quad \text{对所有 } x $
  • $ \cos x = \sum_{k=0}^{\infty} (-1)^k \frac{x^{2k}}{(2k)!}, \quad \text{对所有 } x $

这些展开在近似计算和渐近分析中具有重要作用。


收敛性判别方法

  1. 比值判别法(Ratio Test)
    对级数 ∑ak\sum a_kak,令:
    L=lim⁡k→∞∣ak+1ak∣ L = \lim_{k \to \infty} \left| \frac{a_{k+1}}{a_k} \right| L=klimakak+1

    • L<1L < 1L<1,级数绝对收敛;
    • L>1L > 1L>1,级数发散;
    • L=1L = 1L=1,无法判断。

    应用于幂级数 ∑akxk\sum a_k x^kakxk,收敛半径为:
    R=lim⁡k→∞∣akak+1∣(若极限存在) R = \lim_{k \to \infty} \left| \frac{a_k}{a_{k+1}} \right| \quad \text{(若极限存在)} R=klimak+1ak(若极限存在)

  2. 根值判别法(Root Test)
    令:
    L=lim sup⁡k→∞∣ak∣1/k L = \limsup_{k \to \infty} |a_k|^{1/k} L=klimsupak1/k

    • L<1L < 1L<1,收敛;
    • L>1L > 1L>1,发散;
    • L=1L = 1L=1,无法判断。

    收敛半径为:
    R=1lim sup⁡k→∞∣ak∣1/k R = \frac{1}{\limsup_{k \to \infty} |a_k|^{1/k}} R=limsupkak1/k1

  3. 比较判别法
    0≤ak≤bk0 \leq a_k \leq b_k0akbk∑bk\sum b_kbk 收敛,则 ∑ak\sum a_kak 收敛;若 ∑ak\sum a_kak 发散,则 ∑bk\sum b_kbk 发散。

  4. 积分判别法
    f(k)=akf(k) = a_kf(k)=ak 是正的、单调递减函数,则 ∑k=1∞ak\sum_{k=1}^{\infty} a_kk=1ak∫1∞f(x)dx\int_1^{\infty} f(x) dx1f(x)dx 同敛散。


如何求幂级数的和

  1. 从已知级数出发
    利用基本级数(如几何级数)通过代数变换、求导、积分等操作构造目标级数。

    例如,已知:
    ∑k=0∞xk=11−x \sum_{k=0}^{\infty} x^k = \frac{1}{1 - x} k=0xk=1x1
    两边求导得:
    ∑k=1∞kxk−1=1(1−x)2⇒∑k=1∞kxk=x(1−x)2 \sum_{k=1}^{\infty} k x^{k-1} = \frac{1}{(1 - x)^2} \Rightarrow \sum_{k=1}^{\infty} k x^k = \frac{x}{(1 - x)^2} k=1kxk1=(1x)21k=1kxk=(1x)2x

  2. 逐项积分或求导
    若级数形式类似于某函数的导数或积分,可通过逆向操作求和。

    例如,由:
    11−x=∑k=0∞xk \frac{1}{1 - x} = \sum_{k=0}^{\infty} x^k 1x1=k=0xk
    两边从 0 到 xxx 积分:
    ∫0x11−tdt=−ln⁡(1−x)=∑k=0∞∫0xtkdt=∑k=0∞xk+1k+1=∑k=1∞xkk \int_0^x \frac{1}{1 - t} dt = -\ln(1 - x) = \sum_{k=0}^{\infty} \int_0^x t^k dt = \sum_{k=0}^{\infty} \frac{x^{k+1}}{k+1} = \sum_{k=1}^{\infty} \frac{x^k}{k} 0x1t1dt=ln(1x)=k=00xtkdt=k=0k+1xk+1=k=1kxk
    所以:
    ln⁡(1−x)=−∑k=1∞xkk \ln(1 - x) = -\sum_{k=1}^{\infty} \frac{x^k}{k} ln(1x)=k=1kxk

  3. 构造生成函数
    将序列 {an}\{a_n\}{an} 的生成函数表示为幂级数,并利用代数方法求闭式。

  4. 利用递推关系
    若序列满足线性递推,可设其生成函数为 G(x)=∑anxnG(x) = \sum a_n x^nG(x)=anxn,代入递推式解出 G(x)G(x)G(x)

∑h=0∞h2h=1/2(1−1/2)2=1/2(1/2)2=1/21/4=2 \sum_{h=0}^{\infty} \frac{h}{2^h} = \frac{1/2}{(1 - 1/2)^2} = \frac{1/2}{(1/2)^2} = \frac{1/2}{1/4} = 2 h=02hh=(11/2)21/2=(1/2)21/2=1/41/2=2

因此:
O(n∑h=0⌊lg⁡n⌋h2h)≤O(n∑h=0∞h2h)=O(n⋅2)=O(n) O\left( n \sum_{h=0}^{\lfloor \lg n \rfloor} \frac{h}{2^h} \right) \leq O\left( n \sum_{h=0}^{\infty} \frac{h}{2^h} \right) = O(n \cdot 2) = O(n) Onh=0lgn2hhO(nh=02hh)=O(n2)=O(n)


结论

尽管直观上 BUILD-MAX-HEAP 似乎是 $ O(n \lg n) $,但通过按高度分层分析可得:

BUILD-MAX-HEAP 的时间复杂度为 O(n) \boxed{ \text{BUILD-MAX-HEAP 的时间复杂度为 } O(n) } BUILD-MAX-HEAP 的时间复杂度为 O(n)

这意味着:我们可以在与数组长度成正比的时间内,将一个无序数组构造为一个最大堆

Exercise 6.3-2

题目:
对于 BUILD-MAX-HEAP 中第的循环控制变量 $ i $ 来说,为什么我们要求它是从 $ \lfloor A.\text{length}/2 \rfloor $ 到 1 递减,而不是从 1 到 $ \lfloor A.\text{length}/2 \rfloor $ 递增呢?

解答:

核心原因:
MAX-HEAPIFY 的正确性依赖于一个前提:在调用 MAX-HEAPIFY(A, i) 时,节点 $ i $ 的左右子树都已经是最大堆
为了满足这一前提,必须采用自底向上的处理顺序。


自底向上(递减顺序)—— 正确方式

  • 循环从 $ i = \lfloor n/2 \rfloor $ 递减到 1(1-based 索引)
  • 处理节点 $ i $ 时,其所有后代节点(子树中的节点)索引均大于 $ i $
  • 由于我们是从后往前处理,所有下标大于 $ i $ 的节点已经被处理过,其子树已满足最大堆性质
  • 因此,调用 MAX-HEAPIFY(A, i) 的前提条件成立

结论:
必须从 $ \lfloor n/2 \rfloor $ 递减到 1,以确保在处理每个节点时,其子树已经满足堆性质。这是 BUILD-MAX-HEAP 算法正确性的关键。

Exercise 6.3-3

题目:
证明:对于任一包含 $ n $ 个元素的堆,至多有 $ \left\lceil \frac{n}{2^{h+1}} \right\rceil $ 个高度为 $ h $ 的节点。

解答:

设堆中高度为 $ h $ 的节点有 $ k $ 个。

  • 每个高度为 $ h $ 的节点,其子树至少包含 $ 2^h $ 个叶节点
  • 不同子树的叶节点互不重叠
  • 堆的总叶节点数为 $ \left\lceil \frac{n}{2} \right\rceil $

因此:
k⋅2h≤⌈n2⌉≤n+12 k \cdot 2^h \leq \left\lceil \frac{n}{2} \right\rceil \leq \frac{n + 1}{2} k2h2n2n+1

解得:
k≤n+12h+1 k \leq \frac{n + 1}{2^{h+1}} k2h+1n+1

由于 $ k $ 为整数,且 $ \left\lceil \frac{n}{2^{h+1}} \right\rceil $ 是不小于 $ \frac{n}{2^{h+1}} $ 的最小整数,可得:
k≤⌈n2h+1⌉ k \leq \left\lceil \frac{n}{2^{h+1}} \right\rceil k2h+1n

结论:
高度为 $ h $ 的节点数至多为 $ \left\lceil \frac{n}{2^{h+1}} \right\rceil $。

6.4 堆排序

算法实现

def left(i):"""计算节点 i 的左子节点索引(0-based)"""return 2 * i + 1def right(i):"""计算节点 i 的右子节点索引(0-based)"""return 2 * i + 2def max_heapify(A, i, heap_size):"""维护最大堆性质参数:A: 待调整的数组i: 当前节点索引heap_size: 当前堆的有效大小"""l = left(i)r = right(i)# 找出 i、l、r 中最大值的索引if l < heap_size and A[l] > A[i]:largest = lelse:largest = iif r < heap_size and A[r] > A[largest]:largest = r# 如果最大值不在当前节点,则交换并继续下沉if largest != i:A[i], A[largest] = A[largest], A[i]max_heapify(A, largest, heap_size)def build_max_heap(A):"""将数组 A 构建为最大堆(自底向上)"""if len(A) <= 1:returnheap_size = len(A)# 从最后一个非叶节点开始向前处理start_index = len(A) // 2 - 1for i in range(start_index, -1, -1):max_heapify(A, i, heap_size)def heapsort(A):"""堆排序主函数(原地排序)步骤:1. 构建最大堆2. 重复将根节点(最大值)与末尾元素交换,并缩小堆规模3. 对新根调用 max_heapify 恢复堆性质"""if len(A) <= 1:return# 构建初始最大堆build_max_heap(A)heap_size = len(A)# 从最后一个元素开始,逐步将最大值放到正确位置for i in range(len(A) - 1, 0, -1):A[0], A[i] = A[i], A[0]      # 交换根与当前末尾heap_size -= 1                # 缩小堆大小 因为放在末尾的是已经固定好了的max_heapify(A, 0, heap_size)  # 恢复堆性质

Exercise 6.4-2

题目:
试分析在使用下列循环不变量时,HEAPSORT 的正确性:在算法的 for 循环每次迭代开始时,子数组 A[1…i]是一个包含了数组A[1…n]中第i小元素的最大堆,而子数组A[i+1…n]包含了数组A[1…n]中已排序的n-i个最大元素。

证明:

初始化:
当 $ i = n $ 时:

  • A[1…n] 是 BUILD-MAX-HEAP 构建的最大堆,包含所有未排序元素
  • A[n+1…n] 为空,已排序部分为空
  • 循环不变量成立

保持:
假设第 $ i $ 次迭代前不变量成立。

  • A[1] 是 A[1…i] 的最大值,即当前未排序部分的最大元素
  • 交换 A[1] 与 A[i] 后,该最大值被移至 A[i]
  • 将堆大小减一,A[1…i-1] 为当前堆,A[i…n] 为已排序部分
  • 调用 MAX-HEAPIFY(A, 1) 恢复 A[1…i-1] 的堆性质
  • 下次迭代(i 减 1)开始时,不变量仍成立

终止:
当 $ i = 1 $ 时:

  • A[1…1] 包含最小元素
  • A[2…n] 包含其余元素且已升序排列
  • 整个数组 A[1…n] 按升序排列

结论:
HEAPSORT 正确。

Exercise 6.4-3

题目:
对于一个按升序排列的包含 $ n $ 个元素的有序数组 $ A $ 来说,HEAPSORT 的时间复杂度是多少?如果 $ A $ 是降序呢?

解答:

升序数组:

  • BUILD-MAX-HEAP 需要 $ \Theta(n) $ 时间
    • 虽然数组升序,但 BUILD-MAX-HEAP 从最后一个非叶节点向上调用 MAX-HEAPIFY
    • 尽管每个节点可能需要下沉,但总时间仍为线性(见练习 6.3-3 的分析)
  • for 循环执行 $ n-1 $ 次,每次 MAX-HEAPIFY 耗时 $ \Theta(\log n) $
    • 每次交换后,根节点的值很小(如最小值),需下沉到底层
  • 总时间复杂度:$ \Theta(n \log n) $

降序数组:

  • BUILD-MAX-HEAP 需要 $ \Theta(n) $ 时间
    • 数组降序时,已是最大堆,每个 MAX-HEAPIFY 调用立即返回
    • 但仍需遍历 $ \lfloor n/2 \rfloor $ 个节点,耗时 $ \Theta(n) $
  • for 循环执行 $ n-1 $ 次,每次 MAX-HEAPIFY 耗时 $ \Theta(\log n) $
    • 每次交换后,新根可能破坏堆性质,需调整
  • 总时间复杂度:$ \Theta(n \log n) $

结论:
无论输入数组是升序还是降序,HEAPSORT 的时间复杂度均为 $ \Theta(n \log n) $。
堆排序在所有情况下都表现出稳定的最坏-case 性能。### Exercise 6.4-3

Exercise 6.4-4

题目:
对于一个按升序排列的包含 $ n $ 个元素的有序数组 $ A $ 来说,HEAPSORT 的时间复杂度是多少?如果 $ A $ 是降序呢?

解答:

升序数组:

  • BUILD-MAX-HEAP 需要 $ \Theta(n) $ 时间
    • 虽然数组升序,但 BUILD-MAX-HEAP 从最后一个非叶节点向上调用 MAX-HEAPIFY
    • 尽管每个节点可能需要下沉,但总时间仍为线性(见练习 6.3-3 的分析)
  • for 循环执行 $ n-1 $ 次,每次 MAX-HEAPIFY 耗时 $ \Theta(\log n) $
    • 每次交换后,根节点的值很小(如最小值),需下沉到底层
  • 总时间复杂度:$ \Theta(n \log n) $

降序数组:

  • BUILD-MAX-HEAP 需要 $ \Theta(n) $ 时间
    • 数组降序时,已是最大堆,每个 MAX-HEAPIFY 调用立即返回
    • 但仍需遍历 $ \lfloor n/2 \rfloor $ 个节点,耗时 $ \Theta(n) $
  • for 循环执行 $ n-1 $ 次,每次 MAX-HEAPIFY 耗时 $ \Theta(\log n) $
    • 每次交换后,新根可能破坏堆性质,需调整
  • 总时间复杂度:$ \Theta(n \log n) $

结论:
无论输入数组是升序还是降序,HEAPSORT 的时间复杂度均为 $ \Theta(n \log n) $。
堆排序在所有情况下都表现出稳定的最坏-case 性能。

Exercise 6.4-4

题目:
证明:在最坏情况下,HEAPSORT 的时间复杂度是 $ \Omega(n \log n) $。

解答:

考虑输入数组为严格降序排列的情况。

  1. 建堆阶段
    数组已是最大堆,BUILD-MAX-HEAP 中每个 MAX-HEAPIFY 调用立即返回,耗时 $ \Theta(n) $。

  2. 排序阶段
    对于 $ i = n, n-1, \ldots, 2 $:

    • 交换 A[1]A[i]
    • 新根 A[1] 是原 A[i],值较小
    • 需通过 MAX-HEAPIFY 下沉至叶节点附近
    • 当前堆大小为 $ i $,下沉深度为 $ \lfloor \lg i \rfloor $,耗时 $ \Omega(\lg i) $

    [!IMPORTANT]

    求和分析笔记:∑i=2nΩ(lg⁡i)=Ω(nlg⁡n)\sum_{i=2}^{n} \Omega(\lg i) = \Omega(n \lg n)i=2nΩ(lgi)=Ω(nlgn)

    我们要分析堆排序中 MAX-HEAPIFY 的总调用时间,其形式为:
    ∑i=2nΩ(lg⁡i) \sum_{i=2}^{n} \Omega(\lg i) i=2nΩ(lgi)
    目标是证明这个和为 Ω(nlg⁡n)\Omega(n \lg n)Ω(nlgn)


    1. 符号说明
    • lg⁡i=log⁡2i\lg i = \log_2 ilgi=log2i
    • Ω(lg⁡i)\Omega(\lg i)Ω(lgi) 表示某个函数 f(i)f(i)f(i) 满足 f(i)≥c⋅lg⁡if(i) \geq c \cdot \lg if(i)clgi 对足够大的 iii 成立(c>0c > 0c>0 为常数)
    • 因此 ∑i=2nΩ(lg⁡i)\sum_{i=2}^{n} \Omega(\lg i)i=2nΩ(lgi) 可理解为 Ω(∑i=2nlg⁡i)\Omega\left( \sum_{i=2}^{n} \lg i \right)Ω(i=2nlgi)

    即:
    ∑i=2nΩ(lg⁡i)=Ω(∑i=2nlg⁡i) \sum_{i=2}^{n} \Omega(\lg i) = \Omega\left( \sum_{i=2}^{n} \lg i \right) i=2nΩ(lgi)=Ω(i=2nlgi)


    1. 计算 ∑i=2nlg⁡i\sum_{i=2}^{n} \lg ii=2nlgi

    我们有:
    ∑i=2nlg⁡i=lg⁡2+lg⁡3+⋯+lg⁡n=lg⁡(2⋅3⋅…⋅n)=lg⁡(n!)−lg⁡1=lg⁡(n!) \sum_{i=2}^{n} \lg i = \lg 2 + \lg 3 + \cdots + \lg n = \lg (2 \cdot 3 \cdot \ldots \cdot n) = \lg (n!) - \lg 1 = \lg (n!) i=2nlgi=lg2+lg3++lgn=lg(23n)=lg(n!)lg1=lg(n!)

    因为 lg⁡1=0\lg 1 = 0lg1=0,所以:
    ∑i=2nlg⁡i=lg⁡(n!) \sum_{i=2}^{n} \lg i = \lg (n!) i=2nlgi=lg(n!)


    1. 使用斯特林近似(Stirling’s Approximation)

    斯特林公式给出:
    n!∼2πn(ne)n n! \sim \sqrt{2\pi n} \left( \frac{n}{e} \right)^n n!2πn(en)n

    取对数:
    lg⁡(n!)=lg⁡(2πn(ne)n)=lg⁡2πn+lg⁡(nnen)=12lg⁡(2πn)+nlg⁡n−nlg⁡e \lg(n!) = \lg \left( \sqrt{2\pi n} \left( \frac{n}{e} \right)^n \right) = \lg \sqrt{2\pi n} + \lg \left( \frac{n^n}{e^n} \right) = \frac{1}{2} \lg (2\pi n) + n \lg n - n \lg e lg(n!)=lg(2πn(en)n)=lg2πn+lg(ennn)=21lg(2πn)+nlgnnlge

    展开:
    lg⁡(n!)=nlg⁡n−nlg⁡e+12lg⁡(2πn) \lg(n!) = n \lg n - n \lg e + \frac{1}{2} \lg (2\pi n) lg(n!)=nlgnnlge+21lg(2πn)

    其中:

    • nlg⁡nn \lg nnlgn 是主导项
    • −nlg⁡e-n \lg enlge 是线性项(lg⁡e≈1.44\lg e \approx 1.44lge1.44
    • 12lg⁡(2πn)\frac{1}{2} \lg (2\pi n)21lg(2πn) 是对数项

    因此:
    lg⁡(n!)=nlg⁡n−O(n) \lg(n!) = n \lg n - O(n) lg(n!)=nlgnO(n)


    1. 渐近下界分析

    由于:
    lg⁡(n!)=nlg⁡n−O(n) \lg(n!) = n \lg n - O(n) lg(n!)=nlgnO(n)
    O(n)O(n)O(n)nlg⁡nn \lg nnlgn 增长得慢,所以存在常数 c>0c > 0c>0n0n_0n0,使得对所有 n≥n0n \geq n_0nn0
    lg⁡(n!)≥c⋅nlg⁡n \lg(n!) \geq c \cdot n \lg n lg(n!)cnlgn

    即:
    lg⁡(n!)=Ω(nlg⁡n) \lg(n!) = \Omega(n \lg n) lg(n!)=Ω(nlgn)


    1. 最终结论

    ∑i=2nΩ(lg⁡i)=Ω(∑i=2nlg⁡i)=Ω(lg⁡(n!))=Ω(nlg⁡n) \sum_{i=2}^{n} \Omega(\lg i) = \Omega\left( \sum_{i=2}^{n} \lg i \right) = \Omega(\lg(n!)) = \Omega(n \lg n) i=2nΩ(lgi)=Ω(i=2nlgi)=Ω(lg(n!))=Ω(nlgn)


    1. 补充:无需斯特林也可证明

    即使不用斯特林公式,也可以通过积分估计:

    ∑i=2nlg⁡i≥∫1nlg⁡x dx=∫1nln⁡xln⁡2 dx=1ln⁡2[xln⁡x−x]1n=1ln⁡2(nln⁡n−n+1) \sum_{i=2}^{n} \lg i \geq \int_{1}^{n} \lg x \, dx = \int_{1}^{n} \frac{\ln x}{\ln 2} \, dx = \frac{1}{\ln 2} \left[ x \ln x - x \right]_1^n = \frac{1}{\ln 2} (n \ln n - n + 1) i=2nlgi1nlgxdx=1nln2lnxdx=ln21[xlnxx]1n=ln21(nlnnn+1)

    转换为以 2 为底:
    =nlg⁡n−nln⁡2+1ln⁡2=nlg⁡n−O(n) = n \lg n - \frac{n}{\ln 2} + \frac{1}{\ln 2} = n \lg n - O(n) =nlgnln2n+ln21=nlgnO(n)

    同样得到:
    ∑i=2nlg⁡i=Ω(nlg⁡n) \sum_{i=2}^{n} \lg i = \Omega(n \lg n) i=2nlgi=Ω(nlgn)


    总结

    因此,堆排序在排序阶段的总时间至少为 Ω(nlg⁡n)\Omega(n \lg n)Ω(nlgn),证明其最坏情况时间复杂度为 Ω(nlg⁡n)\Omega(n \lg n)Ω(nlgn)

    总时间为:
    ∑i=2nΩ(lg⁡i)=Ω(∑i=2nlg⁡i)=Ω(nlg⁡n) \sum_{i=2}^{n} \Omega(\lg i) = \Omega\left( \sum_{i=2}^{n} \lg i \right) = \Omega(n \lg n) i=2nΩ(lgi)=Ω(i=2nlgi)=Ω(nlgn)
    (因为 $ \sum_{i=1}^{n} \lg i = \lg(n!) = \Theta(n \lg n) $)

  3. 结论
    存在输入使得 HEAPSORT 运行时间为 $ \Omega(n \lg n) $,故其最坏情况时间复杂度为 $ \Omega(n \lg n) $

6.5 优先队列

下次一定写

http://www.dtcms.com/a/308570.html

相关文章:

  • DooTask非营利性组织:让高效协作触手可及
  • Day 5: 深度学习理论与PyTorch实现 - 神经网络训练的艺术
  • RocketMQ消息队列:从入门到Spring Boot实战
  • 【React】fiber 架构
  • OS架构整理
  • Spring Boot音乐服务器项目-移除喜欢和操作
  • C语言07
  • 【n8n】mysql凭证设置,及注意问题
  • 智能交通顶刊TITS论文分享|跨区域自适应车辆轨迹预测:TRACER框架攻克域偏移难题!
  • Linux进程创建,终止与等待
  • 哈希的概念及其应用
  • Java学习------Executor框架
  • C++语言的发展历程、核心特性与学习指南
  • Tang Prime 20K板OV5640例程
  • 【软件架构】八大架构解析
  • 点控云数据洞察智能体:让汽车行业决策有据可循,让业务增长稳健前行
  • OpenCV 的 Mat 类详解
  • 亚马逊自然流量增长密码:从算法逻辑到运营体系的全维度解析
  • WSL配置网络说明
  • 太阳光模拟器测试包装材料的耐候性
  • SUID/SGID是啥?如何让普通用户拥有root的能力?
  • WinForm之CheckBox 控件
  • Conda环境下配置的基本命令
  • 【Android】PopupWindow实现长按菜单
  • 难以逾越的夏天
  • 小架构step系列31:处理异常
  • documentPictureInPicture API 教程
  • IK 字段级别词典的升级之路
  • 14day-ai入门-人工智能基础学习-OpenCV-图像预处理4
  • 2683. 相邻值的按位异或