Loading [MathJax]/jax/output/HTML-CSS/jax.js
1 Star 0 Fork 39

magi02cccc/代码随想录

forked from pakchoi/代码随想录 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
pics
problems
qita
前序
周总结
0001.两数之和.md
0005.最长回文子串.md
0015.三数之和.md
0017.电话号码的字母组合.md
0018.四数之和.md
0019.删除链表的倒数第N个节点.md
0020.有效的括号.md
0024.两两交换链表中的节点.md
0027.移除元素.md
0028.实现strStr.md
0031.下一个排列.md
0034.在排序数组中查找元素的第一个和最后一个位置.md
0035.搜索插入位置.md
0037.解数独.md
0039.组合总和.md
0040.组合总和II.md
0042.接雨水.md
0045.跳跃游戏II.md
0046.全排列.md
0047.全排列II.md
0051.N皇后.md
0052.N皇后II.md
0053.最大子序和.md
0053.最大子序和(动态规划).md
0054.螺旋矩阵.md
0055.跳跃游戏.md
0056.合并区间.md
0059.螺旋矩阵II.md
0062.不同路径.md
0063.不同路径II.md
0070.爬楼梯.md
0070.爬楼梯完全背包版本.md
0072.编辑距离.md
0077.组合.md
0077.组合优化.md
0078.子集.md
0084.柱状图中最大的矩形.md
0090.子集II.md
0093.复原IP地址.md
0096.不同的二叉搜索树.md
0098.验证二叉搜索树.md
0100.相同的树.md
0101.对称二叉树.md
0102.二叉树的层序遍历.md
0104.二叉树的最大深度.md
0106.从中序与后序遍历序列构造二叉树.md
0108.将有序数组转换为二叉搜索树.md
0110.平衡二叉树.md
0111.二叉树的最小深度.md
0112.路径总和.md
0115.不同的子序列.md
0116.填充每个节点的下一个右侧节点指针.md
0121.买卖股票的最佳时机.md
0122.买卖股票的最佳时机II.md
0122.买卖股票的最佳时机II(动态规划).md
0123.买卖股票的最佳时机III.md
0127.单词接龙.md
0129.求根到叶子节点数字之和.md
0131.分割回文串.md
0132.分割回文串II.md
0134.加油站.md
0135.分发糖果.md
0139.单词拆分.md
0141.环形链表.md
0142.环形链表II.md
0143.重排链表.md
0150.逆波兰表达式求值.md
0151.翻转字符串里的单词.md
0160.相交链表.md
0188.买卖股票的最佳时机IV.md
0189.旋转数组.md
0198.打家劫舍.md
0202.快乐数.md
0203.移除链表元素.md
0205.同构字符串.md
0206.翻转链表.md
0209.长度最小的子数组.md
0213.打家劫舍II.md
0216.组合总和III.md
0222.完全二叉树的节点个数.md
0225.用队列实现栈.md
0226.翻转二叉树.md
0232.用栈实现队列.md
0234.回文链表.md
0235.二叉搜索树的最近公共祖先.md
0236.二叉树的最近公共祖先.md
0239.滑动窗口最大值.md
0242.有效的字母异位词.md
0257.二叉树的所有路径.md
0279.完全平方数.md
0283.移动零.md
0300.最长上升子序列.md
0309.最佳买卖股票时机含冷冻期.md
0322.零钱兑换.md
0332.重新安排行程.md
0337.打家劫舍III.md
0343.整数拆分.md
0344.反转字符串.md
0347.前K个高频元素.md
0349.两个数组的交集.md
0376.摆动序列.md
0377.组合总和Ⅳ.md
0383.赎金信.md
0392.判断子序列.md
0404.左叶子之和.md
0406.根据身高重建队列.md
0416.分割等和子集.md
0435.无重叠区间.md
0450.删除二叉搜索树中的节点.md
0452.用最少数量的箭引爆气球.md
0454.四数相加II.md
0455.分发饼干.md
0459.重复的子字符串.md
0463.岛屿的周长.md
0474.一和零.md
0491.递增子序列.md
0494.目标和.md
0496.下一个更大元素I.md
0501.二叉搜索树中的众数.md
0503.下一个更大元素II.md
0509.斐波那契数.md
0513.找树左下角的值.md
0516.最长回文子序列.md
0518.零钱兑换II.md
0530.二叉搜索树的最小绝对差.md
0538.把二叉搜索树转换为累加树.md
0541.反转字符串II.md
0583.两个字符串的删除操作.md
0617.合并二叉树.md
0647.回文子串.md
0649.Dota2参议院.md
0654.最大二叉树.md
0657.机器人能否返回原点.md
0669.修剪二叉搜索树.md
0673.最长递增子序列的个数.md
0674.最长连续递增序列.md
0684.冗余连接.md
0685.冗余连接II.md
0700.二叉搜索树中的搜索.md
0701.二叉搜索树中的插入操作.md
0704.二分查找.md
0707.设计链表.md
0714.买卖股票的最佳时机含手续费.md
0714.买卖股票的最佳时机含手续费(动态规划).md
0718.最长重复子数组.md
0724.寻找数组的中心索引.md
0738.单调递增的数字.md
0739.每日温度.md
0746.使用最小花费爬楼梯.md
0763.划分字母区间.md
0841.钥匙和房间.md
0844.比较含退格的字符串.md
0860.柠檬水找零.md
0922.按奇偶排序数组II.md
0925.长按键入.md
0941.有效的山脉数组.md
0968.监控二叉树.md
0977.有序数组的平方.md
1002.查找常用字符.md
1005.K次取反后最大化的数组和.md
1035.不相交的线.md
1047.删除字符串中的所有相邻重复项.md
1049.最后一块石头的重量II.md
1143.最长公共子序列.md
1207.独一无二的出现次数.md
1221.分割平衡字符串.md
1356.根据数字二进制下1的数目排序.md
1365.有多少小于当前数字的数字.md
1382.将二叉搜索树变平衡.md
O(n)的算法居然超时了,此时的n究竟是多大?.md
为了绝杀编辑距离,卡尔做了三步铺垫.md
二叉树中递归带着回溯.md
二叉树总结篇.md
二叉树理论基础.md
二叉树的统一迭代法.md
二叉树的迭代遍历.md
二叉树的递归遍历.md
关于时间复杂度,你不知道的都在这里!.md
剑指Offer05.替换空格.md
剑指Offer58-II.左旋转字符串.md
动态规划-股票问题总结篇.md
动态规划总结篇.md
动态规划理论基础.md
双指针总结.md
哈希表总结.md
哈希表理论基础.md
回溯总结.md
回溯算法去重问题的另一种写法.md
回溯算法理论基础.md
字符串总结.md
数组总结篇.md
数组理论基础.md
栈与队列总结.md
栈与队列理论基础.md
根据身高重建队列(vector原理讲解).md
算法模板.md
背包总结篇.md
背包理论基础01背包-1.md
背包理论基础01背包-2.md
背包问题理论基础多重背包.md
背包问题理论基础完全背包.md
贪心算法总结篇.md
贪心算法理论基础.md
链表总结篇.md
链表理论基础.md
面试题02.07.链表相交.md
README.md
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
0376.摆动序列.md 12.80 KB
一键复制 编辑 原始数据 按行查看 历史

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

