🔢 Algorithms✍️ Khoa📅 19/04/2026☕ 5 phút đọc

Algorithms: Sliding Window

1. Khi nào dùng Sliding Window?

Dấu hiệu nhận biết:

  • Bài yêu cầu tìm subarray / substring liên tiếp (contiguous)
  • Câu hỏi dạng "max/min/longest/shortest" trong một khoảng
  • Có constraint như "at most K distinct", "sum equals K"

Hai loại chính:

  • Fixed window: kích thước cố định k
  • Variable window: kích thước thay đổi, expand/shrink theo điều kiện

2. Fixed Window Template

flowchart LR
  A["[1, 3, -1, -3, 5, 3, 6, 7], k=3"] --> W1["Window #1: [1, 3, -1]"]
  W1 --> W2["Window #2: [3, -1, -3]"]
  W2 --> W3["Window #3: [-1, -3, 5]"]
[1, 3, -1, -3, 5, 3, 6, 7]  window size = 3
 └──────┘
    └──────┘
       └──────┘
func fixedWindow(nums []int, k int) int {
    // Bước 1: build window đầu tiên
    windowSum := 0
    for i := 0; i < k; i++ {
        windowSum += nums[i]
    }

    maxSum := windowSum

    // Bước 2: slide — thêm phải, bỏ trái
    for i := k; i < len(nums); i++ {
        windowSum += nums[i]       // thêm element mới vào phải
        windowSum -= nums[i-k]     // bỏ element cũ ở trái
        if windowSum > maxSum {
            maxSum = windowSum
        }
    }
    return maxSum
}

Complexity: O(n) time, O(1) space


3. Variable Window Template

Expand right, shrink left khi vi phạm điều kiện.

func variableWindow(s string) int {
    left := 0
    best := 0
    // state tracking tuỳ bài (map, counter, sum...)
    freq := make(map[byte]int)

    for right := 0; right < len(s); right++ {
        // 1. Expand: thêm s[right] vào window
        freq[s[right]]++

        // 2. Shrink: thu nhỏ khi vi phạm điều kiện
        for isInvalid(freq) {      // điều kiện tuỳ bài
            freq[s[left]]--
            if freq[s[left]] == 0 {
                delete(freq, s[left])
            }
            left++
        }

        // 3. Update answer
        windowLen := right - left + 1
        if windowLen > best {
            best = windowLen
        }
    }
    return best
}

4. Bài kinh điển

Longest Substring Without Repeating Characters

flowchart LR
  S["abcabcbb"] --> A1["window='abc', len=3"]
  A1 --> A2["window='bca', len=3"]
  A2 --> A3["window='cab', len=3"]
  A3 --> A4["window='abc' rồi shrink khi gặp 'a' lặp"]
"abcabcbb"
 a b c a b c b b
 └─────┘         window = "abc", len=3
   └─────┘       window = "bca", len=3
     └─────┘     window = "cab", len=3
       └───┘     window = "abc" → shrink khi thấy 'a' lại
func lengthOfLongestSubstring(s string) int {
    left := 0
    best := 0
    lastSeen := make(map[byte]int) // char → last index

    for right := 0; right < len(s); right++ {
        c := s[right]
        // Nếu char đã thấy VÀ nằm trong window hiện tại
        if idx, ok := lastSeen[c]; ok && idx >= left {
            left = idx + 1 // nhảy left qua vị trí sau lần thấy trước
        }
        lastSeen[c] = right
        if right-left+1 > best {
            best = right - left + 1
        }
    }
    return best
}

Minimum Window Substring

Tìm substring ngắn nhất của s chứa tất cả ký tự của t.

func minWindow(s, t string) string {
    need := make(map[byte]int)
    for i := 0; i < len(t); i++ {
        need[t[i]]++
    }

    have, total := 0, len(need)
    left := 0
    minLen, minLeft := math.MaxInt32, 0
    window := make(map[byte]int)

    for right := 0; right < len(s); right++ {
        c := s[right]
        window[c]++
        // Check nếu char này đã đủ số lượng cần
        if need[c] > 0 && window[c] == need[c] {
            have++
        }

        // Shrink khi đã có đủ
        for have == total {
            if right-left+1 < minLen {
                minLen = right - left + 1
                minLeft = left
            }
            lc := s[left]
            window[lc]--
            if need[lc] > 0 && window[lc] < need[lc] {
                have--
            }
            left++
        }
    }

    if minLen == math.MaxInt32 {
        return ""
    }
    return s[minLeft : minLeft+minLen]
}

Max Sum Subarray of Size K

func maxSumSubarray(nums []int, k int) int {
    sum := 0
    for i := 0; i < k; i++ {
        sum += nums[i]
    }
    max := sum
    for i := k; i < len(nums); i++ {
        sum += nums[i] - nums[i-k]
        if sum > max {
            max = sum
        }
    }
    return max
}

Longest Subarray with At Most K Distinct Characters

func longestSubarrayKDistinct(s string, k int) int {
    freq := make(map[byte]int)
    left, best := 0, 0
    for right := 0; right < len(s); right++ {
        freq[s[right]]++
        for len(freq) > k {
            freq[s[left]]--
            if freq[s[left]] == 0 {
                delete(freq, s[left])
            }
            left++
        }
        if right-left+1 > best {
            best = right - left + 1
        }
    }
    return best
}

5. Các variant nâng cao

Sliding Window Maximum (Deque)

Tìm max trong mỗi window size k — dùng deque monotone.

func maxSlidingWindow(nums []int, k int) []int {
    dq := []int{} // lưu index, giảm dần theo value
    result := []int{}

    for i := 0; i < len(nums); i++ {
        // Bỏ các index đã ra ngoài window
        for len(dq) > 0 && dq[0] < i-k+1 {
            dq = dq[1:]
        }
        // Bỏ các index nhỏ hơn nums[i] từ phía sau
        for len(dq) > 0 && nums[dq[len(dq)-1]] < nums[i] {
            dq = dq[:len(dq)-1]
        }
        dq = append(dq, i)

        if i >= k-1 {
            result = append(result, nums[dq[0]])
        }
    }
    return result
}

Complexity: O(n) — mỗi element vào/ra deque đúng 1 lần.


6. Common mistakes

Lỗi Fix
Quên reset state khi left tăng Luôn decrement/delete khi shrink
Window size tính sai right - left + 1 (inclusive)
Dùng fixed template cho variable bài Nhận ra điều kiện shrink trước
Off-by-one trong fixed window init Loop i < k, không phải i <= k

💡 Interview: Câu hỏi phân biệt sliding window với two pointers là tính liên tiếp. Sliding window luôn yêu cầu subarray/substring liên tiếp. Nếu bài không cần liên tiếp, có thể là two pointers hoặc DP.