寫在前面
李小龍的妻子琳達在《我眼中的布魯斯》回憶裏寫道,她問丈夫:“作爲世界第一,是不是不畏懼所有的對手?”。
李小龍否認:“我不是世界第一,我也有害怕的對手。”
妻子聽到十分驚訝,追問:“什麼樣的對手讓你害怕?”
李小龍說“我不怕會一萬種招式的人,我只怕把一種招式練了一萬遍的對手。”
對於算法題,也是如此,我們追求的不是把一種題解出來,而是找到這道題所有的解法,所以推出小題大做系列,意在深度剖析算法題,做到將一種招式練一萬遍。
希望可以通過這個系列的博客,鍛鍊自己使用多種方式解決遇到的問題的能力。
題幹
給定一個字符串s,找到s中最長的迴文子串。可以假設s的最大長度爲1000。
迴文串是從左向右讀和從右向左讀一樣的字符串。
子串是原始字符串的連續子集,子序列是原始字符串的一個子集,這裏要求是子串。
解法
暴力解法
雙重for循環遍歷所有可能,start和end記錄起始與結束位置,最後截取字符串(截取字符串較爲耗性能,所以前期使用指針記錄,最後截取)。
但是這個方法時間複雜度較高(O(n^3)),js暴力解法在Leetcode是無法通過的。
var longestPalindrome = function (s) {
let len = s.length;
if (len < 2) return s;
let maxLen = 1;
let start = 0; // 記錄最長迴文子串的起始位置
let end = 0; // 記錄最長迴文子串的結束位置
// 雙重for循環,遍歷所有情況
for (let i = 0; i < len; i++) {
for (let j = i; j < len; j++) {
if (isPalindrome(i, j)) {
let tempLen = j - i + 1;
if (tempLen > maxLen) {
maxLen = tempLen;
start = i;
end = j;
}
}
}
}
return s.slice(start, end + 1)
// 判斷是否爲迴文
function isPalindrome(i, j) {
while (i < j) {
if (s.charAt(i) != s.charAt(j)) {
return false
}
i ++;
j --;
}
return true
}
};
中心擴散法
枚舉最長迴文子串的中心可能出現的所有位置,然後向外擴散,考慮中心是一個字符的情況,也考慮中心是兩個字符的情況。
中心是兩個字符的情況時,左指針要在右邊,否則,即使while循環一次也沒進,判斷出來的最長迴文子串也有兩個字符。
時間複雜度O(n^2)
可以通過LeetCode
var longestPalindrome = function (s) {
let len = s.length;
if (len < 2) return s;
let maxLen = 0; // 記錄最長迴文子串長度
let start = 0; // 記錄起始位置
let end = 0; // 記錄結束位置
for (let i = 1; i < len; i++) {
let tempSartEnd = findMaxPalindrome(i);
let tempLen = tempSartEnd[1] - tempSartEnd[0] + 1
if (tempLen > maxLen) {
maxLen = tempLen
start = tempSartEnd[0]
end = tempSartEnd[1]
}
}
return s.slice(start, end + 1)
function findMaxPalindrome(i) {
// 中心爲一個字符的情況
let left = i;
let right = i;
// 中心爲兩個字符的情況(這時候,left_指針要在右邊,因爲這時候及時while循環一次沒走,判斷出來的最長迴文也是兩個字符)
let left_ = i - 1;
let right_ = i;
// 如果迴文子串的中心是一個字符
while (s.charAt(left - 1) && s.charAt(right + 1) && s.charAt(left - 1) == s.charAt(right + 1)) {
left--;
right++;
}
// 如果迴文子串的中心是兩個字符
while (s.charAt(left_ - 1) && s.charAt(right_ + 1) && s.charAt(left_ - 1) == s.charAt(right_ + 1)) {
left_--;
right_++;
}
// 返回較長的
return (right_ - left_) > (right - left) ? [left_, right_] : [left, right]
}
};
動態規劃法
要想判斷第start到end位爲迴文字符串,需要判斷start + 1到end - 1位是不是迴文字符串。
我們可以列出一個表格(二維數組,初始全部爲null)
起始\結束 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | |||||
1 | |||||
2 | |||||
3 | |||||
4 |
當起始位置與結束位置相同時,一定是迴文,所以我們初始化表格的時候將start==end的位置設置爲true
起始\結束 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | √ | ||||
1 | √ | ||||
2 | √ | ||||
3 | √ | ||||
4 | √ |
由於start必定小於等於end,所以我們只需要求表格右上部分即可。
由於想要求start到end是否迴文需要依賴start+1到end-1是否迴文,所以求當前格子需要依賴左下方格子
就有一下幾種情況
- 左下爲null,則比較當前start與end是否相等,相等則此格子爲true,不等爲false。
- 左下爲true,則比較當前start與end是否相等,相等則此格子爲true,不等爲false。
- 左下爲false,此格子爲false。
由於需要依賴左下方格子,所以填表順序如下
起始\結束 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | √ | 1 | 2 | 4 | 7 |
1 | √ | 3 | 5 | 8 | |
2 | √ | 6 | 9 | ||
3 | √ | 10 | |||
4 | √ |
如果發現新的迴文字符串,則與之前最大的迴文字符串組比較,取較大的一個。
var longestPalindrome = function (s) {
let len = s.length;
if (len < 2) return s;
let maxLen = 0; // 記錄最長迴文子串長度
let start = 0; // 記錄起始位置
let end = 0; // 記錄結束位置
let map = new Array(len);
// 初始化表格
for (let i = 0; i < len; i++) {
map[i] = new Array(len).fill(null);
map[i][i] = true
}
// 填表
for (let j = 1; j < len; j++) {
for (let i = 0; i < j; i++) {
if (map[i + 1][j - 1] == true || map[i + 1][j - 1] == null) {
if (s.charAt(i) == s.charAt(j)) {
map[i][j] = true;
let tempMaxLen = j - i + 1;
if (tempMaxLen > maxLen) {
maxLen = tempMaxLen;
start = i;
end = j;
}
} else {
map[i][j] = false;
}
} else {
map[i][j] = false;
}
}
}
return s.slice(start, end + 1)
};
總結
通過這道題,鞏固了很多已經會的知識,手寫出三道算法,用時也不算短,很多時間花在了排查算法漏洞和修改漏洞上,漏洞這個東西,,還是練得少,熟能生巧以後應該會更早的發現漏洞吧。。
一題多解是一個很好的鍛鍊方式,可以通過一道題,鍛鍊自己各個方面的能力,發現自己的短板,同時也可以爲其他題提供新的思路。