引
举例
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST)
创建出来的二叉排序树如下
分析存在的问题
- 左子树全部为空,从形式上看,更像一个单链表.
- 插入速度没有影响
- 查询速度明显降低(因为需要依次比较), 不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
由此我们才引入了平衡二叉树
平衡二叉树
基本介绍
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
- 具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
- 举例说明
例1
例2
创建平衡二叉树的方法
这里提出了一个新名词,左旋转
当右子树的高度高于左子树时进行:左旋转
首先我们先说左旋转的步骤
我们以数列【4,3,6,5,7,8】为例
我们先来看该数列的二叉排序树长什么样子
如下:我们可以看到,该二叉排序树并不满足平衡二叉树的子树的高度差不超过1的要求,我们为了使它的高度差小于等于1,那么我们需要使用到左旋转
左旋转步骤如下
-
创建一个新的结点,创建的新的结点的值等于当前根节点的值
-
把新结点的左子树设置为当前结点的左子树(当前结点就是根结点)
-
把新节点的右子树设置为当前结点的右子树的左子树
-
把当前结点的值更换为右子结点的值
-
把当前结点的右子树设置成当前结点的右子树的右子树
-
把当前结点的左子树指向新创建的那个结点
最终是这个样子的
我们可以看到经过左旋转,得到的新的二叉排序树就是一个平衡二叉树。
其中中间有一个左子节点指向5、右子结点指向7的结点,这个结点因为没有任何引用,所以会被java的垃圾回收机制回收
代码实现:
首先我们需要一个能够判断当以当前结点为根结点的树的高度的方法,和判断当前结点的左子树的高度和判断当前结点的右子树的高度的方法。
因为我们后面使用左旋转还是右旋转的依据就是树的高度
这段代码打断点分析分析还是可以分析明白的
/**
* 返回左子树的高度
* @return
*/
public int leftHeight(){
return left == null ? 0 : left.height();
}
/**
* 返回右子树的高度
* @return
*/
public int rightHeight(){
return right == null ? 0 : right.height();
}
/**
* 返回当前结点的高度(这个高度包含当前结点)
* @return
*/
public int height(){
int height = Math.max(leftHeight(),rightHeight())+1;
return height;
}
然后我们来实现AVL树的左旋转代码
- 创建新的结点,以当前根节点的值创建的
- 把新的结点的左子树设置为当前根结点的左子树
- 把新的结点的右子树设置成当前根结点的右子树的左子树
- 把当前根结点的值替换成右子结点的值
- 把当前根结点的右子树设置为当前节点的右子树的右子树
- 把当前结点的左子树设置成新的节点
/**
* 左旋转
*/
private void leftRotate(){
// 1、创建新的结点,以当前根节点的值创建的
Node newNode = new Node(value);
// 2、把新的结点的左子树设置为当前根结点的左子树
newNode.left = left;
// 3、把新的结点的右子树设置成当前根结点的右子树的左子树
newNode.right = right.left;
// 4、把当前根结点的值替换成右子结点的值
value = right.value;
// 5、把当前根结点的右子树设置为当前节点的右子树的右子树
right = right.right;
// 6、把当前结点的左子树设置成新的节点
left = newNode;
}
然后在Node类中的add方法后面判断左子树和右子树的高度,如果差值大于1,并且是右子树高于左子树,那么就进行左旋转
/**
* 使用二叉排序树的规则进行添加节点
* @param node
*/
public void add(Node node){
if (node == null){
return;
}
// 判断传入的节点的value是否小于当前节点的value
if (node.value < this.value){
// 因为传入节点的value小于当前节点的value,
// 所以当当前节点的左节点为null时,就可以直接把传入进来的这个节点挂到做叶子节点上
if (this.left == null){
// 表示将传入的节点挂到左子结点
this.left = node;
}else {
// 如果左子结点不为null的话,那么表示左子结点还有节点,所以我们需要继续判断
this.left.add(node);
}
}else {
// 如果当前添加的节点的值大于或者等于左子节点的值
// 那么判断有节点是否为null,如果为null则直接挂上,如果不为null,则递归add
if (this.right == null){
// 其实这个null就是归回来的条件
this.right = node;
}else {
this.right.add(node);
}
}
// 当添加完一个结点后,如果:(右子树的高度-左子树的高度)>1,那么就进行左旋转
if (rightHeight() - leftHeight() > 1){
// 左旋转
leftRotate();
}
}
当左子树的高度高于右子树时进行:右旋转
要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
现在来看此时的平衡二叉树长相如下
右旋转的步骤如下
其实就是完全相反的左旋转步骤
代码实现
/**
* 右旋转
*/
private void rightRotate(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
AVLTree中的方法add方法如下
/**
* 使用二叉排序树的规则进行添加节点
* @param node
*/
public void add(Node node){
if (node == null){
return;
}
// 判断传入的节点的value是否小于当前节点的value
if (node.value < this.value){
// 因为传入节点的value小于当前节点的value,
// 所以当当前节点的左节点为null时,就可以直接把传入进来的这个节点挂到做叶子节点上
if (this.left == null){
// 表示将传入的节点挂到左子结点
this.left = node;
}else {
// 如果左子结点不为null的话,那么表示左子结点还有节点,所以我们需要继续判断
this.left.add(node);
}
}else {
// 如果当前添加的节点的值大于或者等于左子节点的值
// 那么判断有节点是否为null,如果为null则直接挂上,如果不为null,则递归add
if (this.right == null){
// 其实这个null就是归回来的条件
this.right = node;
}else {
this.right.add(node);
}
}
// 当添加完一个结点后,如果:(右子树的高度-左子树的高度)>1,那么就进行左旋转
if (rightHeight() - leftHeight() > 1){
// 左旋转
leftRotate();
}
// 当添加完一个结点之后,如果:(左子树的高度-右子树的高度)> 1,那么就需要进行右旋转
if (leftHeight() - rightHeight() > 1){
// 右旋转
rightRotate();
}
}
问题
这时候还是存在问题的,
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 };
这里我们来画图说明
这个数组组成的二叉排序树为
然后使用我们前面的右旋转进行变换的到的是
整理后的如下
我们可以看到,虽然我们进行了右旋转,但是最终还是不满足平衡二叉树的条件
什么情况下才会导致这样的事情出现呢?
这里我们不探究出现的原因,因为原因涉及到数学知识。
在我们进行右旋转的时候,如果根节点的左子树的右子树的高度大于它左子树的左子树的高度时,会出现这样的问题
如何解决呢?
那就是先将根节点的左子树的右子树以根节点的左子树为根节点进行左旋转,然后将旋转后的树,以根节点为准再次进行右旋转,最终就可以得到一个平衡二叉树了
知道如何解决了之后我们进行代码实现,我们还是进行更改node结点类的add方法,并且注意这里我们问题举例是使用的右旋转时的问题,但是其实左旋转也有相同的问题,而且和右旋转完全相反,代码如下:
/**
* 使用二叉排序树的规则进行添加节点
* @param node
*/
public void add(Node node){
if (node == null){
return;
}
// 判断传入的节点的value是否小于当前节点的value
if (node.value < this.value){
// 因为传入节点的value小于当前节点的value,
// 所以当当前节点的左节点为null时,就可以直接把传入进来的这个节点挂到做叶子节点上
if (this.left == null){
// 表示将传入的节点挂到左子结点
this.left = node;
}else {
// 如果左子结点不为null的话,那么表示左子结点还有节点,所以我们需要继续判断
this.left.add(node);
}
}else {
// 如果当前添加的节点的值大于或者等于左子节点的值
// 那么判断有节点是否为null,如果为null则直接挂上,如果不为null,则递归add
if (this.right == null){
// 其实这个null就是归回来的条件
this.right = node;
}else {
this.right.add(node);
}
}
// 当添加完一个结点后,如果:(右子树的高度-左子树的高度)>1,那么就进行左旋转
if (rightHeight() - leftHeight() > 1){
// 如果它的右子树的左子树高度大于它的右子树的右子树的高度高度
if (right != null && right.leftHeight() > right.rightHeight()){
// 先对当前节点的右结点进行右旋转
right.rightRotate();
// 再对旋转后 当前结点进行左旋转
leftRotate();
}else {
// 左旋转
leftRotate();
}
// 必须加return
return;
}
// 当添加完一个结点之后,如果:(左子树的高度-右子树的高度)> 1,那么就需要进行右旋转
if (leftHeight() - rightHeight() > 1){
// 如果它的左子树的右子树高度大于它的左子树的高度
if (left != null && left.rightHeight() > left.leftHeight()){
// 先对当前结点的左结点,进行左旋转
left.leftRotate();
// 再对当前节点进行右旋转
rightRotate();
}else {
// 直接进行右旋转
rightRotate();
}
}
}
完整实现代码
package com.hegong.avl;
/**
* @author 14767
*/
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};
int[] arr = { 10, 11, 7, 6, 8, 9 };
// 创建avl树
AVLTree avlTree = new AVLTree();
// 添加结点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
avlTree.infixOrder();
System.out.println("在没有做平衡处理前");
System.out.println(avlTree.getRoot().height());
System.out.println(avlTree.getRoot().leftHeight());
System.out.println(avlTree.getRoot().rightHeight());
}
}
class AVLTree{
private Node root;
public Node getRoot() {
return root;
}
/**
* 调用节点自身的add方法,按照二叉排序树的规则排序
* @param node
*/
public void add(Node node){
if (root == null){
root = node;
}else {
root.add(node);
}
}
/**
* 调用节点自身的infixOrder方法进行中序遍历方法
*/
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("二叉排序树为空,不能遍历");
}
}
/**
* 查找要删除的结点
* @param value
* @return
*/
public Node search(int value){
if (root == null){
return null;
}else {
return root.search(value);
}
}
/**
* 查找父节点的方法
* @param value
* @return
*/
public Node searchParent(int value){
if (root == null){
return null;
}else {
return root.searchParent(value);
}
}
/**
* 用于返回右子树的最小值,并且删除这个最小值的结点
* @param node 传入的参数作为根结点
* @return 返回的以参数node为根节点的二叉排序树的最小节点的值
*/
public int delRightTreeMin(Node node){
Node target = node;
// 循环查找左系欸但,就会找到最小值
while (target.left != null){
// 因为如果想要找到当前树的最小值,那肯定在左子树的子结点等于null的时候,target就是最小值的结点了
// 所以我们只要target的左子结点不为null就一直找
target = target.left;
}
// 此时如果退出循环,则表示找到最小结点
// 此时的的target就是最小值所在的结点
// 然后我们将最小结点删除
delNode(target.value);
// 返回出最小的值
return target.value;
}
/**
* 删除结点
* @param value 要删除的结点的值
*/
public void delNode(int value){
if (root == null){
// 如果当root为null则直接退出方法,因为没得可删
return;
}else {
// 1、先找到要删除的结点 targetNode
Node targetNode = search(value);
if (targetNode == null){
// 如果没有找到要删除的结点,则return退出方法
return;
}
// 因为要删除的目标结点如果不存在,那么上面就已经退出删除的方法了
// 所以当root的左右子结点都为null的时候,必然要删除的就是root
if (root.left == null && root.right == null){
root = null;
return;
}
// 然后去找targetNode的父节点
Node parent = searchParent(value);
// 我们前面分析的情况一
// 要删除的结点时叶子结点
if (targetNode.right == null && targetNode.left == null){
// 当要删除的结点的左子结点和右子结点都为null的时候表示当前要删除的结点为叶子结点
// 判断targetNode是父结点的左子结点还是右子结点
if (parent.left != null && parent.left.value == value){
parent.left = null;
}else if (parent.right != null && parent.right.value == value){
parent.right = null;
}
}else if (targetNode.left != null && targetNode.right != null){
// 满足上述条件则表示这是我们前面分析的情况3,要删除的结点左右子树都存在的情况
// 因为删除两个子树的情况比较好写,所以我们先写删除两颗子树的情况,
// 因为向右子树找所以传递右子结点
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
}else {
// 又因为上面已经把要删除的结点为叶子结点的情况和要删除的结点有两个子树的情况已经实现了,
// 所以else中都是只有一颗子树的情况,省去了我们写判断的麻烦
if (targetNode.left != null){
// 注意需要判断父结点不为null,因为存在一种情况就是当,只剩下根节点和一个子结点的情况
// 此时如果要删除只有一颗子树的结点,那么这个结点就是根节点
if (parent != null){
// 因为else中都是只有一个子树的情况,所以当targetNode.left!=null则表明唯一存在的子树就是左子树
if (parent.left.value == value){
// 表示要删除的结点为父节点的左子结点
parent.left = targetNode.left;
}else {
// 表示targetNode是parent的右子结点
parent.right =targetNode.left;
}
}else {
// 当要删除的是根节点的时候,我们只需要把它唯一存在的子结点赋给root即可
root = targetNode.left;
}
}else {
if (parent != null){
// 表示要删除的结点的唯一子树为右子树
if (parent.left.value == value){
parent.left = targetNode.right;
}else {
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
}
/**
* 该类为一个节点类
*/
class Node{
int value;
/**
* 右子节点
*/
Node right;
/**
* 左子节点
*/
Node left;
public Node(int value) {
this.value = value;
}
/**
* 返回左子树的高度
* @return
*/
public int leftHeight(){
return left == null ? 0 : left.height();
}
/**
* 返回右子树的高度
* @return
*/
public int rightHeight(){
return right == null ? 0 : right.height();
}
/**
* 返回当前结点的高度(这个高度包含当前结点)
* @return
*/
public int height(){
// 获取右子树和左子树的最大高度,然后加上当前结点的1,返回以当前结点为根节点的树的高度
return Math.max(leftHeight(),rightHeight())+1;
}
/**
* 左旋转
*/
private void leftRotate(){
// 1、创建新的结点,以当前根节点的值创建的
Node newNode = new Node(value);
// 2、把新的结点的左子树设置为当前根结点的左子树
newNode.left = left;
// 3、把新的结点的右子树设置成当前根结点的右子树的左子树
newNode.right = right.left;
// 4、把当前根结点的值替换成右子结点的值
value = right.value;
// 5、把当前根结点的右子树设置为当前节点的右子树的右子树
right = right.right;
// 6、把当前结点的左子树设置成新的节点
left = newNode;
}
/**
* 右旋转
*/
private void rightRotate(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
/**
* 查找要删除的结点
* @param value 想要删除结点的值
* @return 如果找到要返回的结点就返回,否则返回null
*/
public Node search(int value){
// 如果要查找的value等于当前结点的value,那么返回当前结点
if (value == this.value){
return this;
}else if (value < this.value){
// 如果要删除的结点的值小于当前结点的值,那么根据二叉排序树的特点我们应该继续向左找
// 在查找之前我们需要判断左子树是否为空如果为空的话,那么我们就返回null
if (this.left == null){
return null;
}
return this.left.search(value);
}else{
// 如果要删除的结点的值大于或者等于当前结点的值,那么根据二叉排序树的特点我们应该继续向右找
// 判断右子树是否为空,如果为空则返回null
if (this.right == null){
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除结点的父节点
* @param value 要找到的结点的值
* @return 返回的是要找到的结点的父节点,如果没有则返回null
*/
public Node searchParent(int value){
// 情形1:当左子结点不为空并且左子结点的值等于要找的结点的值,那么说明当前结点为左子结点的父节点
boolean conditionLeft = (this.left != null && this.left.value == value);
// 情形2:当右子结点不为空并且右子结点的值等于要找的结点的值,那么说明当前结点为右子结点的父节点
boolean conditionRight = (this.right != null && this.right.value == value);
// 当满足情形1或者情形2任意一个条件,则表示当前结点为要删除结点的父节点,可以返回它
if (conditionLeft || conditionRight){
return this;
}else {
// 如果查找的值小于当前结点得值,并且当前左子结点不为空,则向左查找
if (value < this.value && this.left != null){
return this.left.searchParent(value);
}else if (value >= this.value && this.right != null){
// 如果要查找的值大于等于当前结点的值,并且右子结点的值不为空,则向右查找
return this.right.searchParent(value);
}else {
// 如果只满足一边,必须value小于this.value但是this.left=null
// 或者根节点,同样没有父节点
// 我们这时候就需要返回null
return null;
}
}
}
/**
* 只输出当前节点的值
* @return
*/
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 使用二叉排序树的规则进行添加节点
* @param node
*/
public void add(Node node){
if (node == null){
return;
}
// 判断传入的节点的value是否小于当前节点的value
if (node.value < this.value){
// 因为传入节点的value小于当前节点的value,
// 所以当当前节点的左节点为null时,就可以直接把传入进来的这个节点挂到做叶子节点上
if (this.left == null){
// 表示将传入的节点挂到左子结点
this.left = node;
}else {
// 如果左子结点不为null的话,那么表示左子结点还有节点,所以我们需要继续判断
this.left.add(node);
}
}else {
// 如果当前添加的节点的值大于或者等于左子节点的值
// 那么判断有节点是否为null,如果为null则直接挂上,如果不为null,则递归add
if (this.right == null){
// 其实这个null就是归回来的条件
this.right = node;
}else {
this.right.add(node);
}
}
// 当添加完一个结点后,如果:(右子树的高度-左子树的高度)>1,那么就进行左旋转
if (rightHeight() - leftHeight() > 1){
// 如果它的右子树的左子树高度大于它的右子树的右子树的高度高度
if (right != null && right.leftHeight() > right.rightHeight()){
// 先对当前节点的右结点进行右旋转
right.rightRotate();
// 再对旋转后 当前结点进行左旋转
leftRotate();
}else {
// 左旋转
leftRotate();
}
// 必须加return
return;
}
// 当添加完一个结点之后,如果:(左子树的高度-右子树的高度)> 1,那么就需要进行右旋转
if (leftHeight() - rightHeight() > 1){
// 如果它的左子树的右子树高度大于它的左子树的高度
if (left != null && left.rightHeight() > left.leftHeight()){
// 先对当前结点的左结点,进行左旋转
left.leftRotate();
// 再对当前节点进行右旋转
rightRotate();
}else {
// 直接进行右旋转
rightRotate();
}
}
}
/**
* 中序遍历
* 一直向左递归,直到左边递归到null然后输出当时的节点的值,然后向右进行递归,直到右边递归到null,然后一直归回去
*/
public void infixOrder(){
if (this.left != null){
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null){
this.right.infixOrder();
}
}
}