十一 树结构的实际应用
11.1 堆排序
11.1.1 堆排序的基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复 杂度均为 O(nlogn),它也是不稳定排序。
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
- 大顶堆举例说明
- 小顶堆举例说明
- 一般升序采用大顶堆,降序采用小顶堆。
11.1.2 堆排序基本思想
堆排序的基本思想是:
- 将待排序序列构造成一个大顶堆(或小顶堆)
- 此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
11.1.3 堆排序图解说明
- 步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
- 原始的数组 [4, 6, 8, 5, 9]
(2)此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整。
(3)找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。
(4)这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。
此时,我们就将一个无序序列构造成了一个大顶堆。 - 步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
(1)将堆顶元素 9 和末尾元素 4 进行交换
(2)重新调整结构,使其继续满足堆定义
(3)再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.
(4)后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
11.1.4 堆排序的代码实现
package com.atguigu.tree;
import java.util.Arrays;
//堆排序的实现
public class HeapSort {
public static void main(String[] args) {
int arr[]= {4,6,8,5,9,-1,90,89,56,-999};
heapSort(arr);
}
//编写一个堆排序的方法
public static void heapSort(int arr[]) {
System.out.println("堆排序");
//将无序序列构成一个堆,根据升序佳宁徐需求选择大顶堆或小顶堆
for (int i = arr.length/2-1; i >=0 ; i--) {
adjustHeap(arr, i, arr.length);
}
//将堆顶元素与末尾元素进行交换,使末尾元素最大
//重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末位元素,反复执行调整+交换步骤,直到整个序列有序
int temp=0;
for (int i = arr.length-1; i >0; i--) {
temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;
adjustHeap(arr, 0, i);
}
System.out.println(Arrays.toString(arr));
}
//将一个数组(二叉树),调整成一个大顶堆
/**
* 功能:完成将以i对应的非叶子结点的树调整成大顶堆
* @param arr //待调整的数组
* @param i //表示非叶子结点在数组中的索引
* @param length //表示对多少个元素进行调整,length是在逐渐减少
*/
public static void adjustHeap(int arr[],int i,int length) {
int temp=arr[i]; //先取出当前元素的值,保证在临时变量
//开始调整
for(int k=i*2+1;k<length;k=k*2+1) { //每次循环k值才发生变化,进入到下一个左子节点
if (k+1<length && arr[k]<arr[k+1]) { //左子节点小于右子节点 <=这里是同一个k
k++; //对右子节点进行处理
}
if (arr[k]>temp) { //右子节点大于本节点
arr[i]=arr[k]; //右子节点移到本节点位置
i=k; //当前节点在数组中的标号K给i,已进行下一次比较
}else {
break; //都不符合说明:本节点相对于左右子节点已经最大
}
}
//当比较完成之后,将arr[i]进行赋值,如果发生了交换,就把temp赋给交换后的值,如果没有发生交换,就还给本节点
arr[i]=temp;
}
}
11.2 哈夫曼树
11.2.1 基本介绍
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为 最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
11.2.2 哈夫曼树集合重要概念
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为L-1
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL 最小的就是哈夫曼树
11.2.3 赫夫曼树创建思路图解
- 构成哈夫曼树的步骤
(1)从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
(2) 取出根节点权值最小的两颗二叉树
(3)组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
(4)再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树 - 给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.
11.2.4 哈夫曼树创建的代码
package com.atguigu.tree;
import java.util.ArrayList;
import java.util.Collections;
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("该树为空");
}
}
//创建哈夫曼树的方法
public static Node createHuffmanTree(int[] arr) {
//为了操作方便,
/*1. 遍历arr数组
* 2. 将arr的每个元素构成一个Node
* 3. 将Node放入到ArrayList中
* */
ArrayList<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size()>1) {
//1. 排序从小到大
Collections.sort(nodes);
//2. 取出根节点权值最小的两棵二叉树
//(1)取出权值最小的两棵二叉树
Node leftNode=nodes.get(0);
Node rightNode=nodes.get(1);
//(2) 构建一课新的二叉树
Node parent=new Node(leftNode.value+rightNode.value);
parent.left=leftNode;
parent.right=rightNode;
//(3) 从ArrayList中删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(4) 将parent加入到nodes
nodes.add(parent);
}
//哈夫曼树构建完毕
//返回哈夫曼树的root节点
return nodes.get(0);
}
}
//创建结点类
//为了让Node对象持续排序Collections集合排序,需要实现Comparable接口。
class Node implements Comparable<Node>{
int value; //结点权值
Node left; //指向左子节点
Node right; //指向右子节点
public Node(int value) {
this.value=value;
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
public int compareTo(Node o) {
return this.value-o.value; //表示从小到大排
//return -(this.value-o.value); //从大到小排
}
//一个前序遍历,用于测试
public void preOrder() {
System.out.println(this);
if (this.left!=null) {
this.left.preOrder();
}
if (this.right!=null) {
this.right.preOrder();
}
}
}
11.3 哈夫曼编码
11.3.1 哈夫曼编码
- 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
- 哈夫曼编码是哈哈夫曼树在电讯通信中的经典的应用之一。
- 哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
- 哈夫曼码是可变字长编码(VLC)的一种。Huffman 于1952 年提出一种编码方法,称之为最佳编码。
11.3.2 原理剖析
- 定长编码
- 变长编码
11.3.3 哈夫曼编码
要传输的字符串: i like like like java do you like a java
- 统计各个字符的个数:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
构成赫夫曼树的步骤:
(1)从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
(2)取出根节点权值最小的两颗二叉树
(3)组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
(4)再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理, 就得到一颗赫夫曼树
- 根据赫夫曼树,给各个字符,规定编码 (前缀编码:没有任何一个编码是另一个编码的前缀,不会造成多义性), 向左的路径为 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" 字符串对应的编码为 (注 意这里我们使用的无损压缩)
10101001101111011110100110111101111010011011110111101000011000011100110011110000110 01111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133 - 说明:原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
注意事项
PS:注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,
比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
11.3.4 最佳实践-数据压缩(创建哈夫曼树)
-
步骤1:根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树.
(1) Node { data (存放数据), weight (权值), left 和 right }
(2) 得到 “i like like like java do you like a java” 对应的 byte[] 数组
(3) 编写一个方法,将准备构建赫夫曼树的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
(4) 可以通过List 创建对应的赫夫曼树 -
步骤2.生成赫夫曼树对应的赫夫曼编码 , 如下表:=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
-
步骤3 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下.1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
-
代码实现
package com.atguigu.huffmancode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HuffmanCode {
public static void main(String[] args) {
String content="i like like like java do you like a java";
byte[] contentByte = content.getBytes();
byte[] huffmanCodesBytes=huffmanZip(contentByte);
System.out.println(Arrays.toString(huffmanCodesBytes));
}
//前序遍历
private static void preOrder(Node root) {
if (root!=null) {
root.preOrder();
}else {
System.out.println("哈夫曼树为空");
}
}
//使用一个方法,将前面的方法封装起来,便于我们调用哦
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes); //数组转成list
Node huffmanTreeRoot = createHuffmanTree(nodes);//list转成哈夫曼树
Map<Byte, String> huffmanCodes=getCodes(huffmanTreeRoot); //对应的哈夫曼编码
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes); //根据生成的哈夫曼编码,压缩得到哈夫曼编码字节数组
return huffmanCodeBytes;
}
/**1. 步骤1,创建哈夫曼树
* (1)将准备构建哈夫曼树的Node节点放到List中,形式[Node[date=97,weight=5]]
* @param bytes: 接收一个字节数组
* @return:返回的是一个list形式:[Node[date=97,weight=5],Node[date=32,weight=9],……]
*/
private static List<Node> getNodes(byte[] bytes) {
//1. 创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
// 2. 遍历bytes,统计每个byte出现的次数->map[key,value]
HashMap<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); //如果count!=null,说明map中已经有这个字符了,那我们就把该字符的统计加1,
}
}
//3. 将每个键值对转换成一个Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* (2)将给定的一个链表的数据,构建成一个哈夫曼树
* @param nodes
* @return
*/
private static Node createHuffmanTree(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中
nodes.add(parent);
}
//最后的这个节点就是根节点
return nodes.get(0);
}
//步骤2.生成哈夫曼树对应的哈夫曼编码
//思路:1.将哈夫曼编码表存放在Map<Byte,String>形式
static Map<Byte, String> huffmanCodes= new HashMap<Byte,String>();
//2. 生成哈夫曼编码,需要去拼接路径,定义一个StringBuilder存储某个叶子结点路径
static StringBuilder stringBuilder=new StringBuilder();
//重载个体Codes,
private static Map<Byte, String> getCodes(Node root) {
if (root==null) {
return null;
}
//处理左子树
getCodes(root.left,"0",stringBuilder);
//处理右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* @param node:传入的节点
* @param code :路径,左子节点是0,右子节点是1
* @param stringBuilder:用于拼接路径
*/
private static void getCodes(Node 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());
}
}
}
//步骤3. 依据哈夫曼编码生成哈夫曼编码数据
/**
* 对字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码字符串
* @param bytes :原始字符串对应的byte[]
* @param huffmanCodes:生成的哈夫曼编码map
* @return
* @return:返回哈夫曼编码字符串
*
* 例如"i like like like java do you like a java"=》byte[] contentByte = content.getBytes();
* 返回字符串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
* =》对应的byte[] huffmanCodeBytes,即8位对应一个byte,放到huffmanCodeBytes中
* huffmanCodeBytes[0]=10101000(补码)=》10101000-1=10100111(反码)=》11011000(-88)
* 因此,huffmanCodeBytes[1]=-88
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1. 利用huffmanCodes将bytes转成哈夫曼编码对应的字符串
StringBuffer stringBuffer = new StringBuffer();
//遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}//处理结束后得到:1010100010
//System.out.println(stringBuilder.toString());
//将“101010001……”转成byte[]
//一句话:int len=(stringBuilder。length()+7)/8
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) { //因为每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); //将二进制转化成十进制
index++;
}
//
return huffmanCodeBytes;
}
}
//创建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;
}
@Override
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();
}
}
}
11.3.5 数据解压
- 思路
(1)将HuffmanCodeBytes[-88,-65,-56,-65……]重新转成二进制字符串“1010100010111……”
(2)哈夫曼编码对应的二进制字符串到哈夫曼编码=》“i like like like java do you like a java” - 代码实现
/**
* 将一个byte 转化成一个二进制的字符串
* @param flag:标志是否需要补高位,true表示需要补高位;false表示不需要补高位。
* @param b: 传入的一个byte
* @return : 返回b对应的二进制的字符串(补码)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用变量保存b
int temp=b; //将b转换成int
//如果是正数,我们还要补高位
if (flag) {
temp |= 256; //按位与:256 | 1 =》10000 0000 | 0000 0001 = 10000 0001,
}
String str=Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
if (flag) {
return str.substring(str.length()-8); //如果最后一位是8位,则直接截取8位
}else {
return str; //如果最后一位不足8位,无需补高位,直接返回。
}
}
/**
* 完成对压缩编码的解码
* @param huffmanCodes :哈夫曼编码表map
* @param huffmanBytes:哈夫曼编码得到的字节数组
* @return:就是原来的字符串对应的数组
*/
public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//1. 先得到HuffmanBytes对应的二进制字符串,形式10101000……
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制的字符串
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.toString());
//将字符串安装指定的哈夫曼编码进行解码
//把哈夫曼编码表进行调换,因为反向查询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());
}
//System.out.println(map);
//创建一个集合,存放byte
ArrayList<Byte> list = new ArrayList<>();
//扫描,看编码表中是否存在该字节,如果有则加入到list中,否则继续扫描
for (int i = 0; i < stringBuilder.length();) {
int count=1; //计数器
boolean flag=true;
Byte b = null;
while (flag) {
//10101000……
//递增的取出key 1
String key = stringBuilder.substring(i, i+count); //i不动,让count动
b = map.get(key);
if (b==null) {
count++;
}else { //匹配到,退出while循环
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;
}
11.3.6 文件压缩
/**
* 文件压缩方法
* @param srcFile:希望压缩的文件的路径
* @param desFile:压缩后将压缩文件存放到哪个目录
*/
public static void zipFile(String srcFile, String desFile) {
//创建输入、输出流
FileInputStream iStream = null;
OutputStream oStream = null;
ObjectOutputStream oos = null;
try {
//创建文件输入流
iStream=new FileInputStream(srcFile);
//创建一个和原文件大小一样的byte[]
byte[] b = new byte[iStream.available()];
//读取文件
iStream.read(b);
//直接对源文件压缩
byte[] huffmanBytes = huffmanZip(b);
//创建爱你我呢间的输出流,保存压缩文件
oStream = new FileOutputStream(desFile);
//创建一个和与我呢间输出流不按量的ObjectOutPutStream
oos = new ObjectOutputStream(oStream);
//把哈夫曼编码后的字节数组写入压缩文件
oos.writeObject(huffmanBytes); //
//这里我们以对象流的方式写入哈夫曼编码,是为了以后我们恢复原文件时使用
//注意:一定要把哈夫曼编码压缩文件。
oos.writeObject(huffmanCodes);
}catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
iStream.close();
oStream.close();
oos.close();
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
11.3.7 文件解压
/**
* 压缩文件的解压
* @param zipFile:需要解压的文件
* @param desFile:解压后的目录
*/
public static void unZipFile(String zipFile, String desFile) {
//定义一个文件输入流
InputStream iStream = null;
//定义一个对象输入流
ObjectInputStream ois =null;
//定义文件的输出流
OutputStream oStream =null;
try {
//创建文件输入流
iStream=new FileInputStream(zipFile);
//创建一个和is关联的对象输入流
ois= new ObjectInputStream(iStream);
//读取byte数组,huffmanBytes
byte[] huffmanBytes= (byte[]) ois.readObject();
//读取哈夫曼编码表
Map<Byte, String> huffmanCode = (Map<Byte, String>) ois.readObject();
//解码
byte[] bytes = decode(huffmanCode, huffmanBytes);
//将bytes数组写入到目标文件
oStream=new FileOutputStream(desFile);
//写数据到dstFile文件
oStream.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
oStream.close();
iStream.close();
ois.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
11.3.8 注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml 文件]
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.
11.4 二叉排序树
11.4.1 介绍
- 二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
- 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
- 示例
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
11.4.2 二叉树的创建与遍历
package com.atguigu.binarysorttree;
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]));
}
binarySortTree.infixOrder();
}
}
//创建二叉树
class BinarySortTree{
private Node root;
//添加节点的方法
public void add(Node node) {
if (root == null) {
root = node;
}else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
}else {
System.out.println("当前二叉树为空");
}
}
}
class Node{
int value;
Node left;
Node right;
public Node(int value) {
this.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);
}
}
}
@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();
}
}
}
11.4.3 二叉排序树的删除
- 二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
(1)删除叶子节点 (比如:2, 5, 9, 12)
(2)删除只有一颗子树的节点 (比如:1)
(3)删除有两颗子树的节点. (比如:7, 3,10 ) - 情况一:删除叶子结点 (比如:2, 5, 9, 12)
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 是 parent 的左子结点 还是右子结点 (4) 根据前面的情况来对应删除
左子结点 parent.left = null
右子结点 parent.right = null; - 情况二:第二种情况: 删除只有一颗子树的节点(比如 1)
思路
(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 - 情况三: 删除有两颗子树的节点. (比如:7, 3,10 )
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 从 targetNode 的右子树找到最小的结点 ,即比当前节点值大的最小值(如果是从左子树中找,就应该找到最大的那个)
PS:注意,这里需要两个指针
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点
(6) targetNode.value = temp
- 代码实现
在BinarySortTree类中的方法
//删除节点
public void delNode(int value) {
if (root == null) {
return;
}else {
//1.需要先找到要删除的节点
Node targetNode = search(value);
//如果没有找到要铲除的节点,直接返回
if (targetNode == null) {
return;
}
//如果我们发现当前这个二叉树只有一个节点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去找到父节点
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) {
//如果partner != null; 例如只有10和1 ,此时如果要删除10,其parent就为空
if (parent != null) {
//如果targetNode是parent的左子节点
if (parent.left.value == value) {
parent.left = targetNode.left;
}else { //如果targetNode是parent的右子节点
parent.right = targetNode.left;
}
}else {
root=targetNode.left;
}
}else { //如果要删除的节点有右子节点
if (parent !=null) {
//如果targetNode是parent的左子节点
if (parent.left.value == value) {
parent.left = targetNode.right;
}else { //如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
/**编写方法:查找右子树中的最小值,辅助删除两个子节点中的右节点
* 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;
}
11.5 平衡二叉树(AVL树)
11.5.1 二叉树可能存在的问题
对于一个数列{1,2,3,4,5,6},要求创建一棵二叉排序树(BST),此二叉树存在的问题
(1)左子树全部为空,从形式上看,更像一个单链表.
(2)插入速度没有影响
(3)查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
11.5.2 平衡二叉树
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树,可以保证查询效率较高。
- 具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
11.5.3 应用案例-单选转(左旋转)
单循环是通过代码实现的
- 需要统计左子树的高度和右子树的高度,当满足插值大于1时才需要旋转。
(1)创建新的节点,其值等于当前根节点的值
(2)把当前节点的左子树设置为新节点的左子树
(3)将当前节点的右子树的左子树设置为新节点的右子树
(4)将当前节点的右子节点值设为根节点
(5)将当前节点的右子节点的右子节点设置为根节点的右子节点
(6)将当前节点的设置为新的根节点的左子树
2. 左旋转的实现代码
//左旋转实现平衡二叉树
private void leftRotate() {
//创建新的节点,其值等于当前根节点的值
Node newNode = new Node(value);
//把当前节点的左子树设置为新节点的左子树
newNode.left = left;
//将当前节点的右子树的左子树设置为新节点的右子树
newNode.right = right.left;
//将当前节点的右子节点值设为根节点
value = right.value;
//将当前节点的右子节点的右子节点设置为根节点的右子节点
right = right.right;
//将当前节点的设置为新的根节点的左子树
left = newNode;
}
11.5.4 应用案例-单选转(右旋转)
单循环是通过代码实现的
数列 {10,12, 8, 9, 7, 6}的二叉树的调整
原理同上
//右旋转实现平衡二叉树
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right =newNode;
}
11.5.5 应用案例-双循环
双循环是通过逻辑实现的,并不是通过代码实现的。
-
问题描述
数列{ 10, 11, 7, 6, 8, 9 }
-
问题分析:
当符合右旋转条件时,如果它的左子树的右子树高度>它的左子树的左子树的高度时,
(2)先对当前这个节点的左节点进行左旋转
(3)再对变换后的当前节点进行右旋转
当符合左旋转条件时,如果它的右子树的左子树高度>它的右子树的右子树的高度时,
(2)先对当前这个节点的右节点进行右旋转
(3)再对变换后的当前节点进行左旋转 -
代码实现(总代码)
package com.atguigu.avl;
public class AVLTreeDemo {
public static void main(String[] args) {
int[] arr = { 10, 11, 7, 6, 8, 9 };
// 创建一个AVLTree
AVLTree avlTree = new AVLTree();
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
System.out.println("中序遍历");
avlTree.infixOrder();
}
}
// 创建AVL树
class AVLTree {
private Node root;
// 添加节点的方法
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
// 中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉树为空");
}
}
}
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.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) {
// 如果它的右子树的左子树高度>它的右子树的右子树的高度时,
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 { // 否则,直接进行右旋转
rightHeight();
}
return;
}
}
@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();
}
}
// 返回左子树的高度
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() {
// 加1,加上本身当前节点的层数
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;
}
// 右旋转实现平衡二叉树
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
}