本周讲解了贪心理论基础,以及第一道贪心的题目:贪心算法:分发饼干,可能会给大家一种贪心算法比较简单的错觉,好了,接下来几天的题目难度要上来了,哈哈。

376. 摆动序列

力扣题目链接

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [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

思路1(贪心解法)

本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢?

来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢?

用示例二来举例,如图所示:

376.摆动序列

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

局部最优推出全局最优,并举不出反例,那么试试贪心!

(为方便表述,以下说的峰值都是指局部峰值)

实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

本题代码实现中,还有一些技巧,例如统计峰值的时候,数组最左面和最右面是最不好统计的。

例如序列[2,5],它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。

所以可以针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即preDiff = 0,如图:

376.摆动序列1

针对以上情形,result初始为1(默认最右面有一个峰值),此时curDiff > 0 && preDiff <= 0,那么result++(计算了左面的峰值),最后得到的result就是2(峰值个数为2即摆动序列长度为2)

C++代码如下(和上图是对应的逻辑):

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

思路2(动态规划)

考虑用动态规划的思想来解决这个问题。

很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即nums[i] > nums[i-1]),要么是作为山谷(即nums[i] < nums[i - 1])。

  • 设dp状态dp[i][0],表示考虑前i个数,第i个数作为山峰的摆动子序列的最长长度
  • 设dp状态dp[i][1],表示考虑前i个数,第i个数作为山谷的摆动子序列的最长长度

