트라이(Trie)란 문자열을 저장하고 효율적으로 탐색하기 위한 트리 구조의 자료구조를 말합니다.
트라이는 래딕스 트리(Radix Tree), 접두사 트리(Prefix Tree), 혹은 탐색 트리(retrieval tree)라고도 합니다.
여기서 트라이의 Trie
가 Retrieval
에서 따온 문자라는 말도 있답니다!
이름에서도 알 수 있듯 트라이는 문자열 탐색🔍에서 굉장히 효율적인 구조를 가지고 있기 떄문에, 검색어 자동 완성, 사전에서 찾기, 문자열 검사 등에 활용이 된다고 합니다.
🙋♀️ 문자열 집합 {'bag', 'bad', 'car'}
를 트라이로 구현해봅시다!
위 사진과 같이, 트라이는 한 문자열에서 다음에 나오는 문자가 현재 문자의 자식 노드가 되는 형태로 구성되어 있습니다.
즉, 트리의 head(root)에서부터 자식들을 따라가면서 생성된 문자열이 트라이 자료구조에 저장되어 있다고 볼 수 있습니다.
그리고 저장된 단어는 해당 단어의 마지막 글자에 함께 저장되어, 저장된 단어의 끝임을 알 수 있습니다.
이러한 트라이의 형태로 알 수 있는 중요한 속성 하나가 있는데, 그것은 바로 루트에서부터 내려가는 경로에서 만나는 글자들을 모으면 찾고자 하는 문자열의 접두사를 얻을 수 있다는 것입니다.
문자열 검색을 빠르게 할 수 있습니다.
문자열 탐색 시, 존재하는 모든 문자열을 하나하나씩 비교하여 탐색하는 것보다 훨씬 효율적인 시간복잡도를 가질 수 있습니다.
👉 만약 'bad'라는 단어를 탐색하고 싶다면, 제일 먼저 'b'를 찾고, 'b'의 자식들중에서 'a'를 찾고, 또 다시 'a'의 자식들 중에서 'd'를 찾는 식으로 탐색을 하면 됩니다.
🙋♀️ {'bag', 'bad' 'car'}
문자열을 순서대로 트라이에 추가해봅시다!
'bag'
추가하기가장 위의 노드는 Head
입니다. 따라서, Key
와 Data
는 None
이고, Child
의 값만 가집니다.
처음에 트라이는 Head
만 존재하는 상태이기 때문에, 'bag'
를 차례로 추가해주면 됩니다.
Head
에는 Child
에 'b'
를, Key가 'b'
인 노드는 Child
로 'a'
를, Key
가 'a'
인 노드는 Child
로 'g'
를 넣어줍니다.
그리고, Key
가 'g'
인 노드는 문자열이 끝났기 때문에 더이상 Child
는 존재하지 않으며, 대신 문자열의 마지막임을 알리기 위하여 Data
에 지금까지의 문자열인 'bag'
를 추가해줍니다.
'bad'
추가하기이미 트라이에는 'b'
와 'a'
가 존재합니다.
따라서, 따로 추가하지 않고 각 노드를 따라 아래로 이동합니다.
Key
가 'a'
인 노드의 Child
에 'd'
를 추가해줍니다. 그리고, Key
가 'd'
노드는 존재하지 않으므로, 'd'
노드를 추가해줍니다.
Key
가 'd'
노드에서 문자열이 끝났기 때문에 더이상 Child
는 존재하지 않습니다. 대신, 문자열의 마지막이기 때문에, Data
에 지금까지의 문자열인 bad
를 넣어줍니다.
'car'
추가하기Head
의 Child
를 확인해보면, 'c'
는 존재하지 않기 때문에, Key
가 'c'
인 노드를 만들고 Head
의 Child
에도 'c'
를 추가해줍니다.
그리고는 'bag'
을 추가했던 것과 비슷하게 나머지 'a'
와 'r'
을 추가해줍니다.
제일 긴 문자열의 길이를 L
, 총 문자열들의 수를 M
이라 할 때 시간복잡도는 아래와 같습니다.
- 생성 시간 복잡도 :
각 문자열을 트라이에 삽입하려면, 삽입하려는 문자열의 길이만큼만 노드를 따라가거나 추가해주면 됩니다. 따라서 각 문자열을 추가할 때의 시간복잡도는 입니다.
주어진 모든 문자열들을 삽입할 때의 시간복잡도는 입니다.
문자열의 개수M
개에 대해서M
번 삽입을 진행하기 때문입니다.
- 탐색 시간 복잡도:
문자열을 탐색할 때, 문자열의 길이만큼만 노드를 따라가며 탐색하기 때문에, 만큼 걸립니다.
class Node(object):
def __init__(self, key, data=None):
self.key = key # 해당 노드의 key 값
self.data = data # 해당 노드가 문자열의 마지막인지를 알 수 있는 필드
self.children = {} # 해당 노드의 자식들로, {자식 key: 자식노드 자체}의 형태
class Trie(object):
def __init__(self):
self.head = Node(None) #처음 초기화는 head에 key가 None인 노드 생성
def insert(self, string):
current = self.head #head부터 읽기 시작
for char in string:
if char not in current.children: #문자가 children에 존재하지 않는 경우
current.children[char] = Node(char) #children에 추가
current = current.children[char] #해당 문자 노드로 이동
#모든 문자 노드 추가 후, 마지막 노드의 data에 전체 문자열 추가
current.data = string
def search(self, string):
curr_node = self.head # head부터 찾기 시작
for char in string:
if char in curr_node.children: #문자가 children에 존재하는 경우
curr_node = curr_node.children[char] #해당 문자 노드로 이동
else: return False #문자 노드가 존재하지 않는 경우 False 반환
if curr_node.data != None: #끝까지 이동했을 때 data가 None이 아니라면
return True # True 반환
트라이는 Class를 사용하여 구현하면 됩니다.
우선 각 노드들을 표현하는 Node
클래스를 만든 뒤, 이들이 연결되어 트라이 구조를 만드는 Trie
클래스를 작성해줍니다.
Trie
클래스의 경우, 문자열을 삽입하는 insert
함수와 문자열을 탐색하는 search
함수가 존재합니다.
트라이(Trie) 알고리즘 개념 및 파이썬에서 구현하기(feat. Class)