[Daily morning study] 트라이(Trie) 자료구조와 문자열 검색

#daily morning study

Image


트라이(Trie)란?

트라이는 문자열을 저장하고 탐색하기 위한 트리 기반 자료구조다. 이름은 retrieval(검색)에서 유래했다.

각 노드가 문자 하나를 나타내고, 루트에서 특정 노드까지의 경로가 하나의 문자열(또는 그 접두사)이 된다. 공통 접두사를 공유하는 문자열들을 같은 경로에 모아서 저장하기 때문에 공간 효율이 높고 검색이 빠르다.

핵심 특성

  • 루트 노드는 빈 문자열을 나타냄
  • 각 노드는 자식 노드 배열(보통 알파벳 26개)과 단어 끝 여부를 나타내는 플래그를 가짐
  • 깊이 k인 노드는 길이 k의 접두사를 나타냄

트라이 구조 예시

[“cat”, “car”, “card”, “care”, “dog”]을 트라이에 삽입하면:

(root)
├── c
│   └── a
│       ├── t*          ← "cat" 끝
│       └── r*          ← "car" 끝
│           ├── d*      ← "card" 끝
│           └── e*      ← "care" 끝
└── d
    └── o
        └── g*          ← "dog" 끝

* 표시가 있는 노드는 is_end = True.


시간/공간 복잡도

연산시간 복잡도비고
삽입O(L)L = 문자열 길이
검색O(L) 
삭제O(L) 
공간O(N × M × A)N=단어 수, M=평균 길이, A=알파벳 크기

해시맵 기반 검색도 평균 O(1)이지만, 트라이는 접두사 탐색에 특화되어 있어 자동완성, 사전 구현 등에 적합하다.


Python 구현

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False


class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self.root
        for ch in word:
            if ch not in node.children:
                return False
            node = node.children[ch]
        return node.is_end

    def starts_with(self, prefix: str) -> bool:
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return False
            node = node.children[ch]
        return True

사용 예

trie = Trie()
for word in ["cat", "car", "card", "care", "dog"]:
    trie.insert(word)

print(trie.search("car"))        # True
print(trie.search("ca"))         # False (단어 끝 아님)
print(trie.starts_with("ca"))    # True
print(trie.starts_with("do"))    # True
print(trie.starts_with("fox"))   # False

자동완성 기능 구현

트라이의 대표적 활용 사례인 자동완성은 starts_with 이후 DFS로 구현한다.

def autocomplete(self, prefix: str) -> list[str]:
    node = self.root
    for ch in prefix:
        if ch not in node.children:
            return []
        node = node.children[ch]

    results = []
    self._dfs(node, prefix, results)
    return results

def _dfs(self, node: TrieNode, current: str, results: list[str]) -> None:
    if node.is_end:
        results.append(current)
    for ch, child in node.children.items():
        self._dfs(child, current + ch, results)
trie.autocomplete("car")  # ["car", "card", "care"]

삭제 구현

삭제는 삽입보다 복잡하다. 단순히 is_end를 False로 바꾸는 것만으로는 메모리 누수가 생기기 때문에, 자식이 없는 노드는 실제로 제거해야 한다.

def delete(self, word: str) -> bool:
    return self._delete(self.root, word, 0)

def _delete(self, node: TrieNode, word: str, depth: int) -> bool:
    if depth == len(word):
        if not node.is_end:
            return False  # 단어가 존재하지 않음
        node.is_end = False
        return len(node.children) == 0  # 자식 없으면 노드 제거 가능

    ch = word[depth]
    if ch not in node.children:
        return False

    should_delete_child = self._delete(node.children[ch], word, depth + 1)
    if should_delete_child:
        del node.children[ch]
        return len(node.children) == 0 and not node.is_end

    return False

공간 최적화: 압축 트라이 (Compressed Trie / Radix Tree)

일반 트라이는 단일 자식만 있는 노드가 길게 이어질 때 메모리 낭비가 심하다. 이런 경우 자식이 하나뿐인 노드들을 하나로 합치는 압축 트라이를 쓴다.

일반 트라이:        압축 트라이 (Radix Tree):
(root)              (root)
└── d               └── "dog"*
    └── o
        └── g*

Patricia Tree, Radix Tree라고도 불린다. Go의 표준 라이브러리 라우터, nginx 경로 탐색 등에서 실제로 활용된다.


트라이 vs 해시맵

 트라이해시맵
단어 검색O(L)O(L) 평균
접두사 검색O(P + 결과 수)불가 (전체 탐색 필요)
정렬된 순회가능 (사전순)별도 정렬 필요
메모리공통 접두사 공유각 키를 독립 저장
충돌없음발생 가능

접두사 기반 연산이 필요하면 트라이, 단순 키-값 저장이 필요하면 해시맵이 적합하다.


실전 활용 사례

1. 검색 자동완성

  • 사용자가 입력한 접두사로 가능한 단어 목록 반환
  • 구글, IDE 자동완성, 터미널 탭 완성

2. 철자 교정 (Spell Checker)

  • 단어를 삽입할 때 편집 거리(Edit Distance)와 결합하면 유사 단어 제안 가능

3. IP 라우팅 (Longest Prefix Match)

  • 네트워크 라우터에서 목적지 IP에 가장 길게 일치하는 라우팅 경로 탐색

4. 사전(Dictionary) 구현

  • 단어 존재 여부 빠른 검색 + 사전순 순회

5. 문자열 패턴 매칭

  • Aho-Corasick 알고리즘은 트라이를 기반으로 다중 패턴을 동시에 검색

Aho-Corasick 알고리즘 개요

여러 패턴 문자열을 텍스트에서 동시에 찾는 알고리즘이다. 트라이 + BFS로 실패 링크(failure link)를 구성하여 O(N + M + 매칭 수)로 동작한다 (N = 텍스트 길이, M = 모든 패턴 길이 합).

  • 단순 반복 검색: O(N × M)
  • Aho-Corasick: O(N + M + output)

바이러스 패턴 탐지, 스팸 필터링, grep 다중 패턴 등에 쓰인다.


요약

  • 트라이는 문자열 집합을 트리 형태로 관리하며, 삽입/검색/삭제 모두 O(L)
  • 접두사 탐색과 자동완성에 최적화된 자료구조
  • 메모리 최적화가 필요하면 압축 트라이(Radix Tree)를 고려
  • 다중 패턴 검색이 필요하면 Aho-Corasick(트라이 확장)을 사용