9. 树结构实际应用

9.1 堆排序

9.1.1 堆排序的基本原理

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
  2. 堆是具有一下性质的完全二叉树:每个节点的值都大于或等于其左右子节点的值,称为大顶堆。注意:没有要求节点的左右子节点的值的大小关系。
  3. 每个节点的值都小于或者等于其左右子节点的值,称为小顶堆

大顶堆

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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]。一般降序采用的是小顶堆,升序使用的是大顶堆

堆排序的基本思想

  1. 将待排序序列构造成一个大顶堆
  2. 此时整个序列的最大值就是堆顶的根节点
  3. 将其与末尾元素进行交换,此时末尾就为最大值
  4. 然后将剩余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)]

再简单总结下堆排序的基本思路:

  1. 将一个无序序列构建成一个堆,根据升序降序需要选择大顶堆或者小顶堆
  2. 将堆顶元素与末尾元素进行交换,使最大元素“沉”到数组末端
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换,直到整个序列有序。

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 赫夫曼树的原理

基本介绍

  1. 给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为是哈夫曼树
  2. 哈夫曼树是带权路径长度最短的树,权值较大的节点离根较近

赫夫曼树几个重要的概念

  1. 路径和路径的长度:在一棵树中,从一个节点往下可以达到子节点或者孙节点之间的通路,称为路径。通路中分支的数目称为路径长度,若规定根节点的层数为1,则从根节点到第L层节点的路径为L-1
  2. 节点的权以及带权路径长度:若将树中节点赋值给一个有着某种含义的数值,则这个数值称为该节点的权。**节点的带权路径长度:**从根节点到该节点之间的路径航都与该节点的权的乘积。
  3. **树的带权路径长度:**树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的节点离根节点越近的二叉树才是最优二叉树。
  4. 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. 从小到大进行排序,将每一个数据,每一个数据都是一个节点,每个节点可以看成是一棵简单的二叉树,
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两棵二叉树根节点权值的和
  4. 在将这棵新的二叉树,以根节点的权值大小再次排序,不断重复上面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;}@Overridepublic String toString() {return "Node[value=" + value + "]";}@Overridepublic 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 赫夫曼编码的原理介绍

基本介绍

  1. 赫夫曼编码也翻译成哈夫曼编码,是一种编码方式,属于一种程序算法
  2. 赫夫曼编码是赫夫曼树在电讯通信中经典的应用
  3. 赫夫曼编码广泛应用于数据文件压缩,其压缩率通常在20%~90%之间
  4. 赫夫曼是可变字长编码的一种,是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)]

霍夫曼编码

  1. 需要传输的字符串是: i like like like java do you like a java
  2. 统计各个字符串出现的次数: d:1 y:1 u:1 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
  3. 按照上面字符出现的次数 [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)]

  1. 根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为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

  1. 按照上面的编码介绍我们可以得到,需要传输的字串编码为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdk6ngGA-1618541809502)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210405143008214.png)]

说明

  1. 原来的长度是359,现在的长度是133
  2. 此编码满足前缀编码,即字符的编码都不能是其他编码的前缀。不会造成匹配的多义性

特别注意

注意这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是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)]

思路

  1. Node {data(存放数据),weight(权值),left和right}
  2. 得到’i like like like java do you like a java’ 对应的byte[] 数组
  3. 编写一个方法,将准备构建赫夫曼树的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
  4. 可以通过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); //40List<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){//创建一个ArrayListArrayList<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集合//遍历mapfor (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'  => 97int 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 + "]";}@Overridepublic 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--->11000static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();//2. 在生成赫夫曼编码表示的时候,需要去拼接路径,定义一个StringBuilder存储某个子节点的路径static StringBuilder stringBuilder = new StringBuilder();//为了调用方便,我们重载getCodesprivate 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加入到stringbuilder2stringBuilder2.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;  //记录是第几个bytefor (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,放入到huffmanCodeByteshuffmanCodeBytes[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); //40byte[] 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 赫夫曼解码

具体要求

  1. 前面我们得到赫夫曼编码和对应的解码byte[],即[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
  2. 现在要求使用赫夫曼编码,进行解码,又会重新得到原来的字符串"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); //40byte[] 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->aMap<String, Byte> map = new HashMap<String, Byte>();for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {map.put(entry.getValue(), entry.getKey());}//创建要给集合,存放byteList<Byte> list = new ArrayList<>();//i 可以理解成就是索引,扫描stringBuilderfor (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) {// 使用变量保存 bint temp = b; // 将 b 转成 inttemp |= 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) {//创建一个ArrayListArrayList<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集合//遍历mapfor (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;  //记录是第几个bytefor (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,放入到huffmanCodeByteshuffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);index++;}return huffmanCodeBytes;}//生成赫夫曼树对应的赫夫曼编码//1, 将赫夫曼编码表存放在Map<Byte,String>形式//形式如:32-->01  97-->100  100--->11000static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();//2. 在生成赫夫曼编码表示的时候,需要去拼接路径,定义一个StringBuilder存储某个子节点的路径static StringBuilder stringBuilder = new StringBuilder();//为了调用方便,我们重载getCodesprivate 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加入到stringbuilder2stringBuilder2.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'  => 97int 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 + "]";}@Overridepublic 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);//创建一个和文件输出流关联的ObjectOutputStreamoos = 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数组 huffmanBytesbyte[] 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 赫夫曼压缩文件注意事项

  1. 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt等文件。
  2. 赫夫曼编码是按字节来处理的,因此可以处理所有的文件
  3. 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显

