【Python】堆排序

在排序算法的宏伟殿堂中,如果说快速排序是一位依赖灵感与分割艺术的大师,归并排序是一位崇尚秩序与稳定融合的建筑师,那么**堆排序(Heap Sort)则更像一位深谙自然法则的炼金术士。它不依赖于元素间的线性比较,也不追求递归的优雅分割,而是将一个看似无序的线性数组,重塑为一个蕴含着严格层级秩序(Hierarchical Order)**的内在结构——堆(Heap)。然后,它通过一种极具韵律感的“提取”与“重构”的循环,将这种内在的势能,转化为外在的、完全有序的线性序列。

堆排序的灵魂,不在于“排序”这个动作本身,而在于“堆”这个数据结构的构建与维护。它是一种原地排序算法,拥有媲美归并排序的O(n log n)稳定时间复杂度,同时又具备O(1)的卓越空间效率。

我们将开启这段炼金之旅的第一步。我们将暂时忘掉“排序”,全身心地投入到对“堆”这一核心概念的理解之中。我们将探索它的双重属性,揭示它如何用一个简单的线性数组,完美地“伪装”成一棵复杂的树形结构,并亲手实现其最核心的两个操作:heapify(堆化)与build_heap(建堆)。这是理解后续一切高级应用与优化的起点。

1.1 什么是堆?秩序的双重奏 (What is a Heap? A Duet of Order)

从本质上讲,堆是一种特殊的、基于树形结构的数据结构。为了成为一个合格的“堆”,一个数据集合必须同时满足两个极为严格的条件:结构属性堆属性

1. 结构属性:一棵“几乎”完美的树 (Structural Property: An “Almost” Perfect Tree)

堆在结构上必须是一棵完全二叉树(Complete Binary Tree)。这个概念至关重要,它是我们后续能用数组来表示堆的理论基石。

一棵完全二叉树具有以下特征:

  • 树的每一层,除了最后一层外,都必须被完全填满。
  • 最后一层的所有节点,都必须尽可能地靠左排列

让我们通过可视化的方式来理解:

       (满二叉树 - Full Binary Tree)         (完全二叉树 - Complete Binary Tree)      (非完全二叉树)
              1                                      1                                    1
            /   \                                  /   \                                /   \
           2     3                                2     3                              2     3
          / \   / \                              / \   /                              /     /
         4   5 6   7                            4   5 6                              4     6

       是完全二叉树,也是满二叉树               是完全二叉树,但不是满二叉树         不是完全二叉树 (节点6应在5之后)

这种“层层填满,左对齐”的结构,保证了树的节点之间不存在任何“空隙”(除了最后一层的右侧)。这种无缝的结构,使得节点的位置可以用一种可预测的方式进行编号,从而为数组化表示埋下了伏笔。

2. 堆属性:父辈的“荣光” (Heap Property / Order Property)

堆属性定义了节点值之间的层级关系。根据这种关系的不同,堆又分为两种:

  • 最大堆(Max-Heap): 任意一个节点的值,都必须大于或等于其所有子节点的值。 这意味着,从根节点到任意一个叶子节点的路径上,节点的值是单调不增的。树的根节点,必然是整个堆中的最大值。
  • 最小堆(Min-Heap): 任意一个节点的值,都必须小于或等于其所有子节点的值。 这意味着,从根节点到任意一个叶子节点的路径上,节点的值是单调不减的。树的根节点,必然是整个堆中的最小值。

一个最大堆的示例:

              100
            /     \
           75      80
          /  \    /  \
         30  40  20  15
        / \
       10  5

在这个例子中,你可以任选一个父节点,例如 75,它的值大于其子节点 3040。根节点 100 是整个数据集中的最大值。但请注意,堆属性并没有对兄弟节点之间的关系做出任何规定。例如,7580 之间的大小关系是任意的。堆维护的是垂直的层级秩序,而非水平的线性秩序。

堆排序利用的就是最大堆。 其核心逻辑就是:既然根节点永远是当前堆中的最大值,那么我们只要不断地把根节点“取”出来,放到有序区的末尾,然后调整剩下的元素,使其重新满足最大堆的性质,就能依次得到第1大、第2大、第3大…的元素,从而完成排序。

