ADT - 樹

定義

一棵樹是一些節點的集合。這個集合能夠是空集,若非空,則一棵樹由稱做 根(root) 的節點 r 和 0 個或以上的非空子樹組成。這些子樹中每一棵的根都被來自根 r 的**一條有向邊(edge)**鏈接。node

父子節點,葉子節點定義略。。。算法

從節點 n<sub>1</sub> 到 n<sub>k</sub> 的 路徑(path) 定義爲節點 n<sub>1</sub>, n<sub>2</sub>, ..., n<sub>k</sub> 的一個序列,使得對於 1 ≤ i ≤ k,節點 n<sub>i+1</sub> 是 節點 n<sub>i</sub> 的父親。這條路徑的 長度(length) 定義爲該路徑上邊的條數,即k - 1。數據庫

可見任意兩節點 n<sub>i</sub> 和 n<sub>k</sub> 之間是否存在一條路徑取決於 n<sub>i</sub> 是不是 n<sub>k</sub> 的祖先節點。且若是有,也僅有一條。(由於上面樹的定義裏,一個非根節點有且只有一條邊)性能

而從根節點到任意節點均存在一條惟一的路徑,這條路徑的長度被稱爲該節點的 深度(depth)。一棵樹的深度被定義爲其最深的一個節點的深度。優化

按照以前算法分析的某條定理:若是一個算法可使用常數時間將問題縮小爲原問題的一部分,那麼這個算法的複雜度爲 O(logN)。樹在正常狀況下的查找上顯然符合這個特徵。code

能夠將樹當作是序列的變形,它在序列之上容許了多子節點。排序

二叉樹

二叉樹即每一個節點的子節點數均不超過 2 的狀況。索引


實現

爲了便於操做,實際的樹一般會在定義之上再增長一些特性,如rem

  1. 在每一個節點中記錄其父節點,即反向邊
  2. 刪除時並不真正刪除節點,而是將其計數 -1

下面是一個使用字典實現的二叉查找樹,功能方面只實現了 __contains__ 方法:it

class Node(dict):
    def __init__(self, value, parent=None, left=None, right=None):
        self['value'] = value
        self['count'] = 1
        if parent:
            self['parent'] = parent
        if left:
            self['left'] = left
        if right:
            self['right'] = right


class Tree(object):
    def __init__(self, init_value):
        self.root = Node(init_value)

    def _find_near(self, value):
        node = self.root
        try:
            while True:
                if value == node['value']:
                    return node
                elif value < node['value']:
                    node = node['left']
                else:
                    node = node['right']
        except KeyError:
            return node

    def add(self, value):
        node = self._find_near(value)
        if value == node['value']:
            node['count'] += 1
        elif value < node['value']:
            node['left'] = Node(value)
        else:
            node['right'] = Node(value)

    def rem(self, value):
        node = self._find_near(value)
        if node['value'] == value and node['count'] > 0:
            node['count'] -= 1

    def __contains__(self, value):
        node = self._find_near(value)
        return node['value'] == value and node['count'] > 0

平衡問題

容易看到,二叉樹可能出現一個極端狀態,即全部節點的子節點數均不超過 1 ,此時的樹實際上就是鏈表。而其深度爲 N-1.

所以使一棵樹擁有儘量低的深度,對於查找性能來講顯得尤其重要,這個優化的過程便稱爲平衡,即取,使每一個節點的左右子樹的深度儘量平衡之意。

平衡是個很麻煩的事情,它意味着每次更新操做後都有可能要變動樹的結構。此過程稱爲旋轉(ratation)。如 AVL 樹,要求每一個節點的左子樹和右子樹的高度最多差 1.

B 樹

B-樹的 B 並無什麼肯定的意思,尤爲不要理解爲 Binary,由於通常它並不實現爲二叉樹,而是多路樹。

階爲 M 的 B 樹是這樣一種樹:

  • 其根節點要麼沒有子節點,要麼子節點數在 2 ~ M 之間
  • 除根外,全部內部節點(非葉子節點)的子節點數在 M/2 ~ M 之間
  • 全部的葉子節點都擁有相同深度
  • 全部的數據都存儲在葉子上

這個定義看起來不是很直觀,能夠這樣理解:

M 階 B樹的數據部分是一個序列。除了這個序列外,還額外存在一個樹型結構,用於索引這個序列。具體方法爲:序列從前向後每 M 個元素便向上生出一個父節點,父節點的值等於這 M 個元素的最小值(或最大值,取決於序列的排序),而後這批父節點再每 M 個向上生出次級父節點,如此反覆直到生出根來。

固然,實際 B樹的生成過程是正好相反的。也相應的有更多須要考慮的問題。

B樹每一個節點的子節點數均小於等於 M。另外爲了儘可能下降樹的深度,咱們還規定內部節點的子節點數需大於等於 M/2 (ceil),當不知足此條件時,就要將子節點合併。

由此咱們能夠算出,M 階 B樹的深度最小能夠爲 Log<sub>M</sub>N,最大也不過是 Log<sub>M/2</sub>N。另外由於每一個節點最多有 M 個子節點,因此一次節點內分支選擇的複雜度爲 LogM。故搜索的複雜度爲 (M/2)Log<sub>M/2</sub>N,可化簡爲 O(LogN)。增刪操做可能須要 O(M) 的時間來調整節點數據,所以增刪操做的複雜度爲 O(MLog<sub>M</sub>N) ,可化簡爲 O((M/LogM)LogN)

M 的選擇

由前面的增刪操做複雜度公式前的常數部分 M/LogM 可得 M 的最佳值爲 (2, 3, 4),當再高時,插入效率會變低。而搜索複雜度與 M 無關。

B樹也是數據庫經常使用的一種索引,所以 M 的選擇更多會參考實際的應用場景。如存儲在機械硬盤上的 SQL 數據庫,硬盤尋址操做的開銷比連續讀要高得多,所以使內部節點所佔空間儘可能接近單扇區可用容量是最好的作法。如 512 字節的扇區容量,每一個節點元素佔 4 字節的話,M 就能夠設置爲 128。對於 SSD,或者更大扇區的磁盤,這個數字均可作相應調整。

相關文章
相關標籤/搜索