Bài 8 · Nâng cao · 30 phút
Dự án cuối khoá: 3 bài toán DSA
Biên soạn bởi Nguyễn Anh Tuấn
Ghép mọi cấu trúc và thuật toán vào ba dự án nhỏ chạy được: gợi ý đường đi (đồ thị + BFS), kiểm tra ngoặc cân (stack), bảng xếp hạng (sort + bảng băm).
Mèo con đã đi hết khoá: Big-O để cân đo chi phí; mảng, danh sách, ngăn xếp, hàng đợi để lưu; sắp xếp & tìm kiếm; cây nhị phân tìm kiếm; bảng băm; và đồ thị. Bài cuối này không dạy gì mới - ta ghép tất cả vào ba dự án nhỏ chạy được, mỗi dự án là một bài toán đời thực giải bằng đúng công cụ hợp nhất.
- ▸Dự án 1 - Gợi ý đường đi: đồ thị + BFS tìm đường ít trạm nhất.
- ▸Dự án 2 - Kiểm tra cú pháp: ngăn xếp soát ngoặc cân.
- ▸Dự án 3 - Bảng xếp hạng: bảng băm đếm + sắp xếp.
Bài toán: cho một mạng lưới (ga tàu, bạn bè, các điểm nối nhau), tìm đường đi qua ít chặng nhất giữa hai điểm. Đây là BFS, nhưng có thêm một mẹo: nhớ “cha” của mỗi đỉnh khi lần đầu chạm tới, để cuối cùng lần ngược lại thành đường đi.
duong-di.ts · BFS có ghi cha để dựng lại đường ngắn nhất
const dothi: Record<string, string[]> = {
A: ['B', 'C'], B: ['A', 'D', 'E'], C: ['A', 'F'],
D: ['B'], E: ['B', 'F'], F: ['C', 'E', 'G'], G: ['F'],
};
function duongNganNhat(g: Record<string, string[]>, start: string, goal: string) {
if (start === goal) return [start];
const cha = new Map<string, string>(); // bang bam: con -> cha
const seen = new Set([start]);
const queue = [start]; // hang doi cho BFS
while (queue.length) {
const cur = queue.shift()!;
for (const nb of g[cur]) {
if (seen.has(nb)) continue;
seen.add(nb); cha.set(nb, cur);
if (nb === goal) { // tim thay: lan nguoc theo 'cha'
const path = [goal]; let p = goal;
while (cha.has(p)) { p = cha.get(p)!; path.unshift(p); }
return path;
}
queue.push(nb);
}
}
return null; // khong toi duoc
}
console.log(duongNganNhat(dothi, 'A', 'G').join(' → ')); Kết quả khi chạy
A → C → F → G
Bấm BFS rồi Tự chạy để nhớ lại vì sao BFS luôn chạm đích qua đường ngắn nhất:
Bắt đầu từ A, đưa vào hàng đợi
Hàng đợi: [A] ← lấy ở đầu
Đã thăm: (chưa có)
Cùng đồ thị, cùng điểm xuất phát A: BFS dùng hàng đợi nên lan ra theo từng lớp (gần A trước); DFS dùng ngăn xếp nên đâm sâu một nhánh tới cùng rồi mới quay lui. Khác nhau chỉ ở chỗ lấy phần tử từ ĐẦU hay từ CUỐI.
Công cụ đã dùng
Bài toán: một đoạn code có cân bằng ngoặc không (mọi ( [ { đều đóng đúng thứ tự)? Đây chính là tính LIFO của ngăn xếp: gặp ngoặc mở thì đẩy vào, gặp ngoặc đóng thì lấy ra so khớp.
kiem-ngoac.ts · ngăn xếp soát ngoặc trong code
function ngoacCan(s: string): boolean {
const stack: string[] = [];
const cap: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
for (const ch of s) {
if (ch === '(' || ch === '[' || ch === '{') stack.push(ch);
else if (ch in cap) {
if (stack.pop() !== cap[ch]) return false; // dong khong khop mo gan nhat
}
}
return stack.length === 0; // con ngoac mo chua dong -> chua can
}
for (const code of ['f(a[0], b[1])', 'arr[(i+1])', 'if (x) { y(); }'])
console.log(`${ngoacCan(code)}\t${code}`); Kết quả khi chạy
true f(a[0], b[1])
false arr[(i+1])
true if (x) { y(); }Mô phỏng ngăn xếp đẩy/lấy - đúng thao tác mà bộ kiểm ngoặc dùng:
Ngăn xếp - vào/ra cùng một đầu (LIFO)
Hàng đợi - vào một đầu, ra đầu kia (FIFO)
(ra)
(vào)
Bấm Thêm để đẩy cùng một số vào cả hai, rồi Lấy ra để thấy: ngăn xếp trả về số mới nhất, hàng đợi trả về số cũ nhất. Mọi thao tác thêm/lấy đều là O(1).
Công cụ đã dùng
Bài toán: cho danh sách điểm rải rác của nhiều người, dựng bảng xếp hạng từ cao xuống thấp. Hai bước, hai công cụ: bảng băm để cộng dồn điểm theo tên (tra cứu O(1)), rồi sắp xếp theo điểm giảm dần.
xep-hang.ts · gom điểm bằng Map rồi sắp xếp
function bangXepHang(diem: [string, number][]): [string, number][] {
const tong = new Map<string, number>();
for (const [ten, d] of diem)
tong.set(ten, (tong.get(ten) ?? 0) + d); // bang bam: cong don theo ten
return [...tong].sort((a, b) => b[1] - a[1]); // sap giam dan theo diem
}
const diem: [string, number][] = [
['Miu', 3], ['Bun', 5], ['Miu', 4], ['Ngao', 2], ['Bun', 1],
];
bangXepHang(diem).forEach(([ten, d], i) => console.log(`${i + 1}. ${ten}: ${d}`)); Kết quả khi chạy
1. Miu: 7 2. Bun: 6 3. Ngao: 2
Nhớ lại bảng băm gom theo khoá thế nào:
hash("fox") = 333 → 333 mod 8 = xô 5
Thêm cat rồi act (đảo chữ) để thấy va chạm: cùng giá trị băm nên rơi chung một xô, nối thành chuỗi. Xô càng dài thì tra cứu càng chậm - đó là vì sao cần hàm băm rải đều.
Công cụ đã dùng
Ba dự án trên không có gì “mới” - chúng chỉ là bộ đồ nghề bạn đã rèn suốt khoá, lắp đúng chỗ. Đó chính là điều DSA dạy: không phải học vẹt thuật toán, mà là nhìn một bài toán rồi chọn đúng cấu trúc và thuật toán, cân nhắc bằng Big-O.
- ▸Luyện tập đều đặn: mỗi ngày 1-2 bài để biến công cụ thành phản xạ.
- ▸Đào sâu: cây tự cân bằng (AVL, Red-Black), Dijkstra/A* (đường có trọng số), quy hoạch động, tham lam.
- ▸Mỗi bài toán mới: hỏi “dữ liệu trông thế nào, thao tác nào nhiều nhất” rồi mới chọn cấu trúc.
Chặng kế trong lộ trình
Câu hỏi thường gặp
Đường đi ngắn nhất (BFS) là lõi của chỉ đường, mạng xã hội gợi ý “bạn của bạn”, giải Rubik. Kiểm ngoặc cân là cách trình biên dịch và trình soạn thảo bắt lỗi cú pháp. Bảng xếp hạng (đếm bằng bảng băm rồi sắp) có ở mọi nơi: top bài hát, thống kê bình chọn, đếm từ trong văn bản.
Vì ta cần đường ÍT CẠNH NHẤT. BFS lan theo từng lớp nên lần đầu chạm tới đích là qua đường ngắn nhất (theo số cạnh). DFS có thể tới đích bằng một đường vòng dài, không đảm bảo ngắn nhất.
Cần truy cập theo vị trí: mảng. Thêm/bớt ở đầu nhiều: danh sách liên kết. Vào-sau-ra-trước: ngăn xếp; vào-trước-ra-trước: hàng đợi. Tra cứu “có/không” thật nhanh: bảng băm. Giữ thứ tự mà vẫn tra nhanh: cây nhị phân tìm kiếm. Quan hệ mạng lưới: đồ thị.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Dự án “gợi ý đường đi ít chặng nhất” nên dùng thuật toán nào?
- 1
Mở rộng: độ dài đường đi
Sửa dự án 1 để in thêm ĐỘ DÀI (số cạnh) của đường đi ngắn nhất, không chỉ các đỉnh.
Hoàn thành khi: Độ dài = số đỉnh trên đường trừ 1. Với đường
A → C → F → Gthì độ dài là 3. - 2
Mở rộng: chỉ chỗ lỗi
Sửa dự án 2 để khi ngoặc KHÔNG cân, trả về vị trí (chỉ số) ký tự gây lỗi đầu tiên.
Hoàn thành khi: Khi gặp ngoặc đóng không khớp (hoặc dư), trả về chỉ số của nó; nếu còn ngoặc mở thừa, trả về vị trí ngoặc mở chưa đóng.
- 3
Mở rộng: phá hoà
Sửa dự án 3 để khi hai người BẰNG điểm thì xếp theo tên (a → z).
Hoàn thành khi: Hàm sắp xếp so điểm giảm dần trước; nếu điểm bằng nhau thì so tên tăng dần (
localeCompare). - 4
Tự chọn công cụ
Cho bài: “đếm xem mỗi từ xuất hiện bao nhiêu lần trong một đoạn văn, rồi in 5 từ phổ biến nhất”. Chọn cấu trúc + thuật toán và nêu Big-O.
Hoàn thành khi: Bảng băm để đếm (
O(n)), rồi sắp xếp theo số đếm (O(k log k)với k từ khác nhau), lấy 5 đầu. Nêu rõ vai trò mỗi cấu trúc. - 5
Ghép ba thành một
Phác ý tưởng (không cần code đầy đủ) một mini-app dùng CẢ ba kỹ thuật: đồ thị, ngăn xếp, bảng băm + sắp xếp.
Hoàn thành khi: Một ý tưởng mạch lạc, chỉ rõ chỗ nào dùng cấu trúc nào và vì sao (ví dụ: trình duyệt bản đồ có lịch sử Undo và bảng xếp hạng địa điểm hay tới).
- 6
Nhìn lại cả khoá
Bằng lời mèo con, viết 3 - 4 câu tóm tắt điều giá trị nhất bạn học được từ khoá DSA này.
Hoàn thành khi: Nêu được ý chính: biết đo chi phí bằng Big-O và chọn đúng cấu trúc/thuật toán cho từng bài toán, thay vì chỉ “code cho chạy”.