컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
ds · 08

트라이

자료구조트라이문자열

트라이(Trie)는 문자열을 저장하고 검색하는 데 특화된 트리 자료구조입니다. 각 간선이 문자 하나에 대응하고, 루트에서 어떤 노드까지의 경로가 하나의 문자열(접두사)을 이룹니다. 검색어 자동완성, 맞춤법 검사, IP 라우팅 테이블처럼 공통 접두사를 가진 문자열을 다루는 곳에 사용됩니다.

핵심 개념

노드와 문자 간선

트라이에서는 노드가 아니라 간선이 문자 하나를 나타냅니다. 루트는 빈 문자열이며, 루트에서 임의의 노드까지 내려가는 경로가 곧 하나의 문자열입니다.

핵심 개념

isEnd 플래그

노드가 실제로 저장된 단어의 끝이면 isEnd를 true로 표시합니다. 경로가 존재해도 isEnd가 false면 그 문자열은 저장된 완전한 단어가 아니라 단순 접두사일 뿐입니다.

핵심 개념

삽입 (Insert)

문자를 하나씩 따라가며 이미 있는 노드는 재사용하고, 없는 노드는 새로 만듭니다. 마지막 문자의 노드에 isEnd를 표시하며 완료합니다. 시간복잡도는 단어 길이 L에 비례한 O(L)입니다.

핵심 개념

검색 (Search / contains)

문자를 하나씩 따라가다 경로가 끊기면 즉시 false입니다. 끝까지 도달해도 마지막 노드의 isEnd가 false면 false를 반환합니다. 완전히 일치하는 단어인지 확인합니다.

핵심 개념

접두사 검색 (startsWith)

경로가 끝까지 존재하기만 하면 true입니다. isEnd 여부는 확인하지 않습니다. 자동완성 기능은 이 접두사 검색을 기반으로 후보 단어들을 찾습니다.

핵심 개념

접두사 공유

CAR, CARD, CARE처럼 같은 접두사를 가진 단어들은 그 접두사까지의 경로를 공유합니다. 접두사를 공유하는 단어가 많을수록 해시 테이블 대비 메모리 이점이 생길 수 있습니다.

해시 테이블과의 비교 — 해시 테이블도 문자열을 O(1)~O(L)에 검색할 수 있지만, 접두사 검색(startsWith)은 효율적으로 지원하지 못합니다. 트라이는 접두사 기반 연산이 필요할 때 유리합니다.
시간복잡도 — 삽입·검색·접두사 검색 모두 단어 길이 L에 비례한 O(L)입니다. 저장된 단어의 개수(N)와는 무관하다는 점이 트라이의 핵심 장점입니다(노드에서 자식을 O(1)에 찾을 수 있다는 가정입니다).
공간 트레이드오프 — 각 노드가 자식을 가리키는 포인터(또는 배열/맵)를 들고 있어야 하므로, 저장하는 문자열의 접두사 공유가 적다면 오히려 해시 테이블보다 메모리를 더 사용할 수 있습니다.
활용 사례 — 검색창 자동완성, 맞춤법 검사기, IP 라우팅 테이블(최장 접두사 매칭) 등 "접두사"가 의미를 갖는 곳에서 트라이를 쓸 수 있습니다.

트라이가 필요한 순간

트라이(trie)는 문자열의 접두사를 길로 삼는 트리입니다. '자동완성처럼 이 글자로 시작하는 단어를 모두 찾아라' 같은 접두사 질문에 단어 전체를 훑지 않고 한 갈래만 따라가 답합니다. 이름은 검색을 뜻하는 retrieval에서 왔다고 알려져 있습니다.

숫자로 확인하기

접두사 공유 — CAR, CARD, CARE 세 단어는 문자 합이 3 + 4 + 4 = 11개이지만, 트라이의 문자 노드는 루트를 빼고 C, A, R, D, E 5개뿐입니다.
자식 배열의 메모리 — 영어 소문자용으로 노드마다 26칸의 포인터 배열을 두고 포인터가 8바이트이면 노드 하나가 포인터만 208바이트입니다. 노드가 1,000,000개이면 포인터 배열만 계산해도 약 208MB입니다(64비트 포인터 가정, 다른 필드는 제외).
탐색 비용 — 길이가 10인 문자열을 찾으려면 단어가 천 개든 백만 개든 노드를 최대 10번만 따라갑니다. 노드의 자식을 O(1)에 찾을 수 있다는 가정에서입니다.
IPv4 주소 — 주소가 32비트이므로 비트를 한 글자로 보는 이진 트라이는 루트를 깊이 0으로 할 때 최대 깊이가 32입니다.

