【leetcode】(十五)有序表-AVL树-SB树-红黑树-跳表

本文系统讲解四种有序表实现结构:AVL树(基于高度平衡)、SB树(基于子树节点数平衡)、红黑树(基于颜色约束与旋转)和跳表(基于多层随机索引)。重点分析各自的平衡机制、LL/RR/LR/RL调整策略、插入删除修复逻辑,以及时间复杂度保障原理。强调四者同属O(logN)级有序映射结构,但在实现复杂度、最坏性能保证、并发友好性等方面存在本质差异。

(一)有序表

有序表的操作水平O(logN)级别。

能够实现有序表的结构主要有:

  • 红黑树
  • AVL树
  • SB树(主要用于比赛,方便修改)
  • 跳表(skiplist)

前三种属于平衡搜索二叉树(BST)系列。

1.搜索二叉树

举例:

对于5,3,6,4,5的输入,我们可以得到一个平衡二叉树(初始以5为节点):

           5(2)
         /   \
        /     \
        3(1)   6(1)
         \      
          \      
           4(1)     

其中,括号代表的是统计的次数的记录。

对于其他的数据项,比如“5,A”,我们可以建立<5,A>这个数据项;对于“5,A”和“5,B”,我们可以建立<5,{A,B}>这个数据项,也就是压缩。如果比头节点小,则转移到左边;如果比头节点大,转移到右边;如果有重复值,则压缩数据项存储。这样搜索二叉树就没有重复值。

(1)搜索二叉树的查找

            4
         /    \
        /      \
       2        8
        \      /  \
         \    /    \
          3  7     9
             /
            /
            5

要查找<=6的节点,首先设定一个ans变量
从头节点4开始遍历,ans=4,由于4<=6,于是左树全部满足;
在右树中,遍历8,7,最后发现5也满足,于是更新ans=5

(2)搜索二叉树的删除

            3
         /    \
        /      \
       1        4
        \      
         \    
          2 
             

1.最简单的删除为删除叶子节点。
比如要删除2这个节点,则设置一个past标记和cur标记,past和cur标记

            3
         /    \
        /      \
       1(p)     4
        \      
         \    
          2(cur) 
删除后,变成:

            3
         /    \
        /      \
       1(p)     4
       

2.如果要删除的不是叶子节点,并且左右两个孩子不双全,可以直接删除,让唯一的孩子替代就可以了。
            3
         /    \
        /      \
       2        4
      /          \      
     /            \    
    1              5 

比如要删除4这个节点,则p=3.cur=4
          3(p)
         /    \
        /      \
       2        4(cur)
      /          \      
     /            \    
    1              5 
由于4只有一个右孩子,则删除4后直接让5替代它。
          3(p)
         /    \
        /      \
       2        5
      /               
     /             
    1              
同理要删除2这个节点,则p=3.cur=2
          3(p)
         /    \
        /      \
       2(cur)   5
      /              
     /             
    1              
由于2只有一个右孩子,则删除2后直接让1替代它。
          3(p)
         /    \
        /      \
       1        5 


3.如果需要删除的节点有左右两个孩子。
             3
         /       \
        /         \
       2           7
     /  \         /  \
    /    \       /    \
   1    2.5     6      ...
               / \
              /   \
             4     ...
            / \
           /   \
         null   A
             
如果需要删除头节点3那么怎么办?如果不考虑平衡性:
可以拿左树上最右的节点来替换,也可以拿右树最左的节点来替换。

以这个节点中的4为例:
4被替换为3,然后它的右孩子A代替它。
             4
         /       \
        /         \
       2           7
     /  \         /  \
    /    \       /    \
   1    2.5     6      ...
               / \
              /   \
             A     ...
           
这样整棵树还是搜索二叉树

何为具有平衡性的树?

为了让搜索代价保持在O(logN)水平,那么,必须让树变成平衡二叉树。

比如说:用户的输入为“1,2,3,4”,不调整的情况下,那么整棵树会变成:

1
 \
  2
   \
    3
     \
      4

搜索代价是O(N),如果变成平衡二叉树,那么搜索的体量(是狭义的平衡性,高度差不多,或者左右两树节点个数差不多,而不是严格的两棵树的高度不能相差1)会维持在O(logN)的水平

2.树的左旋和右旋

搜索二叉树和AVL树的区别

搜索二叉树和红黑树的区别

搜索二叉树和SB树的区别

也就是说,搜索二叉树和AVL树,红黑树,SB树的区别就是,AVL树,红黑树,SB树在拥有左旋右旋的基础上,加上了他们自己的特性。

(1)树的左旋

头节点倒向左边就是左旋

                  A
               /     \
              /       \
             B         C
            / \       / \
           /   \     /   \
          E     D   F     G
             

将C的左孩子给A,左旋之后得到:
                    C
                 /     \
                /       \
               A         G
             /   \
            /     \
           B       F
          / \
         /   \
        E     D
                

(2)树的右旋

头节点倒向左边就是左旋

                  A
               /     \
              /       \
             B         C
            / \       / \
           /   \     /   \
          E     D   F     G
             

将B的右孩子给A,右旋之后得到:
                    B
                 /     \
                /       \
               E         A
                        /  \
                       /    \
                      D      C
                            / \
                           /   \
                          F     G
             
                

(3)左旋和右旋的作用

左旋和右旋的作用在于让树变得平衡

举例:

                    A
                 /     \
                /       \
               B         C
                           \
                            \
                             D
                              \
                               \
                                E
             
                

对于AVL树来说,这棵树左孩子高度为1,右孩子高度为3,已经不平衡了。如果用左旋,那么这棵树就会变得平衡,如图所示:

           
                        C
                      /    \
                     /      \
                    A        D
                   /          \
                  /            \
                 B              E
             
                

那么AVL树如何检查到自己是不平衡的呢?

AVL树的增删改查和经典的搜索二叉树没有任何区别,但它的特点是,加完一个节点后往上检查每一个节点是否平衡。

增加节点的情况,举例:

           
                        A
                      /    \
                     /      \
                    B        C
                   /       /  \
                  /       /    \
                 F       D      E
             
                
此时AVL树平衡,那么节点F加一个右孩子后,变成:
                        A
                      /    \
                     /      \
                    B        C
                   /       /  \
                  /       /    \
                 F       D      E
                /
               /
              X

此时,AVL树会从节点X开始查,X是否平衡,F是否平衡,B是否平衡,以此类推。

删除节点的情况,举例:

                  5
               /    \
              /      \  
             3        8
            / \      / \
           /   \    /   \
           1   4   7     C...
                  / \
                 /   \
                6    B...
               /  \  
              /    \
             null   A...

其中,A...、B...、C...代表子树。如果去掉节点5,我们要把节点6设置为根节点,然后把子树A挂在节点7的下面,给那么这棵树会变成:

                  6
               /    \
              /      \  
             3        8
            / \      / \
           /   \    /   \
           1   4   7     C...
                  / \
                 /   \
                A...  B...
               

接下来检查平衡性:

由于6节点现在不在原来的位置,我们从节点7开始往上查7,8,6等节点,看看有没有平衡性。

(二)AVL树及其实现

1、当插入或者删除一个节点时,可能会让整棵AVL不平衡。此时,只需要把最小不平衡子树调整即可恢复整体的平衡性。
2、介绍树的LL、RR、LR、RL调整
3、介绍插入节点时的调整细节
4、介绍删除节点时的调整细节

1.LL型

LL型树的含义是左树的左孩子导致的平衡性失效的树,解决方法是做一次右旋。

举例:

                Z
               /  \
              /    \
             Y     T4
            /  \
           /    \
          X     T3
         / \
        /   \
       T1   T2 

2.RR型

RR型树的含义是右树的右孩子导致的平衡性失效的树,解决方法是做一次左旋。

举例:

                Z
               /  \
              /    \
             T4     Y
                   / \
                  /   \
                 T3    X
                      / \
                     /   \
                    T2   T1

3.LR型

LR型树的含义是左树的右孩子导致的平衡性失效的树,解决方法是左树先做一次左旋,接着整棵树做一次右旋。

举例:

                Z
               /  \
              /    \
             Y     T4
            / \
           /   \
          T1    X
               / \
              /   \
             T2   T3  


第一步先调整左树,左树先左旋,X变成了左树的头节点,于是整棵树变成:

                Z
               /  \
              /    \
             X     T4
            / \
           /   \
          Y    T3
         / \
        /   \
       T1   T2
               
我们发现这棵树变成了LL型,于是:
第二步,整棵树做一次右旋,让X变成整棵树的头节点,于是整棵树变成:

              X     
            /   \
           /     \
          Y       Z
         / \     / \
        /   \   /   \
       T1   T2 T3   T4

4.RL型

RL型树的含义是右树的左孩子导致的平衡性失效的树,解决方法是右树先做一次右旋,接着整棵树做一次左旋。

举例:

                 Z
               /   \
              /     \
             A       B
                    / \
                   /   \
                  C    T3
                 / \
                /   \
               T1   T2

处理方法类似LR树。

5.AVL树实现代码

代码如下

package Class015;

public class AVLTree {
    //普通整数节点:相同值记录出现次数,不另建重复节点
    public static class Node{
        int value;
        int count=1;
        int height=1;
        Node left,right;
        public Node(int value){
            this.value=value;
        }
    }
    private Node root;
    private int height(Node node){
        return node==null?0:node.height;
    }
    private void update(Node node){
        node.height=Math.max(height(node.left),height(node.right))+1;
    }
    private int bf(Node node){
        return height(node.left)-height(node.right);
    }

    //左旋:右孩子上升,右孩子原来的左子树整体接到当前节点右侧
    private Node leftRotate(Node a){
        Node b=a.right;
        a.right=b.left;
        b.left=a;
        update(a);
        update(b);
        return b;
    }

    //右旋:左孩子上升,左孩子原来的右子树整体接到当前节点左侧
    private Node rightRotate(Node a){
        Node b=a.left;
        a.left=b.right;
        b.right=a;
        update(a);
        update(b);
        return b;
    }

    //LL,RR,LR,RL统一维护
    private Node maintain(Node node){
        if (node==null)return null;
        update(node);
        if (bf(node)>1){//左子树高
            if (bf(node.left)<0){//LR:左孩子先左旋
                node.left=leftRotate(node.left);
            }
            return rightRotate(node);//LL或LR第二次旋转
        }
        if (bf(node)<-1){//右子树高
            if (bf(node.right)>0){//RL:右孩子先右旋
                node.right=rightRotate(node.right);
            }
            return leftRotate(node);
        }
        return node;
    }

    public void insert(int value){
        root=insert(root,value);
    }
    private Node insert(Node node,int value){
        if (node==null)return new Node(value);
        if (value<node.value)node.left=insert(node.left,value);
        else if (value>node.value)node.right=insert(node.right,value);
        else {node.count++;return node;}
        return maintain(node);
    }
    public boolean contains(int value){
        Node cur=root;
        while (cur!=null){
            if (value==cur.value)return true;
            cur=value<cur.value?cur.left:cur.right;
        }
        return false;
    }

    public void delete(int value){
        root=delete(root,value);
    }
    private Node delete(Node node,int value){
        if (node==null)return null;
        if (value<node.value){
            node.left=delete(node.left,value);
        }else if (value>node.value){
            node.right=delete(node.right,value);
        }else {
            //删除一个重复值,不改变树形
            if (node.count>1){node.count--;return node;}
            //零个或一个孩子;由孩子替代当前节点
            if (node.left==null)return node.right;
            if (node.right==null)return node.left;
            //两个孩子:用右子树最小节点替换,原后继节点整体摘除
            Node next=node.right;
            while (next.left!=null)next=next.left;
            node.value=next.value;
            node.count=next.count;
            node.right=removeMin(node.right);
        }
        return maintain(node);//沿着删除路径向上恢复平衡
    }
    private Node removeMin(Node node){
        if (node.left==null)return node.right;
        node.left=removeMin(node.left);
        return maintain(node);
    }

    //用普通二叉树上的全部数值重新建立一棵AVL树,对非BST也适用
    //注意:这里是重建,不是对原有非BST结构直接做旋转
    public void buildFromTree(Node anyRoot){
        root=null;
        addAll(anyRoot);
    }
    private void addAll(Node node){
        if (node==null)return;
        for (int i=0;i<node.count;i++)insert(node.value);
        addAll(node.left);
        addAll(node.right);
    }

    //只有在main手工连接节点时才需要补算height;正常insert/delete会自动更新
    private int refreshHeights(Node node){
        if (node==null)return 0;
        int l=refreshHeights(node.left);
        int r=refreshHeights(node.right);
        node.height=Math.max(l,r)+1;
        return node.height;
    }
    public void preOrder(){preOrder(root);System.out.println();}
    public void preOrder(Node node){
        if (node==null)return;
        System.out.print(node.value+" ");
        preOrder(node.left);
        preOrder(node.right);
    }

    public void inOrder(){inOrder(root);System.out.println();}
    private void inOrder(Node node){
        if (node==null)return;
        inOrder(node.left);
        System.out.print(node.value+(node.count>1?"("+node.count+")":"")+" ");
        inOrder(node.right);
    }

