LeetCode_376: 摆动序列

目录

链接

问题

题解

题解1: 暴力递归

题解2: 动态规划

题解3: 动态规划优化

题解4: 空间复杂度优化 (动态规划最优解)

题解5: 贪心


链接

LeetCode_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: 暴力递归

思路: 

去找所有可能摆动子序列的长度并找到它们中的最大值。为了实现这样的算法,我们需要一个回溯函数, calculate(nums,index,isUp) 将 nums 作为输入数组,textindex 记录的是我们从哪个位置开始找最长摆动序列, boolean 变量 isUp 记录的是现在要找的是上升元素还是下降元素。如果函数 calculate 在一个上升摆动之后被调用,我们需要用这个相同的函数找到下降的元素。如果calculate 在一个下降元素之后被调用,那么我们需要用这个函数找到下一个上升的元素

/**
 * 暴力递归
 * @param nums
 * @return
 */
public int wiggleMaxLength_Recursion(int[] nums) {
    if (nums.length < 2)
        return nums.length;
    return 1 + Math.max(calculate(nums, 0, true), calculate(nums, 0, false));
}

private int calculate(int[] nums, int index, boolean isUp) {
    int maxcount = 0;
    for (int i = index + 1; i < nums.length; i++) {
        if ((isUp && nums[i] > nums[index]) || (!isUp && nums[i] < nums[index]))
            maxcount = Math.max(maxcount, 1 + calculate(nums, i, !isUp));
    }
    return maxcount;
}

复杂度分析:
      时间复杂度: O(n!),  calculate()会被调用n!次
      空间复杂度: O(n),   回溯深度为n 

题解2: 动态规划

思路:

为了更好地理解这一方法,用两个数组来 dp ,分别记作 up 和 down 

每当选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小

up[i] 存的是目前为止最长的以第 i 个元素结尾的上升摆动序列的长度

类似的,down[i] 记录的是目前为止最长的以第 i 个元素结尾的下降摆动序列的长度

我们每当找到将第 i 个元素作为上升摆动序列的尾部的时候就更新 up[i] 。现在我们考虑如何更新 up[i] ,我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j] ,满足 j<i 且 nums[i]>nums[j] 。类似的,down[i] 也会被更新

/**
 * 动态规划
 */
public static int wiggleMaxLength(int[] nums) {
    if (nums.length < 2)
        return nums.length;
    //以上升元素结尾(nums[i] > nums[i-1)的最长摆动序列
    int[] up = new int[nums.length];
    //以下降元素结尾(nums[i] < nums[i-1)的最长摆动序列
    int[] down = new int[nums.length];
    //计算nums数组中前i个元素的最长摆动序列
    for (int i = 1; i < nums.length; i++) {
        //遍历前i个元素, 计算摆动序列
        for(int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                /**
                 * 当第i个元素为上升元素时的最长摆动序列
                 * 如果第i个元素为上升元素时, 那么需要找到前一个下降元素为j时的最长摆动序列
                 */
                up[i] = Math.max(up[i],down[j] + 1);
            } else if (nums[i] < nums[j]) {
                /**
                 * 当第i个元素为下降元素时的最长摆动序列
                 *  如果第i个元素为下降元素时, 那么需要找到前一个上升元素为j时的最长摆动序列
                 */
                down[i] = Math.max(down[i],up[j] + 1);
            }
        }
    }
    return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
}

复杂度分析:
       时间复杂度: O(n^2), O循环内嵌套了一个循环
       空间复杂度: O(n),  dp需要两个同样长度的数组

题解3: 动态规划优化

思路:

数组中的任何元素都对应下面三种可能状态中的一种: 

  1. 上升的位置,意味着 nums[i] > nums[i - 1]
  2. 下降的位置,意味着 nums[i] < nums[i - 1]
  3. 相同的位置,意味着 nums[i] == nums[i - 1]

更新的过程如下:

  • 如果 nums[i] > nums[i-1], 意味着这里在摆动上升, 前一个数字肯定处于下降的位置。所以 up[i] = down[i-1] + 1, down[i] 与 down[i-1] 保持相同
  • 如果 nums[i] < nums[i-1], 意味着这里在摆动下降, 前一个数字肯定处于下降的位置。所以down[i] = up[i-1] + 1, up[i] 与 up[i-1] 保持不变
  • 如果nums[i]==nums[i-1],意味着这个元素不会改变任何东西因为它没有摆动。所以down[i] 与 up[i] 和 down[i-1] 与 up[i-1] 都分别保持不变

