本部分介绍“贪心算法“ 。 接下来会介绍动态规划。回顾一下之前脉络:
什么是递归?如何设计递归算法?
||
\/
常见的递归算法应用(快排、归并、堆、)
||
\/
深入递归本质:数学归纳,递推
||
\/
深度遍历优先搜索(DFS)、回溯、剪枝
||
\/
贪心算法、动态规划
那么贪心、动规与前面这些有什么联系呢?为什么要放在这里介绍?
有固定面额的零钱数组coins=[1,2,5],每种有无穷多枚。现给定一个amount,由这些conins组成amount,比如(11 = 5+ 2+2+2)。
问,在所有能构成amount的硬币组合中,所需硬币最少多少枚?
例:
输入:11
输出:3
解释:11=5*2+1,两枚5元的,一枚1元的
思路:我们先不忙解题,来结合前面的介绍来体会一下贪心吧!
我们在来看看dfs与贪心的区别与联系(相信各位心里已经比较清楚了)
上图是全部解空间树,红色数字表示该路径上用一个这个面额的硬币去组成。可以看到11 ==》 10 == 》5==》 0
这条路径最短,即为所求。
现在进一步地,体会一下局部最优即为整体最优:
我们每一次操作都选定一个硬币,这是我们的局部最优,当解完后,我们发现每一次的局部最优也就构成了我们的整体最优解(11=5+5+1)。
进一步的,贪心思想这样想,我们一开始就用最大的面额的最多张数去构成amount,比如11 就用两张5元的。
下面是代码,其中包含了dfs式写法和迭代写法,和前讲述的递归设计取得了形式上的统一:
class Solution_g01{
int[] values = new int[] {1,2,5};
//迭代版
public int findMinCom1(int amount) {
int count = 0;
for(int i = values.length-1 ; i>=0 ; i -- ) {
int use = amount / values[i]; //最多能取多少个,当前最大面额的硬币
count += use;
amount -= use *values[i]; //减去当前最大面额的硬币数,进入下一轮迭代
}
return count;
}
//递归版
public int findMinCom2(int amount) {
int count = 0;
return dfs(amount , values.length -1);
}
private int dfs(int amount, int i) {
if(amount == 0 )
return 0;
int use = amount / values[i];
return use+dfs(amount - use * values[i], i-1);
}
}
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。示例 2:
输入:[5,5,10]
输出:true示例 3:
输入:[10,10]
输出:false示例 4:
输入:[5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。提示:
0 <= bills.length <= 10000
bills[i] 不是 5 就是 10 或是 20
思路:当给别人进行找零时,尽量先用大面额的找个他,比如20找15时,先用10元,再用5元。当所拥有的钱数无法满足找零时返回false。
具体而言:
用两个变量模拟5元和10各有多少张。
依次模拟买东西,根据不同的面额进行相应的处理
5元:fives++
10元:判断fives > 0 , five --, tens++
20元: if tens > 0 : t -= 10;
t>0 且 fives >0 : t-=5, fives –
循环结束返回true
public boolean lemonadeChange(int[] bills) {
int fives = 0,tens = 0 ;
for(int b : bills) {
if(b == 5 ) fives ++;
else if(b == 10) {
if(fives > 0) fives --;
else return false;
tens++;
}else { //拿20过来买
int t = 15;
if(tens > 0) {
t -= 10;
tens --;
}
while(t >0 && fives > 0) {
t -= 5;
fives --;
}
if(t > 0) return false;
}
}
return true;
}
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。示例 2:
输入: [1,2], [1,2,3]
输出: 2
思路:
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i = 0 , j = 0 , res = 0;
for(i = 0 ; i < g.length ; i++) {
while(j < s.length && s[j] < g[i] )j++; //当前饼干小于当前小孩胃口,跳过,寻找下一块饼干
if(j < s.length) { //将当前饼干分给第i个
res ++;
j++;
}
}
return res;
}
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。示例 2:
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。示例 3:
输入: [1,2,3,4,5,6,7,8,9]
输出: 2
进阶:
你能否用 O(n) 时间复杂度完成此题?
public int wiggleMaxLength(int[] nums) {
//去重
int j = 0;
if(nums.length == 0) return 0;
for(int i = 1; i <nums.length ; i ++) {
if(nums[i] != nums[j])
nums[++j] = nums[i];
}
// int nums_new[] = Arrays.copyOf(nums, ++j);
j++;
int res = 2;
if(j <= 2) return j;
for(int i = 1; i +1 < j ; i++) {
int a= nums[i -1],b = nums[i],c = nums[i+1];
//如果是局部最小和局部最大
if(a<b && b >c) res++; //极大值
else if (a>b && b<c) res++; //极小值
}
return res;
}
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:
总人数少于1100人。示例
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
思考:前面放个高的人,根据题意,个高的人是看不到个子比自己低的人,因此可以先考虑身高高的人。
将所有人按照身高从高到低排序。若遇到两个身高相同的,则按照k值从小到大排序(k小的要在前面)
[7,0][7,1][6,1][5,0][5,1][4,4]
接下来就比较容易了,不过刚开始可能不太好想:
[7,0]
应该放在第0个位置上,[5,1]
应该放在下标为1的位置上。 public int[][] reconstructQueue(int[][] people) {
//按照身高排
Arrays.sort(people,new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
int res =o2[0] - o1[0];
if(res == 0) return o1[1] - o2[1];
else return res;
}
});
List<int[]> output = new LinkedList<>();
for(int[] p : people)
output.add(p[1], p);
return output.toArray(new int[people.length][2]);
}
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
Example:
输入:
[[10,16], [2,8], [1,6], [7,12]]输出:
2解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。
先将区间按照左端点从小到大 排序,依次向右边考虑
end表示目前能够到达的最远位置
每添上一段区间,考虑minCount是否需要增加:
public int findMinArrowShots(int[][] points) {
if(points.length < 2) return points.length;
//按照区间起点进行排队
Arrays.sort(points,new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if(o1[0] != o2[0])
return o1[0] - o2[0];
return o1[1] - o2[1];
}
});
int minCount = 1;
//end表示目前能够到达的最远位置
int end = points[0][1];
//贪心过程,每射上一支箭,记录当前能够射穿的区间
for(int i = 1 ; i < points.length; i ++) {
if(points[i][0] <= end)
end = Math.min(end, points[i][1]);
else {
minCount++;
end = points[i][1];
}
}
return minCount;
}
给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。
注意:
num 的长度小于 10002 且 ≥ k。
num 不会包含任何前导零。
示例 1 :输入: num = “1432219”, k = 3
输出: “1219”
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。
示例 2 :输入: num = “10200”, k = 1
输出: “200”
解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :输入: num = “10”, k = 2
输出: “0”
解释: 从原数字移除所有的数字,剩余为空就是0。
思路: 在一个数字中移掉固定k位数字,使得剩下最小。进行如下思考:剩下的的数字长度是一定的,因此我们希望的是高位数字越小越好。
算法:
public String removeKdigits(String num, int k) {
//用一个链表模拟栈
LinkedList<Character> stack = new LinkedList<Character>();
for(char c : num.toCharArray()) {
while(stack.size() > 0 && k > 0 && stack.peekLast() >c) {
stack.removeLast();
k--;
}
stack.add(c);
}
//当K还没用完,序列已经变为顺序了,那么从栈后面直接删除即可
while(k-- > 0)
stack.removeLast();
//将这个栈(链表)转换为最终的string,另外若前面是0,注意删除
StringBuilder res = new StringBuilder();
boolean prezero = true;
for(char c : stack) {
if(prezero && c == '0') continue;
prezero = false;
res.append(c);
}
if(res.length() == 0) return "0";
return res.toString();
}
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。
示例 1:输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:输入:
gas = [2,3,4]
cost = [3,4,3]输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
思路:该题一拿到肯定想到的是暴力枚举。即枚举每一个位置出发,看其加的油和消耗的油能否满足(gas_left > 0)…若走到了一圈,则返回该处下标i,否则进行下一个位置的检测。 最后如若所有位置不成,返回-1。
//暴力
public int canCompleteCircuit(int[] gas, int[] cost) {
for(int i = 0 ,j = 0; i < gas.length ; i ++) {
int gas_left = 0;
for(j = 0 ; j < cost.length; j ++) {
int k = (i + j) % cost.length;
gas_left += gas[k] - cost[k];
if(gas_left < 0)
break;
}
if( j >= gas.length) return i; //走了一圈
}
return - 1;
}
其实上述代码可以进行优化,将O(n^2)降为O(n)级别。
那么也就是说,i 出发是可以到达i 和 j 中间的任一位置。 并且,到达中间某一位置k时,其剩余的油量是大于等于0的,(可能有结余,也可能没有嘛2)
那么现在假设从k出发(出发时是没有结余的),则也是无论如何到不了 j +1 的,最终和 i 出发类似,死在了j 与j+1之间。不知道有没有体会到。。
因此对外层循环进行小改动:
public int canCompleteCircuit(int[] gas, int[] cost) {
for(int i = 0 ,j = 0; i < gas.length ; i +=j+1) { //直接迈过 i 和j 中间的所有点
int gas_left = 0;
for(j = 0 ; j < cost.length; j ++) {
int k = (i + j) % cost.length;
gas_left += gas[k] - cost[k];
if(gas_left < 0)
break;
}
if( j >= gas.length) return i; //走了一圈
}
return - 1;
}
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
示例 2:输入: [1,2,2]
输出: 4
解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这已满足上述两个条件。
思路:两个数组left和right,先后从左和从右遍历。
left[]
时,若left[i] > left[i-1]
,则left[i] = left[i-1] +1
right[]
时,若right[i] > right[i+1]
,则right[i] = righr[i+!] +1
left[i]
和right[i]
中较大的那个 public int candy(int[] ratings) {
int[] left = new int [ratings.length];
Arrays.fill(left,1);
for(int i = 1 ; i < ratings.length ; i ++)
if(ratings[i] > ratings[i - 1])
left[i] = left[i-1] +1;
int res = left[ratings.length -1];
int[] right = new int[ratings.length];
Arrays.fill(right, 1);
for(int i = ratings.length - 2 ; i >=0 ;i --) {
if(ratings[i+1] < ratings[i])
right[i] = right[i +1] +1;
res += Math.max(left[i] ,right[i] );
}
return res;
}
给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:
输入: “bcabc”
输出: “abc”
示例 2:输入: “cbacdcbc”
输出: “acdb”
思路:
curChar < stack.peek()
charLastIndex[stack.peek() - 'a'] >= i
(这里charLastIndex数组保存每个元素在string中最后出现的位置)stack
中的。 public String removeDuplicateLetters(String s) {
int len = s.length();
// 特判
if (len < 2) {
return s;
}
// 记录是否在已经得到的字符串中
boolean[] set = new boolean[26];
// 记录每个字符出现的最后一个位置
int[] lastAppearIndex = new int[26];
for (int i = 0; i < len; i++) {
lastAppearIndex[s.charAt(i) - 'a'] = i;
}
//使用栈来模拟
Stack<Character> stack = new Stack<>();
for (int i = 0; i < len; i++) {
char currentChar = s.charAt(i);
//如果栈中已经存在了,跳过即可
if (set[currentChar - 'a'])
continue;
//栈非空,并且当前字符小于栈顶,同时栈顶元素在后面的字符串中还会出现
while (!stack.empty() && stack.peek() > currentChar &&
lastAppearIndex[stack.peek() - 'a'] >= i) {
char top = stack.pop();
set[top - 'a'] = false;
}
//入栈
stack.push(currentChar);
set[currentChar - 'a'] = true;
}
StringBuilder stringBuilder = new StringBuilder();
while (!stack.empty())
stringBuilder.insert(0, stack.pop());
return stringBuilder.toString();
}
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
思路: 以每个区间的右端点从小到大排序,然后找出不重复的区间,最后用总区间个数去减。
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length <= 1) return 0;
Arrays.sort(intervals,new Comparator<int[]>() {
public int compare(int[] o1, int[] o2) {
return o1 [1] - o2[1];
}
});
int count = 1;
//end 表示当前的末尾端点,初始为第一元素
int end = intervals[0][1];
for(int[] interval : intervals) {
int start = interval[0];
if(start >= end) { //表示找到了一个不重合的
count++;
end = interval[1]; //end更新为当前最右端点
}
}
return intervals.length - count;
}
现在可以再看看第6题射区间问题。其和该题的不同之处在于两点:
因此只需要小改动,相比原题的以左端点排序可能还是要简单点
... ...
int count = 1;
//end 表示当前的末尾端点,初始为第一元素
int end = intervals[0][1];
for(int[] interval : intervals) {
int start = interval[0];
if(start > end) { //表示找到了一个不重合的(这里是严格大于)
count++;
end = interval[1]; //end更新为当前最右端点
}
}
return count;
}
之后也会继续扩充本篇博文,记录一些贪心例题与解答。
往期回顾: