数据结构与算法(五)——树

一、树

1、介绍

  树(Tree)是n(n>=0)个结点的有限集。当n = 0时成为空树。

2、树的储存结构

  双亲表示法、孩子表示法、孩子兄弟表示法(未列举)。
  双亲表示法:以双亲作为索引的关键词的一种存储方式。
  我们可以根据某结点的parent指针找到它的双亲结点,所用的时间复杂度是o(1),索引到parent的值为-1时,表示找到了树结点的根。

  孩子表示法:

   双亲孩子表示法:

二、二叉树

1、介绍

  二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两颗互不相交的,分别称为根结点的左子树和右子树组成。
  二叉树的五种基本形态:

  三个结点的二叉树有五种形态:

2、二叉树的性质

  在二叉树的第 i 层上至多有 2^(i - 1) 个结点。
  对于任何一颗二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0 = n2 + 1。

3、满二叉树

性质:
  每一层的结点都是满的。
  同样深度的二叉树,满二叉树的结点个数最多,叶子也是最多。
  深度为 k 的满二叉树,结点数为 2^k - 1。

4、完全二叉树

性质:
  若结点度为1,则该结点只有左孩子。
  同样结点数的二叉树,完全二叉树的深度最小。
  具有 n 个结点的完全二叉树的深度为 ⌊log2(n)⌋ + 1。
  具有 n 个结点的完全二叉树结点按层序编号,则对任意结点 i ,有:(重要!在堆排序中会用到!)
    i = 0,则 i 是根;i > 1,则双亲是 (i - 1)/ 2
    2i + 1 >= n,则 i 无左孩子。否则其左孩子是 2i + 1
    2i + 2 >= n,则 i 无右孩子。否则其右孩子是 2i + 2

  满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。

5、二叉树的存储结构及遍历

遍历方式:
  前序遍历:ABC
  中序遍历:BAC
  后序遍历:BCA
  层序遍历:ABC
树存储方式的分析:能提高数据存储,读取的效率,比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

顺序存储结构:

  通常而言,顺序存储二叉树只考虑完全二叉树。即只有完全二叉树,我们用顺序存储结构来表示。可以用数组来表示,示例:

  代码示例:顺序存储二叉树,前序,中序,后序遍历

 1 public class MyArrBinaryTree {
 2 
 3     private final int[] arr;
 4 
 5     public MyArrBinaryTree(int[] arr) {
 6         this.arr = arr;
 7     }
 8 
 9     // 前序遍历
10     public void preOrder() {
11         this.preOrder(0);
12     }
13 
14     // 中序遍历
15     public void infixOrder() {
16         this.infixOrder(0);
17     }
18 
19     // 后序遍历
20     public void postOrder() {
21         this.postOrder(0);
22     }
23 
24     private void preOrder(int index) {
25         if (arr == null || arr.length == 0) {
26             return;
27         }
28 
29         System.out.print("->" + arr[index]);
30 
31         int left = 2 * index + 1;
32         if (left < arr.length) {
33             this.preOrder(left);
34         }
35 
36         int right = 2 * index + 2;
37         if (right < arr.length) {
38             this.preOrder(right);
39         }
40     }
41 
42     private void infixOrder(int index) {
43         if (arr == null || arr.length == 0) {
44             return;
45         }
46 
47         int left = 2 * index + 1;
48         if (left < arr.length) {
49             this.infixOrder(left);
50         }
51 
52         System.out.print("->" + arr[index]);
53 
54         int right = 2 * index + 2;
55         if (right < arr.length) {
56             this.infixOrder(right);
57         }
58     }
59 
60     private void postOrder(int index) {
61         if (arr == null || arr.length == 0) {
62             return;
63         }
64 
65         int left = 2 * index + 1;
66         if (left < arr.length) {
67             this.postOrder(left);
68         }
69 
70         int right = 2 * index + 2;
71         if (right < arr.length) {
72             this.postOrder(right);
73         }
74 
75         System.out.print("->" + arr[index]);
76     }
77 }
顺序存储二叉树遍历

  代码示例:测试类

 1 public class Main {
 2     public static void main(String[] args) {
 3         int[] arr = {1, 3, 6, 8, 10, 14};
 4 
 5         MyArrBinaryTree tree = new MyArrBinaryTree(arr);
 6         System.out.println("前");
 7         tree.preOrder();
 8 
 9         System.out.println("\n中");
10         tree.infixOrder();
11 
12         System.out.println("\n后");
13         tree.postOrder();
14     }
15 }
16 
17 // 结果
18 19 ->1->3->8->10->6->14
20 21 ->8->3->10->1->14->6
22 23 ->8->10->3->14->6->1
测试类

