算法思維(1)-括號(hào)問(wèn)題

LC上有非常多很括號(hào)相關(guān)的問(wèn)題届慈。比如說(shuō)有一類是純括號(hào)判斷判斷一個(gè)STRING里的括號(hào)是否合法润匙,或者要加最少多少個(gè)括號(hào)可以使得它合法琅关,或者移除最少多少個(gè)括號(hào)使得它合法等革骨。
另外一類是基于括號(hào)會(huì)有一些運(yùn)算至朗,比如說(shuō)2(XXX) = XXXXXX 這種decode模式的題目屉符。這個(gè)專題我會(huì)介紹2個(gè)這類問(wèn)題的本質(zhì),幫助你在下次閱讀到這類問(wèn)題锹引,知道如何從本質(zhì)去思考矗钟,然后快速找到問(wèn)題的突破點(diǎn)。

本質(zhì)1:括號(hào)合法匹配的本質(zhì)嫌变,就是在一個(gè)字符串任意前綴里吨艇,他的左括號(hào)數(shù)量必須都大于等于右括號(hào)數(shù)量。然后最終的字符串左括號(hào)數(shù)量和右括號(hào)數(shù)量相等腾啥。

本質(zhì)2 : 括號(hào)求表達(dá)式的本質(zhì)东涡,每一個(gè)括號(hào)內(nèi)部是一個(gè)子問(wèn)題冯吓,我們可以直接把子問(wèn)題交給遞歸假設(shè)它已解決,然后只要思考如何匯總子問(wèn)題的解變成全局的解的方法疮跑。

1.括號(hào)合法匹配的本質(zhì)

有了本質(zhì)1的特性组贺,我們?cè)倌嫦蛩伎迹裁磿r(shí)候括號(hào)不合法祖娘,就可以解決一大類問(wèn)題了失尖。不合法無(wú)非2種。1. 右括號(hào)多余左括號(hào)了渐苏。 2. 到最后左括號(hào)還多出了一些掀潮,沒(méi)有被右括號(hào)關(guān)掉

那么只要維護(hù)這2個(gè)不合法信息,我就可以知道括號(hào)是否合法琼富。
我們只要在遇到做左括號(hào)時(shí)對(duì)L++胧辽,右括號(hào)時(shí)對(duì)L--。一旦發(fā)現(xiàn)L<0了公黑,就代表右括號(hào)多出來(lái)了邑商。到最后L!=0就代表左括號(hào)沒(méi)被關(guān)掉凡蚜。

上面是個(gè)基礎(chǔ)技能人断。

我們?cè)僭诨A(chǔ)技能上去衍生一下,看一個(gè)不合法的括號(hào)情況朝蜘,如果加上最少的括號(hào)使得它合法恶迈。這個(gè)其實(shí)很好想,就是對(duì)每一個(gè)不合法的括號(hào)(分為兩類谱醇,中間多出來(lái)的右括號(hào)暇仲,和最后沒(méi)關(guān)掉的左括號(hào))

LC. 921 題 就引刃而解

public int minAddToMakeValid(String S) {
        char[] cs = S.toCharArray();
        int l = 0, r = 0;
        for (char c : cs) {
            if (c == '(') {
                l++;
            } else if (l > 0) {
                l--;
            } else r++;
        }
        return l + r;
    }

下面我們來(lái)看一道對(duì)偶題。最少移除括號(hào)的問(wèn)題
是LC 1249. Minimum Remove to Make Valid Parentheses

如果只是要求數(shù)量的話副渴,上面的代碼原封不動(dòng)就可以用奈附。
這里面需要求一個(gè)具體解。這個(gè)時(shí)候煮剧,就要求我們要移除正確的括號(hào)了斥滤。 如果上面那題增加括號(hào)也要一個(gè)具體解,小伙伴們想想如何改
其實(shí)這邊也很好想勉盅,凡是要R++的地方佑颇,我就跳過(guò)這個(gè)字母。就解決了中間多出來(lái)的右括號(hào)問(wèn)題草娜。然后從后往前再掃一次挑胸,凡是遇到左括號(hào),我就直接去掉宰闰。就一定可以把沒(méi)關(guān)掉的左括號(hào)給關(guān)掉了茬贵。

這里有些讀者會(huì)問(wèn)為什么不可以從左往右的凸克。為什么一定要從右往左呢?

