一、前缀树:Prefix Tree
1.1 前缀树题目举例:一个字符串类型的数组 arr1,另一个字符串类型的数组 arr2
1.2 前缀树的 insert、delete、search、prefixNum 方法
二、贪心算法
题目1:按最低字典序拼接字符串
题目2:切分金条总代价最小
题目3:最多做 K 个项目的最大利润
题目4:安排最多的宣讲场次
题目1、 arr2中有哪些字符,是arr1中出现的?请打印。
题目2、arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。
题目3、arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。
假设刚开始我们,有一个空节点,现在我们有一个操作,往这个空的节点上insert字符串“abc”, 那么我们按照下面的步骤insert:
process: 1、首先看当前节点有没有指向字符'a'的路径,没有的话就创建指向'a'的路径,否则滑过到下一个字符,同样是看看有没有到该字符的路径。一直遍历完字符,并且都创建好了路径。如下图所示:
package com.offer.foundation.class5;
/**
* @author pengcheng
* @date 2019/3/29 - 22:34
* @content: Trie树的基本操作的实现
*/
public class TrieTree {
public static class TrieNode{
private int passNum; // 表示有多少个字符串经过该节点
private int endNum; // 表示有多少个字符串以该节点结尾
private TrieNode[] paths; // 存储的是该节点到下一级所有节点的路径是否存在
public TrieNode(){
passNum = 0;
endNum = 0;
paths = new TrieNode[26]; // 假设只有26个小写字母,即每一个节点拥有26条可能的路径
}
}
private TrieNode root; // 不管什么操作,都是从根节点开始的,所以要记录根节点
public TrieTree(){
// Trie树的初始化
root = new TrieNode();
}
// 往trie树中插入一个字符串
public void insert(String word){
if(word == null){
return;
}
char[] chars = word.toCharArray();
TrieNode node = root;
int index = 0; // index值:0-25 对应 a-z
for(int i = 0; i < chars.length; i++){
index = chars[i] - 'a'; // 计算该字符在当前节点的那条路径上
// 判断该路径是否已经存在
if(node.paths[index] == null){
node.paths[index] = new TrieNode(); // 如果路径不存在,则创建它
}
// 路径已经存在的话,就继续向下走
node = node.paths[index];
node.passNum++; // 划过当前节点的字符串数+1
}
node.endNum++; // 遍历结束了,记录下以该字母结束的字符串数+1
}
// 删除一个字符串
public void delete(String word){
// 删除之前,先判断有没有
if(search(word) == 0){
return;
}
char[] chars = word.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0; i < chars.length; i++){
index = chars[i] - 'a';
// 注意 --
if(--node.paths[index].passNum == 0){
// 如果遍历到某个节点时,将其index处passNum减1后等于0,则说明没有其他字符串经过它了,直接将其设置为null
node.paths[index] = null;
return;
}
node = node.paths[index]; // 继续向下遍历
}
node.endNum--; // 遍历完了,删除了整个单词,则将以该单词最后一个字符结尾的字符串的数目减1
}
// 在trie树中查找word字符串出现的次数
public int search(String word){
if(word == null){
return 0;
}
char[] chars = word.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0; i < chars.length; i++){
index = chars[i] - 'a';
if(node.paths[index] == null){
return 0; // 不存在
}
node = node.paths[index]; // 到达了该字母记录的节点路径,继续往下走
}
// 整个单词的所有字母都在树中,说明单词在树中,返回该单词最后一个字符的endNum
return node.endNum;
}
// 返回有多少单词以pre为前缀的
public int prefixNum(String pre){
if(pre == null){
return 0;
}
char[] chars = pre.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0; i < chars.length; i++){
index = chars[i] - 'a';
if(node.paths[index] == null){
return 0; // 不存在
}
node = node.paths[index]; // 继续向下找
}
return node.passNum; // 找到pre最后一个字符的passNum值
}
}
你自己想出贪心策略,但只能感觉它对不对,理论证明放弃吧!重点在于想很多的贪心策略,用对数器去证明对不对。
package com.offer.foundation.class5;
import java.util.Arrays;
import java.util.Comparator;
/**
* @author pengcheng
* @date 2019/3/30 - 10:53
* @content: 贪心策略:按最低字典序拼接字符串
*/
public class Lowest {
// 自定义比较器:给字符串按照自定义的规则排序
public class MyComparator implements Comparator {
@Override
public int compare(String a, String b) {
return (a + b).compareTo(b + a); // 哪个小哪个放前面
}
}
public String getLowestString(String[] strs){
if(strs == null || strs.length == 0){
return "";
}
// 给字符串数组按照自己定义的规则排序
// 对于制定的贪心策略,先直观分析下对不对,不要去试图证明,可以使用对数器证明
Arrays.sort(strs, new MyComparator());
String res = "";
for (String str : strs) {
res += str;
}
return res;
}
// 测试
public static void main(String[] args) {
Lowest lowest = new Lowest();
String[] str = {"ba", "b","baa"}; // baabab
System.out.println(lowest.getLowestString(str));
}
}
题目:一块金条切成两半,是需要花费和长度数值一样的铜板的。比如:长度为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铜板。
输入一个数组,返回分割的最小代价。
补充:堆结构的扩展与应用【经常用于贪心】:
package com.offer.foundation.class5;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* @author pengcheng
* @date 2019/3/30 - 11:43
* @content: 最小代价问题:实质上是霍夫曼编码问题
*/
public class LowestCost {
// 最小堆
public class MyComparator implements Comparator{
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2; // 谁小把谁放在前面: -表示o1小
}
}
// 输入的是一个数组,数组中的元素则是最终的切割方式,现在要找出这种方式需要花费的最小代价
public int lowestCost(int[] arr){
// 优先级队列是小根堆,谁在前面,就把谁的优先级设置小点
PriorityQueue 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
}
}
题目:costs[]:花费 ,costs[i] 表示 i 号项目的花费 profits[]:利润, profits[i] 表示 i 号项目在扣除花费之后还能挣到的钱(利润)。一次只能做一个项目,最多做 k 个项目,m 表示你初始的资金。(说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目)求你最后获得的最大钱数。
举例说明:
package com.offer.foundation.class5;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* @author pengcheng
* @date 2019/3/30 - 20:07
* @content: 贪心算法:做多做k个项目的最大利润
*/
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 minCostQ = new PriorityQueue<>(new MinCostComparator()); // 成本小顶堆
PriorityQueue 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{
@Override
public int compare(Node o1, Node o2) {
return o1.cost - o2.cost;
}
}
// 利润大顶堆:利润最大的在堆顶
public class MaxProfitComparator implements Comparator{
@Override
public int compare(Node o1, Node o2) {
return o2.profit - o1.profit;
}
}
}
题目:一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。
package com.offer.foundation.class5;
import java.util.Arrays;
import java.util.Comparator;
/**
* @author pengcheng
* @date 2019/3/30 - 20:57
* @content: 贪心算法:安排最多的宣讲场次
*/
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{
@Override
public int compare(Program o1, Program o2) {
return o1.end - o2.end;
}
}
}