目录
1 前缀树
何为前缀树?
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
如何生成前缀树?
pass:加前缀树时经过该节点的次数
end:这个节点为多少个字符串的终结的点
代码:
public static class TrieNode{
public int pass;
public int end;
public TrieNode[] nexts;
//HashMap<Char, Node> nexts;
//TreeMap<Char, Node> nexts;
public TrieNode(){
pass = 0;
end = 0;
//nexts[0] == null 没有走向'a'的路
//nexts[0] != null 有走向'a'的路
// ...
//nexts[25] != null 有走向'z'的路
next = 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();
TrieNode node = root;
node.pass++;
int index = 0;
for (int i = 0; i < chs.length; i++){ //从左往右遍历字符
index = chs[i] - 'a'; //由字符对应成走向哪条路,index为两个字符相减得ASCII值
if( node.nexts[index] == null){
node.nexts[index] = new TrieNode();
}
node = node.nexts[index];
node.pass++;
}
node.end++;
}
public void delete(String word){
if(search(word) != 0){ //确定树中确实加入过word,才删除
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].pass == 0){
// java可以这么做 c++ 要遍历到底去析构
node.nexts[index] = null;
// ...
return;
}
node = node.nexts[index];
}
node.end--;
}
}
//word 这个单词之前加入过几次
public int search(String word){
if(word == null){
return 0;
}
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;
}
//所有加入的字符串中,有几个是以pre这个字符串作为前缀的
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.pass;
}
}
例子:
一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。
2 贪心算法
在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫作贪心算法。
也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。
局部最优 ——?——> 整体最优
2.1 贪心算法在笔试时的解题套路
1. 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
2. 脑补出贪心策略A、贪心策略B、贪心策略C...
3. 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
4. 不要去纠结贪心策略的证明
贪心策略在实现时,经常使用到的技巧:
1. 根据某标准建立一个比较器来排序
2. 根据某标准建立一个比较器来组成堆
2.2 会议室宣讲
题目:一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。
解题思路:取最早结束的会议进行安排,每次安排完后将时间冲突的会议删去,继续在剩余的会议里选择最早结束的会议继续安排。
public static calss 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>{
public int compare(Program o1, Program o2){
return o1.end - o2.end;
}
}
public static int bestArrange(Program[] programs, int timePoint){
Arrays.sort(programs, new ProgramComparator());
int result = 0;
//从左往右依次遍历所有的会议
for(int i = 0; i < programs.length; i++){
if(timePoint <= programs[i].start){
result++;
timePoint = program[i].end;
}
}
return result;
}
2.3 金条分割
题目:一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为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铜板。
输入一个数组,返回分割的最小代价
解题思路:
①将数组中所有数放入小根堆
②每次弹出两个数做结合
③将结合的数再次放入小根堆里
④回到②直到所有数弹出
public static int lessMoney(int[] arr){
PriorityQueue<Integer> pQ = new PriorityQueue<>();
for( int i = 0; i < arr.length; i++)
pQ.add(arr[i]);
}
int sum = 0;
int cur = 0;
while (pQ.size() > 1 ){
cur = pQ.poll() + pQ.poll();
sum += cur;
pQ.add(cur);
}
return sum;
}
2.4 获取最大钱数
输入:
正数数组costs
正数数组profits
正数k
正数m
含义:
costs[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
k表示你只能串行的最多做k个小牧
m表示你初始的资金
说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目
输出:你最后获得的最大钱数
解题思路:
①准备一个花费小根堆和一个利润大根堆
②将所有能完成项目先放入花费小根堆中
③然后将能够完成的项目逐渐弹出至大根堆
④从大根堆弹出当前资金下利润最大的项目
⑤回到②直到小根堆中没有项目
2.5 N皇后问题
N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行不同列,也不在同一条斜线上。
给定一个整数n,返回N皇后的摆法有多少种
n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92
解题思路:
①首先建立一个N*N的list(棋盘)
②首先在第一行放置一个皇后
③接着在第二行放置一个皇后,然后依次判断列和的斜线要求是否满足
④依次完成
代码
public static int num1(int n){
if( n < 1){
return 0;
}
int[] record = new int[n]; //record[i] -> i行的皇后,放在了第几列 用一个数组代替了list
return process1(0, record, n);
}
// record[0,...,i-1]的皇后,任何两个皇后都一定不共行、不共列、不共斜线
// 目前来到了第i行
// record[0,...,i-1]表示之前的行放过的皇后位置
// n代表整体一共有多少行
// 返回值是,摆完所有的皇后,合理的摆法有多少种
public static int process1(int i, int[] record, int n){
if( i == n){ //终止行
return 1;
}
int res = 0;
for(int j = 0; j < n; j++{
//当前i行的皇后,放在j列,会不会和之前(0,...,i-1)的皇后,共行共列或者共斜线
//如果是, 认为有效
//如果不是 认为无效
if(isValid(record, i, j)){ // 当前行在i行,尝试i行所有的列 -> j
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
// record[0,...,i-1]你需要看,record[i...]不需要看
// 返回i行皇后,放在了j列,是否有效
public static boolean isValid(int[] record, int i, int j){
for(int k = 0; k < i; k++){ //之前的某个k行皇后
if( j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)){
return false;
}
}
return true;
}
常数优化版本(用位运算去加速计算)
// 请不要超过32皇后问题
public static int num2(int n){
if(n < 1 || n > 32){
return 0;
}
int limit = n == 32 ? -1 : (1 << n) - 1;
return process2(limit, 0, 0, 0);
}
// colLim 列的限制,1的位置不能放皇后,0的位置可以
// leftDiaLim 左斜线的显示, 1的位置不能放皇后,0的位置可以
// rightDiaLim 右斜线的显示, 1的位置不能放皇后,0的位置可以
public static int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim){
if(colLim == limit){ //说明该行上所有都填满皇后
return 1;
}
int pos = 0;
int mostRightOne = 0;
//所有候选皇后的位置
pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
int res = 0;
while(pos != 0){
mostRightOne = pos & (~pos + 1); // 提取最右侧的1
pos = pos - mostRightOne; // 减去最右侧1后的用于下一个循环的pos
res += process2(limit, colLim | mostRightOne,
(leftDiaLim | mostRightOne) << 1,
(rightDiaLim | mostRightOne) >> 1);
}
return res;
}