0Pricing
Coding Interview Prep · 강의

애너그램과 문자 빈도 맵

빈도 배열과 해시 맵을 사용해 O(n) 해법으로 group-anagrams, valid-anagram, permutation-in-string 문제를 해결합니다.

애너그램과 문자 빈도 맵은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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개의 요소를 찾습니다. Counter와 힙을 사용하는 방법에서는 O(n)에 빈도 맵을 만든 다음, 크기가 k인 최소 힙 또는 Counter.most_common(k)을 사용하여 빈도가 가장 높은 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의 빈도 맵과 같아야 합니다. 윈도우가 이동할 때 들어오는 문자의 개수를 증가시키고 나가는 문자의 개수를 감소시킵니다. 두 Counter 객체를 비교하는 비용은 매번 O(26)이므로 전체 시간 복잡도는 O(n×26) = O(n)입니다. O(1) 등가 확인을 위해 'formed' 카운터를 추적합니다.

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의 문자로 만들 수 있는지 확인합니다(각 잡지 문자는 한 번만 사용할 수 있습니다). 먼저 magazine 문자의 빈도 맵을 만들고, note의 각 문자에 대해 개수를 감소시킵니다. 어떤 개수라도 음수가 되면 False를 반환합니다. 소문자로 제한된 입력에서는 딕셔너리 대신 26개 요소 배열을 사용하여 O(n + m) 시간과 O(1) 공간으로 처리할 수 있습니다.

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

빈도 맵 패턴 확인 목록

다음 빈도 맵 면접 패턴을 익혀 두시기 바랍니다:

  • 유효한 애너그램: 같은 길이 + 같은 빈도 → Counter 등가 비교 또는 배열 비교
  • 애너그램 그룹화: 정렬된 문자열 또는 빈도 튜플을 딕셔너리 키로 사용
  • 빈도가 높은 상위 k개: Counter + 힙 또는 버킷 정렬
  • 문자열 속 순열: 슬라이딩 윈도우 + 빈도 비교
  • 랜섬 노트: 공급 문자의 빈도 맵을 만들고 요구 문자를 확인할 때 감소
  • 순열 애너그램: 홀수 개로 나타나는 문자가 최대 하나
이 모든 문제는 빈도를 지문처럼 사용하는 동일한 핵심 아이디어로 환원됩니다.

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개 요소 배열을, 임의의 문자에는 Counter를 사용할 수 있습니다. 또한 정렬된 문자열 또는 빈도 튜플을 딕셔너리 키로 사용하면 각각 O(n × m log m) 또는 O(n × m) 시간에 모든 애너그램을 함께 그룹화할 수 있습니다. 그리고 XOR은 단일 요소가 홀수 개수로 나타나는 문제에서 쌍을 깔끔하게 제거하므로, 딕셔너리가 필요하지 않을 때 O(n) 시간과 O(1) 공간을 제공합니다. 다음에는 문자열 인코딩, 뒤집기, 팰린드롬 기법을 살펴보겠습니다.

자주 묻는 질문

“애너그램과 문자 빈도 맵” 강의는 무료인가요?

네 — “애너그램과 문자 빈도 맵” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“애너그램과 문자 빈도 맵”에서 뭘 배우나요?

빈도 배열과 해시 맵을 사용해 O(n) 해법으로 group-anagrams, valid-anagram, permutation-in-string 문제를 해결합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“애너그램과 문자 빈도 맵” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 면접을 위한 Python 문자열 API
  2. 부분 문자열을 위한 슬라이딩 윈도우
  3. 애너그램과 문자 빈도 맵
  4. 문자열 인코딩, 뒤집기, 회문
← Coding Interview Prep(으)로 돌아가기