    public void postOrder(){postOrder(root);System.out.println();}
    private void postOrder(Node node){
        if (node==null)return;
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.value+" ");
    }

    //同时检验搜索二叉树顺序,AVL平衡,高度记录
    public boolean isAVL(){
        return check(root,Long.MIN_VALUE,Long.MAX_VALUE)>=0;
    }

    private int check(Node node,long low,long high){
        if (node==null)return 0;
        if (node.value<=low||node.value>=high||node.count<=0)return -1;
        int l=check(node.left,low,node.value);
        int r=check(node.right,node.value,high);
        if (l<0||r<0||Math.abs(l-r)>1||node.height!=Math.max(l,r)+1)return -1;
        return node.height;
    }

    //横向打印,右子树在上
    public void printTree(){printTree(root,0);}
    private void printTree(Node node,int depth){
        if (node==null)return;
        printTree(node.right,depth+1);
        System.out.println("  ".repeat(depth)+node.value);
        printTree(node.left,depth+1);
    }

    public static void main(String [] args){
        // =============== 1. LL型 ===============
        //                 80(Z)
        //                /    \
        //              50(Y)  100(T4)
        //             /   \    /   \
        //           30(X) 60(T3) 90 110
        //          /   \   / \
        //        15(T1)40(T2)55 65
        //        / \   / \
        //       10 20 35 45
        AVLTree ll=new AVLTree();

        ll.root = new Node(80);
        ll.root.left = new Node(50);
        ll.root.right = new Node(100);
        ll.root.left.left = new Node(30);
        ll.root.left.right = new Node(60);
        ll.root.left.left.left = new Node(15);
        ll.root.left.left.right = new Node(40);
        ll.root.left.left.left.left = new Node(10);
        ll.root.left.left.left.right = new Node(20);
        ll.root.left.left.right.left = new Node(35);
        ll.root.left.left.right.right = new Node(45);
        ll.root.left.right.left = new Node(55);
        ll.root.left.right.right = new Node(65);
        ll.root.right.left = new Node(90);
        ll.root.right.right = new Node(110);

        ll.refreshHeights(ll.root);
        System.out.println("\n====LL型:右旋=====");
        System.out.println("旋转前:根="+ll.root.value+", BF="+ll.bf(ll.root));

        ll.root=ll.maintain(ll.root);
        System.out.println("旋转后:根="+ll.root.value+",AVL="+ll.isAVL());

        System.out.println("查找45:"+ll.contains(45));
        System.out.print("前序:");ll.preOrder();
        System.out.print("中序:");ll.inOrder();
        System.out.print("后序:");ll.postOrder();

        ll.insert(42);
        System.out.println("插入42后:"+ll.isAVL());

        ll.delete(40);//删除双孩子节点
        System.out.println("删除40后:" + ll.isAVL());
        ll.delete(10);//删除单叶子节点
        System.out.println("删除10后:" + ll.isAVL());
        ll.delete(15);//删除单孩子节点
        System.out.println("删除15后:" + ll.isAVL());
        ll.delete(ll.root.value);//删除当前根节点

        System.out.println("删除根节点后:" + ll.isAVL());
        System.out.print("最终中序:");ll.inOrder();

        // =============== 2. RR型 ===============
        //              30(Z)
        //             /     \
        //          15(T1)  60(Y)
        //          / \    /   \
        //         10 20 40(T2)80(X)
        //               / \   /  \
        //              35 45 70(T3)100(T4)
        //                    / \   / \
        //                   65 75 90 110

        AVLTree rr = new AVLTree();

        rr.root = new Node(30);
        rr.root.left = new Node(15);
        rr.root.right = new Node(60);
        rr.root.left.left = new Node(10);
        rr.root.left.right = new Node(20);
        rr.root.right.left = new Node(40);
        rr.root.right.right = new Node(80);
        rr.root.right.left.left = new Node(35);
        rr.root.right.left.right = new Node(45);
        rr.root.right.right.left = new Node(70);
        rr.root.right.right.right = new Node(100);
        rr.root.right.right.left.left = new Node(65);
        rr.root.right.right.left.right = new Node(75);
        rr.root.right.right.right.left = new Node(90);
        rr.root.right.right.right.right = new Node(110);

        rr.refreshHeights(rr.root);
        System.out.println("\n===== RR型:左旋 =====");
        System.out.println("旋转前:根=" + rr.root.value
                + ",BF=" + rr.bf(rr.root));

        rr.root = rr.maintain(rr.root);
        System.out.println("旋转后:根=" + rr.root.value
                + ",AVL=" + rr.isAVL());

        System.out.println("查找45:" + rr.contains(45));
        System.out.print("前序:"); rr.preOrder();
        System.out.print("中序:"); rr.inOrder();
        System.out.print("后序:"); rr.postOrder();

        rr.insert(42);
        System.out.println("插入42后:" + rr.isAVL());

        rr.delete(40); // 删除双孩子节点
        System.out.println("删除40后:" + ll.isAVL());
        rr.delete(10); // 删除叶子节点
        System.out.println("删除10后:" + ll.isAVL());
        rr.delete(15); // 删除单孩子节点
        System.out.println("删除15后:" + ll.isAVL());
        rr.delete(rr.root.value); // 删除当前根节点

        System.out.println("删除根节点后:" + ll.isAVL());
        System.out.print("最终中序:"); rr.inOrder();


        // =============== 3. LR型 ===============
        //               80(Z)
        //              /    \
        //            30(Y) 100(T4)
        //           /   \   / \
        //        15(T1) 50(X)90 110
        //        / \   /   \
        //       10 20 40(T2)60(T3)
        //             / \   / \
        //            35 45 55 65

        AVLTree lr = new AVLTree();

        lr.root = new Node(80);
        lr.root.left = new Node(30);
        lr.root.right = new Node(100);
        lr.root.left.left = new Node(15);
        lr.root.left.right = new Node(50);
        lr.root.left.left.left = new Node(10);
        lr.root.left.left.right = new Node(20);
        lr.root.left.right.left = new Node(40);
        lr.root.left.right.right = new Node(60);
        lr.root.left.right.left.left = new Node(35);
        lr.root.left.right.left.right = new Node(45);
        lr.root.left.right.right.left = new Node(55);
        lr.root.left.right.right.right = new Node(65);
        lr.root.right.left = new Node(90);
        lr.root.right.right = new Node(110);

        lr.refreshHeights(lr.root);
        System.out.println("\n===== LR型:先左旋再右旋 =====");
        System.out.println("旋转前:根=" + lr.root.value
                + ",BF=" + lr.bf(lr.root));

        lr.root = lr.maintain(lr.root);
        System.out.println("旋转后:根=" + lr.root.value
                + ",AVL=" + lr.isAVL());

        System.out.println("查找45:" + lr.contains(45));
        System.out.print("前序:"); lr.preOrder();
        System.out.print("中序:"); lr.inOrder();
        System.out.print("后序:"); lr.postOrder();

        lr.insert(42);
        System.out.println("插入42后:" + lr.isAVL());

        lr.delete(40); // 删除T2的双孩子节点
        System.out.println("删除40后:" + ll.isAVL());
        lr.delete(10); // 删除T1的叶子节点
        System.out.println("删除10后:" + ll.isAVL());
        lr.delete(15); // 删除T1的单孩子节点
        System.out.println("删除15后:" + ll.isAVL());
        lr.delete(lr.root.value); // 删除当前根节点

        System.out.println("删除根节点后:" + ll.isAVL());
        System.out.print("最终中序:"); lr.inOrder();


        // =============== 4. RL型 ===============
        //               30(Z)
        //              /     \
        //           15(T1)   80(Y)
        //           / \     /  \
        //          10 20  60(X) 100(T4)
        //                 /  \   / \
        //              50(T2)70(T3)90 110
        //              / \   / \
        //             45 55 65 75

        AVLTree rl = new AVLTree();

        rl.root = new Node(30);
        rl.root.left = new Node(15);
        rl.root.right = new Node(80);
        rl.root.left.left = new Node(10);
        rl.root.left.right = new Node(20);
        rl.root.right.left = new Node(60);
        rl.root.right.right = new Node(100);
        rl.root.right.left.left = new Node(50);
        rl.root.right.left.right = new Node(70);
        rl.root.right.left.left.left = new Node(45);
        rl.root.right.left.left.right = new Node(55);
        rl.root.right.left.right.left = new Node(65);
        rl.root.right.left.right.right = new Node(75);
        rl.root.right.right.left = new Node(90);
        rl.root.right.right.right = new Node(110);

        rl.refreshHeights(rl.root);
        System.out.println("\n===== RL型:先右旋再左旋 =====");
        System.out.println("旋转前:根=" + rl.root.value
                + ",BF=" + rl.bf(rl.root));

        rl.root = rl.maintain(rl.root);
        System.out.println("旋转后:根=" + rl.root.value
                + ",AVL=" + rl.isAVL());

        System.out.println("查找45:" + rl.contains(45));
        System.out.print("前序:"); rl.preOrder();
        System.out.print("中序:"); rl.inOrder();
        System.out.print("后序:"); rl.postOrder();

        rl.insert(42);
        System.out.println("插入42后:" + rl.isAVL());

        rl.delete(50); // 删除T2的双孩子节点
        System.out.println("删除50后:" + ll.isAVL());
        rl.delete(10); // 删除T1的叶子节点
        System.out.println("删除10后:" + ll.isAVL());
        rl.delete(15); // 删除T1的单孩子节点
        System.out.println("删除15后:" + ll.isAVL());
        rl.delete(rl.root.value); // 删除当前根节点

        System.out.println("删除根节点后:" + ll.isAVL());
        System.out.print("最终中序:"); rl.inOrder();
    }
}

运行结果:

====LL型:右旋=====
旋转前:根=80, BF=2
旋转后:根=50,AVL=true
查找45:true
前序:50 30 15 10 20 40 35 45 80 60 55 65 100 90 110 
中序:10 15 20 30 35 40 45 50 55 60 65 80 90 100 110 
后序:10 20 15 35 45 40 30 55 65 60 90 110 100 80 50 
插入42后:true
删除40后:true
删除10后:true
删除15后:true
删除根节点后:true
最终中序:20 30 35 42 45 55 60 65 80 90 100 110 

===== RR型:左旋 =====
旋转前:根=30,BF=-2
旋转后:根=60,AVL=true
查找45:true
前序:60 30 15 10 20 40 35 45 80 70 65 75 100 90 110 
中序:10 15 20 30 35 40 45 60 65 70 75 80 90 100 110 
后序:10 20 15 35 45 40 30 65 75 70 90 110 100 80 60 
插入42后:true
删除40后:true
删除10后:true
删除15后:true
删除根节点后:true
最终中序:20 30 35 42 45 65 70 75 80 90 100 110 

===== LR型:先左旋再右旋 =====
旋转前:根=80,BF=2
旋转后:根=50,AVL=true
查找45:true
前序:50 30 15 10 20 40 35 45 80 60 55 65 100 90 110 
中序:10 15 20 30 35 40 45 50 55 60 65 80 90 100 110 
后序:10 20 15 35 45 40 30 55 65 60 90 110 100 80 50 
插入42后:true
删除40后:true
删除10后:true
删除15后:true
删除根节点后:true
最终中序:20 30 35 42 45 55 60 65 80 90 100 110 

===== RL型:先右旋再左旋 =====
旋转前:根=30,BF=-2
旋转后:根=60,AVL=true
查找45:true
前序:60 30 15 10 20 50 45 55 80 70 65 75 100 90 110 
中序:10 15 20 30 45 50 55 60 65 70 75 80 90 100 110 
后序:10 20 15 45 55 50 30 65 75 70 90 110 100 80 60 
插入42后:true
删除50后:true
删除10后:true
删除15后:true
删除根节点后:true
最终中序:20 30 42 45 55 65 70 75 80 90 100 110 

(三)SB树及其实现

SB树(Size Balanced Tree,大小平衡树)通过**子树的节点数量(size)**判断是否平衡,而不是像AVL树一样使用子树高度。

设 |A| 表示以A为根的子树的节点数量。对于下面的结构:

                    T
                  /   \
                 /     \
                L       R
               / \     / \
              /   \   /   \
             A     B C     D

需要满足:

|L| >= |C| 且 |L| >= |D|
|R| >= |A| 且 |R| >= |B|

其中A、B、C、D代表任意子树,内部可以包含多个节点,不要求这些子树的高度相等。

如果不满足上述条件,就需要通过左旋、右旋以及递归维护 m() 恢复平衡。

1. LL型

LL型是指左子树的左孩子规模过大,导致SB树的平衡性失效。解决方法是对根节点T进行一次右旋,然后重新检查相关节点的平衡性。

举例:

                    T
                  /   \
                 /     \
                L       R
               / \     / \
              /   \   /   \
             A     B C     D

其中,L是左树,R是右树;A、B、C、D是它们的子树,并且这些子树的高度不保证相等。

此时,|A|>|R|,也就是说左子树L的左孩子A的节点数量,大于右子树R的节点数量,违反SB树的平衡条件。

设置m(T),对以T为根节点的子树进行平衡性维护。


第一步,对T进行右旋:

                    L
                  /   \
                 /     \
                A       T
                       / \
                      /   \
                     B     R
                          / \
                         /   \
                        C     D

将L的右孩子B交给T作为左孩子,再让T成为L的右孩子,L成为新的根节点。


第二步,m(T)。

由于旋转后T的左右子树发生变化,因此先检查T的平衡性,并在必要时继续调整。


第三步,m(L)。

由于L现在成为整棵局部子树的根节点,因此还需要检查L的平衡性。

最终完成LL型调整。

2. RR型

RR型是指右子树的右孩子规模过大,导致SB树的平衡性失效。解决方法是对根节点T进行一次左旋,然后重新检查相关节点的平衡性。

举例:

                    T
                  /   \
                 /     \
                L       R
               / \     / \
              /   \   /   \
             A     B C     D

其中,L是左树,R是右树;A、B、C、D是它们的子树,并且这些子树的高度不保证相等。

此时,|D|>|L|,也就是说右子树R的右孩子D的节点数量,大于左子树L的节点数量,违反SB树的平衡条件。

设置m(T),对以T为根节点的子树进行平衡性维护。


第一步,对T进行左旋:

                    R
                  /   \
                 /     \
                T       D
               / \
              /   \
             L     C
            / \
           /   \
          A     B

将R的左孩子C交给T作为右孩子,再让T成为R的左孩子,R成为新的根节点。


第二步,m(T)。

由于旋转后T的左右子树发生变化,因此先检查T的平衡性,并在必要时继续调整。


第三步,m(R)。

由于R现在成为整棵局部子树的根节点,因此还需要检查R的平衡性。

