알고리즘 입문
개념 배우기직접 풀어보기
놀면 뭐 하니,
그냥 하는거야.

글자를 가지처럼! 트라이

코딩냥

안녕, 나 코딩냥이다냥! 오늘은 문자열을 담는 나무를 배운다냥. 이름이 트라이라냥. 단어들이 앞 글자를 공유하면 그 부분을 한 번만 저장한다냥.

자동완성, 사전 검색, 전화번호부가 전부 트라이라냥.

오늘의 학습 목표

  • 트라이가 어떻게 생겼는지 안다.
  • 단어를 삽입한다.
  • 접두사 검색과 완전 일치를 구분한다.
  • 각 노드에 개수를 세어 접두사 통계를 낸다.
  • 접두사 관계를 판별한다.

1단계 · 트라이가 뭐냥 (10분)

코딩냥

트라이는 글자마다 가지가 갈라지는 나무라냥. 루트에서 시작해서, 한 글자씩 따라 내려가면 단어가 완성된다냥.

비유로 이해하기 — 사전의 색인

사전에서 "cat" 을 찾으려면 c 페이지로 가고, 그 안에서 ca, 다시 cat 으로 좁혀간다냥. car 도 ca 까지는 같은 길을 쓴다냥.

트라이는 이 "같은 앞부분은 같은 길" 을 그대로 나무로 만든 거라냥. cat 과 car 은 c → a 까지 한 길을 공유하고, 거기서 t 와 r 로 갈라진다냥.

파이썬에서는 중첩 딕셔너리로 아주 쉽게 만든다냥. 각 노드가 "다음 글자 → 자식 노드" 딕셔너리라냥.

root = {}     # 루트는 빈 딕셔너리라냥

2단계 · 단어 삽입 (12분)

코딩냥

단어를 넣는 건 글자를 따라 내려가며 없으면 만드는 거라냥.

def insert(root, word):
    node = root
    for ch in word:
        node = node.setdefault(ch, {})   # 없으면 만들고 내려간다냥
    node["$"] = True                     # 단어 끝 표시라냥
핵심끝 표시가 중요하다냥

글자를 다 따라 내려간 마지막 노드에 "여기서 단어가 끝난다" 는 표시를 남긴다냥. 위에서는 "$" 를 썼다냥 (글자와 안 겹치는 아무 표시나 된다냥).

이 끝 표시가 있어야 "cat" 이라는 단어가 있다와 "cat" 은 그냥 "cats" 의 앞부분일 뿐이다 를 구분할 수 있다냥.

setdefault(ch, {}) 는 "ch 가 있으면 그걸 주고, 없으면 빈 딕셔너리를 넣고 그걸 준다냥." 트라이 삽입에 딱 맞는 도구라냥.

3단계 · 접두사냐 완전 일치냐 (12분)

코딩냥

트라이의 검색은 두 종류라냥. 글자를 다 따라갈 수 있냐(접두사)와, 거기에 끝 표시가 있냐(완전 일치)라냥.

def find_node(root, s):
    node = root
    for ch in s:
        if ch not in node:
            return None          # 길이 끊기면 없는 거라냥
        node = node[ch]
    return node
구분같은 내려가기, 다른 판정
  • 접두사 검색 — find_node 가 None 이 아니면, s 로 시작하는 단어가 있는 거라냥
  • 완전 일치 — 거기에 더해 "$" in node 여야 s 가 진짜 단어라냥

끝 표시를 확인 안 하면 접두사를 단어로 착각한다냥.

사전에 "apple" 만 있는데 "appl" 을 물으면, 글자는 다 따라가진다냥. 하지만 "appl" 자리에는 끝 표시가 없다냥. 끝 표시를 안 보면 "있다" 고 잘못 답한다냥.

오늘 1번(접두사 개수)과 2번(완전 일치)이 바로 이 차이라냥. 같은 트라이인데 끝 표시를 보느냐 마느냐로 갈린다냥.

4단계 · 노드마다 개수를 세면 (12분)

코딩냥

"이 접두사로 시작하는 단어가 몇 개냥?" 을 빠르게 답하려면, 삽입할 때 지나가는 노드마다 카운트를 올려두면 된다냥.

def insert(root, word):
    node = root
    for ch in word:
        node = node.setdefault(ch, {"#": 0})
        node["#"] += 1        # 이 노드를 지나간 단어 수라냥
핵심지나간 단어 수 = 접두사 개수라냥

어떤 노드의 "#" 는 그 노드까지의 글자를 접두사로 갖는 단어의 수라냥. 그래서 접두사 질의는 그 접두사 끝 노드의 "#" 를 읽기만 하면 된다냥.

단어가 많고 질의가 많아도, 질의마다 접두사 길이만큼만 내려가면 되니 아주 빠르다냥. 오늘 1번 문제라냥.

"서로 다른 접두사가 모두 몇 개냥?" 은 곧 트라이의 노드 수(루트 제외) 라냥. 삽입 중 새 노드를 만들 때마다 세면 된다냥. 오늘 4번 문제라냥.

5단계 · 접두사 관계 판별 (12분)

코딩냥

전화번호부에서 어떤 번호가 다른 번호의 앞부분이면 문제가 생긴다냥. 911 로 걸려는데 9112 도 있으면 헷갈린다냥. 트라이로 이걸 잡는다냥.

삽입하면서 두 가지를 본다냥.

단어 w 를 트라이에 넣는 도중에 —

  • 끝 표시를 지나치면 → 더 짧은 단어가 w 의 접두사라냥
  • 다 넣은 노드에 이미 자식이 있으면 → w 가 더 긴 단어의 접두사라냥

둘 중 하나라도 걸리면 접두사 관계가 있는 거라냥.

node = root
for ch in w:
    if "$" in node:            # 더 짧은 단어가 여기서 끝났다냥
        conflict = True
    node = node.setdefault(ch, {})
if len(node) > 0:              # 내 끝에 이미 자식이 있다냥
    conflict = True
node["$"] = True

넣는 순서에 상관없이 이 두 검사를 매 단어마다 하면 접두사 관계를 놓치지 않는다냥. 짧은 걸 먼저 넣든 긴 걸 먼저 넣든, 둘 중 한 검사에 걸린다냥. 오늘 3번 문제라냥.

오늘 배운 내용 정리

코딩냥

먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!

Q1.트라이가 어떻게 생겼냥?
Q2.단어를 삽입할 때 끝 표시는 왜 남기냥?
Q3.접두사 검색과 완전 일치는 뭐가 다르냥?
Q4.접두사로 시작하는 단어의 수를 어떻게 빨리 구하냥?
Q5.어떤 번호가 다른 번호의 접두사인지 어떻게 판별하냥?

이제 직접 풀어볼 차례다냥

코딩냥

오늘 문제는 다섯 개라냥. 1번 접두사 개수, 2번 완전 일치(1번과 같은 트라이, 끝 표시만 다르다냥), 3번 전화번호 목록, 4번 서로 다른 접두사 수, 5번 가장 긴 사전 접두사라냥.

트라이 삽입을 한 번 짜두면 나머지는 내려가며 무엇을 보느냐만 바뀐다냥. 가보자냥~