超级详细树讲解二 —— 平衡二叉树、哈夫曼树图解+代码
首先很高兴你看到了这篇文章,这篇文章可能会花费你很长很长的时间去看,但是这篇文章包括的内容绝对足够你对树的一个系统性的学习。为什么要写这篇文字呢?因为自己在学习树的时候,有些博客只有图解,有些博客只有代码,在这里我将树的图解和代码都整理在了一起。在每个树都有着对应的图解和代码。相信这篇文字会花费你大量的时间,但是绝对值得。
因为全部加在一起太长了,所以分成了三部分去记录:
超级详细树讲解一 —— 二叉树、二叉查找树、完全二叉树图解+代码
超级详细树讲解二 —— 平衡二叉树、哈夫曼树图解+代码
因为博客的排版问题,但是感觉还是比较丑,如果大家想看好看的版本,大家可以前往pdf传送门进行下载。
pdf包含如下树:
代码的github地址:代码传送门
pdf的github地址:pdf传送门
文章目录
平衡二叉树
背景
当数据出现极端情况的时候,此时的二叉搜索树就变成了一个链表,并不能做到减少时间,此时候平衡二叉树就出来了。
定义
定义:一棵AVL树是其每个结点的左子树和右子树的高度最多相差1的二叉查找树(空树的高度为-1),这个差值也称为平衡因子
平衡二叉树代码
平衡二叉树节点定义
根平常树有点不一样,树节点多了一个当前节点的高度
package com.zejian.structures.Tree.AVLTree;
/**
* @Author : Auraros
* @Description :
* @Data : 2020-11-26 20:29
* @Version : 1.0
*/
public class AVLNode<T extends Comparable> {
public AVLNode<T> left;//左结点
public AVLNode<T> right;//右结点
public T data;
public int height;//当前结点的高度
public AVLNode(T data) {
this(null,null,data);
}
public AVLNode(AVLNode<T> left, AVLNode<T> right, T data) {
this(left,right,data,0);
}
public AVLNode(AVLNode<T> left, AVLNode<T> right, T data, int height) {
this.left=left;
this.right=right;
this.data=data;
this.height = height;
}
}
这里约定空结点(空子树)的高度为-1,叶子结点的高度为0,非叶子节点的高度则根据其子树的高度而计算获取,如下图:
平衡二叉树插入定义
由于任意结点的孩子结点最多有两个,而且导致失衡的必要条件是X结点的两棵子树高度差为2(大于1),因此一般只有以下4种情况可能导致X点失去平衡:
- 在结点X的左孩子结点的左子树中插入元素
- 在结点X的左孩子结点的右子树中插入元素
- 在结点X的右孩子结点的左子树中插入元素
- 在结点X的右孩子结点的右子树中插入元素
第①情况和第④情况是对称的,可以通过单旋转来解决
第②种情况和第③情况是对称的,需要双旋转来解决
平衡二叉树单旋转算法实现
一般情况下,我们把X结点称为失衡点,修复一棵被破坏的AVL树时,找到失衡点是很重要的并把通过一次旋转即可修复平衡的操作叫做单旋转
- 左左单旋转(LL)
代码实现如下:
/**
* 左左单旋转(LL旋转) w变为x的根结点, x变为w的右子树
* @param x
* @return
*/
private AVLNode<T> singleRotateLeft(AVLNode<T> x){
//把w结点旋转为根结点
AVLNode<T> w= x.left;
//同时w的右子树变为x的左子树
x.left=w.right;
//x变为w的右子树
w.right=x;
//重新计算x/w的高度
x.height=Math.max(height(x.left),height(x.right))+1;
w.height=Math.max(height(w.left),x.height)+1;
return w;//返回新的根结点
}
- 右右单旋转(RR)
代码实现:
/**
* 右右单旋转(RR旋转) x变为w的根结点, w变为x的左子树
* @return
*/
private AVLNode<T> singleRotateRight(AVLNode<T> w){
//把w的右节点为根节点
AVLNode<T> x = w.right;
//将x的左子树给w的右子树
w.right = x.left;
//将x的左子树指向w
x.left = w;
//重新计算x/w的高度
w.height=Math.max(height(w.left),height(w.right))+1;
x.height=Math.max(height(x.left),w.height)+1;
//返回新的根结点
return x;
}
平衡二叉树的双旋转算法与实现
这种情况下是单旋转没办法生效的。其实就是最深子树在最左最右的时候可以使用单螺旋,但是最深子树在中间的时候,无论如何单旋转都无法实现。
- 左右双旋转(LR)
在左右双旋转实例图123中,在原AVL树种插入结点7后,结点8变成了失衡点,此时需要把6结点变为根结点才能重新恢复平衡。因此先进行左向旋转再进行右向旋转,最后树恢复平衡。算法代码实现如下:
/**
* 左右旋转(LR旋转) x(根) w y 结点 把y变成根结点
* @return
*/
private AVLNode<T> doubleRotateWithLeft(AVLNode<T> x){
//w先进行RR旋转
x.left=singleRotateRight(x.left);
//再进行x的LL旋转
return singleRotateLeft(x);
}
- 右左双旋转(RL)
代码如下:
/**
* 右左旋转(RL旋转)
* @param w
* @return
*/
private AVLNode<T> doubleRotateWithRight(AVLNode<T> x){
//先进行LL旋转
x.right=singleRotateLeft(x.right);
//再进行RR旋转java
return singleRotateRight(x);
}
平衡二叉树插入方法的实现
根二叉查找树一样,先进行插入,再进行平衡判断,最后决定要不要旋转。
public AVLNode<T> insert(T data, AVLNode<T> node){
if(node == null){
node = new AVLNode<T>(data);
}else if(data.compareTo(node.data) < 0){
node.left = insert(data, node.left);
//插入后计算子树的高度,等于2则需要重新恢复平衡;由于是左边插入,左子树的高度肯定大于右子树的高度
if(height(node.left) - height(node.right) == 2){
//判断插入节点是左还是右
if(data.compareTo(node.left.data) < 0){
//进行LL旋转
node = singleRotateRight(node);
}else {
//进行左右旋转
node = doubleRotateWithLeft(node);
}
}
}else if(data.compareTo(node) > 0){
node.right = insert(data, node.right);
if(height(node.right) - height(node.left) == 2){
if(data.compareTo(node.right.data) < 0){
node = doubleRotateWithRight(node);
}else{
node = singleRotateRight(node);
}
}
}
else ;
node.height = Math.max(height(node.left), height(node.right)) + 1;
return node;
}
平衡二叉树删除定义
二叉查找树中删除方法的实现类似,但是在移除结点后需要进行平衡检测,以便判断是否需要进行平衡修复。
@Override
public void remove(T data) {
if(data == null)
throw new RuntimeException("data can not be null");
this.root = remove(data, root);
}
public AVLNode<T> remove(T data, AVLNode<T> node){
if(node == null)
return null;
int result = data.compareTo(node.data);
if(result < 0) {
node.left = remove(data, node.left);
//检测是否平衡
if (height(node.right) - height(node.left) == 2) {
AVLNode<T> currentNode = node.right;
//判断需要那种旋转
if (height(currentNode.left) > height(currentNode.right)) {
//RL
node = doubleRotateWithRight(node);
} else {
//RR
node = singleRotateRight(node);
}
}
}
//从右子树查找需要删除的元素
else if(result>0){
node.right=remove(data,node.right);
//检测是否平衡
if(height(node.left)-height(node.right)==2){
AVLNode<T> currentNode=node.left;
//判断需要那种旋转
if(height(currentNode.right)>height(currentNode.left)){
//LR
node=doubleRotateWithLeft(node);
}else{
//LL
node=singleRotateLeft(node);
}
}
}
//已找到需要删除的元素,并且要删除的结点拥有两个子节点
else if(node.right!=null&&node.left!=null){
//寻找替换结点
node.data=findMin(node.right).data;
//移除用于替换的结点
node.right = remove( node.data, node.right );
}
else {
//只有一个孩子结点或者只是叶子结点的情况
node=(node.left!=null)? node.left:node.right;
}
//更新高度值
if(node!=null)
node.height = Math.max( height(node.left ), height( node.right ) ) + 1;
return node;
}
其他方法都跟前面实现的二叉查找树的接口是一样的。
平衡二叉树全部代码
代码如下:
package src.Tree.AVLTree;
import src.Link.ArrayList;
import src.Tree.BinaryTree.BinaryNode;
import src.Tree.BinaryTree.BinarySearchTree;
/**
* @Author : Auraros
* @Description :
* @Data : 2020-12-03 20:00
* @Version : 1.0
*/
public class AVLTree <T extends Comparable> implements Tree<T> {
public AVLNode<T> root;
@Override
public boolean isEmpty() {
return root==null;
}
@Override
public int size() {
return size(root);
}
public int size(AVLNode<T> subtree){
if(subtree==null){
return 0;
}else {
return size(subtree.left) + 1 + size(subtree.right);
}
}
@Override
public int height() {
return height(root);
}
/**
* @param p
* @return
*/
public int height(AVLNode<T> p){
return p == null ? -1 : p.height;
}
@Override
public String preOrder() {
String sb=preOrder(root);
if(sb.length()>0){
//去掉尾部","号
sb=sb.substring(0,sb.length()-1);
}
return sb;
}
/**
* 先根遍历
* @param subtree
* @return
*/
public String preOrder(AVLNode<T> subtree){
StringBuilder sb =new StringBuilder();
if (subtree!=null) {
//先访问根结点
sb.append(subtree.data).append(",");
//访问左子树
sb.append(preOrder(subtree.left));
//访问右子树
sb.append(preOrder(subtree.right));
}
return sb.toString();
}
@Override
public String inOrder() {
String sb=inOrder(root);
if(sb.length()>0){
//去掉尾部","号
sb=sb.substring(0,sb.length()-1);
}
return sb;
}
/**
* 中根遍历
* @param subtree
* @return
*/
private String inOrder(AVLNode<T> subtree){
StringBuilder sb =new StringBuilder();
if (subtree!=null) {
//访问左子树
sb.append(inOrder(subtree.left));
//访问根结点
sb.append(subtree.data).append(",");
//访问右子树
sb.append(inOrder(subtree.right));
}
return sb.toString();
}
@Override
public String postOrder() {
String sb=postOrder(root);
if(sb.length()>0){
//去掉尾部","号
sb=sb.substring(0,sb.length()-1);
}
return sb;
}
/**
* 后根遍历
* @param subtree
* @return
*/
private String postOrder(AVLNode<T> subtree){
StringBuilder sb =new StringBuilder();
if (subtree!=null){
//访问左子树
sb.append(postOrder(subtree.left));
//访问右子树
sb.append(postOrder(subtree.right));
//访问根结点
sb.append(subtree.data).append(",");
}
return sb.toString();
}
@Override
public String levelOrder() {
/**
* @see BinarySearchTree#levelOrder()
* @return
*/
return null;
}
@Override
public void insert(T data) {
if(data == null)
throw new RuntimeException("data is not be null");
this.root = insert(data, root);
}
public AVLNode<T> insert(T data, AVLNode<T> node){
if(node == null){
node = new AVLNode<T>(data);
}
int result = data.compareTo(node.data);
if(result < 0){
node.left = insert(data, node.left);
//插入后计算子树的高度,等于2则需要重新恢复平衡;由于是左边插入,左子树的高度肯定大于右子树的高度
if(height(node.left) - height(node.right) == 2){
//判断插入节点是左还是右
if(data.compareTo(node.left.data) < 0){
//进行LL旋转
node = singleRotateRight(node);
}else {
//进行左右旋转
node = doubleRotateWithLeft(node);
}
}
}else if(result > 0){
node.right = insert(data, node.right);
if(height(node.right) - height(node.left) == 2){
if(data.compareTo(node.right.data) < 0){
node = doubleRotateWithRight(node);
}else{
node = singleRotateRight(node);
}
}
}
else ;
node.height = Math.max(height(node.left), height(node.right)) + 1;
return node;
}
/**
* 查找最小值结点
* @param p
* @return
*/
private AVLNode<T> findMin(AVLNode<T> p){
if (p==null)//结束条件
return null;
else if (p.left==null)//如果没有左结点,那么t就是最小的
return p;
return findMin(p.left);
}
@Override
public T findMin() {
return findMin(root).data;
}
@Override
public T findMax() {
return findMax(root).data;
}
/**
* 查找最大值结点
* @param p
* @return
*/
private AVLNode<T> findMax(AVLNode<T> p){
if (p==null)
return null;
else if (p.right==null)//如果没有右结点,那么t就是最大的
return p;
return findMax(p.right);
}
@Override
public BinaryNode findNode(T data) {
/**
* @see BinarySearchTree#findNode(Comparable)
* @return
*/
return null;
}
@Override
public boolean contains(T data) {
return data != null && contain(data, root);
}
public boolean contain(T data , AVLNode<T> subtree){
if (subtree==null)
return false;
int result =data.compareTo(subtree.data);
if (result<0){
return contain(data,subtree.left);
}else if(result>0){
return contain(data,subtree.right);
}else {
return true;
}
}
@Override
public void clear() {
this.root=null;
}
/**
* 左左单旋转(LL旋转)W变为x的根节点,x变为w的右子树
* @param x
* @return
*/
private AVLNode<T> singleRotateLeft(AVLNode<T> x){
//把w结点旋转为根节点
AVLNode<T> w = x.left;
//同时把w的右子树变成x的左子树
x.left = w.right;
//把x变成w的右子树
w.right = x;
//重新解释x/w的高度
x.height = Math.max(height(x.left), height(x.right)) + 1;
w.height = Math.max(height(w.left), x.height)+1;
return w;
}
/**
* 右右单旋转(RR旋转) x变为w的根结点, w变为x的左子树
* @return
*/
private AVLNode<T> singleRotateRight(AVLNode<T> w){
//把w的右节点为根节点
AVLNode<T> x = w.right;
//将x的左子树给w的右子树
w.right = x.left;
//将x的左子树指向w
x.left = w;
//重新计算x/w的高度
w.height=Math.max(height(w.left),height(w.right))+1;
x.height=Math.max(height(x.left),w.height)+1;
//返回新的根结点
return x;
}
/**
* 左右旋转(LR旋转) x(根) w y 结点 把y变成根结点
* @return
*/
private AVLNode<T> doubleRotateWithLeft(AVLNode<T> x){
//w先进行RR旋转
x.left=singleRotateRight(x.left);
//再进行x的LL旋转
return singleRotateLeft(x);
}
/**
* 右左旋转(RL旋转)
* @param x
* @return
*/
private AVLNode<T> doubleRotateWithRight(AVLNode<T> x){
//先进行LL旋转
x.right=singleRotateLeft(x.right);
//再进行RR旋转java
return singleRotateRight(x);
}
@Override
public void remove(T data) {
if(data == null)
throw new RuntimeException("data can not be null");
this.root = remove(data, root);
}
public AVLNode<T> remove(T data, AVLNode<T> node){
if(node == null)
return null;
int result = data.compareTo(node.data);
if(result < 0) {
node.left = remove(data, node.left);
//检测是否平衡
if (height(node.right) - height(node.left) == 2) {
AVLNode<T> currentNode = node.right;
//判断需要那种旋转
if (height(currentNode.left) > height(currentNode.right)) {
//RL
node = doubleRotateWithRight(node);
} else {
//RR
node = singleRotateRight(node);
}
}
}
//从右子树查找需要删除的元素
else if(result>0){
node.right=remove(data,node.right);
//检测是否平衡
if(height(node.left)-height(node.right)==2){
AVLNode<T> currentNode=node.left;
//判断需要那种旋转
if(height(currentNode.right)>height(currentNode.left)){
//LR
node=doubleRotateWithLeft(node);
}else{
//LL
node=singleRotateLeft(node);
}
}
}
//已找到需要删除的元素,并且要删除的结点拥有两个子节点
else if(node.right!=null&&node.left!=null){
//寻找替换结点
node.data=findMin(node.right).data;
//移除用于替换的结点
node.right = remove( node.data, node.right );
}
else {
//只有一个孩子结点或者只是叶子结点的情况
node=(node.left!=null)? node.left:node.right;
}
//更新高度值
if(node!=null)
node.height = Math.max( height(node.left ), height( node.right ) ) + 1;
return node;
}
private void printTree( AVLNode<T> t ) {
if( t != null )
{
printTree( t.left );
System.out.println( t.data );
printTree( t.right );
}
}
/**
* 测试
* @param arg
*/
public static void main(String arg[]){
AVLTree<Integer> avlTree=new AVLTree<>();
for (int i = 1; i <18 ; i++) {
avlTree.insert(i);
}
avlTree.printTree(avlTree.root);
//删除11,8以触发旋转平衡操作
avlTree.remove(11);
avlTree.remove(8);
System.out.println("================");
avlTree.printTree(avlTree.root);
System.out.println("findMin:"+avlTree.findMin());
System.out.println("findMax:"+avlTree.findMax());
System.out.println("15 exist or not : " + avlTree.contains(15) );
System.out.println("先根遍历:"+avlTree.preOrder());
System.out.println("中根遍历:"+avlTree.inOrder());
System.out.println("后根遍历:"+avlTree.postOrder());
}
}
哈夫曼树
定义
Huffman Tree,中文名是哈夫曼树或霍夫曼树,它是最优二叉树。
给定n个权值作为n个叶子结点,构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树。
概念
-
路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径,简单可以看有几条线。
100和80的路径长度是1,50和30的路径长度是2,20和10的路径长度是3。
-
结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
节点20的路径长度是3,它的带权路径长度= 路径长度 * 权 = 3 * 20 = 60。
-
树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
树的WPL= 1100 + 280 + 320 + 310 = 100 + 160 + 60 + 30 = 350。
建树理论
哈夫曼树是一个自底向上构建的二叉树。
首先得到:5 6 7 8 15 (从小到大排序)
最小的两个数相加: 11 7 8 15
再排序: 7 8 11 15
相加: 15 11 15
排序: 11 15 15
相加: 26 15
排序: 15 26
相加: 41
哈夫曼树接口定义
因为哈夫曼树是自底向上建立接口的,所以树的接口相对有一些改动,需要在每个方法面前导入节点。
package src.Tree.HuffmanTree;
/**
* @Author : Auraros
* @Description :
* @Data : 2020-11-25 2:27
* @Version : 1.0
*/
public interface Tree<T extends Comparable> {
/**
* 判断是否为空
*/
boolean isEmpty(HuffmanNode node);
/**
* 二叉树结点个数
*/
int size(HuffmanNode node);
/**
* 返回二叉树的高度或者深度,即结点的最大层次
*/
int height(HuffmanNode node);
/**
* 前序遍历
*
*/
String preOrder(HuffmanNode node);
/**
* 中序遍历
*
*/
String inOrder(HuffmanNode node);
/**
* 后序遍历
*/
String postOrder(HuffmanNode node);
/**
* 层次遍历
*/
String levelOrder( HuffmanNode node);
}
哈夫曼节点类定义
哈夫曼需要用权值进行建树,所以在存储data的基础上还需要一个值来存储权值,故:
package src.Tree.HuffmanTree;
/**
* @Author : Auraros
* @Description :
* @Data : 2020-12-03 22:00
* @Version : 1.0
*/
public class HuffmanNode<T> implements Comparable<HuffmanNode>{
public T data; //节点的权
public HuffmanNode<T> left; //左子节点
public HuffmanNode<T> right; //右子节点
public int weight;
public HuffmanNode(){};
public HuffmanNode(T data, int weight, HuffmanNode left, HuffmanNode right){
this.data = data;
this.left = left;
this.right = right;
this.weight = weight;
}
public HuffmanNode(T data, int weight){
this(data, weight, null, null);
}
@Override
public String toString() {
return "TreeNode[data=" + data + ", weight=" + weight + "]";
}
@Override
public int compareTo(HuffmanNode o) {
return this.weight - o.weight;
}
}
哈夫曼树创建定义
这里需要使用到小顶堆(优先队列的定义),创建树思路如上:
package src.Tree.HuffmanTree;
import java.util.*;
/**
* @Author : Auraros
* @Description :
* @Data : 2020-12-03 22:06
* @Version : 1.0
*/
public class HuffmanTree implements Tree{
public static HuffmanNode createHuffmanTree(ArrayList<HuffmanNode> huffmanNodeList){
PriorityQueue<HuffmanNode> pq = new PriorityQueue<>();
for(HuffmanNode node : huffmanNodeList){
pq.add(node);
}
while (pq.size()>1){
HuffmanNode left = pq.poll();
HuffmanNode right = pq.poll();
HuffmanNode root = new HuffmanNode(null, left.weight + right.weight, left, right);
pq.add(root);
}
return pq.poll();
}
@Override
public int size(HuffmanNode node) {
if(node == null){
return 0;
}else{
return size(node.right) + 1 + size(node.left);
}
}
@Override
public int height(HuffmanNode node) {
if(node == null){
return 0;
}else{
int left = height(node.left);
int right = height(node.right);
return (left - right) > 0 ? left + 1: right + 1; //加上当前层
}
}
@Override
public boolean isEmpty(HuffmanNode root) {
return root == null;
}
@Override
public String preOrder(HuffmanNode node) {
StringBuffer sb = new StringBuffer();
if (node ==null) {//递归结束条件
return "";
}
//先访问根结点
sb.append(node.weight+",");
//遍历左子树
sb.append(preOrder(node.left));
//遍历右子树
sb.append(preOrder(node.right));
return sb.toString().substring(0, sb.length()-1);
}
@Override
public String inOrder(HuffmanNode node) {
StringBuffer sb = new StringBuffer();
if(node==null){
return "";
}
sb.append(inOrder(node.left));
sb.append(node.weight + ",");
sb.append(inOrder(node.right));
return sb.toString().substring(0, sb.length()-1);
}
@Override
public String postOrder(HuffmanNode node) {
StringBuffer sb = new StringBuffer();
if(node == null){
return "";
}
sb.append(postOrder(node.left));
sb.append(postOrder(node.right));
sb.append(node.weight + ",");
return sb.toString().substring(0, sb.length()-1);
}
@Override
public String levelOrder(HuffmanNode root) {
LinkedList<HuffmanNode> queue = new LinkedList<>();
StringBuffer sb = new StringBuffer();
HuffmanNode node = root;
while(node!=null){
sb.append(node.weight + ",");
if(node.left!=null){
queue.add(node.left);
}
if(node.right!= null){
queue.add(node.right);
}
node = queue.poll();
}
return sb.toString().substring(0, sb.length()-1);
}
public static void main(String[] args) {
HuffmanTree huffmanTree = new HuffmanTree();
ArrayList<HuffmanNode> list = new ArrayList<>();
list.add(new HuffmanNode("A", 1));
list.add(new HuffmanNode("B", 1));
list.add(new HuffmanNode("C", 3));
list.add(new HuffmanNode("D", 4));
list.add(new HuffmanNode("E", 5));
list.add(new HuffmanNode("F", 6));
HuffmanNode root = createHuffmanTree(list);
System.out.println(huffmanTree.levelOrder(root));
}
}