Ngăn xếp (stack)
Cập nhật lần cuối
Ngăn xếp (stack) là một tập hợp chỉ có đúng một đầu mở. Bạn thêm một giá trị bằng cách push nó lên đỉnh, và lấy một giá trị ra bằng cách pop phần tử ở đỉnh, nên giá trị vào sau cùng luôn ra đầu tiên. Đó là ý nghĩa của LIFO, và đó là toàn bộ quy tắc: không có cách nào chạm vào phần giữa nếu chưa lấy ra những gì nằm phía trên. Nhấn Phát ở trên và xem cột cao dần sau mỗi lần push và thấp dần từ cùng đầu đó sau mỗi lần pop.
Chính giới hạn đó là mấu chốt. Vì cả hai thao tác chỉ chạm vào đỉnh, mỗi thao tác đều tốn O(1) dù ngăn xếp cao đến đâu, và sự dễ đoán đó là lý do ngăn xếp nằm bên dưới rất nhiều thứ trong máy tính: ngăn xếp lời gọi chạy đệ quy, lịch sử hoàn tác trong trình soạn thảo, việc kiểm tra cặp ngoặc trong bộ phân tích cú pháp, và ngăn xếp tường minh biến tìm kiếm theo chiều sâu đệ quy thành một vòng lặp. Đổi đầu lấy ra và bạn có một hàng đợi.
Độ phức tạp thời gian và không gian
Với ngăn xếp tiêu chuẩn dùng mảng hoặc danh sách liên kết:
| Thao tác | Độ phức tạp | Ghi chú |
|---|---|---|
| Push | O(1) | O(1) khấu hao trên mảng động, thỉnh thoảng phải đổi kích thước. |
| Pop | O(1) | Luôn là phần tử ở đỉnh, nên không phải dời phần tử. |
| Peek (đỉnh) | O(1) | Đọc phần tử ở đỉnh mà không lấy ra. |
| Tìm kiếm | O(n) | Không phải việc của ngăn xếp: bạn phải pop dần xuống. |
| Không gian | O(n) | Mỗi giá trị được lưu chiếm một ô. |
Từng bước
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Ngăn xếp bắt đầu rỗng, đỉnh không trỏ vào đâu cả. |
| 2 | Push ghi giá trị vào vị trí đỉnh và dời đỉnh lên một ô. |
| 3 | Mỗi lần push tiếp theo đặt giá trị ngay phía trên giá trị trước đó. |
| 4 | Pop đọc giá trị ở đỉnh, rồi dời đỉnh xuống một ô. |
| 5 | Giá trị được trả về luôn là giá trị được push gần nhất. |
| 6 | Pop một ngăn xếp rỗng là lỗi, gọi là stack underflow, nên code thực tế kiểm tra is_empty() trước. |
Ví dụ minh họa
Push 3, 7, 5 rồi lấy hết ngăn xếp ra:
| Thao tác | Ngăn xếp (từ đáy lên đỉnh) | Trả về |
|---|---|---|
push(3) | [3] | không có gì |
push(7) | [3, 7] | không có gì |
push(5) | [3, 7, 5] | không có gì |
pop() | [3, 7] | 5, giá trị mới nhất |
pop() | [3] | 7 |
pop() | [] | 3, giá trị cũ nhất, ra sau cùng |
Khi nào nên dùng ngăn xếp
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần lấy lại phần tử mới nhất trước: hoàn tác, nút quay lại, kiểm tra cặp ngoặc | Bạn cần phần tử cũ nhất trước, đó là việc của hàng đợi |
| Bạn đang chuyển một thuật toán đệ quy thành vòng lặp | Bạn cần tìm kiếm hoặc truy cập theo chỉ số vào giữa dữ liệu |
| Bạn đang phân tích cấu trúc lồng nhau như biểu thức, JSON hoặc HTML | Nhiều nơi cần truy cập tùy ý, khi đó mảng hoặc map phù hợp hơn |
Bạn muốn chèn và lấy ra được bảo đảm O(1), không cần cân bằng lại | Bạn cần dữ liệu luôn được sắp xếp, điều mà heap hoặc cây mang lại |
Code Stack
Một cách cài đặt Stack gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Stack bằng Python
1stack = []2
3# Push three values onto the top4for value in [3, 7, 5]:5 stack.append(value)6 print(f"push {value} -> {stack}")7
8# Pop them back off: last in, first out9while stack:10 value = stack.pop()11 print(f"pop {value} -> {stack}")12
13print("empty:", len(stack) == 0)Code Stack bằng JavaScript
1const stack = [];2
3// Push three values onto the top4for (const value of [3, 7, 5]) {5 stack.push(value);6 console.log(`push ${value} ->`, stack);7}8
9// Pop them back off: last in, first out10while (stack.length > 0) {11 const value = stack.pop();12 console.log(`pop ${value} ->`, stack);13}14
15console.log('empty:', stack.length === 0);Code Stack bằng Java
1import java.util.ArrayDeque;2import java.util.Deque;3
4public class Main {5 public static void main(String[] args) {6 Deque<Integer> stack = new ArrayDeque<>();7
8 // Push three values onto the top9 for (int value : new int[] {3, 7, 5}) {10 stack.push(value);11 System.out.println("push " + value + " -> " + stack);12 }13
14 // Pop them back off: last in, first out15 while (!stack.isEmpty()) {16 int value = stack.pop();17 System.out.println("pop " + value + " -> " + stack);18 }19
20 System.out.println("empty: " + stack.isEmpty());21 }22}Code Stack bằng C++
1#include <iostream>2#include <stack>3
4int main() {5 std::stack<int> stack;6
7 // Push three values onto the top8 for (int value : {3, 7, 5}) {9 stack.push(value);10 std::cout << "push " << value << " -> size " << stack.size() << "\n";11 }12
13 // Pop them back off: last in, first out14 while (!stack.empty()) {15 int value = stack.top();16 stack.pop();17 std::cout << "pop " << value << " -> size " << stack.size() << "\n";18 }19
20 std::cout << "empty: " << std::boolalpha << stack.empty() << "\n";21 return 0;22}Code Stack bằng C
1#include <stdio.h>2
3#define CAP 164
5int stack[CAP];6int top = 0; /* index of the next free slot */7
8int main(void) {9 int values[3] = {3, 7, 5};10
11 /* Push three values onto the top */12 for (int i = 0; i < 3; i++) {13 stack[top++] = values[i];14 printf("push %d -> size %d\n", values[i], top);15 }16
17 /* Pop them back off: last in, first out */18 while (top > 0) {19 int value = stack[--top];20 printf("pop %d -> size %d\n", value, top);21 }22
23 printf("empty: %d\n", top == 0);24 return 0;25}Câu hỏi thường gặp về ngăn xếp
LIFO nghĩa là gì?
Ngăn xếp và hàng đợi khác nhau như thế nào?
O(1); ngăn xếp lấy ra từ chính đầu đó (LIFO), hàng đợi lấy ra từ đầu còn lại (FIFO). Mọi thứ khác, kể cả bảng độ phức tạp ở trên, đều giống hệt nhau.Các thao tác chính trên ngăn xếp là gì?
push thêm một giá trị lên đỉnh, pop lấy ra và trả về giá trị ở đỉnh, peek (đôi khi là top) đọc giá trị ở đỉnh mà không lấy ra, và is_empty cho biết còn gì không. Cả bốn đều tốn O(1).Tràn ngăn xếp (stack overflow) là gì?
Ngăn xếp được cài đặt như thế nào?
O(1) khấu hao và thân thiện với cache: list của Python và ArrayDeque của Java hoạt động theo cách này. Danh sách liên kết push và pop ở đầu, tốn O(1) cả trong trường hợp xấu nhất, không cần đổi kích thước nhưng mỗi phần tử tốn thêm một con trỏ. std::stack của C++ là một adaptor mặc định chạy trên std::deque, một mảng phân đoạn, và chấp nhận container khác nếu bạn truyền vào.