1. 程式人生 > 其它 >資料結構與演算法之單調棧

資料結構與演算法之單調棧

技術標籤:C++/C資料結構演算法演算法資料結構單調棧leetcode

資料結構與演算法之單調棧

原文來自個人部落格(求訪問/關注/收藏): https://bbing.com.cn/ CSDN個人部落格不定期轉載

單調棧

顧名思義, 單調棧就是其元素單調的棧, 滿足兩個特性:

  • 是棧
  • 棧元素單調遞減(<)或者單調遞增(>)

當然, 關於第二點也可以是單調不遞減(>=)或者單調不遞增(<=).

構造一個單調棧

從實踐出發, 看看怎麼構建一個單調棧;

比如有一個正整數列表: [2 1 3 4 7 5]

構建其中之一的單調不遞減棧:

  1. 列表是否空? 如果空則轉2, 否則轉3;
  2. 退出;
  3. 從列表中取出元素, 轉4;
  4. 棧是否空? 如果空則轉5, 否則轉6;
  5. 元素入棧, 轉1;
  6. 新元素與棧頂元素比較, 如果滿足大小關係, 入棧, 轉1; 否則, 出棧, 轉4;

按照上述演算法, 構建單調棧:

  • 棧空, 則元素入棧(1->3->4->5)
2
  • 新元素比棧頂元素小(1->3->4->6->4->5)
1
  • 新元素比棧頂元素大, 入棧(1->3->4->6)
1 3
  • 新元素比棧頂元素大, 入棧(1->3->4->6)
1 3 4
  • 新元素比棧頂元素大, 入棧(1->3->4->6)
1 3 4 7
  • 新元素比棧頂元素小, 則彈出棧頂元素, 直到新元素比棧頂元素小或者棧空(1->3->4->6->4->5->2)
1 3 4 5

以上, 已經有幾分像是一個狀態機, 入棧出棧都是狀態的變化.

單調棧

上圖是將單調棧轉換成狀態操作, 藍色字體表示狀態, 黑色字體表示操作, 實線表示狀態轉換, 虛線表示附加操作.

下面, 我們不妨嘗試使用狀態機實現從一個列表生成單調棧的函式.

單調棧的程式碼實現

首先, 我們定義幾個狀態, 根據上述的流程圖, 有以下幾個狀態:

開始/下一個/棧空/棧不空/滿足大小/不滿足大小/列表空/列表不空/退出

enum class STAT
{
    START,
    NEXT,
    SEMPTY_T,
    SEMPTY_F,
    OP_T,
    OP_F,
    LEMPTY_T,
    LEMPTY_F,
    EXIT
};

與狀態對應的, 還有一些操作:

判斷棧是否空/比較操作/判斷佇列是否空/從佇列中取下一個元素/入棧/出棧

auto sempty = [&]() -> bool {
    return s.empty();
};
auto op = [&](const int &a) -> bool {
    return fop(s.top(), a);
};
int lpos = 0;
auto lempty = [&]() -> bool {
    return lpos >= list.size();
};
auto lnext = [&]() -> int {
    return list[lpos++];
};
auto push = [&](const int &a) {
    s.push(a);
};
auto pop = [&]() {
    s.pop();
};

最後就是狀態之間的跳轉規則, 直接按照流程編寫跳轉規則就好

do
{
    switch (stat)
    {
    case STAT::START:
        stat = lempty() ? STAT::LEMPTY_T : STAT::LEMPTY_F;
        break;
    case STAT::NEXT:
        a = lnext();
        stat = sempty() ? STAT::SEMPTY_T : STAT::SEMPTY_F;
        break;
    case STAT::SEMPTY_T:
        push(a);
        stat = STAT::START;
        break;
    case STAT::SEMPTY_F:
        stat = op(a) ? STAT::OP_T : STAT::OP_F;
        break;
    case STAT::OP_T:
        push(a);
        stat = STAT::START;
        break;
    case STAT::OP_F:
        pop();
        stat = sempty() ? STAT::SEMPTY_T : STAT::SEMPTY_F;
        break;
    case STAT::LEMPTY_T:
        stat = STAT::EXIT;
        break;
    case STAT::LEMPTY_F:
        stat = STAT::NEXT;
        break;
    }
} while (STAT::EXIT != stat);

