🌸题目
🍁给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
1 | 输入: "aacecaaa" |
示例 2:
1 | 输入: "abcd" |
🌸分析
在字符串开头补充最少的字符,使得当前字符串成为回文串。
🌸解法一:暴力解法
先判断整个字符串是不是回文串,如果是的话,就直接将当前字符串返回。不是的话,进行下一步。
判断去掉末尾 1 个字符的字符串是不是回文串,如果是的话,就将末尾的 1 个字符加到原字符串的头部返回。不是的话,进行下一步。
判断去掉末尾 2 个字符的字符串是不是回文串,如果是的话,就将末尾的 2 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。
判断去掉末尾 3 个字符的字符串是不是回文串,如果是的话,就将末尾的 3 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。
…
直到判断去掉末尾的 n - 1
个字符,整个字符串剩下一个字符,把末尾的 n - 1
个字符倒置后加到原字符串的头部返回。
举个例子,比如字符串 abbacd。
1 | 原字符串 abbacd |
代码的话,判断是否是回文串的话可以用 125 题 的思想,利用双指针法
1 | //判断是否是回文串, 传入字符串的范围 |
🌸解法二:
根据解法一,我们其实就是在寻找从开头开始的最长回文串(这个很关键,后边所有的解法都是基于这个了),然后将末尾的除去最长回文串部分的几个字符倒置后加到原字符串开头即可。
我们只需要两个指针, i
和 j
,i
初始化为 0,j
初始化为字符串长度减 1。然后依次判断 s[i]
和 s[j]
是否相同,相同的话, i 就进行加 1,j 进行减 1。 s[i] 和 s[j] 不同的话,只将 j 进行减 1。
看几个例子。
1 | abbacde |
当然,上边是最理想的情况,如果 j
在最长回文串外提前出现了和 i
相同的字符会有影响吗?
1 | abbacba |
可以看到上边的两种情况,只要 j
进入了最长回文子串,一定会使得 i
走出最长回文子串。所以我们可以利用双指针写一下代码了。
1 | public String shortestPalindrome(String s) { |
看起来没什么问题,但还有一种情况,那就是 i
提前走出了最长回文子串,看下边的例子。
1 | ababbcefbbaba |
此时我们并没有找到最长回文串,但是我们可以肯定最长回文串一定在 0 到 i 之间,所以我们只需要递归的从s[0, i)
中继续寻找最长回文串即可。
因为上边的所有情况,都保证了 i 一定可以走出最长回文串,只不过可能超出一部分,所以用递归解决即可。代码的整体框架不需要改变。
1 | public String shortestPalindrome(String s) { |
🌸解法三:暴力(判断回文升级)
寻找开头开始的最长回文串,我们回到更暴力的方法。
将原始字符串逆序,然后比较对应的子串即可判断是否是回文串。举个例子。
1 | abbacd |
代码
1 | public String shortestPalindrome(String s) { |
🌸解法四:字符串哈希
在解法三倒置的基础上进行一下优化,参考 这里。
用到了字符串匹配算法 RK 算法的思想,也就是滚动哈希。
解法三中,每次比较两个字符串是否相等都需要一个字符一个字符比较,如果我们把字符串通过 hash 算法映射到数字,就可以只判断数字是否相等即可。
而 hash 算法,这里的话,我们将 a 看做 1,b 看做 2 … 以此类推,然后把字符串看做是 26 进制的一个数字,将其转为十进制后的值作为 hash 值。
举个例子,对于 abcd。
1 | a b c d |
那么 abcd 的 hash 值就是 4+3*26+2*26^2^ + 1*26^3^ 。
这样做的好处是,我们可以通过前一个字符串的 hash
值,算出当前字符串的 hash
值。
举个例子。
对于字符串 abb ,如果我们知道了它的 hash 值是 x ,那么对于 abba 的 hash 值,因为新增加的数字 a 对应 1,所以 abba 的 hash 值就是 (x * 26 + 1)
。
所以代码可以写成下边的样子。
1 | public String shortestPalindrome(String s) { |
提交时出现错误,最直接的问题肯定是由于我们用 int 存储 hash 值,所以一定会出现溢出的情况。溢出以后,接着带来了 hash 冲突,从而使得相同的 hash 值,但是字符串并不相同。
基于上边的分析,我们可以在 pos = i 之前判断一下当前是否是回文串。
1 | public boolean isPalindromic(String s, int start, int end) { |
然后就是换 hash
算法,我们可以把每次的结果取模,这样就不会溢出了。
1 | public String shortestPalindrome(String s) { |
上边确认当前是否是回文串的时候,我们调用了 isPalindromic ,但超时了,这里的话我们还可以和它的逆置字符串进行比较。
1 | public String shortestPalindrome(String s) { |
🌸解法五:KMP算法
这个解法的前提是你熟悉另一种字符串匹配算法,即 KMP 算法。推荐两个链接,大家可以先学习一下,我就不多说了。
之前也有一篇文章写了KMP,那里有详细证明
如果熟悉了 KMP 算法,下边就简单了。
再回想一下解法三,倒置字符串的思路,依次比较对应子串。
1 | abbacd |
如果我们把 abbacd dcabba看成一个字符串,中间加上一个分隔符 #,abbacd#dcabba。
回味一下上边的三条判断,判断 XXX 和 XXX 是否相等,按列看一下。
左半部分 abbacd,abbac , abba 其实就是 abbacd#dcabba 的一些前缀。
右半部分dcabba,cabba,abba 其实就是 abbacd#dcabba 的一些后缀。
寻找前缀和后缀相等。
想一想 KMP 算法,这不就是 next 数组做的事情吗。
而我们中间加了分隔符,也就保证了前缀和后缀相等时,前缀一定在 abbacd 中。
换句话说,我们如果求出了 abbacd#dcabba
的 next 数组,因为我们构造的字符串后缀就是原字符串的倒置,前缀后缀相等时,也就意味着当前前缀是一个回文串,而 next 数组是寻求最长的前缀,我们也就找到了开头开始的最长回文串。
因为 next 数组的含义并不统一,但 KMP 算法本质上都是一样的,所以下边的代码仅供参考。
我的 next 数组 next[i] 所考虑的对应字符串不包含 s[i]。
1 | public String shortestPalindrome(String s) { |
🌸解法六: 马拉车
大家还记得 第 5 题 吗?求最长回文子串。
这里我们已经把题目转换成了求开头开始的最长回文子串,很明显这个问题只是第 5 题的子问题了。但这道题时间复杂度差不多只有 O(n) 才会通过。这就必须使用 第 5 题 介绍的马拉车算法了。
直接把马拉车算法粘贴过来即可,然后在最后稍微修改一下即可
1 | public String preProcess(String s) { |
题解出自windliang