Leetcode 题目解析:211. 添加与搜索单词 - 数据结构设计
系列文章:
一 摘要
在考察算法题时,我们往往离不开数据结构。而常见和常用的数据结构,以堆、栈、单/双链表、HashMap、各种二叉树(二叉树、平衡二叉树、搜索二叉树、红黑树)最为常见。另外,像bitmap等也比较多,尤其是需要位操作的时候。但还有一些数据结构也会占有一席之地,例如树中的Trie树(字典树),在检索类题目中也非常常见。
今天就以一道典型的字典树题目为例,再次熟悉一下这个数据结构。
二 题目描述与示例
2.1 描述
leetcode题目描述:
2.2 示例
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
2.3 基础知识——Trie树
2.3.1 概念
Trie树,又称单词查找树,是一种树形结构,哈希树的一种变种。Trie树的典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
2.3.2 基本特性
根节点不包含字符,除根节点外每一个节点都只包含一个字符;
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
每个节点的所有子节点包含的字符都不相同。
2.3.3 基本操作
Trie树的基本操作,有查找、插入和删除。在实际应用场景中,删除操作比较少见。
Trie树可以用
2.3.4 Trie与哈希表的对比
三 题目解析
3.1 描述解析
题目描述已经比较详细,这个就不用特别解释了。就是把输入的字符串逐个放到我们定义的WordDictionary结构中,并支持查找。这里实现3个方法,构造方法:WordDictionary(),添加方法 addWord(word),查找方法 search(word)。
3.2 示例解析
输入是两个数组,第一个数组是方法数组,按照顺序依次是构造,添加x3,查找x4;第二个数组是方法的参数,根据坐标一一对应。输出是上述8个方法的执行结果,构造方法和添加方法返回null,所以我们只要保证添加结果正确和查找判断是否存在方法准确,再封装成数组结构即可。
四 实现
4.1 关键问题
重点在于查找方法,对于搜索单词,从字典树的根结点开始搜索。由于待搜索的单词可能包含点号,因此在搜索过程中需要考虑点号的处理。对于当前字符是字母和点号的情况,分别按照如下方式处理:
如果当前字符是字母,则判断当前字符对应的子结点是否存在,如果子结点存在则移动到子结点,继续搜索下一个字符,如果子结点不存在则说明单词不存在,返回false;
如果当前字符是点.,由于点号可以表示任何字母,因此需要对当前结点的所有非空子结点继续搜索下一个字符。
重复上述步骤,直到返回false 或搜索完给定单词的最后一个字符。搜索完给定单词的最后一个字符,也就是搜索到的最后一个结点的isEnd标记为true时,判定给定的单词存在。特别情况:当搜索到点号时,只要存在一个非空子结点可以搜索到给定的单词,即返回true。
4.2 代码实现
还是采用Java代码实现,包括Trie树和字典两个类。
4.2.1 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;
}
}
4.2.2 WordDictionary
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;
}
}
4.3 复杂度分析
4.3.1 时间复杂度
初始化动作的时间复杂度为O(1),添加单词为O(∣S∣),搜索单词为 O(∣Σ∣|S∣),其中∣S∣ 是每次添加或搜索的单词的长度,Σ 是字符集,这道题中的字符集为全部小写英语字母,∣Σ∣=26。
最坏情况下,待搜索的单词中的每个字符都是点号,则每个字符都有∣Σ∣ 种可能。
4.3.2 空间复杂度
O(∣T∣⋅∣Σ∣),其中∣T∣ 是所有添加的单词的长度之和,Σ 是字符集,这道题中的字符集为全部小写英语字母,∣Σ∣=26。