数据映射--平衡二叉有序树
http://blog.sina.com.cn/s/blog_693f08470101mnna.html
上次我們提到了使用有序的數組來進行二分查找,從而提高映射查詢的效率,使時間復雜度從O(n)降低到O(log2N).
?
本周讓我來介紹一下二叉樹。
?
一談到二叉樹,相信很多人一定會有一個疑問:?這玩意兒有什么用??(當然這么多人里面肯定包括大學時候的我- -)
?
其實,我個人覺得這并不怪我們,是教科書寫的有點問題,開始的時候沒有給到大家明確的學習意義,開始就去講如何遍歷,如何從樹變森林,如何做樹的前序中序后序遍歷。但這樣的學習會讓整個過程很無聊,太容易讓人放棄了。所以在今天,請允許我用另外的方式來重新講解一下吧~
?
?
首先,明確一下意義,二叉樹主要用來表示樹形結構的數據,主要的應用場景是,實現數據映射,或者實現壓縮算法(哈夫曼樹)。
?
下面,讓我們從二叉樹的特性,來看看二叉樹能做些什么。
1.???????有一個ROOT節點
意味著每一次查詢都需要從一個節點開始進行,與之相對的,如果是圖類的數據結構,則起點是不固定的。
?
2.???????一個節點有兩個子節點。
與擁有多個子節點的樹相比,兩個子節點會讓樹變得更深,但好處我們在后面的二叉排序樹時會看到。
?
3.???????父節點都有一個到子節點的引用(指針)。有些時候,為了遍歷方便,還需要一個從子節點到父節點的引用(指針)
有些時候,我們需要順序的遍歷一棵樹,這時候如果有了雙向指針,那么遍歷就會變得更為方便。
?
一個純粹的二叉樹,除了能支持遍歷以外,沒有什么油水,下面讓我們來看看二叉樹的最主要用途----映射?,是如何利用平衡二叉排序樹來實現的吧。
?
先從二叉排序樹開始說起,所謂的二叉排序樹,其實只是在二叉樹上額外增加了一個條件,左邊的子節點上的數據一定比父節點的小,而右邊子節點一定比父節點的數據大。
我們還是用上周我們使用過的例子來做一下講解:
?
給定有序結果集S={1對應a,2對應b,3對應c,4對應e,6對應f}?,讓我們以這個映射關系來做例子,以便大家能夠更快速的理解。
?
這樣做的最大好處么~就是可以進行順序遍歷了。其他好處似乎是沒有。比如以下這樣的二叉樹:
?
?
?
?
?
這就是一個非常極端的情況了。也就是沒有左面的孩子,只有右面的孩子,這樣的一棵樹其實就退化成了鏈表,因為訪問只能從root開始,所以除了能夠提供順序遍歷之外,無法提供更多的功能了。
?
然而,如果我們再加上一個條件,那么二叉排序樹就立刻能夠麻雀變鳳凰,成為我們的一種重要的實現映射的利器了。
這個條件,就是平衡,于是全稱就變成了:平衡二叉排序樹。我們來看看平衡的定義:
一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹。
?
也就是說,除了最底層的節點,樹的左子節點和右子節點應該保證都有值。?則樹就會平衡,并且樹的高度是log2N。
嘿嘿,看到log2N,是不是有似曾相識的感覺?沒錯,就是二分查找的時間復雜度,或者用更容易理解的一個詞的話,為了查找到指定的數據所需要遍歷節點的次數。
?
為什么二分查找和樹的高度在數值上如此的一致呢?這就是我們下面要重點分析的東西了。
?
首先來回憶一下我們在有序數組章節
(http://blog.sina.com.cn/s/blog_693f08470101mi2o.html)里面提到的二分查找算法的必要條件吧:
1.???????數據能夠按照某種條件進行排序?,?比如S={0,1,2,3,4,5,6,7,100,101,102}?就是排好序的數據,左面的數據一定小于右面的。
2.???????可以通過某種方式,取出該數據集中任意子集的中間值。?比如對于S={0,1,2,3,4,5,6,7,100,101,102},取中值意味著應該取出下標為整數除法?(0 11)/2 = 5?的數字。?如果我們能夠快速而直接的取出這個中間值,也就是能夠快速的取出下標為5的位置所對應的數據,那么我們就能夠進行二分查找。
?
在平衡二叉排序樹中,上面的兩個要求是能夠被滿足的。
首先,數據本身是有序的,左邊的子節點內的數據,一定小于父節點內的數據,而右邊節點內的數據,一定大于父節點內的數據。
其次,在平衡二叉樹中,你會發現父節點永遠是兩個子節點的“中值”,因此可以利用這個中值非常快速的排除掉一半的數據。
?
因此,幾乎可以認為,一顆平衡二叉樹,也一樣可以利用二分查找的方式快速的從整個數據集合上面快速的取到符合要求的結果。
?
那么,既然排序后數組和平衡二叉排序樹都可以以O(log2N)的代價來快速的根據一個key定位到value.那么我為什么不用數組來做這件事,而要選擇使用平衡二叉排序樹呢?
?
這里就涉及到一個問題,排序數組有什么短板沒有呢??當然是有的,就是不支持更新。而平衡二叉樹更新的代價則要小很多,原因也很簡單,因為父節點和子節點之間使用了引用(指針)來進行的數據組織的,所以,需要插入新數據的時候,只需要調整指針就可以讓樹從新平衡并有序了。
?
當然,這種調整的方式有很多種,他們各有優勢和劣勢。不過目前因為不需要我們花功夫去實現這些數據結構了,所以只需要簡單了解一下我覺得就可以了。
?
首先被提出來的平衡樹的方式是AVL樹,然后提出來的是Tree Heap樹,最后目前在實踐中最為高效的紅黑樹。
?
在Java中,目前的TreeMap就是使用了紅黑樹來實現的,各位感興趣也可以去看一下他的代碼,對這類二叉樹的平衡算法,教科書上面教的已經很完美了,帶偽碼帶原理,這里我就不展開了。
?
在文章的末尾,我們還是以我們定義的幾個集合的評價標準來看看這平衡二叉排序樹的技術特性
?
1.???????是否支持范圍查找
因為數據是有序的,所以理論上來說是能夠支持范圍查找的,但從細節來說,支持的方法卻不是完全相同。
?
這里會用到大家在教科書上面學的,二叉樹的遍歷方法中的中序遍歷方法,也即如果要順序的訪問數據,需要不斷地重復?左子樹->根節點->右子樹的遍歷方式,直到查詢結束。
?
如果我們只存了從父節點到子節點的指針,那么在遍歷過程中,我們就必須使用一個額外的棧來存放某個子節點的父節點的引用,否則我們是無法從子節點回到父節點的。
?
而如果我們在每個節點都存放父節點到子節點的指針后,額外的再存一個從子節點到父節點的指針,那么我們就不需要用額外的棧來幫助我們進行遍歷了,可以直接按照中序遍歷的方式即可。
?
2.???????集合是否能夠隨著數據的增長而自動擴展
費了那么半天勁,也就是為了能讓數據增長變得更簡單。?所以我們可以很高興的告知大家,使用平衡排序二叉樹,是可以支持數據自動擴展的,鼓掌~
讓樹能夠在保持有序的前提下盡可能平衡的主要方式就是我們上面提到過的AVL,Tree heap,以及紅黑樹。
?
3.???????讀寫性能如何
在內存中指針的跳轉速度雖然不如使用數組快,不過也是很快的,基本上我們可以認為查詢效率就是O(log2N)。
對于寫來說,效率也是O(log2N)
4.???????是否面向磁盤結構
回憶我們提到過的磁盤的特性,一次取出一塊數據,能比較好的處理順序讀寫,而對隨機讀寫則不擅長。
對于內存來說,根據指針的要求在內存中進行跳躍,代價并不高,但如果這個操作在磁盤中進行,那么根據指針的要求的每一次跳躍,都是一次磁盤的隨機讀寫,因為在取出節點來實際看看之前,我們無法預測這個父節點的子節點被放在磁盤的哪個位置上的。那么查詢一次數據需要跳躍多少次呢?O(log2N-1)次。。跳躍的次數還是非常夸張的。
因此,平衡二叉查找樹,不是個面向磁盤的結構。
?
5.???????并行指標
不大適合并行操作,在進行結構調整的時候讀取肯定是錯誤的,能夠使用的并行讀寫的思路,主要是兩類:
一個是鎖分離
一個是copy on write
不過因為在目前的平衡二叉樹實現中基本都需要做旋轉操作,無法保證這個旋轉的原子性,所以在主流的平衡二叉排序樹中沒見過能夠很好地處理并發的。
?
6.???????內存占用
相比較數組而言,鏈表的內存占用是固定的,每個節點上固定的兩個到下層子節點的指針,以及到父節點的一個指針。?其他空間全部可以存放數據,空間消耗上,如果數組上的數據是全滿的,那么鏈表沒優勢。不過如果要是自增的數組,也即每次都以2倍空間大小做自動擴展,那么鏈表的內存占用一般是優于自動擴展數組的各類實現的,因為自動擴展數組最壞情況下有一半的空間都是空著的。
總結
以上是生活随笔為你收集整理的数据映射--平衡二叉有序树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据映射--B树
- 下一篇: 数据映射--跳表(skiplist)