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
@@ -0,0 +1,9 @@
# 複雜度分析
![複雜度分析](../assets/covers/chapter_complexity_analysis.jpg)
!!! abstract
複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。
它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,194 @@
# 迭代與遞迴
在演算法中,重複執行某個任務是很常見的,它與複雜度分析息息相關。因此,在介紹時間複雜度和空間複雜度之前,我們先來了解如何在程式中實現重複執行任務,即兩種基本的程式控制結構:迭代、遞迴。
## 迭代
<u>迭代(iteration</u>是一種重複執行某個任務的控制結構。在迭代中,程式會在滿足一定的條件下重複執行某段程式碼,直到這個條件不再滿足。
### for 迴圈
`for` 迴圈是最常見的迭代形式之一,**適合在預先知道迭代次數時使用**。
以下函式基於 `for` 迴圈實現了求和 $1 + 2 + \dots + n$ ,求和結果使用變數 `res` 記錄。需要注意的是,Python 中 `range(a, b)` 對應的區間是“左閉右開”的,對應的走訪範圍為 $a, a + 1, \dots, b-1$
```src
[file]{iteration}-[class]{}-[func]{for_loop}
```
下圖是該求和函式的流程框圖。
![求和函式的流程框圖](iteration_and_recursion.assets/iteration.png)
此求和函式的操作數量與輸入資料大小 $n$ 成正比,或者說成“線性關係”。實際上,**時間複雜度描述的就是這個“線性關係”**。相關內容將會在下一節中詳細介紹。
### while 迴圈
`for` 迴圈類似,`while` 迴圈也是一種實現迭代的方法。在 `while` 迴圈中,程式每輪都會先檢查條件,如果條件為真,則繼續執行,否則就結束迴圈。
下面我們用 `while` 迴圈來實現求和 $1 + 2 + \dots + n$
```src
[file]{iteration}-[class]{}-[func]{while_loop}
```
**`while` 迴圈比 `for` 迴圈的自由度更高**。在 `while` 迴圈中,我們可以自由地設計條件變數的初始化和更新步驟。
例如在以下程式碼中,條件變數 $i$ 每輪進行兩次更新,這種情況就不太方便用 `for` 迴圈實現:
```src
[file]{iteration}-[class]{}-[func]{while_loop_ii}
```
總的來說,**`for` 迴圈的程式碼更加緊湊,`while` 迴圈更加靈活**,兩者都可以實現迭代結構。選擇使用哪一個應該根據特定問題的需求來決定。
### 巢狀迴圈
我們可以在一個迴圈結構內巢狀另一個迴圈結構,下面以 `for` 迴圈為例:
```src
[file]{iteration}-[class]{}-[func]{nested_for_loop}
```
下圖是該巢狀迴圈的流程框圖。
![巢狀迴圈的流程框圖](iteration_and_recursion.assets/nested_iteration.png)
在這種情況下,函式的操作數量與 $n^2$ 成正比,或者說演算法執行時間和輸入資料大小 $n$ 成“平方關係”。
我們可以繼續新增巢狀迴圈,每一次巢狀都是一次“升維”,將會使時間複雜度提高至“立方關係”“四次方關係”,以此類推。
## 遞迴
<u>遞迴(recursion</u>是一種演算法策略,透過函式呼叫自身來解決問題。它主要包含兩個階段。
1. **遞**:程式不斷深入地呼叫自身,通常傳入更小或更簡化的參數,直到達到“終止條件”。
2. **迴**:觸發“終止條件”後,程式從最深層的遞迴函式開始逐層返回,匯聚每一層的結果。
而從實現的角度看,遞迴程式碼主要包含三個要素。
1. **終止條件**:用於決定什麼時候由“遞”轉“迴”。
2. **遞迴呼叫**:對應“遞”,函式呼叫自身,通常輸入更小或更簡化的參數。
3. **返回結果**:對應“迴”,將當前遞迴層級的結果返回至上一層。
觀察以下程式碼,我們只需呼叫函式 `recur(n)` ,就可以完成 $1 + 2 + \dots + n$ 的計算:
```src
[file]{recursion}-[class]{}-[func]{recur}
```
下圖展示了該函式的遞迴過程。
![求和函式的遞迴過程](iteration_and_recursion.assets/recursion_sum.png)
雖然從計算角度看,迭代與遞迴可以得到相同的結果,**但它們代表了兩種完全不同的思考和解決問題的範式**。
- **迭代**:“自下而上”地解決問題。從最基礎的步驟開始,然後不斷重複或累加這些步驟,直到任務完成。
- **遞迴**:“自上而下”地解決問題。將原問題分解為更小的子問題,這些子問題和原問題具有相同的形式。接下來將子問題繼續分解為更小的子問題,直到基本情況時停止(基本情況的解是已知的)。
以上述求和函式為例,設問題 $f(n) = 1 + 2 + \dots + n$ 。
- **迭代**:在迴圈中模擬求和過程,從 $1$ 走訪到 $n$ ,每輪執行求和操作,即可求得 $f(n)$ 。
- **遞迴**:將問題分解為子問題 $f(n) = n + f(n-1)$ ,不斷(遞迴地)分解下去,直至基本情況 $f(1) = 1$ 時終止。
### 呼叫堆疊
遞迴函式每次呼叫自身時,系統都會為新開啟的函式分配記憶體,以儲存區域性變數、呼叫位址和其他資訊等。這將導致兩方面的結果。
- 函式的上下文資料都儲存在稱為“堆疊幀空間”的記憶體區域中,直至函式返回後才會被釋放。因此,**遞迴通常比迭代更加耗費記憶體空間**。
- 遞迴呼叫函式會產生額外的開銷。**因此遞迴通常比迴圈的時間效率更低**。
如下圖所示,在觸發終止條件前,同時存在 $n$ 個未返回的遞迴函式,**遞迴深度為 $n$** 。
![遞迴呼叫深度](iteration_and_recursion.assets/recursion_sum_depth.png)
在實際中,程式語言允許的遞迴深度通常是有限的,過深的遞迴可能導致堆疊溢位錯誤。
### 尾遞迴
有趣的是,**如果函式在返回前的最後一步才進行遞迴呼叫**,則該函式可以被編譯器或直譯器最佳化,使其在空間效率上與迭代相當。這種情況被稱為<u>尾遞迴(tail recursion</u>。
- **普通遞迴**:當函式返回到上一層級的函式後,需要繼續執行程式碼,因此系統需要儲存上一層呼叫的上下文。
- **尾遞迴**:遞迴呼叫是函式返回前的最後一個操作,這意味著函式返回到上一層級後,無須繼續執行其他操作,因此系統無須儲存上一層函式的上下文。
以計算 $1 + 2 + \dots + n$ 為例,我們可以將結果變數 `res` 設為函式參數,從而實現尾遞迴:
```src
[file]{recursion}-[class]{}-[func]{tail_recur}
```
尾遞迴的執行過程如下圖所示。對比普通遞迴和尾遞迴,兩者的求和操作的執行點是不同的。
- **普通遞迴**:求和操作是在“迴”的過程中執行的,每層返回後都要再執行一次求和操作。
- **尾遞迴**:求和操作是在“遞”的過程中執行的,“迴”的過程只需層層返回。
![尾遞迴過程](iteration_and_recursion.assets/tail_recursion_sum.png)
!!! tip
請注意,許多編譯器或直譯器並不支持尾遞迴最佳化。例如,Python 預設不支持尾遞迴最佳化,因此即使函式是尾遞迴形式,仍然可能會遇到堆疊溢位問題。
### 遞迴樹
當處理與“分治”相關的演算法問題時,遞迴往往比迭代的思路更加直觀、程式碼更加易讀。以“費波那契數列”為例。
!!! question
給定一個費波那契數列 $0, 1, 1, 2, 3, 5, 8, 13, \dots$ ,求該數列的第 $n$ 個數字。
設費波那契數列的第 $n$ 個數字為 $f(n)$ ,易得兩個結論。
- 數列的前兩個數字為 $f(1) = 0$ 和 $f(2) = 1$ 。
- 數列中的每個數字是前兩個數字的和,即 $f(n) = f(n - 1) + f(n - 2)$ 。
按照遞推關係進行遞迴呼叫,將前兩個數字作為終止條件,便可寫出遞迴程式碼。呼叫 `fib(n)` 即可得到費波那契數列的第 $n$ 個數字:
```src
[file]{recursion}-[class]{}-[func]{fib}
```
觀察以上程式碼,我們在函式內遞迴呼叫了兩個函式,**這意味著從一個呼叫產生了兩個呼叫分支**。如下圖所示,這樣不斷遞迴呼叫下去,最終將產生一棵層數為 $n$ 的<u>遞迴樹(recursion tree</u>。
![費波那契數列的遞迴樹](iteration_and_recursion.assets/recursion_tree.png)
從本質上看,遞迴體現了“將問題分解為更小子問題”的思維範式,這種分治策略至關重要。
- 從演算法角度看,搜尋、排序、回溯、分治、動態規劃等許多重要演算法策略直接或間接地應用了這種思維方式。
- 從資料結構角度看,遞迴天然適合處理鏈結串列、樹和圖的相關問題,因為它們非常適合用分治思想進行分析。
## 兩者對比
總結以上內容,如下表所示,迭代和遞迴在實現、效能和適用性上有所不同。
<p align="center"> 表 <id> &nbsp; 迭代與遞迴特點對比 </p>
| | 迭代 | 遞迴 |
| -------- | -------------------------------------- | ------------------------------------------------------------ |
| 實現方式 | 迴圈結構 | 函式呼叫自身 |
| 時間效率 | 效率通常較高,無函式呼叫開銷 | 每次函式呼叫都會產生開銷 |
| 記憶體使用 | 通常使用固定大小的記憶體空間 | 累積函式呼叫可能使用大量的堆疊幀空間 |
| 適用問題 | 適用於簡單迴圈任務,程式碼直觀、可讀性好 | 適用於子問題分解,如樹、圖、分治、回溯等,程式碼結構簡潔、清晰 |
!!! tip
如果感覺以下內容理解困難,可以在讀完“堆疊”章節後再來複習。
那麼,迭代和遞迴具有什麼內在關聯呢?以上述遞迴函式為例,求和操作在遞迴的“迴”階段進行。這意味著最初被呼叫的函式實際上是最後完成其求和操作的,**這種工作機制與堆疊的“先入後出”原則異曲同工**。
事實上,“呼叫堆疊”和“堆疊幀空間”這類遞迴術語已經暗示了遞迴與堆疊之間的密切關係。
1. **遞**:當函式被呼叫時,系統會在“呼叫堆疊”上為該函式分配新的堆疊幀,用於儲存函式的區域性變數、參數、返回位址等資料。
2. **迴**:當函式完成執行並返回時,對應的堆疊幀會被從“呼叫堆疊”上移除,恢復之前函式的執行環境。
因此,**我們可以使用一個顯式的堆疊來模擬呼叫堆疊的行為**,從而將遞迴轉化為迭代形式:
```src
[file]{recursion}-[class]{}-[func]{for_loop_recur}
```
觀察以上程式碼,當遞迴轉化為迭代後,程式碼變得更加複雜了。儘管迭代和遞迴在很多情況下可以互相轉化,但不一定值得這樣做,有以下兩點原因。
- 轉化後的程式碼可能更加難以理解,可讀性更差。
- 對於某些複雜問題,模擬系統呼叫堆疊的行為可能非常困難。
總之,**選擇迭代還是遞迴取決於特定問題的性質**。在程式設計實踐中,權衡兩者的優劣並根據情境選擇合適的方法至關重要。
@@ -0,0 +1,48 @@
# 演算法效率評估
在演算法設計中,我們先後追求以下兩個層面的目標。
1. **找到問題解法**:演算法需要在規定的輸入範圍內可靠地求得問題的正確解。
2. **尋求最優解法**:同一個問題可能存在多種解法,我們希望找到儘可能高效的演算法。
也就是說,在能夠解決問題的前提下,演算法效率已成為衡量演算法優劣的主要評價指標,它包括以下兩個維度。
- **時間效率**:演算法執行速度的快慢。
- **空間效率**:演算法佔用記憶體空間的大小。
簡而言之,**我們的目標是設計“既快又省”的資料結構與演算法**。而有效地評估演算法效率至關重要,因為只有這樣,我們才能將各種演算法進行對比,進而指導演算法設計與最佳化過程。
效率評估方法主要分為兩種:實際測試、理論估算。
## 實際測試
假設我們現在有演算法 `A` 和演算法 `B` ,它們都能解決同一問題,現在需要對比這兩個演算法的效率。最直接的方法是找一臺計算機,執行這兩個演算法,並監控記錄它們的執行時間和記憶體佔用情況。這種評估方式能夠反映真實情況,但也存在較大的侷限性。
一方面,**難以排除測試環境的干擾因素**。硬體配置會影響演算法的效能。比如在某臺計算機中,演算法 `A` 的執行時間比演算法 `B` 短;但在另一臺配置不同的計算機中,可能得到相反的測試結果。這意味著我們需要在各種機器上進行測試,統計平均效率,而這是不現實的。
另一方面,**展開完整測試非常耗費資源**。隨著輸入資料量的變化,演算法會表現出不同的效率。例如,在輸入資料量較小時,演算法 `A` 的執行時間比演算法 `B` 短;而在輸入資料量較大時,測試結果可能恰恰相反。因此,為了得到有說服力的結論,我們需要測試各種規模的輸入資料,而這需要耗費大量的計算資源。
## 理論估算
由於實際測試具有較大的侷限性,因此我們可以考慮僅透過一些計算來評估演算法的效率。這種估算方法被稱為<u>漸近複雜度分析(asymptotic complexity analysis</u>,簡稱<u>複雜度分析</u>。
複雜度分析能夠體現演算法執行所需的時間和空間資源與輸入資料大小之間的關係。**它描述了隨著輸入資料大小的增加,演算法執行所需時間和空間的增長趨勢**。這個定義有些拗口,我們可以將其分為三個重點來理解。
- “時間和空間資源”分別對應<u>時間複雜度(time complexity</u>和<u>空間複雜度(space complexity</u>。
- “隨著輸入資料大小的增加”意味著複雜度反映了演算法執行效率與輸入資料體量之間的關係。
- “時間和空間的增長趨勢”表示複雜度分析關注的不是執行時間或佔用空間的具體值,而是時間或空間增長的“快慢”。
**複雜度分析克服了實際測試方法的弊端**,體現在以下兩個方面。
- 它獨立於測試環境,分析結果適用於所有執行平臺。
- 它可以體現不同資料量下的演算法效率,尤其是在大資料量下的演算法效能。
!!! tip
如果你仍對複雜度的概念感到困惑,無須擔心,我們會在後續章節中詳細介紹。
複雜度分析為我們提供了一把評估演算法效率的“標尺”,使我們可以衡量執行某個演算法所需的時間和空間資源,對比不同演算法之間的效率。
複雜度是個數學概念,對於初學者可能比較抽象,學習難度相對較高。從這個角度看,複雜度分析可能不太適合作為最先介紹的內容。然而,當我們討論某個資料結構或演算法的特點時,難以避免要分析其執行速度和空間使用情況。
綜上所述,建議你在深入學習資料結構與演算法之前,**先對複雜度分析建立初步的瞭解,以便能夠完成簡單演算法的複雜度分析**。
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,898 @@
# 空間複雜度
<u>空間複雜度(space complexity</u>用於衡量演算法佔用記憶體空間隨著資料量變大時的增長趨勢。這個概念與時間複雜度非常類似,只需將“執行時間”替換為“佔用記憶體空間”。
## 演算法相關空間
演算法在執行過程中使用的記憶體空間主要包括以下幾種。
- **輸入空間**:用於儲存演算法的輸入資料。
- **暫存空間**:用於儲存演算法在執行過程中的變數、物件、函式上下文等資料。
- **輸出空間**:用於儲存演算法的輸出資料。
一般情況下,空間複雜度的統計範圍是“暫存空間”加上“輸出空間”。
暫存空間可以進一步劃分為三個部分。
- **暫存資料**:用於儲存演算法執行過程中的各種常數、變數、物件等。
- **堆疊幀空間**:用於儲存呼叫函式的上下文資料。系統在每次呼叫函式時都會在堆疊頂部建立一個堆疊幀,函式返回後,堆疊幀空間會被釋放。
- **指令空間**:用於儲存編譯後的程式指令,在實際統計中通常忽略不計。
在分析一段程式的空間複雜度時,**我們通常統計暫存資料、堆疊幀空間和輸出資料三部分**,如下圖所示。
![演算法使用的相關空間](space_complexity.assets/space_types.png)
相關程式碼如下:
=== "Python"
```python title=""
class Node:
"""類別"""
def __init__(self, x: int):
self.val: int = x # 節點值
self.next: Node | None = None # 指向下一節點的引用
def function() -> int:
"""函式"""
# 執行某些操作...
return 0
def algorithm(n) -> int: # 輸入資料
A = 0 # 暫存資料(常數,一般用大寫字母表示)
b = 0 # 暫存資料(變數)
node = Node(0) # 暫存資料(物件)
c = function() # 堆疊幀空間(呼叫函式)
return A + b + c # 輸出資料
```
=== "C++"
```cpp title=""
/* 結構體 */
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};
/* 函式 */
int func() {
// 執行某些操作...
return 0;
}
int algorithm(int n) { // 輸入資料
const int a = 0; // 暫存資料(常數)
int b = 0; // 暫存資料(變數)
Node* node = new Node(0); // 暫存資料(物件)
int c = func(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "Java"
```java title=""
/* 類別 */
class Node {
int val;
Node next;
Node(int x) { val = x; }
}
/* 函式 */
int function() {
// 執行某些操作...
return 0;
}
int algorithm(int n) { // 輸入資料
final int a = 0; // 暫存資料(常數)
int b = 0; // 暫存資料(變數)
Node node = new Node(0); // 暫存資料(物件)
int c = function(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "C#"
```csharp title=""
/* 類別 */
class Node(int x) {
int val = x;
Node next;
}
/* 函式 */
int Function() {
// 執行某些操作...
return 0;
}
int Algorithm(int n) { // 輸入資料
const int a = 0; // 暫存資料(常數)
int b = 0; // 暫存資料(變數)
Node node = new(0); // 暫存資料(物件)
int c = Function(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "Go"
```go title=""
/* 結構體 */
type node struct {
val int
next *node
}
/* 建立 node 結構體 */
func newNode(val int) *node {
return &node{val: val}
}
/* 函式 */
func function() int {
// 執行某些操作...
return 0
}
func algorithm(n int) int { // 輸入資料
const a = 0 // 暫存資料(常數)
b := 0 // 暫存資料(變數)
newNode(0) // 暫存資料(物件)
c := function() // 堆疊幀空間(呼叫函式)
return a + b + c // 輸出資料
}
```
=== "Swift"
```swift title=""
/* 類別 */
class Node {
var val: Int
var next: Node?
init(x: Int) {
val = x
}
}
/* 函式 */
func function() -> Int {
// 執行某些操作...
return 0
}
func algorithm(n: Int) -> Int { // 輸入資料
let a = 0 // 暫存資料(常數)
var b = 0 // 暫存資料(變數)
let node = Node(x: 0) // 暫存資料(物件)
let c = function() // 堆疊幀空間(呼叫函式)
return a + b + c // 輸出資料
}
```
=== "JS"
```javascript title=""
/* 類別 */
class Node {
val;
next;
constructor(val) {
this.val = val === undefined ? 0 : val; // 節點值
this.next = null; // 指向下一節點的引用
}
}
/* 函式 */
function constFunc() {
// 執行某些操作
return 0;
}
function algorithm(n) { // 輸入資料
const a = 0; // 暫存資料(常數)
let b = 0; // 暫存資料(變數)
const node = new Node(0); // 暫存資料(物件)
const c = constFunc(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "TS"
```typescript title=""
/* 類別 */
class Node {
val: number;
next: Node | null;
constructor(val?: number) {
this.val = val === undefined ? 0 : val; // 節點值
this.next = null; // 指向下一節點的引用
}
}
/* 函式 */
function constFunc(): number {
// 執行某些操作
return 0;
}
function algorithm(n: number): number { // 輸入資料
const a = 0; // 暫存資料(常數)
let b = 0; // 暫存資料(變數)
const node = new Node(0); // 暫存資料(物件)
const c = constFunc(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "Dart"
```dart title=""
/* 類別 */
class Node {
int val;
Node next;
Node(this.val, [this.next]);
}
/* 函式 */
int function() {
// 執行某些操作...
return 0;
}
int algorithm(int n) { // 輸入資料
const int a = 0; // 暫存資料(常數)
int b = 0; // 暫存資料(變數)
Node node = Node(0); // 暫存資料(物件)
int c = function(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* 結構體 */
struct Node {
val: i32,
next: Option<Rc<RefCell<Node>>>,
}
/* 建立 Node 結構體 */
impl Node {
fn new(val: i32) -> Self {
Self { val: val, next: None }
}
}
/* 函式 */
fn function() -> i32 {
// 執行某些操作...
return 0;
}
fn algorithm(n: i32) -> i32 { // 輸入資料
const a: i32 = 0; // 暫存資料(常數)
let mut b = 0; // 暫存資料(變數)
let node = Node::new(0); // 暫存資料(物件)
let c = function(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "C"
```c title=""
/* 函式 */
int func() {
// 執行某些操作...
return 0;
}
int algorithm(int n) { // 輸入資料
const int a = 0; // 暫存資料(常數)
int b = 0; // 暫存資料(變數)
int c = func(); // 堆疊幀空間(呼叫函式)
return a + b + c; // 輸出資料
}
```
=== "Kotlin"
```kotlin title=""
/* 類別 */
class Node(var _val: Int) {
var next: Node? = null
}
/* 函式 */
fun function(): Int {
// 執行某些操作...
return 0
}
fun algorithm(n: Int): Int { // 輸入資料
val a = 0 // 暫存資料(常數)
var b = 0 // 暫存資料(變數)
val node = Node(0) // 暫存資料(物件)
val c = function() // 堆疊幀空間(呼叫函式)
return a + b + c // 輸出資料
}
```
=== "Ruby"
```ruby title=""
### 類別 ###
class Node
attr_accessor :val # 節點值
attr_accessor :next # 指向下一節點的引用
def initialize(x)
@val = x
end
end
### 函式 ###
def function
# 執行某些操作...
0
end
### 演算法 ###
def algorithm(n) # 輸入資料
a = 0 # 暫存資料(常數)
b = 0 # 暫存資料(變數)
node = Node.new(0) # 暫存資料(物件)
c = function # 堆疊幀空間(呼叫函式)
a + b + c # 輸出資料
end
```
=== "Zig"
```zig title=""
```
## 推算方法
空間複雜度的推算方法與時間複雜度大致相同,只需將統計物件從“操作數量”轉為“使用空間大小”。
而與時間複雜度不同的是,**我們通常只關注最差空間複雜度**。這是因為記憶體空間是一項硬性要求,我們必須確保在所有輸入資料下都有足夠的記憶體空間預留。
觀察以下程式碼,最差空間複雜度中的“最差”有兩層含義。
1. **以最差輸入資料為準**:當 $n < 10$ 時,空間複雜度為 $O(1)$ ;但當 $n > 10$ 時,初始化的陣列 `nums` 佔用 $O(n)$ 空間,因此最差空間複雜度為 $O(n)$ 。
2. **以演算法執行中的峰值記憶體為準**:例如,程式在執行最後一行之前,佔用 $O(1)$ 空間;當初始化陣列 `nums` 時,程式佔用 $O(n)$ 空間,因此最差空間複雜度為 $O(n)$ 。
=== "Python"
```python title=""
def algorithm(n: int):
a = 0 # O(1)
b = [0] * 10000 # O(1)
if n > 10:
nums = [0] * n # O(n)
```
=== "C++"
```cpp title=""
void algorithm(int n) {
int a = 0; // O(1)
vector<int> b(10000); // O(1)
if (n > 10)
vector<int> nums(n); // O(n)
}
```
=== "Java"
```java title=""
void algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10)
int[] nums = new int[n]; // O(n)
}
```
=== "C#"
```csharp title=""
void Algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10) {
int[] nums = new int[n]; // O(n)
}
}
```
=== "Go"
```go title=""
func algorithm(n int) {
a := 0 // O(1)
b := make([]int, 10000) // O(1)
var nums []int
if n > 10 {
nums := make([]int, n) // O(n)
}
fmt.Println(a, b, nums)
}
```
=== "Swift"
```swift title=""
func algorithm(n: Int) {
let a = 0 // O(1)
let b = Array(repeating: 0, count: 10000) // O(1)
if n > 10 {
let nums = Array(repeating: 0, count: n) // O(n)
}
}
```
=== "JS"
```javascript title=""
function algorithm(n) {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "TS"
```typescript title=""
function algorithm(n: number): void {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "Dart"
```dart title=""
void algorithm(int n) {
int a = 0; // O(1)
List<int> b = List.filled(10000, 0); // O(1)
if (n > 10) {
List<int> nums = List.filled(n, 0); // O(n)
}
}
```
=== "Rust"
```rust title=""
fn algorithm(n: i32) {
let a = 0; // O(1)
let b = [0; 10000]; // O(1)
if n > 10 {
let nums = vec![0; n as usize]; // O(n)
}
}
```
=== "C"
```c title=""
void algorithm(int n) {
int a = 0; // O(1)
int b[10000]; // O(1)
if (n > 10)
int nums[n] = {0}; // O(n)
}
```
=== "Kotlin"
```kotlin title=""
fun algorithm(n: Int) {
val a = 0 // O(1)
val b = IntArray(10000) // O(1)
if (n > 10) {
val nums = IntArray(n) // O(n)
}
}
```
=== "Ruby"
```ruby title=""
def algorithm(n)
a = 0 # O(1)
b = Array.new(10000) # O(1)
nums = Array.new(n) if n > 10 # O(n)
end
```
=== "Zig"
```zig title=""
```
**在遞迴函式中,需要注意統計堆疊幀空間**。觀察以下程式碼:
=== "Python"
```python title=""
def function() -> int:
# 執行某些操作
return 0
def loop(n: int):
"""迴圈的空間複雜度為 O(1)"""
for _ in range(n):
function()
def recur(n: int):
"""遞迴的空間複雜度為 O(n)"""
if n == 1:
return
return recur(n - 1)
```
=== "C++"
```cpp title=""
int func() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 遞迴 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}
```
=== "Java"
```java title=""
int function() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 遞迴 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}
```
=== "C#"
```csharp title=""
int Function() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
void Loop(int n) {
for (int i = 0; i < n; i++) {
Function();
}
}
/* 遞迴 O(n) */
int Recur(int n) {
if (n == 1) return 1;
return Recur(n - 1);
}
```
=== "Go"
```go title=""
func function() int {
// 執行某些操作
return 0
}
/* 迴圈 O(1) */
func loop(n int) {
for i := 0; i < n; i++ {
function()
}
}
/* 遞迴 O(n) */
func recur(n int) {
if n == 1 {
return
}
recur(n - 1)
}
```
=== "Swift"
```swift title=""
@discardableResult
func function() -> Int {
// 執行某些操作
return 0
}
/* 迴圈 O(1) */
func loop(n: Int) {
for _ in 0 ..< n {
function()
}
}
/* 遞迴 O(n) */
func recur(n: Int) {
if n == 1 {
return
}
recur(n: n - 1)
}
```
=== "JS"
```javascript title=""
function constFunc() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
function loop(n) {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 遞迴 O(n) */
function recur(n) {
if (n === 1) return;
return recur(n - 1);
}
```
=== "TS"
```typescript title=""
function constFunc(): number {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
function loop(n: number): void {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 遞迴 O(n) */
function recur(n: number): void {
if (n === 1) return;
return recur(n - 1);
}
```
=== "Dart"
```dart title=""
int function() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 遞迴 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}
```
=== "Rust"
```rust title=""
fn function() -> i32 {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
fn loop(n: i32) {
for i in 0..n {
function();
}
}
/* 遞迴 O(n) */
fn recur(n: i32) {
if n == 1 {
return;
}
recur(n - 1);
}
```
=== "C"
```c title=""
int func() {
// 執行某些操作
return 0;
}
/* 迴圈 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 遞迴 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}
```
=== "Kotlin"
```kotlin title=""
fun function(): Int {
// 執行某些操作
return 0
}
/* 迴圈 O(1) */
fun loop(n: Int) {
for (i in 0..<n) {
function()
}
}
/* 遞迴 O(n) */
fun recur(n: Int) {
if (n == 1) return
return recur(n - 1)
}
```
=== "Ruby"
```ruby title=""
def function
# 執行某些操作
0
end
### 迴圈的空間複雜度為 O(1) ###
def loop(n)
(0...n).each { function }
end
### 遞迴的空間複雜度為 O(n) ###
def recur(n)
return if n == 1
recur(n - 1)
end
```
=== "Zig"
```zig title=""
```
函式 `loop()` 和 `recur()` 的時間複雜度都為 $O(n)$ ,但空間複雜度不同。
- 函式 `loop()` 在迴圈中呼叫了 $n$ 次 `function()` ,每輪中的 `function()` 都返回並釋放了堆疊幀空間,因此空間複雜度仍為 $O(1)$ 。
- 遞迴函式 `recur()` 在執行過程中會同時存在 $n$ 個未返回的 `recur()` ,從而佔用 $O(n)$ 的堆疊幀空間。
## 常見型別
設輸入資料大小為 $n$ ,下圖展示了常見的空間複雜度型別(從低到高排列)。
$$
\begin{aligned}
O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
\text{常數階} < \text{對數階} < \text{線性階} < \text{平方階} < \text{指數階}
\end{aligned}
$$
![常見的空間複雜度型別](space_complexity.assets/space_complexity_common_types.png)
### 常數階 $O(1)$
常數階常見於數量與輸入資料大小 $n$ 無關的常數、變數、物件。
需要注意的是,在迴圈中初始化變數或呼叫函式而佔用的記憶體,在進入下一迴圈後就會被釋放,因此不會累積佔用空間,空間複雜度仍為 $O(1)$ :
```src
[file]{space_complexity}-[class]{}-[func]{constant}
```
### 線性階 $O(n)$
線性階常見於元素數量與 $n$ 成正比的陣列、鏈結串列、堆疊、佇列等:
```src
[file]{space_complexity}-[class]{}-[func]{linear}
```
如下圖所示,此函式的遞迴深度為 $n$ ,即同時存在 $n$ 個未返回的 `linear_recur()` 函式,使用 $O(n)$ 大小的堆疊幀空間:
```src
[file]{space_complexity}-[class]{}-[func]{linear_recur}
```
![遞迴函式產生的線性階空間複雜度](space_complexity.assets/space_complexity_recursive_linear.png)
### 平方階 $O(n^2)$
平方階常見於矩陣和圖,元素數量與 $n$ 成平方關係:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic}
```
如下圖所示,該函式的遞迴深度為 $n$ ,在每個遞迴函式中都初始化了一個陣列,長度分別為 $n$、$n-1$、$\dots$、$2$、$1$ ,平均長度為 $n / 2$ ,因此總體佔用 $O(n^2)$ 空間:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic_recur}
```
![遞迴函式產生的平方階空間複雜度](space_complexity.assets/space_complexity_recursive_quadratic.png)
### 指數階 $O(2^n)$
指數階常見於二元樹。觀察下圖,層數為 $n$ 的“滿二元樹”的節點數量為 $2^n - 1$ ,佔用 $O(2^n)$ 空間:
```src
[file]{space_complexity}-[class]{}-[func]{build_tree}
```
![滿二元樹產生的指數階空間複雜度](space_complexity.assets/space_complexity_exponential.png)
### 對數階 $O(\log n)$
對數階常見於分治演算法。例如合併排序,輸入長度為 $n$ 的陣列,每輪遞迴將陣列從中點處劃分為兩半,形成高度為 $\log n$ 的遞迴樹,使用 $O(\log n)$ 堆疊幀空間。
再例如將數字轉化為字串,輸入一個正整數 $n$ ,它的位數為 $\lfloor \log_{10} n \rfloor + 1$ ,即對應字串長度為 $\lfloor \log_{10} n \rfloor + 1$ ,因此空間複雜度為 $O(\log_{10} n + 1) = O(\log n)$ 。
## 權衡時間與空間
理想情況下,我們希望演算法的時間複雜度和空間複雜度都能達到最優。然而在實際情況中,同時最佳化時間複雜度和空間複雜度通常非常困難。
**降低時間複雜度通常需要以提升空間複雜度為代價,反之亦然**。我們將犧牲記憶體空間來提升演算法執行速度的思路稱為“以空間換時間”;反之,則稱為“以時間換空間”。
選擇哪種思路取決於我們更看重哪個方面。在大多數情況下,時間比空間更寶貴,因此“以空間換時間”通常是更常用的策略。當然,在資料量很大的情況下,控制空間複雜度也非常重要。
@@ -0,0 +1,49 @@
# 小結
### 重點回顧
**演算法效率評估**
- 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
- 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
- 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。
**時間複雜度**
- 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
- 最差時間複雜度使用大 $O$ 符號表示,對應函式漸近上界,反映當 $n$ 趨向正無窮時,操作數量 $T(n)$ 的增長級別。
- 推算時間複雜度分為兩步,首先統計操作數量,然後判斷漸近上界。
- 常見時間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$ 和 $O(n!)$ 等。
- 某些演算法的時間複雜度非固定,而是與輸入資料的分佈有關。時間複雜度分為最差、最佳、平均時間複雜度,最佳時間複雜度幾乎不用,因為輸入資料一般需要滿足嚴格條件才能達到最佳情況。
- 平均時間複雜度反映演算法在隨機資料輸入下的執行效率,最接近實際應用中的演算法效能。計算平均時間複雜度需要統計輸入資料分佈以及綜合後的數學期望。
**空間複雜度**
- 空間複雜度的作用類似於時間複雜度,用於衡量演算法佔用記憶體空間隨資料量增長的趨勢。
- 演算法執行過程中的相關記憶體空間可分為輸入空間、暫存空間、輸出空間。通常情況下,輸入空間不納入空間複雜度計算。暫存空間可分為暫存資料、堆疊幀空間和指令空間,其中堆疊幀空間通常僅在遞迴函式中影響空間複雜度。
- 我們通常只關注最差空間複雜度,即統計演算法在最差輸入資料和最差執行時刻下的空間複雜度。
- 常見空間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。
### Q & A
**Q**:尾遞迴的空間複雜度是 $O(1)$ 嗎?
理論上,尾遞迴函式的空間複雜度可以最佳化至 $O(1)$ 。不過絕大多數程式語言(例如 Java、Python、C++、Go、C# 等)不支持自動最佳化尾遞迴,因此通常認為空間複雜度是 $O(n)$ 。
**Q**:函式和方法這兩個術語的區別是什麼?
<u>函式(function</u>可以被獨立執行,所有參數都以顯式傳遞。<u>方法(method</u>與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。
下面以幾種常見的程式語言為例來說明。
- C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。
- Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。
- C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。
**Q**:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小?
不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。
假設取 $n = 8$ ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。
在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 $n = 8$ 之下的最優解法。但對於 $n = 8^5$ 就很好選了,這時增長趨勢已經佔主導了。
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff