系列文章:
算法题目解析:从一道题目看动态规划
Leetcode 题目解析:274. H 指数
Leetcode 题目解析:279. 完全平方数
Leetcode 题目解析:287. 寻找重复数
在考察算法题时,我们往往离不开数据结构。而常见和常用的数据结构,以堆、栈、单/双链表、HashMap、各种二叉树(二叉树、平衡二叉树、搜索二叉树、红黑树)最为常见。另外,像bitmap等也比较多,尤其是需要位操作的时候。但还有一些数据结构也会占有一席之地,例如树中的Trie树(字典树),在检索类题目中也非常常见。
今天就以一道典型的字典树题目为例211. 添加与搜索单词 - 数据结构设计,再次熟悉一下这个数据结构。
leetcode题目描述:
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary() 初始化词典对象
void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配
bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
Trie树,又称单词查找树,是一种树形结构,哈希树的一种变种。Trie树的典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
Trie树的基本操作,有查找、插入和删除。在实际应用场景中,删除操作比较少见。
Trie树可以用O(∣S∣) 的时间复杂度完成向字典树插入元素 和 查询字符串是否在树中两个操作,其中 ∣S∣ 是插入字符串或查询前缀的长度:
题目描述已经比较详细,这个就不用特别解释了。就是把输入的字符串逐个放到我们定义的WordDictionary结构中,并支持查找。这里实现3个方法,构造方法:WordDictionary(),添加方法 addWord(word),查找方法 search(word)。
输入是两个数组,第一个数组是方法数组,按照顺序依次是构造,添加x3,查找x4;第二个数组是方法的参数,根据坐标一一对应。输出是上述8个方法的执行结果,构造方法和添加方法返回null,所以我们只要保证添加结果正确和查找判断是否存在方法准确,再封装成数组结构即可。
重点在于查找方法,对于搜索单词,从字典树的根结点开始搜索。由于待搜索的单词可能包含点号,因此在搜索过程中需要考虑点号的处理。对于当前字符是字母和点号的情况,分别按照如下方式处理:
重复上述步骤,直到返回false 或搜索完给定单词的最后一个字符。搜索完给定单词的最后一个字符,也就是搜索到的最后一个结点的isEnd标记为true时,判定给定的单词存在。特别情况:当搜索到点号时,只要存在一个非空子结点可以搜索到给定的单词,即返回true。
还是采用Java代码实现,包括Trie树和字典两个类。
Trie节点由children(trie数组)和isEnd标记两个元素组成,通过children构成树状结构,通过isEnd标记,标识单词到达结尾。
public class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public Trie[] getChildren() {
return children;
}
public boolean isEnd() {
return isEnd;
}
}
package com.flamingstar.leetcode.tree;
public class WordDictionary {
private Trie root;
public WordDictionary() {
root = new Trie();
}
public void addWord(String word) {
root.insert(word);
}
public boolean search(String word) {
return dfs(word, 0, root);
}
private boolean dfs(String word, int index, Trie node) {
if (index == word.length()) {
return node.isEnd();
}
char ch = word.charAt(index);
if (Character.isLetter(ch)) {
int childIndex = ch - 'a';
Trie child = node.getChildren()[childIndex];
if (child != null && dfs(word, index + 1, child)) {
return true;
}
} else {
for (int i = 0; i < 26; i++) {
Trie child = node.getChildren()[i];
if (child != null && dfs(word, index + 1, child)) {
return true;
}
}
}
return false;
}
}
初始化动作的时间复杂度为O(1),添加单词为O(∣S∣),搜索单词为 O(∣Σ∣|S∣),其中∣S∣ 是每次添加或搜索的单词的长度,Σ 是字符集,这道题中的字符集为全部小写英语字母,∣Σ∣=26。
最坏情况下,待搜索的单词中的每个字符都是点号,则每个字符都有∣Σ∣ 种可能。
O(∣T∣⋅∣Σ∣),其中∣T∣ 是所有添加的单词的长度之和,Σ 是字符集,这道题中的字符集为全部小写英语字母,∣Σ∣=26。