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

การนับความถี่และการจัดกลุ่ม

ใช้ Counter และ defaultdict นับความถี่อักขระ จัดกลุ่มแอนนาแกรมด้วยคีย์ที่เรียงแล้ว และค้นหาองค์ประกอบที่ปรากฏบ่อยที่สุด k อันดับ

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

การนับความถี่: รูปแบบหลัก

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

Counter และ defaultdict(int) ของ Python เป็นเครื่องมือมาตรฐาน ทั้งคู่สร้างการจับคู่ระหว่างองค์ประกอบกับจำนวนครั้งที่พบ โดย Counter ยังรองรับการคำนวณทางเลขคณิตและ most_common เพิ่มเติมด้วย

from collections import Counter

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
freq  = Counter(words)
print(freq)                  # Counter({'apple':3,'banana':2,'cherry':1})
print(freq['apple'])         # 3
print(freq['grape'])         # 0 (not KeyError)
print(freq.most_common(2))   # [('apple',3),('banana',2)]

แอนนาแกรมที่ถูกต้อง (LeetCode 242)

LeetCode 242 «แอนนาแกรมที่ถูกต้อง»: ตรวจสอบว่าสตริงสองรายการเป็นแอนนาแกรมของกันและกันหรือไม่ สตริงสองรายการเป็นแอนนาแกรมกันเมื่อมีความถี่ของอักขระเหมือนกัน คุณสามารถเปรียบเทียบออบเจ็กต์ Counter หรือเรียงลำดับสตริงทั้งสองรายการ การใช้ Counter มีเวลา O(n) ส่วนการเรียงลำดับมีเวลา O(n log n) แนวทางที่ใช้ Counter มีประสิทธิภาพสูงสุดและสื่อความหมายตามนิยามได้โดยตรง

from collections import Counter

def isAnagram(s, t):
    return Counter(s) == Counter(t)

# Alternative: manual frequency array for lowercase letters only
def isAnagram_arr(s, t):
    if len(s) != len(t):
        return False
    freq = [0] * 26
    for c in s: freq[ord(c) - ord('a')] += 1
    for c in t: freq[ord(c) - ord('a')] -= 1
    return all(f == 0 for f in freq)

print(isAnagram('anagram', 'nagaram'))  # True
print(isAnagram('rat', 'car'))          # False
print(isAnagram_arr('listen', 'silent'))  # True

จัดกลุ่มแอนนาแกรม (LeetCode 49)

LeetCode 49 «จัดกลุ่มแอนนาแกรม»: เมื่อกำหนดรายการสตริงมา ให้จัดกลุ่มแอนนาแกรมทั้งหมดไว้ด้วยกัน แนวคิดสำคัญคือ แอนนาแกรมจะมีลำดับอักขระที่เรียงแล้วเหมือนกัน ใช้ defaultdict(list) โดยใช้ทูเพิลที่เรียงอักขระของสตริงเป็นกุญแจ (ทูเพิลสามารถใช้เป็นแฮชได้) แต่ละกลุ่มจะสะสมอยู่ภายใต้กุญแจเดียวกัน เวลา: O(n × L log L) เมื่อ L คือความยาวสูงสุดของสตริง

from collections import defaultdict

def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))   # hashable canonical form
        groups[key].append(s)
    return list(groups.values())

print(groupAnagrams(['eat','tea','tan','ate','nat','bat']))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]

