Phỏng vấn live coding cho junior: 20 phút vừa làm vừa nói

Phỏng vấn live coding cho junior: chia 20 phút thành các bước hỏi lại đề, đi qua ví dụ, viết code, kiểm tra và những câu nên nói to, kèm bài luyện chạy được.

Phỏng vấn live coding là phần bạn viết code trước mặt người phỏng vấn, thường trong 20 đến 45 phút, và họ muốn nghe bạn nghĩ. Bí quyết cho junior là đừng gõ ngay. Bạn hỏi lại đề, đi qua một ví dụ bằng tay, rồi mới code. Bạn có hay im lặng khi bí không? Cùng mình xem Minh làm nhé!

Sáng thứ Năm, đề bài hiện lên

Ở bài trước, Minh ôn một tuần cho buổi phỏng vấn kỹ thuật của công ty A. Minh là nhân vật minh họa, ghép từ những tình huống quen thuộc của sinh viên IT mới ra trường, và công ty A cũng vậy. Buổi phỏng vấn đã tới phần code: trưởng nhóm frontend chia sẻ một trình soạn code trống, một đồng hồ đếm ngược 20 phút, và nói "em vừa làm vừa nói to nhé".

Đề bài hiện lên bốn dòng:

Đề bài (minh họa)
Cho một mảng các ghi chú khoản chi, ví dụ ["cà phê", "cơm trưa", "cà phê"].
Viết hàm trả về tối đa 3 ghi chú xuất hiện nhiều nhất, kèm số lần.

Bài này nhỏ, gần với sản phẩm quản lý chi tiêu của công ty A, và không cần thuật toán nào cao siêu. Minh nghĩ "dễ", và đó chính là lúc nguy hiểm nhất. Phần lớn người rớt ở bài dễ không rớt vì không biết giải, mà vì giải nhanh quá rồi giải sai.

Người phỏng vấn thực sự chấm gì?

Trước khi tới code, mình muốn nói về một điều đổi cách bạn chuẩn bị. Người phỏng vấn ngồi đó không chỉ chờ một hàm chạy đúng. Họ muốn biết nếu bạn vào nhóm, làm việc cùng bạn sẽ như thế nào. Các hướng dẫn về phỏng vấn code, ví dụ bộ mẹo của Interview Cake, thường khuyên những việc giống nhau: nói to quá trình suy nghĩ, thử với ví dụ cụ thể, và đừng im lặng khi bị kẹt. Mình nghĩ lý do khá dễ hiểu. Một đoạn code đúng không cho họ thấy bạn nghĩ gì, còn một đoạn code sai kèm lời giải thích mạch lạc cho họ thấy bạn sửa được.

Với junior, mình nghĩ điều này còn quan trọng hơn. Người ta không kỳ vọng bạn biết hết, nhưng họ kỳ vọng bạn hỏi đúng chỗ và tự phát hiện lỗi của mình. Nên cách chuẩn bị tốt nhất không phải là học thêm thuật toán, mà là có sẵn một quy trình để chạy mỗi khi đề hiện lên.

Chia 20 phút thế nào?

Mình chia 20 phút thành năm đoạn. Con số chỉ là gợi ý để bạn luyện bấm giờ, và buổi phỏng vấn thật sẽ lệch đi đôi chút:

ke-hoach-20-phut.txt (minh họa)
Phút 0–4    Hiểu đề: nhắc lại bằng lời của mình, hỏi những chỗ chưa rõ
Phút 4–7    Ví dụ và cách làm: đi qua một ví dụ bằng tay, nói cách làm trước khi code
Phút 7–15   Viết code, vừa viết vừa nói
Phút 15–18  Chạy với ví dụ, rồi thử các trường hợp lạ: rỗng, trùng, hòa nhau
Phút 18–20  Nói về cách làm: chạy nhanh cỡ nào, nếu có thêm thời gian sẽ sửa gì

Phần mình muốn bạn chú ý là bốn phút đầu tiên không có dòng code nào. Nhiều người thấy đó là phí thời gian, và mình hiểu cảm giác đó khi đồng hồ đang đếm ngược. Nhưng bốn phút đó là phần rẻ nhất trong cả buổi. Hiểu sai đề thì mười phút code sau đó là mười phút bỏ đi.

