欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

圖解紅黑樹及Java進(jìn)行紅黑二叉樹遍歷的方法

 更新時間:2016年05月23日 09:46:12   作者:梁佳賓  
紅黑樹問題是各大計(jì)算機(jī)考研命題以及面試算法題目中的熱門,接下來我們?yōu)榇蠹覉D解紅黑樹及Java進(jìn)行紅黑二叉樹遍歷的方法,需要的朋友可以參考下

紅黑樹
紅黑樹是一種數(shù)據(jù)結(jié)構(gòu)與算法課堂上常常提到但又不會細(xì)講的樹,也是技術(shù)面試中經(jīng)常被問到的樹,然而無論是書上還是網(wǎng)上的資料,通常都比較刻板難以理解,能不能一種比較直觀的方式來理解紅黑樹呢?本文將以圖形的方式來解釋紅黑樹的插入與刪除操作。
對樹結(jié)構(gòu)的學(xué)習(xí)是一個遞進(jìn)的過程,我們通常所接觸的樹都是二叉樹,二叉樹簡單來說就是每個非葉子節(jié)點(diǎn)都有且只有兩個孩子,分別叫做左孩子和右孩子。二叉樹中有一類特殊的樹叫二叉查找樹,二叉查找樹是一種有序的樹,對于每個非葉子節(jié)點(diǎn),其左子樹的值都小于它,其右子樹的值都大于它。比二叉查找樹更進(jìn)一步的是二叉平衡樹,二叉平衡樹除了保證有序外,還能夠保持每個節(jié)點(diǎn)左右子樹的高度相差不超過1。常見的平衡樹有AVL樹,Treap,紅黑樹,伸展樹,等等。
紅黑樹是一種二叉查找樹,但在每個節(jié)點(diǎn)上增加一個存儲位表示節(jié)點(diǎn)的顏色,可以是RED或BLACK。通過對任何一條從根到葉子的路徑上各個節(jié)點(diǎn)著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。
紅黑樹滿足一下5個性質(zhì):

  • 每個節(jié)點(diǎn)是紅色或者黑色;
  • 根節(jié)點(diǎn)是黑色;
  • 每個葉子節(jié)點(diǎn)NIL是黑色;
  • 如果一個節(jié)點(diǎn)是紅色,則它的兩個孩子都是黑色;(每條路徑上不能有兩個連續(xù)的紅色節(jié)點(diǎn))
  • 任一節(jié)點(diǎn)到其所有子孫葉子節(jié)點(diǎn)NIL的路徑上包含相同數(shù)目的黑色節(jié)點(diǎn)。

注意,在紅黑樹中,把傳統(tǒng)二叉樹的葉子節(jié)點(diǎn)的孩子指向NIL,稱NIL為紅黑樹中的葉子節(jié)點(diǎn)。NIL節(jié)點(diǎn)中含有指向父節(jié)點(diǎn)的指針,這可能是需要把null改為NIL的原因。

一、插入操作
首先以二叉查找樹的插入方式(插入的新節(jié)點(diǎn)都在葉子節(jié)點(diǎn)處)插入新的節(jié)點(diǎn),并將其繪為紅色。然后再重繪其顏色或旋轉(zhuǎn)以保持紅黑樹的性質(zhì),調(diào)整分為以下三種情況:
1 新節(jié)點(diǎn)N沒有父節(jié)點(diǎn)(即位于根上)
將新節(jié)點(diǎn)N繪為黑色。

201652393817496.png (461×97)

2 新節(jié)點(diǎn)N的父節(jié)點(diǎn)P為黑色
不用調(diào)整。

201652393842590.png (630×162)

3 新節(jié)點(diǎn)N的父節(jié)點(diǎn)P為紅色
因?yàn)榧t黑樹不允許有兩個連續(xù)的紅色節(jié)點(diǎn)(性質(zhì)4),所以需要調(diào)整,根據(jù)N的叔父節(jié)點(diǎn)顏色分為兩種情況:(我們以 N的父節(jié)點(diǎn)P為左孩子為例,P為右孩子的情況類似,不再詳述)
3.1 新節(jié)點(diǎn)N的叔父節(jié)點(diǎn)U為紅色
將新節(jié)點(diǎn)N的父節(jié)點(diǎn)P和叔父節(jié)點(diǎn)U都繪為黑色,將其祖父節(jié)點(diǎn)G繪為紅色,這樣保證從G到每個null節(jié)點(diǎn)的路徑上所包含的黑色節(jié)點(diǎn)個數(shù)與原來保持一致。但由于我們把G變成了紅色,如果G的父親也是紅色,就可能導(dǎo)致連續(xù)兩個紅色節(jié)點(diǎn)(違反性質(zhì)4),所以,需要重新檢查G是否違反了紅黑樹性質(zhì)。

