返回 LeetCode 刷题

Markdown File

堆和二叉堆基础

堆和二叉堆基础.md

1堆 ADT / 二叉堆笔记
21. 前提条件
3
4堆是一种常用来实现:
5
6优先队列 Priority Queue的数据结构。
7
8普通队列:先进先出 FIFO
9
10优先队列:优先级最高的先出来
11
12例如:
13
14普通队列:
15先来先出
16
17优先队列:
18分数最高 / 数值最小 / 权重最大 的先出
19
20二叉堆是实现优先队列的常见方式,它满足“完全二叉树结构”和“堆序性质”。
21
222. 核心概念
23
24二叉堆首先是一棵:完全二叉树
25
26完全二叉树要求:
27
28除了最后一层,其余层都满;
29最后一层从左到右依次填充。
30
31例如合法:
32
33 1
34 / \
35 3 5
36 / \ /
37 7 9 8
38
39不合法:
40
41 1
42 / \
43 3 5
44 \ \
45 9 8
46
47因为最后一层没有从左到右填。
48
493. 堆的两种类型
50
511. 小根堆 Min Heap
52
53规则:
54
55父节点 <= 子节点
56
57也就是:堆顶最小
58
59例如:
60
61 1
62 / \
63 3 5
64 / \ /
65 7 9 8
66
67每个父节点都比孩子小。
68
69适合:每次取最小值
70
712. 大根堆 Max Heap
72
73规则:父节点 >= 子节点
74
75也就是:堆顶最大
76
77例如:
78
79 9
80 / \
81 7 8
82 / \ /
83 3 1 5
84
85适合:每次取最大值
86
874. 二叉堆不是 BST
88
89BST:
90
91左子树 < 根 < 右子树
92
93堆:
94
95只要求父节点和孩子满足大小关系
96
97例如小根堆:
98
99父节点 <= 左孩子
100父节点 <= 右孩子
101
102但不要求:左孩子 < 右孩子
103
104也不要求中序有序。
105
106所以堆不能像 BST 那样查找某个普通元素。
107
1085. 为什么二叉堆常用数组存
109
110因为二叉堆是完全二叉树。
111
112完全二叉树可以按层序放进数组,不需要真的用 left、right 指针。
113
114例如:
115
116 1
117 / \
118 3 5
119 / \ /
120 7 9 8
121
122数组:
123
124下标: 0 1 2 3 4 5
125值: 1 3 5 7 9 8
126
127也就是层序存储。
128
129数组实现二叉堆时,如果根节点在下标 0,那么对于下标 i,左孩子是 2i+1,右孩子是 2i+2,父节点是 (i-1)/2。
130
1316. 数组下标关系
132
1330 下标写法
134当前节点:i
135
136左孩子:2*i + 1
137
138右孩子:2*i + 2
139
140父节点:(i - 1) / 2
141
142例如数组:
143
144下标: 0 1 2 3 4 5
145值: 1 3 5 7 9 8
146
147对于下标 1:
148
149值 = 3
150
151左孩子下标 = 2*1+1 = 3,值 7
152右孩子下标 = 2*1+2 = 4,值 9
153父节点下标 = (1-1)/2 = 0,值 1
1541 下标写法
155
156当前节点:i
157
158左孩子:2*i
159
160右孩子:2*i + 1
161
162父节点:i / 2
163
164LeetCode / C 数组一般更常用:
165
1660 下标
167
1687. 堆 ADT 常见操作
169
170堆一般支持:
171
172InitHeap 初始化堆
173IsEmpty 判空
174Push / Insert 插入元素
175Pop / Delete 删除堆顶
176Top / FindMin 查看堆顶
177BuildHeap 建堆
178
1798. 插入操作:上滤
180
181以小根堆为例。
182
183插入新元素时:
184
185先放到数组最后
186再和父节点比较
187如果比父节点小,就往上交换
188直到满足堆序性质
189
190这个过程叫:上滤
191sift up / percolate up
192
193例子,小根堆:
194
195 2
196 / \
197 5 7
198 /
199 10
200
201数组:
202
203[2,5,7,10]
204
205插入:
206
2071
208
209先放最后:
210
211[2,5,7,10,1]
212
213对应:
214
215 2
216 / \
217 5 7
218 / \
219 10 1
220
2211 比父节点 5 小,交换:
222
223[2,1,7,10,5]
224
2251 比父节点 2 小,交换:
226
227[1,2,7,10,5]
228
229最终堆顶变成 1。
230
2319. 删除堆顶:下滤
232
233以小根堆为例。
234
235删除堆顶时:
236
2371. 堆顶元素删除
2382. 把最后一个元素放到堆顶
2393. 和较小的孩子交换
2404. 一直往下,直到满足堆序性质
241
242这个过程叫:下滤
243sift down / percolate down
244
245例子:
246
247[1,2,7,10,5]
248
249删除堆顶 1。
250
251把最后一个 5 放到堆顶:
252
253[5,2,7,10]
254
255对应:
256
257 5
258 / \
259 2 7
260 /
261 10
262
2635 和较小孩子 2 比,交换:
264
265[2,5,7,10]
266
267得到:
268
269 2
270 / \
271 5 7
272 /
273 10
274
275恢复小根堆。
276
27710. 堆的结构体模板
278
279用动态数组版,先写结构:
280
281struct Heap {
282 int* data;
283 int size;
284 int capacity;
285};
286
287含义:
288
289data:数组
290size:当前元素个数
291capacity:最大容量
292
29311. 小根堆核心代码
294创建堆
295#include <stdlib.h>
296
297struct Heap {
298 int* data;
299 int size;
300 int capacity;
301};
302
303struct Heap* createHeap(int capacity)
304{
305 struct Heap* heap =
306 malloc(sizeof(struct Heap));
307
308 heap->data =
309 malloc(capacity * sizeof(int));
310
311 heap->size = 0;
312 heap->capacity = capacity;
313
314 return heap;
315}
316交换
317void swap(int* a, int* b)
318{
319 int temp = *a;
320 *a = *b;
321 *b = temp;
322}
323判空
324int isEmpty(struct Heap* heap)
325{
326 return heap->size == 0;
327}
328插入:上滤
329int push(struct Heap* heap, int x)
330{
331 if(heap->size == heap->capacity)
332 {
333 return 0;
334 }
335
336 int i = heap->size;
337 heap->data[i] = x;
338 heap->size++;
339
340 while(i > 0)
341 {
342 int parent = (i - 1) / 2;
343
344 if(heap->data[parent] <= heap->data[i])
345 {
346 break;
347 }
348
349 swap(&heap->data[parent], &heap->data[i]);
350
351 i = parent;
352 }
353
354 return 1;
355}
356
357取堆顶
358int top(struct Heap* heap, int* x)
359{
360 if(isEmpty(heap))
361 {
362 return 0;
363 }
364
365 *x = heap->data[0];
366
367 return 1;
368}
369
370删除堆顶:下滤
371int pop(struct Heap* heap, int* x)
372{
373 if(isEmpty(heap))
374 {
375 return 0;
376 }
377
378 *x = heap->data[0];
379
380 heap->data[0] = heap->data[heap->size - 1];
381 heap->size--;
382
383 int i = 0;
384
385 while(1)
386 {
387 int left = 2 * i + 1;
388 int right = 2 * i + 2;
389 int smallest = i;
390
391 if(left < heap->size
392 && heap->data[left] < heap->data[smallest])
393 {
394 smallest = left;
395 }
396
397 if(right < heap->size
398 && heap->data[right] < heap->data[smallest])
399 {
400 smallest = right;
401 }
402
403 if(smallest == i)
404 {
405 break;
406 }
407
408 swap(&heap->data[i], &heap->data[smallest]);
409
410 i = smallest;
411 }
412
413 return 1;
414}
415
416释放堆
417void destroyHeap(struct Heap* heap)
418{
419 free(heap->data);
420 free(heap);
421}
422
42312. 堆的遍历问题
424
425堆一般不讨论前序、中序、后序遍历
426
427因为堆不是为了遍历而设计的。
428
429堆最重要的是:
430
431快速拿到最大值或最小值
432
433所以常用操作是:
434
435push
436pop
437top
438
439而不是遍历。
440
44113. 堆和 BST 对比
442BST:
443
444左小右大
445适合查找某个值
446中序遍历有序
447
448--------------------------------
449
450堆:
451
452父子满足大小关系
453只保证堆顶最大/最小
454适合快速取最大值/最小值
455不能快速查找任意值
45614. 复杂度
457
458二叉堆插入和删除堆顶都需要沿着树高上滤或下滤,最坏复杂度是 O(log n);查看堆顶是 O(1)。
459
460top:
461O(1)
462
463push:
464O(log n)
465
466pop:
467O(log n)
468
469空间:
470O(n)
47115. 易错点
4721. 堆不是有序数组
473
474小根堆只保证:
475
476父节点 <= 子节点
477
478不保证:
479
480整个数组升序
481
482例如:
483
484[1,3,2,8,5]
485
486是可能合法的小根堆。
487
4882. 堆不是 BST
489
490BST:
491
492左 < 根 < 右
493
494堆:
495
496根 < 左
497根 < 右
498
499左右孩子之间没有固定大小关系。
500
5013. 插入是上滤
502
503新元素先放最后。
504
505然后:
506
507和父节点比较
508一路往上
509
5104. 删除堆顶是下滤
511
512堆顶删除后,把最后一个元素放到堆顶。
513
514然后:
515
516和更小的孩子交换
517一路往下
518
51916. 总结
520堆 ADT / 二叉堆
521
522--------------------------------
523
524用途:实现优先队列
525
526--------------------------------
527
528结构:
529
530完全二叉树
531
532最后一层从左到右填充
533
534--------------------------------
535
536堆序性质:
537
538小根堆:
539 父节点 <= 子节点
540 堆顶最小
541
542大根堆:
543 父节点 >= 子节点
544 堆顶最大
545
546--------------------------------
547
548数组存储:
549
5500下标:
551
552左孩子:
553 2*i + 1
554
555右孩子:
556 2*i + 2
557
558父节点:
559 (i - 1) / 2
560
561--------------------------------
562
563核心操作:
564
565top:
566 查看堆顶
567
568push:
569 插入元素
570 先放最后
571 再上滤
572
573pop:
574 删除堆顶
575 最后一个元素放堆顶
576 再下滤
577
578--------------------------------
579
580复杂度:
581
582top O(1)
583
584push O(log n)
585
586pop O(log n)
587
588--------------------------------
589
590堆不是BST:
591
592BST适合查找任意值
593
594堆适合快速取最大/最小值
595
596二叉堆 = 完全二叉树 + 父子堆序关系 + 数组存储。
Rendered Preview

