一、引言

这是一道非常有趣的题目!
这是一道非常有趣的题目!!
这是一道非常有趣的题目!!!

重要的事情先说三遍 : )

好了,接下来让我们看看这道题:

Say you have an array for which the i^th element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times). However, you may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

这道题居然没有例子?! T_T,然后还有一大段的英文描述~~~

说实话,我第一次看到这道题,拿着百度翻译看题目居然都没有看懂,然后逐字逐句拿着程序运行查看正确结果后才弄清楚了题意,现在翻译如下:

话说你现在有一个数组,这个数组呢,第 i 个元素就是给定货物第 i 天的价格信息。

请你设计一个算法能够达到最高的收益。你需要完成你想要的交易(比如,先买入货物然后再卖出货物多次)。然而,你只能在同一天交易一次(买入或者卖出),并且你在卖出货物之前,必须要先买入货物。

为了避免你们只读了题目无法理清楚头绪(我也是通过提交空代码查看 Expected Answer 才弄清楚题意的),我还是举个我自己的例子来说明下什么叫做最大的收益:

举例:

[2, 4, 1, 0, 1, 0, 3]

看看这个例子,或许有些极端(这个货物在某些天居然是 0 价格~~~),我们来看看我们如何在 7 天(给定数组的长度)内得到最大收益:

第几天 货物价格 我们的选择 收益
2 看到第二天价格升高,今天我们买入 -2
4 看到第三天价格降低,今天我们卖出 +4
1 看到第四天价格降低,今天我们什么也不做 +0
0 看到第五天价格升高,今天我们买入 -0
1 看到第六天价格降低,今天我们卖出 +1
0 看到第七天价格升高,今天我们买入 -0
3 今天是最后一天,卖出我们手上的货物 +3

这个表格非常清晰地说明了我们作为一个追求利益最大化的商人是如何处理手上的资本不断进行货物的买入卖出从而得到利益的。

仔细感受下这个过程,能不能总结出来一个所有情况适用的规律呢?总结不出来也没关系,我也不是一下子就总结出来的,让我们再认真想想:

  1. 首先,我们一直在看的,都是后面的价格,也就是说前面的价格信息对于我们的行为没有任何影响。很好,这里我们获得了第一个信息:我们在一次买入和卖出行为时,只关心后面的价格信息

  2. 然后,我们总是在最高价格的时候卖出的吗?如果是这样的话,我们为什么要在第五天进行卖出呢(第五天的价格并非最高价格)。那么我们是什么时候才进行卖出呢?仔细看看价格规律,看到了吗,我们总是在知道明天会降价的时候,我们才会当天卖出!很好,这里我们得到了一个至关重要的信息:我们总是在明天要降价的情况下卖出手上的货物

  3. 最后,那么我们在何时买入货物呢?同样的道理,我们也不是在最低价格的时候买入(与第 2 条同样的道理),我们是在刚卖出之后,得知后面会涨价的时候,我们才会进行货物的买入操作!很好,我们又获得了一个非常重要的信息:我们总是在知道明天会涨价的情况下买入货物

相信只要你仔细看到了这里,这道题的题意你一定已经弄清楚了,那么就让我们思考下,这道题到底应该怎么做呢?

二、简单的抽象:买入卖出行为背后的映射

在引言里,我通过举例阐述了题意,并且详细分析了作为一个追求利益最大化的商人在每一天的行为。

那么商人的买入卖出行为能不能抽象为我们的代码逻辑呢?

还是让我们再看向引言里我举的例子,总结下我们找到的这三条关键信息:

  1. 我们在一次买入和卖出行为时,只关心后面的价格信息

  2. 我们总是在明天要降价的情况下卖出手上的货物

  3. 我们总是在知道明天会涨价的情况下买入货物

让我们仔细看看这三条信息,“我们只有在知道明天要降价的情况下卖出” 和 “我们只有在知道明天要涨价的情况下买入”,那么如果我们将买入和卖出看作一个单元操作的话:

从买入到卖出货物的过程中,货物的价格总是呈上升趋势

也就是说,只有当我们做到了在价格变化的最低点买入货物,在价格变化的最高点卖出货物,我们才能得到最大的收益。

那么,这个规律相对于程序设计层面上的抽象是什么呢:

将价格数组分割为这样的小数组:每个小数组都是顺序增大的;在每个数组中,我们在左边的最小值进行买入,右边的最大值进行卖出

如果你还没懂的话,我这里简单画了一个图进行阐述:

这里,我们只需要将每个小数组的最右边的元素减去最左边的元素即可得到一次买入卖出的收益值。

