LeetCode 前缀树相关题目
1. 前缀树相关知识(左神版)
本章内容参考自,博客链接非常感谢。
1.1 概念
前缀树是一种字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。值得注意的是,这种数据结构的信息在路上。
1.2 基本性质
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
- 每个节点的所有子节点包含的字符都不相同。
前缀树用上图进行理解,其中节点均用圆圈表示,“abc”等字符加入到整棵树中作为路径加入,而不是以节点形式加入,同时每个字符在加入时均从头结点出发,因此可以看到“abc”和“bce”为两个分支,同时“abc”和“abd”前半段为一个分支,最后形成两个分支。
扩充功能: - 将加入的字符以路径的形式加入节点可以存放是否为字符串结束为止的信息,从而可以在加入“be”这种跟之前走过的路径相同,但是没有最后形成新的分支的字符串统计出数目,最后可以用于整体加入了多少字符串信息的统计。(扩展了字符串加了几次的功能)
- 查询到达的前缀,加的过程中,到达了该节点几次。
1.3 代码
public static class TrieNode{
public int path; //有多少个字符串到达过这个节点
public int end; //有多少个字符串以这个节点结尾
public TrieNode[] nexts; // 保存有哪一个字符通过,如果路上的信息不是有限的字符,也可以使用map来保存
public TrieNode(){
path = 0;
end = 0;
//这里指的是路,每个题具体分析,这里将26个字母作为26条路
nexts = new TrieNode[26];
}
}
public static class Trie{
private TrieNode root;
public Trie(){
//新建的头,就是整体的头节点
root = new TrieNode();
}
//插入一条字符串
public void insert(String word){
if(word == null){
return;
}
char[] chs = word.toCharArray(); //word转为字符数组,开始跑
TrieNode node = root; //根节点,它的path和end不包含信息
int index = 0;
for(int i =0;i<chs.length;i++){
//用ASCII码和a的差值来表示字母,a--0,b--1,c--2,z--25
index = chs[i] - 'a';
//判断当前节点是否有通向chs[i]这个字母的路
if(node.nexts[index] == null){
node.nexts[index] = new TrieNode(); //没有就新建路
}
node = node.nexts[index];
node.path++;
}
node.end++;
}
//删除
public void delete(String word){
if(search(word) != 0){ //先查有没有
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0;i<chs.length;i++){
index = chs[i] - 'a';
//路径信息某个节点减一变成0,
//意味着接下来的字符串全部都是所需要删除的字符串,
//所以下面的字符串直接设置为null
if(--node.nexts[index].path == 0){
node.nexts[index] = null;
return;
}
node = node.nexts[index];
}
node.end--;
}
}
//查找一个word在其中出现过几次
public int search(String word){
if(word == null){
return ;
}
char[] chs = word.toCharArray();
TrieNode node = root;
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; //遇到最后返回end
}
public int prefixNumber(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++){
index = chs[i] - 'a';
if(node.nexts[index] == null){
return 0;
}
node = node.nexts[index];
}
return node.path;
}
}
2. LeetCode相关题目
2.1 LeetCode 208. 实现 Trie (前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
// 定义节点类
class Node {
int pass; // 有多少字符通过
int end; // 有多少个字符串以这个节点结尾
Node[] nexts; // 记录从该节点延伸出去的路径
public Node() {
pass = 0;
end = 0;
nexts = new Node[26];
}
}
class Trie {
// 定义一个头结点
private Node head;
// 构造方法中初始化头节点
public Trie() {
head = new Node();
}
// 向前缀树中插入字符串
public void insert(String word) {
// 判定字符是否为null
if (word == null) return;
char[] arr = word.toCharArray();
int index = 0; // 记录字符在路径数组中的位置
Node dummy = head;
// word不为null,可以确定有一个字符串通过头结点,直接加1
dummy.pass++;
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a'; // 计算当前字符在路径数组中的位置
if (dummy.nexts[index] == null) { // 如果没有该路径,新建一个路径保存起来
Node node = new Node();
node.pass++; // 当前节点通过字符个数加1
dummy.nexts[index] = node;
}
Node node = dummy.nexts[index]; // 节点指针移动到下一个节点
node.pass++;
dummy = node;
}
dummy.end++; // 字符串读取结束,结束节点end值加1
}
public boolean search(String word) {
if (word == null) return false;
char[] arr = word.toCharArray();
int index = 0;
Node dummy = head;
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a';
// 如果在搜索途中发现路径断了说明没有保存该字符
if (dummy.nexts[index] == null) return false;
dummy = dummy.nexts[index];
}
// 最后检查结尾节点是否end值大于0,大于零说明该字符在这里结束了,若不为0,说明没有保存该字符
if (dummy.end > 0) return true;
else return false;
}
// 过程与search一致
public boolean startsWith(String prefix) {
char[] arr = prefix.toCharArray();
if (arr.length == 0) return false;
int index = 0;
Node dummy = head;
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a';
if (dummy.nexts[index] == null) return false;
dummy = dummy.nexts[index];
}
return true;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
2.2 LeetCode 677. 键值映射
设计一个 map ,满足以下几点:
字符串表示键,整数表示值
返回具有前缀等于给定字符串的键的值的总和
实现一个 MapSum 类:
MapSum() 初始化 MapSum 对象
void insert(String key, int val) 插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对 key-value 将被替代成新的键值对。
int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。
思路: 使用前缀树的数据结构,在结束节点上保存该字符串健所对应的值
class MapSum {
// 定义节点类
public static class Node {
int pass;
int end;
int value; // map中键所对应的值
Node[] nexts;
public Node() {
pass = 0;
end = 0;
value = 0;
nexts = new Node[26];
}
}
// 前缀树
private Node root;
public MapSum() {
root = new Node();
}
public void insert(String key, int val) {
if (key == null) return;
Node node = root;
node.pass++;
int index = 0;
char[] arr = key.toCharArray();
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a';
if (node.nexts[index] == null) {
Node add = new Node();
add.pass++;
node.nexts[index] = add;
}
node.nexts[index].pass++;
node = node.nexts[index];
}
// 前面与前缀的基本操作相同
node.end++;
// 只是到达最后一个节点后,将值保存好
node.value = val;
}
// 求以该前缀为键的值的累加和
public int sum(String prefix) {
// 数据检验
if (prefix == null) return 0;
Node node = root;
char[] arr = prefix.toCharArray();
int index = 0;
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a';
if (node.nexts[index] == null) return 0;
node = node.nexts[index];
}
// 此时node到达前缀字符最后后一个结点
int pass = node.pass;
int res = 0;
// 此时指针到达前缀字符的最后一个节点,从该节点向下的路径均是以该前缀字符串为前缀的字符串
// 自然的想到使用宽度优先遍历来得到所有的叶子节点
// 当然也可以使用深度优先遍历
//if (node.end > 0) res += node.value;
// 宽度优先遍历
Queue<Node> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()) {
Node n = queue.poll();
//if (n == null) continue;
if (n.end > 0) res += n.value;
for (Node item : n.nexts) {
if (item != null) queue.add(item);
}
}
return res;
}
}
/**
* Your MapSum object will be instantiated and called as such:
* MapSum obj = new MapSum();
* obj.insert(key,val);
* int param_2 = obj.sum(prefix);
*/
2.3 211. 添加与搜索单词 - 数据结构设计
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary() 初始化词典对象
void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配
bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 ‘.’ ,每个 . 都可以表示任何一个字母。
**分析:**这道题与基本的前缀树在搜索方面不同,该题在搜索时支持通配符,当搜索到通配符后,当前结点之后所有的路径都是有效路径,思路也比较简单,就是采用深度有限遍历的方式遍历前缀树。
class WordDictionary {
public static class Node {
int pass;
int end;
Node[] nexts;
public Node() {
pass = 0;
end = 0;
nexts = new Node[26];
}
}
private Node root;
public WordDictionary() {
root = new Node();
}
// 添加单词的思路与基本操作一样
public void addWord(String word) {
if (word == null) return;
char[] arr = word.toCharArray();
Node node = root;
root.pass++;
int index = 0;
for (int i = 0; i < arr.length; i++) {
index = arr[i] - 'a';
if (node.nexts[index] == null) {
Node add = new Node();
add.pass++;
node.nexts[index] = add;
}
node.nexts[index].pass++;
node = node.nexts[index];
}
node.end++;
}
public boolean search(String word) {
// 从根结点开始搜索
return dfsSearch(word,0, root);
}
/**
* word 表示传入的字符串,字符传中可能有通配符
* index 表示当前字符的位置
* node 表示当前所在的结点
* 采用递归的方式实现dfs
*/
public boolean dfsSearch(String word, int index, Node node) {
// 递归出口,当到达最后一个字符时,检查当前结点是否结尾
if (index == word.length()) {
return node.end > 0;
}
char ch = word.charAt(index);
if (Character.isLetter(ch)) { // 如果的字母字符
int childIndex = ch - 'a';
Node child = node.nexts[childIndex];
// 如果当前结点存在,说明存在通往该字符的路径
// 还需要衍生剩下的子串是否存在
if (child != null && dfsSearch(word, index+1, child)) {
return true;
}
}else { // 如果是通配符
// 如果是通配符,则对当前结点下的每一条路径进行深度有限遍历搜索
for (int i = 0; i < node.nexts.length; i++) {
Node child = node.nexts[i];
if (child != null && dfsSearch(word, index+1, child)) {
return true;
}
}
}
return false;
}
}
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary obj = new WordDictionary();
* obj.addWord(word);
* boolean param_2 = obj.search(word);
*/