자주 하는 오해

트라이의 검색은 O(1)이다 — 문자열 길이 L에 비례하는 O(L)입니다(자식을 배열로 찾는 경우이고, 맵이나 리스트면 알파벳 크기 등이 더해집니다). 해시 테이블도 키 전체를 해시하는 데 O(L)이 듭니다. 달라지는 점은 접두사 연산을 같은 비용으로 지원한다는 것입니다.
트라이는 항상 메모리를 아낀다 — 접두사를 공유하면 줄지만, 공유가 적으면 노드마다 자식 포인터를 들고 있어 해시 테이블보다 오히려 클 수 있습니다.
startsWith와 search는 같은 연산이다 — search는 마지막 노드의 isEnd가 true여야 하고, startsWith는 경로만 있으면 됩니다. 'car'만 저장했다면 'ca'의 startsWith는 true이지만 search는 false입니다.
해시 테이블로는 접두사 검색이 안 된다 — 해시는 키 전체를 해시하므로 접두사만으로 찾을 수 없다는 뜻입니다. 모든 키를 훑거나 접두사별로 따로 저장해야 합니다.

면접에서 이렇게 물어요

면접 질문

자동완성을 트라이로 어떻게 구현하나요?

입력한 접두사를 따라 노드까지 내려간 뒤, 그 노드 아래의 서브트리를 DFS로 훑어 isEnd인 단어를 모읍니다. 후보가 너무 많으면 노드에 빈도나 상위 후보를 미리 저장해 둘 수 있습니다.

면접 질문

트라이의 메모리를 줄이는 방법은 무엇인가요?

자식을 고정 배열 대신 해시맵으로 두거나, 자식이 하나뿐인 체인을 하나의 간선으로 합치는 압축 트라이(radix tree)를 씁니다.

면접 질문

트라이에서 단어를 삭제하려면 어떻게 하나요?

마지막 노드의 isEnd를 끄고, 자식이 없고 다른 단어의 끝도 아닌 노드를 거슬러 올라가며 지웁니다. 다른 단어와 공유하는 노드는 남겨야 합니다.

면접 질문

해시 테이블과 트라이는 언제 각각 쓰나요?

정확히 일치하는 키 조회만 필요하면 해시 테이블이 단순하고 메모리도 적게 듭니다. 접두사 검색, 사전순 순회, 최장 접두사 일치가 필요하면 트라이가 맞습니다.

실제 시스템에서는

Linux 커널의 라우팅 테이블 — Linux 커널의 IPv4 경로 테이블(FIB)은 LC-트라이 계열(경로·레벨 압축 트라이)로 구현되어 있고(net/ipv4/fib_trie.c), 커널 문서의 구현 노트가 레벨 압축과 최장 접두사 일치 조회를 설명합니다. 목적지 주소와 가장 길게 일치하는 접두사를 찾는 용도입니다. 라우팅은 라우팅 토픽에서 봅니다.
문자열 접두사 질의 — 데이터베이스의 B-Tree 인덱스도 접두사가 고정된 문자열 검색을 지원합니다(PostgreSQL 문서의 LIKE 'foo%' 조건). 구조는 다르지만 같은 질문을 풉니다. 자세한 내용은 인덱스 토픽에서 봅니다.

함께 보면 좋은 토픽

트리 — 트라이의 바탕이 되는 트리 구조를 봅니다.
해시 테이블 — 문자열 키를 다루는 다른 방법을 비교합니다.
라우팅 — 최장 접두사 일치가 쓰이는 곳을 봅니다.
인덱스 — 접두사 검색을 지원하는 인덱스를 봅니다.
BFS / DFS — 자동완성에서 서브트리를 훑는 방식을 봅니다.

참고 자료

PostgreSQL 인덱스 종류 — Index Types — PostgreSQL Documentation
논문 — Fredkin, "Trie Memory", Communications of the ACM 3(9), 1960 (이 논문에서 'trie'라는 이름으로 소개됨)
논문 — Nilsson, Karlsson, "IP-address lookup using LC-tries", IEEE Journal on Selected Areas in Communications 17(6), 1999 (리눅스 커널 소스 net/ipv4/fib_trie.c 주석이 인용)

Java — 트라이 노드 정의