经过我们的分析,代码其实就已经出来了:

// my solution 1 , runtime = 6 ms
class Solution1 {
public:int maxProfit(vector<int>& prices) {if (prices.size() <= 1) return 0;auto preIt = prices.begin();vector<int> split;int profit = 0;for (auto curIt = prices.begin() + 1; curIt != prices.end(); ++preIt, ++curIt)if (*preIt > *curIt) split.push_back(preIt - prices.begin());if (split.size() == 0) profit = prices[prices.size() - 1] - prices[0];else {for (int i = 0; i < split.size(); ++i) {if (i == 0) profit += prices[split[0]] - prices[0];else profit += prices[split[i]] - prices[split[i - 1] +1];}profit += prices[prices.size() - 1] - prices[split[split.size() - 1] + 1];}return profit;}
};

这段代码比较复杂,这里好好解释下:

  1. 首先,我们要明确我们要干嘛,我们需要分割数组,怎么分割呢?按照“上一个元素值大于了下一个元素的值”为标准进行分割,所以我们需要两个指针对数组进行遍历,因此,我们需要做的第一步就是判断数组是否有至少 1 个元素

  2. 然后,我们明确了数组含有至少 1 个元素,我们声明两个指针(迭代器),对参数数组 prices 进行遍历,当 preIt 指向的元素值大于了 curIt 指向的元素值,我们就将这个 preIt(也就是一个数组的最右边的分割地)记入我们的分割记录 split 中;直到我们遍历结束,我们也就拥有了数组中间(无开头和结尾)的分割记录 split 数组

  3. 最后,我们需要拿着分组信息计算我们的最大收益值;第一步,我们需要判断分组数组的大小,如果没有分组信息,那么证明价格数组就是规律排序的数组,那么只需要拿价格数组的最右边减去最左边即可;如果有分组信息,我们就需要将各个分组中的最右边减去最左边的值(其中第一个分组的最左边是价格数组的第一个元素,最后一个分组的最右边是价格数组的最后一个元素)递加;最后我们返回我们计算的收益值即可

怎么样,相信经过了我的图文并茂的解释,你应该理解了这段代码的逻辑。

如果你是自己通过分析做出来这道题的,那么从读题到抽象,从抽象到代码实现,都是一个非常有趣的过程~~~

三、更优的追求:逻辑、代码的简化

还有没有其他的方法呢?

这里我想到了一点优化的方案:上一个方法我们分割了数组,将分割信息存储到了 split 数组(split 数组中存储的是位置信息而非价格信息,不能直接用作计算)中去,最后从这个 split 数组中读取数据时处理处理下标处理时有点复杂,能不能简化些呢?

答案当然是可以的:

这里我利用两个数组,前者 buy 数组记录买入日期的价格,后者 sell 数组记录卖出日期的价格,这样我们最后只需要对这个数组进行整合计算即可,代码如下:

// my solution 2 , runtime = 13 ms
class Solution2 {
public:int maxProfit(vector<int>& prices) {if (prices.size() <= 1) return 0;vector<int> buy;vector<int> sell;int profit = 0;buy.push_back(prices[0]);for (auto pre = prices.begin(), cur = prices.begin() + 1; cur != prices.end(); ++pre, ++cur)if (*pre > *cur) {buy.push_back(*cur);sell.push_back(*pre);}sell.push_back(prices[prices.size() - 1]);if (buy.size() == 1 && sell.size() == 1) return prices[prices.size() - 1] - prices[0];for (int i = 0; i < buy.size(); ++i)profit += sell[i] - buy[i];return profit;}
};

代码逻辑和上一个方法是一样的,只是下面这种方法呢,在计算差值的时候能更加简便些。

那么还没有其他的方法呢?我实在是不想再处理数组下标了!

答案当然是有的。

这里我使用了 std::queue 实现了另一个版本的代码,思路与上述方法都是一样的,直接看代码吧:

// my soluiton 3 use std::queue , runtime = 6 ms
class Solution3 {
public:int maxProfit(vector<int>& prices) {if (prices.size() <= 1) return 0;int profit = 0;queue<int> stock;for (auto i : prices) {if (stock.empty() || stock.back() < i) {stock.push(i);}else {profit += stock.back() - stock.front();while (!stock.empty()) stock.pop();stock.push(i);}}profit += stock.back() - stock.front();return profit;}
};

顾名思义, std::queue 也就是队列,这个版本的实现方法与上述方法的思路没有什么不同,只是处理数据的数据结构更换成了队列。