堆 ADT / 二叉堆笔记

  1. 前提条件

堆是一种常用来实现:

优先队列 Priority Queue的数据结构。

普通队列:先进先出 FIFO

优先队列:优先级最高的先出来

例如:

普通队列:
先来先出

优先队列:
分数最高 / 数值最小 / 权重最大 的先出

二叉堆是实现优先队列的常见方式,它满足“完全二叉树结构”和“堆序性质”。

  1. 核心概念

二叉堆首先是一棵:完全二叉树

完全二叉树要求:

除了最后一层,其余层都满;
最后一层从左到右依次填充。

例如合法:

    1
  /   \
 3     5
/ \   /

7 9 8

不合法:

    1
  /   \
 3     5
  \     \
   9     8

因为最后一层没有从左到右填。

  1. 堆的两种类型

  2. 小根堆 Min Heap

规则:

父节点 <= 子节点

也就是:堆顶最小

例如:

    1
  /   \
 3     5
/ \   /

7 9 8

每个父节点都比孩子小。

适合:每次取最小值

  1. 大根堆 Max Heap

规则:父节点 >= 子节点

也就是:堆顶最大

例如:

    9
  /   \
 7     8
/ \   /

3 1 5

适合:每次取最大值

  1. 二叉堆不是 BST

BST:

