算法面试题-美团点评2016研发工程师编程题(二)-字符编码(哈夫曼树)

题目:

解析:这个题目的关键问题是“最短的编码”,这里可以知道应该是Huffman编码了。

哈夫曼编码是一种可变字长编码,也就是说对于不同的字符的编码不是定长的,所以才能比定长编码要短

 

哈夫曼树

哈夫曼编码依靠的就是哈夫曼树,根据每个字符出现的次数作为权重,生成对应的哈夫曼树,对应的编码长度即为最短。

哈夫曼树的构造很简单,每次从所有的权重中选出最小两个分别作为的作为子节点(一般左节点权重小于右节点),权重相加值为父节点权重,再把父节点代替两个子节点重新进入队列中,不断循环最后会得到一颗二叉树。

例子:以2,4,5,8,13为权构造哈夫曼树

① 选出最小两个数为2和4,以2为左节点,4为右节点构造二叉树,和为父节点的值,并且将父节点的权放回

完成后权值为:5,6,8,13

② 选出最小两个数5和6,操作同上

权值为:8,11,13

③ 选出8和11,操作同上

权值为:13,19

④ 最后剩下两个,分别作为左右节点

得到的二叉树就是哈夫曼树。

回到哈夫曼编码,哈夫曼编码对应的就是将每个字符出现的次数作为权,生成对应的哈夫曼树,然后以左子树为0,右子树为1,对每个字符进行编码。

所以对于题目中的例子,生成的哈夫曼树和对应的编码应该是这样的:

注意:二叉树的构造不一定相同,因为部子节点的权是相同的,构造的二叉树会有所不同。

 

代码:

① 定义哈夫曼树节点:

 1     public class Node {
 2         Character value; // 节点字符
 3         int count = 1; // 深度(节点与根节点的距离)
 4         int power = 1; // 权(出现次数)
 5         Node left = null; // 左子节点
 6         Node right = null; // 右子节点
 7 
 8         /**
 9          * 更新子节点的深度
10          */
11         public void notifyChild() {
12             count += 1;
13             if (left != null) {
14                 left.notifyChild();
15             }
16             if (right != null) {
17                 right.notifyChild();
18             }
19         }
20 
21         public Node(Character value) {
22             this.value = value;
23         }
24 
25         public boolean sameValue(char c) {
26             return c == value;
27         }
28 
29         /**
30          * 根据传入的节点作为此节点的子节点
31          * @param left 左节点
32          * @param right 右节点
33          */
34         public void updateChild(Node left, Node right) {
35             this.left = left;
36             this.right = right;
37             this.power = left.power + right.power;
38         }
39 
40     }

11-19行为更新操作,因为最后要得到每个字符的编码长度,这个长度实际上是在构造二叉树的时候可以确定的,因为如果父节点被选出,则其子节点的深度会增加1,这个方法用来通知子节点进行深度加一操作。

② 编写计算方法

 1     void count(String s){
 2         // 计算每个字符出现的次数
 3         ArrayList<Node> chars = new ArrayList<>();
 4         char[] StrChars = s.toCharArray();
 5         chars.add(new Node(StrChars[0]));
 6         for (int j = 1; j < StrChars.length; j++) { 
 7             char c = StrChars[j];
 8             int size = chars.size();
 9             for (int i = 0; i < size; i++) {
10                 if (chars.get(i).sameValue(c)) {
11                     chars.get(i).power += 1;
12                     break;
13                 }
14                 if (i == size - 1) {
15                     chars.add(new Node(c));
16                 }
17             }
18         }
19         
20         // 根据权值排序,从小到大,插入排序
21         ArrayList<Node> nodes = new ArrayList<>();
22         nodes.add(chars.get(0));
23         for (int i = 1; i < chars.size(); i++) {
24             int size = nodes.size();
25             for (int j = 0; j < size; j++) {
26                 Node c = chars.get(i);
27                 if (c.power < nodes.get(j).power) {
28                     nodes.add(j, c);
29                     break;
30                 }
31                 if (j == size - 1) {
32                     nodes.add(c);
33                 }
34             }
35         }
36         chars = nodes;
37         
38         // 构造哈夫曼树
39         while (chars.size() > 1) {
40             Node root = new Node(null);
41             Node left = chars.get(0);
42             Node right = chars.get(1);
43             root.updateChild(left, right); // 生成新节点
44             chars.remove(0); // 删除被选节点
45             chars.remove(0); // 删除被选节点
46             int size = chars.size();
47             if (size == 0) {
48                 chars.add(root); // 退出循环条件
49             }
50             for (int i = 0; i < size; i++) {
51                 if (root.power < chars.get(i).power) {
52                     chars.add(i, root); // 将新生成的节点插入到对应位置使得序列依然有序
53                     root.notifyChild(); // 通知子节点更新深度
54                     break;
55                 }
56                 if (i == size - 1) {
57                     chars.add(root);
58                     root.notifyChild();
59                 }
60             }
61         }
62         
63         // 遍历二叉树,计算最短编码长度
64         int sum = 0;
65         Stack<Node> stack = new Stack<>();
66         Node root = chars.get(0);
67         stack.add(root);
68         while (!stack.isEmpty()) {
69             root = stack.pop();
70             if (root.value != null) {
71                 sum += root.power * root.count;
72             }
73             if (root.right != null) {
74                 stack.push(root.right);
75             }
76             if (root.left != null) {
77                 stack.push(root.left);
78             }
79         }
80         System.out.println(sum);
81     }

