1. 程式人生 > >[LeetCode] Valid Parenthesis String 驗證括號字串

[LeetCode] Valid Parenthesis String 驗證括號字串

Given a string containing only three types of characters: '(', ')' and '*', write a function to check whether this string is valid. We define the validity of a string by these rules:

  1. Any left parenthesis '(' must have a corresponding right parenthesis ')'.
  2. Any right parenthesis ')' must have a corresponding left parenthesis '('
    .
  3. Left parenthesis '(' must go before the corresponding right parenthesis ')'.
  4. '*' could be treated as a single right parenthesis ')' or a single left parenthesis '(' or an empty string.
  5. An empty string is also valid.

Example 1:

Input: "()"
Output: True

Example 2:

Input: "(*)"
Output: True

Example 3:

Input: "(*))"
Output: True

Note:

  1. The string size will be in the range [1, 100].

這道題讓我們驗證括號字串,跟之前那道Valid Parentheses有些類似。不同之處在於這道題不只有小括號,還存在星號,星號可以當左括號,右括號,或空來使用,問我們能不能得到一個合法的括號字串。那麼我們想,如果不存在星號,那麼這題是不是異常的簡單,我們甚至連stack都可以不用,直接用一個變數,遇到左括號,自增1,遇到右括號,如果此時計數器已經為0了,直接返回false,否則自減1,一旦計數器出現了負數,立即返回false,最後還要看變數是否為0即可。但是由於星號的存在,這道題就變的複雜了,由於星號可以當括號用,所以當遇到右括號時,就算此時變數為0,也可以用星號來當左括號使用。那麼星號什麼時候都能當括號來用嗎,我們來看兩個例子 *) 和 *( ,在第一種情況下,星號可以當左括號來用,而在第二種情況下,無論星號當左括號,右括號,還是空,*( 都是不對的。當然這種情況只限於星號和左括號之間的位置關係,而只要星號在右括號前面,就一定可以消掉右括號。那麼我們使用兩個stack,分別存放左括號和星號的位置,遍歷字串,當遇到星號時,壓入星號棧star,當遇到左括號時,壓入左括號棧left,當遇到右括號時,此時如果left和star均為空時,直接返回false;如果left不為空,則pop一個左括號來抵消當前的右括號;否則從star中取出一個星號當作左括號來抵消右括號。當迴圈結束後,我們希望left中沒有多餘的左括號,就算有,我們可以嘗試著用星號來抵消,當star和left均不為空時,進行迴圈,如果left的棧頂左括號的位置在star的棧頂星號的右邊,那麼就組成了 *( 模式,直接返回false;否則就說明星號可以抵消左括號,各自pop一個元素。最終退出迴圈後我們看left中是否還有多餘的左括號,沒有就返回true,否則false,參見程式碼如下:

解法一:

class Solution {
public:
    bool checkValidString(string s) {
        stack<int> left, star;
        for (int i = 0; i < s.size(); ++i) {
            if (s[i] == '*') star.push(i);
            else if (s[i] == '(') left.push(i);
            else {
                if (left.empty() && star.empty()) return false;
                if (!left.empty()) left.pop();
                else star.pop();
            }
        }
        while (!left.empty() && !star.empty()) {
            if (left.top() > star.top()) return false;
            left.pop(); star.pop();
        }
        return left.empty();
    }
};

如果你覺得上面的解法邏輯稍稍複雜了一些,我們來看一種邏輯無比簡單的解法。既然星號可以當左括號和右括號,那麼我們就正反各遍歷一次,正向遍歷的時候,我們把星號都當成左括號,此時用經典的驗證括號的方法,即遇左括號計數器加1,遇右括號則自減1,如果中間某個時刻計數器小於0了,直接返回false。如果最終計數器等於0了,我們直接返回true,因為此時我們把星號都當作了左括號,可以跟所有的右括號抵消。而此時就算計數器大於0了,我們暫時不能返回false,因為有可能多餘的左括號是星號變的,星號也可以表示空,所以有可能不多,我們還需要反向q一下,哦不,是反向遍歷一下,這是我們將所有的星號當作右括號,遇右括號計數器加1,遇左括號則自減1,如果中間某個時刻計數器小於0了,直接返回false。遍歷結束後直接返回true,這是為啥呢?此時計數器有兩種情況,要麼為0,要麼大於0。為0不用說,肯定是true,為啥大於0也是true呢?因為之前正向遍歷的時候,我們的左括號多了,我們之前說過了,多餘的左括號可能是星號變的,也可能是本身就多的左括號。本身就多的左括號這種情況會在反向遍歷時被檢測出來,如果沒有檢測出來,說明多餘的左括號一定是星號變的。而這些星號在反向遍歷時又變做了右括號,最終導致了右括號有剩餘,所以當這些星號都當作空的時候,左右括號都是對應的,即是合法的。你可能會有疑問,右括號本身不會多麼,其實不會的,如果多的話,會在正向遍歷中被檢測出來,參見程式碼如下:

解法二:

class Solution {
public:
    bool checkValidString(string s) {
        int left = 0, right = 0, n = s.size();
        for (int i = 0; i < n; ++i) {
            if (s[i] == '(' || s[i] == '*') ++left;
            else --left;
            if (left < 0) return false;
        }
        if (left == 0) return true;
        for (int i = n - 1; i >= 0; --i) {
            if (s[i] == ')' || s[i] == '*') ++right;
            else --right;
            if (right < 0) return false;
        }
        return true;
    }
};

下面這種方法是用遞迴來寫的,思路特別的簡單直接,感覺應該屬於暴力破解法。使用了變數cnt來記錄左括號的個數,變數start表示當前開始遍歷的位置,那麼在遞迴函式中,首先判斷如果cnt小於0,直接返回false。否則進行從start開始的遍歷,如果當前字元為左括號,cnt自增1;如果為右括號,若cnt此時小於等於0,返回false,否則cnt自減1;如果為星號,我們同時遞迴三種情況,分別是當星號為空,左括號,或右括號,只要有一種情況返回true,那麼就是true了。如果迴圈退出後,若cnt為0,返回true,否則false,參見程式碼如下:

解法三:

class Solution {
public:
    bool checkValidString(string s) {
        return helper(s, 0, 0);
    }
    bool helper(string s, int start, int cnt) {
        if (cnt < 0) return false;
        for (int i = start; i < s.size(); ++i) {
            if (s[i] == '(') {
                ++cnt;
            } else if (s[i] == ')') {
                if (cnt <= 0) return false;
                --cnt;
            } else {
                return helper(s, i + 1, cnt) || helper(s, i + 1, cnt + 1) || helper(s, i + 1, cnt - 1);
            }
        }
        return cnt == 0;
    }
};

下面這種解法是論壇上排第一的解法,感覺思路清新脫俗,博主研究了好久,參考了網友的留言才稍稍弄懂了一些,這裡維護了兩個變數low和high,其中low表示在有左括號的情況下,將星號當作右括號時左括號的個數(這樣做的原因是儘量不多增加右括號的個數),而high表示將星號當作左括號時左括號的個數。是不是很繞,沒辦法。那麼當high小於0時,說明就算把星號都當作左括號了,還是不夠抵消右括號,返回false。而當low大於0時,說明左括號的個數太多了,沒有足夠多的右括號來抵消,返回false。那麼開始遍歷字串,當遇到左括號時,low和high都自增1;當遇到右括號時,只有當low大於0時,low才自減1,保證了low不會小於0,而high直接自減1;當遇到星號時,只有當low大於0時,low才自減1,保證了low不會小於0,而high直接自增1,因為high把星號當作左括號。當此時high小於0,說明右括號太多,返回false。當迴圈退出後,我們看low是否為0,參見程式碼如下:

解法四:

class Solution {
public:
    bool checkValidString(string s) {
        int low = 0, high = 0;
        for (char c : s) {
            if (c == '(') {
                ++low; ++high;
            } else if (c == ')') {
                if (low > 0) --low;
                --high;
            } else {
                if (low > 0) --low;
                ++high;
            }
            if (high < 0) return false;
        }
        return low == 0;
    }
};

類似題目:

參考資料: