Notes & software courses · Free to learn
Aph's Blog
On this page

Sliding Window — พื้นฐาน & แนวคิด

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

เทคนิคเลื่อนช่วงต่อเนื่อง (subarray / substring) ด้วย left-right แล้วอัปเดตค่าแบบทบเดิม — จาก brute force O(n²) เหลือ O(n)

Sliding Window (หน้าต่างเลื่อน) คือเทคนิคหาคำตอบของ "ช่วงต่อเนื่อง" ใน array หรือ string เช่น ผลรวมมากสุดของช่วงยาว k, substring ยาวสุดที่ไม่มีตัวซ้ำ, หรือช่วงที่มี 0 ไม่เกิน k ตัว หลักการคือเก็บ window เดียวไว้แล้วเลื่อนขอบทีละก้าว อัปเดตแค่ตัวที่เข้ากับตัวที่ออก แทนที่จะคำนวณทั้งช่วงใหม่ทุกครั้ง

ขอบ window คุมด้วยตัวแปรสองตัว left กับ right — คล้าย two pointers แต่เดินไปทางเดียวกันเสมอ และสิ่งที่สนใจคือทุกอย่างระหว่างสองขอบ ไม่ใช่แค่ค่าปลายสองข้าง

ทำไมต้องมีท่านี้ — ดู brute force ก่อน

ตัวอย่าง: หาผลรวมมากสุดของช่วงยาว k = 3 ใน nums = [2, 1, 5, 1, 3, 2] — brute force คือลองทุกช่วงยาว 3 แล้วบวกใหม่ทุกครั้ง

วิธีที่ 1: brute force — บวกใหม่ทุกช่วงpython
nums = [2, 1, 5, 1, 3, 2]
k = 3
best = float("-inf")
adds = 0                                   # นับว่าบวกเลขกี่ครั้ง

for i in range(len(nums) - k + 1):         # ทุกจุดเริ่มต้นที่เป็นไปได้
    total = 0
    for j in range(i, i + k):              # บวก k ตัวใหม่ทุกครั้ง
        total += nums[j]
        adds += 1
    best = max(best, total)
    print(f"ช่วง [{i}:{i+k}] = {nums[i:i+k]} รวม = {total}")

print("best =", best, "| บวกไปทั้งหมด", adds, "ครั้ง")
Output
ช่วง [0:3] = [2, 1, 5] รวม = 8
ช่วง [1:4] = [1, 5, 1] รวม = 7
ช่วง [2:5] = [5, 1, 3] รวม = 9
ช่วง [3:6] = [1, 3, 2] รวม = 6
best = 9 | บวกไปทั้งหมด 12 ครั้ง

ช่วงถัดไปซ้อนกับช่วงก่อนเกือบหมด ต่างแค่ตัวซ้ายหลุด ตัวขวาเข้าใหม่ แต่ brute force บวกทั้งก้อนใหม่ทุกครั้ง เมื่อ n และ k ใหญ่ (เช่น n = 10⁵, k = 10⁴) จะกลายเป็น O(nk) ที่รันไม่ทัน

วิธีที่ 2: sliding window — บวกเข้า ลบออกpython
nums = [2, 1, 5, 1, 3, 2]
k = 3
window = sum(nums[:k])                     # สร้างหน้าต่างแรกครั้งเดียว
best = window
ops = k                                    # นับการบวก/ลบ

print(f"เริ่ม   window = {nums[:k]} รวม = {window}")

for i in range(k, len(nums)):
    entering = nums[i]                     # ตัวใหม่เข้าทางขวา
    leaving = nums[i - k]                  # ตัวเก่าหลุดออกทางซ้าย
    window += entering - leaving
    ops += 2
    best = max(best, window)
    print(f"เลื่อน  เข้า {entering}, ออก {leaving} -> รวม = {window}")

