
본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
일반적인 Tree에 대한 전반적인 내용들은 다음 포스팅에서 확인하실 수 있습니다.
Trie(트라이)는 주로 문자열을 저장하고 검색하는 데 특화된 트리 기반의 자료구조입니다.

노드 구조:
공통 접두사 공유:
시간 복잡도:
L은 문자열의 길이입니다.문자열 단위의 중복 불가:
Trie는 문자열 검색에 매우 효율적이며, 특히 접두사 기반 검색(Prefix Search)에 강점을 가집니다.Trie는 효율적인 사전(dictionary)을 구축하는 데 유용하며, 대규모의 단어 목록을 처리하는 데 적합합니다.장점:
해시테이블과는 달리 충돌을 걱정할 필요가 없습니다.단점:
Trie 자료구조만을 위한 별도의 클래스는 없기 때문에, 직접 구현해서 사용하셔야합니다.구현 및 테스트 코드의 전체 코드는 다음과 같습니다.
import java.util.HashMap;
import java.util.Map;
public class Trie {
// Trie Node 정의
static class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isEndOfWord = false; // 해당 노드가 단어의 끝을 나타내는지 여부
}
private final TrieNode root;
// 생성자: Trie 초기화
public Trie() {
root = new TrieNode();
}
// 단어 삽입
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.children.computeIfAbsent(c, k -> new TrieNode());
}
node.isEndOfWord = true;
}
// 단어 검색
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.children.get(c);
if (node == null) {
return false; // 해당 경로에 문자가 없으면 단어가 존재하지 않음
}
}
return node.isEndOfWord;
}
// 접두사 검색
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
node = node.children.get(c);
if (node == null) {
return false; // 접두사가 존재하지 않음
}
}
return true;
}
// 테스트용 메인 메소드
public static void main(String[] args) {
Trie trie = new Trie();
// 단어 삽입
trie.insert("apple");
trie.insert("app");
trie.insert("banana");
// 단어 검색
System.out.println(trie.search("apple")); // true
System.out.println(trie.search("app")); // true
System.out.println(trie.search("ban")); // false
// 접두사 검색
System.out.println(trie.startsWith("app")); // true
System.out.println(trie.startsWith("ban")); // true
System.out.println(trie.startsWith("can")); // false
}
}
그럼 각 단계별로 나누어서 구현 과정을 자세히 살펴보도록 하겠습니다.
Trie 자료구조에서 각 노드는 하나의 문자를 나타내며, 자식 노드와 단어의 끝을 표시하는 플래그가 필요합니다. 이를 위해 TrieNode 정적 클래스를 정의합니다.

static class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isEndOfWord = false; // 단어의 끝을 나타냄
}
children: 각 노드는 자식 노드를 가질 수 있으며, 각 자식은 현재 노드에서 이어지는 문자에 해당합니다.HashMap을 활용합니다.isEndOfWord: 이 플래그는 현재 노드가 하나의 단어의 끝을 나타내는지 여부를 나타냅니다.이제 Trie 클래스의 기본 구조를 정의하고, 루트 노드를 초기화합니다.

public class Trie {
private final TrieNode root;
// 생성자: Trie 초기화
public Trie() {
root = new TrieNode(); // 루트 노드는 공백 노드로 시작
}
}
root: Trie는 항상 루트에서 시작되며, 루트 노드는 공백 노드로 정의됩니다.다음으로, 문자열을 Trie에 삽입하는 insert 메소드를 구현합니다.

public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.children.computeIfAbsent(c, k -> new TrieNode());
}
node.isEndOfWord = true;
}
computeIfAbsent: 현재 문자가 자식 노드에 존재하지 않으면 새로운 TrieNode를 추가합니다.HashMap의 메서드입니다.isEndOfWord를 true로 설정하여 단어가 완성되었음을 표시합니다.이제 Trie에서 특정 단어가 존재하는지 확인하는 search 메소드를 구현합니다.
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.children.get(c);
if (node == null) {
return false; // 문자가 존재하지 않으면 단어가 없는 것
}
}
return node.isEndOfWord;
}
false를 반환합니다.isEndOfWord가 true여야만 단어가 존재하는 것입니다.접두사(Prefix)로 시작하는 단어가 존재하는지 확인하기 위해 startsWith 메소드를 구현합니다.
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
node = node.children.get(c);
if (node == null) {
return false; // 접두사가 존재하지 않으면 false
}
}
return true;
}
false를 반환합니다.true를 반환합니다.마지막으로 main 메소드를 통해 Trie 기능을 테스트합니다.

