📚 Stack – Ngăn xếp LIFO
Stack xử lý phần tử theo nguyên tắc vào sau, ra trước. Từ thao tác push/pop cơ bản, Stack được dùng để kiểm tra dấu ngoặc, tính biểu thức, DFS không đệ quy và giải nhiều bài mảng bằng Monotonic Stack.
🎯 Mục tiêu bài học
Cần hiểu
- Nguyên tắc LIFO.
- Các thao tác
push,top,pop,empty,size. - Vì sao phải kiểm tra rỗng trước top/pop.
- Cách Stack xử lý ngoặc và biểu thức hậu tố.
- Cách Monotonic Stack giảm O(n²) xuống O(n).
Dấu hiệu nhận biết
- Cần đảo ngược thứ tự.
- Cấu trúc mở–đóng lồng nhau.
- Cần quay lại trạng thái gần nhất.
- Tìm phần tử lớn/nhỏ gần nhất.
- Mỗi phần tử được push/pop tối đa một lần.
1. Đặt vấn đề
Cho chuỗi gồm các ký tự
()[]{}. Hãy kiểm tra chuỗi có đúng cặp và đúng thứ tự lồng nhau hay không.
Ví dụ:
({[]}): hợp lệ.([)]: không hợp lệ vì ngoặc đóng không khớp ngoặc mở gần nhất.
1.1. Cần nhớ ngoặc mở nào?
Khi gặp ngoặc đóng, ta phải so với ngoặc mở chưa được đóng gần nhất. Đây chính là phần tử được đưa vào sau cùng.
Với mỗi vị trí i, tìm phần tử đầu tiên bên phải lớn hơn
a[i].
Duyệt trực tiếp từ i+1 cho từng i mất O(n²). Nếu lưu các chỉ số chưa tìm được đáp án trong Stack, mỗi chỉ số chỉ push và pop một lần, tổng O(n).
2. Stack cơ bản trong C++
Thao tác
st.push(x): thêm vào đỉnh.st.top(): xem đỉnh.st.pop(): xóa đỉnh.st.empty(): kiểm tra rỗng.st.size(): số phần tử.
Độ phức tạp
push: O(1).top: O(1).pop: O(1).- Không truy cập ngẫu nhiên bằng chỉ số.
- Muốn duyệt thường phải pop hoặc sao chép Stack.
pop() không trả về phần tử. Mẫu chuẩn là x = st.top(); st.pop();.3. Mô phỏng Stack và highlight code
4. Kiểm tra dấu ngoặc hợp lệ
Chuỗi
Stack ngoặc mở
top(). Cuối chuỗi, Stack phải rỗng.5. Tính biểu thức hậu tố
Trong hậu tố, toán tử đứng sau hai toán hạng. Ví dụ 5 1 2 + 4 * + 3 -.
Stack giá trị
a op b. Điều này đặc biệt quan trọng với phép trừ và chia.6. Monotonic Stack – Next Greater Element
Stack lưu chỉ số các phần tử chưa tìm được phần tử lớn hơn đầu tiên bên phải. Khi gặp a[i] lớn hơn a[st.top()], ta đã tìm được đáp án cho đỉnh Stack.
Stack chỉ số
7. Phân tích thuật toán
| Bài toán | Thời gian | Bộ nhớ | Giải thích |
|---|---|---|---|
| q thao tác Stack | O(q) | O(q) | Mỗi thao tác O(1). |
| Kiểm tra ngoặc | O(n) | O(n) | Mỗi ký tự xử lý một lần. |
| Tính postfix | O(t) | O(t) | Mỗi token push/pop hữu hạn lần. |
| Next Greater Element | O(n) | O(n) | Mỗi chỉ số push/pop tối đa một lần. |
| DFS lặp | O(V+E) | O(V) | Stack thay cho call stack. |
8. Code mẫu
#include
using namespace std;
int main() {
stack st;
st.push(10);
st.push(20);
st.push(30);
cout << st.top() << '
';
if (!st.empty()) {
int value = st.top();
st.pop();
cout << "Removed: " << value << '
';
}
cout << st.size();
} #include
using namespace std;
bool isMatching(char open, char close) {
return (open == '(' && close == ')')
|| (open == '[' && close == ']')
|| (open == '{' && close == '}');
}
int main() {
string s;
cin >> s;
stack st;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
if (st.empty() || !isMatching(st.top(), c)) {
cout << "NO";
return 0;
}
st.pop();
}
}
cout << (st.empty() ? "YES" : "NO");
} #include
using namespace std;
bool isNumber(const string& token) {
if (token.empty()) return false;
int position = 0;
if (token[0] == '-' && token.size() > 1) {
position = 1;
}
for (; position < (int)token.size(); position++) {
if (!isdigit(token[position])) return false;
}
return true;
}
int main() {
string expression;
getline(cin, expression);
stringstream stream(expression);
string token;
stack st;
while (stream >> token) {
if (isNumber(token)) {
st.push(stoll(token));
continue;
}
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
if (token == "+") st.push(left + right);
if (token == "-") st.push(left - right);
if (token == "*") st.push(left * right);
if (token == "/") st.push(left / right);
}
cout << st.top();
} #include
using namespace std;
int main() {
int n;
cin >> n;
vector a(n);
vector answer(n, -1);
for (long long &x : a) cin >> x;
stack st;
for (int i = 0; i < n; i++) {
while (!st.empty() && a[st.top()] < a[i]) {
answer[st.top()] = a[i];
st.pop();
}
st.push(i);
}
for (long long x : answer) {
cout << x << ' ';
}
} def valid_brackets(text):
stack = []
matching = {
')': '(',
']': '[',
'}': '{'
}
for character in text:
if character in '([{':
stack.append(character)
else:
if not stack or stack[-1] != matching[character]:
return False
stack.pop()
return not stack9. Giải thích code
9.1. Vì sao phải kiểm tra empty trước top/pop?
Gọi top() hoặc pop() khi Stack rỗng gây hành vi không xác định.
9.2. Vì sao pop không trả về giá trị?
Trong STL, pop() chỉ xóa. Muốn dùng phần tử, lấy top() trước rồi mới pop.
9.3. Vì sao ngoặc đóng phải so với top?
Do cấu trúc lồng nhau, ngoặc mở được thêm sau cùng phải được đóng trước tiên.
9.4. Vì sao postfix cần pop b trước a?
Hai toán hạng được push theo thứ tự a rồi b, nên b nằm ở đỉnh. Với phép trừ, phải tính a-b, không phải b-a.
9.5. Vì sao NGE lưu chỉ số thay vì giá trị?
Cần gán đáp án cho đúng vị trí. Lưu chỉ số cũng giúp truy cập cả giá trị và vị trí.
9.6. Vì sao vòng while trong NGE vẫn O(n)?
10. Lỗi thường gặp
- Gọi
top()hoặcpop()khi Stack rỗng. - Tưởng
pop()trả về phần tử. - Kiểm tra ngoặc nhưng quên điều kiện Stack phải rỗng ở cuối.
- Postfix lấy sai thứ tự toán hạng khi trừ hoặc chia.
- Monotonic Stack lưu giá trị nhưng cần trả lời theo chỉ số.
- Dùng dấu
<hay<=không đúng khi xử lý phần tử bằng nhau. - Cho rằng vòng while bên trong for luôn tạo O(n²).
- Dùng DFS đệ quy quá sâu mà không cân nhắc Stack lặp.
11. Quiz và bài tập
📘 Cơ bản
- Đảo chuỗi bằng Stack.
- Mô phỏng q thao tác.
- Kiểm tra palindrome bằng Stack.
📗 Trung bình
- Kiểm tra dấu ngoặc.
- Tính postfix.
- DFS không đệ quy.
📙 Nâng cao
- Next Greater/Smaller Element.
- Daily Temperatures.
- Stock Span.
🐉 HSG
- Largest Rectangle in Histogram.
- Trapping Rain Water.
- Cartesian Tree định hướng.
Bài thực hành 1 – Kiểm tra ngoặc
Hỗ trợ bốn loại ngoặc ()[]{} và <>.
Bài thực hành 2 – Next Greater
Trả về cả giá trị và khoảng cách đến phần tử lớn hơn đầu tiên.
Bài thực hành 3 – Postfix
Hỗ trợ số âm, số nhiều chữ số và kiểm tra biểu thức không hợp lệ.
💳 Quét mã ủng hộ tuỳ tâm nhé!