빈도 계산과 그룹화
Counter와 defaultdict로 문자 빈도를 세고, 정렬된 키로 애너그램을 그룹화하며, 빈도가 높은 상위 k개 원소를 찾습니다.
빈도 계산과 그룹화은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
빈도 계산: 핵심 패턴
빈도 계산은 코딩 면접에서 가장 활용도가 높은 패턴 중 하나입니다. 목록이나 문자열에 각 요소가 몇 번 나타나는지 세면 중복, 애너그램, 빈도가 가장 높은 요소, 유효한 배열에 관한 질문에 O(n) 시간에 답할 수 있습니다. 이는 정렬한 뒤 스캔하는 O(n log n)의 대안보다 훨씬 효율적입니다.
파이썬의 Counter와 defaultdict(int)는 표준 도구입니다. 둘 다 요소를 개수에 매핑하며, 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 '유효한 애너그램': 두 문자열이 서로 애너그램인지 판별합니다. 두 문자열의 각 문자 빈도가 같으면 서로 애너그램입니다. 두 카운터 객체를 비교하거나 두 문자열을 모두 정렬하면 됩니다. 카운터를 사용하면 O(n), 정렬하면 O(n log n)입니다. 카운터 방식이 최적이며 정의를 직접적으로 표현합니다.
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을 사용하는 것이 파이썬에서 가장 깔끔한 방법입니다. 시간 복잡도는 서로 다른 문자를 빈도에 따라 정렬하므로 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카운터를 사용한 다수결 투표
LeetCode 169 '다수 원소': n/2번보다 많이 나타나는 요소를 찾습니다. 보이어-무어 투표법이 O(1) 공간을 사용하는 최적의 해법이지만, Counter.most_common(1)을 사용하면 O(n) 시간과 O(n) 공간으로 직접 해결할 수 있습니다. 면접에서 O(1) 공간을 요구한다면 보이어-무어 투표법을 후속 해법으로 제시하고, 추가 공간 사용이 허용된다면 카운터 방식이 더 간결합니다.
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인 것의 개수는 이전 접두사 합 중 (current_prefix_sum - k)와 같은 값의 개수입니다. 인덱스 0에서 시작하는 부분 배열을 처리하려면 맵을 {0: 1}로 초기화합니다.
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는 산술 연산을 지원합니다. +는 병합하고(개수를 더함), -는 뺍니다(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를 사용하고, 더 세밀한 제어가 필요하거나 원소가 제한된 알파벳에서 엄격한 O(1) 공간이 필요할 때만 일반 dict나 배열로 바꾸세요.
빠른 확인
이번 레슨에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.
레슨 요약
이번 레슨에서는 Counter가 most_common, 산술 연산자, 0을 기본값으로 하는 접근을 통해 O(n) 빈도 계산을 제공하는 방법, 표준 형식(정렬된 튜플)으로 그룹화하여 O(nL log L)에 애너그램 그룹화 문제를 해결하는 방법, 그리고 빈도 맵을 사용한 접두사 합으로 부분 배열의 합이 k인 문제를 O(n²)에서 O(n)으로 바꾸는 방법을 배웠습니다. 다음에는 가장 긴 연속 수열 문제와 LRU 캐시 설계를 다룹니다.
자주 묻는 질문
“빈도 계산과 그룹화” 강의는 무료인가요?
네 — “빈도 계산과 그룹화” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“빈도 계산과 그룹화”에서 뭘 배우나요?
Counter와 defaultdict로 문자 빈도를 세고, 정렬된 키로 애너그램을 그룹화하며, 빈도가 높은 상위 k개 원소를 찾습니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“빈도 계산과 그룹화” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.