zoukankan      html  css  js  c++  java
  • 算法面试题-美团点评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 }
    完整代码
  • 相关阅读:
    mysql分区表批量添加/删除range按天分区(int类型)
    mysql分区表批量添加/删除range按天分区(datetime类型)
    使用obd离线安装oceanbase
    mysqldump导出数据自增属性丢失案例
    Docker启动镜像并设置开机自启
    Docker启动mysql与elasticsearch以及nginx的命令
    Redis学习--从节点过期键清理策略
    Redis学习--渐进式rehash实现原理
    Redis学习--主节点过期键清理策略
    Redis学习--慢日志信息
  • 原文地址:https://www.cnblogs.com/Fndroid/p/6254187.html
Copyright © 2011-2022 走看看