LeetCode5. Longest Palindromic Substring 最长回文子串 4种方法
题目链接:https://leetcode.com/problems/longest-palindromic-substring/
题意很简单,就是求一个字符串得最长子串,这里的子串指连续的。
本文给出四个不同时间的解法。在LeetCode上的用时分别是500ms,250ms,60ms以及6ms。
(1)500ms-最朴素解法
这种解法相当于模拟求解了,是一种正向思维,即枚举所有起点和终点,判断长度是否最长,且是回文的。时间复杂度O(n3).代码如下:
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 int maxlen = 0; 5 int len = s.length(); 6 string ans; 7 int r = 0,l = 0; 8 9 for(int i = 0; i < len; i++) 10 { 11 for(int j = len - 1; j > i; j--) 12 { 13 if (j - i + 1 <= maxlen) break; 14 bool flag = 0; 15 for(int k = 0; k < (j - i + 1) / 2; k++) 16 { 17 if(s[i + k] != s[j - k]) 18 { 19 flag = 1; 20 break; 21 } 22 } 23 if(flag == 0) 24 { 25 maxlen = j - i + 1; 26 l = i; 27 r = j; 28 } 29 } 30 } 31 return s.substr(l,r - l + 1); 32 } 33 };
(2)250ms----DP
以空间换时间,定义一个布尔类型的数组p[][],负责存储以 i、j 为左右界的子串是否为回文的。然后再遍历一遍,寻找是回文且最长的。时间复杂度O(n2)。
在求解 p 数组时,采用的策略是不断扩大 p 的长度,易知:
p[i][i] = true;
p[i][i+1] = (s[i] == s[i+1]);
那么,当长度为 k+1 时,有
p[i][i+k] = (p[i + 1][i + k - 1]) && (s[i] == s[i+k]);
从上式可以看出,要求长度为 k+1 时的回文与否,就得知道长度为 k 时的回文与否。因而代码如下:
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 int maxlen = 0; 5 int len = s.length(); 6 int r = 0,l = 0; 7 bool p[1005][1005]; 8 for(int i = 0; i < len; i ++) 9 { 10 p[i][i] = true; 11 p[i][i+1] = s[i] == s[i+1]; 12 } 13 for(int k = 2; k < len; k++) 14 { 15 for(int i = 0; i < len && i + k < len; i++) 16 { 17 p[i][i+k] = s[i] == s[i+k] && p[i+1][i+k-1]; 18 } 19 } 20 for(int i = 0; i < len; i++) 21 for(int j = 0; j < len; j ++) 22 if(p[i][j] && j - i + 1 > maxlen) 23 { 24 l = i; 25 r = j; 26 maxlen = j - i + 1; 27 } 28 29 return s.substr(l,r - l + 1); 30 } 31 };
(3)60ms---中心扩展法
前面两种都是正向思维,后面这两种是以解为起点,逆向构造回文子串。解法(3)最坏时间复杂度是O(n2),虽然时间复杂度与解法(2)是一样的,但是逆向求解很大程度上减少了遍历的次数,达不到回文的基本条件就不会继续扩展,在很大程度上降低了时间代价。
这种解法也可叫做从中间扩散(ExpandAroundCenter)。奇数子串:以 i 为中心,向外扩展;偶数子串:以 (i,i+1)为中心,向外扩展。扩展时满足:左右界不超出字符串边界且对称相等。以上述方法算出以 i 为中心的奇/偶数子串中长的一个作为当前的回文子串长度temp,然后比较temp与maxlen,重复该步骤求出最终解。
其中,确定了temp后,如何获得该子串的左右界需要考虑一下,举个实例就算出来了:l = i - (temp - 1) / 2, r = temp - 1 + l; 其实 l 算出来了,根据 temp = r - l + 1 就可以求出 r 了。
代码如下:
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 int maxlen = 0; 5 int len = s.length(); 6 int r = 0,l = 0; 7 for(int i = 0; i < len; i ++) 8 { 9 int temp = max(expandAroundCenter(s,i,i),expandAroundCenter(s,i,i+1)); 10 if(temp > maxlen) 11 { 12 l = i - (temp - 1) / 2; 13 r = temp - 1 + l; 14 maxlen = temp; 15 } 16 } 17 return s.substr(l,r - l + 1); 18 } 19 private: 20 int expandAroundCenter(string s, int l, int r) 21 { 22 while(l >= 0 && r < s.length() && s[l] == s[r]) 23 { 24 l--; 25 r++; 26 } 27 return r - l - 1; 28 } 29 };
(4)6ms---改进的中心扩展法
由解法(1)到解法(3)的过程中可以看出,缩短时间的思想是不断减少不必要的计算或重复的计算。解法(4)就是在解法(3)的基础上进一步缩减重复的计算。缩减的策略是,不是以一个字符 i 为中心计算其奇数或偶数长度的回文子串,而是以一个由相同字符组成的连续子串为中心向外扩展。仔细想想其实是很有道理的。字符个数总共只有128个,当字符串长度很大时,构成回文子串的字符中最有可能是重复的字符反复出现。从最终的运行时间可能看出这种策略的优势,比解法(3)平均缩短了10倍的时间。
代码如下:
1 class Solution { 2 public: 3 std::string longestPalindrome(std::string s) { 4 if (s.size() < 2) 5 return s; 6 int len = s.size(), max_left = 0, max_len = 1, left, right; 7 for (int start = 0; start < len && len - start > max_len / 2;) { 8 left = right = start; 9 while (right < len - 1 && s[right + 1] == s[right])//求出重复字符组成的子串的右界 10 ++right; 11 start = right + 1;//下一轮遍历的起点。 12 while (right < len - 1 && left > 0 && s[right + 1] == s[left - 1]) {//不断向外扩展 13 ++right; 14 --left; 15 } 16 if (max_len < right - left + 1) { 17 max_left = left; 18 max_len = right - left + 1; 19 } 20 } 21 return s.substr(max_left, max_len); 22 } 23 };
总结:主动寻找构造解的方式 比 被动搜索解的方式 效率更高!