当前位置: 首页 > news >正文

数据结构基础 - 平衡二叉树

简介 : 平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体,也是第一个引入平衡概念的二叉树。1962年,G.M. Adelson-Velsky 和 E.M. Landis发明了这棵树,所以它又叫AVL树(有别于AVL算法)。且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

二叉排序树(二叉查找树)的不足 :

二叉排序树集中了数组的查找优势以及链表的插入、删除优势,因此在数据结构中占有一定的地位。但在一定的情况下二叉排序树又有可能变为链表,例如插入从1~100的数,这时进行数据查找的效率就要降低。

既然我们已经了解到了造成二叉树不平衡的原因就是 : “任何一个节点的左右子树深度差绝对值超过1”, 那么如何让二叉树的左右子树深度差不超过1呢?

这就需要对节点进行旋转,也就是当某个节点的左右子树深度超过1时需要对这个节点进行旋转(旋转之后依旧是左子树小于节点小于右子树),重新调整树的结构以达到重新的平衡。

例如:这两棵二叉树虽然结构不同,但是都是二叉排序树,所谓的旋转就是把左边的深度为3的树旋转为右边深度为2的二叉树。

在这里插入图片描述

平衡二叉树不平衡的情形:

在平衡二叉树进行插入操作时遇到的不平衡情况有多种,但是这么多种情况都可以分解为一下四中基础情景:把它叫做:左左、左右、右右、右左。

在解释这四种情景之前需要先明白一个定义:最小不平衡节点—插入一个节点之后,距离这个插入节点最近的不平衡节点就是最小不平衡节点(如上图左树的10节点)。所有的旋转都是在最小不平衡节点的基础上进行的。

继续解释四种情景命名意义:

左左:节点插入在最小不平衡节点的左子树的左子树上。

左右:节点插入在最小不平衡节点的左子树的右子树上面

右右:节点插入在最小不平衡树的右子树的右子树上面。

右左:节点插入在最小不平衡树的右子树的左子树上面。

例如 : 我们把需要重新平衡的结点叫做α,由于任意结点最多只有两个孩子,因此高度不平衡时,α结点的两颗子树的高度相差2.容易看出,这种不平衡可能出现在下面4中情况中:

1.对α的左孩子的左子树进行一次插入

2.对α的左孩子的右子树进行一次插入

3.对α的右孩子的左子树进行一次插入

4.对α的右孩子的右子树进行一次插入

在这里插入图片描述

1、6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种情况成为左左。

2、6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种情况成为左右。

3、2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种情况成为右左。

4、2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种情况成为右右。

分析 : 情形1和情形4是关于α的镜像对称,情形2和情形3也是关于α的镜像对称,因此理论上看只有两种情况,但编程的角度看还是四种情形。

第一种情况是插入发生在“外边”的情形(左左或右右),该情况可以通过一次单旋转完成调整;第二种情况是插入发生在“内部”的情形(左右或右左),这种情况比较复杂,需要通过双旋转来调整。

下面就具体分析这四种情况:

一、单旋 : (左左、右右)

A:左左:右旋

在这里插入图片描述

左左情景直接右旋即可,不用详解。

B:右右:左旋

在这里插入图片描述

右右情景直接左旋即可,不在详解

二、双旋 : (左右、右左)

A:左右:先左旋再右旋

在这里插入图片描述

上面的左左、右右(图2、图8)看明白了,可这里左右情景为什么要旋转两次呢?为什么先左旋,再右旋呢?

看看这种情况:(图4)

在这里插入图片描述

毫无疑问这也是 左右 情景(左右情景有很多种,图3演示的是最基础的情景,所有 的左右情景的旋转情况和图3都是一样的),那么该怎么旋转呢?
在这里插入图片描述

这样直接右旋不对吧?因为6节点的右子树(以根节点10为中心,靠近内部的子树)7-8经过旋转之后要充当10节点的左子树,这样会导致依旧不平衡。所以在这种左右情景下需要进行两次旋转,先把6的右子树降低高度,然后在进行右旋。即:

在这里插入图片描述

如图7 情景和图3的情景一样,这就是为什么 左右情景 需要先左旋再右旋的原因。

在这里可以记作:最小不平衡节点的左节点的内部(以根节点做对称轴,偏向对称轴的为内部。也就是以7为节点的子树)的子树高度高于外部子树的高度时需要进行两次旋转。

B:右左:先右旋再左旋
在这里插入图片描述

如同左右情景,考虑到图10的 右左情景

在这里插入图片描述

这种情景旋转如图11
在这里插入图片描述

旋转的四种情景就以上这些了。

需要举例说明的是,下面这两对情景旋转是一样的。

下图都是右左情景,具体看代码的旋转方法就明白了在第一次右旋的时候进行的操作。private Node rotateSingleRight(Node node);
在这里插入图片描述

下图都是左右情景,第一次左旋见:private Node rotateSingleLeft(Node node);
在这里插入图片描述

旋转情景弄明白之后就是怎么代码实现了,在实现代码之前需要考虑如何进行树高判断。这里就根据定义来,|左子树树高-右子树树高|<2。如果大于等于2则该节点就不再平衡,需要进行旋转操作。因此在程序中节点中需要定义一个height属性来存储该节点的树高。

由于平衡二叉树的性质,二叉树的高度不会很高,程序使用递归进行数据插入查找不会造成栈溢出异常,所以程序采用递归操作进行插入查找。

平衡的判定策略是在进行递归回溯的时候依照回溯路径更新节点的树高,然后根据|左子树树高-右子树树高|<2来判定该节点是否失衡,进一步对是够旋转进行判定。

程序中的平衡判定策略比较漂亮,当时就是一直卡在这里无法继续进行,然后参考了 AVL树-自平衡二叉查找树(Java实现) 之后采用这种方法才得以解决。

平衡二叉树的删除操作

对于平衡二叉树的删除操作,只要明白一点就可以了:

如果该节点没有左右子树(该节点为叶子节点)或者只有其中一个子树则可以直接进行删除。

否则需要继续进行判定该节点:如果该节点的外部(内外:以根节点做对称轴,靠近对称轴的子树为内部子树)子树树高低于内部子树树高,则找到该节点内部子树的最值(最值:如果内部子树是该节点的右子树,则数值为右子树的最小值;如果内部节点是该节点的左子树,则数值为该节点左子树的最大值)进行数值交换,交换之后删除该节点即可。

删除之后进行回溯的时候要更新节点的树高,然后判断节点是否平衡,不平衡进行旋转。这时对旋转次数的判定就不同于插入时的判定。

如图14 删除11节点

在这里插入图片描述

这种情景需不需要进行两次旋转?该如何判定?

毫无疑问肯定是要进行一次右旋的,但是在右旋之前是不是要进行一次左旋呢?

这就要根据最小不平衡节点的左节点6进行判定,如果6的左节点树高低于6的右节点树高则需要进行一次左旋,最后进行一次右旋结束。

如果6的左子树树高高于6的右子树树高则不需进行左旋可以直接对10节点进行右旋结束操作。

如图15,这种情况肯定需要进行左旋,至于在左旋之前要不要对13节点进行右旋,相信知道该如何判断了。

根据13节点的左右子树高度来判断,左子树(内部)高于右子树(外部)高度则需要进行右旋,图15这种情景是不需要的。

在这里插入图片描述

代码实现:

知道了各种旋转的判定标准,程序中就没有其他什么难点了,下面看一下代码:

