9. 树结构实际应用
9.1 堆排序
9.1.1 堆排序的基本原理
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
- 堆是具有一下性质的完全二叉树:每个节点的值都大于或等于其左右子节点的值,称为大顶堆。注意:没有要求节点的左右子节点的值的大小关系。
- 每个节点的值都小于或者等于其左右子节点的值,称为小顶堆
大顶堆
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TXq3YD0-1618541809464)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404111241068.png)]
我们对堆中的节点按层进行编号,映射到数组中就是下面这个样子:其特点是,arr[i] >= arr[2 * i + 1] && arr[i] >= arr[2 * i + 2]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uk7aZQIl-1618541809466)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404111356926.png)]
小顶堆
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JiF5lpFm-1618541809469)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404111546393.png)]
小顶堆的特点是:arr[i] <= arr[2 * i + 1] && arr[i] <= arr[2 * i + 1]。一般降序采用的是小顶堆,升序使用的是大顶堆
堆排序的基本思想
- 将待排序序列构造成一个大顶堆
- 此时整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如次反复操作,便能得到一个有序序列
可以看到在构建大顶堆的过程中,元素的个数在逐渐减少,最后得到的就是有序序列。
9.1.2 堆排序的过程
步骤一:构造初始堆,将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)
1)假设给定无序序列结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdgxTUQ8-1618541809473)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404142446293.png)]
2)此时我们从最后一个非叶子节点开始(叶节点自然不需要调整,最后一个非叶子节点的计算是:arr.length/2-1=1,也就是下面的6节点),从左至右,从下至上进行调整。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgoipkJu-1618541809476)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404143205856.png)]
3)找到第二个非叶子节点4,由于【4,9,8】中9元素最大,4和9交换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3PMUvO7H-1618541809478)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404143401946.png)]
4)上一步的交换导致字根[4,5,6]结构混乱,继续调整,其中4和6进行交换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RPIKGh2Y-1618541809480)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404143538032.png)]
此时我们就将一个无序序列构建成了一个大顶堆
步骤二:将堆顶元素和末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素,如此进行交换,重建和交换
1)将堆顶元素9和末尾元素4进行交换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sywpJgy6-1618541809482)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404143943049.png)]
2)重新调整结构,使其继续满足堆定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VVrM18W6-1618541809485)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404144120631.png)]
3)在将堆顶元素8与末尾元素5进行交换,得到第二大元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MW7Xcmrz-1618541809486)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404144216025.png)]
4)后续过程,继续调整,交换,如此反复进行,最终使得整个序列有序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVLSV4uA-1618541809488)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210404144355264.png)]
再简单总结下堆排序的基本思路:
- 将一个无序序列构建成一个堆,根据升序降序需要选择大顶堆或者小顶堆
- 将堆顶元素与末尾元素进行交换,使最大元素“沉”到数组末端
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换,直到整个序列有序。
9.1.3 代码实现
public class HeapSort {
public static void main(String[] args) {
//要求将数组进行升序排序
int[] arr = {4, 6, 8, 5, 9};
heapSort(arr);
}
//编写一个堆排序的方法
public static void heapSort(int arr[]) {
int temp = 0;
System.out.println("堆排序!!!!");
//分步完成
// adjustHeap(arr, 1, arr.length);
// System.out.println("第一次" + Arrays.toString(arr));
//完成我们的最终代码
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
//1. 将堆顶元素与末尾元素进行交换,将最大的元素沉到数组末端
//2. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整交换,直到整个序列有序
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 表示非叶子节点再数组中的索引
//length表示堆多少个元素继续调整,而且数值再逐渐的减少
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i]; //先取出当前元素的值,保存在临时变量
//开始调整
//说明:其中k = i * 2 + 1; k 是 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;
}
}
结果的实现
堆排序!!!!
数组=[4, 5, 6, 8, 9]
这个速度很快,八百万的数据只需要三秒,复杂度是O(nlogn)
9.2 赫夫曼树
9.2.1 赫夫曼树的原理
基本介绍
- 给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为是哈夫曼树
- 哈夫曼树是带权路径长度最短的树,权值较大的节点离根较近
赫夫曼树几个重要的概念
- 路径和路径的长度:在一棵树中,从一个节点往下可以达到子节点或者孙节点之间的通路,称为路径。通路中分支的数目称为路径长度,若规定根节点的层数为1,则从根节点到第L层节点的路径为L-1
- 节点的权以及带权路径长度:若将树中节点赋值给一个有着某种含义的数值,则这个数值称为该节点的权。**节点的带权路径长度:**从根节点到该节点之间的路径航都与该节点的权的乘积。
- **树的带权路径长度:**树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的节点离根节点越近的二叉树才是最优二叉树。
- WPL最小的就是赫夫曼树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVfLCfdm-1618541809490)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405095159497.png)]
9.2.2 赫夫曼树创建的思路
给一个数组{13,7,8,3,29,6,1},要求转换称一个赫夫曼树
构建赫夫曼树的步骤
- 从小到大进行排序,将每一个数据,每一个数据都是一个节点,每个节点可以看成是一棵简单的二叉树,
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两棵二叉树根节点权值的和
- 在将这棵新的二叉树,以根节点的权值大小再次排序,不断重复上面1~3的步骤,直到数列中,所有的数据都被处理,就得到一颗二叉树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ptpAqbI9-1618541809494)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405100913933.png)]
9.2.3 代码实现
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root= creatHuffmanTree(arr);
root.preOrder();
}
//编写一个前序遍历的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
} else {
System.out.println("这尼玛是空树,编写你妹呢");
}
}
//创建赫夫曼树的方法
public static Node creatHuffmanTree(int[] arr) {
//第一步为了操作方便
//1. 遍历arr数组
//2. 将arr的每个元素构成一个Node
//3. 将Node放入到Array List中
List<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
//我们处理的过程是一个循环的过程
while (nodes.size() > 1) {
//从小到大排序
Collections.sort(nodes);
System.out.println("ndoes = " + nodes);
//取出根节点权值最小的两棵二叉树
//(1) 取出权值最小的节点(二叉树)
Node leftNode = nodes.get(0);
//(2) 取出权值第二小的节点(二叉树)
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);
nodes.add(parent);
}
//返回的是赫夫曼的root节点
return nodes.get(0);
}
}
//创建节点类,为了让Node对象支持排序Collection集合排序
//让Node实现Comparable接口
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;
}
}
结果
ndoes = [Node[value=1], Node[value=3], Node[value=6], Node[value=7], Node[value=8], Node[value=13], Node[value=29]]
ndoes = [Node[value=4], Node[value=6], Node[value=7], Node[value=8], Node[value=13], Node[value=29]]
ndoes = [Node[value=7], Node[value=8], Node[value=10], Node[value=13], Node[value=29]]
ndoes = [Node[value=10], Node[value=13], Node[value=15], Node[value=29]]
ndoes = [Node[value=15], Node[value=23], Node[value=29]]
ndoes = [Node[value=29], Node[value=38]]
Node[value=67]
Node[value=29]
Node[value=38]
Node[value=15]
Node[value=7]
Node[value=8]
Node[value=23]
Node[value=10]
Node[value=4]
Node[value=1]
Node[value=3]
Node[value=6]
Node[value=13]
9.3 赫夫曼编码
9.3.1 赫夫曼编码的原理介绍
基本介绍
- 赫夫曼编码也翻译成哈夫曼编码,是一种编码方式,属于一种程序算法
- 赫夫曼编码是赫夫曼树在电讯通信中经典的应用
- 赫夫曼编码广泛应用于数据文件压缩,其压缩率通常在20%~90%之间
- 赫夫曼是可变字长编码的一种,是1952年提出的一种编码方式,称之为最佳编码
定长编码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMCN6bFa-1618541809495)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405112141945.png)]
变长编码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhjfwWn4-1618541809497)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405112231133.png)]
霍夫曼编码
- 需要传输的字符串是: i like like like java do you like a java
- 统计各个字符串出现的次数: d:1 y:1 u:1 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
- 按照上面字符出现的次数 [1,1,2,2,4,4,4,5,5,9] 构建一棵赫夫曼树,次数作为权值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QiPZb4t4-1618541809499)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405142336915.png)]
- 根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为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
- 按照上面的编码介绍我们可以得到,需要传输的字串编码为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdk6ngGA-1618541809502)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405143008214.png)]
说明
- 原来的长度是359,现在的长度是133
- 此编码满足前缀编码,即字符的编码都不能是其他编码的前缀。不会造成匹配的多义性
特别注意
注意这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是WPL是一样的,都是最小的,比如:如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wbwexf3o-1618541809503)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405143920045.png)]
总的长度133是不会发生变化的。
9.3.2 数据压缩(创建赫夫曼树)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGXXzMb1-1618541809506)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405144253046.png)]
思路
- Node {data(存放数据),weight(权值),left和right}
- 得到’i like like like java do you like a java’ 对应的byte[] 数组
- 编写一个方法,将准备构建赫夫曼树的Node节点放到List,形似如[Node[data=97,weight=5], Node[]data=32,weight=9]…],要体现出的是:d:1 y:1 u:1 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
- 可以通过List创建对应的赫夫曼树
代码实现
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println(contentBytes.length); //40
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes="+nodes);
//测试一把,创建的二叉树
System.out.println("赫夫曼树");
Node huffmanTree = creatHuffmanTree(nodes);
System.out.println("前序遍历");
huffmanTree.preOrder();
}
private static List<Node> getNodes(byte[] bytes){
//创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//遍历byte,统计每一个byte出现的次数 => map[key,value]
Map<Byte,Integer> counts = new HashMap<>();
for (byte b : bytes){
Integer count = counts.get(b);
if (count == null){
//map还没有这个字符数据,第一次
counts.put(b,1);
} else {
counts.put(b,count + 1);
}
}
//把每一个键值对转成一个Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte,Integer> entry: counts.entrySet()){
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//可以通过List 创建对应的赫夫曼树
private static Node creatHuffmanTree(List<Node> nodes){
while (nodes.size() > 1){
//排序从小到大
Collections.sort(nodes);
//取出第一棵最小的二叉树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//取出一个新的二叉树,它的根节点没有data,只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理的两颗二叉树从Nodes删除
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
//nodes最后的节点,就是赫夫曼树的根节点
return nodes.get(0);
}
//前序遍历的方法
private static void preOrder(Node root){
if (root != null){
root.preOrder();
} else {
System.out.println("这他妈是空的,写尼玛呢");
}
}
}
//创建Node,其中加入的有数据和权值
class Node implements Comparable<Node> {
Byte data; //存放数据(字符)本身,比如'a' => 97
int weight; //权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public String toString() {
return "Node [data =" + data + "weight = " + weight + "]";
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
//前序遍历
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
}
运行结果
40
nodes=[Node [data =32weight = 9], Node [data =97weight = 5], Node [data =100weight = 1], Node [data =101weight = 4], Node [data =117weight = 1], Node [data =118weight = 2], Node [data =105weight = 5], Node [data =121weight = 1], Node [data =106weight = 2], Node [data =107weight = 4], Node [data =108weight = 4], Node [data =111weight = 2]]
赫夫曼树
前序遍历
Node [data =nullweight = 40]
Node [data =nullweight = 17]
Node [data =nullweight = 8]
Node [data =108weight = 4]
Node [data =nullweight = 4]
Node [data =106weight = 2]
Node [data =111weight = 2]
Node [data =32weight = 9]
Node [data =nullweight = 23]
Node [data =nullweight = 10]
Node [data =97weight = 5]
Node [data =105weight = 5]
Node [data =nullweight = 13]
Node [data =nullweight = 5]
Node [data =nullweight = 2]
Node [data =100weight = 1]
Node [data =117weight = 1]
Node [data =nullweight = 3]
Node [data =121weight = 1]
Node [data =118weight = 2]
Node [data =nullweight = 8]
Node [data =101weight = 4]
Node [data =107weight = 4]
9.3.3 生成赫夫曼编码
此时我们已经完成了赫夫曼树,下面继续完成任务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdHfzgao-1618541809508)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210406100855830.png)]
代码实现
//生成赫夫曼树对应的赫夫曼编码
//1, 将赫夫曼编码表存放在Map<Byte,String>形式
//形式如:32-->01 97-->100 100--->11000
static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
//2. 在生成赫夫曼编码表示的时候,需要去拼接路径,定义一个StringBuilder存储某个子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
//为了调用方便,我们重载getCodes
private static Map<Byte,String> getCode(Node root){
if (root == null){
return null;
}
//处理root的左子树
getCode(root.left,"0",stringBuilder);
getCode(root.right,"1",stringBuilder);
return huffmanCodes;
}
//功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合
//node:传入节点
//code:路径,左子节点是0,右子节点是1
//StringBuilder 用于拼接路劲
private static void getCode(Node node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringbuilder2
stringBuilder2.append(code);
if (node != null){
//如果node == null不处理,判断当前node是叶子节点还是非叶子节点
if (node.data == null){
//非叶子节点,递归处理,
//向左递归
getCode(node.left,"0",stringBuilder2);
getCode(node.right,"1",stringBuilder2);
} else {
//说明是一个叶子节点,就表示找到某个叶子节点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
}
测试调用
//测试生成的赫夫曼编码
Map<Byte,String> huffmanCodes = getCode(huffmanTreeRoot);
System.out.println("生成的赫夫曼编码表"+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}
使用赫夫曼编码生成赫夫曼编码数据,按照上面的赫夫曼编码,字符串生成对应的编码数据,然后存储在数组中
private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
//1. 利用huffmanCodes将bytes转换成赫夫曼对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for (byte b : bytes){
stringBuilder.append(huffmanCodes.get(b));
}
//将合并后的赫夫曼字符串拼接成长的字符,101010001011111110.。。。抓换成byte[]
//统计返回 byte[] huffmanCodeBytes长度
int len;
if (stringBuilder.length() % 8 == 0){
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//创建存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0; //记录是第几个byte
for (int i = 0;i < stringBuilder.length(); i+=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);
index++;
}
return huffmanCodeBytes;
}
测试代码
//测试
byte[] huffmanCodeBytes = zip(contentBytes,huffmanCodes);
System.out.println("huffmanCodeBytes="+Arrays.toString(huffmanCodeBytes));
运行的结果
huffmanCodeBytes=[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
9.3.4 将方法进行完善
//使用一个方法,将前面的方法封装起来,便于我们的调用
//bytes 原始的字符串对应的字节数组
//返回的是经过赫夫曼编码处理后的字节数组(压缩后的数组)
private static byte[] huffmanZip(byte[] bytes){
List<Node> nodes = getNodes(bytes);
//根据nodes 创建的赫夫曼树
Node huffmanTreeRoot = creatHuffmanTree(nodes);
//对应的赫夫曼编码(根据赫夫曼树)
Map<Byte,String> huffmanCodes = getCode(huffmanTreeRoot);
//根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes,huffmanCodes);
return huffmanCodeBytes;
}
运行代码
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println(contentBytes.length); //40
byte[] huffmanCodesBytes = huffmanZip(contentBytes);
System.out.println("压缩后的结果是:"+Arrays.toString(huffmanCodesBytes));
结果
40
压缩后的结果是:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
9.3.5 赫夫曼解码
具体要求
- 前面我们得到赫夫曼编码和对应的解码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"
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println(contentBytes.length); //40
byte[] huffmanCodesBytes = huffmanZip(contentBytes);
System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes));
byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
System.out.println("原来的字符串=" + new String((sourceBytes)));
/*
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes="+nodes);
//测试一把,创建的二叉树
System.out.println("赫夫曼树");
Node huffmanTreeRoot = creatHuffmanTree(nodes);
System.out.println("前序遍历");
huffmanTreeRoot.preOrder();
//测试生成的赫夫曼编码
Map<Byte,String> huffmanCodes = getCode(huffmanTreeRoot);
System.out.println("生成的赫夫曼编码表"+huffmanCodes);
//测试
byte[] huffmanCodeBytes = zip(contentBytes,huffmanCodes);
System.out.println("huffmanCodeBytes="+Arrays.toString(huffmanCodeBytes));
*/
}
//完成数据的解压
//1. 将huffmanCodeBytes[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
// 重写先转换成 赫夫曼编码对应的二进制字符串"1010100010111......"
//2. 赫夫曼拜编码对应的二进制的字符串=====>对照赫夫曼编码=========>生成i like like like java do you like a java
//编写一个方法,完成对压缩数据的解码
//1. huffmanCodes 赫夫曼编码表map
//2. huffmanbytes 赫夫曼编码得到的字节数组
//返回的是原来字符串对应的数组
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//1. 先得到huffmanBytes对应的二进制的字符串,形式如:1010100010111....
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转换成二进制的字符串
for (int i = 0; i < huffmanBytes.length - 1; i++) {
byte b = huffmanBytes[i];
//判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString( b));
}
//把字符串按照指定的赫夫曼编码进行解码
//把赫夫曼编码表进行调换,因为反向查询a->100, 100->a
Map<String, Byte> map = new HashMap<String, Byte>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建要给集合,存放byte
List<Byte> list = new ArrayList<>();
//i 可以理解成就是索引,扫描stringBuilder
for (int i = 0; i < stringBuilder.length();) {
int count = 1; //小的计数器
boolean flag = true;
Byte b = null;
while (flag) {
//递增的取出keyi++
String key = stringBuilder.substring(i, i + count); //i不动让count移动,指定匹配到一个字符
b = map.get(key);
if (b == null) {
//说明没有匹配到
count++;
} else {
//匹配到
flag = false;
}
}
list.add(b);
i += count; //i直接移动到count
}
//当for循环之后,我们在list中就存放了所有的字符
//把list中的数据放入到byte[] 并返回
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
//将一个byte转成一个二进制的字符串,如果看不懂,可以参考我们所讲的原码,反码,补码
//b是传入的byte
//flag 标志是否需要补高位,如果是true则需要补高位,如果是false则不需要补高位,如果是最后一个字节无需补高位
//b 是对应的二进制的字符串,(注意是按补码返回)
private static String byteToBitString(byte b) {
// 使用变量保存 b
int temp = b; // 将 b 转成 int
temp |= 0x100; // 如果是正数我们需要将高位补零
// 转换为二进制字符串,正数:高位补 0 即可,然后截取低八位即可;负数直接截取低八位即可
// 负数在计算机内存储的是补码,补码转原码:先 -1 ,再取反
String binaryStr = Integer.toBinaryString(temp);
return binaryStr.substring(binaryStr.length() - 8);
}
//使用一个方法,将前面的方法封装起来,便于我们的调用
//bytes 原始的字符串对应的字节数组
//返回的是经过赫夫曼编码处理后的字节数组(压缩后的数组)
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
//根据nodes 创建的赫夫曼树
Node huffmanTreeRoot = creatHuffmanTree(nodes);
//对应的赫夫曼编码(根据赫夫曼树)
Map<Byte, String> huffmanCodes = getCode(huffmanTreeRoot);
//根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
private static List<Node> getNodes(byte[] bytes) {
//创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//遍历byte,统计每一个byte出现的次数 => map[key,value]
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) {
//map还没有这个字符数据,第一次
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
//把每一个键值对转成一个Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
//可以通过List 创建对应的赫夫曼树
private static Node creatHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//排序从小到大
Collections.sort(nodes);
//取出第一棵最小的二叉树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//取出一个新的二叉树,它的根节点没有data,只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理的两颗二叉树从Nodes删除
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
//nodes最后的节点,就是赫夫曼树的根节点
return nodes.get(0);
}
//前序遍历的方法
private static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("这他妈是空的,写尼玛呢");
}
}
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1. 利用huffmanCodes将bytes转换成赫夫曼对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//将合并后的赫夫曼字符串拼接成长的字符,101010001011111110.。。。抓换成byte[]
//统计返回 byte[] huffmanCodeBytes长度
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//创建存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0; //记录是第几个byte
for (int i = 0; i < stringBuilder.length(); i += 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);
index++;
}
return huffmanCodeBytes;
}
//生成赫夫曼树对应的赫夫曼编码
//1, 将赫夫曼编码表存放在Map<Byte,String>形式
//形式如:32-->01 97-->100 100--->11000
static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
//2. 在生成赫夫曼编码表示的时候,需要去拼接路径,定义一个StringBuilder存储某个子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
//为了调用方便,我们重载getCodes
private static Map<Byte, String> getCode(Node root) {
if (root == null) {
return null;
}
//处理root的左子树
getCode(root.left, "0", stringBuilder);
getCode(root.right, "1", stringBuilder);
return huffmanCodes;
}
//功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合
//node:传入节点
//code:路径,左子节点是0,右子节点是1
//StringBuilder 用于拼接路劲
private static void getCode(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringbuilder2
stringBuilder2.append(code);
if (node != null) {
//如果node == null不处理,判断当前node是叶子节点还是非叶子节点
if (node.data == null) {
//非叶子节点,递归处理,
//向左递归
getCode(node.left, "0", stringBuilder2);
getCode(node.right, "1", stringBuilder2);
} else {
//说明是一个叶子节点,就表示找到某个叶子节点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
}
//创建Node,其中加入的有数据和权值
class Node implements Comparable<Node> {
Byte data; //存放数据(字符)本身,比如'a' => 97
int weight; //权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public String toString() {
return "Node [data =" + data + "weight = " + weight + "]";
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
9.3.6 文件压缩
要求
学习通过赫夫曼编码对一个字符串的编码和解码,下面完成对文件的压缩和解压,具体要求:给你一个图片文件,要求对其进行无损压缩,看看压缩的效果如何。
思路:读取文件=====>得到赫夫曼编码表========>完成压缩
//编写一个方法,将文件进行压缩
/**
*
* @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();
//直接对源文件压缩
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();
os.close();
oos.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
9.3.7 文件解压
//编写一个方法,完成对压缩文件的压缩
/**
*
* @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数组 huffmanBytes
byte[] huffmanBytes = (byte[])ois.readObject();
//读取赫夫曼编码表
Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
//解码
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 e2){
System.out.println(e2.getMessage());
}
}
}
9.3.8 赫夫曼压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt等文件。
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
9.4 二叉排序树
9.4.1 二叉排序树的基本介绍
先看一个需求
给一个数列{7,3,10,12,5,1,9},要求能够高效的完成对数据的查询喝添加
解决方案
- 使用数组:1)数组未排序,优点是直接在数组尾添加,速度快。缺点是:查找速度慢。2)数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需要整体移动,速度慢。
- 使用链式存储----链表:不管链表是否有序,查找速度都很慢,添加数据舒服比数组快,不需要数据整体移动。
- 使用二叉排序树
二叉排序树介绍
二叉排序树:BST(Binary Sort Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点大。特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BORQXdPQ-1618541809512)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407120452685.png)]
9.4.2 二叉排序树的创建和遍历
一个数组创建成对用的二叉排序树,并使用中序遍历二叉排序树。
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();
}
}
//创建二叉排序树
class BinarySortTree{
private Node root;
//添加节点的方法
public void add(Node node){
if (root == null){
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder(){
if (root!=null){
root.infixOrder();
} else {
System.out.println("这他妈是空的,玩尼玛呢");
}
}
}
//创建Node节点
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 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);
}
}
}
//中序遍历
public void infixOrder(){
if (this.left != null){
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null){
this.right.infixOrder();
}
}
}
运行结果
看小爷的中序排序操作输出~~~~~
Node [value=1]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
9.4.3 二叉排序树的删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EopjZPxD-1618541809514)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407123929807.png)]
二叉排序树的删除情况比较复杂,下面有三种情况需要考虑
- 删除叶子节点(比如:2,5,9,12)
- 删除只有一棵子树的节点(比如:1)
- 删除有两棵子树的节点(比如:7,3,10)
第一种情况
- 需要先找到要删除的节点targetNode
- 找到targetNode的父节点parent
- 确定targetNode是parent的左子节点还是右子节点
- 根据前面的情况来对应删除
左子节点:parent.left = null;
右子节点:parent.right = null;
第二种情况
-
需要先找到要删除的节点targetNode
-
找到targetNode的父节点parent
-
确定targetNode 的子节点是左子节点还是右子节点
-
targetNode 是 parent的左子节点还是右子节点
-
如果targetNode有左子节点
5.1 如果targetNode是parent的左子节点
parent.left = targetNode.left;
5.2 如果targetNode是parent的右子节点
parent.right = targetNode.left;
-
如果targetNode有右子节点
6.1 如果targetNode是parent的左子节点
parent.left = targetNode.right;
6.2 如果targetNode是parent的右子节点
parent.right = targetNode.right;
第三种情况
- 需要先找到要删除的节点targetNode
- 找到targetNode的父节点parent
- 从targetNode的左子树找到最小的节点
- 用一个临时变量,将最小节点的值保存temp
- 删除最小的节点
- targetNode.value = temp;
9.4.4 代码实现
删除叶子节点
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环添加节点
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉排序树
System.out.println("看小爷的中序排序操作输出~~~~~");
binarySortTree.infixOrder();
//测试一下删除叶子节点
binarySortTree.delNode(2);
System.out.println("删除节点后");
binarySortTree.infixOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
//查找要删除的节点
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);
}
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
} else {
//1. 需要先去找要删除的节点targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点
if (targetNode == null) {
return;
}
//如果我们发现这个二叉树只有一个节点
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;
}
}
}
}
//添加节点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("这他妈是空的,玩尼玛呢");
}
}
}
//创建Node节点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//查找要删除的节点
/**
* @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; //说明没有找到父节点
}
}
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求
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);
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
运行结果
看小爷的中序排序操作输出~~~~~
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
删除节点后
Node [value=1]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
Process finished with exit code 0
删除只有一个子树的节点
//删除只有一个子树的节点
//如果要删除的节点有左子节点
if (targetNode.left != null){
//如果targetNode 是parent的左子节点
if (parent.left.value == value){
parent.left = targetNode.left;
} else {
//targetNode 是parent的右子节点
parent.right = targetNode.left;
}
} else {
//如果要删除的节点是右子节点
if (parent.left.value == value){
parent.left = targetNode.right;
} else {
//如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
}
结果
看小爷的中序排序操作输出~~~~~
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
删除节点后
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
删除有两个子节点的代码
else if (targetNode.left != null && targetNode.right != null){
//删除有两个子树的节点
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
运行结果
看小爷的中序排序操作输出~~~~~
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
删除节点后
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=9]
Node [value=10]
Node [value=12]
9.4.5 二叉排序树代码中的一个漏洞
当你删除的还剩下最后两个节点的时候,此时进行删除的时候就进入删除只有一个节点的情况,但是parent会出现空指针异常的情况
所以我们需要修改以下代码,来防止异常的出现
史上最完整的代码
package binarySortTree;
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环添加节点
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉排序树
System.out.println("看小爷的中序排序操作输出~~~~~");
binarySortTree.infixOrder();
//测试一下删除叶子节点
binarySortTree.delNode(7);
System.out.println("删除节点后");
binarySortTree.infixOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
//查找要删除的节点
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);
}
}
//编写方法:
//1. 返回的以node为根节点的二叉排序树的最小节点的值
//2. 删除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;
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
} else {
//1. 需要先去找要删除的节点targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点
if (targetNode == null) {
return;
}
//如果我们发现这个二叉树只有一个节点
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) {
//如果targetNode 是parent的左子节点
if (parent != null) {
if (parent.left.value == value) {
parent.left = targetNode.left;
} else {
//targetNode 是parent的右子节点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else {
//如果要删除的节点是右子节点
if (parent != null) {
if (parent.left.value == value) {
parent.left = targetNode.right;
} else {
//如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
//添加节点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("这他妈是空的,玩尼玛呢");
}
}
}
//创建Node节点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//查找要删除的节点
/**
* @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; //说明没有找到父节点
}
}
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求
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);
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
9.5 平衡二叉树
给一个数列{1,2,3,4,5,6},要求创建一棵二叉排序树(BST),可能出现的问题是
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gb9uqD7e-1618541809516)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407182253446.png)]
左边BST存在的问题
- 左边子树全为空,从形式上看,更像一个单链表
- 插入速度没有问题
- 查询速度明显降低(因为需要一次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
- 解决的方案是平衡二叉树
9.5.1 平衡二叉树的基本原理介绍
基本介绍
- 平衡二叉树也叫平衡二叉搜索树(self-balancing binary search tree)又称为AVL树,可以保证查询效率较高
- 具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,平衡二叉树的常用实现方法有:红黑树,AVL,替罪羊树,Treap,伸展树等。
9.5.2 左旋转分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjANU1xo-1618541809518)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407183734876.png)]
代码实现
package AVL;
public class AVLTreeDemo {
public static void main(String[] args) {
int[] arr ={4,3,6,5,7,8};
//创建一个AVLTree对象
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());
System.out.println("树的左子树高度="+avlTree.getRoot().leftHeight());
System.out.println("树的右子树高度="+avlTree.getRoot().rightHeight());
}
}
//创建AVLTree
class AVLTree {
private Node root;
public Node getRoot(){
return root;
}
//查找要删除的节点
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);
}
}
//编写方法:
//1. 返回的以node为根节点的二叉排序树的最小节点的值
//2. 删除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;
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
} else {
//1. 需要先去找要删除的节点targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点
if (targetNode == null) {
return;
}
//如果我们发现这个二叉树只有一个节点
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) {
//如果targetNode 是parent的左子节点
if (parent != null) {
if (parent.left.value == value) {
parent.left = targetNode.left;
} else {
//targetNode 是parent的右子节点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else {
//如果要删除的节点是右子节点
if (parent != null) {
if (parent.left.value == value) {
parent.left = targetNode.right;
} else {
//如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
//添加节点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("这他妈是空的,玩尼玛呢");
}
}
}
//创建Node节点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
//返回当前节点的高度,以该节点为根节点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//左旋转方法
private void leftRotate(){
//创建新的节点,以当前根节点的值
Node newNode = new Node(value);
//把新的节点的左子树设置成当前节点的左子树
newNode.left = left;
//把新的节点的右子树设置成当前节点的右子树的左子树
newNode.right = right.left;
//把当前节点的值替换成右子节点的值
value = right.value;
//把当前节点的右子树设置成当前节点右子树的右子树
right = right.right;
//把当前节点的左子树(左子节点)设置成新的节点
left = newNode;
}
//查找要删除的节点
/**
* @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; //说明没有找到父节点
}
}
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求
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){
leftRotate();
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
结果
中序遍历
Node [value=3]
Node [value=4]
Node [value=5]
Node [value=6]
Node [value=7]
Node [value=8]
在没有平衡处理之前~~~
树的高度=3
树的左子树高度=2
树的右子树高度=2
9.5.3 右旋转分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6jcfx2k-1618541809521)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407201038972.png)]
代码
//右旋转
private void rightRotate(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
9.5.4 双旋转分析
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某种情况下,但旋转不能完成平衡二叉树的转换
比如数组 int[] arr = {10,11,7,6,8,9},或者数组int[] arr = {2,1,6,5,7,3}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFYhI09Z-1618541809523)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210407205408834.png)]
问题分析
- 当符号右旋转调整时
- 如果它的左子树中(右子树的高度大于左子树高度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YEy1IzAc-1618541809524)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210408003313785.png)]
- 然后对其进行左旋转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnCdCGaD-1618541809527)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210408003639337.png)]
- 然后再对整个进行右旋转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I26fURAs-1618541809529)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210408004006877.png)]
代码实现
//当添加完一个节点之后,如果:右子树的高度减去左子树的高度大于1,则进行左旋转
if (rightHeight() - leftHeight() > 1){
if (right != null && right.leftHeight() > right.rightHeight()){
right.rightRotate();
leftRotate();
} else {
leftRotate();
}
return; //必须需要!!!
}
//当添加完一个节点之后,如果:左子树的高度减去右子树的高度大于1,则进行右旋转
if (leftHeight() - rightHeight() > 1){
//如果它的左子树的右子树高度大于它的左子树高度
if (left != null && left.rightHeight() > left.leftHeight()){
//先对当前节点的左节点====>左旋转
left.leftRotate();
//再对当前节点进行右旋转
rightRotate();
} else {
rightRotate();
}
}
}
结果实现
中序遍历
Node [value=6]
Node [value=7]
Node [value=8]
Node [value=9]
Node [value=10]
Node [value=12]
在没有平衡处理之前~~~
树的高度=3
树的左子树高度=2
树的右子树高度=2