跌媽不認?一口氣團滅6道股票算法打打氣

觀感度:🌟🌟🌟🌟🌟javascript

口味:虎皮鳳爪前端

烹飪時間:10minjava

我欲清倉歸去,又恐迅速反彈,踏空不勝寒。與其儲蓄負利,不如廝混其間,少追漲,勿殺跌,夜安眠,不該有恨,獲利總在無心間。月有陰晴圓缺,股有橫盤漲跌,此事股難全。 -- 著名古人白交易git

本文已收錄在前端食堂同名倉庫 Github github.com/Geekhyt,歡迎光臨食堂,若是以爲酒菜還算可口,賞個 Star 對食堂老闆來講是莫大的鼓勵。github

2021 年的基金市場開年至今,暴漲又暴跌。剛迎完財神,期待牛氣沖天的年輕人們,剛剛入場就狠狠的吃了資本市場的一記重錘。面試

各類「人類迷惑行爲大賞」輪番上演,讓本就魔幻的世界變得更加魔幻。若是你最近也跌了,請點個贊,讓咱們互相抱團取暖。算法

回到 LeetCode 這 6 道股票系列題,其實這 6 道題目能夠歸爲一道題目來看:數組

  1. 買賣股票的最佳時機
  2. 買賣股票的最佳時機II
  3. 買賣股票的最佳時機III
  4. 買賣股票的最佳時機 IV
  5. 最佳買賣股票時機含冷凍期
  6. 買賣股票的最佳時機含手續費

與現實略有不一樣,題目中添加了一些限制條件,讀完題分析後不難發現。函數

  1. 第一題只交易一次,也就是 k = 1。
  2. 第二題不限制交易次數,也就是 k = +infinity。
  3. 第三題只交易兩次,也就是 k = 2。
  4. 第四道限制最多交易次數 k 是任意數。
  5. 第五道和第六道不限次數,至關於在第二題的基礎上分別添加了交易冷凍期手續費的額外條件。

咱們天天能作的操做無非是如下這三種:優化

  1. 買入
  2. 賣出
  3. 無操做

不過要注意如下四點限制條件。

  1. 要先買入才能賣出
  2. 題目要求不能同時參與多筆交易,也就是說再次買入前須要賣出手中持有的股票
  3. 無操做基於兩種狀態,手中持有股票沒有持有股票
  4. 交易次數 k 也有限制,只有 k >= 0 時才能夠買入

分析好了這些狀態,接下來就是翻譯成代碼了。

首先,咱們能夠創建一個三維數組來表示上面的這些狀態,先來明確一些變量含義。

  • i: 天數 範圍是 (0 <= i <= n - 1)
  • k: 最大交易次數
  • 0: 沒有持有股票
  • 1: 持有股票
  • n: 股票數組長度
dp[i][k][0]
dp[i][k][1]

// 舉個🌰
dp[2][2][1] // 今天是第 2 天,手中持有股票,最多還能夠進行 2 次交易

咱們最終要求的可得到的最大收益就是 dp[n - 1][k][0],表明最後一天將股票賣出後的最大收益。(這裏賣出必定比持有收益更大,因此是 [0],而不是 [1])

接下來,咱們嘗試列出狀態轉移方程。

// 今天手中沒有持有股票,有兩種可能:
// 1. 昨天沒有持有,今天選擇不操做。 對應: dp[i - 1][k][0]
// 2. 昨天持有,今天賣出了,因此今天沒有股票了。對應: dp[i - 1][k][1] + prices[i]
dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])


// 今天手中持有股票,有兩種可能:
// 1. 昨天手中持有股票,今天選擇不操做。對應: dp[i - 1][k][1]
// 2. 昨天沒有持有股票,今天買入了。對應: dp[i - 1][k - 1][0] - prices[i]
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])

很顯然,賣出股票利潤增長,買入股票利潤減小。由於每次交易包含兩次成對的操做,買入和賣出。