最后, 可以将 up[length-1] 和 down[length-1]中的较大值作为问题的答案, 其中 length是给定数组中的元素数目

/**
 * 线性动态规划 -- 时间复杂度优化
 *
 * @param nums
 * @return
 */
public int wiggleMaxLength_Better(int[] nums) {
    if (nums.length < 2)
        return nums.length;
    int[] up = new int[nums.length];
    int[] down = new int[nums.length];
    up[0] = down[0] = 1;
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] > nums[i - 1]) {
            /**
             * 如果第i个元素大于i-1个元素, 那么需要更新up[i]
             * 注意: 每次更新up[i]使用的是 down[i-1] + 1, 与up[i-1]无关
             */
            up[i] = down[i - 1] + 1;
            down[i] = down[i - 1];
        } else if (nums[i] < nums[i - 1]) {
            /**
             * 如果第i个元素小于i-1个元素, 那么需要更新down[i]
             * 注意: 每次更新down[i]使用的是 up[i-1] + 1, 与down[i-1]无关
             */
            down[i] = up[i - 1] + 1;
            up[i] = up[i - 1];
        } else {
            /**
             * 如果第i个元素等于i-1个元素
             */
            down[i] = down[i - 1];
            up[i] = up[i - 1];
        }
    }
    return Math.max(down[nums.length - 1], up[nums.length - 1]);
}

复杂度分析:
       时间复杂度: O(n), 只需要遍历数组一遍
       空间复杂度: O(n), dp需要两个相同长度的数组

题解4: 空间复杂度优化 (动态规划最优解)

思路: 

在题解3中, dp数组更新过程中更新up[i] 和down[i] , 其实只需要up[i?1] 和down[i?1] 。因此,我们可以通过只记录最后一个元素的值而不使用数组来节省空间

/**
 * 线性动态规划优化 --空间复杂度优化
 *
 * @param nums
 * @return
 */
public int wiggleMaxLength_Best(int[] nums) {
    if (nums.length < 2)
        return nums.length;
    int down = 1, up = 1;
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] > nums[i - 1]){
            up = down + 1;
        }else if (nums[i] < nums[i - 1]){
            down = up + 1;
        }
    }
    return Math.max(down, up);
}

复杂度分析:
       时间复杂度: O(n), 仅遍历数组一次
       空间复杂度: O(1), 只使用了常数级别的额外空间

题解5: 贪心

其实不需要 dp 来解决这个问题。这个问题等价于找数组中交替的最大最小值。因此,如果我们选择中间的数字作为摆动序列的一部分,只会导致序列小于等于只选连续的最大最小元素

下图可以更好地说明此方法:

从上图中,我们可以看到,如果我们选择 C 而不是 D 作为摆动序列的第二个点,我们无法将点 E 也包括进最终序列。因此,我们无法得到最长摆动序列。
因此,为了解决这个问题,维护一个变量 prevdiff ,它的作用是只是当前数字的序列是上升还是下降的。如果prevdiff > 0, 那么表示目前是上升序列,需要找一个下降的元素。因此,更新已找到的序列长度diff(nums[i] - nums[i-1])为负数。类似的,如果 prevdiff < 0, 我们就更新diff(nums[i] - nums[i-1]) 为正数。
当整个数组都被遍历后,我们得到了最终的结果,它就是最长摆动子序列的长度。

/**
 * 贪心
 * @param nums
 * @return
 */
public int wiggleMaxLength_Greedy(int[] nums) {
    if (nums.length < 2)
        return nums.length;
    int prevdiff = nums[1] - nums[0];
    int count = prevdiff != 0 ? 2 : 1;
    for (int i = 2; i < nums.length; i++) {
        int diff = nums[i] - nums[i - 1];
        if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
            count++;
            prevdiff = diff;
        }
    }
    return count;
}

复杂度分析:
       时间复杂度: O(n), 我们需要遍历数组一次
       空间复杂度: O(1), 不需要使用额外的空间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值