堆排序

堆排序基本介绍

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

    我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:

    大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号
  5. 小顶堆举例说明

    小顶堆特点:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
  6. 一般升序采用大顶堆,降序采用小顶堆

堆排序基本思想

堆排序的基本思想是:

  1. 将待排序序列构造成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点。
  3. 将其与末尾元素进行交换,此时末尾就为最大值。
  4. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
    可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

堆排序步骤图解说明

要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。

堆排序代码实现

要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。

代码

package tree;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class HeapSort {public static void main(String[] args) {//        int[] arr = {4, 6, 8, 5, 9};
//        heapSort(arr);
//        System.out.println(Arrays.toString(arr));int[] arr = new int[8000000];for (int i = 0; i < 8000000; i++) {arr[i] = (int) (Math.random() * 80000);// 生成一个0-80000的数据}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间为:" + date1Str);heapSort(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间为:" + date2Str);}// 编写一个堆排序的方法public static void heapSort(int[] arr) {int temp = 0;// 要求将数组按升序排序// 分步完成
//        adjustHeap(arr,1,arr.length);
//        System.out.println("第一次"+ Arrays.toString(arr));
//        adjustHeap(arr,0,arr.length);
//        System.out.println("第二次"+ Arrays.toString(arr));// 完成最终代码// ① 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或者小顶堆for (int i = arr.length / 2 - 1; i >= 0; i--) {adjustHeap(arr, i, arr.length);}// ② 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端// ③ 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序for (int i = arr.length - 1; i > 0; i--) {// 交换temp = arr[i];arr[i] = arr[0];arr[0] = temp;adjustHeap(arr, 0, i);}}/*** 将一个数组(二叉树),调整成一个大顶堆* 功能:完成 将以i对应的非叶子节点的数调整成大顶堆* 举例:int[] arr = {4, 6, 8, 5, 9} => i = 1 => adjustHeap => 得到 arr[] = {4, 9, 8, 5, 6}* 举例:如果我们再次调用adjustHeap => i = 0 => adjustHeap => 得到 arr[] = {9, 6, 8, 5, 4}** @param arr    待调整数组* @param i      表示非叶子节点的在数组中的索引* @param length 表示对多少个元素进行调整,length在逐渐减少*/public static void adjustHeap(int[] arr, int i, int length) {int temp = arr[i];  // 先取出当前元素的值,保存在临时变量// 开始调整// 说明// 1. j = i * 2 + 1 j是i结点的左子节点for (int j = i * 2 + 1; j < length; j = j * 2 + 1) {if (j + 1 < length && arr[j] < arr[j + 1]) {  // 说明左子节点比右子节点小j++;    // j指向右子节点}if (arr[j] > temp) {  // 如果子节点大于父节点arr[i] = arr[j];    // 把较大的值赋给当前结点i = j;  // ★★★★★★ i指向j 继续循环比较} else {break;}}// 当for循环结束后,我们已经将以i为父节点的树的最大值,放在了最顶上(局部大顶堆)arr[i] = temp;//将temp值放到调整后的位置}
}

赫夫曼树

基本介绍

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

赫夫曼树几个重要概念和举例说明

  1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
  2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  3. 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  4. WPL最小的就是赫夫曼树

赫夫曼树创建思路图解

给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.
思路分析(示意图):

构成赫夫曼树的步骤:

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

代码

package huffmantree;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;public class HuffmanTree {public static void main(String[] args) {int[] a = {13, 7, 8, 3, 29, 6, 1};Arrays.sort(a);Node root = createHuffmanTree(a);// 测试preOrder(root);}// 编写一个前序遍历的方法public static void preOrder(Node root) {if (root != null) {root.preOrder();} else {System.out.println("该树为空树");}}/*** 创建赫夫曼树** @param arr 需要创建为赫夫曼树的数组* @return 创建好的赫夫曼树头结点*/public static Node createHuffmanTree(int[] arr) {// 第一步:为了操作方便// 1. 遍历arr数组// 2. 将arr每个元素构建成一个Node// 3. 将Node放入ArrayList中List<Node> nodes = new ArrayList<Node>();for (int value : arr) {nodes.add(new Node(value));}// 我们处理的过程是循环的过程while (nodes.size() > 1) {// 排序:从小到大Collections.sort(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);// 5)将parent加入到nodesnodes.add(parent);}// 返回赫夫曼树的头结点return nodes.get(0);}
}// 创建节点类
// 为了让Node对象支持排序Collections集合排序
// 让Node实现Comparable接口
class Node implements Comparable<Node> {int value; // 结点权值Node left;  // 指向左子节点Node right; // 指向右子节点public Node(int value) {this.value = value;}@Overridepublic String toString() {return "Node{" +"value=" + value +'}';}@Overridepublic int compareTo(Node o) {// 表示从小到大排序return this.value - o.value;// 表示从大到小排序// return -(this.value - o.value);}// 前序遍历public void preOrder() {System.out.print(this.value + " ");if (this.left != null) {this.left.preOrder();}if (this.right != null) {this.right.preOrder();}}
}

赫夫曼编码

基本介绍

  1. 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
  2. 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
  3. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
  4. 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

原理剖析

  1. 通信领域中信息的处理方式1-定长编码
    i like like like java do you like a java // 共40个字符(包括空格)

    105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应Ascii码

    01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制

    按照二进制来传递信息,总的长度是 359 (包括空格)

  2. 通信领域中信息的处理方式2-变长编码

    i like like like java do you like a java // 共40个字符(包括空格)

    d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
    0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
    说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.

    按照上面给各个字符规定的编码,则我们在传输 “i like like like java do you like a java” 数据时,编码就是 10010110100…
    字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码

  3. 通信领域中信息的处理方式3-赫夫曼编码

    i like like like java do you like a java // 共40个字符(包括空格)

    d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
    按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)

赫夫曼编码实现

分析


图解

赫夫曼编码是无损处理方案

注意

这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

赫夫曼编码实践

最佳实践-数据压缩(创建赫夫曼树)

将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,形式如 "1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
"

  1. 步骤1:根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树.
  2. 思路:前面已经分析过了,而且我们已然讲过了构建赫夫曼树的具体实现。

代码实现(创建赫夫曼树)

// 通过List创建对应赫夫曼树
/**
* 通过List创建对应赫夫曼树
* @param nodes 需要创建赫夫曼树的所有节点组成的List
* @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);// 将新的二叉树加入到nodesnodes.add(parent);}// nodes最后剩下的结点就是哈夫曼树的根节点return nodes.get(0);
}

最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)

我们已经生成了 赫夫曼树, 下面我们继续完成任务

  1. 生成赫夫曼树对应的赫夫曼编码 , 如下表: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
  2. 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下.1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
  3. 思路:前面已经分析过了,而且我们讲过了生成赫夫曼编码的具体实现。

代码实现(生成赫夫曼编码和赫夫曼编码后的数据)

// 生成的赫夫曼树对应的赫夫曼编码
// 思路:
// 1. 将赫夫曼编码表存放到一个Map<Byte,String>中
// 2. 在生成赫夫曼编码表时,需要拼接路径,所以创建一个StringBuilder,存储某个叶子节点的路径
static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
static StringBuilder stringBuilder = new StringBuilder();/*** 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes中** @param node          传入结点,默认根节点* @param code          路径:左子节点为0;右子节点为1* @param stringBuilder 用于拼接路径*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);// 将传入的code加入到stringBuilder1stringBuilder1.append(code);if (node != null) { // 如果node==null 不处理// 判断当前node是叶子节点还是非叶子节点if (node.data == null) {// 非叶子节点// 递归处理// 向左递归getCodes(node.left, "0", stringBuilder1);// 向右递归getCodes(node.right, "1", stringBuilder1);} else {  // 叶子节点// 就表示找到某个叶子节点huffmanCodes.put(node.data, stringBuilder1.toString());}}
}/**
* 编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]
*
* @param bytes        这是原始的字符串对应的byte[]
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的byte[]
* 举例:接收"i like like like java do you like a java"对应的byte[]数组 => byte[] contentBytes = str.getBytes()
* 返回的是1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
* 该字符串对应的字节数组byte[] huffmanCodeBytes,即8位对应一个byte,放入huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(补码) => byte[推导 10101000(补码) => 10101000 - 1 => 10100111(反码) => 11011000(原码) = -88]
* 即使 huffmanCodeByte[1] = -88
*/
public 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));}// 将字符串转成byte[] 数组// 统计返回的huffmanCodes长度// 一句话 int len = (stringBuilder.length() + 7) / 8int len;if (stringBuilder.length() % 8 == 0) {len = stringBuilder.length() / 8;} else {len = stringBuilder.length() / 8 + 1;}// 创建存储压缩后的byte数组byte[] huffmanCondeBytes = new byte[len];int index = 0;  // 记录第几个bytefor (int i = 0; i < stringBuilder.length(); i += 8) { // 因为每八位对应一个byte,所以步长+8String strByte;if (i + 8 > stringBuilder.length()) { // 不够八位strByte = stringBuilder.substring(i);} else {strByte = stringBuilder.substring(i, i + 8);}// 将strByte转成数组放到huffmanCondeBytes[index] = (byte) Integer.parseInt(strByte, 2);index++;}return huffmanCondeBytes;
}

