为什么需要树这种数据结构
数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
操作示意图
链式存储方式分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,连接到链表中即可,删除效率也很好)
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
操作示意图
树存储方式的分析
能提高数据存储,读取的效率,比如利用二叉树(binary sort tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
树常用术语
二叉树
树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
二叉树的子节点分为左节点和右节点
如果二叉树的所有叶子节点都在最后一层,并且节点总数=2n-1,n为层数,则我们称为满二叉树
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
前中后遍历
二叉树遍历说明
使用前序,中序和后序对下面的二叉树进行遍历
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
小结 看输出父节点的顺序,就确定是前序,中序还是后序
图解前中后序遍历
代码实现
节点代码
@Data
public class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
//前序遍历
public void preOrder(){
//先输出当前节点(父节点)
System.out.println(this);
//递归输出左节点
if(this.left != null){
this.left.preOrder();
}
//递归输出右节点
if(this.right != null){
this.right.preOrder();
}
}
//中序遍历
public void midOrder(){
if(this.left != null) {
this.left.midOrder();
}
System.out.println(this);
if(this.right!=null){
this.right.midOrder();
}
}
//后序遍历
public void backOrder(){
if(this.left != null) {
this.left.backOrder();
}
if(this.right!=null){
this.right.backOrder();
}
System.out.println(this);
}
}
二叉树代码
@Data
public class BinaryTree {
private HeroNode root;
public void preOrder(){
if(this.root != null){
root.preOrder();
}else{
System.out.println("二叉树为空 无法遍历");
}
}
public void midOrder(){
if(this.root != null){
root.midOrder();
}else{
System.out.println("二叉树为空 无法遍历");
}
}
public void backOrder(){
if(this.root != null){
root.backOrder();
}else{
System.out.println("二叉树为空 无法遍历");
}
}
}
测试代码
public class TestBinaryTree {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
//创建节点
HeroNode root = new HeroNode(1,"宋江");
HeroNode node1 = new HeroNode(2,"吴用");
HeroNode node2 = new HeroNode(3,"卢俊义");
HeroNode node3 = new HeroNode(4,"林冲");
HeroNode node4 = new HeroNode(5,"关胜");
//创建二叉树
root.setLeft(node1);
root.setRight(node2);
node2.setLeft(node4);
node2.setRight(node3);
binaryTree.setRoot(root);
System.out.println("前序遍历");
binaryTree.preOrder();
System.out.println("中序遍历");
binaryTree.midOrder();
System.out.println("后序遍历");
binaryTree.backOrder();
}
}
打印信息
前中后序查找
要求
- 请编写前序查找,中序查找和后序查找节点的方法
- 并分别使用三种查找方式,查找heroNo=5的节点
- 并分析各种查找方式,分别比较了多少次
图解查找节点
代码实现
HeroNode节点类添加方法
//前序查找
public HeroNode preOrderSearch(int no){
System.out.println("前序查找");
//当前节点相等 返回
if(this.no == no ){
return this;
}
//左子节点递归查找
HeroNode rtnNode = null;
if(this.left != null){
rtnNode = this.left.preOrderSearch(no);
}
//左子节点递归是否找到
if(rtnNode != null){
return rtnNode;
}
//如果左子节点没有到,右子节点递归查找
if(this.right!=null){
rtnNode=this.right.preOrderSearch(no);
}
return rtnNode;
}
public HeroNode midOrderSearch(int no){
HeroNode rtnNode = null;
if(this.left!=null){
rtnNode = this.left.midOrderSearch(no);
}
if(rtnNode!=null){
return rtnNode;
}
System.out.println("中序查找");
if(this.no ==no){
return this;
}
if(this.right!=null){
rtnNode = this.right.midOrderSearch(no);
}
return rtnNode;
}
public HeroNode backOrderSearch(int no){
HeroNode rtnNode = null;
if(this.left!=null){
rtnNode = this.left.backOrderSearch(no);
}
if(rtnNode!=null){
return rtnNode;
}
if(this.right!=null){
rtnNode = this.right.backOrderSearch(no);
}
if(rtnNode!=null){
return rtnNode;
}
System.out.println("后序查找");
if(this.no ==no){
return this;
}
return rtnNode;
}
BinaryTree二叉树类添加方法
//前序查找
public HeroNode preOrderSearch(int no){
if(root != null){
return root.preOrderSearch(no);
}else{
return null;
}
}
//中序查找
public HeroNode midOrderSearch(int no){
if(root !=null){
return root.midOrderSearch(no);
}else{
return null;
}
}
//后序查找
public HeroNode backOrderSearch(int no){
if(root !=null){
return root.backOrderSearch(no);
}else{
return null;
}
}
测试添加代码
System.out.println("--------前序查找--------------");
HeroNode preNode = binaryTree.preOrderSearch(5);
System.out.println("前序查找结果:"+preNode);
System.out.println("--------中序查找--------------");
HeroNode midNode = binaryTree.midOrderSearch(5);
System.out.println("中序查找结果:"+midNode);
System.out.println("--------后序遍历--------------");
HeroNode backNode =binaryTree.backOrderSearch(5);
System.out.println("后序查找结果:"+backNode);
打印信息
二叉树删除节点
要求
- 如果删除的节点是叶子节点,则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树
- 测试,删除掉5号叶子节点和3号子树
删除思路分析
完成删除节点的操作
- 如果删除的节点是叶子节点,则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树
思路
如果是树是空树root,如果只有一个root节点,则等价将二叉树置空
如果不是步骤如下:
1 因为二叉树是单向的,所有判断当前节点的子节点是不是需要删除节点,而不能去判断当前这个节点是不是需要删除节点
2 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null;并且返回(结束递归)
3 如果当前节点的右子节点不为空,并且左子节点就是要删除的节点,就将this.right=null;并且返回(结束递归)
4 如果第2步和第3步没有删除节点,那么需要向左子树进行递归删除
5 如果第4步也没有删除节点,则需要向右子树进行递归删除
代码实现
删除方法
//HeroNode节点类添加方法
public void delete(int no){
//如果删除的节点是叶子节点,则删除该节点
if(this.left!=null&&this.left.no==no){
this.left = null;
return;
}
if(this.right!=null&&this.right.no==no){
this.right=null;
return;
}
//如果删除的节点是非叶子节点,则删除该子树 递归进行
if(this.left!=null){
this.left.delete(no);
}
if(this.right!=null){
this.right.delete(no);
}
}
//BinaryTree 类添加方法
public void delete(int no){
if(this.root==null){
System.out.println("空树 无法删除");
}
if(this.root.getNo()==no){
root = null;
}else{
root.delete(no);
}
}
// 测试代码
System.out.println("删除前 前序遍历: ");
binaryTree.preOrder();
binaryTree.delete(3);
System.out.println("删除后 前序遍历: ");
binaryTree.preOrder();
信息打印
顺序存储二叉树
- 基本说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组;
- 要求
上图的二叉树的节点,要求以数组方式来存储 arr={1,2,3,4,5,6,7}
要求在遍历数组arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成节点的遍历 - 顺序存储二叉树特点
1 顺序二叉树通常指考虑完全二叉树
2 第n个元素的左子节点为2n+1
3 第n个元素的右子节点为2n+2
4 第n个元素的父节点为(n-1)/2
注:表示二叉树中第几个元素(按0开始编号)
代码实现(前序遍历)
需求:给一个数组arr={1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。
前序结果应当为:1,2,3,4,5,6,7
public class Array2BinaryTree {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
ArrayToBinaryTree tree = new ArrayToBinaryTree(arr);
System.out.println("数组输出:"+ Arrays.toString(arr));
tree.preOrder(0);
}
}
class ArrayToBinaryTree{
private int arr[];
public ArrayToBinaryTree(int[] arr){
this.arr = arr;
}
public void preOrder(int index){
if(arr == null||arr.length==0){
System.out.println("空数组,不能按二叉树前序遍历");
}
System.out.println("输出当前元素:"+arr[index]);
//向左递归遍历 第n个元素的左节点时为 2*n+1
if(2*index+1<arr.length){
preOrder(2*index+1);
}
//向右递归遍历 第n个元素的右节点时为 2*n+1
if(2*index+2<arr.length){
preOrder(2*index+2);
}
}
}
信息打印
顺序存储二叉树应用实例
八大排序中的堆排序,就会使用到顺序存储二叉树,关于堆排序,在后续二叉树实际应用时讲解。
线索化二叉树
先看一个问题
将数列{1,3,6,8,10,14}构建成一颗二叉树 n+1=7
问题分析:
- 当对上面二叉树进行中序遍历时,数列为{8,3,10,1,6,14}
- 但是 6,8,10,14 这个几个节点的左右指针,并没有完全的利用上
- 如果我们希望重复的利用各个节点的左右指针,让各个节点可以指向自己的前后节点 那么就引入——线索二叉树
1 n个节点二叉树链表中含有n+1 [公司2n-(n-1)=n+1] 个空指针域。利用二叉树表中的空指针域,
存放指向该节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为"线索")
2 这种加上了线索的二叉树表称为线索链表,相应的二叉树称为线索二叉树 (Threaded Binary Tree)。
根据线索性质的不同,线索二叉树可分为前序、中序和后序线索二叉树三种
3 一个节点的前一个节点,称前驱节点
一个节点的后一个节点,称后继节点
思路分析
案列说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为{8,3,10,1,6,14}
说明:当线索化二叉树后,node节点的属性left和right,有如下情况:
1 left指向的是左子树,也可能是指向的前驱节点。比如①节点left指向的左子树,而⑩节点的left指向的就是前驱节点
2 right指向的是右子树,也可能是指向的后继节点。比如①节点right指向的右子树,而⑩节点的right指向的就是后继节点
代码实现
节点
@Data
public class HeroNode {
private int no;
private String name;
private HeroNode left; // 左子树
private HeroNode right; // 右子树
//leftType=0 表示指向左子树 leftType=1 表示指向前驱节点
//leftType=0 表示指向右子树 rightType=1 表示指向后驱节点
private int leftType;
private int rightType;
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
}
定义二叉树
// 定义一个二叉树 BinaryTree
public class ThreadBinaryTree {
private HeroNode root;
// 创建一个指向当前节点的前驱节点的指针
// pre在递归进行线索化 总是指向前一个节点
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
/**
* 编写对二叉树进行中序线索化的方法
* @param node 当前需要线索化的节点
*/
public void threadedNodes(HeroNode node){
// 校验
if(node==null){
return;
}
// 1 线索化左子树
if(node.getLeft()!=null){
threadedNodes(node.getLeft());
}
// 2 线索化当前节点 先处理当前节点前驱节点
if(node.getLeft()==null){
node.setLeftType(1);
node.setLeft(pre);
}
// 处理后继节点
if(pre!=null && pre.getRight()==null){
pre.setRight(node);
pre.setRightType(1);
}
pre = node;
//3 线索化右子树
if(node.getRight()!=null){
threadedNodes(node.getRight());
}
}
}
测试验证
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
// 先创建一棵二叉树
ThreadBinaryTree binaryTree = new ThreadBinaryTree();
// 创建需要的节点
HeroNode root = new HeroNode(1,"1");
HeroNode node1 = new HeroNode(3,"3");
HeroNode node2 = new HeroNode(6,"6");
HeroNode node3 = new HeroNode(8,"8");
HeroNode node4 = new HeroNode(10,"10");
HeroNode node5 = new HeroNode(14,"14");
root.setLeft(node1);
root.setRight(node2);
node1.setLeft(node3);
node1.setRight(node4);
node2.setLeft(node5);
binaryTree.setRoot(root);
binaryTree.threadedNodes(root);
HeroNode left = node4.getLeft();
System.out.println("10号的前驱节点: "+left);
HeroNode right = node4.getRight();
System.out.println("10号的后继节点: "+right);
}
}
信息打印
遍历线索化二叉树
说明:对前面的中小线索化的二叉树,进行遍历
分析 因为线索化后,各个节点指向都有变化,因此袁来的遍历方式不能使用,这时需要使用新的遍历线索化二叉树,各个节点可以通过线性方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。
代码实习
//遍历线索化二叉树
public void threadedList(){
// 定义一个变量 存储当前遍历的节点 从root开始
HeroNode node = root;
while(node != null){
//循环找到 第一个leftType=1 的节点 列子中应该是8
while(node.getLeftType()==0){
node = node.getLeft();
}
System.out.println(node);
//当前节点有指针指向是后继节点 则输出
while(node.getRightType()==1){
//获取当前节点后继节点
node = node.getRight();
System.out.println(node);
}
//循环结束 当前node.getRightType() != 1 替换遍历节点
node = node.getRight();
}
}
// 测试代码
System.out.println("遍历中序线索化二叉树");
binaryTree.threadedList();
打印信息
树结构的实际应用
堆排序
堆排序基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(n log n),它也是不稳定排序
- 堆是具有以下性质的完全二叉树:每个节点的值都大于等于其左右孩子节点的值,称为大顶堆,注意:没有要求节点的左孩子的值和右孩子的值的大小关系
- 每个节点的值都小于或者等于其左右孩子节点的值,称为小顶堆
- 一般升序采用大顶堆;降序采用小顶堆
大顶堆举例说明
大顶堆特点:arr[i]>=arr[2i+1]&&arr[i]>=arr[2i+2] //i对应第几个节点,i从0开始编号
小顶堆举例说明
小顶堆特点:arr[i]=<arr[2i+1] && arr[i]<=arr[2i+2] //i对应第几个节点,i从0开始编号
堆排序的基本思想与图解
堆排序的基本思想是:
将待排序序列构造成一个大顶堆
此时 整个序列的最大值就是堆顶的根节点
将其与末尾元素进行交换,此时末尾就为最大值
然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
要求:给你一个数组{4,6,8,5,9},要求使用堆排序法,将数组升序排序。
(一)构造初始堆,将给定无序序列构造成一个大顶堆
- 假定给定无序序列结构如下
- 此后从第一个非叶子节点开始(叶节点自然不用调用,第一个非叶子节点arr.length/2-1=5/2-1=1,也就是下面的6节点),从左至右,从下至上进行调整。
- 找到第二个非叶节点4,由于[4,9,8]中9元素值最大,4和9交换
- 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6
- 此时 我们将一个无序序列构造成了一个大顶堆。
(二)将堆顶元素与末尾元素进行交换,使用末尾元素最大。然后继续调整堆,再将对顶元素与末尾元素进行交换,得到第二大元素。如此交换、重建、交换。 - 将堆顶元素9和末尾元素4进行交换
- 重新调整结构,使其继续满足堆定义
- 再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
总结 - 将无序序列构造成一个堆,根据升序降序要求选择大顶堆或者小顶堆
- 将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端
- 重新调整结构,使其满足堆定义,然后继续交换对顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
代码实习
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4,6,8,5,9};
//分布调整
//stepAdjustHeap(arr);
heapSort(arr);
}
private static void heapSort(int[] arr) {
//将无序数组变为打顶堆 一次完成
for(int i =arr.length/2-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);
}
System.out.println("排序结果:"+Arrays.toString(arr));
}
private static void stepAdjustHeap(int[] arr) {
System.out.println("将数组调整为大顶堆 分布完成");
adjustHeap(arr,1,arr.length);
System.out.println("第一轮后的结果:"+Arrays.toString(arr)); //[4, 9, 8, 5, 6]
adjustHeap(arr,0,arr.length);
System.out.println("第二轮后的结果:"+Arrays.toString(arr)); //[9, 6, 8, 5, 4]
}
/**
*
* @param arr 传入数列
* @param i 表示非叶子节点的index
* @param length 数组长度
*/
private static void adjustHeap(int[] arr, int i, int length) {
//将非叶子节点值赋值给temp,用于后面交换
int temp = arr[i];
//2*i+1 表示i的左子节点 此时k就是左子节点 k+1为右子节点
for(int k = 2*i+1;k<length;k=k*2+1){
//如果 左子节点<右子节点 则k++ 指向大节点
if(k+1<length && arr[k]<arr[k+1]){
k++;
}
//取较大的子节点和当前(父)节点对比 如果左右节点那个值大 就和 非叶子节点arr[i] 交换
if(arr[k]>temp){
arr[i]=arr[k];
i = k;
}else{
break; // 此处可以跳出是因为 至下而上进行
}
// 完成交换
arr[i]=temp;
}
}
}
哈夫曼树
基本介绍
- 给定n个权值作为n个叶子节点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最有二叉树,也称为哈夫曼树(Huffman tree),还有的翻译为霍夫曼树(赫夫曼树)
- 哈夫曼树是带权路径长度最短的树,权值较大的节点离根较近
哈夫曼树几个重要概念和举例说明
- 路径和路径长度:在一颗树中,从一个节点往下可以达到的孩子或者孙子节点之间的通路,称为路径。
- 通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到L层节点的路径长度为L-1
- 节点的权及带权路径长度:若将树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。节点带权路基长度为:从根节点到该节点之间的路径长度与该节点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有子节点的带权路径长度之和,记为WPL weighted path length,权值越大的节点离根节点越近的二叉树才是最优二叉树
- WPL最小的就是哈夫曼树
图解哈夫曼树创建思路
需求:给一个数列{13,7,8,3,29,6,1},要求转成一颗哈夫曼树
- 从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树,该新的二叉树的根接地那的权值是前面两颗二叉树跟节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗哈夫曼树
代码实现
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13,7,8,3,29,6,1};
System.out.println("前序输出结果:");
preOrder(createHuffmanTree(arr));
}
private static Node createHuffmanTree(int[] arr) {
ArrayList<Node> list = new ArrayList<>();
for (int i:arr) {
Node node = new Node(i);
list.add(node);
}
Collections.sort(list);
//System.out.println(list);
//当节点list长度大于2时就循环
while(list.size()>1){
//取出排序后的最小的两个节点
Node left = list.get(0);
Node right = list.get(1);
//构建一个新的二叉树
Node parent = new Node(left.getValue()+right.getValue());
parent.setLeft(left);
parent.setRight(right);
list.remove(left);
list.remove(right);
list.add(parent);
Collections.sort(list);
//System.out.println(list);
//System.out.println(list.get(0));
}
return list.get(0);
}
//前序遍历
private static void preOrder(Node node){
if(node == null){
System.out.println("该树为空无法遍历");
}
System.out.print(node.getValue() + " ");
if(node.getLeft()!=null){
preOrder(node.getLeft());
}
if(node.getRight()!=null){
preOrder(node.getRight());
}
}
}
//节点类
@Data
public class Node implements Comparable<Node>{
private int value;
private Node left;
private Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node node) {
//从小到大
return this.value-node.getValue();
}
}
打印验证
哈夫曼编码
基本介绍
- 哈夫曼编码(Huffman Coding)是一种编码方式属于一种程序算法
- 哈夫曼编码是哈夫曼树在电讯通信中的经典应用之一
- 哈夫曼编码广泛地用于文件压缩,其压缩率通常在20%~90%之间
- 哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
原理分析
- 通信领域中信息的处理方式1-定长编码
- 通信领域中信息的处理方式2-变长编码
- 通信领域中信息的处理方式3-哈夫曼编码
传输的字符串:- i like like like java do you like a java
- d:1 y:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9
- 按照上面字符出现的次数构建一颗哈夫曼树,次数作为权值
构成哈夫曼树的步骤
1.从小到大进行排序,将每一个数据,每个数据都是一个节点。每个节点可以看成一颗最简单的二叉树
2.取出根节点权值最小的两颗二叉树
3.组成一颗新的二叉树 ,该新的二叉树的根节点的权值是前面两颗二叉树节点的权值的和
4.在将这颗新的二叉树,以根节点的权值大小再次排序,再次排序,不断重复1-2-3-4步骤,知道数列中,所有的数都被处理,就得到一颗哈夫曼树 - 根据哈夫曼树给各个字符,规定编码(前缀编码),向左的路径为0,向右的路径为1,
编码如下:
o:1000 u:10010 d:100110 y:100111 i:101 a:110 k:1110 e:1111 j 0000 v:0001 l 001 空格:01 - 按照上面的哈夫曼编码,我们的"i like like like java do you like a java"字符串对应的编码为(注意这里我们使用的无损压缩)
得到形式如: 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
压缩后长度为133
说明
原长度是359,压缩了(359-133)/359=62.9%
此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多亿性哈夫曼编码是无损处理方案
注意事项
这个赫夫曼树根据排序方法不用,也可能不太一样,这样对应的哈夫曼编码也不完全一样,但是wpl是一样的,都是最小的,最后生成的哈夫曼编码长度是一样, 比如:如果让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树会改变:
最佳实践-数据压缩
将给定的一段文本,比如’i like like like java do you like a java’,根据前面的讲解的赫夫曼编码原理,对其进行数据压缩处理,形式如:
‘1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100’
创建赫夫曼树
功能根据哈夫曼编码压缩数据原理,需要创建’i like like like java do you like a java’对应的哈夫曼树
思路
- Node{data(存放数据),weight(权值),left和right}
- 得到’i like like like java do you like a java’对应的byte[]数组
- 编写一个方法,将准备构建哈夫曼的树Node节点放到list,形式[Node{data=7 weight=5},Node{data=7 weight=5}…],体现d:1 y:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9
- 可以通过list创建对应的哈夫曼树
代码实现
Node 节点类
@Data
public class Node implements Comparable<Node>{
//存放数据本身 比如 'a'->97
private Byte data;
private int weight;
private Node left;
private Node right;
public Node(Byte data,int weight){
this.weight = weight;
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node node) {
//从小到大
return this.weight-node.getWeight();
}
public void preOrder(){
System.out.println(this);
if(this.getLeft()!=null){
this.getLeft().preOrder();
}
if(this.getRight()!=null){
this.getRight().preOrder();
}
}
}
生成哈夫曼树
public class HuffmanCode {
//生成哈夫曼对应的哈夫曼编码
//将哈夫曼树放在Map<Byte,String> 大概形式 a->100 d->11000
static Map<Byte,String> huffmanCode = new HashMap<>();
//在生成哈夫曼编码时,需要拼接路径 定义一个sb 存储某个叶子节点路径
static StringBuilder sb = new StringBuilder();
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] bytes = str.getBytes();
//构建字节node list
List<Node> list = getNodeList(bytes);
//System.out.println(list);
//创建哈夫曼树
Node huffmanTree = createHuffmanTree(list);
huffmanTree.preOrder();
}
private static Node createHuffmanTree(List<Node> list) {
if(list.size()==0){
System.out.println("空list,无法构建哈夫曼树");
}
Collections.sort(list);
while(list.size()>1){
Node left = list.get(0);
Node right = list.get(1);
Node parent = new Node(null,left.getWeight()+right.getWeight());
parent.setLeft(left);
parent.setRight(right);
list.remove(left);
list.remove(right);
list.add(parent);
Collections.sort(list);
}
return list.get(0);
}
//构建字节node list
private static List<Node> getNodeList(byte[] bytes) {
//定义返回值
List<Node> list = new ArrayList<>();
//统计每个元素出现的次数
Map<Byte,Integer> map = new HashMap<>();
for (byte b:bytes) {
Integer count = map.get(b);
if(count==null){
map.put(b,1);
}else{
map.put(b,++count);
}
}
//将对应的node封装传入list
for (Map.Entry<Byte,Integer> entry:map.entrySet()) {
list.add(new Node(entry.getKey(),entry.getValue()));
}
return list;
}
}
生成赫夫曼编码与压缩数据
- 我们已经生成了哈夫曼树对应的哈夫曼编码
如:32(space)=01, 97(a)=100, 100(d)=11000, 117(u)=11001, 101(e)=1110, 118(v)=11011, 105(i)=101, 121(y)=11010, 106(j)=0010, 107(k)=1111, 108(l)=000, 111(o)=0011 - 使用哈夫曼编码来生成哈夫曼编码数据,即按照上面哈夫曼编码将’i like like like java do you like a java’字符串生成对应的编码数据,形式如下:
‘1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100’ - 思路已经分析过了,现在开始实现
代码实现
//主方法调用测试
//创建哈夫曼编码
Map<Byte,String> mapCode = getCodes(huffmanTree);
System.out.println("生成的哈夫曼编码表:"+mapCode);
//压缩
byte[] zip = zip(bytes,mapCode);
System.out.println("压缩后的byte:"+Arrays.toString(zip));
//重载getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null){
return null;
}
//处理root的左子树
getCodes(root.getLeft(),"0",sb);
getCodes(root.getRight(),"1",sb);
return huffmanCode;
}
/**
* 将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到huffmanCode集合中
* @param node 传入节点 默认根节点
* @param code 路径左子节点为0 右子节点为1
* @param sber 拼接路径
*/
private static void getCodes(Node node,String code,StringBuilder sber){
StringBuilder stringBuilder = new StringBuilder(sber);
stringBuilder.append(code);
if(node != null){
if(node.getData()==null){//非叶子节点
getCodes(node.getLeft(),"0",stringBuilder);
getCodes(node.getRight(),"1",stringBuilder);
}else{
huffmanCode.put(node.getData(),stringBuilder.toString());
}
}
}
/**
* 将字符串对应的byte[]数组 通过生成的哈夫曼编码表 返回一个哈夫曼编码 压缩后的byte[]数组
* @param bytes 原始字符串对应的byte[]数组
* @param mapCode 生成的哈夫曼编码
* @return 当前列子会返回 01字符串(哈夫曼树不同 但长度会相同) 101
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> mapCode) {
//1 先用哈夫曼编码把 byte 转换成huffman 对应的字符串
StringBuffer sb = new StringBuffer();
for (byte b:bytes) {
sb.append(mapCode.get(b));
}
System.out.println("huffman对应的字符串:"+sb.toString());
// 将数上面得到的字符串转换为一个byte数组
int num = (sb.length()+7)/8;
byte[] huffmanCodeByte = new byte[num];
//第几个byte
int index = 0;
for (int i = 0; i < sb.length(); i+=8) {
String strByte;
if(i+8>sb.length()){//不够8位
strByte = sb.substring(i);
}else{
strByte = sb.substring(i,i+8);
}
huffmanCodeByte[index++] = (byte) Integer.parseInt(strByte,2);// 二进制
}
return huffmanCodeByte;
}
赫夫曼编码解码
使用哈夫曼编码来解码数据,具体要求是
- 前面得到的哈夫曼编码和对应的编码byte[]:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
- 现在要求使用赫夫曼编码进行解码,又重新回到原来的字符串’i like like like java do you like a java’
- 解码过程就是编码的一个逆向过程操作
代码实现
// 主方法的测试代码
//解码
byte[] source = decode(huffmanCode,huffmanZip);
System.out.println("字符串对应的asc码:"+Arrays.toString(source));
System.out.println("原来字符串为:"+new String(source));
/**
* 将byte 转成一个二进制的字符串
* @param flag 标志当前是否需要补高位 如果是true 需要补高位
* @param b 传入的byte
* @return
*/
private static String byteToBitString(boolean flag,byte b){
int temp = b;//向上强转 b 变成int
//如果是整数 需要补高位
if(flag){//如果当前长度没有8位 不用高位补码
temp |= 256; //按位与 256 = 1 0000 0000 1 = 0000 00001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); // 返回temp 对应二进制的补码
if(flag){
return str.substring(str.length()-8);//取最后8位
}else{
return str;
}
}
/**
* 编写个方法 完成对压缩数据的解码
* @param huffmanCode huffman 编码
* @param huffmanZip Huffman编码得到的字节数组
* @return 原来字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCode, byte[] huffmanZip) {
//1 先得到 HuffmanBytes 的二进制的字符串 形式为"101010..."
StringBuffer sb = new StringBuffer();
boolean flag = false;
for (int i = 0;i<huffmanZip.length;i++){
if(huffmanZip.length-1==i){//最后一个字节
flag = true;
}
sb.append(byteToBitString(!flag,huffmanZip[i]));
}
System.out.println("哈夫曼树对应的二进制字符串:"+sb.toString());
//2 把字符串按照指定的哈夫曼编码进行解码
//2.1需要对哈夫曼编码进行反向查询 a->97 97->a
Map<String,Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry:huffmanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
System.out.println("哈夫曼编码反向结果:"+map);
//2.2 创建一个list 存放byte
List<Byte> list = new ArrayList<>();
for (int i = 0; i < sb.length();) {
int count = 1; //计数器
flag = true ;
Byte b = null;
while(flag){
//一个一个的取出 组建key 去map={000=108, 01=32, 100=97,...}中匹配
String key = sb.substring(i,i+count); // i先不动 让count 移动 知道匹配最后一个字符
b = map.get(key);
if(b == null){//没有匹配到 继续加
count++;
}else{//匹配到了
flag = false;
}
}
//将字符对应的ask码放入list
list.add(b);
i += count;
}
//3 把list中的数据放入byte数组
byte[] b = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
b[i] =list.get(i);
}
return b;
}
整体代码
节点类
@Data
public class Node implements Comparable<Node>{
//存放数据本身 比如 'a'->97
private Byte data;
private int weight;
private Node left;
private Node right;
public Node(Byte data,int weight){
this.weight = weight;
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node node) {
//从小到大
return this.weight-node.getWeight();
}
public void preOrder(){
System.out.println(this);
if(this.getLeft()!=null){
this.getLeft().preOrder();
}
if(this.getRight()!=null){
this.getRight().preOrder();
}
}
}
主要方法与测试代码
public class HuffmanCode {
//生成哈夫曼对应的哈夫曼编码
//将哈夫曼树放在Map<Byte,String> 大概形式 a->100 d->11000
static Map<Byte,String> huffmanCode = new HashMap<>();
//在生成哈夫曼编码时,需要拼接路径 定义一个sb 存储某个叶子节点路径
static StringBuilder sb = new StringBuilder();
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("原始byte数组:"+Arrays.toString(bytes));
// 分步演示
//1 构建字节node list
List<Node> list = getNodeList(bytes);
System.out.println("节点集合:"+list);
//2 创建哈夫曼树
Node huffmanTree = createHuffmanTree(list);
System.out.println("前序遍历哈夫曼树:");
huffmanTree.preOrder();
//3 创建哈夫曼编码
Map<Byte,String> mapCode = getCodes(huffmanTree);
System.out.println("生成的哈夫曼编码表:"+mapCode);
//4 压缩
byte[] zip = zip(bytes,mapCode);
System.out.println("压缩后的byte:"+Arrays.toString(zip));
//封装方法
byte[] huffmanZip = huffmanZip(bytes);
System.out.println("压缩后的byte~:"+Arrays.toString(zip));
//解码
byte[] source = decode(huffmanCode,huffmanZip);
System.out.println("字符串对应的asc码:"+Arrays.toString(source));
System.out.println("原来字符串为:"+new String(source));
}
/**
* 将byte 转成一个二进制的字符串
* @param flag 标志当前是否需要补高位 如果是true 需要补高位
* @param b 传入的byte
* @return
*/
private static String byteToBitString(boolean flag,byte b){
int temp = b;//向上强转 b 变成int
//如果是整数 需要补高位
if(flag){//如果当前长度没有8位 不用高位补码
temp |= 256; //按位与 256 = 1 0000 0000 1 = 0000 00001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); // 返回temp 对应二进制的补码
if(flag){
return str.substring(str.length()-8);//取最后8位
}else{
return str;
}
}
/**
* 编写个方法 完成对压缩数据的解码
* @param huffmanCode huffman 编码
* @param huffmanZip Huffman编码得到的字节数组
* @return 原来字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCode, byte[] huffmanZip) {
//1 先得到 HuffmanBytes 的二进制的字符串 形式为"101010..."
StringBuffer sb = new StringBuffer();
boolean flag = false;
for (int i = 0;i<huffmanZip.length;i++){
if(huffmanZip.length-1==i){//最后一个字节
flag = true;
}
sb.append(byteToBitString(!flag,huffmanZip[i]));
}
System.out.println("哈夫曼树对应的二进制字符串:"+sb.toString());
//2 把字符串按照指定的哈夫曼编码进行解码
//2.1需要对哈夫曼编码进行反向查询 a->97 97->a
Map<String,Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry:huffmanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
System.out.println("哈夫曼编码反向结果:"+map);
//2.2 创建一个list 存放byte
List<Byte> list = new ArrayList<>();
for (int i = 0; i < sb.length();) {
int count = 1; //计数器
flag = true ;
Byte b = null;
while(flag){
//一个一个的取出 组建key 去map={000=108, 01=32, 100=97,...}中匹配
String key = sb.substring(i,i+count); // i先不动 让count 移动 知道匹配最后一个字符
b = map.get(key);
if(b == null){//没有匹配到 继续加
count++;
}else{//匹配到了
flag = false;
}
}
//将字符对应的ask码放入list
list.add(b);
i += count;
}
//3 把list中的数据放入byte数组
byte[] b = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
b[i] =list.get(i);
}
return b;
}
/**
* 优化封装 便于调用
* @param bytes
* @return
*/
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodeList = getNodeList(bytes);
//创建哈夫曼树
Node huffmanTree = createHuffmanTree(nodeList);
//获取哈夫曼树对应的哈夫编码
Map<Byte, String> codes = getCodes(huffmanTree);
//根据生成的哈夫曼编码对原来的数据进行分装 得到压缩后的哈夫曼树的编码
byte[] zip = zip(bytes, codes);
return zip;
}
/**
* 将字符串对应的byte[]数组 通过生成的哈夫曼编码表 返回一个哈夫曼编码 压缩后的byte[]数组
* @param bytes 原始字符串对应的byte[]数组
* @param mapCode 生成的哈夫曼编码
* @return 当前列子会返回 01字符串(哈夫曼树不同 但长度会相同) 101
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> mapCode) {
//1 先用哈夫曼编码把 byte 转换成huffman 对应的字符串
StringBuffer sb = new StringBuffer();
for (byte b:bytes) {
sb.append(mapCode.get(b));
}
System.out.println("huffman对应的字符串:"+sb.toString());
// 将数上面得到的字符串转换为一个byte数组
int num = (sb.length()+7)/8;
byte[] huffmanCodeByte = new byte[num];
//第几个byte
int index = 0;
for (int i = 0; i < sb.length(); i+=8) {
String strByte;
if(i+8>sb.length()){//不够8位
strByte = sb.substring(i);
}else{
strByte = sb.substring(i,i+8);
}
huffmanCodeByte[index++] = (byte) Integer.parseInt(strByte,2);// 二进制
}
return huffmanCodeByte;
}
//重载getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null){
return null;
}
//处理root的左子树
getCodes(root.getLeft(),"0",sb);
getCodes(root.getRight(),"1",sb);
return huffmanCode;
}
/**
* 将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到huffmanCode集合中
* @param node 传入节点 默认根节点
* @param code 路径左子节点为0 右子节点为1
* @param sber 拼接路径
*/
private static void getCodes(Node node,String code,StringBuilder sber){
StringBuilder stringBuilder = new StringBuilder(sber);
stringBuilder.append(code);
if(node != null){
if(node.getData()==null){//非叶子节点
getCodes(node.getLeft(),"0",stringBuilder);
getCodes(node.getRight(),"1",stringBuilder);
}else{
huffmanCode.put(node.getData(),stringBuilder.toString());
}
}
}
private static Node createHuffmanTree(List<Node> list) {
if(list.size()==0){
System.out.println("空list,无法构建哈夫曼树");
}
Collections.sort(list);
while(list.size()>1){
Node left = list.get(0);
Node right = list.get(1);
Node parent = new Node(null,left.getWeight()+right.getWeight());
parent.setLeft(left);
parent.setRight(right);
list.remove(left);
list.remove(right);
list.add(parent);
Collections.sort(list);
}
return list.get(0);
}
//构建字节node list
private static List<Node> getNodeList(byte[] bytes) {
//定义返回值
List<Node> list = new ArrayList<>();
//统计每个元素出现的次数
Map<Byte,Integer> map = new HashMap<>();
for (byte b:bytes) {
Integer count = map.get(b);
if(count==null){
map.put(b,1);
}else{
map.put(b,++count);
}
}
//将对应的node封装传入list
for (Map.Entry<Byte,Integer> entry:map.entrySet()) {
list.add(new Node(entry.getKey(),entry.getValue()));
}
return list;
}
}
部分打印信息
最佳实践-文件压缩
我们学习了通过哈夫曼编码对一个字符串进行编码和解码,下面我们来完成对文件的压缩和解压,
具体要求:给你一个图片文件,要求对其进行无损压缩,看看压缩效果如果
思路:读取文件->哈夫曼编码->完成压缩
代码实现
/**
*
* @param srcFile 源文件
* @param dstFile 压缩文件
*/
private 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);
oos = new ObjectOutputStream(os);
//这里以对象流的方式写入哈夫曼编码的字节数组
oos.write(huffmanBytes);//先把huffmanBytes写入
oos.writeObject(huffmanCode);//写入哈夫曼编码 用于恢复数据使用
}catch (Exception e){
e.printStackTrace();
}finally {
try{
oos.close();
os.close();
is.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
最佳实践-文件解压
代码实现
/**
*
* @param zipFile 压缩的文件
* @param dstFile 解压后的源文件
*/
private static void unZipFile(String zipFile, String dstFile) {
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
try{
is = new FileInputStream(zipFile);
ois = new ObjectInputStream(is);
//读取byte数组 huffmanBytes
byte[] huffmanBytes = (byte[])ois.readObject();
//读取哈夫曼码
Map<Byte,String> huffmanCode = (Map<Byte,String>) ois.readObject();
//解码
byte[] bytes = decode(huffmanCode,huffmanBytes);
//将数据写到 dstFile中
os = new FileOutputStream(dstFile);
os.write(bytes);
}catch (Exception e){
e.printStackTrace();
}finally {
try{
os.close();
ois.close();
is.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
哈夫曼编码注意事项
- 如果文件本身就是经过压缩处理的,那么使用哈夫曼编码再压缩效率不会有明显变化,比如视频,ppt等等文件
- 哈夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显。
二叉树序树
先看一个需求
给一个数列(7,3,10,12,5,1,9),要求能够高效的完成对数据的查询和添加
解决方案分析
- 使用数组
数组未排序,优点:直接早数组尾部添加,速度快。缺点:查找速度慢
数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需要整体移动,速度慢 - 使用链式存储-链表
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动 - 使用二叉树
二叉树序树:BST(binary sort(search) tree)对于二叉序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大
特别说明:如果有相同的值,可以将该节点存放在左子节点活右节点
比如 针对前面的数据(7,3,10,12,5,1,9),对应的二叉树排序树为:
二叉排序树创建与遍历
代码实现
因为比较简单就直接上代码实现。
节点类
@Data
public class Node {
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
public void add(Node node){
if(node == null){
return;
}
if(this.getValue()>node.getValue()){
if(this.getLeft() != null){
this.getLeft().add(node);
}else {
this.setLeft(node);
}
}else{
if(this.getRight() != null){
this.getRight().add(node);
}else{
this.setRight(node);
}
}
}
public void midOrder(){
if(this.getLeft()!=null){
this.getLeft().midOrder();
}
System.out.print(this +" ");
if(this.getRight()!=null){
this.getRight().midOrder();
}
}
public void preOrder(){
System.out.print(this +" ");
if(this.getLeft()!=null){
this.getLeft().preOrder();
}
if(this.getRight()!=null){
this.getRight().preOrder();
}
}
}
二叉排序树
@Data
public class BinarySortTree {
private Node root;
public void add(Node node){
if(root == null){
root = node;
}else{
root.add(node);
}
}
public void midOrder(){
if(root == null){
System.out.println("空树,无法遍历");
return;
}
root.midOrder();
}
public void preOrder(){
if(root == null){
System.out.println("空树,无法遍历");
return;
}
root.preOrder();
}
}
测试类
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr ={7,3,10,12,5,1,9};
BinarySortTree tree = new BinarySortTree();
for (int i:arr) {
Node node = new Node(i);
tree.add(node);
}
tree.midOrder();
// tree.preOrder();
}
}
二叉排序树的删除
二叉排序树的删除情况比较复杂,有三种情况需要考虑
- 删除叶子节点(比如:2,5,9,12)
- 删除只有一颗子树的节点(比如:1)
- 删除有两颗子树的节点(比如:7,3,10)
思路分析
1 删除叶子节点(比如:2,5,9,12)
- 先去找到要删除的节点 targetNode
- 找到targetNode的父节点 parent
- 确定targetNode是parent的左子节点 还是右子节点
- 根据前面的情况来对应删除
左子节点:parent.left = null
右子节点:parent.right = null
2 删除只有一颗子树的节点(比如:1)
- 先去找到要删除的节点 targetNode
- 找到targetNode的父节点 parent
- 确定targetNode是否有子节点,如果有是左子节点 还是右子节点
- 确定targetNode是parent的左子节点 还是右子节点
- 如果targetNode有左子节点
如果targetNode是parent左子节点:
parent.left = targetNode.left
如果targetNode是parent右子节点:
parent.right = targetNode.left - 如果targetNode有右子节点
如果targetNode是parent左子节点:
parent.left = targetNode.right
如果targetNode是parent右子节点:
parent.right = targetNode.right
3 删除有两颗子树的节点(比如:7,3,10)
- 先去找到要删除的节点 targetNode
- 找到targetNode的父节点 parent
- 从targetNode的右子树找到最小的节点
- 用一个临时变量,将最小节点的值保存temp
- 删除该最小节点
- targetNode.value = temp
代码实现
Node节点类添加方法
/**
* 查找要删除的对象节点 targetNode
* @param value
*/
public Node search(int value){
//当前对象就是value对象
if(this.getValue()==value){
return this;
}
//查询value小于当前对象值 说明在往左查询 否则往右查询
else if(value<this.getValue()){
if(this.getLeft()!=null){
return this.getLeft().search(value);
}
return null;
}else {
if(this.getRight()!=null){
return this.getRight().search(value);
}
return null;
}
}
/**
* 查找要删除对象的父节点
* @param value 希望删除节点的值
* @return
*/
public Node searchParent(int value){
if((this.getLeft()!=null&&this.getLeft().getValue()==value)
||(this.getRight()!=null&&this.getRight().getValue()==value)){
return this;
}else {
// 查询value小于当前对象值 说明需要继续往左查询 否则往右查询
if(value<this.getValue()&&this.getLeft()!=null){
return this.getLeft().searchParent(value);
}else if (value>=this.getValue()&&this.getRight()!=null){
return this.getRight().searchParent(value);
}else{
return null;
}
}
}
BinarySortTree类添加方法
/**
* 删除方法
* @param value 删除节点的值
*/
public void delNode(int value){
if(root == null){
return;
}else{
//查到要删除的节点
Node targetNode = root.search(value);
if(root.getLeft()==null&&root.getRight()==null){
root =null;
return;
}
//查找要删除节点的父节点
Node parent = root.searchParent(value);
// 1 删除叶子节点
if(targetNode.getLeft()==null&&targetNode.getRight()==null){
if(parent.getValue()>value){
parent.setLeft(null);
}else{
parent.setRight(null);
}
}else if(targetNode.getLeft()!=null&&targetNode.getRight()!=null){// 2 删除2颗节点
int min = delRightTreeMin(targetNode);
//将要删除的值替换为 最小的值
targetNode.setValue(min);
}else{//3 删除只有一颗子树的节点
if(targetNode.getLeft()!=null){
if(parent.getLeft().getValue()==value){
parent.setLeft(targetNode.getLeft());
}else{
parent.setRight(targetNode.getLeft());
}
}else{
if(parent.getLeft().getValue()==value){
parent.setLeft(targetNode.getRight());
}else {
parent.setRight(targetNode.getRight());
}
}
}
}
}
/**
* 找到要删除的节点右子节点开始的最小节点(右子节点向左走) 返回该节点的值
* 删除该节点
* @param node 传入要删除的节点
* @return 返回从要删除的节点的右子节点开始的最小节点(右子节点的向左走)
*/
public int delRightTreeMin(Node node){
Node target = node.getRight();
while(target.getLeft()!=null){
target = target.getLeft();
}
//删除最小的值
delNode(target.getValue());
// 并将最下的值返回给
return target.getValue();
}
delRightTreeMin(Node node)也可以从最左变找到最大的值,然后删除返回替换;有时间可以练习。
二叉排序树的注意事项
如果删的数列为 (10,1),且删除10 这个根节点时,会报空指针异常,所有需要加非空校验;
修改代码如下
/**
* 删除方法
* @param value 删除节点的值
*/
public void delNode(int value){
if(root == null){
return;
}else{
//查到要删除的节点
Node targetNode = root.search(value);
if(root.getLeft()==null&&root.getRight()==null){
root =null;
return;
}
//查找要删除节点的父节点
Node parent = root.searchParent(value);
// 1 删除叶子节点
if(targetNode.getLeft()==null&&targetNode.getRight()==null){
if(parent.getValue()>value){
parent.setLeft(null);
}else{
parent.setRight(null);
}
}else if(targetNode.getLeft()!=null&&targetNode.getRight()!=null){// 2 删除2颗节点
int min = delRightTreeMin(targetNode);
//将要删除的值替换为 最小的值
targetNode.setValue(min);
}else{//3 删除只有一颗子树的节点
if(targetNode.getLeft()!=null){
if(parent != null){
if(parent.getLeft().getValue()==value){
parent.setLeft(targetNode.getLeft());
}else{
parent.setRight(targetNode.getLeft());
}
}else{
root = targetNode.getLeft();
}
}else{
if(parent != null){
if(parent.getLeft().getValue()==value){
parent.setLeft(targetNode.getRight());
}else {
parent.setRight(targetNode.getRight());
}
}else{
root = targetNode.getRight();
}
}
}
}
}
平衡二叉树(AVL树)
看一个案例(说明二叉排序树可能的问题)
给一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST),并分析问题所在
左边BST存在的问题分析:
- 左子树全部为空,从形式上看,更像一个单链表
- 插入速度没有影响
- 查询速度明显降低(因为需要一次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
解决方案-平衡二叉树(AVL)
基本介绍
- 平衡二叉树也叫平衡二叉搜索树(self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高
- 具有以下特点: 它是一颗空树 或 它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树。
平衡二叉树的常用实现方法有 红黑树、AVL 、替罪羊树、Treap、伸展树等。
单旋转(左、右旋转)
要求:给你一个数列,创建出对应的平衡二叉树,数列{4,3,6,5,7,8}
左旋转思路分析
//左旋~根节点为当前节点
public void leftRotate(){
// 1创建一个新的节点,为当前节点的值
Node newNode = new Node(this.getValue());
// 2把这个新的节点的左子树设置当前节点的左子树
newNode.setLeft(this.getLeft());
// 3把新的节点右子树设置成当前节点的右子树的左子树
newNode.setRight(this.getRight().getLeft());
// 4把当前节点的值换成右子节点的值
this.setValue(this.getRight().getValue());
// 5把当前节点右子树设置成右子树的右子树
this.setRight(this.getRight().getRight());
// 6 把当前节点左子树设置新节点
this.setLeft(newNode);
}
要求:给你一个数列,创建出对应的平衡二叉树,数列{10,12,8,9,7,6}
左旋转思路
//右旋~根节点为当前节点
public void rightRotate(){
Node newNode = new Node(this.getValue());
newNode.setRight(this.getRight());
newNode.setLeft(this.getLeft().getRight());
this.setValue(this.getLeft().getValue());
this.setLeft(this.getLeft().getLeft());
this.setRight(newNode);
}
双旋转
前面的两个数列,进行单旋转就可以将非平衡二叉树转成平衡二叉树树,但是在某些情况下 ,单旋转不能完成平衡二叉树的转换;比如
int[] arr = { 10, 11, 7, 6, 8, 9 };运行原来的代码可以看到,并不是AVL树
public void add(Node node){
if(node == null){
return;
}
if(this.getValue()>node.getValue()){
if(this.getLeft() != null){
this.getLeft().add(node);
}else {
this.setLeft(node);
}
}else{
if(this.getRight() != null){
this.getRight().add(node);
}else{
this.setRight(node);
}
}
//当添加完一个节点后 如果右子树高度-左子树高度 > 1 左旋转
if(rightHeight()-leftHeight()>1){
//如果 右子树的左高度 > 右子树右高度
if(this.getRight()!=null &&
this.getRight().leftHeight()>this.getRight().rightHeight()){
//先对右子节点进行右旋转
this.getRight().rightRotate();
// 然后在对当前节点进行左旋转
leftRotate();
}else{
leftRotate();
}
return;
}
//当添加完一个节点后 如果左子树高度-右子树高度 > 1 右旋转
if(leftHeight()-rightHeight()>1){
if(this.getLeft()!=null &&
this.getLeft().rightHeight()>this.getLeft().leftHeight()){
this.getLeft().leftRotate();
rightRotate();
}else{
rightRotate();
}
}
}
整体代码实现
节点类
@Data
public class Node {
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//左旋~根节点为当前节点
public void leftRotate(){
// 1创建一个新的节点,为当前节点的值
Node newNode = new Node(this.getValue());
// 2把这个新的节点的左子树设置当前节点的左子树
newNode.setLeft(this.getLeft());
// 3把新的节点右子树设置成当前节点的右子树的左子树
newNode.setRight(this.getRight().getLeft());
// 4把当前节点的值换成右子节点的值
this.setValue(this.getRight().getValue());
// 5把当前节点右子树设置成右子树的右子树
this.setRight(this.getRight().getRight());
// 6 把当前节点左子树设置新节点
this.setLeft(newNode);
}
//右旋~根节点为当前节点
public void rightRotate(){
Node newNode = new Node(this.getValue());
newNode.setRight(this.getRight());
newNode.setLeft(this.getLeft().getRight());
this.setValue(this.getLeft().getValue());
this.setLeft(this.getLeft().getLeft());
this.setRight(newNode);
}
public int leftHeight(){
if(this.left == null){
return 0;
}
return this.left.height();
}
public int rightHeight(){
if(this.right==null){
return 0;
}
return this.right.height();
}
//获取高度
public int height(){
return Math.max(this.getLeft()==null?0:this.left.height(),
this.getRight()==null?0:this.getRight().height())+1;
}
/**
* 查找要删除的对象节点 targetNode
* @param value
*/
public Node search(int value){
//当前对象就是value对象
if(this.getValue()==value){
return this;
}
//查询value小于当前对象值 说明在往左查询 否则往右查询
else if(value<this.getValue()){
if(this.getLeft()!=null){
return this.getLeft().search(value);
}
return null;
}else {
if(this.getRight()!=null){
return this.getRight().search(value);
}
return null;
}
}
/**
* 查找要删除对象的父节点
* @param value 希望删除节点的值
* @return
*/
public Node searchParent(int value){
if((this.getLeft()!=null&&this.getLeft().getValue()==value)
||(this.getRight()!=null&&this.getRight().getValue()==value)){
return this;
}else {
// 查询value小于当前对象值 说明需要继续往左查询 否则往右查询
if(value<this.getValue()&&this.getLeft()!=null){
return this.getLeft().searchParent(value);
}else if (value>=this.getValue()&&this.getRight()!=null){
return this.getRight().searchParent(value);
}else{
return null;
}
}
}
public void add(Node node){
if(node == null){
return;
}
if(this.getValue()>node.getValue()){
if(this.getLeft() != null){
this.getLeft().add(node);
}else {
this.setLeft(node);
}
}else{
if(this.getRight() != null){
this.getRight().add(node);
}else{
this.setRight(node);
}
}
//当添加完一个节点后 如果右子树高度-左子树高度 > 1 左旋转
if(rightHeight()-leftHeight()>1){
//如果 右子树的左高度 > 右子树右高度
if(this.getRight()!=null &&
this.getRight().leftHeight()>this.getRight().rightHeight()){
//先对右子节点进行右旋转
this.getRight().rightRotate();
// 然后在对当前节点进行左旋转
leftRotate();
}else{
leftRotate();
}
return;
}
//当添加完一个节点后 如果左子树高度-右子树高度 > 1 右旋转
if(leftHeight()-rightHeight()>1){
if(this.getLeft()!=null &&
this.getLeft().rightHeight()>this.getLeft().leftHeight()){
this.getLeft().leftRotate();
rightRotate();
}else{
rightRotate();
}
}
}
public void midOrder(){
if(this.getLeft()!=null){
this.getLeft().midOrder();
}
System.out.println(this +" ");
if(this.getRight()!=null){
this.getRight().midOrder();
}
}
}
测试类
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 };
AVLTree tree = new AVLTree();
for (int i:arr) {
Node node = new Node(i);
tree.add(node);
}
tree.midOrder();
System.out.println("AVL树高度:" +tree.getRoot().height());
System.out.println("left高度:" +tree.getRoot().leftHeight());
System.out.println("right高度:" +tree.getRoot().rightHeight());
}
}
AVLTree 和上面的BinarySortTree没有改动就不贴出来了
多路查找树
二叉树与B树
二叉树的问题分析
二叉树的操作效率较高,但是也存在问题
- 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿),就会存在如下问题:
- 在构建二叉树时,需要多次进行i/o操作(海量数据在时刻或者文件中),节点海量,构建二叉树时速度有影响
- 节点海量,也会影响二叉树的高度很大,会降低操作速度
多叉树
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点就是多叉树
多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
举例说明2-3树和2-3-4树
2-3树
2-3树是最简单的B树结构,具体如下特点:
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点交二节点,二节点要么没有子节点,要么有两子节点。
- 有三个子节点的节点交三节点,三节点要么没有子节点,要么有三子节点。
- 2-3树是由二节点和三节点构成的树
B树、B+树和B*树
B树
B-tree树及B树,B即balanced,平衡的意思。有人把B-tree翻译成B-树,容易让人造成误解。会以为B-树是一种树,而B树又是另一种树,实际上B-tree就是指的B树
B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率
- 如果B树通过重新组织节点,降低了树的高度
- 文件系统及数据库系统的设计者利用磁盘预读原理,将一个节点的大小设计为等于一个页(页的大小通常为4K),这样每个节点只需要一次i/o就可以完全载入
- 将树的度M设置为1024,在600亿个元素中最多只需要4次i/o就可以读取到想要的元素,B树广泛应用于文件存储系统及数据库系统中
前面介绍的2-3树和2-3-4树,他们就是B树,这里我们再做一个我说明:学习mysql时经常听说到某种类型的索引是B树或者B+树索引
- B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4
- B树的搜索,从根节点开始,对节点内关键字(有序)序列进行二分查找,如果命中则结束,否则进行查询关键字所属范围的儿子节点;重复,知道所有对应的儿子指针为空,或者已经是叶子节点
- 关键字结合分布在整课树中,即叶子节点和非叶子节点都存房数据
- 搜索有可能在非叶子节点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
B+树的介绍
B+树 是B树的变体,也是一种多路搜索树
说明:
- B+ 树的搜索与B树也是基本相同,区别是B+树只有达到叶子节点才命中(B树可以在非叶子节点命中),其性能也等价于在关键字全集内做一次二分查找
- 所有关键字都出现在叶子节点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的
- 不可能在非叶子节点命中
- 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然
B*树的介绍
B*树 是B+树的变体,在B+树的非根和非叶子节点再增加指向兄弟的指针
说明:
- B*树定义了非叶子及诶点关键字格式至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2
- 从第一个特点我们可以看出,B*树分配新及诶大的概率比B+树要低,控件使用率更高