Thư Viện Câu Hỏi Phỏng Vấn
Tổng hợp các câu hỏi tuyển dụng thực tế theo nhiều cấp độ từ Entry đến Expert để bạn tự tin chinh phục nhà tuyển dụng.
Làm thế nào để phát hiện chu trình (cycle) trong đồ thị có hướng (Directed Graph)?
Có thể sử dụng thuật toán duyệt đồ thị DFS (Depth-First Search) kết hợp kỹ thuật tô màu các đỉnh (hoặc sử dụng Recursion Stack):
- Trạng thái 3 màu:
- Trắng (Unvisited): Đỉnh chưa được duyệt.
- Xám (Visiting): Đỉnh đang nằm trong nhánh đệ quy hiện tại (chưa duyệt xong các đỉnh kề).
- Đen (Visited): Đỉnh và toàn bộ các đỉnh kề của nó đã duyệt xong.
- Phát hiện chu trình: Trong quá trình duyệt DFS, nếu gặp một đỉnh kề đang ở trạng thái Xám, điều đó có nghĩa là có một cạnh ngược (back edge) trỏ về tổ tiên của nó trên nhánh đệ quy, tức là đồ thị tồn tại chu trình.
Hãy giải thích cơ chế của kỹ thuật Sliding Window (Cửa sổ trượt) trên mảng dữ liệu?
Sliding Window được dùng để tìm kiếm một phân đoạn con (subarray) thỏa mãn điều kiện nhất định trên mảng mà không cần tính toán lại từ đầu các phần tử trùng lặp:
- Cơ chế: Duy trì một \'cửa sổ\' được định nghĩa bởi hai chỉ số
startvàend. Khi mở rộng cửa sổ sang phải (end++), ta cộng thêm phần tử mới vào kết quả tạm thời. Khi điều kiện bị vi phạm, ta thu hẹp cửa sổ từ bên trái (start++) và trừ bớt giá trị của phần tử bị loại bỏ. - Hiệu năng: Giúp chuyển đổi các thuật toán vét cạn độ phức tạp O(n^2) thành thuật toán tuyến tính O(n) vì mỗi phần tử chỉ được duyệt qua tối đa 2 lần.
Hãy phân biệt sự khác nhau giữa tiếp cận Bottom-Up (Tabulation) và Top-Down (Memoization) trong Quy hoạch động?
Bài toán cái túi (0/1 Knapsack Problem) được giải quyết bằng Quy hoạch động như thế nào?
Bài toán yêu cầu chọn các đồ vật có khối lượng và giá trị sao cho tổng khối lượng không vượt quá W và tổng giá trị lớn nhất:
- Trạng thái: Gọi
dp[i][w]là giá trị lớn nhất có thể đạt được khi chọn trongiđồ vật đầu tiên với giới hạn khối lượng làw. - Công thức truy hồi: Với đồ vật thứ
icó khối lượngweight[i]và giá trịval[i]:- Nếu
weight[i] > w: Không thể chọn đồ vật này ->dp[i][w] = dp[i-1][w]. - Nếu
weight[i] <= w: Ta chọn giá trị lớn hơn giữa việc lấy hoặc không lấy đồ vật ->dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + val[i]).
- Nếu
- Kết quả: Nằm ở ô
dp[n][W]với độ phức tạp thời gian là O(n*W).
Sự khác biệt thế nào giữa các thuật toán sắp xếp ổn định (Stable Sort) và không ổn định (Unstable Sort)? Tại sao tính chất này quan trọng?
Làm thế nào để thuật toán Quick Sort đạt hiệu năng tốt nhất và tránh rơi vào trường hợp xấu nhất O(n^2)?
Trường hợp xấu nhất O(n^2) của Quick Sort xảy ra khi mảng đầu vào đã được sắp xếp (hoặc ngược lại) và ta liên tục chọn phần tử chốt (pivot) ở vị trí biên (đầu hoặc cuối mảng), dẫn đến việc phân chia mảng bị lệch hoàn toàn (một bên 0 phần tử, một bên n-1 phần tử).
- Giải pháp khắc phục:
- Chọn Pivot ngẫu nhiên (Randomized Quick Sort): Chọn pivot ngẫu nhiên trong khoảng mảng đang xét thay vì cố định.
- Kỹ thuật Median-of-Three: Chọn pivot là trung vị của ba phần tử: đầu mảng, giữa mảng và cuối mảng. Kỹ thuật này giúp phân chia mảng cân bằng nhất có thể.
- Xáo trộn mảng (Shuffle): Thực hiện xáo trộn ngẫu nhiên mảng đầu vào trước khi tiến hành sắp xếp.
Hợp đồng thông minh (Smart Contract) là gì và làm thế nào để ngăn chặn lỗ hổng Reentrancy Attack trong Solidity?
Smart Contract là chương trình tự thực thi mã nguồn được lưu trữ và chạy trên blockchain khi các điều kiện thỏa thuận được đáp ứng.
- Reentrancy Attack: Xảy ra khi một hợp đồng gọi đến một hợp đồng bên ngoài chưa tin cậy trước khi nó cập nhật trạng thái số dư (state) của chính nó. Hợp đồng độc hại có thể gọi ngược lại (re-enter) hàm rút tiền liên tục để rút hết quỹ trước khi giao dịch ban đầu kịp cập nhật số dư.
- Phòng chống:
- Checks-Effects-Interactions Pattern: Luôn thực hiện kiểm tra điều kiện (checks) trước, sau đó thay đổi trạng thái số dư trong bộ nhớ (effects), rồi mới thực hiện chuyển tiền (interactions).
- ReentrancyGuard: Sử dụng modifier
nonReentrantcủa OpenZeppelin để khóa hàm trong suốt quá trình thực thi.
Sự khác biệt giữa cây AVL và cây Đỏ-Đen (Red-Black Tree)? Khi nào nên chọn loại nào?
Cả hai đều là cây nhị phân tìm kiếm tự cân bằng nhưng có sự khác biệt về mức độ cân bằng:
- Cây AVL: Cân bằng nghiêm ngặt hơn (chênh lệch chiều cao tối đa là 1). Do đó, cây AVL có tốc độ tìm kiếm nhanh hơn vì chiều cao tối ưu hơn. Tuy nhiên, chi phí tự cân bằng (xoay cây) khi thêm/xóa phần tử rất đắt.
- Cây Đỏ-Đen: Cân bằng lỏng lẻo hơn. Thao tác thêm/xóa nhanh hơn AVL vì số lần xoay cây tối đa ít hơn. Tốc độ tìm kiếm chậm hơn một chút so với AVL.
- Lựa chọn: Chọn AVL cho các ứng dụng có tần suất đọc/tìm kiếm dữ liệu lớn hơn nhiều so với ghi. Chọn Red-Black cho các ứng dụng có tần suất thêm/xóa dữ liệu thường xuyên (ví dụ: Map và Set trong C++ STL được cài đặt bằng cây Đỏ-Đen).
Khái niệm Load Factor (hệ số tải) trong Hash Table là gì và tầm quan trọng của nó khi Rehashing?
Hệ số tải (Load Factor - alpha) là tỷ lệ giữa số lượng phần tử hiện có (n) chia cho dung lượng của bảng băm (k): alpha = n/k.
- Tầm quan trọng: Khi hệ số tải vượt quá một ngưỡng nhất định (thường là 0.75), xác suất xảy ra xung đột (collision) tăng cao, làm giảm hiệu năng tìm kiếm từ O(1) về O(n).
- Rehashing: Khi đạt ngưỡng, Hash Table sẽ tự động tăng kích thước bảng băm (thường gấp đôi) và tính toán lại vị trí (hash index) cho toàn bộ phần tử cũ sang bảng mới. Điều này giúp giữ hệ số tải luôn thấp để đảm bảo tốc độ truy cập luôn đạt O(1).
Thuật toán tìm kiếm chuỗi KMP (Knuth-Morris-Pratt) hoạt động như thế nào để đạt độ phức tạp O(n + m)?
Thuật toán KMP tìm kiếm chuỗi mẫu (pattern) trong chuỗi văn bản (text) mà không cần quay lui con trỏ của chuỗi văn bản khi gặp ký tự không khớp:
- Mảng tiền tố (LPS Array): KMP xây dựng trước một mảng LPS (Longest Proper Prefix which is also Suffix). LPS[i] lưu độ dài của tiền tố khớp với hậu tố dài nhất của chuỗi mẫu tính đến chỉ số i.
- So khớp: Khi duyệt chuỗi văn bản, nếu gặp ký tự không khớp tại vị trí
jcủa chuỗi mẫu, ta không lùi con trỏ chuỗi văn bản về mà dịch chuyển con trỏ chuỗi mẫu sang vị tríLPS[j-1]để tiếp tục so sánh. Điều này tránh được các phép so sánh trùng lặp vô ích.
Làm thế nào để đảo ngược một Danh sách liên kết đơn (Singly Linked List) bằng phương pháp lặp (Iterative)?
Để đảo ngược danh sách liên kết đơn tại chỗ, ta sử dụng 3 con trỏ prev, curr, và next:
- Khởi tạo:
prev = null,curr = head. - Duyệt qua danh sách cho đến khi
curr == null: a. Lưu node tiếp theo:next = curr.next. b. Đảo ngược liên kết của node hiện tại:curr.next = prev. c. Dịch chuyển hai con trỏ tiến lên:prev = curr,curr = next. - Khi vòng lặp kết thúc, gán
head = prevđể làm node đầu mới. Độ phức tạp thời gian là O(n) và độ phức tạp không gian là O(1).
Làm thế nào để kiểm tra một Cây nhị phân có phải là Cây nhị phân tìm kiếm (BST - Binary Search Tree) hợp lệ hay không?
Một Cây nhị phân tìm kiếm (BST) hợp lệ nếu mọi node đều thỏa mãn điều kiện: Tất cả các node thuộc cây con bên trái phải có giá trị nhỏ hơn node hiện tại, và tất cả các node thuộc cây con bên phải phải có giá trị lớn hơn node hiện tại.
- Sai lầm phổ biến: Chỉ kiểm tra cục bộ xem node con bên trái có nhỏ hơn node cha và node con bên phải có lớn hơn node cha hay không. Cách này sai nếu node con bên trái của con bên phải lại nhỏ hơn node gốc.
- Giải pháp tối ưu: Duyệt cây và truyền kèm phạm vi giá trị hợp lệ [min, max] cho từng node:
- Node gốc bắt đầu với phạm vi [-infinity, +infinity].
- Khi đi sang con bên trái, cập nhật giá trị max bằng giá trị của node hiện tại: [min, node.val].
- Khi đi sang con bên phải, cập nhật giá trị min bằng giá trị của node hiện tại: [node.val, max].
- Nếu giá trị của bất kỳ node nào nằm ngoài phạm vi của nó, trả về false. Độ phức tạp thời gian là O(n) và độ phức tạp không gian là O(h) với h là chiều cao của cây do đệ quy.
Độ phức tạp thời gian khấu hao (Amortized Time Complexity) là gì? Cho ví dụ thực tế trong cấu trúc dữ liệu?
Độ phức tạp thời gian khấu hao (Amortized Complexity) là công cụ phân tích hiệu năng trung bình của một chuỗi các thao tác liên tiếp trên một cấu trúc dữ liệu, trong đó có một vài thao tác cực kỳ tốn thời gian nhưng tần suất xuất hiện rất hiếm, trong khi đa số thao tác khác lại cực kỳ nhanh.
- Khác biệt với Average Case: Khấu hao đảm bảo hiệu năng trung bình của chuỗi thao tác trong trường hợp xấu nhất (Worst-Case), chứ không dựa trên phân phối xác suất ngẫu nhiên của dữ liệu.
- Ví dụ thực tế (ArrayList/Vector):
- Thao tác thêm phần tử vào cuối
push_back()thường tốn O(1) nếu mảng còn chỗ trống. - Khi mảng đầy, ta phải tạo mảng mới có dung lượng gấp đôi và sao chép toàn bộ n phần tử cũ sang, tốn O(n).
- Tuy nhiên, thao tác tốn O(n) này chỉ xảy ra sau n lần thêm phần tử thành công. Tổng chi phí cho n lần thêm là n * O(1) + O(n) = O(n). Chia trung bình cho n thao tác, chi phí mỗi lần chỉ là O(1). Ta nói độ phức tạp khấu hao của
push_backlà O(1).
- Thao tác thêm phần tử vào cuối
Hàng đợi ưu tiên (Priority Queue) hoạt động như thế nào và sự khác biệt khi triển khai bằng Heap so với Mảng/Danh sách liên kết?
Hàng đợi ưu tiên là cấu trúc dữ liệu mà mỗi phần tử được gắn với một độ ưu tiên. Phần tử có độ ưu tiên cao nhất luôn được lấy ra trước, bất kể thời điểm thêm vào.
- Triển khai bằng Mảng/Danh sách liên kết:
- Nếu giữ mảng không sắp xếp: Thêm phần tử tốn O(1), lấy phần tử lớn nhất tốn O(n) vì phải quét toàn bộ mảng.
- Nếu giữ mảng sắp xếp: Thêm phần tử tốn O(n) vì phải dịch chuyển các phần tử khác, lấy ra tốn O(1).
- Triển khai bằng Binary Heap (Tối ưu nhất):
- Heap là cây nhị phân gần hoàn chỉnh thỏa mãn tính chất Heap (cha luôn lớn hơn hoặc nhỏ hơn con).
- Thao tác thêm phần tử
enqueuetốn O(log n) nhờ quá trình sift-up. - Thao tác lấy ra phần tử đầu
dequeuetốn O(log n) nhờ quá trình sift-down. - Xem phần tử đầu
peektốn O(1). - Đây là sự cân bằng tuyệt vời giúp quản lý các tác vụ ưu tiên động cực kỳ hiệu quả.
Cấu trúc dữ liệu Heap (Min-Heap / Max-Heap) là gì? Thao tác thêm phần tử (Heapify-up) và xóa phần tử gốc (Heapify-down) có độ phức tạp là bao nhiêu?
Heap là một cây nhị phân gần như hoàn chỉnh (thường được biểu diễn dưới dạng mảng để tiết kiệm bộ nhớ) thỏa mãn tính chất:
- Max-Heap: Giá trị của node cha luôn lớn hơn hoặc bằng giá trị các con của nó (Gốc là giá trị lớn nhất).
- Min-Heap: Giá trị của node cha luôn nhỏ hơn hoặc bằng giá trị các con của nó (Gốc là giá trị nhỏ nhất).
- Thao tác Thêm (Heapify-up): Phần tử mới được chèn vào cuối cây (cuối mảng). Sau đó, nó được so sánh với cha và hoán vị đi lên nếu vi phạm thuộc tính Heap. Chiều cao tối đa của cây là log n, nên độ phức tạp là O(log n).
- Thao tác Xóa gốc (Heapify-down): Thay thế phần tử gốc bằng phần tử cuối cùng của mảng, sau đó xóa phần tử cuối cùng này. Tiếp theo, so sánh gốc mới với các con và hoán vị đi xuống với con lớn hơn (hoặc nhỏ hơn đối với Min-Heap) để tái lập cấu trúc Heap. Độ phức tạp là O(log n).



.png)
.png)