完全二叉樹(shù)、線索二叉樹(shù)及樹(shù)的順序存儲(chǔ)結(jié)構(gòu)在上篇文章中,我們學(xué)習(xí)了二叉樹(shù)的基本鏈?zhǔn)浇Y(jié)構(gòu)以及建樹(shù)和遍歷相關(guān)的操作。今天我們學(xué)習(xí)的則是一些二叉樹(shù)相關(guān)的概念以及二叉樹(shù)的一種變形形式。 完全二叉樹(shù)什么叫完全二叉樹(shù)呢?在說(shuō)到完全二叉樹(shù)之前,我們先說(shuō)另外一個(gè)名詞:“滿二叉樹(shù)”。像我們之前文章中演示過(guò)的那個(gè)二叉樹(shù),就是一顆“滿二叉樹(shù)”。在這顆樹(shù)中,所有的結(jié)點(diǎn)都有兩個(gè)孩子結(jié)點(diǎn),沒(méi)有哪個(gè)結(jié)點(diǎn)是只有一個(gè)孩子結(jié)點(diǎn)的,并且所有最底層的葉子結(jié)點(diǎn)都在同一層,這種樹(shù)就稱為“滿二叉樹(shù)”,也稱為“完美二叉樹(shù)”。 是不是非常漂亮的一顆樹(shù)?沒(méi)錯(cuò),這種二叉樹(shù)非常地完美,它沒(méi)有多余的結(jié)點(diǎn),也沒(méi)有缺少的結(jié)點(diǎn),非常的漂亮。但是,在現(xiàn)實(shí)中,完美的東西是很稀少的,人生總會(huì)有一點(diǎn)缺憾嘛。我們盡量不要讓自己有太多的缺憾,但也總不能過(guò)上沒(méi)有一絲缺憾的人生。所以,我們?cè)试S葉結(jié)點(diǎn)出現(xiàn)在最下層和次下層,而且最下層的葉結(jié)點(diǎn)集中在樹(shù)的左部,也就是葉結(jié)點(diǎn)只能有左子樹(shù),那么,這樣的一顆略帶缺憾的樹(shù)就叫做“完全二叉樹(shù)”。不要擔(dān)心它不完美,因?yàn)檫@樣略帶缺憾的人生才是最真實(shí)的嘛,所以“完全二叉樹(shù)”是一種理想的樹(shù)結(jié)構(gòu)。 從定義中,我們可以看出,一顆“滿二叉樹(shù)”,必定是一顆“完全二叉樹(shù)”,而一顆葉子結(jié)點(diǎn)都在一層的并且所有結(jié)點(diǎn)都有左右孩子結(jié)點(diǎn)的“完全二叉樹(shù)”也就是一顆”滿二叉樹(shù)“。 為什么要講”滿二叉樹(shù)“和”完全二叉樹(shù)“呢?當(dāng)然是為了我們接下來(lái)的內(nèi)容做鋪墊。因?yàn)椤睗M二叉樹(shù)“是最符合二叉樹(shù)性質(zhì)的一顆樹(shù)。還記得樹(shù)系列的第一篇文章中介紹過(guò)的二叉樹(shù)的那五個(gè)性質(zhì)嗎?當(dāng)時(shí)我們就是以那顆”滿二叉樹(shù)“為例進(jìn)行講解的。而其中的 性質(zhì)5 ,就是我們學(xué)習(xí)使用順序結(jié)構(gòu)存儲(chǔ)二叉樹(shù)的基礎(chǔ)。 二叉樹(shù)的順序存儲(chǔ)通過(guò)”滿二叉樹(shù)“的概念,以及二叉樹(shù)的 性質(zhì)5 我們就可以實(shí)現(xiàn)使用一個(gè)數(shù)組來(lái)存儲(chǔ)順序結(jié)構(gòu)的實(shí)現(xiàn)。 $treeList = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; 相信大家不陌生吧,在上篇文章中,我們就是通過(guò)這個(gè)數(shù)組來(lái)建立鏈樹(shù)的,而這個(gè)數(shù)組其實(shí)就是一個(gè)線性存儲(chǔ)的二叉樹(shù)。我們通過(guò)對(duì)比二叉樹(shù)的 性質(zhì)5 來(lái)看一下。
這下想必大家就明白了用數(shù)組是如何表示一顆二叉樹(shù)結(jié)構(gòu)了吧。而且數(shù)組這種結(jié)構(gòu)更加的一維,更能體現(xiàn)出對(duì)于樹(shù)的操作就是二維化一維的一種表示,也就是非線性轉(zhuǎn)線性,這樣才能讓我們方便地操作這些數(shù)據(jù)。 針對(duì)順序存儲(chǔ)結(jié)構(gòu),也就是數(shù)組元素的遍歷,也是可以使用先序、中序、后序以及層序的形式。不過(guò)這些遍歷方法都需要根據(jù)二叉樹(shù)的 性質(zhì)5 來(lái)進(jìn)行遍歷。但更重要的是,只要給我一個(gè)下標(biāo),我們通過(guò)二叉樹(shù)的性質(zhì),就可能很容易地知道它的下級(jí)結(jié)點(diǎn)和上級(jí)結(jié)點(diǎn)的位置,能夠快速地獲得這些結(jié)點(diǎn)的信息。這一大特點(diǎn)是鏈?zhǔn)浇Y(jié)構(gòu)的二叉樹(shù)所沒(méi)有的。 如果我們要存儲(chǔ)的不是一顆”滿二叉樹(shù)“呢?甚至它都不是一顆完全二叉樹(shù)的情況下,只需要將對(duì)應(yīng)的結(jié)點(diǎn)設(shè)置為空值就行了。比如: $treeList = ['', 'A', 'B', 'C', 'D', 'E', 'I', '', '', '', '', 'H', '', 'J', '', '']; 這顆樹(shù)的結(jié)構(gòu)所對(duì)應(yīng)的二叉樹(shù)圖形就是這樣的: 然后在建鏈樹(shù)的方法中,我們只需要再增加一個(gè)判斷就可以了。我們就可以通過(guò)這樣一個(gè)順序存儲(chǔ)的二叉樹(shù)快速地生成一顆鏈?zhǔn)酱鎯?chǔ)的二叉樹(shù),方便我們之后的操作。 // 建立二叉樹(shù) 線索二叉樹(shù)一環(huán)套一環(huán),接下來(lái)我們?cè)賮?lái)講講”線索二叉樹(shù)“。這又是個(gè)什么東西呢? 從上面的學(xué)習(xí)中,我們知道了”滿二叉樹(shù)“和”完全二叉樹(shù)“。但是這種結(jié)構(gòu)都是非常理想的樹(shù)結(jié)構(gòu),不過(guò)真實(shí)的情況可能大部分都是”理想很豐滿,現(xiàn)實(shí)很骨感“。很多樹(shù)并不能形成那樣的完全二叉樹(shù)的形式,更別提”滿二叉樹(shù)“了。而樹(shù)的遍歷又經(jīng)常會(huì)使用?;蛘哧?duì)列來(lái)實(shí)現(xiàn),這兩種遍歷方式基本都是線性的,也就是最好情況下也是 O(n) 的時(shí)間復(fù)雜度。那么,有沒(méi)有什么更快一點(diǎn)的方式來(lái)提高遍歷的效率呢? 我們這樣來(lái)嘗試一下:
這樣有什么好處呢?我們可以避免掉大范圍的遞歸操作,從而加快樹(shù)的遍歷速度。在整個(gè)算法中,它并沒(méi)有什么優(yōu)勢(shì),因?yàn)槲覀冃枰獙⒁活w樹(shù)進(jìn)行線索化,也就是去改變它的葉子結(jié)點(diǎn)的左右孩子的指向,這也是一次遍歷。但是,如果你的操作是經(jīng)常需要遍歷,而且是來(lái)回的多次遍歷,那么它的整體性能是要強(qiáng)于普通二叉樹(shù)的遍歷的。因?yàn)樵谝淮尉€索化之后,它的遍歷就是在快速的查找葉子結(jié)點(diǎn)的基礎(chǔ)上進(jìn)行普通的線性遍歷操作,而不是遞歸操作。 對(duì)于線索二叉樹(shù)來(lái)說(shuō),我們需要改變樹(shù)的結(jié)點(diǎn)存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)。 // 線索二叉樹(shù)結(jié)點(diǎn) 我們?cè)黾恿藘蓚€(gè)標(biāo)志位,當(dāng) lTag 或 rTag 為 1 時(shí),lChild 或 rChild 分別指向前驅(qū)或后繼結(jié)點(diǎn)。這樣在最后的遍歷時(shí),我們就可以快速地通過(guò)這個(gè) tag 標(biāo)志位分辨出結(jié)點(diǎn)的指向狀態(tài)。 然后我們先簡(jiǎn)單地建立一顆樹(shù)。使用上一節(jié)中的那個(gè)示例。 // 建立二叉樹(shù) 接下來(lái)就是最重要的線索化過(guò)程,我們可以建立前序、中序、后序的線索二叉樹(shù)。對(duì)應(yīng)的,在最后的線索二叉樹(shù)遍歷時(shí)獲得的結(jié)果也將是這三種遍歷方式所對(duì)應(yīng)的結(jié)果。在這里,我們學(xué)習(xí)最普遍的也是最經(jīng)典的”中序線索二叉樹(shù)“。所以,我們以中序遍歷的形式將這顆樹(shù)線索化。 // 線索化 關(guān)于算法的具體步驟在注釋中已經(jīng)寫得很詳細(xì)了。一句話總結(jié)就是在中序遍歷的過(guò)程中,根據(jù)結(jié)點(diǎn)的信息來(lái)確定它的左右孩子的形式,如果有左右孩子就繼續(xù),如果沒(méi)有任一一個(gè)孩子的話,就將左右結(jié)點(diǎn)指向前驅(qū)或者后繼。建立之后的線索二叉樹(shù)就如圖所示: 最后就是遍歷了。我們需要的是能夠快速地獲得最左葉子結(jié)點(diǎn)的信息,然后就是下一跳的信息,這時(shí),線索的威力就發(fā)揮出來(lái)了。 // 以 $p 為根的中序線索二叉樹(shù)中,中序序列下的第一個(gè)結(jié)點(diǎn),也就是最左邊那個(gè)結(jié)點(diǎn) 當(dāng)遇到 lTag 不為 0 的結(jié)點(diǎn)時(shí),這個(gè)結(jié)點(diǎn)就是最左的那個(gè)結(jié)點(diǎn)了,如果這個(gè)不為空的話,【輸出】它。接著我們獲得下一跳的結(jié)點(diǎn),也就是判斷這個(gè)結(jié)點(diǎn)的右孩子 rTag 標(biāo)志,如果是為 0 的,也就是它還有右孩子,【輸出】后向下查找,直到找到一個(gè) rTag 也為 1 的結(jié)點(diǎn),直接返回這個(gè)結(jié)點(diǎn)的后繼,也就是中序遍歷的中間那個(gè)結(jié)點(diǎn),【輸出】它。 最后輸出的順序是不是和我們中序遍歷的結(jié)果一樣呢?注意看代碼,在遍歷中序線索二叉樹(shù)的時(shí)候,我們沒(méi)有用一個(gè)遞歸吧,全部是使用的 while() 和 for() 就完成了對(duì)這個(gè)線索二叉樹(shù)的遍歷。 總結(jié)堅(jiān)持到現(xiàn)在不容易,不能再小看數(shù)據(jù)結(jié)構(gòu)了吧?現(xiàn)在還只是樹(shù),我們的圖都還沒(méi)開(kāi)始呢!當(dāng)然,也不要害怕,一步一步的學(xué),慢慢掌握,不要幻想一口氣吃成個(gè)胖子。即使是寫完這篇文章了,我也不可能馬上就手寫出一個(gè)中序的線索二叉樹(shù)來(lái)的。大家還是以理解原理為主,如果說(shuō)真能手寫的話,那也是為了面試而去背的或者是為了考研而準(zhǔn)備的。如果有這樣為了應(yīng)付的小同學(xué)出現(xiàn)在面試中的話,我反而要更多問(wèn)一些其它的問(wèn)題,畢竟臨時(shí)抱佛腳的準(zhǔn)備遠(yuǎn)不如深入理解帶來(lái)的感悟更能打動(dòng)人! 測(cè)試代碼: https://github.com/zhangyue0503/Data-structure-and-algorithm/blob/master/4.樹(shù)/source/4.3完全二叉樹(shù)、線索二叉樹(shù)及樹(shù)的順序存儲(chǔ)結(jié)構(gòu).php 參考資料: 《數(shù)據(jù)結(jié)構(gòu)》第二版,嚴(yán)蔚敏 《數(shù)據(jù)結(jié)構(gòu)》第二版,陳越 《數(shù)據(jù)結(jié)構(gòu)高分筆記》2020版,天勤考研 |
|
來(lái)自: 硬核項(xiàng)目經(jīng)理 > 《待分類》