下面是完整的程式碼:

#include <stack>
#include <vector>
#include <iostream>
#include <string>

using namespace std;

enum class STAT
{
    START,
    NEXT,
    SEMPTY_T,
    SEMPTY_F,
    OP_T,
    OP_F,
    LEMPTY_T,
    LEMPTY_F,
    EXIT
};

template <typename FOP>
void mStack(stack<int> &s, const vector<int> &list, FOP &&fop)
{
    auto sempty = [&]() -> bool {
        return s.empty();
    };
    auto op = [&](const int &a) -> bool {
        return fop(s.top(), a);
    };
    int lpos = 0;
    auto lempty = [&]() -> bool {
        return lpos >= list.size();
    };
    auto lnext = [&]() -> int {
        return list[lpos++];
    };
    auto push = [&](const int &a) {
        s.push(a);
    };
    auto pop = [&]() {
        s.pop();
    };

    STAT stat = STAT::START;
    int a = -1;
    do
    {
        switch (stat)
        {
        case STAT::START:
            stat = lempty() ? STAT::LEMPTY_T : STAT::LEMPTY_F;
            break;
        case STAT::NEXT:
            a = lnext();
            stat = sempty() ? STAT::SEMPTY_T : STAT::SEMPTY_F;
            break;
        case STAT::SEMPTY_T:
            push(a);
            stat = STAT::START;
            break;
        case STAT::SEMPTY_F:
            stat = op(a) ? STAT::OP_T : STAT::OP_F;
            break;
        case STAT::OP_T:
            push(a);
            stat = STAT::START;
            break;
        case STAT::OP_F:
            pop();
            stat = sempty() ? STAT::SEMPTY_T : STAT::SEMPTY_F;
            break;
        case STAT::LEMPTY_T:
            stat = STAT::EXIT;
            break;
        case STAT::LEMPTY_F:
            stat = STAT::NEXT;
            break;
        }
    } while (STAT::EXIT != stat);
}

int main()
{
    vector<int> cs{2, 1, 3, 4, 7, 5};
    stack<int> ss;

    mStack(ss, cs, [](const int &a, const int &b) { return a < b; });

    while (!ss.empty())
    {
        cout << ss.top() << " ";
        ss.pop();
    }

    return 1;
}

聊一兩句狀態機

要寫狀態機, 需要明確有幾個狀態, 和狀態的對應操作, 也需要明確狀態間的跳轉規則;

個人認為, 狀態機的程式碼很容易維護, 只需要關注下一個狀態就行了, 需求變更的時候, 改起來非常的方便;

上面的狀態機肯定還有很多優化空間的, 但是目前還沒打算研究這個專題, 後期會專門系統地看看狀態機的編寫方法;

每日溫度

這是對應leetcode題739.

給一個每天的溫度列表, 返回一個列表, 表示至少多少天后的溫度比這一天高, 例如輸入溫度[73, 74, 75, 71, 69, 72, 76, 73], 返回[1, 1, 4, 2, 1, 1, 0, 0]。

這是比較經典的單調棧的例子, “第一個比當前大/小的元素”.

我們可以從後往前構造一個單調遞減棧, 棧中元素是溫度的下標, 用溫度大小做比較;

程式碼如下:

vector<int> dailyTemperatures(vector<int> &T)
{
    stack<int> st;
    int si = T.size();
    vector<int> rd(si);

    for (int i = si - 1; i >= 0; i--)
    {
        while (!st.empty() && T[i] >= T[st.top()])
        {
            st.pop();
        }
        if (!st.empty())
        {
            rd[i] = st.top() - i;
        }
        else
        {
            rd[i] = 0;
        }
        st.push(i);
    }

    return rd;
}