Foundry
자료구조
중급
핵심

트라이 (Trie)

문자열을 문자 단위로 나눠 트리에 담아, 접두사 검색을 문자열 길이에 비례하는 시간으로 처리하는 자료구조

트라이 (Trie)

문자열을 문자 하나씩 트리의 간선에 담아, 공통 접두사를 한 경로로 공유하는 자료구조

왜 필요한가

"자바"로 시작하는 단어를 모두 찾는 작업을 생각해봅시다.

방법시간문제
전체 순회하며 비교단어 수에 비례단어가 100만 개면 100만 번 비교
정렬 후 이진 탐색로그 시간접두사 범위를 찾을 수 있지만 삽입이 비싸다
해시 테이블상수 시간정확히 일치하는 것만 찾는다. 접두사 검색 불가
트라이접두사 길이에 비례단어가 몇 개든 "자바" 3글자만 따라가면 된다

마지막 줄이 핵심입니다. 저장된 단어 수와 무관하게 찾는 문자열의 길이만큼만 내려갑니다.

구조

"cat", "car", "card" 를 담으면 이렇게 됩니다.

        (root)
          |
          c
          |
          a
         / \
        t   r
       (끝)  |\
            (끝) d
                (끝)

"car" 와 "card" 가 c-a-r 경로를 공유합니다. 공통 접두사가 길수록 메모리를 아낍니다.

연산

연산시간설명
삽입문자열 길이에 비례문자마다 한 칸 내려간다
검색문자열 길이에 비례같다
접두사 검색접두사 길이 + 결과 수그 아래를 모두 모은다
삭제문자열 길이에 비례자식이 없는 노드를 지우며 올라온다

해시 테이블도 검색이 문자열 길이에 비례합니다(해시를 계산해야 하므로). 트라이의 고유한 값은 접두사 검색과 정렬 순서입니다.

쓰이는 곳

용도이유
검색어 자동완성입력 중인 접두사로 후보를 찾는다
사전과 맞춤법 검사존재 여부와 비슷한 단어 찾기
IP 라우팅 테이블가장 긴 접두사 일치
금칙어 필터여러 단어를 한 번에 훑는다

한계

메모리를 많이 씁니다. 노드마다 자식 포인터 배열을 두면 알파벳 26개짜리 배열이 노드마다 생깁니다. 실무에서는 이렇게 줄입니다.

방법내용
해시맵으로 자식 관리있는 자식만 담는다
압축 트라이자식이 하나뿐인 경로를 하나의 노드로 합친다
이중 배열 트라이배열 두 개로 표현해 메모리를 크게 줄인다
면접에서 이렇게 나옵니다

Q.트라이가 해시 테이블보다 나은 점은 무엇인가요?

접두사로 찾을 수 있고, 정렬 순서가 유지됩니다.

항목해시 테이블트라이
정확히 일치 검색상수 시간문자열 길이에 비례
접두사 검색불가. 전체를 훑어야 한다접두사 길이만큼 내려가면 된다
정렬 순서없다순회하면 사전순으로 나온다
공통 접두사 메모리각 문자열을 따로 저장경로를 공유한다
최악의 경우충돌이 몰리면 느려진다문자열 길이로 보장된다

자동완성이 트라이를 쓰는 이유가 두 번째 줄입니다. 해시 테이블로 "자바"로 시작하는 단어를 찾으려면 저장된 모든 키를 확인해야 합니다.

다섯 번째 줄도 실무에서 의미가 있습니다. 해시는 평균이 상수 시간이지만 최악은 그렇지 않고, 트라이는 데이터 분포와 무관하게 문자열 길이로 보장됩니다.

흔한 실수: 트라이가 검색이 더 빠르다고 답하는 것. 정확히 일치하는 검색은 해시 테이블이 대개 빠릅니다. 트라이의 값은 속도가 아니라 접두사와 순서입니다.

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

접두사까지 내려간 뒤, 그 아래를 모아 인기순으로 정렬해 돌려줍니다.

순서동작
1입력된 접두사를 따라 노드를 찾는다
2그 노드 아래의 완성된 단어를 모은다
3인기도나 최근성으로 정렬해 상위 N개를 준다

2번이 그대로 하면 비쌉니다. "ㅅ" 하나만 입력해도 그 아래 수십만 단어를 다 모으게 됩니다. 그래서 이렇게 줄입니다.

방법내용
노드에 상위 N개 미리 저장각 노드가 자기 아래의 인기 검색어 10개를 들고 있는다
최소 접두사 길이2글자 이상 입력해야 응답한다
결과 캐시인기 접두사의 결과를 캐시한다

첫 번째가 실제 서비스가 쓰는 방식입니다. 조회 시점에 모으는 대신 갱신 시점에 미리 계산해 둡니다.

흔한 실수: 매 입력마다 서버를 부르는 것. 타이핑 속도로 요청이 나가면 서버가 감당하지 못합니다. 입력이 멈춘 뒤 잠시 기다려 보내고, 이전 요청은 취소해야 합니다.

Q.트라이의 메모리 문제를 어떻게 줄이나요?

자식 포인터를 어떻게 담는지, 경로를 얼마나 합치는지로 줄입니다.

방법내용대가
해시맵으로 자식 관리배열 26칸 대신 있는 자식만접근이 배열보다 조금 느리다
압축 트라이자식이 하나뿐인 경로를 한 노드로 합친다삽입 시 노드를 쪼개야 한다
이중 배열 트라이두 개의 정수 배열로 표현구현이 복잡하고 갱신이 어렵다
정렬 배열 + 이진 탐색접두사 범위를 이진 탐색으로 찾는다삽입이 비싸다. 읽기 전용이면 유리

문제의 크기를 보면 이렇습니다. 노드마다 26칸 포인터 배열을 두면 노드 하나가 200바이트가 넘습니다. 단어 100만 개에 평균 8글자면 노드가 수백만 개라 메모리가 기가바이트 단위로 갑니다.

흔한 실수: 압축 트라이를 모든 경우의 답으로 보는 것. 공통 접두사가 많은 데이터에서는 합칠 경로가 적어 효과가 작습니다. 반대로 무작위 문자열이라면 크게 줄어듭니다.

Q.가장 긴 접두사 일치는 어디에 쓰이나요?

여러 규칙 중 가장 구체적인 것을 골라야 할 때 씁니다.

용도규칙 예고르는 것
IP 라우팅10.0.0.0/8, 10.1.0.0/16더 좁은 범위(/16)
URL 라우팅/api, /api/users더 긴 경로
전화번호 요금82, 8210더 긴 국번
금칙어 필터특정 단어와 그 확장형더 긴 일치

트라이가 이 문제에 맞는 이유가 있습니다. 접두사를 따라 내려가면서 지나온 노드 중 규칙이 있는 마지막 지점을 기억하면 됩니다. 한 번의 순회로 답이 나옵니다.

규칙을 모두 비교하는 방식은 규칙 수에 비례하고, 트라이는 키 길이에 비례합니다. 라우팅 테이블에 수십만 개 규칙이 있어도 IP 32비트만 따라가면 됩니다.

흔한 실수: 규칙을 길이순으로 정렬해 순차 비교하는 것. 동작하지만 규칙이 늘면 그만큼 느려집니다. 라우터가 초당 수백만 패킷을 처리해야 하는 이유로 트라이 계열 구조를 씁니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

더 깊이 공부하기

읽었으면 문제로 확인해보세요

자료구조 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.