# Alternative key: tuple of 26 character counts (O(L) not O(L log L))
def groupAnagrams_v2(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(ord(c) - ord('a') for c in sorted(s))
        groups[tuple(Counter(s)[chr(ord('a')+i)] for i in range(26))].append(s)
    return list(groups.values())

องค์ประกอบที่พบบ่อยที่สุด K รายการ (LeetCode 347)

LeetCode 347 «องค์ประกอบที่พบบ่อยที่สุด K รายการ»: ให้คืนค่าองค์ประกอบ k รายการที่พบบ่อยที่สุด แนวทางโดยตรงมีเวลา O(n log n): นับความถี่ เรียงตามจำนวนครั้งจากมากไปน้อย แล้วเลือก k รายการแรก แนวทางที่มีประสิทธิภาพสูงสุดและใช้เวลา O(n) คือ การเรียงลำดับแบบบักเก็ต: สร้างบักเก็ตที่มีดัชนีตามความถี่ตั้งแต่ 1 ถึง n ใส่แต่ละองค์ประกอบลงในบักเก็ตตามความถี่ขององค์ประกอบนั้น จากนั้นตรวจบักเก็ตจากความถี่สูงสุดไปต่ำสุดเพื่อเก็บองค์ประกอบให้ครบ k รายการ

from collections import Counter

def topKFrequent(nums, k):
    freq  = Counter(nums)
    # Bucket sort by frequency
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, count in freq.items():
        buckets[count].append(num)
    result = []
    for i in range(len(buckets) - 1, -1, -1):
        result.extend(buckets[i])
        if len(result) >= k:
            return result[:k]
    return result

print(topKFrequent([1,1,1,2,2,3], 2))  # [1, 2]
print(topKFrequent([1], 1))             # [1]

เรียงอักขระตามความถี่ (LeetCode 451)

LeetCode 451 «เรียงอักขระตามความถี่»: จัดเรียงสตริงใหม่ให้อักขระปรากฏตามลำดับความถี่จากมากไปน้อย นับความถี่ เรียงอักขระตามความถี่จากมากไปน้อย แล้วนำมาต่อกัน การใช้ most_common เป็นแนวทางใน Python ที่กระชับและชัดเจนที่สุด เวลา: O(n log n) สำหรับการเรียงอักขระที่ไม่ซ้ำตามความถี่

from collections import Counter

def frequencySort(s):
    freq = Counter(s)
    return ''.join(ch * count for ch, count in freq.most_common())

print(frequencySort('tree'))    # 'eetr' or 'eert'
print(frequencySort('cccaaa'))  # 'cccaaa' or 'aaaccc'
print(frequencySort('Aabb'))    # 'bbAa' or 'bbaA'

ตัวจัดตารางงาน (LeetCode 621)

LeetCode 621 «ตัวจัดตารางงาน»: เมื่อกำหนดงานและช่วงพัก n มา ให้หาเวลาต่ำสุดที่ใช้ทำงานทั้งหมดให้เสร็จ แนวคิดสำคัญคือ งานที่มีความถี่สูงสุดจะเป็นตัวกำหนดโครงสร้าง จัดวางสำเนาของงานที่มีความถี่สูงสุดจำนวน max_count ชุด โดยมีช่องว่าง (n) ช่องระหว่างกัน เวลาต่ำสุดทั้งหมด = max((max_count - 1) * (n + 1) + num_tasks_with_max_count, total_tasks) หากมีงานที่แตกต่างกันมากพอจะเติมช่องว่างได้ เวลาว่างจะเป็น 0

from collections import Counter

def leastInterval(tasks, n):
    freq      = Counter(tasks)
    max_count = max(freq.values())
    # How many tasks share the max frequency
    num_max   = sum(1 for v in freq.values() if v == max_count)
    # Minimum slots needed based on most frequent task
    min_slots = (max_count - 1) * (n + 1) + num_max
    return max(min_slots, len(tasks))

print(leastInterval(['A','A','A','B','B','B'], 2))  # 8
print(leastInterval(['A','A','A','B','B','B'], 0))  # 6
print(leastInterval(['A','A','A','A','B','B','B','C','C','D'], 2))  # 10

การลงคะแนนหาเสียงข้างมากด้วย Counter

LeetCode 169 «องค์ประกอบเสียงข้างมาก»: ค้นหาองค์ประกอบที่ปรากฏมากกว่า n/2 ครั้ง แม้วิธีลงคะแนนของ Boyer-Moore จะเป็นวิธีที่เหมาะสมที่สุดและใช้พื้นที่ O(1) แต่การใช้ Counter.most_common(1) ก็แก้ปัญหาได้โดยตรงในเวลา O(n) และใช้พื้นที่ O(n) หากโจทย์สัมภาษณ์ระบุว่าต้องใช้พื้นที่ O(1) ให้เสนอ Boyer-Moore เป็นแนวทางต่อยอด แต่หากอนุญาตให้ใช้พื้นที่เพิ่มเติม Counter จะกระชับและชัดเจนกว่า

from collections import Counter

def majorityElement_counter(nums):
    freq = Counter(nums)
    return freq.most_common(1)[0][0]

# Boyer-Moore O(1) space
def majorityElement_moore(nums):
    candidate, count = None, 0
    for num in nums:
        if count == 0:
            candidate = num
        count += (1 if num == candidate else -1)
    return candidate

nums = [2, 2, 1, 1, 2, 2, 2]
print(majorityElement_counter(nums))  # 2
print(majorityElement_moore(nums))    # 2

อักขระตัวแรกที่ไม่ซ้ำ

LeetCode 387 «อักขระตัวแรกที่ไม่ซ้ำในสตริง»: ค้นหาดัชนีของอักขระตัวแรกที่ปรากฏเพียงครั้งเดียว ใช้แนวทางสองรอบ: รอบแรกสร้างการนับความถี่ รอบที่สองค้นหาอักขระตัวแรกที่มีจำนวนครั้งเท่ากับ 1 เวลา: O(n) พื้นที่: O(1) เนื่องจากชุดอักขระมีขนาดคงที่ที่ 26 อักขระ

from collections import Counter

def firstUniqChar(s):
    freq = Counter(s)
    for i, ch in enumerate(s):
        if freq[ch] == 1:
            return i
    return -1

print(firstUniqChar('leetcode'))   # 0 (l)
print(firstUniqChar('loveleetcode'))  # 2 (v)
print(firstUniqChar('aabb'))       # -1

ผลรวมของส่วนย่อยเท่ากับ K (LeetCode 560)

LeetCode 560 «ผลรวมของส่วนย่อยเท่ากับ K»: นับจำนวนส่วนย่อยที่มีผลรวมเท่ากับ k วิธีตรวจสอบทุกกรณีมีเวลา O(n²) แนวทางที่ใช้เวลา O(n) คือ รักษาผลรวมคำนำหน้าที่คำนวณต่อเนื่องและแผนที่ความถี่ของผลรวมคำนำหน้าที่พบแล้ว สำหรับแต่ละตำแหน่ง i จำนวนส่วนย่อยที่สิ้นสุดที่ i และมีผลรวมเป็น k จะเท่ากับจำนวนผลรวมคำนำหน้าก่อนหน้าที่เท่ากับ (ผลรวมคำนำหน้าปัจจุบัน - k) กำหนดค่าเริ่มต้นของแผนที่เป็น {0: 1} เพื่อรองรับส่วนย่อยที่เริ่มจากดัชนี 0

from collections import defaultdict

def subarraySum(nums, k):
    freq         = defaultdict(int)
    freq[0]      = 1   # prefix sum of 0 seen once (empty prefix)
    prefix_sum   = 0
    count        = 0
    for num in nums:
        prefix_sum += num
        # How many earlier prefix sums allow a k-sum subarray ending here
        count      += freq[prefix_sum - k]
        freq[prefix_sum] += 1
    return count

print(subarraySum([1, 1, 1], 2))            # 2
print(subarraySum([1, 2, 3], 3))            # 2
print(subarraySum([1, -1, 1, -1, 1], 0))   # 4

การคำนวณและการตัดกันของ Counter

Counter รองรับการคำนวณทางเลขคณิต: + รวมจำนวนครั้งเข้าด้วยกัน, - ลบจำนวนครั้งโดยไม่ให้ค่าต่ำกว่า 0, & เลือกค่าต่ำสุด (การตัดกัน) และ | เลือกค่าสูงสุด (การรวมกัน) การดำเนินการเหล่านี้ช่วยลดความซับซ้อนของโจทย์ เช่น «ค้นหาอักขระร่วมในสตริงหลายรายการ» หรือ «ลบอักขระให้น้อยที่สุดเพื่อทำให้สตริงหนึ่งเป็นแอนนาแกรมของอีกสตริงหนึ่ง»

from collections import Counter

A = Counter('abccdd')
B = Counter('ccdde')

print('Add:      ', dict(A + B))  # sum of counts
print('Subtract: ', dict(A - B))  # A - B, clipped at 0
print('Intersect:', dict(A & B))  # min of shared counts
print('Union:    ', dict(A | B))  # max counts

# Min steps to make s anagram of t (LeetCode 1347)
s, t = 'leetcode', 'practice'
diff = Counter(t) - Counter(s)
print('Chars to add:', sum(diff.values()))  # 5

สรุป: เมื่อใดควรใช้การนับความถี่

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

ควรใช้ Counter เพื่อให้โค้ดชัดเจนเสมอ และเปลี่ยนไปใช้ dict ธรรมดาหรืออาร์เรย์เฉพาะเมื่อจำเป็นต้องควบคุมรายละเอียดมากขึ้น หรือต้องการพื้นที่ O(1) อย่างเคร่งครัดเมื่อชุดอักขระมีขอบเขตแน่นอน

แบบทดสอบสั้น ๆ

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

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า Counter ช่วยนับความถี่ในเวลา O(n) พร้อมรองรับ most_common ตัวดำเนินการทางเลขคณิต และการเข้าถึงโดยมีค่าเริ่มต้นเป็นศูนย์ การจัดกลุ่มตามรูปแบบมาตรฐาน (ทูเพิลที่เรียงลำดับแล้ว) ช่วยแก้โจทย์การจัดกลุ่มแอนนาแกรมในเวลา O(nL log L) และ ผลรวมคำนำหน้าร่วมกับแผนที่ความถี่ช่วยลดเวลาของโจทย์ผลรวมส่วนย่อยเท่ากับ k จาก O(n²) เหลือ O(n) ต่อไปเราจะจัดการกับโจทย์ลำดับจำนวนต่อเนื่องที่ยาวที่สุดและการออกแบบแคช LRU

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

บทเรียน “การนับความถี่และการจัดกลุ่ม” ฟรีหรือไม่

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

คุณจะเรียนรู้อะไรในบทเรียน “การนับความถี่และการจัดกลุ่ม”

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

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

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

บทเรียน “การนับความถี่และการจัดกลุ่ม” ใช้เวลานานแค่ไหน

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

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

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

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

  1. ภายในฟังก์ชันแฮชและการจัดการการชนกัน
  2. ผลรวมสองค่าและรูปแบบหลากหลาย
  3. การนับความถี่และการจัดกลุ่ม
  4. ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU
← กลับไปที่ DSA Interview Prep