为什么使用队列呢?因为对于这种只处理头跟尾数据的情况,我思考觉得使用队列是最适合的。

关于 std::queue 还是有必要说一下的,比如说这里:

while (!stock.empty()) stock.pop();

其实这里还可以简化为:

// C++11
stock = {};

那么这里为什么要这么写呢?不能直接使用 clear() 方法吗?

对的,std::queue 还真的不支持 clear() 操作,别说它,甚至 std::stack 也不支持,那么这是为什么呢?

这段解释来源于 StackOverflow 社区,感兴趣的同学可以点击这里 why std::queue doesn’t support clear() function?。

这段话比较生涩,我尽自己最大的努力翻译一下:

队列是一种适配器容器,是一种使用了特殊的密封容器作为它的底层实现的容器,提供特殊访问底层容器的一些方法。

这也意味着队列使用的是已经存在的容器,它也确实只是这个容器的实现 FIFO (先进先出)的接口类而已。

这也就解释了队列不能使用清空操作的原因。如果你需要清空一个队列,那么这意味着你实际上需要的是一个实体类而非队列,所以你应该使用底层容器来替代而非一个队列。

这里翻译的非常生涩,麻烦大家忍痛看看哈 ~~~ 大概含义也解释清楚了,这里使用 std::queue 明显使我们的问题直接简化了一个数量级,可谓是体会到了数据结构的威力。

尽管想了很久很久,能不能再优化再优化,还是黔驴技穷了 T_T ~~~

也罢,让我们看看最高票答案的简洁优雅吧!

四、简洁的美:谁叫我是最高票答案呢

既然你已经耐心的看到了这里,那么我也就大发慈悲地拿出了最高票答案来震撼震撼你的心灵 :

// perfect solution tree lines code
class Solution4 {
public:int maxProfit(vector<int> &prices) {int ret = 0;for (size_t p = 1; p < prices.size(); ++p)ret += max(prices[p] - prices[p - 1], 0);return ret;}
};

什么!只有这么几行代码?!
什么!这里的 std::max 是干什么用的?!

让我们看看作者的解释吧:

suppose the first sequence is “a <= b <= c <= d”, the profit is “d - a = (b - a) + (c - b) + (d - c)” without a doubt. And suppose another one is “a <= b >= b’ <= c <= d”, the profit is not difficult to be figured out as “(b - a) + (d - b’)”. So you just target at monotone sequences.

简单翻译下:

假定第一种顺序是这样的: a <= b <= c <= d,那么此时的收益值为:d - a = (b - a) + (c - b) + (d - c),这一点毋容置疑(这一块只要你理解了我之前的逻辑,这个式子很好理解);现在假想第二种顺序:a <= b >= b’ <= c <= d,那么现在的收益值应该不难得出为:(b - a) + (d - b)。那么你就可以得到结果了。

说实话,这个方法理解起来比较难但是我们把第二种顺序的收益值计算方式分开:

(b - a) + (d - c) == (b - a) + (c - b’) + (d - c)

看到这里如果你还不明白,就跟着代码走一遍 a <= b >= b’ <= c <= d 顺序就知道结果是怎么出来的了。

我的天!
太神奇了!
真心觉得巧妙,作者一定是一个数学天才!在这里给这段代码的作者点 999+ 个赞~~~

五、总结

这道题我做了好久好久,从一开始的分析,到之后做出了第一个方法,然后开始思考如何优化代码,之后思考出来了使用队列;再之后思考不出来更优的办法了,点开看了看最高票答案,又一次心灵受到了强烈的震撼;真是一次奇妙的体验啊。

写了三个版本的代码,又体验了一把 std::queue,自己的体验来说,收获还是很多的;最高票答案虽说非常巧妙,可是我觉得要是直接写出了最高票答案其实难免觉得有点不尽兴。因为其他没想到这个方法的人,绕了太阳系走了一大圈,最终走到了目的地,虽说不如最高票答案简洁优雅,却也是提升了不少技能点呢~~~

这篇博客也写了好久,认认真真地写清楚了自己的实现逻辑,因为自己的能力不足,在代码的编写上,逻辑的分析上,甚至部分英文的翻译上难免有些出入,请各位读者谅解。

最后的最后,献给每一个正在辛苦刷 LeetCode 的人:

To be Stonger!

