[208. Implement Trie (Prefix Tree)] (알고리즘)
Trie
문제
A trie (pronounced as “try”) or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.
Implement the Trie class:
Trie()Initializes the trie object.void insert(String word)Inserts the stringwordinto the trie.boolean search(String word)Returnstrueif the stringwordis in the trie (i.e., was inserted before), andfalseotherwise.boolean startsWith(String prefix)Returnstrueif there is a previously inserted stringwordthat has the prefixprefix, andfalseotherwise.
Example 1:
Input [“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”] [[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]] Output [null, null, true, false, true, null, true]
Explanation Trie trie = new Trie(); trie.insert(“apple”); trie.search(“apple”); // return True trie.search(“app”); // return False trie.startsWith(“app”); // return True trie.insert(“app”); trie.search(“app”); // return True
Constraints:
1 <= word.length, prefix.length <= 2000wordandprefixconsist only of lowercase English letters.- At most
3 * 104calls in total will be made toinsert,search, andstartsWith.
스스로 고민
접근법
- 트라이 자료구조 만들기
- 새로 입력 받은 문자가 Trie에 있는 경우 true를 반환 그렇지 않으면 false를 반환
- 이전에 삽입된 문자열 중에 접두사를 가진 문자열이 있는 경우 true 반환 그렇지 않으면 false
- 해당 단어로 시작하거나 연관이 있는 문자열 정도로 이해하면 편하다.
의사코드
- TrieNode를 만들어준다 링크드 리스트처럼 하나의 노드이다. 한 가지 특이한 점은 딕셔너리를 사용하며 딕셔셔너리 키 값에 노드를 여러개를 넣는다고 상상? 하면 편한다.
- 그리고 문자가 끝나는 위치를 알 수 있게
is_end_of_word라는 인스턴스 변수를 만들어준다. - TrieNode를 사용할 Trie 클래스를 만들어준다.
-
여기 안에 단어를 삽입할 insert, search 그리고 해당 접미사 접두사로 시작하는지 검사할 수 있는 startsWith 매서드를 만들어준다.
- 코드가 너무 길어져서 구현코드 부분에서 따로 설명을 하는 게 좋을 것 같다.
구현 코드
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def startsWith(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
trie = Trie()
trie.insert("apple")
print(trie.search("apple")) # True
print(trie.search("app")) # False
print(trie.startsWith("app")) # True
trie.insert("app")
print(trie.search("app")) # True
insert 부분
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
- root 로 사용할 TrieNode를 만들어준다.
- 반복문을 통해 단어를 쪼개준다.
- 예를 들어 apple이라는 단어가 들어왔을 경우 반복문으로 a , p , p , l , e 이렇게 문자로 각각 쪼개주고 self.root에 저장된 TrieNode를 node로 선언해준다.
- TrieNode 에는
children이라는 딕셔너리와 단어가 끝났는지 판별해주는is_end_of_word가 존재한다. - 위의 코드를 이해하기 위해 apple이라는 단어가 들어가면 어떻게 될지 상상해보자.
trie = Trie()
trie.insert("apple")
- 반복문에서 쪼개진 apple 중 첫 번째 문자 a는
node.children에 존재하는지 검사를 한다. - 딕셔너리에는 아무것도 존재하지 않으므로
a라는 키 값에TrieNode가 벨류 값으로 들어가진다. - 그럼 처음에는 node 가 root 노드였지면 현재는 a 키 값으로 새롭게 열린 노드다.
- 두 번째
p는 어떻게 될까? root 노드에는 a 가 있지만 딕셔너리 a 키 값으로 TriNode가 새롭게 열렸고 a 키 값으로 열린TrieNode안 children 에는 값이 아무것도 없다 (비어있는 딕셔너리) - 그럼 위와 같이 키 p 값으로 새로운
TrieNode가 새롭게 만들어진다. - 세 번째
p도 이전 p 키 값으로 새롭게 만들어진TrieNode안에는 아무것도 없으니 또 새로운TrieNode가 만들어진다 a -> p -> p -> l - > e이런 식으로 만들어진다.e는 반복문이 끝나는 지점으로 반복문이 다 끝난 후에node.is_end_of_word = True가 실행되면서 해당 단어가 끝난걸 알려준다.
search 부분
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
- 위에서 만들어진 노드들을 쭉쭉 타고 들어가서 반복문이 끝나고 그 지점의
is_end_of_word값을 리턴해준다. - apple의 경우 p에서 아직 끝나지 않은 상태인데 app 이라는 단어를 검색할 경우 p의 마지막부분에서 반복문이 끝나고 이 때 return으로 p의
is_end_of_word가 반환된다. - apple을 저장할 때
p의 값은 디폴트가 False였으니 False가 반환고 이를 통해 해당 단어가 없다고 판단을 할 수 있다. - 아예 일치하지 않는 단어일 경우 Falsle를 반환한다.
startsWith 부분
def startsWith(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
- 노드가 계속 열린다면 해당 단어는 True를 반환하고 중간에 끊기거나 없다면 False를 반환해서 접두사가 일치 하는지 알 수 있다.
시간 복잡도와 공간 복잡도
시간 복잡도:
insert함수: 단어를 Trie에 삽입하는 데는 O(M)의 시간이 걸린다. 여기서 M은 삽입하려는 단어의 길이다.search함수: 단어를 Trie에서 찾는 데는 O(M)의 시간이 걸린다. 여기서 M은 찾고자 하는 단어의 길이다.startsWith함수: 주어진 접두사로 시작하는 단어가 Trie에 있는지 확인하는 데는 O(P)의 시간이 걸린다. 여기서 P는 접두사의 길이다.
공간 복잡도:
-
Trie 자료 구조는 모든 가능한 단어의 문자열을 저장하지 않고 각 문자 노드가 가리키는 자식 노드에 따라 공간을 사용한다. 따라서 공간 복잡도는 Trie에 저장된 총 문자 수에 비례한다.
-
만약 Trie에 N개의 단어를 삽입한다면 최악의 경우 모든 단어가 서로 다른 문자로 이루어져 있다고 가정하면, 전체 Trie의 공간 복잡도는 대략 O(N)입니다.
회고 과정
- 처음에 개념을 이해하는 게 조금 어려워서 시간이 많이 걸렸다.
- 문제를 좀 더 풀어봐야 확실히 이해할 수 있을 것 같다.
다른 사람 코드를 보고 그 기준으로 어떤 부분을 개선 할 수 있는지 최종 회고
class Trie:
def __init__(self):
self.m = {}
def insert(self, word: str) -> None:
cur = self.m
for c in word:
if c not in cur:
cur[c] = {}
cur = cur[c]
cur["#"] = ""
def search(self, word: str) -> bool:
cur = self.m
for c in word:
if c not in cur:
return False
cur = cur[c]
return "#" in cur
def startsWith(self, prefix: str) -> bool:
cur = self.m
for c in prefix:
if c not in cur:
return False
cur = cur[c]
return True
- 굳이 노드를 만들지 않고 오로지 딕셔너리로만 문제를 해결한 케이스다.
- 항상 쉽게쉽게 생각하려고 노력을 해야겠다.
댓글남기기