Skip to content
DevRecall
← Roadmap
02

Data Structures & Algorithms for Seniors

drafted

Big-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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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ậtMột câuVí dụ backend
Hash map (Map)Tra cứu trung bình O(1), Map trong JS giữ thứ tự chènDedupe, index theo id, LRU
Dynamic arrayTruy cập O(1), push amortized O(1), chèn/xoá đầu O(n)Buffer, batch
Deque / ring bufferThêm/bớt hai đầu O(1)Queue job, sliding window log
Binary heapLấy min/max O(1), push/pop O(log n)Top-K, scheduler, merge k sorted
TrieCây theo ký tự, tra prefix O(độ dài key)Autocomplete, routing theo path
Graph + BFS/DFSDuyệt quan hệ; BFS cho đường ngắn nhất không trọng sốDependency, permission inheritance
Topological sortThứ tự tuyến tính của DAG, phát hiện cycleBuild order, migration order
Sliding windowDuy trì cửa sổ trên stream thay vì tính lạiRate limit, moving average
Binary searchO(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+treeCây nhiều nhánh tối ưu cho disk pageIndex 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 KBUnique 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ánCách ACách BChọ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 limitFixed window counter: rẻ, burst ở ranh giớiSliding window log: chính xác, tốn memoryToken bucket khi cần cho phép burst có kiểm soát
MembershipSet: chính xác, O(n) memoryBloom filter: false positive, rất ít memoryHàng trăm triệu key, chấp nhận sai một chiều
Distinct countSet: chính xácHyperLogLog: sai ~1%, vài KBAnalytics quy mô lớn
Queue in-memoryArray.shift(): có thể O(n)Ring buffer / head index: O(1)Queue lớn, throughput cao
AutocompleteLIKE 'abc%' với indexTrie in-memory / search engine (edge n-gram)Tập từ nhỏ vs lớn/đa ngôn ngữ
Dedupe streamLưu hết idTTL window / BloomTuỳ 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/indexOf trong vòng lặp → O(n²) ẩn; đổi sang Set.
  • 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ùng Map hoặc Object.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.sort stable 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ếu next.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)).

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 44 / 44

[10, 9, 1, 100].sort();              // ?
[10, 9, 1, 100].sort((a, b) => a - b); // ?
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;
}