链式存储结构:

  代码示例:二叉链表结构

1 class TreeNode {
2     public int val;
3     public TreeNode left;
4     public TreeNode right;
5 
6     public TreeNode(int val) {
7         this.val = val;
8     }
9 }
二叉链表结构

  对示例图做遍历,如图:

  代码示例:链表二叉树,前序,中序,后序遍历,查找,删除

  1 public class MyBinaryTree {
  2 
  3     // 根结点
  4     private TreeNode root;
  5 
  6     public MyBinaryTree(TreeNode root) {
  7         this.root = root;
  8     }
  9 
 10     // 前序遍历
 11     public void preOrder() {
 12         this.preOrder(root);
 13     }
 14 
 15     // 中序遍历
 16     public void infixOrder() {
 17         this.infixOrder(root);
 18     }
 19 
 20     // 后序遍历
 21     public void postOrder() {
 22         this.postOrder(root);
 23     }
 24 
 25     // 前序遍历查找 5
 26     public TreeNode preOrderSearch(int val) {
 27         return this.preOrderSearch(root, val);
 28     }
 29 
 30     // 中序遍历查找
 31     public TreeNode infixOrderSearch(int val) {
 32         return this.infixOrderSearch(root, val);
 33     }
 34 
 35     // 后序遍历查找
 36     public TreeNode postOrderSearch(int val) {
 37         return this.postOrderSearch(root, val);
 38     }
 39 
 40     // 删除结点
 41     public void delTreeNode(int val) {
 42         if (root != null) {
 43             if (root.val == val) {
 44                 root = null;
 45             } else {
 46                 this.delTreeNode(root, val);
 47             }
 48         }
 49     }
 50 
 51     private void delTreeNode(TreeNode root, int val) {
 52         // 判断左孩子
 53         if (root.left != null && root.left.val == val) {
 54             root.left = null;
 55             return;
 56         }
 57         // 判断右孩子
 58         if (root.right != null && root.right.val == val) {
 59             root.right = null;
 60             return;
 61         }
 62 
 63         if (root.left != null) {
 64             this.delTreeNode(root.left, val);
 65         }
 66         if (root.right != null) {
 67             this.delTreeNode(root.right, val);
 68         }
 69 
 70     }
 71 
 72     // 前序遍历查找 5
 73     private TreeNode preOrderSearch(TreeNode root, int val) {
 74         if (root != null) {
 75             // 前序遍历查找次数 4
 76             System.out.println("进入前序遍历查找");
 77             if (root.val == val) {
 78                 return root;
 79             }
 80 
 81             final TreeNode node = this.preOrderSearch(root.left, val);
 82             if (node != null) {
 83                 return node;
 84             }
 85 
 86             return this.preOrderSearch(root.right, val);
 87         }
 88 
 89         return null;
 90     }
 91 
 92     // 中序遍历查找
 93     private TreeNode infixOrderSearch(TreeNode root, int val) {
 94         if (root != null) {
 95             final TreeNode node = this.infixOrderSearch(root.left, val);
 96             if (node != null) {
 97                 return node;
 98             }
 99 
100             // 中序遍历查找次数 3
101             System.out.println("进入中序遍历查找");
102             if (root.val == val) {
103                 return root;
104             }
105 
106             return this.infixOrderSearch(root.right, val);
107         }
108 
109         return null;
110     }
111 
112     // 后序遍历查找
113     private TreeNode postOrderSearch(TreeNode root, int val) {
114         if (root != null) {
115             TreeNode node = this.postOrderSearch(root.left, val);
116             if (node != null) {
117                 return node;
118             }
119 
120             node = this.postOrderSearch(root.right, val);
121             if (node != null) {
122                 return node;
123             }
124 
125             // 后序遍历查找次数 2
126             System.out.println("进入后序遍历查找");
127             if (root.val == val) {
128                 return root;
129             }
130         }
131 
132         return null;
133     }
134 
135     // 前序遍历
136     private void preOrder(TreeNode root) {
137         if (root != null) {
138             System.out.print(root.val + "->");
139             this.preOrder(root.left);
140             this.preOrder(root.right);
141         }
142     }
143 
144     // 中序遍历
145     private void infixOrder(TreeNode root) {
146         if (root != null) {
147             this.infixOrder(root.left);
148             System.out.print(root.val + "->");
149             this.infixOrder(root.right);
150         }
151     }
152 
153     // 后序遍历
154     private void postOrder(TreeNode root) {
155         if (root != null) {
156             this.postOrder(root.left);
157             this.postOrder(root.right);
158             System.out.print(root.val + "->");
159         }
160     }
161 }
162 
163 class TreeNode {
164     public int val;
165     public TreeNode left;
166     public TreeNode right;
167 
168     public TreeNode(int val) {
169         this.val = val;
170     }
171 }
链式存储二叉树

  代码示例:测试类

 1 public class Main {
 2     public static void main(String[] args) {
 3         final TreeNode root = new TreeNode(1);
 4 
 5         final TreeNode node2 = new TreeNode(2);
 6         final TreeNode node3 = new TreeNode(3);
 7         final TreeNode node4 = new TreeNode(4);
 8         final TreeNode node5 = new TreeNode(5);
 9 
10         root.left = node2;
11         root.right = node3;
12         node3.left = node5;
13         node3.right = node4;
14 
15         MyBinaryTree tree = new MyBinaryTree(root);
16 
17         System.out.println("前序遍历");
18         tree.preOrder(); // 1->2->3->5->4->
19 
20         System.out.println("\n中序遍历");
21         tree.infixOrder(); // 2->1->5->3->4->
22 
23         System.out.println("\n后序遍历");
24         tree.postOrder(); // 2->5->4->3->1->
25 
26         System.out.println("\n");
27         tree.infixOrderSearch(5);
28 
29         tree.delTreeNode(5);
30         System.out.println("\n删除 5 后,前序遍历");
31         tree.preOrder(); // 1->2->3->4->
32     }
33 }
34 
35 // 结果
36 前序遍历
37 1->2->3->5->4->
38 中序遍历
39 2->1->5->3->4->
40 后序遍历
41 2->5->4->3->1->
42 
43 进入中序遍历查找
44 进入中序遍历查找
45 进入中序遍历查找
46 
47 删除 5 后,前序遍历
48 1->2->3->4->
测试类

