LeetCode 热题 HOT 100 第5天: “最长回文子串”

继续刷LeetCode 热题 HOT 100 的题目,并且在博客更新我的solutions。在csdn博客中我会尽量用文字解释清楚,相关Java代码大家可以前往我的个人博客jinhuaiyu.com中查看。
今天的题目有多种解法,但我一开始也只想出动态规划,为了进一步了解大佬们的思路,又学习了中心扩展和“马拉车”(Manacher 算法)。最后一种能够达到O(n)的复杂度,但是异常的难(对我现在来说),但是我还是尽量取理解了官方给的代码代码。
时间:2022.1.8
题目:最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成
solution 1: 动态规划
用一个bool型二维数组来记录以i和j位置为两端字符的子串是否为回文子串,再定义两个指针记录最长回文子串的两端。通过两层循环来从右往左进行判断,外层循环中i从右往左移动,内层循环中j从i处往右移动。判断以i和j为两端的子串是否回文的条件如下:
ifPal[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 3 || ifPal[i + 1][j - 1]);
如果i和j处字符不等,那肯定不是。如果相等,且满足以下两个条件之一的就一定是:1、ij距离不超过2;2、ij中间包裹的子串(不包括ij)是回文。这样就充分利用了之前迭代过程中记录的值,实现了动态规划。
最后还需要在每次循环时判断并及时更新记录最长回文子串的两端的指针。

solution 2: 中心扩展法
所有的回文子串分为两种,中心是一个字符,或中心是两个一样的字符,然后两侧进行同样字符的扩展。我们称这个中心为边界条件。
为什么叫边界条件,我们可以用递归的思路想,如果要判断一个字符串是不是回文,需要判断其两端字符是不是相等且去掉首尾后中间的字符串是不是回文。递归到最后就会出现前面分析的两种情况,这就是递归的边界。
那么我们可以通过对字符串s进行一次遍历,针对每一种可能的边界条件进行讨论。比如针对s[i],我们讨论以它为中心能扩展出多长的回文字符串;或者针对s[i]和s[i+1],能扩展多长的以它们两个为中心的回文字符串。
在这里,每轮循环要讨论奇偶两种情况,并将最长的结果更新。扩展的方法就是同时从中心的左右两侧往外1个1个移动并进行判断(注意不要越界)。
因此,中心扩展法的时间复杂度也为O(n²)(外层遍历,里层扩展)。

solution 3: Manacher 算法(看不懂可以在评论区继续讨论)
前面提到回文子串有奇偶两种,我们可以通过一个巧妙的操作将它们统一起来:向字符串的头尾以及每两个字符中间添加一个特殊字符 #,比如字符串 aaba 处理后会变成 #a#a#b#a#。那么原先长度为偶数的回文字符串 aa 会变成长度为奇数的回文字符串 #a#a#,而长度为奇数的回文字符串 aba 会变成长度仍然为奇数的回文字符串 #a#b#a#,我们就不需要再考虑长度为偶数的回文字符串了。(注意这里的特殊字符不需要是没有出现过的字母,我们可以使用任何一个字符来作为这个特殊字符。这是因为,当我们只考虑长度为奇数的回文字符串时,每次我们比较的两个字符奇偶性一定是相同的,所以原来字符串中的字符不会与插入的特殊字符互相比较,不会因此产生问题。)
将奇偶统一后,我们定义臂长为中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ,其臂长为 length。
关键的地方来了,这里我先直接截图官方解答:
在这里插入图片描述
在这里插入图片描述
我的补充说明:
和中心扩展法一样,还是需要对字符串进行一次遍历,但不需要每个字符都从中心开始扩展以找到最大回文,本算法节省的地方就在这里。
对于特定的j及其臂长length,遍历到i时,如果在其臂长范围内,那么可以利用到对称的位置的最长回文子串。如果i+对称位置的回文臂长超过了j的臂长,那么j的回文特性就失效了,i的最长回文臂长还需要往j的臂长外进行探索。如果没有超过j的臂长,那么j的回文特性保证了i和其关于j对称的字符拥有同样的扩展回文。对于前一种情况,扩展时不再是从i开始扩展,而是从j的臂展尽头开始扩展(臂展范围内被j的回文特性保证一定是回文的)。
j并不是固定的,而是随着i向右的遍历不断右移的(如果i的臂长尽头比j的臂长尽头靠右,则j=i,否则不变),j不能超过i。
在整个遍历过程中,既有外层循环遍历i,也有内层找i的扩展回文,难道时间复杂度不是O(n²)吗?非也。我们不要用i来思考时间复杂度,而是用j的右臂尽头来想,设其为right。right随着i的遍历不断向右移,直到达到字符串尽头。由于对于每个位置,扩展要么从当前的最右侧臂长 right 开始,要么只会进行一步(此时认为扩展方法的一步就结束了,复杂度为O(1),因为这种情况下i和其关于j对称的点回文一样),而 right 最多向前走 O(n)步(扩展的循环次数就是和right的移动一致),因此算法的复杂度为 O(n)。

靠文字确实很难说清楚部分细节,带有详细注释的代码放在放在我的个人博客http://jinhuaiyu.com/leetcode-longest-palindromic-substring/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值