堆(Heap)

1. 堆(Heap)

1.1. Python实现堆的插入、堆顶删除和排序

复制代码
class MaxHeap:
    def __init__(self):
        # 初始化空堆,使用列表表示
        self.heap = []

    def insert(self, val):
        # 插入元素并执行上浮
        self.heap.append(val)
        self._sift_up(len(self.heap) - 1)

    def delete(self):
        # 删除并返回堆顶元素
        if not self.heap:
            return None
        self._swap(0, len(self.heap) - 1)
        max_val = self.heap.pop()
        self._sift_down(0)
        return max_val

    def build_heap(self, arr):
        # 从任意数组构建大顶堆
        self.heap = arr[:]
        for i in range((len(self.heap) - 2) // 2, -1, -1):
            self._sift_down(i)

    def heapsort(self):
        # 原地堆排序(不会额外开辟空间)
        n = len(self.heap)
        # 先构建堆
        for i in range((n - 2) // 2, -1, -1):
            self._sift_down(i)

        # 每次把最大值(堆顶)放到末尾,然后缩小堆范围
        for end in range(n - 1, 0, -1):
            self._swap(0, end)
            self._sift_down(0, end)

    def _sift_up(self, idx):
        # 上浮操作,保持堆结构
        parent = (idx - 1) // 2
        while idx > 0 and self.heap[idx] > self.heap[parent]:
            self._swap(idx, parent)
            idx = parent
            parent = (idx - 1) // 2

    def _sift_down(self, idx, size=None):
        # 下沉操作,保持堆结构(可传入范围用于排序)
        if size is None:
            size = len(self.heap)
        while True:
            largest = idx
            left = 2 * idx + 1
            right = 2 * idx + 2

            if left < size and self.heap[left] > self.heap[largest]:
                largest = left
            if right < size and self.heap[right] > self.heap[largest]:
                largest = right

            if largest == idx:
                break

            self._swap(idx, largest)
            idx = largest

    def _swap(self, i, j):
        # 辅助函数:交换堆中两个元素
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]

    def __str__(self):
        # 返回堆内容的字符串表示
        return str(self.heap)

import heapq

def heapsort_asc(iterable):
    heap = []
    for value in iterable:
        heapq.heappush(heap, value)  # 构建最小堆
    return [heapq.heappop(heap) for _ in range(len(heap))]  # 升序弹出

# 示例
nums = [7, 2, 5, 3, 1]
print("升序堆排序结果:", heapsort_asc(nums))

1.2. 堆的定义

1. 堆的基本概念

  • 是一种特殊的树形数据结构,广泛应用于算法中,如堆排序。
  • 堆的两个主要特征
  1. 完全二叉树:除了最后一层,其他层的节点都已满,最后一层的节点靠左排列。
  2. 节点值的大小关系

大顶堆(Max-Heap):每个节点的值都大于或等于其子节点的值。

小顶堆(Min-Heap):每个节点的值都小于或等于其子节点的值。

2. 堆的实现与存储

  • 堆通常使用数组来存储,因为完全二叉树的结构适合用数组表示。
  • 对于列表下标为 i 的节点:

左子节点下标:2i + 1

右子节点下标:2i + 2

父节点下标:i -1 // 2

1.3. 堆的核心操作

完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。

具体python代码见5.1,以大顶堆为例。

1. 往堆中插入一个元素

  • 将新元素插入堆的最后,然后进行堆化(Heapify)操作来恢复堆的性质。
  • 堆化分为两种方式:

自下而上堆化:从插入的节点开始,依次与父节点比较,直到堆的性质恢复。

2. 删除堆顶元素

删除堆顶元素(最大或最小)后,将最后一个元素移动到堆顶,并进行自上而下堆化来恢复堆的性质。

时间复杂度:

一个包含 n个节点的完全二叉树,树的高度不会超过 log2n。堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是 O(logn)。插入数据和删除堆顶元素的主要逻辑就是堆化,所以,往堆中插入一个元素和删除堆顶元素的时间复杂度都是 O(logn)

3. 堆排序

堆排序的过程大致分解成两个大的步骤,建堆排序

建堆:

  • 将数组构建成一个堆。
  • 可以从前往后逐个插入元素构建堆(自下而上堆化)。
  • 或者从后往前处理,使用自上而下堆化的方式来构建堆。

排序:

每次将堆顶元素(最大或最小)与最后一个元素交换,将堆的大小减小 1,然后进行堆化。重复此过程直到数组排序完成。

不是稳定的排序算法。

时间复杂度分析:

建堆:

虽然每个节点堆化的时间复杂度为 O(log n),但由于堆的层数递减,因此建堆的时间复杂度是 O(n)

排序:

每次删除堆顶元素并堆化的时间复杂度为 O(log n),重复此过程 n 次,因此堆排序的总时间复杂度是 O(n log n)

**快速排序 vs 堆排序:**性能对比表(实际开发视角)

|-----------|------------------------|------------------------------------------|
| 对比维度 | 快速排序(Quick Sort) | 堆排序(Heap Sort) |
| 数据访问模式 | 局部顺序访问:访问相邻元素,缓存友好 | 非顺序访问:跳跃式访问(如下标 1 → 2 → 4 → 8),缓存不友好 |
| 缓存命中率 | 高(连续访问内存,有利于 CPU 缓存预取) | 低(访问分散,频繁触发缓存未命中) |
| 数据交换次数 | 相对较少,尤其在数据部分有序的情况下 | 多:建堆过程中频繁打乱已有顺序,导致额外的交换 |
| 对初始有序性敏感 | 是(部分有序时性能更好) | 否(建堆打乱顺序) |
| 排序过程破坏有序度 | 否,尽量保持已有的顺序结构 | 是,建堆导致已有顺序被打乱 |

1.4. 堆的应用

1. 优先级队列(Priority Queue)

定义:

  • 与普通队列不同,出队顺序按优先级高低而非 FIFO。
  • 堆可高效实现优先级队列:

插入元素:O(logn)

取出优先级最高元素(堆顶):O(logn)

应用示例:

合并有序小文件:

  • 场景:将 100 个 100MB 的有序小文件合并为一个大文件。
  • 方案:

每个文件取一个字符串放入小顶堆。

每次从堆顶取最小值写入结果文件,再从对应文件中取下一个值加入堆。

时间复杂度大幅优于线性遍历(数组)。

高性能定时器:

  • 维护很多任务,每个任务有执行时间。
  • 传统方法每秒轮询扫描任务表,低效。
  • 使用小顶堆优化:

堆顶存放最早执行的任务

定时器睡眠到该任务执行时间,节省资源。

每次取堆顶更新下一次唤醒时间。

2. 利用堆求 Top K 问题

Top K 分为两类:

静态 Top K(一次性获取):

  • 使用K 大小的小顶堆
  • 遍历整个数组:

若当前元素 > 堆顶:替换堆顶,重新堆化。

  • 时间复杂度:O(nlogK)

动态 Top K(实时查询):

  • 维护一个固定大小为 K 的小顶堆。
  • 插入新数据时:

若新数据 > 堆顶:替换堆顶。

否则忽略。

  • 查询时直接返回堆中元素。

3. 利用堆求中位数(及任意百分位数据)

静态数据集合:

  • 可直接排序取中位数,代价高但结果固定。

动态数据集合:

  • 方法:双堆法

大顶堆(max-heap):存前半部分数据。

小顶堆(min-heap):存后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。

保持两个堆平衡(大小相差不超过1)。

中位数取决于两个堆的堆顶元素。

优势:

  • 每次插入数据后,只需调整两个堆即可,效率高。
  • 实现实时获取中位数,时间复杂度约为 O(logn)
相关推荐
uyeonashi6 分钟前
【Boost搜索引擎】构建Boost站内搜索引擎实践
开发语言·c++·搜索引擎
再睡一夏就好9 分钟前
从硬件角度理解“Linux下一切皆文件“,详解用户级缓冲区
linux·服务器·c语言·开发语言·学习笔记
TIF星空1 小时前
【使用 C# 获取 USB 设备信息及进行通信】
开发语言·经验分享·笔记·学习·microsoft·c#
Smile丶凉轩3 小时前
Qt 界面优化(绘图)
开发语言·数据库·c++·qt
reasonsummer4 小时前
【办公类-100-01】20250515手机导出教学照片,自动上传csdn+最大化、最小化Vs界面
开发语言·python
Doker 多克4 小时前
Python-Django系列—日志
python·日志
S01d13r5 小时前
LeetCode 解题思路 48(编辑距离、只出现一次的数字)
算法·leetcode·职场和发展
C_Liu_5 小时前
C语言:深入理解指针(5)
java·c语言·算法
small_wh1te_coder5 小时前
从经典力扣题发掘DFS与记忆化搜索的本质 -从矩阵最长递增路径入手 一步步探究dfs思维优化与编程深度思考
c语言·数据结构·c++·stm32·算法·leetcode·深度优先
枫景Maple5 小时前
LeetCode 45. 跳跃游戏 II(中等)
算法·leetcode