Hỏi lại đề những gì?

Mỗi đề đều có những chỗ mơ hồ, và người phỏng vấn thường cố tình để vậy. Câu hỏi của bạn cho họ thấy bạn nhìn ra được chỗ mơ hồ. Với đề trên, Minh hỏi ba nhóm câu:

Minh hỏi lại đề (minh họa)
Minh: Ghi chú "cà phê" và "Cà phê " có tính là một không ạ? Một cái viết hoa và một cái có dấu cách ở cuối.
Người phỏng vấn: Tính là một.
Minh: Nếu hai ghi chú xuất hiện số lần bằng nhau thì em xếp theo thứ tự nào ạ?
Người phỏng vấn: Cái nào xuất hiện trước trong mảng thì đứng trước.
Minh: Mảng có thể có ghi chú trống không ạ, và kết quả em trả về dạng nào, chữ gốc hay chữ đã chuẩn hóa?
Người phỏng vấn: Có thể có ghi chú trống, bỏ qua. Em trả về dạng chữ thường, bỏ dấu cách thừa.

Đoạn này chỉ mất khoảng một phút, nhưng nó đã chốt bốn luật mà nếu không hỏi thì Minh phải tự đoán. Mình ghi chúng lại thành một dòng ở đầu file code, vì đó là bản đặc tả của bài:

Bốn luật sau khi hỏi lại đề
// Hai ghi chú chỉ khác chữ hoa/thường hoặc dấu cách thừa là MỘT ghi chú;
// ghi chú trống bị bỏ qua; số lần bằng nhau thì ghi chú xuất hiện trước trong mảng đứng trước;
// kết quả hiển thị dạng đã chuẩn hóa (chữ thường, không dấu cách thừa).

Bạn có thể thấy người phỏng vấn trả lời gọn. Đó là bình thường. Họ không cố giấu, và nhiều khi chính họ cũng chưa nghĩ tới câu hỏi đó. Nếu đề của họ thật sự có một chỗ họ chưa quyết định, họ sẽ nói "em cứ chọn một cách và nói lý do", và đó cũng là một câu trả lời tốt để bạn nghe.

Nếu gõ ngay thì chuyện gì xảy ra?

Để bạn thấy vì sao bốn phút đó đáng, mình thử làm ngược lại. Giả sử Minh bỏ qua việc hỏi và gõ ngay trong 6 phút. Cách ai cũng nghĩ tới đầu tiên là đếm bằng một object, rồi sắp xếp theo số lần:

bai-luyen/ban-dau.ts
// Bản viết vội trong 6 phút đầu, trước khi hỏi lại đề. Chạy: node bai-luyen/ban-dau.ts
function topGhiChu(ghiChu: string[]) {
  const dem: Record<string, number> = {};
  for (const g of ghiChu) dem[g] = (dem[g] ?? 0) + 1;
  return Object.entries(dem)
    .map(([ghiChu, soLan]) => ({ ghiChu, soLan }))
    .sort((a, b) => b.soLan - a.soLan)
    .slice(0, 3);
}

// Ví dụ người phỏng vấn đưa ra.
console.log(topGhiChu(["cà phê", "Cà phê ", "cơm trưa", "cà phê", "bún bò", "cơm trưa", "  "]));

Code này ngắn, đọc gọn, và nhìn qua thì đúng. Mảng ví dụ có bảy phần tử, trong đó có "Cà phê " viết hoa kèm dấu cách thừa và một ghi chú trống. Mình chạy với Node.js 22.18 trở lên, không cần cài gì:

node bai-luyen/ban-dau.ts
[
  { ghiChu: 'cà phê', soLan: 2 },
  { ghiChu: 'cơm trưa', soLan: 2 },
  { ghiChu: 'Cà phê ', soLan: 1 }
]

Kết quả sai. "cà phê" xuất hiện ba lần nếu tính cả "Cà phê ", nhưng code chỉ đếm 2. "Cà phê " chen vào như một ghi chú riêng. Và "bún bò", một ghi chú có thật, bị đẩy ra ngoài top 3. Tất cả sai vì code so sánh chuỗi nguyên văn, đúng điều mà câu hỏi đầu tiên đã làm rõ.