因此只有買入的時候須要將 k - 1,那麼最大利潤就是上面這兩種可能性中的最大值。

第一題 k = 1

將狀態轉移方程套入本題的條件,k = 1,列出狀態轉移方程。

dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i])
dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i])
            = Math.max(dp[i - 1][1][1], -prices[i])
// k = 0 時,dp[i - 1][0][0] = 0

觀察發現 k 既然都是 1 且不會改變,也就是說 k 對狀態轉移已經沒有影響了,咱們能夠進一步化簡方程。

dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], -prices[i])

對於第 0 天,咱們須要進行初始化:

  • 不持有:dp[0][0] = 0
  • 持有:dp[0][1] = -prices[0] (花了 prices[0] 的錢買入股票)
const maxProfit = function(prices) {
    let n = prices.length
    let dp = Array.from(new Array(n), () => new Array(2))
    dp[0][0] = 0
    dp[0][1] = -prices[0]
    for (let i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], -prices[i])
    }
    return dp[n - 1][0]
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(n)

咱們發如今轉移的時候,dp[i] 只會從 dp[i - 1] 轉移得來,所以第一維能夠去掉,空間複雜度優化到 O(1)。

const maxProfit = function(prices) {
    let n = prices.length
    let dp = Array.from(new Array(n), () => new Array(2))
    dp[0] = 0
    dp[1] = -prices[0]
    for (let i = 1; i < n; i++) {
        dp[0] = Math.max(dp[0], dp[1] + prices[i])
        dp[1] = Math.max(dp[1], -prices[i])
    }
    return dp[0]
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

咱們也能夠將變量名變得更加友好一些。

  • profit_out:賣出時的利潤
  • profit_in:買入時的利潤
const maxProfit = function(prices) {
    let n = prices.length
    let profit_out = 0
    let profit_in = -prices[0]
    for (let i = 1; i < n; i++) {
        profit_out = Math.max(profit_out, profit_in + prices[i])
        profit_in = Math.max(profit_in,  -prices[i])
    }
    return profit_out
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

第二題 k = +infinity

將狀態轉移方程套入本題的條件,k = +infinity。

dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])
            = Math.max(dp[i - 1][k][1], dp[i - 1][k][0] - prices[i])

咱們發現數組中的 k 一樣已經不會改變了,也就是說 k 對狀態轉移已經沒有影響了,能夠進一步化簡方程。

dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i])

對於第 0 天,咱們要給出初始值:

  • 不持有:dp[0][0] = 0
  • 持有:dp[0][1] = -prices[0] (花了 prices[0] 的錢買入股票)
const maxProfit = function(prices) {
    let n = prices.length
    let dp = Array.from(new Array(n), () => new Array(2))
    dp[0][0] = 0 
    dp[0][1] = -prices[0]
    for (let i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i])
    }
    return dp[n - 1][0]
}

一樣在轉移的時候,dp[i] 只會從 dp[i - 1] 轉移得來,所以第一維能夠去掉,空間複雜度優化到 O(1)。

const maxProfit = function(prices) {
    let n = prices.length
    let dp = Array.from(new Array(n), () => new Array(2))
    dp[0] = 0
    dp[1] = -prices[0]
    for (let i = 1; i < n; i++) {
        let tmp = dp[0] // 中間變量可省略,由於當天買入賣出不影響結果
        dp[0] = Math.max(dp[0], dp[1] + prices[i])
        dp[1] = Math.max(dp[1], tmp - prices[i])
    }
    return dp[0]
}

同上題同樣,咱們能夠將變量名變得更加友好一些。

const maxProfit = function(prices) {
    let n = prices.length
    let profit_out = 0
    let profit_in = -prices[0]
    for (let i = 1; i < n; i++) {
        profit_out = Math.max(profit_out, profit_in + prices[i])
        profit_in = Math.max(profit_in, profit_out - prices[i])
    }
    return profit_out
}

