返回 LeetCode 刷题

Markdown File

Huffman树

huffman树.md

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的结点
Rendered Preview

一、Huffman 树 / Huffman 编码

  1. 前提条件

Huffman 树本质是:

贪心 + 二叉树 + 小根堆

它解决的是:

如何让编码总长度最短

常见场景:

数据压缩
字符编码
最优二叉树

  1. 核心概念

假设字符出现频率:

A: 5
B: 9
C: 12
D: 13
E: 16
F: 45

出现次数越多的字符,应该编码越短。

出现次数越少的字符,编码可以长一点。

所以 Huffman 的核心贪心策略是:
每次选权值最小的两棵树合并

  1. WPL 是什么

WPL:带权路径长度

计算方式:WPL = 所有叶子结点的 权值 × 深度 之和

例如:A 权值 5,深度 3

贡献 = 5 × 3 = 15

Huffman 树就是让:

WPL 最小的二叉树。

  1. 构造过程

权值:

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. 为什么可以累加合并值

每次合并两个结点,相当于这两个子树所有叶子的深度都 +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 = 每次合并权值之和

  1. Huffman 树结点结构
    struct HuffmanNode {
    int weight;
    struct HuffmanNode* left;
    struct HuffmanNode* right;
    };

含义:

weight:权值
left:左孩子
right:右孩子

叶子结点就是原始字符。

内部结点是合并出来的。

  1. 小根堆实现

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;

}

意思:

堆顶最小

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

再下滤恢复小根堆

  1. 构造 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 树。

  1. 只求 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

  1. Huffman 编码

建好树以后:

左边记 0
右边记 1

从根到叶子的路径,就是该字符编码。

例如:

向左,向右,向左

编码就是 010

注意:

Huffman 编码不是唯一的

因为左右孩子可以互换。

但:

WPL 是一样的

  1. Huffman 易错点

  2. 每次取两个最小,不是取一个最小。

  3. Huffman 树没有度为 1 的结点。
    只有叶子结点和度为 2 的结点。

  4. n 个叶子结点的 Huffman 树,总结点数是 2n - 1。

  5. Huffman 编码不唯一,但 WPL 最小值唯一。

  6. 只求 WPL 时,可以不用真正建树,直接累加每次合并值。

  7. Huffman 总结

Huffman 树

核心:
每次取两个最小权值合并

目标:
WPL 最小

WPL:
权值 × 深度 之和

构造:
所有权值入小根堆
每次弹出两个最小
合并成新结点
再放回堆

编码:
左0右1
根到叶子的路径就是编码

重要性质:
n 个叶子
总结点数 2n-1
没有度为1的结点