/* ── 트라이 노드: 문자 → 자식 노드 맵 + 단어 끝 표시 ─────── */
public class TrieNode {
    Map<Character, TrieNode> children = new HashMap<>();
    boolean isEnd = false;   // 이 노드에서 끝나는 완전한 단어가 있는지 여부
}

public class Trie {
    private final TrieNode root = new TrieNode();
}

노드 자체는 문자를 담지 않고, children 맵의 key(문자)가 간선을 나타냅니다.
즉 "문자는 간선에, 도착 여부는 노드에" 저장하는 구조입니다.
isEnd는 경로가 존재하는 것과 완전한 단어인 것을 구분하는 핵심 플래그입니다 — 예를 들어 "CARD"를 삽입하면 C→A→R 경로 위의 R 노드는 "CAR"의 끝이자 동시에 "CARD"로 가는 중간 경로이기도 합니다.

Java — 삽입 (Insert)

/* ── 삽입: 문자를 하나씩 따라가며 없는 노드는 새로 생성 ──── */
public void insert(String word) {
    TrieNode cur = root;
    for (char ch : word.toCharArray()) {
        cur = cur.children.computeIfAbsent(ch, c -> new TrieNode());
    }
    cur.isEnd = true;   // 마지막 문자 노드에 단어의 끝 표시
}

computeIfAbsent 한 줄이 "이미 있으면 재사용, 없으면 새로 생성"을 모두 처리합니다 — 시각화의 삽입 탭에서 본 보라색(신규 생성)·주황색(재사용) 구분이 바로 이 지점입니다.
시간복잡도는 단어 길이 L에 비례한 O(L)이며, 트라이에 이미 저장된 단어 개수와는 무관합니다.

Java — 검색 (Search / contains)

/* ── 검색: 경로가 존재하고, 마지막 노드가 isEnd=true인지 확인 ─ */
public boolean search(String word) {
    TrieNode node = findNode(word);
    return node != null && node.isEnd;
}

/* ── 경로만 따라가는 공통 헬퍼 (search / startsWith 공용) ──── */
private TrieNode findNode(String s) {
    TrieNode cur = root;
    for (char ch : s.toCharArray()) {
        cur = cur.children.get(ch);
        if (cur == null) return null;   // 경로 중간에 끊김 → 즉시 실패
    }
    return cur;
}

search("CA")는 경로(C→A)는 존재하지만 A 노드의 isEnd가 false이므로 false를 반환합니다 — "경로 존재"와 "완전한 단어"는 다른 개념이라는 점이 트라이 검색의 핵심입니다.
시간복잡도는 O(L)로, 해시 테이블의 평균 O(1)~O(L)과 비슷한 수준입니다.

Java — 접두사 검색 (startsWith)

/* ── 접두사 검색: 경로만 끝까지 있으면 true (isEnd 확인 안 함) ─ */
public boolean startsWith(String prefix) {
    return findNode(prefix) != null;
}

search와 startsWith는 같은 findNode 헬퍼를 재사용하되, 마지막에 isEnd를 확인하느냐만 다릅니다.
예를 들어 startsWith("CA")는 경로만 있으면 되므로 true지만, 앞서 본 search("CA")는 isEnd가 false라서 false입니다 — 자동완성 기능은 이 startsWith를 이용해 후보 단어를 좁혀 나갑니다.
해시 테이블은 이런 접두사 기반 조회를 지원하지 못하므로, 이 지점이 트라이가 해시 테이블보다 우위를 갖는 대표적인 이유입니다.

Q1

트라이(Trie)의 주요 용도는?

같은 접두사를 가진 문자열들이 경로를 공유합니다.
트라이는 문자열을 문자 단위로 트리 경로에 저장해, 공통 접두사를 가진 단어들이 경로를 공유하므로 접두사 검색(자동완성 등)에 효율적입니다.

Q2

트라이에서 'isEnd' 표시가 필요한 이유는?

'app'과 'apple'을 둘 다 저장하면 'app' 경로 중간에 단어 끝 표시가 필요합니다.
isEnd 표시가 없으면 'app'이 저장된 단어인지, 단순히 'apple'의 경로 중간 지점인지 구분할 수 없습니다.

Q3

트라이에서 문자열 검색(contains)의 시간 복잡도는(알파벳 크기 무시 시)?

한 글자씩 경로를 따라 내려갑니다.
트라이 검색은 찾으려는 문자열의 길이만큼 경로를 따라 내려가면 되므로, 저장된 단어 개수와 무관하게 O(문자열 길이)입니다.