這是因?yàn)槊屏ぃ阋菑淖笸铱赡苁强梢晕剑部赡苁且瞥艘粋€(gè)合法右括號(hào)的左半邊,引入了新的不合法右括號(hào)比如
()(, 你要是從左移除可能會(huì)變成)(

那為什么從右往左沒(méi)這個(gè)問(wèn)題呢舆逃?
這里有2個(gè)思考角度

  • 第一個(gè)角度就是蚂维,因?yàn)樗杏依ㄌ?hào)前必然已經(jīng)有一個(gè)左括號(hào)了。還多出來(lái)一些左括號(hào)路狮,一定是要么在所有右括號(hào)后面虫啥。那么從右往左就可以WORK,如果是在合法右括號(hào)左邊奄妨。比如((),那么即使你移走了一個(gè)左括號(hào)穆壕,前面多出來(lái)的左括號(hào)也可以立刻補(bǔ)位铝噩。

  • 如果有另一個(gè)平行宇宙矮固,括號(hào)是這樣使用的)(, 那么我們就可以在那個(gè)平行宇宙 用規(guī)則1 去先把多出來(lái)的右括號(hào)( 給刪去蝇刀。這時(shí)就是想只要怎么把這個(gè)宇宙的括號(hào)規(guī)則給映射去那個(gè)宇宙,其實(shí)就是reverse一下就可以

舉個(gè)例子( () () ( reverse一下 變成( )( )( (, 記住這個(gè)時(shí)候(是那個(gè)宇宙的右括號(hào)了直焙。我們套用規(guī)則1景东,可以發(fā)現(xiàn)第一個(gè)和最后一個(gè) 是多出來(lái)的右括號(hào)。去掉之后合法括號(hào)為)( )(
那么本質(zhì)這邊就是在原宇宙從右向左掃描奔誓。

上面那道題代碼就如下所示

public String minRemoveToMakeValid(String s) {
        char[] cs = s.toCharArray();
        int l = 0, idx = 0;
        for (int i = 0; i < cs.length; i++) {
            if (cs[i] == '(') {
                l++;
            } else if (cs[i] == ')') {
                if (l == 0) continue;
                l--;
            }
            cs[idx++] = cs[i];
        }
        int j = cs.length - 1, len = idx - l;
        for (int i = idx - 1; i >= 0; i--) {
            if (l > 0 && cs[i] == '(') l--;
            else cs[j--] = cs[i];
        }
       return new String(cs, j + 1, len);
    }

如果我要最少移除括號(hào)的所有可能解斤吐,應(yīng)該如何求呢

我們之前找了數(shù)量,然后找了任一解厨喂,下面問(wèn)題如果是所有解和措,應(yīng)該怎么求呢?
這就是一道LC HARD問(wèn)題了蜕煌。

是LC 301. Remove Invalid Parentheses
https://leetcode.com/problems/remove-invalid-parentheses/

這道題其實(shí)本質(zhì)還是找所有可以刪除的括號(hào)
我們一開(kāi)始先把不合法的左括號(hào)數(shù)量和 右括號(hào)的數(shù)量用之前的套路給統(tǒng)計(jì)出來(lái)派阱。
如果有不合法右括號(hào),就一定要先從左到向刪右括號(hào)幌绍。因?yàn)樵谟依ㄌ?hào)被刪干凈前颁褂,左括號(hào)一定是不夠的。不然就不會(huì)出現(xiàn)不合法的右括號(hào)傀广,所以在前面刪左括號(hào)是沒(méi)必要的。等右括號(hào)刪干凈了彩届。然后之前跳到最后嘗試刪左括號(hào)伪冰。
因?yàn)榇鸢敢ブ兀杂羞B續(xù)K個(gè)相同格式的括號(hào)的情況下只看第一個(gè)樟蠕。這是基本的去重思路了贮聂。當(dāng)然偷懶可以用SET來(lái)去重靠柑。

List<String> res = new ArrayList<>();
    public List<String> removeInvalidParentheses(String s) {
        char[] cs = s.toCharArray();
        // 統(tǒng)計(jì)左右括號(hào)不合法數(shù)量, l代表左括號(hào)不合法數(shù)量,r代表右括號(hào)不合法數(shù)量
        int l = 0, r = 0;
        for (int i = 0; i < cs.length; i++) {
            char c = cs[i];
            if (c == '(') {
                l++;
            } else if (c == ')') {
                if (l > 0) l--;
                else r++;
            }
        }
        dfs(cs, 0, cs.length - 1, l, r, (char)0);
        return new ArrayList<>(res);
    }
    void dfs(char[] cs, int st, int ed, int l, int r, char pre) {
        if (l == 0 && r == 0) {
            String valid = valid(cs);
            if (valid != null)
                res.add(valid);
            return;
        }
        if (st > ed) return;
        if (r > 0) {
            char cur = cs[st];
            if (cur == ')' && pre != ')') {
                cs[st] = 0;
                dfs(cs, st + 1, ed, l, r - 1, cs[st]);
            }
            cs[st] = cur;
            dfs(cs, st + 1, ed, l, r, cur);
        } else { // l > 0
            char cur = cs[ed];
            if (cur == '(' && pre != '(') {
                cs[ed] = 0;
                dfs(cs, st, ed - 1, l - 1, r, cs[ed]);
            }
            cs[ed] = cur;
            dfs(cs, st, ed - 1, l, r, cur);
        }
    }
// 合法的返回結(jié)果字符串吓懈,不然返回null
    String valid(char[] cs) {
        StringBuilder sb = new StringBuilder();
        int l = 0;
        for (char c : cs) {
            if (c == 0) continue;
            sb.append(c);
            if (c == '(') l++;
            else if (c == ')') {
                if (--l < 0) return null;
            }
        }
        return l > 0 ? null : sb.toString();
    }

當(dāng)然我們用角度2去寫(xiě)這個(gè)題也是可以的
思路為先考慮右括號(hào)不合法歼冰,再反向考慮左括號(hào)不合法。當(dāng)遇到一個(gè)右括號(hào)不合法耻警,我們需要枚舉之前所有的右括號(hào)依次去掉都是解隔嫡。
然后考慮左括號(hào),可以用平行世界法甘穿。

List<String> res = new ArrayList<>();
public List<String> removeInvalidParentheses(String s) {
    dfs(s, 0, '(', ')');
    return res;
}
void dfs(String ans, int lastJ, char L, char R) {
    int l = 0;
    for (int i = 0; i < ans.length(); i++) {
        char c  = ans.charAt(i);
        if (c == L) l++;
        else if (c == R) l--;
        if (l >= 0) continue;
// 枚舉之前所有的右括號(hào)依次去掉
        for (int j = lastJ; j <= i; j++) {
            if (ans.charAt(j) == R && (j == lastJ || ans.charAt(j - 1) != R))
                dfs(ans.substring(0, j) + ans.substring(j + 1), j, L, R);
        }
        return;
    }
    String rev = new StringBuilder(ans).reverse().toString();
    if (L == '(') dfs(rev,0, R, L); // 去平行宇宙
    else res.add(rev);
}

最后我們來(lái)看另一道HARD題腮恩,
是LC. 32. Longest Valid Parentheses

這道題,就是要求一個(gè)最長(zhǎng)的合法括號(hào)匹配子串温兼。根據(jù)我們的定義知道秸滴,只要遇到了不合法的右括號(hào),那么就要和之前的字符串做一個(gè)了斷了募判。因?yàn)橹灰诉@個(gè)前綴就不會(huì)再合法荡含。那么根據(jù)前面的MAX來(lái)跟前全局的MAX。之后從頭開(kāi)始届垫。

下一個(gè)問(wèn)題是内颗,如果滿足了沒(méi)有任何前綴有多出來(lái)的右括號(hào),我怎么知道這個(gè)前綴中最長(zhǎng)的合法子串是多少長(zhǎng)呢敦腔。
所謂合法就是意味著還要滿足第二個(gè)條件均澳,左右括號(hào)數(shù)量相等。那么我們就需要特判一下這個(gè)條件符衔。

另一個(gè)棘手問(wèn)題是找前,當(dāng)左括號(hào)>右括號(hào)時(shí),我們不知道后面還有沒(méi)有右括號(hào)可以閉起來(lái)判族。這樣就會(huì)造成一旦之前有個(gè)多出來(lái)的左括號(hào)躺盛,第二個(gè)條件始終沒(méi)法滿足。但是去掉這個(gè)多出來(lái)的左括號(hào)形帮,是可以造成后面滿足條件的槽惫。解決這個(gè)問(wèn)題,就是從右向左再找一遍辩撑,就可以規(guī)避掉最前面的左括號(hào)搗亂了界斜。這時(shí)就是個(gè)平行宇宙的概念。

代碼如下

public int longestValidParentheses(String s) {
        String rev = new StringBuilder(s).reverse().toString();
        return Math.max(help(s, '(', ')'), help(rev, ')', '('));
    }
    int help(String s, char L, char R) {
        int leftCnt = 0, rightCnt = 0, res = 0;
        for (char c : s.toCharArray()) {
            if (c == L) leftCnt++;
            else rightCnt++;
            if (leftCnt == rightCnt) res = Math.max(res, 2 * leftCnt);
            else if (rightCnt > leftCnt) {
                rightCnt = leftCnt = 0;
            }
        }
        return res;
    }

LC上還有一道題是帶通配符的括號(hào)是否匹配

LC 678. Valid Parenthesis String合冀。

其實(shí)就是說(shuō)*既可以被當(dāng)做左括號(hào)或者右括號(hào)或者空白字符各薇。看這個(gè)字符串是否匹配君躺。
其實(shí)本質(zhì)就是在維護(hù)括號(hào)匹配的2大特性峭判。如果當(dāng)中出現(xiàn)了右括號(hào)不合法开缎,我們就看之前的通配符用作左括號(hào)。如果最后是左括號(hào)多出來(lái)了林螃,我們就維護(hù)一個(gè)變量奕删,盡可能壓縮之前的左括號(hào)。
所以我們就有了一個(gè)容錯(cuò)區(qū)間疗认,LMIN是防止左括號(hào)用不完的情況所以代表完残,只要左括號(hào)多出來(lái)了,我來(lái)一個(gè)通配符我都當(dāng)它是右括號(hào)侮邀。 LMAX代表坏怪,希望左括號(hào)越多越好,這樣萬(wàn)一后面來(lái)了很多右括號(hào)绊茧,我也有很多儲(chǔ)備盡可能不被擊穿铝宵。

public boolean checkValidString(String s) {
    int lmax = 0, lmin = 0;
    for (char c : s.toCharArray()) {
        if (c == '(') {
            lmax++; lmin++;
        } else if (c == ')') {
            lmax--; 
            lmin = Math.max(0, lmin - 1);
            if (lmax < 0) return false; // 儲(chǔ)備也被右括號(hào)擊穿了,就徹底不合法
        } else { // 通配符华畏,提升儲(chǔ)備同時(shí)防止左括號(hào)過(guò)多
            lmax++;
            lmin = Math.max(0, lmin - 1);
        }
    }
    return lmin == 0; // 盡可能的消耗左括號(hào)還是消耗不完鹏秋,不合法
}

2.括號(hào)求表達(dá)式的本質(zhì)

在講這個(gè)問(wèn)題之前,我們先來(lái)思考一個(gè)非常簡(jiǎn)單的問(wèn)題亡笑,就是求二叉樹(shù)的深度侣夷。一般二叉樹(shù)的問(wèn)題,很多都可以用遞歸來(lái)解決仑乌。我們?cè)谒伎嫉臅r(shí)候百拓,就是讓左子樹(shù)返回一些信息,同時(shí)讓右子樹(shù)范圍一些信息晰甚。父節(jié)點(diǎn)就根據(jù)左右子樹(shù)返回的信息做一些處理衙传,再返回。這就是二叉樹(shù)里遞歸的思想厕九。

其實(shí)在解決一些表達(dá)式計(jì)算的時(shí)候蓖捶,是和二叉樹(shù)遞歸有很多類似的地方”庠叮基本都可以轉(zhuǎn)換過(guò)去俊鱼。而思維的突破點(diǎn)就是如果定義葉子節(jié)點(diǎn),內(nèi)部節(jié)點(diǎn)如何根據(jù)葉子結(jié)算得到值返回給上層畅买。這2個(gè)思考點(diǎn)并闲。

我們來(lái)看一道題
LC. 856. Score of Parentheses
這道題,其實(shí)() 就是葉子節(jié)點(diǎn)皮获,他的分?jǐn)?shù)為1分焙蚓。
內(nèi)部節(jié)點(diǎn)其實(shí)就是外層的括號(hào),他的函數(shù)就是對(duì)他的孩子節(jié)點(diǎn)返回的值求和 然后再乘以2.
所以一個(gè)這樣的表達(dá)式可以畫(huà)成如下的樹(shù)((()())())
這里整個(gè)表達(dá)式 是個(gè)根節(jié)點(diǎn)洒宝。

