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

แอนนาแกรมและแผนผังความถี่อักขระ

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

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

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