最佳实践-数据解压(使用赫夫曼编码解码)

使用赫夫曼编码来解码数据,具体要求是

  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"
  3. 思路:解码过程,就是编码的一个逆向操作。

代码实现(使用赫夫曼编码解压)

/**
* 将一个byte转成一个二进制字符串
*
* @param b    接收的byte
* @param flag 标识是否需要补高位,如果是true,表示需要补高位,如果是false不补
*             如果是最后一个字节,无须补高位
* @return b对应的二进制字符串(注意是按照补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {// 使用变量保存bint temp = b;// 如果是正数,需要补高位if (flag) {temp |= 256; // 按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001}String str = Integer.toBinaryString(temp); // 返回的是temp对应的二进制的补码if (flag) {str = str.substring(str.length() - 8);}return str;
}/*** 编写一个方法,完成对压缩数据的解码** @param huffmanCodes 赫夫曼编码表Map* @param huffmanBytes 赫夫曼编码得到的字节数组* @return 原来的字符串对应的数组*/
public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {// 1. 先得到huffmanBytes对应的二进制的字符串,形式1010100010111...StringBuilder stringBuilder = new StringBuilder();// 将byte数组转成二进制字符串for (int i = 0; i < huffmanBytes.length; i++) {// 判断是不是最后一个字节boolean flag = (i == huffmanBytes.length - 1);stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));}// System.out.println(stringBuilder.toString());// 把字符串按照指定的赫夫曼编码进行解码// 把赫夫曼编码表反向调换 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) {// 递增取出一个'1' 或者 '0'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;
}

