最近碰到了一道面試題,雖然不難但是臨試沒想出好的解法,記錄下來以作分享。
題目:消除字符串中全部的b
和連續的ac
用例:
'aabbc' -> 'a'
'aaabbbccc' -> ''
'abcdcba' -> 'dca'
注意結合用例理解這個題目的意思,轉化后的字符串中不能有任何b
和連續的ac
,而不是僅對初始值進行一次轉換。
暴力法
既然最后得到的字符串中不能有任何b
和連續的ac
,那么我們可以很容易地想到使用正則連續地進行處理,直到處理前后的字符串相同,可以很容易地寫出下面的代碼:
function solution (str) {
const reg = /ac|b/g
let strCopy = str
do {
str = strCopy
strCopy = str.replace(reg, '')
} while (strCopy !== str)
return strCopy
}
但是暴力法顯然不夠好,它每次重復地去執行替換,對於aaaaaccccc
這種字符串,需要執行5次。
那么有沒有更好的方法呢?通過觀察我們發現其實需要替換的字符序列必定符合{n個a}{m個b}{n個c}(m, n不都為0)
這種格式。b我們可以先不去管它,先從格式開頭的a入手。那么如果找到了a,怎么知道后面的字符序列中有沒有c出現呢?而c出現的個數是否能跟a的個數匹配呢?我們可以先保存連續a的數量,然后如果后面出現了符合格式的c,則減少a的個數,直到a耗盡或者格式匹配失敗。
存儲a的解法
function solution(str) {
let countA = 0 // 連續a的個數
let result = ''
for (let i = 0; i < str.length; i++) {
if (str[i] === 'a') {
countA++
} else if (str[i] === 'b') {
continue
} else if (str[i] === 'c') {
if (countA === 0) {
result += 'c'
} else {
countA--
}
} else {
// 遇到其他字符,則保存的a需要釋放
while (countA) {
result += 'a'
countA--
}
result += str[i]
}
}
// 最后需要釋放所有保存的a
while (countA) {
result += 'a'
countA--
}
return result
}
存儲a的解法有點類似於棧,遇到a入棧,遇到c出棧,遇到abc之外的字符排空棧。
這是一種時間復雜度O(n)的解法,但是在遇到類似aaaaaad
這種字符串的時候,它還不夠好,因為最后保存的a都要釋放出來,有額外的時間開銷。
雙指針的解法
這是本題較好的一種解法,設兩個指針cur和loc分別從頭開始出發,cur每次移動一格,另一個指針loc保留當前的操作位置,如果cur指向的字符是c且loc指向的是a,則將loc回移一位(ac抵消了),如果遇到其他非b的字符,則將loc處的字符置為cur處的字符,一直進行直到到cur到達字符串尾部,此時取字符串開頭到loc指針之間的子串即為本題的解。這種解法妙就妙在loc處的字符是即時更新的,一些邊界條件都自動消除了。
畫一個圖更好理解一點,比如有字符串abeabcdaabbcg
,它經過處理后應該得到aedag
,下面是操作過程的圖解:
這種方法在C++, Java中是可以實現in-place更新的,但Javascript字符串是不可變的,所以體現不出來。
function solution(str) {
let result = str.split('')
let location = -1
for (let i = 0; i < str.length; i++) {
let cur = str[i]
if (cur === 'c' && location >= 0 && result[location] === 'a') {
location --
} else if (cur !== 'b') {
result[++location] = cur
}
}
return result.slice(0, location + 1).join('')
}
最后做個技術總結,這道題難度不大,考察的是對字符串算法的理解,雙指針、棧、動態規划等思想在字符串算法問題中還是有很多應用的,還是要通過學習去總結歸納。