0Pricing
DSA Interview Prep · บทเรียน

หน้าต่างเลื่อนสำหรับสตริงย่อย

สร้างหน้าต่างเลื่อนขนาดแปรผันเพื่อค้นหาสตริงย่อยที่ยาวที่สุดโดยไม่มีอักขระซ้ำ และหน้าต่างที่สั้นที่สุดซึ่งมีอักขระเป้าหมายครบทั้งหมด

หน้าต่างเลื่อนสำหรับสตริงย่อย เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

แนวคิดหน้าต่างเลื่อน

หน้าต่างเลื่อนจะรักษาอาร์เรย์ย่อย (หรือสตริงย่อย) ที่อยู่ระหว่างตัวชี้ซ้ายและตัวชี้ขวา แทนที่จะคำนวณคุณสมบัติของอาร์เรย์ย่อยที่เป็นไปได้ทุกชุดใหม่ตั้งแต่ต้นด้วยเวลา O(n²) หน้าต่างจะขยายไปทางขวาโดยเพิ่มสมาชิกทีละหนึ่งตัว และหดเข้าทางซ้ายโดยนำสมาชิกออกทีละหนึ่งตัว พร้อมรักษาสถานะสะสมด้วยเวลา O(1) ต่อขั้นตอน จึงได้อัลกอริทึมที่ใช้เวลา O(n) หน้าต่างนี้เรียกว่า “เลื่อน” เพราะเคลื่อนไปข้างหน้าผ่านอาร์เรย์โดยไม่ย้อนกลับ

# Fixed-size window sum: O(n) after O(k) setup
def max_sum_window(nums, k):
    window_sum = sum(nums[:k])  # initial window
    best = window_sum
    for i in range(k, len(nums)):
        window_sum += nums[i]       # add new right
        window_sum -= nums[i - k]   # remove old left
        best = max(best, window_sum)
    return best

print(max_sum_window([2,1,5,1,3,2], 3))  # 9  ([5,1,3])

ขนาดหน้าต่างคงที่เทียบกับขนาดหน้าต่างแปรผัน

หน้าต่างเลื่อนมีอยู่สองรูปแบบ ในหน้าต่างขนาดคงที่ ตัวชี้ทั้งสองจะเลื่อนไปด้วยจังหวะเดียวกัน และหน้าต่างจะมีสมาชิก 정확 k ตัวเสมอ ในหน้าต่างขนาดแปรผัน ตัวชี้ขวาจะขยายอย่างเต็มที่ ส่วนตัวชี้ซ้ายจะหดเข้าก็ต่อเมื่อหน้าต่างไม่เป็นไปตามเงื่อนไข หน้าต่างขนาดแปรผันใช้แก้ปัญหาอย่าง “สตริงย่อยที่ยาวที่สุดโดยไม่มีอักขระซ้ำ” ซึ่งไม่ทราบขนาดหน้าต่างที่เหมาะสมล่วงหน้า

# Variable window: longest substring with at most k distinct chars
def longest_k_distinct(s, k):
    from collections import defaultdict
    freq = defaultdict(int)
    left = 0
    best = 0
    for right in range(len(s)):
        freq[s[right]] += 1
        while len(freq) > k:    # window invalid: shrink
            freq[s[left]] -= 1
            if freq[s[left]] == 0:
                del freq[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

print(longest_k_distinct('eceba', 2))   # 3  ('ece')
print(longest_k_distinct('aa', 1))      # 2

สตริงย่อยที่ยาวที่สุดโดยไม่มีอักขระซ้ำ

นี่คือปัญหาหน้าต่างเลื่อนแบบแปรผันที่มีชื่อเสียงที่สุด ให้ใช้เซตเพื่อติดตามอักขระในหน้าต่างปัจจุบัน ขยายไปทางขวา และเมื่อพบอักขระซ้ำ ให้หดจากทางซ้ายจนกว่าอักขระซ้ำนั้นจะถูกนำออก รุ่นที่เร็วกว่าจะใช้ตารางแฮชซึ่งเก็บดัชนีล่าสุดของอักขระแต่ละตัว ทำให้ตัวชี้ซ้ายกระโดดข้ามอักขระซ้ำได้ในขั้นตอนเดียว แทนที่จะเลื่อนไปทีละน้อย

def length_of_longest_substring(s):
    char_idx = {}  # char -> last seen index
    left = 0
    best = 0
    for right, c in enumerate(s):
        if c in char_idx and char_idx[c] >= left:
            left = char_idx[c] + 1  # jump past duplicate
        char_idx[c] = right
        best = max(best, right - left + 1)
    return best

print(length_of_longest_substring('abcabcbb'))  # 3 ('abc')
print(length_of_longest_substring('bbbbb'))     # 1
print(length_of_longest_substring('pwwkew'))    # 3 ('wke')

สตริงย่อยของหน้าต่างที่สั้นที่สุด

เมื่อกำหนดสตริง s และ t ให้ค้นหาหน้าต่างที่เล็กที่สุดใน s ซึ่งมีอักขระทั้งหมดของ t ใช้แผนผังความถี่สองชุด: need (อักขระที่ต้องมี) และ have (อักขระในหน้าต่างปัจจุบันที่ตรงตามข้อกำหนด) ติดตามจำนวนอักขระที่ไม่ซ้ำกันใน t ซึ่งตรงตามข้อกำหนดแล้ว (ตัวนับ formed) ขยายไปทางขวาเพื่อเพิ่มอักขระ และเมื่อครอบคลุม t ทั้งหมดแล้ว ให้หดจากทางซ้ายเพื่อลดขนาดหน้าต่าง เวลาในการทำงานคือ O(|s| + |t|)

from collections import Counter

def min_window(s, t):
    if not t or not s: return ''
    need = Counter(t)
    have = {}
    formed = 0
    required = len(need)
    left = 0
    best = float('inf'), 0, 0
    for right, c in enumerate(s):
        have[c] = have.get(c, 0) + 1
        if c in need and have[c] == need[c]:
            formed += 1
        while formed == required:
            if right - left + 1 < best[0]:
                best = right - left + 1, left, right
            have[s[left]] -= 1
            if s[left] in need and have[s[left]] < need[s[left]]:
                formed -= 1
            left += 1
    return s[best[1]:best[2]+1] if best[0] != float('inf') else ''

print(min_window('ADOBECODEBANC', 'ABC'))  # 'BANC'

แม่แบบหน้าต่างเลื่อน

ปัญหาหน้าต่างเลื่อนแบบแปรผันส่วนใหญ่ใช้แม่แบบเดียวกัน: ขยายไปทางขวาเพื่อเพิ่มอักขระใหม่ อัปเดตสถานะของหน้าต่าง ตรวจสอบความถูกต้อง และหากไม่ถูกต้อง ให้หดจากทางซ้ายจนกว่าจะกลับมาถูกต้องอีกครั้ง ข้อสังเกตสำคัญคือตัวชี้ซ้ายจะเคลื่อนไปข้างหน้าเท่านั้น — ไม่เคยย้อนกลับ — ดังนั้นงานทั้งหมดจากการหดหน้าต่างทุกครั้งจึงใช้เวลา O(n) หน้าต่างจะพบสมาชิกแต่ละตัวไม่เกินสองครั้ง (ครั้งหนึ่งตอนเพิ่ม และอีกครั้งตอนนำออก)

def sliding_window_template(s, condition_check, update_state, remove_state):
    """
    Generic sliding window skeleton.
    Adapt condition_check, update_state, remove_state per problem.
    """
    left = 0
    state = {}  # or whatever state you need
    best = 0
    for right in range(len(s)):
        update_state(state, s[right])      # expand window
        while not condition_check(state):  # window invalid
            remove_state(state, s[left])   # shrink window
            left += 1
        best = max(best, right - left + 1)
    return best

การเรียงสับเปลี่ยนในสตริง

ตรวจสอบว่ามีการเรียงสับเปลี่ยนใด ๆ ของรูปแบบ p อยู่ในสตริงย่อยของ s หรือไม่ การตรวจสอบการเรียงสับเปลี่ยนเทียบเท่ากับการตรวจสอบว่าหน้าต่างมีความถี่อักขระเหมือนกับ p รักษาหน้าต่างเลื่อนที่มีอักขระจำนวนเท่ากับ len(p) พอดี แล้วเปรียบเทียบจำนวนนับความถี่ การเปรียบเทียบโครงสร้างนับความถี่ทั้งหมดในแต่ละขั้นใช้เวลา O(26) (เป็นค่าคงที่สำหรับตัวอักษรอังกฤษพิมพ์เล็ก) จึงมีเวลารวม O(n × 26) = O(n)

from collections import Counter

def check_inclusion(p, s):
    if len(p) > len(s): return False
    need  = Counter(p)
    window = Counter(s[:len(p)])
    if need == window: return True
    for right in range(len(p), len(s)):
        left = right - len(p)
        window[s[right]] += 1
        window[s[left]]  -= 1
        if window[s[left]] == 0:
            del window[s[left]]
        if window == need:
            return True
    return False

print(check_inclusion('ab', 'eidbaooo'))  # True ('ba')
print(check_inclusion('ab', 'eidboaoo'))  # False

สตริงย่อยแบบแอนนาแกรม: นับทั้งหมด

ค้นหาดัชนีเริ่มต้นทั้งหมดของแอนนาแกรมของ p ใน s วิธีนี้ใช้เทคนิคหน้าต่างขนาดคงที่แบบเดียวกับการเรียงสับเปลี่ยนในสตริง แต่แทนที่จะคืนค่า True เมื่อพบการจับคู่ครั้งแรก เราจะรวบรวมตำแหน่งที่ตรงกันทั้งหมด ขนาดหน้าต่างคงที่ที่ len(p) จากนั้นเลื่อนหน้าต่างผ่าน s และเปรียบเทียบจำนวนนับความถี่ในแต่ละขั้น

from collections import Counter

def find_anagrams(s, p):
    result = []
    need = Counter(p)
    k = len(p)
    window = Counter(s[:k])
    if window == need:
        result.append(0)
    for right in range(k, len(s)):
        window[s[right]] += 1
        left_char = s[right - k]
        window[left_char] -= 1
        if window[left_char] == 0:
            del window[left_char]
        if window == need:
            result.append(right - k + 1)
    return result

print(find_anagrams('cbaebabacd', 'abc'))  # [0, 6]

สตริงย่อยที่ยาวที่สุดซึ่งมีอักขระไม่ซ้ำกันไม่เกิน 2 ตัว

นี่คือรูปแบบหนึ่งของหน้าต่างเลื่อน: ค้นหาสตริงย่อยที่ยาวที่สุดซึ่งมีอักขระไม่ซ้ำกันไม่เกิน 2 ตัว รักษาแผนผังความถี่ของอักขระในหน้าต่างปัจจุบัน เมื่อแผนผังมีรายการเกิน 2 รายการ ให้เลื่อนตัวชี้ซ้ายไปทางขวา (ลดความถี่ และลบรายการหากมีค่าเป็นศูนย์) จนกว่าเงื่อนไขจะกลับมาเป็นจริง กรณีนี้เป็นกรณีพิเศษของปัญหา “อักขระไม่ซ้ำกันไม่เกิน k ตัว” โดย k=2

def longest_substring_two_distinct(s):
    from collections import defaultdict
    freq = defaultdict(int)
    left = 0
    best = 0
    for right, c in enumerate(s):
        freq[c] += 1
        while len(freq) > 2:
            freq[s[left]] -= 1
            if freq[s[left]] == 0:
                del freq[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

print(longest_substring_two_distinct('eceba'))     # 3  ('ece')
print(longest_substring_two_distinct('ccaabbb'))   # 5  ('aabbb')

ค่าสูงสุดของหน้าต่างเลื่อน

ค้นหาค่าสูงสุดในทุกหน้าต่างที่มีขนาด k การตรวจสอบค่าสูงสุดของแต่ละหน้าต่างด้วยวิธีตรงไปตรงมาใช้เวลา O(n×k) วิธีที่เหมาะสมที่สุดใช้คิวสองทางแบบโมโนโทนิกของดัชนี: รักษาคิวสองทางให้มีค่าลดหลั่น เพื่อให้ด้านหน้าเป็นดัชนีของค่าสูงสุดในหน้าต่างปัจจุบันเสมอ นำดัชนีออกจากด้านหน้าเมื่อดัชนีเหล่านั้นหลุดออกจากหน้าต่าง และนำดัชนีออกจากด้านหลังเมื่อมีสมาชิกที่ใหญ่กว่าเข้ามา เวลารวมคือ O(n)

from collections import deque

def max_sliding_window(nums, k):
    dq = deque()  # stores indices, decreasing values
    result = []
    for i, n in enumerate(nums):
        # Remove indices outside window
        while dq and dq[0] < i - k + 1:
            dq.popleft()
        # Maintain decreasing order
        while dq and nums[dq[-1]] < n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:  # window is full
            result.append(nums[dq[0]])
    return result

print(max_sliding_window([1,3,-1,-3,5,3,6,7], 3))
# [3, 3, 5, 5, 6, 7]

เมื่อใดควรใช้หน้าต่างเลื่อน

ควรพิจารณาใช้หน้าต่างเลื่อนเมื่อพบกรณีต่อไปนี้:

  • สตริงย่อย / อาร์เรย์ย่อยที่มีเงื่อนไข (ความยาวสูงสุด ผลรวม = k อักขระไม่ซ้ำกันไม่เกิน k ตัว)
  • หน้าต่างขนาดคงที่ที่ต้องคำนวณค่ารวม (ค่าสูงสุด ผลรวม ความถี่)
  • คำถามเกี่ยวกับช่วงที่อยู่ติดกัน (ไม่ใช่เซตย่อยที่เลือกอย่างอิสระ)
โปรดอย่าใช้หน้าต่างเลื่อน (NOT) กับ: การเลือกสมาชิกที่ไม่อยู่ติดกัน ปัญหาที่ต้องพิจารณาการเรียงสับเปลี่ยนทั้งหมด (ให้ใช้การค้นหาแบบย้อนกลับ) หรือปัญหาที่ไม่สามารถปรับปรุงสถานะของหน้าต่างทีละขั้นได้ คำถามสำคัญคือ: เมื่อเพิ่มหรือนำสมาชิกออกหนึ่งตัว คุณสามารถอัปเดตสถานะด้วยเวลา O(1) ได้หรือไม่

# Recognising sliding window problems:

# 1. Fixed window: 'maximum average of subarray of length k'
def max_avg(nums, k):
    s = sum(nums[:k])
    best = s
    for i in range(k, len(nums)):
        s += nums[i] - nums[i-k]
        best = max(best, s)
    return best / k

print(max_avg([1,12,-5,-6,50,3], 4))  # 12.75

# 2. Variable window: 'smallest subarray with sum >= target'
def min_sub_len(target, nums):
    left = s = 0
    best = float('inf')
    for right, n in enumerate(nums):
        s += n
        while s >= target:
            best = min(best, right - left + 1)
            s -= nums[left]; left += 1
    return 0 if best == float('inf') else best
print(min_sub_len(7, [2,3,1,2,4,3]))  # 2

การนับหน้าต่างที่ถูกต้อง: ไม่เกิน K

ปัญหาบางอย่างถามจำนวนอาร์เรย์ย่อยที่เป็นไปตามเงื่อนไข เคล็ดลับที่มีประโยชน์คือ นับอาร์เรย์ย่อยที่มีอักขระไม่ซ้ำกันไม่เกิน k ตัว แล้วลบออกเพื่อให้ได้จำนวนที่มีอักขระไม่เกิน k ตัวพอดี: exactly(k) = at_most(k) - at_most(k-1) การเรียกใช้ฟังก์ชันสำหรับกรณีไม่เกิน k แต่ละครั้งใช้เวลา O(n) จึงใช้เวลารวม O(n) ฟังก์ชันกรณีไม่เกิน k จะนับหน้าต่างที่จำนวนอักขระไม่ซ้ำกันไม่เกิน k ด้วยการรวมค่า right - left + 1 (จำนวนจุดเริ่มต้นด้านซ้ายที่ถูกต้องทั้งหมดสำหรับจุดสิ้นสุดด้านขวาแต่ละตำแหน่ง)

from collections import defaultdict

def subarrays_at_most_k(s, k):
    freq = defaultdict(int)
    left = 0
    count = 0
    for right, c in enumerate(s):
        freq[c] += 1
        while len(freq) > k:
            freq[s[left]] -= 1
            if freq[s[left]] == 0: del freq[s[left]]
            left += 1
        count += right - left + 1  # all valid windows ending at right
    return count

def subarrays_exactly_k(s, k):
    return subarrays_at_most_k(s, k) - subarrays_at_most_k(s, k-1)

print(subarrays_exactly_k('araaci', 2))  # 9

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

ทบทวนบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า หน้าต่างเลื่อนช่วยลดเวลา O(n²) ได้ด้วยการรักษาสถานะของหน้าต่างสะสม ซึ่งอัปเดตด้วยเวลา O(1) เมื่อสมาชิกเข้าหรือออกจากหน้าต่าง หน้าต่างขนาดคงที่จะเลื่อนตัวชี้ทั้งสองไปด้วยจังหวะเดียวกัน ส่วนหน้าต่างขนาดแปรผันจะขยายไปทางขวาอย่างเต็มที่ และหดเข้าทางซ้ายเฉพาะเมื่อมีการละเมิดเงื่อนไข และ สตริงย่อยของหน้าต่างที่สั้นที่สุดกับการเรียงสับเปลี่ยนในสตริงต่างใช้สถานะหน้าต่างที่เป็นแผนผังความถี่ โดยมีตัวนับติดตามจำนวนอักขระที่ต้องการซึ่งตรงตามข้อกำหนดแล้ว ถัดไป เราจะสำรวจแอนนาแกรมและแผนผังความถี่ของอักขระ

คำถามที่พบบ่อย

บทเรียน “หน้าต่างเลื่อนสำหรับสตริงย่อย” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “หน้าต่างเลื่อนสำหรับสตริงย่อย” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “หน้าต่างเลื่อนสำหรับสตริงย่อย”

สร้างหน้าต่างเลื่อนขนาดแปรผันเพื่อค้นหาสตริงย่อยที่ยาวที่สุดโดยไม่มีอักขระซ้ำ และหน้าต่างที่สั้นที่สุดซึ่งมีอักขระเป้าหมายครบทั้งหมด คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “หน้าต่างเลื่อนสำหรับสตริงย่อย” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ส่วนติดต่อสตริงของ Python สำหรับการสัมภาษณ์
  2. หน้าต่างเลื่อนสำหรับสตริงย่อย
  3. แอนนาแกรมและแผนผังความถี่อักขระ
  4. การเข้ารหัสสตริง การกลับลำดับ และพาลินโดรม
← กลับไปที่ DSA Interview Prep