mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-26 04:06:29 +08:00
416 lines
14 KiB
Markdown
416 lines
14 KiB
Markdown
|
# 堆疊
|
|||
|
|
|||
|
<u>堆疊(stack)</u>是一種遵循先入後出邏輯的線性資料結構。
|
|||
|
|
|||
|
我們可以將堆疊類比為桌面上的一疊盤子,如果想取出底部的盤子,則需要先將上面的盤子依次移走。我們將盤子替換為各種型別的元素(如整數、字元、物件等),就得到了堆疊這種資料結構。
|
|||
|
|
|||
|
如下圖所示,我們把堆積疊元素的頂部稱為“堆疊頂”,底部稱為“堆疊底”。將把元素新增到堆疊頂的操作叫作“入堆疊”,刪除堆疊頂元素的操作叫作“出堆疊”。
|
|||
|
|
|||
|
![堆疊的先入後出規則](stack.assets/stack_operations.png)
|
|||
|
|
|||
|
## 堆疊的常用操作
|
|||
|
|
|||
|
堆疊的常用操作如下表所示,具體的方法名需要根據所使用的程式語言來確定。在此,我們以常見的 `push()`、`pop()`、`peek()` 命名為例。
|
|||
|
|
|||
|
<p align="center"> 表 <id> 堆疊的操作效率 </p>
|
|||
|
|
|||
|
| 方法 | 描述 | 時間複雜度 |
|
|||
|
| -------- | ---------------------- | ---------- |
|
|||
|
| `push()` | 元素入堆疊(新增至堆疊頂) | $O(1)$ |
|
|||
|
| `pop()` | 堆疊頂元素出堆疊 | $O(1)$ |
|
|||
|
| `peek()` | 訪問堆疊頂元素 | $O(1)$ |
|
|||
|
|
|||
|
通常情況下,我們可以直接使用程式語言內建的堆疊類別。然而,某些語言可能沒有專門提供堆疊類別,這時我們可以將該語言的“陣列”或“鏈結串列”當作堆疊來使用,並在程式邏輯上忽略與堆疊無關的操作。
|
|||
|
|
|||
|
=== "Python"
|
|||
|
|
|||
|
```python title="stack.py"
|
|||
|
# 初始化堆疊
|
|||
|
# Python 沒有內建的堆疊類別,可以把 list 當作堆疊來使用
|
|||
|
stack: list[int] = []
|
|||
|
|
|||
|
# 元素入堆疊
|
|||
|
stack.append(1)
|
|||
|
stack.append(3)
|
|||
|
stack.append(2)
|
|||
|
stack.append(5)
|
|||
|
stack.append(4)
|
|||
|
|
|||
|
# 訪問堆疊頂元素
|
|||
|
peek: int = stack[-1]
|
|||
|
|
|||
|
# 元素出堆疊
|
|||
|
pop: int = stack.pop()
|
|||
|
|
|||
|
# 獲取堆疊的長度
|
|||
|
size: int = len(stack)
|
|||
|
|
|||
|
# 判斷是否為空
|
|||
|
is_empty: bool = len(stack) == 0
|
|||
|
```
|
|||
|
|
|||
|
=== "C++"
|
|||
|
|
|||
|
```cpp title="stack.cpp"
|
|||
|
/* 初始化堆疊 */
|
|||
|
stack<int> stack;
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1);
|
|||
|
stack.push(3);
|
|||
|
stack.push(2);
|
|||
|
stack.push(5);
|
|||
|
stack.push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
int top = stack.top();
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
stack.pop(); // 無返回值
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
int size = stack.size();
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
bool empty = stack.empty();
|
|||
|
```
|
|||
|
|
|||
|
=== "Java"
|
|||
|
|
|||
|
```java title="stack.java"
|
|||
|
/* 初始化堆疊 */
|
|||
|
Stack<Integer> stack = new Stack<>();
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1);
|
|||
|
stack.push(3);
|
|||
|
stack.push(2);
|
|||
|
stack.push(5);
|
|||
|
stack.push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
int peek = stack.peek();
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
int pop = stack.pop();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
int size = stack.size();
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
boolean isEmpty = stack.isEmpty();
|
|||
|
```
|
|||
|
|
|||
|
=== "C#"
|
|||
|
|
|||
|
```csharp title="stack.cs"
|
|||
|
/* 初始化堆疊 */
|
|||
|
Stack<int> stack = new();
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.Push(1);
|
|||
|
stack.Push(3);
|
|||
|
stack.Push(2);
|
|||
|
stack.Push(5);
|
|||
|
stack.Push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
int peek = stack.Peek();
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
int pop = stack.Pop();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
int size = stack.Count;
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
bool isEmpty = stack.Count == 0;
|
|||
|
```
|
|||
|
|
|||
|
=== "Go"
|
|||
|
|
|||
|
```go title="stack_test.go"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// 在 Go 中,推薦將 Slice 當作堆疊來使用
|
|||
|
var stack []int
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack = append(stack, 1)
|
|||
|
stack = append(stack, 3)
|
|||
|
stack = append(stack, 2)
|
|||
|
stack = append(stack, 5)
|
|||
|
stack = append(stack, 4)
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
peek := stack[len(stack)-1]
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
pop := stack[len(stack)-1]
|
|||
|
stack = stack[:len(stack)-1]
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
size := len(stack)
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
isEmpty := len(stack) == 0
|
|||
|
```
|
|||
|
|
|||
|
=== "Swift"
|
|||
|
|
|||
|
```swift title="stack.swift"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// Swift 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用
|
|||
|
var stack: [Int] = []
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.append(1)
|
|||
|
stack.append(3)
|
|||
|
stack.append(2)
|
|||
|
stack.append(5)
|
|||
|
stack.append(4)
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
let peek = stack.last!
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
let pop = stack.removeLast()
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
let size = stack.count
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
let isEmpty = stack.isEmpty
|
|||
|
```
|
|||
|
|
|||
|
=== "JS"
|
|||
|
|
|||
|
```javascript title="stack.js"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// JavaScript 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用
|
|||
|
const stack = [];
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1);
|
|||
|
stack.push(3);
|
|||
|
stack.push(2);
|
|||
|
stack.push(5);
|
|||
|
stack.push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
const peek = stack[stack.length-1];
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
const pop = stack.pop();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
const size = stack.length;
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
const is_empty = stack.length === 0;
|
|||
|
```
|
|||
|
|
|||
|
=== "TS"
|
|||
|
|
|||
|
```typescript title="stack.ts"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// TypeScript 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用
|
|||
|
const stack: number[] = [];
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1);
|
|||
|
stack.push(3);
|
|||
|
stack.push(2);
|
|||
|
stack.push(5);
|
|||
|
stack.push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
const peek = stack[stack.length - 1];
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
const pop = stack.pop();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
const size = stack.length;
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
const is_empty = stack.length === 0;
|
|||
|
```
|
|||
|
|
|||
|
=== "Dart"
|
|||
|
|
|||
|
```dart title="stack.dart"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// Dart 沒有內建的堆疊類別,可以把 List 當作堆疊來使用
|
|||
|
List<int> stack = [];
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.add(1);
|
|||
|
stack.add(3);
|
|||
|
stack.add(2);
|
|||
|
stack.add(5);
|
|||
|
stack.add(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
int peek = stack.last;
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
int pop = stack.removeLast();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
int size = stack.length;
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
bool isEmpty = stack.isEmpty;
|
|||
|
```
|
|||
|
|
|||
|
=== "Rust"
|
|||
|
|
|||
|
```rust title="stack.rs"
|
|||
|
/* 初始化堆疊 */
|
|||
|
// 把 Vec 當作堆疊來使用
|
|||
|
let mut stack: Vec<i32> = Vec::new();
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1);
|
|||
|
stack.push(3);
|
|||
|
stack.push(2);
|
|||
|
stack.push(5);
|
|||
|
stack.push(4);
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
let top = stack.last().unwrap();
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
let pop = stack.pop().unwrap();
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
let size = stack.len();
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
let is_empty = stack.is_empty();
|
|||
|
```
|
|||
|
|
|||
|
=== "C"
|
|||
|
|
|||
|
```c title="stack.c"
|
|||
|
// C 未提供內建堆疊
|
|||
|
```
|
|||
|
|
|||
|
=== "Kotlin"
|
|||
|
|
|||
|
```kotlin title="stack.kt"
|
|||
|
/* 初始化堆疊 */
|
|||
|
val stack = Stack<Int>()
|
|||
|
|
|||
|
/* 元素入堆疊 */
|
|||
|
stack.push(1)
|
|||
|
stack.push(3)
|
|||
|
stack.push(2)
|
|||
|
stack.push(5)
|
|||
|
stack.push(4)
|
|||
|
|
|||
|
/* 訪問堆疊頂元素 */
|
|||
|
val peek = stack.peek()
|
|||
|
|
|||
|
/* 元素出堆疊 */
|
|||
|
val pop = stack.pop()
|
|||
|
|
|||
|
/* 獲取堆疊的長度 */
|
|||
|
val size = stack.size
|
|||
|
|
|||
|
/* 判斷是否為空 */
|
|||
|
val isEmpty = stack.isEmpty()
|
|||
|
```
|
|||
|
|
|||
|
=== "Ruby"
|
|||
|
|
|||
|
```ruby title="stack.rb"
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
=== "Zig"
|
|||
|
|
|||
|
```zig title="stack.zig"
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
??? pythontutor "視覺化執行"
|
|||
|
|
|||
|
https://pythontutor.com/render.html#code=%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%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
|||
|
|
|||
|
## 堆疊的實現
|
|||
|
|
|||
|
為了深入瞭解堆疊的執行機制,我們來嘗試自己實現一個堆疊類別。
|
|||
|
|
|||
|
堆疊遵循先入後出的原則,因此我們只能在堆疊頂新增或刪除元素。然而,陣列和鏈結串列都可以在任意位置新增和刪除元素,**因此堆疊可以視為一種受限制的陣列或鏈結串列**。換句話說,我們可以“遮蔽”陣列或鏈結串列的部分無關操作,使其對外表現的邏輯符合堆疊的特性。
|
|||
|
|
|||
|
### 基於鏈結串列的實現
|
|||
|
|
|||
|
使用鏈結串列實現堆疊時,我們可以將鏈結串列的頭節點視為堆疊頂,尾節點視為堆疊底。
|
|||
|
|
|||
|
如下圖所示,對於入堆疊操作,我們只需將元素插入鏈結串列頭部,這種節點插入方法被稱為“頭插法”。而對於出堆疊操作,只需將頭節點從鏈結串列中刪除即可。
|
|||
|
|
|||
|
=== "LinkedListStack"
|
|||
|
![基於鏈結串列實現堆疊的入堆疊出堆疊操作](stack.assets/linkedlist_stack_step1.png)
|
|||
|
|
|||
|
=== "push()"
|
|||
|
![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png)
|
|||
|
|
|||
|
=== "pop()"
|
|||
|
![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png)
|
|||
|
|
|||
|
以下是基於鏈結串列實現堆疊的示例程式碼:
|
|||
|
|
|||
|
```src
|
|||
|
[file]{linkedlist_stack}-[class]{linked_list_stack}-[func]{}
|
|||
|
```
|
|||
|
|
|||
|
### 基於陣列的實現
|
|||
|
|
|||
|
使用陣列實現堆疊時,我們可以將陣列的尾部作為堆疊頂。如下圖所示,入堆疊與出堆疊操作分別對應在陣列尾部新增元素與刪除元素,時間複雜度都為 $O(1)$ 。
|
|||
|
|
|||
|
=== "ArrayStack"
|
|||
|
![基於陣列實現堆疊的入堆疊出堆疊操作](stack.assets/array_stack_step1.png)
|
|||
|
|
|||
|
=== "push()"
|
|||
|
![array_stack_push](stack.assets/array_stack_step2_push.png)
|
|||
|
|
|||
|
=== "pop()"
|
|||
|
![array_stack_pop](stack.assets/array_stack_step3_pop.png)
|
|||
|
|
|||
|
由於入堆疊的元素可能會源源不斷地增加,因此我們可以使用動態陣列,這樣就無須自行處理陣列擴容問題。以下為示例程式碼:
|
|||
|
|
|||
|
```src
|
|||
|
[file]{array_stack}-[class]{array_stack}-[func]{}
|
|||
|
```
|
|||
|
|
|||
|
## 兩種實現對比
|
|||
|
|
|||
|
**支持操作**
|
|||
|
|
|||
|
兩種實現都支持堆疊定義中的各項操作。陣列實現額外支持隨機訪問,但這已超出了堆疊的定義範疇,因此一般不會用到。
|
|||
|
|
|||
|
**時間效率**
|
|||
|
|
|||
|
在基於陣列的實現中,入堆疊和出堆疊操作都在預先分配好的連續記憶體中進行,具有很好的快取本地性,因此效率較高。然而,如果入堆疊時超出陣列容量,會觸發擴容機制,導致該次入堆疊操作的時間複雜度變為 $O(n)$ 。
|
|||
|
|
|||
|
在基於鏈結串列的實現中,鏈結串列的擴容非常靈活,不存在上述陣列擴容時效率降低的問題。但是,入堆疊操作需要初始化節點物件並修改指標,因此效率相對較低。不過,如果入堆疊元素本身就是節點物件,那麼可以省去初始化步驟,從而提高效率。
|
|||
|
|
|||
|
綜上所述,當入堆疊與出堆疊操作的元素是基本資料型別時,例如 `int` 或 `double` ,我們可以得出以下結論。
|
|||
|
|
|||
|
- 基於陣列實現的堆疊在觸發擴容時效率會降低,但由於擴容是低頻操作,因此平均效率更高。
|
|||
|
- 基於鏈結串列實現的堆疊可以提供更加穩定的效率表現。
|
|||
|
|
|||
|
**空間效率**
|
|||
|
|
|||
|
在初始化串列時,系統會為串列分配“初始容量”,該容量可能超出實際需求;並且,擴容機制通常是按照特定倍率(例如 2 倍)進行擴容的,擴容後的容量也可能超出實際需求。因此,**基於陣列實現的堆疊可能造成一定的空間浪費**。
|
|||
|
|
|||
|
然而,由於鏈結串列節點需要額外儲存指標,**因此鏈結串列節點佔用的空間相對較大**。
|
|||
|
|
|||
|
綜上,我們不能簡單地確定哪種實現更加節省記憶體,需要針對具體情況進行分析。
|
|||
|
|
|||
|
## 堆疊的典型應用
|
|||
|
|
|||
|
- **瀏覽器中的後退與前進、軟體中的撤銷與反撤銷**。每當我們開啟新的網頁,瀏覽器就會對上一個網頁執行入堆疊,這樣我們就可以通過後退操作回到上一個網頁。後退操作實際上是在執行出堆疊。如果要同時支持後退和前進,那麼需要兩個堆疊來配合實現。
|
|||
|
- **程式記憶體管理**。每次呼叫函式時,系統都會在堆疊頂新增一個堆疊幀,用於記錄函式的上下文資訊。在遞迴函式中,向下遞推階段會不斷執行入堆疊操作,而向上回溯階段則會不斷執行出堆疊操作。
|