🎯 Bài giảng: Cấu trúc dữ liệu Stack (Ngăn xếp)
Stack là một trong những cấu trúc dữ liệu cơ bản nhất, được dùng rộng rãi trong thuật toán và rất gần gũi với đời sống (chồng sách, đĩa, khay cốc...).
I. Giới thiệu & Ví dụ thực tế
Hình dung bạn xếp chồng sách. Muốn lấy sách ở trên cùng bạn phải nhấc quyển vừa đặt gần nhất. Đó là nguyên lý LIFO – Last In, First Out (vào sau, ra trước).
Nguyên tắc LIFO – “vào sau ra trước”.
II. Lý thuyết cơ bản
- Push(x): thêm phần tử x vào đỉnh.
- Pop(): lấy phần tử ở đỉnh ra khỏi stack.
- Top(): xem (không xoá) phần tử ở đỉnh.
- Empty(): kiểm tra rỗng.
- Size(): số lượng phần tử hiện có.
#include
#include
using namespace std;
int main() {
stack st;
st.push(10);
st.push(20);
cout << st.top() << "
"; // 20
st.pop(); // xoá 20
cout << st.size() << "
"; // 1
}
III. Trình mô phỏng Stack: hình ảnh + mô tả từng dòng lệnh
Trong mô phỏng dưới đây, giao diện được chia làm 3 cột:
- 🖱 Cột 1 – Thao tác: Các nút mô phỏng hành động của người lập trình (Push, Pop, Top, Size, Reset).
- 💻 Cột 2 – Dòng lệnh tương ứng: Mỗi thao tác sẽ tô sáng các dòng lệnh C++ mà máy tính thực hiện.
- 📊 Cột 3 – Kết quả minh hoạ: Stack hiển thị theo thời gian thực sau khi lệnh được chạy.
🖱 Thao tác
💻 Code đang thực thi
📊 Kết quả Stack
size=0VI. Khi nào dùng Stack & Cách nhận biết bài toán cần Stack
Stack được gắn tag cho những bài toán mà thứ tự xử lý dữ liệu phụ thuộc vào nguyên tắc “vào sau ra trước” hoặc khi ta cần ghi nhớ tạm thời các trạng thái để quay lại sau. Dưới đây là các nhóm bài toán phổ biến sử dụng Stack, kèm lý do và ví dụ.
| Nhóm bài toán | Đặc điểm nhận biết | Ví dụ & Ứng dụng | Tại sao dùng Stack? |
|---|---|---|---|
| 1. Đảo ngược dữ liệu | Dữ liệu được đưa vào rồi cần xử lý theo thứ tự ngược lại. | Đảo chuỗi, đảo mảng, in ngược số, đảo thứ tự từ trong câu. | Stack lưu tạm các phần tử, pop ra ngược thứ tự ban đầu (LIFO). |
| 2. Kiểm tra cấu trúc cặp (ngoặc, thẻ XML,...) | Có cặp mở – đóng cần khớp và đúng thứ tự. | Kiểm tra dấu ngoặc, kiểm tra HTML/XML hợp lệ. | Khi gặp mở → push, gặp đóng → pop; nếu lệch cặp hoặc còn sót → sai. |
| 3. Biểu thức toán học | Gồm toán tử, toán hạng cần tính theo thứ tự ưu tiên. | Chuyển biểu thức trung tố ↔ hậu tố, tính hậu tố/postfix, tiền tố/prefix. | Stack dùng để lưu toán tử và tạm toán hạng trong khi tính. |
| 4. Duyệt cây & đồ thị (DFS, Backtracking) | Cần ghi nhớ trạng thái trước khi quay lui hoặc đi nhánh khác. | DFS không đệ quy, thuật toán quay lui, giải mê cung. | Stack thay thế ngăn gọi đệ quy để nhớ “nút đang đi”. |
| 5. Stack đơn điệu – tìm phần tử kế tiếp lớn hơn / nhỏ hơn | So sánh phần tử với các phần tử đứng trước để tìm vị trí đặc biệt. | Next Greater Element, Daily Temperatures, dãy đơn điệu tăng/giảm. | Duyệt mảng, dùng stack lưu chỉ số; khi gặp phần tử lớn hơn thì pop để gán kết quả. |
| 6. Tính diện tích hoặc vùng liên tiếp (Histogram, Rain Water) | Bài có biểu đồ cột, cần tìm vùng lớn nhất/liên tục. | Diện tích hình chữ nhật lớn nhất trong histogram, hứng nước mưa. | Dùng stack lưu chỉ số cột tăng dần. Khi gặp cột thấp hơn → pop để tính vùng vừa khép lại. |
| 7. Quản lý lịch sử hành động (Undo/Redo, Browser) | Hệ thống cho phép quay lại bước trước hoặc tiến lên bước sau. | Undo/Redo trong Word, lịch sử duyệt web. | Hai stack: một cho “Undo”, một cho “Redo” – pop qua lại giữa hai ngăn. |
| 8. Mô phỏng hệ thống gọi lồng nhau | Hàm hoặc quy trình được gọi lồng nhau (call stack). | Hàm đệ quy, chương trình thông dịch biểu thức lồng. | Hệ thống máy tính thực tế dùng call stack để lưu địa chỉ trả về. |
| 9. Đánh giá thời gian / khoảng hoạt động | Bài liên quan đến thời gian bắt đầu – kết thúc xen kẽ nhau. | Tính thời gian CPU chạy hàm, log hệ thống dạng start/end. |
Mỗi khi gặp “start” → push, “end” → pop để tính khoảng thời gian. |
💡 Tóm lại: Hễ bài toán cần “ghi nhớ tạm thời để quay lại”, “đảo ngược thứ tự”, hoặc “đóng–mở cặp” theo quy luật → gần như chắc chắn có thể giải bằng Stack.
V. Hệ thống bài tập theo cấp độ (có giải thích chi tiết)
📘 Egg
1️⃣ Bài 1: Đảo ngược chuỗi ký tự (cơ bản)
Đề: Nhập chuỗi S (không chứa khoảng trắng). In chuỗi đảo ngược bằng Stack.
Ý tưởng: Duyệt từng ký tự, push vào stack → pop ra dần để in ngược.
Bước làm: (1) Tạo stack. (2) Duyệt S và push. (3) Trong khi chưa rỗng, pop và in. (4) Kết thúc.
Độ phức tạp: O(n) thời gian, O(n) bộ nhớ (n là độ dài chuỗi).
Lưu ý: Nếu có khoảng trắng, dùng getline thay vì cin>>.
#include
#include
using namespace std;
int main(){
string s; cin >> s;
stack st;
for(char c : s) st.push(c);
while(!st.empty()){ cout << st.top(); st.pop(); }
}
Test mẫu: hello → olleh
1️⃣ Bài 2: Tổng các phần tử (luyện thao tác)
Đề: Nhập n, sau đó n số nguyên. Tính tổng bằng cách lấy ra từng phần tử qua stack.
Ý tưởng: Dùng stack để “lưu rồi lấy ngược”, đồng thời cộng dồn.
Bước làm: Push toàn bộ → while !empty: cộng top rồi pop.
Độ phức tạp: O(n). Lưu ý: Dùng long long nếu tổng lớn.
#include
#include
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(nullptr);
int n; long long x,sum=0; cin>>n;
stack st;
for(int i=0;i>x; st.push(x);}
while(!st.empty()){ sum+=st.top(); st.pop(); }
cout<
📗 Chicken
2️⃣ Bài 3: Kiểm tra dấu ngoặc hợp lệ
Đề: Chuỗi gồm ()[]{}<>. Hãy kiểm tra có hợp lệ không (mở – đóng đúng cặp, đúng thứ tự).
Ý tưởng: Gặp ngoặc mở → push. Gặp ngoặc đóng → pop để so cặp. Nếu sai hoặc rỗng → không hợp lệ.
Bước làm: Duyệt chuỗi:
• Nếu là mở: push.
• Nếu là đóng: nếu rỗng → sai; else kiểm tra xem có khớp cặp không.
• Cuối cùng stack phải rỗng.
Độ phức tạp: O(n). Lưu ý: Thêm cặp <> nếu muốn.
#include
using namespace std;
bool match(char o, char c){
return (o=='('&&c==')')||(o=='['&&c==']')||(o=='{'&&c=='}')||(o=='<'&&c=='>');
}
int main(){
ios::sync_with_stdio(false); cin.tie(nullptr);
string s; cin>>s;
stack st;
for(char c : s){
if(c=='('||c=='['||c=='{'||c=='<') st.push(c);
else {
if(st.empty() || !match(st.top(), c)) return cout<<"NO",0;
st.pop();
}
}
cout<<(st.empty()?"YES":"NO");
}
Test: ({[]}) → YES; ([)] → NO; <[]>() → YES.
2️⃣ Bài 4: Đảo thứ tự từ trong câu
Đề: Nhập một câu gồm nhiều từ cách nhau bằng khoảng trắng. In lại các từ theo thứ tự ngược.
Ý tưởng: Tách từ, push từng từ vào stack rồi pop dần để in ngược.
Bước làm: stringstream để tách từ → stack
Độ phức tạp: O(n) theo số từ/ký tự.
#include
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(nullptr);
string s, w; getline(cin, s);
stack st;
stringstream ss(s);
while(ss>>w) st.push(w);
bool first=true;
while(!st.empty()){
if(!first) cout<<' ';
cout<
Ví dụ: I love coding → coding love I
📙 Chicken farmer
3️⃣ Bài 5: Tính biểu thức hậu tố (Postfix Evaluation)
Đề: Cho biểu thức hậu tố gồm số nguyên và toán tử + - * / cách nhau bởi khoảng trắng. Hãy tính giá trị.
Ý tưởng: Gặp số → push. Gặp toán tử → pop 2 số theo thứ tự (a trước, b sau), tính a op b, rồi push kết quả.
Bước làm: Duyệt token bằng stringstream. Dùng stack. Cẩn thận thứ tự khi trừ/chia.
Độ phức tạp: O(n) theo số token.
Lưu ý: Kiểm tra lỗi chia cho 0 nếu cần; nếu có âm/đa chữ số, vẫn nhận diện bằng isdigit(token[0]) hoặc kiểm tra toàn chuỗi.
#include
using namespace std;
bool isNumber(const string &t){
if(t.empty()) return false;
int i=0; if(t[0]=='-'||t[0]=='+') i=1;
for(;i<(int)t.size();++i) if(!isdigit(t[i])) return false;
return true;
}
int main(){
ios::sync_with_stdio(false); cin.tie(nullptr);
string s; getline(cin, s);
stringstream ss(s); string tok;
stack st;
while(ss>>tok){
if(isNumber(tok)) st.push(stoll(tok));
else{
long long b=st.top(); st.pop();
long long a=st.top(); st.pop();
if(tok=="+") st.push(a+b);
else if(tok=="-") st.push(a-b);
else if(tok=="*") st.push(a*b);
else if(tok=="/") st.push(a/b); // giả sử b != 0, chia số nguyên
}
}
cout<
Ví dụ: 5 1 2 + 4 * + 3 - → 14
3️⃣ Bài 6: Next Greater Element (NGE) – Dãy đơn điệu
Đề: Với mỗi phần tử A[i], tìm phần tử bên phải đầu tiên lớn hơn nó; nếu không có in -1.
Ý tưởng (stack chỉ số tăng dần): Duyệt i từ trái sang phải. Trong khi A[i] > A[st.top()], cập nhật kết quả cho vị trí đó và pop. Sau đó push i.
Bước làm: Dùng stack lưu chỉ số. Mảng kết quả khởi tạo -1.
Độ phức tạp: O(n) vì mỗi chỉ số push/pop tối đa 1 lần.
Lưu ý: Đây là “stack đơn điệu” – mẫu rất quan trọng cho nhiều bài như nhiệt độ hàng ngày, diện tích hình chữ nhật lớn nhất…
#include
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin>>n; vector a(n), ans(n, -1);
for(int i=0;i>a[i];
stack st;
for(int i=0;i a[st.top()]){
ans[st.top()] = a[i];
st.pop();
}
st.push(i);
}
for(int i=0;i':' ');
}
Ví dụ: 4 | 4 5 2 10 → 5 10 10 -1
VII. Tổng kết & Gợi ý mở rộng
- Nắm chắc thao tác cơ bản → áp dụng vào đệ quy, backtracking, DFS.
- Học thêm stack đơn điệu để giải các bài dãy số/nhiệt độ/biểu đồ.
- Thử tự cài đặt stack bằng
vector(tự viếtpush/pop/back).
📘 Stack – nền tảng luyện tư duy thuật toán. Hãy luyện từ dễ → khó, và quan sát “vào sau – ra trước” trong mọi tình huống.
💳 Quét mã ủng hộ tuỳ tâm nhé!