手寫紅黑樹的簡單實現

基於《算法》一書的紅黑樹的插入和刪除。看過不一樣的教材,也有不一樣的實現方式,可是最終的結果也大體相同,感受這個比較容易理解,就採用這種的方式來進行簡單實現。java

定義樹節點的實體類型

private static final boolean RED = true;
private static final boolean BLACK = false;

/**
     * 紅黑樹的節點結構
     * 保存的值,左節點,右節點以及顏色(true爲紅色,false爲黑色)
     * 默認添加一個紅節點
     *
     * @param <E>
     */
static final class RedBlackTreeNode<E extends Comparable<E>> {

  E val;
  RedBlackTreeNode<E> left;
  RedBlackTreeNode<E> right;
  boolean color = RED;


  RedBlackTreeNode(E val) {
    this.val = val;
  }

}

這裏簡單的定義了一下紅黑樹,而且只有節點,並非map這樣的k-v結構。若是定義k-v結構到時比較的時候比較k便可。node

用了泛型,而且要支持比較(繼承自Comparable),否則沒法比較大小進行插入。算法

而後定義了一個值,左節點和右節點,而後顏色默認爲紅色。函數

再增長一個構造函數便可優化

定義公共方法

主要作的就是插入和刪除節點,爲了方便查看是否符合添加了一箇中序遍歷的打印方法this

public class RedBlackTree<E extends Comparable<E>> {

    RedBlackTreeNode<E> head;
    
  public void add(E e) {
    ...
  }
    
  
  public void remove(E e){
    ...
  }
  
  public void printTree(){
    ...
  }
  
}

定義這些公共方法來對外部調用,具體實現能夠放到私有方法中。debug

變換操做

在紅黑樹的變換中主要有三個:左旋,右旋,變色。接下來咱們就來實現這三個方法。3d

旋轉操做能夠保持紅黑樹的兩個重要性質:有序性和完美平衡性code

左旋

private RedBlackTreeNode<E> rotateLeft(RedBlackTreeNode<E> node) {
  //變換位置
  RedBlackTreeNode<E> result = node.right;
  node.right = result.left;
  result.left = node;
  //換色
  result.color = node.color;
  node.color = RED;
  return result;
}

當右節點爲紅色, 左節點爲空或者黑色時,須要進行左旋操做。blog

首先定義一個變量存儲右節點,而後將右節點的左節點做爲父節點(傳入參數)的右節點。這時與右節點(定義的變量)斷開了關聯。

而後將定義的變量(右節點)的左節點設置爲參數節點(左節點以前已經賦值到參數節點的右節點上)。

還需進行一步換色,將定義變量的顏色設置爲父節點的顏色(不影響上一級的操做),而後將父節點設置爲紅色。

將定義的變量做爲父節點返回。

右旋

private RedBlackTreeNode<E> rotateRight(RedBlackTreeNode<E> node) {
  //變換位置
  RedBlackTreeNode<E> result = node.left;
  node.left = result.right;
  result.right = node;
  //變色
  result.color = node.color;
  node.color = RED;
  return result;
}

當左節點爲紅色,左節點的左節點也爲紅色時,須要進行右旋操做。

這個與左旋基本相似,將左節點做爲父節點返回,而後對其餘節點也要確保不丟失,還有換色操做不能影響紅黑樹的特性。

換色

private void flipColor(RedBlackTreeNode<E> node) {
   node.left.color = BLACK;
   node.right.color = BLACK;
   node.color = RED;
}

當兩個子節點都爲紅色時,須要進行換色

讓兩個子節點變爲黑色,父節點變爲紅色

完成公共方法的實現

剛纔咱們在上面有提到,須要判斷節點的顏色,雖然咱們在節點的類型中定義了color屬性,可是考慮到其餘狀況仍是寫一個方法來完成判斷顏色的功能:

isRed(Node)

private boolean isRed(RedBlackTreeNode<E> node) {
   if (node == null) {
      return false;
   }
   return node.color;
}

當節點爲空時返回false即爲黑色,否則判斷節點的color屬性是否爲紅色。

