[LeetCode Top Interview 150]
[문제 링크]
[문제 설명]
- trie ("try"로 발음) or prefix tree 는 문자열 데이터셋에서 키를 효율적으로 저장하고 검색하는데 사용되는 트리 데이터 구조
- Trie 클래스 구현
- Trie() : 개체를 초기화
- void insert(String word) : 문자열 word를 Trie에 삽입
- booelan search(String word) : 문자열이 word트라이에 있는지(즉, 이전에 삽입되었는지) 확인하여 있다면 true 반환, 그렇지 않다면 false 반환
- bollean startsWith(String prefix) : 이전에 삽입된 문자열에 prefix 접두사가 있으면 true 반환, 그렇지 않으면 false 반환
[예시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
[제약]
- 1 <= word.length, prefix.length <= 2000
- word와 prefix는 영문 소문자로만 구성
- 최대 호출 횟수는 3 * 104 (insert, search and startsWith)
[코드를 작성하기 전에]
일단 수업 내용을 이해하고, 복습하는 차원에서 천천히 코드를 작성해보았다. Trie를 사용하는 가장 중요한 이유는 문자열 검색에 최적화된 자료구조이기 때문이다. 완전한 단어 검색이라면 해시 테이블을 사용하는 것이 더 효율적이라고 한다. (시간 복잡도가 O(1))
그러나, 트라이 같은 경우 사전 순 정렬이 가능하고, 시간 복잡도는 해시 테이블보다는 조금 느리다.
[시간 복잡도]
접두어 탐색 부분 : 상수 시간 O(p) / 재귀적 탐색 부분 : O(n)
[공간 복잡도]
- 재귀의 깊이는 트라이의 최대 높이나 깊이에 비례
- 트라이의 깊이는 문자열의 최대 길이로 제한되며, 이 최대 깊이를 d라고 할 때 재귀 호출로 인해 공간 복잡도는 O(d)
class Node {
public Map<Character, Node> children;
public boolean isEndOfWord;
public Node() {
this.children = new HashMap<>();
this.isEndOfWord = false;
}
}
private Node root;
public Trie() {
this.root = new Node();
}
public void insert(String word) {
insert(this.root, word);
}
public void insert(Node node, String word) {
if (word.length() == 0) {
node.isEndOfWord = true;
return;
}
final char c = word.charAt(0);
Node child = node.children.get(c);
if (child == null) {
child = new Node();
node.children.put(c, child);
}
insert(child, word.substring(1));
}
public boolean search(String word) {
return search(this.root, word);
}
private boolean search(final Node node, final String word) {
if (word.length() == 0) {
return node.isEndOfWord;
}
char c = word.charAt(0);
Node child = node.children.get(c);
if (child == null) {
return false;
}
return search(child, word.substring(1));
}
public boolean startsWith(String prefix) {
return startsWith(this.root, prefix);
}
public boolean startsWith(String prefix) {
Node currentNode = this.root;
for (char c: prefix.toCharArray()) {
Node child = currentNode.children.get(c);
if (child == null) return false;
currentNode = child;
}
return true;
}
class Trie {
class Node {
public Map<Character, Node> children;
public boolean isEndOfWord;
public Node() {
this.children = new HashMap<>();
this.isEndOfWord = false;
}
}
private Node root;
public Trie() {
this.root = new Node();
}
public void insert(String word) {
insert(this.root, word);
}
public void insert(Node node, String word) {
if (word.length() == 0) {
node.isEndOfWord = true;
return;
}
final char c = word.charAt(0);
Node child = node.children.get(c);
if (child == null) {
child = new Node();
node.children.put(c, child);
}
insert(child, word.substring(1));
}
public boolean search(String word) {
return search(this.root, word);
}
private boolean search(final Node node, final String word) {
if (word.length() == 0) {
return node.isEndOfWord;
}
char c = word.charAt(0);
Node child = node.children.get(c);
if (child == null) {
return false;
}
return search(child, word.substring(1));
}
public boolean startsWith(String prefix) {
return startsWith(this.root, prefix);
}
private boolean startsWith(final Node node, final String prefix) {
Node currentNode = this.root;
for (char c: prefix.toCharArray()) {
Node child = currentNode.children.get(c);
if (child == null) return false;
currentNode = child;
}
return true;
}
}
[회고]
Trie 이론이나 동작 원리 부분은 굉장히 흥미롭고 재밌었는데, 실제로 구현하려니까 어려웠다.. 자료구조, 알고리즘.. 갈 길이 먼 것 같다…