1.2 数组的化身:堆的线性代数表示 (The Array Incarnation: Linear Algebraic Representation of a Heap)

如果每次操作堆,我们都需要处理复杂的树节点对象和指针,那么其效率和便捷性将大打折扣。而完全二叉树的优美结构,允许我们使用一个简单的一维数组,来分毫不差地表示整个树,而无需任何额外的指针开销。这正是堆在工程实践中如此高效和迷人的关键所在。

映射规则:
我们将树的节点,按照从上到下、从左到右的顺序,依次存入数组中。数组的索引从0开始。

示例映射:

              100 (索引 0)
            /     \
           75(1)   80(2)
          /  \    /  \
      30(3) 40(4) 20(5) 15(6)

对应的数组表示: arr = [100, 75, 80, 30, 40, 20, 15]

在这种映射关系下,一个节点的父节点和子节点在数组中的索引,存在着美妙的数学规律。对于数组中索引为 i 的任意一个节点:

  • 其父节点的索引: parent(i) = (i - 1) // 2 (这里使用整数除法)

    • 例如,节点 40 的索引是 4。其父节点的索引是 (4 - 1) // 2 = 1,对应的值是 75。正确。
    • 节点 80 的索引是 2。其父节点的索引是 (2 - 1) // 2 = 0,对应的值是 100。正确。
  • 其左子节点的索引: left_child(i) = 2 * i + 1

    • 例如,节点 75 的索引是 1。其左子节点的索引是 2 * 1 + 1 = 3,对应的值是 30。正确。
  • 其右子节点的索引: right_child(i) = 2 * i + 2

    • 例如,节点 75 的索引是 1。其右子节点的索引是 2 * 1 + 2 = 4,对应的值是 40。正确。

通过这三个简单的公式,我们就在一个线性数组之上,重建了完整的树形层级关系。我们可以随时从任何一个“节点”(即数组元素)出发,找到它的“亲属”,而这一切,都只是廉价的整数运算。

1.3 堆的心跳:heapify(堆化)操作 (The Heartbeat of the Heap: The heapify Operation)

heapify(有时也称为 sift_downpercolate_down)是堆数据结构中最核心、最基本的操作。所有更复杂的操作(如建堆、插入、删除)都构建于它之上。

heapify的使命:
它的任务是:给定一个节点 i,假设它的左子树和右子树已经分别是合格的最大堆,但节点 i 本身可能违反了最大堆的属性(即它可能比它的某个子节点小)。heapify(i) 的功能,就是让节点 i “下沉”(sift down)到树中的正确位置,从而使得以 i 为根的整个子树,重新成为一个合格的最大堆。

操作流程(以最大堆为例):

  1. 找出“最大”候选人: 在节点 i、其左子节点 left、其右子节点 right 这三者中,找到值最大的那个节点。我们称其索引为 largest
  2. 判断与交换:
    • 如果 largest 的索引就是 i 本身,这意味着节点 i 已经比它的两个子节点都大(或等),它已经满足了最大堆属性。heapify过程结束。
    • 如果 largest 的索引不是 i(例如,是leftright),这意味着节点 i 的位置是“错误”的。此时,将 arr[i]arr[largest] 的值进行交换
  3. 递归地向下修复: 当交换发生后,原来的 arr[i] 的值被换到了 largest 的位置。这个新值可能会破坏以 largest 为根的子树的堆属性。因此,我们必须递归地对索引为 largest 的节点,调用heapify,继续向下进行修复,直到不再发生交换为止。

Pythonic 实现: heapify

def heapify(arr, n, i):
    """
    对以索引 i 为根的子树进行堆化(sift-down),使其满足最大堆性质。

    参数:
        arr (list): 存储堆的数组。
        n (int): 堆的大小(有效元素的数量)。
        i (int): 需要进行堆化的子树的根节点索引。
    """
    largest = i      # 假设根节点 i 就是最大的
    left = 2 * i + 1   # 计算左子节点的索引
    right = 2 * i + 2  # 计算右子节点的索引

    # 步骤1: 找出根、左、右三者中的最大值
    # 检查左子节点是否存在 (小于堆大小n),并且是否大于当前最大值
    if left < n and arr[left] > arr[largest]:
        largest = left

    # 检查右子节点是否存在,并且是否大于当前最大值
    if right < n and arr[right] > arr[largest]:
        largest = right

    # 步骤2: 判断与交换
    # 如果最大的节点不是当前的根节点 i,则说明需要调整
    if largest != i:
        # 交换根节点与最大的那个子节点
        arr[i], arr[largest] = arr[largest], arr[i]

        # 步骤3: 递归地向下修复
        # 因为交换可能破坏了下一层子树的堆属性,所以要对被交换的子节点递归调用heapify
        heapify(arr, n, largest)