還有一箇中序打印的方法

printTree()

public void printTree() {
   print(head);
}

private void print(RedBlackTreeNode<E> node) {
   if (node == null) {
      return;
   }
   print(node.left);
   System.out.print(node.val + " ");
   print(node.right);
}

在對外部的方法中調用了內部方法,傳入了頭結點。

因爲是中序遍歷,因此須要先遍歷左節點,而後打印本身,而後遍歷右節點。這是一個遞歸操做,因此須要定義終止條件:當節點爲空時就返回。

具體實現

add()

public void add(E val) throws IllegalAccessException {
   if (val == null) {
      throw new IllegalAccessException("不能添加null值");
   }
   head = addVal(val, head);
   //最終將根節點設置爲黑色
   head.color = BLACK;
}

private RedBlackTreeNode<E> addVal(E val, RedBlackTreeNode<E> node) {
        //達到最終的節點,若是爲空則新建一個紅色的節點
        if (node == null) {
            return new RedBlackTreeNode<E>(val);
        }
        if (val.compareTo(node.val) < 0) {
            //若是小,則左節點爲 新建節點返回的節點(可能會通過調整)
            node.left = addVal(val, node.left);
        } else if (val.compareTo(node.val) > 0) {
            //若是大,則右節點爲  新建節點後返回的節點(可能會通過調整)
            node.right = addVal(val, node.right);
        } else {
            //值相等
            return node;
        }
        //判斷平衡等操做
        if (isRed(node.right) && !isRed(node.left)) {
            //右節點爲紅色,左節點爲空或者黑色時須要進行左旋
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            //左節點爲紅色,左節點的左節點也爲紅色時,須要進行右旋
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            //當兩個子節點都爲紅色時,須要進行變色
            flipColor(node);
        }
        return node;
    }

​ 在公共方法中首先進行了一個參數校驗,若是爲空則沒法比較因此就拋出一個異常。

而後調用私有方法進行添加節點:傳入的參數爲要添加的值,樹的頭結點。

在私有方法中首先判斷了傳入的節點是否爲空,若是爲空則新建一個紅色節點返回。

當不爲空時進行大小判斷,判斷是添加在左子樹仍是右子樹上,而後遞歸調用當前方法,傳入要添加的值和左節點或右節點,若是相等則直接返回當期節點便可(不是map不用從新改變value)。而且添加後可能會進行調整,因此須要從新賦值。

接下來就是判斷是否符合紅黑樹的規定,而後進行左旋,右旋,變色等操做。這時也會進行從新調整,因此須要從新賦值。

操做完成後返回到公共方法中。

在公共方法中將頭結點的顏色設置爲黑色,保證紅黑樹的特性。

remove()

public void remove(E val) throws IllegalAccessException {
   if (val == null) {
      throw new IllegalAccessException("不容許null值操做");
   }
   if (head == null) {
      throw new IllegalAccessException("樹爲空");
   }
   head = removeVal(val, head);
}

private RedBlackTreeNode<E> removeVal(E val, RedBlackTreeNode<E> node) throws IllegalAccessException {
   if (node == null) {
      throw new IllegalAccessException("val not exist!");
   }
   if (val.compareTo(node.val) < 0) {
      node.left = removeVal(val, node.left);
   } else if (val.compareTo(node.val) > 0) {
      node.right = removeVal(val, node.right);
   } else {
      if (node.right != null) {
         node = getRightMinNode(node);
      } else if (node.left != null) {
         node = getLeftMaxNode(node);
      } else {
         node = null;
      }
   }
  //判斷平衡等操做
  if (node != null) {
            //判斷平衡等操做
            if (isRed(node.right) && !isRed(node.left)) {
                //右節點爲紅色,左節點爲空或者黑色時須要進行左旋
                node = rotateLeft(node);
            }
            if (isRed(node.left) && isRed(node.left.left)) {
                //左節點爲紅色,左節點的左節點也爲紅色時,須要進行右旋
                node = rotateRight(node);
            }
            if (isRed(node.left) && isRed(node.right)) {
                //當兩個子節點都爲紅色時,須要進行變色
                flipColor(node);
            }
        }
   return node;
}

