樹的存儲和常用操作

樹是我們?nèi)粘i_發(fā)中經(jīng)常會用到的一種數(shù)據(jù)類型,常用的應用場景有:無限級分類、任務列表等層次數(shù)據(jù)。為了便于理解,我們首先假設需要存儲如下圖所示的樹結(jié)構(gòu):

圖1

對這樣一顆樹常用的樹操作有:

  • 樹節(jié)點查詢:
  • 查詢某節(jié)點下的所有子節(jié)點(子樹)
  • 查詢某節(jié)點的所有父級(路徑查詢)
  • 添加節(jié)點
  • 移動節(jié)點

下面介紹兩種我個人常用的樹存儲模型以及對樹常用操作的實現(xiàn)。

  1. 鄰接表模型

我們在使用關聯(lián)數(shù)據(jù)庫存儲樹結(jié)構(gòu)的時候,通常使用的方法是一條數(shù)據(jù)存儲一個節(jié)點,在節(jié)點中添加一個parentId字段存儲當前節(jié)點的父節(jié)點ID來實現(xiàn)樹的存儲,這種方法我們稱為鄰接表模型。使用Mysql存儲這顆樹的數(shù)據(jù)表結(jié)構(gòu)可能類似以下結(jié)構(gòu):

ID name parentId
1 root 0
2 A 1
3 B 1
4 C 1
5 A1 2
6 A2 2
7 C1 4
8 C2 4
9 A2.1 6

下面,我們通過樹的常用操作分析下這種存儲模型的性能。

2.1 查詢某節(jié)點下的所有子節(jié)點(子樹查詢)

假設我們現(xiàn)在需要查詢節(jié)點A的子樹,根據(jù)圖1所示我們知道節(jié)點A的直接子節(jié)點有A1、A2,而A2又有子節(jié)點A2.1。而在鄰接表模型中我們是通過parentId來管理樹的層次接口,那么我們要獲取A節(jié)點的子樹就需要:

  1. 根據(jù)A節(jié)點的ID 2獲取到A節(jié)點的直接子節(jié)點A1、A2。
  2. 遍歷A節(jié)點的直接子節(jié)點獲取所有孫級節(jié)點。
  3. 遍歷A節(jié)點的所有孫級節(jié)點獲取更下層節(jié)點,以次類推直到獲取到整顆子樹。

通過這個查詢流程我們發(fā)現(xiàn)要獲取A節(jié)點的子樹實際上需要進行很多次查詢性能是非常低的。當然,我們可以優(yōu)化擴展鄰接表模型以優(yōu)化這類查詢的性能。比如增加一個parentIds字段存儲一個節(jié)點的所有父節(jié)點路徑,例如A2.1的parentIds為”,0,2,6,”這時候我們可以通過如下SQL查詢到A節(jié)點的子樹:select * from true where parentIds like "%,2,%"。但是這種方法依然存在缺陷,首先,like查詢的性能在大表下會有一定的影響,其次是在插入新節(jié)點時需要預先獲取其所有父節(jié)點ID再寫入parentIds字段中。

2.2 查詢某節(jié)點的所有父級節(jié)點(路徑查詢)

假設我們現(xiàn)在需要查詢節(jié)點A2.1的路徑,根據(jù)圖1所示我們知道A2.1的路徑是A2.1 > A2 > A > root。類似2.1的查詢方法我們需要:

  1. 通過A2.1的parentId查詢到A2.1的直接父級A2
  2. 通過A2的parentId查詢到A2.1的爺級節(jié)點A
  3. 通過A的parentId查詢到A2.1的祖級節(jié)點root

同樣,要實現(xiàn)這樣的查詢性能也是非常低的。當然,我們也可以像2.1那樣優(yōu)化在節(jié)點中添加一個childrenId字段存儲節(jié)點的所有子節(jié)點ID來進行查詢優(yōu)化,但是很顯然的問題如果這是一個顆很大的樹,那么root節(jié)點的children字段將會非常大!噢No!

2.3 添加新節(jié)點

假設現(xiàn)在需要為A2.1添加一個子A2.1.1,很簡單只需要插入一條數(shù)據(jù)將parentId設置為9就可以了。

2.4 移動節(jié)點

移動節(jié)點也非常簡單快速,假設我們需要將A2.1節(jié)點移動到節(jié)點C下只需要更新A2.1節(jié)點的parentId字段為4就可以了。

  1. 先根遍歷樹模型

先根遍歷樹模型是另一種常用的樹存儲模型,實現(xiàn)方法是從根節(jié)點左側(cè)開始分別為每個節(jié)點進行編號直到樹的最底層,再從最底成節(jié)點右側(cè)反向編號直到回到根節(jié)點。如下圖所示:

圖2

這樣每個節(jié)點都獲得了一個左值和右值,使用Mysql存儲的表結(jié)構(gòu)類似下表:

ID name leftValue rightValue
1 root 1 18
2 A 2 9
3 B 10 11
4 C 12 17
5 A1 3 4
6 A2 5 8
7 C1 13 14
8 C2 15 16
9 A2.1 6 7

同樣,基于先根遍歷樹模型我們看下如何進行樹的常用操作。

3.1 查詢某節(jié)點下的所有子節(jié)點(子樹查詢)

同樣假設需要查詢節(jié)點A的子樹,通過觀察圖2中節(jié)點的左右值可以發(fā)現(xiàn)A節(jié)點的所有子節(jié)點左值肯定都大于A節(jié)點的左值,而右值肯定都小于A節(jié)點的右值!根據(jù)這條規(guī)則我們使用一條SQL就可以查詢到A節(jié)點的子樹,SQL如下:

select * from tree
    where leftValue > a.leftValue
        and rightValue < a.rightValue
    order by leftValue asc;

可見使用這種存儲模型查詢子樹的性能遠遠高于鄰接表模型。

3.2 查詢某節(jié)點的所有父級節(jié)點(路徑查詢)

同樣假設需要查詢節(jié)點A2.1的路徑,通過觀察圖2中節(jié)點A2.1的所有父節(jié)點左右值可以發(fā)現(xiàn),A2.1的所有父節(jié)點左值肯定都小于A2.1的左值,而右值肯定都大于節(jié)點A2.1的右值!根據(jù)這條規(guī)則我們同樣可以使用一條SQL查詢到A2.1的所有父節(jié)點,SQL如下:

select * from tree
    where leftValue < a21.leftValue
        and rightValue > a21.rightValue
    order by leftValue acs;

可見使用這種存儲模型查詢節(jié)點路徑的性能也是遠遠高于鄰接表模型。

3.3 添加新節(jié)點

通過前面兩種查詢操作可以看到先根遍歷樹模型在查詢數(shù)據(jù)方面性能非常卓越。可惜人無完人,方法也是!這種模型在進行數(shù)據(jù)插入方面可就不能直接一條SQL搞定了。

還是以向A2.1添加一個A2.1.1節(jié)點為例。因為這種模型下樹初始化后節(jié)點的左右值是連續(xù)的,所以如果想要添加一個節(jié)點就必須先給新節(jié)點騰出一個空位來。根據(jù)左右值的編碼規(guī)則我們可以推算出新節(jié)點A2.1.1的左值應該是7、右值應該是8。如果要騰出這樣一個位置的話,就必須將A2.1、A2、A、Root節(jié)點的右值加2,將B、C、C1、C2的左值和右值都加2。SQL如下:

update tree
    set rightValue = rightValue+2
    where leftValue <= a21.leftValue
        and rightValue >= a21.rightValue;
update true
    set leftValue = leftValue + 2,rightValue = rightValue + 2
    where leftValue > a21.rightValue;

這種就為新節(jié)點A2.1.1空出了一個位置,接下來再使用insert語句將A2.1.1插入即可。由于需要更新A2.1所以右值節(jié)點和父節(jié)點的所以大一顆比較大的數(shù)下插入一個新節(jié)點性能消耗還是比較大的。

3.4 移動節(jié)點

使用先根遍歷數(shù)模型移動節(jié)點位置和插入新節(jié)點的方法基本相同,也是需要先在新位置為節(jié)點空出一個位置,然后再將節(jié)點的左右值修改為新位置的左右值。

總結(jié)

兩種樹的存儲模型以及它們分別實現(xiàn)樹的常用操作方法就介紹完成了,可以發(fā)現(xiàn)這兩種模型各有優(yōu)缺點。

鄰接表模型插入新節(jié)點、移動節(jié)點位置性能非常好,但是要進行復雜的樹查詢性能就很糟糕了。
先根遍歷樹模型在查詢方面性能突出,但是在插入新節(jié)點和移動節(jié)點時雖然SQL也只有兩三條但是在一個大表下考慮到數(shù)據(jù)量以及索引重建性能還是會有問題。
所以,世界上沒有最好的語言更沒有最好的方法!只有根據(jù)自己的業(yè)務特點選擇最合適存儲模型才是最佳實踐,如果對樹的操作更多是查詢應該使用先根遍歷樹模型;如果對樹的寫操作比較多則應該使用鄰接表模型。當然也可以將兩種模型同時使用,插入的時候使用鄰接表模型然后在后臺重建左右值結(jié)構(gòu),查詢的時候使用先根遍歷樹模型。結(jié)合現(xiàn)代的新技術(shù)還可以分析樹的規(guī)模,如果樹的規(guī)??隙ú粫艽髣t還可以使用redis存儲JSON對象的方式來進行存儲??傊痪湓挘菏紫确治鰳I(yè)務場景,再根據(jù)業(yè)務場景選擇最合適的方法!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,384評論 0 12
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,653評論 19 139
  • 分布式系統(tǒng)面臨的第一個問題就是數(shù)據(jù)分布,即將數(shù)據(jù)均勻地分布到多個存儲節(jié)點。另外,為了保證可靠性和可用性,需要將數(shù)據(jù)...
    olostin閱讀 4,934評論 2 26
  • 樹的概述 樹是一種非常常用的數(shù)據(jù)結(jié)構(gòu),樹與前面介紹的線性表,棧,隊列等線性結(jié)構(gòu)不同,樹是一種非線性結(jié)構(gòu) 1.樹的定...
    Jack921閱讀 4,765評論 1 31
  • 在大家通常的印象里,旅游景點一般都規(guī)模宏大,小一點的,起碼也是一家店鋪,一間餐廳或者一尊雕塑。而美國這座小鎮(zhèn)最大的...
    旅行者鏡頭閱讀 335評論 0 0

友情鏈接更多精彩內(nèi)容