一、多級分類問題
在實際開發的過程中,會經常遇到多級分類的問題。譬如,導航欄、菜單、商品種類、多級聯動、字典表等等的多級分類問題。這時可以新增一個 pid
字段進行數據關聯,它本質上其實就是一棵樹。樹就可以很好的解決多級分類的子分類查詢。
但是這種方式有一個致命的問題:查詢效率過低!!!
當我們在程序裏查詢某個子節點時,要先從根節點進行遞歸查詢,時間複雜度是 O(n)
。
那麼有沒有一種方式,改進樹的查詢效率呢?答案是肯定的!很多樹都在標準的樹上進行改進過,比如二叉樹、紅黑樹、堆等等。但這都不是重點,今天要分享的是 預排序遍歷樹算法(MPTT)
。
MPTT
正是爲了解決多層級關係數據的查詢效率問題,它的時間複雜度竟然能高效到一個常量,即 O(1)
。是不是很不可思議,接下來就讓我們一起學習預排序遍歷樹算法,看下它到底是如何實現的。
二、預排序遍歷樹
預排序遍歷樹算法全稱是:Modified Preorder Tree Traversal
簡稱 MPTT
。
1. ORM 映射
class Tree(Base, BaseNestedSets):
__tablename__ = 'tree'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(8), nullable=True, default=None)
def __repr__(self):
return f'<Tree(id={self.id}, name={self.name})>'
2. MPTT 分析
在上述代碼中,只定義了兩個字段: id
、name
。但是數據庫裏面卻額外多出了 5
個字段,分別是: lft
、rgt
、level
、tree_id
、parent_id
。
這些多出來的字段就是爲了定義樹的結構和層級。下面我們就來分析一下,每個字段的作用是什麼。
-
tree_id
:樹的id
,用來區分數據庫中衆多樹的某一顆樹。 -
level
:一顆標準的樹會有高度、深度、層級,根節點的層級是1
,子節點的層級是父節點層級加1
。 -
parent_id
:父id
,節點的父級id
,根節點沒有父節點,所以值爲NULL
。 -
lft
:節點左值。 -
rgt
:節點右值。
節點的左值和右值是 MPTT
的核心,也是這個算法實現特別巧妙的地方,將樹遍歷的時間複雜度降爲 O(1)
。接下來就重點分析一下左值與右值是如何進行樹遍歷的。
一顆標準的樹結構:
數據庫數據對應關係:
數據層次結構:
- 【1】
- - 【2】
- - - 【3】
- - 【4】
- - - 【5】
- - - 【6】
- - 【7】
- - - 【8】
- - - - 【9】
- - - 【10】
- - - - 【11】
遍歷整棵樹
遍歷整棵樹只需要查找 tree_id
等於 1
的條件即可
找到某節點下所有的子孫節點
查找節點 4
的所有子孫節點,以 4
作爲參考點。左值大於 6
且右值小於 11
的所有子孫節點,就是節點 4
的所有子孫節點。
找到某節點下所有的子節點
查找節點 1
的所有子節點,以 1
作爲參考點。tree_id
等於 1
且 level
等於 2
。
查找某節點的路徑
查找節點 9
的所有上級路徑,以 9
作爲參考點。左值小於 14
且右值大於 15
的所有節點,就是節點 ``9的路徑。結果是:
1 -> 7 -> 8 -> 9`。
3. MPTT 平衡算法
MPTT
在遍歷的時候很快,但是其他的操作就會變得很慢,所以使用 MPTT
要儘量避免查詢之外的其他操作。
那爲什麼除了查詢操作其他的操作會很慢呢?
這是因爲節點在插入、更新(移動)、刪除會破壞樹的平衡。所以在做這些操作的時候需要對數進行調整,達到新的平衡。
新增
以新增節點操作爲例,算法可分解爲以下幾個步驟:
-
如果要在不存在的樹中新增節點,即要創建一顆新樹。那麼它是沒有
parent_id
的,所以parent_id
值爲NULL
,level
是1
,tree_id
是根據已有樹的最大tree_id
加1
。 -
如果要在已存在的樹中新增節點。那麼它的
parent_id
是父節點的id
,level
是父節點的level
加1
,tree_id
和父節點保持一致。 -
修復被破壞平衡的其他節點的左值。大於
parent_id
右值的所有節點的左值加2
。 -
修復被破壞平衡的其他節點的右值。大於等於
parent_id
右值的所有節點的右值加2
。
刪除
和增加類似,只不過刪除一個節點以後對左值和右值進行相反的操作,即減 2
。
更新(移動)
更新(移動)其實就是刪除一個老節點,再新增一個新節點,具體算法參考上面的例子。
三、標準樹和預排序遍歷樹的優劣對比
-
標準樹
:適用於增刪 操作較多的場景,每次刪改只需要修改一條數據。在查詢方面,隨着分類層級的增加鄰接表的遞歸查詢效率逐漸降低。 -
預排序遍歷樹
:適用於查詢操作較多的場景,查詢的效率不受分類層級的增加的影響,但是隨着數據的增多,每增刪數據,都要同時操作多條受影響數據,執行效率逐漸下降。
沒有完美的算法,在實際開發過程中具體要選擇哪一種存儲結構和算法,需要根據具體的應用場景來做選擇。