/**
* @date:2018年9月04日
*/
// 存储数据类型必须实现Comparable接口,实现比较方法
public class AVLTree<T extends Comparable<T>> {private Node<T> root;// 定义节点存储数据private static class Node<T> {Node<T> left;// 左孩子Node<T> right;// 右孩子T data; // 存储数据int height; // 树高public Node(Node<T> left, Node<T> right, T data) {this.left = left;this.right = right;this.data = data;this.height = 0;}}// 对外公开的方法进行插入public Node<T> insert(T data) {return root = insert(data, root);}// 私有方法进行递归插入,返回插入节点private Node<T> insert(T data, Node<T> node) {// 递归终止条件if (node == null)return new Node<T>(null, null, data);// 比较插入数据和待插入节点的大小int compareResult = data.compareTo(node.data);if (compareResult > 0) {// 插入node的右子树node.right = insert(data, node.right);// 回调时判断是否平衡if (getHeight(node.right) - getHeight(node.left) == 2) {// 不平衡进行旋转// 判断是需要进行两次旋转还是需要进行一次旋转int compareResult02 = data.compareTo(node.right.data);if (compareResult02 > 0)// 进行一次左旋(右右)node = rotateSingleLeft(node);else// 进行两次旋转,先右旋,再左旋node = rotateDoubleLeft(node);}} else if (compareResult < 0) {// 插入node的左子树node.left = insert(data, node.left);// 回调时进行判断是否平衡if (getHeight(node.left) - getHeight(node.right) == 2) {// 进行旋转// 判断是需要进行两次旋转还是需要进行一次旋转int intcompareResult02 = data.compareTo(node.left.data);if (intcompareResult02 < 0)// 进行一次左旋(左左)node = rotateSingleRight(node);else// 进行两次旋转,先左旋,再右旋node = rotateDoubleRight(node);}}// 重新计算该节点的树高node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;return node;}// 右右情况--进行左旋private Node<T> rotateSingleLeft(Node<T> node) {Node<T> rightNode = node.right;node.right = rightNode.left;rightNode.left = node;// 旋转结束计算树高node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;rightNode.height = Math.max(node.height, getHeight(rightNode.right)) + 1;return rightNode;}// 左左情况--进行右旋private Node<T> rotateSingleRight(Node<T> node) {Node<T> leftNode = node.left;node.left = leftNode.right;leftNode.right = node;// 旋转结束计算树高node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;leftNode.height = Math.max(getHeight(leftNode.left), node.height) + 1;return leftNode;}// 右左情况--先右旋再左旋private Node<T> rotateDoubleLeft(Node<T> node) {// 先进行右旋node.right = rotateSingleRight(node.right);// 再加上左旋node = rotateSingleLeft(node);return node;}// 左右--先左旋再右旋private Node<T> rotateDoubleRight(Node<T> node) {// 先进行左旋node.left = rotateSingleLeft(node.left);// 在进行右旋node = rotateSingleRight(node);return node;}// 计算树高private int getHeight(Node<T> node) {return node == null ? -1 : node.height;}// public 方法供外部进行删除调用public Node<T> remove(T data) {return root = remove(data, root);}// 递归进行删除,返回比较节点private Node<T> remove(T data, Node<T> node) {if (node == null) {// 不存在此节店,返回null.不需要调整树高return null;}int compareResult = data.compareTo(node.data);if (compareResult == 0) {// 存在此节点进入/*** 找到节点之后进行节点删除操作 判断node是否有子树,如果没有子树或者只有一个子树则直接进行删除*     如果有两个子树,则需要判断node的平衡系数balance*         如果balance为0或者1则把node和node的左子树的最大值进行交换 否则把node和右子树的最小值进行交换*         交换数据之后删除该节点 删除之后判断delete节点的父节点是否平衡,如果不平衡进行节点旋转* 旋转之后返回delete节点的父节点进行回溯* */if (node.left != null && node.right != null) { // 此节点存在左右子树// 判断node节点的balance,然后进行数据交换删除节点int balance = getHeight(node.left) - getHeight(node.right);Node<T> temp = node;// 保存需要进行删除的node节点if (balance == -1) {// 与右子树的最小值进行交换exChangeRightData(node, node.right);} else {// 与左子树的最大值进行交换exChangeLeftData(node, node.left);}// 此时已经交换完成并且把节点删除完成,则需要重新计算该节点的树高temp.height = Math.max(getHeight(temp.left), getHeight(temp.right)) + 1;// 注意此处,返回的是temp,也就是保存的需要删除的节点,而不是替换的节点return temp;} else {// 把node的子节点返回调用处等于删除了node节点// 此处隐含了一个node.left ==null && node.right == null 的条件,这时返回nullreturn node.left != null ? node.left : node.right;}} else if (compareResult > 0) {// 没找到需要删除的节点继续递归进行寻找node.right = remove(data, node.right);// 删除之后进行树高更新node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;// 如果不平衡则进行右旋调整。if (getHeight(node.left) - getHeight(node.right) == 2) {// 进行旋转Node<T> leftSon = node.left;// 判断是否需要进行两次右旋还是一次右旋// 判断条件就是比较leftSon节点的左右子节点树高if (leftSon.left.height > leftSon.right.height) {// 右旋一次node = rotateSingleRight(node);} else {// 两次旋转,先左旋,后右旋node = rotateDoubleRight(node);}}return node;} else if (compareResult < 0) {// 没找到需要删除的节点继续递归进行寻找node.left = remove(data, node.left);// 删除之后进行树高更新node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;// 如果不平衡进行左旋操作if (getHeight(node.left) - getHeight(node.right) == 2) {// 进行旋转Node<T> rightSon = node.right;// 判断是否需要进行两次右旋还是一次右旋// 判断条件就是比较rightSon节点的左右子节点树高if (rightSon.right.height > rightSon.left.height) {node = rotateSingleLeft(node);} else {// 先右旋再左旋node = rotateDoubleLeft(node);}}return node;}return null;}// 递归寻找right节点的最大值private Node<T> exChangeLeftData(Node<T> node, Node<T> right) {if (right.right != null) {right.right = exChangeLeftData(node, right.right);} else {// 数据进行替换node.data = right.data;// 此处已经把替换节点删除return right.left;}right.height = Math.max(getHeight(right.left), getHeight(right.right)) + 1;// 回溯判断left是否平衡,如果不平衡则进行左旋操作。int isbanlance = getHeight(right.left) - getHeight(right.right);if (isbanlance == 2) {// 进行旋转Node<T> leftSon = node.left;// 判断是否需要进行两次右旋还是一次右旋// 判断条件就是比较leftSon节点的左右子节点树高if (leftSon.left.height > leftSon.right.height) {// 右旋一次return node = rotateSingleRight(node);} else {// 两次旋转,先左旋,后右旋return node = rotateDoubleRight(node);}}return right;}// 递归寻找left节点的最小值private Node<T> exChangeRightData(Node<T> node, Node<T> left) {if (left.left != null) {left.left = exChangeRightData(node, left.left);} else {node.data = left.data;// 此处已经把替换节点删除return left.right;}left.height = Math.max(getHeight(left.left), getHeight(left.right)) + 1;// 回溯判断left是否平衡,如果不平衡则进行左旋操作。int isbanlance = getHeight(left.left) - getHeight(left.right);if (isbanlance == -2) {// 进行旋转Node<T> rightSon = node.right;// 判断是否需要进行两次右旋还是一次右旋// 判断条件就是比较rightSon节点的左右子节点树高if (rightSon.right.height > rightSon.left.height) {return node = rotateSingleLeft(node);} else {// 先右旋再左旋return node = rotateDoubleLeft(node);}}return left;}// ************************中序输出  输出结果有小到大*************************************public void inorderTraverse() {inorderTraverseData(root);}// 递归中序遍历private void inorderTraverseData(Node<T> node) {if (node.left != null) {inorderTraverseData(node.left);}System.out.print(node.data + "、");if (node.right != null) {inorderTraverseData(node.right);}}
}