三、线索二叉树

1、介绍

  n个结点的二叉树中含有 n+1 个空指针域。利用空指针域,存放该结点在某种遍历次序下的前驱和后继,这种附加的指针称为"线索"。

  三种遍历结果
    前序:1 3 8 10 6 14
    中序:8 3 10 1 14 6
    后序:8 10 3 14 6 1
  我们发现,只有中序遍历可以很好的利用红色结点的 "^" 来存放前驱和后继的指针。下面给出中序线索二叉树:

2、如何区分孩子还是线索

  结点:

  ltag == 0,left 指向左孩子;ltag == 1,left 指向前驱。
  rtag == 0,right 指向右孩子;rtag == 1,right 指向后继。

  代码示例:线索二叉链表结构

 1 class ThreadedTreeNode {
 2     public int val;
 3     public ThreadedTreeNode left;
 4     public ThreadedTreeNode right;
 5 
 6     public int ltag;
 7     public int rtag;
 8 
 9     public ThreadedTreeNode(int val) {
10         this.val = val;
11     }
12 
13     @Override
14     public String toString() {
15         return "ThreadedTreeNode{" +
16                 "val=" + val +
17                 '}';
18     }
19 }
线索二叉链表结构

3、中序线索化及遍历

  代码示例:中序线索化二叉树

  1 public class MyThreadedBinaryTree {
  2     private final ThreadedTreeNode root;
  3     private ThreadedTreeNode pre;
  4 
  5     public MyThreadedBinaryTree(ThreadedTreeNode root) {
  6         this.root = root;
  7     }
  8 
  9     // 树的线索化
 10     public void threaded() {
 11         this.threaded(root);
 12     }
 13 
 14     // 线索化:处理结点的顺序就是一个 中序遍历的 递归形式
 15     private void threaded(ThreadedTreeNode root) {
 16         if (root == null) {
 17             return;
 18         }
 19 
 20         // 递归左孩子
 21         this.threaded(root.left);
 22 
 23         if (root.left == null) {
 24             root.left = pre;
 25             root.ltag = 1;
 26         }
 27 
 28         if (pre != null && pre.right == null) {
 29             pre.right = root;
 30             pre.rtag = 1;
 31         }
 32 
 33         pre = root;
 34 
 35         // 递归右孩子
 36         this.threaded(root.right);
 37     }
 38 
 39     // 中序线索遍历
 40     public void threadedOrder() {
 41         this.threadedOrder(root);
 42     }
 43 
 44     private void threadedOrder(ThreadedTreeNode root) {
 45         // 为了不改变 root 指针.
 46         ThreadedTreeNode temp = root;
 47 
 48         while (temp != null) {
 49             // 找到最左边的孩子结点,也就是中序遍历的第一个结点
 50             while (temp.ltag == 0) {
 51                 temp = temp.left;
 52             }
 53 
 54             System.out.print("->" + temp.val);
 55 
 56             // 右边是线索,即 有后继指针
 57             while (temp.rtag == 1) {
 58                 temp = temp.right;
 59                 System.out.print("->" + temp.val);
 60             }
 61 
 62             temp = temp.right;
 63         }
 64     }
 65 
 66     // 中序遍历
 67     public void infixOrder() {
 68         this.infixOrder(root);
 69     }
 70 
 71     private void infixOrder(ThreadedTreeNode root) {
 72         if (root == null) {
 73             return;
 74         }
 75 
 76         if (root.left != null && root.ltag == 0) {
 77             this.infixOrder(root.left);
 78         }
 79 
 80         System.out.print("->" + root.val);
 81 
 82         if (root.right != null && root.rtag == 0) {
 83             this.infixOrder(root.right);
 84         }
 85     }
 86 }
 87 
 88 class ThreadedTreeNode {
 89     public int val;
 90     public ThreadedTreeNode left;
 91     public ThreadedTreeNode right;
 92 
 93     public int ltag;
 94     public int rtag;
 95 
 96     public ThreadedTreeNode(int val) {
 97         this.val = val;
 98     }
 99 
100     @Override
101     public String toString() {
102         return "ThreadedTreeNode{" +
103                 "val=" + val +
104                 '}';
105     }
106 }
中序线索化二叉树

  代码示例:测试类

 1 public class Main {
 2     public static void main(String[] args) {
 3         ThreadedTreeNode root = new ThreadedTreeNode(1);
 4 
 5         ThreadedTreeNode node3 = new ThreadedTreeNode(3);
 6         ThreadedTreeNode node6 = new ThreadedTreeNode(6);
 7         ThreadedTreeNode node8 = new ThreadedTreeNode(8);
 8         ThreadedTreeNode node10 = new ThreadedTreeNode(10);
 9         ThreadedTreeNode node14 = new ThreadedTreeNode(14);
10 
11         root.left = node3;
12         root.right = node6;
13         node3.left = node8;
14         node3.right = node10;
15         node6.left = node14;
16 
17         MyThreadedBinaryTree tree = new MyThreadedBinaryTree(root);
18         tree.threaded();
19 
20         // 测试.以10号结点测试
21         System.out.println("10号的前驱是=" + node10.left);
22         System.out.println("10号的后继是=" + node10.right);
23 
24         System.out.println("使用中序线索遍历方式");
25         tree.threadedOrder();
26     }
27 }
28 
29 // 结果
30 10号的前驱是=ThreadedTreeNode{val=3}
31 10号的后继是=ThreadedTreeNode{val=1}
32 使用中序线索遍历方式
33 ->8->3->10->1->14->6
测试类

