前缀树,查找树,trie数据结构:建前缀树,查字符串,查前缀,删除前缀树中的字符串
提示:前缀树是非常重要的数据结构,经常在大厂面试题中考,一考就很难!
文章目录
- 前缀树,查找树,trie数据结构:建前缀树,查字符串,查前缀,删除前缀树中的字符串
- 前缀树就是查找树
- 建树,插入word
- 查找前缀树中有几个字符串word
- 查找前缀树种有几个字符串以word为前缀
- 删除前缀树中的一个字符串word
- 手撕前缀树的代码
- 总结
前缀树就是查找树
所谓前缀树,就是很多字符串【字符串数组】,他们的前缀都一样,这样我们可以新建一个数据结构Trie存起来
把字符串按照链表,挂在相同的字符路径上,也就是前缀一样的放一条路,复用一下字符。
即:那些前缀相同的,就可以复用一条路,不必存那么多相同的字符串,节省空间
一路路径是一个链表,由于一节节点,有多条分支【这里是最多26条分支代表a–z字符的走向】
多条分支就是树!【2条就是二叉树】
而且,每次我们查找前缀,或查找字符串时,也可以快速根据前缀树查到——既然是查,也叫查找树
但是前缀树用得多
比如:字符串数组arr=abc,ab,bc,kst,bc,a
咱如果用数组存,还真的把每一个字符串都存了,浪费很多空间
但是前缀树,利用链表,复用相同的前缀,就可以节约空间,方便查找。
前缀树的链表中,它的节点,有2个属性,p和e代表啥呢?
(1)p即pass,也就是当前节点,有多少个字符串经过了当前字符,比如a,由于有a,abc,ab经过,所以a有仨,p=3
所以
(2)e即end,也就是当前节点代表的字符,有多少个字符串以它结尾,比如图中bc的c,其e=2是因为2个bc的存在,所以e=2
(3)前缀树,还有一个属性,那就是多分支路径nexts,它是一个26长度的字符数组,代表a–z的每一条分支
啥意思呢?
0 1 2 …… 25每一个位置代表此路径是
a b c…… z字符
public static class Node{
public int pass;
public int end;
public Node[] nexts;//有一个自身类型的节点数组,nexts里面放的是node
public Node(){
//一旦生成自己就要初始化,不然没法计数
pass = 0;
end = 0;
nexts = new Node[26];//初始化啥都没有
}
}
前缀树是一个新的数据结构:Trie
在前缀树中,咱们它就一个节点root,今后所有的字符串都挂接在这上面
//前缀树
public static class Trie{
//前缀树的私有信息就是一个root,root是一个三属性节点
private Node root;
public Trie(){
root = new Node();//初始root节点
}
今后这个前缀树,可以快速地查找和使用,我们需要打牢这个基础
建树,插入word
给你一个字符串word,咱需要把它给加到已有的前缀树root上
//add(word)建前缀树,一个一个字符串加
public void add(String word)
来了一个字符串word
将其转化为字符数组str
cur从root开始挂,必然root.pass++
然后咱就找cur的path=str[i]-'a’分支【path=0—15代表cur是a–z字符分支】
原来没有这路,就新建一个节点,代表有了这个字符分支的路
然后cur.pass++
比如添加abc
则:root下属nexts[0]有效,代表挂a,pass=1
cur=a,cur下属nexts[1]有效,代表挂b,pass=1
cur=c,cur下属nexts[2]有效,代表挂c,pass=1,且此时abc已经结束,挂c的cur.end=1,代表abc挂好了,以c结尾的字符串有1个。
查找前缀树中有几个字符串word
//search(word)
//查的就是word末尾的end
public int search(String word)
有了前面建好的前缀树root
每次从root开始查找
比如目前前缀树root上有这些字符串:
String s1 = "abc";
String s2 = "abd";
String s3 = "abe";
请你查找s3 = "abe"
有几个?
咱们每次沿途找,a,b,e,只要cur下属nexts[path]有就行【path=str[i]-'a’分支【path=0—15代表cur是a–z字符分支】】
一旦查到abe,看e的end,就是几个。返回。
如果没,提前终止,返回0个
比如:查abf有几个啊?
查找前缀树种有几个字符串以word为前缀
//prefixSearch(word)
//以word为前缀,也就是说word最后一个字符,cur的pass是多少,就有多少个经过这里
public int prefixSearch(String word)
有了search的经验,咱只需要查word最后一个字符的pass,就是途径了word的字符串有几个,也就是word为前缀的有即可
比如:查ab前缀有几个啊?
很显然,3个
如果一条分支,查不到路径,提前终止,返回0
删除前缀树中的一个字符串word
//delete(word)
//删除一个字符串,沿途root,cur的pass全部干掉一遍,如过提前发现某一只已经pass=0,则整条路废了
public void delete(String word)
有了search,删除也就好办了
删除前,查一下word有吗,如果结果为0,不用删了
否则的话
(1)root.pass–;root必定少了一个,因为要删除word呗
(2)cur沿着root的word路径下去,如果发现哪条分支
–cur.nexts[path].pass==0了:提前让下属之路的pass-1,如果pass竟然等于零,说明就这一条路,就一个串。
也就是整条路,整个链表的节点的pass提前减为0了,那整条链表都得null,提前返回。
下面不用遍历了,因为前面头断了,后面尾就消失了。
比如,上面这个abe,就一条路,提前就over了
(3)上面的条件没进去,则说明pass>=2,所以只需要沿途下去就行,注意,pass在(2)中–了,不要重复干
然后最后end那–end即可,说明word已经被删除了
比如上面这个,有abe,也有abek,那你删除abe,只需要动e,其pass–,end–即可,代表abe废了,但是ebek还在的。
手撕前缀树的代码
//复习前缀树:
public static class TrieReview{
public Node root;
public TrieReview(){
root = new Node();
}
//add(word)建前缀树,一个一个字符串加
public void add(String word){
if (word.compareTo("") == 0) return;
char[] str = word.toCharArray();//转化为字符数组去一个个找路径,加
//没有新建一个点,代表这个位置的字符存在0--25==a--z
Node cur = root;//挂在root上的
cur.pass++;//新加入,必然root有一个字符串经过
int path = 0;//索引每一个字符的方向
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';//0--25==a--z
if (cur.nexts[path] == null) cur.nexts[path] = new Node();//代表存在了
cur = cur.nexts[path];//然去这个分支,代表加入了str[i]字符了
cur.pass++;//统计,当前cur字符来一个新的
}
//全部加完之后,代表多了一个str
cur.end++;
}
//search(word)
//查的就是word末尾的end
public int search(String word){
if (word.compareTo("") == 0) return 0;
Node cur = root;//自然是从root开始查
int path = 0;
char[] str = word.toCharArray();
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
if (cur.nexts[path] == null) return 0;//压根这条路就没有
cur = cur.nexts[path];
}
//查完返回end
return cur.end;
}
//prefixSearch(word)
//以word为前缀,也就是说word最后一个字符,cur的pass是多少,就有多少个经过这里
public int prefixSearch(String word){
if (word.compareTo("") == 0) return 0;
char[] str = word.toCharArray();
int path = 0;
Node cur = root;
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
if (cur.nexts[path] == null) return 0;//压根这条路没有
cur = cur.nexts[path];//沿着路径找
}
//前缀玩车个,返回pass
return cur.pass;
}
//delete(word)
//删除一个字符串,沿途root,cur的pass全部干掉一遍,如过提前发现某一只已经pass=0,则整条路废了
public void delete(String word){
if (word.compareTo("") == 0) return;
//先查,暂时有没有这个字符串,有才需要删除,否则不必
if (search(word) == 0) return;
char[] str = word.toCharArray();
Node cur = root;
root.pass--;//root必然损失1个字符串
int path = 0;
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
if (--cur.nexts[path].pass == 0) {//减过了pass待会不要多减了
//唯独,就一个,则提前删除返回
cur.nexts[path] = null;
return;
}
//这条路如果至少有2个以上,只需要沿途干掉pass,最后干掉end--
cur = cur.nexts[path];
}
//最后必然干end
cur.end--;
}
}
//测试一波:
public static void test2(){
TrieReview trie = new TrieReview();
String s1 = "abc";
String s2 = "abd";
String s3 = "abe";
trie.add(s1);
trie.add(s2);
trie.add(s3);
System.out.println(trie.search(s3));//1
System.out.println(trie.prefixSearch("ab"));//3
System.out.println("--------------");
trie.delete(s3);
System.out.println(trie.search(s3));
System.out.println(trie.prefixSearch("ab"));
}
public static void main(String[] args) {
// test();
test2();
}
测试一波:
1
3
--------------
0
2
建树成功
查s3有1个
ab前缀有3个
——————
删除s3,之后,s3就0个
ab前缀也只有2个了
总结
提示:重要经验:
1)前缀树=查找树,是重复利用同样前缀的多叉树,节点有pass和end,控制pass和end含义就知道哪些字符有多少个
2)未来大厂面试题中,很多都是前缀树解决的,但是遇到前缀树的题目,很难,多见,多总结把