print("best =", best, "| บวก/ลบไปทั้งหมด", ops, "ครั้ง")
Output
เริ่ม   window = [2, 1, 5] รวม = 8
เลื่อน  เข้า 1, ออก 2 -> รวม = 7
เลื่อน  เข้า 3, ออก 1 -> รวม = 9
เลื่อน  เข้า 2, ออก 5 -> รวม = 6
best = 9 | บวก/ลบไปทั้งหมด 9 ครั้ง

ได้คำตอบเดียวกัน (best = 9 จาก [5, 1, 3]) แต่งานลดลง และสำคัญกว่านั้นคืองานโตตาม n ไม่ใช่ n×k — นี่คือเหตุผลที่เหลือ O(n)

Fixed-size sliding window: เลื่อน window ยาว k โดยบวกตัวเข้าและลบตัวออก
Fixed-size window: ความยาวกคงที่ ทุกครั้งที่เลื่อนแค่ drop ตัวซ้าย + add ตัวขวา ไม่ต้องบวกใหม่ทั้งก้อน
หัวใจของความเร็ว

แต่ละ index เข้า window ผ่าน right ครั้งเดียว และออกผ่าน left ไม่เกินครั้งเดียว รวมไม่เกิน 2n ก้าว → O(n) เสมอ ถ้า left ถอยหลังหรือคำนวณทั้งช่วงใหม่ทุกครั้ง แปลว่ายังไม่ใช่ sliding window จริง

สองแบบที่ต้องแยกให้ออก: Fixed vs Variable

ทุกโจทย์ sliding window ตกอยู่ในหนึ่งในสองแบบ การตั้งชื่อให้ถูกตั้งแต่แรกคือครึ่งหนึ่งของการแก้

แบบที่ 1 — Fixed-size (ขนาดคงที่)

โจทย์บอกความยาว k มาชัดเจน window ไม่เปลี่ยนขนาด แค่เลื่อนไปทีละ 1: บวกตัวเข้าขวา ลบตัวออกซ้าย ใช้เมื่อเห็น "ของช่วงยาว k", "substring of length k", "ทุก window ขนาด k"

template fixed-sizepython
def fixed_window(arr, k):
            window = sum(arr[:k])          # สร้างหน้าต่างแรก
            best = window
            for right in range(k, len(arr)):
                window += arr[right]       # ตัวใหม่เข้า
                window -= arr[right - k]   # ตัวเก่าออก
                best = max(best, window)
            return best

แบบที่ 2 — Variable-size (ขนาดยืดหยุ่น)

ไม่มีความยาวตายตัว: ขยายขวา (expand) รับของเข้าเรื่อย ๆ พอ window invalid ค่อยหดซ้าย (shrink) จนถูกต้องแล้วบันทึกคำตอบ ใช้เมื่อเห็น "ยาวที่สุด / สั้นที่สุด ที่ยัง…", "at most k", "ไม่มีตัวซ้ำ"

template variable-sizepython
def variable_window(arr):
            left = 0
            state = 0                      # ผลรวม / จำนวน / อะไรก็ตามที่ track
            best = 0
            for right in range(len(arr)):
                # 1) Expand: รับ arr[right] เข้าหน้าต่าง
                state += arr[right]

                # 2) Shrink: หดซ้ายจนกว่าหน้าต่างจะถูกต้อง
                while is_invalid(state):   # <- แทนที่ด้วยเงื่อนไขของโจทย์
                    state -= arr[left]
                    left += 1

                # 3) Update: ตรงนี้ [left..right] valid แล้ว
                best = max(best, right - left + 1)
            return best
Variable-size sliding window: expand right, shrink left while invalid, update best
Variable-size window: วนซ้ำ Expand → Shrink while invalid → Update best ทุกครั้งที่ขยับ right
จังหวะอัปเดตคำตอบสำคัญ

"ยาวที่สุด": อัปเดต best หลัง shrink (window valid แล้ว) · "สั้นที่สุด": อัปเดต best ก่อนหด ตอนยัง valid อยู่ — สลับจังหวะนี้คำตอบจะเพี้ยนแบบเงียบ ๆ

สัญญาณในโจทย์ — รู้ได้ยังไงว่าต้องใช้ sliding window

ต้องมีสองอย่างพร้อมกัน: (1) คำตอบเป็นช่วงต่อเนื่อง (contiguous — ติดกัน ไม่กระโดดข้ามแบบ subsequence) และ (2) ต้องการค่าดีที่สุดหรือนับภายใต้เงื่อนไข (ยาวสุด / สั้นสุด / ผลรวมมากสุด / ไม่เกิน k ตัว)

สัญญาณในโจทย์ใช้แบบไหนstate ที่ track
ช่วงยาว k ชัดเจน ("of length k", "size k")Fixedผลรวม / จำนวนสระ / freq map ของขนาด k
ยาวที่สุดที่ยังตรงเงื่อนไขVariable (maximize)count / set / freq map
สั้นที่สุดที่ยังตรงเงื่อนไขVariable (minimize)ผลรวม / freq ที่ยังขาด
at most k … (ไม่เกิน k ตัว / k ชนิด)Variableตัวนับของสิ่งที่จำกัด
ไม่มีตัวซ้ำใน substringVariableset หรือ last-seen index
สัญญาณนี้ไม่ใช่ sliding window — ใช้อะไรแทน
subsequence (ไม่ต้องติดกัน) เช่น Is SubsequenceTwo Pointers แบบ same-direction
ผลรวมช่วงใดก็ได้ที่มีเลขติดลบ (max subarray ทั่วไป)Kadane / DP — ไม่ใช่ window
subarray sum = K ที่มีเลขติดลบได้Prefix Sum + Hash Map
หาคู่ค่าสองตัว ไม่สนของระหว่างกลางTwo Pointers / Hash Map

ลองไล่ variable window ของจริง

ตัวอย่างคลาสสิก: หา substring ยาวสุดที่ไม่มีตัวซ้ำ (LC3) — invariant คือ "ห้ามมีตัวซ้ำใน window" พอตัวซ้ำเข้าทางขวา ต้องหดซ้ายจนตัวเก่าหลุดออก

variable window — longest substring without repeatingpython
s = "abcabcbb"
seen = set()
left = 0
best = 0

for right, ch in enumerate(s):
    while ch in seen:                      # ซ้ำแล้ว → หดซ้าย
        seen.remove(s[left])
        left += 1
    seen.add(ch)                           # รับตัวใหม่เข้า
    best = max(best, right - left + 1)
    print(f"right={right} ch={ch!r} window={s[left:right+1]!r} best={best}")

print("คำตอบ:", best)
Output
right=0 ch='a' window='a' best=1
right=1 ch='b' window='ab' best=2
right=2 ch='c' window='abc' best=3
right=3 ch='a' window='bca' best=3
right=4 ch='b' window='cab' best=3
right=5 ch='c' window='abc' best=3
right=6 ch='b' window='cb' best=3
right=7 ch='b' window='b' best=3
คำตอบ: 3

ตอน right=3 ตัว 'a' ซ้ำ → while เอา 'a' ตัวแรกออก (left ไปที่ 1) window เหลือ "bca" ยาว 3 เท่าเดิม จากนั้นทุกครั้งที่ซ้ำ left ก็ไล่ตาม ความยาวดีสุดค้างที่ 3 ("abc")

Window state — อะไรที่ต้องถือไว้ในมือ

