이 주제에 대한 튜토리얼은 여기에서 확인할 수 있다.
트라이는 접두사 트리(prefix tree)라고도 불리며, 어떤 구현에서는 기수 트리(radix tree)라고도 한다. 이는 연관 데이터 구조를 저장하기 위해 사용되는 특별한 형태의 트리다. 사전을 저장하는 트라이는 다음과 같은 모습을 보인다:
트라이의 주요 사용 사례 중 하나는 영어 단어를 저장하는 것이다. 트라이의 각 노드는 단어의 한 문자를 나타낸다. 여러 노드가 연결되면 하나의 단어를 구성한다.
트라이는 특정 상황에서 매우 유용하다. 주요 장점은 다음과 같다:
Trie
구조는 조회 작업에 매우 적합하다. 영어를 모델링한 Trie
구조에서 특정 단어를 찾는 것은 몇 번의 포인터 탐색으로 가능하다:
func contains(word: String) -> Bool {
guard !word.isEmpty else { return false }
// 1
var currentNode = root
// 2
var characters = Array(word.lowercased())
var currentIndex = 0
// 3
while currentIndex < characters.count,
let child = currentNode.children[characters[currentIndex]] {
currentNode = child
currentIndex += 1
}
// 4
if currentIndex == characters.count && currentNode.isTerminating {
return true
} else {
return false
}
}
contains
메서드는 상당히 직관적이다:
root
에 대한 참조를 생성한다. 이 참조는 노드 체인을 따라 내려갈 수 있게 해준다.isTerminating
은 이 노드가 단어의 끝인지 여부를 나타내는 불리언 플래그다. 이 if
조건이 충족되면 trie
에서 단어를 찾았다는 의미다.Trie
에 단어를 삽입하려면, 노드를 따라 이동하면서 단어의 끝을 나타내는 노드를 표시하거나 새로운 노드를 추가해야 한다.
func insert(word: String) {
guard !word.isEmpty else {
return
}
// 1
var currentNode = root
// 2
for character in word.lowercased() {
// 3
if let childNode = currentNode.children[character] {
currentNode = childNode
} else {
currentNode.add(value: character)
currentNode = currentNode.children[character]!
}
}
// 단어가 이미 존재하는지 확인
guard !currentNode.isTerminating else {
return
}
// 4
wordCount += 1
currentNode.isTerminating = true
}
Trie
에 있는 경우가 이에 해당한다. 글자가 이미 존재하면 해당 노드를 재사용하고 체인을 더 깊이 탐색한다. 그렇지 않으면 새로운 노드를 생성한다.isTerminating
을 true
로 설정해 해당 노드가 단어의 끝임을 표시한다.Trie에서 키를 제거하는 작업은 약간 까다롭다. 여러 가지 경우를 고려해야 하기 때문이다. Trie의 노드는 여러 단어 간에 공유될 수 있다. 예를 들어, “Apple”과 “App”이라는 두 단어를 생각해 보자. Trie 내부에서 “App”을 나타내는 노드 체인은 “Apple”과 공유된다.
만약 “Apple”을 제거하려면, “App” 체인을 그대로 유지해야 한다.
func remove(word: String) {
guard !word.isEmpty else {
return
}
// 1
guard let terminalNode = findTerminalNodeOf(word: word) else {
return
}
// 2
if terminalNode.isLeaf {
deleteNodesForWordEndingWith(terminalNode: terminalNode)
} else {
terminalNode.isTerminating = false
}
wordCount -= 1
}
findTerminalNodeOf
함수는 Trie를 순회하며 word
를 나타내는 마지막 노드를 찾는다. 만약 문자 체인을 따라갈 수 없다면 nil
을 반환한다.deleteNodesForWordEndingWith
함수는 뒤로 순회하며 word
를 나타내는 노드를 삭제한다.Trie
에서 어떤 값의 길이를 n이라고 가정한다.
contains
- 최악의 경우 O(n)insert
- O(n)remove
- O(n)count
: Trie
에 있는 키의 개수를 반환한다 - O(1)words
: Trie
에 있는 모든 키를 리스트로 반환한다 - O(1)isEmpty
: Trie
가 비어 있으면 true
, 그렇지 않으면 false
를 반환한다 - O(1)자세한 내용은 위키피디아 Trie 항목을 참고한다.
Swift Algorithm Club을 위해 Christian Encarnacion이 작성. Kelvin Lau가 리팩토링
remove
메서드를 리팩토링했다.parent
를 parentNode
로 변경하는 등의 수정을 가했다. 이렇게 하면 해당 변수가 노드 자체임을 강조할 수 있다.words
프로퍼티를 추가했다. 이 프로퍼티는 트라이를 재귀적으로 탐색하며 트라이에 포함된 모든 단어를 배열로 구성한다.TrieNode
에 isLeaf
프로퍼티를 추가했다.count
와 isEmpty
프로퍼티를 구현했다.XCTest
를 사용해 트라이를 테스트하는 코드를 작성했다. 여러 성능 테스트도 포함되어 있으며, 모든 테스트를 통과했다.