트라이
트라이(Trie)는 문자열을 저장하고 검색하는 데 특화된 트리 자료구조입니다. 각 간선이 문자 하나에 대응하고, 루트에서 어떤 노드까지의 경로가 하나의 문자열(접두사)을 이룹니다. 검색어 자동완성, 맞춤법 검사, IP 라우팅 테이블처럼 공통 접두사를 가진 문자열을 다루는 곳에 사용됩니다.
핵심 개념
노드와 문자 간선
트라이에서는 노드가 아니라 간선이 문자 하나를 나타냅니다. 루트는 빈 문자열이며, 루트에서 임의의 노드까지 내려가는 경로가 곧 하나의 문자열입니다.
핵심 개념
isEnd 플래그
노드가 실제로 저장된 단어의 끝이면 isEnd를 true로 표시합니다. 경로가 존재해도 isEnd가 false면 그 문자열은 저장된 완전한 단어가 아니라 단순 접두사일 뿐입니다.
핵심 개념
삽입 (Insert)
문자를 하나씩 따라가며 이미 있는 노드는 재사용하고, 없는 노드는 새로 만듭니다. 마지막 문자의 노드에 isEnd를 표시하며 완료합니다. 시간복잡도는 단어 길이 L에 비례한 O(L)입니다.
핵심 개념
검색 (Search / contains)
문자를 하나씩 따라가다 경로가 끊기면 즉시 false입니다. 끝까지 도달해도 마지막 노드의 isEnd가 false면 false를 반환합니다. 완전히 일치하는 단어인지 확인합니다.
핵심 개념
접두사 검색 (startsWith)
경로가 끝까지 존재하기만 하면 true입니다. isEnd 여부는 확인하지 않습니다. 자동완성 기능은 이 접두사 검색을 기반으로 후보 단어들을 찾습니다.
핵심 개념
접두사 공유
CAR, CARD, CARE처럼 같은 접두사를 가진 단어들은 그 접두사까지의 경로를 공유합니다. 접두사를 공유하는 단어가 많을수록 해시 테이블 대비 메모리 이점이 생길 수 있습니다.
트라이가 필요한 순간
트라이(trie)는 문자열의 접두사를 길로 삼는 트리입니다. '자동완성처럼 이 글자로 시작하는 단어를 모두 찾아라' 같은 접두사 질문에 단어 전체를 훑지 않고 한 갈래만 따라가 답합니다. 이름은 검색을 뜻하는 retrieval에서 왔다고 알려져 있습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
자동완성을 트라이로 어떻게 구현하나요?
입력한 접두사를 따라 노드까지 내려간 뒤, 그 노드 아래의 서브트리를 DFS로 훑어 isEnd인 단어를 모읍니다. 후보가 너무 많으면 노드에 빈도나 상위 후보를 미리 저장해 둘 수 있습니다.
면접 질문
트라이의 메모리를 줄이는 방법은 무엇인가요?
자식을 고정 배열 대신 해시맵으로 두거나, 자식이 하나뿐인 체인을 하나의 간선으로 합치는 압축 트라이(radix tree)를 씁니다.
면접 질문
트라이에서 단어를 삭제하려면 어떻게 하나요?
마지막 노드의 isEnd를 끄고, 자식이 없고 다른 단어의 끝도 아닌 노드를 거슬러 올라가며 지웁니다. 다른 단어와 공유하는 노드는 남겨야 합니다.
면접 질문
해시 테이블과 트라이는 언제 각각 쓰나요?
정확히 일치하는 키 조회만 필요하면 해시 테이블이 단순하고 메모리도 적게 듭니다. 접두사 검색, 사전순 순회, 최장 접두사 일치가 필요하면 트라이가 맞습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
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' 표시가 필요한 이유는?
Q3
트라이에서 문자열 검색(contains)의 시간 복잡도는(알파벳 크기 무시 시)?