การนับความถี่และการจัดกลุ่ม
ใช้ Counter และ defaultdict นับความถี่อักขระ จัดกลุ่มแอนนาแกรมด้วยคีย์ที่เรียงแล้ว และค้นหาองค์ประกอบที่ปรากฏบ่อยที่สุด k อันดับ
การนับความถี่และการจัดกลุ่ม เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding 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) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การนับความถี่และการจัดกลุ่ม”
ใช้ Counter และ defaultdict นับความถี่อักขระ จัดกลุ่มแอนนาแกรมด้วยคีย์ที่เรียงแล้ว และค้นหาองค์ประกอบที่ปรากฏบ่อยที่สุด k อันดับ คุณปฏิบัติ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ภายในฟังก์ชันแฮชและการจัดการการชนกัน
- ผลรวมสองค่าและรูปแบบหลากหลาย
- การนับความถี่และการจัดกลุ่ม
- ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU