歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。java
咱們知道二叉查找樹的遍歷有前序遍歷、中序遍歷、後序遍歷。node
(1)前序遍歷,先遍歷我,再遍歷個人左子節點,最後遍歷個人右子節點;git
(2)中序遍歷,先遍歷個人左子節點,再遍歷我,最後遍歷個人右子節點;github
(3)後序遍歷,先遍歷個人左子節點,再遍歷個人右子節點,最後遍歷我;微信
這裏的前中後都是以「我」的順序爲準的,我在前就是前序遍歷,我在中就是中序遍歷,我在後就是後序遍歷。ide
下面讓咱們看看經典的中序遍歷是怎麼實現的:學習
public class TreeMapTest { public static void main(String[] args) { // 構建一顆10個元素的樹 TreeNode<Integer> node = new TreeNode<>(1, null).insert(2) .insert(6).insert(3).insert(5).insert(9) .insert(7).insert(8).insert(4).insert(10); // 中序遍歷,打印結果爲1到10的順序 node.root().inOrderTraverse(); } } /** * 樹節點,假設不存在重複元素 * @param <T> */ class TreeNode<T extends Comparable<T>> { T value; TreeNode<T> parent; TreeNode<T> left, right; public TreeNode(T value, TreeNode<T> parent) { this.value = value; this.parent = parent; } /** * 獲取根節點 */ TreeNode<T> root() { TreeNode<T> cur = this; while (cur.parent != null) { cur = cur.parent; } return cur; } /** * 中序遍歷 */ void inOrderTraverse() { if(this.left != null) this.left.inOrderTraverse(); System.out.println(this.value); if(this.right != null) this.right.inOrderTraverse(); } /** * 經典的二叉樹插入元素的方法 */ TreeNode<T> insert(T value) { // 先找根元素 TreeNode<T> cur = root(); TreeNode<T> p; int dir; // 尋找元素應該插入的位置 do { p = cur; if ((dir=value.compareTo(p.value)) < 0) { cur = cur.left; } else { cur = cur.right; } } while (cur != null); // 把元素放到找到的位置 if (dir < 0) { p.left = new TreeNode<>(value, p); return p.left; } else { p.right = new TreeNode<>(value, p); return p.right; } } }
從上面二叉樹的遍歷咱們很明顯地看到,它是經過遞歸的方式實現的,可是遞歸會佔用額外的空間,直接到線程棧整個釋放掉纔會把方法中申請的變量銷燬掉,因此當元素特別多的時候是一件很危險的事。ui
(上面的例子中,沒有申請額外的空間,若是有聲明變量,則能夠理解爲直到方法完成纔會銷燬變量)this
那麼,有沒有什麼方法不用遞歸呢?線程
讓咱們來看看java中的實現:
@Override public void forEach(BiConsumer<? super K, ? super V> action) { Objects.requireNonNull(action); // 遍歷前的修改次數 int expectedModCount = modCount; // 執行遍歷,先獲取第一個元素的位置,再循環遍歷後繼節點 for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) { // 執行動做 action.accept(e.key, e.value); // 若是發現修改次數變了,則拋出異常 if (expectedModCount != modCount) { throw new ConcurrentModificationException(); } } }
是否是很簡單?!
(1)尋找第一個節點;
從根節點開始找最左邊的節點,即最小的元素。
final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; // 從根節點開始找最左邊的節點,即最小的元素 if (p != null) while (p.left != null) p = p.left; return p; }
(2)循環遍歷後繼節點;
尋找後繼節點這個方法咱們在刪除元素的時候也用到過,當時的場景是有右子樹,則從其右子樹中尋找最小的節點。
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { if (t == null) // 若是當前節點爲空,返回空 return null; else if (t.right != null) { // 若是當前節點有右子樹,取右子樹中最小的節點 Entry<K,V> p = t.right; while (p.left != null) p = p.left; return p; } else { // 若是當前節點沒有右子樹 // 若是當前節點是父節點的左子節點,直接返回父節點 // 若是當前節點是父節點的右子節點,一直往上找,直到找到一個祖先節點是其父節點的左子節點爲止,返回這個祖先節點的父節點 Entry<K,V> p = t.parent; Entry<K,V> ch = t; while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }
讓咱們一塊兒來分析下這種方式的時間複雜度吧。
首先,尋找第一個元素,由於紅黑樹是接近平衡的二叉樹,因此找最小的節點,至關因而從頂到底了,時間複雜度爲O(log n);
其次,尋找後繼節點,由於紅黑樹插入元素的時候會自動平衡,最壞的狀況就是尋找右子樹中最小的節點,時間複雜度爲O(log k),k爲右子樹元素個數;
最後,須要遍歷全部元素,時間複雜度爲O(n);
因此,總的時間複雜度爲 O(log n) + O(n * log k) ≈ O(n)。
雖然遍歷紅黑樹的時間複雜度是O(n),可是它實際是要比跳錶要慢一點的,啥?跳錶是啥?安心,後面會講到跳錶的。
到這裏紅黑樹就整個講完了,讓咱們再回顧下紅黑樹的特性:
(1)每一個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每一個葉子節點(NIL)是黑色。(注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!)
(4)若是一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。
除了上述這些標準的紅黑樹的特性,你還能講出來哪些TreeMap的特性呢?
(1)TreeMap的存儲結構只有一顆紅黑樹;
(2)TreeMap中的元素是有序的,按key的順序排列;
(3)TreeMap比HashMap要慢一些,由於HashMap前面還作了一層桶,尋找元素要快不少;
(4)TreeMap沒有擴容的概念;
(5)TreeMap的遍歷不是採用傳統的遞歸式遍歷;
(6)TreeMap能夠按範圍查找元素,查找最近的元素;
(7)歡迎補充...
微信用戶請「閱讀原文」,進入倉庫查看,其它渠道直接點擊此連接便可跳轉。
上面咱們說到的刪除元素的時候,若是當前節點有右子樹,則從右子樹中尋找最小元素所在的位置,把這個位置的元素放到當前位置,再把刪除的位置移到那個位置,再看有沒有替代元素,balabala。
那麼,除了這種方式,還有沒有其它方式呢?
答案固然是確定的。
上面咱們說的紅黑樹的插入元素、刪除元素的過程都是標準的紅黑樹是那麼幹的,其實也不必定要徹底那麼作。
好比說,刪除元素,若是當前節點有左子樹,那麼,咱們能夠找左子樹中最大元素的位置,而後把這個位置的元素放到當前節點,再把刪除的位置移到那個位置,再看有沒有替代元素,balabala。
舉例說明,好比下面這顆紅黑樹:
咱們刪除10這個元素,從左子樹中找最大的,找到了9這個元素,那麼把9放到10的位置,而後把刪除的位置移到原來9的位置,發現不須要做平衡(紅+黑節點),直接把這個位置刪除就能夠了。
一樣是知足紅黑樹的特性的。
因此,死讀書不如無書,學習的過程也是一個不斷重塑知識的過程。
如今公衆號文章沒辦法留言了,若是有什麼疑問或者建議請直接在公衆號給我留言。
歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。