Điểm đáng nói là Minh có thể phát hiện lỗi này mà không cần chạy code. Chỉ cần đi qua ví dụ bằng tay ở phút 4 đến 7, nói to từng phần tử: "phần tử đầu là cà phê, đếm một. Phần tử hai là Cà phê viết hoa, cái này em đã thống nhất là cùng ghi chú..." Câu "đã thống nhất" làm lộ ra rằng code chưa xử lý nó. Đi qua ví dụ bằng tay là bước mà mình khuyên bạn không bao giờ bỏ, vì nó rẻ hơn viết xong rồi gỡ lỗi trước mặt người ta.

Bản sửa, sau khi hỏi lại đề

Cách sửa gồm hai phần. Mình chuẩn hóa mỗi ghi chú trước khi đếm, bằng một hàm nhỏ, và dùng Map thay cho object để thứ tự thêm vào được giữ nguyên:

bai-luyen/top-ghi-chu.ts
// Bài luyện 20 phút: cho danh sách ghi chú khoản chi, trả về tối đa 3 ghi chú xuất hiện nhiều nhất.
// Luật (sau khi hỏi lại đề): hai ghi chú chỉ khác chữ hoa/thường hoặc dấu cách thừa là MỘT ghi chú;
// ghi chú trống bị bỏ qua; số lần bằng nhau thì ghi chú xuất hiện trước trong mảng đứng trước;
// kết quả hiển thị dạng đã chuẩn hóa (chữ thường, không dấu cách thừa).
export type DongTop = { ghiChu: string; soLan: number };

const chuanHoa = (ghiChu: string) => ghiChu.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();

export function topGhiChu(ghiChu: string[], soDong = 3): DongTop[] {
  const dem = new Map<string, number>(); // Map giữ thứ tự thêm vào, nên "xuất hiện trước" còn nguyên
  for (const g of ghiChu) {
    const khoa = chuanHoa(g);
    if (khoa === "") continue;
    dem.set(khoa, (dem.get(khoa) ?? 0) + 1);
  }
  // sort của JavaScript ổn định (từ ES2019): số lần bằng nhau thì giữ nguyên thứ tự thêm vào.
  return [...dem]
    .map(([ghiChu, soLan]) => ({ ghiChu, soLan }))
    .sort((a, b) => b.soLan - a.soLan)
    .slice(0, soDong);
}

Mỗi dòng trong hàm chuanHoa giải quyết một thứ cụ thể. trim bỏ dấu cách hai đầu, replace(/\s+/g, " ") gộp nhiều dấu cách ở giữa thành một, và toLowerCase đưa về chữ thường. Dòng đầu, normalize("NFC"), có vẻ thừa, và mình sẽ giải thích ngay bên dưới vì sao nó không thừa.

Hai chú thích trong code là hai chỗ Minh nên nói to khi viết, vì đó là hai quyết định mà người phỏng vấn muốn nghe lý do. Thứ nhất, Map giữ thứ tự mà phần tử được thêm vào, nên "ghi chú xuất hiện trước đứng trước" được thỏa mà không cần viết thêm code. Thứ hai, sort của JavaScript là sắp xếp ổn định từ ES2019. Nghĩa là hai phần tử có số lần bằng nhau giữ nguyên thứ tự trước khi sắp xếp. Nếu Minh không nhớ điều này, một cách an toàn khác là so thêm vị trí xuất hiện đầu tiên, và nói rõ đó là cách làm chắc ăn.

Chạy ví dụ rồi thử các trường hợp lạ

Phút 15 đến 18 là lúc kiểm tra. Minh chạy ví dụ của người phỏng vấn trước, rồi tự nghĩ ra các trường hợp biên và nói to từng cái: mảng rỗng, hai ghi chú hòa nhau, ít hơn ba ghi chú khác nhau, nhiều hơn ba, và ghi chú trống. Mình viết các trường hợp đó thành test với trình chạy test có sẵn của Node.js:

bai-luyen/top-ghi-chu.test.ts
// Chạy: node bai-luyen/top-ghi-chu.test.ts   (Node.js 22.18+, không cần cài gì)
import assert from "node:assert/strict";
import { test } from "node:test";

