AVL树的实现(代码实现 + 测试)
我们先给出AVL树的实现代码:
- 注意: 此时这个AVL树中只是在增加结点的时候做了平衡化处理, 但是在删除结点的时候没有做平衡化处理
package com.ffyc.tree.avl树;
//首先我们先来创建一个Node结点类, 我们创建一个Node结点类之后在Node结点类中实现两个递归方法, 一个方法是递归来增加结点的方法, 一个是来递归遍历的方法
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
//添加结点的方法
//递归的形式添加结点, 注意: 需要满足是二叉排序树的需求
/**
* @param node 这个node是待添加的结点
*/
public void add(Node node){
//这个递归方法我们每次递归的时候是改变的方法的调用者
//1.判断入参
// 注意: 这个时候只是我们对入参的判断 , 并不是递归的结束条件
if(node == null){//如果待添加结点是一个null的时候那么我们就不用添加这个节点了, 直接就退出就可以了
return; //由于此时我们实现的递归方法是没有返回值的, 所以这个时候我们我们直接退出就可以了
}
//如果node的值不为空, 那么就说明这个节点是合法的, 所以我们就要来判断这个节点要添加的位置
if(node.value < this.value) {//如果这个时候node节点的value值要小于我们的this的value的值, 那么我们肯定是要去左子树上继续查找, 所以这个时候我们就要使用左子节点来递归调用此方法
if(this.left != null){
//如果当前节点的左子节点不为空的时候此时我们才继续向左子树去遍历,如果这个时候左子节点为空了, 那么就表明这个位置其实我们是已经找到了
this.left.add(node);
}else{
//如果当前节点的左子节点为空, 那么就证明这个位置就是我们节点要添加的位置, 我们此时就就将我们的节点直接放到当前节点的左子节点的位置即可
this.left = node;
}
}else { //如果node的值(也就是待添加结点的值大于或者是等于我们当前节点的value值的时候我们就要向右子树上进行遍历)
// 这个时候一定要注意: 我们此时如果等于的时候也是向右子树去查找位置了, 就说明最后的时候我们如果两个结点的值是一样大的, 那儿我们将这个节点添加到了右子树上
// 对应的如果执行其他的二叉排序树的操作的时候如果我们要判断两个值相同的情况的时候我们就要去右子树上去看
if(this.right != null) {
//判断当前节点的右子节点是否为null ,如果不为空的时候我们就递归的向右子树进行一个遍历
this.right.add(node);
}else{
//如果此时当前节点的右子节点为空了,那么就表示我们找到了待插入节点的插入位置, 我们直接将我们的待插入节点插入即可
this.right = node;
}
}
// ==== 添加结点完成 ====
//当添加完一个节点之后, 如果左子树高度 - 右子树高度 > 1, 说明不平衡, 并且类型为L开头
if(leftHeight() - rightHeight() > 1){
//如果它的左子节点的左子树高度大于它的右子树高度, 说明类型为LL
if(left != null && left.leftHeight() > left.rightHeight()){
//直接对当前节点进行一个右旋即可 --> 当前节点就是此时LL型的最小不平衡子树的根节点
rightRotate();
}else{ //说明为LR型(此时肯定是左子节点的右子树高度大于左子树高度)
//先对当前节点的左子节点进行一个左旋 --> 就能得到一个LL型最小不平衡子树
left.leftRototate();
//再对当前节点进行一个右旋 ---> 也就是是对转换之后的LL型最小不平衡子树进行了一个右旋
rightRotate();
}
}
//当添加完一个节点之后, 如果右子树高度 - 左子树高度 > 1, 说明不平衡, 并且最小不平衡子树类型以R开头
if(rightHeight() - leftHeight() > 1){
//如果它的右子节点的右子树高度大于它的左子树高度, 说明类型为RR型
if(right != null && right.rightHeight() > right.leftHeight()){
//直接对当前节点进行一个左旋即可
leftRototate();
}else{ //说明为RL型
//先对当前节点的右子节点进行一个右旋
right.rightRotate();
//然后对当前节点进行一个左旋
leftRototate();
}
}
}
//中序遍历(注意: 我们的二叉排序树的中序遍历之后的结果是一个有序序列, 如果这个时候我们构建的二叉排序树使用中序遍历之后的结果是一个有序的序列的时候我们就说明我们这个二叉排序树是没有问题的)
// 注意:我们此时的中序遍历只是为了判断我们的当前的二叉排序树的创建和添加元素的方法是否是正确的
//因为此时我们的二叉排序树的中序遍历的方法是编写到了Node结点类中, 所以其实执行递归调用的时候改变的是这个遍历方法的调用者
public void infixOrder(){
//此递归方法中我们并没有将递归终止条件写明, 这个方法时没有返回值的, 所以我们这里采用的方式是如果满足递归条件的时候我们才执行递归操作,
//如果不满足递归条件的时候我们就直接执行完了, 因为这个方法中没有任何是需要执行的
if(this.left != null) {
//递归的向左判断
this.left.infixOrder();
}
//输出当前元素
System.out.println(this.value);
if(this.right != null) {
//递归的向右判断
this.right.infixOrder();
}
}
//结点类中查找待删除结点的递归方法
public Node search(int val){
//1. 递归结束条件
if(this.value == val){
//如果当前节点的value值等于val值, 那么就表示当前节点就是我们要查询的结点
return this;
}
//如果val值大于当前节点的value值, 那么就向右子树去判断
else if(val > this.value){
if(this.right == null){
return null;
}else{
//此时一定要记得将我们查询到的结点一层一层的返回回来, 因为我们此时其实就是要求我们的待删除结点, 当找到了待删除结点之后一定要将待删除结点返回, 如果找不到就返回一个null
return this.right.search(val);
}
}
//如果val值小于当前节点的value值, 那么就向左子树去判断
else{
if(this.left == null){
//如果左子节点为null的时候就表示当前二叉排序树中没有我们正在查询的结点
return null;
}else{
return this.left.search(val);
}
}
}
//Node类中的递归查找待删除结点的父节点的方法
public Node searchParent(int val){
//1. 递归终止条件
// 如果当前节点的左子节点或者右子节点的值等于val值, 那么就表明当前的结点为待删除结点的父节点, 我们就直接将当前节点返回即可
if((this.left != null && this.left.value == val) || (this.right != null && this.right.value == val)){
return this;
}
//如果当前节点的左右孩子节点的值都不和val相等, 那么就判断当前节点的value属性值和val值的大小
//如果当前节点的value属性值大于val值, 那么就去左子树上进行查找
if(this.value > val && this.left != null){
return this.left.searchParent(val);
}
if(this.value < val && this.right != null){
return this.right.searchParent(val);
}
return null;
}
//重写toString()方法
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//返回以当前节点为根节点的树的高度
public int height(){
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//返回左子树的高度
public int leftHeight(){
if(left == null){
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHeight(){
if(right == null){
return 0;
}
return right.height();
}
//左旋转算法
private void leftRototate(){
//创建新的结点, 以当前根节点的值
Node newNode = new Node(this.value);
//让新的结点的左子节点指向当前根节点的左子节点
newNode.left = this.left;
//让新的结点的右子树指向当前节点的右子节点的左子树
newNode.right = this.right.left;
//把当前节点的值替换成其右子节点的值
this.value = right.value;
//让当前结点的右子节点指向当前结点的右子节点的右子树
right = right.right;
//让当前结点的左子节点指向新创建的结点
left = newNode;
}
//右旋转算法
private void rightRotate(){
//创建新的结点, 以当前根节点的值
Node newNode = new Node(this.value);
//让新的结点的右子节点指向当前根节点的右子树
newNode.right = this.right;
//让新结点的左子节点指向当前根节点左子节点的右子树
newNode.left = this.left.right;
//把当前结点的值替换为当前根节点的左子节点的值
this.value = this.left.value;
//让当前节点的左子节点指向当前节点的左子节点的左子树
this.left = this.left.left;
//让当前节点的右子节点指向新结点
right = newNode;
}
}
/**
* 创建一个二叉排序树类,在二叉排序树类中向外提供添加元素和遍历的方法
*/
public class AVLTree {
//声明当前二叉排序树类的根节点
private Node root;
//创建添加元素的方法
public void add(Node node){
//这个时候我们要使用root引用数据来调用add()方法, 此时我们就要在调用方法之前判断当前的root引用是否为空, 要做一个合法性判断
if(root != null){
root.add(node);
}else{
//如果根节点为空的时候就直接将这值副给根节点即可
root = node;
}
}
//中序遍历方法
public void infixOrder(){
//这个时候同样我们也是要使用root引用数据来调用方法, 所以我们就要判断这个root引用是否为空
if(root != null){
root.infixOrder();
}else{
//如果这个root值不为空的时候就直接退出即可,顺便可以打印一个提示信息
System.out.println("当前链表为空~~~");
return;
}
}
//查询待删除节点的方法
public Node search(int val){
if(root != null) {
//判断引用是否为空,如果引用不为空就调用递归方法
return root.search(val);
}else{
//如果引用为空, 返回一个null
return null;
}
}
//查询待删除结点的父节点的方法
public Node searchParent(int val){
if(root != null) {
//如果引用是否为空, 如果引用为空, 那么调用方法的时候出现空指针, 所以我们要避免这种空指针
return root.searchParent(val);
}else{
return null;
}
}
//二叉排序树中删除结点的方法
public void delNode(int val) {
//需求先找到删除的结点targetNode
Node targetNode = search(val);
//求出待删除结点的父节点
Node parent = searchParent(val);
/**
* 删除叶子结点的方法
* 分为两种情况:
* 1. 如果只有一个节点,也就是只有一个根节点的时候也是叶子结点的删除
* 2. 对于一般情况 , 我们就是判断targetNode是parent的左子节点还是右子节点, 并且对应的删除
* 3. 然后判断第一种情况是否是区别于第二种情况而特殊存在的, 如果是特殊的, 那么就需要进行一个特殊判断
*/
if(root == null){
//如果树为空, 那么肯定就是没有删除结点的, 所以就直接返回即可
return;
}else{
//如果没有找到删除的结点
if(targetNode == null){
return ;
}
//如果删除的只有一个根节点, 那么就直接将根节点置空
if(root.left == null && root.right == null){
root = null;
return ;
}
//表示targetNode是一个叶子结点
if(targetNode.right == null && targetNode.left == null){
//判断targetNode结点是parent结点的左子节点还是右子节点
//如果targetNode是parent结点的左子节点
/*
这个时候我们要使用parent引用来调用left属性, 所以就要判断parent是否为空,由于此时我们删除的是叶子结点, 删除的结点为根节点的情况,也就是
parent为空的情况已经被我们排除了, 所以我们不用再次重复判断
*/
if(targetNode == parent.left){
parent.left = null;
}else{
parent.right = null;
}
}else if(targetNode.left != null && targetNode.right != null){
//因为我们判断待删除结点有两个子节点比较容易, 判断待删除结点有一个子节点比较困难, 所以我们就直接判断待删除结点有两个结点的情况,最后else中就是有一个子节点的情况
/*
这个方法不仅仅要删除掉待删除结点的右子树上的最小的值所在结点, 并且还要将这个最小值返回
*/
int midVal = delRightTreeMin(targetNode.right);
//然后更新待删除结点的value属性为这个右树上的最小值
targetNode.value = midVal;
}else{// 删除只有一颗子节点的结点
//如果要删除的结点有左子节点
if(targetNode.left != null){
//如果targetNode是parent的左子节点
/*
这个时候其实还有一种情况我们没有判断, 就是如果待删除结点有一个子节点的时候待删除结点是根节点的时候, 那么这个时候我们这样判断就会出现一个空指针异常
*/
if(parent != null) {
if (parent.left != null && parent.left == targetNode) {
parent.left = targetNode.left;
}
//如果targetNode是parent结点的右子节点
if (parent.right != null && parent.right == targetNode) {
parent.right = targetNode.left;
}
}else{
/*
如果待删除结点是根节点的时候,并且如果这个时候根节点有左子节点, 这个时候就直接让root = targetNode.left即可,
这里可以替代为root = root.left, 因为此时targetNode其实就是root
*/
root = targetNode.left;
}
}else{ //如果要删除的结点有右子节点
//如果targetNode是parent的左子节点
if(parent != null) {
if (parent.left != null && parent.left == targetNode) {
parent.left = targetNode.right;
}
//如果targetNode是parent的右子节点
if (parent.right != null && parent.right == targetNode) {
parent.right = targetNode.right;
}
}else{
root = targetNode.right;
}
}
}
}
}
private int delRightTreeMin(Node node) {
/*
因为是要找到最小值, 所以肯定是要一直向左遍历, 遍历到最左边的时候就是最小值了
*/
while(node.left != null){
node = node.left;
}
//退出while循环的时候node就是指向了最小值结点
/*
此时我们要删除这个最小值结点, 但是注意: 删除的时候一定是调用我们的删除结点的方法, 因为此时这个结点可能是一个叶子结点也可能是有一个子节点的结点, 所以删除还是比较麻烦的
*/
delNode(node.value);
return node.value;
}
//求当前树的根节点右子树高度的算法
public int rightHeight(){
return root.rightHeight();
}
//求当前树的根节点左子树高度的算法
public int leftHeight(){
return root.leftHeight();
}
//求当前树的高度的算法
public int Height(){
return root.height();
}
}
然后给出测试代码:
/*
这里我们进行一个测试:
*/
public static void main(String[] args) {
//创建一个平衡化二叉树
AVLTree avlTree = new AVLTree();
//根据序列{10, 11, 7, 6, 8, 9}建树
Node node1 = new Node(10);
Node node2 = new Node(11);
Node node3 = new Node(7);
Node node4 = new Node(6);
Node node5 = new Node(8);
Node node6 = new Node(9);
avlTree.add(node1);
avlTree.add(node2);
avlTree.add(node3);
avlTree.add(node4);
avlTree.add(node5);
avlTree.add(node6);
//判断根节点的左右子树高度差是否大于1, 如果此时高度差不大于1, 就说明我们的AVL树构建成功了
//如果是不同的二叉搜索树, 那么根据这个序列建树的时候建立的肯定不是一个平衡树, 也就是左右子树高度差肯定大于1了
System.out.println("此时的根节点的左子树高度为: " + avlTree.leftHeight()); //2
System.out.println("此时的根节点的右子树高度为: " + avlTree.rightHeight()); //2
//然后我们可以再做一个测试:
AVLTree avlTree2 = new AVLTree();
//根据序列{1, 2, 3, 4, 5, 6}建树
Node node11 = new Node(1);
Node node12 = new Node(1);
Node node13 = new Node(1);
Node node14 = new Node(1);
Node node15 = new Node(1);
Node node16 = new Node(1);
avlTree2.add(node11);
avlTree2.add(node12);
avlTree2.add(node13);
avlTree2.add(node14);
avlTree2.add(node15);
avlTree2.add(node16);
System.out.println("此时的根节点的左子树高度为: " + avlTree2.leftHeight()); //2
System.out.println("此时的根节点的右子树高度为: " + avlTree2.rightHeight()); //2
}