Window ไม่ใช่แค่ left กับ right — ต้องมี state ที่อัปเดตได้เร็วตอนของเข้า/ออก เลือกแบบที่ง่ายสุดที่บอกได้ว่า window ยัง valid ไหมใน O(1)

  • ตัวเลขเดียว: ผลรวม, จำนวน 0, จำนวนสระ — ใช้กับ fixed window หรือเงื่อนไขตัวเลข
  • set: จำว่ามีตัวไหนอยู่ใน window — ใช้กับ "ห้ามซ้ำ"
  • freq map / Counter: นับความถี่ต่อตัวหรือต่อค่า — ใช้กับ "at most k distinct", anagram, ครอบคลุมตัวอักษรของ t
  • deque: เก็บตัวแข่ง max/min ใน window — ใช้ตอนถามค่ามากสุด/น้อยสุดในทุก window ยาว k (นอก LC75 หมวดนี้ แต่เจอบ่อยตอนสัมภาษณ์)
บั๊กอันดับ 1 ของ state

ขยับ left/right แล้วลืมอัปเดต state ให้ตรงกัน — เช่น left += 1 แต่ลืมลบ count ของตัวที่หลุดออก window ในโค้ดกับ state จริงจะไม่ตรงกัน คำตอบเพี้ยนโดยไม่มี error

ต่างจาก Two Pointers ตรงไหน

Two PointersSliding Window
สนใจอะไรค่าที่ปลายสองข้าง (หรือ slow/fast)ทั้งช่วงระหว่าง left..right
ทิศทางเข้าหากัน / คนละความเร็ว / คนละแถวทั้งคู่ไปทางเดียวกัน (ซ้าย→ขวา)
คำถามหลักมีคู่ไหม / กรอง in-place / รวมสองแถวช่วงต่อเนื่องที่ดีที่สุดภายใต้เงื่อนไข
ต้องเรียงก่อนไหมแบบ opposite ends ต้องเรียงไม่ต้อง — ลำดับเดิมมีความหมาย

พูดอย่างสั้น: sliding window คือ two pointers แบบ same-direction ที่ถือ "ของทั้งช่วง" เป็น state ไว้ด้วย ถ้าโจทย์พูดถึง contiguous / subarray / substring + optimize → นึก window ก่อน

กับดักที่เจอบ่อย

  • ความยาว window ผิด: ช่วง inclusive [left..right] ยาว = right - left + 1 ไม่ใช่ right - left
  • เริ่ม best = 0 ทั้งที่คำตอบอาจติดลบ — initialize ด้วยหน้าต่างแรก หรือ float("-inf")
  • fixed window ลืมสร้างหน้าต่างแรกก่อน loop หรือลบตัวออกผิดตำแหน่ง (ต้อง right - k ไม่ใช่ left)
  • variable window อัปเดต best ตอนยัง invalid — ต้อง shrink ให้จบก่อน
  • เลขติดลบ + เงื่อนไขผลรวม: อย่าสมมติว่าขยายแล้วผลรวมต้องโต ถ้าเป็น max subarray ทั่วไปให้ไป Kadane
  • left ถอยหลังได้ พัง amortized O(n) ทันที — left เดินหน้าอย่างเดียว

สี่ข้อในหมวดนี้ เรียงยังไง

ข้อโจทย์แบบstate
14LC643 Maximum Average Subarray IFixed kผลรวม → หาร k ตอนท้าย
15LC1456 Max Vowels in Substring of Length KFixed kนับสระใน window
16LC1004 Max Consecutive Ones IIIVariableนับ 0 (พลิกได้ ≤ k)
17LC1493 Longest Subarray of 1's After Deleting OneVariableนับ 0 (ลบได้ 1 ตัว = k=1)

สองข้อแรกซ้อม fixed ให้มือขึ้น สองข้อหลังเป็น variable แบบ "at most k ตัวที่ผิดกติกา" — ข้อ 17 คือข้อ 16 ที่ k ล็อกเป็น 1 ถ้าเข้าใจข้อ 16 แล้ว ข้อ 17 จะรู้เรื่องทันที

พร้อมแล้วไปต่อ

เริ่มจากข้อ 14 Maximum Average Subarray I — fixed window ตัวแรก กดถัดไปได้เลย