image.png

大方向是這樣购公,具體的寫(xiě)法可以有很多種。比如這種乘以2的算子是可以下推的雁歌,所以我們就可以不用構(gòu)建這個(gè)樹(shù)宏浩。而是在葉子節(jié)點(diǎn)的時(shí)候,直接把父親節(jié)點(diǎn)們下推的算子全部作用上靠瞎,然后加到RESULT里即可比庄。這樣可以把時(shí)間復(fù)雜度從O(N^2) 優(yōu)化到 O(N)
因?yàn)榍罢撸覀冃枰业矫恳粋€(gè)左括號(hào)匹配的對(duì)應(yīng)右括號(hào)乏盐,然后遞歸去算佳窑。那么遞歸里每一層都是O(N),最壞情況下是O(N ^2)
我們看下算子下推的寫(xiě)法

public int scoreOfParentheses(String S) {
    int depth = 0, res = 0;
    char[] cs = S.toCharArray();
    for (int i = 0; i < cs.length; i++) {
        if (cs[i] == '(') depth++;
        else {
            depth--;
            if (cs[i - 1] == '(') 
                res += 1 << depth; // 葉子節(jié)點(diǎn)父能,把父親們積累的DEPTH一次作用上
        }
    }
    return res;
}

有了這個(gè)思路神凑,我們?cè)賮?lái)看另2道題。
一道是394. Decode String
另一道是1190. Reverse Substrings Between Each Pair of Parentheses
在這類題目里何吝,葉子節(jié)點(diǎn)就是括號(hào)里面不帶括號(hào)的STRING溉委。而外層的括號(hào),就是內(nèi)部節(jié)點(diǎn)爱榕。在第一題中瓣喊,我們可以構(gòu)建的樹(shù)長(zhǎng)成下圖。

