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