Giao diện
Trie (Prefix Tree) - The Autocomplete Engine
"Trie là nền tảng của mọi hệ thống gợi ý." - HPN
Định nghĩa
Trie (đọc là "try", từ reTRIEval) là cấu trúc dữ liệu dạng cây, trong đó:
- Mỗi node đại diện cho một ký tự
- Mỗi path từ root đến node đại diện cho một prefix
- Các từ có chung prefix sẽ chia sẻ đường đi
📘 Automaton Theory
Trie thực chất là một Deterministic Finite Automaton (DFA) cho bài toán prefix matching. Mỗi state (node) chuyển sang state khác dựa trên input character.
Tại sao cần Trie? (The Why)
So sánh với HashMap
| Thao tác | HashMap | Trie |
|---|---|---|
| Insert | ||
| Search exact | ||
| Prefix search | ||
| Autocomplete | Không hỗ trợ | Native |
| Memory | Tốt hơn | Kém hơn |
Với
🎯 INSIGHT QUAN TRỌNG
HashMap KHÔNG THỂ tìm kiếm prefix hiệu quả. Bạn phải duyệt TẤT CẢ keys để tìm những từ bắt đầu bằng "hel". Trie chỉ cần đi theo path h → e → l, sau đó DFS từ đó.
Mô hình hóa
Cấu trúc Trie với words: ["HPN", "HPC", "HERO"]
Quan sát:
HPNvàHPCchia sẻ prefixHP- Dấu
✓đánh dấu end of word - Tổng cộng 7 nodes thay vì 9 ký tự (tiết kiệm nhờ shared prefix)
Cơ chế Hoạt động
Insert Operation
Insert "HPN":
1. Start at ROOT
2. 'H' không tồn tại → Tạo node 'H'
3. 'P' không tồn tại → Tạo node 'P' dưới 'H'
4. 'N' không tồn tại → Tạo node 'N' dưới 'P'
5. Đánh dấu 'N' là end_of_word = TrueSearch Operation
Search "HPC":
1. Start at ROOT
2. 'H' tồn tại → Di chuyển đến 'H'
3. 'P' tồn tại → Di chuyển đến 'P'
4. 'C' tồn tại → Di chuyển đến 'C'
5. Check end_of_word → True → FOUND!Prefix Search (Autocomplete)
Autocomplete "HP":
1. Navigate to node 'P' (sau "HP")
2. DFS từ 'P' để thu thập tất cả words
3. Return: ["HPN", "HPC"]Production Implementation
python
# HPN Engineering Standard
# Implementation: Trie với Autocomplete Support
from typing import List, Optional, Dict
from collections import deque
class TrieNode:
"""
Node trong Trie structure.
Memory Optimization: Sử dụng __slots__ để giảm 40% memory overhead
so với dict-based attributes.
"""
__slots__ = ('children', 'is_end_of_word', 'frequency')
def __init__(self) -> None:
self.children: Dict[str, 'TrieNode'] = {}
self.is_end_of_word: bool = False
# Frequency tracking cho ranking autocomplete results
self.frequency: int = 0
class Trie:
"""
Production-ready Trie với Autocomplete support.
Features:
- O(L) insert/search/delete
- Top-N autocomplete với frequency ranking
- Case-insensitive option
- Memory-optimized với __slots__
Industry Usage:
- ElasticSearch (Inverted Index)
- Mobile keyboard suggestions
- IDE autocompletion
"""
def __init__(self, case_insensitive: bool = True) -> None:
self.root = TrieNode()
self.case_insensitive = case_insensitive
self._word_count = 0
def _normalize(self, word: str) -> str:
"""Normalize input based on case sensitivity setting."""
return word.lower() if self.case_insensitive else word
def insert(self, word: str, frequency: int = 1) -> None:
"""
Insert word vào Trie.
Args:
word: Từ cần insert
frequency: Tần suất xuất hiện (cho ranking)
Time: O(L) với L = len(word)
Space: O(L) worst case (từ mới hoàn toàn)
Edge Cases:
- Empty string: Bỏ qua, không insert
- Duplicate: Cộng dồn frequency
"""
if not word: # Edge case: empty string
return
word = self._normalize(word)
current = self.root
for char in word:
if char not in current.children:
current.children[char] = TrieNode()
current = current.children[char]
# Nếu là từ mới, tăng word count
if not current.is_end_of_word:
self._word_count += 1
current.is_end_of_word = True
current.frequency += frequency # Accumulate frequency
def search(self, word: str) -> bool:
"""
Tìm kiếm exact match.
Time: O(L)
Returns:
True nếu word tồn tại trong Trie
"""
if not word: # Edge case: empty string
return False
node = self._find_node(word)
return node is not None and node.is_end_of_word
def starts_with(self, prefix: str) -> bool:
"""
Kiểm tra có từ nào bắt đầu bằng prefix không.
Time: O(L) với L = len(prefix)
"""
if not prefix: # Empty prefix matches everything
return self._word_count > 0
return self._find_node(prefix) is not None
def autocomplete(self, prefix: str, top_n: int = 10) -> List[str]:
"""
Trả về top N gợi ý cho prefix, sắp xếp theo frequency.
Args:
prefix: Prefix cần autocomplete
top_n: Số lượng kết quả tối đa
Returns:
List các từ match, sorted by frequency (descending)
Time: O(L + K) với K = số từ có prefix này
Example:
>>> trie.insert("hello", frequency=100)
>>> trie.insert("help", frequency=50)
>>> trie.autocomplete("hel", top_n=2)
['hello', 'help'] # Sorted by frequency
"""
if not prefix:
# Edge case: empty prefix -> return top frequent words
prefix = ""
prefix_node = self.root
else:
prefix = self._normalize(prefix)
prefix_node = self._find_node(prefix)
if prefix_node is None:
return []
# DFS để thu thập tất cả words từ prefix_node
results: List[tuple] = [] # (frequency, word)
self._dfs_collect(prefix_node, prefix, results)
# Sort by frequency (descending), lấy top N
results.sort(key=lambda x: -x[0])
return [word for _, word in results[:top_n]]
def delete(self, word: str) -> bool:
"""
Xóa word khỏi Trie.
Chiến lược: Chỉ unmark is_end_of_word, không xóa nodes
(tránh phức tạp và vẫn đảm bảo correctness)
Returns:
True nếu word tồn tại và đã xóa
"""
if not word:
return False
node = self._find_node(word)
if node is None or not node.is_end_of_word:
return False
node.is_end_of_word = False
node.frequency = 0
self._word_count -= 1
return True
def _find_node(self, prefix: str) -> Optional[TrieNode]:
"""Navigate to node representing prefix."""
prefix = self._normalize(prefix)
current = self.root
for char in prefix:
if char not in current.children:
return None
current = current.children[char]
return current
def _dfs_collect(
self,
node: TrieNode,
current_word: str,
results: List[tuple]
) -> None:
"""DFS helper để thu thập tất cả words từ node."""
if node.is_end_of_word:
results.append((node.frequency, current_word))
for char, child_node in node.children.items():
self._dfs_collect(child_node, current_word + char, results)
@property
def size(self) -> int:
"""Số lượng words trong Trie."""
return self._word_count
# ============================================
# USAGE EXAMPLE
# ============================================
if __name__ == "__main__":
# Initialize
trie = Trie(case_insensitive=True)
# Insert với frequency (từ search logs)
trie.insert("python", frequency=1000)
trie.insert("pytorch", frequency=500)
trie.insert("pandas", frequency=800)
trie.insert("pip", frequency=200)
# Exact search
print(trie.search("python")) # True
print(trie.search("pythonic")) # False
# Autocomplete
suggestions = trie.autocomplete("py", top_n=3)
print(suggestions) # ['python', 'pytorch'] - sorted by frequency
# Prefix check
print(trie.starts_with("pan")) # True
print(trie.starts_with("java")) # FalseDeep Dive: Space Complexity
The Memory Trade-off
Worst Case: Mỗi word không share prefix nào
words, mỗi word dài - Space:
Với ALPHABET_SIZE = 26 (lowercase letters):
Mỗi TrieNode chứa:
- Dict với tối đa 26 child pointers: ~26 × 8 bytes = 208 bytes
- is_end_of_word: 1 byte
- frequency: 8 bytes
≈ 220 bytes/node (worst case)⚠️ MEMORY WARNING
Với 1 triệu từ, mỗi từ 10 ký tự, worst case: 10 triệu nodes × 220 bytes = 2.2 GB RAM
Đây là lý do Trie thường được dùng với data có high prefix overlap (URLs, IP addresses, dictionary words).
Memory Optimization Techniques
| Technique | Description | Savings |
|---|---|---|
__slots__ | Loại bỏ instance __dict__ | ~40% |
| Compressed Trie | Gộp nodes có 1 child | Variable |
| Array thay Dict | Fixed alphabet size | ~30% |
| LOUDS encoding | Succinct data structure | ~95% |
Độ phức tạp Tổng hợp
| Operation | Time | Space (auxiliary) |
|---|---|---|
| Insert | ||
| Search | ||
| Delete | ||
| Autocomplete | ||
| Build Trie |
Với:
= độ dài từ = số kết quả autocomplete = số từ trong dictionary
Ứng dụng Thực tế
1. ElasticSearch - Inverted Index
ElasticSearch sử dụng FST (Finite State Transducer) - một dạng compressed Trie - để lưu inverted index:
"python" → [doc1, doc5, doc7]
"pytorch" → [doc2, doc5]2. Mobile Keyboard Suggestions
Gboard, SwiftKey sử dụng Trie kết hợp với:
- Frequency data từ user typing history
- Language model để rank suggestions
3. IP Routing Tables
Routers sử dụng Patricia Trie để match longest prefix:
192.168.1.0/24 → Interface 1
192.168.0.0/16 → Interface 2Anti-patterns (Cạm bẫy)
❌ SAI LẦM PHỔ BIẾN
1. Dùng Trie cho dataset nhỏ
python
# ❌ WRONG: 100 words, dùng Trie làm gì?
trie = Trie()
for word in small_list: # 100 words
trie.insert(word)
# ✅ RIGHT: Dùng set + list comprehension
suggestions = [w for w in words if w.startswith(prefix)]2. Không xem xét memory footprint
python
# ❌ WRONG: Insert 10 triệu random UUIDs
for uuid in generate_random_uuids(10_000_000):
trie.insert(uuid) # Mỗi UUID unique → No shared prefix → BOOM
# ✅ RIGHT: Dùng Bloom Filter hoặc Hash-based approach3. Quên normalize input
python
# ❌ WRONG: Case-sensitive khi không cần
trie.insert("Python")
trie.search("python") # False! User sẽ confused
# ✅ RIGHT: Normalize trong constructor
trie = Trie(case_insensitive=True)Khi nào KHÔNG dùng Trie?
| Điều kiện | Alternative |
|---|---|
| Dataset < 1000 items | list + startswith() |
| Không cần prefix search | set hoặc dict |
| Memory-constrained | Bloom Filter hoặc Hash |
| Random strings (no shared prefix) | HashMap |
💡 HPN's Golden Rule
"Trie sáng giá khi data có PREFIX LOCALITY." Ví dụ: Dictionary words, URLs, IP addresses, file paths.