目录
1.堆排序
2.哈夫曼树
3.哈夫曼编码
4.二叉排序树
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.二叉排序树
二叉排序树介绍:
二叉排序树(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);
}
}
运行结果: