หน้าต่างเลื่อนสำหรับสตริงย่อย
สร้างหน้าต่างเลื่อนขนาดแปรผันเพื่อค้นหาสตริงย่อยที่ยาวที่สุดโดยไม่มีอักขระซ้ำ และหน้าต่างที่สั้นที่สุดซึ่งมีอักขระเป้าหมายครบทั้งหมด
หน้าต่างเลื่อนสำหรับสตริงย่อย เป็นบทเรียน 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 ตัว)
- หน้าต่างขนาดคงที่ที่ต้องคำนวณค่ารวม (ค่าสูงสุด ผลรวม ความถี่)
- คำถามเกี่ยวกับช่วงที่อยู่ติดกัน (ไม่ใช่เซตย่อยที่เลือกอย่างอิสระ)
# 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ส่วนติดต่อสตริงของ Python สำหรับการสัมภาษณ์
- หน้าต่างเลื่อนสำหรับสตริงย่อย
- แอนนาแกรมและแผนผังความถี่อักขระ
- การเข้ารหัสสตริง การกลับลำดับ และพาลินโดรม