🔥工程代码已上传至github:github.com/doublev2026…
堆排序
堆排序基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),它也是不稳定排序。
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
- 一般升序采用大顶堆,降序采用小顶堆
大顶堆举例说明:
大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号
小顶堆举例说明:
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
堆排序基本思想
堆排序的基本思想是:
- 将待排序序列构造成一个大顶堆;(采用数组进行操作)
- 此时,整个序列的最大值就是堆顶的根节点;
- 将其与末尾元素进行交换,此时末尾就为最大值;
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
堆排序图解说明
要求:数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。 (升序采用大顶堆)
再总结下堆排序的基本思想:
- 将无序序列构造成一个堆,根据升序降序需求选择大顶堆或者小顶堆;
- 将堆顶元素和末尾元素交换,将最大元素“沉”到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素和末尾元素,反复执行调整加交换步骤,直到整个序列有序。
堆排序代码实现
要求:对数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
堆排序的速度非常快,在我的机器上八百万数据 2 秒左右。O(nlogn)
package com.datastructures.tree2;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class HeapSort {
public static void main(String[] args) {
// int[] arr = {4, 6, 8, 5, 9};
// heapSort(arr);
int[] arr = new int[8000000];
for (int i = 0; i < 8000000; i++) {
arr[i] = (int) (Math.random() * 8000000);
}
System.out.println("排序前");
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是 = " + date1str);
heapSort(arr);
Date date2 = new Date();
String date2str = simpleDateFormat.format(date2);
System.out.println("排序前的时间是 = " + date2str); // 2s
}
/**
* 堆排序的方法
*
* @param arr 待排序的数组
*/
public static void heapSort(int[] arr) {
int temp = 0;
System.out.println("堆排序");
// 分步测试
// adjustHeap(arr, 1, arr.length); // {4,9,8,5,6}
// System.out.println("第一次:" + Arrays.toString(arr));
// adjustHeap(arr, 0, arr.length); // {9,6,8,5,4}
// System.out.println("第二次:" + Arrays.toString(arr));
// 最终的循环代码:
// (1) 将无序序列构造成一个堆,根据升序降序需求选择大顶堆或者小顶堆;
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
// (2) 将堆顶元素和末尾元素交换,将最大元素“沉”到数组末端;
// (3) 重新调整结构,使其满足堆定义,然后继续交换堆顶元素和末尾元素,反复执行调整加交换步骤,直到整个序列有序。
for (int j = arr.length - 1; j > 0; j--) {
// 交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j); // 最后一个数就不参与下次计算了。每次在变少
}
System.out.println("数组最终 = " + Arrays.toString(arr));
}
/**
* 将一个数组(二叉树),调整成一个大顶堆。
* 以 i 对应的非叶子节点的树调整为大顶堆。
* <p>
* 举例:int[] arr = {4,6,8,5,9},i = 1 得到:{4,9,8,5,6}
* 再次调用,int[] arr = {4,9,8,5,6},i = 0 得到:{9,6,8,5,4}。这是第一次大顶堆的结果
*
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 表示对多少个元素进行调整,length 是在逐渐减少的
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i]; // 取出当前元素的值,保存在临时变量。temp = 6
// 开始调整。k = i*2+1,是i节点的左子节点
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) { // 说明左子节点的值小于右子节点的值
k++; // k指向右子节点
}
if (arr[k] > temp) { // 如果子节点大于父节点
arr[i] = arr[k]; // 把较大的值赋给当前节点
i = k; // ⚠️ i指向k,继续循环比较
} else {
break;
}
}
// 当 for 循环结束后,我们已经将以 i 为父节点的树的最大值,放在了最顶(局部的)
arr[i] = temp; // 将 temp 值放到调整后的位置
}
}
赫夫曼树
赫夫曼树基本介绍
给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
赫夫曼树重要概念:
路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径路径长度:通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1结点的权: 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积树的带权路径长度: 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) 。权值越大的结点离根结点越近的二叉树才是最优二叉树- WPL最小的就是赫夫曼树
赫夫曼树图解说明
需求:将一个数列 {13, 7, 8, 3, 29, 6, 1},转成一颗赫夫曼树
构成赫夫曼树的步骤:
- 从小到大进行排序, 将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树 , 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
赫夫曼树代码实现
package com.datastructures.tree2;
/**
* 节点类
*/
public class Node implements Comparable<Node> {
int value; // 节点的权值
Node left; // 指向左子节点
Node right; // 指向右子节点
// 前序遍历方法
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
return this.value - o.value; // 从小到大排序
}
}
package com.datastructures.tree2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
public static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("空树无法进行遍历");
}
}
/**
* 创建赫夫曼树的方法,传入数组,返回赫夫曼树的根
*
* @param arr 需要创建成赫夫曼树的数组
* @return 返回创建好后的赫夫曼树的 root 节点
*/
public static Node createHuffmanTree(int[] arr) {
// 第一步,为了操作方便,需要:
// 1、遍历 arr 数组
// 2、将 arr 的每个元素构成一个 Node
// 3、将 Node 放入到 ArrayList 中
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
// 第二步,循环处理
while (nodes.size() > 1) {
// 1、从小到大排序
Collections.sort(nodes);
System.out.println("nodes = " + nodes);
// 2、取出根节点权值最小的两颗二叉树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
// 3、组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
// 4、从 ArrayList 删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// 5、将 parent 加入到 nodes,后面会重新排序
nodes.add(parent);
}
// 第三步,返回哈夫曼树的 root 节点
return nodes.get(0);
}
}
赫夫曼编码
赫夫曼编码基本介绍
- 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法;
- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一;
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间;
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。
赫夫曼编码原理剖析
通信领域中信息的处理方式1:定长编码
- 按照二进制来传递信息,总的长度是359,包括空格(在线转码工具可百度搜索)
i like like like java do you like a java// 共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// 对应Ascii码
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//对应的二进制
通信领域中信息的处理方式2:变长编码
- 按照给各个字符规定的编码,则我们在传输 "i like like like java do you like a java" 数据时,编码就是 10,0,101,10,100...
- 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码(这个在赫夫曼编码中,再举例说明)。但是上面的方式明显有匹配的多义性问题。
i like like like java do you like a java// 共40个字符(包括空格)
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// 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如空格出现了9次,编码为0 ,其它依次类推。
通信领域中信息的处理方式3:赫夫曼编码
赫夫曼编码方式的步骤如下:
-
假设传输的字符串是:
i like like like java do you like a java //共40个字符(包括空格) -
计算字符个数:
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 ,编码如下:
o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110
k: 1110 e: 1111 j: 0000 v: 0001 l: 001 空格: 01
- 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为(注意这里我们使用的无损压缩):
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
- 通过赫夫曼编码处理长度为133
- 原来长度是359(定长编码),压缩了 (359-133) / 359 = 62.9%
- 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性问题
- 赫夫曼编码是无损处理方案
注意事项:
这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的。比如:如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
数据压缩
需求:将给出的一段文本,比如 "i like like like java do you like a java" ,根据前面赫夫曼编码原理,对其进行数据压缩处理形式如下
"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110"
创建赫夫曼树
功能:根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java" 对应的赫夫曼树
思路:
-
Node { data数据,weight权值,left,right } -
得到 "i like like like java do you like a java" 对应的 byte[] 数组
-
编写一个方法,将准备构建赫夫曼树的Node节点放到List
- 形式
[Node[date=97 ,weight=5], Node[date=32,weight=9]...] - 体现
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9
- 形式
-
可以通过List创建对应的赫夫曼树
package com.datastructures.tree2.huffman;
/**
* 带数据和权值的节点类
*/
public class NodeNew implements Comparable<NodeNew> {
Byte data; // 存放数据的字符,比如a=97, 空格=32
int weight; // 节点的权值,表示字符出现的次数
NodeNew left; // 指向左子节点
NodeNew right; // 指向右子节点
// 前序遍历方法
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public NodeNew(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(NodeNew o) {
return this.weight - o.weight; // 从小到大排序
}
}
/**
* @param bytes 接收字节数组
* @return 返回 NodeNew 的 List,形式比如:[Node[date=97 ,weight=5], Node[date=32,weight=9]...]
*/
private static List<NodeNew> getNodes(byte[] bytes) {
ArrayList<NodeNew> nodes = new ArrayList<>();
// 遍历bytes,统计出每一个byte出现的次数
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { // 第一次存入某个字符
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
// 把每一个键值对转成一个 NodeNew 对象,并加入到 nodes 集合中
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new NodeNew(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 创建赫夫曼树的方法,传入 List
*/
private static NodeNew createHuffmanTree(List<NodeNew> nodes) {
while (nodes.size() > 1) {
// 从小到大排序
Collections.sort(nodes);
// 取出根节点权值最小的两颗二叉树
NodeNew leftNode = nodes.get(0);
NodeNew rightNode = nodes.get(1);
// 组成一颗新的二叉树, 该新的二叉树的根节点没有 data 只有 weight
NodeNew parent = new NodeNew(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
// 从 nodes 删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// 将 parent 加入到 nodes,后面会重新排序
nodes.add(parent);
}
// 返回哈夫曼树的 root 节点
return nodes.get(0);
}
生成赫夫曼编码和赫夫曼编码后的数据
我们已经生成了赫夫曼树, 下面继续完成
- 生成赫夫曼树对应的赫夫曼编码 , 如下表:
- 空格=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
- 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下
- 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
/**
* 生成赫夫曼树对应的赫夫曼编码表
*/
// 1、将赫夫曼编码表存放在 Map<Byte, String> 形式中
static Map<Byte, String> huffmanCodes = new HashMap<>();
// 2、在生成赫夫曼编码表,需要去拼接路径,定义一个 StringBuilder 存储某个叶子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
// 为了方便我们重载一下 getCodes 方法
private static Map<Byte, String> getCodes(NodeNew root) {
if (root == null) {
return null;
}
// 处理root的左子树
getCodes(root.left, "0", stringBuilder);
// 处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到 huffmanCodes 集合中
*
* @param node 传入节点
* @param code 路径:左子节点是 0,右子节点是 1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(NodeNew node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将code加入到 stringBuilder2 中,生成路径
stringBuilder2.append(code);
if (node != null) {
// 判断当前 node 是叶子节点还是非叶子节点
if (node.data == null) { // 非叶子节点
// 递归处理
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else { // 叶子节点,表示找到了最后,可以完整生成路径了
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
// System.out.println("bytes = " + Arrays.toString(bytes));
// 利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println("stringBuilder = " + stringBuilder);
System.out.println("stringBuilder.length = " + stringBuilder.length()); // 133
// 原字符串长度是40,现在是 133。不能直接使用该字符串传递
// 将"1010100010111111..."转成 byte[]
// 统计返回 byte[] huffmanCodeBytes 的长度:int len = (stringBuilder.length() + 7) / 8
int length;
if (stringBuilder.length() % 8 == 0) {
length = stringBuilder.length() / 8;
} else {
length = stringBuilder.length() / 8 + 1;
}
// 创建存储压缩后的 byte 数组
byte[] huffmanCodeBytes = new byte[length];
int index = 0; // 记录这是第几个byte
for (int i = 0; i < stringBuilder.length(); i += 8) { // 因为是每8位对应一个byte,所以步长+8
String strByte;
if (i + 8 > stringBuilder.length()) { // 最后几位
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
// 将 strByte 转成一个 byte ,放入到 huffmanCodeBytes
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2); // radix 二进制
index++;
}
System.out.println("huffmanCodeBytes = " + Arrays.toString(huffmanCodeBytes));
System.out.println("huffmanCodeBytes.length = " + huffmanCodeBytes.length); // 17 这个长度就可以传递了
return huffmanCodeBytes;
}
将上面压缩步骤的方法封装成一个方法:
/**
* 压缩步骤的方法封装
* @param bytes 原始的字符串对应的字节数组
* @return 经过赫夫曼编码处理后的字节数组(压缩后的数组)
*/
private static byte[] huffmanZip(byte[] bytes) {
List<NodeNew> nodes = getNodes(bytes);
// 根据 nodes 创建赫夫曼树
NodeNew huffmanTreeRoot = createHuffmanTree(nodes);
// 生成对应的赫夫曼编码表
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
// 根据赫夫曼编码,压缩得到赫夫曼编码字节数组
byte[] zip = zip(bytes, huffmanCodes);
return zip;
}
使用赫夫曼编码解码
使用赫夫曼编码来解码数据,具体要求是:
- 前面我们得到了赫夫曼编码和对应的编码byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
- 现在要求使用赫夫曼编码,进行解码,又重新得到原来的字符串"i like like like java do you like a java"
思路:解码过程就是编码的一个逆向操作。
1、字节转二进制字符串:
/**
* 将一个 byte 转成一个二进制的字符串
*
* @param flag 标志是否需要补高位。如果是true表示需要补高位,如果是false表示不需要补高位。
* 如果是最后一个字节,无需补高位,传入 false 即可
* @param b 传入的byte
* @return 返回byte对应的二进制字符串(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
// 使用变量保存 byte,转成int
int temp = b;
// 将byte转换为无符号整数
if (temp < 0) {
temp += 256;
}
String str = Integer.toBinaryString(temp);
// 如果是正数我们还存在补高位
if (flag) {
// 按位与 256, 1 0000 0000 | 0000 0001 变为 1 0000 0001
// 需要补高位,确保是8位
if (temp >= 0 && temp < 256) {
// 确保字符串长度为8位,不足8位在前面补0
int length = str.length();
for (int i = 0; i < 8 - length; i++) {
str = "0" + str;
}
}
}
return str;
}
2、赫夫曼解码:
/**
* 编写一个方法,完成对压缩数据的解码
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组(压缩后的)
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
// 先得到 huffmanBytes 对应的二进制字符串,就是将[-88, -65, -56, -65,...] 转成 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
// 判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
// System.out.println("解码--stringBuilder:" + stringBuilder);
// 把字符串按照指定的赫夫曼编码进行解码
// 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
// 创建一个集合,存放 byte
List<Byte> list = new ArrayList<>();
// i 可以理解成就是索引,扫描 stringBuilder,然后去 map 里面映射
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 小的计数器
boolean flag = true;
Byte b = null;
while (flag) {
// 防止索引越界
if (i + count > stringBuilder.length()) {
break;
}
// 1010100010111... 递增的取出 key
String key = stringBuilder.substring(i, i + count); // i 不动,让 count 移动,直到匹配到一个字符
b = map.get(key);
if (b == null) {
count++;
} else { // 这里匹配到了
flag = false;
}
}
// 如果没有找到对应的编码,说明数据有问题
if (b == null) {
throw new RuntimeException("解码失败:无效的赫夫曼编码数据");
}
list.add(b);
i += count; // i直接移动到count
}
// 当 for 循环结束后,我们的 list 中就存放了所有的字符,"i like like like java do you like a java"
// 把 list 中的数据放入到 byte[] 并返回
byte[] result = new byte[list.size()];
for (int i = 0; i < result.length; i++) {
result[i] = list.get(i);
}
return result;
}
文件压缩
需求:给一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
思路:读取文件-> 得到赫夫曼编码表-> 完成压缩
/**
* 将一个文件进行压缩
*
* @param srcFile 希望压缩的文件的全路径
* @param dstFile 压缩后文件放到哪个目录
*/
public static void zipFile(String srcFile, String dstFile) {
// 创建文件输入流
FileInputStream is = null;
// 创建文件输出流
OutputStream os = null;
ObjectOutputStream oos = null;
try {
// 创建文件的输入流
is = new FileInputStream(srcFile);
// 创建一个和源文件大小一样的 byte[]
byte[] b = new byte[is.available()];
// 读取文件
is.read(b);
// 直接对源文件压缩
byte[] huffmanBytes = huffmanZip(b);
// 创建文件输出流
os = new FileOutputStream(dstFile);
// 创建一个和文件输出流关联的 ObjectOutputStream
oos = new ObjectOutputStream(os);
// 把赫夫曼编码后的字节数组写入压缩文件
oos.writeObject(huffmanBytes);
// 这里我们以对象流的方式写入赫夫曼编码,是为了以后我们恢复源文件时使用
// 注意一定要把赫夫曼编码写入压缩文件!!!
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
is.close();
oos.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
文件解压/恢复
需求:将前面压缩的图片文件,重新恢复成原来的文件。
思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
/**
* 将一个压缩文件进行解压
*
* @param zipFile 准备解压的文件
* @param dstFile 将文件解压到哪个路径
*/
public static void unzipFile(String zipFile, String dstFile) {
// 定义文件输入流
InputStream is = null;
// 定义对象输入流
ObjectInputStream ois = null;
// 定义文件输出流
OutputStream os = null;
try {
// 创建文件输入流
is = new FileInputStream(zipFile);
// 创建一个和 is 关联的对象输入流
ois = new ObjectInputStream(is);
// 读取 byte 数组
byte[] huffmanBytes = (byte[]) ois.readObject();
if (huffmanBytes == null) {
throw new RuntimeException("压缩文件格式错误:无法读取数据");
}
// 读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
if (huffmanCodes == null || huffmanCodes.isEmpty()) {
throw new RuntimeException("压缩文件格式错误:无法读取赫夫曼编码表");
}
// 解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
// 将 bytes 数组写入到目标文件
os = new FileOutputStream(dstFile);
// 写输入到dstFile
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频、ppt等文件
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
赫夫曼编码代码汇总
节点代码:
package com.datastructures.tree2.huffman;
/**
* 带数据和权值的节点类
*/
public class NodeNew implements Comparable<NodeNew> {
Byte data; // 存放数据的字符,比如a=97, 空格=32
int weight; // 节点的权值,表示字符出现的次数
NodeNew left; // 指向左子节点
NodeNew right; // 指向右子节点
// 前序遍历方法
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public NodeNew(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(NodeNew o) {
return this.weight - o.weight; // 从小到大排序
}
}
赫夫曼代码:
package com.datastructures.tree2.huffman;
import java.io.*;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
// String str = "i like like like java do you like a java";
// byte[] bytes = str.getBytes();
// System.out.println("字符串的长度为:" + bytes.length); // 40
// System.out.println("字符串为:" + Arrays.toString(bytes));
//
// List<NodeNew> nodes = getNodes(bytes);
// /*
// Node节点列表 =
// [Node{data=32, weight=9},Node{data=97, weight=5},Node{data=100, weight=1},Node{data=101, weight=4},
// Node{data=117, weight=1},Node{data=118, weight=2},Node{data=105, weight=5},Node{data=121, weight=1},
// Node{data=106, weight=2},Node{data=107, weight=4},Node{data=108, weight=4},Node{data=111, weight=2}]
// */
// System.out.println("Node节点列表 = " + nodes);
//
// NodeNew huffmanTreeRoot = createHuffmanTree(nodes);
// System.out.println("赫夫曼树的根节点 = " + huffmanTreeRoot); // Node{data=null, weight=40}
// // preOrder(huffmanTreeRoot);
//
// Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
// // {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
// System.out.println("生成赫夫曼树对应的赫夫曼编码表 = " + huffmanCodes);
//
// byte[] zip = zip(bytes, huffmanCodes);
// // [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
// System.out.println("生成赫夫曼编码数据 = " + Arrays.toString(zip));
//
//// byte[] huffmanZip = huffmanZip(bytes);
//// System.out.println("压缩后的数据为 = " + Arrays.toString(huffmanZip));
//
// byte[] decode = decode(huffmanCodes, zip);
// System.out.println("解码后的数据为 = " + new String(decode));
// 测试文件压缩和解压
zipFile("download.jpeg", "download.zip");
System.out.println("压缩文件成功");
unzipFile("download.zip", "download_unzip.png");
System.out.println("解压文件成功");
}
// ------------------------------【文件解压】------------------------------
/**
* 将一个压缩文件进行解压
*
* @param zipFile 准备解压的文件
* @param dstFile 将文件解压到哪个路径
*/
public static void unzipFile(String zipFile, String dstFile) {
// 定义文件输入流
InputStream is = null;
// 定义对象输入流
ObjectInputStream ois = null;
// 定义文件输出流
OutputStream os = null;
try {
// 创建文件输入流
is = new FileInputStream(zipFile);
// 创建一个和 is 关联的对象输入流
ois = new ObjectInputStream(is);
// 读取 byte 数组
byte[] huffmanBytes = (byte[]) ois.readObject();
if (huffmanBytes == null) {
throw new RuntimeException("压缩文件格式错误:无法读取数据");
}
// 读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
if (huffmanCodes == null || huffmanCodes.isEmpty()) {
throw new RuntimeException("压缩文件格式错误:无法读取赫夫曼编码表");
}
// 解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
// 将 bytes 数组写入到目标文件
os = new FileOutputStream(dstFile);
// 写输入到dstFile
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// ------------------------------【文件压缩】------------------------------
/**
* 将一个文件进行压缩
*
* @param srcFile 希望压缩的文件的全路径
* @param dstFile 压缩后文件放到哪个目录
*/
public static void zipFile(String srcFile, String dstFile) {
// 创建文件输出流
OutputStream os = null;
ObjectOutputStream oos = null;
// 创建文件输入流
FileInputStream is = null;
try {
// 创建文件的输入流
is = new FileInputStream(srcFile);
// 创建一个和源文件大小一样的 byte[]
byte[] b = new byte[is.available()];
// 读取文件
is.read(b);
// 直接对源文件压缩
byte[] huffmanBytes = huffmanZip(b);
// 创建文件输出流
os = new FileOutputStream(dstFile);
// 创建一个和文件输出流关联的 ObjectOutputStream
oos = new ObjectOutputStream(os);
// 把赫夫曼编码后的字节数组写入压缩文件
oos.writeObject(huffmanBytes);
// 这里我们以对象流的方式写入赫夫曼编码,是为了以后我们恢复源文件时使用
// 注意一定要把赫夫曼编码写入压缩文件!!!
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
oos.close();
os.close();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// ------------------------------【数据解压】【使用赫夫曼编码解码】------------------------------
/**
* 编写一个方法,完成对压缩数据的解码
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组(压缩后的)
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
// 先得到 huffmanBytes 对应的二进制字符串,就是将[-88, -65, -56, -65,...] 转成 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
// 判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
// System.out.println("解码--stringBuilder:" + stringBuilder);
// 把字符串按照指定的赫夫曼编码进行解码
// 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
// 创建一个集合,存放 byte
List<Byte> list = new ArrayList<>();
// i 可以理解成就是索引,扫描 stringBuilder,然后去 map 里面映射
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 小的计数器
boolean flag = true;
Byte b = null;
while (flag) {
// 防止索引越界
if (i + count > stringBuilder.length()) {
break;
}
// 1010100010111... 递增的取出 key
String key = stringBuilder.substring(i, i + count); // i 不动,让 count 移动,直到匹配到一个字符
b = map.get(key);
if (b == null) {
count++;
} else { // 这里匹配到了
flag = false;
}
}
// 如果没有找到对应的编码,说明数据有问题
if (b == null) {
throw new RuntimeException("解码失败:无效的赫夫曼编码数据");
}
list.add(b);
i += count; // i直接移动到count
}
// 当 for 循环结束后,我们的 list 中就存放了所有的字符,"i like like like java do you like a java"
// 把 list 中的数据放入到 byte[] 并返回
byte[] result = new byte[list.size()];
for (int i = 0; i < result.length; i++) {
result[i] = list.get(i);
}
return result;
}
/**
* 将一个 byte 转成一个二进制的字符串
*
* @param flag 标志是否需要补高位。如果是true表示需要补高位,如果是false表示不需要补高位。
* 如果是最后一个字节,无需补高位,传入 false 即可
* @param b 传入的byte
* @return 返回byte对应的二进制字符串(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
// 使用变量保存 byte,转成int
int temp = b;
// 将byte转换为无符号整数
if (temp < 0) {
temp += 256;
}
String str = Integer.toBinaryString(temp);
// 如果是正数我们还存在补高位
if (flag) {
// 按位与 256, 1 0000 0000 | 0000 0001 变为 1 0000 0001
// 需要补高位,确保是8位
if (temp >= 0 && temp < 256) {
// 确保字符串长度为8位,不足8位在前面补0
int length = str.length();
for (int i = 0; i < 8 - length; i++) {
str = "0" + str;
}
}
}
return str;
}
// ------------------------------【数据压缩】【封装方法】------------------------------
/**
* 压缩步骤的方法封装
*
* @param bytes 原始的字符串对应的字节数组
* @return 经过赫夫曼编码处理后的字节数组(压缩后的数组)
*/
private static byte[] huffmanZip(byte[] bytes) {
List<NodeNew> nodes = getNodes(bytes);
// 根据 nodes 创建赫夫曼树
NodeNew huffmanTreeRoot = createHuffmanTree(nodes);
// 生成对应的赫夫曼编码表
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
// 根据赫夫曼编码,压缩得到赫夫曼编码字节数组
byte[] zip = zip(bytes, huffmanCodes);
return zip;
}
// ------------------------------【数据压缩】【生成赫夫曼编码】------------------------------
/**
* 将字符串对应的 byte[] 数组,通过生成的赫夫曼编码表,范湖一个赫夫曼编码压缩后的 byte[]
*
* @param bytes 原始的字符串对应的 byte[]
* @param huffmanCodes 上一步生成的赫夫曼编码表
* @return 返回赫夫曼编码处理后的 byte[]
*
* <p>
* 例如:byte[] bytes = [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]
* Map<Byte, String> huffmanCodes = {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
* 返回结果 byte[] huffmanCodeBytes = 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
* <p>
* 对应的 byte[] huffmanCodeBytes,8位对应一个byte
* huffmanCodeBytes[0] = 10101000(补码) --> byte [10101000 --> 10101000-1 符号位不变--> 10100111(反码) -->11011000(原码) = -88]
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
// System.out.println("bytes = " + Arrays.toString(bytes));
// 利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
// System.out.println("stringBuilder = " + stringBuilder);
// System.out.println("stringBuilder.length = " + stringBuilder.length()); // 133
// 原字符串长度是40,现在是 133。不能直接使用该字符串传递
// 将"1010100010111111..."转成 byte[]
// 统计返回 byte[] huffmanCodeBytes 的长度:int len = (stringBuilder.length() + 7) / 8
int length;
if (stringBuilder.length() % 8 == 0) {
length = stringBuilder.length() / 8;
} else {
length = stringBuilder.length() / 8 + 1;
}
// 创建存储压缩后的 byte 数组
byte[] huffmanCodeBytes = new byte[length];
int index = 0; // 记录这是第几个byte
for (int i = 0; i < stringBuilder.length(); i += 8) { // 因为是每8位对应一个byte,所以步长+8
String strByte;
if (i + 8 > stringBuilder.length()) { // 最后几位
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
// 将 strByte 转成一个 byte ,放入到 huffmanCodeBytes
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2); // radix 二进制
index++;
}
// System.out.println("huffmanCodeBytes = " + Arrays.toString(huffmanCodeBytes));
// System.out.println("huffmanCodeBytes.length = " + huffmanCodeBytes.length); // 17 这个长度就可以传递了
return huffmanCodeBytes;
}
// ------------------------------【数据压缩】【生成赫夫曼编码表】------------------------------
/**
* 生成赫夫曼树对应的赫夫曼编码表
*/
// 1、将赫夫曼编码表存放在 Map<Byte, String> 形式中
static Map<Byte, String> huffmanCodes = new HashMap<>();
// 2、在生成赫夫曼编码表,需要去拼接路径,定义一个 StringBuilder 存储某个叶子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
// 为了方便我们重载一下 getCodes 方法
private static Map<Byte, String> getCodes(NodeNew root) {
if (root == null) {
return null;
}
// 处理root的左子树
getCodes(root.left, "0", stringBuilder);
// 处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到 huffmanCodes 集合中
*
* @param node 传入节点
* @param code 路径:左子节点是 0,右子节点是 1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(NodeNew node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将code加入到 stringBuilder2 中,生成路径
stringBuilder2.append(code);
if (node != null) {
// 判断当前 node 是叶子节点还是非叶子节点
if (node.data == null) { // 非叶子节点
// 递归处理
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else { // 叶子节点,表示找到了最后,可以完整生成路径了
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
// ------------------------------【数据压缩】【创建赫夫曼树】------------------------------
/**
* 创建赫夫曼树的方法,传入 List
*/
private static NodeNew createHuffmanTree(List<NodeNew> nodes) {
while (nodes.size() > 1) {
// 从小到大排序
Collections.sort(nodes);
// 取出根节点权值最小的两颗二叉树
NodeNew leftNode = nodes.get(0);
NodeNew rightNode = nodes.get(1);
// 组成一颗新的二叉树, 该新的二叉树的根节点没有 data 只有 weight
NodeNew parent = new NodeNew(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
// 从 nodes 删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// 将 parent 加入到 nodes,后面会重新排序
nodes.add(parent);
}
// 返回哈夫曼树的 root 节点
return nodes.get(0);
}
// ------------------------------获取Nodes列表------------------------------
/**
* @param bytes 接收字节数组
* @return 返回 NodeNew 的 List,形式比如:[Node[date=97 ,weight=5], Node[date=32,weight=9]...]
*/
private static List<NodeNew> getNodes(byte[] bytes) {
ArrayList<NodeNew> nodes = new ArrayList<>();
// 遍历bytes,统计出每一个byte出现的次数
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { // 第一次存入某个字符
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
// 把每一个键值对转成一个 NodeNew 对象,并加入到 nodes 集合中
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new NodeNew(entry.getKey(), entry.getValue()));
}
return nodes;
}
// ------------------------------前序遍历------------------------------
private static void preOrder(NodeNew root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("赫夫曼树为空,无法遍历");
}
}
}
二叉排序树 BST
需求:对数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
解决方案:
- 使用数组
- 数组未排序,优点:直接在数组尾添加,速度快。缺点:查找速度慢
- 数组排序,优点:可以使用二分查找,查找速度快。缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢
- 使用链式存储-链表
- 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动
- 使用二叉排序树
二叉排序树介绍
二叉排序树:BST(Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
二叉排序树创建与遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) ,创建成对应的二叉排序树如上图所示。
二叉排序树删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
- 删除叶子节点(比如:2, 5, 9, 12)
- 删除只有一颗子树的节点(比如:1)
- 删除有两颗子树的节点(比如:7, 3,10)
情况一:删除叶子节点(比如:2, 5, 9, 12)的思路
- 先去找到要删除的结点 targetNode
- 找到targetNode 的父结点 parent
- 确定 targetNode 是 parent 的左子结点还是右子结点
- 根据前面的情况来对应删除
- 左子结点 parent.left = null;
- 右子结点 parent.right = null;
情况二:删除只有一颗子树的节点(比如:1)
- 先去找到要删除的结点 targetNode
- 找到targetNode 的父结点 parent
- 确定 targetNode 的子结点是左子结点还是右子结点 (后面要重新链接的时候用)
- targetNode 是 parent 的左子结点还是右子结点
- 如果targetNode 有左子结点
- 如果 targetNode 是 parent 的左子结点,parent.left = targetNode.left;
- 如果 targetNode 是 parent 的右子结点,parent.right = targetNode.left;
- 如果targetNode 有右子结点
- 如果 targetNode 是 parent 的左子结点,parent.left = targetNode.right;
- 如果 targetNode 是 parent 的右子结点,parent.right = targetNode.right
情况三:删除有两颗子树的节点(比如:7,3,10)
- 先去找到要删除的结点 targetNode
- 找到 targetNode 的父结点 parent
- 从 targetNode 的右子树找到最小的结点(因为中间的要比左边的大)
- 用一个临时变量,将最小结点的值保存 temp = 12
- 删除该最小结点,然后 targetNode.value = temp
代码实现
package com.datastructures.tree3;
/**
* Node 节点
*/
public class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 中序遍历
*/
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
/**
* 递归的形式添加节点,形成二叉排序树:
* 任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
*
* @param node 要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的节点的值,和当前字数的根节点的值关系
if (node.value < this.value) { // 要添加的节点的值小于当前节点的值
// 如果当前节点的左子节点为null
if (this.left == null) {
this.left = node;
} else {
// 递归的向左子树添加
this.left.add(node);
}
} else { // 要添加的节点的值大于等于当前节点的值
if (this.right == null) {
this.right = node;
} else {
// 递归的向右子树添加
this.right.add(node);
}
}
}
/**
* 查找要删除的节点
*
* @param value 希望删除的节点的值
* @return 如果找到返回该节点,否则返回 null
*/
public Node search(int value) {
if (value == this.value) { // 找到就是该节点
return this;
} else if (value < this.value) { // 向左子树递归查找
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { // 向右子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除的节点的父节点
*
* @param value 要找到的节点的值
* @return 返回的要删除节点的父节点,没有就返回 null
*/
public Node searchParent(int value) {
// 如果当前节点就是要删除的节点的父节点,直接返回
if ((this.left != null && this.left.value == value)
|| (this.right != null && this.right.value == value)) {
return this;
} else {
// 如果要查找的值小于当前节点的值,并且当前节点的左子节点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); // 向左子树递归查找
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); // 向右子树递归查找
} else {
return null; // 没有找到父节点
}
}
}
}
package com.datastructures.tree3;
public class BinarySortTree {
private Node root;
public Node getRoot() {
return root;
}
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("二叉排序树为空,无法进行中序遍历");
}
}
/**
* 添加节点的方法
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
/**
* 查找要删除的节点
*/
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
/**
* 查找父节点
*/
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 返回/删除 node 为根节点的二叉排序树的最小节点
*
* @param node 传入的节点(当做二叉排序树的根节点)
* @return 返回的以 node 为根节点的二叉排序树的最小节点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
// 循环的查找左子节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
// 这时 target 就指向了最小节点
// 删除最小节点
delNode(target.value);
return target.value;
}
/**
* 删除节点
*
* @param value 要删除的值
*/
public void delNode(int value) {
if (root == null) {
return;
}
// 先去找到要删除的结点 targetNode
Node targetNode = search(value);
// 如果没有找到要删除的节点
if (targetNode == null) {
return;
}
// 如果当前这颗二叉排序树只有一个节点,把这个节点删掉就可以(因为走到这一步,代表 targetNode 不为空)
if (root.left == null && root.right == null) {
root = null;
return;
}
// 去找到 targetNode 的父节点
Node parent = searchParent(value);
// 如果要删除的节点是叶子节点
if (targetNode.left == null && targetNode.right == null) {
// 判断 targetNode 是父节点的左子节点还是右子节点
if (parent.left != null && parent.left.value == value) { // 左子节点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) { // 右子节点
parent.right = null;
}
}
// 删除有两颗子树的节点
else if (targetNode.left != null && targetNode.right != null) {
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
}
// 删除只有一颗子树的节点
else {
// 如果要删除的节点有左子节点
if (targetNode.left != null) {
if (parent != null) {
// 如果 targetNode 是 parent 的左子节点
if (parent.left.value == value) {
parent.left = targetNode.left;
}
// 如果 targetNode 是 parent 的右子节点
else {
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
}
// 如果要删除的节点有右子节点
else {
if (parent != null) {
// 如果 targetNode 是 parent 的左子节点
if (parent.left.value == value) {
parent.left = targetNode.right;
}
// 如果 targetNode 是 parent 的右子节点
else {
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
package com.datastructures.tree3;
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9};
BinarySortTree binarySortTree = new BinarySortTree();
// 循环的添加节点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
// 中序遍历二叉排序树
System.out.println("中序遍历二叉排序树 = ");
binarySortTree.infixOrder(); // 1,3,5,7,9,10,12
// 测试添加叶子节点
binarySortTree.add(new Node(2));
System.out.println("中序遍历二叉排序树添加 2 节点 = ");
binarySortTree.infixOrder(); // 1,2,3,5,7,9,10,12
// 测试删除叶子节点
// binarySortTree.delNode(5); // 1,2,3,7,9,10,12
// binarySortTree.delNode(1); // 2,3,5,7,9,10,12
binarySortTree.delNode(3); // 1,2,5,7,9,10,12
System.out.println("二叉排序树删除节点后 root = " + binarySortTree.getRoot());
System.out.println("二叉排序树删除节点后 = ");
binarySortTree.infixOrder();
}
}
平衡二叉树 AVL
二叉排序树有一个问题,假如有一个数组{1,2,3,4,5,6},要求创建一颗二叉排序树,就会导致:
- 左子树全部为空,从形式上看,更像一个单链表;
- 插入速度没有影响;
- 查询速度明显降低(因为需要依次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢;
解决方案:平衡二叉树
平衡二叉树介绍
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree),又被称为AVL树,可以保证查询效率较高。
它具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 平衡二叉树的常用实现方法有红黑树、AVL算法、替罪羊树、Treap、伸展树等。
单旋转-左旋转
需求:将数列 {4,3,6,5,7,8},创建出对应的平衡二叉树
思路分析:对节点A进行左旋转的步骤
- 将A节点的右节点的左节点,指向A节点
- 将A节点的右节点,指向A节点的右节点的左节点
代码实现:
/**
* 左旋转方法
*/
private void leftRotate() {
// 创建新的节点,以当前根节点的值
Node newNode = new Node(value);
// 把新节点的左子树设置了当前节点的左子树
newNode.left = left;
// 把新节点的右子树设置为当前节点的右子树的左子树
newNode.right = right.left;
// 把当前节点的值换为右子节点的值
value = right.value;
// 把当前节点的右子树设置为右子树的右子树
right = right.right;
// 把当前节点的左子树设置为新节点
left = newNode;
}
单旋转-右旋转
需求:将数列 {10,12, 8, 9, 7, 6},创建出对应的平衡二叉树
思路分析:对节点A进行右旋转的步骤
- 将A节点的左节点的右节点,指向A节点
- 将A节点的左节点,指向A节点的左节点的右节点
代码实现:
/**
* 右旋转方法
*/
private void rightRotate() {
// 创建新的节点,以当前根节点的值
Node newNode = new Node(value);
// 把新节点的右子树设置了当前节点的右子树
newNode.right = right;
// 把新节点的左子树设置为当前节点的左子树的右子树
newNode.left = left.right;
// 把当前节点的值换为左子节点的值
value = left.value;
// 把当前节点的左子树设置为左子树的左子树
left = left.left;
// 把当前节点的右子树设置为新节点
right = newNode;
}
双旋转
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树。但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列:
int[] arr = { 10, 11, 7, 6, 8, 9 }; // 运行原来的代码可以看到,并没有转成AVL树
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成AVL树
问题分析:
解决思路分析:
- 当符合右旋转的条件时
- 如果它的左子树的右子树高度大于它的左子树的左子树高度
- 先对当前这个结点的左节点进行左旋转
- 再对当前结点进行右旋转的操作即可
/**
* 递归的形式添加节点,形成二叉排序树:
* 任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
*
* @param node 要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的节点的值,和当前字数的根节点的值关系
if (node.value < this.value) { // 要添加的节点的值小于当前节点的值
// 如果当前节点的左子节点为null
if (this.left == null) {
this.left = node;
} else {
// 递归的向左子树添加
this.left.add(node);
}
} else { // 要添加的节点的值大于等于当前节点的值
if (this.right == null) {
this.right = node;
} else {
// 递归的向右子树添加
this.right.add(node);
}
}
/**
* 当添加完一个节点后,如果 (右子树的高度-左子树的高度) > 1,左旋转
*/
if (rightHeight() - leftHeight() > 1) {
// 如果它的右子树的左子树高度大于它的右子树的右子树的高度
if (right != null && right.leftHeight() > right.rightHeight()) {
// 先对右子节点进行右旋转
right.rightRotate();
// 然后对当前节点进行左旋转
leftRotate();
}
// 直接进行左旋转即可
else {
leftRotate();
}
return; // 必须要 return !!就不要处理下面的右旋转了
}
/**
* 当添加完一个节点后,如果 (左子树的高度-右子树的高度) > 1,右旋转
*/
if (leftHeight() - rightHeight() > 1) {
// 如果它的左子树的右子树高度大于它的左子树的左子树高度
if (left != null && left.rightHeight() > left.leftHeight()) {
// 先对当前这个结点的左节点进行左旋转
left.leftRotate();
// 再对当前结点进行右旋转
rightRotate();
}
// 直接进行右旋转即可
else {
rightRotate();
}
}
}
代码实现
package com.datastructures.tree4;
/**
* Node 节点
*/
public class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
// 返回以该节点为根节点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
// 返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
/**
* 左旋转方法
*/
private void leftRotate() {
// 创建新的节点,以当前根节点的值
Node newNode = new Node(value);
// 把新节点的左子树设置了当前节点的左子树
newNode.left = left;
// 把新节点的右子树设置为当前节点的右子树的左子树
newNode.right = right.left;
// 把当前节点的值换为右子节点的值
value = right.value;
// 把当前节点的右子树设置为右子树的右子树
right = right.right;
// 把当前节点的左子树设置为新节点
left = newNode;
}
/**
* 右旋转方法
*/
private void rightRotate() {
// 创建新的节点,以当前根节点的值
Node newNode = new Node(value);
// 把新节点的右子树设置了当前节点的右子树
newNode.right = right;
// 把新节点的左子树设置为当前节点的左子树的右子树
newNode.left = left.right;
// 把当前节点的值换为左子节点的值
value = left.value;
// 把当前节点的左子树设置为左子树的左子树
left = left.left;
// 把当前节点的右子树设置为新节点
right = newNode;
}
/**
* 中序遍历
*/
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
/**
* 递归的形式添加节点,形成二叉排序树:
* 任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
*
* @param node 要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的节点的值,和当前字数的根节点的值关系
if (node.value < this.value) { // 要添加的节点的值小于当前节点的值
// 如果当前节点的左子节点为null
if (this.left == null) {
this.left = node;
} else {
// 递归的向左子树添加
this.left.add(node);
}
} else { // 要添加的节点的值大于等于当前节点的值
if (this.right == null) {
this.right = node;
} else {
// 递归的向右子树添加
this.right.add(node);
}
}
/**
* 当添加完一个节点后,如果 (右子树的高度-左子树的高度) > 1,左旋转
*/
if (rightHeight() - leftHeight() > 1) {
// 如果它的右子树的左子树高度大于它的右子树的右子树的高度
if (right != null && right.leftHeight() > right.rightHeight()) {
// 先对右子节点进行右旋转
right.rightRotate();
// 然后对当前节点进行左旋转
leftRotate();
}
// 直接进行左旋转即可
else {
leftRotate();
}
return; // 必须要 return !!就不要处理下面的右旋转了
}
/**
* 当添加完一个节点后,如果 (左子树的高度-右子树的高度) > 1,右旋转
*/
if (leftHeight() - rightHeight() > 1) {
// 如果它的左子树的右子树高度大于它的左子树的左子树高度
if (left != null && left.rightHeight() > left.leftHeight()) {
// 先对当前这个结点的左节点进行左旋转
left.leftRotate();
// 再对当前结点进行右旋转
rightRotate();
}
// 直接进行右旋转即可
else {
rightRotate();
}
}
}
}
package com.datastructures.tree4;
public class AVLTree {
private Node root;
public Node getRoot() {
return root;
}
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("二叉排序树为空,无法进行中序遍历");
}
}
/**
* 添加节点的方法
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
}
package com.datastructures.tree4;
public class AVLTreeDemo {
public static void main(String[] args) {
// int[] arr = {4, 3, 6, 5, 7, 8};
// int[] arr = {10, 12, 8, 9, 7, 6};
int[] arr = {10, 11, 7, 6, 8, 9};
// int[] arr = {2, 1, 6, 5, 7, 3};
AVLTree avlTree = new AVLTree();
// 循环的添加节点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
// 中序遍历二叉排序树
System.out.println("中序遍历二叉排序树 = ");
avlTree.infixOrder();
System.out.println("进行平衡处理");
System.out.println("树的高度 = " + avlTree.getRoot().height()); // 3
System.out.println("树的左子树高度 = " + avlTree.getRoot().leftHeight()); // 2
System.out.println("树的右子树高度 = " + avlTree.getRoot().rightHeight()); // 2
System.out.println("当前根节点 = " + avlTree.getRoot()); // 8
}
}