9.4 二叉排序树

9.4.1 二叉排序树的基本介绍

先看一个需求

给一个数列{7,3,10,12,5,1,9},要求能够高效的完成对数据的查询喝添加

解决方案

  1. 使用数组:1)数组未排序,优点是直接在数组尾添加,速度快。缺点是:查找速度慢。2)数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需要整体移动,速度慢。
  2. 使用链式存储----链表:不管链表是否有序,查找速度都很慢,添加数据舒服比数组快,不需要数据整体移动。
  3. 使用二叉排序树

二叉排序树介绍

二叉排序树: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;}@Overridepublic String toString(){return "Node [value="+value+"]";}//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求public void add(Node node){if (node == null){return;}//判断传入的节点值和当前子树的根节点值的关系if (node.value < this.value){//如果当前节点左子节点为nullif (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)]

二叉排序树的删除情况比较复杂,下面有三种情况需要考虑

  1. 删除叶子节点(比如:2,5,9,12)
  2. 删除只有一棵子树的节点(比如:1)
  3. 删除有两棵子树的节点(比如:7,3,10)

第一种情况

  1. 需要先找到要删除的节点targetNode
  2. 找到targetNode的父节点parent
  3. 确定targetNode是parent的左子节点还是右子节点
  4. 根据前面的情况来对应删除

左子节点:parent.left = null;

右子节点:parent.right = null;

第二种情况

  1. 需要先找到要删除的节点targetNode

  2. 找到targetNode的父节点parent

  3. 确定targetNode 的子节点是左子节点还是右子节点

  4. targetNode 是 parent的左子节点还是右子节点

  5. 如果targetNode有左子节点

    5.1 如果targetNode是parent的左子节点

    parent.left = targetNode.left;

    5.2 如果targetNode是parent的右子节点

    parent.right = targetNode.left;

  6. 如果targetNode有右子节点

    6.1 如果targetNode是parent的左子节点

    parent.left = targetNode.right;

    6.2 如果targetNode是parent的右子节点

    parent.right = targetNode.right;

第三种情况

  1. 需要先找到要删除的节点targetNode
  2. 找到targetNode的父节点parent
  3. 从targetNode的左子树找到最小的节点
  4. 用一个临时变量,将最小节点的值保存temp
  5. 删除最小的节点
  6. 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. 需要先去找要删除的节点targetNodeNode 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;  //说明没有找到父节点}}}@Overridepublic String toString() {return "Node [value=" + value + "]";}//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求public void add(Node node) {if (node == null) {return;}//判断传入的节点值和当前子树的根节点值的关系if (node.value < this.value) {//如果当前节点左子节点为nullif (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. 需要先去找要删除的节点targetNodeNode 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;  //说明没有找到父节点}}}@Overridepublic String toString() {return "Node [value=" + value + "]";}//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求public void add(Node node) {if (node == null) {return;}//判断传入的节点值和当前子树的根节点值的关系if (node.value < this.value) {//如果当前节点左子节点为nullif (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存在的问题

  1. 左边子树全为空,从形式上看,更像一个单链表
  2. 插入速度没有问题
  3. 查询速度明显降低(因为需要一次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
  4. 解决的方案是平衡二叉树

9.5.1 平衡二叉树的基本原理介绍

基本介绍

  1. 平衡二叉树也叫平衡二叉搜索树(self-balancing binary search tree)又称为AVL树,可以保证查询效率较高
  2. 具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过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. 需要先去找要删除的节点targetNodeNode 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;  //说明没有找到父节点}}}@Overridepublic String toString() {return "Node [value=" + value + "]";}//添加节点的方法,使用递归的方式进行添加,注意需要满足二叉排序树的要求public void add(Node node) {if (node == null) {return;}//判断传入的节点值和当前子树的根节点值的关系if (node.value < this.value) {//如果当前节点左子节点为nullif (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)]

问题分析

  1. 当符号右旋转调整时
  2. 如果它的左子树中(右子树的高度大于左子树高度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YEy1IzAc-1618541809524)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210408003313785.png)]

  1. 然后对其进行左旋转

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnCdCGaD-1618541809527)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210408003639337.png)]

  1. 然后再对整个进行右旋转

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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