1.4 建造壁垒:build_heap(建堆) (Building the Citadel: build_heap)

现在我们拥有了修复单个节点的工具heapify,下一个问题是:如何将一个完全无序的普通数组,高效地转化为一个完整的最大堆?

朴素的想法:
从头到尾遍历数组,对每个元素都调用一次heapify?这并不可行,因为heapify的前提是子树已经堆化。

正确且高效的思路:
回顾heapify的前提——它的子树必须是堆。在树的结构中,所有的叶子节点,天然就可以被视为一个只包含一个元素的、合法的堆。

因此,我们可以从最后一个非叶子节点开始,向前(向数组头部)依次对每个节点调用heapify,直到处理完根节点(索引0)。当我们处理一个节点i时,由于我们是倒序处理的,所有在它之后的节点(即它的子孙节点)都已经被处理过,这意味着它的左右子树必然已经是合格的堆了。这完美地满足了heapify的执行前提。

  • 最后一个非叶子节点的索引: 在一个大小为n的堆中,最后一个元素的索引是n-1。它的父节点索引是 ((n-1) - 1) // 2 = (n // 2) - 1。这就是我们开始heapify的起点。

Pythonic 实现: build_heap

def build_heap(arr):
    """
    将一个任意的数组原地转换成一个最大堆。

    参数:
        arr (list): 待转换的数组。
    """
    n = len(arr) # 获取数组大小

    # 计算最后一个非叶子节点的索引
    start_index = n // 2 - 1

    # 从最后一个非叶子节点开始,倒序遍历到根节点
    # 对每个节点执行 heapify 操作
    for i in range(start_index, -1, -1):
        heapify(arr, n, i)

build_heap的时间复杂度分析: 这是一个非直观但非常重要的结论。虽然build_heap包含一个循环,循环内部调用了heapifyO(log n)),但build_heap的整体时间复杂度并非O(n log n),而是**O(n)**。

直观解释:

  • heapify的运行时间取决于节点的高度。
  • 在堆中,绝大多数节点都集中在树的底层。
  • 树的最底层(约n/2个节点)是叶子,heapify成本为0。
  • 倒数第二层(约n/4个节点),高度为1,heapify成本很低。
  • 只有靠近根节点的少数节点,才需要付出O(log n)heapify成本。
  • 将所有节点的成本加权求和,会得到一个收敛的级数,其总和是O(n)。这证明了build_heap是一个非常高效的线性时间操作。
1.5 炼金术的施展:堆排序算法 (Performing the Alchemy: The Heap Sort Algorithm)

万事俱备。我们已经可以将任意数组高效地转化为一个最大堆。现在,排序的最后一步变得异常简单和清晰。

堆排序的完整流程:

  1. 建堆阶段 (Build Heap Phase):

    • 调用build_heap(arr),将整个输入数组原地转换成一个最大堆。此步过后,arr[0]就是数组中的最大元素。
  2. 排序提取阶段 (Sorting Extraction Phase):

    • 进行n-1次循环,从i = n-1递减到1。在每一次循环中:

      • 提取最大值: 将当前堆的根节点arr[0](即当前未排序部分的最大值)与堆的最后一个元素arr[i]进行交换
      • “隔离”最大值: 交换后,当前的最大值已经被放到了数组的末尾arr[i],这个位置就是它最终应该在的有序位置。我们将这部分视为“已排序区”,在后续操作中不再理会它。
      • 缩小堆: 通过将传递给heapify的堆大小参数n减一(在我们的循环中,就是用i作为堆的大小),来逻辑上“缩小”堆的范围。
      • 恢复堆: 交换后,新的根节点arr[0]是一个较小的值,它破坏了最大堆的属性。我们对新的根节点调用heapify(arr, i, 0)(注意,此时堆的大小是i),来恢复剩余i个元素的堆结构。
    • 当循环结束时,整个数组就从后到前,依次被填上了第1大、第2大、…、第n大的元素,从而变得完全有序。