import { topGhiChu } from "./top-ghi-chu.ts";

test("ví dụ của người phỏng vấn", () => {
  const ketQua = topGhiChu(["cà phê", "Cà phê ", "cơm trưa", "cà phê", "bún bò", "cơm trưa", "  "]);
  assert.deepEqual(ketQua, [
    { ghiChu: "cà phê", soLan: 3 },
    { ghiChu: "cơm trưa", soLan: 2 },
    { ghiChu: "bún bò", soLan: 1 },
  ]);
});

test("danh sách rỗng thì kết quả rỗng", () => {
  assert.deepEqual(topGhiChu([]), []);
});

test("ghi chú trống hoặc chỉ có dấu cách bị bỏ qua", () => {
  assert.deepEqual(topGhiChu(["", "   "]), []);
});

test("số lần bằng nhau thì ghi chú xuất hiện trước đứng trước", () => {
  assert.deepEqual(topGhiChu(["trà sữa", "cà phê", "cà phê", "trà sữa"]).map((d) => d.ghiChu), ["trà sữa", "cà phê"]);
});

test("ít hơn 3 ghi chú khác nhau thì trả đúng số đó", () => {
  assert.equal(topGhiChu(["cà phê", "cà phê"]).length, 1);
});

test("dấu tiếng Việt gõ dạng tổ hợp (NFD) vẫn là cùng một ghi chú", () => {
  const nfd = "cà phê".normalize("NFD");
  assert.deepEqual(topGhiChu([nfd, "cà phê"]), [{ ghiChu: "cà phê", soLan: 2 }]);
});

test("có hơn 3 ghi chú khác nhau thì chỉ lấy 3", () => {
  assert.equal(topGhiChu(["a", "b", "c", "d"]).length, 3);
});
node bai-luyen/top-ghi-chu.test.ts
# tests 7
# pass 7
# fail 0

Trong buổi phỏng vấn thật, Minh sẽ không có thời gian viết file test. Minh sẽ chạy ví dụ trong console của trình soạn code và đọc kết quả to lên. Mình viết test ở đây để bạn tự luyện và để thấy từng luật có một ca kiểm tra riêng.

Test thứ sáu có thể làm bạn thắc mắc, vì sao phải thử tiếng Việt gõ ở dạng "tổ hợp"? Chữ "à" có hai cách biểu diễn trong máy tính. Dạng NFC là một ký tự duy nhất, còn dạng NFD là chữ "a" cộng một dấu huyền rời. Hai chuỗi nhìn giống hệt nhau trên màn hình nhưng so sánh bằng === thì khác. Chữ "cà phê" dạng NFD dài 8 đơn vị, dạng NFC dài 6, theo kết quả mình chạy. Một số bàn phím điện thoại và một số nguồn dữ liệu cho ra dạng NFD, nên dòng normalize("NFC") bảo vệ bạn khỏi một lỗi gần như không thể thấy bằng mắt.

Để chắc là test làm được việc của nó, mình thử bỏ từng luật khỏi chuanHoa rồi chạy lại. Bỏ dòng bỏ qua ghi chú trống thì một test đỏ. Bỏ normalize("NFC") thì một test đỏ, bỏ trim() thì hai test đỏ, và bỏ toLowerCase() thì một test đỏ. Không luật nào là thừa.

Bí thì nói gì?

Mọi người đều bí ở một lúc nào đó, và im lặng là cách tệ nhất để xử lý. Người phỏng vấn không đọc được suy nghĩ của bạn, nên một phút im lặng trông giống như bạn đã bỏ cuộc. Mình gợi ý bạn chuẩn bị sẵn vài câu để nói trong những lúc đó:

cau-noi-khi-bi.txt (minh họa)
Khi chưa rõ đề:        "Em xin hỏi lại một chút: ..."
Khi cần nghĩ:          "Cho em một phút để nghĩ cách làm."
Khi đang kẹt:          "Em đang kẹt ở chỗ ... Em thử hai hướng: ..."
Khi quên cú pháp:      "Em không nhớ chính xác tên hàm này. Em dự định dùng một hàm để ..., em tra nhanh được không ạ?"
Khi phát hiện lỗi:     "Em thấy chỗ này chưa đúng, vì ... Em sửa như sau."