四、哈夫曼树(最优二叉树)

1、介绍

  哈夫曼树是带权路径长度(WPL)最短的树。有 n 个叶子结点的哈夫曼树中,总结点数是 2n - 1。

  代码示例:创建哈夫曼树

  1 // 哈夫曼树
  2 public class HuffmanTree<T> {
  3 
  4     private TreeNode<T> root;
  5     private int wpl = 0;
  6     private Map<T, String> huffmanCodes;
  7 
  8     /**
  9      * 构建哈夫曼树的数组
 10      *
 11      * @param arr
 12      */
 13     public HuffmanTree(int[] arr) {
 14         if (arr == null || arr.length == 0) {
 15             return;
 16         }
 17 
 18         if (arr.length == 1) {
 19             root = new TreeNode<>(null, arr[0]);
 20             return;
 21         }
 22 
 23         List<TreeNode<T>> treeNodes = new ArrayList<>();
 24         for (int i : arr) {
 25             treeNodes.add(new TreeNode<>(null, i));
 26         }
 27 
 28         this.createHuffmanTree(treeNodes);
 29     }
 30 
 31     public HuffmanTree(List<TreeNode<T>> treeNodes) {
 32         if (treeNodes == null || treeNodes.size() == 0) {
 33             return;
 34         }
 35 
 36         this.createHuffmanTree(treeNodes);
 37     }
 38 
 39     private void createHuffmanTree(List<TreeNode<T>> treeNodes) {
 40         // arr = 2 4
 41         while (treeNodes.size() > 1) {
 42             Collections.sort(treeNodes);
 43 
 44             final TreeNode<T> left = treeNodes.get(0);
 45             final TreeNode<T> right = treeNodes.get(1);
 46 
 47             final TreeNode<T> parent = new TreeNode<>(null, left.val + right.val);
 48             parent.left = left;
 49             parent.right = right;
 50 
 51             // 2 4 6
 52             treeNodes.add(parent);
 53 
 54             treeNodes.remove(0);
 55             treeNodes.remove(0);
 56         }
 57 
 58         // 6
 59         root = treeNodes.get(0);
 60     }
 61 
 62     /**
 63      * 计算带权路径长度 wpl
 64      *
 65      * @return
 66      */
 67     public int wpl() {
 68         if (wpl == 0) {
 69             this.calWpl(root, 0);
 70         }
 71         return wpl;
 72     }
 73 
 74     private void calWpl(TreeNode<T> root, int deep) {
 75         if (root != null) {
 76             // 叶子结点
 77             if (root.left == null && root.right == null) {
 78                 wpl = wpl + root.val * deep;
 79             }
 80 
 81             this.calWpl(root.left, deep + 1);
 82             this.calWpl(root.right, deep + 1);
 83         }
 84     }
 85 
 86     /**
 87      * 获取元素的哈夫曼编码。左 0 右 1
 88      *
 89      * @return
 90      */
 91     public Map<T, String> getHuffmanCodes() {
 92         if (huffmanCodes == null) {
 93             huffmanCodes = new HashMap<>();
 94             this.getHuffmanCodes(root, "", new StringBuilder());
 95         }
 96         return huffmanCodes;
 97     }
 98 
 99     private void getHuffmanCodes(TreeNode<T> root, String code, StringBuilder builder) {
100         if (root == null) {
101             return;
102         }
103 
104         StringBuilder temp = new StringBuilder(builder);
105         temp.append(code);
106 
107         // 叶子结点
108         if (root.left == null && root.right == null) {
109             huffmanCodes.put(root.data, temp.toString());
110         } else {
111             // 递归左孩子
112             this.getHuffmanCodes(root.left, "0", temp);
113 
114             // 递归右孩子
115             this.getHuffmanCodes(root.right, "1", temp);
116         }
117     }
118 
119 }
120 
121 class TreeNode<T> implements Comparable<TreeNode<T>> {
122 
123     public T data;
124 
125     /**
126      * 结点的值;出现的频次;权重weight
127      */
128     public int val;
129 
130     public TreeNode<T> left;
131     public TreeNode<T> right;
132 
133     public TreeNode(T data, int val) {
134         this.data = data;
135         this.val = val;
136     }
137 
138     @Override
139     public int compareTo(TreeNode<T> o) {
140         return this.val - o.val;
141     }
142 
143     @Override
144     public String toString() {
145         return "TreeNode{" +
146                 "data=" + data +
147                 ", val=" + val +
148                 '}';
149     }
150 }
创建哈夫曼树

  代码示例:测试类

 1 // 测试类
 2 public class Main {
 3 
 4     public static void main(String[] args) {
 5 //        int[] arr = new int[]{2, 4, 5, 7};
 6 //
 7 //        final HuffmanTree<Byte> huffmanTree = new HuffmanTree<>(arr);
 8 //        final int wpl = huffmanTree.wpl();
 9 //        System.out.println(wpl);
10 //
11 //        final Map<Byte, String> huffmanCodes = huffmanTree.getHuffmanCodes();
12 //        System.out.println(huffmanCodes);
13 
14         List<TreeNode<Character>> treeNodes = new ArrayList<>();
15         TreeNode<Character> node1 = new TreeNode<>('A', 7);
16         TreeNode<Character> node2 = new TreeNode<>('B', 5);
17         TreeNode<Character> node3 = new TreeNode<>('C', 2);
18         TreeNode<Character> node4 = new TreeNode<>('D', 4);
19         treeNodes.add(node1);
20         treeNodes.add(node2);
21         treeNodes.add(node3);
22         treeNodes.add(node4);
23 
24         final HuffmanTree<Character> tree = new HuffmanTree<>(treeNodes);
25         final int wpl = tree.wpl();
26         System.out.println(wpl);
27 
28         final Map<Character, String> huffmanCodes = tree.getHuffmanCodes();
29         System.out.println(huffmanCodes);
30     }
31 }
32 
33 // 结果
34 35
35 {A=0, B=10, C=110, D=111}
测试类