这段测试程序可以进行测试:

/**
* @date:2016年9月04日
*/
public class AVLTreeTest {@Testpublic void test01() {AVLTree tree = new AVLTree();int array[] = { 28, 35, 5, 35, 26, 30, 1, 21, 18, 35, 7, 30, 25, 1, 7, };for (int i = 0; i < array.length; i++) {System.out.print(array[i] + ",");tree.insert(array[i]);}System.out.println();tree.inorderTraverse();tree.remove(12);System.out.println();tree.inorderTraverse();}@Testpublic void test02() {AVLTree tree = new AVLTree();int temp = 0;for (int i = 0; i < 15; i++) {int num = (int) (Math.random() * 40);System.out.print(num + ",");tree.insert(num);temp = num;}System.out.println();tree.inorderTraverse();tree.remove(temp);// 删除插入的最后一个数据System.out.println();tree.inorderTraverse();}
}
http://www.dtcms.com/a/313585.html

相关文章:

  • async/await和Promise之间的关系是什么?(补充)
  • NSA稀疏注意力深度解析:DeepSeek如何将Transformer复杂度从O(N²)降至线性,实现9倍训练加速
  • 能表示旋转的矩阵是一个流形吗?
  • 【大模型篇】:GPT-Llama-Qwen-Deepseek
  • 数据结构重点内容
  • Go语言实战案例:多协程并发下载网页内容
  • 《 ThreadLocal 工作机制深度解析:高并发场景的利与弊》
  • Mysql深入学习:InnoDB执行引擎篇
  • C++ : 反向迭代器的模拟实现
  • 【图像处理基石】如何使用deepseek进行图像质量的分析?
  • vllm0.8.5:思维链(Chain-of-Thought, CoT)微调模型的输出结果包括</think>,提供一种关闭思考过程的方法
  • MCP协议:CAD地图应用的AI智能化解决方案(唯杰地图MCP)
  • 【数据结构与算法】数据结构初阶:排序内容加餐(二)——文件归并排序思路详解(附代码实现)
  • 【C++】面向对象编程
  • C语言(长期更新)第8讲 函数递归
  • 网络通信与Socket套接字详解
  • C#模式匹配用法与总结
  • 网页 URL 转 Markdown API 接口
  • 大模型中的Token和Tokenizer:核心概念解析
  • 【Unity3D实例-功能-镜头】俯视角
  • MySQL极简安装挑战
  • 数据结构代码
  • IO流-数据流
  • 语义分割--deeplabV3+
  • 企业级AI Agent构建实践:从理论到落地的完整指南
  • 机器学习中的经典算法
  • 算法讲解--最大连续1的个数
  • C++异常与智能指针,资源泄露
  • CMake 命令行参数完全指南
  • 【动态规划算法】路径问题