我們以2[b2[c]]3[a]為例子

image.png

在第二個(gè)問(wèn)題里
我們以(u(love)i)為例子

image.png

在這類問(wèn)題里黔酥,我們可以使用一個(gè)stack,然后用后序遍歷的方式來(lái)計(jì)算藻三。因?yàn)槊總€(gè)節(jié)點(diǎn)最后返回的是一個(gè)STRING,所以我們可以在STACK里存的是STRINGBUILDER跪者,那么返回的時(shí)候TOSTRING即可棵帽。
當(dāng)我們遇到一個(gè)左括號(hào),我們推入棧一個(gè)STRINGBUILDER坑夯,然后之后的操作就是在棧頂?shù)腟TRINGBUILDER里做岖寞,直到遇到右括號(hào),然后把STRINGBUILDER 彈出之后柜蜈,就是在父節(jié)點(diǎn)那層獲得了孩子節(jié)點(diǎn)做完的返回的信息仗谆,然后加入到父節(jié)點(diǎn)的STRINGBUILDER(在棧頂了)中計(jì)算。
所以2道題差不多淑履,代碼如下
第一題對(duì)每個(gè)節(jié)點(diǎn)隶垮,除了要維護(hù)STRING,還要維護(hù)乘法系數(shù)秘噪,所以需要2個(gè)棧

