feat: Traditional Chinese version (#1163)

* First commit

* Update mkdocs.yml

* Translate all the docs to traditional Chinese

* Translate the code files.

* Translate the docker file

* Fix mkdocs.yml

* Translate all the figures from SC to TC

* 二叉搜尋樹 -> 二元搜尋樹

* Update terminology.

* Update terminology

* 构造函数/构造方法 -> 建構子
异或 -> 互斥或

* 擴充套件 -> 擴展

* constant - 常量 - 常數

* 類	-> 類別

* AVL -> AVL 樹

* 數組 -> 陣列

* 係統 -> 系統
斐波那契數列 -> 費波那契數列
運算元量 -> 運算量
引數 -> 參數

* 聯絡 -> 關聯

* 麵試 -> 面試

* 面向物件 -> 物件導向
歸併排序 -> 合併排序
范式 -> 範式

* Fix 算法 -> 演算法

* 錶示 -> 表示
反碼 -> 一補數
補碼 -> 二補數
列列尾部 -> 佇列尾部
區域性性 -> 區域性
一摞 -> 一疊

* Synchronize with main branch

* 賬號 -> 帳號
推匯 -> 推導

* Sync with main branch

* First commit

* Update mkdocs.yml

* Translate all the docs to traditional Chinese

* Translate the code files.

* Translate the docker file

* Fix mkdocs.yml

* Translate all the figures from SC to TC

* 二叉搜尋樹 -> 二元搜尋樹

* Update terminology

* 构造函数/构造方法 -> 建構子
异或 -> 互斥或

* 擴充套件 -> 擴展

* constant - 常量 - 常數

* 類	-> 類別

* AVL -> AVL 樹

* 數組 -> 陣列

* 係統 -> 系統
斐波那契數列 -> 費波那契數列
運算元量 -> 運算量
引數 -> 參數

* 聯絡 -> 關聯

* 麵試 -> 面試

* 面向物件 -> 物件導向
歸併排序 -> 合併排序
范式 -> 範式

* Fix 算法 -> 演算法

* 錶示 -> 表示
反碼 -> 一補數
補碼 -> 二補數
列列尾部 -> 佇列尾部
區域性性 -> 區域性
一摞 -> 一疊

* Synchronize with main branch

* 賬號 -> 帳號
推匯 -> 推導

* Sync with main branch

* Update terminology.md

* 操作数量(num. of operations)-> 操作數量

* 字首和->前綴和

* Update figures

* 歸 -> 迴
記憶體洩漏 -> 記憶體流失

* Fix the bug of the file filter

* 支援 -> 支持
Add zh-Hant/README.md

* Add the zh-Hant chapter covers.
Bug fixes.

* 外掛 -> 擴充功能

* Add the landing page for zh-Hant version

* Unify the font of the chapter covers for the zh, en, and zh-Hant version

* Move zh-Hant/ to zh-hant/

* Translate terminology.md to traditional Chinese
This commit is contained in:
Yudong Jin
2024-04-06 02:30:11 +08:00
committed by GitHub
parent 33d7f8a2e5
commit 5f7385c8a3
1875 changed files with 102923 additions and 18 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,164 @@
# 二元樹陣列表示
在鏈結串列表示下,二元樹的儲存單元為節點 `TreeNode` ,節點之間透過指標相連線。上一節介紹了鏈結串列表示下的二元樹的各項基本操作。
那麼,我們能否用陣列來表示二元樹呢?答案是肯定的。
## 表示完美二元樹
先分析一個簡單案例。給定一棵完美二元樹,我們將所有節點按照層序走訪的順序儲存在一個陣列中,則每個節點都對應唯一的陣列索引。
根據層序走訪的特性,我們可以推導出父節點索引與子節點索引之間的“對映公式”:**若某節點的索引為 $i$ ,則該節點的左子節點索引為 $2i + 1$ ,右子節點索引為 $2i + 2$** 。下圖展示了各個節點索引之間的對映關係。
![完美二元樹的陣列表示](array_representation_of_tree.assets/array_representation_binary_tree.png)
**對映公式的角色相當於鏈結串列中的節點引用(指標)**。給定陣列中的任意一個節點,我們都可以透過對映公式來訪問它的左(右)子節點。
## 表示任意二元樹
完美二元樹是一個特例,在二元樹的中間層通常存在許多 `None` 。由於層序走訪序列並不包含這些 `None` ,因此我們無法僅憑該序列來推測 `None` 的數量和分佈位置。**這意味著存在多種二元樹結構都符合該層序走訪序列**。
如下圖所示,給定一棵非完美二元樹,上述陣列表示方法已經失效。
![層序走訪序列對應多種二元樹可能性](array_representation_of_tree.assets/array_representation_without_empty.png)
為了解決此問題,**我們可以考慮在層序走訪序列中顯式地寫出所有 `None`** 。如下圖所示,這樣處理後,層序走訪序列就可以唯一表示二元樹了。示例程式碼如下:
=== "Python"
```python title=""
# 二元樹的陣列表示
# 使用 None 來表示空位
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```
=== "C++"
```cpp title=""
/* 二元樹的陣列表示 */
// 使用 int 最大值 INT_MAX 標記空位
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```
=== "Java"
```java title=""
/* 二元樹的陣列表示 */
// 使用 int 的包裝類別 Integer ,就可以使用 null 來標記空位
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
=== "C#"
```csharp title=""
/* 二元樹的陣列表示 */
// 使用 int? 可空型別 ,就可以使用 null 來標記空位
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Go"
```go title=""
/* 二元樹的陣列表示 */
// 使用 any 型別的切片, 就可以使用 nil 來標記空位
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
```
=== "Swift"
```swift title=""
/* 二元樹的陣列表示 */
// 使用 Int? 可空型別 ,就可以使用 nil 來標記空位
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```
=== "JS"
```javascript title=""
/* 二元樹的陣列表示 */
// 使用 null 來表示空位
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "TS"
```typescript title=""
/* 二元樹的陣列表示 */
// 使用 null 來表示空位
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Dart"
```dart title=""
/* 二元樹的陣列表示 */
// 使用 int? 可空型別 ,就可以使用 null 來標記空位
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Rust"
```rust title=""
/* 二元樹的陣列表示 */
// 使用 None 來標記空位
let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];
```
=== "C"
```c title=""
/* 二元樹的陣列表示 */
// 使用 int 最大值標記空位,因此要求節點值不能為 INT_MAX
int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```
=== "Kotlin"
```kotlin title=""
/* 二元樹的陣列表示 */
// 使用 null 來表示空位
val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
```
![任意型別二元樹的陣列表示](array_representation_of_tree.assets/array_representation_with_empty.png)
值得說明的是,**完全二元樹非常適合使用陣列來表示**。回顧完全二元樹的定義,`None` 只出現在最底層且靠右的位置,**因此所有 `None` 一定出現在層序走訪序列的末尾**。
這意味著使用陣列表示完全二元樹時,可以省略儲存所有 `None` ,非常方便。下圖給出了一個例子。
![完全二元樹的陣列表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png)
以下程式碼實現了一棵基於陣列表示的二元樹,包括以下幾種操作。
- 給定某節點,獲取它的值、左(右)子節點、父節點。
- 獲取前序走訪、中序走訪、後序走訪、層序走訪序列。
```src
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
```
## 優點與侷限性
二元樹的陣列表示主要有以下優點。
- 陣列儲存在連續的記憶體空間中,對快取友好,訪問與走訪速度較快。
- 不需要儲存指標,比較節省空間。
- 允許隨機訪問節點。
然而,陣列表示也存在一些侷限性。
- 陣列儲存需要連續記憶體空間,因此不適合儲存資料量過大的樹。
- 增刪節點需要透過陣列插入與刪除操作實現,效率較低。
- 當二元樹中存在大量 `None` 時,陣列中包含的節點資料比重較低,空間利用率較低。
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+353
View File
@@ -0,0 +1,353 @@
# AVL 樹 *
在“二元搜尋樹”章節中我們提到,在多次插入和刪除操作後,二元搜尋樹可能退化為鏈結串列。在這種情況下,所有操作的時間複雜度將從 $O(\log n)$ 劣化為 $O(n)$ 。
如下圖所示,經過兩次刪除節點操作,這棵二元搜尋樹便會退化為鏈結串列。
![AVL 樹在刪除節點後發生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
再例如,在下圖所示的完美二元樹中插入兩個節點後,樹將嚴重向左傾斜,查詢操作的時間複雜度也隨之劣化。
![AVL 樹在插入節點後發生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在論文“An algorithm for the organization of information”中提出了 <u>AVL 樹</u>。論文中詳細描述了一系列操作,確保在持續新增和刪除節點後,AVL 樹不會退化,從而使得各種操作的時間複雜度保持在 $O(\log n)$ 級別。換句話說,在需要頻繁進行增刪查改操作的場景中,AVL 樹能始終保持高效的資料操作效能,具有很好的應用價值。
## AVL 樹常見術語
AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二元樹的所有性質,因此是一種<u>平衡二元搜尋樹(balanced binary search tree</u>。
### 節點高度
由於 AVL 樹的相關操作需要獲取節點高度,因此我們需要為節點類別新增 `height` 變數:
=== "Python"
```python title=""
class TreeNode:
"""AVL 樹節點類別"""
def __init__(self, val: int):
self.val: int = val # 節點值
self.height: int = 0 # 節點高度
self.left: TreeNode | None = None # 左子節點引用
self.right: TreeNode | None = None # 右子節點引用
```
=== "C++"
```cpp title=""
/* AVL 樹節點類別 */
struct TreeNode {
int val{}; // 節點值
int height = 0; // 節點高度
TreeNode *left{}; // 左子節點
TreeNode *right{}; // 右子節點
TreeNode() = default;
explicit TreeNode(int x) : val(x){}
};
```
=== "Java"
```java title=""
/* AVL 樹節點類別 */
class TreeNode {
public int val; // 節點值
public int height; // 節點高度
public TreeNode left; // 左子節點
public TreeNode right; // 右子節點
public TreeNode(int x) { val = x; }
}
```
=== "C#"
```csharp title=""
/* AVL 樹節點類別 */
class TreeNode(int? x) {
public int? val = x; // 節點值
public int height; // 節點高度
public TreeNode? left; // 左子節點引用
public TreeNode? right; // 右子節點引用
}
```
=== "Go"
```go title=""
/* AVL 樹節點結構體 */
type TreeNode struct {
Val int // 節點值
Height int // 節點高度
Left *TreeNode // 左子節點引用
Right *TreeNode // 右子節點引用
}
```
=== "Swift"
```swift title=""
/* AVL 樹節點類別 */
class TreeNode {
var val: Int // 節點值
var height: Int // 節點高度
var left: TreeNode? // 左子節點
var right: TreeNode? // 右子節點
init(x: Int) {
val = x
height = 0
}
}
```
=== "JS"
```javascript title=""
/* AVL 樹節點類別 */
class TreeNode {
val; // 節點值
height; //節點高度
left; // 左子節點指標
right; // 右子節點指標
constructor(val, left, right, height) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "TS"
```typescript title=""
/* AVL 樹節點類別 */
class TreeNode {
val: number; // 節點值
height: number; // 節點高度
left: TreeNode | null; // 左子節點指標
right: TreeNode | null; // 右子節點指標
constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "Dart"
```dart title=""
/* AVL 樹節點類別 */
class TreeNode {
int val; // 節點值
int height; // 節點高度
TreeNode? left; // 左子節點
TreeNode? right; // 右子節點
TreeNode(this.val, [this.height = 0, this.left, this.right]);
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* AVL 樹節點結構體 */
struct TreeNode {
val: i32, // 節點值
height: i32, // 節點高度
left: Option<Rc<RefCell<TreeNode>>>, // 左子節點
right: Option<Rc<RefCell<TreeNode>>>, // 右子節點
}
impl TreeNode {
/* 建構子 */
fn new(val: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
val,
height: 0,
left: None,
right: None
}))
}
}
```
=== "C"
```c title=""
/* AVL 樹節點結構體 */
TreeNode struct TreeNode {
int val;
int height;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
/* 建構子 */
TreeNode *newTreeNode(int val) {
TreeNode *node;
node = (TreeNode *)malloc(sizeof(TreeNode));
node->val = val;
node->height = 0;
node->left = NULL;
node->right = NULL;
return node;
}
```
=== "Kotlin"
```kotlin title=""
/* AVL 樹節點類別 */
class TreeNode(val _val: Int) { // 節點值
val height: Int = 0 // 節點高度
val left: TreeNode? = null // 左子節點
val right: TreeNode? = null // 右子節點
}
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
```
“節點高度”是指從該節點到它的最遠葉節點的距離,即所經過的“邊”的數量。需要特別注意的是,葉節點的高度為 $0$ ,而空節點的高度為 $-1$ 。我們將建立兩個工具函式,分別用於獲取和更新節點的高度:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{update_height}
```
### 節點平衡因子
節點的<u>平衡因子(balance factor</u>定義為節點左子樹的高度減去右子樹的高度,同時規定空節點的平衡因子為 $0$ 。我們同樣將獲取節點平衡因子的功能封裝成函式,方便後續使用:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{balance_factor}
```
!!! note
設平衡因子為 $f$ ,則一棵 AVL 樹的任意節點的平衡因子皆滿足 $-1 \le f \le 1$ 。
## AVL 樹旋轉
AVL 樹的特點在於“旋轉”操作,它能夠在不影響二元樹的中序走訪序列的前提下,使失衡節點重新恢復平衡。換句話說,**旋轉操作既能保持“二元搜尋樹”的性質,也能使樹重新變為“平衡二元樹”**。
我們將平衡因子絕對值 $> 1$ 的節點稱為“失衡節點”。根據節點失衡情況的不同,旋轉操作分為四種:右旋、左旋、先右旋後左旋、先左旋後右旋。下面詳細介紹這些旋轉操作。
### 右旋
如下圖所示,節點下方為平衡因子。從底至頂看,二元樹中首個失衡節點是“節點 3”。我們關注以該失衡節點為根節點的子樹,將該節點記為 `node` ,其左子節點記為 `child` ,執行“右旋”操作。完成右旋後,子樹恢復平衡,並且仍然保持二元搜尋樹的性質。
=== "<1>"
![右旋操作步驟](avl_tree.assets/avltree_right_rotate_step1.png)
=== "<2>"
![avltree_right_rotate_step2](avl_tree.assets/avltree_right_rotate_step2.png)
=== "<3>"
![avltree_right_rotate_step3](avl_tree.assets/avltree_right_rotate_step3.png)
=== "<4>"
![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png)
如下圖所示,當節點 `child` 有右子節點(記為 `grand_child` )時,需要在右旋中新增一步:將 `grand_child` 作為 `node` 的左子節點。
![有 grand_child 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
“向右旋轉”是一種形象化的說法,實際上需要透過修改節點指標來實現,程式碼如下所示:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
```
### 左旋
相應地,如果考慮上述失衡二元樹的“映象”,則需要執行下圖所示的“左旋”操作。
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
同理,如下圖所示,當節點 `child` 有左子節點(記為 `grand_child` )時,需要在左旋中新增一步:將 `grand_child` 作為 `node` 的右子節點。
![有 grand_child 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
可以觀察到,**右旋和左旋操作在邏輯上是映象對稱的,它們分別解決的兩種失衡情況也是對稱的**。基於對稱性,我們只需將右旋的實現程式碼中的所有的 `left` 替換為 `right` ,將所有的 `right` 替換為 `left` ,即可得到左旋的實現程式碼:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
```
### 先左旋後右旋
對於下圖中的失衡節點 3 ,僅使用左旋或右旋都無法使子樹恢復平衡。此時需要先對 `child` 執行“左旋”,再對 `node` 執行“右旋”。
![先左旋後右旋](avl_tree.assets/avltree_left_right_rotate.png)
### 先右旋後左旋
如下圖所示,對於上述失衡二元樹的映象情況,需要先對 `child` 執行“右旋”,再對 `node` 執行“左旋”。
![先右旋後左旋](avl_tree.assets/avltree_right_left_rotate.png)
### 旋轉的選擇
下圖展示的四種失衡情況與上述案例逐個對應,分別需要採用右旋、先左旋後右旋、先右旋後左旋、左旋的操作。
![AVL 樹的四種旋轉情況](avl_tree.assets/avltree_rotation_cases.png)
如下表所示,我們透過判斷失衡節點的平衡因子以及較高一側子節點的平衡因子的正負號,來確定失衡節點屬於上圖中的哪種情況。
<p align="center"> 表 <id> &nbsp; 四種旋轉情況的選擇條件 </p>
| 失衡節點的平衡因子 | 子節點的平衡因子 | 應採用的旋轉方法 |
| ------------------ | ---------------- | ---------------- |
| $> 1$ (左偏樹) | $\geq 0$ | 右旋 |
| $> 1$ (左偏樹) | $<0$ | 先左旋後右旋 |
| $< -1$ (右偏樹) | $\leq 0$ | 左旋 |
| $< -1$ (右偏樹) | $>0$ | 先右旋後左旋 |
為了便於使用,我們將旋轉操作封裝成一個函式。**有了這個函式,我們就能對各種失衡情況進行旋轉,使失衡節點重新恢復平衡**。程式碼如下所示:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
```
## AVL 樹常用操作
### 插入節點
AVL 樹的節點插入操作與二元搜尋樹在主體上類似。唯一的區別在於,在 AVL 樹中插入節點後,從該節點到根節點的路徑上可能會出現一系列失衡節點。因此,**我們需要從這個節點開始,自底向上執行旋轉操作,使所有失衡節點恢復平衡**。程式碼如下所示:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
```
### 刪除節點
類似地,在二元搜尋樹的刪除節點方法的基礎上,需要從底至頂執行旋轉操作,使所有失衡節點恢復平衡。程式碼如下所示:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
```
### 查詢節點
AVL 樹的節點查詢操作與二元搜尋樹一致,在此不再贅述。
## AVL 樹典型應用
- 組織和儲存大型資料,適用於高頻查詢、低頻增刪的場景。
- 用於構建資料庫中的索引系統。
- 紅黑樹也是一種常見的平衡二元搜尋樹。相較於 AVL 樹,紅黑樹的平衡條件更寬鬆,插入與刪除節點所需的旋轉操作更少,節點增刪操作的平均效率更高。
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+129
View File
@@ -0,0 +1,129 @@
# 二元搜尋樹
如下圖所示,<u>二元搜尋樹(binary search tree</u>滿足以下條件。
1. 對於根節點,左子樹中所有節點的值 $<$ 根節點的值 $<$ 右子樹中所有節點的值。
2. 任意節點的左、右子樹也是二元搜尋樹,即同樣滿足條件 `1.`
![二元搜尋樹](binary_search_tree.assets/binary_search_tree.png)
## 二元搜尋樹的操作
我們將二元搜尋樹封裝為一個類別 `BinarySearchTree` ,並宣告一個成員變數 `root` ,指向樹的根節點。
### 查詢節點
給定目標節點值 `num` ,可以根據二元搜尋樹的性質來查詢。如下圖所示,我們宣告一個節點 `cur` ,從二元樹的根節點 `root` 出發,迴圈比較節點值 `cur.val``num` 之間的大小關係。
-`cur.val < num` ,說明目標節點在 `cur` 的右子樹中,因此執行 `cur = cur.right`
-`cur.val > num` ,說明目標節點在 `cur` 的左子樹中,因此執行 `cur = cur.left`
-`cur.val = num` ,說明找到目標節點,跳出迴圈並返回該節點。
=== "<1>"
![二元搜尋樹查詢節點示例](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
=== "<3>"
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png)
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
二元搜尋樹的查詢操作與二分搜尋演算法的工作原理一致,都是每輪排除一半情況。迴圈次數最多為二元樹的高度,當二元樹平衡時,使用 $O(\log n)$ 時間。示例程式碼如下:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
```
### 插入節點
給定一個待插入元素 `num` ,為了保持二元搜尋樹“左子樹 < 根節點 < 右子樹”的性質,插入操作流程如下圖所示。
1. **查詢插入位置**:與查詢操作相似,從根節點出發,根據當前節點值和 `num` 的大小關係迴圈向下搜尋,直到越過葉節點(走訪至 `None` )時跳出迴圈。
2. **在該位置插入節點**:初始化節點 `num` ,將該節點置於 `None` 的位置。
![在二元搜尋樹中插入節點](binary_search_tree.assets/bst_insert.png)
在程式碼實現中,需要注意以下兩點。
- 二元搜尋樹不允許存在重複節點,否則將違反其定義。因此,若待插入節點在樹中已存在,則不執行插入,直接返回。
- 為了實現插入節點,我們需要藉助節點 `pre` 儲存上一輪迴圈的節點。這樣在走訪至 `None` 時,我們可以獲取到其父節點,從而完成節點插入操作。
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
```
與查詢節點相同,插入節點使用 $O(\log n)$ 時間。
### 刪除節點
先在二元樹中查詢到目標節點,再將其刪除。與插入節點類似,我們需要保證在刪除操作完成後,二元搜尋樹的“左子樹 < 根節點 < 右子樹”的性質仍然滿足。因此,我們根據目標節點的子節點數量,分 0、1 和 2 三種情況,執行對應的刪除節點操作。
如下圖所示,當待刪除節點的度為 $0$ 時,表示該節點是葉節點,可以直接刪除。
![在二元搜尋樹中刪除節點(度為 0 ](binary_search_tree.assets/bst_remove_case1.png)
如下圖所示,當待刪除節點的度為 $1$ 時,將待刪除節點替換為其子節點即可。
![在二元搜尋樹中刪除節點(度為 1 ](binary_search_tree.assets/bst_remove_case2.png)
當待刪除節點的度為 $2$ 時,我們無法直接刪除它,而需要使用一個節點替換該節點。由於要保持二元搜尋樹“左子樹 $<$ 根節點 $<$ 右子樹”的性質,**因此這個節點可以是右子樹的最小節點或左子樹的最大節點**。
假設我們選擇右子樹的最小節點(中序走訪的下一個節點),則刪除操作流程如下圖所示。
1. 找到待刪除節點在“中序走訪序列”中的下一個節點,記為 `tmp`
2.`tmp` 的值覆蓋待刪除節點的值,並在樹中遞迴刪除節點 `tmp`
=== "<1>"
![在二元搜尋樹中刪除節點(度為 2 ](binary_search_tree.assets/bst_remove_case3_step1.png)
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png)
=== "<3>"
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png)
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
刪除節點操作同樣使用 $O(\log n)$ 時間,其中查詢待刪除節點需要 $O(\log n)$ 時間,獲取中序走訪後繼節點需要 $O(\log n)$ 時間。示例程式碼如下:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
```
### 中序走訪有序
如下圖所示,二元樹的中序走訪遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的走訪順序,而二元搜尋樹滿足“左子節點 $<$ 根節點 $<$ 右子節點”的大小關係。
這意味著在二元搜尋樹中進行中序走訪時,總是會優先走訪下一個最小節點,從而得出一個重要性質:**二元搜尋樹的中序走訪序列是升序的**。
利用中序走訪升序的性質,我們在二元搜尋樹中獲取有序資料僅需 $O(n)$ 時間,無須進行額外的排序操作,非常高效。
![二元搜尋樹的中序走訪序列](binary_search_tree.assets/bst_inorder_traversal.png)
## 二元搜尋樹的效率
給定一組資料,我們考慮使用陣列或二元搜尋樹儲存。觀察下表,二元搜尋樹的各項操作的時間複雜度都是對數階,具有穩定且高效的效能。只有在高頻新增、低頻查詢刪除資料的場景下,陣列比二元搜尋樹的效率更高。
<p align="center"> 表 <id> &nbsp; 陣列與搜尋樹的效率對比 </p>
| | 無序陣列 | 二元搜尋樹 |
| -------- | -------- | ----------- |
| 查詢元素 | $O(n)$ | $O(\log n)$ |
| 插入元素 | $O(1)$ | $O(\log n)$ |
| 刪除元素 | $O(n)$ | $O(\log n)$ |
在理想情況下,二元搜尋樹是“平衡”的,這樣就可以在 $\log n$ 輪迴圈內查詢任意節點。
然而,如果我們在二元搜尋樹中不斷地插入和刪除節點,可能導致二元樹退化為下圖所示的鏈結串列,這時各種操作的時間複雜度也會退化為 $O(n)$ 。
![二元搜尋樹退化](binary_search_tree.assets/bst_degradation.png)
## 二元搜尋樹常見應用
- 用作系統中的多級索引,實現高效的查詢、插入、刪除操作。
- 作為某些搜尋演算法的底層資料結構。
- 用於儲存資料流,以保持其有序狀態。
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+662
View File
@@ -0,0 +1,662 @@
# 二元樹
<u>二元樹(binary tree</u>是一種非線性資料結構,代表“祖先”與“後代”之間的派生關係,體現了“一分為二”的分治邏輯。與鏈結串列類似,二元樹的基本單元是節點,每個節點包含值、左子節點引用和右子節點引用。
=== "Python"
```python title=""
class TreeNode:
"""二元樹節點類別"""
def __init__(self, val: int):
self.val: int = val # 節點值
self.left: TreeNode | None = None # 左子節點引用
self.right: TreeNode | None = None # 右子節點引用
```
=== "C++"
```cpp title=""
/* 二元樹節點結構體 */
struct TreeNode {
int val; // 節點值
TreeNode *left; // 左子節點指標
TreeNode *right; // 右子節點指標
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
```
=== "Java"
```java title=""
/* 二元樹節點類別 */
class TreeNode {
int val; // 節點值
TreeNode left; // 左子節點引用
TreeNode right; // 右子節點引用
TreeNode(int x) { val = x; }
}
```
=== "C#"
```csharp title=""
/* 二元樹節點類別 */
class TreeNode(int? x) {
public int? val = x; // 節點值
public TreeNode? left; // 左子節點引用
public TreeNode? right; // 右子節點引用
}
```
=== "Go"
```go title=""
/* 二元樹節點結構體 */
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
/* 建構子 */
func NewTreeNode(v int) *TreeNode {
return &TreeNode{
Left: nil, // 左子節點指標
Right: nil, // 右子節點指標
Val: v, // 節點值
}
}
```
=== "Swift"
```swift title=""
/* 二元樹節點類別 */
class TreeNode {
var val: Int // 節點值
var left: TreeNode? // 左子節點引用
var right: TreeNode? // 右子節點引用
init(x: Int) {
val = x
}
}
```
=== "JS"
```javascript title=""
/* 二元樹節點類別 */
class TreeNode {
val; // 節點值
left; // 左子節點指標
right; // 右子節點指標
constructor(val, left, right) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "TS"
```typescript title=""
/* 二元樹節點類別 */
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val; // 節點值
this.left = left === undefined ? null : left; // 左子節點引用
this.right = right === undefined ? null : right; // 右子節點引用
}
}
```
=== "Dart"
```dart title=""
/* 二元樹節點類別 */
class TreeNode {
int val; // 節點值
TreeNode? left; // 左子節點引用
TreeNode? right; // 右子節點引用
TreeNode(this.val, [this.left, this.right]);
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* 二元樹節點結構體 */
struct TreeNode {
val: i32, // 節點值
left: Option<Rc<RefCell<TreeNode>>>, // 左子節點引用
right: Option<Rc<RefCell<TreeNode>>>, // 右子節點引用
}
impl TreeNode {
/* 建構子 */
fn new(val: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
val,
left: None,
right: None
}))
}
}
```
=== "C"
```c title=""
/* 二元樹節點結構體 */
typedef struct TreeNode {
int val; // 節點值
int height; // 節點高度
struct TreeNode *left; // 左子節點指標
struct TreeNode *right; // 右子節點指標
} TreeNode;
/* 建構子 */
TreeNode *newTreeNode(int val) {
TreeNode *node;
node = (TreeNode *)malloc(sizeof(TreeNode));
node->val = val;
node->height = 0;
node->left = NULL;
node->right = NULL;
return node;
}
```
=== "Kotlin"
```kotlin title=""
/* 二元樹節點類別 */
class TreeNode(val _val: Int) { // 節點值
val left: TreeNode? = null // 左子節點引用
val right: TreeNode? = null // 右子節點引用
}
```
=== "Ruby"
```ruby title=""
```
=== "Zig"
```zig title=""
```
每個節點都有兩個引用(指標),分別指向<u>左子節點(left-child node</u>和<u>右子節點(right-child node</u>,該節點被稱為這兩個子節點的<u>父節點(parent node</u>。當給定一個二元樹的節點時,我們將該節點的左子節點及其以下節點形成的樹稱為該節點的<u>左子樹(left subtree</u>,同理可得<u>右子樹(right subtree</u>。
**在二元樹中,除葉節點外,其他所有節點都包含子節點和非空子樹**。如下圖所示,如果將“節點 2”視為父節點,則其左子節點和右子節點分別是“節點 4”和“節點 5”,左子樹是“節點 4 及其以下節點形成的樹”,右子樹是“節點 5 及其以下節點形成的樹”。
![父節點、子節點、子樹](binary_tree.assets/binary_tree_definition.png)
## 二元樹常見術語
二元樹的常用術語如下圖所示。
- <u>根節點(root node</u>:位於二元樹頂層的節點,沒有父節點。
- <u>葉節點(leaf node</u>:沒有子節點的節點,其兩個指標均指向 `None` 。
- <u>邊(edge</u>:連線兩個節點的線段,即節點引用(指標)。
- 節點所在的<u>層(level</u>:從頂至底遞增,根節點所在層為 1 。
- 節點的<u>度(degree</u>:節點的子節點的數量。在二元樹中,度的取值範圍是 0、1、2 。
- 二元樹的<u>高度(height</u>:從根節點到最遠葉節點所經過的邊的數量。
- 節點的<u>深度(depth</u>:從根節點到該節點所經過的邊的數量。
- 節點的<u>高度(height</u>:從距離該節點最遠的葉節點到該節點所經過的邊的數量。
![二元樹的常用術語](binary_tree.assets/binary_tree_terminology.png)
!!! tip
請注意,我們通常將“高度”和“深度”定義為“經過的邊的數量”,但有些題目或教材可能會將其定義為“經過的節點的數量”。在這種情況下,高度和深度都需要加 1 。
## 二元樹基本操作
### 初始化二元樹
與鏈結串列類似,首先初始化節點,然後構建引用(指標)。
=== "Python"
```python title="binary_tree.py"
# 初始化二元樹
# 初始化節點
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
n4 = TreeNode(val=4)
n5 = TreeNode(val=5)
# 構建節點之間的引用(指標)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "C++"
```cpp title="binary_tree.cpp"
/* 初始化二元樹 */
// 初始化節點
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);
// 構建節點之間的引用(指標)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Java"
```java title="binary_tree.java"
// 初始化節點
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 構建節點之間的引用(指標)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "C#"
```csharp title="binary_tree.cs"
/* 初始化二元樹 */
// 初始化節點
TreeNode n1 = new(1);
TreeNode n2 = new(2);
TreeNode n3 = new(3);
TreeNode n4 = new(4);
TreeNode n5 = new(5);
// 構建節點之間的引用(指標)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Go"
```go title="binary_tree.go"
/* 初始化二元樹 */
// 初始化節點
n1 := NewTreeNode(1)
n2 := NewTreeNode(2)
n3 := NewTreeNode(3)
n4 := NewTreeNode(4)
n5 := NewTreeNode(5)
// 構建節點之間的引用(指標)
n1.Left = n2
n1.Right = n3
n2.Left = n4
n2.Right = n5
```
=== "Swift"
```swift title="binary_tree.swift"
// 初始化節點
let n1 = TreeNode(x: 1)
let n2 = TreeNode(x: 2)
let n3 = TreeNode(x: 3)
let n4 = TreeNode(x: 4)
let n5 = TreeNode(x: 5)
// 構建節點之間的引用(指標)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "JS"
```javascript title="binary_tree.js"
/* 初始化二元樹 */
// 初始化節點
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// 構建節點之間的引用(指標)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "TS"
```typescript title="binary_tree.ts"
/* 初始化二元樹 */
// 初始化節點
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// 構建節點之間的引用(指標)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Dart"
```dart title="binary_tree.dart"
/* 初始化二元樹 */
// 初始化節點
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 構建節點之間的引用(指標)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Rust"
```rust title="binary_tree.rs"
// 初始化節點
let n1 = TreeNode::new(1);
let n2 = TreeNode::new(2);
let n3 = TreeNode::new(3);
let n4 = TreeNode::new(4);
let n5 = TreeNode::new(5);
// 構建節點之間的引用(指標)
n1.borrow_mut().left = Some(n2.clone());
n1.borrow_mut().right = Some(n3);
n2.borrow_mut().left = Some(n4);
n2.borrow_mut().right = Some(n5);
```
=== "C"
```c title="binary_tree.c"
/* 初始化二元樹 */
// 初始化節點
TreeNode *n1 = newTreeNode(1);
TreeNode *n2 = newTreeNode(2);
TreeNode *n3 = newTreeNode(3);
TreeNode *n4 = newTreeNode(4);
TreeNode *n5 = newTreeNode(5);
// 構建節點之間的引用(指標)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
// 初始化節點
val n1 = TreeNode(1)
val n2 = TreeNode(2)
val n3 = TreeNode(3)
val n4 = TreeNode(4)
val n5 = TreeNode(5)
// 構建節點之間的引用(指標)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "Ruby"
```ruby title="binary_tree.rb"
```
=== "Zig"
```zig title="binary_tree.zig"
```
??? pythontutor "視覺化執行"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### 插入與刪除節點
與鏈結串列類似,在二元樹中插入與刪除節點可以透過修改指標來實現。下圖給出了一個示例。
![在二元樹中插入與刪除節點](binary_tree.assets/binary_tree_add_remove.png)
=== "Python"
```python title="binary_tree.py"
# 插入與刪除節點
p = TreeNode(0)
# 在 n1 -> n2 中間插入節點 P
n1.left = p
p.left = n2
# 刪除節點 P
n1.left = n2
```
=== "C++"
```cpp title="binary_tree.cpp"
/* 插入與刪除節點 */
TreeNode* P = new TreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1->left = P;
P->left = n2;
// 刪除節點 P
n1->left = n2;
```
=== "Java"
```java title="binary_tree.java"
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1.left = P;
P.left = n2;
// 刪除節點 P
n1.left = n2;
```
=== "C#"
```csharp title="binary_tree.cs"
/* 插入與刪除節點 */
TreeNode P = new(0);
// 在 n1 -> n2 中間插入節點 P
n1.left = P;
P.left = n2;
// 刪除節點 P
n1.left = n2;
```
=== "Go"
```go title="binary_tree.go"
/* 插入與刪除節點 */
// 在 n1 -> n2 中間插入節點 P
p := NewTreeNode(0)
n1.Left = p
p.Left = n2
// 刪除節點 P
n1.Left = n2
```
=== "Swift"
```swift title="binary_tree.swift"
let P = TreeNode(x: 0)
// 在 n1 -> n2 中間插入節點 P
n1.left = P
P.left = n2
// 刪除節點 P
n1.left = n2
```
=== "JS"
```javascript title="binary_tree.js"
/* 插入與刪除節點 */
let P = new TreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1.left = P;
P.left = n2;
// 刪除節點 P
n1.left = n2;
```
=== "TS"
```typescript title="binary_tree.ts"
/* 插入與刪除節點 */
const P = new TreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1.left = P;
P.left = n2;
// 刪除節點 P
n1.left = n2;
```
=== "Dart"
```dart title="binary_tree.dart"
/* 插入與刪除節點 */
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1.left = P;
P.left = n2;
// 刪除節點 P
n1.left = n2;
```
=== "Rust"
```rust title="binary_tree.rs"
let p = TreeNode::new(0);
// 在 n1 -> n2 中間插入節點 P
n1.borrow_mut().left = Some(p.clone());
p.borrow_mut().left = Some(n2.clone());
// 刪除節點 p
n1.borrow_mut().left = Some(n2);
```
=== "C"
```c title="binary_tree.c"
/* 插入與刪除節點 */
TreeNode *P = newTreeNode(0);
// 在 n1 -> n2 中間插入節點 P
n1->left = P;
P->left = n2;
// 刪除節點 P
n1->left = n2;
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
val P = TreeNode(0)
// 在 n1 -> n2 中間插入節點 P
n1.left = P
P.left = n2
// 刪除節點 P
n1.left = n2
```
=== "Ruby"
```ruby title="binary_tree.rb"
```
=== "Zig"
```zig title="binary_tree.zig"
```
??? pythontutor "視覺化執行"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
!!! note
需要注意的是,插入節點可能會改變二元樹的原有邏輯結構,而刪除節點通常意味著刪除該節點及其所有子樹。因此,在二元樹中,插入與刪除通常是由一套操作配合完成的,以實現有實際意義的操作。
## 常見二元樹型別
### 完美二元樹
如下圖所示,<u>完美二元樹(perfect binary tree</u>所有層的節點都被完全填滿。在完美二元樹中,葉節點的度為 $0$ ,其餘所有節點的度都為 $2$ ;若樹的高度為 $h$ ,則節點總數為 $2^{h+1} - 1$ ,呈現標準的指數級關係,反映了自然界中常見的細胞分裂現象。
!!! tip
請注意,在中文社群中,完美二元樹常被稱為<u>滿二元樹</u>。
![完美二元樹](binary_tree.assets/perfect_binary_tree.png)
### 完全二元樹
如下圖所示,<u>完全二元樹(complete binary tree</u>只有最底層的節點未被填滿,且最底層節點儘量靠左填充。
![完全二元樹](binary_tree.assets/complete_binary_tree.png)
### 完滿二元樹
如下圖所示,<u>完滿二元樹(full binary tree</u>除了葉節點之外,其餘所有節點都有兩個子節點。
![完滿二元樹](binary_tree.assets/full_binary_tree.png)
### 平衡二元樹
如下圖所示,<u>平衡二元樹(balanced binary tree</u>中任意節點的左子樹和右子樹的高度之差的絕對值不超過 1 。
![平衡二元樹](binary_tree.assets/balanced_binary_tree.png)
## 二元樹的退化
下圖展示了二元樹的理想結構與退化結構。當二元樹的每層節點都被填滿時,達到“完美二元樹”;而當所有節點都偏向一側時,二元樹退化為“鏈結串列”。
- 完美二元樹是理想情況,可以充分發揮二元樹“分治”的優勢。
- 鏈結串列則是另一個極端,各項操作都變為線性操作,時間複雜度退化至 $O(n)$ 。
![二元樹的最佳結構與最差結構](binary_tree.assets/binary_tree_best_worst_cases.png)
如下表所示,在最佳結構和最差結構下,二元樹的葉節點數量、節點總數、高度等達到極大值或極小值。
<p align="center"> 表 <id> &nbsp; 二元樹的最佳結構與最差結構 </p>
| | 完美二元樹 | 鏈結串列 |
| --------------------------- | ------------------ | ------- |
| 第 $i$ 層的節點數量 | $2^{i-1}$ | $1$ |
| 高度為 $h$ 的樹的葉節點數量 | $2^h$ | $1$ |
| 高度為 $h$ 的樹的節點總數 | $2^{h+1} - 1$ | $h + 1$ |
| 節點總數為 $n$ 的樹的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+89
View File
@@ -0,0 +1,89 @@
# 二元樹走訪
從物理結構的角度來看,樹是一種基於鏈結串列的資料結構,因此其走訪方式是透過指標逐個訪問節點。然而,樹是一種非線性資料結構,這使得走訪樹比走訪鏈結串列更加複雜,需要藉助搜尋演算法來實現。
二元樹常見的走訪方式包括層序走訪、前序走訪、中序走訪和後序走訪等。
## 層序走訪
如下圖所示,<u>層序走訪(level-order traversal</u>從頂部到底部逐層走訪二元樹,並在每一層按照從左到右的順序訪問節點。
層序走訪本質上屬於<u>廣度優先走訪(breadth-first traversal</u>,也稱<u>廣度優先搜尋(breadth-first search, BFS</u>,它體現了一種“一圈一圈向外擴展”的逐層走訪方式。
![二元樹的層序走訪](binary_tree_traversal.assets/binary_tree_bfs.png)
### 程式碼實現
廣度優先走訪通常藉助“佇列”來實現。佇列遵循“先進先出”的規則,而廣度優先走訪則遵循“逐層推進”的規則,兩者背後的思想是一致的。實現程式碼如下:
```src
[file]{binary_tree_bfs}-[class]{}-[func]{level_order}
```
### 複雜度分析
- **時間複雜度為 $O(n)$** :所有節點被訪問一次,使用 $O(n)$ 時間,其中 $n$ 為節點數量。
- **空間複雜度為 $O(n)$** :在最差情況下,即滿二元樹時,走訪到最底層之前,佇列中最多同時存在 $(n + 1) / 2$ 個節點,佔用 $O(n)$ 空間。
## 前序、中序、後序走訪
相應地,前序、中序和後序走訪都屬於<u>深度優先走訪(depth-first traversal</u>,也稱<u>深度優先搜尋(depth-first search, DFS</u>,它體現了一種“先走到盡頭,再回溯繼續”的走訪方式。
下圖展示了對二元樹進行深度優先走訪的工作原理。**深度優先走訪就像是繞著整棵二元樹的外圍“走”一圈**,在每個節點都會遇到三個位置,分別對應前序走訪、中序走訪和後序走訪。
![二元搜尋樹的前序、中序、後序走訪](binary_tree_traversal.assets/binary_tree_dfs.png)
### 程式碼實現
深度優先搜尋通常基於遞迴實現:
```src
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
```
!!! tip
深度優先搜尋也可以基於迭代實現,有興趣的讀者可以自行研究。
下圖展示了前序走訪二元樹的遞迴過程,其可分為“遞”和“迴”兩個逆向的部分。
1. “遞”表示開啟新方法,程式在此過程中訪問下一個節點。
2. “迴”表示函式返回,代表當前節點已經訪問完畢。
=== "<1>"
![前序走訪的遞迴過程](binary_tree_traversal.assets/preorder_step1.png)
=== "<2>"
![preorder_step2](binary_tree_traversal.assets/preorder_step2.png)
=== "<3>"
![preorder_step3](binary_tree_traversal.assets/preorder_step3.png)
=== "<4>"
![preorder_step4](binary_tree_traversal.assets/preorder_step4.png)
=== "<5>"
![preorder_step5](binary_tree_traversal.assets/preorder_step5.png)
=== "<6>"
![preorder_step6](binary_tree_traversal.assets/preorder_step6.png)
=== "<7>"
![preorder_step7](binary_tree_traversal.assets/preorder_step7.png)
=== "<8>"
![preorder_step8](binary_tree_traversal.assets/preorder_step8.png)
=== "<9>"
![preorder_step9](binary_tree_traversal.assets/preorder_step9.png)
=== "<10>"
![preorder_step10](binary_tree_traversal.assets/preorder_step10.png)
=== "<11>"
![preorder_step11](binary_tree_traversal.assets/preorder_step11.png)
### 複雜度分析
- **時間複雜度為 $O(n)$** :所有節點被訪問一次,使用 $O(n)$ 時間。
- **空間複雜度為 $O(n)$** :在最差情況下,即樹退化為鏈結串列時,遞迴深度達到 $n$ ,系統佔用 $O(n)$ 堆疊幀空間。
+9
View File
@@ -0,0 +1,9 @@
# 樹
![](../assets/covers/chapter_tree.jpg)
!!! abstract
參天大樹充滿生命力,根深葉茂,分枝扶疏。
它為我們展現了資料分治的生動形態。
+54
View File
@@ -0,0 +1,54 @@
# 小結
### 重點回顧
- 二元樹是一種非線性資料結構,體現“一分為二”的分治邏輯。每個二元樹節點包含一個值以及兩個指標,分別指向其左子節點和右子節點。
- 對於二元樹中的某個節點,其左(右)子節點及其以下形成的樹被稱為該節點的左(右)子樹。
- 二元樹的相關術語包括根節點、葉節點、層、度、邊、高度和深度等。
- 二元樹的初始化、節點插入和節點刪除操作與鏈結串列操作方法類似。
- 常見的二元樹型別有完美二元樹、完全二元樹、完滿二元樹和平衡二元樹。完美二元樹是最理想的狀態,而鏈結串列是退化後的最差狀態。
- 二元樹可以用陣列表示,方法是將節點值和空位按層序走訪順序排列,並根據父節點與子節點之間的索引對映關係來實現指標。
- 二元樹的層序走訪是一種廣度優先搜尋方法,它體現了“一圈一圈向外擴展”的逐層走訪方式,通常透過佇列來實現。
- 前序、中序、後序走訪皆屬於深度優先搜尋,它們體現了“先走到盡頭,再回溯繼續”的走訪方式,通常使用遞迴來實現。
- 二元搜尋樹是一種高效的元素查詢資料結構,其查詢、插入和刪除操作的時間複雜度均為 $O(\log n)$ 。當二元搜尋樹退化為鏈結串列時,各項時間複雜度會劣化至 $O(n)$ 。
- AVL 樹,也稱平衡二元搜尋樹,它透過旋轉操作確保在不斷插入和刪除節點後樹仍然保持平衡。
- AVL 樹的旋轉操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或刪除節點後,AVL 樹會從底向頂執行旋轉操作,使樹重新恢復平衡。
### Q & A
**Q**:對於只有一個節點的二元樹,樹的高度和根節點的深度都是 $0$ 嗎?
是的,因為高度和深度通常定義為“經過的邊的數量”。
**Q**:二元樹中的插入與刪除一般由一套操作配合完成,這裡的“一套操作”指什麼呢?可以理解為資源的子節點的資源釋放嗎?
拿二元搜尋樹來舉例,刪除節點操作要分三種情況處理,其中每種情況都需要進行多個步驟的節點操作。
**Q**:為什麼 DFS 走訪二元樹有前、中、後三種順序,分別有什麼用呢?
與順序和逆序走訪陣列類似,前序、中序、後序走訪是三種二元樹走訪方法,我們可以使用它們得到一個特定順序的走訪結果。例如在二元搜尋樹中,由於節點大小滿足 `左子節點值 < 根節點值 < 右子節點值` ,因此我們只要按照“左 $\rightarrow$ 根 $\rightarrow$ 右”的優先順序走訪樹,就可以獲得有序的節點序列。
**Q**:右旋操作是處理失衡節點 `node``child``grand_child` 之間的關係,那 `node` 的父節點和 `node` 原來的連線不需要維護嗎?右旋操作後豈不是斷掉了?
我們需要從遞迴的視角來看這個問題。右旋操作 `right_rotate(root)` 傳入的是子樹的根節點,最終 `return child` 返回旋轉之後的子樹的根節點。子樹的根節點和其父節點的連線是在該函式返回後完成的,不屬於右旋操作的維護範圍。
**Q**:在 C++ 中,函式被劃分到 `private``public` 中,這方面有什麼考量嗎?為什麼要將 `height()` 函式和 `updateHeight()` 函式分別放在 `public``private` 中呢?
主要看方法的使用範圍,如果方法只在類別內部使用,那麼就設計為 `private` 。例如,使用者單獨呼叫 `updateHeight()` 是沒有意義的,它只是插入、刪除操作中的一步。而 `height()` 是訪問節點高度,類似於 `vector.size()` ,因此設定成 `public` 以便使用。
**Q**:如何從一組輸入資料構建一棵二元搜尋樹?根節點的選擇是不是很重要?
是的,構建樹的方法已在二元搜尋樹程式碼中的 `build_tree()` 方法中給出。至於根節點的選擇,我們通常會將輸入資料排序,然後將中點元素作為根節點,再遞迴地構建左右子樹。這樣做可以最大程度保證樹的平衡性。
**Q**:在 Java 中,字串對比是否一定要用 `equals()` 方法?
在 Java 中,對於基本資料型別,`==` 用於對比兩個變數的值是否相等。對於引用型別,兩種符號的工作原理是不同的。
- `==` :用來比較兩個變數是否指向同一個物件,即它們在記憶體中的位置是否相同。
- `equals()`:用來對比兩個物件的值是否相等。
因此,如果要對比值,我們應該使用 `equals()` 。然而,透過 `String a = "hi"; String b = "hi";` 初始化的字串都儲存在字串常數池中,它們指向同一個物件,因此也可以用 `a == b` 來比較兩個字串的內容。
**Q**:廣度優先走訪到最底層之前,佇列中的節點數量是 $2^h$ 嗎?
是的,例如高度 $h = 2$ 的滿二元樹,其節點總數 $n = 7$ ,則底層節點數量 $4 = 2^h = (n + 1) / 2$ 。