最佳实践-文件压缩

我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压,

  1. 具体要求:给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
  2. 思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩

代码实现(文件压缩)

/*** 将一个文件进行压缩* @param srcFile 传入的希望压缩的文件的全路径* @param dstFile 压缩后将压缩文件放到哪个目录*/
public static void zipFile(String srcFile,String dstFile){// 创建一个文件输入流FileInputStream is = null;// 创建输出流OutputStream os = null;// 创建对象输出流ObjectOutputStream oos = null;try {is = new FileInputStream(srcFile);// 创建一个和源文件大小一样的byte数组byte[] b = new byte[is.available()];// 读取文件is.read(b);// 直接对源文件亚索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());}}}

最佳实践-文件解压(文件恢复)

  1. 具体要求:将前面压缩的文件,重新恢复成原来的文件。
  2. 思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
/*** 编写一个方法,完成对压缩文件的解压* @param zipFile 准备解压的文件* @param dstFile 将文件解压到哪个路径*/
public static void unZip(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 {is.close();ois.close();os.close();}catch (Exception e){System.out.println(e.getMessage());}}
}

赫夫曼编码压缩文件注意事项

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

赫夫曼编码代码汇总

package huffmancode;import java.io.*;
import java.util.*;
import java.util.zip.ZipFile;public class HuffmanCode {public static void main(String[] args) {/* String str = "i like like like java do you like a java";byte[] contentBytes = str.getBytes();byte[] huffmanCodesBytes = huffmanZip(contentBytes);System.out.println(Arrays.toString(huffmanCodesBytes));// 解码byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);System.out.println(new String(sourceBytes));*//*   分布过程List<Node> nodes = getNodes(contentBytes);// System.out.println(nodes);// 测试创建的二叉树// System.out.println("赫夫曼树:");Node huffmanTreeRoot = createHuffmanTree(nodes);// preOrder(huffmanTreeRoot);// 测试是否生成对应哈夫曼编码Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);System.out.println(Arrays.toString(huffmanCodeBytes));*/// 测试压缩文件
//        String srcFile = "d://src.pdf";
//        String desFile = "d://dst.zip";
//        zipFile(srcFile,desFile);
//        System.out.println("压缩文件成功");// 测试解压文件String zipFile = "d://dst.zip";String dstFile = "d://src2.pdf";unZip(zipFile,dstFile);System.out.println("解压成功");}/*** 将一个文件进行压缩* @param srcFile 传入的希望压缩的文件的全路径* @param dstFile 压缩后将压缩文件放到哪个目录*/public static void zipFile(String srcFile,String dstFile){// 创建一个文件输入流FileInputStream is = null;// 创建输出流OutputStream os = null;// 创建对象输出流ObjectOutputStream oos = null;try {is = new FileInputStream(srcFile);// 创建一个和源文件大小一样的byte数组byte[] b = new byte[is.available()];// 读取文件is.read(b);// 直接对源文件亚索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());}}}/*** 编写一个方法,完成对压缩文件的解压* @param zipFile 准备解压的文件* @param dstFile 将文件解压到哪个路径*/public static void unZip(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 {is.close();ois.close();os.close();}catch (Exception e){System.out.println(e.getMessage());}}}// 完成数据的解压// 思路:// 1. 将huffmanCodeBytes[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]转成赫夫曼编码对应的二进制字符串//    重写先转成赫夫曼编码对应的二进制的字符串"1010100010111..."// 2. 将赫夫曼编码对应的二进制字符串转成原始字符串/*** 编写一个方法,完成对压缩数据的解码** @param huffmanCodes 赫夫曼编码表Map* @param huffmanBytes 赫夫曼编码得到的字节数组* @return 原来的字符串对应的数组*/public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {// 1. 先得到huffmanBytes对应的二进制的字符串,形式1010100010111...StringBuilder stringBuilder = new StringBuilder();// 将byte数组转成二进制字符串for (int i = 0; i < huffmanBytes.length; i++) {// 判断是不是最后一个字节boolean flag = (i == huffmanBytes.length - 1);stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));}// System.out.println(stringBuilder.toString());// 把字符串按照指定的赫夫曼编码进行解码// 把赫夫曼编码表反向调换 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) {// 递增取出一个'1' 或者 '0'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转成一个二进制字符串** @param b    接收的byte* @param flag 标识是否需要补高位,如果是true,表示需要补高位,如果是false不补*             如果是最后一个字节,无须补高位* @return b对应的二进制字符串(注意是按照补码返回)*/private static String byteToBitString(boolean flag, byte b) {// 使用变量保存bint temp = b;// 如果是正数,需要补高位if (flag) {temp |= 256; // 按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001}String str = Integer.toBinaryString(temp); // 返回的是temp对应的二进制的补码if (flag) {str = str.substring(str.length() - 8);}return str;}/*** 使用一个方法,将前面的方法封装起来,便于调用** @param bytes 原始的字符串对应的字节数组* @return 返回的是经过赫夫曼编码处理后(压缩后)的数组*/private static byte[] huffmanZip(byte[] bytes) {List<Node> nodes = getNodes(bytes);// 根据nodes创建赫夫曼树Node huffmanTreeRoot = createHuffmanTree(nodes);// 根据赫夫曼树创建对应的赫夫曼编码Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);// 根据赫夫曼编码对原始赫夫曼编码亚索byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);return huffmanCodeBytes;}/*** 编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]** @param bytes        这是原始的字符串对应的byte[]* @param huffmanCodes 生成的赫夫曼编码map* @return 返回赫夫曼编码处理后的byte[]* 举例:接收"i like like like java do you like a java"对应的byte[]数组 => byte[] contentBytes = str.getBytes()* 返回的是1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110* 该字符串对应的字节数组byte[] huffmanCodeBytes,即8位对应一个byte,放入huffmanCodeBytes* huffmanCodeBytes[0] = 10101000(补码) => byte[推导 10101000(补码) => 10101000 - 1 => 10100111(反码) => 11011000(原码) = -88]* 即使 huffmanCodeByte[1] = -88*/public 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));}// 将字符串转成byte[] 数组// 统计返回的huffmanCodes长度// 一句话 int len = (stringBuilder.length() + 7) / 8int len;if (stringBuilder.length() % 8 == 0) {len = stringBuilder.length() / 8;} else {len = stringBuilder.length() / 8 + 1;}// 创建存储压缩后的byte数组byte[] huffmanCondeBytes = new byte[len];int index = 0;  // 记录第几个bytefor (int i = 0; i < stringBuilder.length(); i += 8) { // 因为每八位对应一个byte,所以步长+8String strByte;if (i + 8 > stringBuilder.length()) { // 不够八位strByte = stringBuilder.substring(i);} else {strByte = stringBuilder.substring(i, i + 8);}// 将strByte转成数组放到huffmanCondeBytes[index] = (byte) Integer.parseInt(strByte, 2);index++;}return huffmanCondeBytes;}// 为了调用方便,我们重载getNodesprivate static Map<Byte, String> getCodes(Node root) {if (root == null) {return null;} else {// 处理root左子树getCodes(root.left, "0", stringBuilder);getCodes(root.right, "1", stringBuilder);}return huffmanCodes;}/*** @param bytes 接收一个字节数组* @return 返回一个List 形式[Node{date=97,weight=5},Node{date=32,weight=9}...]*/private static List<Node> getNodes(byte[] bytes) {// 1. 创建一个ArrayListArrayList<Node> nodes = new ArrayList<>();// 2. 遍历bytes,统计每个byte出现的次数 => mapMap<Byte, Integer> counts = new HashMap<>();for (byte b : bytes) {// Map仍然没有该字符数据counts.merge(b, 1, Integer::sum);}// 把每一个键值对转换为Node对象,加入nodes集合for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {nodes.add(new Node(entry.getKey(), entry.getValue()));}return nodes;}/*** 通过List创建对应赫夫曼树* @param nodes 需要创建赫夫曼树的所有节点组成的List* @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);// 将新的二叉树加入到nodesnodes.add(parent);}// nodes最后剩下的结点就是哈夫曼树的根节点return nodes.get(0);}// 生成的赫夫曼树对应的赫夫曼编码// 思路:// 1. 将赫夫曼编码表存放到一个Map<Byte,String>中// 2. 在生成赫夫曼编码表时,需要拼接路径,所以创建一个StringBuilder,存储某个叶子节点的路径static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();static StringBuilder stringBuilder = new StringBuilder();/*** 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes中** @param node          传入结点,默认根节点* @param code          路径:左子节点为0;右子节点为1* @param stringBuilder 用于拼接路径*/private static void getCodes(Node node, String code, StringBuilder stringBuilder) {StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);// 将传入的code加入到stringBuilder1stringBuilder1.append(code);if (node != null) { // 如果node==null 不处理// 判断当前node是叶子节点还是非叶子节点if (node.data == null) {// 非叶子节点// 递归处理// 向左递归getCodes(node.left, "0", stringBuilder1);// 向右递归getCodes(node.right, "1", stringBuilder1);} else {  // 叶子节点// 就表示找到某个叶子节点huffmanCodes.put(node.data, stringBuilder1.toString());}}}// 前序遍历方法private static void preOrder(Node root) {if (root != null) {root.preOrder();} else {System.out.println("该树为空!");}}
}class Node implements Comparable<Node> {Byte data;  // 存放数据(字符)本身,比如'a' => 97, ' ' => 32int weight; // 权值,表示字符出现次数Node left;Node right;public Node(Byte data, int weight) {this.data = data;this.weight = weight;}@Overridepublic String toString() {return "Node{" +"data=" + data +", weight=" + weight +'}';}@Overridepublic int compareTo(Node o) {// 从小到大排序return this.weight - o.weight;}// 前序遍历public void preOrder() {System.out.print(this.data + ":" + this.weight + " ");if (this.left != null) {this.left.preOrder();}if (this.right != null) {this.right.preOrder();}}
}

二叉排序树

先看一个需求

给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加。

解决方案分析

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

二叉排序树介绍

二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:

二叉排序树创建和遍历

一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :

代码

package binarysourtree;public class BinarySortTreeDemo {public static void main(String[] args) {int[] arr = {7, 3, 10, 12, 5, 1, 9};BinarySortTree binarySortTree = new BinarySortTree();for(int a:arr){binarySortTree.add(new Node(a));}binarySortTree.infixOrder();}
}// 创建二叉排序树
class BinarySortTree {private Node root;// 添加节点的方法public void add(Node node) {if (root == null) { // 如果root为空,直接把node加上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) {// 如果当前结点左子节点为nullif (this.left == null) {this.left = node;} else {// 递归向左子树添加this.left.add(node);}} else { // 添加的节点的值大于等于当前结点的值// 如果当前结点右子节点为nullif (this.right == null) {this.right = node;} else {// 递归向右子树添加this.right.add(node);}}}@Overridepublic 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();}}
}

二叉排序树的删除

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

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

思路分析

代码实现

package binarysourtree;public class BinarySortTreeDemo {public static void main(String[] args) {int[] arr = {7, 3, 10, 12, 5, 1, 9};BinarySortTree binarySortTree = new BinarySortTree();for (int a : arr) {binarySortTree.add(new Node(a));}System.out.println("中序遍历二叉树");binarySortTree.infixOrder();// 测试删除叶子节点
//        binarySortTree.delNode(2);
//        System.out.println("删除节点后···");
//        binarySortTree.infixOrder();// 测试删除只有一个子树的结点
//        binarySortTree.delNode(1);
//        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. 返回/*** 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;    // root置空return;}// 2.去找到targetNode的父节点Node parentNode = searchParent(value);if (targetNode.left == null && targetNode.right == null) {// Ⅰ. 如果删除的结点是叶子节点(没有子树)// 判断targetNode是父节点的左子节点还是右子节点if (parentNode.left != null && parentNode.left.value == value) { // 是左子节点parentNode.left = null;} else if (parentNode.right != null && parentNode.right.value == value) { // 是右子节点parentNode.right = null;}} else if (targetNode.left != null && targetNode.right != null) {// Ⅱ. 删除有两棵子树的结点int minVal = delRightTreeMin(targetNode.right);targetNode.value = minVal;// 该方法是从右子树中找最小的结点// 也可以从左子树中找最大的结点} else {// Ⅲ. 删除只有一棵子树的结点if (targetNode.left != null) {// 如果要删除的结点有左子节点// 如果targetNode是parentNode的左子节点if (parentNode.left.value == value) {parentNode.left = targetNode.left;} else {// 如果targetNode是parentNode的右子节点parentNode.right = targetNode.left;}} else {// 如果要删除的结点有右子节点if (parentNode.left.value == value) {parentNode.left = targetNode.right;} else {parentNode.right = targetNode.right;}}}}}// 添加节点的方法public void add(Node node) {if (root == null) { // 如果root为空,直接把node加上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;/*** 查找要删除结点的父节点** @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;    // 没有找到父节点}}}/*** 查找要删除的结点** @param value 希望删除的结点的值* @return 如果找到返回该节点,否则返回null*/public Node search(int value) {if (this.value == value) {// 找到就是该节点return this;} else if (value < this.value) {if (this.left != null) {return this.left.search(value);} else {return null;}} else {if (this.right == null) {return null;} else {return this.right.search(value);}}}public Node(int value) {this.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 { // 添加的节点的值大于等于当前结点的值// 如果当前结点右子节点为nullif (this.right == null) {this.right = node;} else {// 递归向右子树添加this.right.add(node);}}}@Overridepublic 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();}}}

平衡二叉树

看一个案例(说明二叉排序树可能的问题)

给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.

上图BST 存在的问题分析:

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

基本介绍

  1. 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
  2. 具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
  3. 举例说明, 看看下面哪些AVL树, 为什么?

应用案例-单旋转(左旋转)

要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}

思路分析(示意图)

代码实现

// 结点左旋转
private void leftRotate() {// 1. 创建一个新的结点newNode,值等于当前根结点的值Node newNode = new Node(value);// 2. 把新结点的左子树设置成当前结点的左子树newNode.left = left;// 3. 把新结点的右子树设置成当前结点的右子树的左子树newNode.right = right.left;// 4. 把当前结点的值换成右子结点的值value = right.value;// 5. 把当前结点的右子树设置成当前结点右子树的右子树right = right.right;// 6. 把当前结点的左子树设置成最开始创建的新结点left = newNode;
}

应用案例-单旋转(右旋转)

要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}

思路分析(示意图)

代码实现

// 结点右旋转
private void rightRotate() {// 1. 创建一个新的结点newNode,值等于当前根结点的值Node newNode = new Node(value);// 2. 把新结点的右子树设置成当前结点的右子树newNode.right = right;// 3. 把新结点的左子树设置成当前结点的左子树的右子树newNode.left = left.right;// 4. 把当前结点的值换成左子结点的值value = left.value;// 5. 把当前结点的左子树设置成当前结点左子树的左子树left = left.left;// 6. 把当前结点的右子树设置成最开始创建的新结点right = newNode;
}

应用案例-双旋转

前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列

  1. int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL树.
  2. int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL树

双旋转分析

平衡二叉树完整代码

双旋转主要体现在每次添加节点的时候,根据左右子树判断如何旋转,在调整前,需要根据左右子树的左右子树高度情况判断是否需要对子树的子树进行旋转。即结点对象Node的add方法中后两个if语句中的if判断语句。

package avl;import java.util.Map;public class AVLTreeDemo {public static void main(String[] args) {//        int[] arr = {4, 3, 6, 5, 7, 8};
//        int[] arr = {10, 12, 8, 9, 7, 6};int[] arr = {10, 11, 7, 6, 8, 9};// 创建一个AVLTree对象AVLTree avlTree = new AVLTree();// 添加结点for (int a : arr) {avlTree.add(new Node(a));}// 遍历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. 返回/*** 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;    // root置空return;}// 2.去找到targetNode的父结点Node parentNode = searchParent(value);if (targetNode.left == null && targetNode.right == null) {// Ⅰ. 如果删除的结点是叶子结点(没有子树)// 判断targetNode是父结点的左子结点还是右子结点if (parentNode.left != null && parentNode.left.value == value) { // 是左子结点parentNode.left = null;} else if (parentNode.right != null && parentNode.right.value == value) { // 是右子结点parentNode.right = null;}} else if (targetNode.left != null && targetNode.right != null) {// Ⅱ. 删除有两棵子树的结点int minVal = delRightTreeMin(targetNode.right);targetNode.value = minVal;// 该方法是从右子树中找最小的结点// 也可以从左子树中找最大的结点} else {// Ⅲ. 删除只有一棵子树的结点if (targetNode.left != null) {// 如果要删除的结点有左子结点if (parentNode != null) {// 如果targetNode是parentNode的左子结点if (parentNode.left.value == value) {parentNode.left = targetNode.left;} else {// 如果targetNode是parentNode的右子结点parentNode.right = targetNode.left;}} else {root = targetNode.left;}} else {// 如果要删除的结点有右子结点if (parentNode != null) {if (parentNode.left.value == value) {parentNode.left = targetNode.right;} else {parentNode.right = targetNode.right;}} else {root = targetNode.right;}}}}}// 添加结点的方法public void add(Node node) {if (root == null) { // 如果root为空,直接把node加上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;}// 结点左旋转private void leftRotate() {// 1. 创建一个新的结点newNode,值等于当前根结点的值Node newNode = new Node(value);// 2. 把新结点的左子树设置成当前结点的左子树newNode.left = left;// 3. 把新结点的右子树设置成当前结点的右子树的左子树newNode.right = right.left;// 4. 把当前结点的值换成右子结点的值value = right.value;// 5. 把当前结点的右子树设置成当前结点右子树的右子树right = right.right;// 6. 把当前结点的左子树设置成最开始创建的新结点left = newNode;}// 结点右旋转private void rightRotate() {// 1. 创建一个新的结点newNode,值等于当前根结点的值Node newNode = new Node(value);// 2. 把新结点的右子树设置成当前结点的右子树newNode.right = right;// 3. 把新结点的左子树设置成当前结点的左子树的右子树newNode.left = left.right;// 4. 把当前结点的值换成左子结点的值value = left.value;// 5. 把当前结点的左子树设置成当前结点左子树的左子树left = left.left;// 6. 把当前结点的右子树设置成最开始创建的新结点right = newNode;}// 返回以该结点为根结点的树的高度public int height() {return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;}// 返回左子树的高度public int leftHeight() {return left == null ? 0 : left.height();}// 返回右子树的高度public int rightHeight() {return right == null ? 0 : right.height();}/*** 查找要删除结点的父结点** @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;    // 没有找到父结点}}}/*** 查找要删除的结点** @param value 希望删除的结点的值* @return 如果找到返回该结点,否则返回null*/public Node search(int value) {if (this.value == value) {// 找到就是该结点return this;} else if (value < this.value) {if (this.left != null) {return this.left.search(value);} else {return null;}} else {if (this.right == null) {return null;} else {return this.right.search(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 { // 添加的结点的值大于等于当前结点的值// 如果当前结点右子结点为nullif (this.right == null) {this.right = node;} else {// 递归向右子树添加this.right.add(node);}}// 当添加完一个结点后,(如果右子树高度-左子树高度) > 1if (rightHeight() - leftHeight() > 1) {// 如果它的右子树的左子树高度大于它的右子树的右子树高度if (right != null && right.leftHeight() > right.rightHeight()) {// 先对当前结点右节点(右子树)=> 右旋转right.rightRotate();// 再对当前结点进行左旋转leftRotate();}else{// 直接进行左旋转即可leftRotate();}return; // 必须结束当前判断}// 当添加完一个结点后,(如果左子树高度-右子树高度) > 1if (leftHeight() - rightHeight() > 1) {// 如果它的左子树的右子树高度大于它的左子树的左子树高度if (left != null && left.rightHeight() > left.leftHeight()) {// 先对当前结点左节点(左子树)=> 左旋转left.leftRotate();// 再对当前结点进行右旋转rightRotate();}else{// 直接进行右旋转即可rightRotate();}}}@Overridepublic 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();}}}

Java编程:树(实际应用)相关推荐

  1. Java 编程技巧之数据结构

    Photo @markusspiske 文 | 常意 导读 唐宋八大家之一欧阳修在<卖油翁>中写道: 翁取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿.因曰:"我 ...

  2. html 注释_java基础- Java编程规范与注释

    一 前言 java编程规约是指在java代码编写过程中通俗的约定:通常我们是要遵守这些规范:这好比我们在生活中要遵守的道德准则,如果你做的事情超出了道德的底线,那就有可能会受到社会抨击:在java编程 ...

  3. Java 编程的动态性 第1 部分: 类和类装入--转载

    原文地址:http://www.ibm.com/developerworks/cn/java/j-dyn0429/ 本文是这个新系列文章的第一篇,该系列文章将讨论我称之为 Java 编程的动态性的一系 ...

  4. 对象导论---JAVA编程思想

    第一章 对象导论 序言 "我们之所以将自然界分解,组织成各种概念,并按其含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来-除非赞成这个协定中规定 ...

  5. 《Java编程艺术》目录

    目    录 第1部分  新手上路篇 第1章  初识Java    2 1.1  什么是Java    2 1.2  Java能做什么    2 1.3  Java软件包    3 1.3.1  什么 ...

  6. 阿里巴巴工程师:Java 编程技巧之数据结构

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"1024"获取公众号专属1024GB资料 来源:阿里巴巴中间件 导读 编码过程中踩过的 ...

  7. java编程学习方法_在线学习Java编程的最佳方法

    java编程学习方法 1.简介 Java是使用最广泛的编程语言之一. 根据Github的最新报告 ,Java被列为仅次于JavaScript的第二大最常用的编程语言. 掌握Java的人有很多话题. 好 ...

  8. 在线学习Java编程的最佳方法

    1.简介 Java是使用最广泛的编程语言之一. 根据Github的最新报告 ,Java被列为仅次于JavaScript的第二大最常用的编程语言. 掌握Java的人有很多话题. 好消息是,您可以在线找到 ...

  9. eclipse java ui,Eclipse Forms筹划漂亮UI之高级组件[Java编程]

    赞助商链接 本文"Eclipse Forms筹划漂亮UI之高级组件[Java编程]"是由七道奇为您精心收集,来源于网络转载,文章版权归文章作者所有,本站不对其观点以及内容做任何评价 ...

  10. java三判式,Java编程设计模式,第 3 部分

    Java编程设计模式,第 3 部分 Java 8 被动迭代式特性介绍 2015 年 8 月 10 日发布 系列内容: 此内容是该系列 3 部分中的第 # 部分: Java编程设计模式,第 3 部分 h ...

最新文章

  1. SCCM 2007系列5 播发操作系统下
  2. 利用freopen()函数和fc命令简化程序调试
  3. C++:基于范围的for循环
  4. 安卓手机 服务器 性能,鲁大师Q3安卓手机性能榜发布:华为Mate30 Pro第三 这款手机称王...
  5. 微信朋友圈五月十大谣言:60岁以上老人打962899可享免费服务
  6. 滴滴试行网约车遗失物品处理规则:司机返还遗失物品将收费
  7. java将图片(jpg/bmp/png)转16位bmp(RBG565)
  8. android ios 混合编程,React Native与原生(Android、iOS)混编,三端痛点解析
  9. ffmpeg drawtext同时添加多行文本
  10. android+祖玛游戏源码,Flash祖玛游戏源代码
  11. react-countup 电子表数字样式 定时滚动 大屏需要 炫酷
  12. 解决allwinner bootlogo.bmg 开机logo 修改 后变绿
  13. Go:Unresolved dependency
  14. 软件需求说明书/ 概要设计说明书/项目开发计划/详细设计说明书(说明要点及要点解释)
  15. Qt 5.15版本安装教程
  16. ESP32 LVGL8.1 ——Style Text 设置文字样式 (Style 8)
  17. 第十一周博客作业西北师范大学|李晓婷
  18. 定个可以实现的小目标
  19. 关于多个ul标签并列时下对齐的问题
  20. 地理围栏算法解析(Geo-fencing)

热门文章

  1. 15. module create
  2. 1. JavaScript Array 对象
  3. 3.Magento的布局(Layout),块(Block)和模板(Template)
  4. 【BearChild】
  5. CodeForces 255C. Almost Arithmetical Progression (DP)
  6. xcode 左边导航栏中,类文件后面的标记“A”,M,?……等符号的含义???(转)...
  7. 程序员生存定律——打造属于自己的稀缺性(转载)
  8. 使用Java 自身Timer API实现定时器的方法
  9. 如何为SQL Server2008添加登录账户并配置权限
  10. 推荐一款支持文件同步的云盘pCloud