问题描述
在开发SQL编辑器时,实现自动补全功能是提高用户体验的重要一环。小C需要实现一个功能,根据用户输入的字符片段,快速从已知的SQL关键字和数据库相关名称中找到所有以该片段开头的候选词,并按字典序输出。
例如,当用户输入 s 时,编辑器需要自动提示以 s 开头的所有可能选项,如 select。如果用户输入 fr,则需要提示 from 和 from_mobile。如果在提示中只有一个选项符合,如输入 from_ 时只提示 from_mobile。
测试样例
样例1:
输入:
num = 8,data = ["select", "from", "where", "limit", "origin_log_db", "event_log_table", "user_id", "from_mobile"], input = "f"输出:'from,from_mobile'
样例2:
输入:
num = 8,data = ["select", "from", "where", "limit", "origin_log_db", "event_log_table", "user_id", "from_mobile"], input = "wh"输出:'where'
样例3:
输入:
num = 8,data = ["select", "from", "where", "limit", "origin_log_db", "event_log_table", "user_id", "from_mobile"], input = "z"输出:'-1'
样例4:
输入:
num = 8,data = ["select", "from", "where", "limit", "origin_log_db", "event_log_table", "user_id", "from_mobile"], input = "origin"输出:'origin_log_db'
问题思路与分析
这道题其实可以采用字典树的数据结构来解决,那么什么是字典树呢?
字典树(Trie),又称为前缀树或字典树,是一种树形数据结构,专门用于存储字符串集合,特别适用于需要高效检索和匹配字符串的场景。字典树的核心思想是通过共享公共前缀来减少存储空间,从而提高检索效率。
下图便是一个字典树的结构:
代码层面上来讲,字典树的核心组成部分包括 next 数组和 isEnd 标志。
主要组成部分
next 数组:
- next 数组用于存储子节点的引用。每个节点的 next 数组大小通常为26,对应26个小写字母。如果需要支持大写字母或其他字符,可以相应地调整数组大小。
- 例如,对于小写字母,next 数组的索引范围是0到25,分别对应字符 'a' 到 'z'。如果需要支持大写字母,可以将 next 数组的大小扩展到52,或者使用哈希表来存储子节点。
isEnd 标志:
- isEnd 是一个布尔标志,用于标记当前节点是否是一个完整单词的结尾。
- 如果 isEnd 为 true,则表示从根节点到当前节点的路径构成一个完整的单词。
节点结构
class TrieNode {
boolean isEnd;
TrieNode[] next;
TrieNode() {
next = new TrieNode[26]; // 假设只包含小写字母
isEnd = false;
}
}
基本操作
1.插入操作:
- 从根节点开始,逐字符插入单词。
- 如果当前字符对应的子节点不存在,则创建一个新的子节点。
- 插入完所有字符后,将最后一个节点的 isEnd 标志设置为 true。
void insert(TrieNode root, String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a'; // 计算字符在 'a' 到 'z' 中的位置
if (node.next[index] == null) {
node.next[index] = new TrieNode();
}
node = node.next[index];
}
node.isEnd = true;
}
2.查找操作:
- 从根节点开始,逐字符查找单词。
- 如果某个字符对应的子节点不存在,则返回 false。
- 如果成功遍历到单词的最后一个字符,并且该节点的 isEnd 标志为 true,则返回 true,即完整地找到整个单词。
boolean search(TrieNode root, String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a'; // 计算字符在 'a' 到 'z' 中的位置
if (node.next[index] == null) {
return false;
}
node = node.next[index];
}
return node.isEnd;
}
3.前缀匹配:
- 从根节点开始,逐字符查找前缀。
- 如果某个字符对应的子节点不存在,则返回 false。
- 如果成功遍历到前缀的最后一个字符,则返回 true,表示存在以该前缀开头的单词,和查找操作一个流程,只是不需要isEnd为true了。
boolean startsWith(TrieNode root, String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int index = c - 'a'; // 计算字符在 'a' 到 'z' 中的位置
if (node.next[index] == null) {
return false;
}
node = node.next[index];
}
return true;
}
4.支持大写字母
如果需要支持大写字母,可以对根节点进行特殊处理,或者使用更大的 next 数组。以下是支持大写字母的示例:
class TrieNode {
boolean isEnd;
TrieNode[] next;
TrieNode() {
next = new TrieNode[52]; // 支持小写字母和大写字母
isEnd = false;
}
}
void insert(TrieNode root, String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index;
if (Character.isLowerCase(c)) {
index = c - 'a';
} else {
index = 26 + c - 'A';
}
if (node.next[index] == null) {
node.next[index] = new TrieNode();
}
node = node.next[index];
}
node.isEnd = true;
}
boolean search(TrieNode root, String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index;
if (Character.isLowerCase(c)) {
index = c - 'a';
} else {
index = 26 + c - 'A';
}
if (node.next[index] == null) {
return false;
}
node = node.next[index];
}
return node.isEnd;
}
boolean startsWith(TrieNode root, String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int index;
if (Character.isLowerCase(c)) {
index = c - 'a';
} else {
index = 26 + c - 'A';
}
if (node.next[index] == null) {
return false;
}
node = node.next[index];
}
return true;
}
Trie树的应用
Trie 树在现代软件开发中有着广泛的应用,尤其在需要进行字符串快速检索和前缀匹配的场景中表现出色。
在日常使用的搜索引擎中,当你开始输入搜索关键词时,搜索框下方会立即显示一些相关的搜索建议,这就是 Trie 树实现自动补全的典型应用。类似地,在使用输入法时,输入法会根据你已经输入的字符预测可能的词组和句子。在集成开发环境(IDE)中,当你编写代码时,IDE 能够实时提供代码补全建议,这些功能背后都可能使用了 Trie 树来提供高效的前缀匹配服务。
在文字处理软件中,Trie 树常被用于实现拼写检查功能。当你输入一个词时,系统能够快速判断这个词是否存在于词典中,如果发现可能的拼写错误,还能够提供相似的正确拼写建议。这种功能不仅提高了文字处理的效率,还能帮助用户避免拼写错误。