แอนนาแกรมและแผนผังความถี่อักขระ
แก้โจทย์การจัดกลุ่มแอนนาแกรม แอนนาแกรมที่ถูกต้อง และการเรียงสับเปลี่ยนในสตริงด้วยอาร์เรย์ความถี่และแผนผังแฮช เพื่อให้ได้คำตอบในเวลา O(n)
แอนนาแกรมและแผนผังความถี่อักขระ เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
แอนนาแกรมคืออะไร
สตริงสองชุดเป็นแอนนาแกรมกัน หากมีอักขระชุดเดียวกันด้วยความถี่เท่ากัน เพียงเรียงลำดับต่างกัน 'listen' และ 'silent' เป็นแอนนาแกรมกัน วิธีตรวจสอบความถูกต้องที่ง่ายที่สุดคือเรียงลำดับสตริงทั้งสองแล้วเปรียบเทียบ ซึ่งใช้เวลา O(n log n) สำหรับวิธีที่ใช้เวลา O(n) ให้เปรียบเทียบแผนผังความถี่ของอักขระ ปัญหาแอนนาแกรมเป็นหัวข้อพื้นฐานในการสัมภาษณ์ด้านสตริง เพราะทดสอบเทคนิคหลายแบบ ได้แก่ การแฮช การเรียงลำดับ และอาร์เรย์ความถี่
def is_anagram_sort(s, t):
return sorted(s) == sorted(t) # O(n log n)
def is_anagram_counter(s, t):
from collections import Counter
return Counter(s) == Counter(t) # O(n)
def is_anagram_array(s, t):
if len(s) != len(t): return False
freq = [0] * 26
for a, b in zip(s, t):
freq[ord(a) - ord('a')] += 1
freq[ord(b) - ord('a')] -= 1
return all(f == 0 for f in freq) # O(n)
print(is_anagram_array('anagram', 'nagaram')) # True
print(is_anagram_array('rat', 'car')) # Falseอาร์เรย์ความถี่สำหรับตัวอักษรพิมพ์เล็ก
เมื่อชุดอักขระมีขอบเขต (เช่น มีเฉพาะ a-z พิมพ์เล็ก) ให้แทนที่ตารางแฮชด้วยอาร์เรย์ความถี่ขนาด 26 การใช้ดัชนีจาก ord(c) - ord('a') จะจับคู่ 'a'→0, 'b'→1, ..., 'z'→25 อาร์เรย์ทำงานเร็วกว่าพจนานุกรมในทางปฏิบัติ เนื่องจากใช้ประโยชน์จากตำแหน่งข้อมูลในแคชและไม่มีต้นทุนจากการแฮช เคล็ดลับนี้ปรากฏในปัญหาแอนนาแกรมที่ถูกต้อง การเรียงสับเปลี่ยนในสตริงด้วยแอนนาแกรม และการเรียงสับเปลี่ยนให้เป็นพาลินโดรม
def build_freq(s):
freq = [0] * 26
for c in s:
freq[ord(c) - ord('a')] += 1
return freq
def is_anagram_fast(s, t):
return len(s) == len(t) and build_freq(s) == build_freq(t)
# Palindrome permutation: at most one odd-count character
def can_form_palindrome(s):
freq = build_freq(s)
odd_count = sum(1 for f in freq if f % 2 == 1)
return odd_count <= 1
print(can_form_palindrome('carerace')) # True ('racecar')
print(can_form_palindrome('hello')) # Falseจัดกลุ่มแอนนาแกรม
จัดกลุ่มรายการสตริงเพื่อให้แอนนาแกรมทั้งหมดอยู่รวมกัน วิธีมาตรฐานที่ใช้เวลา O(n×m log m) ใช้สตริงที่เรียงลำดับแล้วเป็นคีย์ของตารางแฮช แอนนาแกรมทั้งหมดจะสร้างคีย์ที่เรียงลำดับเหมือนกัน จึงถูกจัดให้อยู่ในกลุ่มเดียวกัน วิธีที่ใช้เวลา O(n×m) ใช้ทูเพิลของจำนวนนับอักขระเป็นคีย์ ซึ่งคำนวณช้ากว่าเล็กน้อยแต่ไม่ต้องเรียงลำดับเลย โดยทั่วไปมักเลือกวิธีใช้คีย์ที่เรียงลำดับแล้ว เพราะเข้าใจได้ง่ายกว่า
from collections import defaultdict
def group_anagrams(strs):
groups = defaultdict(list)
for s in strs:
key = tuple(sorted(s)) # or ''.join(sorted(s))
groups[key].append(s)
return list(groups.values())
words = ['eat','tea','tan','ate','nat','bat']
result = group_anagrams(words)
for g in sorted(result, key=len, reverse=True):
print(sorted(g))
# ['ate', 'eat', 'tea']
# ['nat', 'tan']
# ['bat']คีย์แอนนาแกรมด้วยทูเพิลจำนวนนับ
สำหรับวิธีจัดกลุ่มแอนนาแกรมที่ใช้เวลา O(n×m) ให้แทนความถี่ของสตริงแต่ละชุดด้วยทูเพิลของจำนวนนับ 26 ค่า: tuple(freq_array) วิธีนี้ไม่ต้องเรียงลำดับ แต่ต้องใช้เวลา O(26×n×m) เพื่อสร้างคีย์ทั้งหมด ทูเพิลสามารถใช้การแฮชใน Python ได้ จึงใช้เป็นคีย์พจนานุกรมได้ วิธีนี้ควรกล่าวถึงเมื่อผู้สัมภาษณ์ถามหา “วิธีใดก็ได้ที่ใช้เวลา O(n×m)” เพราะแสดงให้เห็นว่าคุณเข้าใจข้อแลกเปลี่ยนของวิธีต่าง ๆ
from collections import defaultdict
def group_anagrams_count(strs):
groups = defaultdict(list)
for s in strs:
freq = [0] * 26
for c in s:
freq[ord(c) - ord('a')] += 1
key = tuple(freq) # tuple is hashable
groups[key].append(s)
return list(groups.values())
print(group_anagrams_count(['eat','tea','tan','ate','nat','bat']))องค์ประกอบที่มีความถี่สูงสุด K อันดับแรก
ค้นหาองค์ประกอบ k รายการที่มีความถี่สูงสุดในอาร์เรย์ วิธีใช้ตัวนับความถี่ร่วมกับฮีป: สร้างแผนผังความถี่ในเวลา O(n) แล้วดึงความถี่ k ค่าสูงสุดออกมาโดยใช้ฮีปขั้นต่ำขนาด k หรือ Counter.most_common(k) วิธีเรียงลำดับแบบแบ่งกลุ่มที่ใช้เวลา O(n) จะสร้างกลุ่มที่มีดัชนีตามความถี่ (ตั้งแต่ 0 ถึง n) แล้วรวบรวมองค์ประกอบโดยเรียงจากความถี่สูงไปต่ำ วิธีนี้สวยงามและเหมาะเมื่อ k มีค่ามาก
from collections import Counter
import heapq
def top_k_frequent_heap(nums, k):
freq = Counter(nums)
return heapq.nlargest(k, freq, key=freq.get)
def top_k_frequent_bucket(nums, k):
freq = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
for num, cnt in freq.items():
buckets[cnt].append(num)
result = []
for i in range(len(buckets)-1, -1, -1):
result.extend(buckets[i])
if len(result) >= k: break
return result[:k]
print(top_k_frequent_heap([1,1,1,2,2,3], 2)) # [1, 2]
print(top_k_frequent_bucket([1,1,1,2,2,3], 2)) # [1, 2]แผนผังความถี่สำหรับการเรียงสับเปลี่ยนในสตริง
ตรวจสอบว่าการเรียงสับเปลี่ยนใด ๆ ของสตริง p เป็นสตริงย่อยของ s หรือไม่ แผนผังความถี่ของหน้าต่างที่มีความยาว |p| ต้องเท่ากับแผนผังความถี่ของ p เมื่อหน้าต่างเลื่อน ให้เพิ่มจำนวนนับของอักขระที่เข้ามา และลดจำนวนนับของอักขระที่ออกไป การเปรียบเทียบโครงสร้างนับความถี่สองชุดใช้เวลา O(26) ในแต่ละครั้ง จึงมีเวลารวม O(n×26) = O(n) ติดตามตัวนับจำนวนอักขระที่ครบตามเงื่อนไขเพื่อให้ตรวจสอบความเท่ากันได้ในเวลา O(1)
def check_inclusion_fast(p, s):
if len(p) > len(s): return False
need = [0] * 26
have = [0] * 26
for c in p:
need[ord(c)-ord('a')] += 1
for i in range(len(p)):
have[ord(s[i])-ord('a')] += 1
if need == have: return True
for i in range(len(p), len(s)):
have[ord(s[i])-ord('a')] += 1
have[ord(s[i-len(p)])-ord('a')] -= 1
if need == have: return True
return False
print(check_inclusion_fast('ab', 'eidbaooo')) # True
print(check_inclusion_fast('ab', 'eidboaoo')) # Falseจำนวนอักขระขั้นต่ำเพื่อสร้างแอนนาแกรม
เมื่อกำหนดสตริงสองชุด ให้ค้นหาจำนวนอักขระขั้นต่ำที่ต้องลบเพื่อทำให้สตริงหนึ่งเป็นแอนนาแกรมของอีกสตริง คำนวณแผนผังความถี่ของสตริงทั้งสอง แล้วหาคำตอบด้วยผลรวมของค่าความแตกต่างสัมบูรณ์ของความถี่ อักขระที่มีอยู่ในสตริงหนึ่งแต่ไม่มีในอีกสตริงจะต้องถูกลบออกทั้งหมด วิธีแก้ที่ใช้เวลา O(n) นี้ใช้รูปแบบ “รวมแล้วหาผลต่าง” กับแผนผังความถี่
from collections import Counter
def min_steps_to_anagram(s, t):
freq_s = Counter(s)
freq_t = Counter(t)
steps = 0
# For each unique char across both strings:
all_chars = set(freq_s) | set(freq_t)
for c in all_chars:
steps += abs(freq_s.get(c, 0) - freq_t.get(c, 0))
return steps
# Or more concisely:
def min_steps_counter(s, t):
diff = Counter(s) - Counter(t)
return sum(diff.values())
print(min_steps_to_anagram('leetcode', 'practice')) # 5
print(min_steps_counter('leetcode', 'practice')) # 5แผนผังความถี่สำหรับบันทึกเรียกค่าไถ่
ตรวจสอบว่าอักขระทั้งหมดใน note สามารถจัดหาได้จากอักขระใน magazine หรือไม่ (อักขระแต่ละตัวในนิตยสารใช้ได้เพียงครั้งเดียว) สร้างแผนผังความถี่ของอักขระในนิตยสาร จากนั้นสำหรับอักขระแต่ละตัวในบันทึก ให้ลดจำนวนนับลง หากจำนวนนับใดติดลบ ให้ส่งคืนค่าเท็จ วิธีนี้ใช้เวลา O(n + m) และใช้พื้นที่ O(1) สำหรับข้อมูลนำเข้าที่จำกัดเฉพาะตัวอักษรพิมพ์เล็ก โดยใช้อาร์เรย์ 26 ช่องแทนพจนานุกรม
def can_construct(note, magazine):
freq = [0] * 26
for c in magazine:
freq[ord(c) - ord('a')] += 1
for c in note:
freq[ord(c) - ord('a')] -= 1
if freq[ord(c) - ord('a')] < 0:
return False # insufficient supply
return True
print(can_construct('aa', 'aab')) # True
print(can_construct('aa', 'ab')) # False
print(can_construct('bg', 'efjbdfbdgbjjbghiklgdch')) # Trueการแฮชสตริงย่อยแอนนาแกรมที่ยาวที่สุด
หากต้องการตรวจสอบว่าสตริงย่อยสองชุดของสตริงเดียวกันเป็นแอนนาแกรมกันหรือไม่ ให้ใช้แฮชพหุนามของความถี่อักขระที่มีคุณสมบัติสลับลำดับได้ (ไม่ขึ้นกับลำดับ) XOR ของค่าประจำอักขระมีคุณสมบัติสลับลำดับได้และอัปเดตได้ในเวลา O(1) แต่มีโอกาสชนกันสูง วิธีที่ดีกว่าคือการแฮชด้วยผลคูณของจำนวนเฉพาะ (จับคู่อักขระแต่ละตัวกับจำนวนเฉพาะที่แตกต่างกัน ผลคูณจึงไม่ขึ้นกับลำดับ) นี่เป็นเทคนิคเฉพาะทางสำหรับการสัมภาษณ์ระดับสูง
# Prime product hash: each char maps to a prime
PRIMES = [2,3,5,7,11,13,17,19,23,29,31,37,41,
43,47,53,59,61,67,71,73,79,83,89,97,101]
def char_hash(s):
h = 1
for c in s:
h *= PRIMES[ord(c) - ord('a')]
return h
# Two windows with equal hash are likely anagrams
print(char_hash('listen')) # same as:
print(char_hash('silent')) # should matchรายการตรวจสอบรูปแบบแผนผังความถี่
จดจำรูปแบบการสัมภาษณ์ที่ใช้แผนผังความถี่เหล่านี้:
- แอนนาแกรมที่ถูกต้อง: ความยาวเท่ากัน + ความถี่เท่ากัน → เปรียบเทียบความเท่ากันของตัวนับหรือเปรียบเทียบอาร์เรย์
- จัดกลุ่มแอนนาแกรม: ใช้สตริงที่เรียงลำดับแล้วหรือทูเพิลความถี่เป็นคีย์ของพจนานุกรม
- องค์ประกอบ k อันดับแรกที่มีความถี่สูงสุด: ใช้ตัวนับร่วมกับฮีปหรือการเรียงลำดับแบบแบ่งกลุ่ม
- การเรียงสับเปลี่ยนในสตริง: หน้าต่างเลื่อน + การเปรียบเทียบความถี่
- บันทึกเรียกค่าไถ่: แผนผังความถี่ของสิ่งที่มีอยู่ แล้วลดจำนวนนับตามสิ่งที่ต้องการ
- การเรียงสับเปลี่ยนให้เป็นพาลินโดรม: มีอักขระที่มีจำนวนนับเป็นเลขคี่ได้ไม่เกินหนึ่งตัว
from collections import Counter
# Palindrome permutation
def palindrome_permutation(s):
return sum(v % 2 for v in Counter(s).values()) <= 1
# First unique character
def first_unique(s):
freq = Counter(s)
for i, c in enumerate(s):
if freq[c] == 1:
return i
return -1
# Character replacement for longest repeat
def char_replacement(s, k):
freq = Counter()
left = best = max_freq = 0
for right, c in enumerate(s):
freq[c] += 1
max_freq = max(max_freq, freq[c])
if (right - left + 1) - max_freq > k:
freq[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best
print(palindrome_permutation('carerace')) # True
print(first_unique('leetcode')) # 0
print(char_replacement('AABABBA', 1)) # 4องค์ประกอบที่แตกต่าง: XOR สำหรับความถี่
XOR เป็นเครื่องมือทรงพลังสำหรับปัญหาความถี่เมื่อมีสมาชิกที่ปรากฏเป็นจำนวนคี่อยู่เพียงหนึ่งตัว XOR ของตัวเลขกับตัวมันเองจะหักล้างกันเป็น 0: a XOR a = 0 เมื่อทำ XOR กับสมาชิกทั้งหมด โดยทุกค่าปรากฏเป็นจำนวนคู่ยกเว้นหนึ่งค่า จะเหลือเพียงค่าที่ปรากฏเป็นจำนวนคี่ วิธีนี้ใช้เวลา O(n) และพื้นที่ O(1) โดยไม่ต้องใช้ตารางแฮช และยังประยุกต์ใช้ค้นหาตัวเลขสองค่าที่ปรากฏเป็นจำนวนคี่ได้ด้วยคุณสมบัติของ XOR
def single_number(nums):
result = 0
for n in nums:
result ^= n # XOR cancels pairs
return result
print(single_number([4,1,2,1,2])) # 4
print(single_number([2,2,1])) # 1
# Find the unique character in an anagram check:
def find_difference(s, t):
result = 0
for c in s + t:
result ^= ord(c)
return chr(result)
print(find_difference('abcd', 'abcde')) # 'e'ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า แผนผังความถี่ของอักขระเป็นเครื่องมือหลักในการตรวจจับแอนนาแกรม — ใช้อาร์เรย์ 26 ช่องสำหรับชุดอักขระที่มีขอบเขต หรือตัวนับสำหรับอักขระทั่วไป คีย์พจนานุกรมที่เป็นสตริงเรียงลำดับแล้วหรือทูเพิลความถี่จะจัดกลุ่มแอนนาแกรมทั้งหมดไว้ด้วยกัน โดยใช้เวลา O(n × m log m) หรือ O(n × m) ตามลำดับ และ XOR ช่วยตัดคู่สมาชิกออกได้อย่างเรียบร้อยสำหรับปัญหาที่มีสมาชิกเดี่ยวปรากฏเป็นจำนวนคี่ โดยใช้เวลา O(n) และพื้นที่ O(1) เมื่อไม่จำเป็นต้องใช้พจนานุกรม ถัดไป เราจะสำรวจการเข้ารหัสสตริง การกลับลำดับ และเทคนิคพาลินโดรม
คำถามที่พบบ่อย
บทเรียน “แอนนาแกรมและแผนผังความถี่อักขระ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “แอนนาแกรมและแผนผังความถี่อักขระ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “แอนนาแกรมและแผนผังความถี่อักขระ”
แก้โจทย์การจัดกลุ่มแอนนาแกรม แอนนาแกรมที่ถูกต้อง และการเรียงสับเปลี่ยนในสตริงด้วยอาร์เรย์ความถี่และแผนผังแฮช เพื่อให้ได้คำตอบในเวลา O(n) คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “แอนนาแกรมและแผนผังความถี่อักขระ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ส่วนติดต่อสตริงของ Python สำหรับการสัมภาษณ์
- หน้าต่างเลื่อนสำหรับสตริงย่อย
- แอนนาแกรมและแผนผังความถี่อักขระ
- การเข้ารหัสสตริง การกลับลำดับ และพาลินโดรม