第三題 k = 2

前面兩種狀況,不管是 k = 1,仍是 k = +infinity 的狀況下,k 對狀態轉移方程是沒有影響的。

不過當 k = 2 時,k 就對狀態轉移方程有影響了。列出狀態轉移方程:

dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])

這個時候 k 沒法化簡,咱們須要使用兩次循環對 k 進行窮舉。

for (let i = 0; i < n; i++) {
    for (let k = maxK; k >= 1; k--) {
        dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
        dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i])
    }
}

不過由於 k 的取值範圍比較小,咱們也能夠直接將它們所有列舉出來。

dp[i][2][0] = Math.max(dp[i - 1][2][0], dp[i - 1][2][1] + prices[i])
dp[i][2][1] = Math.max(dp[i - 1][2][1], dp[i - 1][1][0] - prices[i])
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i])
dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i])
            = Math.max(dp[i - 1][1][1], -prices[i])

有了上面兩道題的鋪墊,咱們後面幾道題就直接寫出降維後的解法。

const maxProfit = function(prices) {
    let n = prices.length
    let dp_i10 = 0
    let dp_i11 = -prices[0]
    let dp_i20 = 0
    let dp_i21 = -prices[0]
    for (let i = 1; i < n; i++) {
        dp_i20 = Math.max(dp_i20, dp_i21 + prices[i])
        dp_i21 = Math.max(dp_i21, dp_i10 - prices[i])
        dp_i10 = Math.max(dp_i10, dp_i11 + prices[i])
        dp_i11 = Math.max(dp_i11, -prices[i])
    }
    return dp_i20
}

同上面同樣,咱們能夠將變量名變得更加友好一些。

const maxProfit = function(prices) {
    let profit_1_in = -prices[0], profit_1_out = 0
    let profit_2_in = -prices[0], profit_2_out = 0
    let n = prices.length
    for (let i = 1; i < n; i++) {
        profit_2_out = Math.max(profit_2_out, profit_2_in + prices[i])
        profit_2_in = Math.max(profit_2_in, profit_1_out - prices[i])
        profit_1_out = Math.max(profit_1_out, profit_1_in + prices[i])
        profit_1_in = Math.max(profit_1_in, -prices[i])
    }
    return profit_2_out
}

第四題 k 是任意數

一個有收益的交易至少須要兩天(在前一天買入,在後一天賣出,前提是買入價格低於賣出價格)。

若是股票價格數組的長度爲 n,則有收益的交易的數量最多爲 n / 2(整數除法)。所以 k 的臨界值是 n / 2。

若是給定的 k 不小於臨界值,即 k >= n / 2,則能夠將 k 擴展爲正無窮,也就是第二題的狀況,以下函數 maxProfit2。

const maxProfit = function(k, prices) {
    let n = prices.length
    const maxProfit2 = function(prices) {
        let profit_out = 0
        let profit_in = -prices[0]
        for (let i = 1; i < n; i++) {
            profit_out = Math.max(profit_out, profit_in + prices[i])
            profit_in = Math.max(profit_in, profit_out - prices[i])
        }
        return profit_out
    }
    if (k > n / 2) {
        return maxProfit2(prices)
    }
    let profit = new Array(k)
    // 初始化買入賣出時的利潤,將每次交易買入、賣出時的利潤放在一個對象中,實現降維
    for (let j = 0; j <= k; j++) {
        profit[j] = {
            profit_in: -prices[0],
            profit_out: 0
        }
    }
    for (let i = 0; i < n; i++) {
        for (let j = 1; j <= k; j++) {
            profit[j] = {
                profit_out: Math.max(profit[j].profit_out, profit[j].profit_in + prices[i]), 
                profit_in: Math.max(profit[j].profit_in, profit[j-1].profit_out - prices[i])
            }
        }
    }
    return profit[k].profit_out
}