左子树 < 根 < 右子树

堆:

只要求父节点和孩子满足大小关系

例如小根堆:

父节点 <= 左孩子
父节点 <= 右孩子

但不要求:左孩子 < 右孩子

也不要求中序有序。

所以堆不能像 BST 那样查找某个普通元素。

  1. 为什么二叉堆常用数组存

因为二叉堆是完全二叉树。

完全二叉树可以按层序放进数组,不需要真的用 left、right 指针。

例如:

    1
  /   \
 3     5
/ \   /

7 9 8

数组:

下标: 0 1 2 3 4 5
值: 1 3 5 7 9 8

也就是层序存储。

数组实现二叉堆时,如果根节点在下标 0,那么对于下标 i,左孩子是 2i+1,右孩子是 2i+2,父节点是 (i-1)/2。

  1. 数组下标关系

0 下标写法
当前节点:i

左孩子:2*i + 1

右孩子:2*i + 2

父节点:(i - 1) / 2

例如数组:

下标: 0 1 2 3 4 5
值: 1 3 5 7 9 8

对于下标 1:

值 = 3

左孩子下标 = 21+1 = 3,值 7
右孩子下标 = 2
1+2 = 4,值 9
父节点下标 = (1-1)/2 = 0,值 1
1 下标写法

当前节点:i

左孩子:2*i

右孩子:2*i + 1

父节点:i / 2

LeetCode / C 数组一般更常用:

0 下标

  1. 堆 ADT 常见操作

堆一般支持:

InitHeap 初始化堆
IsEmpty 判空
Push / Insert 插入元素
Pop / Delete 删除堆顶
Top / FindMin 查看堆顶
BuildHeap 建堆

  1. 插入操作:上滤

以小根堆为例。

插入新元素时:

先放到数组最后
再和父节点比较
如果比父节点小,就往上交换
直到满足堆序性质

这个过程叫:上滤
sift up / percolate up

例子,小根堆:

    2
  /   \
 5     7
/

10

数组:

[2,5,7,10]

插入:

1

先放最后:

[2,5,7,10,1]

对应:

    2
  /   \
 5     7
/ \

10 1

1 比父节点 5 小,交换:

[2,1,7,10,5]

1 比父节点 2 小,交换:

[1,2,7,10,5]

最终堆顶变成 1。

  1. 删除堆顶:下滤

