01 | 前缀树
-
前缀树又名字典树,单词查找树,Trie树,多路树形结构,和hash效率有一拼,是一种用于快速检索的多叉树结构。多用于词频搜索或者模糊查询。
-
查询时只与样本长度有关,而与样本量无关。
-
如下图所示,将字母写在边上。
-
代码实现:
public class Code_01_TrieTree { public static class TrieNode { public int path; public int end; public TrieNode[] nexts; public TrieNode() { path = 0; //有多少个结点到达过 end = 0; //有多少个字符串以这个结点结尾 nexts = new TrieNode[26]; //通向子节点的路,如果题目所给的范围不确定就用map } } 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; 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.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'; if (--node.nexts[index].path == 0) { //如果某个结点-1之后==0,则说明此节点之后的结点也是-1之后==0,因此直接=null即可。 node.nexts[index] = null; return; } node = node.nexts[index]; } node.end--; } } //查找某个单词插入了几次 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; } //查某个字符串前缀数量是多少 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; } } public static void main(String[] args) { Trie trie = new Trie(); System.out.println(trie.search("zuo")); trie.insert("zuo"); System.out.println(trie.search("zuo")); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.insert("zuo"); trie.insert("zuo"); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.insert("zuoa"); trie.insert("zuoac"); trie.insert("zuoab"); trie.insert("zuoad"); trie.delete("zuoa"); System.out.println(trie.search("zuoa")); System.out.println(trie.prefixNumber("zuo")); } }
-
举个栗子:一个字符串类型的数组arr1,另一个字符串类型的数组arr2。
arr2中有哪些字符,是arr1中出现的?请打印
- 对应上述代码中的search(String word)功能。
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请 打印
- 对应上述代码中的prefixNumber(String pre)
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印 arr2中出现次数最大的前缀。
- 对应上述代码中的prefixNumber(String pre)
02 | 贪心算法
不要企图证明贪心的正确性,想几个贪心策略之后,用对数器去证明正确的即可。
- 贪心算法:在对问题求解时,总是做出在当前看来是最好的选择(最小,最大,最优等等)。也就是说,不从整体最优上加以考虑,它所做出的仅是在某种意义上的局部最优解。
- 贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
- 贪心算法的解题思路:把求解的问题分成若干个子问题,对每一子问题求解,得到子问题的局部最优解;把子问题的解局部最优解合成原来解问题的一个解。
- 贪心策略靠经验和积累,没有其他办法。
1.按最低字典序拼接字符串
给定一个字符串类型的数组strs,找到一种拼接方式,使得把所 有字符串拼起来之后形成的字符串具有最低的字典序。
- 字典序:对于字符串,先按首字符排序,如果首字符相同,再按第二个字符排序,以此类推。如aa,ab,ba,bb,bc就是一个字典序。
- 解题思路:str1.str2 <= str2.str1,则 str1 放前面,否则 str2 放前面(根据两个字符串拼接的结果的大小来决定排序),不能直接根据str1和str2的大小比较决定位置排放,比如:b和ba,最小的字典序应该是bab而不是bba。
- 代码实现:
import java.util.Arrays; import java.util.Comparator; public class Code_05_LowestLexicography { public static class MyComparator implements Comparator<String> { @Override public int compare(String a, String b) { return (a + b).compareTo(b + a); } } public static String lowestString(String[] strs) { if (strs == null || strs.length == 0) { return ""; } Arrays.sort(strs, new MyComparator()); String res = ""; for (int i = 0; i < strs.length; i++) { res += strs[i]; } return res; } public static void main(String[] args) { String[] strs1 = { "jibw", "ji", "jp", "bw", "jibw" }; System.out.println(lowestString(strs1)); String[] strs2 = { "ba", "b" }; System.out.println(lowestString(strs2)); } }
2.切分金条总代价最小
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如 长度为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铜板。
输入一个数组,返回分割的最小代价。
-
解题思路:
- 典型的哈夫曼编码问题
- 如下图所示:两个叶子节点合并过程中代价就是他们的和,最后只需将所有的非叶子节点值相加即为总代价。
- 贪心步骤如下:
1)将数组中元素加到小根堆中
2)每次从小根堆中弹出两数(最小的两个)进行相加得c,总代价+c,将c放回到小根堆中
3)重复1、2步骤,直至堆中只剩下一个数时结束。
-
当总共的代价是由子代价一种累加累乘或者是一个公式都有可能用哈夫曼编码贪出来。
-
根据自定义的比较器不同来实现不同的堆,比较器就是贪心的标准。
-
代码实现:
public class LowestCost { // 最小堆 public class MyComparator implements Comparator<Integer>{ @Override public int compare(Integer o1, Integer o2) { return o1 - o2; // 谁小把谁放在前面: -表示o1小 } } public int lowestCost(int[] arr){ // 优先级队列是小根堆,谁在前面,就把谁的优先级设置小点 PriorityQueue<Integer> pq = new PriorityQueue<>(new MyComparator()); for (int i : arr) { pq.add(i); } int costTotal = 0; // 总的代价 int costOne = 0; // 两数合并的代价 // 等于1的时候,说明堆里面只有一个元素了,即已经合并完成 while(pq.size() > 1){ costOne = pq.poll() + pq.poll(); // 合并堆里面最小的两个元素 costTotal += costOne; // 两小数合并的结果 pq.add(costOne); // 将两小数合并的结果重新添加到堆里 } return costTotal; } public static void main(String[] args) { LowestCost lc = new LowestCost(); int[] arr = {10, 20, 30, 40}; int res = lc.lowestCost(arr); System.out.println(res); // 190 = 10 + 20 + 30 + 30 + 40 + 60 } }
3.最多做k个项目的最大利润(IPO)
输入: 参数1:代价数组costs; 参数2: 利润数组profits; 参数3:正数k; 参数4:启动资金W 。costs[i]表示i号项目的花费 profits[i]表示i号项目的利润, k表示你不能并行、只能串行的最多做k个项目 。
说明:你每做完一个项目,马上获得的收益,可以支持你去做下 一个 项目。 一次只能做一个项目。
输出: 你最后获得的最大钱数。
-
解题思路:
- 准备一个小根堆,这个小根堆是按照花费,谁花费低谁放到小根堆的头部。
- 准备一个大根堆,这个大根堆是按照收益高,谁收益高谁放在大根堆的头部。
- 若小根堆不为空,项目也没做完 K 个,则每次先从小根堆解锁能够做的项目,放入大根堆
- 大根堆不为空,从大根堆弹出堆顶项目来做(即利润最大的项目,每次只弹出堆顶一个项目来做);
- 把 W 加上利润,初始资金增加,再重复3)、4)步骤。
-
举个栗子:
-
代码实现:
public class IPO { // 项目节点 public class Node{ private int profit; // 项目利润 private int cost; // 项目成本 public Node(int profit, int cost){ this.profit = profit; this.cost = cost; } } /** * @param k :最多做k个项目 * @param fund :总的资金 * @param profits :每个项目的利润数组 * @param cost :每个项目的成本数组 * @return */ public int findMaxCapital(int k, int fund, int[] profits, int[] cost){ // 初始化每个项目节点信息 Node[] nodes = new Node[profits.length]; for (int i = 0; i < profits.length; i++) { nodes[i] = new Node(profits[i], cost[i]); } // 优先级队列是谁小谁放在前面,比较器决定谁小 PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator()); // 成本小顶堆 PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator()); // 利润大顶堆 for (int i = 0; i < nodes.length; i++) { minCostQ.add(nodes[i]); // 将所有的项目插入成本堆中 } // 开始解锁项目,赚取利润 for (int i = 0; i < k; i++) { // 解锁项目的前提条件:成本堆中还有项目未被解锁并且该项目的成本小于当前的总资金 while(!minCostQ.isEmpty() && minCostQ.peek().cost <= fund){ maxProfitQ.add(minCostQ.poll()); // 将当前成本最小的项目解锁 } if(maxProfitQ.isEmpty()){ // 如果maxProfitQ为空,则说明没有当前资金能够解锁的新项目了,之前解锁的项目也做完了,即无项目可做了 return fund; // 最后的总金额 } fund += maxProfitQ.poll().profit; // 做利润最大的项目 } return fund; // k个项目都做完了 } // 成本小顶堆:成本最小的在堆顶 public class MinCostComparator implements Comparator<Node>{ @Override public int compare(Node o1, Node o2) { return o1.cost - o2.cost; } } // 利润大顶堆:利润最大的在堆顶 public class MaxProfitComparator implements Comparator<Node>{ @Override public int compare(Node o1, Node o2) { return o2.profit - o1.profit; } } }
4.安排最多的宣讲场次
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目 的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数 组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会 议室进行 的宣讲的场次最多。
返回这个最多的宣讲场次。
- 解题思路:
- 不能按照哪个项目开始的早先安排哪个,因为可能开始早的占用时间非常长,显然不合理;
- 项目持续的时间短优先安排也不合理,因为可能存在时间短的项目时间点正好在其他两个时间长项目中间,这样因为这一个项目就会浪费掉其他两个项目,显然也是不合理的;
- 按照哪个项目先结束来排。先做结束最早的项目,然后淘汰因为这个做这个项目而不能做的项目(时间冲突),依次这样去做。
- 代码实现:
public class BestArrange { public class Program{ public int start; // 项目开始时间 public int end; // 项目结束时间 public Program(int start, int end){ this.start = start; this.end = end; } } /** * @param programs :项目数组 * @param cur :当前时间 * @return :能够安排的最大项目数 */ public int getBestArrange(Program[] programs, int cur){ // 也可以用堆来做,都一样 Arrays.sort(programs, new ProgramComparator()); int res = 0; for (int i = 0; i < programs.length; i++) { // 只有当前时间早于第i个项目的开始时间时,才可以安排 if(cur <= programs[i].start){ res++; // 安排上了 cur = programs[i].end; // 当前时间推移到本次安排项目的结束时间,下个项目的开始时间必须在这个时间之后 } } return res; } // 按照项目的结束时间早来排序,即实现小根堆 public class ProgramComparator implements Comparator<Program>{ @Override public int compare(Program o1, Program o2) { return o1.end - o2.end; } } }