121.买卖股票的最佳时机

目标

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0。

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

说明:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

思路

虽然这是一道简单题,但是我也想了一个多小时。算法实现起来简单,但是算法的设计,如何得到满足条件的结果也不是那么直观。

说回这道题,要求得最大利润,我们都明白要低买高卖。难道这道题就是让求数组的最大值与最小值?显然不是。我们的目标是求取一个买入点,再其之后的某天卖出获得的利润最大。只能操作一次,可以看成是长线交易(后面还有一道可以多次操作的题)。

暴力解法是遍历股票价格数组,然后向后循环,求得最大利润。时间复杂度是O(n!),不可行。

思考下面的问题:

取得最大利润是否必须在最低点买入?不是,比如[2,8,1,6]

取得最大利润是否必须在最高点卖出?不是,比如[3,8,1,7]

但是,如果遇到了更低的价格,那么它一定是当前最佳的买入点。因为不管后续价格怎么波动,想要获得最大收益,被减数当然越小越好。

因此,问题可以转化为:任取一个买入点,向后遍历并记录最大利润,直到出现一个更低的买入点,在这之前的一段时间内,我们所选的就是最佳买入点,可以记录这段时间内的最大利润。然后以新的最佳买入点继续向后遍历并记录最大利润,直到一个新的最佳买入点出现。在这一过程中如果最大利润比之前的值更大就替换掉。这样我们就得到了问题的答案。

其实,一开始我并没有这么清晰的思路。只是跟着感觉走:

  1. 首先,如果遍历时发现股票价格是递减的,那么我们肯定不能买入。这里,我们需要计算后一天减去前一天的收益,如果连着都是负数,那么直接跳过。
  2. 然后,如果找到了第一个正收益的点,那么我们买入。之后,我们开始累加收益,这样累加得到的是当天到买入点的收益。即 如果a是我们的买入点,那么往后利润的累加和 profitSum(b - a) + (c - b) + (d - c) + (e - d) = e - a。在这一过程中,我们记录最大收益 profitMax
  3. 接下来,我们需要找到更低的买入点。如果我们的买入点是i,当我们遍历到第j天的时候,profitSum = prices[j] - prices[i],而当天与前一天的利润为 profit = prices[j] - prices[j-1],如果profit > profitSum,那么prices[j] - prices[j-1] > prices[j] - prices[i]prices[j-1] < prices[i]j-1 就是新的最佳买入点。这时我们需要将利润和重置,然后再比较最大利润。

最开始写的时候忽略了查找最新买入点的这个步骤,增加的这个判断 profit > profitSum 是根据错误案例调试出来的,感觉就是如果一天的收益就比之前的历史收益大了,那就没必要再累加前面的利润了,应该重新开始。

代码

/**
 * @date 2024-03-03 1:44
 */
public class MaxProfit {

    public int maxProfit(int[] prices) {
        int profitSum = 0;
        int profitMax = 0;
        for (int i = 1; i < prices.length; i++) {
            int profit = prices[i] - prices[i - 1];
            if (profitSum <= 0 && profit <= 0) {
                continue;
            }
            profitSum += profit;
            if (profit > profitSum) {
                //  最开始的时候没写这个判断
                profitSum = profit;
            }
            if (profitSum > profitMax) {
                profitMax = profitSum;
            }

        }
        return profitMax;
    }

    public static void main(String[] args) {
        MaxProfit main = new MaxProfit();
        System.out.println(main.maxProfit(new int[]{3, 3, 5, 0, 0, 3, 1, 4}));
    }

}

理清楚之后发现许多操作是没必要的,比如累加每天的利润,实际就是当天价格减去买入点价格。

    public int maxProfit(int[] prices) {
        int buyPoint = 0;
        int profitMax = 0;
        for (int i = 1; i < prices.length; i++) {
            int profit = prices[i] - prices[buyPoint];
            if (profit <= 0) {
                buyPoint = i;
            } else if (profit > profitMax) {
                profitMax = profit;
            }

        }
        return profitMax;
    }

性能

优化后

169.多数元素

目标

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3
示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2

说明:

  • n == nums.length
  • 1 <= n <= 5 * 104
  • -109 <= nums[i] <= 109

进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

思路

今天做个简单题吧。要求时间复杂度是O(n),那么就不能嵌套循环,空间复杂度度是O(1),也就不能开辟新数组。很自然的可以想到:累加当前出现次数 count 最多的元素 res,如果遇到其它元素则减1,当 count 为负数时,说明当前出现次数最多的元素 可能 发生改变,将res替换为当前元素,并将count置1。

这里说 可能,是因为只要遇到与所选元素值不相等的, count 就减1。我们并不清楚这些其它元素值是否都相同,只能够推出当初所选的多数被其它少数反超了。但是从总体来考虑,如果我们所选择的真是多数元素,那么它一定会在后面再次反超。

官网介绍了一种投票算法 Boyer-Moore,应该也是这种思路吧。

官网还给出了一种分治算法,主要思想是:如果将数组分成两部分,那么数组的众数至少是一部分的众数。递归求解,然后回溯合并,确定子数组众数。不过时间复杂度O(nlog⁡n),参考算法导论P53主定理。空间复杂度:O(log⁡n),递归用到的栈空间。

代码

/**
 * @date 2024-03-02 22:24
 */
public class MajorityElement {
    public int majorityElement(int[] nums) {
        int res = nums[0];
        int count = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] != res) {
                count--;
                // 出错点:反超时应当将值设为1,参考错误用例[10,9,9,9,10]
                if (count < 0) {
                    res = nums[i];
                    count = 1;
                }
            } else {
                count++;
            }
            // 本以为加上可以提高性能,谁知道还慢了2ms
            // if (count > Math.floor(nums.length / 2.0)) {
            //     break;
            // }
        }
        return res;
    }

    public static void main(String[] args) {
        MajorityElement main = new MajorityElement();
        System.out.println(main.majorityElement(new int[]{10, 9, 9, 9, 10}));
    }
}

性能

本以为判断条件可以提高效率,谁知道还慢了2ms,耗时增加了2倍,因为每次算出答案基本上也都循环完了。