第九话 树结构实际应用相关推荐

  1. Android简易实战教程--第九话《短信备份~二》

    这一篇,承接地八话.使用高效的方式备份短信--xml序列化器. 存储短信,要以对象的方式存储.首先创建javabean: package com.itydl.createxml.domain;publ ...

  2. 【JavaSE系列】 第九话 —— 多态那些事儿

    ☕导航小助手☕  

  3. mybatis第十话 - mybaits整个事务流程的源码分析

    1.故事前因 在分析mybatis源码时一直带的疑问,一直以为事务是在SqlSessionTemplate#SqlSessionInterceptor#invoke完成的,直到断点才发现并不简单! 在 ...

  4. eva每一集片尾曲是谁唱的_【跪求】EVA 18集片尾曲的歌手名,考验大家的听力~...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 = = FLY ME TO THE MOON的详细整理 ED各话FLY ME TO THE MOON版本明细 其中clare是本作中歌曲的原唱者,YOKO ...

  5. eva每一集片尾曲是谁唱的_evaTV版的片尾曲是不是每集的都不一样啊?

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 Fly Me To The Moon有多少个版本? FLY ME TO THE MOON的详细整理 ED各话FLY ME TO THE MOON版本明细 ...

  6. PullToRefreshListView刷新2

    public class FragmentScrollView extends Fragment {private PullToRefreshScrollView refreshScrollView; ...

  7. 数据结构--图的存储结构

    系列文章目录 第九话  数据结构之图的存储 文章目录 一.了解什么是图 二.图的定义和基本术语 三.存储结构之邻接矩阵 1.邻接矩阵的介绍 2.邻接矩阵的创建 3.主函数中实现 四.存储结构之邻接表 ...

  8. 【HTML】HTML网页设计----动漫网站设计

    1.引言 设计结课作业,课程设计无处下手,网页要求的总数量太多?没有合适的模板?数据库,java,python,vue,html作业复杂工程量过大?毕设毫无头绪等等一系列问题.你想要解决的问题,在微信 ...

  9. 憎恨之心最强套装攻略_憎恨之心S套装最好 | 手游网游页游攻略大全

    发布时间:2016-06-17 巫师3DLC石之心新月套装控制台代码分享.很多玩家想要获得巫师3DLC石之心新增套装新月套,但是又懒得去找,那就只好用控制台了,今天99单机小编和大家分享巫师3DLC石 ...

  10. 纹章之谜一人攻略——英雄战争篇

    纹章之谜一人攻略--英雄战争篇 战前准备 1.角色选择:说真的,英雄战争篇的人物相当难选呢!这里偶仅从成长率方面考虑,因此选了ロディ(罗迪).本来想选セシル(塞希尔)的,可是她的力和防御的成长率实在是 ...

最新文章

  1. 怎样判断子进程已经结束 process.waitFor();的问题
  2. 一种消息和任务队列——beanstalkd
  3. 浅谈 maxMemory , totalMemory , freeMemory 和 OOM 与 native Heap
  4. asp.net core 3.0 gRPC框架小试
  5. 不具有继承关系的Delegate如何进行类型转换?
  6. 《C语言及程序设计》实践参考——太乐了
  7. 选书不迷茫,国内原创佳作推荐,附赠神级优惠码༼⍤༽
  8. Docker中配置国内镜像
  9. 恭喜我司李震博士被聘为南京航空航天大学兼职教授
  10. 最小割最大流算法matlab,matlab练习程序(最大流/最小割)
  11. UCD的产品设计原则
  12. linux中ps-p,linux下ps命令
  13. 解决jsp中文乱码的两种方式
  14. 转载:汇总详解:矩阵的迹以及迹对矩阵求导
  15. WIN11添加我的电脑图标等的方法
  16. 【重复制造精讲】5、MF50计划
  17. 十八、绘制游戏背景图片
  18. 问题2:路径级别不清楚
  19. 微信支付报错:用户传入的appid不正确,请联系商户处理
  20. 程序员中的奇葩,使用php构建魔兽世界

热门文章

  1. (DDIA)SQL与NoSQL数据模型简介
  2. 1148. 简单密码破解
  3. 协议栈skb _buff
  4. 爬取智联招聘岗位描述并根据描述生成词云
  5. PhotoShop 之移动选区
  6. js[中英文排序-获取中文拼音]
  7. 直播APP软件开发,直播系统开发的技术架构揭秘
  8. CSS布局:多种方案实现固定页脚(sticky footer)
  9. 最大化参数 火车头_火车采集器,您身边的的网页数据采集专家!
  10. 人类学家胡家奇谈科技发展:让它回归理性