201652393927178.png (922×248)

3.2 新節(jié)點(diǎn)N的叔父節(jié)點(diǎn)U為黑色
若新節(jié)點(diǎn)N是其父節(jié)點(diǎn)P的左孩子:將其父節(jié)點(diǎn)P繪為黑色,祖父節(jié)點(diǎn)G繪為紅色,然后對G進(jìn)行一次右旋轉(zhuǎn)。

201652393947586.png (771×246)

若新節(jié)點(diǎn)N是其父節(jié)點(diǎn)P的右孩子:對其父節(jié)點(diǎn)進(jìn)行一次左旋轉(zhuǎn),問題轉(zhuǎn)化為左孩子的情況。

201652394009586.png (810×245)

二、刪除操作
《算法導(dǎo)論》和維基百科上的做法都是,當(dāng)刪除一個黑色節(jié)點(diǎn)D時,把D的黑色“下推”至其子節(jié)點(diǎn)C,也就是說C除了本身的顏色外多了一重額外的黑色,然后不斷把這重額外的黑色沿樹上移,直到碰到一個紅色節(jié)點(diǎn),把其變?yōu)楹谏员WC路徑上黑色節(jié)點(diǎn)數(shù)目不變,或者移到樹的根部,這樣所有路徑上的黑色節(jié)點(diǎn)數(shù)目都減一,保持相等。上移過程中可能需要旋轉(zhuǎn)和修改一些節(jié)點(diǎn)的顏色,以保證路徑上黑色節(jié)點(diǎn)數(shù)目不變。
這種做法可能有利于代碼的實(shí)現(xiàn)(可用迭代的方式),但卻不便于理解(個人認(rèn)為)。本著理解優(yōu)先的目的,我根據(jù)被刪除節(jié)點(diǎn)D的孩子是否為NIL做如下分類:
1 被刪除節(jié)點(diǎn)D的兩個孩子都是NIL
1.1 被刪除節(jié)點(diǎn)D是紅色
用NIL替換D即可。

201652394028190.png (616×162)

1.2 被刪除節(jié)點(diǎn)D是黑色(我們以D是左孩子為例)
1.2.1 被刪除節(jié)點(diǎn)D的兄弟節(jié)點(diǎn)B的兩個孩子都為NIL
將D的兄弟節(jié)點(diǎn)B繪為紅色,父節(jié)點(diǎn)P繪為黑色。

201652394046686.png (519×177)

圖中半紅半黑表示該節(jié)點(diǎn)可能為紅色,也可能為黑色。如果P原來是紅色,這樣修改后路徑上的黑色節(jié)點(diǎn)數(shù)目和刪除D之前一樣;如果P原來是黑色,那么刪除D后會導(dǎo)致路徑上黑色節(jié)點(diǎn)的數(shù)目比刪除前少了一個,所以還需繼續(xù)檢查經(jīng)過P的路徑上黑色節(jié)點(diǎn)數(shù)目的改變是否影響了紅黑樹的性質(zhì)。
1.2.2 被刪除節(jié)點(diǎn)D的兄弟節(jié)點(diǎn)B有一個孩子不為NIL
這個孩子一定是紅色的,否則從D的父節(jié)點(diǎn)到各個葉子節(jié)點(diǎn)的路徑上黑色節(jié)點(diǎn)的數(shù)目就會不等(違反性質(zhì)5)。
若這個孩子為右孩子,將B的這個右孩子繪為黑色,B繪為其父節(jié)點(diǎn)P原來的顏色,P繪為黑色,然后對P進(jìn)行一次左旋轉(zhuǎn)。

201652394103606.png (921×259)

若這個孩子為左孩子,將B的這個左孩子繪為黑色,B繪為紅色,然后對B進(jìn)行一次右旋轉(zhuǎn),問題轉(zhuǎn)化為右孩子的情況。

