Sliding Window — Kỹ nghệ Cửa sổ trượt
Khi đối mặt với mảng lộn xộn (Unsorted Array) mà Two Pointers bó tay, các Junior Developer thường nhanh chóng đầu hàng và quay lại với công thức O(n²): Lồng 2 vòng lặp for vào nhau.
Đặc biệt, lỗi này cực k ỳ phổ biến trong những bài toán yêu cầu tìm kiếm một đoạn mảng con liên tiếp (Sub-array hoặc Sub-string).
Làm thế nào để tính tổng, tính trung bình, hay đếm ký tự của một đoạn mảng con chỉ bằng một lần quét duy nhất? Chào mừng bạn đến với Mẫu thiết kế tinh tế nhất dành cho Mảng lộn xộn: Sliding Window (Cửa sổ trượt).
📋 Agenda
Thời gian đọc ước tính: ~7 phút
Sau bài này, bạn sẽ:
- ✅ Hiểu được sự lãng phí của việc tính toán lại từ đầu (Recalculation).
- ✅ Giải thích được cơ chế "Trượt" và "Cộng thêm - Trừ đi" của Cửa sổ.
- ✅ Tự tay áp dụng Sliding Window để giải quyết bài toán tính Tổng mảng con lớn nhất bằng TypeScript.
- ✅ Nhận diện được các "Keyword" từ phỏng vấn viên để biết lúc nào dùng Pattern này.
Yêu cầu đầu vào:
- 🔹 Có kiến thức về Time Complexity (Big O).
❓ Tại sao lại phải tính đi tính lại?
Bài toán (Problem Statement):
Viết hàm maxSubarraySum nhận vào một mảng các số nguyên (Lộn xộn) và một con số n. Tính Tổng lớn nhất của n phần tử nằm liên tiếp nhau.
Ví dụ: maxSubarraySum([2, 6, 9, 2, 1, 8, 5, 6, 3], 3)
Yêu cầu: Tìm 3 ngày liên tiếp có doanh thu cao nhất. Trả về Tổng số đó. (Nhìn bằng mắt thường ta thấy [6, 9, 2] là 17, hoặc [8, 5, 6] là 19. Vậy đáp án là 19).
Cách giải ngây ngô (Naive Solution - O(n²)):
Bạn tính tổng 3 ngày đầu tiên: 2 + 6 + 9 = 17.
Sau đó bạn nhích lên 1 ngày, tính tổng 3 ngày tiếp theo: 6 + 9 + 2 = 17.
Lại nhích lên 1 ngày, tính tiếp: 9 + 2 + 1 = 12.
...
Bạn có thấy sự vô lý không?
Khi tính 6 + 9 + 2, máy tính phải thực hiện lại phép cộng 6 + 9. Trong khi ở phép tính trước, nó đã biết 6 + 9 là gì rồi cơ mà! Nếu n = 100, máy tính phải lặp lại 99 phép cộng vô ích mỗi lần nó nhích lên 1 bước.
📖 Trực quan hóa "Cửa sổ trượt"
Giải pháp (Solution):
Sliding Window. Chúng ta tạo ra một "Cửa sổ" (Window) có kích thước là n.
- Lúc đầu, cửa sổ bao trọn
nphần tử đầu tiên. Chúng ta tính Tổng của cửa sổ này ĐÚNG 1 LẦN. - Khi cửa sổ trượt sang phải 1 bước, thay vì tính lại từ đầu, chúng ta chỉ việc CỘNG THÊM phần tử mới vừa lọt vào cửa sổ, và TRỪ ĐI phần tử cũ vừa bị rơi ra khỏi cửa sổ.
Bằng cách này, dù kích thước cửa sổ n là 3 hay 3 triệu, mỗi lần trượt chúng ta chỉ tốn đúng 2 phép tính (1 phép Trừ, 1 phép Cộng). Tổng thời gian từ O(n * m) rút xuống cực hạn chỉ còn O(N) (Chạy đúng 1 vòng lặp for dọc mảng).
🔨 Implement Sliding Window bằng TypeScript
// filename: sliding-window.ts
function maxSubarraySum(arr: number[], num: number): number | null {
// Edge case: Mảng ngắn hơn cả độ rộng cửa sổ
if (arr.length < num) return null;
let maxSum = 0;
let tempSum = 0; // Biến lưu Tổng tạm thời của Cửa sổ hiện tại
// 1. Tạo Cửa sổ đầu tiên: Cộng tổng của `num` phần tử đầu
for (let i = 0; i < num; i++) {
maxSum += arr[i];
}
// Gán tạm thời bằng max
tempSum = maxSum;
// 2. Trượt cửa sổ: Bắt đầu từ vị trí `num` (Ngay sát vách phải của cửa sổ cũ)
for (let i = num; i < arr.length; i++) {
// 💡 LÕI THUẬT TOÁN:
// Tổng mới = Tổng cũ - Phần tử bị rớt ra đuôi + Phần tử mới lọt vào đầu
// Phần tử rớt đuôi nằm ở vị trí (i - num)
tempSum = tempSum - arr[i - num] + arr[i];
// Cập nhật lại kỷ lục Max nếu tổng mới to hơn
maxSum = Math.max(maxSum, tempSum);
}
return maxSum;
}
// Chạy thử
console.log(maxSubarraySum([2, 6, 9, 2, 1, 8, 5, 6, 3], 3)); // Output: 19