public String decodeString(String s) {
    char[] cs = s.toCharArray();
    Deque<Integer> nums = new ArrayDeque<>();
    Deque<StringBuilder> sbs = new ArrayDeque<>();
    sbs.push(new StringBuilder());
    int num = 0;
    StringBuilder sb = new StringBuilder();
    for (char c : cs) {
        if (c == '[') {
            nums.push(num);
            sbs.push(new StringBuilder());
            num = 0;
        } else if (c == ']') {
            int cnt = nums.pop();
            String cur = sbs.pop().toString();
            for (int i = 0; i < cnt; i++) {
                sbs.peek().append(cur);
            }
        } else if (Character.isDigit(c)) {
            num = num * 10 + (c - '0');
        } else {
            sbs.peek().append(c);
        }
    }
    return sbs.peek().toString();
}

第二題就是出棧的時(shí)候需要做一個(gè)REVERSE

public String reverseParentheses(String s) {
        Deque<StringBuilder> stk = new ArrayDeque<>();
        stk.push(new StringBuilder());
        for (char c : s.toCharArray()) {
            if (c == '(') stk.push(new StringBuilder());
            else if (c == ')') {
                String res = stk.pop().reverse().toString();
                stk.peek().append(res);
            } else stk.peek().append(c);
        }
        return stk.peek().toString();
    }

