简介:
AVL树本质上是一颗二叉查找树,但是它又具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为平衡二叉树。下面是平衡二叉树和非平衡二叉树对比的例图:
平衡因子(bf):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1;
AVL树的作用:
我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。 例如:我们按顺序将一组数据1,2,3,4,5,6分别插入到一颗空二叉查找树和AVL树中,插入的结果如下图:
由上图可知,同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉树的高度是O(N),而AVL树就不会出现这种情况,树的高度始终是O(lgN).高度越小,对树的一些基本操作的时间复杂度就会越小。这也就是我们引入AVL树的原因
AVL树的基本操作:
AVL树的操作基本和二叉查找树一样,这里我们关注的是两个变化很大的操作:插入和删除!
我们知道,AVL树不仅是一颗二叉查找树,它还有其他的性质。如果我们按照一般的二叉查找树的插入方式可能会破坏AVL树的平衡性。同理,在删除的时候也有可能会破坏树的平衡性,所以我们要做一些特殊的处理,包括:单旋转和双旋转!
AVL树的插入,单旋转的第一种情况---右旋:
由上图可知:在插入之前树是一颗AVL树,而插入之后结点T的左右子树高度差的绝对值不再 < 1,此时AVL树的平衡性被破坏,我们要对其进行旋转。由上图可知我们是在结点T的左结点的左子树上做了插入元素的操作,我们称这种情况为左左情况,我们应该进行右旋转(只需旋转一次,故是单旋转)。具体旋转步骤是:
T向右旋转成为L的右结点,同时,Y放到T的左孩子上。这样即可得到一颗新的AVL树,旋转过程图如下:
左左情况的右旋举例:
AVL树的插入,单旋转的第一种情况---左旋:
由上图可知:在插入之前树是一颗AVL树,而插入之后结点T的左右子树高度差的绝对值不再 < 1,此时AVL树的平衡性被破坏,我们要对其进行旋转。由上图可知我们是在结点T的右结点的右子树上做了插入元素的操作,我们称这种情况为右右情况,我们应该进行左旋转(只需旋转一次,故事单旋转)。具体旋转步骤是:
T向右旋转成为R的左结点,同时,Y放到T的左孩子上。这样即可得到一颗新的AVL树,旋转过程图如下:
右右情况的左旋举例:
以上就是插入操作时的单旋转情况!我们要注意的是:谁是T谁是L,谁是R还有谁是X,Y,Z!T始终是开始不平衡的左右子树的根节点。显然L是T的左结点,R是T的右节点。X、Y、Y是子树当然也可以为NULL.NULL归NULL,但不能破坏插入时我上面所说的左左情况或者右右情况。
AVL树的插入,双旋转的第一种情况---左右(先左后右)旋:
由 上图可知,我们在T结点的左结点的右子树上插入一个元素时,会使得根为T的树的左右子树高度差的绝对值不再 < 1,如果只是进行简单的右旋,得到的树仍然是不平衡的。我们应该按照如下图所示进行二次旋转:
左右情况的左右旋转实例:
AVL树的插入,双旋转的第二种情况---右左(先右后左)旋:
由上图可知,我们在T结点的右结点的左子树上插入一个元素时,会使得根为T的树的左右子树高度差的绝对值不再 < 1,如果只是进行简单的左旋,得到的树仍然是不平衡的。我们应该按照如下图所示进行二次旋转:
右左情况的右左旋转实例:
AVL树的插入代码实现:
1.创建结点
public class Node { int value; Node left;//左节点 Node right;//右节点 public Node(int value) { this.value = value; } /*添加节点的方法满足二叉排序树*/ public void add(Node node){ if (node==null){ return; } //判断传入节点和当前子树节点的值 if (node.value<this.value){ if (this.left ==null){//左子节点为空直接添加 this.left=node; }else { this.left.add(node);//递归左子树添加 } }else {//添加的节点大于当前节点的值 if (this.right==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; } /*当添加一个节点后,满足平衡二叉树的条件左子树>右子树+1*/ if (leftHeight()-rightHeight()>1){ if (left!=null && left.rightHeight() > left.leftHeight()){ left.leftRotate();//对当前接的的左子树进行左旋转... /*再对当前节点进行右旋*/ rightRotate(); }else {//直接进行右旋 rightRotate(); } } }
2.创建屁AVL树
public class AVLTree { private Node root; public Node getRoot() { return root; } public void setRoot(Node root) { this.root = root; } /*查找要删除的节点*/ public Node search(int va1ue){ if (root==null){ return null; }else { return root.search(va1ue); } } /*查找删除节点的父节点*/ public Node searchParent(int value){ if (root==null){ return null; }else { return root.searchParent(value); } } /** * * @description:TODO * @params:1.节点(当做二叉的根节点) * 1.删除最小节点 * @return: 当前节点作为二叉数根节点的最小节点值 * @author: 苏兴旺 * @time: 2020/3/14 12:38 */ public int delRightTree(Node node){ Node targe = node; //一直往左边找就是最小值 while (targe.left!=null){ targe = targe.left; } delNode(targe.value); //指向最小节点 return targe.value; } /*删除节点 * 1.找待删除的节点taget * 2.找到待删除节点的父节点parent * 3.确定 taget 是父节点 左节点 右节点 * 4.根据实际情况进行删除 * 左节点==null * 右节点==null * * 第二种:删除的节点只有一个子树 * 1.找待删除的节点taget * 2.找到待删除节点的父节点parent * 3.确定taget的子节点是左节点还是右子节点 * 4.taget是parent左子节点还是右子节点 * 5.如果taget有左子节点 * 5.1 如果taget是parent左节点 parent.左 = taget.左 * 5.2如果taget是parent右节点parent.右 = taget.左 * 6.如果taget有右子节点 * 6.1 如果taget是parent左节点 parent.左 = taget.右 * 6.2如果taget是parent右节点parent.右 = taget.右 * * 情况三删除有二个数的节点: *.1找待删除的节点taget * 2.找到待删除节点的父节点parent * 3.从taget 的右节点找到最小的节点 * 4.用临时变量保存保存最小节点temp * 5.删除该节点 * taget.value=temp*/ public void delNode(int value){ if (root == null){ return; }else { Node target = search(value); if (target == null){//找到删除的节点taget return; } //target 树只有一个root接电脑 if (root.left==null && root.right==null){ root=null; return; } //找到删除节点的父节点 Node parent = searchParent(value); if (target.left==null&&target.right==null){//为叶子节点 if (parent.left!=null &&parent.left.value==value){//判断target节点是父节点的左节点还是右节点 parent.left=null; }else if (parent.right!=null&& parent.right.value==value){ parent.right=null; } }else if (target.left!=null && target.right!=null){//情况三删除有二个数的节点: // 情况三删除有二个数的节点: //1.找待删除的节点taget //2.找到待删除节点的父节点parent // 3.从taget 的右节点找到最小的节点 // 4.用临时变量保存保存最小节点temp // 5.删除该节点 int minVal = delRightTree(target.right); target.value=minVal; }else {//删除的节点只有一个子树 if (target.left!=null){//如果要删除的有左子节点 if (parent.left.value==value){//target是父节点的左节点 parent.left=target.left; }else {//target是父节点的右节点 parent.right = target.left; } }else {//如果要删除的有右子节点 if (parent.left.value==value){//target是父节点的左节点 parent.left=target.right; }else {//target是父节点的右节点 parent.right=target.right; } } } } } /*添加方法*/ public void add(Node node){ if (root==null){ root = node; }else { root.add(node); } } /*中序遍历*/ public void infixOrder(){ if (root==null){ System.out.println("二叉排序树为空!!!"); }else { root.infixOrder(); } } }
3.进行测试
public class ALMTreeDemo { 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}; 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()); System.out.println("当前根节点:" +avlTree.getRoot()); System.out.println("当前根节点的左节点:" +avlTree.getRoot().left); System.out.println("当前根节点的右节点:" +avlTree.getRoot().right.left); } }