Hết tuần 4, bạn đã thoải mái với Easy LeetCode. Subquery là chìa khóa unlock đa số Medium — đặc biệt các bài "tìm cái lớn/nhỏ nhất theo nhóm", "tìm dòng có liên hệ với dòng khác", "lọc theo aggregate".
Sau tuần 5 bạn cần làm được
Đặt subquery đúng vị trí (SELECT / FROM / WHERE / HAVING) tùy yêu cầu.
Phân biệt correlated vs non-correlated subquery, và chọn đúng theo bài.
Dùng EXISTS/NOT EXISTS như reflex thay cho IN/NOT IN.
Áp dụng UNION, INTERSECT, EXCEPT và biết khi nào cần UNION ALL.
Refactor một bài giữa ba paradigm: subquery → self-join → (sẽ có) window function.
Insight
Sau tuần 5, bạn giải được khoảng 50% Medium LeetCode SQL. Tuần 6 (window functions) sẽ unlock nửa còn lại — và làm gọn nhiều bài Medium tuần này từ 15 dòng xuống 5 dòng.
1.2
1.2 Mental model — query lồng query
Subquery là một SELECT đặt bên trong một SELECT khác. Engine xử lý theo nguyên tắc: chạy subquery trước (nếu độc lập), kết quả trở thành đầu vào cho query ngoài.
Ba câu hỏi để phân loại một subquery
Trả về cái gì? Một giá trị (scalar), một dòng (row), hay một bảng (table)?
Đặt ở đâu? SELECT, FROM, WHERE, HAVING?
Có tham chiếu cột của outer không? Nếu có ⇒ correlated, nếu không ⇒ non-correlated.
2. Vị trí của Subquery
2.1
2.1 Scalar subquery — trả 1 giá trị
Định nghĩa
Scalar subquery trả về chính xác 1 dòng × 1 cột. Có thể đặt ở bất cứ nơi nào một biểu thức được phép.
Trong SELECT
-- So tổng đơn của khách với tổng toàn hệ thống
SELECT
customer_id,
SUM(total) AS my_total,
(SELECT SUM(total) FROM orders) AS grand_total
FROM orders
GROUP BY customer_id;
Trong WHERE — so sánh với scalar
-- Đơn lớn hơn trung bình toàn hệ thống
SELECT * FROM orders
WHERE total > (SELECT AVG(total) FROM orders);
Bẫy: subquery trả nhiều dòng
Nếu subquery vô tình trả >1 dòng, query lỗi runtime: "Subquery returned more than 1 value". Khi cần "1 giá trị duy nhất", luôn dùng aggregator (MAX, MIN, AVG) hoặc LIMIT 1 để bảo đảm.
2.2
2.2 Subquery trong WHERE — IN, comparison, ANY/ALL
IN — kiểm tra thuộc tập
-- Khách đã có ít nhất 1 đơn paid
SELECT name FROM customers
WHERE customer_id IN (
SELECT customer_id FROM orders WHERE status = 'paid'
);
ANY và ALL — so sánh với mọi/một dòng
-- Đơn lớn hơn ÍT NHẤT MỘT đơn của khách VIP
SELECT * FROM orders
WHERE total > ANY (
SELECT total FROM orders WHERE customer_id = 1
);
-- Tương đương: total > MIN của tập đó
-- Đơn lớn hơn TẤT CẢ đơn của khách VIP
SELECT * FROM orders
WHERE total > ALL (
SELECT total FROM orders WHERE customer_id = 1
);
-- Tương đương: total > MAX của tập đó
Khuyên
ANY/ALL hiếm khi xuất hiện trong code production. Phần lớn người đọc thấy lạ. Viết lại bằng MIN/MAX dễ đọc và nhanh hơn:
WHERE total > (SELECT MAX(total) FROM orders WHERE customer_id = 1)
Trong interview, có thể đề cập ANY/ALL khi cần "show off" — nhưng default vẫn dùng MIN/MAX.
2.3
2.3 Derived table — subquery trong FROM
Subquery đặt trong FROM được coi như một bảng tạm (derived table / inline view). Phải có alias bắt buộc.
-- Top 5 khách theo doanh thu (yêu cầu 2 bước: aggregate rồi sort/limit)
SELECT s.customer_id, s.revenue, c.name
FROM (
SELECT customer_id, SUM(total) AS revenue
FROM orders
GROUP BY customer_id
) s -- alias bắt buộc
JOIN customers c ON c.customer_id = s.customer_id
ORDER BY s.revenue DESC
LIMIT 5;
Khi nào dùng derived table
Cần aggregate trước rồi join hoặc filter sau.
Cần áp window function rồi filter (window không dùng được trong WHERE).
Cần break query phức tạp thành các bước.
Tuần 7 sẽ học CTE (WITH) — cú pháp đẹp hơn cho cùng mục đích. Tạm dùng derived table cho đến khi học CTE.
2.4
2.4 Tổng kết — vị trí và yêu cầu
Vị trí
Yêu cầu
Use case điển hình
SELECT
Scalar (1×1)
Tính chỉ số "phụ" cùng kết quả chính
FROM
Bảng (n×m), phải có alias
Aggregate-then-join, multi-step query
WHERE — comparison
Scalar (1×1)
So với trung bình, max, min toàn hệ thống
WHERE — IN / NOT IN
1 cột, n dòng
Kiểm tra thuộc/không thuộc tập
WHERE — EXISTS / NOT EXISTS
Kết quả có hay không (n×m bất kỳ)
Tồn tại / không tồn tại — Section 4
HAVING
Scalar
Lọc nhóm theo so sánh với aggregate khác
HAVING với subquery
-- Phòng ban có lương trung bình > trung bình toàn công ty
SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);
3. Correlated vs Non-correlated
3.1
3.1 Non-correlated — subquery độc lập
Định nghĩa
Non-correlated subquery không tham chiếu bất kỳ cột nào của outer query. Có thể chạy độc lập như một query bình thường.
Ví dụ
SELECT name FROM customers
WHERE customer_id IN (
SELECT customer_id FROM orders WHERE total > 1000
-- Không nhắc đến customers ở trong → non-correlated
);
Engine xử lý
Chạy subquery một lần, lưu kết quả tạm.
Mỗi dòng outer kiểm tra xem có thuộc kết quả tạm không.
Hệ quả performance
Non-correlated thường nhanh — engine có thể hash hoặc sort kết quả tạm để lookup O(1) hoặc O(log n) cho mỗi dòng outer. Optimizer thậm chí có thể chuyển thành JOIN tự động.
3.2
3.2 Correlated — subquery phụ thuộc outer
Định nghĩa
Correlated subquery tham chiếu cột của outer query. Không thể chạy độc lập.
Ví dụ — đơn lớn nhất của mỗi khách
SELECT *
FROM orders o
WHERE total = (
SELECT MAX(total)
FROM orders o2
WHERE o2.customer_id = o.customer_id -- tham chiếu o (outer) → correlated
);
Engine xử lý
Với mỗi dòng outer, chạy subquery lại với giá trị cột tham chiếu thay thế.
Nếu outer có 1M dòng, subquery chạy 1M lần (về mặt logic — optimizer có thể tối ưu).
Cảnh báo performance
Correlated subquery tự nhiên là O(n²) nếu engine không tối ưu. Trong dữ liệu lớn, có thể chậm. Phương án thay thế: JOIN với derived table aggregate, hoặc window function (tuần 6).
3.3
3.3 Khi nào dùng cái nào
Dùng non-correlated khi
Filter điều kiện cùng cho mọi dòng outer.
So với một giá trị toàn cục (AVG, MAX, MIN của cả bảng).
Tập "thành viên" ổn định, không phụ thuộc dòng.
Dùng correlated khi
Cần điều kiện thay đổi theo dòng outer.
"Lớn nhất theo nhóm" trước khi học window function.
Existence check (EXISTS/NOT EXISTS — Section 4).
Cùng một bài, cả hai cách
Yêu cầu: tìm employee có salary cao nhất phòng họ.
-- Cách 1: correlated subquery (chạy mỗi dòng outer)
SELECT * FROM employees e
WHERE salary = (
SELECT MAX(salary) FROM employees e2 WHERE e2.dept = e.dept
);
-- Cách 2: derived table non-correlated (chạy 1 lần, JOIN)
SELECT e.* FROM employees e
JOIN (
SELECT dept, MAX(salary) AS max_sal FROM employees GROUP BY dept
) m ON m.dept = e.dept AND m.max_sal = e.salary;
Cách 2 thường nhanh hơn trên dữ liệu lớn — chỉ một lần aggregate. Cách 1 đọc tự nhiên hơn cho người mới.
4. EXISTS sâu
4.1
4.1 EXISTS — chỉ kiểm tra "có hay không"
Định nghĩa
EXISTS (subquery) trả TRUE nếu subquery có ít nhất 1 dòng, FALSE nếu rỗng. Nội dung dòng không quan trọng — vì vậy có thể dùng SELECT 1 hoặc SELECT *, kết quả như nhau.
-- Khách CÓ ít nhất 1 đơn
SELECT name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);
Hầu hết EXISTS là correlated — phải có liên kết với outer (qua predicate trong WHERE) thì mới có nghĩa. EXISTS với non-correlated subquery thường vô nghĩa (luôn TRUE hoặc luôn FALSE).
Tại sao EXISTS nhanh
Engine có thể dừng ngay khi tìm được 1 dòng match — không cần scan hết. Trong khi đó COUNT(*) > 0 sẽ scan toàn bộ. Trong production code "có hay không":
-- KHÔNG hiệu quả
WHERE (SELECT COUNT(*) FROM orders WHERE customer_id = c.id) > 0
-- HIỆU QUẢ
WHERE EXISTS (SELECT 1 FROM orders WHERE customer_id = c.id)
4.2
4.2 EXISTS vs IN — khi NULL xuất hiện
Hai cách viết "thành viên" khác nhau ở semantic NULL:
Aspect
IN
EXISTS
Semantic NULL
3VL — UNKNOWN có thể lan
Boolean thuần — TRUE/FALSE
Subquery có NULL ⇒
Có thể trả unexpected (NOT IN bug!)
Không bị ảnh hưởng
Performance trên large data
Engine có thể materialize trước
Engine có thể dừng sớm khi tìm thấy
Cú pháp đọc
Ngắn, "natural"
Dài hơn, cần predicate liên kết
Cùng kết quả nếu data sạch
-- IN
SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
-- EXISTS
SELECT name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
Hai query trên cho kết quả giống nhau khi orders.customer_id không có NULL.
NOT IN vs NOT EXISTS — recap
Đã học tuần 2 và tuần 4. Nhắc lại lần nữa vì đây là bug đặc thù xuất hiện thường xuyên: NOT IN với subquery có NULL ⇒ tập rỗng. NOT EXISTS không bao giờ có bug này.
Reflex: thấy "không thuộc" trong yêu cầu ⇒ NOT EXISTS, không phải NOT IN.
4.3
4.3 Pattern interview — "có ít nhất một X thỏa Y"
Pattern này xuất hiện rất thường xuyên trong các bài Medium. Một vài dạng phrase:
"Khách hàng đã mua ít nhất một sản phẩm thuộc category X."
"Bài viết có ít nhất một comment trong tháng vừa qua."
"Học sinh đã hoàn thành ít nhất một bài kiểm tra với điểm > 8."
Ví dụ
Yêu cầu: customer đã mua ít nhất một sản phẩm thuộc category 'Electronics'.
SELECT DISTINCT c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.customer_id = c.customer_id
AND p.category = 'Electronics'
);
Anti-pattern: dùng JOIN + DISTINCT
-- Cũng đúng nhưng có thể chậm và cần DISTINCT
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE p.category = 'Electronics';
JOIN nhân lên dòng (1 khách × 5 đơn × 10 items = 50 dòng) rồi DISTINCT để gom lại. EXISTS chỉ kiểm tra "có hay không" → ngắn ngủi và sạch hơn về tư duy.
5. Set Operations
5.1
5.1 UNION vs UNION ALL
Định nghĩa
UNION: ghép kết quả hai query, loại trùng.
UNION ALL: ghép kết quả hai query, giữ trùng.
-- Khách VN hoặc khách AU (không có trùng vì 1 khách chỉ ở 1 nước)
SELECT name FROM customers WHERE country = 'Vietnam'
UNION ALL
SELECT name FROM customers WHERE country = 'Australia';
-- Khách có đơn paid HOẶC đơn pending — dùng UNION nếu sợ trùng
SELECT customer_id FROM orders WHERE status = 'paid'
UNION
SELECT customer_id FROM orders WHERE status = 'pending';
Khuyên — UNION ALL khi có thể
UNION phải sort/hash để loại trùng → tốn CPU và memory.
UNION ALL chỉ ghép — nhanh hơn nhiều.
Khi bạn biết hai tập rời nhau (như VN vs AU), dùng UNION ALL.
Nguyên tắc: default UNION ALL, chỉ UNION khi thực sự cần loại trùng.
5.2
5.2 INTERSECT và EXCEPT
INTERSECT — phần giao
-- Khách CẢ paid VÀ pending
SELECT customer_id FROM orders WHERE status = 'paid'
INTERSECT
SELECT customer_id FROM orders WHERE status = 'pending';
EXCEPT (PostgreSQL/SQL Server) hoặc MINUS (Oracle) — phần hiệu
-- Khách paid NHƯNG chưa có đơn pending
SELECT customer_id FROM orders WHERE status = 'paid'
EXCEPT
SELECT customer_id FROM orders WHERE status = 'pending';
Phương ngữ — MySQL không có INTERSECT, EXCEPT
MySQL chỉ có UNION/UNION ALL. Mô phỏng:
-- INTERSECT mô phỏng
SELECT customer_id FROM orders WHERE status = 'paid'
AND customer_id IN (SELECT customer_id FROM orders WHERE status = 'pending');
-- EXCEPT mô phỏng (dùng NOT EXISTS để an toàn)
SELECT DISTINCT customer_id FROM orders o WHERE status = 'paid'
AND NOT EXISTS (SELECT 1 FROM orders o2
WHERE o2.customer_id = o.customer_id
AND o2.status = 'pending');
LeetCode mặc định MySQL ⇒ luyện kỹ hai pattern này.
5.3
5.3 Quy tắc tương thích schema
Để dùng UNION/INTERSECT/EXCEPT, hai query phải:
Cùng số cột.
Cột tương ứng cùng kiểu (hoặc convertible — engine sẽ implicit cast).
Tên cột lấy từ query đầu tiên — query sau bỏ qua tên.
Ví dụ — tổng hợp dữ liệu từ hai bảng cấu trúc tương tự
-- Bảng customers và bảng leads cùng có (id, name, email)
SELECT id, name, email, 'customer' AS source FROM customers
UNION ALL
SELECT id, name, email, 'lead' AS source FROM leads
ORDER BY name;
Trick "tag source" bằng literal column rất hữu ích cho báo cáo.
Bẫy ORDER BY
ORDER BY chỉ đặt ở cuối toàn bộ, không trong từng SELECT (trừ khi có LIMIT). Tên cột reference phải tồn tại ở query đầu tiên.
6. Bridge — Refactor patterns
6.1
6.1 Cùng một bài, ba paradigm
Yêu cầu: tìm employee có salary cao nhất phòng họ. Schema: employees(id, name, dept, salary).
Cách A — Correlated subquery (intuitive)
SELECT name, dept, salary
FROM employees e
WHERE salary = (
SELECT MAX(salary) FROM employees e2 WHERE e2.dept = e.dept
);
Cách B — Self-join với derived table aggregate (faster on large data)
SELECT e.name, e.dept, e.salary
FROM employees e
JOIN (
SELECT dept, MAX(salary) AS max_sal
FROM employees GROUP BY dept
) m ON m.dept = e.dept AND m.max_sal = e.salary;
Cách C — Window function (tuần 6 preview)
SELECT name, dept, salary FROM (
SELECT name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rk
FROM employees
) t WHERE rk = 1;
Cảm nhận
Cả ba đều đúng. Cách A đọc dễ nhất; Cách B nhanh trên dữ liệu lớn; Cách C ngắn nhất và xử lý ties tốt nhất. Sau khi học window function, bạn sẽ thấy Cách A và B trở thành "code di sản".
6.2
6.2 Khi nào chọn paradigm nào
Paradigm
Mạnh khi
Yếu khi
Correlated subquery
Bài đơn giản, trực giác cao, không cần tối ưu
Dữ liệu lớn (O(n²) tự nhiên), nhiều bước, ties phức tạp
Ranking, running totals, top-N per group, percentile
Cần dialect mới (PostgreSQL/MySQL 8+/SQL Server 2012+); chưa học
Điều quan trọng cho mock interview
Bạn không phải chọn đúng paradigm ngay lần đầu. Nhưng sau khi viết, đề xuất một paradigm thay thế và giải thích trade-off: "Tôi viết bằng correlated subquery. Trên dữ liệu lớn, có thể refactor thành self-join với derived table aggregate để chạy nhanh hơn. Hoặc với window function trong dialect mới, query gọn hơn nhiều — sẽ trông như thế này..."
Đây là tín hiệu của senior candidate.
7. Worked Examples
7.1
7.1 LeetCode 1581 — Customers Without Transactions
Schema
Visits(visit_id INT PK, customer_id INT)
Transactions(transaction_id INT PK, visit_id INT, amount INT)
Yêu cầu
Với mỗi customer, đếm số lượt visit không tạo bất kỳ transaction nào. Trả về (customer_id, count_no_trans).
Phân rã
"Visit không có transaction nào" ⇒ anti-join giữa Visits và Transactions.
Đếm theo customer ⇒ GROUP BY.
Lời giải
SELECT customer_id, COUNT(*) AS count_no_trans
FROM Visits v
WHERE NOT EXISTS (
SELECT 1 FROM Transactions t WHERE t.visit_id = v.visit_id
)
GROUP BY customer_id;
Tín hiệu trong đề
"không có / chưa từng / chưa được" ⇒ phản xạ là NOT EXISTS. Đừng nhảy ngay sang LEFT JOIN — đôi khi đúng nhưng cần thêm WHERE NULL và dễ nhầm. NOT EXISTS thẳng tắp nhất.
7.2
7.2 LeetCode 176 — Second Highest Salary (Medium)
Schema
Employee(id INT PK, salary INT)
Yêu cầu
Trả về salary cao thứ hai. Nếu không có (chỉ có 1 mức salary), trả NULL.
Bẫy
Phải xử lý ties — nếu 3 người cùng salary cao nhất, "thứ hai" là gì?
Phải trả NULL khi không có second.
Lời giải
-- Cách 1: subquery bao ngoài để bắt NULL
SELECT (
SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
) AS SecondHighestSalary;
-- Cách 2: dùng MAX với điều kiện loại đỉnh
SELECT MAX(salary) AS SecondHighestSalary
FROM Employee
WHERE salary < (SELECT MAX(salary) FROM Employee);
Vì sao bao ngoài bằng SELECT?
Subquery LIMIT 1 OFFSET 1 không trả dòng nào nếu chỉ có 1 mức salary. Bao bằng SELECT (...) tận dụng đặc tính scalar subquery — không có dòng ⇒ trả NULL. Trick rất gọn cho yêu cầu "trả NULL nếu không có".
Tuần 6 sẽ có cách viết với DENSE_RANK đẹp hơn cho bài tổng quát N-th highest.
7.3
7.3 LeetCode 184 — Department Highest Salary (Medium)
Schema
Employee(id INT PK, name, salary INT, departmentId INT)
Department(id INT PK, name)
Yêu cầu
Với mỗi phòng, trả về tên phòng + tên employee có salary cao nhất phòng (có thể nhiều người nếu ties).
Lời giải — dùng tuple subquery (rất sạch)
SELECT
d.name AS Department,
e.name AS Employee,
e.salary AS Salary
FROM Employee e
JOIN Department d ON d.id = e.departmentId
WHERE (e.departmentId, e.salary) IN (
SELECT departmentId, MAX(salary)
FROM Employee
GROUP BY departmentId
);
Pattern: tuple subquery
Subquery trả về nhiều cột, outer compare bằng tuple (col1, col2) IN (...). Pattern cực gọn cho "tìm dòng có giá trị max/min theo nhóm". Hoạt động trên MySQL, PostgreSQL. SQL Server không hỗ trợ trực tiếp — phải EXISTS hoặc JOIN với derived table.
8. Knowledge Check
8.Q
Knowledge Check — Tuần 5
Q1: Subquery nào là CORRELATED?
B là correlated — subquery tham chiếu o.customer_id của outer. A và D là non-correlated; C là derived table (cũng non-correlated). Correlated chạy lại với mỗi dòng outer; non-correlated chạy 1 lần.
Q2: Bảng A(x) chứa {1, 2, 3, 4}, B(y) chứa {2, 3, 5}. Query SELECT x FROM A UNION SELECT y FROM B trả gì?
UNION loại trùng, ghép thành tập. UNION ALL mới giữ trùng (cho A). C là kết quả của INTERSECT. D là kết quả của EXCEPT theo chiều cụ thể. Trong production, default UNION ALL nếu biết hai tập rời nhau — UNION đắt hơn vì phải sort/hash.
Q3: Yêu cầu "khách CÓ ít nhất 1 đơn paid và CÓ ít nhất 1 đơn cancelled". Query nào sạch nhất?
A sai logic — lấy cả khách CHỈ có paid hoặc CHỈ có cancelled. B sai logic — không có dòng nào status='paid' AND status='cancelled' đồng thời (status là 1 giá trị/dòng). D đúng nhưng MySQL không có INTERSECT. C đúng — hai EXISTS độc lập, semantic rõ ràng, chạy mọi dialect.
9. Bài tập về nhà
9.1
9.1 Bài tập LeetCode (5 problem, mix)
1581. Customer Who Visited but Did Not Make Any Transactions (Easy) — đã giải tại lớp.
176. Second Highest Salary (Medium) — đã giải tại lớp; viết lại không nhìn.
184. Department Highest Salary (Medium) — đã giải tại lớp; thêm: viết lại bằng correlated subquery (Cách A của Section 6).
1731. The Number of Employees Which Report to Each Employee (Easy) — bonus tuần 4; giải lại bằng EXISTS thay vì JOIN.
1112. Highest Grade For Each Student (Medium) — pattern tuple subquery như LC 184. Bẫy: ties phải lấy course có mã nhỏ nhất.
Bonus — refactor exercise
Lấy LC 184 Department Highest Salary bạn vừa giải, viết bằng cả ba paradigm (correlated, derived table, và preview window function nếu bạn đã đọc RANK()). So sánh:
Số dòng code mỗi cách.
EXPLAIN plan trên local DB.
Cách nào dễ thêm điều kiện "top 3 thay vì top 1" nhất?
9.2
9.2 Đọc thêm và tự kiểm tra
Đọc
SQL Cookbook (Anthony Molinaro) — Chương 14 (Subqueries) toàn bộ.
SQL Antipatterns (Bill Karwin) — Chương "Naive Trees" về self-correlated query trong hierarchies.
PostgreSQL docs — 4.2.11 Subquery Expressions để hiểu sâu ANY, ALL, EXISTS.
Tự kiểm tra (không nhìn slide)
Bạn liệt kê được 4 vị trí subquery có thể đặt và yêu cầu của mỗi vị trí?
Bạn giải thích được vì sao correlated subquery tự nhiên O(n²)?
Bạn viết được tuple subquery cho "max theo nhóm" trong dưới 2 phút?
Bạn nói được khi nào dùng UNION vs UNION ALL?
Bạn refactor được correlated subquery thành derived table aggregate cho bài "top 1 per group"?
Tuần sau — Window Functions (vũ khí giải Hard)
Tuần 6: OVER, PARTITION BY, ROW_NUMBER, RANK, DENSE_RANK, LAG/LEAD, running totals, frame clauses. Đây là tuần "click moment" — bạn sẽ thấy nhiều bài Medium tuần này thu nhỏ thành 5 dòng code, và Hard LeetCode bắt đầu giải được.