121. Best Time to Buy and Sell Stock

121. Best Time to Buy and Sell Stock

這個題目算是面試的高頻題目,因為一個題目可以不斷的延伸,而且根據不同的條件,在延伸的過程中不需要更改太多的答案。所以這個題目的解法雖然簡單,但是熟悉各個解題的方法更是重要。

這個題目是給定一個歷史股價,且如果只能進行一次買賣,最大的獲利為何?

既然是要找最大獲利,即是股價的最低點與最高點之間的差,剛開始在寫刷題時,我就很天真的想那就找到歷史股價中的最大值與最小值,最大獲利就是兩個一相減就好,這就落入了一個很簡單的小陷阱,那就是股票的獲利一定是要先有參與市場,也就是先有了買入的動作,才會有賣出的動作(咦?我不能賣空嗎?😂)

也就是說最大獲利,不只是找到最低點的股價與最高點的股價,當買在了底點,還一定要賣得掉,當股價不斷的下跌,最大了獲利反而是不參與,整體獲利為零。

有了這個條件,其實這個題目變成了有暴力解的解法,那就是我在每個股價點,都去往回找,找出以前最小的股價點就好。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        max_profit = 0

        for i in range(len(prices)):
            for j in range(i):
                if prices[i] > prices[j]:
                    max_profit = max(max_profit, prices[i] - prices[j])

        return max_profit

以上這樣的方法邏輯上沒有錯,但是當刷題刷久了,就會有一個感知是這個題目應該存在優化的地方,畢竟這個解法的時間複雜度是 \(O(n^2)\)。並且在 Leetcode 中會遇到 TLE 。

需要優化的地方很明顯,那就是每次檢查過去歷史股價低點是一件沒有意義的事情,股價的最低點出現在某個位置後,剩餘的震盪其實都是沒有必要的,因此在尋找的過程中,應該要記錄起來,又或是說其實可以先計算出歷史的股價低點紀錄:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        
        historical_low_price = []
        historical_low_price.append(prices[0])
        for i in range(1, len(prices)):
            historical_low_price.append(min(historical_low_price[i - 1], prices[i]))
        

        max_profit = 0
        for i in range(len(prices)):
            if prices[i] > historical_low_price[i]:
                max_profit = max(max_profit, prices[i] - historical_low_price[i])

        return max_profit

如此一來,時間複雜度可以降低到 \(O(n)\),但是空間複雜度也變成了 \(O(n)\)。

面試時,就有可能會被繼續問到,空間複雜度能不能降低呢?為什麼說空間複雜度可以被降低?以這個歷史股價為例子:

[1, 2, 3, 4, 5, 6, 7]

其歷史股價低點的紀錄就會變成:

[1, 1, 1, 1, 1, 1, 1]

似乎並不需要真的記錄所有的低點,真正重要的就是當股價到 i 天時,前面的最低點的價格就好。

題目已知的時間軸中,一開始的的價格就會是一開始的低點價格,而且這個價格其實是不可能可有任何獲利的,因為沒有買進,就不會有賣出,得知了這個標準,就可以開始檢查各個歷史股價,找出最大值了。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:

        min_cost = prices[0]

        max_profit = 0
        for i in range(1, len(prices)):
            if prices[i] > min_cost:
                max_profit = max(max_profit, prices[i] - min_cost)
            min_cost = min(min_cost, prices[i])

        return max_profit

如同本文開頭所說,這個題目的做法很多且延伸也很多,當上面的做法都知道後,就可以展開其他的想法。

雖然說這個題目一定是刷題會刷到的題目,但是如果刷題刷多了,這個題目其實很像是給定一個陣列,透過不同的選擇,找出題目的最佳解,這個題目其實很符合動態規劃題目的模式,這個題目其實存在著很多比較直覺的解法,動態規劃其實是滿難想的。

TODO: 動態轉移方程

class Solution:
    def maxProfit(self, prices: List[int]) -> int:

        @cache
        def dfs(i, hold, remaining):
            if remaining == 0:
                return 0
            if i == len(prices):
                return 0

            do_nothing = dfs(i + 1, hold, remaining)

            do_something = 0

            if hold:
                do_something = prices[i] + dfs(i + 1, False, remaining - 1)
            else:
                do_something = -prices[i] + dfs(i + 1, True, remaining)

            return max(do_nothing, do_something)

        return dfs(0, False, 1)

相關題目