1.6 完整的Python实现与剖析 (The Complete Python Implementation and Analysis)
# heapify 和 build_heap 函数已在上面定义

def heap_sort(arr):
    """
    对一个列表进行原地堆排序。

    参数:
        arr (list): 待排序的列表。
    """
    n = len(arr) # 获取列表长度

    # 步骤1: 建堆阶段
    # 将原始的无序列表原地构建成一个最大堆
    build_heap(arr)

    # 步骤2: 排序提取阶段
    # 从堆的最后一个元素开始,倒序遍历到第二个元素 (索引为1)
    for i in range(n - 1, 0, -1):
        # 将堆顶元素 (当前最大值) 与当前范围的最后一个元素交换
        # 这步操作将最大值放到了它在排序后数组中的最终位置
        arr[i], arr[0] = arr[0], arr[i]

        # 交换后,新的堆顶元素可能违反了最大堆性质
        # 我们需要在缩小的堆上 (大小为 i) 对根节点 (索引 0) 重新进行堆化
        heapify(arr, i, 0)

# --- 示例 ---
# data_to_sort = [12, 11, 13, 5, 6, 7, 30, -1]
# print(f"原始数组: {data_to_sort}")
# heap_sort(data_to_sort)
# print(f"堆排序后: {data_to_sort}")
1.7 初始性能评估:时间、空间与稳定性 (Initial Performance Evaluation: Time, Space, and Stability)
  • 时间复杂度 (Time Complexity): O(n log n)

    • build_heap阶段的成本是 O(n)
    • 排序提取阶段包含一个 n-1 次的循环。循环体内部,交换是O(1)heapify操作的成本是O(log k),其中k是当前堆的大小。由于kn-1递减到1,总成本是 sum(log k) for k=1 to n-1,这个和的结果是O(n log n)
    • 总时间复杂度 = O(n) + O(n log n) = O(n log n)。堆排序的时间复杂度在最坏、平均、最好情况下都是O(n log n),非常稳定。
  • 空间复杂度 (Space Complexity): O(1)

    • 堆排序是**原地排序(In-Place)**的典范。除了存储输入数组本身所需的空间外,它几乎不需要任何额外的辅助空间。heapify的递归版本会使用O(log n)的栈空间,但这通常被认为是O(1)的辅助空间,因为它与输入规模n相比极小。这是堆排序相比归并排序O(n)空间的一个巨大优势。
  • 稳定性 (Stability): 不稳定 (Unstable)

    • 堆排序是一个不稳定的排序算法。
    • 原因: 在排序提取阶段的交换操作 arr[i], arr[0] = arr[0], arr[i],很可能会改变值相等的元素的原始相对顺序。
    • 反例: 考虑数组 [item(3, 'a'), item(5, 'x'), item(3, 'b')]。在建堆和排序过程中,item(3, 'a')item(3, 'b') 的相对位置很容易被最后的交换操作打乱。例如,如果某一时刻item(3, 'b')被换到了堆顶,而item(3, 'a')在数组末尾,那么一次交换就会颠倒它们的顺序。

**第二章:超越排序:作为优先队列的堆与heapq模块的深层实现 **

我们将进行一次视角的彻底转换。我们将不再把堆看作一个静态的、一次性建成的结构,而是看作一个动态的、支持高效插入和删除的数据结构。我们将首先深入剖析Python标准库heapq模块——这个看似简单却极其高效的工具集——的内部工作原理,并揭示其选择最小堆的深层原因。接着,我们将亲手封装一个面向对象的、功能更完备的优先队列类。最后,我们将运用堆(优先队列)来解决一系列经典的算法问题,例如“数据流中的第K大元素”、“合并K个有序列表”以及“Top K高频元素”,从而深刻理解其在处理动态数据和维护动态顺序方面的无与伦比的威力。

2.1 最小堆的“偏爱”:heapq模块的设计哲学与实现剖析 (The “Preference” for Min-Heaps: Design Philosophy and Implementation Analysis of the heapq Module)