注意:

① 这里排序使用了插入排序,时间复杂度较高,但是通过面试题是没问题的,考虑到sort方法不能用,就只能折中了。

② 这里的节点的深度是指子节点到根节点的距离,如例子中E到根节点距离为2

③ 深度的计算只有字符节点是正确的,因为方法中不加判断的对根节点和子节点都加一了,但是计算只要求子节点,所以忽略这个问题。

 

完整代码如下:

  1 package com.fndroid;
  2 
  3 import java.util.*;
  4 
  5 public class Main {
  6 
  7     public static void main(String[] args) {
  8         Main solution = new Main();
  9         Scanner scanner = new Scanner(System.in);
 10         while (scanner.hasNext()) {
 11             solution.count(scanner.next());
 12         }
 13     }
 14 
 15     void count(String s){
 16         // 计算每个字符出现的次数
 17         ArrayList<Node> chars = new ArrayList<>();
 18         char[] StrChars = s.toCharArray();
 19         chars.add(new Node(StrChars[0]));
 20         for (int j = 1; j < StrChars.length; j++) {
 21             char c = StrChars[j];
 22             int size = chars.size();
 23             for (int i = 0; i < size; i++) {
 24                 if (chars.get(i).sameValue(c)) {
 25                     chars.get(i).power += 1;
 26                     break;
 27                 }
 28                 if (i == size - 1) {
 29                     chars.add(new Node(c));
 30                 }
 31             }
 32         }
 33 
 34         // 根据权值排序,从小到大,插入排序
 35         ArrayList<Node> nodes = new ArrayList<>();
 36         nodes.add(chars.get(0));
 37         for (int i = 1; i < chars.size(); i++) {
 38             int size = nodes.size();
 39             for (int j = 0; j < size; j++) {
 40                 Node c = chars.get(i);
 41                 if (c.power < nodes.get(j).power) {
 42                     nodes.add(j, c);
 43                     break;
 44                 }
 45                 if (j == size - 1) {
 46                     nodes.add(c);
 47                 }
 48             }
 49         }
 50         chars = nodes;
 51 
 52         // 构造哈夫曼树
 53         while (chars.size() > 1) {
 54             Node root = new Node(null);
 55             Node left = chars.get(0);
 56             Node right = chars.get(1);
 57             root.updateChild(left, right); // 生成新节点
 58             chars.remove(0); // 删除被选节点
 59             chars.remove(0); // 删除被选节点
 60             int size = chars.size();
 61             if (size == 0) {
 62                 chars.add(root); // 退出循环条件
 63             }
 64             for (int i = 0; i < size; i++) {
 65                 if (root.power < chars.get(i).power) {
 66                     chars.add(i, root); // 将新生成的节点插入到对应位置使得序列依然有序
 67                     root.notifyChild(); // 通知子节点更新深度
 68                     break;
 69                 }
 70                 if (i == size - 1) {
 71                     chars.add(root);
 72                     root.notifyChild();
 73                 }
 74             }
 75         }
 76 
 77         // 遍历二叉树,计算最短编码长度
 78         int sum = 0;
 79         Stack<Node> stack = new Stack<>();
 80         Node root = chars.get(0);
 81         stack.add(root);
 82         while (!stack.isEmpty()) {
 83             root = stack.pop();
 84             if (root.value != null) {
 85                 sum += root.power * root.count;
 86             }
 87             if (root.right != null) {
 88                 stack.push(root.right);
 89             }
 90             if (root.left != null) {
 91                 stack.push(root.left);
 92             }
 93         }
 94         System.out.println(sum);
 95     }
 96 
 97     public class Node {
 98         Character value; // 节点字符
 99         int count = 1; // 深度(节点与根节点的距离)
100         int power = 1; // 权(出现次数)
101         Node left = null; // 左子节点
102         Node right = null; // 右子节点
103 
104         /**
105          * 更新子节点的深度
106          */
107         public void notifyChild() {
108             count += 1;
109             if (left != null) {
110                 left.notifyChild();
111             }
112             if (right != null) {
113                 right.notifyChild();
114             }
115         }
116 
117         public Node(Character value) {
118             this.value = value;
119         }
120 
121         public boolean sameValue(char c) {
122             return c == value;
123         }
124 
125         /**
126          * 根据传入的节点作为此节点的子节点
127          * @param left 左节点
128          * @param right 右节点
129          */
130         public void updateChild(Node left, Node right) {
131             this.left = left;
132             this.right = right;
133             this.power = left.power + right.power;
134         }
135 
136     }
137 }
完整代码

 

转载于:https://www.cnblogs.com/Fndroid/p/6254187.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值