2、哈夫曼编码

  哈夫曼编码是哈夫曼树在电讯通信中的经典应用之一。哈夫曼编码广泛应用于数据文件压缩,其压缩率通常在20%~90%之间,具体压缩率依赖于数据的特性。
  定长编码:像ASCII编码,都是8位表示一个字符。
  变长编码:单个编码的长度不一致,可以根据整体出现频率来调节。
  哈夫曼编码:是可变字长编码(VLC)的一种,Huffman于1952年提出一种编码方法,称之为最佳编码。哈夫曼编码是一种前缀编码。
  前缀码:就是没有任何码字是其他码字的前缀,不会造成匹配的多义性。

  定长编码:

  // 待编码的字符串,共 40 个字符(包括空格)
  i like like like java do you like a java

  // 对应Ascii码,40个
  105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32
  100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97

  // 对应的二进制
  01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100
  01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101
  00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111
  00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011
  01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001
  // 按照二进制来传递信息,总的长度是 359 (包括空格)

  变长编码:

  // 各个字符对应的个数
  d:1  y:1  u:1  j:2  v:2  o:2  l:4  k:4  e:4  i:5  a:5   :9

  // 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小
  0= ,1=a,10=i,11=e,100=k,101=l,110=o,111=v,1000=j,1001=u,1010=y,1011=d

  // 按这种编码方式,传输数据时,编码就是:
  10010110100...

  // 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码。
  // 这种编码方式有二义性。

  哈夫曼编码:统计每个字符出现的次数,以此构建一颗哈夫曼树,按路径左 0 右 1 进行编码。

  // 编码
  o:1000  u:10010  d:100110  y:100111
  i:101   a:110   k:1110   e:1111
  j:0000  v:0001   l:001     :01

  // 对应编码后的串
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

  // 压缩率
  长度为 133。原来长度 359。压缩率为:(359-133) / 359 = 62.9%