当你第一次接触Python的heapq模块时,可能会感到一丝困惑。与其他语言(如Java的PriorityQueue)提供一个完整的类不同,heapq提供的是一组直接作用于列表(list)的函数,例如heapq.heappush()heapq.heappop()。并且,它默认实现的是最小堆,而非我们在堆排序中使用的最大堆。这些设计决策背后,蕴含着深刻的Pythonic哲学和对性能的极致追求。

为何是函数,而非类?

  • 性能与底层访问: Python的列表list是在C语言层面实现的、高度优化的动态数组。通过直接在list上操作,heapq的函数可以避免Python层面面向对象方法调用所带来的额外开销(如self的传递、方法查找等)。这使得heapq的操作速度极快,几乎是在C语言的速度上运行。
  • 灵活性与通用性: heapq将数据存储(list)与堆算法(functions)解耦。这意味着你可以将任何序列类型(只要它支持索引访问和修改),通过heapq的函数,赋予其堆的性质。这种“鸭子类型”的哲学,使得heapq非常灵活。
  • Pythonic的“工具箱”思想: Python的标准库设计,倾向于提供一组小而精的、正交的(orthogonal)工具,让用户可以像搭积木一样组合它们来解决问题,而不是提供一个庞大、笨重的“全能”类。heapq正是这一思想的体现。

为何默认是最小堆?
这是一个基于广泛应用场景和算法通用性的权衡。

  • 与排序的内在联系: 稳定排序算法(如Timsort)的迭代过程,与重复地从数据源中寻找并提取最小值,在逻辑上是相通的。许多复杂的排序和合并算法,其基础就是“k路合并”,而k路合并的核心就是从k个源中高效地找出最小值,这正是最小堆的专长。
  • 通用性与转换成本: 从一个最小堆,我们可以非常轻易地模拟出一个最大堆。技巧是:在插入元素时,存入其相反数或一个包装好的、颠倒了比较逻辑的对象。
    import heapq
    
    # 模拟最大堆
    max_heap = []
    data = [3, 1, 4, 1, 5, 9, 2, 6]
    for item in data:
        # 存入相反数,使得原来的最大值变成最小值
        heapq.heappush(max_heap, -item)
        
    # 弹出时,再次取反,恢复原值
    # 弹出的顺序将是 9, 6, 5, ...
    largest = -heapq.heappop(max_heap) # largest will be 9
    
    这种转换的开销极小。因此,提供一个高效的最小堆实现,就等同于同时提供了最大堆的能力。

heapq核心函数实现剖析

heapq模块的源代码(在CPython中是用C实现的)本质上就是我们第一章中heapify思想的变体,但方向相反。heappushheappop的核心是**“上浮”(sift-up)“下沉”(sift-down)**。

  • heapq.heappush(heap, item) -> sift-up(上浮)

    1. 添加到末尾: 首先,将新元素item直接添加到列表的末尾。这保证了树的“完全性”结构属性。
    2. 破坏与修复: 添加后,新元素可能会违反最小堆的性质(即它可能比它的父节点小)。
    3. 上浮循环: 从新添加的节点(最后一个元素)开始,不断地将它和它的父节点进行比较。如果它比父节点小,就与父节点交换位置。这个过程一直持续,直到它不再比父节点小,或者它已经到达了堆顶(索引0)。这个过程被称为sift-uppercolate-up
  • heapq.heappop(heap) -> sift-down(下沉)

    1. 提取最小值: 堆顶heap[0]永远是最小值。
    2. 结构维护: 为了保持堆的结构,我们不能直接删除堆顶。一个绝妙的技巧是:将堆的最后一个元素,移动到堆顶的位置,然后将列表的大小减一。
    3. 破坏与修复: 此时,新的堆顶元素(原来的最后一个元素)几乎肯定违反了最小堆的性质。
    4. 下沉修复: 从堆顶(索引0)开始,执行一次**sift-down**操作(这与我们在第一章为最大堆实现的heapify完全相同,只是比较逻辑变成了<而不是>)。将新的堆顶元素不断地与它较小的那个子节点交换,直到它“下沉”到正确的位置。

用Python模拟heapq的核心逻辑

# 以下代码是为了教学目的,用纯Python模拟heapq的核心实现。
# 实际使用中,请直接用 import heapq。

