读书笔记-红黑树

791 阅读7分钟

前言

大家好,头回写博客,欢迎批评,以后我会尽量做到一个月2更,最近在重新温故算法。 今日提供读书笔记红黑树

目的

记录所学,温故知新

Java中对应的结构

TreeMap,以下是自己安装书中实现的原理,工作中应使用TreeMap

红黑树的定义

红黑树(Red Black Tree) 是一种自平衡二叉查找树.
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡

时间界限与特点

红黑树的插入,删除操作在最坏情况下花费

log N

红黑树是具有如下着色性质的二叉查找树:

  1. 每个节点要么着成红色,要么着成黑色
  2. 根是黑色的
  3. 如果一个节点是红色,那么他的子节点必须是黑色
  4. 从一个节点到一个null引用,每一条路径必须包含相同数目的黑色节点。

使用该着色法则,保证红黑树的高度最多为:

2*log (N+1)

插入操作

自底向上插入及遇到的问题

自底向上插入

如果新插入的项的父节点是黑色,那么插入结束,默认新插入的节点是红色的.

如果父亲是红色的,就有几种情形(每种都有对称形式,假设父亲是曾祖父的左儿子).

  • 父亲的兄弟是黑色,

如下图:

父亲的兄弟是黑色并且父亲的右儿子为黑色的调整示意图
父亲的兄弟是黑色的调整示意图

    • 曾祖父为黑色,当前节点为父亲的右儿子且为红色.

父亲的兄弟是黑色并且父亲的右儿子为红色
父亲的兄弟是黑色并且父亲的右儿子为红色

  • 父亲的兄弟是红色时, 需要进行上滤 如下图过程:

进行上滤的示例
进行上滤的示例

遇到的问题

上滤需要一个栈或者保持父链来实现,并且过程复杂.

自顶向下插入

概念:在向下的过程中如果看到一个节点current有两个红儿子,可将该节点呈红色,两个儿子变为黑色。

红黑树颜色翻转
红黑树颜色翻转

当current节点的父亲parent也是红色时候,进行适当的选择,以该方式向下进行插入操作屏蔽了X节点的兄弟节点也是红色的可能. 代码:

/**
	 * 自顶向下插入
	 */
	public void insert( AnyType item ){
		  nullNode.element=item;
		  current=parent=grand=header;
		  //自顶向下调整,避免插入的父节点和父节点的兄弟节点为黑色的情况,该情况复杂不利于恢复平衡信心.
		  while(compare(item,current)!=0){
			  great=grand;
			  grand=parent;
			  parent=current;
			  current=compare(item,current)<0?current.left:current.right;
			  if(current.left.color==RED&&current.right.color==RED){
				  handleReorientAfterInsert(item);
			  }
		  }
		 
		  if(current!=nullNode){//重复元素跳过
			  return;
		  }
		  //找到位置
		  //构建新的节点
		  current=new RedBlackNode<AnyType>(item,nullNode,nullNode);
		  //维护与父节点的关系
		  if(compare(item,parent)<0){
			  parent.left=current;
		  }else{
 			  parent.right=current;
		  }
		  //插入完成后,维护平衡信息
		  handleReorientAfterInsert(item);
		  nullNode.element=null;
	}
/**
	 * 插入后维护平衡信息
	 * @param item
	 */
	private void handleReorientAfterInsert(AnyType item) {
		//初步调整的变换颜色 自己变为红色,两个儿子变为红色
		current.color=RED;
		current.left.color=BLACK;
		current.right.color=BLACK;
		if(parent.color==RED){
			//调整后破坏了红黑树性质,需要旋转
			//分两种类型 一字形和之字形,之字形比一字形调整了多一步
			grand.color = RED;
			if((compare(item,grand)<0)!=(compare(item,parent)<0)){//之字形
				parent=rotate(item,grand);
				//调整parent和他的儿子,并将调整后的节点W设置成parent
			}
			//调整完成,重新设置当前节点
			current=rotate(item,great);
			//并将当前节点设置为黑色
			current.color=BLACK;
		}
		//保证根节点是黑色
		header.right.color=BLACK;
	}

