字典树的实现

字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。假设组成所有单词的字符仅是“a”~“z”,实现字典树结构,并包含以下四个主要功能。

  • void insert(String word):添加word,可重复添加;
  • void delete(String word):删除word,如果word添加过多次,仅删除一个;
  • boolean search(String word):查询word是否在字典树中;
  • int prefixNumber(String pre):返回以字符串pre为前缀的单词数量。

字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间,比如加入“abc”、“abcd”、“adb”、“b”、“bcd”、“efg”、“hik”之后,字典树如下图所示,其中橙色节点表示一个终止节点。

字典树的实现_第1张图片
字典树的基本性质如下:
  • 根节点没有字符路径。除根节点外,每一个节点都被一个字符路径找到;
  • 从根节点出发到任何一个节点,如果将沿途经过的字符连接起来,一定为某个加入过的字符串的前缀;
  • 每个节点向下的所有字符串路径上的字符都不同;

在字典树上搜索添加过的单词的步骤如下:

  1. 从根节点开始搜索;
  2. 取得要查找单词的第一个字母,并根据该字母选择对应的字符路径向下继续搜索;
  3. 字符路径指向的第二层节点上,根据第二个字母选择对应的字符路径向下继续搜索;
  4. 一直向下搜索,如果单词搜索完后,找到的最后一个节点是一个终止节点。比如上图,搜索单词“abc”时,单词的最后一个字符’c’对应的在字典树中的字符路径a->b->c中’c’为终止节点,那么说明字典树中包含该单词;如果找到的最后一个节点不是一个终止节点,比如查找单词“hi”,其中i不是终止节点,那么说明字典树没添加过该单词;如果单词还没搜索完,但是字典树就已经没有后续节点了,也说明字典树没添加过该单词。

在字典树上添加一个单词的步骤同理,这里不再赘述。下面介绍有关字典树节点的类型,参见如下代码中的TrieNode类。

public class TrieNode {
	public int path;
	public int end;
	public TrieNode[] map;

	public TrieNode() {
		path = 0;
		end = 0;
		map = new TrieNode[26];// 26个字母
	}
}

TrieNode类中,path表示有多少个单词共用这个节点,如此我们才能知道某字符串pre为前缀的单词数量。end表示有多少个单词以这个节点结尾,只要end大于0,那么说明存在单词以该节点结尾,亦表示该节点为终止节点。下面介绍本题的Trie树类如何实现。

  • insert方法:首先我们需要将指针指向根节点node,并将插入的单词word分为字符数组。由于当前节点下面可能有26个节点,我们取单词word的第一个字符并减去字符’a’来获取一个下标值,这个下标值指的是单词word的第一个字符应该插入到当前节点node下面的26个节点中的某个节点的位置。如果该位置的节点为空,那么就新建一个节点,代表单词word的第一个字符节点。若不为空,那么就说明该字符已经插入过了,指针指向下一个节点,并将其path加1。当单词的所有字符都遍历后,其最后一个字符所对应的节点的end需加1。具体实现代码如下所示:

    public void insert(String word) {
    		if (word == null)
    			return;
    		TrieNode node = root;
    		node.path++;
    		char[] words = word.toCharArray();
    		int index = 0;
    		for (int i = 0; i < words.length; i++) {
    			index = words[i] - 'a';
    			if (node.map[index] == null) {
    				node.map[index] = new TrieNode();
    			}
    			node = node.map[index];
    			node.path++;
    		}
    		node.end++;
    	}
    
  • search方法:search方法基本思路与insert方法是相似的。不同的是,当在查找某个单词时,若该单词还没查找完,单词字符序列没有与之对应的字典树中的字符节点序列,也就在查找过程中发现了空节点,那么说明该单词不存在与字典树中。若单词查找完了,但是该单词的最后一个字符所对应的字典树中节点的end的值为0,即该节点不是终止节点,那么也说明该单词不存在与字典树中。具体实现代码如下所示:

    public boolean search(String word) {
    	if (word == null)
    		return false;
    	TrieNode node = root;
    	char[] words = word.toCharArray();
    	int index = 0;
    	for (int i = 0; i < words.length; i++) {
    		index = words[i] - 'a';
    		if (node.map[index] == null)
    			return false;
    		node = node.map[index];
    	}
    	return node.end > 0;
    }
    
  • delete方法:首先需要确保要删除的单词是存在字典树中的。然后在删除的过程中,需要将对应的字符路径的每个节点的path值减1,同时判断是否等于0。若某个节点的path等于0,那么直接将当前节点置为空即可,比如下图的情况,节点中的第一个值为path,第二个值为end。当要删除单词“cef”时,发现c的节点的path-1后等于0,那么直接将该节点设置为空即可,不需要再往后遍历。