def _sift_up(heap, pos):
    """
    模拟 heappush 中的上浮操作 (最小堆)。
    参数 pos 是新元素插入的位置 (通常是末尾)。
    """
    new_item = heap[pos] # 新插入的元素
    # 当我们还没有到达堆顶 (pos > 0)
    while pos > 0:
        parent_pos = (pos - 1) >> 1 # 使用位运算右移1位,等效于 // 2,但可能更快
        parent = heap[parent_pos]
        if new_item < parent:
            # 如果新元素比父节点小,将父节点向下移动
            heap[pos] = parent
            pos = parent_pos # 继续向上追溯
        else:
            # 否则,找到了新元素的正确位置
            break
    heap[pos] = new_item # 将新元素放置在最终找到的位置

def heappush_py(heap, item):
    """模拟 heapq.heappush"""
    heap.append(item) # 添加到末尾
    _sift_up(heap, len(heap) - 1) # 从末尾开始上浮

def _sift_down(heap, pos, n):
    """
    模拟 heappop 中的下沉操作 (最小堆)。
    参数 pos 是需要下沉的节点的起始位置 (通常是堆顶)。
    参数 n 是堆的有效大小。
    """
    new_item = heap[pos] # 需要下沉的元素
    while (2 * pos + 1) < n: # 当至少存在左子节点时
        left_child_pos = 2 * pos + 1
        right_child_pos = left_child_pos + 1
        
        # 找到两个子节点中较小的那个
        smaller_child_pos = left_child_pos
        if right_child_pos < n and heap[right_child_pos] < heap[left_child_pos]:
            smaller_child_pos = right_child_pos
            
        # 如果需要下沉的元素比它较小的子节点还要大,则交换
        if new_item > heap[smaller_child_pos]:
            heap[pos] = heap[smaller_child_pos]
            pos = smaller_child_pos # 继续向下追溯
        else:
            # 否则,找到了正确位置
            break
    heap[pos] = new_item

def heappop_py(heap):
    """模拟 heapq.heappop"""
    if not heap:
        raise IndexError("pop from an empty heap")
    
    last_item = heap.pop() # 弹出最后一个元素
    if heap:
        # 如果堆中还有元素
        return_item = heap[0] # 记录下堆顶的最小值
        heap[0] = last_item   # 将最后一个元素放到堆顶
        _sift_down(heap, 0, len(heap)) # 从堆顶开始下沉
        return return_item
    return last_item # 如果堆中只有一个元素,直接返回

# --- 示例 ---
# my_heap = []
# data = [3, 1, 4, 1, 5, 9, 2, 6]
# for item in data:
#     heappush_py(my_heap, item)
#     # print(f"Pushed {item}, heap is now: {my_heap}")

# print("\nPopping from heap:")
# while my_heap:
#     print(f"Popped: {heappop_py(my_heap)}, heap is now: {my_heap}")

2.2 构建一个面向对象的优先队列 (Building an Object-Oriented Priority Queue)

虽然heapq的函数式接口性能极高,但在复杂的应用程序中,我们通常更喜欢一个封装良好的来表示优先队列。这个类可以在内部使用heapq来获得高性能,同时对外提供更清晰、更符合面向对象思想的接口(如put(), get(), is_empty())。

设计一个优先队列类 PriorityQueue:

  • 内部存储: 使用一个私有列表 _queue 来存储数据。
  • 优先级与数据分离: 为了处理复杂对象和自定义优先级,队列中存储的应该是元组 (priority, data)。这使得堆的比较操作只作用于priority,而data可以是任意类型的“载荷”。
  • 处理插入顺序: 当两个元素的优先级相同时,我们希望先插入的元素先被取出(FIFO特性)。这是一种稳定性要求。为了实现这一点,我们可以引入一个单调递增的计数器,将存储的元组扩展为 (priority, insertion_count, data)。这样,当优先级相同时,会比较插入顺序,保证了稳定性。

Pythonic 实现: PriorityQueue

import heapq
import itertools

