目录
【贪心算法理论基础】
1、本质
贪心算法通过寻找【局部最优】,进而找到【全局最优】。
2、步骤
贪心算法一般分为如下三步:
- 判断是否存在局部最优和全局最优的关系,存在再使用贪心算法
- 求解局部最优
- 将局部最优解堆叠成全局最优解
【455.分发饼干】简单题
方法一 先喂饱胃口小的孩子,遍历孩子
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int res = 0;
int j = 0;
// 策略:先喂饱胃口小的孩子,遍历孩子
for (int i = 0; i < g.length && j < s.length; i++){
// 找喂饱第i个孩子的饼干,找到就res+1
while (j < s.length){
if (s[j] >= g[i]) {
res++;
j++;
break;
}
j++;
}
}
return res;
}
}
方法二 先喂饱胃口小的孩子,遍历饼干
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int j = 0; // j指向孩子
int res = 0;
// 策略:先喂饱胃口小的孩子,遍历饼干(i和j都要在可索引范围内)
for (int i = 0; i < s.length && j < g.length; i++){ // i指向饼干
if (s[i] >= g[j]){
j++;
res++;
}
}
return res;
}
}
方法三 先喂饱胃口大的孩子,遍历孩子
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int j = s.length - 1;
int res = 0;
for (int i = g.length - 1; i >= 0 && j >= 0; i--){
if (s[j] >= g[i]){
j--;
res++;
}
}
return res;
}
}
方法四 先喂饱胃口大的孩子,遍历饼干
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int j = g.length - 1;
int res = 0;
// 策略:先喂饱胃口大的孩子,从后往前遍历饼干
for (int i = s.length - 1; i >= 0 && j >= 0 ; i--){
// 如果后面的孩子胃口太大喂不饱,就先喂前面的
while (j >= 0){
if (s[i] >= g[j]){
res++;
j--;
break;
}
j--;
}
}
return res;
}
}
- 时间复杂度: O(nlogn),快速排序O(nlogn),双指针遍历O(m+n)
- 空间复杂度: O(1)
【376. 摆动序列】中等题(很难理解)
思路:
1、使用贪心算法,局部最优 -> 全局最优。
局部最优是指每遍历一个点就获取截止到这个元素的最长摆动子序列,全局最优是指当遍历到最后时,局部最优可以转化为全局最优。
2、关键是求最长摆动子序列的长度,只需要记录属于最长摆动子序列的节点个数即可。
不需要删除多余的点,只要避开这些点,不对这些多余的点计数就行
3、汇总出三种要记录的情况,即第一次出现摆动的点、位于上下坡转折的点、最后一个点。
利用摆动标记mark,区分前两种情况,最后一个点一定要记录结果,对应初始值res=1。
- mark = 0时,证明前面没有出现过摆动。如果出现摆动就是第一次摆动,就设置摆动标记mark的值,并res+1
- 如果当前摆动为上坡,则mark = 1
- 如果当前摆动为下坡,则mark = -1
- mark != 0时,证明前面已经出现过摆动了。如果当前节点也出现了摆动,就判断当前节点的摆动和上一个摆动是否相反,也就是判断当前节点是否位于上下坡的转折点,是的话就更新摆动标记mark的值,并res+1
- 如果当前摆动为上坡,而且mark标记的上个摆动为下坡(mark = -1),则res+1,更新mark为1
- 如果当前摆动为下坡,而且mark标记的上个摆动为上坡(mark = 1),则res+1,更新mark为-1
例子:[0,0,0,1,2,1,1,2,1,1,0,1,1,2,2,1],结果res返回7。
(注意:例子中包含普通平坡、连续上坡(连续下坡也一样)、下坡和上坡之间的平坡、包含平坡的连续下坡、包含平坡的连续上坡,上坡和下坡之间的平坡。)
4、考虑两种特殊情况,即长度为1和2的数组
nums.length = 1和nums.length = 2都可以包含在for循环中,不需要单独处理,原因如下:
- nums.length = 1,不符合【i < nums.length - 1】,不进入for循环,直接返回1。
- nums.length = 2,for循环只遍历 i = 0,如果出现摆动,即两个元素相异,res+1,返回2,如果没出现摆动,则两个元素相同,返回1。
class Solution {
public int wiggleMaxLength(int[] nums) {
// nums.length = 1或2的情况可以包含在下面for循环中
// if (nums.length == 1) return 1; // 不进入for循环,直接返回1
// if (nums.length == 2) return nums[0] == nums[1] ? 1 : 2; // 有摆动才返回2,否则返回1
int mark = 0; // 记录是否出现摆动,没出现摆动为0,出现摆动后上坡为1,下坡为-1
int res = 1; // 初始值为1,是因为最后一个值肯定要算入结果
// 策略:只有第一次出现摆动,以及上下坡的转折点的时候,才res++
for (int i = 0; i < nums.length - 1; i++){
int delta = nums[i+1] - nums[i]; // 计算当前节点和下一个节点的差值
// 如果之前都没出现摆动(第一次出现摆动)
if (mark == 0){
if (delta > 0) {
mark = 1; // 第一次出现的摆动为上坡
res++;
}
if (delta < 0) {
mark = -1; // 第一次出现的摆动为下坡
res++;
}
}
else{
// 如果遇到上坡,判断上一个摆动是否为下坡,是再记录结果并更新摆动
if (delta > 0 && mark == -1){
res++;
mark = 1; // 记录上坡
}
// 如果遇到下坡,判断上一个摆动是否为上坡,是再记录结果并更新摆动
if (delta < 0 && mark == 1){
res++;
mark = -1; // 记录下坡
}
// 注意:此处包含对了平坡和持续上坡/下坡的忽略
}
}
return res;
}
}
- 时间复杂度: O(n),for循环遍历一次数组
- 空间复杂度: O(1),没有额外的空间开销
【53. 最大子数组和】中等题
方法一 贪心算法
思路:
1、局部最优:每遍历一个元素,都记录着截止到当前元素的最大子数组和(当前元素不一定是最大子数组和对应子数组的结尾)
2、全局最优:遍历到最后一个元素,就相当于获得了全局最大子数组和
class Solution {
public int maxSubArray(int[] nums) {
int res = nums[0];
int tmp = 0;
for (int i = 0; i < nums.length; i++){
tmp += nums[i]; // 已确保上一个子数组和>=0,直接加上当前元素值即可,不会拖累当前元素值
res = Math.max(res, tmp); // 对比,记录最大的子数组和
tmp = tmp < 0 ? 0 : tmp; // 如果当前的子数组和已经是负数,则要避免其拖累下一个值,重新初始化为0即可
}
return res;
}
}
- 时间复杂度: O(n),for循环遍历一次数组
- 空间复杂度: O(1),没有额外的空间开销
方法二 动态规划
思路:
1、确定f(i)的含义,此处为以第i个元素结尾的最大子数组和。因为,数组的最大子数组和肯定是以某个元素结尾的子数组之和,因此只要比较所有元素结尾的最大子数组和即可。
2、确定递推关系,此处只有f(i)和f(i-1)的关系,因为f(i)只与f(i-1)有关
- 以前面元素结尾的最大子数组和<0,只会拖累当前元素,还不如直接重新开始
- 以前面元素结尾的最大子数组和>=0,对当前元素来说,加上只会增大(或不变)
class Solution {
public int maxSubArray(int[] nums) {
// 确定f(i)的含义,此处为以第i个元素结尾的最大子数组和
int dp = 0; // 用于标记以当前元素为结尾的数组对应的最大子数组和
int max = nums[0]; // 用于记录最大子数组和
// 确定递推关系,此处只有f(i)和f(i-1)的关系,因为f(i)只与f(i-1)有关
for (int i = 0; i < nums.length; i++){
if (dp < 0) dp = nums[i]; // 前面的最大子数组和<0,则会拖累当前元素,直接重新开始
else dp += nums[i]; // 前面的最大子数组和>=0,对当前元素来说,加上只会增大(或不变)
max = Math.max(max, dp);
}
return max;
}
}
- 时间复杂度: O(n),for循环遍历一次数组
- 空间复杂度: O(1),没有额外的空间开销,因为f(i)只与f(i-1)有关,直接用一个变量dp记录即可