數據結構和算法:預排序遍歷樹算法

一、多級分類問題

在實際開發的過程中,會經常遇到多級分類的問題。譬如,導航欄、菜單、商品種類、多級聯動、字典表等等的多級分類問題。這時可以新增一個 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 分析

在上述代碼中,只定義了兩個字段: idname。但是數據庫裏面卻額外多出了 5個字段,分別是: lftrgtleveltree_idparent_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 等於 1level 等於 2

查找某節點的路徑

查找節點 9 的所有上級路徑,以 9 作爲參考點。左值小於 14 且右值大於 15 的所有節點,就是節點 ``9的路徑。結果是:1 -> 7 -> 8 -> 9`。

3. MPTT 平衡算法

MPTT 在遍歷的時候很快,但是其他的操作就會變得很慢,所以使用 MPTT 要儘量避免查詢之外的其他操作。

在這裏插入圖片描述

那爲什麼除了查詢操作其他的操作會很慢呢?

這是因爲節點在插入、更新(移動)、刪除會破壞樹的平衡。所以在做這些操作的時候需要對數進行調整,達到新的平衡。

新增

以新增節點操作爲例,算法可分解爲以下幾個步驟:

  • 如果要在不存在的樹中新增節點,即要創建一顆新樹。那麼它是沒有 parent_id 的,所以 parent_id 值爲 NULLlevel1tree_id 是根據已有樹的最大 tree_id1

  • 如果要在已存在的樹中新增節點。那麼它的 parent_id 是父節點的 idlevel 是父節點的 level1tree_id 和父節點保持一致。

  • 修復被破壞平衡的其他節點的左值。大於 parent_id 右值的所有節點的左值加 2

  • 修復被破壞平衡的其他節點的右值。大於等於 parent_id 右值的所有節點的右值加 2

刪除

和增加類似,只不過刪除一個節點以後對左值和右值進行相反的操作,即減 2

更新(移動)

更新(移動)其實就是刪除一個老節點,再新增一個新節點,具體算法參考上面的例子。

三、標準樹和預排序遍歷樹的優劣對比

  • 標準樹:適用於增刪 操作較多的場景,每次刪改只需要修改一條數據。在查詢方面,隨着分類層級的增加鄰接表的遞歸查詢效率逐漸降低。

  • 預排序遍歷樹:適用於查詢操作較多的場景,查詢的效率不受分類層級的增加的影響,但是隨着數據的增多,每增刪數據,都要同時操作多條受影響數據,執行效率逐漸下降。

沒有完美的算法,在實際開發過程中具體要選擇哪一種存儲結構和算法,需要根據具體的應用場景來做選擇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章