二叉堆详解实现优先级队列.md 14.6 KB
Newer Older
L
labuladong 已提交
1 2
# 二叉堆详解实现优先级队列

3 4
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
5
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
6 7 8 9
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
10
![](https://labuladong.gitee.io/pictures/souyisou1.png)
L
labuladong 已提交
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线,[第 17 期刷题打卡挑战](https://aep.xet.tech/s/2jPp5X) 最后一天报名!另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
L
labuladong 已提交
13

14 15 16 17


**-----------**

L
labuladong 已提交
18 19
二叉堆(Binary Heap)没什么神秘,性质比二叉搜索树 BST 还简单。其主要操作就两个,`sink`(下沉)和 `swim`(上浮),用以维护二叉堆的性质。其主要应用有两个,首先是一种排序方法「堆排序」,第二是一种很有用的数据结构「优先级队列」。

L
labuladong 已提交
20
本文参考《算法 4》的代码,以实现优先级队列(Priority Queue)为例,来讲讲一下二叉堆怎么运作的。
L
labuladong 已提交
21 22 23

### 一、二叉堆概览

L
labuladong 已提交
24
首先,二叉堆和二叉树有啥关系呢,为什么人们总是把二叉堆画成一棵二叉树?
L
labuladong 已提交
25

L
labuladong 已提交
26
因为,二叉堆在逻辑上其实是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针:
L
labuladong 已提交
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

```java
// 父节点的索引
int parent(int root) {
    return root / 2;
}
// 左孩子的索引
int left(int root) {
    return root * 2;
}
// 右孩子的索引
int right(int root) {
    return root * 2 + 1;
}
```

L
labuladong 已提交
43
画个图你立即就能理解了,比如 `arr` 是一个字符数组,注意数组的第一个索引 0 空着不用:
L
labuladong 已提交
44

L
labuladong 已提交
45
![](https://labuladong.gitee.io/pictures/heap/1.png)
L
labuladong 已提交
46

L
labuladong 已提交
47
你看到了,因为这棵二叉树是「完全二叉树」,所以把 `arr[1]` 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处。
L
labuladong 已提交
48

L
labuladong 已提交
49
为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。
L
labuladong 已提交
50

L
labuladong 已提交
51
二叉堆还分为最大堆和最小堆。**最大堆的性质是:每个节点都大于等于它的两个子节点**。类似的,最小堆的性质是:每个节点都小于等于它的子节点。
L
labuladong 已提交
52 53 54

两种堆核心思路都是一样的,本文以最大堆为例讲解。

L
labuladong 已提交
55
对于一个最大堆,根据其性质,显然堆顶,也就是 `arr[1]` 一定是所有元素中最大的元素。
L
labuladong 已提交
56 57 58 59 60

### 二、优先级队列概览

优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。

L
labuladong 已提交
61
数据结构的功能无非增删查改,优先级队列有两个主要 API,分别是 `insert` 插入一个元素和 `delMax` 删除最大元素(如果底层用最小堆,那么就是 `delMin`)。
L
labuladong 已提交
62 63 64

下面我们实现一个简化的优先级队列,先看下代码框架:

L
labuladong 已提交
65
> tip:这里用到 Java 的泛型,`Key` 可以是任何一种可比较大小的数据类型,比如 Integer 等类型。
L
labuladong 已提交
66 67 68 69 70 71 72

```java
public class MaxPQ
    <Key extends Comparable<Key>> {
    // 存储元素的数组
    private Key[] pq;
    // 当前 Priority Queue 中的元素个数
L
labuladong 已提交
73
    private int size = 0;
L
labuladong 已提交
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

    public MaxPQ(int cap) {
        // 索引 0 不用,所以多分配一个空间
        pq = (Key[]) new Comparable[cap + 1];
    }

    /* 返回当前队列中最大元素 */
    public Key max() {
        return pq[1];
    }

    /* 插入元素 e */
    public void insert(Key e) {...}

    /* 删除并返回当前队列中最大元素 */
    public Key delMax() {...}

L
labuladong 已提交
91 92
    /* 上浮第 x 个元素,以维护最大堆性质 */
    private void swim(int x) {...}
L
labuladong 已提交
93

L
labuladong 已提交
94 95
    /* 下沉第 x 个元素,以维护最大堆性质 */
    private void sink(int x) {...}
L
labuladong 已提交
96 97

    /* 交换数组的两个元素 */
L
labuladong 已提交
98
    private void swap(int i, int j) {
L
labuladong 已提交
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
        Key temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;
    }

    /* pq[i] 是否比 pq[j] 小? */
    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    /* 还有 left, right, parent 三个方法 */
}
```

空出来的四个方法是二叉堆和优先级队列的奥妙所在,下面用图文来逐个理解。

### 三、实现 swim 和 sink

L
labuladong 已提交
117
为什么要有上浮 `swim` 和下沉 `sink` 的操作呢?为了维护堆结构。
L
labuladong 已提交
118 119 120

我们要讲的是最大堆,每个节点都比它的两个子节点大,但是在插入元素和删除元素时,难免破坏堆的性质,这就需要通过这两个操作来恢复堆的性质了。

L
labuladong 已提交
121
对于最大堆,会破坏堆性质的有两种情况:
L
labuladong 已提交
122

L
labuladong 已提交
123
1、如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行**下沉**
L
labuladong 已提交
124

L
labuladong 已提交
125
2、如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的**上浮**
L
labuladong 已提交
126 127 128 129 130 131 132 133 134 135

当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个 `while` 循环。

细心的读者也许会问,这两个操作不是互逆吗,所以上浮的操作一定能用下沉来完成,为什么我还要费劲写两个方法?

是的,操作是互逆等价的,但是最终我们的操作只会在堆底和堆顶进行(等会讲原因),显然堆底的「错位」元素需要上浮,堆顶的「错位」元素需要下沉。

**上浮的代码实现:**

```java
L
labuladong 已提交
136
private void swim(int x) {
L
labuladong 已提交
137
    // 如果浮到堆顶,就不能再上浮了
L
labuladong 已提交
138 139 140 141 142
    while (x > 1 && less(parent(x), x)) {
        // 如果第 x 个元素比上层大
        // 将 x 换上去
        swap(parent(x), x);
        x = parent(x);
L
labuladong 已提交
143 144 145 146 147 148
    }
}
```

画个 GIF 看一眼就明白了:

L
labuladong 已提交
149
![](https://labuladong.gitee.io/pictures/heap/swim.gif)
L
labuladong 已提交
150 151 152 153 154 155

**下沉的代码实现:**

下沉比上浮略微复杂一点,因为上浮某个节点 A,只需要 A 和其父节点比较大小即可;但是下沉某个节点 A,需要 A 和其**两个子节点**比较大小,如果 A 不是最大的就需要调整位置,要把较大的那个子节点和 A 交换。

```java
L
labuladong 已提交
156
private void sink(int x) {
L
labuladong 已提交
157
    // 如果沉到堆底,就沉不下去了
L
labuladong 已提交
158
    while (left(x) <= size) {
L
labuladong 已提交
159
        // 先假设左边节点较大
L
labuladong 已提交
160
        int max = left(x);
L
labuladong 已提交
161
        // 如果右边节点存在,比一下大小
L
labuladong 已提交
162 163 164 165 166 167 168
        if (right(x) <= size && less(max, right(x)))
            max = right(x);
        // 结点 x 比俩孩子都大,就不必下沉了
        if (less(max, x)) break;
        // 否则,不符合最大堆的结构,下沉 x 结点
        swap(x, max);
        x = max;
L
labuladong 已提交
169 170 171 172 173 174
    }
}
```

画个 GIF 看下就明白了:

L
labuladong 已提交
175
![](https://labuladong.gitee.io/pictures/heap/sink.gif)
L
labuladong 已提交
176 177 178 179 180 181 182

至此,二叉堆的主要操作就讲完了,一点都不难吧,代码加起来也就十行。明白了 `sink``swim` 的行为,下面就可以实现优先级队列了。

### 四、实现 delMax 和 insert

这两个方法就是建立在 `swim``sink` 上的。

L
labuladong 已提交
183
**`insert` 方法先把要插入的元素添加到堆底的最后,然后让其上浮到正确位置**
L
labuladong 已提交
184

L
labuladong 已提交
185
![](https://labuladong.gitee.io/pictures/heap/insert.gif)
L
labuladong 已提交
186 187 188

```java
public void insert(Key e) {
L
labuladong 已提交
189
    size++;
L
labuladong 已提交
190
    // 先把新元素加到最后
L
labuladong 已提交
191
    pq[size] = e;
L
labuladong 已提交
192
    // 然后让它上浮到正确的位置
L
labuladong 已提交
193
    swim(size);
L
labuladong 已提交
194 195 196
}
```

L
labuladong 已提交
197
**`delMax` 方法先把堆顶元素 `A` 和堆底最后的元素 `B` 对调,然后删除 `A`,最后让 `B` 下沉到正确位置**
L
labuladong 已提交
198 199 200 201 202 203

```java
public Key delMax() {
    // 最大堆的堆顶就是最大元素
    Key max = pq[1];
    // 把这个最大元素换到最后,删除之
L
labuladong 已提交
204 205 206
    swap(1, size);
    pq[size] = null;
    size--;
L
labuladong 已提交
207 208 209 210 211 212
    // 让 pq[1] 下沉到正确位置
    sink(1);
    return max;
}
```

L
labuladong 已提交
213
![](https://labuladong.gitee.io/pictures/heap/delete.gif)
L
labuladong 已提交
214

215
至此,一个优先级队列就实现了,插入和删除元素的时间复杂度为 `O(logK)``K` 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。
L
labuladong 已提交
216 217 218 219 220 221 222 223 224 225 226

### 五、最后总结

二叉堆就是一种完全二叉树,所以适合存储在数组中,而且二叉堆拥有一些特殊性质。

二叉堆的操作很简单,主要就是上浮和下沉,来维护堆的性质(堆有序),核心代码也就十行。

优先级队列是基于二叉堆实现的,主要操作是插入和删除。插入是先插到最后,然后上浮到正确位置;删除是调换位置后再删除,然后下沉到正确位置。核心代码也就十行。

也许这就是数据结构的威力,简单的操作就能实现巧妙的功能,真心佩服发明二叉堆算法的人!

L
labuladong 已提交
227
最后,更多二叉堆/优先级队列相关的题目练习见 [二叉堆(优先级队列)的经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_633faa64e4b0eca59c3a1aa3/1)
L
labuladong 已提交
228

L
labuladong 已提交
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260


<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
 - [双指针技巧秒杀七道链表题目](https://labuladong.github.io/article/fname.html?fname=链表技巧)
 - [如何调度考生的座位](https://labuladong.github.io/article/fname.html?fname=座位调度)
 - [快速排序详解及应用](https://labuladong.github.io/article/fname.html?fname=快速排序)

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
| [1104. Path In Zigzag Labelled Binary Tree](https://leetcode.com/problems/path-in-zigzag-labelled-binary-tree/?show=1) | [1104. 二叉树寻路](https://leetcode.cn/problems/path-in-zigzag-labelled-binary-tree/?show=1) |
| [1845. Seat Reservation Manager](https://leetcode.com/problems/seat-reservation-manager/?show=1) | [1845. 座位预约管理系统](https://leetcode.cn/problems/seat-reservation-manager/?show=1) |
| [23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/?show=1) | [23. 合并K个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/?show=1) |
| [347. Top K Frequent Elements](https://leetcode.com/problems/top-k-frequent-elements/?show=1) | [347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/?show=1) |
| [451. Sort Characters By Frequency](https://leetcode.com/problems/sort-characters-by-frequency/?show=1) | [451. 根据字符出现频率排序](https://leetcode.cn/problems/sort-characters-by-frequency/?show=1) |
| [662. Maximum Width of Binary Tree](https://leetcode.com/problems/maximum-width-of-binary-tree/?show=1) | [662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/?show=1) |
| [692. Top K Frequent Words](https://leetcode.com/problems/top-k-frequent-words/?show=1) | [692. 前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/?show=1) |
| [703. Kth Largest Element in a Stream](https://leetcode.com/problems/kth-largest-element-in-a-stream/?show=1) | [703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/?show=1) |
L
labuladong 已提交
261
| - | [剑指 Offer 40. 最小的k个数](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/?show=1) |
L
labuladong 已提交
262
| - | [剑指 Offer II 059. 数据流的第 K 大数值](https://leetcode.cn/problems/jBjn9C/?show=1) |
L
labuladong 已提交
263
| - | [剑指 Offer II 060. 出现频率最高的 k 个数字](https://leetcode.cn/problems/g5c51o/?show=1) |
L
labuladong 已提交
264 265 266 267 268 269
| - | [剑指 Offer II 078. 合并排序链表](https://leetcode.cn/problems/vvXgSW/?show=1) |

</details>



270 271
**_____________**

L
labuladong 已提交
272
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
273

L
labuladong 已提交
274
![](https://labuladong.gitee.io/pictures/souyisou2.png)
L
labuladong 已提交
275 276


B
brucecat 已提交
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
======其他语言代码======

### javascript

```js
/**
 * 最大堆
 */
function left(i) {
  return i * 2 + 1;
}

function right(i) {
  return i * 2 + 2;
}

function swap(A, i, j) {
  const t = A[i];
  A[i] = A[j];
  A[j] = t;
}

class Heap {
  constructor(arr) {
    this.data = [...arr];
    this.size = this.data.length;
  }

  /**
   * 重构堆
   */
  rebuildHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i--) {
      this.maxHeapify(i);
    }
  }

  isHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i++) {
      const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
      const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;

      const max = Math.max(this.data[i], l, r);

      if (max !== this.data[i]) {
        return false;
      }
      return true;
    }
  }

  sort() {
    for (let i = this.size - 1; i > 0; i--) {
      swap(this.data, 0, i);
      this.size--;
      this.maxHeapify(0);
    }
  }

  insert(key) {
    this.data[this.size++] = key;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  delete(index) {
    if (index >= this.size) {
      return;
    }
    this.data.splice(index, 1);
    this.size--;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  /**
   * 堆的其他地方都满足性质
   * 唯独跟节点,重构堆性质
   * @param {*} i
   */
  maxHeapify(i) {
    let max = i;

    if (i >= this.size) {
      return;
    }

    // 求左右节点中较大的序号
    const l = left(i);
    const r = right(i);
    if (l < this.size && this.data[l] > this.data[max]) {
      max = l;
    }

    if (r < this.size && this.data[r] > this.data[max]) {
      max = r;
    }

    // 如果当前节点最大,已经是最大堆
    if (max === i) {
      return;
    }

    swap(this.data, i, max);

    // 递归向下继续执行
    return this.maxHeapify(max);
  }
}

module.exports = Heap;
```
L
labuladong 已提交
395