高级数据结构 -- Trie前缀树(字典树)

387 阅读3分钟

 

因为热爱所以坚持,因为热爱所以等待。熬过漫长无戏可演的日子,终于换来了人生的春天,共勉!!!

字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。Trie经常被搜索引擎系统。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。

假设组成所有单词的字符仅是“a”~"z",请实现字典树结构,并包含以下四个主要功能:

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

实现代码如下:

package 树;
class TrieNode {
	public int pass;
	public int end;
	public TrieNode[] nexts;
	public TrieNode() {
		//pass表示经过包含这个字符的字符串的个次数
		pass = 0;
		//end表示以这个字符结尾的字符串的个数
		end = 0;
		//表示26个字母
		nexts = new TrieNode[26];
	}
}

public class Trie {
	private TrieNode root;

	public Trie() {
		root = new TrieNode();
	}
	
	public void insert(String word) {
		if (word == null) {
			return;
		} 
		char[] chs = word.toCharArray();
		TrieNode node = root;
		node.pass++; //经过的次数+1
		int index = 0;		
		for (int i = 0; i < chs.length; i++) {  //从左往右遍历字符
			index = chs[i] - 'a';               
			//如果没有这条路劲,则新建路径
			if (node.nexts[index] == null) {
				node.nexts[index] = new TrieNode();
			}
			//有则前往子路径
			node = node.nexts[index];
			node.pass++;
		}
		node.end++;//字符串结束,以该字符结尾的字符串数量+1
	}
	
	public int search(String word) {
		if (word == null) {
			return 0;
		}
		TrieNode node = root;
		char[] chs = word.toCharArray();
		int index = 0;
		for (int i = 0; i < chs.length; i++) {
			index = chs[i] - 'a';
			if (node.nexts[index] == null) { //没有改子路径,说明没有插入过该字符串
				return 0;
			}
			node = node.nexts[index]; 
		}
		return node.end; //返回以这个单词结尾的字符串的个数
	}
	
	public int searchPrefix(String pre) {
		if (pre == null) {
			return 0;
		}
		char[] chs = pre.toCharArray();
		TrieNode node = root;
		int index = 0;
		for (int i = 0; i < chs.length; i++) {
			if (node.nexts[index] == null) { //没有改子路径,说明没有插入过该字符串
				return 0;
			}
			node = node.nexts[index];
		}
		return node.pass; //返回以该字符串为前缀的字符串的数量
	}
	
	public void delete(String word) {
		int flag = search(word); //先查找
		if (flag == 0) {
			return;
		}
		char[] chs = word.toCharArray();
		TrieNode node = root;
		node.pass--;  //从根节点开始-1
		int index = 0;
		for (int i = 0; i < chs.length; i++) {  //从左往右遍历字符
			index = chs[i] - 'a';               
			if (--node.nexts[index].pass == 0) { //如果--pass == 0 ,说明后面路劲必定都要剪掉,直接node.nexts[index] 置为null,后面GC会自动回收剪掉的路劲
				node.nexts[index] = null;
				return;
			}
			node.pass--; //数量减一
			node = node.nexts[index];
		}
		node.end--; 已该字符结尾的字符串数量减一
	}
	
}

插入操作

以 { "abc","abcd","abce","bcd","bcf","cde" }举例,将所有字符串插入后结果如下:

从根节点的pass值我们可以知道,一共插入了6个字符串

删除操作

 删除“abcd” 后:

 查询操作

以查询"abc" 为例子

 返回1,存在"abc"字符串,且数量为1

前缀查询操作

以查询"ab" 为例子

 返回pass==2,表示有两个字符串以ab为前缀


leetCode例题:677. 键值映射

实现一个 MapSum 类,支持两个方法,insert 和 sum:

MapSum() 初始化 MapSum 对象
void insert(String key, int val) 插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对将被替代成新的键值对。
int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。

示例:

输入:
["MapSum", "insert", "sum", "insert", "sum"]
[[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]
输出:
[null, null, 3, null, 5]

解释:
MapSum mapSum = new MapSum();
mapSum.insert("apple", 3);  
mapSum.sum("ap");           // return 3 (apple = 3)
mapSum.insert("app", 2);    
mapSum.sum("ap");           // return 5 (apple + app = 3 + 2 = 5)


提示:

1 <= key.length, prefix.length <= 50
key 和 prefix 仅由小写英文字母组成
1 <= val <= 1000
最多调用 50 次 insert 和 sum

思路:使用前缀树思想,保存字符路径

递归解法:

class MapSum {
    public MapSum() {
    }

     private class Node {
        Node[] nexts = new Node[26];
        int value;
    }
    private Node root = new Node();

    public void insert(String key, int val) {
        insert(key, root, val);
    }
    public void insert(String key,Node node, int val) {
        if (key == null) {
            return;
        }
        if (key.length() == 0) {
            node.value = val;
            return;
        }
        int index = key.charAt(0) - 'a';
        if (node.nexts[index] == null) {
            node.nexts[index] = new Node();
        }       
        node = node.nexts[index];
        insert(key.substring(1),node,val);
    }
    
    public int sum(String prefix) {
        return sum(prefix,root);
    }   
    public int sum(String prefix,Node node) {
        if (node == null) return 0;
        if (prefix == null) return 0;
        if (prefix.length() != 0) {
            int index = prefix.charAt(0) - 'a';
            node = node.nexts[index];
            return sum(prefix.substring(1),node);
        }
        int sumAll = node.value;
        for (Node n : node.nexts) {
            sumAll += sum(prefix,n);
        } 
        return sumAll;
    }
}

迭代解法:

class MapSum {
    
    public MapSum() {
    }
     private class TrieNode {
        TrieNode[] nexts = new TrieNode[26];
        int value;
    }

	TrieNode root = new TrieNode();
    Queue<TrieNode> queue = new LinkedList<>();
	
    public void insert(String key, int val) {
        if (key == null) {
            return;
        }
        char[] chs = key.toCharArray();
        TrieNode node = root;
        int index = 0;
        for (int i = 0; i < chs.length; i++) {
            index = chs[i] - 'a';
            if (node.nexts[index] == null) {
                node.nexts[index] = new TrieNode();
            }
            node = node.nexts[index];
        }
        node.sum = val;
        
    }
    
    public int sum(String prefix) {
        if (prefix == null) {
            return 0;
        }
        TrieNode node = root;
        int index = 0;
        char[] chs = prefix.toCharArray();
        for (int i = 0; i < chs.length; i++) {
            index = chs[i] - 'a';
            if (node.nexts[index] == null) {
                return 0;
            }
            node = node.nexts[index];
        }
        int sumAll = node.sum;
        queue.add(node);
        while (!queue.isEmpty()) {
            TrieNode[] next = queue.remove().nexts;
            for (int i = 0;i < 26;i++) {
                if (next[i] != null) {
                    sumAll += next[i].sum;
                    queue.add(next[i]);
                }  
                
            }
        }
        return sumAll;
    }
}