201652394118597.png (870×264)

1.2.3 被刪除節(jié)點(diǎn)D的兄弟節(jié)點(diǎn)B的兩個孩子都不為NIL
若B為紅色,則B的兩個孩子一定為黑色。將B繪為黑色,B的左孩子繪為紅色,然后對P進(jìn)行一次左旋轉(zhuǎn)。

201652394133085.png (958×259)

若B為黑色,則B的兩個孩子一定為紅色。將B的父節(jié)點(diǎn)P繪為黑色,B的右孩子繪為黑色,B繪為其父節(jié)點(diǎn)P原來的顏色,然后對P進(jìn)行一次左旋轉(zhuǎn)。

201652394149885.png (960×259)

2 被刪除節(jié)點(diǎn)D的兩個孩子都不是NIL
按照二叉查找樹刪除節(jié)點(diǎn)的方法找到D的后繼節(jié)點(diǎn)S,交換D和S的內(nèi)容(顏色保持不變),被刪除節(jié)點(diǎn)變?yōu)镾,如果S有不為NIL的節(jié)點(diǎn),那么繼續(xù)用S的后繼節(jié)點(diǎn)替換S,直到被刪除節(jié)點(diǎn)的兩個孩子都為NIL,問題轉(zhuǎn)化為被刪除節(jié)點(diǎn)D的兩個孩子都為NIL的情況。
3 被刪除節(jié)點(diǎn)D有一個孩子不是NIL
這個孩子C一定是紅色節(jié)點(diǎn),否則從D到各個NIL節(jié)點(diǎn)的路徑上的黑色節(jié)點(diǎn)數(shù)目就會不同(違反性質(zhì)5)。
交換D和C的內(nèi)容(顏色保持不變),被刪除節(jié)點(diǎn)變?yōu)镃,問題轉(zhuǎn)化為被刪除節(jié)點(diǎn)D的兩個孩子都為NIL的情況。

201652394222004.png (616×165)

二叉樹的遍歷
二叉樹的遍歷有三種:前序遍歷、中序遍歷和后序遍歷。每種遍歷的實(shí)現(xiàn)又有遞歸和迭代兩種,這篇文章我們來討論如何用比較優(yōu)雅的代碼來實(shí)現(xiàn)二叉樹的遍歷。
首先我來定義一個二叉樹的節(jié)點(diǎn):

public class TreeNode {
  int val;
  TreeNode left;
  TreeNode right;
  
  public TreeNode(int x) {
    val = x;
  }
}

 
一、前序遍歷(Preorder Traversal)
簡單來講,前序遍歷就是先訪問父節(jié)點(diǎn),再訪問左孩子,最后訪問右孩子,即以父、左、右的順序來遍歷。
遞歸實(shí)現(xiàn)非常簡單,代碼如下:

public class Solution {
  List<Integer> result = new ArrayList<Integer>();
  
  public List<Integer> preorderTraversal(TreeNode root) {
    dfs(root);
    return result;
  }
  
  private void dfs(TreeNode root) {
    if (root == null) {
      return;
    } 
    result.add(root.val);
    dfs(root.left);
    dfs(root.right);
  }
}

迭代實(shí)現(xiàn)需要借助一個棧,存儲沒被訪問的右節(jié)點(diǎn),代碼如下:

public class Solution {
 
  public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<Integer>();
    
    if (root == null) {
      return result;
    }
    
    Stack<TreeNode> stack = new Stack<TreeNode>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
      TreeNode curr = stack.pop();
      result.add(curr.val);
      
      if (curr.right != null) {
        stack.push(curr.right);
      }
      if (curr.left != null) {
        stack.push(curr.left);
      }
    }
    
    return result;
  }
}

 
二、中序遍歷(Inorder Traversal)
簡單來講,中序遍歷就是先訪問左孩子,再訪問父節(jié)點(diǎn),最后訪問右孩子,即以左、父、右的順序遍歷。
遞歸代碼也比較容易,如下所示:

public class Solution {
 
  public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<Integer>();
    recurse(root, result);
    return result;
  }
  
  private void recurse(TreeNode root, List<Integer> result) {
    if (root == null) return;
    recurse(root.left, result);
    result.add(root.val);
    recurse(root.right, result);
  }
}