最终完成RR型调整。

3. LR型

LR型是指左子树的右孩子规模过大,导致SB树的平衡性失效。解决方法是先对左子树进行一次左旋,再对根节点进行一次右旋,最后重新维护相关节点的平衡性。

举例:

                    T
                  /   \
                 /     \
                L       R
               / \     / \
              /   \   /   \
             A     B C     D
                  / \
                 /   \
                E     F

其中,L是左树,R是右树;A、C、D、E、F都是子树,B为L的右孩子。这些子树的高度不保证相等。

此时,|B|>|R|,也就是说左子树L的右孩子B的节点数量,大于右子树R的节点数量,违反SB树的平衡条件。

设置m(T),对以T为根节点的子树进行平衡性维护。


第一步,对左子树L进行左旋:

                    T
                  /   \
                 /     \
                B       R
               / \     / \
              /   \   /   \
             L     F C     D
            / \
           /   \
          A     E

将B的左孩子E交给L作为右孩子,再让L成为B的左孩子,B成为T的左孩子。


第二步,对T进行右旋:

                    B
                  /   \
                 /     \
                L       T
               / \     / \
              /   \   /   \
             A     E F     R
                          / \
                         /   \
                        C     D

将B的右孩子F交给T作为左孩子,再让T成为B的右孩子,B成为新的根节点。


第三步,m(L)。

L现在成为B的左孩子,需要重新检查L的平衡性。


第四步,m(T)。

T现在成为B的右孩子,需要重新检查T的平衡性。


第五步,m(B)。

B现在成为整棵局部子树的根节点,需要检查其平衡性。

最终完成LR型调整。

4. RL型

RL型是指右子树的左孩子规模过大,导致SB树的平衡性失效。解决方法是先对右子树进行一次右旋,再对根节点进行一次左旋,最后重新维护相关节点的平衡性。

举例:

                    T
                  /   \
                 /     \
                L       R
               / \     / \
              /   \   /   \
             A     B C     D
                    / \
                   /   \
                  E     F

其中,L是左树,R是右树;A、B、D、E、F都是子树,C为R的左孩子。这些子树的高度不保证相等。

此时,|C|>|L|,也就是说右子树R的左孩子C的节点数量,大于左子树L的节点数量,违反SB树的平衡条件。

设置m(T),对以T为根节点的子树进行平衡性维护。


第一步,对右子树R进行右旋:

                    T
                  /   \
                 /     \
                L       C
               / \     / \
              /   \   /   \
             A     B E     R
                        /   \
                       F     D

将C的右孩子F交给R作为左孩子,再让R成为C的右孩子,C成为T的右孩子。


第二步,对T进行左旋:

                    C
                  /   \
                 /     \
                T       R
               / \     / \
              /   \   /   \
             L     E F     D
            / \
           /   \
          A     B

将C的左孩子E交给T作为右孩子,再让T成为C的左孩子,C成为新的根节点。


第三步,m(T)。

T现在成为C的左孩子,需要重新检查T的平衡性。


第四步,m(R)。

R现在成为C的右孩子,需要重新检查R的平衡性。


第五步,m(C)。

C现在成为整棵局部子树的根节点,需要检查其平衡性。

最终完成RL型调整。

5. 四种调整的比较

类型

失衡判断

旋转方式

后续维护顺序

LL

|A|>|R|

T右旋

m(T) → m(L)

RR

|D|>|L|

T左旋

m(T) → m(R)

LR

|B|>|R|

L左旋,T右旋

m(L) → m(T) → m(B)

RL

|C|>|L|

R右旋,T左旋

m(T) → m(R) → m(C)

其中,m(T) 表示对以T为根的子树调用SB树的平衡维护函数 maintain()。

需要注意:以上展示的是四种失衡情况对应的旋转与递归维护过程。maintain() 在调整某棵子树时还可能继续触发旋转,因此完整实现中必须更新节点的 size,并在必要时递归维护,不能仅执行一次固定旋转就结束。

6.SB树实现代码

SB树实现代码如下:

package Class015;

public class Code_SizeBalancedTreeMap {
    public static class SBTNode <K extends Comparable<K>,V>{
        public K key;
        public V value;
        public SBTNode<K,V>l;
        public SBTNode<K,V>r;
        public int size;

        public SBTNode(K key,V value){
            this.key=key;
            this.value=value;
            size=1;
        }
    }
    public static class SizeBalancedTreeMap<K extends Comparable<K>,V> {
        private SBTNode<K, V> root;

        //size表示子树包含的节点数量(不同key的数量),空树为0
        private int size(SBTNode<K, V> cur) {
            return cur == null ? 0 : cur.size;
        }

        private void updateSize(SBTNode<K, V> cur) {
            cur.size = size(cur.l) + size(cur.r) + 1;
        }

        //右旋:左孩子上升,原左孩子的右子树交给当前节点
        private SBTNode<K, V> rightRotate(SBTNode<K, V> cur) {
            SBTNode<K, V> leftNode = cur.l;
            cur.l = leftNode.r;
            leftNode.r = cur;
            updateSize(cur);
            updateSize(leftNode);
            return leftNode;
        }

        //左旋:右孩子上升,原右孩子的左子树交给当前节点。
        private SBTNode<K, V> leftRotate(SBTNode<K, V> cur) {
            SBTNode<K, V> rightNode = cur.r;
            cur.r = rightNode.l;
            rightNode.l = cur;
            updateSize(cur);
            updateSize(rightNode);
            return rightNode;
        }

        //根据侄子树与叔叔树的节点数量判断LL,LR,RR,RL.
        private SBTNode<K, V> matain(SBTNode<K, V> cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = size(cur.l);
            int rightSize = size(cur.r);
            int leftLeftSize = cur.l == null ? 0 : size(cur.l.l);
            int leftRightSize = cur.l == null ? 0 : size(cur.l.r);
            int rightLeftSize = cur.r == null ? 0 : size(cur.r.l);
            int rightRightSize = cur.r == null ? 0 : size(cur.r.r);

            if (leftLeftSize > rightSize) {//LL
                cur = rightRotate(cur);
                cur.r = matain(cur.r);
                cur = matain(cur);
            } else if (leftRightSize > rightSize) {//LR
                cur.l = leftRotate(cur.l);
                cur = rightRotate(cur);
                cur.l = matain(cur.l);
                cur.r = matain(cur.r);
                cur = matain(cur);
            } else if (rightRightSize > leftSize) {//RR
                cur = leftRotate(cur);
                cur.l = matain(cur.l);
                cur = matain(cur);
            } else if (rightLeftSize > leftSize) {//RL
                cur.r = rightRotate(cur.r);
                cur = leftRotate(cur);
                cur.l = matain(cur.l);
                cur.r = matain(cur.r);
                cur = matain(cur);
            }
            return cur;
        }

        //仅用于main手工连接测试树后,自底向上补算size
        //正常使用put/remove时不需要调用
        private int refreshSizes(SBTNode<K, V> cur) {
            if (cur == null) return 0;
            cur.size = refreshSizes(cur.l) + refreshSizes(cur.r) + 1;
            return cur.size;
        }

        //新key建节点;已有的key更新value,不增加节点数
        public void put(K key, V value) {
            if (key == null) {
                throw new IllegalArgumentException("key 不能为 null");
            }
            root = add(root, key, value);
        }

        private SBTNode<K, V> add(SBTNode<K, V> cur, K key, V value) {
            if (cur == null) {
                return new SBTNode<>(key, value);
            }
            int cmp = key.compareTo(cur.key);
            if (cmp < 0) {
                cur.l = add(cur.l, key, value);
            } else if (cmp > 0) {
                cur.r = add(cur.r, key, value);
            } else {
                cur.value = value;
                return cur;
            }
            updateSize(cur);
            return matain(cur);
        }

        //按搜索二叉树的规则查找
        private SBTNode<K, V> find(K key) {
            if (key == null) {
                throw new IllegalArgumentException("key不能为null");
            }
            SBTNode<K, V> cur = root;
            while (cur != null) {
                int cmp = key.compareTo(cur.key);
                if (cmp == 0) {
                    return cur;
                }
                cur = cmp < 0 ? cur.l : cur.r;
            }
            return null;
        }

        public boolean containsKey(K key) {
            return find(key) != null;
        }

        public V get(K key) {
            SBTNode<K, V> cur = find(key);
            return cur == null ? null : cur.value;
        }

        //删除指定key,若节点有两个孩子,取右树最左节点(后继)替换
        public void remove(K key) {
            if (containsKey(key)) {
                root = delete(root, key);
            }
        }

        private SBTNode<K, V> delete(SBTNode<K, V> cur, K key) {
            int cmp = key.compareTo(cur.key);
            if (cmp < 0) {
                cur.l = delete(cur.l, key);
            } else if (cmp > 0) {
                cur.r = delete(cur.r, key);
            } else {
                if (cur.l == null) {
                    return cur.r;
                }
                if (cur.r == null) {
                    return cur.l;
                }
                SBTNode<K, V> next = cur.r;
                while (next.l != null) {
                    next = next.l;
                }
                cur.key = next.key;
                cur.value = next.value;
                cur.r = removeMin(cur.r);
            }
            updateSize(cur);
            return matain(cur);
        }

        //将右子树最小节点整体删除,子树高度/规模沿着路径更新。
        private SBTNode<K, V> removeMin(SBTNode<K, V> cur) {
            if (cur.l == null) {
                return cur.r;
            }
            cur.l = removeMin(cur.l);
            updateSize(cur);
            return matain(cur);
        }

        public int size() {
            return size(root);
        }

        //中序遍历必然按照key从小到大输出
        public void inOrder() {
            inOrder(root);
            System.out.println();
        }

        private void inOrder(SBTNode<K, V> cur) {
            if (cur == null) {
                return;
            }
            inOrder(cur.l);
            System.out.print(cur.key + "=" + cur.value + "  ");
            inOrder(cur.r);
        }

        //查看整棵树的结构(右孩子打印在上方)
        public void printTree() {
            printTree(root, 0);
        }

        private void printTree(SBTNode<K, V> cur, int depth) {
            if (cur == null) {
                return;
            }
            printTree(cur.r, depth + 1);
            System.out.println("   ".repeat(depth)
                    + cur.key + "(Size=" + cur.size + ")");
            printTree(cur.l, depth + 1);
        }

        //用于检查所有节点的SBT性质和size是否正确。
        public boolean isSBT() {
            return check(root, null, null) >= 0;
        }

        private int check(SBTNode<K, V> cur, K low, K high) {
            if (cur == null) {
                return 0;
            }
            if ((low != null && cur.key.compareTo(low) <= 0)
                    || (high != null && cur.key.compareTo(high) >= 0)) {
            return -1;
        }

        int left = check(cur.l, low, cur.key);
        int right=check(cur.r,cur.key,high);
        if(left<0||right<0||cur.size!=left+right+1){
            return -1;
        }
        int ll=cur.l==null?0:size(cur.l.l);
        int lr=cur.l==null?0:size(cur.l.r);
        int rl=cur.r==null?0:size(cur.r.l);
        int rr=cur.r==null?0:size(cur.r.r);
        if (ll>right||lr>right||rl>left||rr>left){
            return -1;
        }
        return cur.size;
        }
    }
    public static void main(String[] args) {
        System.out.println("===== 原有插入、查询、删除测试 =====");
        SizeBalancedTreeMap<Integer, String> tree = new SizeBalancedTreeMap<>();
        int[] nums = {50, 30, 80, 15, 40, 60, 100, 10, 20, 35, 45, 55, 65, 90, 110};
        for (int num : nums) {
            tree.put(num, "v" + num);
        }

        System.out.println("初始中序遍历:");
        tree.inOrder();
        System.out.println("初始结构:");
        tree.printTree();
        System.out.println("节点数=" + tree.size() + ",满足SBT=" + tree.isSBT());
        System.out.println("查找40:" + tree.get(40));
        System.out.println("是否包含99:" + tree.containsKey(99));

        tree.put(42, "v42");           // 插入新节点
        tree.put(40, "更新的40");       // 原有 key:仅更新 value
        System.out.println("插入42并更新40:");
        tree.inOrder();
        System.out.println("满足SBT=" + tree.isSBT());

        tree.remove(10);               // 删除叶子节点
        tree.remove(40);               // 删除有两个孩子的节点
        tree.remove(50);               // 删除根节点(此时可能有两个孩子)
        System.out.println("删除10、40、50后:");
        tree.inOrder();
        System.out.println("节点数=" + tree.size() + ",满足SBT=" + tree.isSBT());

        // ========== 1. LL:|A|=4 > |R|=3 ==========
        //                  T=80
        //                /      \
        //              L=40     R=100
        //             /   \     /   \
        //           A=20  B=60  90   110
        //           / \    /
        //         15  25  55
        //         /
        //        10
        SizeBalancedTreeMap<Integer, Integer> ll = new SizeBalancedTreeMap<>();
        ll.root = new SBTNode<>(80, 80);
        ll.root.l = new SBTNode<>(40, 40);
        ll.root.r = new SBTNode<>(100, 100);
        ll.root.l.l = new SBTNode<>(20, 20);
        ll.root.l.r = new SBTNode<>(60, 60);
        ll.root.l.l.l = new SBTNode<>(15, 15);
        ll.root.l.l.r = new SBTNode<>(25, 25);
        ll.root.l.l.l.l = new SBTNode<>(10, 10);
        ll.root.l.r.l = new SBTNode<>(55, 55);
        ll.root.r.l = new SBTNode<>(90, 90);
        ll.root.r.r = new SBTNode<>(110, 110);
        ll.refreshSizes(ll.root);
        System.out.println("\n===== LL:右旋 =====");
        System.out.println("旋转前:根=" + ll.root.key + ",A=" + ll.size(ll.root.l.l) + ",R=" + ll.size(ll.root.r)
                + ",平衡=" + ll.isSBT());
        ll.root = ll.matain(ll.root);
        System.out.println("旋转后:根=" + ll.root.key + ",平衡=" + ll.isSBT());
        if (ll.root.key != 40 || !ll.isSBT() || ll.size() != 11) throw new AssertionError("LL错误");
        ll.printTree();
        System.out.print("中序:"); ll.inOrder();
        System.out.println("查找55:" + ll.containsKey(55));
        ll.put(42, 42);
        if (!ll.isSBT()) throw new AssertionError("LL插入失败");
        ll.remove(10); // 删除叶子
        if (!ll.isSBT()) throw new AssertionError("LL删除10失败");
        ll.remove(20); // 删除两个孩子的节点
        if (!ll.isSBT()) throw new AssertionError("LL删除20失败");
        System.out.print("插入42、删除10和20后:"); ll.inOrder();
        System.out.println("节点数=" + ll.size() + ",平衡=" + ll.isSBT());

        // ========== 2. RR:|D|=4 > |L|=3 ==========
        //            T=30
        //           /    \
        //        L=10    R=70
        //       /  \    /   \
        //      5   20  C=50 D=90
        //             /     /  \
        //            45    85  100
        //                  /
        //                 80
        SizeBalancedTreeMap<Integer, Integer> rr = new SizeBalancedTreeMap<>();
        rr.root = new SBTNode<>(30, 30);
        rr.root.l = new SBTNode<>(10, 10);
        rr.root.r = new SBTNode<>(70, 70);
        rr.root.l.l = new SBTNode<>(5, 5);
        rr.root.l.r = new SBTNode<>(20, 20);
        rr.root.r.l = new SBTNode<>(50, 50);
        rr.root.r.r = new SBTNode<>(90, 90);
        rr.root.r.l.l = new SBTNode<>(45, 45);
        rr.root.r.r.l = new SBTNode<>(85, 85);
        rr.root.r.r.r = new SBTNode<>(100, 100);
        rr.root.r.r.l.l = new SBTNode<>(80, 80);
        rr.refreshSizes(rr.root);
        System.out.println("\n===== RR:左旋 =====");
        System.out.println("旋转前:根=" + rr.root.key + ",D=" + rr.size(rr.root.r.r) + ",L=" + rr.size(rr.root.l)
                + ",平衡=" + rr.isSBT());
        rr.root = rr.matain(rr.root);
        System.out.println("旋转后:根=" + rr.root.key + ",平衡=" + rr.isSBT());
        if (rr.root.key != 70 || !rr.isSBT() || rr.size() != 11) throw new AssertionError("RR错误");
        rr.printTree();
        System.out.print("中序:"); rr.inOrder();
        System.out.println("查找85:" + rr.containsKey(85));
        rr.put(48, 48);
        if (!rr.isSBT()) throw new AssertionError("RR插入失败");
        rr.remove(5); // 删除叶子
        if (!rr.isSBT()) throw new AssertionError("RR删除5失败");
        rr.remove(90); // 删除两个孩子的节点
        if (!rr.isSBT()) throw new AssertionError("RR删除90失败");
        System.out.print("插入48、删除5和90后:"); rr.inOrder();
        System.out.println("节点数=" + rr.size() + ",平衡=" + rr.isSBT());

        // ========== 3. LR:|B|=4 > |R|=3 ==========
        //                T=80
        //               /    \
        //             L=30  R=100
        //            /   \  /   \
        //          A=15  B=50 90 110
        //          /     /   \
        //         10  E=40  F=60
        //             /
        //            35
        SizeBalancedTreeMap<Integer, Integer> lr = new SizeBalancedTreeMap<>();
        lr.root = new SBTNode<>(80, 80);
        lr.root.l = new SBTNode<>(30, 30);
        lr.root.r = new SBTNode<>(100, 100);
        lr.root.l.l = new SBTNode<>(15, 15);
        lr.root.l.r = new SBTNode<>(50, 50);
        lr.root.l.l.l = new SBTNode<>(10, 10);
        lr.root.l.r.l = new SBTNode<>(40, 40);
        lr.root.l.r.r = new SBTNode<>(60, 60);
        lr.root.l.r.l.l = new SBTNode<>(35, 35);
        lr.root.r.l = new SBTNode<>(90, 90);
        lr.root.r.r = new SBTNode<>(110, 110);
        lr.refreshSizes(lr.root);
        System.out.println("\n===== LR:左子树左旋、根右旋 =====");
        System.out.println("旋转前:根=" + lr.root.key + ",B=" + lr.size(lr.root.l.r) + ",R=" + lr.size(lr.root.r)
                + ",平衡=" + lr.isSBT());
        lr.root = lr.matain(lr.root);
        System.out.println("旋转后:根=" + lr.root.key + ",平衡=" + lr.isSBT());
        if (lr.root.key != 50 || !lr.isSBT() || lr.size() != 11) throw new AssertionError("LR错误");
        lr.printTree();
        System.out.print("中序:"); lr.inOrder();
        System.out.println("查找35:" + lr.containsKey(35));
        lr.put(42, 42);
        if (!lr.isSBT()) throw new AssertionError("LR插入失败");
        lr.remove(10); // 删除叶子
        if (!lr.isSBT()) throw new AssertionError("LR删除10失败");
        lr.remove(80); // 删除两个孩子的节点
        if (!lr.isSBT()) throw new AssertionError("LR删除80失败");
        System.out.print("插入42、删除10和80后:"); lr.inOrder();
        System.out.println("节点数=" + lr.size() + ",平衡=" + lr.isSBT());

        // ========== 4. RL:|C|=4 > |L|=3 ==========
        //               T=30
        //             /      \
        //           L=10      R=80
        //          /  \     /    \
        //         5   20  C=60    D=100
        //                / \       \
        //               50  70     110
        //                    \
        //                     75
        SizeBalancedTreeMap<Integer, Integer> rl = new SizeBalancedTreeMap<>();
        rl.root = new SBTNode<>(30, 30);
        rl.root.l = new SBTNode<>(10, 10);
        rl.root.r = new SBTNode<>(80, 80);
        rl.root.l.l = new SBTNode<>(5, 5);
        rl.root.l.r = new SBTNode<>(20, 20);
        rl.root.r.l = new SBTNode<>(60, 60);
        rl.root.r.r = new SBTNode<>(100, 100);
        rl.root.r.l.l = new SBTNode<>(50, 50);
        rl.root.r.l.r = new SBTNode<>(70, 70);
        rl.root.r.l.r.r = new SBTNode<>(75, 75);
        rl.root.r.r.r = new SBTNode<>(110, 110);
        rl.refreshSizes(rl.root);
        System.out.println("\n===== RL:右子树右旋、根左旋 =====");
        System.out.println("旋转前:根=" + rl.root.key + ",C=" + rl.size(rl.root.r.l) + ",L=" + rl.size(rl.root.l)
                + ",平衡=" + rl.isSBT());
        rl.root = rl.matain(rl.root);
        System.out.println("旋转后:根=" + rl.root.key + ",平衡=" + rl.isSBT());
        if (rl.root.key != 60 || !rl.isSBT() || rl.size() != 11) throw new AssertionError("RL错误");
        rl.printTree();
        System.out.print("中序:"); rl.inOrder();
        System.out.println("查找75:" + rl.containsKey(75));
        rl.put(55, 55);
        if (!rl.isSBT()) throw new AssertionError("RL插入失败");
        rl.remove(5); // 删除叶子
        if (!rl.isSBT()) throw new AssertionError("RL删除5失败");
        rl.remove(80); // 删除两个孩子的节点
        if (!rl.isSBT()) throw new AssertionError("RL删除80失败");
        System.out.print("插入55、删除5和80后:"); rl.inOrder();
        System.out.println("节点数=" + rl.size() + ",平衡=" + rl.isSBT());

        System.out.println("\nLL、RR、LR、RL四组测试全部通过");
    }
}

运行结果:

===== 原有插入、查询、删除测试 =====
初始中序遍历:
10=v10  15=v15  20=v20  30=v30  35=v35  40=v40  45=v45  50=v50  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
初始结构:
         110(Size=1)
      100(Size=3)
         90(Size=1)
   80(Size=7)
         65(Size=1)
      60(Size=3)
         55(Size=1)
50(Size=15)
         45(Size=1)
      40(Size=3)
         35(Size=1)
   30(Size=7)
         20(Size=1)
      15(Size=3)
         10(Size=1)
节点数=15,满足SBT=true
查找40:v40
是否包含99:false
插入42并更新40:
10=v10  15=v15  20=v20  30=v30  35=v35  40=更新的40  42=v42  45=v45  50=v50  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
满足SBT=true
删除10、40、50后:
15=v15  20=v20  30=v30  35=v35  42=v42  45=v45  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
节点数=13,满足SBT=true

===== LL:右旋 =====
旋转前:根=80,A=4,R=3,平衡=false
旋转后:根=40,平衡=true
         110(Size=1)
      100(Size=3)
         90(Size=1)
   80(Size=6)
      60(Size=2)
         55(Size=1)
40(Size=11)
      25(Size=1)
   20(Size=4)
      15(Size=2)
         10(Size=1)
中序:10=10  15=15  20=20  25=25  40=40  55=55  60=60  80=80  90=90  100=100  110=110  
查找55:true
插入42、删除10和20后:15=15  25=25  40=40  42=42  55=55  60=60  80=80  90=90  100=100  110=110  
节点数=10,平衡=true

===== RR:左旋 =====
旋转前:根=30,D=4,L=3,平衡=false
旋转后:根=70,平衡=true
      100(Size=1)
   90(Size=4)
      85(Size=2)
         80(Size=1)
70(Size=11)
      50(Size=2)
         45(Size=1)
   30(Size=6)
         20(Size=1)
      10(Size=3)
         5(Size=1)
中序:5=5  10=10  20=20  30=30  45=45  50=50  70=70  80=80  85=85  90=90  100=100  
查找85:true
插入48、删除5和90后:10=10  20=20  30=30  45=45  48=48  50=50  70=70  80=80  85=85  100=100  
节点数=10,平衡=true

===== LR:左子树左旋、根右旋 =====
旋转前:根=80,B=4,R=3,平衡=false
旋转后:根=50,平衡=true
         110(Size=1)
      100(Size=3)
         90(Size=1)
   80(Size=5)
      60(Size=1)
50(Size=11)
      40(Size=2)
         35(Size=1)
   30(Size=5)
      15(Size=2)
         10(Size=1)
中序:10=10  15=15  30=30  35=35  40=40  50=50  60=60  80=80  90=90  100=100  110=110  
查找35:true
插入42、删除10和80后:15=15  30=30  35=35  40=40  42=42  50=50  60=60  90=90  100=100  110=110  
节点数=10,平衡=true

===== RL:右子树右旋、根左旋 =====
旋转前:根=30,C=4,L=3,平衡=false
旋转后:根=60,平衡=true
         110(Size=1)
      100(Size=2)
   80(Size=5)
         75(Size=1)
      70(Size=2)
60(Size=11)
      50(Size=1)
   30(Size=5)
         20(Size=1)
      10(Size=3)
         5(Size=1)
中序:5=5  10=10  20=20  30=30  50=50  60=60  70=70  75=75  80=80  100=100  110=110  
查找75:true
插入55、删除5和80后:10=10  20=20  30=30  50=50  55=55  60=60  70=70  75=75  100=100  110=110  
节点数=10,平衡=true

LL、RR、LR、RL四组测试全部通过

(四)红黑树

红黑树特点(简明):

(1)节点要么是红要么是黑

(2)头节点(整棵树)和叶节点(null)必须为黑

(3)任何两个红节点不能相邻

(4)cur当前头,每条到结束的路,黑一样多(最长的路是红黑交替,最短的路是全黑。这样就能保证最长的路不会超过最短的路的两倍)

1.红黑树的四条性质

(1)每个节点要么是红色,要么是黑色。

(2)根节点必须是黑色,所有空叶节点(null,通常称为NIL)也视为黑色。

(3)红色节点不能有红色孩子,即任意两个红色节点不能直接相连。

(4)从任意节点出发,到它所有后代NIL节点的路径,包含的黑色节点数量必须相同。

例如,下面是一棵合法的红黑树:

                  40(B)
                 /     \
             20(R)     60(R)
             /   \     /   \
          10(B) 30(B) 50(B) 70(B)
           / \    / \    / \    / \
         NIL NIL NIL NIL NIL NIL NIL NIL

B:Black(黑色)
R:Red(红色)
NIL:黑色空节点

从根节点40到任意NIL的路径,都有相同数量的黑色节点。例如:

40(B) → 20(R) → 10(B) → NIL(B)
40(B) → 60(R) → 70(B) → NIL(B)

每条路径均包含3个黑色节点。

红黑树允许不同路径的长度不同,但是因为红节点不能连续出现,最长路径的长度不会超过最短路径的两倍。因此,红黑树的高度为 O(log N),查找、插入和删除的时间复杂度也都是 O(log N)。

2.红黑树的四种旋转调整

与AVL树、SB树一样,红黑树也有LL、RR、LR、RL四种结构。

但红黑树的关键区别是:除了旋转,还必须调整节点颜色。

设 G 为祖父节点,P 为父节点,X 为发生红红冲突的节点,T1~T4 为四棵可能非空的子树。下面展示的是插入修复过程中的局部失衡结构,并非已经满足红黑树性质的结构。

以下先假设叔叔节点为黑色。

(1)LL型:右旋

LL型指P是G的左孩子,X又是P的左孩子。

旋转前:

                    G(B)
                   /    \
                 P(R)   T4(B)
                /   \
              X(R)  T3(B)
             /   \
           T1(B) T2(B)

由于P和X都是红色,出现两个相邻红节点,违反红黑树性质。

第一步,对G右旋:

                    P(R)
                   /    \
                 X(R)   G(B)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