自顶向下删除

  • 删除操作归结于可以删除红色的树叶;
  • 如果要删除的节点有右儿子,以右儿子的最小元内容替换要删除节点内容,之后删除右儿子最小元来进行删除。
  • 如果只有左儿子,以左儿子最大元内容替换要删除节点的内容,之后删除左儿子最大元
  • 如果要删除的节点没有儿子, 将该节点调整成红色,将父节点对应的引用设置成nullNode
  • 3.如果没有儿子
    • 若父节点为header,将树变为空树
    • 否则如果当前节点为黑色,进行调整,保证删除项为红色,之后将要删除项的父节点的引用设置为nullNode.

红色树叶删除简单,如果要删除的是黑色分为如下几种情:

  1. X与兄弟T的儿子都是黑色

    X与兄弟T的儿子都是黑色
    X与兄弟T的儿子都是黑色

  2. X的儿子是黑色,兄弟T有一个左儿子是红色

    X的儿子是黑色,兄弟T有一个左儿子是红色
    X的儿子是黑色,兄弟T有一个左儿子是红色

  3. X的儿子是黑色,兄弟T有一个右儿子是红色

    X的儿子是黑色,兄弟T有一个右儿子是红色
    X的儿子是黑色,兄弟T有一个右儿子是红色

  4. X的儿子是黑色,兄弟T儿子都是红色

    X的儿子是黑色,兄弟T儿子都是红色
    X的儿子是黑色,兄弟T儿子都是红色

