1一、Huffman 树 / Huffman 编码
21. 前提条件
3
4Huffman 树本质是:
5
6贪心 + 二叉树 + 小根堆
7
8它解决的是:
9
10如何让编码总长度最短
11
12常见场景:
13
14数据压缩
15字符编码
16最优二叉树
17
182. 核心概念
19
20假设字符出现频率:
21
22A: 5
23B: 9
24C: 12
25D: 13
26E: 16
27F: 45
28
29出现次数越多的字符,应该编码越短。
30
31出现次数越少的字符,编码可以长一点。
32
33所以 Huffman 的核心贪心策略是:
34每次选权值最小的两棵树合并
35
363. WPL 是什么
37
38WPL:带权路径长度
39
40计算方式:WPL = 所有叶子结点的 权值 × 深度 之和
41
42例如:A 权值 5,深度 3
43
44贡献 = 5 × 3 = 15
45
46Huffman 树就是让:
47
48WPL 最小的二叉树。
49
504. 构造过程
51
52权值:
53
545, 9, 12, 13, 16, 45
55
56每次取最小两个合并。
57
58第一步:
59
605 + 9 = 14
61
62现在:
63
6412, 13, 14, 16, 45
65
66第二步:
67
6812 + 13 = 25
69
70现在:
71
7214, 16, 25, 45
73
74第三步:
75
7614 + 16 = 30
77
78现在:
79
8025, 30, 45
81
82第四步:
83
8425 + 30 = 55
85
86现在:
87
8845, 55
89
90第五步:
91
9245 + 55 = 100
93
94结束。
95
96所以总代价 WPL 可以在合并时直接累加:
97
9814 + 25 + 30 + 55 + 100 = 224
99
100
1015. 为什么可以累加合并值
102
103每次合并两个结点,相当于这两个子树所有叶子的深度都 +1。
104
105所以新增的 WPL 正好是:
106
107左子树总权值 + 右子树总权值
108
109也就是合并后的权值。
110
111---补充一下我对于此的理解:Huffman算法维护的不是一棵树,而是一片森林(Forest)。
112
113开始例如:
114权值:
115
1165
117
1187
119
12010
121
12215
123
124此时:根本没有树。
125
126只有:
1275 7 10 15
128
129每一个叶子:就是一棵树。(每个叶子都计算过自己的权重)
130
131所以:开始有 n 棵树
132
133结束只剩 1 棵树
134
135整个算法就是:不断把两棵树合并。
136
137 为什么用小根堆?
138
139因为每一步都要找:森林里面权值最小的两棵树
140
141例如:
142
143森林:
144
1455
146
1477
148
14910
150
15115
152
153最小:5和7
154
155所以最适合的数据结构:小根堆
156
157小根堆维护的是:森林
158
159不是二叉树。
160
161--- HuffmanNode到底是什么?
162
163
164struct HuffmanNode
165{
166 int weight;
167
168 struct HuffmanNode* left;
169
170 struct HuffmanNode* right;
171};
172
173
174这里不是堆
175
176而是:一棵树的根节点。
177
178例如开始:
179
180A
181
182weight=5
183
184就是:
185A
186
187后来:
188
189变成:
190
191 12
192 / \
193 5 7
194
195那么整个:12
196
197就是:
198
199HuffmanNode*
200
201以后:放进堆。
202
203所以:
204
205堆里面放的是:树根指针。
206
207----为什么比较weight?
208
209因为:
210
211例如:
212
213 22
214 / \
215 10 12
216 / \
217 5 7
218
219对于小根堆来说。
220
221它根本不关心:
222
223里面长什么样。
224
225它只关心:整棵树总权值22
226
227所以:
228
229比较:
230
231node->weight即可。
232
233left
234
235right
236
237完全不用比较。
238
239因为:left/right
240
241只是保存树结构。
242
243--- 一个完整例子
244
245开始权值:
246
2475
248
2497
250
25110
252
25315
254
255森林:
256
2575
258
2597
260
26110
262
26315
264
265放入小根堆:
266
267Heap
268
269↓
270
2715
272
2737
274
27510
276
27715
278
279第一步:
280
281pop:
282
2835
284
2857
286
287建立:
288
289 12
290 / \
291 5 7
292
293放回堆:
294
295Heap
296
297↓
298
29910
300
30112(树)
302
30315
304
305注意:
306
307这里:
308
309Heap里面:
310
311已经不是:
312
3135
314
3157
316
317而是:
318
319 12
320 / \
321 5 7
322
323这一整棵树。
324
325第二步:
326
327取:
328
32910
330
33112
332
333建立:
334
335 22
336 / \
337 10 12
338 / \
339 5 7
340
341放回:
342
343Heap:
344
34515
346
34722(树)
348
349第三步:
350
351取:
352
35315
354
35522
356
357建立:
358
359 37
360 / \
361 15 22
362 / \
363 10 12
364 / \
365 5 7
366
367结束。
368
369森林:
370
371只剩:一棵树
372
373这就是:
374
375Huffman树。
376
377---- 小根堆为什么存树?
378
379真正内存里面:
380
381其实是:
382
383Heap
384
385↓
386
387data[0]
388
389↓
390
391Tree Root
392
393↓
394
395整棵树
396
397所以:
398
399Heap
400
401不是存树
402
403而是存树根指针。
404
405例如:
406
407data[0]
408
409↓
410
41122
412
413因为:
414
41522
416
417知道left
418
419知道right
420
421所以:
422
423整个树:
424
425都找得到。
426
427----为什么swapNode是二级指针?
428
429例如:
430
431Heap:
432
433data[0]
434
435↓
436
43722
438
439data[1]
440
441↓
442
44315
444
445交换的是:
446
447两个树根指针
448
449所以:
450
451swapNode(
452 &heap->data[0],
453 &heap->data[1]
454);
455
456参数:
457
458就是:
459
460HuffmanNode**
461
462不是:
463
464HuffmanNode*
465
466这一点和我们之前哈希那里讲的:
467
468交换指针
469
470必须传地址
471
472完全一样。
473
474把 Huffman 想成:
475维护一片森林
476
477↓
478
479小根堆维护这些树
480
481↓
482
483每次挑两棵最轻的树
484
485↓
486
487合成一棵更大的树
488
489↓
490
491放回森林
492
493↓
494
495直到森林只剩最后一棵树
496
497
498因此:
499Huffman 的 WPL = 每次合并权值之和
500
5016. Huffman 树结点结构
502struct HuffmanNode {
503 int weight;
504 struct HuffmanNode* left;
505 struct HuffmanNode* right;
506};
507
508含义:
509
510weight:权值
511left:左孩子
512right:右孩子
513
514叶子结点就是原始字符。
515
516内部结点是合并出来的。
517
5187. 小根堆实现
519
520Huffman 每次都要取两个最小值。
521
522所以最适合用:小根堆
523
524我们先写一个存 struct HuffmanNode* 的小根堆。
525
5267.1 堆结构
527#include <stdlib.h>
528
529#define MAXN 1000
530
531struct HuffmanNode {
532 int weight;
533 struct HuffmanNode* left;
534 struct HuffmanNode* right;
535};
536
537struct MinHeap {
538 struct HuffmanNode* data[MAXN];
539 int size;
540};
541
5427.2 创建结点
543struct HuffmanNode* createNode(int weight)
544{
545 struct HuffmanNode* node =
546 malloc(sizeof(struct HuffmanNode));
547
548 node->weight = weight;
549 node->left = NULL;
550 node->right = NULL;
551
552 return node;
553}
554
5557.3 交换堆元素
556void swapNode(struct HuffmanNode** a,
557 struct HuffmanNode** b)
558{
559 struct HuffmanNode* temp = *a;
560 *a = *b;
561 *b = temp;
562}
563
564注意这里是:
565
566struct HuffmanNode**
567
568因为堆数组里存的是:
569
570struct HuffmanNode*
571
572交换两个指针,就要传指针的地址。
573
5747.4 push 上滤
575void push(struct MinHeap* heap,
576 struct HuffmanNode* node)
577{
578 int i = heap->size;
579
580 heap->data[i] = node;
581 heap->size++;
582
583 while(i > 0)
584 {
585 int parent = (i - 1) / 2;
586
587 if(heap->data[parent]->weight
588 <= heap->data[i]->weight)
589 {
590 break;
591 }
592
593 swapNode(&heap->data[parent],
594 &heap->data[i]);
595
596 i = parent;
597 }
598}
599
600意思:新结点先放到堆尾
601
602如果比父节点小,就往上换
603
6047.5 pop 取出最小值
605struct HuffmanNode* pop(struct MinHeap* heap)
606{
607 if(heap->size == 0)
608 {
609 return NULL;
610 }
611
612 struct HuffmanNode* ans = heap->data[0];
613
614 heap->data[0] = heap->data[heap->size - 1];
615 heap->size--;
616
617 int i = 0;
618
619 while(1)
620 {
621 int left = 2 * i + 1;
622 int right = 2 * i + 2;
623 int smallest = i;
624
625 if(left < heap->size &&
626 heap->data[left]->weight
627 < heap->data[smallest]->weight)
628 {
629 smallest = left;
630 }
631
632 if(right < heap->size &&
633 heap->data[right]->weight
634 < heap->data[smallest]->weight)
635 {
636 smallest = right;
637 }
638
639 if(smallest == i)
640 {
641 break;
642 }
643
644 swapNode(&heap->data[i],
645 &heap->data[smallest]);
646
647 i = smallest;
648 }
649
650 return ans;
651}
652
653意思:
654
655堆顶最小
656
657删除堆顶后,把最后一个元素放堆顶
658
659再下滤恢复小根堆
660
6618. 构造 Huffman 树
662struct HuffmanNode* buildHuffmanTree(int weights[],
663 int n)
664{
665 struct MinHeap heap;
666 heap.size = 0;
667
668 for(int i = 0; i < n; i++)
669 {
670 push(&heap, createNode(weights[i]));
671 }
672
673 while(heap.size > 1)
674 {
675 struct HuffmanNode* left = pop(&heap);
676 struct HuffmanNode* right = pop(&heap);
677
678 struct HuffmanNode* parent =
679 createNode(left->weight + right->weight);
680
681 parent->left = left;
682 parent->right = right;
683
684 push(&heap, parent);
685 }
686
687 return pop(&heap);
688}
689
690代码理解
691
692先把所有权值放进小根堆:
693
6945, 9, 12, 13, 16, 45
695
696每次:
697
698left = pop(&heap);
699right = pop(&heap);
700
701取两个最小。
702
703然后:
704
705parent = createNode(left->weight + right->weight);
706
707合成新树。
708
709再:
710
711push(&heap, parent);
712
713放回堆。
714
715直到堆里只剩一棵树。
716
717这棵树就是 Huffman 树。
718
7199. 只求 WPL 的话就建堆之后两两都加起来求和就行
720
721int huffmanWPL(int weights[], int n)
722{
723 struct MinHeap heap;
724 heap.size = 0;
725
726 for(int i = 0; i < n; i++)
727 {
728 push(&heap, createNode(weights[i]));
729 }
730
731 int wpl = 0;
732
733 while(heap.size > 1)
734 {
735 struct HuffmanNode* a = pop(&heap);
736 struct HuffmanNode* b = pop(&heap);
737
738 int sum = a->weight + b->weight;
739
740 wpl += sum;
741
742 push(&heap, createNode(sum));
743 }
744
745 return wpl;
746}
747
748例子:
749
7505,9,12,13,16,45
751
752返回:224
753
75410. Huffman 编码
755
756建好树以后:
757
758左边记 0
759右边记 1
760
761从根到叶子的路径,就是该字符编码。
762
763例如:
764
765向左,向右,向左
766
767编码就是 010
768
769注意:
770
771Huffman 编码不是唯一的
772
773因为左右孩子可以互换。
774
775但:
776
777WPL 是一样的
778
77911. Huffman 易错点
780
7811. 每次取两个最小,不是取一个最小。
782
7832. Huffman 树没有度为 1 的结点。
784 只有叶子结点和度为 2 的结点。
785
7863. n 个叶子结点的 Huffman 树,总结点数是 2n - 1。
787
7884. Huffman 编码不唯一,但 WPL 最小值唯一。
789
7905. 只求 WPL 时,可以不用真正建树,直接累加每次合并值。
791
79212. Huffman 总结
793
794Huffman 树
795
796核心:
797 每次取两个最小权值合并
798
799目标:
800 WPL 最小
801
802WPL:
803 权值 × 深度 之和
804
805构造:
806 所有权值入小根堆
807 每次弹出两个最小
808 合并成新结点
809 再放回堆
810
811编码:
812 左0右1
813 根到叶子的路径就是编码
814
815重要性质:
816 n 个叶子
817 总结点数 2n-1
818 没有度为1的结点
一、Huffman 树 / Huffman 编码
- 前提条件
Huffman 树本质是:
贪心 + 二叉树 + 小根堆
它解决的是:
如何让编码总长度最短
常见场景:
数据压缩
字符编码
最优二叉树
- 核心概念
假设字符出现频率:
A: 5
B: 9
C: 12
D: 13
E: 16
F: 45
出现次数越多的字符,应该编码越短。
出现次数越少的字符,编码可以长一点。
所以 Huffman 的核心贪心策略是:
每次选权值最小的两棵树合并
- WPL 是什么
WPL:带权路径长度
计算方式:WPL = 所有叶子结点的 权值 × 深度 之和
例如:A 权值 5,深度 3
贡献 = 5 × 3 = 15
Huffman 树就是让:
WPL 最小的二叉树。
- 构造过程
权值:
5, 9, 12, 13, 16, 45
每次取最小两个合并。
第一步:
5 + 9 = 14
现在:
12, 13, 14, 16, 45
第二步:
12 + 13 = 25
现在:
14, 16, 25, 45
第三步:
14 + 16 = 30
现在:
25, 30, 45
第四步:
25 + 30 = 55
现在:
45, 55
第五步:
45 + 55 = 100
结束。
所以总代价 WPL 可以在合并时直接累加:
14 + 25 + 30 + 55 + 100 = 224
- 为什么可以累加合并值
每次合并两个结点,相当于这两个子树所有叶子的深度都 +1。
所以新增的 WPL 正好是:
左子树总权值 + 右子树总权值
也就是合并后的权值。
---补充一下我对于此的理解:Huffman算法维护的不是一棵树,而是一片森林(Forest)。
开始例如:
权值:
5
7
10
15
此时:根本没有树。
只有:
5 7 10 15
每一个叶子:就是一棵树。(每个叶子都计算过自己的权重)
所以:开始有 n 棵树
结束只剩 1 棵树
整个算法就是:不断把两棵树合并。
为什么用小根堆?
因为每一步都要找:森林里面权值最小的两棵树
例如:
森林:
5
7
10
15
最小:5和7
所以最适合的数据结构:小根堆
小根堆维护的是:森林
不是二叉树。
--- HuffmanNode到底是什么?
struct HuffmanNode
{
int weight;
struct HuffmanNode* left;
struct HuffmanNode* right;
};
这里不是堆
而是:一棵树的根节点。
例如开始:
A
weight=5
就是:
A
后来:
变成:
12
/ \
5 7
那么整个:12
就是:
HuffmanNode*
以后:放进堆。
所以:
堆里面放的是:树根指针。
----为什么比较weight?
因为:
例如:
22
/ \
10 12
/ \
5 7
对于小根堆来说。
它根本不关心:
里面长什么样。
它只关心:整棵树总权值22
所以:
比较:
node->weight即可。
left
right
完全不用比较。
因为:left/right
只是保存树结构。
--- 一个完整例子
开始权值:
5
7
10
15
森林:
5
7
10
15
放入小根堆:
Heap
↓
5
7
10
15
第一步:
pop:
5
7
建立:
12
/ \
5 7
放回堆:
Heap
↓
10
12(树)
15
注意:
这里:
Heap里面:
已经不是:
5
7
而是:
12
/ \
5 7
这一整棵树。
第二步:
取:
10
12
建立:
22
/ \
10 12
/ \
5 7
放回:
Heap:
15
22(树)
第三步:
取:
15
22
建立:
37
/ \
15 22
/ \
10 12
/ \
5 7
结束。
森林:
只剩:一棵树
这就是:
Huffman树。
---- 小根堆为什么存树?
真正内存里面:
其实是:
Heap
↓
data[0]
↓
Tree Root
↓
整棵树
所以:
Heap
不是存树
而是存树根指针。
例如:
data[0]
↓
22
因为:
22
知道left
知道right
所以:
整个树:
都找得到。
----为什么swapNode是二级指针?
例如:
Heap:
data[0]
↓
22
data[1]
↓
15
交换的是:
两个树根指针
所以:
swapNode(
&heap->data[0],
&heap->data[1]
);
参数:
就是:
HuffmanNode**
不是:
HuffmanNode*
这一点和我们之前哈希那里讲的:
交换指针
必须传地址
完全一样。
把 Huffman 想成:
维护一片森林
↓
小根堆维护这些树
↓
每次挑两棵最轻的树
↓
合成一棵更大的树
↓
放回森林
↓
直到森林只剩最后一棵树
因此:
Huffman 的 WPL = 每次合并权值之和
- Huffman 树结点结构
struct HuffmanNode {
int weight;
struct HuffmanNode* left;
struct HuffmanNode* right;
};
含义:
weight:权值
left:左孩子
right:右孩子
叶子结点就是原始字符。
内部结点是合并出来的。
- 小根堆实现
Huffman 每次都要取两个最小值。
所以最适合用:小根堆
我们先写一个存 struct HuffmanNode* 的小根堆。
7.1 堆结构
#include <stdlib.h>
#define MAXN 1000
struct HuffmanNode {
int weight;
struct HuffmanNode* left;
struct HuffmanNode* right;
};
struct MinHeap {
struct HuffmanNode* data[MAXN];
int size;
};
7.2 创建结点
struct HuffmanNode* createNode(int weight)
{
struct HuffmanNode* node =
malloc(sizeof(struct HuffmanNode));
node->weight = weight;
node->left = NULL;
node->right = NULL;
return node;
}
7.3 交换堆元素
void swapNode(struct HuffmanNode** a,
struct HuffmanNode** b)
{
struct HuffmanNode* temp = *a;
*a = *b;
*b = temp;
}
注意这里是:
struct HuffmanNode**
因为堆数组里存的是:
struct HuffmanNode*
交换两个指针,就要传指针的地址。
7.4 push 上滤
void push(struct MinHeap* heap,
struct HuffmanNode* node)
{
int i = heap->size;
heap->data[i] = node;
heap->size++;
while(i > 0)
{
int parent = (i - 1) / 2;
if(heap->data[parent]->weight
<= heap->data[i]->weight)
{
break;
}
swapNode(&heap->data[parent],
&heap->data[i]);
i = parent;
}
}
意思:新结点先放到堆尾
如果比父节点小,就往上换
7.5 pop 取出最小值
struct HuffmanNode* pop(struct MinHeap* heap)
{
if(heap->size == 0)
{
return NULL;
}
struct HuffmanNode* ans = 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]->weight
< heap->data[smallest]->weight)
{
smallest = left;
}
if(right < heap->size &&
heap->data[right]->weight
< heap->data[smallest]->weight)
{
smallest = right;
}
if(smallest == i)
{
break;
}
swapNode(&heap->data[i],
&heap->data[smallest]);
i = smallest;
}
return ans;
}
意思:
堆顶最小
删除堆顶后,把最后一个元素放堆顶
再下滤恢复小根堆
-
构造 Huffman 树
struct HuffmanNode* buildHuffmanTree(int weights[],
int n)
{
struct MinHeap heap;
heap.size = 0;
for(int i = 0; i < n; i++)
{
push(&heap, createNode(weights[i]));
}
while(heap.size > 1)
{
struct HuffmanNode* left = pop(&heap);
struct HuffmanNode* right = pop(&heap);
struct HuffmanNode* parent =
createNode(left->weight + right->weight);
parent->left = left;
parent->right = right;
push(&heap, parent);
}
return pop(&heap);
}
代码理解
先把所有权值放进小根堆:
5, 9, 12, 13, 16, 45
每次:
left = pop(&heap);
right = pop(&heap);
取两个最小。
然后:
parent = createNode(left->weight + right->weight);
合成新树。
再:
push(&heap, parent);
放回堆。
直到堆里只剩一棵树。
这棵树就是 Huffman 树。
- 只求 WPL 的话就建堆之后两两都加起来求和就行
int huffmanWPL(int weights[], int n)
{
struct MinHeap heap;
heap.size = 0;
for(int i = 0; i < n; i++)
{
push(&heap, createNode(weights[i]));
}
int wpl = 0;
while(heap.size > 1)
{
struct HuffmanNode* a = pop(&heap);
struct HuffmanNode* b = pop(&heap);
int sum = a->weight + b->weight;
wpl += sum;
push(&heap, createNode(sum));
}
return wpl;
}
例子:
5,9,12,13,16,45
返回:224
- Huffman 编码
建好树以后:
左边记 0
右边记 1
从根到叶子的路径,就是该字符编码。
例如:
向左,向右,向左
编码就是 010
注意:
Huffman 编码不是唯一的
因为左右孩子可以互换。
但:
WPL 是一样的
-
Huffman 易错点
-
每次取两个最小,不是取一个最小。
-
Huffman 树没有度为 1 的结点。
只有叶子结点和度为 2 的结点。
-
n 个叶子结点的 Huffman 树,总结点数是 2n - 1。
-
Huffman 编码不唯一,但 WPL 最小值唯一。
-
只求 WPL 时,可以不用真正建树,直接累加每次合并值。
-
Huffman 总结
Huffman 树
核心:
每次取两个最小权值合并
目标:
WPL 最小
WPL:
权值 × 深度 之和
构造:
所有权值入小根堆
每次弹出两个最小
合并成新结点
再放回堆
编码:
左0右1
根到叶子的路径就是编码
重要性质:
n 个叶子
总结点数 2n-1
没有度为1的结点