LeetCode之路:122. Best Time to Buy and Sell Stock II相关推荐

  1. 【贪心 和 DP + 卖股票】LeetCode 122. Best Time to Buy and Sell Stock II

    LeetCode 122. Best Time to Buy and Sell Stock II Solution1:我的答案 贪心和DP傻傻分不清! class Solution { public: ...

  2. [LeetCode]122. Best Time to Buy and Sell Stock II

    [LeetCode]122. Best Time to Buy and Sell Stock II 题目描述 思路 I的后续 将数组分为几个小部分, 划分标准是 [i] < [i - 1](划分 ...

  3. 【Leetcode】122. Best Time to Buy and Sell Stock II买卖股票的最佳时机 II

    Best Time to Buy and Sell Stock II 买卖股票的最佳时机 II买卖股票的最佳时机 II Say you have an array for which the ith ...

  4. 【leetcode-Python】-Dynamic Programming -122. Best Time to Buy and Sell Stock II

    目录 题目链接 题目描述 示例 解题思路 Python实现 时间复杂度与空间复杂度 Python实现·优化空间复杂度 时间复杂度与空间复杂度 解题思路二·贪心算法 Python实现 时间复杂度与空间复 ...

  5. leetcode python3 简单题122. Best Time to Buy and Sell Stock II

    1.编辑器 我使用的是win10+vscode+leetcode+python3 环境配置参见我的博客: 链接 2.第一百二十二题 (1)题目 英文: Say you have an array pr ...

  6. LeetCode 122 Best Time to Buy and Sell Stock II(股票买入卖出的最佳时间 II)

    翻译 话说你有一个数组,其中第i个元素表示第i天的股票价格.设计一个算法以找到最大利润.你可以尽可能多的进行交易(例如,多次买入卖出股票).然而,你不能在同一时间来多次交易.(例如,你必须在下一次买入 ...

  7. 122 Best Time to Buy and Sell Stock II 买卖股票的最佳时机 II

    假设有一个数组,它的第 i 个元素是一个给定的股票在第 i 天的价格. 设计一个算法来找到最大的利润.你可以完成尽可能多的交易(多次买卖股票).然而,你不能同时参与多个交易(你必须在再次购买前出售股票 ...

  8. 122 Best Time to Buy and Sell Stock II

    You may complete as many transactions as you like (ie, buy one and sell one share of the stock multi ...

  9. 122. Best Time to Buy and Sell Stock II

    一.题目 1.审题 2.分析 给出一个股票的每日价格的数组,可以多次交易,求最大利润是多大. 二.解答 1.思路: 方法一. 求最大利润,从后往前看,若以现在的价格卖出,前一天的价格买入的话,即可完成 ...

最新文章

  1. 最大的ai计算机模型,Microsoft构建了世界排名前五的超级计算机,用于在Azure上训练大型AI模型...
  2. SAP MM公司间STO里的一步法转库?
  3. ha-2:read-project-properties (default) on project
  4. 从真实项目中抠出来的设计模式——第一篇:策略模式
  5. Andorid之教你全手工去除定制软件
  6. rk3288 android编译环境搭建,RK3288系统编译及环境搭建
  7. 一则 Oracle 和 SqlServer 语法区别 (原创)
  8. css12个技巧,12个CSS高级技巧汇总
  9. 西瓜书之误差逆传播公式推导、源码解读及各种易混淆概念
  10. QT图表chart-饼状图
  11. 摩斯密码Java/Python实现
  12. 软件测试难吗?0基础可以学吗?上手时间快吗?如何从零开始学习软件测试?
  13. Node 异步I/O 实现
  14. 【C++】相对路径与绝对路径以及斜杠与反斜杠的区别
  15. 美的智能家电搭载华为鸿蒙系统产品将大批量上市;蔡天乐将出任麦德龙中国总裁 | 美通企业日报...
  16. i5四核八线程怎么样_四核八线程和六核六线程cpu相比哪个好?
  17. 有关培养小孩的自制力
  18. 用Python实现不同数据源的对象匹配【实验记录】
  19. 完结篇 | 吴恩达deeplearning.ai专项课程精炼笔记全部汇总
  20. 数据降维(data dimension reduction)

热门文章

  1. 自动对焦模块理论基础及其硬件实现浅析(一)
  2. ThoughtWorks全球CEO郭晓谈软件人才的招聘与培养
  3. Java 8 的发展
  4. 【开源STM32自平衡小车】 教你如何自己DIY一辆双轮自平衡小车
  5. PostgreSQL远程数据库连接 PostgreSQL pg_hba.conf 文件简析
  6. ASP.NET-洗衣店管理系统
  7. colored manual page
  8. flask学习之4:图片验证码
  9. 海贼王热血航线正在连接服务器,航海王热血航线维护中是怎么回事 无法登录解决方法_航海王热血航线...
  10. 什么是DNA微阵列技术?