以上每种情形都有与只对应的对称类型。如果X节点是红色,我们生产新的X,P,T向下探索 相关代码:

	/**
	 * 删除一个节点,
	 * 依据可以删除一个叶子,
	 * 自顶向下删除,
	 * 1如果要删除项有右儿子,先删除右儿子最小项,之后使用原右儿子的最小项内容替换要删除项的内容.
	 * 2.如果只有左儿子,先删除左儿子最大,之后使用左儿子的最大项替换要删除项的内容.
	 * 3.如果没有儿子
	 * 	若父节点为header,将树变为空树
	 *  否则如果当前节点为黑色,进行调整,保证删除项为红色,之后将要删除项的父节点的引用设置为nullNode.
	 * @param x
	 */
	public AnyType remove( AnyType x ){
		//需要自己尝试书写
		//先查找是否存在,存在后删除
		 RedBlackNode<AnyType>p=find(x);
		 RedBlackNode<AnyType>pParent=parent;
		 if (p == null){
	        return null;
		 }
		 AnyType item=p.element;
		 //自顶向下删除
		 //找到后,如果存在左儿子和右儿子(或 只有右儿子),
		 //使用右儿子的最小,替换当前 ,之后删除右儿子最小
		 //只有左儿子使用左儿子最大替换,
		 RedBlackNode<AnyType>replacement=findReplaceMent(p);
		 if(replacement!=null){
			 //进行替换
			 p.element=remove(replacement.element);

		 }else{
			 //没有替换者,
			 if(pParent==header){
				makeEmpty();
			 }else{
				 if(p.color==BLACK){
					 //将p地调整为红色
					 fixbeforedelete(p.element) ;
					 pParent=parent;
				 }
				//调整为删除
				 if(pParent.left==p){
					pParent.left=nullNode;
				  }else if(pParent.right==p){
					pParent.right=nullNode;
			      }

			 }
		 }
		 current=p;
		 parent=pParent;
		 return item;
	}
	
	/**
	 * 删除前调整数的平衡信息,保证要删除的项是红色
	 * @param item
	 */
	private void fixbeforedelete(AnyType item) {
		grand=header;
		RedBlackNode<AnyType>p=header.right;
		RedBlackNode<AnyType>x=nullNode;
		RedBlackNode<AnyType>t=nullNode;
		RedBlackNode<AnyType>i=find(item);
		//先把p涂成红色,最后恢复
		p.color=RED;
		x=item.compareTo(p.element)<=0?p.left:p.right;
		t=item.compareTo(p.element)<=0?p.right:p.left;
		//保证要删除的项是红色
		while(i.color!=RED){
			if(x.color==RED||
				(x.color==BLACK&&(x.left.color==RED&&x.right.color==RED)||
				t.color==BLACK&&(x.left.color==RED||x.right.color==RED))	
			){
				//x为红色或x儿子为红色,x为黑色&&t为黑色,x有一个儿子为红色,向下探索
				grand=p;
				p=x;
				x=item.compareTo(p.element)<0?p.left:p.right;
				t=item.compareTo(p.element)<0?p.right:p.left;
			}else if(x.color==BLACK&&t.color==BLACK
					&&x.right.color==BLACK&&x.left.color==BLACK){
				//3中情况需要,调整的情况
				if(t.left.color==BLACK&&t.right.color==BLACK){
					//t的两个儿子,直接变换p和t,x的颜色,重新再该位置下探
					p.color=BLACK;
					t.color=RED;
					x.color=RED;
				}else if(t.left.color==RED&&t.right.color==RED){
					//t有两个红色的儿子,调整后下探
					if(p.right==t){
						RedBlackNode<AnyType>red=t.left;
						p.right=red.left;
						t.left=red.right;
						red.right=t;
						red.left=p;	
						//更新祖父节点
						if(grand.left==p){
							grand.left=red;
						}else{
							grand.right=red;
						}
						grand=red;
						p.color=BLACK;
						x.color=RED;
						t=p.right;	
					}else{
						RedBlackNode<AnyType>red=t.right;
						p.left=red.right;
						t.right=red.left;
						red.right=p;
						red.left=t;
						if(grand.left==p){
							grand.left=red;
						}else{
							grand.right=red;
						}
						grand=red;
						p.color=BLACK;
						x.color=RED;
						t=p.left;
					}
				}else if(p.right==t&&t.left.color==RED){
					//右左,之字调整后继续下探
					RedBlackNode<AnyType>red=t.left;
					p.right=red.left;
					t.left=red.right;
					red.right=t;
					red.left=p;	
					if(grand.left==p){
						grand.left=red;
					}else{
						grand.right=red;
					}
					grand=red;
					p.color=BLACK;
					x.color=RED;
					t=p.right;
				}else if(p.left==t&&(t.right.color==RED)){
					//左右,之字调整后继续下探
					RedBlackNode<AnyType>red=t.right;
					p.left=red.right;
					t.right=red.left;
					red.right=p;
					red.left=t;
					if(grand.left==p){
						grand.left=red;
					}else{
						grand.right=red;
					}
					grand=red;
					p.color=BLACK;
					x.color=RED;
					t=p.left;
				}else if(p.right==t&&t.right.color==RED){
					//右右 一字,交换t和p
					p.right=t.left;
					t.left=p;
					if(grand.left==p){
						grand.left=t;
					}else{
						grand.right=t;
					}
					grand=t;
					t.color=RED;
					p.color=BLACK;
					t=p.right;
				}else if(p.left==t&&t.left.color==RED){
					//左左 一字 交换t和p
					p.left=t.right;
					t.right=p;
					if(grand.left==p){
						grand.left=t;
					}else{
						grand.right=t;
					}
					grand=t;
					t.color=RED;
					p.color=BLACK;
					t=p.left;
				}
		}else if(x.color==BLACK&&p.color==BLACK&&t.color==RED){
			//x的兄弟为黑色,x和x的父节点都是红色,调整t和p,保证p为红色后,继续下探
			if(p.left==x){
				p.right=t.left;
				t.left=p;
				if(grand.left==p){
					grand.left=t;
				}else{
					grand.right=t;
				}
				grand=t;
				t.color=BLACK;
				p.color=RED;
				t=p.right;
			}else{
				p.left=t.right;
				t.right=p;
				if(grand.left==p){
					grand.left=t;
				}else{
					grand.right=t;
				}
				grand=t;
				t.color=BLACK;
				p.color=RED;
				t=p.left;
			}
		}else if(header.right==p&&x.color==BLACK
				&&p.color==RED&&t.color==RED){
			p.color=BLACK;
		}
	}
		header.right.color=BLACK;
		parent=p;
}

总结

  1. 红黑树的操作在最坏情况下花费
log N
  1. 插入操作采用自顶向下操作保证要插入的节点的父亲是黑色。
  2. 删除操作采用自顶向下操作保证要删除的节点为红色。

红黑树完整代码地址

github.com/floor07/Dat…