public static void main(String[] args) {
Trie trie = new Trie();
// 단어 삽입
trie.insert("apple");
trie.insert("app");
trie.insert("banana");
// 단어 검색
System.out.println(trie.search("apple")); // true
System.out.println(trie.search("app")); // true
System.out.println(trie.search("ban")); // false
// 접두사 검색
System.out.println(trie.startsWith("app")); // true
System.out.println(trie.startsWith("ban")); // true
System.out.println(trie.startsWith("can")); // false
}
Trie의 특성을 활용해서 구현하실 수 있습니다.Trie 자료구조만을 위한 별도의 라이브러리는 없기 때문에, 직접 구현해서 사용하셔야합니다.구현 및 테스트 코드의 전체 코드는 다음과 같습니다.
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
# 단어 삽입
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
# 단어 검색
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
# 접두사 검색
def starts_with(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
# 테스트
trie = Trie()
# 단어 삽입
trie.insert("apple")
trie.insert("app")
trie.insert("banana")
# 단어 검색
print(trie.search("apple")) # True
print(trie.search("app")) # True
print(trie.search("ban")) # False
# 접두사 검색
print(trie.starts_with("app")) # True
print(trie.starts_with("ban")) # True
print(trie.starts_with("can")) # False
그럼 각 단계별로 나누어서 구현 과정을 자세히 살펴보도록 하겠습니다.
Trie 자료구조에서 각 노드는 하나의 문자를 나타내며, 자식 노드와 단어의 끝을 표시하는 플래그가 필요합니다. 이를 위해 TrieNode 클래스를 정의합니다.

class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
children: 각 노드는 자식 노드를 가질 수 있으며, 각 자식은 현재 노드에서 이어지는 문자에 해당합니다.children은 딕셔너리로 구현되어 문자를 키로, 노드를 값으로 저장합니다.is_end_of_word: 이 플래그는 현재 노드가 하나의 단어의 끝을 나타내는지 여부를 나타냅니다.이제 Trie 클래스의 기본 구조를 정의하고, 루트 노드를 초기화합니다.

class Trie:
def __init__(self):
self.root = TrieNode() # 루트 노드는 공백 노드로 시작
root: Trie는 항상 루트에서 시작되며, 루트 노드는 공백 노드로 정의됩니다.다음으로, 문자열을 Trie에 삽입하는 insert 메소드를 구현합니다. 문자열의 각 문자를 순차적으로 탐색하며, 노드가 존재하지 않으면 새 노드를 추가합니다.

def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
children: 문자가 자식 노드에 없으면, 새로운 TrieNode를 추가합니다.is_end_of_word를 True로 설정하여 단어가 완성되었음을 표시합니다.이제 Trie에서 특정 단어가 존재하는지 확인하는 search 메소드를 구현합니다.
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False # 문자가 존재하지 않으면 단어가 없는 것
node = node.children[char]
return node.is_end_of_word
False를 반환합니다.is_end_of_word가 True여야만 단어가 존재하는 것입니다.접두사(Prefix)로 시작하는 단어가 존재하는지 확인하기 위해 starts_with 메소드를 구현합니다.
def starts_with(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False # 접두사가 존재하지 않으면 False
node = node.children[char]
return True
False를 반환합니다.True를 반환합니다.마지막으로 Trie의 기능을 테스트합니다.

# 테스트
trie = Trie()
# 단어 삽입
trie.insert("apple")
trie.insert("app")
trie.insert("banana")
# 단어 검색
print(trie.search("apple")) # True
print(trie.search("app")) # True
print(trie.search("ban")) # False
# 접두사 검색
print(trie.starts_with("app")) # True
print(trie.starts_with("ban")) # True
print(trie.starts_with("can")) # False
Trie의 성능을 분석할 때는 주로 삽입, 검색, 접두사 검색의 시간 복잡도와 공간 복잡도를 고려합니다.
| 연산 | 시간 복잡도 | 설명 |
|---|---|---|
| 삽입 | O(L) | 단어의 길이 L에 비례하여 각 문자를 노드에 추가합니다. |
| 검색 | O(L) | 단어의 길이 L에 비례하여 각 문자를 순차적으로 탐색합니다. |
| 접두사 검색 | O(L) | 접두사 길이 L에 비례하여 각 문자를 순차적으로 탐색합니다. |
L은 단어의 길이입니다. 일반적인 경우, 각 연산은 단어의 길이에 비례하여 시간이 걸리며, 일반적으로 상수 시간에 비해 크기가 크지 않기 때문에 효율적입니다.
Trie의 공간 복잡도는 저장되는 단어의 수와 각 단어의 문자 수에 따라 결정됩니다.
최악의 경우: O(N * L)
여기서 N은 저장된 단어의 개수, L은 평균적인 단어의 길이를 의미합니다. 단어들이 공통 접두사를 공유하지 않으면 각 문자가 별도의 노드로 저장되어 공간 복잡도가 증가합니다.
최선의 경우: O(N)
모든 단어가 동일한 접두사를 가질 경우, Trie는 매우 압축된 형태가 되어 공간 복잡도가 감소합니다.
Trie는 단어의 접두사를 공유하는 만큼 메모리 사용을 최적화할 수 있지만, 자식 노드가 많은 경우 공간 복잡도가 커질 수 있습니다.
Trie의 기본 구조를 바탕으로 여러 가지 변형이 존재합니다.
압축된 Trie는 단일 자식을 가진 노드들을 하나로 합쳐서 저장하는 방식입니다. 삼진 탐색 트리는 Trie와 비슷한 구조지만, 각 노드는 3개의 자식 노드(작음, 같음, 큼)를 가집니다. Trie의 공간 복잡도를 줄이기 위한 방법으로, 각 노드에 3개의 자식만 존재합니다.Trie의 대체 자료구조로 자주 사용됩니다.Patricia Trie는 압축된 Trie의 일종으로, 비트 수준에서 문자열을 처리합니다. 이번 포스팅에서는 Trie 자료구조의 개념과 주요 특징을 살펴보고, Java와 Python에서 각각의 구현 방식을 단계별로 살펴보았습니다.
Trie의 시간 및 공간 복잡도에 대해 분석하고, 압축된 Trie, 삼진 탐색 트리, Patricia Trie와 같은 변형들을 통해 Trie의 확장 가능성도 함께 알아보았습니다.Trie는 특히 접두사 검색이나 자동 완성 기능에 매우 유용한 자료구조로, 효율적인 문자열 처리에 강점을 가지고 있습니다.