上面這種寫法有別于前序遍歷的遞歸代碼,前序遍歷的遞歸我們使用了成員變量來存儲遍歷的結(jié)果,這里我們使用方法參數(shù)來保存遍歷的結(jié)果。兩種寫法都可以,喜歡哪種就使用哪種。
中序遍歷的迭代實(shí)現(xiàn)沒有前序遍歷那么簡單,雖然也需要借助一個棧,但迭代終止的條件卻有所不同。想象一下,對于一棵二叉樹,我們最先訪問的節(jié)點(diǎn)是其最左邊的節(jié)點(diǎn),我們當(dāng)然可以通過一個 while 循環(huán)到達(dá)其最左邊,可是當(dāng)我們回退時,我們?nèi)绾沃滥硞€節(jié)點(diǎn)的左孩子是否已經(jīng)訪問過了?我們使用一個 curr 變量記錄當(dāng)前訪問的節(jié)點(diǎn),當(dāng)我們把一棵子樹的右節(jié)點(diǎn)都訪問完畢時,我們就該回退該子樹的父節(jié)點(diǎn)了,而此時 curr 為 null,所以我們可以以此來區(qū)分一個節(jié)點(diǎn)的左子樹是否已被訪問過。代碼如下:

public class Solution {
  
  public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<Integer>();
    
    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode curr = root;
    
    while (curr != null || !stack.isEmpty()) {
      while (curr != null) {
        stack.push(curr);
        curr = curr.left;
      }
      curr = stack.pop();
      result.add(curr.val);
      
      curr = curr.right;
    }
    
    return result;
  }
}

 
三、后序遍歷(Postorder Traversal)
簡單來講,后序遍歷就是先訪問左孩子,在訪問右孩子,最后訪問父節(jié)點(diǎn), 即以左、右、父的順序遍歷。
仿照中序遍歷,可以很容易地寫出后序遍歷的遞歸實(shí)現(xiàn):

public class Solution {
 
  public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<Integer>();
    recurse(root, result);
    return result;
  }
  
  private void recurse(TreeNode root, List<Integer> result) {
    if (root == null) return;
    recurse(root.left, result);
    recurse(root.right, result);
    result.add(root.val);
  }
}

后序遍歷的迭代,也需要一個標(biāo)識要區(qū)分一個節(jié)點(diǎn)的左右孩子是否已經(jīng)訪問過了,如果沒有,則依次訪問其左右孩子,如果訪問過了,則訪問該節(jié)點(diǎn)。為此,我們用一個 pre 變量來表示上一個訪問的節(jié)點(diǎn),如果上一個訪問的節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)的左孩子或右孩子,那么說明當(dāng)前節(jié)點(diǎn)的左右孩子已經(jīng)訪問過了,那么就可以訪問該節(jié)點(diǎn)了,否則,則需要進(jìn)入左右孩子依次訪問。代碼如下:

public class Solution {
 
  public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<Integer>();
    
    Stack<TreeNode> stack = new Stack<TreeNode>();
    if (root != null) stack.push(root);
    
    TreeNode pre = root;
    
    while (!stack.isEmpty()) {
      TreeNode curr = stack.peek();
      if (curr.left == pre || curr.right == pre || (curr.left == null && curr.right == null)) {
        result.add(curr.val);
        stack.pop();
        pre = curr;
      } else {
        if (curr.right != null) stack.push(curr.right);
        if (curr.left != null) stack.push(curr.left);
      }
    }
    
    return result;
  }
}

后序遍歷的迭代還有另外一種比較簡單的實(shí)現(xiàn),我們知道先序遍歷的順序是父、左、右,而后序遍歷的順序是左、右、父,那么如果我們把先序遍歷稍作修改,改成父、右、左的順序,那么就剛好與后序遍歷的順序相反了,以如此順序訪問完,最后我們對訪問結(jié)果做個反轉(zhuǎn)就可以了。而先序遍歷的迭代實(shí)現(xiàn)相對來說比較容易,仿照上面寫法我們可以如下實(shí)現(xiàn):

public class Solution {
 
  public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<Integer>();
    
    Stack<TreeNode> stack = new Stack<TreeNode>();
    if (root != null) stack.push(root);
    
    while (!stack.isEmpty()) {
      TreeNode curr = stack.pop();
      result.add(curr.val);
      if (curr.left != null) stack.push(curr.left);
      if (curr.right != null) stack.push(curr.right);
    }
 
