자동 완성, 사전 구현, 접두사 검색등에 사용되는 트라이(Trie)라는 자료구조에 대해 정리한 글입니다.
Trie (트라이)는 문자열을 저장하고 검색하는 데 효율적인 트리 기반 데이터 구조이며
문자열의 공통 접두사를 공유하여 메모리를 절약하고 탐색 속도를 높일 수 있습니다.
다음과 같은 문자열 집합이 있다고 가정합니다: ["cat", "car", "can", "dog"]
트라이 구조는 다음과 같이 구성됩니다:
이 구조를 통해 "ca"로 시작하는 모든 단어를 쉽게 찾을 수 있습니다 (e.g., "cat", "car", "can").
트라이(Trie)를 타입스크립트로 구현하는 방법을 설명하겠습니다. 트라이는 기본적으로 트리 구조를 활용하여 각 노드가 특정 문자에 해당하며, 단어의 끝을 표시할 수 있도록 합니다.
각 노드는 자신의 자식 노드와 단어의 끝인지 여부를 가지고 있어야 합니다.
class TrieNode {
children: Map<string, TrieNode>;
isEndOfWord: boolean;
constructor() {
this.children = new Map();
this.isEndOfWord = false;
}
}
children: 각 문자를 키로 하고, 해당 문자에 연결된 노드를 값으로 하는 맵입니다.isEndOfWord: 해당 노드가 단어의 끝을 나타내는지 여부를 나타내는 불리언 값입니다.트라이 클래스는 단어를 삽입하고 검색하며, 접두사 기반 검색을 할 수 있는 메서드를 포함합니다.
class Trie {
root: TrieNode;
constructor() {
this.root = new TrieNode();
}
// 단어 삽입
insert(word: string): void {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new TrieNode());
}
current = current.children.get(char)!;
}
current.isEndOfWord = true;
}
// 단어 검색
search(word: string): boolean {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
return false;
}
current = current.children.get(char)!;
}
return current.isEndOfWord;
}
// 키워드로 시작하는 모든 단어 반환
startsWith(prefix: string): string[] {
let current = this.root;
// 1. 접두사에 해당하는 노드로 이동
for (const char of prefix) {
if (!current.children.has(char)) {
return []; // 접두사가 없으면 빈 배열 반환
}
current = current.children.get(char)!;
}
// 2. 접두사 이후의 모든 단어 수집
const results: string[] = [];
this.collectWords(current, prefix, results);
return results;
}
// 헬퍼 함수: 특정 노드부터 시작해 단어들을 수집
private collectWords(node: TrieNode, prefix: string, results: string[]): void {
if (node.isEndOfWord) {
results.push(prefix); // 단어 끝에 도달하면 결과에 추가
}
for (const [char, childNode] of node.children) {
this.collectWords(childNode, prefix + char, results); // 자식 노드들에 대해 재귀 호출
}
}
}
insert(word: string): void: 단어를 트라이에 삽입하는 메서드입니다. 각 문자를 따라가며 노드를 추가하고, 단어의 마지막 노드에서는 isEndOfWord를 true로 설정합니다.search(word: string): boolean: 단어가 트라이에 존재하는지 확인합니다. 존재하면 true, 없으면 false를 반환합니다.startsWith(prefix: string): string[]: 주어진 prefix로 시작하는 모든 단어를 트라이에서 찾아 반환합니다.collectWords(node: TrieNode, prefix: string, results: string[]): void: 재귀적으로 트라이의 특정 노드에서 시작하는 모든 단어를 수집하는 헬퍼 함수입니다.const trie = new Trie();
trie.insert("cat");
trie.insert("car");
trie.insert("can");
trie.insert("dog");
console.log(trie.startsWith("ca")); // ["cat", "car", "can"]
console.log(trie.startsWith("do")); // ["dog"]
console.log(trie.startsWith("z")); // []
위 예시에서 cat, car, can을 삽입하고 검색 및 접두사 검사를 수행합니다.
Trie와 배열을 각각 사용하여 문자열을 저장하고 검색, 삽입, 삭제하는 경우, 시간 복잡도와 공간 복잡도는 상당히 다르게 나타납니다. 두 자료구조의 복잡도를 비교해보겠습니다.
| 연산 | Trie 시간 복잡도 | 배열 시간 복잡도 (includes / push / remove) |
|---|---|---|
| 탐색 | (O(L)) | (O(N ⋅ L)) |
| 삽입 | (O(L)) | (O(1)) (평균) , (O(N ⋅ L)) (중복 체크) |
| 삭제 | (O(L)) | (O(N ⋅ L)) |
L: 문자열의 길이N: 배열에 저장된 문자열의 수탐색 (Search)
includes 메서드를 사용할 때, 최악의 경우 배열의 모든 원소를 검색해야 합니다.삽입 (Insert)
삭제 (Delete)
remove 메서드는 삭제할 요소를 검색하고 제거해야 하므로, 최악의 경우 배열을 모두 검색해야 하며 (O(N ⋅ L))의 시간 복잡도를 가집니다.| 연산 | Trie 공간 복잡도 | 배열 공간 복잡도 |
|---|---|---|
| 저장 용량 | (O(∑L)) | (O(N ⋅ L)) |