算法7:平衡二叉树(AVLTree)
二叉排序树(BST, Binary Sort Tree)也称二叉查找树(Binary Search Tree), 或二叉搜索树。
定义:一颗二叉树,满足以下属性:
- 左子树的所有的值小于根节点的值
- 右子树的所有值大于根节点的值
- 左、右子树满足以上两点
那么我们如何去构建一个自己的二叉排序树呢?算法实现思路如下:
二叉排序树只是对值进行了排序,但是如果我们对二叉排序树进行树的平衡操作,即对树进行旋转,那么就引出了本文的重要知识点,平衡二叉树(AVL Tree)。《大话数据结构》是这样解释的:
下面通过几张图片来对AVL树有一个基本的认识:
下面通过过右旋,左右旋来做一个简单的介绍。(左旋和右旋相反,左右旋和右左旋相反)
右旋:即对树的根节点重新判定,即左子树最下层的左节点新增节点值,那么可以通过将新生成的根节点往右拉,即可实现平衡。即将新的根节点40拉到旧的根节点50的位置。
左右旋:如下图中所展示,先对左子树进行左旋,将原来左子树根节点20变为30,即将30往左拉; 然后再进行右旋,即将整颗树的新的根节点30往右拉
下面直接接上代码,实现AVL Tree的构造
package code.code_04; public class AVLTree { //节点 public class Node { int data; //数据 Node left; //左子节点 Node right;//右子节点 int height; // 记录该节点所在的高度 public Node(int data) { this.data = data; this.height = 1; //只要右节点,那么当前节点的高度就为1. } } public static void printTree(Node root) { System.out.println(root.data); if(root.left !=null){ System.out.print("left:"); printTree(root.left); } if(root.right !=null){ System.out.print("right:"); printTree(root.right); } } //获取节点的高度 public static int getHeight(Node p){ return p == null ? 0 : p.height; // 空树的高度为0 } /* * 右旋转 * 右旋示意图(先把30的左节点指向25,然后再将节点20的右节点指向30,即对结点20进行右旋) * 30 20 * / \ / \ * 20 40 10 30 * / \ --RR旋转- / / \ * 10 25 5 25 40 * / * 5 * */ public Node RRRotate(Node p) { //记录下节点p的左子树,防止树断开 Node node = p.left; //失衡点的左子树的根结点20作为新的结点 p.left = node.right; //将新节点的右子树25成为失衡点30的左子树,等同于p.left = p.left.right node.right = p; //将失衡点30作为新结点的右子树 //重新设置失衡点30和新节点20的高度 p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1; node.height = Math.max(getHeight(node.left), p.height) + 1; return node; } /* * LL旋转 * 左旋示意图(先把20的右节点指向25,然后再将节点30的左节点指向20,即对结点30进行左旋)) * 20 30 * / \ / \ * 10 30 20 40 * / \ --LL旋转- / \ \ * 25 40 10 25 50 * \ * 50 * */ public Node LLRotate(Node p){ //记录下节点p的左子树,防止树断开 Node node = p.right; //失衡点的左子树的根结点20作为新的结点 p.right = node.left; //将新节点的右子树25成为失衡点30的左子树 node.left = p; //将失衡点30作为新结点的右子树 //重新设置失衡点30和新节点20的高度 p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1; node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; return node; } // LR旋转 public Node LRRotate(Node p){ p.left = LLRotate(p.left); // 先将失衡点p的左子树进行RR旋转 return RRRotate(p); // 再将失衡点p进行LL平衡旋转并返回新节点代替原失衡点p } // RL平衡旋转 public Node RLRotate(Node p){ p.right = RRRotate(p.right); // 先将失衡点p的右子树进行LL平衡旋转 return LLRotate(p); // 再将失衡点p进行RR平衡旋转并返回新节点代替原失衡点p } public Node insert(Node root, int data) { //如果节点不存在,则直接创建新节点返回,新节点的高度默认为1 if (root == null) { root = new Node(data); return root; } if (data < root.data) { //递归一直往下找,直到找到合适的位置为止 root.left = insert(root.left, data); //如果树失衡,则进行平行操作 if (getHeight(root.left) - getHeight(root.right) > 1) { /** * 如果data等于root.left.data,那说明root之前没有左节点。 * * 如果data小于root.left.data,那说明data是插入在root.left的子节点中,此时 * 我们还需要判断data具体是插入在root.left的左子节点还是右子节点。 * * 1.如果是插入root.left的左子节,那么需要进行右旋转 * 2.如果是插入root.left的右子节点,那么需要先左旋转,然后再右右(即左右旋转) */ if (data < root.left.data) { //插入节点在失衡结点的左子树的左边 System.out.println("RR旋转"); root = RRRotate(root); } else { //插入节点在失衡结点的左子树的右边 System.out.println("左右旋转"); root = LRRotate(root); } } } else { root.right = insert(root.right, data); if (getHeight(root.right) - getHeight(root.left) > 1) { if(data <= root.right.data){//插入节点在失衡结点的右子树的左边 System.out.println("RL旋转"); root = RLRotate(root); }else{ System.out.println("LL旋转");//插入节点在失衡结点的右子树的右边 root = LLRotate(root); } } } //重新调整root节点的高度值 root.height = Math.max(getHeight(root.left), getHeight(root.right)) + 1; return root; } public static void main(String[] args) { AVLTree tree = new AVLTree(); Node root = null; /* root = tree.insert(root,30); root = tree.insert(root,20); root = tree.insert(root,40); root = tree.insert(root,10); root = tree.insert(root,25);*/ //插入节点在失衡结点的左子树的左边, demo逐个放开验证 //root = tree.insert(root,5); //右旋 demo1 //root = tree.insert(root,15); //右旋 demo2 //root = tree.insert(root,24); //左右旋 demo3 //root = tree.insert(root,26); //左右旋 demo4 //测试左旋,右左旋 root = tree.insert(root,20); root = tree.insert(root,10); root = tree.insert(root,30); root = tree.insert(root,25); root = tree.insert(root,40); //root = tree.insert(root,39); //左旋 demo5 //root = tree.insert(root,41); //左旋 demo6 //root = tree.insert(root,24); //右左旋 demo7 root = tree.insert(root,26); //右左旋 demo8 //打印树,按照先打印左子树,再打印右子树的方式 tree.printTree(root); } }
下面通过一张图片,总结一下分别在什么情况下需要左旋,左右旋,右旋,右左旋。
左旋,右旋只需要一次旋转即可完成树的平衡
左右旋,右左旋需要2次旋转才能完成树的平衡
参考资料:
《大话数据结构》
https://blog.csdn.net/xiaojin21cen/article/details/97602146