Stack — Ngăn xếp và Nguyên lý LIFO
Khi học về Array hay Linked List, bạn có toàn quyền tự do: muốn chèn dữ liệu vào đầu, vào cuối, hay móc dữ liệu từ giữa ra đều được. Sự tự do đó tuy mạnh mẽ nhưng lại đi kèm với những rủi ro về tính nhất quán dữ liệu.
Đôi khi, để giải quyết một lớp bài toán cụ thể, chúng ta cần hạn chế sự tự do lại bằng các nguyên tắc nghiêm ngặt. Chào mừng bạn đến với cấu trúc dữ liệu bị ràng buộc nhiều nhất nhưng cũng hữu dụng nhất: Stack (Ngăn xếp).
📋 Agenda
Thời gian đọc ước tính: ~6 phút
Sau bài này, bạn sẽ:
- ✅ Hiểu được nguyên lý LIFO của Stack.
- ✅ Giải thích được cách tính năng
Undo(Ctrl+Z) hay Call Stack của JavaScript hoạt động. - ✅ Tự tay implement một Stack bằng Array trong TypeScript.
- ✅ Phân biệt được khi nào dùng Stack là thừa thãi, khi nào là bắt buộc.
Yêu cầu đầu vào:
- 🔹 Có kiến thức cơ bản về Array.
❓ Tại sao lại phải tự trói buộc mình?
Vấn đề (Problem Statement): Bạn đang làm chức năng "Undo" (Ctrl + Z) cho một ứng dụng soạn thảo văn bản (như Microsoft Word). Nếu dùng Array thông thường và cho phép Dev tùy ý chèn/xóa ở mọi vị trí, sẽ rất dễ xảy ra lỗi ai đó lỡ tay xóa mất lịch sử Undo ở giữa, làm hỏng toàn bộ luồng khôi phục dữ liệu theo thời gian.
Giải pháp (Solution):
Bạn cần một cấu trúc ép buộc quy tắc: "Cái gì bỏ vào sau cùng thì phải được lấy ra đầu tiên". Không ai được phép lấy/xóa dữ liệu ở giữa hay ở đáy. Cấu trúc đó gọi là Stack. Bằng cách giới hạn API chỉ còn push (đẩy vào đỉnh) và pop (lấy ra từ đỉnh), chúng ta đảm bảo dữ liệu luôn được xử lý theo đúng trật tự thời gian mong muốn một cách an toàn nhất.
📖 Stack hoạt động như thế nào?
Định nghĩa: Stack là một cấu trúc dữ liệu tuyến tính hoạt động theo nguyên lý LIFO (Last In, First Out) — Vào sau, Ra trước.
Trực quan hóa cấu trúc
Hãy tưởng tượng một chồng đĩa trong nhà hàng. Bạn chỉ có thể đặt đĩa mới lên trên cùng, và khi lấy đĩa ra, bạn cũng phải lấy cái ở trên cùng. Nếu cố rút cái đĩa ở giữa, cả chồng sẽ đổ!
Các thao tác cốt lõi (Time Complexity đều là O(1))
- push(item): Đặt một phần tử lên Đỉnh.
- pop(): Lấy (và xóa) phần tử ở Đỉnh ra ngoài.
- peek() / top(): Chỉ xem phần tử ở Đỉnh mà không xóa.
🔨 Implement Stack bằng TypeScript
Chúng ta có thể implement Stack bằng Linked List hoặc Array. Vì trong JavaScript/TypeScript, thao tác thêm/xóa ở cuối mảng (push/pop) đã được V8 Engine tối ưu hóa cực tốt (O(1)), nên việc bọc một Array lại thành Stack là cách làm ph ổ biến và hiệu quả nhất.
1. Định nghĩa Interface (Hợp đồng)
// filename: stack.ts
interface IStack<T> {
push(item: T): void;
pop(): T | undefined;
peek(): T | undefined;
isEmpty(): boolean;
readonly size: number;
}
2. Triển khai Class
// filename: stack.ts
class Stack<T> implements IStack<T> {
// 💡 Sử dụng Array làm kho lưu trữ ngầm (Underlying Data Structure)
// Đặt là private để KHÔNG AI có thể dùng ngoặc vuông truy cập `items[1]`!
private items: T[] = [];
// O(1) - Thêm vào đỉnh (cuối mảng)
push(item: T): void {
this.items.push(item);
}
// O(1) - Rút từ đỉnh (cuối mảng)
pop(): T | undefined {
return this.items.pop();
}
// O(1) - Nhìn phần tử trên cùng
peek(): T | undefined {
if (this.isEmpty()) return undefined;
return this.items[this.items.length - 1];
}
// O(1)
isEmpty(): boolean {
return this.items.length === 0;
}
// Getter cho kích thước
get size(): number {
return this.items.length;
}
}
Ví dụ: Xây dựng tính năng Undo/Redo
const undoStack = new Stack<string>();
const redoStack = new Stack<string>();
// Người dùng gõ văn bản
undoStack.push("Type 'Hello'");
undoStack.push("Type ' World'");
undoStack.push("Format 'Bold'");
// Người dùng bấm Ctrl + Z (Undo)
const lastAction = undoStack.pop();
console.log(`Hoàn tác hành động: ${lastAction}`); // Format 'Bold'
redoStack.push(lastAction); // Lưu vào Redo để lỡ đổi ý