目录
1.堆排序
2.哈夫曼树
3.哈夫曼编码
4.二叉排序树(BST)
5.平衡二叉树(AVL树)
1.堆排序
堆的介绍:
堆是具有以下性质的完全二叉树:
1.每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆。注意 : 没有要求节点的左孩子的值和右孩子的值的大小关系。
采用顺序存储后为:
2.每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆。
堆排序介绍:
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。一般升序采用大顶堆,降序采用小顶堆。
堆排序基本思想:
以升序为例:
1.将待排序序列构造成一个大顶堆;
2.此时,整个序列的最大值就是堆顶的根节点,顺序存储中数组下标为0。
3.将其与末尾元素进行交换,此时末尾就为最大值。
4.然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
这样在不断构建大顶堆的过程中,组成大顶堆的元素个数逐渐减少,最后就得到一个有序序列了。
堆排序步骤图解说明:
以原始二叉树顺序存储的数组 [4, 6, 8, 5, 9]为例:
第一步:构造初始堆:将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
从最后一个非叶子结点开始,即节点6,从右至左,从下至上进行调整:
节点9比6大,9和6两个节点交换位置,构成以9为根的大顶堆。以此类推,找到第二个非叶节点 4,4 和 9 交换:
这时,交换导致了子树[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。
此时,实现了将一个无序序列构造成了一个大顶堆。
实际上述的过程可以归纳为从下往上找到最大值不断往上移,在判断非叶子节点是否需要交换的时候,其下面的子树经过前面的调整一定是大顶堆。所以当非叶子节点和它的子节点交换后,导致子树混乱时,只需要将这个较小数,不断往下一层移动就可以了。因为原子树本身是大顶堆,只是变了一个数,只需要这样简单处理就行。
第二步:将堆顶元素与顺序存储数组中末尾的元素进行交换,使末尾元素最大,并暂时将该最大值看成剥离出二叉树:
然后按照上述过程继续调整为大顶堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换:
反复循环上述过程,最终使得整个序列有序:
堆排序代码实现:
package sort;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4, 6, 8, 5, 9};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] array){
//初始i指向最底层的最右边的非叶子节点(最后一个非叶子节点),将完全二叉树构建成一个大顶堆
for (int i = array.length/2-1; i >= 0; i--) {
adjustToBigHeap(array,i,array.length);
}
//将堆顶节点交换到数组尾部,将去掉尾部的数组(二叉树)再次调整为大顶堆,循环进行该过程,直到只剩一个节点
for (int j = array.length-1; j > 0; j--) {
int temp = array[j];
array[j] = array[0];
array[0] = temp;
//只变了堆顶一个元素,将该较小值不断往下层移即可
adjustToBigHeap(array, 0, j);
}
}
/**
* 将二叉树调整为大顶堆
* @param array 待调整二叉树的顺序存储数组
* @param i 非叶子节点下标
* @param length 截取的二叉树的顺序存储数组的长度(堆顶元素交换到数组末尾后不再参与大顶堆的构建了)
*/
public static void adjustToBigHeap(int[] array, int i, int length){
//初始指向该非叶子节点的左子节点
for (int k = i*2+1; k < length; k = k*2+1) {
int nodeData = array[i];
//以该非叶子节点为树根,将该树调整为大顶堆
if (k+1<length && array[k]<array[k+1]){//右子节点存在,且左子节点的值小于右子节点,则k指向较大值
k++;
}
if (array[k]>nodeData){//该非叶子节点的叶子节点值比它大,则交换
array[i] = array[k];
array[k] = nodeData;
//该非叶子节点交换后,可能导致子树混乱,不再为大顶堆。所以需要将该较小值不断往下移,直到满足大顶堆条件。
//所以循环进行,继续访问交换后的较小值的子节点,将子节点的值同这个较小值比较,比它大则较小值继续下移,比它小则已经是大顶堆。
i = k;
}else {//已经满足大顶堆条件则不需要交换,子树也不会混乱,直接退出循环。
break;
}
}
}
}
2.哈夫曼树
哈弗曼树介绍:
1.给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)或赫夫曼树。
2.哈弗曼树是带权路径长度最短的树,权值较大的节点离根较近。
哈弗曼树介绍中的概念说明:
1.路径和路径长度:在一棵树中,从一个节点往下可以达到的孩子或孙子节点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定树根节点的层数为1,则从根节点到第L层结点的路径长度为L-1。
2.节点的权及带权路径长度:若将二叉树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。节点的带权路径长度为:从根结点到该节点之间的路径长度与该节点的权的乘积。
3.树的带权路径长度:树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的节点离根节点越近时的二叉树WPL才最小,才是最优二叉树。
4.WPL最小的就是哈夫曼树。
如上图中中间那颗二叉树才是哈弗曼树。
构建哈弗曼树步骤:
1.将权值序列进行从小到大排序,每个权值对应一个叶子节点;
2.取出权值序列中最小的两个权值作为叶子节点组成一颗子树,它们的父节点为两者的权值和;
3.将上述构成的子树的根节点的权值放入权值序列,并重新排序;
4.重复上述3个步骤,直到权值序列中只有一个权值(整棵树根节点),就得到了一颗哈弗曼树。
图解构建哈弗曼树过程:
例如将权值序列:{13, 7, 8, 3, 29, 6, 1}构建成一颗哈弗曼树。首先将该序列排序为升序:
取出最小的两个权值1和3,作为叶子节点构成一颗子树,它们的父节点权值为二者权值的和4。并将该子树根节点的权值4插入权值序列尾部,重新排序:
重复上述步骤,取出4和6,构成子树,将根节点10插入权值序列尾部,重新排序:
重复上述步骤,取出7和8,构成子树,将根节点15插入权值序列尾部,重新排序:
重复上述步骤,取出10和13,构成子树,将根节点23插入权值序列尾部,重新排序:
重复上述步骤,取出15和23,构成子树,将根节点38插入权值序列尾部,重新排序:
最后,权值序列只剩最后一个值,即为整棵树根节点,哈弗曼树则构造完成。
代码实现:
package tree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 节点类Node,实现Comparable接口,便于排序时直接使用Collections集合工具
*/
class Node implements Comparable<Node>{
public int data;//节点权值
public Node left, right;//左右孩子节点指针
public Node(int data) {
this.data = data;
}
@Override
public int compareTo(Node o) {
return this.data - o.data;
}
}
/**
* 哈夫曼树
*/
public class HuffmanTree {
public static void main(String[] args) {
int array[] = { 13, 7, 8, 3, 29, 6, 1 };
HuffmanTree huffmanTree = new HuffmanTree();
huffmanTree.generateHuffmanTree(array);
System.out.println("先序遍历构建的哈夫曼树:");
huffmanTree.preOrderTraverse();
}
private Node root;
/**
* 构建生成哈夫曼树
* @param array 权值数组
* @return
*/
public void generateHuffmanTree(int[] array){
if (array==null || array.length<2){
throw new RuntimeException("不满足构成哈夫曼树条件");
}
//将权值放到集合中方便取出插入操作和排序
List<Node> nodes = new ArrayList<>();
for (int i = 0; i < array.length; i++) {
nodes.add(new Node(array[i]));
}
//开始构建哈夫曼树,直到结合中只有一个权值
while (nodes.size()>1){
//升序排序
Collections.sort(nodes);
//取出两个最小的权值,构成子树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
Node parentNode = new Node(leftNode.data + rightNode.data);
parentNode.left = leftNode;
parentNode.right = rightNode;
nodes.remove(leftNode);
nodes.remove(rightNode);
//将parentNode插入到权值序列尾部
nodes.add(parentNode);
}
//保存根节点
this.root = nodes.get(0);
}
/**
* 重载preOrderTraverse
*/
public void preOrderTraverse(){
this.preOrderTraverse(this.root);
}
/**
* 先序遍历哈夫曼树
* @param node
*/
public void preOrderTraverse(Node node){
if (node==null){
return;
}
System.out.print(node.data+" ");
preOrderTraverse(node.left);
preOrderTraverse(node.right);
}
}
3.哈夫曼编码
通信领域中信息的处理方式:
1.定长编码:以下图中字符串
i like like like java do you like a java
中40个字符(包括空格)为例,在计算机中按照二进制来传递信息,总的长度是359(包括空格)。
2.变长编码:实际上定长编码数据太长太大了,在通信领域中通常会采用边长编码。变长编码首先统计字符串中每个字符出现的次数,如上例字符串中:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 ' ':9
。然后则可以将这12个字符串从0开始编码,一般原则是出现次数越多的字符编码越小,比如空格出现了9 次, 编码为0,依次类推得到:0=' ' , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
。那么按照上述各个字符规定的编码,在传输i like like like java do you like a java
数据时,编码就是:10010110100…
但是这里我们设计的变长编码方式存在一个问题:a
的编码1
是其他字符含1编码的前缀,这就会导致当我们解码10010110100…这串编码时,遇到编码1
时不能确定是直接翻译成a
字符呢还是这个1
是其他字符编码的一部分。这就造成匹配的多义性。
所以字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。哈夫曼编码就很好的解决了上述问题。
赫夫曼编码介绍:
1.哈夫曼编码(Huffman Coding),是一种编码方式, 属于一种程序算法;
2.哈夫曼编码是哈夫曼树在电讯通信中的经典的应用之一。
3.哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。
4.赫夫曼码是可变字长编码(VLC)的一种。由Huffman于1952年提出的一种编码方法,被称之为最佳编码。
哈夫曼编码步骤:
1.统计字符串中每个字符出现的次数;
2.将各个字符出现的次数作为权值,构建一颗哈弗曼树;
3.根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 ,向右的路径为 1。如哈夫曼树介绍中的示例哈夫曼树编码后如下图所示:
这样示例字符串i like like like java do you like a java
的字符哈夫曼编码结果为:o=1000 u=10010 d= 100110 y=100111 i=101 a=110 k=1110 e=1111 j=0000 v=0001 l=001 ' '=01
。进而整个字符串的哈夫曼编码为:10101001101111011110100110111101111010011011110111101000011000011100110011110000110 01111000100100100110111101111011100100001100001110,通过赫夫曼编码处理后长度为133,远比定长编码的359短,起到了压缩的作用。并且可以校验任意一个字符的编码不是其他字符编码的前缀,这样在解码时就可以不断的匹配,将编码翻译成字符。所以赫夫曼编码也是无损处理方案。
解压和压缩代码实现:
package tree;
import java.util.*;
class HmBiNode implements Comparable<HmBiNode>{
public Byte data;//存放数据(字符)的ASCII码值
public int weight;//权值,即字符出现的次数
public HmBiNode left, right;
public HmBiNode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(HmBiNode o) {
return this.weight-o.weight;
}
@Override
public String toString() {
if (this.data==null){
return "HmBiNode{" +
"data=null" +
", weight=" + weight +
'}';
}
return "HmBiNode{" +
"data=" + (char)data.byteValue() +
", weight=" + weight +
'}';
}
/**
* 先序遍历该节点作为根节点的二叉树
*/
public void preOrderTraverse(){
System.out.print(this+" ");
if (this.left!=null){
this.left.preOrderTraverse();
}
if (this.right!=null){
this.right.preOrderTraverse();
}
}
}
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
System.out.println("待压缩的字符串:"+content);
byte[] bytes = content.getBytes();
System.out.println("哈夫曼编码压缩前原始数据的数据长度及数据:");
System.out.println("长度:"+bytes.length+",数据:"+Arrays.toString(bytes));
HuffmanCode huffmanCode = new HuffmanCode();
byte[] zipBytes = huffmanCode.zip(bytes);
System.out.println("哈夫曼编码压缩后的数据长度及数据:");
System.out.println("长度:"+zipBytes.length+",数据:"+Arrays.toString(zipBytes));
System.out.println("压缩率:"+(float)(bytes.length-zipBytes.length)/bytes.length *100 +"%" );
System.out.println("开始解压.....");
byte[] unzipBytes = huffmanCode.unzip(zipBytes);
System.out.println("解压后的原始数据数据长度及数据:");
System.out.println("长度:"+unzipBytes.length+",数据:"+Arrays.toString(unzipBytes));
System.out.println("解压后的字符串:"+ new String(unzipBytes));
}
//哈夫曼编码表
private Map<Byte, String> huffmanCodesForm = new HashMap<>();
/**
* 对数据进行压缩
* @param bytes
*/
public byte[] zip(byte[] bytes){
//解析需要编码的数据,得到哈夫曼树节点
List<HmBiNode> nodes = this.getNodes(bytes);
System.out.println("统计每个字符出现的次数,得到哈夫曼树的叶子节点集合为:");
System.out.println(nodes);
//构建生成哈夫曼树,返回根节点
HmBiNode root = this.generateHuffmanTree(nodes);
System.out.println("先序遍历构建的哈夫曼树:");
root.preOrderTraverse();
//生成哈夫曼编码表:初始根节点没有父节点,编码不能是0或1,给定空字符串。
this.generateCodesForm(root);
System.out.println("\n生成的哈夫曼编码表为:");
System.out.println(this.huffmanCodesForm);
//实现对原始整个数据的编码
byte[] code = this.code(bytes);
return code;
}
/**
* 对数据进行解压
* @param bytes
* @return
*/
public byte[] unzip(byte[] bytes){
//解压得到原始数据的哈夫曼编码数据
String huffmanCodeData = this.bytesToBitString(bytes);
System.out.println("将压缩数据解压为原数据的哈夫曼编码数据:");
System.out.println(huffmanCodeData);
//对照哈夫曼编码表,将哈夫曼编码数据解码为原来的数据
byte[] decode = this.decode(huffmanCodeData);
return decode;
}
/**
* 统计每个字符出现的次数,得到哈夫曼树的叶子节点
* @param bytes 数据字节数组
* @return
*/
private List<HmBiNode> getNodes(byte[] bytes){
//统计每个字符出现的次数
Map<Byte, Integer> counts = new HashMap<>();//存放字符出现的次数,键:字符ASCII 值:次数
for (byte b : bytes){
Integer count = counts.get(b);
if (count==null){//没有记录过,次数初始为1
counts.put(b,1);
}else {//再次出现,次数加1
counts.put(b, count+1);
}
}
//生成节点
List<HmBiNode> nodes = new ArrayList<>();
for (Map.Entry<Byte, Integer> entry : counts.entrySet()){
nodes.add(new HmBiNode(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 构建生成哈夫曼树
* @param nodes 节点权值集合
* @return 返回哈夫曼树根节点
*/
private HmBiNode generateHuffmanTree(List<HmBiNode> nodes){
if (nodes==null || nodes.size()<2){
throw new RuntimeException("不满足构成哈夫曼树条件");
}
//构建哈夫曼树,直到结合中只有一个权值
while (nodes.size()>1){
//升序排序
Collections.sort(nodes);
//取出两个最小的权值,构成子树
HmBiNode leftNode = nodes.get(0);
HmBiNode rightNode = nodes.get(1);
HmBiNode parentNode = new HmBiNode(null,leftNode.weight + rightNode.weight);
parentNode.left = leftNode;
parentNode.right = rightNode;
nodes.remove(leftNode);
nodes.remove(rightNode);
//将parentNode插入到权值序列尾部
nodes.add(parentNode);
}
//保存根节点
return nodes.get(0);
}
/**
* 通过生成的哈夫曼树,递归遍历找到每个叶子节点;
* 遍历过程中实现对每个数据的编码(左边0,右边1),得到编码表;
* @param node
* @param code
* @param codeStr
*/
private void generateCodesForm(HmBiNode node, String code, StringBuilder codeStr){
//拷贝之前的编码(0、1),并添加上当前的编码。拷贝的原因是避免在同一个引用上操作,不断叠加;
// 每个字符的编码是不同的,应该是单独的字符串对象
StringBuilder stringBuilder = new StringBuilder(codeStr);
stringBuilder.append(code);
//如果当前节点为叶子节点,则结束对该叶子节点的编码,并将该字符的编码结果存入编码表
if (node.data!=null){
huffmanCodesForm.put(node.data, stringBuilder.toString());
return;
}
//非叶子节点不断递归遍历、编码
generateCodesForm(node.left,"0",stringBuilder);
generateCodesForm(node.right,"1",stringBuilder);
}
/**
* 重载generateCodesForm
* @param root
*/
private void generateCodesForm(HmBiNode root){
this.generateCodesForm(root,"",new StringBuilder());
}
/**
* 依托于解析后生成的哈夫曼编码表,实现对原始数据转为编码数据
* 并将编码后的数据不断取8位存储到字节数组byte[]中,实现压缩
* @param bytes
* @return
*/
private byte[] code(byte[] bytes){
StringBuilder stringBuilder = new StringBuilder();
//将原字符转换为对应编码
for (byte b : bytes){
stringBuilder.append(this.huffmanCodesForm.get(b));
}
System.out.println("对原数据哈夫曼编码后:");
System.out.println(stringBuilder.toString());
//将编码后的字符串转为byte(8位)数组
int length = (stringBuilder.length()+7)/8;//每个字节byte 8位,计算可以分为多少个字节来存储
int index = 0;
byte[] huffmanCodeBytes = new byte[length];
for (int i = 0; i < stringBuilder.length(); i += 8) {
//截取符合byte长度(8位)的二进制字符串
String substring = null;
if (i+8>stringBuilder.length()){//字符串不够8位
substring = stringBuilder.substring(i);//从i直接截取到末尾
}else {
substring = stringBuilder.substring(i, i + 8);
}
//将二进制字符串转为整型,再转为byte,byte存储的是该整数的二进制原码的补码
// 如:10101000(2)=168(10)。10101000(原码)的补码为:11011000(2)=-88
huffmanCodeBytes[index++] = (byte) Integer.parseInt(substring,2);
}
return huffmanCodeBytes;
}
/**
* 将压缩的字节数组解压缩为原数据的哈夫曼编码数据
* @param bytes
* @return 返回哈夫曼编码数据
*/
private String bytesToBitString(byte[] bytes){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
if (bytes[i]>=0){//byte为正数时需要补为8位(高位的0不显示)补码
if (i==bytes.length-1){//如果是最后一个则编码时可能就不够8位,不够8位没有符号位,则一定为正数,就不作补位
String str = Integer.toBinaryString(bytes[i]);
stringBuilder.append(str);
continue;
}
//将原码与100000000(2)=256(10)作按位或运算,这样高位有1,不会作为正数省略,然后再取8位
String str = Integer.toBinaryString(bytes[i]|256);
str = str.substring(str.length()-8);
stringBuilder.append(str);
}else {//byte为负数时,由于是转为的int(4个字节32位),所以只取前8位补码
String str = Integer.toBinaryString(bytes[i]);
str = str.substring(str.length()-8);
stringBuilder.append(str);
}
}
return stringBuilder.toString();
}
/**
* 通过解压缩后得到的原数据的哈弗曼编码数据,解码得到原数据
* @param huffmanCodeData
* @return
*/
private byte[] decode(String huffmanCodeData){
//反转原哈夫曼编码表,之前是原数据对应编码,现在反转为编码对应原数据
Map<String, Byte> form = new HashMap<>();
for (Map.Entry<Byte, String> entry : this.huffmanCodesForm.entrySet()){
form.put(entry.getValue(), entry.getKey());
}
List<Byte> data = new ArrayList<>();
//扫描原数据的哈夫曼编码数据,与反转编码表比对,进行解码
for (int i = 0; i < huffmanCodeData.length();) {
String code = "";
//没有与反转编码表比对成功,则增加扫描长度
while (form.get(code)==null){
code += huffmanCodeData.substring(i,i+1);
i++;
}
//比对成功后当前code对应反转编码表中的数据则为原始数据
data.add(form.get(code));
//比对成功后i定位到比对完的位置,i++后则开始对下一个编码的扫描
}
//将集合转换为byte数组
byte[] bytes = new byte[data.size()];
for (int i = 0; i < data.size(); i++) {
bytes[i] = data.get(i);
}
return bytes;
}
}
运行结果:
删掉上面示例代码中的控制台输出,实现真正对文件进行压缩和解压缩:
//在上述代码中添加zipFile和unzipFile两个方法,并修改main方法
public static void main(String[] args) {
HuffmanCode huffmanCode = new HuffmanCode();
//压缩文件到源文件目录
boolean zipResult = huffmanCode.zipFile("F:/myReport.txt");
System.out.println(zipResult?"压缩文件成功!":"压缩文件失败!");
//解压缩文件到源文件目录
boolean unzipResult = huffmanCode.unzipFile("F:/myReport.myzip");
System.out.println(unzipResult?"解压缩文件成功!":"解压缩文件失败!");
}
//哈夫曼编码表
private Map<Byte, String> huffmanCodesForm = new HashMap<>();
/**
* 压缩文件到源文件目录
* @param srcFilePath 待压缩文件的全路径
*/
public boolean zipFile(String srcFilePath){
FileInputStream in = null;
ObjectOutputStream objOut = null;
boolean result = true;
try {
in = new FileInputStream(srcFilePath);
//创建和源文件一样的字节数组
byte[] bytes = new byte[in.available()];
//读取源文件
in.read(bytes);
//压缩源文件字节数组
byte[] zipBytes = this.zip(bytes);
//将压缩后的源文件字节数组写入压缩文件
String suffix = srcFilePath.substring(srcFilePath.indexOf("."));//源文件后缀
String aimFilePath = srcFilePath.replace(suffix,".myzip");
objOut = new ObjectOutputStream(new FileOutputStream(aimFilePath));
objOut.writeObject(zipBytes);
//写入哈夫曼编码表
objOut.writeObject(this.huffmanCodesForm);
//最后写入源文件后缀
objOut.writeObject(suffix);
}catch (Exception e){
result = false;
throw e;
}finally {//关闭字节流对象
try {
in.close();
objOut.flush();
objOut.close();
return result;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
/**
* 解压缩文件到源文件目录
* @param srcFilePath 待解压缩文件的全路径
*/
public boolean unzipFile(String srcFilePath){
ObjectInputStream objIn = null;
FileOutputStream out = null;
boolean result = true;
try {
objIn = new ObjectInputStream(new FileInputStream(srcFilePath));
//读取源文件原数据byte数组
byte[] bytes = (byte[]) objIn.readObject();
//继续读取哈夫曼编码表
this.huffmanCodesForm = (Map<Byte, String>) objIn.readObject();
//读取源文件后缀
String suffix = (String)objIn.readObject();
String aimFilePath = srcFilePath.replace(srcFilePath.substring(srcFilePath.indexOf(".")),"-unzip"+suffix);
out = new FileOutputStream(aimFilePath);
//解压缩字节数组
byte[] unzipBytes = this.unzip(bytes);
//将解压缩后字节数组写出
out.write(unzipBytes);
}catch (Exception e){
System.out.println("解压缩出错:"+e.getMessage());
result = false;
}finally {//关闭字节流对象
try {
objIn.close();
out.flush();
out.close();
return result;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
压缩结果:
解压结果:
4.二叉排序树(BST)
二叉排序树介绍:
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。二叉排序树是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。
查找节点:
从根节点开始,若关键字值等于待查找值,查找成功;否则,小于关键字,向左递归查找;大于关键字,向右递归查找
插入节点:
二叉排序树是一种动态树表。其特点是:树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的结点时再进行插入。新插入的结点一定是一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
删除节点:
在二叉排序树删去一个结点,分三种情况讨论:
1.删除节点为叶子结点,也就是无左右子树,可以直接删除;
2.删除节点只有左子树或右子树,只需要将其父节点直接指向其左子树或者右子树;
3.删除节点左右子树均不为空,不能简单的删除,但是可以根据二叉排序树的规律将待删除节点与其后继节点交换(与其左子树最大值交换或右子树最小值交换)后转换为前两种情况。
总结:第一种情况其实也可以看成只有左或右子树的情况,只是子树为空,所以第一、二种情况可以用相同逻辑处理。而第三种情况,只需要多一步交换操作,然后又可以按照第一、二种情况来处理。
代码实现:
package tree;
public class BinarySortTree {
public static void main(String[] args) {
int[] array = {7, 3, 10, 14, 5, 1, 9, 12, 11, 13, 4, 1};
BinarySortTree binarySortTree = new BinarySortTree();
for (int i = 0; i < array.length; i++) {
System.out.print(binarySortTree.insert(array[i])?array[i]+"插入成功!":array[i]+"插入失败!已经存在!");
}
System.out.println("\n中序遍历结果:");
binarySortTree.inOrderTraverse();
System.out.print("\n删除关键字10:");
System.out.print(binarySortTree.delete(10)!=null?"删除成功!":"删除失败!关键字不存在!");
System.out.println("删除关键字10后的中序遍历结果:");
binarySortTree.inOrderTraverse();
System.out.print("\n删除关键字1:");
System.out.print(binarySortTree.delete(1)!=null?"删除成功!":"删除失败!关键字不存在!");
System.out.println("删除关键字1后的中序遍历结果:");
binarySortTree.inOrderTraverse();
System.out.print("\n删除关键字5:");
System.out.print(binarySortTree.delete(5)!=null?"删除成功!":"删除失败!关键字不存在!");
System.out.println("删除关键字5后的中序遍历结果:");
binarySortTree.inOrderTraverse();
System.out.print("\n再次删除关键字5:");
System.out.print(binarySortTree.delete(5)!=null?"删除成功!":"删除失败!关键字不存在!");
System.out.println("删除关键字5后的中序遍历结果:");
binarySortTree.inOrderTraverse();
}
private Node root;
//指向查找过程中的前驱节点,方便插入和删除操作
private Node preNode;
/**
* 二叉排序树查找关键字
* @param node
* @param key
* @return
*/
public Node search(Node node, int key){
if (node==null || node.data == key){//节点为null或查找成功,则直接返回节点
return node;
}else if (key < node.data){//向左递归查找
this.preNode = node;//保存前驱节点
return search(node.left, key);
}else {//向右递归查找
this.preNode = node;//保存前驱节点
return search(node.right, key);
}
}
/**
* 插入关键字
* @param key
* @return
*/
public boolean insert(int key){
Node searchNode = search(this.root, key);
if (this.preNode==null){//前驱节点为null,则该二叉排序树为空数,插入节点为根节点
this.root = new Node(key);
return true;
}else if (searchNode!=null){//查找成功,不需要做插入操作,插入失败
return false;
}else { //根据关键字和前驱节点值的大小比较,将关键字插入到合适的子节点
Node node = new Node(key);
if (key < this.preNode.data){
this.preNode.left = node;
}
else {
this.preNode.right = node;
}
return true;
}
}
/**
* 删除关键字
* @param key
* @return
*/
public Node delete(int key){
Node delNode = search(this.root, key);
if (delNode == null){//待删除关键字不存在,删除失败
return null;
}else{
//第三种情况:同时存在左右子树
if (delNode.left!=null && delNode.right!=null){
//查找左子树的关键字最大的节点,或右子树的关键字最小的节点。这里查找右子树
this.preNode = delNode;
Node minNode = delNode.right;
while (minNode.left!=null){
this.preNode = minNode;
minNode = minNode.left;
}
//将待删除节点与其右子树最小关键字节点交换
int tempData = delNode.data;
delNode.data = minNode.data;
minNode.data = tempData;
//交换后,将待删除节点的指针指向minNode
delNode = minNode;
}
//经过前一步处理,下面只有前两种情况,只有一个子树或者没有任何子树,而没有子树其实也可以看成是只有一个空子树
Node childNode = null;
//获取其子树根节点
if (delNode.left!=null){
childNode = delNode.left;
}else {
childNode = delNode.right;
}
//待删除节点前驱节点为null,说明删除结点为根节点
if (this.preNode == null){
this.root = childNode;
}else {
//待删除节点是其前驱节点的左子节点
if (this.preNode.left == delNode){
this.preNode.left = childNode;
}else {
this.preNode.right = childNode;
}
}
}
//返回删除的节点
return delNode;
}
/**
* 重载inOrderTraverse
*/
public void inOrderTraverse(){
this.inOrderTraverse(this.root);
}
/**
* 中序遍历二叉排序树
* @param node
*/
private void inOrderTraverse(Node node){
if (node == null){
return;
}
inOrderTraverse(node.left);
System.out.print(node.data+" ");
inOrderTraverse(node.right);
}
}
运行结果:
5.平衡二叉树(AVL树)
二叉排序树存在的问题:
如果给定一组关键字是这样的:{1,2,3,4,5,6},依次插入创建一颗二叉排序树,如下图所示:
容易发现,此时的二叉排序树形似一个单链表了。所以在这种情况下二叉排序的优势全都没了,甚至可能比单链表还慢,因为在遍历时还要判断其兄弟节点。由此提出了平衡二叉树,对二叉排序树进行了优化,解决了上述问题。
平衡二叉树介绍:
平衡二叉树,也叫平衡二叉搜索树(Self-balancing binary search tree),又称为AVL树(有别于AVL算法)。实际上就是遵循以下两个特点的二叉树:
1.它是一棵空树或它的左右两个子树的高度差的绝对值不超过1;
2.左右两个子树都是一棵平衡二叉树。
其实就是在二叉树的基础上,若树中每棵子树都满足其左子树和右子树的深度差都不超过 1,则这棵二叉树就是平衡二叉树。
平衡因子:
平衡二叉树中每个节点都有其各自的平衡因子,表示的就是其左子树深度同右子树深度的差。平衡二叉树中各节点平衡因子的取值只可能是:0、1 和 -1,即绝对值小于1。
如上图所示,其中(a)的两棵二叉树中由于各个节点的平衡因子数的绝对值都不超过 1,所以 (a) 中两棵二叉树都是平衡二叉树;而(b)的两棵二叉树中有节点的平衡因子数的绝对值超过 1,所以都不是平衡二叉树。
节点数据结构:
class AVLNode {
public int data;//节点权值
public int bf;//平衡因子
public AVLNode left, right;//左右孩子节点指针
public AVLNode(int data, int bf) {
this.data = data;
this.bf = bf;
}
}
旋转平衡过程:
当新插入节点导致二叉树失去平衡时,就必须考虑插入元素后恢复二叉树的平衡性质,实际上也总是可以通过对树进行简单的修复来让其重新恢复到平衡,而这样的简单操作称之为旋转,并且有单旋转和双旋转之分。另外,我们需要明白的是,无论是插入还是删除,只有那些从插入或者删除节点到根节点的路径上的结点的平衡才有可能被改变,因为只有这些节点的子树才可能发生变化,所以最终也只需针对这些节点进行平衡修复操作即可。
假设节点X是失衡节点,它必须重新恢复平衡,由于任意节点的孩子节点最多有两个,而且导致失衡的必要条件是X节点的两棵子树高度差大于1(即平衡因子的绝对值大于1),因此一般只有以下4种情况可能导致X节点失去平衡:
① 在结点X的左孩子结点的左子树中插入元素;
② 在结点X的左孩子结点的右子树中插入元素;
③ 在结点X的右孩子结点的左子树中插入元素;
④ 在结点X的右孩子结点的右子树中插入元素。
以上4种情况,其中第①情况和第④情况是对称的,可以通过单旋转来解决,而第②种情况和第③情况是对称的,需要双旋转来解决。
假设距离插入节点最近的失衡节点为 a。则调整的规律可归纳为以下 4 种情况:
单向右旋平衡处理:若由于节点 a 的左子树为根节点的左子树上插入节点,导致节点 a 的平衡因子由 1 增至 2,致使以 a 为根节点的子树失去平衡,则只需进行一次向右的顺时针旋转,如下图这种情况:a的左子节点变为根节点,a变为新根节点的右子节点,a的左子节点的右子树变为a的左子树。
单向左旋平衡处理:如果由于节点 a 的右子树为根节点的右子树上插入节点,导致节点 a 的平衡因子由 -1变为 -2,则以 a 为根节点的子树需要进行一次向左的逆时针旋转,如下图这种情况:a的右子节点变为根节点,a变为新根节点的左子节点,a的右子节点的左子树变为a的右子树。
双向旋转(先左后右)平衡处理:如果由于节点a 的左子树为根节点的右子树上插入节点,导致节点 a 平衡因子由 1 增至 2,致使以 a 为根节点的子树失去平衡,则需要进行两次旋转操作,如下图这种情况:以a的左子节点为根节点先左旋,然后再以a为根节点右旋。
双向旋转(先右后左)平衡处理:如果由于节点 a 的右子树为根节点的左子树上插入节点,导致节点 a 平衡因子由 -1 变为 -2,致使以 a 为根节点的子树失去平衡,则需要进行两次旋转(先右旋后左旋)操作,如下图这种情况:以a的右子节点为根节点先右旋,然后再以a为根节点左旋。
代码实现:
package tree;
class AVLNode {
public int data;//节点权值
public int bf;//平衡因子
public AVLNode left, right;//左右孩子节点指针
public AVLNode(int data, int bf) {
this.data = data;
this.bf = bf;
}
}
public class AVLTree {
public static void main(String[] args) {
int[] array = {1, 23, 45, 34, 98, 9, 4, 35, 23};
// int[] array = {98, 9, 4};//最简单右旋测试
// int[] array = {98, 9, 10};//最简单先左旋,再右旋测试
// int[] array = {8, 9 , 10};//最简单左旋测试
// int[] array = {8, 10 , 9};//最简单先右旋,再左旋测试
AVLTree avlTree = new AVLTree();
for (int i = 0; i < array.length; i++) {
System.out.print(avlTree.insert(array[i])?array[i]+"插入成功!":array[i]+"插入失败,已经存在!");
}
System.out.println("\n先序遍历结果:");
avlTree.preOrderTraverse();
}
private AVLNode root;
//指向访问节点过程中的前驱节点,方便插入和删除操作
private AVLNode preNode;
//标识插入新节点时是否导致回溯过程中的节点高度增加或者是否需要更新回溯的父节点的平衡因子
private boolean isUpdateBF = false;
/**
* 二叉排序树查找关键字
* @param node
* @param key
* @return
*/
public AVLNode search(AVLNode node, int key){
if (node==null || node.data == key){//节点为null或查找成功,则直接返回节点
return node;
}else if (key < node.data){//向左递归查找
this.preNode = node;//保存前驱节点
return search(node.left, key);
}else {//向右递归查找
this.preNode = node;//保存前驱节点
return search(node.right, key);
}
}
/**
* 重载insert
* @param key
* @return
*/
public boolean insert(int key){
return this.insert(this.root, key);
}
/**
* 插入关键字,并在插入过程中旋转最小失衡节点
* 由于插入后需要更新所有相关节点的平衡因子,并做失衡节点的旋转,
* 所以不能像二叉排序树一样利用search函数简单插入,需要自己亲自遍历插入过程中的每个节点
* @param key
* @return
*/
private boolean insert(AVLNode node, int key){
if (node == null){//没有找到关键词。新插入的节点都没子树,所以平衡因子为0
if (this.root == null){//为根节点
this.root = new AVLNode(key, 0);
}else {
if (key < this.preNode.data){//左子节点
this.preNode.left = new AVLNode(key, 0);
}else {//右子节点
this.preNode.right = new AVLNode(key, 0);
}
this.isUpdateBF = true;//插入新节点后需要更新其父节点的平衡因子,标识为true
}
return true;
}else{//继续递归查找
if (key < node.data){//向左递归插入
this.preNode = node;
if (!insert(node.left, key)){//插入失败直接返回
return false;
}
// 判断插入过程是否会导致整棵子树的深度 +1:
if (this.isUpdateBF){
//判断当前节点的平衡因子,如果其平衡因子本身为1(即左子树高度本身更高1),再在其左子树上插入关键字,则失衡了,需要旋转
//如果为0或-1则不会失衡,只需要更新和插入节点相关的一些父节点的平衡因子
switch (node.bf){
case 1:{//原本平衡因子1,左子树插入结点,平衡因子变为2,该节点变为最小失衡节点需要旋转
//继续查看当前节点node的左子节点作为根的子树失衡的原因:
AVLNode lNode = node.left;
//如果bf值为1,则说明添加在node.left的左子树中,LL,需要对其进行单右旋处理;
//反之bf为-1,则说明添加在node.left的右子树中,LR,需要进行双向先左旋后右旋的处理
if (lNode.bf==1){
R_Rotate(node);
//单右旋完后更新平衡因子,如以最简单情况下的{98, 9, 4}为例
node.bf = node.right.bf = 0;
}else if (lNode.bf==-1){
L_Rotate(lNode);//先左旋失衡节点的left节点
R_Rotate(node);//再右旋失衡节点
//双旋完后更新平衡因子,如以最简单情况下的{98, 9, 10}为例
node.bf = node.right.bf = node.left.bf = 0;
}
//做了旋转平衡处理,父节点平衡因子已经最新
this.isUpdateBF = false;
};break;
case 0:{//原本平衡因子0,左子树插入结点,更新平衡因子为1
node.bf = 1;
//该节点平衡因子为1,说明其高度没有发生了变化,其父节点左右子树高度差发生变化,需要更新父节点平衡因子
this.isUpdateBF = true;
};break;
case -1:{//原本平衡因子-1,左子树插入结点,更新平衡因子为0
node.bf = 0;
//该节点平衡因子为0,说明其高度没有发生变化,其父节点左右子树高度差没变化,不需要更新父节点平衡因子
this.isUpdateBF = false;
};break;
}
}
}else if (key > node.data){//向右递归插入
this.preNode = node;
if (!insert(node.right, key)){
return false;
}
// 判断插入过程是否会导致整棵子树的深度 +1:
if (this.isUpdateBF){
//判断当前节点的平衡因子,如果其平衡因子本身为1(即左子树高度本身更高1),再在其左子树上插入关键字,则失衡了,需要旋转
//如果为0或-1则不会失衡,只需要更新和插入节点相关的一些父节点的平衡因子
switch (node.bf){
case 1:{//原本平衡因子1,右子树插入结点,更新平衡因子为0
node.bf = 0;
//该节点平衡因子为0,说明其高度没有发生变化,其父节点左右子树高度差没变化,不需要更新父节点平衡因子
this.isUpdateBF = false;
};break;
case 0:{//原本平衡因子0,右子树插入结点,更新平衡因子为-1
node.bf = -1;
//该节点平衡因子为-1,说明其高度没有发生了变化,其父节点左右子树高度差发生变化,需要更新父节点平衡因子
this.isUpdateBF = true;
};break;
case -1:{//原本平衡因子-1,右子树插入结点,平衡因子变为-2,该节点变为最小失衡节点需要旋转
//继续查看当前节点node的右子节点作为根的子树失衡的原因:
AVLNode rNode = node.right;
//如果bf值为-1,则说明添加在node.right的右子树中,RR,需要对其进行单左旋处理;
//反之bf为1,则说明添加在node.right的左子树中,RL,需要进行双向先右旋后左旋的处理
if (rNode.bf==1){
R_Rotate(rNode);
L_Rotate(node);
//单左旋完后更新平衡因子,如以最简单情况下的{8, 9, 10}为例
node.bf = node.left.bf = 0;
}else if (rNode.bf==-1){
L_Rotate(node);
//单右旋完后更新平衡因子,如以最简单情况下的{8, 10, 9}为例
node.bf = node.left.bf = node.right.bf = 0;
}
//做了旋转平衡处理,父节点平衡因子已经最新
this.isUpdateBF = false;
};break;
}
}
}else {//找到关键字,不需要插入,插入失败
return false;
}
}
return true;
}
/**
* 当前节点为根节点的子树左旋
* @param node
*/
void L_Rotate(AVLNode node){
AVLNode tempNode = new AVLNode(node.data, node.bf);//备份当前根节点关键字
node.data = node.right.data;//将右子节点的关键字交换到当前节点,实现将右子节点变为根节点
tempNode.right = node.right.left;//原根节点的右子节点的左子树现在作为原根节点的右子树
node.right = node.right.right;//原根节点的右子节点的右子树现在作为新根节点的右子树
node.left = tempNode;//原根节点作为新根节点的左子树
}
/**
* 当前节点为根节点的子树右旋
* @param node
*/
void R_Rotate(AVLNode node){
AVLNode tempNode = new AVLNode(node.data, node.bf);//备份当前根节点关键字
node.data = node.left.data;//将左子节点的关键字交换到当前节点,实现将左子节点变为根节点
tempNode.left = node.left.right;//原根节点的左子节点的右子树现在作为原根节点的左子树
node.left = node.left.left;//原根节点的左子节点的左子树现在作为新根节点的子树
node.right = tempNode;//原根节点作为新根节点的右子树
}
/**
* 重载inOrderTraverse
*/
public void inOrderTraverse(){
this.inOrderTraverse(this.root);
}
/**
* 中序遍历二叉排序树
* @param node
*/
private void inOrderTraverse(AVLNode node){
if (node == null){
return;
}
inOrderTraverse(node.left);
System.out.print(node.data+" ");
inOrderTraverse(node.right);
}
/**
* 重载preOrderTraverse
*/
public void preOrderTraverse(){
this.preOrderTraverse(this.root);
}
/**
* 先序遍历二叉排序树
* @param node
*/
private void preOrderTraverse(AVLNode node){
if (node == null){
return;
}
System.out.print(node.data+" ");
preOrderTraverse(node.left);
preOrderTraverse(node.right);
}
}