概述
字典树(Trie),又称单词查找树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计、排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。
性质
字典树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
字典树的三个性质:
- 根节点不包含字符,根节点外每一个节点都只包含一个字符
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
- 每个节点的所有子节点包含的字符都不相同
数据结构实现
C语言实现
1. 定义数据结构
typedef struct TrieNode {
struct TrieNode *children[26]; // 最多26个子节点,因为只有26个英文字母
int cnt; // 记录该单词出现的次数,只有叶子节点时cnt才需要+1
} TrieNode;
2. 初始化根结点
/**
* 初始化Trie树根结点
*/
void initTrie(TrieNode **root)
{
int i;
*root = (TrieNode *)malloc(sizeof(TrieNode));
(*root)->cnt = 0;
for (i = 0; i < 26; i++) {
(*root)->children[i] = NULL;
}
}
3. 插入单词到Trie树
/**
* Trie树插入操作
*/
void insert(char *str, TrieNode *root)
{
int i;
TrieNode *p = root;
while (*str != '\0') {
if (p->children[*str - 'a'] == NULL) {
TrieNode *tmp = (TrieNode *)malloc(sizeof(TrieNode));
for (i = 0; i < 26; i ++) {
tmp->children[i] = NULL;
}
tmp->cnt = 0;
p->children[*str - 'a'] = tmp;
p = p->children[*str - 'a'];
} else {
p = p->children[*str - 'a'];
}
str++;
}
p->cnt++;
}
4. 统计查找单词数量
/**
* 统计前缀出现次数
*/
int count(char *search, TrieNode *root)
{
TrieNode *p = root;
while (*search != '\0') {
if (p->children[*search - 'a'] == NULL) {
return 0;
} else {
p = p->children[*search - 'a'];
search ++;
}
}
return p->cnt;
}
5. 清理Trie树/**
* 清理Trie树
*/
void delTrie(TrieNode *root)
{
int i;
for (i = 0; i < 26; i++) {
if (root->children[i] != NULL) {
delTrie(root->children[i]);
}
}
free(root);
}
Java语言实现
同C语言实现相比,使用Java会让代码更具抽象性,代码会显得高内聚,低耦合。Java实现的Trie树代码如下:
public static class TrieNode {
private TrieNode[] children;
private int cnt;
public TrieNode() {
children = new TrieNode[26];
cnt = 0;
}
/**
* 插入单词
*/
public void insert(String word) {
if (word == null || word.length() == 0) {
return;
}
TrieNode p = this;
for (int i = 0, len = word.length(); i < len; i++) {
int loc = word.charAt(i) - 'a';
if (p.children[loc] == null) {
p.children[loc] = new TrieNode();
}
p = p.children[loc];
}
p.cnt++;
}
/**
* 搜索单词
*/
public TrieNode search(String word) {
if (word == null || word.length() == 0) {
return null;
}
TrieNode p = this;
for (int i = 0, len = word.length(); i < len; i++) {
int loc = word.charAt(i) - 'a';
if (p.children[loc] == null) {
return null;
}
p = p.children[loc];
}
return p;
}
public int getCnt() {
return cnt;
}
}
时间复杂度
插入、查找的时间复杂度均为O(n),n为字符串的长度。空间复杂度较高,O(26^n),典型空间换时间。
参考题目
自从进入Android领域,我就被Java语言的抽象性吸引住了,目前做ACM基本都是用Java实现。这里举一个牛客网上的例子.
题目:
请设计一个高效的方法,找出任意指定单词在一篇文章中的出现频数。
给定一个string数组article和数组大小n及一个待统计单词word,请返回该单词在文章中的出现频数。保证文章的词数小于等于1000。
给定一个string数组article和数组大小n及一个待统计单词word,请返回该单词在文章中的出现频数。保证文章的词数小于等于1000。
解答:
import java.util.*;
public class Frequency {
public int getFrequency(String[] article, int n, String word) {
TrieNode root = new TrieNode();
for (int i = 0; i < n; i ++) {
root.insert(article[i]);
}
TrieNode p = root.search(word);
if (p == null) {
return 0;
} else {
return p.cnt;
}
}
public static class TrieNode {
public TrieNode[] son;
public int cnt;
public TrieNode() {
this.son = new TrieNode[26];
this.cnt = 0;
}
public void insert(String word) {
if (word == null || word.length() == 0) {
return;
}
TrieNode p = this;
for (int i = 0, len = word.length(); i < len; i++) {
int loc = word.charAt(i) - 'a';
if (p.son[loc] == null) {
p.son[loc] = new TrieNode();
}
p = p.son[loc];
}
p.cnt++;
}
public TrieNode search(String word) {
if (word == null || word.length() == 0) {
return null;
}
TrieNode p = this;
for (int i = 0, len = word.length(); i < len; i++) {
int loc = word.charAt(i) - 'a';
if (p.son[loc] == null) {
return null;
}
p = p.son[loc];
}
return p;
}
}
}
后记
其实博主写这篇博客,也是因为电话面试涂鸦移动的时候被问到了智能提示,其实智能提示实现不难,trie树+top k,中文的话转成拼音存储即可,我当时虽然回答出来了,但是连连看的题目没回答上,蛋疼