3、数据压缩、解压

  正数:原码,反码,补码都一样;负数:

 

  代码示例:创建哈夫曼树

  1 public class HuffmanCode {
  2 
  3     /**
  4      * 哈夫曼编码表.A=0 B=10 C=110 D=111
  5      */
  6     private static Map<Byte, String> huffmanCodes;
  7 
  8     /**
  9      * 数据压缩,哈夫曼编码
 10      *
 11      * @param content 待压缩的文本
 12      * @return 压缩后的字节数组 [1, 85, 109, -1, 7]
 13      */
 14     public static byte[] huffmanZip(String content) {
 15         if (content == null || content.length() == 0) {
 16             return new byte[0];
 17         }
 18 
 19         // 原始的文本 字节数组
 20         final byte[] bytes = content.getBytes();
 21 
 22         // 1.统计字节数组中 每个字符出现的次数
 23         Map<Byte, Integer> map = new HashMap<>();
 24         for (byte b : bytes) {
 25             final Integer integer = map.get(b);
 26             if (integer == null) {
 27                 map.put(b, 1);
 28             } else {
 29                 map.put(b, integer + 1);
 30             }
 31         }
 32 
 33         // 2.构建哈夫曼树
 34         List<TreeNode<Byte>> treeNodes = new ArrayList<>();
 35         for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
 36             treeNodes.add(new TreeNode<>(entry.getKey(), entry.getValue()));
 37         }
 38 
 39         final HuffmanTree<Byte> tree = new HuffmanTree<>(treeNodes);
 40         huffmanCodes = tree.getHuffmanCodes();
 41 
 42         // 3.得到二进制的编码串 00000001010101010110110111111111111
 43         StringBuilder builder = new StringBuilder();
 44         for (byte b : bytes) {
 45             builder.append(huffmanCodes.get(b));
 46         }
 47 
 48         return ByteUtil.encode(builder.toString());
 49     }
 50 
 51     /**
 52      * 数据解压,哈夫曼解码
 53      *
 54      * @param bytes 压缩后的字节数组 [1, 85, 109, -1, 7]
 55      * @return 解压后的文本
 56      */
 57     public static String huffmanUnZip(byte[] bytes) {
 58         if (huffmanCodes == null) {
 59             System.out.println("没有哈夫曼编码表~");
 60             return null;
 61         }
 62 
 63         // 1.得到二进制的字符串 "00000001010..."
 64         final String binStr = ByteUtil.decode(bytes);
 65 
 66         // 2.111-->D
 67         Map<String, Byte> map = new HashMap<>();
 68         for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
 69             map.put(entry.getValue(), entry.getKey());
 70         }
 71 
 72         // 3.解码
 73         List<Byte> list = new ArrayList<>();
 74         for (int i = 0; i < binStr.length(); ) {
 75             int count = 1;
 76             Byte temp = null;
 77 
 78             // 循环匹配二进制串
 79             while ((temp = map.get(binStr.substring(i, i + count))) == null) {
 80                 count++;
 81             }
 82 
 83             list.add(temp);
 84 
 85             i += count;
 86         }
 87 
 88         byte[] result = new byte[list.size()];
 89         for (int i = 0; i < result.length; i++) {
 90             result[i] = list.get(i);
 91         }
 92 
 93         return new String(result);
 94     }
 95 }
 96 
 97 // 工具类
 98 public class ByteUtil {
 99 
100     /**
101      * 编码.每8位一个字节 装入到byte[]
102      *
103      * @param binaryStr 二进制串."00000001010..."
104      * @return 字节数组 byte[]
105      */
106     public static byte[] encode(String binaryStr) {
107         if (binaryStr == null || binaryStr.length() == 0) {
108             return new byte[0];
109         }
110 
111         final int len = (binaryStr.length() + 7) / 8;
112         byte[] bytes = new byte[len];
113         int index = 0;
114 
115         for (int i = 0; i < binaryStr.length(); i += 8) {
116             // s = 00000001(补码)
117             final String s = i + 8 > binaryStr.length() ? binaryStr.substring(i) : binaryStr.substring(i, i + 8);
118 
119             bytes[index] = (byte) Integer.parseInt(s, 2);
120             index++;
121         }
122 
123         return bytes;
124     }
125 
126     /**
127      * 解码.将字节数组解码为二进制串
128      *
129      * @param bytes 字节数组
130      * @return 二进制串."00000001010..."
131      */
132     public static String decode(byte[] bytes) {
133         StringBuilder builder = new StringBuilder();
134 
135         for (int i = 0; i < bytes.length; i++) {
136             int temp = bytes[i];
137 
138             // 最后一个,且是正数
139             if (i == bytes.length - 1 && temp > 0) {
140                 // 不存在补位
141                 builder.append(Integer.toBinaryString(temp));
142             } else {
143                 // 不是最后一个,必然是8位.正数的话需要补位
144                 temp |= 256; // 按位或 [256 = 1 0000 0000] | [1 = 0000 0001] --> [257 = 1 0000 0001]
145                 final String str = Integer.toBinaryString(temp); // 返回的是temp对应的二进制的补码
146 
147                 builder.append(str.substring(str.length() - 8));
148             }
149 
150         }
151 
152         return builder.toString();
153     }
154 
155     public static void main(String[] args) {
156         String binaryStr = "00000001010101010110110111111111111";
157         final byte[] encode = encode(binaryStr);
158 
159         // [1, 85, 109, -1, 7]
160         System.out.println(Arrays.toString(encode));
161 
162         final String decode = decode(encode);
163         System.out.println(decode);
164     }
165 
166 }
哈夫曼数据压缩、解压

  代码示例:测试类

 1 // 测试类
 2 public class Main {
 3     public static void main(String[] args) {
 4         String content = "AAAAAAABBBBBCCDDDD";
 5         // 1.压缩
 6         final byte[] bytes = HuffmanCode.huffmanZip(content);
 7         System.out.println(Arrays.toString(bytes));
 8 
 9         // 2.解压
10         final String s = HuffmanCode.huffmanUnZip(bytes);
11         System.out.println(s);
12     }
13 }
14 
15 // 结果
16 [1, 85, 109, -1, 7]
17 AAAAAAABBBBBCCDDDD
测试类

