Buổi 12 — Practice Round 1: FAANG Mastery
Navigation
← 11-Segment-Tree | → 13-Practice-FAANG-2 Giai đoạn: FAANG Mastery | Loại: Mock Interview
1. Mục tiêu
Buổi này simulate điều kiện Google/Meta on-site interview thực tế. Mục tiêu không phải là giải đúng 100%, mà là luyện tư duy có cấu trúc dưới áp lực thời gian.
Mindset trước khi bắt đầu
Interviewer không chỉ chấm “đúng hay sai” — họ đánh giá cách bạn suy nghĩ, khả năng giao tiếp technical, và xử lý edge cases. Một giải pháp O(N²) được giải thích rõ ràng còn tốt hơn O(N) viết im lặng không giải thích.
Điều kiện buổi luyện:
- Thời gian: 45 phút / problem set
- Không tra cứu solution trong quá trình làm
- Nói to suy nghĩ theo framework UMPIRE
- Sau khi hết giờ: phân tích lại approach, so sánh với optimal
2. Interview Simulation Rules
Framework UMPIRE — Phải nói to từng bước
| Bước | Viết tắt | Hành động |
|---|---|---|
| 1 | U — Understand | Đọc đề, hỏi clarifying questions, vẽ ví dụ cụ thể |
| 2 | M — Match | Nhận diện pattern: “Bài này giống dạng Sliding Window vì…“ |
| 3 | P — Plan | Pseudocode bằng lời, nêu Time/Space complexity dự kiến |
| 4 | I — Implement | Viết code sạch, đặt tên biến có nghĩa |
| 5 | R — Review | Chạy tay qua test case, kiểm tra edge cases |
| 6 | E — Evaluate | Phân tích complexity, đề xuất optimizations |
Quy tắc bắt buộc
- KHÔNG bắt đầu code trước bước Plan
- PHẢI nêu ít nhất 2 clarifying questions trước khi code
- PHẢI phân tích complexity sau khi code xong
- Nếu bí, NÊU RÕ “Tôi đang nghĩ hướng X, nhưng chưa chắc vì Y”
Quy tắc chấm điểm nhanh (tự chấm)
- Hoàn thành trong 45 phút + optimal: 5 điểm
- Hoàn thành trong 45 phút + suboptimal: 4 điểm
- Hoàn thành nhưng overtime: 3 điểm
- Partial solution + giải thích rõ approach: 2 điểm
- Stuck hoàn toàn: 1 điểm → cần review lại fundamentals
3. Problem Set A — Two Pointers & Sliding Window Focus
Problem A1: #3 Longest Substring Without Repeating Characters (Medium)
Đề bài: Cho string s, tìm độ dài của substring dài nhất không có ký tự lặp lại.
Ví dụ:
Input: s = "abcabcbb" → Output: 3 (substring "abc")
Input: s = "bbbbb" → Output: 1 (substring "b")
Input: s = "pwwkew" → Output: 3 (substring "wke")
Clarifying questions cần hỏi:
- Input có thể là empty string không? → Trả về 0
- Có uppercase/lowercase/số/ký tự đặc biệt không? → Chỉ có ASCII printable characters
- Cần trả về substring hay chỉ độ dài? → Chỉ độ dài
Approach 1: Brute Force — O(N²) / O(min(N, M))
// Approach 1: Brute Force — kiểm tra từng substring
// Time: O(N²) | Space: O(min(N, M)) với M là kích thước alphabet
function lengthOfLongestSubstring_Brute(s: string): number {
let maxLen = 0;
for (let i = 0; i < s.length; i++) {
const seen = new Set<string>();
let j = i;
// Mở rộng substring từ i cho đến khi gặp ký tự trùng
while (j < s.length && !seen.has(s[j])) {
seen.add(s[j]);
j++;
}
maxLen = Math.max(maxLen, j - i);
}
return maxLen;
}Approach 2: Sliding Window với HashMap — O(N) / O(min(N, M))
// Approach 2 (Optimal): Sliding Window + HashMap lưu index gần nhất
// Key insight: khi gặp ký tự trùng tại index j,
// thay vì dịch left từng bước → nhảy thẳng đến (lastSeen[s[j]] + 1)
// Time: O(N) — mỗi ký tự xử lý tối đa 2 lần (add + remove)
// Space: O(min(N, M)) — HashMap chứa tối đa min(N, kích thước alphabet) entries
function lengthOfLongestSubstring(s: string): number {
// Map từ character → index gần nhất của nó
const lastSeen = new Map<string, number>();
let maxLen = 0;
let left = 0; // con trỏ trái của sliding window
for (let right = 0; right < s.length; right++) {
const char = s[right];
// Nếu char đã xuất hiện VÀ index của nó nằm trong window hiện tại
// → phải dịch left qua để loại bỏ ký tự trùng
if (lastSeen.has(char) && lastSeen.get(char)! >= left) {
// Nhảy left đến ngay sau vị trí xuất hiện trước đó của char
left = lastSeen.get(char)! + 1;
}
// Cập nhật vị trí gần nhất của char
lastSeen.set(char, right);
// Cập nhật max length: window hiện tại có kích thước (right - left + 1)
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// Test cases
console.log(lengthOfLongestSubstring("abcabcbb")); // 3
console.log(lengthOfLongestSubstring("bbbbb")); // 1
console.log(lengthOfLongestSubstring("pwwkew")); // 3
console.log(lengthOfLongestSubstring("")); // 0
console.log(lengthOfLongestSubstring(" ")); // 1
console.log(lengthOfLongestSubstring("au")); // 2Pattern nhận dạng
Bất kỳ bài nào hỏi “tìm subarray/substring thoả điều kiện X” + muốn tối ưu thì nên nghĩ đến Sliding Window. Key: định nghĩa rõ khi nào window hợp lệ và khi nào cần thu nhỏ window.
Problem A2: #76 Minimum Window Substring (Hard)
Đề bài: Cho string s và string t, tìm substring ngắn nhất trong s chứa tất cả ký tự của t (kể cả duplicates).
Ví dụ:
Input: s = "ADOBECODEBANC", t = "ABC" → Output: "BANC"
Input: s = "a", t = "a" → Output: "a"
Input: s = "a", t = "aa" → Output: "" (không tìm được)
Clarifying questions:
- Nếu không tìm được → trả về
""(empty string) svàtcó phân biệt hoa thường không? → Cótcó ký tự trùng không? → Có, phải bao gồm đủ số lần xuất hiện
// Minimum Window Substring — Sliding Window với frequency counting
// Time: O(|s| + |t|) — mỗi ký tự trong s được duyệt tối đa 2 lần
// Space: O(|s| + |t|) — lưu frequency maps
function minWindow(s: string, t: string): string {
if (s.length === 0 || t.length === 0) return "";
// Bước 1: Đếm frequency của từng ký tự trong t
const need = new Map<string, number>();
for (const char of t) {
need.set(char, (need.get(char) ?? 0) + 1);
}
// Số lượng ký tự PHÂN BIỆT cần thoả mãn
const required = need.size;
// Sliding window pointers
let left = 0;
let right = 0;
// `formed`: số ký tự phân biệt đã thoả mãn đủ frequency trong window
let formed = 0;
// Frequency của ký tự trong window hiện tại
const windowCounts = new Map<string, number>();
// Kết quả: [window length, left index, right index]
let result: [number, number, number] = [-1, 0, 0];
while (right < s.length) {
// Thêm ký tự s[right] vào window
const char = s[right];
windowCounts.set(char, (windowCounts.get(char) ?? 0) + 1);
// Kiểm tra xem ký tự này đã thoả mãn yêu cầu của t chưa
// (frequency trong window >= frequency cần thiết)
if (
need.has(char) &&
windowCounts.get(char) === need.get(char)
) {
formed++;
}
// Co window lại từ trái khi đã có đủ tất cả ký tự cần thiết
while (left <= right && formed === required) {
// Cập nhật kết quả nếu window hiện tại nhỏ hơn
const windowSize = right - left + 1;
if (result[0] === -1 || windowSize < result[0]) {
result = [windowSize, left, right];
}
// Loại bỏ ký tự s[left] khỏi window
const leftChar = s[left];
windowCounts.set(leftChar, windowCounts.get(leftChar)! - 1);
// Nếu sau khi loại bỏ, ký tự này không còn đủ frequency
if (
need.has(leftChar) &&
windowCounts.get(leftChar)! < need.get(leftChar)!
) {
formed--;
}
left++; // Thu hẹp window từ trái
}
right++; // Mở rộng window từ phải
}
// Trả về substring, hoặc "" nếu không tìm được
return result[0] === -1
? ""
: s.substring(result[1], result[1] + result[0]);
}
// Test cases
console.log(minWindow("ADOBECODEBANC", "ABC")); // "BANC"
console.log(minWindow("a", "a")); // "a"
console.log(minWindow("a", "aa")); // ""
console.log(minWindow("aa", "aa")); // "aa"Template Sliding Window tổng quát
left = 0, right = 0 while right < n: add s[right] to window while window_is_valid: ← hoặc window_is_invalid tùy bài update answer remove s[left] from window left++ right++Bài tìm minimum: thu nhỏ khi hợp lệ Bài tìm maximum: thu nhỏ khi không hợp lệ
4. Problem Set B — Graph & DP Focus
Problem B1: #207 Course Schedule (Medium)
Đề bài: Có numCourses khoá học (đánh số 0 đến numCourses-1). prerequisites[i] = [a, b] nghĩa là phải học b trước a. Hỏi có thể hoàn thành tất cả khoá học không?
Ví dụ:
Input: numCourses = 2, prerequisites = [[1,0]] → Output: true
Input: numCourses = 2, prerequisites = [[1,0],[0,1]] → Output: false (cycle!)
Key insight: Bài này tương đương hỏi “Directed graph có cycle không?”
Approach 1: DFS Cycle Detection
// Approach 1: DFS với 3 trạng thái (WHITE/GRAY/BLACK)
// WHITE (0) = chưa thăm, GRAY (1) = đang trong DFS stack, BLACK (2) = đã xong
// Time: O(V + E) | Space: O(V + E)
function canFinish_DFS(numCourses: number, prerequisites: number[][]): boolean {
// Xây dựng adjacency list
const graph: number[][] = Array.from({ length: numCourses }, () => []);
for (const [course, prereq] of prerequisites) {
graph[prereq].push(course); // prereq → course
}
// 0 = unvisited (WHITE), 1 = visiting (GRAY), 2 = visited (BLACK)
const state = new Array(numCourses).fill(0);
// DFS trả về true nếu phát hiện cycle
function hasCycle(node: number): boolean {
if (state[node] === 1) return true; // GRAY → back edge → cycle!
if (state[node] === 2) return false; // BLACK → đã xử lý, không có cycle
state[node] = 1; // Đánh dấu đang thăm (GRAY)
for (const neighbor of graph[node]) {
if (hasCycle(neighbor)) return true;
}
state[node] = 2; // Đánh dấu hoàn thành (BLACK)
return false;
}
// Chạy DFS từ mỗi node chưa thăm
for (let i = 0; i < numCourses; i++) {
if (state[i] === 0 && hasCycle(i)) return false;
}
return true; // Không có cycle → có thể hoàn thành
}Approach 2: Kahn’s Algorithm (BFS Topological Sort)
// Approach 2: Kahn's Algorithm — BFS topo sort
// Ý tưởng: node có in-degree = 0 là node có thể học đầu tiên
// Nếu sau khi xử lý hết, vẫn còn node chưa xử lý → có cycle
// Time: O(V + E) | Space: O(V + E)
function canFinish_Kahn(numCourses: number, prerequisites: number[][]): boolean {
// Xây dựng adjacency list và in-degree array
const graph: number[][] = Array.from({ length: numCourses }, () => []);
const inDegree = new Array(numCourses).fill(0);
for (const [course, prereq] of prerequisites) {
graph[prereq].push(course);
inDegree[course]++; // course cần 1 prerequisite
}
// Queue chứa các node có in-degree = 0 (có thể học ngay)
const queue: number[] = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
let processedCount = 0;
let head = 0; // Index pointer — O(1) dequeue (xem 03-Stack-and-Queue § Pitfall 2)
while (head < queue.length) {
const course = queue[head++];
processedCount++;
// Giảm in-degree của tất cả khoá học phụ thuộc vào course này
for (const nextCourse of graph[course]) {
inDegree[nextCourse]--;
// Nếu in-degree về 0, có thể học khoá này rồi
if (inDegree[nextCourse] === 0) {
queue.push(nextCourse);
}
}
}
// Nếu xử lý đủ tất cả courses → không có cycle
return processedCount === numCourses;
}
// Test
console.log(canFinish_DFS(2, [[1,0]])); // true
console.log(canFinish_DFS(2, [[1,0],[0,1]])); // false
console.log(canFinish_Kahn(2, [[1,0]])); // true
console.log(canFinish_Kahn(2, [[1,0],[0,1]])); // falseSo sánh 2 approaches
DFS Kahn’s BFS Phát hiện cycle Qua GRAY node processedCount < numCourses Topo order output Có (DFS finish order) Có (queue order) Dễ implement Trung bình Dễ hơn Interview preference Thường hỏi cả 2 Intuitive hơn
Problem B2: #416 Partition Equal Subset Sum (Medium)
Đề bài: Cho array số nguyên dương nums. Hỏi có thể chia array thành 2 subset có tổng bằng nhau không?
Ví dụ:
Input: nums = [1,5,11,5] → Output: true ([1,5,5] và [11])
Input: nums = [1,2,3,5] → Output: false (không chia được)
Key insight: Tổng array = S → tìm subset có tổng = S/2. Đây là bài 0/1 Knapsack.
// Partition Equal Subset Sum — 0/1 Knapsack DP
// Time: O(N × target) | Space: O(target) với space optimization
// target = totalSum / 2
function canPartition(nums: number[]): boolean {
const totalSum = nums.reduce((a, b) => a + b, 0);
// Nếu tổng lẻ → không thể chia đôi bằng nhau
if (totalSum % 2 !== 0) return false;
const target = totalSum / 2;
// dp[j] = true nếu có thể tạo sum = j từ các phần tử đã xét
// Khởi tạo: dp[0] = true (sum = 0 luôn đạt được với tập rỗng)
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
// Duyệt ngược để tránh dùng cùng phần tử 2 lần (0/1 knapsack)
// Nếu duyệt xuôi → unbounded knapsack (dùng nhiều lần được)
for (let j = target; j >= num; j--) {
// dp[j] = có thể đạt sum j KHÔNG dùng num
// OR có thể đạt sum (j - num) TRƯỚC ĐÓ và thêm num vào
dp[j] = dp[j] || dp[j - num];
}
// Optimization: early exit nếu đã tìm được
if (dp[target]) return true;
}
return dp[target];
}
// Trace through example: nums = [1, 5, 11, 5], target = 11
// Initial: dp = [T, F, F, F, F, F, F, F, F, F, F, F]
// After 1: dp = [T, T, F, F, F, F, F, F, F, F, F, F]
// After 5: dp = [T, T, F, F, F, T, T, F, F, F, F, F]
// After 11: dp = [T, T, F, F, F, T, T, F, F, F, F, T] ← dp[11] = true!
console.log(canPartition([1, 5, 11, 5])); // true
console.log(canPartition([1, 2, 3, 5])); // false
console.log(canPartition([1, 1])); // true
console.log(canPartition([1, 2, 5])); // falseMẹo nhớ 0/1 vs Unbounded Knapsack
- Duyệt ngược (j từ target → num): mỗi item dùng tối đa 1 lần (0/1 Knapsack)
- Duyệt xuôi (j từ num → target): item có thể dùng nhiều lần (Unbounded Knapsack)
5. Problem Set C — Hard Challenge
Problem C1: #124 Binary Tree Maximum Path Sum (Hard)
Đề bài: Cho binary tree, tìm path (đường đi qua bất kỳ node nào, không cần qua root) có tổng lớn nhất. Path phải là một đường liên tục.
Ví dụ:
-10
/ \
9 20
/ \
15 7
Output: 42 (path: 15 → 20 → 7)
1
/ \
2 3
Output: 6 (path: 2 → 1 → 3)
// Binary Tree Maximum Path Sum — Recursive với Global Max
// Key insight: với mỗi node, path đi QUA node đó có thể dùng:
// - Chỉ node đó
// - Node + left subtree path
// - Node + right subtree path
// - Node + cả left + right (nhưng chỉ tính được khi node là "đỉnh" của path)
// Hàm helper trả về "max gain" khi đi QUA node theo 1 hướng (để parent dùng)
// Time: O(N) — duyệt mỗi node đúng 1 lần
// Space: O(H) — call stack depth = chiều cao cây
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function maxPathSum(root: TreeNode | null): number {
// globalMax lưu kết quả tốt nhất tìm được trong toàn bộ quá trình DFS
let globalMax = -Infinity;
// Hàm này trả về: max gain khi dùng node này và ĐI MỘT HƯỚNG (để parent sử dụng)
// Không thể đi cả 2 hướng vì parent cần nối thêm → sẽ tạo nhánh rẽ
function maxGain(node: TreeNode | null): number {
if (node === null) return 0;
// Tính max gain từ subtree trái và phải
// Dùng Math.max(0, ...) để loại bỏ nhánh âm (không dùng còn hơn dùng âm)
const leftGain = Math.max(0, maxGain(node.left));
const rightGain = Math.max(0, maxGain(node.right));
// Tổng path đi QUA node này (có thể dùng cả 2 nhánh — node là "đỉnh" của path)
const pathThroughNode = node.val + leftGain + rightGain;
// Cập nhật global max
globalMax = Math.max(globalMax, pathThroughNode);
// Trả về max gain KHI ĐI 1 HƯỚNG (để parent node có thể nối tiếp)
return node.val + Math.max(leftGain, rightGain);
}
maxGain(root);
return globalMax;
}
// Test
// -10
// / \
// 9 20
// / \
// 15 7
const root1 = new TreeNode(-10,
new TreeNode(9),
new TreeNode(20, new TreeNode(15), new TreeNode(7))
);
console.log(maxPathSum(root1)); // 42
// 1
// / \
// 2 3
const root2 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
console.log(maxPathSum(root2)); // 6
// Edge case: single negative node
console.log(maxPathSum(new TreeNode(-3))); // -3Edge case quan trọng
- Node có giá trị âm: dùng
Math.max(0, gain)để bỏ qua nhánh âm- Tất cả nodes đều âm: phải chọn ít nhất 1 node →
globalMax = -Infinityban đầu- Single node tree: trả về giá trị của node đó (dù âm)
6. Pattern Drill Table
Luyện nhận diện pattern trước khi code. Nhìn vào mô tả → đoán pattern ngay trong 10 giây.
| # | Mô tả bài toán | Pattern |
|---|---|---|
| 1 | Tìm subarray dài nhất có sum ≤ K | Sliding Window |
| 2 | Đếm số distinct elements trong mỗi window kích thước K | Sliding Window + HashMap |
| 3 | Số operations tối thiểu để kết nối tất cả nodes | Union Find / MST (Kruskal/Prim) |
| 4 | Tìm K phần tử lớn nhất trong stream dữ liệu | Heap (Min-Heap kích thước K) |
| 5 | Đường đi ngắn nhất giữa 2 nodes trong unweighted graph | BFS |
| 6 | Có tồn tại path thoả điều kiện trong cây không | DFS / Recursion |
| 7 | Số cách để đạt tổng = X, dùng mỗi phần tử tối đa 1 lần | 0/1 Knapsack DP |
| 8 | Tìm tất cả subsets/permutations/combinations | Backtracking |
| 9 | Two strings: edit distance / longest common subsequence | 2D DP |
| 10 | Mảng đã sort, tìm cặp có tổng = target | Two Pointers |
| 11 | Interval scheduling: chọn nhiều nhất không overlap | Greedy (sort by end time) |
| 12 | Chuỗi có dấu ngoặc hợp lệ không? / tính toán expression | Stack |
| 13 | Tìm ancestor chung gần nhất của 2 nodes trong BST | BST property / DFS |
| 14 | Số islands / connected components trong grid | BFS/DFS hoặc Union Find |
| 15 | Minimum cost để đi từ góc trên-trái → góc dưới-phải | Dynamic Programming (Grid DP) |
Pattern Recognition Speed Drill
Che cột Pattern, nhìn từng mô tả và nói to pattern trong 5 giây. Lặp lại cho đến khi tất cả 15 bài nhận ra trong < 30 giây tổng cộng.
7. Interview Self-Assessment Rubric
Tự chấm sau mỗi buổi mock interview. Mục tiêu: tổng điểm ≥ 20/25 trước khi apply.
| Dimension | 1 — Struggling | 3 — On Track | 5 — FAANG Ready |
|---|---|---|---|
| Problem Comprehension | Hiểu sai đề, bỏ qua constraints quan trọng | Hiểu đúng sau khi hỏi clarifying questions | Ngay lập tức nắm constraints, tự vẽ ví dụ edge cases |
| Pattern Recognition | Không biết approach sau 10 phút | Nhận ra pattern sau 3-5 phút với gợi ý | Nhận ra pattern trong 1-2 phút, biết nhiều approaches |
| Code Quality | Code sai syntax, biến đặt tên a/b/c, nhiều bug | Code chạy được, tên biến tạm ổn, vài minor bugs | Clean code, tên biến tự mô tả, không có bug, có comments |
| Complexity Analysis | Không biết Big-O của solution mình viết | Phân tích được Time/Space sau khi được nhắc | Chủ động nêu Time/Space cho từng approach, giải thích tại sao |
| Communication | Im lặng code, không giải thích, hỏi mới trả lời | Giải thích được khi được hỏi, dùng UMPIRE đôi lúc | Liên tục nói to suy nghĩ, proactively giải thích trade-offs |
Cách tính điểm tổng:
- 20-25: FAANG Ready — nộp CV ngay
- 15-19: On Track — luyện thêm 2-3 tuần
- 10-14: Getting There — cần review lại weak areas
- < 10: Need More Practice — quay lại buổi học cơ bản
Track progress theo thời gian
Ghi điểm sau mỗi mock session:
[Date]: P=X, PR=X, CQ=X, CA=X, C=X → Total=XX
8. Common FAANG Interview Mistakes
Mistake #1: Bắt đầu code ngay khi đọc xong đề
Vấn đề: Mất 20 phút code sai hướng vì hiểu sai constraint. Fix: Luôn dành 3-5 phút đầu để hỏi clarifying questions và vẽ ví dụ cụ thể. Hỏi: “Có thể có negative numbers không?”, “Input có thể empty không?”, “Cần optimize cho Time hay Space?”
Mistake #2: Không confirm approach trước khi code
Vấn đề: Code xong 30 phút, interviewer nói “Có approach tốt hơn không?” — không còn thời gian sửa. Fix: Sau khi plan xong, nói to approach và complexity rồi hỏi “Approach này có ổn không, hay anh/chị muốn tôi tối ưu hơn?” trước khi gõ code.
Mistake #3: Bỏ qua edge cases
Vấn đề: Code pass basic test nhưng fail trên
null, empty array, single element, negative numbers, overflow. Fix: Sau khi code xong, tự hỏi 5 câu: (1) Input rỗng? (2) Một phần tử? (3) Tất cả phần tử giống nhau? (4) Giá trị âm? (5) Overflow/underflow?
Mistake #4: Không chủ động phân tích complexity
Vấn đề: Interviewer phải hỏi “Time complexity của bạn là gì?” — đây là dấu hiệu bạn không proactive. Fix: Sau khi code xong (không cần đợi hỏi), tự nêu: “Solution này có Time complexity O(N log N) vì… và Space O(N) vì… Có thể optimize xuống O(N) nếu…”
Mistake #5: Tên biến bí ẩn dưới áp lực
Vấn đề: Khi nervous, code ra
a, b, c, x, y, tmp— interviewer không follow được logic. Fix: Quy tắc bất di bất dịch:left/rightcho two pointers,windowStart/windowEndcho sliding window,visited/parent/distancecho graph,dp[i]với comment giải thích ý nghĩa. Tên biến tự mô tả = code sạch = điểm cao.
9. Buổi tiếp theo
Navigation
Tiếp theo: 13-Practice-FAANG-2 — Final Mock: FAANG Mastery Complete Buổi cuối cùng: 3 round interview simulation với Hard problems, FAANG readiness checklist đầy đủ, và định hướng sau vault.
Chuẩn bị cho buổi 13:
- Ôn lại Heap (cho #295 Find Median from Data Stream)
- Ôn lại Linked List merge (cho #23 Merge K Sorted Lists)
- Ôn lại BFS/DFS trên Binary Tree (cho #297 Serialize/Deserialize)
- Self-assess điểm rubric của buổi này trước khi sang buổi 13