java数据结构树
1.为什么需要树这种数据结构
1)数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 [示意图]
2)链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历) 【示意图】
3)树存储方式的分析
能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。【示意图,后面详讲】
案例: [7, 3, 10, 1, 5, 9, 12]
2.二叉树的基本操作
1)二叉树的遍历
前序遍历: 先输出父节点,再遍历左子树和右子树
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
小结: 看输出父节点的顺序,就确定是前序,中序还是后序
图示
代码实现
//创建HeroNode结点
class HeroNode {
Integer id;
String name;
HeroNode left;//默认为空
HeroNode right;//默认为空
//前序遍历的方法
public void preOrder() {
//打印本结点
System.out.println(this);
//递归向左子树
if (this.left != null) {
this.left.preOrder();
}
//递归向右子树
if (this.right != null) {
this.right.preOrder();
}
}
//中序遍历的方法
public void infixOrder() {
//递归向左子树
if (this.left != null) {
this.left.infixOrder();
}
//打印本结点
System.out.println(this);
//递归向右子树
if (this.right != null) {
this.right.infixOrder();
}
}
//后序遍历的方法
public void postOrder() {
//递归向左子树
if (this.left != null) {
this.left.postOrder();
}
//递归向右子树
if (this.right != null) {
this.right.postOrder();
}
//打印本结点
System.out.println(this);
}
}
2)二叉树的查找
同样查找也和遍历一样分为前序中序和后序
//创建HeroNode结点
class HeroNode {
Integer id;
String name;
HeroNode left;//默认为空
HeroNode right;//默认为空
//前序遍历查找
public HeroNode preOrdersearch(int no) {
System.out.println("前序遍历查找");
//比较当前结点是不是
if (this.id == no) {
return this;
}
//不是判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrdersearch(no);
}
if (resNode != null) {
return resNode;
}
if (this.right != null) {
resNode = this.right.preOrdersearch(no);
}
return resNode;
}
//中序遍历查找
public HeroNode infixOrdersearch(int no) {
//判断当前节点的左子节点不为空
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrdersearch(no);
}
if (resNode != null) {
return resNode;
}
System.out.println("中序遍历查找");
//比较当前结点是不是
if (this.id == no) {
return this;
}
//进行左遍历查找
if (this.right != null) {
resNode = this.right.infixOrdersearch(no);
}
return resNode;
}
//后续遍历查找
public HeroNode posyOrdersearch(int no) {
HeroNode resNode = null;
//判断当前的左子节点不为空
if (this.left != null) {
resNode = this.left.posyOrdersearch(no);
}
if (resNode != null) {
return resNode;
}
//如果左子树没有找到则向右子树递归遍历查找
if (this.right != null) {
resNode = this.right.posyOrdersearch(no);
}
if (resNode != null) {
return resNode;
}
System.out.println("后序遍历查找");
//如果左右子数,都没有
if (this.id == no) {
return this;
} else {
return resNode;
}
}
}
3)删除结点
到了删除我们这里有两种删除方式,就是如果被删除结点有子树,那么应该如何操作
//创建HeroNode结点
class HeroNode {
Integer id;
String name;
HeroNode left;//默认为空
HeroNode right;//默认为空
//删除结点
//如果是叶子结点就删除结点
//如果是非叶子结点就删除该子树
public void delete(int no) {
//先判断左子结点
if (this.left != null && this.left.id == no) {
this.left = null;
return;
}
//判断右子结点
if (this.right != null && this.right.id == no) {
this.right = null;
return;
}
//我们向左子树递归删除
if (this.left != null) {
this.left.delete(no);
}
//我们向右子树递归删除
if (this.right != null) {
this.right.delete(no);
}
}
}
这个操作就会让我们连同被删除结点的子树也会删除掉
下面我们对之前的方法升级不让他删除子结点
//创建HeroNode结点
class HeroNode {
Integer id;
String name;
HeroNode left;//默认为空
HeroNode right;//默认为空
//删除结点
//如果是叶子结点就删除结点
//如果是非叶子结点就删除该子树
public void delete(int no) {
//先判断左子结点
if (this.left != null && this.left.id == no) {
HeroNode heroNode = this.left.right;
this.left = this.left.left;
this.left.right = heroNode;
return;
}
//判断右子结点
if (this.right != null && this.right.id == no) {
HeroNode heroNode = this.right.right;
this.right = this.right.left;
this.right.right = heroNode;
return;
}
//我们向左子树递归删除
if (this.left != null) {
this.left.delete(no);
}
//我们向右子树递归删除
if (this.right != null) {
this.right.delete(no);
}
}
}
不过这样有bug,就是当要删除的结点的左结点的右结点有数据会丢失
3.顺序存储二叉树
从数据存储来看,数组存储方式和树
的存储方式可以相互转换,即数组可
以转换成树,树也可以转换成数组,
看右面的示意图。
1)顺序存储二叉树的特点:
1.顺序二叉树通常只考虑完全二叉树
2.第n个元素的左子节点为 2 * n + 1
3.第n个元素的右子节点为 2 * n + 2
4.第n个元素的父节点为 (n-1) / 2
5.n : 表示二叉树中的第几个元素(按0开始编号
2)代码实现
/**
* 顺序存储二叉树
*/
public class ArraryBinaryTree {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
//创建一个arrTree
ArrTree arrTree = new ArrTree(arr);
// arrTree.preOrder();//1,2,4,5,3,6,7
arrTree.infixOrder(0);
// arrTree.postOrder(0);
}
}
class ArrTree {
private int[] arr;//存储数据结点的数组
public ArrTree(int[] arr) {
this.arr = arr;
}
//重载preOrder
public void preOrder(){
this.preOrder(0);
}
//编写一个方法,完成顺序存储二叉树的前序遍历
/**
* 数组的下标
*
* @param index
*/
public void preOrder(int index) {
//如果数组为空,或者index
if (arr == null || arr.length == 0) {
System.out.println("数组为空不能按照二叉树的前序遍历");
}
//输出当前元素
System.out.println(arr[index]);
//向左递归遍历
if ((index * 2 + 1) < arr.length) {
preOrder(2 * index + 1);
}
//向右递归遍历
if ((index * 2 + 2) < arr.length) {
preOrder(index * 2 + 2);
}
}
//编写一个方法,完成顺序存储二叉树的中序遍历
/**
* 数组的下标
*
* @param index
*/
public void infixOrder(int index) {
//如果数组为空,或者index
if (arr == null || arr.length == 0) {
System.out.println("数组为空不能按照二叉树的前序遍历");
}
//向左递归遍历
if ((index * 2 + 1) < arr.length) {
infixOrder(2 * index + 1);
}
//输出当前元素
System.out.println(arr[index]);
//向右递归遍历
if ((index * 2 + 2) < arr.length) {
infixOrder(index * 2 + 2);
}
}
//编写一个方法,完成顺序存储二叉树的中序遍历
/**
* 数组的下标
*
* @param index
*/
public void postOrder(int index) {
//如果数组为空,或者index
if (arr == null || arr.length == 0) {
System.out.println("数组为空不能按照二叉树的前序遍历");
}
//向左递归遍历
if ((index * 2 + 1) < arr.length) {
postOrder(2 * index + 1);
}
//向右递归遍历
if ((index * 2 + 2) < arr.length) {
postOrder(index * 2 + 2);
}
//输出当前元素
System.out.println(arr[index]);
}
}
顺序存储二叉树的一个重要应用就是堆排序
3)堆排序
堆排序基本介绍
1)堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
2)堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
3)每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
4)大顶堆举例说明
大顶堆有这样的特点第一个最大
我们只需要把一直把最大的移到后面就ok
堆排序的基本思想是:
1)将待排序序列构造成一个大顶堆
2)此时,整个序列的最大值就是堆顶的根节点。
3)将其与末尾元素进行交换,此时末尾就为最大值。
4)然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
首先要创建一个方法
//将一个数组(二叉树),调整成一个大顶堆
/**
* 功能:完成将以i对应的非叶子结点的树调整成大顶堆
*
* @param arr 待调整的数组
* @param i 表示非叶子结点的数组索引
* @param length 表示对多少个元素进行调整,lenght在逐渐减少
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];//先取出当前元素的值,保存在临时遍历
//开始调整
// k = i * 2 + 1,是i结点的左子结点
// k = k * 2 + 1这个式子很关键他可以循环
// 首先因为我们的堆之前是排好序的,所以接下来的循环肯定能得到一个排好序的堆
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) {//说明左子结点的值小于右子结点的值
k++;//用交换浪费资源
}
if (arr[k] > temp) {//如果子结点大于父结点
arr[i] = arr[k];//把较大的值赋给当前结点
i = k; //!!!将i指向k
} else {
break;
}
}
//当for循环结束后,我们将以i为结点的树的最大值放到顶部
arr[i] = temp;
}
这个方法有一个很精妙的地方就是他之比较二叉树的一段,也就是说在比较之前呢,他的两个子树就已经的大顶堆了
那么我们编写的堆排序方法就是这样的
//编写一个堆排序的方法
public static void heapSort(int[] arr) {
for (int i = arr.length - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
// System.out.println(Arrays.toString(arr));
int temp = 0;
for (int j = arr.length - 1; j > 0; j--) {
//交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
}
4.线索化二叉树
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7 "
问题分析:
1)当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
2)但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.
3)如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
4)解决方案-线索二叉树
1)线索化二叉树的基本介绍
线索二叉树基本介绍
1)n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
2)这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
3)一个结点的前一个结点,称为前驱结点
4)一个结点的后一个结点,称为后继结点
举一个例子
过程就是先向左遍历到把,pre留给8然后return;
之后到3这让8的右子节点指向node
2)代码实现
这时我们之前用到的那个结点和我们原来用到的一样
//创建HeroNode
class ThreadedNode {
Integer id;
String name;
ThreadedNode left;//默认为空
ThreadedNode right;//默认为空
//定义两个属性
//1.如果leftType == 0 表示指向左子树如果是 1表示指向前驱结点
//2.如果leftType == 0 表示指向右子树如果是 1表示指向前驱结点
int leftType;
int rightType;
public ThreadedNode(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
但是呢树是不一样的
这里面多了一个这样的方法
//实现了线索化的二叉树
class ThreadedTree {
private ThreadedNode root;
//为了实现线索化,需要创建一个指向前驱结点的指针
private ThreadedNode pre = null;
public void threadedNodes() {
this.threadedNodes(root);
}
public void setRoot(ThreadedNode root) {
this.root = root;
}
//遍历线索二叉树
public void threadedList(){
//定义一个变量储存当前遍历的结点,从root开始
ThreadedNode node = root;
while (node != null) {
//循环找到leftType == 1的结点,第一个找到的就是8结点
//后面随着遍历而变化,因为当leftType == 1时 说明该结点按照线索化
//处理后面的有效结点
while (node.leftType == 0){
node = node.left;
}
//打印当前这个结点
System.out.println(node);
//如果当前结点的右指针指向后继结点,就一直输出
while (node.rightType == 1){
node = node.right;
System.out.println(node);
}
//替换这个遍历的结点
node = node.right;
}
}
//编写对二叉树进行中序线索化的方法
public void threadedNodes(ThreadedNode node) {
//如果当前结点为空,不需要线索化
if (node == null) {
return;
}
//1)先线索化左子树
threadedNodes(node.left);
//2)线索化当前结点
//处理当前前序结点
if (node.left == null) {
//让当前结点的左指针指向前驱接待你
node.left = pre;
//修改当前结点的左指指针类型
node.leftType = 1;
}
//处理后继结点
if (pre != null && pre.right == null) {
//让前驱结点的右指针指向当前结点
pre.right = node;
//修改前驱结点的右指针类型
pre.rightType = 1;
}
//!!!每处理一个结点之后让当前结点是下一个结点的前驱结点
pre = node;
//3)再线索化右子树
threadedNodes(node.right);
}
}
5.赫夫曼树
1)赫夫曼树的特点
基本介绍
1.给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
2.赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
例如中间的就是赫夫曼树
2)构建
赫夫曼树的构建:先把arr[int]转换为arr[Node],之后寻找最小的两个树构成一个3结点的子树同时把这个新生成的树的root结点加入到arr中
/**
* 赫夫曼树
* 霍夫曼编码是霍夫曼树在通信行业的经典应用之一
* 霍夫曼广泛用于数据文件的压缩,其压缩率在20%~90%
*/
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node huffmanTree = createHuffmanTree(arr);
// System.out.println(huffmanTree);
huffmanTree.preOrder();
}
//创建赫夫曼树的方法
public static Node createHuffmanTree(int[] arr) {
//为了方便操作
//遍历arr数组
//将arr的每个元素构成一个Node
//将Node放入到ArrayList
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
//排序从小到大
Collections.sort(nodes);
// System.out.println("处理之前"+nodes);
//取出根节点权值最小的两颗二叉树
//1.取出前两个
Node left = nodes.get(0);
Node right = nodes.get(1);
//2.构成二叉树
Node parent = new Node(left.value + right.value);
parent.left = left;
parent.right = right;
//3.从arraylist中删除处理过的二叉树
nodes.remove(left);
nodes.remove(right);
//4.把新的加入到parent
nodes.add(parent);
// System.out.println("处理之后"+nodes);
}
return nodes.get(0);
}
}
//创建结点类
//为了让Node,对象支持排序实现一个Collections集合
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 +
'}';
}
//排序规则
@Override
public int compareTo(Node o) {
//从小到大排
return this.value - o.value;
}
//前序遍历
public void preOrder(){
if (this.left == null && this.right == null) {
System.out.println(this);
}
if (this.left != null){
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
3)赫夫曼编码
package Tree;
import java.io.*;
import java.util.*;
/**
* 通过赫夫曼编码压缩编码
* 把 i like like like java do you like a java转化为赫夫曼编码
* 思路:
* 1)Node{data(存放数据),weight(权重)}
* 2)得到字符串对应的byte数组
* 3)编写一个方法将准备构建赫夫曼树的Node结点放到List,形式[Node{date=97,weight=5}],[Node{date=32,weight=9}]
* 4)通过List创建对应的赫夫曼树
*/
public class DataCompression {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] bytes = str.getBytes();
// System.out.println(bytes.length);
List<Node1> nodes = getNode1(bytes);
Node1 huffmanTree = createHuffmanTree(nodes);
//测试一把是否生成了对应的赫夫曼编码
Map<Byte, String> codes = getCodes(huffmanTree);
byte[] zip = zip(str);
//最后压缩的对应的byte值
System.out.println(Arrays.toString(zip));
byte[] decode = decode(codes, zip);
System.out.println(new String(decode));
//压缩文件的代码
String srcFile = "D:\\src.bmp";
String dstFile = "D:\\cst.zip";
zipFile(srcFile,dstFile);
System.out.println("压缩文件成功");
String zipFile = "D:\\cst.zip";
dstFile = "D:\\src2.bmp";
unzipFile(zipFile,dstFile);
System.out.println("文件解压成功");
}
/** *
* 编写一个方法,完成对压缩文件的解压
* @param zipFile 压缩文件路径
* @param dstFile 解压文件路径
*/
public static void unzipFile(String zipFile,String dstFile){
//定义文件输入流
InputStream is = null;
//定义一个对象输入流
ObjectInputStream ois = null;
//定义文件输出流
OutputStream os = null;
try{
//创建文件输入流
is = new FileInputStream(zipFile);
//创建一个和is关联的对象输入流
ois = new ObjectInputStream(is);
//读取byte数组
byte[] bytes = (byte[]) ois.readObject();
//读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
//解码,s原始的byte
byte[] s = decode(huffmanCodes, bytes);
//创建一个文件输入流
os = new FileOutputStream(dstFile);
//写出数据
os.write(s);
} catch (Exception e) {
System.out.println(e);
} finally {
try {
os.close();
ois.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 编写一个方法将文件进行压缩
* @param srcFile 你传入的希望压缩的文件的全路径
* @param dstFile 我们压缩后将压缩文件放到那个目录
*/
public static void zipFile(String srcFile,String dstFile) {
//创建输入流
FileInputStream is = null;
//对象输入流
ObjectOutputStream oos = null;
//创建输出流
OutputStream os = null;
try {
is = new FileInputStream(srcFile);
//创建一个和源文件大小一样的byte[]
byte[] b = new byte[is.available()];
//读取文件
is.read(b);
//使用赫夫曼编码
//1)获取文件对应的压缩码
byte[] zip = zip(b);
//2)创建压缩文件
os = new FileOutputStream(dstFile);
//创建一个和文件输出关联的对象输出流
oos = new ObjectOutputStream(os);
//这里我们以对象流的方式写入赫夫曼编码,是为了我们以后恢复源文件使用
oos.writeObject(zip);//我们是把赫夫曼的编码
//同时我们要把赫夫曼编码写入恢复文件要不然无法恢复
oos.writeObject(huffmanCodes);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
os.close();
oos.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//编写一个方法将字符串解码处理
//1.第一步将zip转换为二进制字符串
//2.将二进制对应的字符串转换为对应的字符串
/**
* @param huffmanCodes 赫夫曼编码表map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来对应的字符串数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//1.先得到huffmanBytes 对应的二进制字符串
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));
}
//把字符串按照赫夫曼编码进行解码
//把赫夫曼编码表进行调换,反向查询
Map<String, Byte> map = new HashMap<String, Byte>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建一个集合,存放byte
List<Byte> list = new ArrayList<Byte>();
//扫描字符串
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
//取出一个字符
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;
}
//当for循环结束之后list存放了所有的字符
//把list中的数据放入到byte[]并返回
byte b[] = new byte[list.size()];
for (int i = 0; i < list.size();i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个byte转换为一个二进制字符串
*
* @param b 出入的byte
* @param flag 标记是否需要补位
* @return
*/
private static String byteToBitString(boolean flag, byte b) {
//使用一个变量保存b
int temp = b;
//如果是整数我们要与上一个256
if (flag) {
temp |= 256;
}
String s = Integer.toBinaryString(temp);//返回的是temp对应的补码
if (flag) {
return s.substring(s.length() - 8);
} else {
return s;
}
}
//编写一个方法再重载一次
private static byte[] zip(byte[] bytes){
List<Node1> nodes = getNode1(bytes);
//创建赫夫曼树
Node1 huffmanTree = createHuffmanTree(nodes);
//生成对应的赫夫曼编码
Map<Byte, String> codes = getCodes(huffmanTree);
//压缩
byte[] zip = zip(bytes, codes);
return zip;
}
//编写一个方法解码重载一下
private static byte[] zip(String str) {
byte[] bytes = str.getBytes();
List<Node1> nodes = getNode1(bytes);
//创建赫夫曼树
Node1 huffmanTree = createHuffmanTree(nodes);
//生成对应的赫夫曼编码
Map<Byte, String> codes = getCodes(huffmanTree);
//压缩
byte[] zip = zip(bytes, codes);
return zip;
}
//编写一个方法,将字符串对应的byte[] 数组通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]数组
/**
* @param bytes 这时原始的byte[]
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的byte[]
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1.先利用赫夫曼编码表,将bytes转成赫夫曼编码字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
// System.out.println(stringBuilder.toString());
//将这个字符串转换为byte数组
//统计返回的字符串长度
int len = stringBuilder.length() % 8 == 0 ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1;
//创建存储
byte[] huffmanCodeBytes = new byte[len];
int index = 0;
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;
}
//生成赫夫曼树对应的赫夫曼编码
//思路
//1.将赫夫曼编码存放到Map集合中
static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
//2.在生成一个赫夫曼编码表示,需要去拼接路径
static StringBuilder stringBuilder = new StringBuilder();
/**
* 为了方便调用我们重载这个方法
*
* @param node
*/
private static Map<Byte, String> getCodes(Node1 node) {
if (node == null) {
return null;
}
//处理左子树
getCodes(node.left, "0", stringBuilder);
//处理右子树
getCodes(node.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 功能将传入的node结点的所有叶子结点的赫夫曼编码,存放到huffmanCodes集合
*
* @param node 传入结点
* @param code 路径,传入左子节点是0,右子节点是1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node1 node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2
stringBuilder2.append(code);
if (node != null) {//如果node == null不处理
//判断当前node是叶子节点还是非叶子结点
if (node.data == null) {
//非叶子结点
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else {
//叶子结点
//表示找到某个叶子结点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
//前序遍历的方法
private static void preOrder(Node1 root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("赫夫曼树为空");
}
}
/**
* 它可以接收一个字节数组,然后转换为Node1
*
* @param bytes
* @return
*/
private static List<Node1> getNode1(byte[] bytes) {
//1.创建ArrayList
ArrayList<Node1> nodes = new ArrayList<>();
//遍历bytes,统计每个byte出现的次数,存储每个byte出现的次数
HashMap<Byte, Integer> hashMap = new HashMap<>();
for (byte b : bytes) {
Integer count = hashMap.get(b);
if (count == null) {//Map没有这个字符数据
hashMap.put(b, 1);
} else {
hashMap.put(b, count + 1);
}
}
//把每个键值对转换为Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
nodes.add(new Node1(entry.getKey(), entry.getValue()));
}
return nodes;
}
//可以通过List 建立对应的赫夫曼树
private static Node1 createHuffmanTree(List<Node1> nodes) {
while (nodes.size() > 1) {
//排序,从小到大
Collections.sort(nodes);
//取出第一颗最小的二叉树
Node1 leftNode = nodes.get(0);
//取出第二颗最小的二叉树
Node1 rightNode = nodes.get(1);
//创建一棵新的二叉树,他的根节点没有data只有权值
Node1 parent = new Node1(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);
}
}
//创建Node,待数据和权值
class Node1 implements Comparable<Node1> {
Byte data;//存放
int weight;//权值,表示字符出现的次数
Node1 left;
Node1 right;
public Node1(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node1 o) {
//从小到大排
return this.weight - o.weight;
}
@Override
public String toString() {
return "Node1{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
6.二叉排序树
先看一个需求给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
使用数组
1)数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢. [示意图]
2)数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。[示意图]
使用链式存储-链表
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。[示意图]使用二叉排序树
二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
1)二叉排序树的创建和遍历
创建非常简单add方法在原来的基础之上增加一个判断当前结点值和目标添加结点的值
//添加结点的方法
//递归添加
public void add(Node node) {
if (node == null) {
return;
}
//判断传入结点的值和当前子树的根节点的关系
if (node.value < this.value) {
//如果当前结点左子结点为空
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);
}
}
}
遍历和之前一样
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
2)二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
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 的右子树找到最小的结点
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点
(6) targetNode.value = temp
//删除结点
public void deleNode(int value) {
if (root == null) {
return;
} else {
//先找到要删除的结点
Node targetNode = search(value);
//如果没有找到要删除的结点
if (targetNode == null) {
return;
}
//如果我们发现这棵树只有一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
//找到parent
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) {//如果要删除的结点有两个子结点
//定义一个Node让他等于targetNode.left,我们利用他找到targetNode右子树最小的
//Node node = targetNode.left;找最大的
Node node = targetNode.right;
while (node.left != null){
node = targetNode.left;
}
//这时node指向的最小
deleNode(node.value);
targetNode.value = node.value;
} else {//删除只有一颗子树的结点
if (targetNode.left != null) {//如果要删除的结点有左子结点
if (parent != null) {
//如果targetNode是parent的左
if (parent.left.value == value) {
parent.left = targetNode.left;
} else {//说明是右子结点
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
} else {//如果要删除的结点是右子结点
if (parent != null) {
if (parent.left.value == value) {
parent.left = targetNode.right;
} else {
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
这是Node中查找父节点的方法
//查找要删除结点的父节点
public Node searParent(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.searParent(value);
} else if (value >= this.value && this.right != null) {
return this.right.searParent(value);
} else {
System.out.println("没有找到父节点");
return null;
}
}
}
7.平衡二叉树
1)单旋转
看一个案例(说明二叉排序树可能的问题)
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在
左边BST 存在的问题分析:
1)左子树全部为空,从形式上看,更像一个单链表.
2)插入速度没有影响
3)查询速度明显降低(因为需要依次比较), 不能发挥BST
的优势,因为每次还需要比较左子树,其查询速度比
单链表还慢
4)解决方案-平衡二叉树(AVL)
我们对于平衡二叉树的操作是在之前二叉排序树的基础之上升级将他进行左旋或者右旋
//添加结点的方法
//递归添加
public void add(Node node) {
if (node == null) {
return;
}
//判断传入结点的值和当前子树的根节点的关系
if (node.value < this.value) {
//如果当前结点左子结点为空
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);
}
}
//为什么this是root因为在java中谁调用方法谁是this
//当添加完一个结点后,如果:(右子树高度-左子树高度)>1,左旋转
if (rightHeight() - leftHeight() > 1) {
leftRotate();
}
if (leftHeight() - rightHeight() > 1) {
rightRotate();
}
}
//右旋转
public void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
//左旋转
public void leftRotate() {
//创建新的结点,以当前跟结点的值
Node newNode = new Node(value);
//把新的结点的左子树设置成当前结点的左子树
newNode.left = left;
//把新的结点的右子树设置成带你过去结点的右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成右子树的右子树
right = right.right;
//把当前结点的左子节点设置成新的结点
left = newNode;
}
左旋思路
1.先建立一个和当前root结点一样的结点
2.把新结点的左子树设置成root的左子树
3.把新结点的右子树设置成root的右子树的左子树
4.把root的值设置成root的右结点的值
5.把root的右节点设置成原来右节点的右节点
6.把root的左结点设置成新节点
2)双旋转
//添加结点的方法
//递归添加
public void add(Node node) {
if (node == null) {
return;
}
//判断传入结点的值和当前子树的根节点的关系
if (node.value < this.value) {
//如果当前结点左子结点为空
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);
}
}
//为什么this是root因为在java中谁调用方法谁是this
//当添加完一个结点后,如果:(右子树高度-左子树高度)>1,左旋转
if (rightHeight() - leftHeight() > 1) {
if (right != null && right.leftHeight() > right.rightHeight()) {
//先对当前结点的左子结点进行
right.rightRotate();
//再对当前结点进行右旋转
leftHeight();
} else {
leftRotate();
}
return;//必须要
}
if (leftHeight() - rightHeight() > 1) {
if (left != null && left.rightHeight() > left.leftHeight()) {
//先对当前结点的左子结点进行
left.leftRotate();
//再对当前结点进行右旋转
rightRotate();
} else {
rightRotate();
}
}
}
//把当前结点的右子树设置成右子树的右子树
right = right.right;
//把当前结点的左子节点设置成新的结点
left = newNode;
}
左旋思路
1.先建立一个和当前root结点一样的结点
2.把新结点的左子树设置成root的左子树
3.把新结点的右子树设置成root的右子树的左子树
4.把root的值设置成root的右结点的值
5.把root的右节点设置成原来右节点的右节点
6.把root的左结点设置成新节点
##### 2)双旋转
[外链图片转存中...(img-CGzQzVry-1647846281143)]
```java
//添加结点的方法
//递归添加
public void add(Node node) {
if (node == null) {
return;
}
//判断传入结点的值和当前子树的根节点的关系
if (node.value < this.value) {
//如果当前结点左子结点为空
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);
}
}
//为什么this是root因为在java中谁调用方法谁是this
//当添加完一个结点后,如果:(右子树高度-左子树高度)>1,左旋转
if (rightHeight() - leftHeight() > 1) {
if (right != null && right.leftHeight() > right.rightHeight()) {
//先对当前结点的左子结点进行
right.rightRotate();
//再对当前结点进行右旋转
leftHeight();
} else {
leftRotate();
}
return;//必须要
}
if (leftHeight() - rightHeight() > 1) {
if (left != null && left.rightHeight() > left.leftHeight()) {
//先对当前结点的左子结点进行
left.leftRotate();
//再对当前结点进行右旋转
rightRotate();
} else {
rightRotate();
}
}
}
我们只需要在右旋转之前加入这个left != null && left.rightHeight() > left.leftHeight()判断堆左结点进行左旋