第二題有一個(gè)O(N)的解法狸吞,由于解法和本章無(wú)關(guān),所以沒(méi)有介紹,有興趣的小伙伴可以去DISCUSS里學(xué)習(xí)蹋偏。

上面的題目便斥,節(jié)點(diǎn)的定義和節(jié)點(diǎn)的計(jì)算都是比較簡(jiǎn)單直觀的。一般LC HARD的題威始,就會(huì)稍微復(fù)雜一些枢纠。我找到了如下3題。

  1. Number of Atoms
  2. Parse Lisp Expression
  3. Brace Expansion II

對(duì)726題來(lái)說(shuō)
每一個(gè)括號(hào)里我們需要統(tǒng)計(jì)元素的個(gè)數(shù)黎棠,所以需要返回一個(gè)MAP作為信息晋渺,返回到上層后,還需要乘以系數(shù)脓斩,加入父節(jié)點(diǎn)的MAP里木西。因?yàn)樽詈笮枰凑兆帜感蚺判颍允褂肨REEMAP即可随静。

public String countOfAtoms(String formula) {
        Map<String, Integer> m = help(formula.toCharArray());
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Integer> e : m.entrySet()) {
            sb.append(e.getKey());
            if (e.getValue() > 1) sb.append(e.getValue());
        }
        return sb.toString();
    }
    int i = 0;
    private Map<String, Integer> help(char[] cs) {
        Map<String, Integer> m = new TreeMap<>();
        while (i < cs.length) {
            if (cs[i] == '(') {
                i++;
                // 拿孩子節(jié)點(diǎn)的信息
                Map<String, Integer> chd = help(cs);
                // 拿系數(shù)
                int num = 0;
                while (i < cs.length && Character.isDigit(cs[i])) num = num * 10 + cs[i++] - '0';
                for (Map.Entry<String, Integer> e : chd.entrySet()) {
                    String key = e.getKey(); 
                    int val = num * e.getValue();
                    m.compute(key, (k,v)->{
                        if (v == null) return val;
                        return v + val;
                    });
                }
            } else if (cs[i] == ')') {
                i++;
                break; // 本節(jié)點(diǎn)MAP處理完畢
            } else {
                // 處理子葉子節(jié)點(diǎn)
                StringBuilder sb = new StringBuilder(""+cs[i++]);
                while (i < cs.length && cs[i] >= 'a' && cs[i] <= 'z') 
                    sb.append(cs[i++]);
                int num = 0;
                while (i < cs.length && Character.isDigit(cs[i])) num = num * 10 + cs[i++] - '0';
                int val = Math.max(1, num);
                m.compute(sb.toString(), (k,v)->{
                    if (v == null) return val;
                    return v + val;
                });
            }
        }
        return m;
    }

我們?cè)賮?lái)看736題
736題對(duì)于一個(gè)樹(shù)節(jié)點(diǎn)來(lái)說(shuō)八千,就是用括號(hào)括起來(lái)的運(yùn)算,里面可能包括子運(yùn)算挪挤。最后返回的是一個(gè)整數(shù)叼丑。節(jié)點(diǎn)有3種類型分別是ADD, MULT, LET。
而葉子節(jié)點(diǎn)就是一個(gè)純數(shù)字扛门。為什么這么定義呢鸠信?因?yàn)槲覀儼l(fā)現(xiàn)5 和 (add 2 3)是可以等價(jià)的。所以純數(shù)字是可以定義為葉子節(jié)點(diǎn)论寨。
然后進(jìn)一步分析星立,ADD, MULT是內(nèi)部節(jié)點(diǎn)的時(shí)候,會(huì)有2個(gè)孩子葬凳。而LET可能有多個(gè)孩子绰垂。
如果內(nèi)部節(jié)點(diǎn)是LET,其之后的結(jié)構(gòu)必然是一個(gè)變量加一個(gè)子節(jié)點(diǎn)火焰。(子節(jié)點(diǎn)可能是葉子節(jié)點(diǎn)劲装,可能是需要遞歸計(jì)算的內(nèi)部節(jié)點(diǎn)),最后一個(gè)子節(jié)點(diǎn)為L(zhǎng)ET的返回值昌简。同時(shí)上層LET的變量表需要透?jìng)鞯狡浜⒆庸?jié)點(diǎn)中占业。
所以大概的樹(shù)結(jié)構(gòu)如下
(let x 2 (mult x (let x 3 y 4 (add x y))))為例

