算法刷题笔记(下)
贪心算法
分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。
所以你应该输出 1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出 2。
提示:
1 <= g.length <= 3 * 1040 <= s.length <= 3 * 1041 <= g[i], s[j] <= 231 - 1
思路
贪心,小饼干先喂饱小胃口。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int result = 0;
int index = 0;
// 遍历饼干
for (int i = 0; i < s.length; i++) {
if (index < g.length && s[i] >= g[index]) {
result++;
index++;
}
}
return result;
}
}
摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]和[1, 7, 4, 5, 5]不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000
**进阶:**你能否用 O(n) 时间复杂度完成此题?
思路
摆动序列需要满足条件prevDiff(nums[i] - nums[i - 1])和currDiff(nums[i + 1] - nums[i])异号,满足就统计。这是一个基本的思路,但是要考虑以下三种情况:
- 上下坡中存在平坡
- 数组首尾两端
- 单调坡中存在平坡
上下坡中存在平坡
比如[1,2,2,2,1],这种情况摆动序列是3。当 i 指向第一个 2 的时候,也就是i = 1,prevDiff > 0 && currDiff = 0,当i = 3时,prevDiff = 0 && currDiff < 0;我们统一忽略左边三个2的规则,那么满足prevDiff = 0 && currDiff < 0也是摆动序列。所以满足摆动序列的条件应该是:(currDiff < 0 && prevDiff >= 0) || (currDiff > 0 && prevDiff <= 0)。
数组首尾两端
当数组只有两个元素的时候,再使用前面的判断条件会出现数组越界问题,对于只有两个元素的数组我们特殊处理就可以了,如果两个元素不相等,则摆动序列为2,否则为1,并将这个值作为初始值,即result会初始化为1。
单调坡中存在平坡
比方说数组是[1,2,2,2,3,4],从图中可以看出摆动序列长度是3,但实际上是2,所以我们要在产生摆动的时候再更新prevDiff的值。

class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
if (n < 2) {
return n;
}
int prevDiff = nums[1] - nums[0];
int result = prevDiff != 0 ? 2 : 1;
for (int i = 2; i < n; i++) {
int currDiff = nums[i] - nums[i - 1];
if ((currDiff < 0 && prevDiff >= 0) || (currDiff > 0 && prevDiff <= 0)) {
result++;
prevDiff = currDiff;
}
}
return result;
}
}
最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 104
**进阶:**如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
思路
局部最优:当前连续和为负数的时刻立刻放弃,从下一个元素重新计算连续和,因为负数加上下一个元素后连续和只有越来越小,最终不断更新局部最优从而获得全局最优。
class Solution {
public int maxSubArray(int[] nums) {
int result = Integer.MIN_VALUE;
int sum = 0;
for (int n : nums) {
sum += n;
if (sum > result) {
result = sum;
}
if (sum <= 0) {
sum = 0;
}
}
return result;
}
}
买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
提示:
1 <= prices.length <= 3 * 1040 <= prices[i] <= 104
对于上涨日直接卖赚取最大利润;下降的日志则不卖,这样永不亏钱,最终累加所有的利润即是最大利润。
class Solution {
public int maxProfit(int[] prices) {
int result = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
result += prices[i]- prices[i - 1];
}
}
return result;
}
}
跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 1040 <= nums[i] <= 105
思路
题目并没有要求每次跳几步,也就是说最终能够跳的距离大于数组的长度就可以判断为能跳到最后一个下标了。所以我们可以用贪心来做,每一次都跳到最远的位置,最终得到跳跃的最大范围。
class Solution {
public boolean canJump(int[] nums) {
if (nums.length <= 1) {
return true;
}
int sum = 0;
// 这里需要注意 i <= sum
for (int i = 0; i <= sum; i++) {
sum = Math.max(sum, i + nums[i]);
if (sum >= nums.length - 1) {
return true;
}
}
return false;
}
}
跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 1040 <= nums[i] <= 1000- 题目保证可以到达
nums[n-1]
思路
每次跳跃的时候,根据可到达的范围里面挑选一个nums[i] + i最大的值作为下一次跳跃的起点,这样每次都是跳到最远处。

具体实现需要维护一个下一次跳远最大距离max,当前跳跃范围的右边界end,每次走到右边界end时更新end,并将跳跃次数加一。
此外遍历的时候不需要遍历最后一个元素,因为题目保证了一定可以到达最后一个位置,所以当我们遍历到倒数第二个位置的时候end的值一定大于或者等于数组的最后一个位置,否者就无法保证一定可以到达nums[n - 1]。
class Solution {
public int jump(int[] nums) {
int step = 0;
// 当前跳跃范围的右边界(下一次跳跃的起点)
int end = 0;
// 下一次跳跃的最远位置
int max = 0;
for (int i = 0; i < nums.length - 1; i++) {
max = Math.max(max, i + nums[i]);
if (i == end) {
end = max;
step++;
}
}
return step;
}
}