第二步,改变颜色:P变黑,G变红。

                    P(B)
                   /    \
                 X(R)   G(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

这样既消除了红红冲突,也保持了各条路径的黑色节点数量。

(2)RR型:左旋

RR型指P是G的右孩子,X又是P的右孩子。

旋转前:

                    G(B)
                   /    \
                 T1(B)  P(R)
                       /   \
                    T2(B)  X(R)
                           /   \
                         T3(B) T4(B)

此时P和X均为红色。

第一步,对G左旋:

                    P(R)
                   /    \
                 G(B)   X(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

第二步,P变黑,G变红:

                    P(B)
                   /    \
                 G(R)   X(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

RR型与LL型完全对称。

(3)LR型:先左旋,再右旋

LR型指P是G的左孩子,但X是P的右孩子。

旋转前:

                    G(B)
                   /    \
                 P(R)   T4(B)
                /   \
              T1(B)  X(R)
                    /   \
                  T2(B) T3(B)

第一步,对P左旋,使X上升:

                    G(B)
                   /    \
                 X(R)   T4(B)
                /   \
              P(R)  T3(B)
             /   \
           T1(B) T2(B)

第二步,对G右旋:

                    X(R)
                   /    \
                 P(R)   G(B)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

第三步,X变黑,G变红:

                    X(B)
                   /    \
                 P(R)   G(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

最终由X成为新根,消除红红冲突。

(4)RL型:先右旋,再左旋

RL型指P是G的右孩子,但X是P的左孩子。

旋转前:

                    G(B)
                   /    \
                 T1(B)  P(R)
                       /   \
                     X(R)  T4(B)
                    /   \
                  T2(B) T3(B)

第一步,对P右旋:

                    G(B)
                   /    \
                 T1(B)  X(R)
                       /   \
                     T2(B)  P(R)
                           /   \
                         T3(B) T4(B)

第二步,对G左旋:

                    X(R)
                   /    \
                 G(B)   P(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

第三步,X变黑,G变红:

                    X(B)
                   /    \
                 G(R)   P(R)
                /   \   /   \
              T1(B) T2(B) T3(B) T4(B)

RL型与LR型完全对称。

(5)四种调整总结

类型

结构

旋转方式

变色方式

LL

左孩子的左孩子

G右旋

P黑、G红

RR

右孩子的右孩子

G左旋

P黑、G红

LR

左孩子的右孩子

P左旋、G右旋

X黑、G红

RL

右孩子的左孩子

P右旋、G左旋

X黑、G红

以上图中将T1~T4画为黑色子树的根,便于观察指针变化;实际实现时这些子树可以含有更多节点,旋转不会丢失它们,也不要求它们的高度相等。

3.插入节点时的调整细节

红黑树的新节点默认插入为红色。因为增加一个红色节点不会直接增加某条路径的黑色节点数量。

插入后有三种情况:

情况一:父节点是黑色。 不违反红黑树性质,无需调整。

情况二:父节点和叔叔节点都是红色。 将父节点、叔叔节点变黑,将祖父节点变红,再从祖父节点继续向上检查。

例如:

插入后:                  变色后:

       G(B)                    G(R)
      /    \                  /    \
    P(R)   U(R)             P(B)   U(B)
    /                       /
  X(R)                    X(R)

如果祖父节点变红后又与自己的红色父节点相邻,就继续向上修复;根节点最终设为黑色。

情况三:父节点红色、叔叔节点黑色。 根据LL、RR、LR、RL执行对应旋转和变色操作。

4.删除节点时的调整细节

删除比插入复杂,因为删除黑色节点可能减少某些路径的黑色节点数量。

  • 删除红色叶节点:一般直接删除,不改变黑高。

  • 删除黑色节点且由红色孩子替代:将替代节点变黑。

  • 删除黑色节点且无法直接补偿黑高:需要执行删除修复。

设 X 为需要补偿黑高的位置,W 为X的兄弟节点。以X为父节点的左孩子为例:

情况

兄弟W及其孩子

修复操作

1

W为红色

W变黑、父节点变红,对父节点左旋

2

W为黑色,两个孩子也为黑色

W变红,将黑高不足的问题向父节点传递

3

W为黑色,左孩子红、右孩子黑

W变红、左孩子变黑,对W右旋

4

W为黑色,右孩子红

调整W及父节点颜色,对父节点左旋

如果X在右边,则左右完全对称。

删除操作实际上仍然使用左旋、右旋和变色,但其触发条件与插入时的LL、RR、LR、RL不同。

5.红黑树的实现

代码如下:

package Class015;

/**
 * 红黑树入门:请按下面的顺序阅读源码。
 * 1. Node / NIL:了解颜色、父节点和空孩子。
 * 2. leftRotate / rightRotate:只改变连接关系,不会改变中序遍历顺序。
 * 3. put / insertFix:像普通BST一样插入红节点,然后消除连续红节点。
 * 4. remove / transplant / deleteFix:删除节点,并补回可能减少的黑高。
 * 5. isRedBlackTree / check:逐条检查红黑树性质。
 * 6. main:四种旋转的真实8节点示例,已画出插入前、中间、完成后的树。
 *
 * 四条核心性质:根黑、NIL黑、红节点没有红孩子、每条到NIL的路径黑高相同。
 * 另外它必须满足二叉搜索树顺序:左键小于根键,右键大于根键。
 * B=黑色,R=红色;“旋转”与“变色”是两种不同操作。
 */
public class Code_RedBlackTreeMap {
    // K代表可以比较大小的键(例如Integer);V代表对应的值(例如String)。
    // 同一个key再次put会覆盖旧value,而不是多建一个节点。
    public static class RedBlackTreeMap<K extends Comparable<K>, V> {
        // 这里用boolean表示颜色:true=红,false=黑;红黑颜色与键值无关。
        private static final boolean RED = true;
        private static final boolean BLACK = false;

        // NIL是所有空孩子共用的黑色哨兵;它不算实际节点。
        private static class Node<K, V> {
            K key;                  // 用于BST查找和排序的关键字
            V value;                // 与key关联的数据
            Node<K, V> l, r, parent; // 左孩子、右孩子、父节点;用于向上修复平衡
            boolean color;          // RED或BLACK

            Node(K key, V value, boolean color) {
                this.key = key;
                this.value = value;
                this.color = color;
            }
        }

        // 重要:本实现不用Java的null表示空孩子,而统一用“黑色NIL对象”。
        // 例如叶子节点的l和r都等于NIL,这样可以直接读取NIL.color(黑色)。
        // NIL本身不计入count,也不能拿来比较key;它的key=null只是占位。
        private final Node<K, V> NIL = new Node<>(null, null, BLACK);
        private Node<K, V> root; // 空树时root==NIL
        private int count;       // 真正的节点数量,不包括NIL
        private String lastInsertCase = "无旋转"; // 仅用于main记录最后一次插入旋转类型

        public RedBlackTreeMap() {
            // 哨兵的左右孩子指向自身:删除修复读取w.l.color时不会空指针。
            NIL.l = NIL;
            NIL.r = NIL;
            // 初始父亲也指向自己;删除过程中NIL.parent可能被临时设为实际父节点,
            // 因为即使x==NIL,deleteFix(x)仍需要从x.parent找到它的兄弟。
            NIL.parent = NIL;
            root = NIL;
        }

        /* 一、左旋(x必须有真实的右孩子y)
         * 旋转前:                  旋转后:
         *         x                       y
         *        / \                     / \
         *       A   y                   x   C
         *          / \                 / \
         *         B   C               A   B
         * 子树B只是从y的左边挪到x的右边,A/B/C内部结构完全不变。
         * 关键顺序:先接B,再接y的父节点,最后让x降到y左侧。
         * BST顺序仍为A < x < B < y < C,旋转本身不负责变色。
         */
        private void leftRotate(Node<K, V> x) {
            Node<K, V> y = x.r;       // y是即将上升的右孩子
            x.r = y.l;                 // y原左子树B交给x,作为x的新右子树
            if (y.l != NIL) y.l.parent = x; // B若非空,也必须更改B的父指针

            y.parent = x.parent;       // y接替x在“原父节点”下面的位置
            if (x.parent == NIL) root = y; // 原x是根节点:现在y成为根
            else if (x == x.parent.l) x.parent.l = y; // 原x是左孩子
            else x.parent.r = y;       // 原x是右孩子

            y.l = x;                   // x下降为y的左孩子
            x.parent = y;              // x父指针也要指回y
        }

        /* 二、右旋(与左旋完全对称)
         * 旋转前:                  旋转后:
         *         x                       y
         *        / \                     / \
         *       y   C                   A   x
         *      / \                         / \
         *     A   B                       B   C
         * 右旋后,y上升,x下降,B从y的右子树变为x的左子树。
         */
        private void rightRotate(Node<K, V> x) {
            Node<K, V> y = x.l;       // y是即将上升的左孩子
            x.l = y.r;                 // 把子树B交给x
            if (y.r != NIL) y.r.parent = x; // 维护B的父指针

            y.parent = x.parent;       // y接替x原来的位置
            if (x.parent == NIL) root = y; // x原本是根
            else if (x == x.parent.r) x.parent.r = y;
            else x.parent.l = y;

            y.r = x;                   // x下降为y的右孩子
            x.parent = y;
        }

        /* 三、put(key,value)的流程
         * ① 依照BST规则找到插入位置:小于往左、大于往右、相等覆盖value。
         * ② 新节点涂红:不增加路径黑色节点数,但可能与红色父节点相邻。
         * ③ 调用insertFix(z)解决可能出现的“红父 + 红子”冲突。
         * 例:put(30,"v30")就是用30进行定位,用"v30"保存关联值。
         */
        public void put(K key, V value) {
            if (key == null) throw new IllegalArgumentException("key不能为null");
            lastInsertCase = "无旋转"; // 清空上一次put记录的旋转类型

            Node<K, V> p = NIL;    // p始终追踪cur的父节点;新节点最终接到p下面
            Node<K, V> cur = root; // cur从根开始寻找插入位置
            while (cur != NIL) {
                p = cur;
                int cmp = key.compareTo(cur.key);
                if (cmp == 0) { // 已有同样的key,只覆盖value,不改变结构和count
                    cur.value = value;
                    return;
                }
                cur = cmp < 0 ? cur.l : cur.r; // 左小右大
            }

            Node<K, V> z = new Node<>(key, value, RED); // 新节点先标红!
            z.l = NIL;  // 红黑树中“没有左孩子”是NIL,不是null
            z.r = NIL;
            z.parent = p;
            if (p == NIL) root = z;                 // 空树:新节点成为根
            else if (key.compareTo(p.key) < 0) p.l = z; // 新节点接到父亲左侧
            else p.r = z;                          // 接到父亲右侧

            count++;     // 仅新key才增加节点数
            insertFix(z); // 把可能出现的连续红节点修好,并确保根是黑色
        }

        /* 四、insertFix(z):插入修复(读懂这个函数就读懂红黑树插入)
         * z是当前产生红红冲突的“孩子”;P=z.parent父亲;G=P.parent祖父;U=叔叔。
         * 只要父亲是黑色,就没有红红冲突,while自然结束。
         * 父亲是红色时,祖父G一定存在(根节点强制为黑色)。
         *
         * ① U为红色:P和U都变黑,G变红;把z上移到G,继续看上层是否冲突。
         * ② U为黑色:利用LL/RR/LR/RL旋转,随后把新子树根变黑、原G变红。
         *    LL:右旋G;RR:左旋G;LR:先左旋P、再右旋G;RL:先右旋P、再左旋G。
         * 注意:这里判断的是“红色冲突+叔叔颜色”,不是AVL的高度差。
         */
        private void insertFix(Node<K, V> z) {
            while (z.parent.color == RED) { // 父红+子红 -> 违规,需要处理
                Node<K, V> g = z.parent.parent; // 祖父节点
                if (z.parent == g.l) { // P位于G左侧,可能是LL或LR
                    Node<K, V> u = g.r; // U是G右孩子(可为黑色NIL)
                    if (u.color == RED) { // 情况A:父亲P与叔叔U都红 -> 只变色
                        z.parent.color = BLACK; // P变黑
                        u.color = BLACK;        // U变黑
                        g.color = RED;          // G变红:本层两边黑高仍相等
                        z = g;                  // 把冲突向上交给G继续检查
                    } else {
                        if (z == z.parent.r) { // 情况B1:LR,z位于P右侧,呈“折线”
                            lastInsertCase = "LR";
                            z = z.parent; // 让z临时指向旧P,以便左旋(z)
                            leftRotate(z); // 将LR先整理为LL
                        } else { // 情况B2:LL,z位于P左侧,可以直接右旋G
                            lastInsertCase = "LL";
                        }
                        // 此时已统一成为LL结构:P上升、G下降
                        z.parent.color = BLACK;       // 新子树根P变黑
                        z.parent.parent.color = RED;  // 原祖父G变红
                        rightRotate(z.parent.parent); // 对G右旋,消除连续红节点
                    }
                } else { // P位于G右侧,是RR或RL;与上面左右对称
                    Node<K, V> u = g.l; // 叔叔在G左边
                    if (u.color == RED) { // 情况A:叔叔红,只变色并向上检查
                        z.parent.color = BLACK;
                        u.color = BLACK;
                        g.color = RED;
                        z = g;
                    } else {
                        if (z == z.parent.l) { // RL:z在P左侧,先将折线整理为RR
                            lastInsertCase = "RL";
                            z = z.parent;
                            rightRotate(z); // 先右旋旧P
                        } else { // RR:z在P右侧
                            lastInsertCase = "RR";
                        }
                        z.parent.color = BLACK;       // 新子树根变黑
                        z.parent.parent.color = RED;  // 原G变红
                        leftRotate(z.parent.parent);  // 对G左旋
                    }
                }
            }
            root.color = BLACK; // 可能经历多轮变色使根临时变红,最后统一改黑
        }

        /* 五、查询:和普通二叉搜索树完全一样,不涉及颜色和旋转。
         * 返回实际节点或NIL;get取value,containsKey检查是否找到。
         */
        private Node<K, V> find(K key) {
            if (key == null) throw new IllegalArgumentException("key不能为null");
            Node<K, V> cur = root;
            while (cur != NIL) {
                int cmp = key.compareTo(cur.key);
                if (cmp == 0) return cur;
                cur = cmp < 0 ? cur.l : cur.r;
            }
            return NIL;
        }

        public boolean containsKey(K key) { return find(key) != NIL; }

        public V get(K key) {
            Node<K, V> cur = find(key);
            return cur == NIL ? null : cur.value;
        }

        /* 六、remove(key):先按BST方法真正删除节点,再按颜色修复。
         * 关键区分:z=用户要删的节点,y=最终从原位置被移走的节点,
         *           x=原y位置上接替它的孩子(也可能是黑色NIL)。
         *
         * z有0/1个孩子:用唯一孩子(可能NIL)直接顶替z。
         * z有2个孩子:找右子树中最小的y(中序后继),把y搬到z的位置;
         *             y原来位置由x=y.r补上。右子树最小节点没有左孩子。
         *
         * 为什么保存oldColor?只有“从原位置移走的y”原本是黑色,
         * 才可能导致该路径黑色节点减少1,因而需要deleteFix(x)。
         */
        public void remove(K key) {
            Node<K, V> z = find(key);
            if (z == NIL) return;

            Node<K, V> y = z;          // 默认真正被移走的就是z
            boolean oldColor = y.color; // 保存被移走节点的原颜色
            Node<K, V> x;             // 用来承担黑高补偿的接替节点

            if (z.l == NIL) {       // z没有左孩子:右孩子直接上移(也可为NIL)
                x = z.r;
                transplant(z, z.r);
            } else if (z.r == NIL) { // z没有右孩子:左孩子直接上移
                x = z.l;
                transplant(z, z.l);
            } else {                // z有两个孩子:找到右子树最小节点y
                y = minimum(z.r);   // y是中序后继:它的key最接近、但大于z.key
                oldColor = y.color; // 真正离开原位置的是y,修复看y原颜色
                x = y.r;            // y不可能有左孩子,原位置由其右孩子顶替
                if (y.parent == z) { // y恰好是z的直接右孩子
                    x.parent = y;   // x可能是NIL,修复时仍需要从它找到父亲y
                } else {           // y藏在右子树更深处,必须先从原位置摘出y
                    transplant(y, y.r); // 把原位置的y用它的右孩子x替换
                    y.r = z.r;          // y接收z原有的整棵右子树
                    y.r.parent = y;
                }
                transplant(z, y);  // y接替z在整个树里的位置
                y.l = z.l;         // y接收z原有的整棵左子树
                y.l.parent = y;
                y.color = z.color; // y到达z位置后要使用z原来的颜色,维持那里的黑高
            }
            count--;                            // 实际删掉一个key
            if (oldColor == BLACK) deleteFix(x); // 被移走的是黑节点才需补黑高
            root.color = BLACK;                 // 删除结束:根保持黑色
        }

        // 返回cur子树中key最小的节点:沿左指针走到底即可。
        private Node<K, V> minimum(Node<K, V> cur) {
            while (cur.l != NIL) cur = cur.l;
            return cur;
        }

        /* transplant(u,v):“把u所在位置的父指针连接改为v”。
         * 例如:      p                p
         *            /                /
         *           u       ->       v
         * 注意:这里只负责“父亲连接谁”,不会自动接好v的左右孩子,也不变色。
         * 如果v=NIL也会写v.parent:为deleteFix中追踪空位置的父节点服务。
         */
        private void transplant(Node<K, V> u, Node<K, V> v) {
            if (u.parent == NIL) root = v;       // u原本是整棵树的根
            else if (u == u.parent.l) u.parent.l = v; // u是父亲的左孩子
            else u.parent.r = v;                 // u是父亲的右孩子
            v.parent = u.parent;                 // v的新父亲与u旧父亲相同
        }

        /* 七、deleteFix(x):删除修复,是整份代码最难的部分。
         * 可以把x想成“少了一个黑色节点”的位置(教材常称双黑,Double Black)。
         * “双黑”只是便于理解的概念,并没有真的存储第三种颜色。
         * P=x.parent,W=兄弟(sibling),W的两个孩子=侄子节点。
         * 当x在P左边时,W在P右边。修复思路只有4种:
         *
         * 情况1  W红:W改黑、P改红、对P左旋,转化为“W黑”的情形。
         * 情况2  W黑且两个侄子黑:W改红,黑高不足向P上传(x=P)。
         * 情况3  W黑、近侧侄子红、远侧黑:右旋W,转化为情况4。
         * 情况4  W黑、远侧侄子红:对P左旋并变色,一次解决黑高不足。
         *
         * 当x在P右边时,整套操作左右互换。
         * 注意:NIL是黑色,x本身可能等于NIL;此时x.parent仍有意义。
         */
        private void deleteFix(Node<K, V> x) {
            while (x != root && x.color == BLACK) { // x已变红或x已是根就不用继续
                if (x == x.parent.l) { // x是P的左孩子;W=x.parent.r在右边
                    Node<K, V> w = x.parent.r;
                    if (w.color == RED) { // 情况1:兄弟红,先把兄弟转成黑色
                        w.color = BLACK;
                        x.parent.color = RED;
                        leftRotate(x.parent); // W上升,P下降,新的W会是黑色
                        w = x.parent.r;       // P已下降,重新获取x真正的兄弟
                    }
                    // 此时W为黑色。两个侄子全黑,就无法直接借一个红节点补偿。
                    if (w.l.color == BLACK && w.r.color == BLACK) { // 情况2
                        w.color = RED; // W让出一层黑高,相当于向上转移“缺黑”
                        x = x.parent; // 问题交给P,继续while向祖先检查
                    } else {
                        if (w.r.color == BLACK) { // 情况3:远侧侄子黑,近侧一定红
                            w.l.color = BLACK; // 近侧侄子变黑
                            w.color = RED;    // W变红
                            rightRotate(w);  // 对W右旋,制造情况4
                            w = x.parent.r;   // 重新定位兄弟
                        }
                        // 情况4:远侧侄子w.r为红;旋转后可以一次平衡黑高
                        w.color = x.parent.color; // 让W继承原P的颜色
                        x.parent.color = BLACK;   // P变黑
                        w.r.color = BLACK;        // 远侧侄子变黑
                        leftRotate(x.parent);     // 对P左旋
                        x = root;                 // 已修好,令x为根使while结束
                    }
                } else { // x在P的右侧,W在左;和上面的四种情况完全镜像
                    Node<K, V> w = x.parent.l;
                    if (w.color == RED) { // 镜像情况1:兄弟红
                        w.color = BLACK;
                        x.parent.color = RED;
                        rightRotate(x.parent); // 对P右旋
                        w = x.parent.l;
                    }
                    if (w.r.color == BLACK && w.l.color == BLACK) { // 镜像情况2
                        w.color = RED;
                        x = x.parent; // 缺黑向上转移
                    } else {
                        if (w.l.color == BLACK) { // 镜像情况3:远侧(左)黑,近侧(右)红
                            w.r.color = BLACK;
                            w.color = RED;
                            leftRotate(w); // 对W左旋,转化为镜像情况4
                            w = x.parent.l;
                        }
                        // 镜像情况4:远侧侄子w.l红,对P右旋并变色
                        w.color = x.parent.color;
                        x.parent.color = BLACK;
                        w.l.color = BLACK;
                        rightRotate(x.parent);
                        x = root;
                    }
                }
            }
            // x是红色就把它涂黑,补足黑高;若x是根也保持根黑。
            x.color = BLACK;
        }

        public int size() { return count; } // 不统计黑色NIL哨兵

        // 教学观察用:只记录最后一次put所触发的“最后一种旋转类型”,不是完整历史。
        public String lastInsertCase() { return lastInsertCase; }

        // 八、中序遍历:左 -> 根 -> 右。颜色不影响BST的升序输出。
        public void inOrder() {
            inOrder(root);
            System.out.println();
        }

        private void inOrder(Node<K, V> cur) {
            if (cur == NIL) return;
            inOrder(cur.l);
            System.out.print(cur.key + "=" + cur.value + "  ");
            inOrder(cur.r);
        }

        // 九、横向打印:先打印右子树,再打印根,最后打印左子树。
        // 因此控制台中的“上面”是右子树,“下面”是左子树;B黑、R红。
        public void printTree() { printTree(root, 0); }

        private void printTree(Node<K, V> cur, int depth) {
            if (cur == NIL) return;
            printTree(cur.r, depth + 1);
            System.out.println("    ".repeat(depth) + cur.key
                    + (cur.color == RED ? "(R)" : "(B)"));
            printTree(cur.l, depth + 1);
        }

        /* 十、校验整棵红黑树。下面逐条落实红黑树性质:
         * 1. NIL必须黑;根必须黑;根.parent必须是NIL。
         * 2. 每个节点key必须在(low,high)范围内,保证BST顺序。
         * 3. 红节点的两个孩子不能红;实际子节点的parent必须正确。
         * 4. 左右递归得到的黑高必须相同。
         * 5. 实际访问节点数必须等于count,避免漏节点/多节点。
         */
        public boolean isRedBlackTree() {
            if (NIL.color != BLACK || root == null) return false;
            if (root == NIL) return count == 0;
            if (root.color != BLACK || root.parent != NIL) return false;
            int[] visited = {0}; // 使用长度为1的数组让递归共享已访问节点数
            int blackHeight = check(root, null, null, visited);
            return blackHeight >= 0 && visited[0] == count;
        }

        /* check返回:合法时返回“从cur到任一NIL路径的黑高”;非法返回-1。
         * 例如:B -> R -> NIL的黑高为2(B和NIL各占1)。
         * low/high是祖先传下来的键值边界,不仅检查当前节点与直接孩子。
         */
        private int check(Node<K, V> cur, K low, K high, int[] visited) {
            if (cur == NIL) return 1; // 黑色NIL计入每条路径的黑高
            if (cur == null || cur.l == null || cur.r == null) return -1; // 不允许真null
            // BST合法范围:low < cur.key < high;等号也不合法(key不能重复)。
            if ((low != null && cur.key.compareTo(low) <= 0)
                    || (high != null && cur.key.compareTo(high) >= 0)) return -1;
            // 指针必须双向对应:父节点指向孩子,孩子也要指回父节点。
            if ((cur.l != NIL && cur.l.parent != cur)
                    || (cur.r != NIL && cur.r.parent != cur)) return -1;
            // 红色节点不能紧挨着红色孩子(红红冲突)。
            if (cur.color == RED && (cur.l.color == RED || cur.r.color == RED)) return -1;

            visited[0]++;
            int leftBH = check(cur.l, low, cur.key, visited);
            int rightBH = check(cur.r, cur.key, high, visited);
            // 左右黑高不等,说明有路径“少黑/多黑”,不是合法红黑树。
            if (leftBH < 0 || rightBH < 0 || leftBH != rightBH) return -1;
            // 本节点黑色就给黑高+1;红色不计入黑高。
            return leftBH + (cur.color == BLACK ? 1 : 0);
        }
    }

    public static void main(String[] args) {
        // 测试阅读建议:每次看到put或remove后,查看isRedBlackTree()是否为true。
        // main中的LL/RR/LR/RL是“四种插入修复”,不是四种删除修复。
        // 下方保留了之前的真实节点结构图与原有测试调用,一行逻辑也未删。
        /*
         * 普通测试:依次put(50,30,80,...,110),共15个不同的key。
         * 所有节点插入完毕后,实际形成的红黑树如下(B黑、R红;省略黑色NIL):
         *
         *                         50(B)
         *                       /       \
         *                   30(R)       80(R)
         *                  /    \       /    \
         *               15(B) 40(B)  60(B) 100(B)
         *              /  \   /  \   /  \   /   \
         *           10(R)20(R)35(R)45(R)55(R)65(R)90(R)110(R)
         *
         * 先查找,再插入42、更新40的value,最后依次删除10、40、50;
         * 每次关键操作都用isRedBlackTree()验证颜色、黑高和BST次序。
         */
        System.out.println("===== 普通插入、查找、删除 =====");
        RedBlackTreeMap<Integer, String> tree = new RedBlackTreeMap<>();
        int[] nums = {50, 30, 80, 15, 40, 60, 100, 10, 20, 35, 45, 55, 65, 90, 110};
        for (int n : nums) tree.put(n, "v" + n);
        System.out.print("中序:"); tree.inOrder();
        tree.printTree();
        System.out.println("节点数=" + tree.size() + ",红黑树=" + tree.isRedBlackTree());
        System.out.println("查找40=" + tree.get(40) + ",包含99=" + tree.containsKey(99));
        tree.put(42, "v42");
        tree.put(40, "更新的40");
        if (!tree.isRedBlackTree()) throw new AssertionError("普通插入错误");
        tree.remove(10); // 删除叶子节点
        if (!tree.isRedBlackTree()) throw new AssertionError("删除10错误");
        tree.remove(40); // 删除双孩子节点
        if (!tree.isRedBlackTree()) throw new AssertionError("删除40错误");
        tree.remove(50); // 删除根节点
        if (!tree.isRedBlackTree()) throw new AssertionError("删除50错误");
        System.out.print("删除后中序:"); tree.inOrder();
        System.out.println("节点数=" + tree.size() + ",红黑树=" + tree.isRedBlackTree());

        // 四组分别由put()构造7节点合法红黑树,最后插入第8个节点触发指定旋转。
        // 注意:四组都是“插入 -> 叔叔红先变色 -> 上层发生红红冲突 -> 旋转”。
        // 旋转瞬间的T1、T2、T3、T4都非空;图中(B)=黑色、(R)=红色、NIL省略。
        /*
         * 1. LL型:插入50,在更高一层对80右旋
         *
         * 插入前(7节点,符合红黑树性质):
         *                 80(B)
         *                /     \
         *             60(R)   90(B)
         *            /    \
         *         20(B)   70(B)
         *         /   \
         *      10(R)  40(R)
         *
         * 插入50(R)作为40的右孩子 -> 40(R)与50(R)冲突。
         * 叔叔10(R)为红:10、40变黑,20变红;20(R)又与60(R)冲突。
         * 此时祖父80(B),叔叔90(B),成为LL型:
         *                 80(B)
         *                /     \
         *             60(R)   90(B)
         *            /    \
         *         20(R)   70(B)
         *         /   \
         *      10(B)  40(B)
         *                \
         *                50(R)
         *
         * 对80右旋,并将60变黑、80变红,最终:
         *                   60(B)
         *                 /       \
         *             20(R)      80(R)
         *            /   \      /   \
         *         10(B) 40(B)  70(B) 90(B)
         *                  \
         *                  50(R)
         */
        System.out.println("\n===== LL:插入50,右旋 =====");
        RedBlackTreeMap<Integer, Integer> ll = new RedBlackTreeMap<>();
        for (int n : new int[]{70, 80, 90, 60, 20, 40, 10}) ll.put(n, n);
        System.out.println("插入前:"); ll.printTree();
        if (!ll.isRedBlackTree()) throw new AssertionError("LL初始树错误");
        ll.put(50, 50);
        System.out.println("触发=" + ll.lastInsertCase() + ",红黑树=" + ll.isRedBlackTree());
        if (!"LL".equals(ll.lastInsertCase()) || !ll.isRedBlackTree()) throw new AssertionError("LL错误");
        ll.printTree();
        System.out.print("中序:"); ll.inOrder();
        System.out.println("查找40=" + ll.containsKey(40));
        ll.put(55, 55);
        if (!ll.isRedBlackTree()) throw new AssertionError("LL插入错误");
        ll.remove(10);
        if (!ll.isRedBlackTree()) throw new AssertionError("LL删除10错误");
        ll.remove(70);
        if (!ll.isRedBlackTree()) throw new AssertionError("LL删除70错误");
        System.out.println("操作后节点数=" + ll.size() + ",红黑树=" + ll.isRedBlackTree());

        /*
         * 2. RR型:插入90,在更高一层对20左旋
         *
         * 插入前(7节点,符合红黑树性质):
         *                  20(B)
         *                 /     \
         *              10(B)   50(R)
         *                     /     \
         *                  40(B)   70(B)
         *                         /    \
         *                      60(R)   80(R)
         *
         * 插入90(R)作为80的右孩子 -> 80(R)与90(R)冲突。
         * 叔叔60(R)为红:60、80变黑,70变红;70(R)又与50(R)冲突。
         * 此时祖父20(B),叔叔10(B),成为RR型:
         *                  20(B)
         *                 /     \
         *              10(B)   50(R)
         *                     /     \
         *                  40(B)   70(R)
         *                         /    \
         *                      60(B)   80(B)
         *                                  \
         *                                  90(R)
         *
         * 对20左旋,并将50变黑、20变红,最终:
         *                     50(B)
         *                   /       \
         *               20(R)      70(R)
         *              /   \      /   \
         *           10(B) 40(B)  60(B) 80(B)
         *                                    \
         *                                    90(R)
         */
        System.out.println("\n===== RR:插入90,左旋 =====");
        RedBlackTreeMap<Integer, Integer> rr = new RedBlackTreeMap<>();
        for (int n : new int[]{50, 20, 10, 40, 70, 80, 60}) rr.put(n, n);
        System.out.println("插入前:"); rr.printTree();
        if (!rr.isRedBlackTree()) throw new AssertionError("RR初始树错误");
        rr.put(90, 90);
        System.out.println("触发=" + rr.lastInsertCase() + ",红黑树=" + rr.isRedBlackTree());
        if (!"RR".equals(rr.lastInsertCase()) || !rr.isRedBlackTree()) throw new AssertionError("RR错误");
        rr.printTree();
        System.out.print("中序:"); rr.inOrder();
        System.out.println("查找70=" + rr.containsKey(70));
        rr.put(65, 65);
        if (!rr.isRedBlackTree()) throw new AssertionError("RR插入错误");
        rr.remove(10);
        if (!rr.isRedBlackTree()) throw new AssertionError("RR删除10错误");
        rr.remove(50);
        if (!rr.isRedBlackTree()) throw new AssertionError("RR删除50错误");
        System.out.println("操作后节点数=" + rr.size() + ",红黑树=" + rr.isRedBlackTree());

        /*
         * 3. LR型:插入30,先对20左旋,再对80右旋
         *
         * 插入前(7节点,符合红黑树性质):
         *                    80(B)
         *                   /     \
         *                20(R)   90(B)
         *               /     \
         *            10(B)   60(B)
         *                   /    \
         *                50(R)  70(R)
         *
         * 插入30(R)作为50的左孩子 -> 50(R)与30(R)冲突。
         * 叔叔70(R)为红:50、70变黑,60变红;60(R)又与20(R)冲突。
         * 此时祖父80(B),叔叔90(B),成为LR型:
         *                    80(B)
         *                   /     \
         *                20(R)   90(B)
         *               /     \
         *            10(B)   60(R)
         *                   /    \
         *                50(B)  70(B)
         *                /
         *             30(R)
         *
         * 对20左旋,再对80右旋;60变黑、80变红,最终:
         *                    60(B)
         *                  /       \
         *               20(R)     80(R)
         *              /   \     /   \
         *           10(B) 50(B) 70(B) 90(B)
         *                 /
         *              30(R)
         */
        System.out.println("\n===== LR:插入30,先左旋再右旋 =====");
        RedBlackTreeMap<Integer, Integer> lr = new RedBlackTreeMap<>();
        for (int n : new int[]{90, 80, 10, 20, 70, 50, 60}) lr.put(n, n);
        System.out.println("插入前:"); lr.printTree();
        if (!lr.isRedBlackTree()) throw new AssertionError("LR初始树错误");
        lr.put(30, 30);
        System.out.println("触发=" + lr.lastInsertCase() + ",红黑树=" + lr.isRedBlackTree());
        if (!"LR".equals(lr.lastInsertCase()) || !lr.isRedBlackTree()) throw new AssertionError("LR错误");
        lr.printTree();
        System.out.print("中序:"); lr.inOrder();
        System.out.println("查找70=" + lr.containsKey(70));
        lr.put(35, 35);
        if (!lr.isRedBlackTree()) throw new AssertionError("LR插入错误");
        lr.remove(10);
        if (!lr.isRedBlackTree()) throw new AssertionError("LR删除10错误");
        lr.remove(90);
        if (!lr.isRedBlackTree()) throw new AssertionError("LR删除90错误");
        System.out.println("操作后节点数=" + lr.size() + ",红黑树=" + lr.isRedBlackTree());

        /*
         * 4. RL型:插入70,先对80右旋,再对30左旋
         *
         * 插入前(7节点,符合红黑树性质):
         *                  30(B)
         *                 /     \
         *              20(B)   80(R)
         *                     /     \
         *                  50(B)   90(B)
         *                 /    \
         *              40(R)  60(R)
         *
         * 插入70(R)作为60的右孩子 -> 60(R)与70(R)冲突。
         * 叔叔40(R)为红:40、60变黑,50变红;50(R)又与80(R)冲突。
         * 此时祖父30(B),叔叔20(B),成为RL型:
         *                  30(B)
         *                 /     \
         *              20(B)   80(R)
         *                     /     \
         *                  50(R)   90(B)
         *                 /    \
         *              40(B)  60(B)
         *                       \
         *                       70(R)
         *
         * 对80右旋,再对30左旋;50变黑、30变红,最终:
         *                     50(B)
         *                   /       \
         *               30(R)      80(R)
         *              /   \      /   \
         *           20(B) 40(B)  60(B) 90(B)
         *                            \
         *                            70(R)
         */
        System.out.println("\n===== RL:插入70,先右旋再左旋 =====");
        RedBlackTreeMap<Integer, Integer> rl = new RedBlackTreeMap<>();
        for (int n : new int[]{20, 30, 80, 90, 60, 50, 40}) rl.put(n, n);
        System.out.println("插入前:"); rl.printTree();
        if (!rl.isRedBlackTree()) throw new AssertionError("RL初始树错误");
        rl.put(70, 70);
        System.out.println("触发=" + rl.lastInsertCase() + ",红黑树=" + rl.isRedBlackTree());
        if (!"RL".equals(rl.lastInsertCase()) || !rl.isRedBlackTree()) throw new AssertionError("RL错误");
        rl.printTree();
        System.out.print("中序:"); rl.inOrder();
        System.out.println("查找60=" + rl.containsKey(60));
        rl.put(75, 75);
        if (!rl.isRedBlackTree()) throw new AssertionError("RL插入错误");
        rl.remove(20);
        if (!rl.isRedBlackTree()) throw new AssertionError("RL删除20错误");
        rl.remove(80);
        if (!rl.isRedBlackTree()) throw new AssertionError("RL删除80错误");
        System.out.println("操作后节点数=" + rl.size() + ",红黑树=" + rl.isRedBlackTree());

        System.out.println("\nLL、RR、LR、RL四种插入调整全部通过");
    }
}

运行结果:

===== 普通插入、查找、删除 =====
中序:10=v10  15=v15  20=v20  30=v30  35=v35  40=v40  45=v45  50=v50  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
            110(R)
        100(B)
            90(R)
    80(R)
            65(R)
        60(B)
            55(R)
50(B)
            45(R)
        40(B)
            35(R)
    30(R)
            20(R)
        15(B)
            10(R)
节点数=15,红黑树=true
查找40=v40,包含99=false
删除后中序:15=v15  20=v20  30=v30  35=v35  42=v42  45=v45  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
节点数=13,红黑树=true

===== LL:插入50,右旋 =====
插入前:
    90(B)
80(B)
        70(B)
    60(R)
            40(R)
        20(B)
            10(R)
触发=LL,红黑树=true
        90(B)
    80(R)
        70(B)
60(B)
            50(R)
        40(B)
    20(R)
        10(B)
中序:10=10  20=20  40=40  50=50  60=60  70=70  80=80  90=90  
查找40=true
操作后节点数=7,红黑树=true

===== RR:插入90,左旋 =====
插入前:
            80(R)
        70(B)
            60(R)
    50(R)
        40(B)
20(B)
    10(B)
触发=RR,红黑树=true
            90(R)
        80(B)
    70(R)
        60(B)
50(B)
        40(B)
    20(R)
        10(B)
中序:10=10  20=20  40=40  50=50  60=60  70=70  80=80  90=90  
查找70=true
操作后节点数=7,红黑树=true

===== LR:插入30,先左旋再右旋 =====
插入前:
    90(B)
80(B)
            70(R)
        60(B)
            50(R)
    20(R)
        10(B)
触发=LR,红黑树=true
        90(B)
    80(R)
        70(B)
60(B)
        50(B)
            30(R)
    20(R)
        10(B)
中序:10=10  20=20  30=30  50=50  60=60  70=70  80=80  90=90  
查找70=true
操作后节点数=7,红黑树=true

===== RL:插入70,先右旋再左旋 =====
插入前:
        90(B)
    80(R)
            60(R)
        50(B)
            40(R)
30(B)
    20(B)
触发=RL,红黑树=true
        90(B)
    80(R)
            70(R)
        60(B)
50(B)
        40(B)
    30(R)
        20(B)
中序:20=20  30=30  40=40  50=50  60=60  70=70  80=80  90=90  
查找60=true
操作后节点数=7,红黑树=true

LL、RR、LR、RL四种插入调整全部通过

(五)跳表

描述:跳表中的外部结构可以看成一个有序表,但是它内部是许多指针组成的有序表,可以视为多链表。

跳表(Skip List)是一种基于有序链表和多层索引实现的数据结构,可以完成键值对的插入、删除和查找。

SkipListMap<K,V> 是跳表实现的有序映射结构,功能与之前学习的 AVLTreeMap、SizeBalancedTreeMap 和 RedBlackTreeMap 类似。

它们的主要区别在于:AVL树、SB树和红黑树通过旋转维持树的平衡,而跳表通过随机生成多层索引,提高查找效率。

1.为什么需要跳表?

假设一个普通有序链表如下:

HEAD → 10 → 20 → 30 → 40 → 50 → 60 → 70 → null

如果要查找 60,只能从头节点开始逐个比较:

10 → 20 → 30 → 40 → 50 → 60

普通链表无法像数组一样通过下标随机访问,因此即使元素有序,也不能直接进行二分查找。

最坏情况下,需要遍历全部 \(N\) 个节点,时间复杂度为:

O(N) 

跳表的核心思想是:在原有链表上方建立若干层稀疏索引,让查找过程能够跳过大量不必要的节点。

2.跳表的基本结构

假设跳表中存储了7个有序元素:

第2层:HEAD --------→ 20 ------------→ 50 ------------→ null
                       |                |
第1层:HEAD --------→ 20 ----→ 40 ----→ 50 ----→ 70 ----→ null
                       |        |        |        |
第0层:HEAD → 10 ----→ 20 → 30 → 40 ----→ 50 → 60 → 70 → null

其中:

  • 第0层包含所有节点,是完整的有序链表。

  • 第1层包含部分节点,作为第0层的索引。

  • 第2层包含更少的节点,作为更高层的索引。

图中竖线表示同一个节点在不同层的索引关系;实际实现通常使用一个节点保存多个 next 指针,而不是复制多个键值节点。

例如节点50出现在第0、1、2层,因此它具有3个向后指针;节点30只出现在第0层,因此只有1个向后指针。

为什么跳表能够加快查找?

假设需要查找 60。

从最高层开始:

第2层:HEAD → 20 → 50
                     ↓
第1层:              50 → 70(超过60,不前进)
                     ↓
第0层:              50 → 60(找到)

查找规则是:能向右就向右,下一节点超过目标值就向下,直到找到目标或到达第0层。

这与搜索二叉树根据节点大小决定向左、向右移动的思想很相似。

3.跳表的查找过程

对于 SkipListMap,节点需要根据 key 的大小进行比较。

假设查找:

map.get(60);

其基本过程为:

1. 从HEAD的最高层出发。
2. 如果右侧节点的key小于60,则向右移动。
3. 如果右侧节点的key大于或等于60,则向下一层。
4. 重复,直到第0层。
5. 如果找到key=60,返回对应的value;
   否则说明该key不存在。

对应的简化伪代码:

Node cur = head;

for (int level = maxLevel; level >= 0; level--) {
    while (cur.next[level] != null
            && cur.next[level].key.compareTo(key) < 0) {
        cur = cur.next[level];
    }
}

// 此时cur是第0层中小于key的最后一个节点
Node next = cur.next[0];

if (next != null && next.key.compareTo(key) == 0) {
    return next.value;
}

return null;

跳表查找的期望时间复杂度为 O(log N)。

4.插入节点时如何维护跳表?

与AVL树不同,跳表插入节点时不需要左旋或右旋。

例如,现在插入 35:

map.put(35, "v35");

首先根据查找过程,找到第0层中35的插入位置:

原来:
20 → 30 → 40 → 50

插入35:
20 → 30 → 35 → 40 → 50

接下来需要确定新节点能够出现在多少层。

随机生成索引层数

跳表通常使用随机方法决定新节点的层数。假设每次提升到下一层的概率为:p=1/2.那么节点具有不同层数的概率为:

节点拥有的层数

概率

1层

1/2

2层

1/4

3层

1/8

4层

1/16

k层

1/2^k

这里表示恰好拥有k层,假设不限制最大层数。

例如,本次随机结果决定节点35拥有2层,那么:

第2层:HEAD --------→ 20 ----------------→ 50
                       |                    |
第1层:HEAD --------→ 20 → 35 → 40 ------→ 50 → 70
                       |    |    |           |    |
第0层:HEAD → 10 ----→ 20 → 30 → 35 → 40 → 50 → 60 → 70

插入35之后,只需要修改第0、1层的相关指针,不需要改变其他节点的键值,也不需要对整张跳表进行旋转。

5.删除节点时如何维护跳表?

假设删除:

map.remove(40);

首先按照查找规则确定40在各层中的位置,然后修改相应的前驱节点指针。

删除前:

第1层:20 → 35 → 40 → 50
第0层:20 → 30 → 35 → 40 → 50


删除后:

第1层:20 → 35 ------→ 50
第0层:20 → 30 → 35 → 50

如果删除了最高层的最后一个索引节点,使最高层只剩下 HEAD,就可以相应降低跳表的最高层数。删除不需要重新随机生成层数,也不需要旋转。

6.SkipListMap在Java中的基本节点设计

跳表的节点通常需要保存键值和一个多层指针数组:

public static class SkipListNode<K extends Comparable<K>, V> {

    public K key;
    public V value;

    // nextNodes.get(0):第0层的下一个节点
    // nextNodes.get(1):第1层的下一个节点
    // ...
    public ArrayList<SkipListNode<K, V>> nextNodes;

    public SkipListNode(K key, V value) {
        this.key = key;
        this.value = value;
        nextNodes = new ArrayList<>();
    }
}

因此,一个节点在跳表中的层数,实际上由它拥有的 nextNodes 指针数量决定。

整个 SkipListMap 通常还需要维护:

private SkipListNode<K, V> head; // 头节点
private int maxLevel;             // 当前最高层
private int size;                 // 实际键值节点数量

核心操作包括 put()、get()、containsKey() 和 remove()。

7.跳表与AVL树、SB树、红黑树的区别

对比项

AVL树

SB树

红黑树

跳表

基本结构

二叉树

二叉树

二叉树

多层有序链表

平衡依据

高度差

子树节点数

颜色与黑高

随机索引层数

左右旋转

需要

需要

需要

不需要

查找复杂度

O(log N)

O(log N)

O(log N)

期望 O(log N)

插入复杂度

O(log N)

O(log N)

O(log N)

期望 O(log N)

删除复杂度

O(log N)

O(log N)

O(log N)

期望 O(log N)

最坏情况

O(log N)

O(log N)

O(log N)

O(N)

跳表采用概率平衡,因此不像AVL树、红黑树那样提供严格的最坏情况对数时间保证;但当随机层数分布合理时,其期望性能良好,通常实现也比较直观。

8.跳表的实现

package Class015;

import java.util.ArrayList;

public class Code_SkipListMap {

    public static class SkipListNode<K extends Comparable<K>,V>{
        //key用于排序,val是对应的值
        public K key;
        public V val;
        //nextNodes.get(i):当前节点在第i层的下一个节点
        public ArrayList<SkipListNode<K,V>>nextNodes;

        public SkipListNode(K k,V v){
            key=k;
            val=v;
            nextNodes=new ArrayList<>();
        }
        //头节点key为null,认为它小于所有实际key
        public boolean isKeyLess(K otherKey){
            return otherKey !=null&&(key==null||key.compareTo(otherKey)<0);
        }
        public boolean isKeyEqual(K otherKey){
            return (key==null&&otherKey==null)||(key !=null&&otherKey !=null &&key.compareTo(otherKey)==0);
        }
    }

    public static class SkipListMap<K extends Comparable<K>,V>{
        private static final double PROBABILITY=0.5;
        private SkipListNode<K,V>head;//头节点,不保存实际键值
        private int size;//实际节点数量(不包括head)
        private int maxLevel;//最高层编号,第0层保存全部节点

        public SkipListMap(){
            head=new SkipListNode<>(null,null);
            head.nextNodes.add(null);//初始化第0层
            size=0;
            maxLevel=0;
        }

        //在指定层,从cur开始找到key左侧最靠右的节点
        private SkipListNode<K,V>mostRightLessNodeInLevel(
                K key,SkipListNode<K,V>cur,int level){
            SkipListNode<K,V>next=cur.nextNodes.get(level);
            while (next!=null&&next.isKeyLess(key)){
                cur=next;
                next=cur.nextNodes.get(level);
            }
            return cur;
        }

        //从最高层向下查找,返回第0层中严格小于key的最后一个节点
        private SkipListNode<K,V>mostRightLessNodeInTree(K key){
            if (key==null){
                return null;
            }
            int level=maxLevel;
            SkipListNode<K,V>cur=head;
            while (level>=0){
                cur=mostRightLessNodeInLevel(key,cur,level--);
            }
            return cur;
        }

        //查找是否存在key
        public boolean containsKey(K key){
            if (key==null){
                return false;
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>next=less.nextNodes.get(0);
            return next!=null&&next.isKeyEqual(key);
        }

        //取得key对应的value,不存在则返回null
        public V get(K key){
            if (key==null){
                return null;
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>next=less.nextNodes.get(0);
            return next!=null&&next.isKeyEqual(key)?next.val:null;
        }
        //插入新key,或更新已经存在的key对应的value
        public void put(K key,V value){
            if (key==null){
                return;//null保留给head节点
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>next=less.nextNodes.get(0);
            if (next!=null&&next.isKeyEqual(key)){
                next.val=value;//更新已有key,不改变层数和size
                return;
            }

            //每次以0.5的概率继续向上一层
            int newLevel=0;
            while (Math.random()<PROBABILITY){
                newLevel++;
            }
            while (newLevel>maxLevel){
                head.nextNodes.add(null);
                maxLevel++;
            }

            SkipListNode<K,V>newNode=new SkipListNode<>(key,value);
            for (int i=0;i<=newLevel;i++){
                newNode.nextNodes.add(null);
            }
            //自顶向下找每层的插入位置,只连接新节点拥有的层
            SkipListNode<K,V>cur=head;
            for (int level=maxLevel;level>=0;level--){
                cur=mostRightLessNodeInLevel(key,cur,level);
                if (level<=newLevel){
                    newNode.nextNodes.set(level,cur.nextNodes.get(level));
                    cur.nextNodes.set(level,newNode);
                }

            }
            size++;
        }
        //删除key在所有层上的指针,再移除空的最高层
        public void remove(K key){
            if (key==null){
                return;
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>target=less.nextNodes.get(0);
            if (target==null||!target.isKeyEqual(key)){
                return;
            }
            SkipListNode<K,V>cur=head;
            for (int level=maxLevel;level>=0;level--){
                cur=mostRightLessNodeInLevel(key,cur,level);
                SkipListNode<K,V>next=cur.nextNodes.get(level);
                if (next!=null&&next.isKeyEqual(key)){
                    cur.nextNodes.set(level,next.nextNodes.get(level));
                }
            }
            size--;
            //第0层始终保留;更高层若为空则删除
            while (maxLevel>0&&head.nextNodes.get(maxLevel)==null){
                head.nextNodes.remove(maxLevel);
                maxLevel--;
            }
        }
        public int size(){
            return size;
        }
        public boolean isEmpty(){
            return size==0;
        }
        //最小key,第0层head的下一个节点
        public K firstKey(){
            SkipListNode<K,V>first=head.nextNodes.get(0);
            return first==null?null:first.key;
        }
        //最大key:从高层向右走到尽头,再逐层下降
        public K lastKey(){
            SkipListNode<K,V>cur=head;
            for (int level=maxLevel;level>=0;level--){
                while (cur.nextNodes.get(level)!=null){
                    cur=cur.nextNodes.get(level);
                }
            }
            return cur==head ?null:cur.key;
        }
        //<=key的最大键
        public K floorKey(K key){
            if (key==null){
                return null;
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>next=less.nextNodes.get(0);
            if (next!=null&&next.isKeyEqual(key)){
                return next.key;
            }
            return less==head?null:less.key;
        }
        //>=key的最小值
        public K ceilingKey(K key){
            if (key==null){
                return null;
            }
            SkipListNode<K,V>less=mostRightLessNodeInTree(key);
            SkipListNode<K,V>next=less.nextNodes.get(0);
            return next==null?null:next.key;
        }
        //第0层是完整的有序链表
        public void inOrder(){
            SkipListNode<K,V>cur=head.nextNodes.get(0);
            while (cur!=null){
                System.out.print(cur.key+"="+cur.val+"  ");
                cur=cur.nextNodes.get(0);
            }
            System.out.println();
        }

        //打印全部层;注意随机层数使得每次运行的结构可能不同
        public void printAllLevels(){
            for(int level=maxLevel;level>=0;level--){
                System.out.print("第"+level+"层:HEAD");
                SkipListNode<K,V>cur=head.nextNodes.get(level);
                while (cur!=null) {
                    System.out.print("->" + cur.key);
                    cur = cur.nextNodes.get(level);
                }
                System.out.println("->null");
            }
        }
    }
    public static void main(String[] args) {
        SkipListMap<Integer, String> map = new SkipListMap<>();
        int[] nums = {50, 30, 80, 15, 40, 60, 100,
                10, 20, 35, 45, 55, 65, 90, 110};

        System.out.println("===== 1. 插入15个节点 =====");
        for (int n : nums) {
            map.put(n, "v" + n);
        }
        map.printAllLevels();
        System.out.print("第0层中序:");
        map.inOrder();
        System.out.println("节点数=" + map.size());

        System.out.println("\n===== 2. 查找 =====");
        System.out.println("get(40)=" + map.get(40));
        System.out.println("containsKey(99)=" + map.containsKey(99));
        System.out.println("firstKey=" + map.firstKey()
                + ",lastKey=" + map.lastKey());
        System.out.println("floorKey(42)=" + map.floorKey(42)
                + ",ceilingKey(42)=" + map.ceilingKey(42));

        System.out.println("\n===== 3. 插入和更新 =====");
        map.put(42, "v42");
        map.put(40, "更新的40");
        System.out.print("第0层中序:");
        map.inOrder();
        System.out.println("节点数=" + map.size());

        System.out.println("\n===== 4. 删除 =====");
        map.remove(10);
        map.remove(40);
        map.remove(50);
        System.out.print("删除10、40、50后:");
        map.inOrder();
        map.printAllLevels();
        System.out.println("节点数=" + map.size());
    }
}

运行结果:

===== 1. 插入15个节点 =====
第5层:HEAD->80->null
第4层:HEAD->80->null
第3层:HEAD->80->null
第2层:HEAD->20->80->90->100->null
第1层:HEAD->10->20->30->35->40->55->60->80->90->100->110->null
第0层:HEAD->10->15->20->30->35->40->45->50->55->60->65->80->90->100->110->null
第0层中序:10=v10  15=v15  20=v20  30=v30  35=v35  40=v40  45=v45  50=v50  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
节点数=15

===== 2. 查找 =====
get(40)=v40
containsKey(99)=false
firstKey=10,lastKey=110
floorKey(42)=40,ceilingKey(42)=45

===== 3. 插入和更新 =====
第0层中序:10=v10  15=v15  20=v20  30=v30  35=v35  40=更新的40  42=v42  45=v45  50=v50  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
节点数=16

===== 4. 删除 =====
删除10、40、50后:15=v15  20=v20  30=v30  35=v35  42=v42  45=v45  55=v55  60=v60  65=v65  80=v80  90=v90  100=v100  110=v110  
第5层:HEAD->80->null
第4层:HEAD->80->null
第3层:HEAD->80->null
第2层:HEAD->20->80->90->100->null
第1层:HEAD->20->30->35->42->55->60->80->90->100->110->null
第0层:HEAD->15->20->30->35->42->45->55->60->65->80->90->100->110->null
节点数=13

(六)左神面经

1)aws,阿里云平台,调研这里面的技术文档,选择5-6个很火的技术研究,奠定技术概念15天。
2)开源对应产品,转化为5-6个对应的经典产品,花费两个月时间学会例子(简单搭建或简单代码,不用全部构建),达到别人问什么能够回答什么的程度。
3)编造业务,10天。调研产品,编造业务,比如只检查一个业务中使用的消息队列的技术和原理,应对求职者的需求。
 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值