注意:贪心算法没有必要去严格证明局部最优能得出全局最优,感觉能行写代码就是了。而且贪心算法的题目只有一个基本思想,没有统一解法,都去证明会很费劲。
455. 分发饼干
思路:提高饼干的利用率,尺寸大的饼干尽可能去喂胃口大的小孩。这样才能喂更多的小孩。因为要先用尺寸大的饼干满足胃口大的小孩,所以要对小孩和饼干都进行排序。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);// 孩子按照胃口排序
Arrays.sort(s);// 饼干按照尺寸排序
int i=g.length-1;
int j=s.length-1;
int count=0;//满足的孩子数目
while(i>=0 && j>=0){
if(s[j]>=g[i]){//分配饼干
j--;
i--;
count++;
}
else{
i--;
//跳过这个孩子,找一个胃口更小的孩子分这个饼干
//尽可能分这个饼干,如果遍历了所有孩子都没能分出这个饼干,则更小的饼干也分不出去
}
}
return count;
}
}
121. 买卖股票的最佳时机
思路:初始化最低价格为第 1 天的价格,最大利润为 0。之后从第二天开始遍历,如果当天价格低于最低价格,就更新最低价格;否则看当天卖出会获得多少利润,若大于目前记录的最大利润,则替换记录的最大利润。
class Solution {
public int maxProfit(int[] prices) {
int minPrice = prices[0]; // 初始化最低价格为第一天的价格
int maxProfit = 0; // 初始化最大利润为0
for (int i = 1; i < prices.length; i++) {
// 更新最低价格(如果当前价格更低)
if (prices[i] < minPrice) {
minPrice = prices[i];
}
// 计算当前的最大利润(如果卖出当前股票能获得的利润更大)
else if (prices[i] - minPrice > maxProfit) {
maxProfit = prices[i] - minPrice;
}
}
return maxProfit; // 返回最大利润
}
}
376. 摆动序列
要点:
-
用 preSub 表示当前元素与前一个元素的差值,postSub 表示后一个元素与当前元素的差值,如果 preSub > 0 && postSub <0 或者 preSub < 0 && postSub > 0 就表示当前元素是一个摆动的位置。记录下来。
-
平坡上只记录最右边的元素,所以条件改为 preSub >= 0 && postSub <0 或者 preSub <= 0 && postSub > 0
-
上面只统计了中间的元素,没有考虑首尾。本题考虑首元素:在首元素前添加一个值相同的辅助元素,用于看首元素是不是峰值;考虑尾元素:峰值计数直接从 1 开始,默认尾部有一个峰值。
-
preSub 当且仅当出现峰值时才赋值为原来的 postSub
class Solution {
public int wiggleMaxLength(int[] nums) {
if(nums.length<=1){
return nums.length;
}
//最左边加一个与第一个元素相同的值作为辅助,用来计算左右差值
int preSub=0;//当前元素减前一个元素
int postSub=0;//后一个元素减当前元素,初始值是随便给的
int count=1;//最右边默认有一个峰值
for(int i=0; i<nums.length-1; i++){
postSub=nums[i+1]-nums[i];
if(preSub>=0&&postSub<0 || preSub<=0&&postSub>0){
count++;
preSub=postSub;//只在出现峰值时跟上来
}
}
return count;
}
}
53. 最大子数组和
前缀和:时间超限
class Solution {
public int maxSubArray(int[] nums) {
int[] preSum = new int[nums.length+1];
int sum = 0;
for(int i=0; i<nums.length; i++){
sum+=nums[i];
preSum[i+1]=sum;
}
int maxSum = Integer.MIN_VALUE;
for(int i=0; i<preSum.length; i++){
for(int j=i+1; j<preSum.length; j++){
int tmp=preSum[j]-preSum[i];
if(tmp>maxSum){
maxSum=tmp;
}
}
}
return maxSum;
}
}
贪心思路:① 只要不断累加,总和就有可能变大。但是,如果之前的连续和成为负数,就应该抛弃,从新的数字开始累加,防止被之前的和拖累。② 累加只是有可能让总和变大,并不一定真的变大(如:前面连续和是 4,还剩一个数字是 -1),所以还要用一个变量来记录这个过程中出现过的最大和。这样等到流程走完后,变量保存的就是全局最大和。
class Solution {
public int maxSubArray(int[] nums) {
int maxSum = Integer.MIN_VALUE;
int sum = 0;
for(int i=0; i<nums.length; i++){
if(sum<0){
sum=nums[i];
}else{
sum+=nums[i];
}
maxSum=sum>maxSum?sum:maxSum;
}
return maxSum;
}
}
122. 买卖股票的最佳时机 II
class Solution {
public int maxProfit(int[] prices) {
int sum=0;
for(int i=1; i<prices.length; i++){
int profit=prices[i]-prices[i-1];
if(profit>0){
sum+=profit;
}
}
return sum;
}
}
55. 跳跃游戏
思路:站在第一个元素的位置上开始跳,跳最大步数得到可以到达的最远点。因为每次跳的步数可以≤最大步数,所以起点到这个最远点之间的所有位置都可以到达。而这些中间点可能产生更远的跳跃点来延长总的跳跃范围,所以要枚举这些中间点(包括这个最远点),看是否超越了原范围,这个过程还要更新最远点。如果枚举完了发现最远点还不是终点,则说明到达不了终点。
class Solution {
public boolean canJump(int[] nums) {
int maxIndex=0;
// if (nums.length == 1) return true;
for(int i=0; i<=maxIndex; i++){
maxIndex=Math.max(maxIndex, i+nums[i]);
if(maxIndex>=nums.length-1){
return true;
}
}
return false;
}
}
1005. K 次取反后最大化的数组和
基本思路:将数组升序排序,先从头开始将负数修改为正数,如果还不够 k 次,就将修改后的数组再次升序排序,然后反复修改其中的最小值。
为什么反复修改最小值?因为经过第一轮修改后,数组中的数字无非就是 0 或 正整数。如果是 0,反复修改不会改变数组总和;如果是正整数,反复修改后仍是正整数也不会改变数组总和,反复修改后是负数,则修改绝对值小的正整数对数组总和的影响更小。
所以修改 0 总和不变,修改最小的正整数也有可能使总和减小。所以有 0 就反复修改 0,没有 0 就反复修改最小的正整数,总体来看就是修改最小值。
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums);
// 将所有负数修改为正数
for(int i=0; i<nums.length; i++){
if(k==0 || nums[i]>=0){//够k次了||后面都是非负数了
break;
}
nums[i]=-nums[i];
k--;
}
//如果还不够k次修改,就反复修改现阶段数组中的最小值,直到k消耗完
//现阶段的数组:负数全变为正数之后的数组
if(k!=0){
Arrays.sort(nums);
while(k>0){
nums[0]=-nums[0];
k--;
}
}
//修改全部完成,求和
int sum=0;
for(int i=0; i<nums.length; i++){
sum+=nums[i];
}
return sum;
}
}
134. 加油站
首先,以第0个加油站作为起点,模拟汽车行进的过程。如果从当前加油站出发后,油的净增量(加油-耗油)加上汽车中原有的油量大于0,则可以出发,否则更换起点为下一个加油站。
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sumGas=0;
int curGas=0;
int start=0;
for(int i=0; i<gas.length; i++){
//gas[i]-cost[i]: 从当前加油站出发能剩余多少油
sumGas+=gas[i]-cost[i];//一圈下来统计油的净增量
curGas+=gas[i]-cost[i];//从某个加油站出发后,剩余油量累计
if(curGas<0){//到达不了下一个加油站
start=i+1;//换下一个加油站作为起点
curGas=0;//剩余油量重新累计
}
}
if(sumGas<0) return -1;//总净增油量<0,跑不完全程
return start;
}
}
860. 柠檬水找零
不要给支付的钱数排序,按照原来的顺序找零。
基本思路就是模拟整个过程。遇到 5 美元顾客就直接手下,把 5 美元零钱计数加一;遇到 10 美元先看 5 美元够不够找零,够找零再更新 5 美元和 10 美元数量;遇到 20 美元顾客先看使用两种找零方式要用的零钱是否够用,够用再更新 10 美元数量。在这个过程中只要遇到找不开的情况就返回 false,流程都走完就说明没问题,返回 true。
class Solution {
public boolean lemonadeChange(int[] bills) {
int fiveD=0, tenD=0;//商家手里5美元和10美元的数量
for(int i=0; i<bills.length; i++){
if(bills[i]==5){//5美元顾客
fiveD++;
}else if(bills[i]==10){//10美元顾客
if(fiveD>=1){//用一张5美元找零
fiveD--;
tenD++;
}else{//没零钱
return false;
}
}else{//20美元顾客
if(tenD>=1 && fiveD>=1){//用一张10美元和一张5美元找零
tenD--;
fiveD--;
}else if(fiveD>=3){//用三张5美元找零
fiveD-=3;
}else{//没零钱
return false;
}
}
}
return true;
}
}
406. 根据身高重建队列
思路:遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。
将people数组按照维度一(身高)降序排列,因为要看前面有几个人高于当前这个人的身高。遇到身高相同的人,维度二(前面有几个人身高更高或相同)小的在前。
然后再遍历上面排好序的people数组,按照“前面有几个人身高≥当前人身高”逐个将people元素插入正确的位置。后面的元素插入到前面之后不会改变破坏前面已经排好的序列,因为后面的people身高都≤前面people的身高。
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, (a,b)->{
if(a[0]!=b[0]){// 身高不同,就按身高降序排序
return b[0]-a[0];
}
return a[1]-b[1];// 如果身高相同,则按照 k 值升序排序
});
List<int[]> list = new ArrayList<>();
for(int i=0; i<people.length; i++){
list.add(people[i]);
}
for(int i=0; i<list.size(); i++){
int[] tmp=people[i];
if(people[i][1]!=i){
list.add(tmp[1], tmp);
list.remove(i+1);
}
}
for(int i=0; i<people.length; i++){
people[i]=list.get(i);
}
return people;
}
}
452. 用最少数量的箭引爆气球
思路:先将气球按照左边界排序,然后遍历气球。如果当前气球的左边界 > 前一个气球的右边界,则两个气球没有重叠,不能用同一个弓箭射穿,所以弓箭数+1;否则两个气球就有重叠,需要更新当前气球的右边界为两个气球右边界的最小值,这是为了在看下一个气球时判断是否能与前一个气球共用一个弓箭。
class Solution {
public int findMinArrowShots(int[][] points) {
// 没有气球,不需要弓箭
if(points.length==0)
return 0;
//首先将气球按照左边界升序排序
Arrays.sort(points, (a,b)->Integer.compare(a[0], b[0]));
// 用这种排序会溢出
// Arrays.sort(points, (a,b)->{
// return a[0]-b[0];
// });
int arrow=1;//只要有气球,就至少需要一个弓箭,所以初始化为1
for(int i=1; i<points.length; i++){
if(points[i][0]>points[i-1][1]){//当前气球与前一个气球不重叠
arrow++;
}else{//重叠,选取两者右边界的最小值
points[i][1]=Math.min(points[i-1][1], points[i][1]);
}
}
return arrow;
}
}
435. 无重叠区间
思路:可以借鉴上一题射气球
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b)->Integer.compare(a[0],b[0]));
int num=1;//重叠的组数
for(int i=1; i<intervals.length; i++){
//端点重叠不叫重叠,所以按射气球的说法,一箭射不爆,所以带等号
if(intervals[i][0]>=intervals[i-1][1]){
num++;
}else{
intervals[i][1]=Math.min(intervals[i-1][1], intervals[i][1]);
}
}
// 先看能至少用几根箭射穿所有区间,箭数与区间总数的差值就是至少要去掉的区间数
return intervals.length-num;
}
}
763. 划分字母区间
题意:一个区间中只要包含 a,就要把所有的 a 都包含进来。
思路:首先用一个数组统计每个字符最后出现的位置。然后遍历字符串,用一个变量通过不断更新的方式记录遍历过的字符最后出现的位置。如果遍历时到达了这个变量记录的位置,就说明可以截取了。
下面两种解法的不同在于记录每个字符最后出现的位置时,使用的方式不同。一个使用 Map 集合,一个使用将字符映射成数组下标然后存储最后出现的位置
class Solution {
public List<Integer> partitionLabels(String s) {
Map<Character, Integer> map = new HashMap<>();
// map存储每个字符最后出现的位置
for(int i=0; i<s.length(); i++){
map.put(s.charAt(i),i);
}
List<Integer> res = new ArrayList<>();
int left=0, right=0;
for(int i=0; i<s.length(); i++){
// 当前字符最后出现的位置、right 取最大
right = Math.max(map.get(s.charAt(i)), right);
if(i==right){
res.add(right-left+1);
left=i+1;
}
}
return res;
}
}
class Solution {
public List<Integer> partitionLabels(String s) {
int[] last = new int[26];
// 下标0处存储a最后出现的位置,下标1处存储b最后出现的位置...
for(int i=0; i<s.length(); i++){
last[s.charAt(i)-'a']=i;
}
List<Integer> res = new ArrayList<>();
int left=0, right=0;
for(int i=0; i<s.length(); i++){
// 当前字符最后出现的位置、right 取最大
right = Math.max(last[s.charAt(i)-'a'], right);
if(i==right){
res.add(right-left+1);
left=i+1;
}
}
return res;
}
}
56. 合并区间
思路:先将区间按照左边界升序排序。第一个区间直接放入结果集,然后从第二个区间开始向后遍历。如果当前区间与结果集中的最后一个区间有重叠,就将当前区间合并到结果集中的最后一个区间;如果没有重叠,就直接将当前区间加入结果集。将当前区间合并到结果集中最后一个区间时,只需更改最后一个区间的右边界为两者右边界的最大值,左边界无需更改,因为其左边界一定≤当前区间的左边界。
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> resList = new ArrayList<>();
Arrays.sort(intervals, (a,b)->Integer.compare(a[0], b[0]));
// 左边界排序后,第一个区间直接放入结果集,方便进行后面的合并操作
resList.add(intervals[0]);
for(int i=1; i<intervals.length; i++){
// 结果集中的最后一个区间
int[] pre = resList.get(resList.size()-1);
// 当前遍历到的区间与之有重叠
if(intervals[i][0]<=pre[1]){
pre[1]=Math.max(intervals[i][1], pre[1]);
}else{
resList.add(intervals[i]);
}
}
// list集合转数组:list对象.toArray()
// 想得到什么类型的数组就new一个什么类型的数组,并初始化长度与list相同
// 二维数组的第二个维度可以不指定长度
// 数组存储的是list集合元素的引用
return resList.toArray(new int[resList.size()][]);
}
}
738. 单调递增的数字
时间超限代码:
class Solution {
public int monotoneIncreasingDigits(int n) {
for(int i=n; i>=0; i--){
if(ascend(i)){
return i;
}
}
//实际上走不到这里,但是不return不符合语法
return -1;
}
private boolean ascend(Integer num){
String str = num.toString();
for(int i=1; i<str.length(); i++){
if(str.charAt(i-1)>str.charAt(i)){
return false;
}
}
return true;
}
}
AC 代码思路:从后向前遍历,一旦遇到前一个位置上的数字>当前位置上的数字,就将前一个数字减一(当前数字只能减不能增,只有前面数字减一,后面数字才有机会≥前面数字),然后当前数字以及之后的数字都变成 9。
为什么不是只把当前数字变成 9,而是当前和之后的数字都要变成 9?举个例子,1000,只有遍历到倒数第三个 0 时,才会将 1 变成 0,0 变成 9,此时,如果不把后面的数字都变成 9,就会得到 900,不符合递增规则。
为什么从后向前遍历,而不是从前向后?举个例子,332,如果从前向后遍历,33 符合规则,32 不符合,所以要把 3 减一变成 2,然后 2 变成 9,最后得到 329,不符合递增规则。
class Solution {
public int monotoneIncreasingDigits(int n) {
// int转String,String转char数组
char[] chArr = Integer.toString(n).toCharArray();
// 有的整数可能不需要改变,所以不会进入下面for循环中的if
// 这种情况不需要替换9的操作,所以flag赋初值为数组长度
int flag = chArr.length;
for(int i=chArr.length-1; i>0; i--){
if(chArr[i-1]>chArr[i]){
//注意是-1,不是-'1',减字符1就减多了
chArr[i-1]-=1;
flag=i;
}
}
for(int i=flag; i<chArr.length; i++){
chArr[i]='9';
}
// char数组转String,String转int
return Integer.parseInt(new String(chArr));
}
}