红黑树的删除_深入理解红黑树
前言
前面的文章已經介紹過二叉搜索樹,AVL樹,以及2-3Tree,今天我們再來學習一下二叉搜索樹里面的大佬,它就是紅黑樹。紅黑樹(英語:Red–black tree)是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,它在1972年由魯道夫·貝爾發明,被稱為"對稱二叉B樹",它現代的名字源于Leo J. Guibas和Robert Sedgewick于1978年寫的一篇論文,紅黑樹的結構復雜,但它的操作有著良好的最壞情況運行時間,并且在實踐中高效:它可以在O(logN)時間內完成查找,插入和刪除。
同樣是自平衡的二叉搜索樹的AVL樹,由于保持了嚴格的均衡策略,導致在插入和刪除頻繁時,性能會下降的比較厲害,而紅黑樹則是不強調嚴格的均衡性,所以在刪除和插入的時候,綜合性能要高于AVL樹,但查詢性能則略低于AVL樹,所以這也是紅黑樹為什么被廣泛應用的原因,因為你綜合性能比較優越。
紅黑樹的性質
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色為紅色或黑色。在符合二叉搜索樹的基礎上,對于任何有效的紅黑樹,為了保持均衡性,又增加了額外的定義也就是其性質,如下:
(1)根節點是黑色
(2)每個節點的顏色,只能黑色或者紅色
(3)所有的葉子節點都是黑色,注意葉子節點是null節點,不存儲數據,就是了為了補全節點用的,從這個定義來說紅黑樹是保持均衡性的滿二叉樹。
(4)每個紅色節點必須有兩個黑色的節點,翻譯成另一種意思就是,在紅黑樹里面不會出現兩個相鄰的紅色節點
(5)從任一節點出發到其葉子節點,都必須包含相同數目的黑色節點。
(6)在紅黑樹里面新加入的節點的顏色是紅色的,注意這一條性質,很多網上的資料都沒有說明。
下面看一個具體的圖示:
基于這些性質,才確保了紅黑樹是一個綜合性能高效的動態數據結構,二叉搜索樹的性能依賴于樹的高度,不平衡的二叉搜索樹的問題就在于,如果插入數據是有序的,那么二叉樹會退化成鏈表,從而導致時間復雜度變為O(N),試想一下現在有42億的數據,如果時間復雜度是O(N),那么搜索效率可想而知,但如果是均衡的二叉樹,那么查詢次數只需要大概32次便可以,這就是均衡的重要性。紅黑樹不是從根到葉子的最長的可能路徑不多于最短的可能路徑的兩倍長,結果是這個樹大致上是平衡的,為什么說最長路徑不多于最短路徑的兩倍,我們從性質4就能夠推導出來,如果每個紅色節點必須要有兩個黑色節點,那么同樣數量的紅色節點和黑色節點,紅色節點每一個都需要一個黑色子節點,這樣最壞情況下最長路徑的的長度剛好是最短的2倍。
第二個問題是為什么新加入的節點必須是紅色的,因為從性質5上來看,要保持相等黑色節點數量才能符合紅黑樹的性質,如果插入的節點默認是黑色的,那么一定會破壞紅黑樹的性質,而紅色則不一定,除非它的父節點也是紅色,這么看來紅色破壞性質的幾率更小。
第三個問題,我們可以思考下,紅黑樹可不可以全是由黑色節點組成。這個理論上是可以的,只有一個root節點,它的顏色肯定是黑色的,此外,理想情況下一顆完美二叉樹的也可以由全部黑色組成,但 紅黑樹的新增節點為紅色,所以想組成完美二叉樹基本不可能。
紅黑樹的平衡原理
不同于AVL樹在每一個節點上維持了高度字段輔以旋轉策略來維持平衡,紅黑樹在插入和刪除的時候,維持平衡的手段主要是:變色+旋轉
(一)插入操作
在紅黑樹上進行插入操作和刪除操作會導致不再匹配紅黑樹的性質。恢復紅黑樹的性質需要少量O(logN)的顏色變更(實際是非常快速的)和不超過三次樹旋轉(對于插入操作是兩次)。雖然插入和刪除很復雜,但操作時間仍可以保持為 O(logN)次。
情況一:只需要變色維持紅黑樹的性質
考慮這樣一種情況,新插入節點的父節點不是黑色節點,也不是root節點,并且它的叔叔節點是紅色,如下圖:
這種情況下,比較容易解決,因為新插入的節點默認是紅色的,所以出現了雙紅節點,違背了紅黑樹的性質,所以需要變色,解決手段是把父節點和叔叔節點變成黑色,然后爺爺節點變成紅色,接著從 爺爺節點出發,依次校驗修復直到根節點。
情況二:變色+旋轉
考慮這樣一種情況,新插入節點的父節點不是黑色節點,也不是root節點,并且它的叔叔節點是黑色,這種情況下處理就復雜了,不能簡單通過變色來處理了。
旋轉分四種情況:
case1:左左
這種情況下,新插入的節點和父節點都是左孩子,所以定義做左左case,解決策略是以爺爺節點右旋,然后變色。如下圖:
case2:左右
這種情況下,新插入的節點是右孩子,但父節點是左孩子,所以定義做左右case,解決策略是先以父節點左旋,然后以爺爺節點右旋,然后變色,如下圖:
case3:右右
這種情況下,新插入的節點是右孩子,且父節點也是右孩子,所以定義做右右case,解決策略是爺爺節點左旋,然后變色,如下圖:
case4:右左
這種情況下,新插入的節點是左孩子,但父節點也是右孩子,所以定義做右左case,解決策略是先以父節點右旋,然后以爺爺節點左旋,最后變色,如下圖:
這些是插入操作的維持均衡的手段,總體來說,相比AVL樹略復雜,下面我們看下刪除操作
(二)刪除操作
刪除操作相比插入操作更加復雜,對于二叉搜索樹來說,刪除場景分三種,刪除的節點無孩子節點,刪除的節點有一個孩子,刪除的節點有兩個孩子節點,但是在紅黑樹里面,每個葉子節點,都會定義其下面有兩個黑色的null節點,再加上顏色操作,考慮的場景就復雜了。
情況一:簡單的刪除場景
這種情況下,孩子節點和父親節點的顏色不一樣,我們直接刪除即可,如下圖:
情況二:復雜的場景
這種情況下,孩子節點和父節點都是黑色,稱為雙黑,前面說過黑色節點的刪除,畢然破壞紅黑樹的性質,所以要做修復:
case 1:兄弟節點是黑色,并且兄弟節點的有一個紅色孩子節點:如下圖:
右右場景:
右左場景:
case 2:兄弟節點是黑色,并且它的子節點都是黑色,需要通過變色來處理,并且遞歸處理到根節點:如下圖:
case 3: 兄弟節點是紅色,那么它的子節點肯定都是黑色,這種情況需要旋轉和變色來處理,如下圖:
紅黑樹的實現
這里用的是java語言,源碼如下:
public class RBTree<K extends Comparable<K>,V> {
private final static boolean RED = true;
private final static boolean BLACK = false;
private Node<K,V> root;
private String prettyString;
public void add(K key, V value) {
Node<K,V> current=root;
Node<K,V> parent=null;
if(root==null){
root=new Node(key,value,RED);
root.color=BLACK;
}else{
while (current.key!=null){
parent=current;
if(key.compareTo(current.key)>0){
current=current.right;
}else{
current=current.left;
}
}
//能到此處說明current.key=null
Node newNode=new Node(key,value,RED);
newNode.parent=parent;//parent賦值
if(key.compareTo(parent.key)<0){
parent.left =newNode;//左孩子
}else if(key.compareTo(parent.key)>0){
parent.right =newNode;//右孩子
}
//進行矯正
addFixTree(newNode);
}
}
public void addFixTree(Node target){
//新添加的節點都是紅色
if(target.parent.isBlack()){//父節點顏色是黑色,沒有違背任何紅黑樹性質,直接返回
return;
}else if(target.uncle().isRed()){
//如果叔叔節點是紅色,那么只需要變色即可=> 父節點和叔叔全變黑色,爺爺節點變黑色,然后
//從爺爺節點開始,重復此步驟,對整棵樹的可能修改的顏色進行校正
recolor(target);
}else if(target.uncle().isBlack()){
//如果叔叔節點的顏色是黑色,需要分四種情況做旋轉,這一點與AVL樹的情況類似
//1.左旋 2.右旋 3.左右旋 4.右左旋
//這個地方不需要判斷是否null
//left-left case
if(target.parent.isLeft()&&target.isLeft()){
leftLeftCase(target);//只右旋 10 7 18 5 3
}else if(target.parent.isLeft()&&target.isRight()){
leftRightCase(target);//先左旋,然后右旋 10 7 18 5 6
}else if(target.parent.isRight()&&target.isRight()){
rightRightCase(target);//只左旋 5 4 9 10 11
}else if(target.parent.isRight()&&target.isLeft()){
rightLeftCase(target);//先右旋,然后左旋 5 4 9 12 10
}
}
}
public void leftRightCase(Node target){
rotateLeft(target.parent);//左旋
rotateRight(target.parent);//右旋
target=target.left;
rotateColor(target);
}
public void rightLeftCase(Node target){
rotateRight(target.parent);//右旋
rotateLeft(target.parent);//左旋
target=target.right;
rotateColor(target);
}
public void leftLeftCase(Node target){
//左-左的情況,是需要右旋,右旋的節點是該節點的爺爺節點做為參照,具體見:https://www.geeksforgeeks.org/c-program-red-black-tree-insertion/
rotateRight(target.grandParent());
rotateColor(target);
}
public void rotateColor(Node target){
//變色
if(target.isRed()&&target.parent.isRed()){
target.parent.setBlack();//parent為黑,子節點為兩個紅
if(target.isLeft()){
target.parent.right.setRed();
}else{
target.parent.left.setRed();
}
root.parent=null;
}
}
public void rightRightCase(Node target){
rotateLeft(target.grandParent());
rotateColor(target);
}
/****
*
* @param p
*/
public void rotateRight(Node p){
if(p!=null) {
Node l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p; //設置parent節點
l.parent=p.parent;
if(p.isRoot()){//如果p是root
root=l;
}else if(p.isRight()){
p.parent.right=l;//如果p原來是父的右孩子,就得把新的l接到原來p.parent.right
}else {
p.parent.left=l;//如果p原來是父的左孩子,就得把新的l接到原來p.parent.right
}
l.right=p;//設置右孩子
p.parent=l;//設置父節點
}
}
public void rotateLeft(Node p){
if(p!=null){
Node r=p.right;
p.right=r.left;
if(r.left!=null){
r.left.parent=p;
}
r.parent=p.parent;
if(p.isRoot()){
root=r;
}else if(p.isLeft()){
p.parent.left=r;
}else {
p.parent.right=r;
}
r.left=p;
p.parent=r;
}
}
private void setRoot(Node target){
root=target;
if(target!=null){
root.setBlack();
}
}
public void recolor(Node target){
if(target.isRoot()){
target.setBlack();
return;
}
//進來該方法的targe的顏色一定是紅色的,所以不需要在判斷
//recolor方法會調用遞歸多次,需要需要判斷父節點是否為黑色,黑色不需要進行染色處理
if(target.parent.isBlack()){
return;
}
//走到這里targe.parent 肯定是紅色的
Node uncle=target.uncle();
//
if(uncle!=null && uncle.isRed()){
target.parent.setBlack();
uncle.setBlack();
Node grandParent=target.grandParent();
//能進到這個方法,肯定grandParent不為null,取uncle的時候判斷了
grandParent.setRed();
recolor(grandParent);//遞歸變色
}else {
//走到這里,說明是本身是紅色,父節點是紅色,叔叔為黑,連續的雙紅,需要做修正
addFixTree(target);
}
}
public void inorder(Node root) {
if (root.key == null) {
return;
}
inorder(root.left);
System.out.println(root.key);
inorder(root.right);
}
/***
* 根據key搜索指定節點
* @param k
* @return
*/
public Node<K,V> search(K k){
Node<K,V> p=root;
while (p.key!=null){
int cmp=k.compareTo(p.key);
if(cmp<0){
p=p.left;
}else if(cmp>0){
p=p.right;
}else {
return p;
}
}
return null;
}
public Node<K,V> successor(Node<K,V> t){
//找到右子樹里面找到最小的
Node<K,V> p=t.right;
while (p.left.key!=null){
p=p.left;
}
return p;
}
public void delete(K k){
Node<K,V> p=search(k);
if(p==null){ return;}
if(p.left.key!=null&&p.right.key!=null){//擁有2個孩子節點
Node<K,V> s=successor(p);//找到后繼
p.key=s.key; //改變p的key為s.key
p.data=s.data;//改變p的data為s.data
//注意上面是指針傳遞,所以p的內容已經被修改
p = s;//這里又把s.內存地址賦值給p,對p上一個的內容的不會產生影響
}
//獲取需要被替換掉的節點
Node<K,V> replacement=p.left.key!=null?p.left:p.right;
if(replacement!=null){
//去掉找到的p
replacement.parent=p.parent;
//連接p.parent和末尾的節點
if(p.parent==null){
root=replacement;
}else if(p.isLeft()){
p.parent.left=replacement;
}else{
p.parent.right=replacement;
}
//p節點的所有的引用置為null,方便gc
p.left=p.right=p.parent=null;
//如果刪除的是黑色節點,就會導致不平衡,所以需要修復
if(p.isBlack()){
fixAfterDeletion(replacement);
}
}else if(p.parent==null){
root=null;
}else {//沒有兩個孩子,只有單個孩子,直接用父的引用直接其后面的即可
if(p.isBlack()){
fixAfterDeletion(p);//刪掉的是黑色就得做均衡
}
if(p.parent!=null){
if(p.isLeft()){
p.parent.left=new Node<>();
}else if(p.isRight()){
p.parent.right=new Node<>();
}
p.parent=null;
}
}
}
private void fixAfterDeletion(Node<K,V> x){
while (x!=root&&x.isBlack()){
if(x.isLeft()){
Node<K,V> sib=x.parent.right;
if(sib.isRed()){//如果x的兄弟節點是紅色
sib.setBlack();//給x的兄弟設置成黑色
x.parent.setRed();//給他們的父節點設置成紅色
rotateLeft(x.parent);//左邊刪除了,所以左邊少節點,需要左旋
sib=x.parent.right;//新的兄弟節點
}
//如果兄弟節點的孩子都是黑色,需要將其設置成紅色
if(sib.left.isBlack()&&sib.right.isBlack()){
sib.setBlack();
x=x.parent;//繼續向上遍歷修復
}else {
if(sib.right.isBlack()){
//兄弟的右邊是黑色,左邊是紅色
sib.left.setBlack();//需要將其左邊設置黑色
sib.setRed();//sib父節點設置成紅色
rotateRight(sib);//右旋
sib=x.parent.right;
}
sib.color=x.parent.color;
x.parent.setBlack();
sib.right.setBlack();
rotateLeft(x.parent);
x=root;
}
}else{
//與if里面相反的邏輯
Node<K,V> sib=x.parent.left;
if(sib.isRed()){
sib.setBlack();
x.parent.setRed();
rotateRight(x.parent);
sib=x.parent.left;
}
if(sib.right.isBlack()&&sib.left.isBlack()){
sib.setRed();;
x = x.parent;
}else {
if(sib.left.isBlack()){
sib.right.setBlack();
sib.setRed();;
rotateLeft(sib);
sib=x.parent.left;
}
sib.color=x.parent.color;
x.parent.setBlack();
sib.left.setBlack();
rotateRight(x.parent);
x=root;
}
}
}
x.setBlack();
}
public static void main(String[] args) {
RBTree<Integer,Integer> rbTree=new RBTree();
// rbTree.add(30,5);
rbTree.add(20,4);
rbTree.add(10,9);
rbTree.add(30,10);
rbTree.add(25,10);
rbTree.add(35,10);
rbTree.delete(20);
rbTree.inorder(rbTree.root);
// System.out.println(rbTree.search(1));
}
public Object remove(Comparable key) {
return null;
}
public Object lookup(Comparable key) {
return null;
}
public String toPrettyString() {
return null;
}
class Node<K extends Comparable<K>,V>{
private K key;
private V data;
private Node<K,V> left;
private Node<K,V> right;
private Node<K,V> parent;
private boolean color;
public Node(){
this.key=null;
this.data=null;
this.color=BLACK;//新添加的Node的節點顏色為黑色
}
public Node(K key,V data,boolean color){
this.key=key;
this.data=data;
this.color=color;
this.left =new Node();
this.right =new Node();
}
public boolean hasRightChild(){
if(this.right !=null){ return true; }
return false;
}
public boolean isLeft(){
if(this.parent.left ==this) {return true;}
return false;
}
public boolean isRight(){
if(this.parent.right ==this) {return true;}
return false;
}
//找爺爺節點
public Node grandParent(){
if(parent!=null){
return parent.parent;
}
return null;
}
public boolean isRoot(){
return parent==null;
}
public boolean isBlack(){
return this.color==BLACK;
}
public boolean isRed(){
return this.color==RED;
}
public void setBlack(){
this.color=BLACK;
}
public void setRed(){
this.color=RED;
}
// 找叔叔節點
public Node uncle(){
Node grandParent=grandParent();
if(grandParent==null){
return null;
}else if(parent==grandParent.left){
return grandParent.right; //父節點是左,那么父節點的右邊是叔叔節點
}else {
return grandParent.left; // 父節點本身是右,那么父節點的左邊是叔叔節點
}
}
public int compareTo(Node<K,V> node){
return this.key.compareTo(node.key);
}
public String nodeColor(){
String color="";
if(this==null||this.color==BLACK){
color="B";
} else if(this.color==RED){
color="R";
}
return color;
}
@Override
public String toString() {
String retString="";
if(this.key==null){
retString ="nil";
}else{
retString=this.key+"="+nodeColor();
}
return retString;
}
}
}
紅黑樹 VS AVL樹
紅黑樹和AVL樹都是平衡二叉搜索樹里面最常見的兩種類型,都支持在O(logN)的時間內,完成插入,刪除和查詢操作,但由于紅黑樹的綜合性能要好于AVL樹,所以在實際應用中更加常見。
區別主要有兩點:
(1)對于搜索操作來說,AVL樹是嚴格均衡樹,其搜索性能要好于紅黑樹
(2)對于插入和刪除操作來說,紅黑樹的性能更好,它的旋轉次數更少,因為不需要維持嚴格的平衡。
此外還有一點需要了解,AVL樹在每個節點要存儲一個int類型的高度字段,而紅黑樹需要在每個節點存儲一個boolean類型的顏色標記,還有父節點的引用地址,所以從額外的空間復雜度的大小來說兩者基本持平。
總結
本文主要介紹了平衡二叉樹里面的紅黑樹的相關內容,紅黑樹是一種綜合效率非常好的動態數據結構,所以在實際應用中非常廣泛,比如Java里面的TreeMap和TreeSet都是基于紅黑樹實現的,紅黑樹的是一種近似平衡的二叉樹,其平均時間復雜度為O(logN),綜合性能處于logN和2logN之間,相比AVL樹更加適合插入和刪除頻繁的場景,由于維持平衡需要變色和旋轉來配合操作,所以導致紅黑樹的代碼實現相比AVL樹復雜了數倍,對于紅黑樹,我們的重點在于要理解維持平衡的原理和思想,掌握了這些就可以使得我們對于平衡二叉樹的認知更上一層樓。
歷史文章:
什么是2-3樹
什么是平衡二叉樹
什么是二叉搜索樹
總結
以上是生活随笔為你收集整理的红黑树的删除_深入理解红黑树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sigmoid函数解决溢出_梯度消失和梯
- 下一篇: 初识python教案青岛版八年级_青岛版