数据结构系列——Trie树

Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

Trie树结构

Trie的核心思想是空间换时间:利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

举例:将tea,ten,to,in,inn,int几个单词构建成一个Trie树,看一下具体的Tried树的结构:

图:Trie树结构
Trie树结构

从图中可以看出Trie树的某些特性:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。
Trie树实现

Trie树的插入、删除、查找的操作都是一样的,只需要简单的对树进行一遍遍历即可,时间复杂度:O(n)(n是字符串的长度)。
对于Tried树的实现可以使用数组和链表两种方式:

  • 数组:由于我们知道一个Tried树节点的子节点的数量是固定26个(针对不同情况会不同,比如兼容数字,则是36等),所以可以使用固定长度的数组来保存节点的子节点
    • 优点:在对子节点进行查找时速度快
    • 缺点:浪费空间,不管子节点有多少个,总是需要分配26个空间
  • 链表:使用链表的话我们需要在每个子节点中保存其兄弟节点的链接,当我们在一个节点的子节点中查找是否存在一个字符时,需要先找到其子节点,然后顺着子节点的链表从左往右进行遍历
    • 优点:节省空间,有多少个子节点就占用多少空间,不会造成空间浪费
    • 缺点:对子节点进行查找相对较慢,需要进行链表遍历,同时实现也较数组麻烦

下面是最简单的Trie树的实现,采用数组的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* <p>
* 最简单的Trie树结构,仅表示出Trie树的结构,实际应用需进行扩展
* </p>
*
* @author Vicky
* @email vicky01200059@163.com
* @2015年11月23日
*
*/

public class Trie {
protected TrieNode root = new TrieNode('a');// TrieTree的根节点

/**
* 插入
*
* @param word
*/

public void insertWord(String word) {
TrieNode index = this.root;
for (char c : word.toLowerCase().toCharArray()) {
index = index.addChild(c);
}
return;
}

/**
* TrieTree的节点
*/

private class TrieNode {
/** 该节点的字符 */
private final char nodeChar;//
/** 一个TrieTree的节点的子节点 */
private TrieNode[] childNodes = null;

public TrieNode(char nodeChar) {
super();
this.nodeChar = nodeChar;
}

public TrieNode addChild(char ch) {
int index = ch - 'a';
if (null == childNodes) {
this.childNodes = new TrieNode[26];
}
if (null == childNodes[index]) {
childNodes[index] = new TrieNode(ch);
}
return childNodes[index];
}

@Override
public String toString() {
return "TrieNode [nodeChar=" + nodeChar + "]";
}

}

public static void main(String[] args) {
Trie trie = new Trie();
trie.insertWord("Vicky");
}
}
Trie树应用

Trie树的应用主要是集中于字符串的处理。
(1)字符串检索

  • 精确查找:给定一组字符串,查找某个字符串是否出现过
  • 前缀匹配:给定一组字符串,查找以某个字符串为前缀的字符串集合

(2)最长公共前缀

  • 查找一组字符串的最长公共前缀,只需要将这组字符串构建成Trie树,然后从跟节点开始遍历,直到出现多个节点为止(即出现分叉)

(3)排序

  • 将一组字符串按照字典序进行排序,只需构建成Trie树,然后按照先序遍历即可

。。。

下面我们针对Trie树的应用,选择一个相对简单且有代表性的案例:从一组单词中查找所有以“vi”开头的字符串,同时查找“Vicky”是否出现过。(由于我们只保存26个字符,所以不区分大小写,也不支持非单词字符)。

从网上download下一个单词表,根据单词表构建Trie树,并进行查找,单词表可以从https://github.com/dwyl/english-words进行下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package com.vicky.datastructure.tree.trie;

/**
* <p>
* 一个支持前缀查找以及精确查找的Trie树
* </p>
*
* @author Vicky
* @email vicky01200059@163.com
* @2015年11月23日
*
*/

public class PrefixTrie {
private TrieNode root = new TrieNode('a');// TrieTree的根节点

/**
* 插入
*
* @param word
*/

public void insertWord(String word) {
TrieNode index = this.root;
for (char c : word.toLowerCase().toCharArray()) {
index = index.addChild(c);
index.addPrefixCount();
}
index.addCount();
return;
}

/**
* 查找
*
* @param word
* @return
*/

public boolean selectWord(String word) {
TrieNode index = this.root;
for (char c : word.toLowerCase().toCharArray()) {
index = index.getChild(c);
if (null == index) {
return false;
}
}
return index.getCount() > 0;
}

/**
* 查找前缀出现的次数
*
* @param prefix
* @return
*/

public int selectPrefixCount(String prefix) {
TrieNode index = this.root;
for (char c : prefix.toLowerCase().toCharArray()) {
index = index.getChild(c);
if (null == index) {
return 0;
}
}
return index.getPrefixCount();
}

/**
* TrieTree的节点
*/

private class TrieNode {
/** 该节点的字符 */
private final char nodeChar;//
/** 一个TrieTree的节点的子节点 */
private TrieNode[] childNodes = null;
private int count = 0;// 单词数量,用于判断一个单词是否存在
private int prefixCount = 0;// 前缀数量,用于查找该前缀出现的次数

public TrieNode(char nodeChar) {
super();
this.nodeChar = nodeChar;
}

public TrieNode addChild(char ch) {
int index = ch - 'a';
if (null == childNodes) {
this.childNodes = new TrieNode[26];
}
if (null == childNodes[index]) {
childNodes[index] = new TrieNode(ch);
}
return childNodes[index];
}

public TrieNode getChild(char ch) {
int index = ch - 'a';
if (null == childNodes || null == childNodes[index]) {
return null;
}
return childNodes[index];
}

public void addCount() {
this.count++;
}

public int getCount() {
return this.count;
}

public void addPrefixCount() {
this.prefixCount++;
}

public int getPrefixCount() {
return this.prefixCount;
}

@Override
public String toString() {
return "TrieNode [nodeChar=" + nodeChar + "]";
}

}
}

package com.vicky.datastructure.tree.trie;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Pattern;

import org.junit.Before;
import org.junit.Test;

public class TrieUsedTest {
private PrefixTrie trie;

@Before
public void before() throws IOException {
Pattern pattern = Pattern.compile("[a-zA-Z]+");

// 从文件中读取单词,构建TriedTree
InputStreamReader read = new InputStreamReader(this.getClass().getResourceAsStream("words.txt"));
BufferedReader reader = new BufferedReader(read);
trie = new PrefixTrie();
String line = null;
while (null != (line = reader.readLine())) {
line = line.trim();
if (!pattern.matcher(line).matches()) {// 去除非法单词,如包含“-”
continue;
}
trie.insertWord(line);
}
}

/**
* 测试使用TriedTree搜索前缀出现的次数
*/

@Test
public void searchPrefixWords() {
String prefix = "vi";
System.out.println(trie.selectPrefixCount(prefix));
System.out.println(trie.selectWord("Vicky"));
}
}

代码中为了支持精确查找和前缀查找两种方式,我们对TrieNode进行修改,增加了private int count = 0;private int prefixCount = 0;两个变量,分别用于保存单词出现的次数,以及前缀出现的次数。测试结果可通过使用文本编辑器进行查找对比。

更多关于Trie树的应用场景可阅读参考文章1。

参考文章

数据结构之Trie树
从Trie树(字典树)谈到后缀树(10.28修订)
6天通吃树结构—— 第五天 Trie树