堆疊經典問題

常見

括號匹配

給定一個包含 (, ), {, }, [, ] 的字串,判斷這個字串中的括號是否匹配。若匹配則輸出 yes,否則輸出 no

匹配的意思是每個左括號都有對應的右括號,且括號之間的內容也是匹配的,就像這樣:

() 是匹配的
([]) 是匹配的
{()} 是匹配的
{[()]} 是匹配的
{[}] 不是匹配的
{[} 不是匹配的

首先我們可以思考看看最暴力的作法怎麼做,就是一直從字串左邊往右邊掃描,遇到一個合法的括號就把它移除,直到字串變成空字串,這樣就可以確定括號是匹配的。

發現中間有一個 {} 直接消除掉
({}) -> ()

消除完後發現還有一個 () 直接消除掉
() -> ""

最後發現字串是空的,括號是匹配的

這個作法的問題在於每次消除一個括號都要從頭開始掃描一次字串,還要考慮到把字串後面的內容往前搬花費的時間,非常的慢。不過既然每次都要重新掃一次、移除字串在把後面的內容往前搬動,不如就直接邊掃邊刪除吧?

作法具體來說就是,我們可以用一個堆疊來記錄目前還沒有被匹配的括號,當掃到一個右括號時,就可以從堆疊中取出一個左括號來匹配,如果堆疊是空的,或是取出的左括號和右括號不匹配,就可以直接判定這個字串不合法。

來看看這個範例,首先我們會有一個空堆疊與一個字串 [(){[]}]

alt text

接著我們從左到右掃描字串,遇到左括號就放入堆疊:

alt text

繼續掃描,我們遇到的還是左括號,所以繼續放入堆疊:

alt text

再繼續掃描,遇到右括號 ),這時候我們可以從堆疊中取出一個左括號來匹配:

alt text

匹配成功的話,就可以把左括號從堆疊中移除:

alt text

繼續掃描,遇到左括號 {,放入堆疊:

alt text

再繼續掃描,遇到左括號 [,放入堆疊:

alt text

再繼續掃描,遇到右括號 ],這時候我們可以從堆疊中取出一個左括號來匹配,配對成功,移除左括號:

alt text

再繼續掃描,遇到右括號 },這時候我們可以從堆疊中取出一個左括號來匹配,配對成功,移除左括號:

alt text

再繼續掃描,遇到右括號 ],這時候剛好把最後一個左括號取出來配對成功,移除左括號:

alt text

掃完整個字串後,堆疊是空的,這表示括號是匹配的。

剛剛作法的邏輯大致上就是這樣:

  • 如果遇到左括號,就放入堆疊。
  • 如果遇到右括號,就從堆疊中取出一個左括號來匹配,如果堆疊是空的,或是取出的左括號和右括號不匹配,就表示這個字串不合法。
  • 如果掃描完整個字串後,堆疊是空的,就表示括號是匹配的。

這個作法的時間複雜度是 O(n),其中 n 是字串的長度,因為我們只需要掃描一次字串,並且每次操作堆疊的時間複雜度是 O(1)。

程式碼如下:

string s = "[(){[]}]";
stack<char> stk;

// 掃描字串
for (char c : s) {
    // 遇到左括號就放入堆疊
    if (c == '(' || c == '{' || c == '[') {
        stk.push(c);
    // 遇到右括號就從堆疊中取出一個左括號來匹配
    } else {
        // 如果堆疊是空的,就表示這個字串不合法
        if (stk.empty()) {
            cout << "no" << endl;
            exit(0);
        }
        char t = stk.top();
        stk.pop();
        // 如果取出的左括號和右括號不匹配,就表示這個字串不合法
        if (
            (c == ')' && t != '(') ||
            (c == '}' && t != '{') ||
            (c == ']' && t != '[')
        ) {
            cout << "no" << endl;
            exit(0);
        }
    }
}

if (stk.empty()) {
    cout << "yes" << endl;
} else {
    cout << "no" << endl;
}