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二叉堆 = 完全二叉树 + 父子堆序关系 + 数组存储。
堆 ADT / 二叉堆笔记
- 前提条件
堆是一种常用来实现:
优先队列 Priority Queue的数据结构。
普通队列:先进先出 FIFO
优先队列:优先级最高的先出来
例如:
普通队列:
先来先出
优先队列:
分数最高 / 数值最小 / 权重最大 的先出
二叉堆是实现优先队列的常见方式,它满足“完全二叉树结构”和“堆序性质”。
- 核心概念
二叉堆首先是一棵:完全二叉树
完全二叉树要求:
除了最后一层,其余层都满;
最后一层从左到右依次填充。
例如合法:
1
/ \
3 5
/ \ /
7 9 8
不合法:
1
/ \
3 5
\ \
9 8
因为最后一层没有从左到右填。
-
堆的两种类型
-
小根堆 Min Heap
规则:
父节点 <= 子节点
也就是:堆顶最小
例如:
1
/ \
3 5
/ \ /
7 9 8
每个父节点都比孩子小。
适合:每次取最小值
- 大根堆 Max Heap
规则:父节点 >= 子节点
也就是:堆顶最大
例如:
9
/ \
7 8
/ \ /
3 1 5
适合:每次取最大值
- 二叉堆不是 BST
BST:
左子树 < 根 < 右子树
堆:
只要求父节点和孩子满足大小关系
例如小根堆:
父节点 <= 左孩子
父节点 <= 右孩子
但不要求:左孩子 < 右孩子
也不要求中序有序。
所以堆不能像 BST 那样查找某个普通元素。
- 为什么二叉堆常用数组存
因为二叉堆是完全二叉树。
完全二叉树可以按层序放进数组,不需要真的用 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。
- 数组下标关系
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
右孩子下标 = 21+2 = 4,值 9
父节点下标 = (1-1)/2 = 0,值 1
1 下标写法
当前节点:i
左孩子:2*i
右孩子:2*i + 1
父节点:i / 2
LeetCode / C 数组一般更常用:
0 下标
- 堆 ADT 常见操作
堆一般支持:
InitHeap 初始化堆
IsEmpty 判空
Push / Insert 插入元素
Pop / Delete 删除堆顶
Top / FindMin 查看堆顶
BuildHeap 建堆
- 插入操作:上滤
以小根堆为例。
插入新元素时:
先放到数组最后
再和父节点比较
如果比父节点小,就往上交换
直到满足堆序性质
这个过程叫:上滤
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。
- 删除堆顶:下滤
以小根堆为例。
删除堆顶时:
- 堆顶元素删除
- 把最后一个元素放到堆顶
- 和较小的孩子交换
- 一直往下,直到满足堆序性质
这个过程叫:下滤
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
恢复小根堆。
- 堆的结构体模板
用动态数组版,先写结构:
struct Heap {
int* data;
int size;
int capacity;
};
含义:
data:数组
size:当前元素个数
capacity:最大容量
- 小根堆核心代码
创建堆
#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);
}
- 堆的遍历问题
堆一般不讨论前序、中序、后序遍历
因为堆不是为了遍历而设计的。
堆最重要的是:
快速拿到最大值或最小值
所以常用操作是:
push
pop
top
而不是遍历。
- 堆和 BST 对比
BST:
左小右大
适合查找某个值
中序遍历有序
堆:
父子满足大小关系
只保证堆顶最大/最小
适合快速取最大值/最小值
不能快速查找任意值
14. 复杂度
二叉堆插入和删除堆顶都需要沿着树高上滤或下滤,最坏复杂度是 O(log n);查看堆顶是 O(1)。
top:
O(1)
push:
O(log n)
pop:
O(log n)
空间:
O(n)
15. 易错点
- 堆不是有序数组
小根堆只保证:
父节点 <= 子节点
不保证:
整个数组升序
例如:
[1,3,2,8,5]
是可能合法的小根堆。
- 堆不是 BST
BST:
左 < 根 < 右
堆:
根 < 左
根 < 右
左右孩子之间没有固定大小关系。
- 插入是上滤
新元素先放最后。
然后:
和父节点比较
一路往上
- 删除堆顶是下滤
堆顶删除后,把最后一个元素放到堆顶。
然后:
和更小的孩子交换
一路往下
- 总结
堆 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适合查找任意值
堆适合快速取最大/最小值
二叉堆 = 完全二叉树 + 父子堆序关系 + 数组存储。