前言
- 本文主要探讨平衡二叉树的实现过程,对于原理还请自行翻阅其它资料进行学习
1.平衡二叉树简介
1.1什么是平衡二叉树
了解平衡二叉树之前我们首先需要知道什么是树结构
- . 树结构
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。
树的定义:
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 树由若干个节点组成
- 如果一颗树不为空,那么至少拥有一个根节点,且根节点没有父节点
- 每个节点可以拥有若干个子节点
- 除了根节点以外,每个子节点可以分为多个不相交的子树
- 二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分 [1] 。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点 [1] 。
二叉树定义:
- 二叉树由若干个节点组成
- 如果一颗二叉树不为空,那么至少拥有一个根节点,且根节点没有父节点
- 每个节点可以拥有最多两个子节点,分别为左子节点和右子节点
- 若任意节点的左子树不空,则左子树上所有的节点值均小于该节点的值
- 若任意节点的右子树不空,则右子树上所有节点的值均大于该节点的值
- 每个子节点也必须符合2-5的规范
二叉树有以下5中基本形态:
二叉树的缺点:
虽然说二叉树能提高查找的效率 O(logn),但当插入的数据是一个有序的数列时,二叉树看起来像一个链表一样,搜索效率降低为O(n)
比如我插入的数据为{1,2,3,4,5,6}
为了避免这种情况存在,在 1962 年,一个姓 AV 的大佬(G. M. Adelson-Velsky) 和一个姓 L 的大佬( Evgenii Landis)提出「平衡二叉树」(AVL) 。
插入 {1,2,3,4,5,6} 这种数据结果演变为下图:
- 平衡二叉树
在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL 树得名于它的发明者 G. M. Adelson-Velsky 和 Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。
平衡二叉树定义:
- 平衡二叉树由若干个节点组成
- 如果一颗二叉树不为空,那么至少拥有一个根节点,且根节点没有父节点
- 每个节点可以拥有最多两个子节点,分别为左子节点和右子节点
- 若任意节点的左子树不空,则左子树上所有的节点值均小于该节点的值
- 若任意节点的右子树不空,则右子树上所有节点的值均大于该节点的值
- 没有键值相等的节点
- 左树和右树的高度差的绝对值不能超过1
- 每个子节点也必须符合3-7的规范
以下是一颗平衡二叉树(图1)
- 我们以根节点4为参照系,可以看到这颗二叉树不为空,有且仅有1个根节点,满足定义2
- 每个节点最多有两个子节点,满足定义3
- 根节点4的左子树2不为空,2的子节点为{1,3},则根节点4的左子树节点集合为[2,1,3],该集合的任意值小于根节点4的值,满足定义4
- 根节点4的右子树7不为空,7的子节点为{6,9},6的子节点为{5,null},9的子节点为{8,10},则根节点4的右子树节点集合为[7,6,9,5,8,10],该集合的任意值大于根节点4的值,满足定义5
- 该树没有值相等的节点,满足定义6
- 4的左子树深度为2,右子树深度为3,则左右高度差=左子树深度-右子树深度=2-3=-1,可以看到根节点头上有一个红色的-1,表示左右子树高度差,-1的绝对值是1,满足定义7
- 根节点4的每个子节点也符合以上规范,满足定义8
1.2平衡二叉树的作用
二叉树支持动态的插入和查找,保证操作在O(height)时间,这就是完成了哈希表不便完成的工作,动态性。但是二叉树有可能出现worst-case,如果输入序列已经排序,则时间复杂度为O(N)
平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内。
所以如果输入结合确定,所需要的就是查询,则可以考虑使用哈希表,如果输入集合不确定,则考虑使用平衡二叉树/红黑树,保证达到最大效率
平衡二叉树主要优点集中在快速查找。
2.关键词概述
-
根节点(root_node)
父节点为空的节点,一颗不为空的平衡二叉树有且只有一个根节点
-
父节点(parent_node)
除了根节点,每个不为空的节点都有且只有一个父节点
-
左子树(left_tree)
任意节点的左分支节点树(表示的是左分支下的节点集合)
-
右子树(right_tree)
任意节点的右分支节点树(表示的是右分支下的节点集合)
-
左子节点(left_child)
任意节点左子树中的第一个节点(表示的是单个的节点)
-
右子节点(right_child)
任意节点右子树中的第一个节点(表示的是单个的节点)
-
叶子节点(leaf_node)
除根节点以外,若任意节点的左右子节点都为空,则称这个节点为叶子节点,表示没有后继分支
-
平衡因子(BalanceFactor)
表示任意节点的左右子树高度差
计算公式为:bf = 左子树高度 - 右子树高度
如果bf的绝对值>1代表此树失衡 -
最小不平衡子树(MinUnBalanceTree)
最小的失衡节点
3.代码实现思路
3.1节点类(TreeNode)
根据上文平衡二叉树的定义可知,节点类需要拥有下列字段
- data (节点值)
- bf (平衡因子)
- parent_node (父节点)
- left_child (左子节点)
- right_child (右子节点)
- count (节点数据被重复插入的次数)
新建一个节点类(TreeNode)
/**
* 存放数据的节点类
*/
static class TreeNode {
private int bf = 0;//BalanceFactor(平衡因子)
private int data;//该节点存放的数据
private TreeNode parent_node;//该节点的父节点
private TreeNode left_child, right_child;//该节点的左右子节点
private int count = 0;//记录该条数据被重复插入的次数
public TreeNode(int data) {
this.data = data;
}
/**
* 此处注意不能直接打印左右子节点和父节点的全部节点信息
* 因为节点之间有互相引用关系,如果全部打印会导致堆栈溢出
* 故此处值打印父节点和左右子节点的值
*
* @return
*/
@Override
public String toString() {
String p_data = null, l_data = null, r_data = null;
if (parent_node != null) {
p_data = String.valueOf(parent_node.data);
}
if (left_child != null) {
l_data = String.valueOf(left_child.data);
}
if (right_child != null) {
r_data = String.valueOf(right_child.data);
}
return "TreeNode{" +
" data=" + data +
", bf=" + bf +
", count=" + count +
", parent_node_data=" + p_data +
", left_child_data=" + l_data +
", right_child_data=" + r_data +
'}';
}
/**
* 因为平衡二叉树的节点值唯一,所以此处只需要比较值相等
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TreeNode node = (TreeNode) o;
return data == node.data;
}
@Override
public int hashCode() {
return Objects.hash(bf, data, parent_node, left_child, right_child, count);
}
}
3.2 树类(AVLTree)
树类需要满足的条件有:
- 任意节点左子树上的节点值小于该节点的值,右子树上的节点值大于该节点的值,该节点的子节点也适用这个规律
- 每个节点左右子树的深度差绝对值不大于1
新建一个树类(AVLTree)
/**
* 平衡二叉树
* 条件:
* 1.任意节点左子树上的节点值小于该节点的值,右子树上的节点值大于该节点的值,该节点的子节点也适用这个规律
* 2.每个节点左右子树的深度差绝对值不大于1
*/
static class AVLTree {
}
树类中需要一个队列来存放我们的节点值
//用于存放节点的队列
private Queue tree = new LinkedList();
现在我们来插入第一个节点 ,写一个插入节点的方法
/**
* 插入节点
*
* @param data 节点值
*/
public void insertNode(int data) {
TestAVL_Tree.TreeNode new_node = new TestAVL_Tree.TreeNode(data);
tree.add(new_node);
}
写一个测试类,插入数据7来查看调用结果
AVLTree avlTree = new AVLTree();
avlTree.insertNode(7);
System.out.println("最终结果:" + avlTree.tree.toString());
最终结果:
[TreeNode{ data=7, bf=0, count=0, parent_node_data=null, left_child_data=null, right_child_data=null}]
此时的AVL树状态为:
- 由于根节点是必定没有父节点的,此处就不在标出
- 因为左右子树都是NULL,此时平衡因子为0,
接着来插入第二个节点6
avlTree.insertNode(6);
因为6的值比根节点7小,所以放在根节点的左子节点中,如图
- 根节点7的的左树中新增节点6
- 因为左树中插入了新节点,需要重新计算平衡因子。此时根节点7的左树深度为1,右树深度为0,bf=1,AVL树保持平衡状态
如果根节点7在插入之前已经有子节点呢?
此时会有4种可能
- 插入的新节点小于根节点的左子节点,此时插入的方式称为左左(left_left),即在节点左子树下的左子节点下插入
插入流程:
1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值;
2.根节点的值为7,新节点的值为4;由于新节点的值 < 根节点的值,传递给根节点的左子节点,继续进行比较
3.左子节点的值为5,新节点的值为4;由于新节点的值 < 左子节点的值,且左子节点没有后继的子节点可以继续进行传递,则将新节点插入到左子节点的左子树下。
-
插入的新节点大于根节点的左子节点,此时插入的方式称为左右(left_right),即在节点左子树下的右子节点下插入
插入流程:*
1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值,右子树上所有节点的值均大于该节点的值
2.根节点的值为7,新节点的值为6;由于新节点的值 < 根节点的值,传递给根节点的左子节点,继续进行比较
3.左子节点的值为5,新节点的值为6;由于新节点的值 > 左子节点的值,且左子节点没有后继的子节点可以继续进行传递,则将新节点插入到左子节点的右子树下。
-
插入的新节点大于根节点的右子节点此时插入的方式称为右右(right_right),即在节点右子树下的右子节点下插入
插入流程:
1.由上文AVL树的定义可知,任意节点右子树上所有节点的值均大于该节点的值
2.根节点的值为7,新节点的值为15;由于新节点的值 > 根节点的值,传递给根节点的右子节点,继续进行比较
3.右子节点的值为10,新节点的值为15;由于新节点的值 > 右子节点的值,且右子节点没有后继的子节点可以继续进行传递,则将新节点插入到右子节点的右子树下。 -
插入的新节点小于根节点的右子节点此时插入的方式称为右左(right_left),即在节点右子树下的左子节点下插入
插入流程:
1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值,右子树上所有节点的值均大于该节点的值
2.根节点的值为7,新节点的值为8;由于新节点的值 > 根节点的值,传递给根节点的右子节点,继续进行比较
3.右子节点的值为10,新节点的值为8;由于新节点的值 < 右子节点的值*,且右子节点没有后继的子节点可以继续进行传递,则将新节点插入到右子节点的左子树下。
/**
* 计算新插入节点的位置
*
* @param old_node 旧节点
* @param new_node 新节点
*/
private void indexOf(TreeNode old_node, TreeNode new_node) {
if (new_node.data == old_node.data) {//新节点的值等于旧节点的值
old_node.count++;//不计算新节点的下表,直接在旧节点的count+1;
} else if (new_node.data < old_node.data) {//新节点的值小于旧节点的值
if (old_node.left_child == null) {//如果左子树为空,就放入旧节点的左子树
old_node.left_child = new_node;
new_node.parent_node = old_node;//把当前旧节点设置为新节点的父节点
} else {
indexOf(old_node.left_child, new_node);//如果左子树不为空,则递归计算位置
}
} else if (new_node.data > old_node.data) {//新节点的值大于等于旧节点的值
if (old_node.right_child == null) {//如果右子树为空,就放入旧节点的右子树
old_node.right_child = new_node;
new_node.parent_node = old_node;//把当前旧节点设置为新节点的父节点
} else {
indexOf(old_node.right_child, new_node);//如果右子树不为空,则递归计算位置
}
}
}
可以看到以上4种插入情况都会导致根节点bf的绝对值 >1,此时二叉树失衡,需要通过旋转来恢复平衡状态
针对4种插入情况(左左、左右、右右、右左),有4种不同的旋转策略,两种旋转的方法,分别为左旋和右旋
==左旋和右旋不同于逆时针旋和顺时针旋转,下面两个动图便于理解:==
左旋
左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点
右旋
右旋就是反过来,将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点
即左旋就是往左变换,右旋就是往右变换。不管是左旋还是右旋,旋转的目的都是将节点多的一支出让节点给另一个节点少的一支
左左
- 针对LL型(left_left)的旋转策略为:右旋
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0
则需要对节点5执行右旋
- 旧根节点(节点 7)为新根节点(节点 5)的右子树
- 新根节点(节点 5)的 右子树(如果存在)为旧根节点的左子树
旋转之前首先要找到最小不平横子树
寻找过程为:
1.从新插入的节点开始,向上寻找父节点,判断父节点的bf绝对值是否大于1
2.如果 > 1,则此父节点就是最小不平衡子树
3.如果 < 或 = 1,则继续往上追朔父节点的父节点,直到找到bf>1的祖父节点
/**
* 计算最小不平衡子树
* 可能存在的情况:
* 1.该节点是根节点(没有父节点)
* 2.该节点的父节点平衡因子>1,代表此节点是最小不平衡子树
* 3.该节点的父节点平衡因子<1,递归继续向上查找
*
* @param node 新插入的节点
* @return 最小不平衡子树
*/
private TreeNode countMinUnBalanceNode(TreeNode node) {
TreeNode from_node = node.parent_node;//获得新插入节点的父节点
if (from_node != null) {//此节点不是根节点
int bf_abs = Math.abs(from_node.bf);
if (bf_abs > 1) {//平衡因子绝对值>1代表此节点失衡,并且是最小不平衡子数
return from_node;
} else if (bf_abs <= 1) {
return countMinUnBalanceNode(from_node);//否则递归调用向上查找
}
} else {//此节点是根节点 只需要判断根节点的不平衡因子,不需要往上递归
int bf_abs = Math.abs(node.bf);
if (bf_abs > 1) {//平衡因子绝对值>1代表此节点失衡,并且是最小不平衡子数
return node;
}
}
return null;
}
节点右旋的代码
/**
* 节点右旋
*
* @param node 需要右旋的节点
*/
private void rightRotate(TreeNode node) {
TreeNode left_child = node.left_child;//当前节点的左子树
TreeNode parent_node = node.parent_node;//当前节点的父节点
if (parent_node != null) {//如果当前节点不是根节点
//判断当前节点是父节点的左子树还是右子树
if (node.equals(parent_node.left_child)) {
node.parent_node.left_child = left_child;//用左子树替换掉当前节点在父节点的位置
} else if (node.equals(parent_node.right_child)) {
node.parent_node.right_child = left_child;//用左子树替换掉当前节点在父节点的位置
}
}
left_child.parent_node = parent_node;//原左子树(现父节点)的父节点改为原父节点(现右子树)的父节点
TreeNode left_child_right = left_child.right_child;//获取原左子树的右节点
//如果不为空则把左子树的右节点设为原父节点的左子树
if (left_child_right != null) {
left_child_right.parent_node = node;
}
node.left_child = left_child_right;//原左子树右节点的父节点改为现右子树(原父节点)的左节点
left_child.right_child = node;//现父节点(原左子节点)的右子树改为原父节点
node.parent_node = left_child;//现右子树(原父节点)的父节点改为原左子树(现父节点)
reCountNodeBF();//右旋完成重新计算节点的平衡因子
System.out.println("右旋后: " + tree.toString());
}
右右
- 针对RR型(right_right)的旋转策略为:左旋
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为0,右子树深度为2
则需要对节点10执行左旋
- 旧根节点(节点7)为新根节点(节点 10)的左子树
- 新根节点(节点10)的左子树(如果存在)为旧根节点的右子树
节点左旋的代码
/**
* 节点左旋
*
* @param node 需要左旋的节点
*/
private void leftRotate(TreeNode node) {
TreeNode right_child = node.right_child;//当前节点的右子树
TreeNode parent_node = node.parent_node;//当前节点的父节点
if (parent_node != null) {//如果当前节点不是根节点
//判断当前节点是父节点的左子树还是右子树
if (node.equals(parent_node.left_child)) {
parent_node.left_child = right_child;//用左子树替换掉当前节点在父节点的位置
} else if (node.equals(parent_node.right_child)) {
parent_node.right_child = right_child;//用左子树替换掉当前节点在父节点的位置
}
}
right_child.parent_node = parent_node;//原右子树(现父节点)的父节点改为原父节点(现左子树)的父节点
TreeNode right_child_left = right_child.left_child;//获取原右子树的左节点
//如果不为空则把右子树左节点的父节点设为原父节点(现左子树的)的右节点
if (right_child_left != null) {
right_child_left.parent_node = node;
}
node.right_child = right_child_left;//原右子树左节点的父节点改为现左子树(原父节点)的右节点
right_child.left_child = node;//现父节点(原右子树)的左子树改为原父节点
node.parent_node = right_child;//现左子树(原父节点)的父节点改为原右子树(现父节点)
reCountNodeBF();//左旋完成重新计算节点的平衡因子
System.out.println("左旋后: " + tree.toString());
}
左右
- 针对LR型(left_right)的旋转策略为:先左旋转成左左,再右旋
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0,且新节点(6)小于根节点的左子节点(5)
则需要先对节点6执行左旋再右旋
右左
- 针对RL型(right_left)的旋转策略为:先右旋转成右右,再左旋
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为0,右子树深度为2,且新节点(6)小于根节点的右子节点(5)
则需要先对节点6执行右旋再左旋
/**
* 执行旋转
* 可能出现4中情况:
* 1.左左 (直接右旋)
* 2.左右(先左旋变为左左再右旋)
* 3.右右(直接左旋)
* 4.右左(先右旋变为右右再左旋)
*
* @param new_node 新插入的节点
* @param minUnBalanceTree 最小不平衡子树
*/
private void executeRotation(TreeNode new_node, TreeNode minUnBalanceTree) {
System.out.println("旋转前: " + tree.toString());
TreeNode old_node = new_node.parent_node;
if (minUnBalanceTree.bf > 1) {//左边多,需要右旋
if (new_node != old_node.left_child) {//左右(当前节点不是旧节点的左子树)
leftRotate(old_node);//先左旋变为左左
minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
}
rightRotate(minUnBalanceTree);//右旋
} else if (minUnBalanceTree.bf < -1) {//右边多,需要左旋
if (new_node != old_node.right_child) {//右左(当前节点不是旧节点的右子树)
rightRotate(old_node);//先右旋变为右右
minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
}
leftRotate(minUnBalanceTree);//左旋
}
}
插入节点的方法修改后为
/**
* 插入节点
* 执行步骤:
* 1.计算插入节点的坐标
* 2.添加到队列
* 3.重新计算每个节点的平衡因子
* 4.计算最小不平衡子树
* 5.如果有最小不平衡子树,则执行旋转
*
* @param data 节点值
*/
public void insertNode(int data) {
TreeNode new_node = new TreeNode(data);
int size = tree.size();
if (size == 0) {//如果树里没有节点,是第一次添加
tree.add(new_node);
} else {
indexOf(getRootNode(), new_node);//计算插入的位置
if (new_node.parent_node != null) {
tree.add(new_node);//添加到队列
}
reCountNodeBF();//重新计算每个节点的平衡因子
TreeNode minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
if (minUnBalanceTree != null) {//代表失衡
System.out.println("最小不平衡子树为: " + minUnBalanceTree.toString());
executeRotation(new_node, minUnBalanceTree);//执行旋转
}
}
}
同理我们可以很轻松的写出一个插入节点值数组的方法
/**
* 插入数组
*
* @param arr 节点值数组
*/
public void insertArr(int[] arr) {
for (int data : arr) {
insertNode(data);
}
}
查找结点
-
原理
先来看一张图
查找节点的思路类似于二分法比如我们想查找的节点为3
根据平衡二叉树的定义可知节点左子树上的值小于节点本身,右子树上的值大于节点本身
先从根节点开始判断,根节点的值为4,大于我们的查找值3,则我们的目标节点只可能存在于根节点的左子树
根节点的左子节点的值为2,小于我们查找值3,则我们的目标节点只可能存在于此节点的右子树
节点2的右子节点值为3,等于我们的查找值,则此节点为我们的目标节点同理,如果我们查找一个不存在的节点7
先从根节点开始判断,根节点的值为4,小于我们的查找值7,则我们的目标节点只可能存在于根节点的** 右子树**
根节点的右子节点的值为5,小于我们查找值7,则我们的目标节点只可能存在于此节点的右子树
节点6的右子节点为空,并且节点值小于我们的查找值7,此时代表目标节点不存在 代码实现
/**
* 查找节点
* 类似于二分法
*
* @param data 节点值
* @return Node
*/
public TreeNode searchNode(int data) {
TreeNode rootNode = getRootNode();//得到根节点
return dichotomousSearch(data, rootNode);//从根节点开始往下查找(使用二分法)
}
/**
* 递归二分查找
* 有以下几种情况:
* 1.目标值等于比较节点的值,代表已找到
* 2.目标值小于比较节点的值,,则从比较节点的左子树开始递归查找
* 3.目标值大于比较节点的值,则从比较节点的右子树开始递归查找
*
* @param target_data 目标值
* @param compare_node 比较目标值的节点
* @return Node
*/
private TreeNode dichotomousSearch(int target_data, TreeNode compare_node) {
if (target_data == compare_node.data) {//如果目标值等于比较节点的值,则返回比较节点
return compare_node;
} else if (target_data < compare_node.data && compare_node.left_child != null) {//如果目标值大于比较节点的值,则从比较节点的左子树开始递归查找
return dichotomousSearch(target_data, compare_node.left_child);
} else if (target_data > compare_node.data && compare_node.right_child != null) {//如果目标值大于比较节点的值,则从比较节点的右子树开始递归查找
return dichotomousSearch(target_data, compare_node.right_child);
}
return null;
}
总结一下,插入节点的流程为:
- 遍历二叉树,判断新插入的节点是否已经存在,若存在,则对该节点count++,不重复插入
- 计算新节点的坐标(它的父节点是谁,它是父节点的左子节点还是右子节点)
- 把节点添加到二叉树
- 遍历二叉树,重新计算所有节点的平衡因子,看是否有节点失衡
- 如果有失衡则从新插入的节点开始,找出最小不平衡子树
- 根据失衡的类型(左左,左右,右左,右右),执行不同的旋转策略
删除结点
删除节点的情况比较复杂,因为删除节点和添加节点一样,有可能会导致二叉树失衡
删除过程可分为三种情况
- 被删除的节点为叶子节点(比如上图中的节点1、3、6),即没有左右子节点
- 被删除的节点只有左子树或只有右子树(比如上图中的节点5)
- 被删除的节点既有左子树又有右子树(比如上图中的节点4,2)
我们需要知道这么一点,左子树上节点的删除相当于我们在右子树上插入了一个新节点,右子树上节点的删除相当于在左子树上插入了一个新节点,根据这一点,我们进行判断并采取对应的平衡调整操作。
分析一下这三种情况:
-
被删除的节点为叶子节点
例1:
比如上图中的节点{8,14,25,31,40}都是叶子节点,我们来删除节点14删除后的树结构为:
删除一个几点我们需要重被删除节点的父节点开始,往上寻找是否有失衡节点
节点14的父节点值为10,节点10的左子树深度为1,右子树深度为0,则bf = 1;未失衡,继续往上寻找
节点10的父节点值为20,节点20的左子树深度为2,右子树深度为3,则bf = -1;未失衡,有于此节点已经是根节点,停止寻找
-
此时二叉树处于平衡状态
例2:
上图中的节点{7,21,40}都是叶子节点,我们来删除节点7删除后的树结构为:
依照例1的规律,检索节点的步骤为
- 节点7的父节点值为8,节点8没有子树,则bf = 0;未失衡,继续往上寻找
- 节点8的父节点值为20,节点20的左子树深度为1,右子树深度为3,则bf = -2,此节点失衡,则此节点为最小不平衡子树
- 此时二叉树处于失衡状态
那么该如何调整呢?
上文提过,在左子树上删除节点其实就相当于在右子树上插入节点。
我们找到节点20的右子树上的子节点30,发现节点30的左子树高度比右子树高,这就相当于在节点20的右子树节点30的左子树下插入了一个新的节点。这就需要进行RL型(右左)调整。
==注意:这里的RL型(右左)调整不同于上面插入节点的调整步骤!!!==
先对最小不平衡子树(节点20)的右子节点(30)进行右旋调整,调整后为:
接下来对最小不平衡子树(节点20)进行左旋调整,调整后为:
通过检索可得,此时的二叉树保持平衡状态
例3:
我们来删除节点8,删除后的树结构为:
通过检索可以得出,根节点(25)为最小不平衡子树,bf = -2,此树处于失衡状态,需要进行调整。
上文说过,在左子树上删除节点其实就相当于在右子树上插入节点。但要如何调整还取决于该失衡节点的右子树的左右子树的高度差。
调整的步骤为:
- 找到最小不平衡子树(根节点25)的右子节点(30)
- 节点30的左右子树高度一样,故只需要RR型(右右)调整
调整后的二叉树为:
总结一下,删除叶子节点的几个步骤为:
- 将该结点直接从树中删除
- 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
- 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)
代码实现:
/**
* 删除一个叶子节点(没有左右子树的节点)
*
* @param node 被删除节点
* @return Result
*/
private Result deleteNodeByLeafNode(TreeNode node) {
if (node.parent_node == null) {//该节点是根节点(此情况下二叉树只有一个节点)
tree.remove(node);//从队列中移除此节点
return new Result(true, node);
} else {//该节点是叶子节点
if (node == node.parent_node.left_child) {//该节点是父节点的左子节点
node.parent_node.left_child = null;//节点置空
} else if (node == node.parent_node.right_child) {//该节点是父节点的右子节点
node.parent_node.right_child = null;//节点置空
}
}
tree.remove(node);//从队列中移除此节点
return new Result(false, node);
}
删除结果回调类:
/**
* 删除结果回调类
*/
static class Result {
private boolean isSkipBuild;//是否跳过检索
private TreeNode target_node;//标记的节点
public Result(boolean isSkipBuild, TreeNode target_node) {
this.isSkipBuild = isSkipBuild;
this.target_node = target_node;
}
}
判断如何执行旋转的方法:
/**
* 整理节点树
* 判断是否需要执行旋转
*
* @param target_node
*/
private void rebuild(TreeNode target_node) {
TreeNode minUnBalanceTree = countMinUnBalanceNode(target_node);//查找最小不平衡子树,从被删除节点的父节点开始层级查找
if (minUnBalanceTree == null) return;
System.out.println("最小不平衡子树为: " + minUnBalanceTree.toString());
if (minUnBalanceTree.bf > 1) {//被删除节点属于最小不平衡子树的右分支
if (minUnBalanceTree.left_child.bf >= 0) {//左树和右树相等或左树比右树高
System.out.println("旋转前: " + tree.toString());
rightRotate(minUnBalanceTree);
} else if (minUnBalanceTree.left_child.bf < 0) {//右树比左树高
leftRotate(minUnBalanceTree.left_child);
rightRotate(minUnBalanceTree);
}
} else if (minUnBalanceTree.bf < -1) {//被删除节点属于最小不平衡子树的左分支
if (minUnBalanceTree.right_child.bf > 0) {//左树比右树高
rightRotate(minUnBalanceTree.right_child);
leftRotate(minUnBalanceTree);
} else if (minUnBalanceTree.right_child.bf <= 0) {//右树和左树等高或右树比左树高
System.out.println("旋转前: " + tree.toString());
leftRotate(minUnBalanceTree);
}
}
//递归向上检索
rebuild(minUnBalanceTree.parent_node);
}
- 被删除的节点只有一颗子树(左子树或右子树)
**例1:**
![在这里插入图片描述](https://img-blog.csdnimg.cn/e11dbc4ff2db48af80451c4930c7d057.png#pic_center)
该树中,只有一颗子树的节点为{29,40},我们来删除节点29
**删除后的树结构为:**
![在这里插入图片描述](https://img-blog.csdnimg.cn/525100bb144947b9a3f98b225d87801e.png#pic_center)
可以看到此时二叉树处于平衡状态
如果我们将**例1**中删除的节点改为**40**,则需要用节点**40**的右子树或者左子树替换掉节点40在其父节点中的位置
因为节点**40**只有右子树,此时我们用右子节点**50**进行替换
**删除后的树结构为:**
![在这里插入图片描述](https://img-blog.csdnimg.cn/892d705fc72b47318ff02bfa3ea336d8.png#pic_center)
通过检索可知节点30失衡,最小不平衡子树为30, **删除右子树的节点相当于在左子树上插入新的节点** ,也就是相当于在左子树的右子树上插入节点。因此我们这里要进行LR型调整。
***调整步骤:***
- 我们首先找到被删除节点40的替代节点50
- 从节点50开始,向上检索失衡节点
- 检索到根节点(30)失衡,bf = 2,
- 找到最小不平衡子树(根节点30)的左子节点(25),执行左旋
- 再对最小不平衡子树(根节点30)执行右旋
左旋后的树结构为:
右旋后的树结构为:
总结一下,删除有一个子节点的节点的步骤为:
- 将该结点直接从树中删除
- 将该节点的后继子节点(左子节点或右子节点)替换该节点原来的位置
- 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
- 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)
代码实现:
/**
* 删除一个只有左子树的节点
*
* @param node 被删除节点
* @return Result
*/
private Result deleteNodeByOnlyleftTree(TreeNode node) {
if (node.parent_node != null && node.data < node.parent_node.data) {//如果存在父节点并且此节点的值小于父节点的值(代表此节点为父节点的左子节点)
node.parent_node.left_child = node.left_child;//当前父节点的左子节点替换为此节点的左子节点
node.left_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
} else if (node.parent_node != null && node.data > node.parent_node.data) {//如果存在父节点并且此节点的值大于于父节点的值(代表此节点为父节点的右子节点)
node.parent_node.right_child = node.left_child;//当前父节点的右子节点替换为此节点的左子节点
node.left_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
} else {//代表被删除的节点是根节点且根节点只有左子节点
node.left_child.parent_node = null;//直接将该节点左子节点的父节点引用置空,并跳过检索
tree.remove(node);//从队列中移除此节点
return new Result(true, node);
}
tree.remove(node);//从队列中移除此节点
return new Result(false, node);
}
/**
* 删除一个只有右子树的节点
*
* @param node 被删除节点
* @return Result
*/
private Result deleteNodeByOnlyRightTree(TreeNode node) {
if (node.parent_node != null && node.data < node.parent_node.data) {//如果存在父节点并且此节点的值小于父节点的值(代表此节点为父节点的左子节点)
node.parent_node.left_child = node.right_child;//当前父节点的左子节点替换为此节点的右子节点
node.right_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
} else if (node.parent_node != null && node.data > node.parent_node.data) {//如果存在父节点并且此节点的值大于于父节点的值(代表此节点为父节点的右子节点)
node.parent_node.right_child = node.right_child;//当前父节点的右子节点替换为此节点的右子节点
node.right_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
} else {//代表被删除的节点是根节点且根节点只有右子节点
node.right_child.parent_node = null;//直接将该节点右子节点的父节点引用置空,并跳过检索
tree.remove(node);//从队列中移除此节点
return new Result(true, node);
}
tree.remove(node);//从队列中移除此节点
return new Result(false, node);
}
-
被删除的节点既有左子树又右子树
例1:
满足此条件的节点有{10,20},我们来删除根节点(20)
这里和情况下2不同,因为根节点同时拥有左右子树,所以不能简单的用左右子树去替换被删除节点的位置我们来分析一下情况:
- 由根节点(20)的平衡因子为1可以知道,根节点的左树深度是比右树高的
- 此时我们需要找到根节点左子树中的最大值节点
- 由上图可知最大值节点为15,我们将节点15的值和节点20的值进行替换(==仅仅是值的替换,不改变两个节点的其它信息,比如说父节点引用、左右子节点引用、平衡因子、插入次数==)
替换后的树结构为:
- 替换后再删除值为20的节点
删除后的树结构为:
此时我们发现被删除的值为20的节点的父节点(10)bf = 2,处于失衡的状态,此时最小不平衡子树为节点10。
需要对以10为根节点的子树进行调整【这个地方进行删除操作时,相当于在以10为根节点的左子树的右子树上进行插入操作】,也就是进行LR调整。先对节点10的左子节点(8)进行左旋,让其成为左左
左旋后的树结构为:
再对最小不平衡子树(节点10)进行右旋
右旋后的树结构为:
此时的二叉树保持平衡
总结一下,删除有左右子树的节点的步骤为:
- 判断被删除的节点bf 值 属于[0,1] 的区间还是 = -1( 属于[0,1] 说明该节点左子树比右子树深或和右子树一样深,等于-1表示右子树比左子树深)
- 如果是左子树深度 > = 右子树深度,则找到左子树中的最大值节点
- 如果是右子树深度 > 左子树深度,则找到右子树中的最小值节点
- 将找到的最大或最小值节点与被删除的节点进行值的交换(==仅仅只交换值,不修改其它属性==)
- 删除原值所在的节点
- 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
- 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)
PS: 关于最大值节点和最小值节点,这里我们来探究一个问题。
为什么左子树深度 > = 右子树深度,则找左子树中的最大值节点去替换,右子树深度 > 左子树深度,则找到右子树中的最小值节点去替换呢?这里能不能反过来呢?
我们先来看反过来会有什么后果,还是上面的那个例子
我们删除节点20,本来是用左子树中的最大值节点15去替换它,这回我们反过来,用左子树中的最小值节点8去替换
替换后的树结构为:
-
替换后再删除值为20的节点
删除后的树结构为:
此时根节点(8)的左子节点值10,右子节点值为25。由上文二叉树的定义可知,任意节点左子树上的值均小于该节点,右子树上的值均大于该节点。很显然节点8的左子节点值大于此节点,不符合二叉树的要求。所以这里只能用左子树上的最大值节点来替换。
如何获取左子树上的最大值节点
例图:
比如我想获取节点根节点4的左子树中最大值节点3,由于任意节点的右子树值都大于该节点的值 ,获取节点3其实就是获取根节点左子树中最右边的那个节点。
那如果节点左子树中没有最右边的节点怎么办呢?
例如我想获取节点7左子树中的最大值节点,节点7的左子树为节点6,由于节点6的右子节点为空,此时节点6就是左子树中的最大值节点。
代码实现:
这里提供两种实现思路
-
递归
/** * 获取目标节点的最大值节点 * 获取节点的最大值其实就是获取该节点的最右子节点 * * @param node * @return */ private TreeNode getMaxNode(TreeNode node) { if (node == null) return null; return node.right_child == null ? node : getMaxNode(node.right_child); }
-
循环
/** * 获取目标节点的最大值节点 * 获取节点的最大值其实就是获取该节点的最右子节点 * * @param node * @return */ private TreeNode getMaxNode(TreeNode node) { if (node == null) return null; while (node.right_child != null) { node = node.right_child; } return node; }
同理可以得出获取节点右子树中的最小值节点
代码实现:
- 递归
/** * 获取目标节点的最小值节点 * 获取节点的最小值其实就是获取该节点的最左子节点 * * @param node * @return */ private TreeNode getMinNode(TreeNode node) { if (node == null) return null; return node.left_child== null ? node : getMinNode(node.left_child); }
-
循环
/** * 获取目标节点的最小值节点 * 获取节点的最小值其实就是获取该节点的最左子节点 * * @param node * @return */ private TreeNode getMinNode(TreeNode node) { if (node == null) return null; while (node.left_child!= null) { node = node.left_child; } return node; }
删除节点完整的代码为:
/**
* 删除节点
* * 有四种情况:
* * 1.删除的节点没有左右子节点(叶子节点或根节点)
* * 2.删除的节点有左子节点
* * 3.删除的节点有右子节点
* * 4.删除的节点有左右子节点
*
* @param data 节点值
* @return ture:删除成功,false:删除失败
*/
public boolean deleteNode(int data) {
TreeNode node = searchNode(data);//查找该节点
if (node == null) return false;
boolean isSkipBuild = false;//是否跳过rebuild()
Result result = null;//删除结果回调
/**
* 如果删除的节点没有左右子节点
* 1.判断该节点是否是根节点(删除没有左右子树的根节点可以跳过检索)
* 2.判断该节点是父节点的左子节点还是右子节点,把其在父节点的引用链置空
* 3.删除该节点
* 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
*/
if (node.left_child == null && node.right_child == null) {
result = deleteNodeByLeafNode(node);//删除一个叶子节点(没有左右子树的节点)
node = result.target_node;
isSkipBuild = result.isSkipBuild;
System.out.println("删除的节点为: " + node.toString());
/**
* 如果删除的节点只有左子树
* 1.判断该节点是否是根节点(删除只有左子树的根节点可以跳过检索)
* 2.判断该节点是父节点的左子节点还是右子节点,将其原来在父节点中的引用链替换为该节点的左子节点,将该节点的左子节点的父节点替换为该节点的父节点
* 3.删除节点
* 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
*/
} else if (node.left_child != null && node.right_child == null) {
result = deleteNodeByOnlyleftTree(node);//删除一个只有左子树的节点
node = result.target_node;
isSkipBuild = result.isSkipBuild;
System.out.println("删除的节点为: " + node.toString());
/**
* 如果删除的节点只有右子树
* 1.判断该节点是否是根节点(删除只有右子树的根节点可以跳过检索)
* 2.判断该节点是父节点的左子节点还是右子节点,将其原来在父节点中的引用链替换为该节点的右子节点,将该节点的右子节点的父节点替换为该节点的父节点
* 3.删除节点
* 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
*/
} else if (node.right_child != null && node.left_child == null) {
result = deleteNodeByOnlyRightTree(node);//删除一个只有右子树的节点
node = result.target_node;
isSkipBuild = result.isSkipBuild;
System.out.println("删除的节点为: " + node.toString());
/**
* 如果删除的节点有左右子树
* 1.判断该节点的平衡因子是否为0或1,如果是,则寻找到该节点左子树中的最大值节点(left_max节点),把该节点与left_max节点的值进行交换(只交换值,不替换引用链,计算因子等)
* 2.判断判断该节点的平衡因子是否为-1,如果是,则寻找到该节点右子树中的最小值节点(right_min节点),把该节点与right_min节点的值进行交换(只交换值,不替换引用链,计算因子等)
* 3.删除替换值后的left_max节点或right_min节点
* 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
*/
} else if (node.left_child != null && node.right_child != null) {
if (node.bf == 0 || node.bf == 1) {//如果该节点的平衡因子是0或1
TreeNode max_node = getMaxNode(node.left_child);//获取被删除节点左子树中的最大值节点(该节点只可能是一个叶子节点,或者是一个只有左子树的节点)
int temp_data = node.data;//节点值互换
node.data = max_node.data;
max_node.data = temp_data;
if (max_node.left_child != null) {//该节点是一个只有左子树的节点
result = deleteNodeByOnlyleftTree(max_node);//删除一个只有左子树的节点
node = result.target_node;
isSkipBuild = result.isSkipBuild;
} else {//该节点是一个叶子节点
result = deleteNodeByLeafNode(max_node);//删除一个叶子节点(没有左右子树的节点)
node = result.target_node;
isSkipBuild = result.isSkipBuild;
}
System.out.println("删除的节点为: " + max_node.toString());
} else if (node.bf == -1) {//如果该节点的平衡因子是-1
TreeNode min_node = getMinNode(node.right_child);//获取被删除节点右子树中的最小值节点(该节点只可能是一个叶子节点,或者是一个只有右子树的节点)
int temp_data = node.data;//节点值互换
node.data = min_node.data;
min_node.data = temp_data;//节点值互换
if (min_node.right_child != null) {//该节点是一个只有右子树的节点
result = deleteNodeByOnlyRightTree(min_node);//删除一个只有右子树的节点
node = result.target_node;
isSkipBuild = result.isSkipBuild;
} else {//该节点是一个叶子节点
result = deleteNodeByLeafNode(min_node);//删除一个叶子节点(没有左右子树的节点)
node = result.target_node;
isSkipBuild = result.isSkipBuild;
}
System.out.println("删除的节点为: " + min_node.toString());
}
}
reCountNodeBF();//重新计算平衡因子
if (!isSkipBuild) {
rebuild(node);
}
return true;
}
修改节点
修改节点比较简单,这里只说一下实现思路,不画图做原理的探讨。修改一个节点的值等于删除这个节点再插入一个新节点
修改步骤:
- 检索二叉树,判断修改的节点是否存在
- 删除检索到的节点
- 新建一个节点,节点值为修改值
代码实现:
/**
* 修改节点
* 修改一个节点的值等于删除这个节点再把期望值赋给新节点
*
* @param target_value 目标值
* @param expected_value 期望值
* @return 修改结果
*/
public boolean modifyNode(int target_value, int expected_value) {
TreeNode node = searchNode(target_value);
if (node == null) return false;//查无此节点
if (deleteNode(target_value)) {//删除目标值节点
insertNode(expected_value);//如果删除成功则插入一个新节点,节点值为目标值
} else {
return false;
}
return true;
}
最后贴上完整的源码资源,有兴趣的可以复制下来测试。
源码地址
==ps:代码并未做性能上的优化,有兴趣的可以深入研究==