JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

樹:分叉的結構

到目前為止,你的資料都活在一條線上。**[[tree|樹]]** 讓它分叉——一個元素指向好幾個別的,就像家譜向外擴散,或資料夾裡裝著資料夾。這篇導讀給你詞彙(根、父、子、葉、高度、深度),展示至關重要的 **[[binary-tree|二元樹]]**,並用你已經會的 **[[dsa-recursion|遞迴]]** 以三種經典順序走遍每個節點。

從一條線到一根枝

陣列、鏈結串列、堆疊、佇列——到目前為止每一種結構都是線性的:每個元素後面最多只有一個元素。但現實裡許多東西並不排成一隊。一個資料夾裝著好幾個資料夾,每個裡面又裝著更多。公司的組織架構有一個老闆,下面是幾名直接下屬,再下面是他們的下屬。一本書分成章,章又分成節。這一切底下的形狀,就是 [[tree|樹]]:一個節點指向好幾個*孩子*,而每個孩子本身又是一棵更小的樹的頂端。

注意計算機科學裡這個有趣的顛倒:我們把樹畫成倒過來的。那個唯一的起點節點坐在最上面,叫作;枝條朝*下*生長,伸向葉子。就是這樣——樹不過是用邊連起來的一堆節點,頂上有一個特殊節點,而且任何地方都沒有環(順著邊往下走,你永遠回不到出發點)。

          (1)        <- root
         /   \
      (2)     (3)
      / \       \
   (4) (5)      (6)   <- leaves: 4, 5, 6

  edges go DOWNWARD; 1 is the parent of 2 and 3.
計算機科學家畫樹的方式:根在上,葉在下。

你會用到的詞

樹自帶一套小小的親屬詞彙,值得現在就釘牢,因為後面每一篇導讀都要靠它。

  1. — 頂端那個唯一的節點,是唯一沒有父節點的。
  2. 父 / 子 — 若有一條邊從 A 向下連到 B,則 A 是 B 的*父*,B 是 A 的*子*。除根以外,每個節點都恰好有一個父節點。
  3. — 沒有孩子的節點;枝條的末端。
  4. 節點的深度 — 它距離根有幾條邊(根的深度為 0)。
  5. 樹的高度 — 它最深那片葉子的深度;整棵樹有幾層那麼高。

二元樹:最多兩個孩子

一般的樹允許一個節點有任意多個孩子,但貫穿這一整級台階的主力是 [[binary-tree|二元樹]]:每個節點最多有兩個孩子,整整齊齊地叫作*左*和*右*。這個俐落的限制讓二元樹既易於儲存,又(如後面幾篇所示)在查找和排優先級上強得驚人。在 C++ 裡,一個節點不過是個小小的結構體,裝著一個值和兩個指針——而某個孩子是 `nullptr`,只表示「這邊沒有枝」。

struct Node {
    int value;
    Node* left  = nullptr;   // child on the left, or nullptr
    Node* right = nullptr;   // child on the right, or nullptr
};

// build a tiny tree:    (1)
//                      /   \
//                   (2)     (3)
Node* root  = new Node{1};
root->left  = new Node{2};
root->right = new Node{3};
一個二元樹節點,以及手工搭出的三節點樹。孩子為 nullptr 表示「枝到此為止」。

用遞迴走遍這棵樹

你要怎麼*訪問樹裡的每一個節點*?這正是上一級學的 [[dsa-recursion|遞迴]] 大放異彩的地方。關鍵洞見:二元樹本身就是*遞迴定義*的——它是一個節點,加上一棵左子樹,加上一棵右子樹,而每棵子樹本身又是一棵二元樹。於是程式碼恰好照著這個形狀寫。要處理一棵樹,你就處理這個節點,再遞迴進入每個孩子;基準情形是空樹(`nullptr`),這時你什麼也不做、直接返回。

唯一要選的,是你*在什麼時候*處理這個節點——相對於它的孩子而言。在孩子之前處理 → *前序*。在左、右之間 → *中序*。在兩個孩子之後 → *後序*。同樣的三行,只是換了順序——而每種順序各有用處(下一篇你會看到,中序能把一棵搜索樹按有序讀出來)。

void inOrder(Node* n) {
    if (n == nullptr) return;     // base case: empty subtree
    inOrder(n->left);             // 1. all of the left subtree
    std::cout << n->value << ' '; // 2. then THIS node
    inOrder(n->right);            // 3. then all of the right subtree
}
// pre-order:  visit node BEFORE the two recursive calls
// post-order: visit node AFTER  the two recursive calls

// for the tree   (2)              in-order  -> 1 2 3
//               /   \             pre-order -> 2 1 3
//            (1)     (3)          post-order-> 1 3 2
中序遍歷。把 cout 那行上移或下移,就得到前序或後序。每個節點訪問一次:O(n)。