Trie
Trie란 Prefix Tree나 Radix Tree라고도 불리는 트리 형태의 자료구조로, reTRIEval 에서 유래한 이름을 가지고 있다.
Trie는 각 노드가 string
으로 된 key
를 나타내고, 각 엣지 (간선)가 하나의 알파벳을 나타내는 트리 형태의 자료구조로, key-value
쌍을 저장하도록 하면 map
처럼 associative array
로 이용할 수 있고, 그냥 key
만 저장하면 어떤 string
이 집합에 속하는지 확인하기 위한 set
처럼 삽입/삭제가 가능한 집합 자료구조로 이용할 수 있다.
흔히 사용되는 std::map/std::set
의 내부 구현인 Red-black tree
(balanced binary search tree/self-balancing binary search tree
)와 비교해보면 각 연산 (검색, 삽입)이 다음과 같은 시간 복잡도를 갖는다 ($L$: key
로 이용되는 문자열의 최대 길이, $N$: key
의 개수, 즉 자료구조의 원소 개수):
연산 | BBST | Trie |
---|---|---|
삽입 | $O(L\mathrm{log}N)$ | $O(L)$ |
검색 | $O(L\mathrm{log}N)$ | $O(L)$ |
{"AA", "AC", "BC", "BCG", "C", "CC"}
의 문자열을 원소로 갖는 Trie를 그림으로 나타내면 다음과 같다:
$\otimes$로 표현되는 노드는 해당 노드로 끝나는 문자열이 존재한다는 의미인데, 예를 들어 이 집합에 "A"
는 존재하지 않지만 "AA"
는 존재하므로 "AA"
를 나타내는 노드에서는 $\otimes$로 표현되는 노드로의 간선이 존재하지만 "A"
를 나타내는 노드에서는 간선이 존재하지 않는 것을 확인할 수 있다. (이후 $\otimes$ 로 나타나는 노드로 연결되는 간선을 "$"
로 표현)
연산의 경우 BST처럼 삽입 과정이 검색과 굉장히 닮아있기 때문에, 검색부터 살펴보도록 하자.
"BCG"
를 검색하는 경우
루트에서 시작해서, 해당 알파벳을 나타내는 간선을 따라 쭉쭉 따라가면 끝. 단, "BGC"
를 "BCG$"
로 고려해야 한다. 만약 중간에 NULL
을 만나게 된다면 (존재하지 않는 간선을 선택하게 된다면) 해당 문자열은 집합에 존재하지 않는다는 결론이 나오고, 그렇지 않을 경우 해당 문자열이 집합에 존재한다는 것을 확인할 수 있다.
"A"
를 검색하는 경우
마찬가지로 "A$"
를 생각하면, 중간에 $
을 나타내는 간선이 존재하지 않아 실패하는 것을 볼 수 있다.
삽입
검색을 보면서 깨달았겠지만, 삽입은 검색을 하는 도중 실패한 부분부터 간선을 추가해나가는 식으로 반복하면 간단!
삭제
삭제 역시도 각 노드에 rereference count 를 관리하면서 0이 되는 순간 노드와 간선을 지워주는 형태로 구현하면 간단하다.