字典树的实现_第2张图片

另一种情况是要删除单词“a”时,a的节点的path-1后不等于0,但是因为已经遍历到终止节点了,那么需要将end的值减1。具体实现代码如下:

public void delete(String word) {
	if (search(word)) {
		char[] words = word.toCharArray();
		TrieNode node = root;
		node.path--;
		int index = 0;
		for (int i = 0; i < words.length; i++) {
			index = words[i] - 'a';
			if (--node.map[index].path == 0) {
				node.map[index] = null;
				return;
			}
			node = node.map[index];
		}//for
		node.end--;
	}//if
}
  • prefixNumber方法:和查找操作同理,不断的在树中查找前缀pre,若该前缀存在,那么返回其最后一个节点的path值,如不存在直接返回0。具体实现代码如下:

    public int prefixNumber(String pre) {
    	if (pre == null)
    		return 0;
    	TrieNode node = root;
    	char[] pres = pre.toCharArray();
    	int index = 0;
    	for (int i = 0; i < pres.length; i++) {
    		index = pres[i] - 'a';
    		if (node.map[index] == null)
    			return 0;
    		node = node.map[index];
    	}
    	return node.path;
    }
    

下面是一个扩展的字典树,支持所有字符,而不仅仅是26个字母,具体实现如下所示:

import java.util.HashMap;

public class TrieTree {
	//字典树节点
	class TrieNode {
		public int path;
		public int end;
		public HashMap<Character, TrieNode> map;

		public TrieNode() {
			path = 0;
			end = 0;
			map = new HashMap<>();
		}
	}
	
	private TrieNode root;
	
	public TrieTree() {
		root = new TrieNode();
	}
	
	public void insert(String word) {
		if (word == null)
			return;
		TrieNode node = root;
		node.path++;
		char[] words = word.toCharArray();
		for (int i = 0; i < words.length; i++) {
			if (node.map.get(words[i]) == null) {
				node.map.put(words[i], new TrieNode());
			}
			node = node.map.get(words[i]);
			node.path++;
		}
		node.end++;
	}
	
	public boolean search(String word) {
		if (word == null)
			return false;
		TrieNode node = root;
		char[] words = word.toCharArray();
		for (int i = 0; i < words.length; i++) {
			if (node.map.get(words[i]) == null)
				return false;
			node = node.map.get(words[i]);
		}
		return node.end > 0;
	}
	
	public void delete(String word) {
		if (search(word)) {
			char[] words = word.toCharArray();
			TrieNode node = root;
			node.path--;
			for (int i = 0; i < words.length; i++) {
				if (--node.map.get(words[i]).path == 0) {
					node.map.remove(words[i]);
					return;
				}
				node = node.map.get(words[i]);
			}//for
			node.end--;
		}//if
	}
	
	public int prefixNumber(String pre) {
		if (pre == null)
			return 0;
		TrieNode node = root;
		char[] pres = pre.toCharArray();
		for (int i = 0; i < pres.length; i++) {
			if (node.map.get(pres[i]) == null)
				return 0;
			node = node.map.get(pres[i]);
		}
		return node.path;
	}
	
	public static void main(String[] args) {
		TrieTree trie = new TrieTree();
		System.out.println(trie.search("程龙颖"));//f
		trie.insert("程龙颖");
		System.out.println(trie.search("程龙颖"));//t
		trie.delete("程龙颖");
		System.out.println(trie.search("程龙颖"));//f
		trie.insert("程龙颖");
		trie.insert("程龙颖");
		trie.delete("程龙颖");
		System.out.println(trie.search("程龙颖"));//t
		trie.delete("程龙颖");
		System.out.println(trie.search("程龙颖"));//f
		trie.insert("程龙颖一");
		trie.insert("程龙颖二");
		trie.insert("程龙颖一三");
		trie.insert("程龙颖一四");
		trie.delete("程龙颖一");
		System.out.println(trie.search("程龙颖一"));//f
		System.out.println(trie.prefixNumber("程龙颖"));//3
	}
}

本文参考《程序员代码面试指南—IT名企算法与数据结构题目最优解》

你可能感兴趣的:(字典树)