在公共方法中進行參數校驗,若是刪除的是null,則拋出異常。

而後當樹爲空時也不能進行刪除操做。刪除操做也可能會進行結構修改,因此也須要進行從新賦值。

用參數與當前節點比較,若是小則遞歸傳入左節點,若是大則遞歸傳入右節點,當節點爲空時表示要刪除的節點再也不樹中,我在這裏是拋出了異常,可能有些不太穩當。

若是與當前節點相同,則刪除當前節點。這時就暴露了一個問題,噹噹前節點有子節點時若是進行刪除。其實這也分爲幾種狀況即上面代碼中的第20-26行:

  1. 當前節點無子節點,刪除當前節點即置爲null便可。

  2. 將右子節點的最小節點做爲當前節點的替代,而後刪除這個最小節點。

    /**
     * 獲取右側樹的最小節點
     *
     * @param node
     * @return
     */
    private RedBlackTreeNode<E> getRightMinNode(RedBlackTreeNode<E> node) {
       RedBlackTreeNode<E> parent = node.right;
       if (parent.left == null) {
          node.right = parent.right;
          return parent;
       }
       RedBlackTreeNode<E> result = parent.left;
       //可能有優化的地方
       while (result.left != null) {
          parent = parent.left;
          result = parent.left;
       }
       parent.left = null;
       return result;
    }
  3. 當右節點爲空時,找到左節點的最大值做爲當前節點的替代,而後刪除這個最大節點。

    private RedBlackTreeNode<E> getLeftMaxNode(RedBlackTreeNode<E> node) {
       RedBlackTreeNode<E> parent = node.left;
       if (parent.right == null) {
          node.right = parent.left;
          return parent;
       }
       RedBlackTreeNode<E> result = parent.right;
       while (result.right != null) {
          parent = parent.right;
          result = parent.right;
       }
       parent.right = null;
       return result;
    }

進行替換後,須要檢查是否符合紅黑樹的特性是否須要左旋,右旋,變色等操做。

驗證

public static void main(String[] args) throws IllegalAccessException {
   RedBlackTree<Integer> redBlackTree = new RedBlackTree<Integer>();
   redBlackTree.add(1);
   redBlackTree.add(3);
   redBlackTree.add(5);
   redBlackTree.add(7);
   redBlackTree.add(9);
   redBlackTree.add(2);
   redBlackTree.add(4);
   redBlackTree.printTree();
   System.out.println();
   redBlackTree.remove(2);
   redBlackTree.printTree();
   System.out.println();
   redBlackTree.remove(11);
}

首先咱們依次添加[1,3,5,7,9,2,4]。而後將樹打印,按照預期結果打印出的結果應該是順序的1~9。而後咱們刪除2節點,若是咱們將插入過程畫出來會發現若是刪除2,則會形成1,3兩個紅節點的鏈接,這不符合紅黑樹的規定,因此須要進行調整。而後再次進行打印查看結果是否爲有誤。

最後咱們刪除一個不存在的值,看它是否會報錯。

1 2 3 4 5 7 9 
1 3 4 5 7 9 
Exception in thread "main" java.lang.IllegalAccessException: val not exist!
    at RedBlackTree.removeVal(RedBlackTree.java:33)
    at RedBlackTree.removeVal(RedBlackTree.java:38)
    at RedBlackTree.removeVal(RedBlackTree.java:38)
    at RedBlackTree.removeVal(RedBlackTree.java:38)
    at RedBlackTree.remove(RedBlackTree.java:28)
    at Test.main(Test.java:21)

經過輸出能夠看出結果符合咱們的要求,而後也能夠經過debug的方法查看刪除2節點後的節點狀況發現與在草稿上手畫版一致。

給出一個剛纔插入的圖畫過程。

刪除2節點後的狀況

本文由博客一文多發平臺 OpenWrite 發佈! 博主郵箱:liunaijie1996@163.com,有問題能夠郵箱交流。

相關文章
相關標籤/搜索