剑指Offer——Trie树(字典树)
剑指Offer——Trie树(字典树)
Trie树
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。
分析:这题当然可以用hash来解决,但是本文重点介绍的是trie树,因为在某些方面它的用途更大。比如说对于某一个单词,我们要询问它的前缀是否出现过。这样hash就不好搞了,而用trie还是很简单。
假设我要查询的单词是abcd,那么在他前面的单词中,以b,c,d,f之类开头的我显然不必考虑。而只要找以a开头的中是否存在abcd就可以了。同样的,在以a开头中的单词中,我们只要考虑以b作为第二个字母的,一次次缩小范围和提高针对性,这样一个树的模型就渐渐清晰了。
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,我们构建的树就是如下图这样的(图片来自百度百科):
如上图所示,对于每一个节点,从根遍历到他的过程就是一个单词,如果这个节点被标记为红色,就表示这个单词存在,否则不存在。
那么,对于一个单词,我只要顺着他从根走到对应的节点,再看这个节点是否被标记为红色就可以知道它是否出现过了。的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:
最容易想到的:1.即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。
2.使用hash:我们用hash存下所有字符串的所有前缀子串,建立存有子串hash的复杂度为O(n*len),而查询的复杂度为O(n)* O(1)= O(n)。
3.使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以称为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度也只是O(len)。(次比较可以从26^5=11881376个可能的关键字中检索出指定的关键字。而利用二叉查找树至少要进行次比较。
应用
1. 字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2、给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3、给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
5、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
6、寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G(京东笔试题简答题与此类似)。
(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。
2. 字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:
1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少. 解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
1. 利用并查集(Disjoint Set),可以采用经典的Tarjan 算法;
2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
3. 排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例:给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
4. 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等。
举例
下面以字典树的构建与单词查找为例。
TrieTreeNode.java
package cn.edu.ujn.trieTree;
public class TrieTreeNode {
int nCount; //记录该字符出现次数
char ch; //记录该字符
TrieTreeNode[] child; // 记录子节点
final int MAX_SIZE = 26;
public TrieTreeNode() {
nCount=1;
child = new TrieTreeNode[MAX_SIZE];
}
}
TrieTree.java
package cn.edu.ujn.trieTree;
public class TrieTree {
//字典树的插入和构建
public void createTrie(TrieTreeNode node,String str){
if (str == null || str.length() == 0) {
return;
}
char[] letters = str.toCharArray();
for (int i = 0; i < letters.length; i++) {
int pos = letters[i] - 'a'; // 用相对于a字母的值作为下标索引,也隐式地记录了该字母的值
if (node.child[pos] == null) {
node.child[pos] = new TrieTreeNode();
}else {
node.child[pos].nCount++;
}
node.ch = letters[i];
node = node.child[pos];
}
}
//字典树的查找
public int findCount(TrieTreeNode node,String str){
if (str == null || str.length() == 0) {
return -1;
}
char[] letters = str.toCharArray();
for (int i = 0; i < letters.length; i++) {
int pos = letters[i] - 'a';
if (node.child[pos] == null) {
return 0;
}else {
node = node.child[pos];
}
}
return node.nCount;
}
}
Test.java
package cn.edu.ujn.trieTree;
public class Test {
public static void main(String[] args) {
/**
* Problem Description 老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计
* 出以某个字符串为前缀的单词数量(单词本身也是自己的前缀).
*/
String[] strs = { "banana", "band", "bee", "absolute", "acm" };
String[] prefix = { "ba", "b", "band", "abc" };
TrieTree tree = new TrieTree();
TrieTreeNode root = new TrieTreeNode();
for (String s : strs) {
tree.createTrie(root, s);
}
// tree.printAllWords();
for (String pre : prefix) {
int num = tree.findCount(root, pre);
System.out.println(pre + " " + num);
}
}
}
小结
看过上面的代码,是否发现这个代码有什么问题呢?尽管这个实现方式查找的效率很高,时间复杂度是O(m),m是要查找的单词中包含的字母的个数。但是确浪费大量存放空指针的存储空间。因为不可能每个节点的子节点都包含26个字母的。所以对于这个问题,字典树存在的意义是解决快速搜索的问题,所以采取以空间换时间的作法也毋庸置疑。
Trie树占用内存较大,例如:处理最大长度为20、全部为小写字母的一组字符串,则可能需要 2620 个节点来保存数据。而这样的树实际上稀疏的十分厉害,可以采用左儿子右兄弟的方式来改善,也可以采用需要多少子节点则添加多少子节点来解决(不要类似网上的示例,每个节点初始化时就申请一个长度为26的数组)。
Wiki上提到了采用三数组Trie(Tripple-Array Trie)和二数组Trie(Double-Array Trie)来解决该问题,此外还有压缩等方式来缓解该问题。
示例优化
TrieTreeNode.java
package cn.edu.ujn.trieTreeMap;
import java.util.HashMap;
import java.util.Map;
public class TrieNode {
int nCount; //记录该字符出现次数
Map<Character, TrieNode> childdren; // 记录子节点
public TrieNode() {
nCount = 1;
childdren = new HashMap<Character, TrieNode>();
}
}
TrieTree.java
package cn.edu.ujn.trieTreeMap;
// 利用Map动态创建节点
public class TrieTree {
// 字典树的插入和构建
public void insert(TrieNode node, String word) {
for (int i = 0; i < word.length(); i++) {
Character c = new Character(word.charAt(i));
if (!node.childdren.containsKey(c)) {
node.childdren.put(c, new TrieNode());
}else{
node.childdren.get(c).nCount++;
}
node = node.childdren.get(c);
}
}
// 字典树的查找
public int search(TrieNode node, String word) {
for (int i = 0; i < word.length(); i++) {
Character c = new Character(word.charAt(i));
if (!node.childdren.containsKey(c)) {
return 0;
}
node = node.childdren.get(c);
}
return node.nCount;
}
}
Test.java
package cn.edu.ujn.trieTreeMap;
public class Test {
public static void main(String[] args) {
/**
* Problem Description 老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计
* 出以某个字符串为前缀的单词数量(单词本身也是自己的前缀).
*/
String[] strs = { "banana", "band", "bee", "absolute", "acm" };
String[] prefix = { "ba", "b", "band", "abc" };
TrieTree tree = new TrieTree();
TrieNode node = new TrieNode();
for (String s : strs) {
tree.insert(node, s);
}
// tree.printAllWords();
for (String pre : prefix) {
int num = tree.search(node, pre);
System.out.println(pre + " " + num);
}
}
}
计算结果如下:
经过以上方法的改进,可避免冗余节点的存在。将字典树的优势进一步放大。当然,也可以使用左儿子右兄弟的形式创建字典树。此方法后续介绍~
文件读入
package cn.edu.ujn.trieTreeMap;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) {
/**
* Problem Description 老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计
* 出以某个字符串为前缀的单词数量(单词本身也是自己的前缀).
*/
String[] strs = { "banana", "band", "bee", "absolute", "acm" };
String[] prefix = { "网易", "软件", "band", "abc" };
TrieTree tree = new TrieTree();
TrieNode node = new TrieNode();
BufferedReader br = null;
try {
File file= new File("C://Users//SHQ//Desktop//Offer.txt");
//读取语料库words.txt
br = new BufferedReader(new InputStreamReader(new FileInputStream(file.getAbsolutePath()),"GBK"));
String word="";
while ((word = br.readLine()) != null) {
tree.insert(node, word);
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
/*for (String s : strs) {
tree.insert(node, s);
}*/
// tree.printAllWords();
for (String pre : prefix) {
int num = tree.search(node, pre);
System.out.println(pre + " " + num);
}
}
}
计算结果如下:
Offer.txt文本内容如下:
可知计算结果正确。而且出现了中文字符,对于数字的操作同理。而利用第一种方法就无法实现固定分配内存。只能使用动态分配机制。
美文美图
剑指Offer——Trie树(字典树)的更多相关文章
- 剑指Offer - 九度1520 - 树的子结构
剑指Offer - 九度1520 - 树的子结构2013-11-30 22:17 题目描述: 输入两颗二叉树A,B,判断B是不是A的子结构. 输入: 输入可能包含多个测试样例,输入以EOF结束.对于每 ...
- 【剑指offer】q50:树节点最近的祖先
#@ root: the root of searched tree #@ nodeToFind: the tree-node to be found #@ path: the path from r ...
- 剑指offer(17)树的子结构
题目描述 输入两棵二叉树A,B,判断B是不是A的子结构.(ps:我们约定空树不是任意一个树的子结构) 题目分析 分析如何判断树B是不是树A的子结构,只需要两步.很容易看出来这是一个递归的过程.一般在树 ...
- [剑指Offer]判断一棵树为平衡二叉树(递归)
题目链接 https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=0&tqId=0&rp=2&a ...
- 【剑指Offer】17、树的子结构
题目描述: 输入两棵二叉树A,B,判断B是不是A的子结构.(ps:我们约定空树不是任意一个树的子结构) 解题思路: 要查找树A中是否存在和树B结构一样的子树,我们可以分为两步:第一步, ...
- 剑指offer(17)层次遍历树
题目: 从上往下打印出二叉树的每个节点,同层节点从左至右打印. public class Solution { ArrayList<Integer> list = new ArrayLis ...
- 剑指Offer:面试题18——树的子结构(java实现)
问题描述: 输入两棵二叉树A和B,判断B是不是A的子结构.二叉树结点的定义如下: public class TreeNode { int val = 0; TreeNode left = null; ...
- 【剑指offer】Q18:树的子结构
类似于字符串的匹配,我们总是找到第一个匹配的字符,在继续比較以后的字符是否所有同样,假设匹配串的第一个字符与模式串的第一个不同样,我们就去查看匹配串的下一个字符是否与模式串的第一个同样,相应到这里,就 ...
- 《剑指offer》内容总结
(1)剑指Offer——Trie树(字典树) Trie树 Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种.典型应用是统计和排序大量的字符串(但不仅限于字符串),所以经常 ...
随机推荐
- [NOIp 2014]解方程
Description 已知多项式方程: a0+a1x+a2x^2+..+anx^n=0 求这个方程在[1, m ] 内的整数解(n 和m 均为正整数) Input 输入文件名为equation .i ...
- codefroces 911G Mass Change Queries
题意翻译 给出一个数列,有q个操作,每种操作是把区间[l,r]中等于x的数改成y.输出q步操作完的数列. 输入输出格式 输入格式: The first line contains one intege ...
- 【BZOJ4003】【JLOI2015】城池攻占
Description 小铭铭最近获得了一副新的桌游,游戏中需要用 m 个骑士攻占 n 个城池.这 n 个城池用 1 到 n 的整数表示.除 1 号城池外,城池 i 会受到另一座城池 fi 的管辖,其 ...
- bzoj 3998: [TJOI2015]弦论
Description 对于一个给定长度为N的字符串,求它的第K小子串是什么. Input 第一行是一个仅由小写英文字母构成的字符串S 第二行为两个整数T和K,T为0则表示不同位置的相同子串算作一个. ...
- ●BZOJ 1853 [Scoi2010]幸运数字
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=1853 题解: 容斥原理,暴力搜索,剪枝(这剪枝剪得真玄学) 首先容易发现,幸运号码不超过 2 ...
- 【Toll!Revisited(uva 10537)】
题目来源:蓝皮书P331 ·这道题使得我们更加深刻的去理解Dijkstra! 在做惯了if(dis[u]+w<dis[v])的普通最短路后,这道选择路径方案不是简单的比大小的题横在了 ...
- Linux查看日志方法总结(1)
注:日志文件为:test.log 1.tail -f test.log 查看当前打印的日志(平时就知道这方法!打印出的长度有限制.) 以下为网上搜集的: 2.先必须了解两个最基本的命令: tail ...
- 智能优化算法对TSP问题的求解研究
要求: TSP 算法(Traveling Salesman Problem)是指给定 n 个城市和各个城市之间的距离,要 求确定一条经过各个城市当且仅当一次的最短路径,它是一种典型的优化组合问题,其最 ...
- Linux配置服务器的一点总结
一.Linux初始化服务 首先搞清楚四个概念: 进程:正在运行的程序,有自己独立的内存空间. 线程:是进程的下属单位,开销较进程小,没有自己独立的内存空间. 作业:由一系列进程组成,来完成某一项任务. ...
- 关于 printf scanf getchar
float默认小数6位 右对齐.-m 左对齐 在调用printf函数输出数据时,当数据的实际位宽大于printf函数中的指定位宽时,将按照数据的实际位宽输出数据. .n表精度 输出%符号 注意点 #i ...