一、二叉搜索树
你以为jdk1.8的HashMap仅由数组和链表组成?Too young too simple。你以为红黑树很难?其实就是升级版的二叉搜索树而已。啥,你连二叉搜索树都不知道?行吧老实往下看把。
1.1性质
二叉搜索树也叫二叉排序树,算法导论(第三版)中是这样定义的,对任意两个结点x,y,如果y是x的左子结点,则y.key≤x.key,如果y是x的右子结点,则y.key≥x.key。二叉搜索树包含4个属性:key、left、right、parent。
class Node {
Node p;//parent
Node l;//left
Node r;//right
int key;
}
(实际上每个叶子结点及根结点都有指向NIL结点的指针,这里为了简化就不介绍NIL结点了)
1.2遍历
三种常见的遍历方式代码其实都是一样的,区别在于操作结点的位置,想了解的朋友可以自行百度,此处不详细介绍。
//前序遍历
public static void preOrderTraversal(Node node){
if(node != null){
System.out.println(node);
preOrderTraversal(node.l);
preOrderTraversal(node.r);
}
}
//中序遍历
public static void inOrderTraversal(Node node){
if(node != null){
inOrderTraversal(node.l);
System.out.println(node);
inOrderTraversal(node.r);
}
}
//后序遍历
public static void postOrderTraversal(Node node){
if(node != null){
postOrderTraversal(node.l);
postOrderTraversal(node.r);
System.out.println(node);
}
}
1.3插入
虽然定义如此,但是当x.key==y.key时,常把结点y放在x的右结点,以下为一颗简单的二叉搜索树的插入方法。
//迭代插入子结点
public void add(int k){
if(this.key==0){
this.key=k;
return;
}
Node c=this,p;
while(true){
p=c;
if(k<c.key){
if((c=c.l)==null){
p.l=new Node(k,p);
return;
}
}else{
if((c=c.r)==null){
p.r=new Node(k,p);
return;
}
}
}
}
//递归插入子结点
public static void add(Node root,Node n){
if(root==null||root.key==0){
root=n;
return;
}
if(root.key>n.key){
if(root.l==null){
root.l=n;
return;
}else{
add(root.l,n);
}
}else{
if(root.r==null){
root.r=n;
return;
}else{
add(root.r,n);
}
}
}
1.4查找
//递归查找子结点
public Node treeSearch(int key){
return treeSearch(this,key);
}
public Node treeSearch(Node n,int key){
if(n.key==0||n.key==key)
return n;
if(n.key>key)
return treeSearch(n.l,key);
else
return treeSearch(n.r,key);
}
//迭代查找子结点
public Node treeSearch2(int key){
Node n=this;
while(n.key!=0&&n.key!=key){
if(n.key>key)
n=n.l;
else
n=n.r;
}
return n;
}
1.5删除
从一颗二叉搜索树删除一个结点y,基本可以概括为一下三种情况:
1.y没有孩子结点,直接删除并修改父结点指针。
2.y只有一个孩子,修改孩子的父结点指针和父结点的孩子指针。
3.y有两个孩子,寻找y的后继z(一定在y的右子树中),并让z占据树中y的位置。y原来的左子树成为z的新左子树,y原来的右子树部分成为z的新右子树。
public void delete(Node n){
Node min;
if(n.l==null)
transplant(n,n.r);
else if(n.r == null)
transplant(n,n.l);
else{
min=findMinimumNode(n.r);
System.out.println(min);
//如果n的右子树最小结点和n不是父子关系
if(min.p!=n){
//使最小结点替代n的位置
min.r=n.r;
n.r.p=min;
}
transplant(n,min);
min.l=n.l;
n.l.p=min;
min.p.l=null;
//如果删除的为根节点,重新定义根节点
if(this==n){
this.key=min.key;
this.l=min.l;
this.r=min.r;
}
}
}
//判断u是u.p的左结点还是右结点,并使u的另一侧结点后继
private void transplant(Node u, Node v) {
if(u.p==null){
return;
}
if(u==u.p.l)
u.p.l=v;
else
u.p.r=v;
if(v!=null)
v.p=u.p;
}
//找Node孩子中最小的结点
public Node findMinimumNode(Node node){
Node p=node;
for(Node n=node.l;n!=null;p=n,n=n.l){}
return p;
}
二、红黑树
2.1性质
红黑树是一颗二叉搜索树,每个结点有一个额外属性记录颜色,非黑即红。红黑树通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍。红黑二叉树包含5个属性:color、key、left、right、parent。
一颗红黑树是满足下面性质的二叉搜索树:
1.每个结点非黑即红。
2.根结点是黑色的。
3.每个叶结点(NIL)是黑色的。
4.红结点的子结点一定为黑色。
5.对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
注:
NIL结点:实际上就是空结点,当新结点存在默认初始值时,NIL结点可以提供快速初始化的作用。
哨兵结点:实际上,为了节省内存,所有叶结点和根节点都指向一个NIL,这个NIL也称作哨兵。
黑高:从某个结点x出发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数为该结点黑高。下图红黑树黑高为2。
class Node{
Node p;
Node l;
Node r;
boolean red;
Integer key;
public Node(int key){
this.key=key;
}
}
2.2旋转
红黑树在运行插入和删除时花费的时间为O(lgn),修改过后很有可能违反性质,为了维护性质必须修改指针结构,于是就有了旋转操作。下图介绍了左旋和右旋。
以左旋为例,需要更新指针的结点有4个,分别为x.p,x,y,y.l,对应上图为10,4,6,5。当且仅当x为跟结点时还需将根结点的指针指向y。
更新列表 | 更新前 | 更新后 |
---|---|---|
①x.p.l/x.p.r | x | y |
②x.p | - | y |
③x.r | y | y.l |
④y.p | x | x.p |
⑤y.l | - | x |
⑥y.l.p | y | x |
下图为左旋JAVA代码,正常情况一共更新6个指针。(为了节省时间以及方便理解,下面的代码默认NIL结点为null。)
private void leftRotate(Node root,Node x){
//x:Node(4),y:Node(6)
Node y=x.r;
//3.更新x的右结点
x.r=y.l;
if(y.l!=null)
//6.更新y的左结点的父结点
y.l.p=x;
//4.更新y的父指针
y.p=x.p;
//如果x为跟结点,更新y为新的跟结点
if(x.p==null)
root=y;
//如果x为左结点
else if(x==x.p.l)
//1.更新x的父结点的左结点
x.p.l=y;
//如果x为右结点
else
//1.更新x的父结点的右结点
x.p.r=y;
//5.更新y的左结点
y.l=x;
//2.更新x的父结点
x.p=y;
}
2.3插入
红黑树的结点插入操作较为复杂,相比二叉搜索树多了一个调色逻辑,这里用图解的方式介绍可能出现的三种情况。
代码如下:
//和二叉搜索树的插入一样,只是多了一个颜色变化
public void redBlackTreeInsert(Node root,Node z){
Node x=root;
Node y=null;
//获取z的父结点
while(x!=null){
y=x;
if(z.key<x.key)
x=x.l;
else
x=x.r;
}
z.p=y;
//将z的父结点指向z
if(y==null)
root=z;
else if(z.key<y.key)
y.l=z;
else
y.r=z;
z.red=true;
//调色
redBlackTreeInsertFixup(root,z);
}
private void redBlackTreeInsertFixup(Node root, Node z) {
while(z.p.red){
//如果z的父亲是一个左结点
if(z.p==z.p.p.l){
//获取z的叔结点
Node y=z.p.p.r;
//情况一,如果z的叔结点为红色
if(y.red){
//让z的父亲变黑
z.p.red=false;
//让z的叔叔变黑
y.red=false;
//让z的爷爷变红
z.p.p.red=true;
//让z=z的爷爷继续循环
z=z.p.p;
}else if(z==z.p.r){//情况二
z=z.p;
//左旋,保证黑高
leftRotate(root,z);
}else{//情况三
z.p.red=false;
z.p.p.red=true;
//右旋,保证黑高
rightRotate(root,z.p.p);
}
}else{//和上面类似
Node y=z.p.p.l;
if(y.red){
z.p.red=false;
y.red=false;
z.p.p.red=true;
z=z.p.p;
}else if(z==z.p.l){
z=z.p;
rightRotate(root,z);
}else{
z.p.red=false;
z.p.p.red=true;
leftRotate(root,z.p.p);
}
}
}
root.red=true;
}
2.4删除
红黑树的删除比较复杂,这里就偷下懒不写了,有机会补上- -!。
三、附录
这里推荐一个数据结构在线画图网站,包括红黑树,完全平衡二叉树等等,对学习数据结构十分有帮助。此网站由美国旧金山大学提供:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html。(此网站二叉搜索树删除逻辑和算法导论(第三版)中有所出入,但是符合性质,感兴趣的读者可以自行研究)