则转移方程为:

  • dp[i][0] = max(dp[i][0], dp[j][1] + 1),其中0 < j < inums[j] < nums[i],表示将nums[i]接到前面某个山谷后面,作为山峰。
  • dp[i][1] = max(dp[i][1], dp[j][0] + 1),其中0 < j < inums[j] > nums[i],表示将nums[i]接到前面某个山峰后面,作为山谷。

初始状态:

由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1

C++代码如下:

class Solution {
public:
    int dp[1005][2];
    int wiggleMaxLength(vector<int>& nums) {
        memset(dp, 0, sizeof dp);
        dp[0][0] = dp[0][1] = 1;

        for (int i = 1; i < nums.size(); ++i)
        {
            dp[i][0] = dp[i][1] = 1;

            for (int j = 0; j < i; ++j)
            {
                if (nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1);
            }

            for (int j = 0; j < i; ++j)
            {
                if (nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1);
            }
        }
        return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
    }
};
  • 时间复杂度:O(n2)
  • 空间复杂度:O(n)

进阶

可以用两棵线段树来维护区间的最大值

  • 每次更新dp[i][0],则在tree1nums[i]位置值更新为dp[i][0]
  • 每次更新dp[i][1],则在tree2nums[i]位置值更新为dp[i][1]
  • 则dp转移方程中就没有必要j从0遍历到i-1,可以直接在线段树中查询指定区间的值即可。

时间复杂度:O(nlogn)

空间复杂度:O(n)

总结

贪心的题目说简单有的时候就是常识,说难就难在都不知道该怎么用贪心

本题大家如果要去模拟删除元素达到最长摆动子序列的过程,那指定绕里面去了,一时半会拔不出来。

而这道题目有什么技巧说一下子能想到贪心么?

其实也没有,类似的题目做过了就会想到。

此时大家就应该了解了:保持区间波动,只需要把单调区间上的元素移除就可以了。

其他语言版本

Java

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length <= 1) {
            return nums.length;
        }
        //当前差值
        int curDiff = 0;
        //上一个差值
        int preDiff = 0;
        int count = 1;
        for (int i = 1; i < nums.length; i++) {
            //得到当前差值
            curDiff = nums[i] - nums[i - 1];
            //如果当前差值和上一个差值为一正一负
            //等于0的情况表示初始时的preDiff
            if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
                count++;
                preDiff = curDiff;
            }
        }
        return count;
    }
}
// DP
class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 0 i 作为波峰的最大长度
        // 1 i 作为波谷的最大长度
        int dp[][] = new int[nums.length][2];

        dp[0][0] = dp[0][1] = 1;
        for (int i = 1; i < nums.length; i++){
            //i 自己可以成为波峰或者波谷
            dp[i][0] = dp[i][1] = 1;

            for (int j = 0; j < i; j++){
                if (nums[j] > nums[i]){
                    // i 是波谷
                    dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
                }
                if (nums[j] < nums[i]){
                    // i 是波峰
                    dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
                }
            }
        }

        return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
    }
}

