前缀树
一般应用场景如下:两个字符串类型的数组arr1和arr2。求问arr2中有哪些字符是arr1中出现的,有哪些是作为arr1中某个字符串前缀出现的,有哪些字符是出现次数最大的前缀。
这种不直接用哈希表是因为哈希表没办法做到统计以xx为前缀的单词共出现了几次。
板子
class Trie {
class TrieNode{
public int pass;//存储的是这个节点被经过几次。譬如nexts[0].pass=1就说明接下来的a节点被经过1次。
public int end;//存储的是以这个节点为结尾的有几个字符串。
public TrieNode[] nexts;//存储这个节点的相邻节点
public TrieNode() {
pass = 0;
end = 0;
nexts = new TrieNode[26];//因为一般题目都是小写字母的字符串,所以只用开26个
}
}
public 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++;
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++;
}
public boolean search(String word) {
if(word==null) return true;
char[] chs = word.toCharArray();
int index = 0;
TrieNode node = root;
for(int i=0;i<chs.length;i++){
index = chs[i]-'a';
if(node.nexts[index]==null) return false;
node = node.nexts[index];
}
if(node.end==0) return false;
return true;
}
public boolean startsWith(String prefix) {//返回是否有以prefix为前缀的字符串
if(prefix==null) return true;
char[] chs = prefix.toCharArray();
int index = 0;
TrieNode node = root;
for(int i=0;i<chs.length;i++){
index = chs[i]-'a';
if(node.nexts[index]==null) return false;
node = node.nexts[index];
}
return true;
}
}
class Trie {
class TrieNode{
public int pass;
public int end;
public TrieNode[] nexts;
public TrieNode() {
pass = 0;
end = 0;
nexts = new TrieNode[26];
}
}
public 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++;
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++;
}
public boolean search(String word) {
if(word==null) return true;
char[] chs = word.toCharArray();
int index = 0;
TrieNode node = root;
for(int i=0;i<chs.length;i++){
index = chs[i]-'a';
if(node.nexts[index]==null) return false;
node = node.nexts[index];
}
if(node.end==0) return false;
return true;
}
public int countWordsEqualTo(String word){//查询指定字符串出现了几次
if(word==null){
if(root.end==1) return 1;
else return 0;
}
char[] chs = word.toCharArray();
int index = 0;
TrieNode node = root;
for(int i=0;i<chs.length;i++){
index = chs[i]-'a';
if(node.nexts[index]==null) return 0;//如果在查询的路上就发现没路了,那么肯定返回0
node = node.nexts[index];
}
return node.end;
}
public int countWordsStartingWith(String prefix){//查询多少字符串以prefix为前缀
if(prefix==null) return root.pass;//注意如果前缀为空,那么返回的是字典树中一共存储了多少个字符串
char[] chs = prefix.toCharArray();
int index = 0;
TrieNode node = root;
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.pass;
}
public void erase(String word){//删除
if(!search(word)) return;//注意一定要先查询字符串是否存在再删除
char[] chs = word.toCharArray();
int index = 0;
TrieNode node = root;
node.pass--;
for(int i=0;i<chs.length;i++){
index = chs[i]-'a';
if(--node.nexts[index].pass==0) {//说明删到最后一个节点了
node.nexts[index]=null;
return;
}
node = node.nexts[index];
}
node.end--;
}
}
相关力扣题目
LeetCode 208. 实现 Trie (前缀树)
板子一可解决,用时31ms,击败94.33%使用 Java 的用户。
LeetCode 1804. 实现 Trie (前缀树) II
板子二可解决,用时72ms,击败68.85%使用 Java 的用户
贪心算法
贪心策略在实现时,经常使用到的技巧:
根据某标准建立一个比较器来排序
根据某标准建立一个比较器来组成堆
举例题目:会议室安排
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目)还有这个会议室开放的最早时间,你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。 返回这个最多的宣讲场次。
我们发现,以会议开始的时间越早越先被安排是不对的,可以轻易举出反例如下,因为这样的话我们只会安排会议a。
同时,如果以会议持续的时间越短,越先被安排,也是不对的。如下我们只会安排会议b。
正确的应该是以会议结束的时间越早,越先安排,代码如下:
public class Program{
public int start;
public int end;
public Program(int start, int end){
this.start = start;
this.end = end;
}
}
public static class ProgramComparator implements Comparator<Program>{
@Override
public int compare(Program p1, Program p2){
return p1.end-p2.end;
}
}
public boolean ManageMeetings(Program[] programs, int timePoint) {
Arrays.sort(programs, new ProgramComparator());
int result = 0;
for(int i=0;i<programs.length;i++){
if(programs[i].start>=timePoint){
timePoint=programs[i].end;
result++;
}
}
return result;
}
贪心算法正确性的实战验证(对数器)
1.实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
2.脑补出贪心策略A、贪心策略B、贪心策略C.,.
3.用解法X和对数器,去验证每一个贪心策略
举例:字符串重排后字典序最小(用来说明理论上来验证贪心的正确性是多么复杂的)
给定一组字符串数组,重新排列每个字符串的顺序,使之拼接后组成一个字典序最小的字符串。
如果字符串a和字符串b拼接,a在前的话我们记作a#b,b在前的话我们记作b#a。
这道题的正确贪心策略是,如果a#b的字典序小于b#a的字典序,那么我们优先安排a。
那么如何证明这个策略是有效的呢?首先我们要证明这个策略具有传递性。
什么是传递性,就是譬如一般的数字比较,如果a<b,b<c,那么我们就能得出a<c。
有什么策略是不具有传递性的吗?有的,譬如剪刀石头布就不具有传递性。
也就是说,我们接下来要证明如下:(黄色的小于号代表是字典序小于的意思)
if a#b<b#a
and b#c<c#b
then a#c<c#a
证明具体过程如下:我们记字符串a的长度为stra,字符串b的长度为strb,字符串c的长度为strc
字典序就是将字符串转化为十六进制后,比较数字的大小。那么a#b的字典序就是a16^strb+b(从这里开始,之后的a都代表着原来的字符串转化为十六进制后的数,其他字母同理)
在这我们再次记16^strb为m(b),那么a#b的字典序就是am(b)+b,即
a#b=a*m(b)+b
b#a=b*m(a)+a
b#c=b*m(c)+c
c#b=c*m(b)+b
a#c=a*m(c)+c
c#a=c*m(a)+a
我们要证明的再次转变成了(以下的小于号代表的就是数学上的小于号)
if a*m(b)+b<b*m(a)+a,记为式子A
and b*m(c)+c<c*m(b)+b,记为式子B
then a*m(c)+c<c*m(a)+a,记为式子C
我们将式子A-b再*c,得到a*m(b)*c<(b*m(a)+a-b)*c
右边部分展开来,即a*m(b)*c<b*m(a)*c+a*c-b*c
此时左边部分,记作X
我们将式子B-c再*a,得到b*m(c)*a<(c*m(b)+b-c)*a
右边部分展开来,即b*m(c)*a<c*m(b)*a+b*a-c*a
再将右边的最后两项移到左边来,即b*m(c)*a+c*a-b*a<c*m(b)*a
此时右边部分,记作Y
我们发现X=Y。那么就可以推出b*m(c)*a+c*a-b*a<X/Y<b*m(a)*c+a*c-b*c
也就是b*m(c)*a+c*a-b*a<b*m(a)*c+a*c-b*c
把相同项c*a去掉,得到b*m(c)*a-b*a<b*m(a)*c-b*c
再把相同因子b去掉,得到m(c)*a-a<m(a)*c-c
再把左右的常数项调换位置,得到m(c)*a+c<m(a)*c+a
我们发现,这就是式子C,得证
这只是第一步,证明传递性,我们还有之后证明,两两交换位置后,策略没有更优。三三交换位置后,策略没有更优,四四交换位置后,策略没有更优……
所以不建议理论上去证明贪心的正确性,直接用对数器在实践上证明即可。
举例题目:
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金
条,不管切成长度多大的两半,都要花费20个铜板。
一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60.
金条要分成10,20,30三个部分。如果先把长度60的金条分成10和50,花费60;
再把长度50的金条分成20和30,花费50;一共花费110铜板。
但是如果先把长度60的金条分成30和30,花费60;再把长度30金条分成10和20,
花费30;一共花费90铜板。
输入一个数组,返回分割的最小代价。