image.png

public int evaluate(String exp) {
    // hashmap 需要下傳
    return eval(exp, new HashMap<>());
}
private int eval(String exp, Map<String, Integer> vals) {
    // 如果沒(méi)有以括號(hào)開(kāi)頭,那么是葉子節(jié)點(diǎn)
    if (exp.charAt(0) != '(') {
        if (exp.charAt(0) == '-' || Character.isDigit(exp.charAt(0))) { // 純數(shù)字
            return Integer.parseInt(exp);
        } else { // 變量值
            return vals.get(exp);
        }
    }
    // 以括號(hào)開(kāi)頭纯赎,處理內(nèi)部節(jié)點(diǎn)谦疾,后面操作后的參數(shù)按照空格切割。
    List<String> exps = parse(exp);
    // 繼承父親的MAP犬金,構(gòu)建自己這層的MAP
    Map<String, Integer> curVals = new HashMap<>(vals);
    if (exp.startsWith("(a")) { // 如果是加法念恍,遞歸2個(gè)孩子
        return eval(exps.get(0), curVals) + eval(exps.get(1), curVals);
    } else if (exp.startsWith("(m")) { // 如果是乘法六剥,遞歸2個(gè)孩子
        return eval(exps.get(0), curVals) * eval(exps.get(1), curVals);
    } else { // 如果是LET,遞歸(SIZE / 2) 個(gè)孩子
        for (int i = 1; i < exps.size() - 1; i += 2) {
            curVals.put(exps.get(i - 1), eval(exps.get(i), curVals));
        }
        // 最后一個(gè)作為返回值
        return eval(exps.get(exps.size() - 1), curVals);
    }
}
List<String> parse(String curExp) {
    int st = curExp.startsWith("(m") ? 6 : 5;
    curExp = curExp.substring(st);
    char[] cs = curExp.toCharArray();
    int left = 1, i = 0, pre = 0;
    List<String> res = new ArrayList<>();
    for (;left > 0; i++) { // 只關(guān)心本層的括號(hào) 和直接變量峰伙; 遇到上層括號(hào)疗疟,循環(huán)退出
        if (cs[i] == '(') left++;
        else if (cs[i] == ')') { 
            if (left-- == 1) res.add(curExp.substring(pre, i));
        } else if (left == 1 && cs[i] == ' ') {
            res.add(curExp.substring(pre, i));
            pre = i + 1;
        }
    }
    return res;
}

最后一道題1096

我們可以把{}里的值當(dāng)做內(nèi)部節(jié)點(diǎn),純字母當(dāng)做葉子節(jié)點(diǎn)词爬。
然后內(nèi)部節(jié)點(diǎn)返回的是LIST<STRING>秃嗜, 那么純字母返回的其實(shí)就是只含一個(gè)STRING的LIST权均。
在外層父節(jié)點(diǎn)顿膨,根據(jù)孩子節(jié)點(diǎn)的信息做計(jì)算。
父節(jié)點(diǎn)可以做2種運(yùn)算叽赊,distinct union或者對(duì)2個(gè)集合做笛卡爾乘積恋沃。這取決于節(jié)點(diǎn)之間是否有逗號(hào)。
那么構(gòu)建樹(shù)必指,如下圖
"{{a,z},a{b,c},{ab,z}}"為例

image.png

所以這遞歸函數(shù)中囊咏,遇到左括號(hào),調(diào)用遞歸塔橡,拿到孩子的信息梅割,把這個(gè)信息放入,自己的處理隊(duì)列中葛家。遇到逗號(hào)户辞,就之前積攢的東西,放進(jìn)SET里去做DISTINCT UNION癞谒。遇到右括號(hào)底燎,代表自己這個(gè)節(jié)點(diǎn)使命結(jié)束,退出來(lái)之后弹砚,對(duì)所有隊(duì)列的東西做笛卡爾積然后DISTINCT UNION返回給上層双仍。