    Collections.reverse(result);
    
    return result;
  }
}

 
四、總結(jié)
三種遍歷的遞歸實(shí)現(xiàn)都很容易。前序遍歷的迭代實(shí)現(xiàn)最好寫,只需要一個棧就好;中序遍歷最難,循環(huán)條件除了判斷棧是否為空,還要判斷當(dāng)前節(jié)點(diǎn)是否為空,以表示是否左子樹已經(jīng)遍歷完畢;后續(xù)遍歷的迭代如果轉(zhuǎn)化為前序遍歷的迭代,就容易很多,否則,也需要記錄上一個訪問的節(jié)點(diǎn),以表示當(dāng)前節(jié)點(diǎn)的左右子樹是否已經(jīng)訪問完畢。

相關(guān)文章

  • Java面試題之基本語法(圖解)

    Java面試題之基本語法(圖解)

    這篇文章主要介紹了關(guān)于Java面試題之基本語法的相關(guān)資料,文中通過圖片說明介紹的很詳細(xì),相信對大家具有一定的參考價值,有需要的朋友們下面來一起看看吧。
    2017-02-02
  • Java遞歸基礎(chǔ)與遞歸的宏觀語意實(shí)例分析

    Java遞歸基礎(chǔ)與遞歸的宏觀語意實(shí)例分析

    這篇文章主要介紹了Java遞歸基礎(chǔ)與遞歸的宏觀語意,結(jié)合實(shí)例形式分析了java遞歸的相關(guān)原理、操作技巧與注意事項(xiàng),需要的朋友可以參考下
    2020-03-03
  • RSA加密的方式和解密方式實(shí)現(xiàn)方法(推薦)

    RSA加密的方式和解密方式實(shí)現(xiàn)方法(推薦)

    下面小編就為大家?guī)硪黄猂SA加密的方式和解密方式實(shí)現(xiàn)方法(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • 淺談web項(xiàng)目讀取classpath路徑下面的文件

    淺談web項(xiàng)目讀取classpath路徑下面的文件

    這篇文章主要介紹了淺談web項(xiàng)目讀取classpath路徑下面的文件,具有一定借鑒價值,需要的朋友可以參考下
    2018-01-01
  • mybatis 遍歷foreach中or拼接的操作

    mybatis 遍歷foreach中or拼接的操作

    這篇文章主要介紹了mybatis 遍歷foreach中or拼接的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • JAVA線程sleep()和wait()詳解及實(shí)例

    JAVA線程sleep()和wait()詳解及實(shí)例

    這篇文章主要介紹了JAVA線程sleep()和wait()詳解及實(shí)例的相關(guān)資料,探討一下sleep()和wait()方法的區(qū)別和實(shí)現(xiàn)機(jī)制,需要的朋友可以參考下
    2017-05-05
  • WebFlux 服務(wù)編排使用優(yōu)勢詳解

    WebFlux 服務(wù)編排使用優(yōu)勢詳解

    這篇文章主要為大家介紹了WebFlux 服務(wù)編排使用優(yōu)勢示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • Java遞歸實(shí)現(xiàn)評論多級回復(fù)功能

    Java遞歸實(shí)現(xiàn)評論多級回復(fù)功能

    這篇文章主要介紹了Java遞歸實(shí)現(xiàn)評論多級回復(fù)功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • Java數(shù)據(jù)結(jié)構(gòu)之集合框架與常用算法詳解

    Java數(shù)據(jù)結(jié)構(gòu)之集合框架與常用算法詳解

    Java集合框架是Java中常用的數(shù)據(jù)結(jié)構(gòu)庫,包括List、Set、Map等多種數(shù)據(jù)結(jié)構(gòu),支持快速的元素添加、刪除、查找等操作,可以用于解決各種實(shí)際問題。Java中也有多種常用算法,如排序、查找、遞歸等,在數(shù)據(jù)處理和分析中有廣泛應(yīng)用
    2023-04-04
  • java避免多層嵌套循環(huán)用到的一些小技巧分享

    java避免多層嵌套循環(huán)用到的一些小技巧分享

    這篇文章主要介紹了java避免多層嵌套循環(huán)用到的一些小技巧分享,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-10-10

最新評論