Skip to content

基礎的樹型資料結構

目錄
介紹
Binary tree
Binary search tree

關於Tree

樹是一種常見的資料結構,如生活中可用於表示父系族譜、電腦中可以表示檔案系統
除此之外也是加速搜尋的一種資料結構

樹的定義

樹是一種圖,所以具有點集合、邊集合
樹有兩個限制

  1. 樹是一個連通圖
  2. 樹不會有Cycle

這兩個定義可以導出一些性質
比如: 任何兩點間只會且只會有一條path、 樹是邊最少的連通圖…

可以用父系族譜來理解
任何一個被寫在族譜上的人,一定跟其他人有關係(條件1)
並且不可能有人同時是兩個父親的兒子(條件2)

舉例:

Binary Tree

比樹多加了一層限制 : 每個節點最多只會有兩個子節點
上面的例子中,右一就不是binary tree,右二、右三則符合binary tree

Traversal

任意一張圖都可以用BFS、DFS來遍歷,樹也不例外
但由於樹的特性,使我們可以用以下四種方式遍歷,並得到不同的順序

在此我們規定一律先探索左邊、再探索右邊
並以下圖為範例

一. PreOrder
先到先做事 (做事可以是讀取節點值、修改節點、刪除節點 ….等操作)
然後才依序拜訪子節點

做事順序為: A、B、C、D(C做完退回B、然後往右走)、E(B做完退回C、退回A…)

二. InOrder
第一(左)子節點做完事,才做事,然後再拜訪右節點

做事順序為: C (因為無左子節點==左子節點完事)、B、D、A、E

三. PostOrder
依序拜訪子節點 、節點都做完事、才做事

做事順序為: C、D、B、E、A

Note 這三種traversal都是固定順序拜訪節點的,只是做事時機不同
因此在遞迴時調整 traversal(左節點) traversal(右節點) DoSomething() 順序即可

Note2 因為固定都是下圖的順序

因此可以想像每個節點有個旗子,pre在節點右邊;in在節點下方;post在節點左邊
並依這個箭頭順序,碰到旗子,就是做事時機

四.level-order
就是BFS

Binary Search Tree

在binary tree的條件下,加上"比自己值大的節點在自己的右方、比自己值小的節點在自己左方”
下圖是個例子: 可以驗證看看

特化搜尋

這個資料結構如其名,就是為了搜尋效率而生,並且類似於binary search
以上圖為例
要搜尋9
首先問10,因為比10小,所以排除了比10大的右邊所有節點,如果這棵樹夠"平衡”(左右兩側所有節點數目相近),則幾乎少搜尋一半的數量

而最糟情況下,BST只需要O(樹高)的時間完成搜尋

平衡的重要性

完美的平衡狀態是在任意節點的左子節點 == 右子節點
實務上可以差一個節點的高度 (考慮null)

此時樹高將會是logn
因此搜尋複雜度為O(樹高) == O(logn)

但是BST也可能依資料輸入順序導致形狀不同
比如 輸入 5 2 3 1 7 6 8
會是完美的平衡狀態

但 輸入 1 2 3 4 5 6 7 8
則會退化成1維的linked list,此時樹高==n,造成搜尋複雜度降為O(n)

因此以下的變體都是在追求樹的平衡

旋轉

以下變體都會用到的操作-旋轉

旋轉可以在改變形狀(就有機會改變樹高)的同時,不破壞BST的性質

舉例而言

可以看到直觀上y點往上旋轉
仔細看則是y與x父子角色對調 同時B過繼給x

  • 不破壞其特性

原因大小順序沒有破壞
仍是 A中所有節點 < y < B中所有節點 < x < C中所有節點

直覺上,用一條線從左掃到右邊,只要相對位置沒有改變,怎麼旋轉都沒有關係

  • 實作

實作上會用指標的調換

#維護父指標  
y->parent = x->parent;  
x->parent = y ; 
B->parent = x ;  # 過繼  

#維護子指標  
y->right = x ;
x->left = B; 

左旋 右旋

上述例子是一個右旋,因為將節點往右上翻過去
而左旋則是相對右旋,基本上是對稱的操作

想像如果將linked list中間的點進行左旋,將會接近平衡狀態
就能改善樹高問題

操作的時間複雜度

插入: O(樹高) 最糟情況為插入點在leaf
搜尋: O(樹高)
刪除
依目標節點的子樹存在分為三種情況:

  1. 沒有子樹 - 直接改為Null即可
  2. 只有一邊有子樹 - 直接把child取代原節點即可
  3. 有兩棵子樹-
    這種情況最麻煩,不能單純選一邊節點取代自己
    因為不保證兩個子樹一定是中點
    舉例如下

因此需要找 子樹中的極端值做中點
例如左子樹的最大值、右子樹的最小值

因為左子樹的最大值一定比右子樹都小
並且左子樹也不需要更動
右子樹最小值同理

刪除時間複雜度因此為O(2*樹高)

AVL tree

為BST加上條件: 左右子樹最高差不能超過1
這樣可以保證搜尋最佳化 (O(logn) n是節點數)

平衡因子 (Balance Factor)

用來記錄此節點左右子樹高差
我們這邊定義為 左樹高 - 右樹高
因此若範圍在 -1,0,+1 都是符合條件的

常見對樹的操作為插入、讀取、刪除、搜尋
很顯然唯讀型的讀取、搜尋 不可能破壞AVL tree性質

而插入或刪除則有可能造成左右兩側差距變大,因此我們先討論插入的情況

插入新節點

可以遞迴的定義插入AVL tree的情況:

Base Case :
樹符合AVL tree 條件

Recursive Case:
已知原本樹符合AVL tree條件
插入新節點後,會有下列三種情況

  1. 原本BF=0 ,插入新節點會變成 -1 or +1
  2. 原本BF=+1 or -1 ,插入新節點會變成 0
  3. 原本BF=+1 or -1 ,插入新節點會變成 +2 or -2

很明顯地,我們只需要解決第三種情況,就可以依數學歸納法保證插入一定
不會破壞AVL tree 性質

Case 3的四種情況

探討BF違反的最低處節點,可以分為四種情況

  1. LL (不平衡的節點被插入在)在左子節點的左邊
  2. RR 右子節點的右邊
    這兩種情況很單純,只要把問題子節點透過旋轉拉上來即可

以下為LL例子

Note
為甚麼在意問題的子節點?旋轉的標的也是?
想想一根桿子,要拉成一半ㄑ型,必須從中點,子節點就是那個中點

  1. LR 左子節點的右邊
  2. RL 右子節點的左邊

這兩種情況就需要對子節點的子節點做兩次旋轉
以LR為例,就要先與子節點做右璇
然後再與問題最低點做左旋

原因:先把LR問題或RL問題歸化成LL問題或RR問題,再依法I解決即可

刪除節點基本上就是
結合BST的刪除法,加上AVL的調整

時間複雜度

由此可見,搜尋一定可以維持在O(logn)的良好狀態

但犧牲的成本是每次插入、刪除要花常數時間 旋轉
O(logn+k)
k是rotation時間

red-black tree