4、文件压缩、解压

  代码示例:哈夫曼文件压缩、解压。注:下面方法对文件内容有换行,空格等可能有bug。请读者自行修正。

 1 public class FileUtil {
 2 
 3     /**
 4      * 文件解压
 5      *
 6      * @param zipFile 源文件,压缩文件
 7      * @param dstFile 目的文件,路径
 8      * @throws Exception 失败抛出异常
 9      */
10     public static void unZipFile(String zipFile, String dstFile) throws Exception {
11         try (InputStream is = new FileInputStream(zipFile);
12              ObjectInputStream ois = new ObjectInputStream(is);
13              OutputStream os = new FileOutputStream(dstFile);) {
14 
15             // 1.读取压缩文件
16             byte[] huffmanBytes = (byte[]) ois.readObject();
17             final Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
18 
19             // 2.哈夫曼解压
20             final String s = HuffmanCode.huffmanUnZip(huffmanBytes, huffmanCodes);
21 
22             os.write(s.getBytes());
23         }
24     }
25 
26     /**
27      * 文件压缩
28      *
29      * @param srcFile 源文件,全路径
30      * @param dstFile 目的文件
31      * @throws Exception 失败抛出异常
32      */
33     public static void zipFile(String srcFile, String dstFile) throws Exception {
34         try (OutputStream os = new FileOutputStream(dstFile);
35              ObjectOutputStream oos = new ObjectOutputStream(os);
36              FileInputStream is = new FileInputStream(srcFile);) {
37 
38             // 1.通过哈夫曼压缩得到 字节数组,哈夫曼编码表
39             byte[] b = new byte[is.available()];
40             is.read(b);
41 
42             final byte[] bytes = HuffmanCode.huffmanZip(new String(b));
43             final Map<Byte, String> huffmanCodes = HuffmanCode.huffmanCodes;
44 
45             // 2.写入压缩文件
46             oos.writeObject(bytes);
47             oos.writeObject(huffmanCodes);
48         }
49     }
50 }
哈夫曼文件压缩、解压

  代码示例:测试类

 1 // 测试类
 2 public class Main {
 3 
 4     public static void main(String[] args) throws Exception {
 5         //测试压缩文件
 6         String srcFile = "d://huffman_test.xml";
 7         String dstFile = "d://huffman_test.zip";
 8 
 9         FileUtil.zipFile(srcFile, dstFile);
10         System.out.println("压缩文件ok~~");
11 
12         // 测试解压文件
13         String zipFile = "d://huffman_test.zip";
14         String dstFile1 = "d://src.xml";
15 
16         FileUtil.unZipFile(zipFile, dstFile1);
17         System.out.println("解压成功!");
18     }
19 }
测试类
posted @ 2020-12-28 23:31  Craftsman-L  阅读(351)  评论(0编辑  收藏  举报