第一部分、Trie树
1.1、什么是Trie树
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
它有3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,我们构建的树就是如下图这样的
如上图所示,对于每一个节点,从根遍历到他的过程就是一个单词,如果这个节点被标记为红色,就表示这个单词存在,否则不存在。
那么,对于一个单词,我只要顺着他从根走到对应的节点,再看这个节点是否被标记为红色就可以知道它是否出现过了。
把这个节点标记为红色,就相当于插入了这个单词。
1.2、前缀查询
“比如说对于某一个单词,我们要询问它的前缀是否出现过。这样hash就不好搞了,而用trie还是很简单“。下面,咱们来看看这个前缀查询问题: 已知n个由小写字母构成的平均长度为10的单词,判断其中
是否存在某个串为另一个串的前缀子串。下面对比3种方法:
- 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。
- 使用hash:我们用hash存下所有字符串的所有的前缀子串,建立存有子串hash的复杂度为O(n*len),而查询的复杂度为O(n)* O(1)= O(n)。
- 使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d….等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度也只是O(len)。(说白了,就是Trie树的平均高度h为len,所以Trie树的查询复杂度为O(h)=O(len)。好比一棵二叉平衡树的高度为logN,则其查询,插入的平均时间复杂度亦为O(logN))。有了这样一种数据结构,我们可以用它来保存一个字典,要查询改字典里是否有相应的词,是否非常的方便呢?我们也可以做智能提示,我们把用户已经搜索的词存在Trie里,每当用户输入一个词的时候,我们可以自动提示
1.3、Trie树的应用
除了本文引言处所述的问题能应用Trie树解决之外,Trie树还能解决下述问题:
- 3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
- 9、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
- 10、 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
- 13、寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。
Java代码
package TrieDemo;
import java.util.LinkedList;
/**
* Created by fernando on 12/3/16.
*/
public class Node {
char content; // the character in the node
boolean isEnd; // whether the end of the words
int count; // the number of words sharing this character
LinkedList<Node> childList; // the child list
public Node(char c) {
childList = new LinkedList<Node>();
isEnd = false;
content = c;
count = 0;
}
public Node subNode(char c) {
if (childList != null) {
for (Node eachChild : childList) {
if (eachChild.content == c) {
return eachChild;
}
}
}
return null;
}
}
package TrieDemo;
/**
* Created by fernando on 12/3/16.
*/
public class Trie {
private Node root;
public Trie() {
root = new Node(' ');
}
public void insert(String word) {
if (search(word) == true) return;
Node current = root;
for (int i = 0; i < word.length(); i++) {
Node child = current.subNode(word.charAt(i));
if (child != null) {
current = child;
} else {
current.childList.add(new Node(word.charAt(i)));
current = current.subNode(word.charAt(i));
}
current.count++;
}
// Set isEnd to indicate end of the word
current.isEnd = true;
}
public boolean search(String word) {
Node current = root;
for (int i = 0; i < word.length(); i++) {
if (current.subNode(word.charAt(i)) == null)
return false;
else
current = current.subNode(word.charAt(i));
}
/*
* This means that a string exists, but make sure its
* a word by checking its 'isEnd' flag
*/
if (current.isEnd == true) return true;
else return false;
}
public void deleteWord(String word) {
if (search(word) == false) return;
Node current = root;
for (char c : word.toCharArray()) {
Node child = current.subNode(c);
if (child.count == 1) {
current.childList.remove(child);
return;
} else {
child.count--;
current = child;
}
}
current.isEnd = false;
}
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("ball");
trie.insert("balls");
trie.insert("sense");
// testing deletion
System.out.println(trie.search("balls"));
System.out.println(trie.search("ba"));
trie.deleteWord("balls");
System.out.println(trie.search("balls"));
System.out.println(trie.search("ball"));
}
}