第五題 k 爲正無窮但有冷卻時間

每次賣出以後都要等一天才能繼續交易,也就是第 i 天選擇買的時候,要從 i - 2 狀態轉移。

列出狀態轉移方程。

dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 2][k - 1][0] - prices[i])
            = Math.max(dp[i - 1][k][1], dp[i - 2][k][0] - prices[i])

k 一樣對狀態轉移已經沒有影響了,能夠進一步化簡方程。

dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i])
const maxProfit = function(prices) {
    let n = prices.length
    let dp_i0 = 0
    let dp_i1 = -prices[0];
    let dp_pre = 0 // 表明 dp[i-2][0]
    for (let i = 0; i < n; i++) {
        let tmp = dp_i0
        dp_i0 = Math.max(dp_i0, dp_i1 + prices[i])
        dp_i1 = Math.max(dp_i1, dp_pre - prices[i])
        dp_pre = tmp
    }
    return dp_i0
}

同上面同樣,咱們能夠將變量名變得更加友好一些。

const maxProfit = function(prices) {
    let n = prices.length
    let profit_in = -prices[0]
    let profit_out = 0
    let profit_freeze = 0
    for (let i = 1; i < n; i++) {
        let temp = profit_out
        profit_out = Math.max(profit_out, profit_in + prices[i])
        profit_in = Math.max(profit_in, profit_freeze - prices[i])
        profit_freeze = temp
    }
    return profit_out
}

第六題 k 爲正無窮但有手續費

在第二題的基礎上,添加了手續費。

每次交易要支付手續費,只要把手續費從利潤中減去便可,能夠列出以下兩種方程。

第一種方程:在每次買入股票時扣除手續費

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee)

第二種方程:在每次賣出股票時扣除手續費

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
const maxProfit = function(prices, fee) {
    let n = prices.length
    let dp = Array.from(new Array(n), () => new Array(2))
    dp[0] = 0
    dp[1] = -prices[0]
    for (let i = 1; i < n; i++) {
        let tmp = dp[0]
        dp[0] = Math.max(dp[0], dp[1] + prices[i] - fee)
        dp[1] = Math.max(dp[1], tmp - prices[i])
    }
    return dp[0]
}

同上面同樣,咱們能夠將變量名變得更加友好一些。

const maxProfit = function(prices, fee) {
    let profit_out = 0
    let profit_in = -prices[0]
    for (let i = 1; i < prices.length; i++) {
        profit_out = Math.max(profit_out, profit_in + prices[i] - fee)
        profit_in = Math.max(profit_in, profit_out - prices[i])
    }
    return profit_out
}

團滅完股票系列算法再來個首尾呼應,講一講所謂的投資時鐘。

經濟分爲兩個大週期:經濟復甦期經濟衰退期。結合通脹和流動性的組合,能夠分爲四個小週期,衰退前期、衰退後期、復甦前期以及復甦後期

不一樣的經濟週期對應着不一樣的資產和市場風格。任何的資產都有周期性,沒有隻漲不跌的資產,即使是茅臺這樣的核心消費資產在不合適的週期裏也能平均回調 30% 以上,即便鋼鐵這種夕陽產業在合適的週期也能漲個 50%。

搞清楚了當下位於哪一個週期,調整資產進行合理的配置,才能不作韭菜。

站在巨人的肩膀上

2021 組團刷題計劃

年初立了一個 flag,上面這個倉庫在 2021 年寫滿 100 道前端面試高頻題解,目前進度已經完成了 50%

若是你也準備刷或者正在刷 LeetCode,不妨加入前端食堂,一塊兒並肩做戰,刷個痛快。

❤️愛心三連擊

1.若是你以爲食堂酒菜還合胃口,就點個贊支持下吧,你的是我最大的動力。

2.關注公衆號前端食堂,吃好每一頓飯!

3.點贊、評論、轉發 === 催更!
image

相關文章
相關標籤/搜索