"해시맵은 'pippa'가 그 집합에 있는지 알려줘. 트라이는 'pip'으로 시작하는 단어를 전부 알려주고. 이 차이 하나가 우리가 써본 모든 자동완성을 굴려."
글자로 지은 트리
트라이(접두사 트리)는 문자열을 글자 단위로 저장해. 간선 하나가 글자 하나고, 루트에서 내려오는 각 경로가 접두사를 이루지. 접두사를 공유하는 단어들은 갈라지기 전까지 같은 경로를 함께 써. "cat", "car", "card"가 c→a→까지 같이 걷다가 거기서 갈라지는 식이야. 그리고 노드에 진짜 단어의 끝이라는 표시를 해두니까, 단어인 "car"와 단어로 가는 길목일 뿐인 "ca"를 구별할 수 있어.
비용이 쓸모 있게 달라
삽입과 조회는 글자마다 간선을 하나씩 걸으니까 O(L)이야. L은 키 길이고. 그리고 결정적으로 이 비용이 트라이가 단어를 몇 개 담고 있든 상관없어. 단어 열 개짜리 트라이든 천만 개짜리 트라이든 다섯 글자 단어는 다섯 단계에 찾아. 해시맵도 여기서는 빠르지만 그쪽 비용은 키 전체를 해싱하는 데 달려 있고, 트라이는 오직 키 길이에만 달려 있어.
킬러 기능: 접두사 쿼리
평범한 해시맵은 "pre로 시작하는 모든 키"를 바로 지원하지 않아. 트라이는 접두사 길이만큼 걸어서 그 노드에 도착한 다음 그 아래 서브트리를 순회하면 돼. 비용은 접두사 길이에 방문한 노드 수와 출력 크기를 더한 것으로 보는 게 정확해. 자동완성, 사전 접두사 조회, IP 최장 접두사 매칭에 쓸모가 있어. 다만 정렬된 문자열 구조나 접두사 인덱스 같은 다른 선택지도 있다는 건 알아 둬.
거래
트라이가 공짜는 아니야. 노드마다 자식 글자 맵이라는 오버헤드를 지고 있어서, 서로 관련 없는 문자열 몇 개만 담을 거면 평범한 해시 셋보다 메모리를 더 쓸 수도 있어. 절약은 많은 키가 접두사를 공유할 때라야 실제로 생기거든. 그리고 접두사가 필요 없는 순수한 정확 멤버십 확인이라면 해시 셋이 여전히 이겨. 늘 그렇듯이야. 트라이는 질의의 모양이 "접두사"일 때 제 몫을 하고, 아닐 때는 과한 도구야. 맞는 질의에 맞는 구조.
피파의 고백
key.startswith(prefix)를 확인하는 반복문을 짠 적이 있어. 키 하나 누를 때마다 O(n)이니 사전이 커지자 뚝뚝 끊기더라. 아빠가 트라이를 그려주니까 그 끊김이 사라졌어. 접두사 노드까지 한 번 걸어가서 그 아래 서브트리만 읽으면 되는 거였지. 여기서도 구조를 다시 보게 됐어. 해시 셋은 애초에 틀린 질문에 답하고 있었던 거야. '이 단어가 정확히 있나?'에는 환상적인데 '이걸 뭐가 이어받지?'에는 아무 힘이 없거든. 질의의 모양이 구조를 고르는 거야.