以小根堆为例。

删除堆顶时:

  1. 堆顶元素删除
  2. 把最后一个元素放到堆顶
  3. 和较小的孩子交换
  4. 一直往下,直到满足堆序性质

这个过程叫:下滤
sift down / percolate down

例子:

[1,2,7,10,5]

删除堆顶 1。

把最后一个 5 放到堆顶:

[5,2,7,10]

对应:

    5
  /   \
 2     7
/

10

5 和较小孩子 2 比,交换:

[2,5,7,10]

得到:

    2
  /   \
 5     7
/

10

恢复小根堆。

  1. 堆的结构体模板

用动态数组版,先写结构:

struct Heap {
int* data;
int size;
int capacity;
};

含义:

data:数组
size:当前元素个数
capacity:最大容量

  1. 小根堆核心代码
    创建堆
    #include <stdlib.h>

struct Heap {
int* data;
int size;
int capacity;
};

struct Heap* createHeap(int capacity)
{
struct Heap* heap =
malloc(sizeof(struct Heap));

heap->data =
    malloc(capacity * sizeof(int));

heap->size = 0;
heap->capacity = capacity;

return heap;

}
交换
void swap(int* a, int* b)
{
int temp = *a;
*a = b;
b = temp;
}
判空
int isEmpty(struct Heap
heap)
{
return heap->size == 0;
}
插入:上滤
int push(struct Heap
heap, int x)
{
if(heap->size == heap->capacity)
{
return 0;
}

int i = heap->size;
heap->data[i] = x;
heap->size++;

while(i > 0)
{
    int parent = (i - 1) / 2;

    if(heap->data[parent] <= heap->data[i])
    {
        break;
    }

    swap(&heap->data[parent], &heap->data[i]);

    i = parent;
}

return 1;

}

取堆顶
int top(struct Heap* heap, int* x)
{
if(isEmpty(heap))
{
return 0;
}

*x = heap->data[0];

return 1;

}

删除堆顶:下滤
int pop(struct Heap* heap, int* x)
{
if(isEmpty(heap))
{
return 0;
}

*x = heap->data[0];

heap->data[0] = heap->data[heap->size - 1];
heap->size--;

int i = 0;

while(1)
{
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int smallest = i;

    if(left < heap->size
        && heap->data[left] < heap->data[smallest])
    {
        smallest = left;
    }

    if(right < heap->size
        && heap->data[right] < heap->data[smallest])
    {
        smallest = right;
    }

    if(smallest == i)
    {
        break;
    }

    swap(&heap->data[i], &heap->data[smallest]);

    i = smallest;
}

return 1;

}

释放堆
void destroyHeap(struct Heap* heap)
{
free(heap->data);
free(heap);
}

  1. 堆的遍历问题

堆一般不讨论前序、中序、后序遍历

因为堆不是为了遍历而设计的。

堆最重要的是:

快速拿到最大值或最小值

所以常用操作是:

push
pop
top

而不是遍历。

  1. 堆和 BST 对比
    BST:

左小右大
适合查找某个值
中序遍历有序


堆:

父子满足大小关系
只保证堆顶最大/最小
适合快速取最大值/最小值
不能快速查找任意值
14. 复杂度

二叉堆插入和删除堆顶都需要沿着树高上滤或下滤,最坏复杂度是 O(log n);查看堆顶是 O(1)。

top:
O(1)

push:
O(log n)

pop:
O(log n)

空间:
O(n)
15. 易错点

  1. 堆不是有序数组

小根堆只保证:

父节点 <= 子节点

不保证:

整个数组升序

例如:

[1,3,2,8,5]

是可能合法的小根堆。

  1. 堆不是 BST

BST:

左 < 根 < 右

堆:

根 < 左
根 < 右

左右孩子之间没有固定大小关系。

  1. 插入是上滤

新元素先放最后。

然后:

和父节点比较
一路往上

  1. 删除堆顶是下滤

堆顶删除后,把最后一个元素放到堆顶。

然后:

和更小的孩子交换
一路往下

  1. 总结
    堆 ADT / 二叉堆

用途:实现优先队列


结构:

完全二叉树

最后一层从左到右填充


堆序性质:

小根堆:
父节点 <= 子节点
堆顶最小

大根堆:
父节点 >= 子节点
堆顶最大


数组存储:

0下标:

左孩子:
2*i + 1

右孩子:
2*i + 2

父节点:
(i - 1) / 2


核心操作:

top:
查看堆顶

push:
插入元素
先放最后
再上滤

pop:
删除堆顶
最后一个元素放堆顶
再下滤


复杂度:

top O(1)

push O(log n)

pop O(log n)


堆不是BST:

BST适合查找任意值

堆适合快速取最大/最小值

二叉堆 = 完全二叉树 + 父子堆序关系 + 数组存储。