1. 深入理解字典树
1.1 核心思想与数据结构本质
字典树(Trie)是一种空间换时间的典型数据结构,其核心思想是利用字符串的公共前缀来优化存储和查询。
数据结构本质:
- 字典树是一棵多叉树,每个节点的子节点数量取决于字符集大小
- 它将字符串的路径信息编码到树的结构中
- 通过路径压缩技术减少冗余存储
与普通树的区别:
- 普通树:节点存储数据
- 字典树:边存储字符,路径表示字符串
1.2 字典树的数学模型
从数学角度看,字典树可以看作是一个有限状态自动机:
- 状态:树中的节点
- 转移函数:字符到子节点的映射
- 接受状态:标记为单词结尾的节点
对于一个包含 n 个字符串的集合,设平均字符串长度为 m,则:
- 时间复杂度:O(n×m) 用于构建,O(m) 用于查询
- 空间复杂度:最坏情况 O(σ×n×m),其中 σ 是字符集大小
2. 字典树的详细结构设计
2.1 节点结构的多种实现方式
#include <iostream>
#include <vector>
#include <unordered_map>
#include <memory>
#include <string>
using namespace std;
// 方案1:固定数组(小写字母a-z)
class TrieNodeArray {
public:
static const int ALPHABET_SIZE = 26;
bool isEnd;
TrieNodeArray* children[ALPHABET_SIZE];
TrieNodeArray() : isEnd(false) {
// 初始化所有子节点为空
for (int i = 0; i < ALPHABET_SIZE; i++) {
children[i] = nullptr;
}
}
// 字符转索引
int charToIndex(char c) {
return c - 'a';
}
// 索引转字符
char indexToChar(int idx) {
return 'a' + idx;
}
};
// 方案2:哈希表(通用,支持任意字符)
class TrieNodeMap {
public:
bool isEnd;
unordered_map<char, TrieNodeMap*> children;
TrieNodeMap() : isEnd(false) {}
};
// 方案3:向量(平衡空间和时间)
class TrieNodeVector {
public:
bool isEnd;
vector<pair<char, TrieNodeVector*>> children;
TrieNodeVector() : isEnd(false) {}
// 查找子节点
TrieNodeVector* findChild(char c) {
for (auto& pair : children) {
if (pair.first == c) {
return pair.second;
}
}
return nullptr;
}
// 添加子节点
void addChild(char c, TrieNodeVector* node) {
children.push_back({c, node});
}
};
// 方案4:智能指针版本(现代C++风格)
class TrieNodeSmart {
public:
bool isEnd;
unordered_map<char, unique_ptr<TrieNodeSmart>> children;
TrieNodeSmart() : isEnd(false) {}
};
2.2 完整的Trie类设计
template<typename NodeType>
class Trie {
private:
unique_ptr<NodeType> root;
// 递归销毁
void destroyTrie(NodeType* node) {
if (!node) return;
// 对于哈希表和向量版本需要特殊处理
if constexpr (is_same_v<NodeType, TrieNodeMap> ||
is_same_v<NodeType, TrieNodeVector>) {
for (auto& pair : node->children) {
destroyTrie(pair.second);
}
}
else if constexpr (is_same_v<NodeType, TrieNodeArray>) {
for (int i = 0; i < NodeType::ALPHABET_SIZE; i++) {
if (node->children[i]) {
destroyTrie(node->children[i]);
}
}
}
delete node;
}
// 查找节点的模板实现
template<typename T = NodeType>
T* findNodeImpl(const string& str, T* current) {
for (char c : str) {
if constexpr (is_same_v<T, TrieNodeArray>) {
int idx = current->charToIndex(c);
if (idx < 0 || idx >= T::ALPHABET_SIZE || !current->children[idx]) {
return nullptr;
}
current = current->children[idx];
}
else if constexpr (is_same_v<T, TrieNodeMap>) {
auto it = current->children.find(c);
if (it == current->children.end()) {
return nullptr;
}
current = it->second;
}
else if constexpr (is_same_v<T, TrieNodeVector>) {
current = current->findChild(c);
if (!current) return nullptr;
}
}
return current;
}
public:
Trie() {
root = make_unique<NodeType>();
}
~Trie() {
if (root) {
destroyTrie(root.get());
}
}
// 插入操作
void insert(const string& word) {
NodeType* current = root.get();
for (char c : word) {
if constexpr (is_same_v<NodeType, TrieNodeArray>) {
int idx = current->charToIndex(c);
if (!current->children[idx]) {
current->children[idx] = new TrieNodeArray();
}
current = current->children[idx];
}
else if constexpr (is_same_v<NodeType, TrieNodeMap>) {
if (current->children.find(c) == current->children.end()) {
current->children[c] = new TrieNodeMap();
}
current = current->children[c];
}
else if constexpr (is_same_v<NodeType, TrieNodeVector>) {
TrieNodeVector* child = current->findChild(c);
if (!child) {
child = new TrieNodeVector();
current->addChild(c, child);
}
current = child;
}
}
current->isEnd = true;
}
// 搜索操作
bool search(const string& word) {
NodeType* node = findNodeImpl(word, root.get());
return node && node->isEnd;
}
// 前缀搜索
bool startsWith(const string& prefix) {
return findNodeImpl(prefix, root.get()) != nullptr;
}
// 获取所有以prefix开头的单词
vector<string> getWordsWithPrefix(const string& prefix) {
vector<string> result;
NodeType* node = findNodeImpl(prefix, root.get());
if (node) {
collectWords(node, prefix, result);
}
return result;
}
private:
void collectWords(NodeType* node, const string& currentWord, vector<string>& result) {
if (node->isEnd) {
result.push_back(currentWord);
}
if constexpr (is_same_v<NodeType, TrieNodeArray>) {
for (int i = 0; i < NodeType::ALPHABET_SIZE; i++) {
if (node->children[i]) {
char c = node->indexToChar(i);
collectWords(node->children[i], currentWord + c, result);
}
}
}
else if constexpr (is_same_v<NodeType, TrieNodeMap>) {
for (auto& pair : node->children) {
collectWords(pair.second, currentWord + pair.first, result);
}
}
else if constexpr (is_same_v<NodeType, TrieNodeVector>) {
for (auto& pair : node->children) {
collectWords(pair.second, currentWord + pair.first, result);
}
}
}
};
3. 高级功能实现
3.1 带权重的字典树
class WeightedTrieNode {
public:
int weight; // 权重(如词频)
bool isEnd;
unordered_map<char, unique_ptr<WeightedTrieNode>> children;
WeightedTrieNode() : weight(0), isEnd(false) {}
};
class WeightedTrie {
private:
unique_ptr<WeightedTrieNode> root;
public:
WeightedTrie() : root(make_unique<WeightedTrieNode>()) {}
// 插入带权重的单词
void insert(const string& word, int weight = 1) {
auto current = root.get();
for (char c : word) {
if (current->children.find(c) == current->children.end()) {
current->children[c] = make_unique<WeightedTrieNode>();
}
current = current->children[c].get();
}
current->isEnd = true;
current->weight += weight; // 累加权重
}
// 获取单词权重
int getWeight(const string& word) {
auto node = findNode(word);
return node && node->isEnd ? node->weight : 0;
}
// 获取前缀权重(所有以该前缀开头的单词权重之和)
int getPrefixWeight(const string& prefix) {
auto node = findNode(prefix);
if (!node) return 0;
return dfsSumWeight(node);
}
private:
WeightedTrieNode* findNode(const string& str) {
auto current = root.get();
for (char c : str) {
if (current->children.find(c) == current->children.end()) {
return nullptr;
}
current = current->children[c].get();
}
return current;
}
int dfsSumWeight(WeightedTrieNode* node) {
int sum = node->isEnd ? node->weight : 0;
for (auto& pair : node->children) {
sum += dfsSumWeight(pair.second.get());
}
return sum;
}
};
3.2 支持删除操作
class DeletableTrie {
private:
struct TrieNode {
bool isEnd;
unordered_map<char, unique_ptr<TrieNode>> children;
TrieNode() : isEnd(false) {}
};
unique_ptr<TrieNode> root;
public:
DeletableTrie() : root(make_unique<TrieNode>()) {}
void insert(const string& word) {
auto current = root.get();
for (char c : word) {
if (current->children.find(c) == current->children.end()) {
current->children[c] = make_unique<TrieNode>();
}
current = current->children[c].get();
}
current->isEnd = true;
}
bool search(const string& word) {
auto node = findNode(word);
return node && node->isEnd;
}
// 删除操作
bool remove(const string& word) {
return removeHelper(root.get(), word, 0);
}
private:
TrieNode* findNode(const string& str) {
auto current = root.get();
for (char c : str) {
if (current->children.find(c) == current->children.end()) {
return nullptr;
}
current = current->children[c].get();
}
return current;
}
// 递归删除辅助函数
bool removeHelper(TrieNode* node, const string& word, int depth) {
if (depth == word.length()) {
// 到达字符串末尾
if (!node->isEnd) {
return false; // 单词不存在
}
node->isEnd = false;
// 如果没有子节点,可以删除这个节点
return node->children.empty();
}
char c = word[depth];
auto it = node->children.find(c);
if (it == node->children.end()) {
return false; // 路径不存在
}
// 递归删除
bool shouldDeleteChild = removeHelper(it->second.get(), word, depth + 1);
if (shouldDeleteChild) {
node->children.erase(it);
// 如果当前节点不是单词结尾且没有其他子节点,也应该被删除
return !node->isEnd && node->children.empty();
}
return false;
}
};
3.3 支持通配符搜索
class WildcardTrie {
private:
struct TrieNode {
bool isEnd;
unordered_map<char, unique_ptr<TrieNode>> children;
TrieNode() : isEnd(false) {}
};
unique_ptr<TrieNode> root;
public:
WildcardTrie() : root(make_unique<TrieNode>()) {}
void insert(const string& word) {
auto current = root.get();
for (char c : word) {
if (current->children.find(c) == current->children.end()) {
current->children[c] = make_unique<TrieNode>();
}
current = current->children[c].get();
}
current->isEnd = true;
}
// 支持通配符 '.' 的搜索
bool searchWithWildcard(const string& word) {
return searchHelper(root.get(), word, 0);
}
private:
bool searchHelper(TrieNode* node, const string& word, int depth) {
if (depth == word.length()) {
return node->isEnd;
}
char c = word[depth];
if (c == '.') {
// 通配符匹配任意字符
for (auto& pair : node->children) {
if (searchHelper(pair.second.get(), word, depth + 1)) {
return true;
}
}
return false;
} else {
// 普通字符匹配
auto it = node->children.find(c);
if (it == node->children.end()) {
return false;
}
return searchHelper(it->second.get(), word, depth + 1);
}
}
};
4. 性能优化技术
4.1 内存池优化
class TrieMemoryPool {
private:
struct TrieNode {
bool isEnd;
unordered_map<char, TrieNode*> children;
TrieNode() : isEnd(false) {}
};
// 内存池
vector<unique_ptr<TrieNode[]>> memoryBlocks;
vector<TrieNode*> freeList;
static const size_t BLOCK_SIZE = 1000;
TrieNode* allocateNode() {
if (freeList.empty()) {
// 分配新块
auto block = make_unique<TrieNode[]>(BLOCK_SIZE);
memoryBlocks.push_back(move(block));
// 将新块中的所有节点加入空闲列表
for (size_t i = 0; i < BLOCK_SIZE; i++) {
freeList.push_back(&memoryBlocks.back()[i]);
}
}
TrieNode* node = freeList.back();
freeList.pop_back();
new(node) TrieNode(); // 调用构造函数
return node;
}
void deallocateNode(TrieNode* node) {
node->~TrieNode(); // 调用析构函数
freeList.push_back(node);
}
unique_ptr<TrieNode> root;
public:
TrieMemoryPool() {
root = unique_ptr<TrieNode>(allocateNode());
}
void insert(const string& word) {
TrieNode* current = root.get();
for (char c : word) {
if (current->children.find(c) == current->children.end()) {
current->children[c] = allocateNode();
}
current = current->children[c];
}
current->isEnd = true;
}
// ... 其他方法类似
};
4.2 压缩字典树 (Patricia Trie)
class PatriciaTrieNode {
public:
string edge; // 边上的字符串
bool isEnd;
unordered_map<char, unique_ptr<PatriciaTrieNode>> children;
PatriciaTrieNode(const string& e = "") : edge(e), isEnd(false) {}
};
class PatriciaTrie {
private:
unique_ptr<PatriciaTrieNode> root;
// 找到最长公共前缀
int longestCommonPrefix(const string& s1, const string& s2) {
int len = min(s1.length(), s2.length());
for (int i = 0; i < len; i++) {
if (s1[i] != s2[i]) {
return i;
}
}
return len;
}
public:
PatriciaTrie() : root(make_unique<PatriciaTrieNode>()) {}
void insert(const string& word) {
insertHelper(root.get(), word);
}
private:
void insertHelper(PatriciaTrieNode* node, const string& word) {
if (word.empty()) {
node->isEnd = true;
return;
}
char firstChar = word[0];
auto it = node->children.find(firstChar);
if (it == node->children.end()) {
// 没有以该字符开头的边,直接添加
auto newNode = make_unique<PatriciaTrieNode>(word);
newNode->isEnd = true;
node->children[firstChar] = move(newNode);
return;
}
PatriciaTrieNode* child = it->second.get();
int lcp = longestCommonPrefix(word, child->edge);
if (lcp == child->edge.length()) {
// word包含child->edge,继续递归
insertHelper(child, word.substr(lcp));
return;
}
if (lcp == word.length()) {
// child->edge包含word,需要分割
string remaining = child->edge.substr(lcp);
child->edge = word;
auto newChild = make_unique<PatriciaTrieNode>(remaining);
newChild->children = move(child->children);
newChild->isEnd = child->isEnd;
child->children.clear();
child->children[remaining[0]] = move(newChild);
child->isEnd = true;
return;
}
// 部分匹配,需要分割节点
string common = child->edge.substr(0, lcp);
string remainingWord = word.substr(lcp);
string remainingEdge = child->edge.substr(lcp);
child->edge = common;
auto newChild1 = make_unique<PatriciaTrieNode>(remainingEdge);
newChild1->children = move(child->children);
newChild1->isEnd = child->isEnd;
auto newChild2 = make_unique<PatriciaTrieNode>(remainingWord);
newChild2->isEnd = true;
child->children.clear();
child->children[remainingEdge[0]] = move(newChild1);
child->children[remainingWord[0]] = move(newChild2);
child->isEnd = false;
}
};
5. 实际应用场景
5.1 拼写检查器
class SpellChecker {
private:
Trie<TrieNodeMap> dictionary;
unordered_set<string> suggestions;
// 生成可能的拼写错误
vector<string> generateEdits(const string& word) {
vector<string> edits;
// 删除操作
for (int i = 0; i < word.length(); i++) {
edits.push_back(word.substr(0, i) + word.substr(i + 1));
}
// 交换操作
for (int i = 0; i < word.length() - 1; i++) {
string swapped = word;
swap(swapped[i], swapped[i + 1]);
edits.push_back(swapped);
}
// 替换操作
for (int i = 0; i < word.length(); i++) {
for (char c = 'a'; c <= 'z'; c++) {
if (c != word[i]) {
edits.push_back(word.substr(0, i) + c + word.substr(i + 1));
}
}
}
// 插入操作
for (int i = 0; i <= word.length(); i++) {
for (char c = 'a'; c <= 'z'; c++) {
edits.push_back(word.substr(0, i) + c + word.substr(i));
}
}
return edits;
}
public:
void addWord(const string& word) {
dictionary.insert(word);
}
bool check(const string& word) {
return dictionary.search(word);
}
vector<string> suggest(const string& word) {
suggestions.clear();
// 直接搜索
if (dictionary.search(word)) {
return {word};
}
// 生成编辑距离为1的单词
auto edits = generateEdits(word);
for (const string& edit : edits) {
if (dictionary.search(edit)) {
suggestions.insert(edit);
}
}
// 如果没有找到,尝试编辑距离为2
if (suggestions.empty()) {
for (const string& edit1 : edits) {
auto edits2 = generateEdits(edit1);
for (const string& edit2 : edits2) {
if (dictionary.search(edit2)) {
suggestions.insert(edit2);
}
}
}
}
return vector<string>(suggestions.begin(), suggestions.end());
}
};
5.2 自动补全系统
class AutoComplete {
private:
Trie<TrieNodeMap> trie;
priority_queue<pair<int, string>> topSuggestions;
public:
void addWord(const string& word, int frequency = 1) {
trie.insert(word);
// 这里可以维护一个频率映射
}
vector<string> getSuggestions(const string& prefix, int limit = 5) {
auto words = trie.getWordsWithPrefix(prefix);
// 按长度排序(短的优先)
sort(words.begin(), words.end(), [](const string& a, const string& b) {
if (a.length() != b.length()) {
return a.length() < b.length();
}
return a < b; // 字典序
});
// 限制返回数量
if (words.size() > limit) {
words.resize(limit);
}
return words;
}
// 支持模糊匹配的自动补全
vector<string> getFuzzySuggestions(const string& input, int maxDistance = 1) {
vector<string> result;
auto allWords = trie.getWordsWithPrefix("");
for (const string& word : allWords) {
if (editDistance(input, word) <= maxDistance) {
result.push_back(word);
}
}
// 按编辑距离排序
sort(result.begin(), result.end(), [&input](const string& a, const string& b) {
int distA = editDistance(input, a);
int distB = editDistance(input, b);
if (distA != distB) {
return distA < distB;
}
return a.length() < b.length();
});
return result;
}
private:
int editDistance(const string& s1, const string& s2) {
int m = s1.length(), n = s2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
}
}
}
return dp[m][n];
}
};
6. 完整的测试框架
void runTests() {
cout << "=== Trie 测试开始 ===" << endl;
// 测试基本功能
Trie<TrieNodeMap> trie;
// 插入测试
vector<string> words = {"apple", "app", "application", "apply", "banana", "band"};
for (const string& word : words) {
trie.insert(word);
cout << "插入: " << word << endl;
}
// 搜索测试
cout << "\n搜索测试:" << endl;
cout << "app: " << (trie.search("app") ? "找到" : "未找到") << endl;
cout << "apple: " << (trie.search("apple") ? "找到" : "未找到") << endl;
cout << "appl: " << (trie.search("appl") ? "找到" : "未找到") << endl;
cout << "band: " << (trie.search("band") ? "找到" : "未找到") << endl;
// 前缀测试
cout << "\n前缀测试:" << endl;
cout << "app: " << (trie.startsWith("app") ? "存在" : "不存在") << endl;
cout << "ban: " << (trie.startsWith("ban") ? "存在" : "不存在") << endl;
cout << "cat: " << (trie.startsWith("cat") ? "存在" : "不存在") << endl;
// 自动补全测试
cout << "\n自动补全测试 (app):" << endl;
auto suggestions = trie.getWordsWithPrefix("app");
for (const string& word : suggestions) {
cout << " " << word << endl;
}
// 删除测试
cout << "\n删除测试:" << endl;
DeletableTrie delTrie;
delTrie.insert("hello");
delTrie.insert("help");
cout << "删除前 hello: " << (delTrie.search("hello") ? "存在" : "不存在") << endl;
delTrie.remove("hello");
cout << "删除后 hello: " << (delTrie.search("hello") ? "存在" : "不存在") << endl;
cout << "help 仍然存在: " << (delTrie.search("help") ? "存在" : "不存在") << endl;
// 通配符搜索测试
cout << "\n通配符搜索测试:" << endl;
WildcardTrie wcTrie;
wcTrie.insert("hello");
wcTrie.insert("help");
wcTrie.insert("held");
cout << "搜索 h.llo: " << (wcTrie.searchWithWildcard("h.llo") ? "找到" : "未找到") << endl;
cout << "搜索 hel.: " << (wcTrie.searchWithWildcard("hel.") ? "找到" : "未找到") << endl;
cout << "搜索 h...o: " << (wcTrie.searchWithWildcard("h...o") ? "找到" : "未找到") << endl;
cout << "=== Trie 测试结束 ===" << endl;
}
int main() {
runTests();
// 演示实际应用
cout << "\n=== 实际应用演示 ===" << endl;
// 拼写检查器演示
SpellChecker checker;
checker.addWord("apple");
checker.addWord("apply");
checker.addWord("app");
cout << "拼写检查 'aple': ";
auto suggestions = checker.suggest("aple");
for (const string& sug : suggestions) {
cout << sug << " ";
}
cout << endl;
// 自动补全演示
AutoComplete ac;
ac.addWord("programming");
ac.addWord("program");
ac.addWord("programmer");
ac.addWord("progress");
cout << "自动补全 'prog': ";
auto acSuggestions = ac.getSuggestions("prog");
for (const string& sug : acSuggestions) {
cout << sug << " ";
}
cout << endl;
return 0;
}
7. 复杂度深入分析
7.1 时间复杂度
| 操作 | 最好情况 | 平均情况 | 最坏情况 |
|---|---|---|---|
| 插入 | O(m) | O(m) | O(m) |
| 搜索 | O(m) | O(m) | O(m) |
| 前缀搜索 | O(m) | O(m) | O(m) |
| 删除 | O(m) | O(m) | O(m) |
| 自动补全 | O(m + k) | O(m + k) | O(m + k) |
其中:
- m:字符串长度
- k:匹配的单词数量
7.2 空间复杂度
| 实现方式 | 空间复杂度 | 说明 |
|---|---|---|
| 数组实现 | O(σ×N) | σ为字符集大小,N为节点数 |
| 哈希表实现 | O(N) | N为节点数,常数因子较小 |
| 向量实现 | O(N) | 空间效率介于数组和哈希表之间 |
| 压缩字典树 | O(N) | 节点数显著减少 |
7.3 性能对比
// 性能测试代码
void performanceTest() {
const int TEST_SIZE = 10000;
vector<string> testWords;
// 生成测试数据
for (int i = 0; i < TEST_SIZE; i++) {
string word = "word" + to_string(i);
testWords.push_back(word);
}
// 测试Trie插入性能
auto start = chrono::high_resolution_clock::now();
Trie<TrieNodeMap> trie;
for (const string& word : testWords) {
trie.insert(word);
}
auto end = chrono::high_resolution_clock::now();
auto trieInsertTime = chrono::duration_cast<chrono::microseconds>(end - start);
// 测试搜索性能
start = chrono::high_resolution_clock::now();
for (const string& word : testWords) {
trie.search(word);
}
end = chrono::high_resolution_clock::now();
auto trieSearchTime = chrono::duration_cast<chrono::microseconds>(end - start);
cout << "Trie插入 " << TEST_SIZE << " 个单词: " << trieInsertTime.count() << " 微秒" << endl;
cout << "Trie搜索 " << TEST_SIZE << " 个单词: " << trieSearchTime.count() << " 微秒" << endl;
}
8. 设计模式应用
8.1 工厂模式创建不同类型的Trie
enum class TrieType {
ARRAY,
HASHMAP,
VECTOR,
SMART_POINTER
};
class TrieFactory {
public:
template<typename T>
static unique_ptr<Trie<T>> createTrie() {
return make_unique<Trie<T>>();
}
static unique_ptr<Trie<TrieNodeMap>> createDefaultTrie() {
return createTrie<TrieNodeMap>();
}
static unique_ptr<Trie<TrieNodeArray>> createArrayTrie() {
return createTrie<TrieNodeArray>();
}
};
8.2 观察者模式(监控Trie变化)
class TrieObserver {
public:
virtual void onInsert(const string& word) = 0;
virtual void onRemove(const string& word) = 0;
virtual ~TrieObserver() = default;
};
class ObservableTrie : public Trie<TrieNodeMap> {
private:
vector<TrieObserver*> observers;
public:
void addObserver(TrieObserver* observer) {
observers.push_back(observer);
}
void removeObserver(TrieObserver* observer) {
observers.erase(
remove(observers.begin(), observers.end(), observer),
observers.end()
);
}
void insert(const string& word) override {
Trie<TrieNodeMap>::insert(word);
for (auto* observer : observers) {
observer->onInsert(word);
}
}
bool remove(const string& word) {
bool result = DeletableTrie::remove(word);
if (result) {
for (auto* observer : observers) {
observer->onRemove(word);
}
}
return result;
}
};
9. 总结
字典树作为一种强大的字符串处理数据结构,具有以下优势:
- 高效查询:O(m)时间复杂度,与字典大小无关
- 前缀共享:节省存储空间,特别适合有公共前缀的字符串集合
- 功能丰富:支持自动补全、拼写检查、通配符搜索等高级功能
- 可扩展性强:易于添加权重、删除、监控等特性
675

被折叠的 条评论
为什么被折叠?



