你了解Trie树吗

Trie树介绍

  Trie树,也叫字典树,所以自然也是一个树形结构.Trie树是一种专门用来处理字符串匹配的数据结构,用来在一组字符串集合中快速查找某个字符串.
  那么,在一组字符串集合中快速查找某个字符串由哪些方法呢?
  很容易想到,有散列表、红黑树、BF或RK算法、BM或KMP算法,既然如此为什么还需要Trie树呢?
  这是因为Trie树相比较散列表或者红黑树这些数据结构有它的优势,那就是当字符串集合字符集不大而且字符串集合的前缀重合比较多的时候,Trie树的查找效率是非常高效的!
  比如:当我们要在how、hi、her、hello、so、see这几个字符串中查找某个字符串是否存在,如果一一比对效率是非常低的,但是如果我们提前把这几个字符串组织成Trie树,那就非常高效了!其实Trie树的本质很简单:就是利用字符串集合之间的公共前缀,将重复的前缀合并在一起。如图所示就是构造出的Trie树:
你了解Trie树吗_第1张图片

  根节点不包含任何信息,每个节点表示一个字符串的字符,从根节点到红色节点的一条路径表示一个完整的字符串。红色节点就是为了表示是否是一个完整字符,所以不一定是叶子节点。
Trie树的构造其实也非常简单,构造的每一步就是往Trie树中插入一个字符串。
你了解Trie树吗_第2张图片
  在Trie中查找一个字符串,比如查找how,其实就先将字符串分割为h、o、w然后从Trie树中的根节点开始一一比对,如图所示的绿色路径。
你了解Trie树吗_第3张图片
  如果我们查找的是ho,那么绿色路径中的o节点不是红色的,也就是ho仅仅是某个字符串如how的前缀而已,并不是能完全匹配任何字符串。

构造并存储一颗Trie树

  Trie树是一颗多叉树,二叉树一个节点有左右两个子节点,使用两个指针来存储,而多叉树呢?这里有个办法(一般很多教材都这么用),假如说存储的字符串的字符集是26个英文字母,那这里就这么用:

class TrieNode {
  char data;
  TrieNode children[26];
}

  TrieNode children[26]中的下标从0到25,0表示a,1表示b,2表示c,3表示d,以此类推,25表示z,那么就拿上边的Trie树来说根节点下有h和s,那么TrieNode children[26]的下标index=7位置存储的就是h,index=18这个位置存储的就是s,其他为null。可以看出每一个Trie树节点(也就是字符串的某个字符)为了存储多叉子节点,用了这样一个26个元素的数组来存储子节点,可以看出会存在许多的null节点,尤其是这个节点只有一两个子节点的时候,空间会被浪费很多。

在Trie树中查找字符串

在Trie树中查找字符串时,将字符串分割成一个个字符,到Trie树的路径中匹配。代码可能更好理解一些。

Trie树代码

package com.study.algorithm;
/**
 * 
 * @author jeffSheng
 * @date 20191003
 *
 */
public class Trie {
	private TrieNode root = new TrieNode('/'); // 存储无意义字符

	/**
	 *      构造trie树  往 Trie 树中插入一个字符串
	 * @param text
	 */
	public void insert(char[] text) {
		TrieNode p = root;
		for (int i = 0; i < text.length; ++i) {
			int index = text[i] - 'a';
			//如果当前字符不存在p节点下则新建一个放到p下
			if (p.children[index] == null) {
				TrieNode newNode = new TrieNode(text[i]);
				p.children[index] = newNode;
			}
			//如果p下此字符已经存在(如果不存在之前就创建了),则把此节点当做p节点
			p = p.children[index];
		}
		//构造万char数组后字符串存储到trie树中完毕,标识p节点为结束那个节点
		p.isEndingChar = true;
	}

	/**
	 *     在 Trie 树中查找一个字符串
	 * @param pattern
	 * @return
	 */
	public boolean find(char[] pattern) {
		TrieNode p = root;
		for (int i = 0; i < pattern.length; ++i) {
			int index = pattern[i] - 'a';
			//如果当前字符不存在于p下,则说明trie树中不存在字符串pattern
			if (p.children[index] == null) {
				return false; // 不存在 pattern
			}
			//如果当前字符存在于p下,则把当前字符当做p继续往下查找pattern的其他剩余字符
			p = p.children[index];
		}
		//如果查找完pattern后,发现p的结束字符不是true,说明查找的是前缀,不是一个完整字符串,比如hello,你查的是he
		if (p.isEndingChar == false)
			return false; // 不能完全匹配,只是前缀
		else
			return true; // 找到 pattern
	}

	public class TrieNode {
		public char data;//char是2个字节
		public TrieNode[] children = new TrieNode[26];//每个数组指针变量占用8字节
		public boolean isEndingChar = false;//boolean单独使用4个字节,在数组中1个字节

		public TrieNode(char data) {
			this.data = data;
		}
	}
}

Trie树的时间复杂度分析

  如果在一组字符串集合中频繁查找某些字符串Trie树就很高效。构建Trie树的过程,需要将所有字符串加入Trie中,就是说会将所有字符串的每个字符扫描一遍,时间复杂度是O(N),N
  就是所有字符串的长度和,不过一旦构建成功,后续查询效率就很高。比如待查询字符串长度k,那其实只需要比对K个节点就能完成查询了,时间复杂度O(k),k就是要查找的字符串的长度。

Trie树耗内存的原因

  刚才也说了,Trie树存储每个节点都需要一个26长度的数组去存储子节点,而子节点很可能么有26个,这就造成了空间的浪费。数组指针变量的存储需要8个字节(根据OS不同可能是4字节)。英文字母还好说,如果字符集包括大写字母、中文或数字,那空间就更多了。
  不过可以将存储子节点的数组换成有序数组、跳表、散列表、红黑树等,来提高存储利用率,不过可能牺牲了查询效率,比如用有序数组,每次插入一个字符串就得维护有序数组的有序性,效率可能就稍微慢了点。

  其他方面,Trie树相比红黑树或散列表需要自己去实现代码,而红黑树或散列表直接用编程语言的实现就行了。而且由于Trie每个节点存储指针来连接子节点对缓存不友好,性能会打折扣。但是Trie树的优势就在于查找前缀相同下的一组字符串集合,不适合精确查找。

你可能感兴趣的:(算法与数据结构)