(一)有序表
有序表的操作水平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天。调研产品,编造业务,比如只检查一个业务中使用的消息队列的技术和原理,应对求职者的需求。
本文系统讲解四种有序表实现结构:AVL树(基于高度平衡)、SB树(基于子树节点数平衡)、红黑树(基于颜色约束与旋转)和跳表(基于多层随机索引)。重点分析各自的平衡机制、LL/RR/LR/RL调整策略、插入删除修复逻辑,以及时间复杂度保障原理。强调四者同属O(logN)级有序映射结构,但在实现复杂度、最坏性能保证、并发友好性等方面存在本质差异。
5567

被折叠的 条评论
为什么被折叠?



