赞
踩
了解平衡二叉树之前我们首先需要知道什么是树结构
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。
树的定义:
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分 [1] 。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点 [1] 。
二叉树定义:
二叉树有以下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)***
二叉树支持动态的插入和查找,保证操作在O(height)时间,这就是完成了哈希表不便完成的工作,动态性。但是二叉树有可能出现worst-case,如果输入序列已经排序,则时间复杂度为O(N)
平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内。
所以如果输入结合确定,所需要的就是查询,则可以考虑使用哈希表,如果输入集合不确定,则考虑使用平衡二叉树/红黑树,保证达到最大效率
平衡二叉树主要优点集中在快速查找。
根节点(root_node)
父节点为空的节点,一颗不为空的平衡二叉树有且只有一个根节点
父节点(parent_node)
除了根节点,每个不为空的节点都有且只有一个父节点
左子树(left_tree)
任意节点的左分支节点树(表示的是左分支下的节点集合)
右子树(right_tree)
任意节点的右分支节点树(表示的是右分支下的节点集合)
左子节点(left_child)
任意节点左子树中的第一个节点(表示的是单个的节点)
右子节点(right_child)
任意节点右子树中的第一个节点(表示的是单个的节点)
叶子节点(leaf_node)
除根节点以外,若任意节点的左右子节点都为空,则称这个节点为叶子节点,表示没有后继分支
平衡因子(BalanceFactor)
表示任意节点的左右子树高度差
计算公式为:bf = 左子树高度 - 右子树高度
如果bf的绝对值>1代表此树失衡
最小不平衡子树(MinUnBalanceTree)
最小的失衡节点
根据上文平衡二叉树的定义可知,节点类需要拥有下列字段
新建一个节点类(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); } }
树类需要满足的条件有:
新建一个树类(AVLTree)
/**
* 平衡二叉树
* 条件:
* 1.任意节点左子树上的节点值小于该节点的值,右子树上的节点值大于该节点的值,该节点的子节点也适用这个规律
* 2.每个节点左右子树的深度差绝对值不大于1
*/
static class AVLTree {
}
树类中需要一个队列来存放我们的节点值
//用于存放节点的队列
private Queue<TestAVL_Tree.TreeNode> tree = new LinkedList<TestAVL_Tree.TreeNode>();
现在我们来插入第一个节点 ,写一个插入节点的方法
/**
* 插入节点
*
* @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树状态为:
接着来插入第二个节点6
avlTree.insertNode(6);
因为6的值比根节点7小,所以放在根节点的左子节点中,如图
如果根节点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种不同的旋转策略,两种旋转的方法,分别为左旋和右旋
左旋和右旋不同于逆时针旋和顺时针旋转,下面两个动图便于理解:
左旋
左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点
右旋
右旋就是反过来,将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点
即左旋就是往左变换,右旋就是往右变换。不管是左旋还是右旋,旋转的目的都是将节点多的一支出让节点给另一个节点少的一支
左左
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0
则需要对节点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()); }
右右
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为0,右子树深度为2
则需要对节点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()); }
左右
此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0,且新节点(6)小于根节点的左子节点(5)
则需要先对节点6执行左旋再右旋
右左
此情况下最小不平衡子树为根节点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; }
总结一下,插入节点的流程为:
删除结点
删除节点的情况比较复杂,因为删除节点和添加节点一样,有可能会导致二叉树失衡
删除过程可分为三种情况
我们需要知道这么一点,左子树上节点的删除相当于我们在右子树上插入了一个新节点,右子树上节点的删除相当于在左子树上插入了一个新节点,根据这一点,我们进行判断并采取对应的平衡调整操作。
分析一下这三种情况:
被删除的节点为叶子节点
例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,此树处于失衡状态,需要进行调整。
上文说过,在左子树上删除节点其实就相当于在右子树上插入节点。但要如何调整还取决于该失衡节点的右子树的左右子树的高度差。
调整的步骤为:
调整后的二叉树为:
总结一下,删除叶子节点的几个步骤为:
代码实现:
/** * 删除一个叶子节点(没有左右子树的节点) * * @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:
该树中,只有一颗子树的节点为{29,40},我们来删除节点29
删除后的树结构为:
可以看到此时二叉树处于平衡状态
如果我们将例1中删除的节点改为40,则需要用节点40的右子树或者左子树替换掉节点40在其父节点中的位置
因为节点40只有右子树,此时我们用右子节点50进行替换
删除后的树结构为:
通过检索可知节点30失衡,最小不平衡子树为30, 删除右子树的节点相当于在左子树上插入新的节点 ,也就是相当于在左子树的右子树上插入节点。因此我们这里要进行LR型调整。
调整步骤:
左旋后的树结构为:
右旋后的树结构为:
总结一下,删除有一个子节点的节点的步骤为:
代码实现:
/** * 删除一个只有左子树的节点 * * @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的节点的父节点(10)bf = 2,处于失衡的状态,此时最小不平衡子树为节点10。
需要对以10为根节点的子树进行调整【这个地方进行删除操作时,相当于在以10为根节点的左子树的右子树上进行插入操作】,也就是进行LR调整。先对节点10的左子节点(8)进行左旋,让其成为左左
左旋后的树结构为:
再对最小不平衡子树(节点10)进行右旋
右旋后的树结构为:
此时的二叉树保持平衡
总结一下,删除有左右子树的节点的步骤为:
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:代码并未做性能上的优化,有兴趣的可以深入研究
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。