Câu "em tra nhanh được không ạ" có thể thấy lạ, nhưng nó phản ánh đúng công việc thật. Lập trình viên nào cũng tra tài liệu hằng ngày. Không có quy luật chung về việc có được tra hay không, mỗi công ty cho phép khác nhau, nên hỏi trước là cách an toàn. Điều đáng ngại hơn là giả vờ nhớ rồi viết sai tên hàm.

Còn khi phát hiện ra lỗi của chính mình, như Minh làm ở phút 15, hãy nói ngay. Theo mình, một người tự bắt được lỗi của mình và giải thích được vì sao đáng tin hơn một người viết ra code đúng mà không nói được nó chạy thế nào.

Hai phút cuối: nói về cách làm

Còn khoảng hai phút, Minh không cần viết thêm code. Đây là lúc nói về những gì đã làm, và người phỏng vấn thường hỏi: "hàm này chạy nhanh cỡ nào?". Với bài này, câu trả lời gọn là: hàm duyệt mảng một lần để đếm, và sắp xếp danh sách ghi chú khác nhau. Nếu có *n* ghi chú thì việc đếm tốn tỉ lệ với *n*, còn sắp xếp tốn tỉ lệ với số ghi chú khác nhau nhân với logarit của nó. Minh không cần nói công thức chính xác. Nói được đúng hai phần "duyệt một lần" và "sắp xếp" là đủ.

Minh cũng có thể nói điều sẽ làm nếu có thêm thời gian, như bỏ dấu tiếng Việt khi so sánh để "ca phe" và "cà phê" gộp làm một, hoặc nhận số dòng lấy ra làm tham số, vốn đã có sẵn là soDong = 3. Nói rõ bạn biết phần nào chưa làm cho thấy bạn nhìn thấy bức tranh rộng hơn, và đó là điều người ta tìm ở một junior.

Luyện thế nào để lên phòng phỏng vấn không run?

Phần quan trọng nhất của bài này thật ra nằm ngoài buổi phỏng vấn. Quy trình này chỉ thuần thục sau khi bạn đã chạy nó vài lần, và cách luyện hiệu quả nhất mình biết là tự đặt bài, bấm giờ, và nói to một mình. Gõ code mà nói thành tiếng là một kỹ năng khác với gõ code trong im lặng, và nó khó chịu trong lần đầu. Một vài lần sau thì nó thành thói quen.

Mỗi lần luyện, bạn chọn một bài nhỏ, đặt đồng hồ 20 phút, và đi đủ năm đoạn ở trên. Cuối buổi, nhìn lại hai câu hỏi: bạn có hỏi lại đề không, và bạn có đi qua ví dụ bằng tay trước khi gõ không. Hai thói quen này đáng hơn mười bài thuật toán thêm.

Sau vòng kỹ thuật là gì?

Minh ra khỏi buổi phỏng vấn kỹ thuật với cảm giác khá ổn. Hàm chạy đúng ví dụ, ba ca biên được nói ra, và Minh sửa được một lỗi nhỏ ở phút 16. Vài ngày sau, công ty A nhắn lại: Minh được mời sang vòng tiếp theo, một buổi nói chuyện với bộ phận nhân sự.

Vòng này không có đồng hồ đếm ngược và không có trình soạn code. Người ngồi đối diện sẽ hỏi những câu như "kể cho chị nghe lần em làm việc nhóm gặp mâu thuẫn" hay "điểm yếu lớn nhất của em là gì". Những câu này không có đáp án đúng để kiểm tra bằng test, và nhiều bạn kỹ thuật tốt lại lúng túng ở đây. Cách trả lời một câu hỏi về hành vi cho có cấu trúc, mà không nghe như đọc thuộc lòng, là việc của các bài viết sau.

Bạn theo dõi cả hành trình ở series Hành trình junior. Nếu muốn luyện thêm, câu giới thiệu bản thân trong một phút và các câu khác có trong ngân hàng câu hỏi phỏng vấn.

Nhận bài tiếp theo qua email.

Một email mỗi tuần, chỉ khi có điều đáng đọc. Không spam.