[10, 9, 1, 100].sort(); // ?
[10, 9, 1, 100].sort((a, b) => a - b); // ?
Data Structures & Algorithms for Seniors
draftedBig-O thực dụng và các bài toán backend hay gặp: LRU, rate limiter, top-K, debounce/throttle, promise pool, trie, topo sort, interval — viết bằng TypeScript.
0/44 rated · mastery 0%
Bài học
10 bài · ~177 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~17 phút· 6 câuverify
Big-O thực dụng: hằng số, amortized và I/O
Big-O đo gì và không đo gì, amortized O(1) của dynamic array, O(n) ẩn trong Array.shift/includes, N+1 query, và cách một senior ưu tiên tối ưu theo bottleneck thật.
- 02~16 phút· 5 câuverify
Hash map trong JavaScript: Map, Object, Set và tấn công worst case
Hash table hoạt động ra sao, khi nào dùng Map/Object/Set, thứ tự key, prototype pollution, HashDoS/ReDoS và dedupe chính xác bằng hash set + unique constraint.
- 03~19 phút· 5 câuverify
Sort, binary search, interval, chia để trị & quy hoạch động
Sort trong JS (comparator, stability, TimSort), binary search trên dữ liệu và trên đáp án, merge/conflict interval, divide & conquer, DP memoization vs tabulation và pricing engine deterministic.
- 04~18 phút· 5 câuverify
Heap, top-K, K-way merge và scheduler
Binary heap trong array, top-K O(n log K) trên stream, heavy hitters xấp xỉ, merge kết quả nhiều shard và bẫy deep pagination, reservoir sampling, và scheduler cho 10 triệu delayed job.
- 05~18 phút· 6 câuverify
Graph, cây và trie: BFS/DFS, topo sort, RBAC và autocomplete
Mô hình hoá quan hệ thành graph, BFS cho đường ngắn nhất, DFS và phát hiện cycle, topological sort (Kahn), đệ quy sâu và stack overflow, transitive closure cho RBAC, trie và các lựa chọn autocomplete.
- 06~17 phút· 4 câuverify
Sliding window và rate limiter: từ O(n·k) tới Redis Lua
Kỹ thuật sliding window (cộng vào, trừ ra), monotonic deque, bốn thuật toán rate limit (fixed window, sliding log, sliding counter, token bucket) và cài đặt phân tán atomic bằng Redis sorted set + Lua.
- 07~15 phút· 4 câu
Debounce, throttle và promise pool: kiểm soát nhịp độ bất đồng bộ
Debounce vs throttle (leading/trailing, maxWait, cancel/flush), lỗi hay gặp trong React và race giữa response cũ/mới, và promise pool giới hạn N tác vụ song song giữ thứ tự, xử lý lỗi, abort và streaming.
- 08~19 phút· 4 câuverify
LRU, LFU, W-TinyLFU và cache hai tầng
Cài LRU O(1) bằng Map, TTL và giới hạn theo bytes, LFU O(1) và vấn đề aging, W-TinyLFU, eviction xấp xỉ của Redis, cold start/stampede/scan pollution, và thiết kế L1 in-process + L2 Redis cho 30 pod.
- 09~17 phút· 4 câuverify
B+tree, skip list và pagination: cấu trúc có thứ tự trong DB và Redis
Vì sao database dùng B+tree thay vì hash hay BST, page split và UUIDv4 vs UUIDv7, skip list trong Redis sorted set, và chi phí thật của offset so với keyset pagination (đo bằng EXPLAIN trên PostgreSQL).
- 10~20 phút· 4 câuverify
Bloom filter, HyperLogLog và consistent hashing
Cấu trúc xác suất đổi độ chính xác lấy memory: Bloom filter (membership, cache penetration, sizing), HyperLogLog (đếm distinct, merge theo ngày/tuần), và consistent hashing/hash slot để chia key cho nhiều node; áp dụng vào dedupe webhook quy mô lớn.
Tóm tắt & cheat sheet của track
TL;DR
- Senior interview hiếm khi hỏi cây đỏ-đen; họ hỏi cài đặt một primitive production (LRU cache, rate limiter, promise pool, debounce) và lý do chọn cấu trúc dữ liệu cho một workload thật.
- Big-O là để so sánh khi n lớn; ngoài đời còn hằng số, cache locality, allocation/GC, và I/O (một query N+1 đắt hơn mọi vòng lặp O(n²) trên 100 phần tử).
- Bộ công cụ tối thiểu: hash map, array, heap, deque, trie, graph (BFS/DFS/topo sort), sliding window, two pointers, binary search, interval merge, cộng các cấu trúc xác suất (Bloom filter, HyperLogLog).
- Biết cấu trúc nào đang nằm bên trong công cụ bạn dùng: B-tree (index DB), skip list (Redis sorted set), hash ring (sharding), inverted index (Elasticsearch).
Khái niệm cốt lõi
| Cấu trúc / kỹ thuật | Một câu | Ví dụ backend |
|---|---|---|
Hash map (Map) | Tra cứu trung bình O(1), Map trong JS giữ thứ tự chèn | Dedupe, index theo id, LRU |
| Dynamic array | Truy cập O(1), push amortized O(1), chèn/xoá đầu O(n) | Buffer, batch |
| Deque / ring buffer | Thêm/bớt hai đầu O(1) | Queue job, sliding window log |
| Binary heap | Lấy min/max O(1), push/pop O(log n) | Top-K, scheduler, merge k sorted |
| Trie | Cây theo ký tự, tra prefix O(độ dài key) | Autocomplete, routing theo path |
| Graph + BFS/DFS | Duyệt quan hệ; BFS cho đường ngắn nhất không trọng số | Dependency, permission inheritance |
| Topological sort | Thứ tự tuyến tính của DAG, phát hiện cycle | Build order, migration order |
| Sliding window | Duy trì cửa sổ trên stream thay vì tính lại | Rate limit, moving average |
| Binary search | O(log n) trên dữ liệu có thứ tự (hoặc hàm đơn điệu) | Tìm version lỗi, cursor pagination |
| B-tree / B+tree | Cây nhiều nhánh tối ưu cho disk page | Index PostgreSQL/SQL Server |
| Bloom filter | "Chắc chắn không có" hoặc "có thể có", tốn ít bộ nhớ | Chặn lookup key không tồn tại |
| HyperLogLog | Đếm distinct xấp xỉ với vài KB | Unique visitors (PFCOUNT) |
Cơ chế hoạt động
Chọn cấu trúc dữ liệu cho một yêu cầu
LRU cache = hash map + thứ tự truy cập
Trong JS, Map giữ thứ tự chèn nên delete + set đưa key về cuối, và map.keys().next().value là key cũ nhất → mọi thao tác O(1) mà không cần tự viết doubly linked list.
Token bucket rate limiter
Trade-offs
| Bài toán | Cách A | Cách B | Chọn khi |
|---|---|---|---|
| Top-K trên n phần tử | Sort toàn bộ O(n log n) | Min-heap kích thước K: O(n log K) | K ≪ n hoặc dữ liệu là stream |
| Rate limit | Fixed window counter: rẻ, burst ở ranh giới | Sliding window log: chính xác, tốn memory | Token bucket khi cần cho phép burst có kiểm soát |
| Membership | Set: chính xác, O(n) memory | Bloom filter: false positive, rất ít memory | Hàng trăm triệu key, chấp nhận sai một chiều |
| Distinct count | Set: chính xác | HyperLogLog: sai ~1%, vài KB | Analytics quy mô lớn |
| Queue in-memory | Array.shift(): có thể O(n) | Ring buffer / head index: O(1) | Queue lớn, throughput cao |
| Autocomplete | LIKE 'abc%' với index | Trie in-memory / search engine (edge n-gram) | Tập từ nhỏ vs lớn/đa ngôn ngữ |
| Dedupe stream | Lưu hết id | TTL window / Bloom | Tuỳ yêu cầu chính xác |
Pitfalls
[10, 9, 1].sort()→[1, 10, 9]— sort mặc định so sánh chuỗi; luôn truyền comparator.arr.includes/indexOftrong vòng lặp → O(n²) ẩn; đổi sangSet.Array.shift()trên queue lớn,arr.splice(0, 1)trong vòng lặp.- Đệ quy sâu (cây 100k node, DFS) →
RangeError: Maximum call stack size exceeded; dùng stack tường minh. - Object làm hash map với key từ user (
__proto__,constructor) → dùngMaphoặcObject.create(null). - Tối ưu Big-O mà quên I/O: N+1 query, gọi Redis trong vòng lặp thay vì
MGET/pipeline. - Rate limiter/LRU in-memory trên nhiều pod → mỗi pod một trạng thái riêng.
Cheat sheet
- Hash map: get/set O(1) trung bình, O(n) worst · Heap: push/pop O(log n), peek O(1), heapify O(n) · Binary search: O(log n).
- Sort so sánh: tối thiểu O(n log n);
Array.prototype.sortstable từ ES2019. - Top-K lớn nhất → min-heap size K; K nhỏ nhất → max-heap.
- Interval merge: sort theo
start, gộp nếunext.start <= cur.end→ O(n log n). - Topo sort (Kahn): đếm in-degree, queue các node in-degree 0; nếu output < số node → có cycle.
- Debounce: chạy sau khi ngừng gọi X ms · Throttle: chạy tối đa một lần mỗi X ms.
- Promise pool: N worker cùng kéo từ một iterator chung; giữ kết quả theo index.
- Amortized O(1): push vào dynamic array (thỉnh thoảng resize O(n) nhưng trung bình vẫn O(1)).
References
Interview Questions
Showing 44 / 44
class LRU<K, V> {
private map = new Map<K, V>();
constructor(private max: number) {}
get(key: K): V | undefined {
if (!this.map.has(key)) return undefined;
const v = this.map.get(key)!;
this.map.delete(key); // move to the "newest" end
this.map.set(key, v);
return v;
}
set(key: K, value: V) {
if (this.map.has(key)) this.map.delete(key);
else if (this.map.size >= this.max) {
this.map.delete(this.map.keys().next().value!); // oldest
}
this.map.set(key, value);
}
}
class TokenBucket {
private tokens: number;
private last = Date.now();
constructor(private capacity: number, private refillPerSec: number) {
this.tokens = capacity;
}
tryRemove(cost = 1): boolean {
const now = Date.now();
this.tokens = Math.min(this.capacity, this.tokens + ((now - this.last) / 1000) * this.refillPerSec);
this.last = now;
if (this.tokens < cost) return false;
this.tokens -= cost;
return true;
}
}
async function pool<T, R>(items: T[], limit: number, fn: (x: T, i: number) => Promise<R>): Promise<R[]> {
const results = new Array<R>(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++; // claim index before awaiting
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
// count then keep a min-heap of size k (MinHeap: any binary heap keyed by count)
const counts = new Map<string, number>();
for (const e of events) counts.set(e.productId, (counts.get(e.productId) ?? 0) + 1);
const heap = new MinHeap<[string, number]>((a, b) => a[1] - b[1]);
for (const entry of counts) {
heap.push(entry);
if (heap.size > 10) heap.pop();
}
type Interval = [start: number, end: number];
function merge(xs: Interval[]): Interval[] {
const s = [...xs].sort((a, b) => a[0] - b[0]);
const out: Interval[] = [];
for (const [st, en] of s) {
const last = out.at(-1);
if (last && st <= last[1]) last[1] = Math.max(last[1], en);
else out.push([st, en]);
}
return out;
}
merge([[9, 12], [11, 14], [15, 17]]); // [[9,14],[15,17]]
function topoSort(deps: Record<string, string[]>): string[] {
const indeg = new Map<string, number>();
const out = new Map<string, string[]>();
for (const [n, ds] of Object.entries(deps)) {
indeg.set(n, indeg.get(n) ?? 0);
for (const d of ds) {
indeg.set(n, (indeg.get(n) ?? 0) + 1);
indeg.set(d, indeg.get(d) ?? 0);
out.set(d, [...(out.get(d) ?? []), n]);
}
}
const q = [...indeg].filter(([, v]) => v === 0).map(([k]) => k);
const order: string[] = [];
for (let i = 0; i < q.length; i++) {
const n = q[i];
order.push(n);
for (const m of out.get(n) ?? []) {
indeg.set(m, indeg.get(m)! - 1);
if (indeg.get(m) === 0) q.push(m);
}
}
if (order.length !== indeg.size) throw new Error('cycle detected');
return order;
}
topoSort({ api: ['db', 'cache'], cache: [], db: [], worker: ['db'] });
// ['db', 'cache', 'worker', 'api']
function maxSumK(xs: number[], k: number) {
let sum = xs.slice(0, k).reduce((a, b) => a + b, 0);
let best = sum;
for (let i = k; i < xs.length; i++) {
sum += xs[i] - xs[i - k];
best = Math.max(best, sum);
}
return best;
}
const queue: Job[] = [];
export const enqueue = (j: Job) => queue.push(j);
export function dequeue(): Job | undefined {
return queue.shift();
}
export function remove(id: string) {
const i = queue.findIndex((j) => j.id === id);
if (i >= 0) queue.splice(i, 1);
}
function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number) {
let t: ReturnType<typeof setTimeout> | undefined;
const debounced = function (this: unknown, ...args: A) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), ms);
};
debounced.cancel = () => clearTimeout(t);
return debounced;
}
const o: Record<string, string> = {};
o['b'] = 'x'; o['10'] = 'y'; o['a'] = 'z'; o['2'] = 'w';
console.log(Object.keys(o));
const m = new Map<string, string>([['b', 'x'], ['10', 'y'], ['a', 'z'], ['2', 'w']]);
console.log([...m.keys()]);
function throttle<A extends unknown[]>(fn: (...a: A) => void, ms: number) {
let last = 0;
let pending: A | null = null;
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: A) => {
const now = Date.now();
const remaining = ms - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pending = args;
timer ??= setTimeout(() => {
last = Date.now();
timer = undefined;
if (pending) fn(...pending);
pending = null;
}, remaining);
}
};
}
async function enrich(orders: Order[]) {
const out = [];
for (const o of orders) {
const customer = (await db.customers.findAll()).find((c) => c.id === o.customerId);
if (!out.some((x) => x.id === o.id)) out.push({ ...o, customerName: customer?.name });
}
return out;
}
type Node = { id: string; children: Node[] };
function collectIds(n: Node, acc: string[] = []): string[] {
acc.push(n.id);
for (const c of n.children) collectIds(c, acc);
return acc;
}
// prod: an imported tree is a 120,000-level-deep chain; one node references an ancestor
-- KEYS[1] = rl:{userId}; ARGV = now_ms, window_ms, limit, request_id
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])
local n = redis.call('ZCARD', KEYS[1])
if n >= tonumber(ARGV[3]) then return 0 end
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[4])
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
const EMAIL = /^([a-zA-Z0-9]+)*@example\.com$/;
EMAIL.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // takes seconds or longer
function reservoir<T>(stream: Iterable<T>, k: number): T[] {
const r: T[] = [];
let i = 0;
for (const x of stream) {
if (i < k) r.push(x);
else {
const j = Math.floor(Math.random() * (i + 1));
if (j < k) r[j] = x;
}
i++;
}
return r;
}