Python

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        preC,curC,res = 0,0,1  #题目里nums长度大于等于1,当长度为1时,其实到不了for循环里去,所以不用考虑nums长度
        for i in range(len(nums) - 1):
            curC = nums[i + 1] - nums[i]
            if curC * preC <= 0 and curC !=0:  #差值为0时,不算摆动
                res += 1
                preC = curC  #如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值
        return res

Go

func wiggleMaxLength(nums []int) int {
    var count,preDiff,curDiff  int
    count=1
    if len(nums)<2{
        return count
    }
    for i:=0;i<len(nums)-1;i++{
        curDiff=nums[i+1]-nums[i]
        //如果有正有负则更新下标值||或者只有前一个元素为0(针对两个不等元素的序列也视作摆动序列,且摆动长度为2)
        if (curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0){
            preDiff=curDiff
            count++
        }
    }
    return count
}

Javascript

贪心

var wiggleMaxLength = function(nums) {
    if(nums.length <= 1) return nums.length
    let result = 1
    let preDiff = 0
    let curDiff = 0
    for(let i = 0; i < nums.length - 1; i++) {
        curDiff = nums[i + 1] - nums[i]
        if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
            result++
            preDiff = curDiff
        }
    }
    return result
};

动态规划

var wiggleMaxLength = function(nums) {
    if (nums.length === 1) return 1; 
    // 考虑前i个数,当第i个值作为峰谷时的情况(则第i-1是峰顶)
    let down = 1;
    // 考虑前i个数,当第i个值作为峰顶时的情况(则第i-1是峰谷)
    let up = 1;
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] < nums[i - 1]) {
            down = Math.max(up + 1, down);
        }
        if (nums[i] > nums[i - 1]) {
            up = Math.max(down + 1, up)
        }
    }
    return Math.max(down, up);
};

C

贪心

int wiggleMaxLength(int* nums, int numsSize){
    if(numsSize <= 1) 
        return numsSize;

    int length = 1;
    int preDiff , curDiff;
    preDiff = curDiff = 0;
    for(int i = 0; i < numsSize - 1; ++i) {
        // 计算当前i元素与i+1元素差值
        curDiff = nums[i+1] - nums[i];

        // 若preDiff与curDiff符号不符,则子序列长度+1。更新preDiff的符号
        // 若preDiff与curDiff符号一致,当前i元素为连续升序/连续降序子序列的中间元素。不被记录入长度
        // 注:当preDiff为0时,curDiff为正或为负都属于符号不同
        if((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
            preDiff = curDiff;
            length++;
        }
    }

    return length;
}

TypeScript

贪心

function wiggleMaxLength(nums: number[]): number {
    let length: number = nums.length;
    if (length <= 1) return length;
    let preDiff: number = 0;
    let curDiff: number = 0;
    let count: number = 1;
    for (let i = 1; i < length; i++) {
        curDiff = nums[i] - nums[i - 1];
        if (
            (preDiff <= 0 && curDiff > 0) ||
            (preDiff >= 0 && curDiff < 0)
        ) {
            preDiff = curDiff;
            count++;
        }
    }
    return count;
};

动态规划

function wiggleMaxLength(nums: number[]): number {
    const length: number = nums.length;
    if (length <= 1) return length;
    const dp: number[][] = new Array(length).fill(0).map(_ => []);
    dp[0][0] = 1;   // 第一个数作为波峰
    dp[0][1] = 1;   // 第一个数作为波谷
    for (let i = 1; i < length; i++) {
        dp[i][0] = 1;
        dp[i][1] = 1;
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
        }
        for (let j = 0; j < i; j++) {
            if (nums[j] > nums[i]) dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
        }
    }
    return Math.max(dp[length - 1][0], dp[length - 1][1]);
};

Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/nybaba/leetcode-master.git
git@gitee.com:nybaba/leetcode-master.git
nybaba
leetcode-master
代码随想录
master

搜索帮助