int i = 0;
public List<String> braceExpansionII(String expression) {
    String e = "{" + expression + "}";
    return help(e.toCharArray());
}
List<String> help(char[] cs) {
    assert cs[i++] == '{';
    List<List<String>> pre = new ArrayList<>();
    TreeSet<String> set = new TreeSet<>();
    while (i < cs.length) {
        if (cs[i] == ',') {
            // 求之前進(jìn)預(yù)備隊(duì)列的笛卡爾積,然后放進(jìn)DISTINCT UNION
            set.addAll(combine(pre));
            pre = new ArrayList<>();
            i++;
        } else if (cs[i] == '{') {
             // 把孩子的返回桌吃,放進(jìn)笛卡爾積預(yù)備隊(duì)列里
            pre.add(help(cs));
        } else if (cs[i] == '}') break;
        else {
            // 葉子節(jié)點(diǎn)
            StringBuilder sb = new StringBuilder();
            while (Character.isLowerCase(cs[i])) {
                sb.append(cs[i++]);
            }
            pre.add(Arrays.asList(sb.toString()));
        }
    }
    assert cs[i++] == '}';
    // 最后把余下的預(yù)備隊(duì)列里的數(shù)計(jì)算完朱沃,放進(jìn)DISTINCT UNION中
    set.addAll(combine(pre));
    return new ArrayList<>(set);
}
private List<String> combine(List<List<String>> in) {
    List<String> res = Arrays.asList("");
    for (List<String> i: in) {
        List<String> tmp = res;
        res = new ArrayList<>();
        for (String pre : tmp) {
            for (String post : i) {
                res.add(pre + post);
            }
        }
    }
    return res;
}

總結(jié)

今天我們主要講解了2種括號(hào)的套路和思維方式。

  • 第一種就是依靠括號(hào)匹配2個(gè)特性來(lái)找到突破點(diǎn)去解題茅诱。特性1是任意前綴左括號(hào)數(shù)量大于右括號(hào)逗物,特性2最終左括號(hào)右括號(hào)數(shù)量相等。
  • 第二種就是定義出葉子節(jié)點(diǎn)和內(nèi)部節(jié)點(diǎn)的計(jì)算單元让簿,找出每個(gè)單元的返回的數(shù)據(jù)結(jié)構(gòu)敬察,然后思考如何利用孩子的解取組合出父親的解。

最后給大家留一道思考題尔当。之前我們只討論了一種括號(hào)的合法匹配莲祸。如果括號(hào)有3種蹂安,{},[],()。 且前者可以包含后者锐帜,后者不能包含前者田盈,如果判斷括號(hào)是匹配的呢。如果不匹配最少刪除多少個(gè)使之匹配呢缴阎?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末允瞧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蛮拔,更是在濱河造成了極大的恐慌述暂,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件建炫,死亡現(xiàn)場(chǎng)離奇詭異畦韭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)肛跌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)艺配,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人衍慎,你說(shuō)我怎么就攤上這事转唉。” “怎么了稳捆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵赠法,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我眷柔,道長(zhǎng)期虾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任驯嘱,我火速辦了婚禮镶苞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鞠评。我一直安慰自己茂蚓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布剃幌。 她就那樣靜靜地躺著聋涨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪负乡。 梳的紋絲不亂的頭發(fā)上牍白,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音抖棘,去河邊找鬼茂腥。 笑死狸涌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的最岗。 我是一名探鬼主播帕胆,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼般渡!你這毒婦竟也來(lái)了懒豹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤驯用,失蹤者是張志新(化名)和其女友劉穎脸秽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體晨汹,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豹储,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淘这。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巩剖,死狀恐怖铝穷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佳魔,我是刑警寧澤曙聂,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站鞠鲜,受9級(jí)特大地震影響宁脊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贤姆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一榆苞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧霞捡,春花似錦坐漏、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至砰碴,卻和暖如春躏筏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呈枉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工趁尼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留檐什,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓弱卡,卻偏偏與公主長(zhǎng)得像乃正,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子婶博,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348