class PriorityQueue:
    """
    一个功能完备、线程安全的、面向对象的优先队列实现。
    内部使用 heapq 模块保证性能。
    """
    def __init__(self):
        self._queue = []  # 内部使用列表存储堆元素
        self._counter = itertools.count() # 一个无限的、单调递增的计数器,用于保证稳定性
        
    def put(self, item, priority=0):
        """
        向优先队列中添加一个元素。

        参数:
            item (any): 需要存储的数据项。
            priority (number): 该项的优先级,数值越小,优先级越高。
        """
        # heapq是最小堆,所以我们直接使用priority
        # 将 (优先级, 插入顺序, 数据项) 作为一个元组推入堆中
        count = next(self._counter) # 获取一个唯一的、递增的插入顺序号
        entry = (priority, count, item)
        heapq.heappush(self._queue, entry)

    def get(self):
        """
        从优先队列中移除并返回优先级最高的元素。
        如果队列为空,则引发 IndexError。
        """
        if self.is_empty():
            raise IndexError("get from an empty priority queue")
        
        # heappop会返回优先级最高的元组
        priority, count, item = heapq.heappop(self._queue)
        return item # 只返回原始数据项

    def peek(self):
        """
        查看但不移除优先级最高的元素。
        """
        if self.is_empty():
            raise IndexError("peek from an empty priority queue")
        # 堆顶 _queue[0] 就是优先级最高的元组
        priority, count, item = self._queue[0]
        return item

    def is_empty(self):
        """检查队列是否为空"""
        return not self._queue

    def __len__(self):
        """返回队列中的元素数量"""
        return len(self._queue)

# --- 示例 ---
# class Task:
#     def __init__(self, name):
#         self.name = name
#     def __repr__(self):
#         return f"Task({self.name})"

# pq = PriorityQueue()

# # 添加任务,优先级越小越高
# pq.put(Task("处理用户请求"), priority=2)
# pq.put(Task("发送邮件通知"), priority=5)
# pq.put(Task("写入数据库"), priority=1)
# pq.put(Task("清理临时文件"), priority=5) # 与发送邮件优先级相同
# pq.put(Task("生成报表"), priority=3)


# print(f"队列中有 {len(pq)} 个任务。")
# print(f"优先级最高的任务是: {pq.peek()}")

# print("\n按优先级处理任务:")
# while not pq.is_empty():
#     task = pq.get()
#     print(f"  正在处理: {task}")
# # 预期输出顺序: 写入数据库, 处理用户请求, 生成报表, 发送邮件通知, 清理临时文件
# # 注意,发送邮件和清理临时文件优先级相同,但发送邮件先被插入,所以先被处理。

这个PriorityQueue类,优雅地解决了heapq模块接口的“原始性”问题,提供了一个健壮、易用且高性能的数据结构,可以直接应用于需要复杂优先级调度的各种应用程序中。

2.3 经典应用场景:堆在算法问题中的威力 (Classic Application Scenarios: The Power of Heaps in Algorithmic Problems)

优先队列(堆)是解决一系列特定类型算法问题的“银弹”。这些问题通常具有以下特征:需要从一个动态变化的数据集合中,反复地、高效地找到最大或最小的元素。

问题一:数据流中的第K大元素 (Kth Largest Element in a Stream)

问题描述: 设计一个类,可以不断地向其中添加数字,并能随时高效地返回当前所有数字中第K大的那个。

解决方案: 使用一个大小固定为K最小堆

  1. 初始化: 创建一个空的最小堆。
  2. 添加元素 add(val):
    • 如果堆的大小还不足K,直接将val推入堆中。
    • 如果堆的大小已经等于K
      • 比较val堆顶元素heap[0](即当前堆中的最小值,也就是已见过的数字中第K大的那个)。
      • 如果val比堆顶元素还小(或等于),说明它不可能成为新的第K大元素,直接忽略它。
      • 如果val比堆顶元素大,说明它有潜力成为新的第K大元素。此时,我们调用heapq.heapreplace(heap, val)。这个高效的操作会先heappop()掉堆顶的最小值,然后再heappush(val)新元素,一步完成替换。
  3. 获取结果: 任何时候,堆顶heap[0]就是当前数据流中的第K大元素。

Pythonic 实现:

import heapq

class KthLargest:
    def __init__(self, k, nums):
        """
        初始化。
        k: 我们要找的是第 k 大的元素。
        nums: 初始的数字流。
        """
        self.k = k  # 存储 k 的值
        self.heap = [] # 初始化一个最小堆

        # 用初始数字填充堆
        for num in nums:
            self.add(num)

    def add(self, val):
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值