Bagaking细讲算法 堆 I: 堆的原理和应用场景

本文详细介绍了堆这种数据结构的原理和应用场景,包括堆的定义、性质、实现方式以及复杂度分析。堆通常用于实现优先队列,解决排序、第K顺序统计量和多路归并等问题。通过优化,可以降低插入和删除操作的时间复杂度,提高算法性能。
摘要由CSDN通过智能技术生成

堆 I: 堆的原理和应用场景

堆这种数据结构由 J. W. J. Williams 于 1964 年在介绍堆排时提出. 原理简单, 而有着广泛的应用.

我们直接来看其在 wiki 上的定义:

a heap is a specialized tree-based data structure which is essentially an almost complete tree that satisfies the heap property

堆是一个满足堆的性质的近乎完全的树型数据结构

heap property: in a max heap, for any given node C, if P is a parent node of C, then the key (the value) of P is greater than or equal to the key of C. In a min heap, the key of P is less than or equal to the key of C. The node at the “top” of the heap (with no parents) is called the root node.

堆的性质: 对于任一节点 C, 其父节点 P 的值始终大于等于(或始终小于等于) C, 则称这个堆结构具备堆的性质. 和树一样, 定义堆结构的根节点为唯一没有父节点的节点. 对于每个C始终有P >= C的为最大堆, 反之为最小堆.

显然, 最大堆的根是所有项里的最大值, 所以又叫大根堆或大顶堆. 同理, 最小堆的根则是最小值, 又叫小根堆或小顶堆.

堆的结构实现

一般来说会用链式结构来实现一棵树, 用这种方法实现的数据结构叫做 explicit data structure, 即除了原本的信息之外还保存了很多. 相对的, 有 implicit 的实现方式.

implicit data structure or space-efficient data structure is a data structure that stores very little information other than the main or required data: a data structure that requires low overhead. They are called “implicit” because the position of the elements carries meaning and relationship between elements

implicit data structure: 在主要数据之外只需要存储非常少的额外信息的数据结构, 即需要很低额外开销(Overhead) 的数据结构. 之所以叫做 “implicit”, 是因为每个元素所在的位置已经完全可以说明元素之间的关系.

N叉树(包含2叉树), 或任意分叉规则明确的树(比如 K-D Tree), 都可以实现为 implicit data structure 的数据结构. 以2叉树为例, 一个常见的规则是将2叉树表示为一个数组.

如果用元素从0开始的数组实现: 根节点为 0, 对于任意一个元项 i 其左子节点和右子节点的下标分别是 2i + 1, 2i + 2, 而其父节点为 (i - 1) / 2. 也常见 1 为根节点, 则 i 的左右子节点分别为 2i, 2i + 1, 父节点为 i / 2, 用O(1)空间换取略小的计算常数. 在此基础上, 扩展到N叉树, 或是改变左右子节点的寻址规则, 都是简单的.

不过这种实现对于一般情况有一个问题, 则是当树节点不完全, 甚至比较稀疏的情况下, 有较多的空间浪费. 对于平衡树或近乎完全的树, 则不会有这个问题. 由于堆定义上就是近乎的树结构, 因此我们一般用 implicit 的方式实现堆.

堆的实现 性质的保持

为了便于理解和举例, 我们基于最常见的二叉堆讨论.

二叉堆可以认为是一棵符合堆的性质的二叉树, 因此它也是一个"近乎完全"的二叉树:

In a complete binary tree every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible.

在一个完全二叉树中, 除了最后一层的其它各层是完全填满的, 并且最后一层中的所有节点全部集中在左边, 这样的二叉树叫做完全二叉树. 虽然堆的定义中, 只需要 almost complete tree, 但我们一般当成一棵完全二叉树来讨论.

结构上, 如上一节所说:

  • 访问根节点: seq[0]
  • 访问i的左子节点: seq[2*i+1]
  • 访问i的左子节点: seq[2*i+2]
  • 访问i的父子节点: seq[(i-1)/2]

这样就有了二叉树的实现.

现在我们来赋予其堆的性质, 堆只需要查询最大值或最小值, 也就是堆顶项的值. 为了保持其值始终是最值, 我们需要在每个项插入时, 保持堆的性质即可. 对于没有任何项的堆, 显然是符合堆的性质的, 而堆的性质被改变, 只可能发生在插入 insert 或弹出 extract (extract_min, extract_max) 项时.

为了方便表述, 最大/最小的表达统一为最小, 较大/较小的表达统一为较小. extract_max/extract_min 的表达统一为 extract.

insert: 插入时只有 C 本身有可能不对, 于是我们可以使C逐次上浮 siftUp, 当C上浮到其正确的位置, 则整个树满足堆的性质: 先将待插入的项 C 插入到二叉堆的末端 seq[length++] = v, 此时这个项的位置是可能不对的. 于是将其和其父节点 P 进行比较, 如果 P <= C, 或 P 不存在, 则 C 已经在其正确的位置了. 否则将 C 上移 swap(i, (i-1)/2), 直到满足条件.

extract: 堆的弹出操作弹出的是 root 元素, 为了维持堆的性质, 我们需要找出整个堆里面最小的值, 显然, 这个值必然是 root 的左右子节点的其中一个. 所以要找出较小的那个, 将其至于堆顶 seq[1] < seq[2] ? swap(0, 1) : swap(0, 2). 但这样空位就被下移了, 还好堆的左右子树必也都为一个堆, 所以我们可以迭代的对这个子树进行上述流程. 可以使当前序列恢复成堆.
这个操作相当于是把堆顶的空位移到了堆底, 但这样有两个问题, 一是每次都需要把空位从顶移到底, 而是可能会导致树的最后一层存在 gap, 大多数情况下需要额外维护.
所以一般的做法, 是将整个列表的长度先缩短, 并同时将最后一项移到root上 seq[0] = seq[--length], 这样堆里面只有 root 的位置是不对的, 只要让这个项下沉 siftDown 到其应该在的位置就行了.
伪代码:

switch(posOfMinInThree){
   
    case i: return;
    case left: swap(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值