马拉车算法(求一个字符串的最长回文子串)

问题描述:

给定一个字符串 s,找到 s 中最长的回文子串。

示例输入:
"abc1234321b"
示例输出:
1234321
方法一:暴力匹配(Brute Force)

根据回文子串的定义,枚举所有长度大于等于2的子串,依此判断他们是否是回文。在具体实现时,可以只针对大于“当前得到的最长回文子串长度”的子串进行“回文验证”。

方法一代码:
public static void main(String[] args) {
		
		String string = "abc1234321b";
		System.out.println("原串:" + string);
		System.out.println("最长回文串:" + longestPalindrome(string));
		
	}

	public static String longestPalindrome(String s) {
		int len = s.length();
		if (len < 2) {
			return s;
		}

		int maxLen = 1;
		String res = s.substring(0, 1);

		// 枚举所有长度大于等于 2 的子串
		for (int i = 0; i < len - 1; i++) {
			for (int j = i + 1; j < len; j++) {
				if (j - i + 1 > maxLen && valid(s, i, j)) {
					maxLen = j - i + 1;
					res = s.substring(i, j + 1);
				}
			}
		}
		return res;
	}

	private static boolean valid(String s, int left, int right) {
		// 验证子串 s[left, right] 是否为回文串
		while (left < right) {
			if (s.charAt(left) != s.charAt(right)) {
				return false;
			}
			left++;
			right--;
		}
		return true;
	}

方法一执行结果:
在这里插入图片描述

解析: 暴力解法时间复杂度高,但是思路清晰、编写简单,因为编写的正确性的可能性很大,可以使用暴力匹配算法检验我们编写的其他算法是否正确。
复杂度分析: O(N^3), N 是字符串长度,枚举字符串的左边界和右边界,然后继续验证子串是否是回文子串,这三种操作都与 N 相关。
空间复杂度: O(1), 只使用到常数个临时变量,与字符串长度无关。

方法二:中心扩散法

暴力法采用的是双指针两边夹,验证是否是回文子串,时间复杂度较高,除了枚举字符串左右边界以外,比较容易想到的是枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。

中心扩展算法的思路: 遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。

枚举“中心位置”时间复杂度为O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为O(N),因此时间复杂度可以降到O(N^2)。

======注意:回文子串的长度为奇数和偶数的时候,“回文中心”的形式是不一样的。

<1>奇数回文串的“中心”是一个字符。如:回文串 “bab” 的中心是字符 “a”
<2>偶数回文串的“中心”是位于中间的两个字符的“空隙”,如:回文串 “abba” 的中心是两个 “b” 中间的空隙。

在这里插入图片描述
我们可以看一下一个字符串可能的回文子串的中心在哪里?
在这里插入图片描述
我们可以设计一个方法,兼容以上两种情况:

  1. 如果传入重合的索引编码,进行中心扩散,此时得到的回文子串的长度是奇数;
  2. 如果传入相邻的索引编码,进行中心扩散,此时得到的回文子串的长度是偶数。

==== 以下是个人处理方式:有点麻烦,但是感觉容易理解;(方法三中有体现)====
【在所给定的字符串的每一个字符中间插入一个特殊字符,如:“#”,这样得到的字符串一定是奇数个字符。也就是说得到的回文子串的中心一定是字符(也有可能是“#”),然后再把得到的回文子串中的特殊字符去掉。】
字符预处理

方法二代码:
public static void main(String[] args) {

		String string = "abc1234321b";
		System.out.println("原串:" + string);
		System.out.println("最长回文串:" + longestPalindrome(string));

	}

	public static String longestPalindrome(String s) {
		int len = s.length();
		if (len < 2) {
			return s;
		}
		int maxLen = 1;
		String res = s.substring(0, 1);
		// 中心位置枚举到 len - 2 即可
		for (int i = 0; i < len - 1; i++) {
			String oddStr = centerSpread(s, i, i);
			String evenStr = centerSpread(s, i, i + 1);
			String maxLenStr = oddStr.length() > evenStr.length() ? oddStr : evenStr;
			if (maxLenStr.length() > maxLen) {
				maxLen = maxLenStr.length();
				res = maxLenStr;
			}
		}
		return res;
	}

	private static String centerSpread(String s, int left, int right) {
		// left = right 的时候,此时回文中心是一个空隙,回文串的长度是奇数
		// right = left + 1 的时候,此时回文中心是任意一个字符,回文串的长度是偶数
		int len = s.length();
		int i = left;
		int j = right;
		while (i >= 0 && j < len) {
			if (s.charAt(i) == s.charAt(j)) {
				i--;
				j++;
			} else {
				break;
			}
		}
		// 这里要小心,跳出 while 循环时,恰好满足 s.charAt(i) != s.charAt(j),因此不能取 i,不能取 j
		return s.substring(i + 1, j);
	}

方法二执行结果:
在这里插入图片描述
解析:

时间复杂度:O(N^2)

空间复杂度:O(1),只使用到常数个临时变量,与字符串长度无关。

事实上,还有时间复杂度更优的算法,是由计算机科学家 Manacher 发明的。

方法三:Manacher算法(马拉车算法)
专门用来解决“最长回文子串”问题,时间复杂度为O(N)。

维基百科中对于 Manacher 算法是这样描述的:
[Manacher(1975)] 发现了一种线性时间算法,可以在列出给定字符串中从字符串头部开始的所有回文。并且,Apostolico, Breslauer & Galil (1995) 发现,同样的算法也可以在任意位置查找全部最大回文子串,并且时间复杂度是线性的。因此,他们提供了一种时间复杂度为线性的最长回文子串解法。替代性的线性时间解决 Jeuring (1994), Gusfield (1997)提供的,基于后缀树(suffix trees)。也存在已知的高效并行算法。

Manacher 算法本质上还是 中心扩散法,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,在遍历的过程中,记录了已经遍历过的子串的信息,也是典型的以空间换时间思想的体现。

Manacher算法的具体流程:
第一步: 对原始字符进行预处理(添加分隔符)
首先在字符串的首尾和相邻的字符中插入分割符,例如 “babad” 添加分割符“#”之后得到 “#b#a#b#a#d#”
说明:
1. 分隔符是一个字符,种类也只有一个,并且这个字符一定不能是原始字符串中出现的字符;
2. 加入了分隔符以后,使得“间隙”有了具体的位置,方便后续的讨论,并且 新字符串中的任意一个回文子串在原始字符串中一定能找到唯一的一个回文子串与之对应, 因此对新字符串的回文子串的研究就能得到原始字符串的回文子串。
3. 新字符串的回文子串的长度一定是奇数;
4. 新字符串的回文子串一定以分隔符作为两边的边界,因此分隔符起到“哨兵”的作用。
在这里插入图片描述
第二步:计算辅助数组p
辅助数组 “p” 记录新字符串中以每个字符为回文中心的回文子串的信息。
手动的计算方法仍然是“中心扩散法”,此时记录以当前字符为中心,向左右两边同时扩散,记录能够扩散的最大步数。
以字符串 “abbabb” 为例,说明如何手动计算得到辅助数组 “p” ,填写下边这张表。

在这里插入图片描述
第1行数组 char :原始字符串加上分隔符以后的每个字符。

第2行数组 index :这个数组是新字符串的索引数组,它的值从0开始的索引编号。

我们首先填写 p[0]
char[0] = ‘#’ 为中心,同时向左右两边扩散,走 1 步就碰到边界了,因此能扩散的步数为 0,因此p[0] = 0
在这里插入图片描述
下边填写 p[1]
char[1] = ‘a’ 为中心,同时向左右两边同时扩散,走 1 步,左右都是 ‘#’ ,构成回文子串,于是再继续同时向左右扩散,左边就碰到边界了,最多能扩散的步数为 1 步,因此 p[1] = 1 ;
在这里插入图片描述
下面填写 p[2].
char[2] = ‘#’ 为中心,同时向左右扩散,走 1 步,左边是 ‘a’ ,右边是 ‘b’ ,不匹配,最多能扩散的步数为 0 ,因此 p[2] = 0 ;
在这里插入图片描述
下面填写 p[3].
char[3] = ‘b’ 为中心,同时向左右扩散,走 1 步,左边是 ‘#’ ,右边是 ‘b’ ,构成回文子串,继续同时向左右扩散,左边是 ‘a’ ,右边是 ‘b’ ,不匹配,最多能扩散的步数为 1,因此 p[3] = 1 ;
在这里插入图片描述
下面填写 p[4].
char[4] = ‘#’ 为中心,同时向左右扩散,最多可以走 4 步,左边到边界,右边是 ‘#’ ,匹配,最多能扩散的步数为 4 ,因此 p[4] = 4 ;
在这里插入图片描述
继续完成 p 数组剩下的部分。
分析到这里,后边的数字就不难推出,最后写成如下表格:
在这里插入图片描述
说明: 有些资料将辅助数组 p 定义为回文半径数组,即 p[i] 记录了以新字符串第 i 个字符为中心的回文字符串的半径(包括第 i 个字符),与我们这里定义的辅助数组 p 有一个字符的偏差,本质上是一样的。
下边是辅助数组 p 的结论:辅助数组 p 的最大值是5,对应了原字符串 ‘abbabb’ 的“最长回文子串”: “bbabb” 。这个结论具有一般,即:

辅助数组 p 的最大值就是“最长回文子串”的长度。

因此,我么可以在计算辅助数组 p 的过程中记录这个最大值,并且记录最长回文子串。
简要说明为什么:
1. 如果新回文子串的中心是一个字符,那么原始回文子串的中心也是一个字符,在新回文子串中,向两边扩散的特点是:“先分隔符,后字符”,同样扩散的步数因为有分隔符 # 的作用,在新字符串中每扩散两步,虽然实际上只扫到一个有效字符,但是相当于在原始字符串中相当于计算了两个字符。因为最后一定以分隔符结尾,还要计算一个,正好这个就可以把原始回文子串的中心算进去;
在这里插入图片描述
2. 如果新回文子串的中心是 #,那么原始回文子串的中心就是一个“空隙”。在新回文子串中,向两边扩散的特点是:“先字符,后分隔符”,扩散的步数因为有分隔符 # 的作用,在新字符串中每扩散两步,虽然实际上只扫到一个有效字符,但是相当于在原始字符串中相当于计算了两个字符。
因此,辅助数组 p 的最大值就是”最长回文子串“的长度这个结论是成立的,可以看下边的图理解上边说的2点。
在这里插入图片描述

方法三代码:
public static void main(String[] args) {

		String string = "abc1234321b";
		System.out.println("原串:" + string);
		System.out.println("最长回文串:" + longestPalindrome(string));

	}
	// 计算最长回文字符串
	public static String longestPalindrome(String s) {
		int len = s.length();
		if (len < 2) {
			return s;
		}
		// 对原始字符串进行预处理,即加上分隔符
		String str = addBoundaries(s, '#');
		int sLen = 2 * len + 1;
		int maxLen = 1;

		int start = 0;
		for (int i = 0; i < sLen; i++) {
			int curLen = centerSpread(str, i);
			if (curLen > maxLen) {
				maxLen = curLen;
				start = (i - maxLen) / 2;
			}
		}
		return s.substring(start, start + maxLen);
	}

	// 计算以center为中心的字符串左右回文的步数
	private static int centerSpread(String s, int center) {
		// left = right 的时候,此时回文中心是一个空隙,回文串的长度是奇数
		// right = left + 1 的时候,此时回文中心是任意一个字符,回文串的长度是偶数
		int len = s.length();
		int i = center - 1;
		int j = center + 1;
		int step = 0;
		while (i >= 0 && j < len && s.charAt(i) == s.charAt(j)) {
			i--;
			j++;
			step++;
		}
		return step;
	}

	/**
	 * 创建预处理字符串
	 *
	 * @param s      原始字符串
	 * @param divide 分隔字符
	 * @return 使用分隔字符处理以后得到的字符串
	 */
	private static String addBoundaries(String s, char divide) {
		int len = s.length();
		if (len == 0) {
			return "";
		}
		if (s.indexOf(divide) != -1) {
			throw new IllegalArgumentException("参数错误,您传递的分割字符,在输入字符串中存在!");
		}
		
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < len; i++) {
			sb.append(divide); // 在最前边加上特殊字符"#"
			sb.append(s.charAt(i)); // 在字符中间加上特殊字符"#"
		}
		sb.append(divide); // 在字符最后加上特殊字符"#"
		return sb.toString(); // 将StringBuilder对象转化为字符串
	}

方法三执行结果:
在这里插入图片描述
此模块仅作为个人学习理解使用:

/*
    解释说明:s = "abc1234321b" str = "#a#b#c#1#2#3#4#3#2#1#b#"
        i = 0: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 1: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 2: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 3: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 4: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 5: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 6: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 7: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 8: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 9: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 10: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 11: curLen = 1, s = "a"; maxLen = 1, start = 0;
        i = 12: curLen = 0, s = "a"; maxLen = 1, start = 0;
        i = 13: curLen = 7, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 14: curLen = 0, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 15: curLen = 1, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 16: curLen = 0, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 17: curLen = 1, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 18: curLen = 0, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 19: curLen = 1, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 20: curLen = 0, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 21: curLen = 1, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
        i = 22: curLen = 0, s = "1234321"; maxLen = 7, start = (13 - 7) / 2 = 3;
    结果:最长回文子串的起始位置为原字符串索引为3的位置,长度为7
    s.substring(3,10) = "1234321"作为结果输出。
 */

复杂度分析:
● 时间复杂度:O(N^2),N是原始字符串的长度,新字符串的长度是 2 * N + 1,不计系数与常数项,因此时间复杂度仍为O(N^2)
● 空间复杂度:O(N)

科学家的工作:充分利用新字符串的回文性质,计算辅助数组 p

上面的代码不太智能的地方是,对新字符串每一个位置进行中心扩散,会导致原始字符串的每一个字符被访问多次,一个比较极端的情况就是: #a#a#a#a#a#a#a#a#。事实上,计算机科学家 Manacher 就改进了这种算法,使得在填写新的辅助数组 p 的值的时候,能够参考已经填写过的辅助数组 p 的值,使得新字符串每个字符只访问了一次,整体时间复杂度由
在这里插入图片描述
具体做法是:在遍历的过程中,除了循环变量 i 以外,我们还需要记录两个变量,它们是 maxRightcenter ,它们分别的含义如下:

maxRight: 记录当前向右扩展的最远边界,即从开始到现在使用“中心扩散法”能得到的回文子串,它能延伸到的最右端的位置 。对于 maxRight 我们说明 3 点:

  1. “向右最远”是在计算辅助数组 p 的过程中,向右边扩散能走的索引最大的位置,注意:得到一个 maxRight 所对应的回文子串,并不一定是当前得到的“最长回文子串”,很可能的一种情况是,某个回文子串可能比较短,但是它正好在整个字符串比较靠后的位置;
  2. maxRight 的下一个位置可能是被程序看到的,停止的原因有 2 点:(1)左边界不能扩散,导致右边界受限制也不能扩散,maxRight 的下一个位置看不到;(2)正是因为看到了 maxRight 的下一个位置,导致 maxRight 不能继续扩散。
  3. 为什么 maxRight 很重要?因为扫描是从左向右进行的, maxRight 能够提供的信息最多,它是一个重要的分类讨论的标准,因此我们需要一个变量记录它。
    center: center 是与 maxRight 相关的一个变量,它是上述 maxRight 的回文中心的索引值。对于 center 的说明如下:
  4. center 的形式化定义:

在这里插入图片描述
说明: x + p[x] 的最大值就是我们定义的 maxRight,i 是循环变量,0<= x< i 表示是在 i 之前的所有索引里得到的最大值 maxRight,它对应的回文中心索引就是上述式子。

  1. maxRightcenter 是一一对应的关系,即一个 center 的值唯一对应了一个 maxRight 的值;因此 maxRightcenter 必须要同时更新。
    下面的讨论就根据循环变量 imaxRight 的关系展开讨论:

情况 1:当 i >= maxRight 的时候,这就是一开始,以及刚刚把一个回文子串扫描完的情况,此时只能够根据“中心扩散法”一个一个扫描,逐渐扩大 maxRight

情况 2:当 i < maxRight 的时候,根据新字符的回文子串的性质,循环变量关于 center 对称的那个索引(记为 mirror)的 p 值就很重要。

我们先看 mirror 的值是多少,因为 center 是中心,imirror 关于 center 中心对称,因此 (mirror + i) / 2 = center ,所以 mirror = 2 * center - i

根据 p[mirror] 的数值从小到大,具体可以分为如下 3 种情况:

情况 2(1)p[mirror] 的数值比较小,不超过 maxRight - i

说明:maxRight - i 的值,就是从 i 关于 center 的镜像点开始向左走(不包括它自己),到 maxRight 关于 center 的镜像点的步数

在这里插入图片描述
从图上可以看出,由于“以 center 为中心的回文子串”的对称性,导致了“以 i 为中心的回文子串”与“以 center 为中心的回文子串”也具有对称性,“以 i 为中心的回文子串”与“以 center 为中心的回文子串”不能再扩散了,此时,直接把数值抄过来即可,即 p[i] = p[mirror]

情况 2(2)p[mirror] 的数值恰好等于 maxRight - i
在这里插入图片描述
说明:仍然是依据“以 center为中心的回文子串”的对称性,导致了“以 i 为中心的回文子串”与“以 center为中心的回文子串”也具有对称性。

  1. 因为靠左边的 f 与靠右边的 g 的原因,导致“以 center 为中心的回文子串”不能继续扩散;
  2. 但是“以 i 为中心的回文子串” 还可以继续扩散。
    因此,可以先把 p[mirror] 的值抄过来,然后继续“中心扩散法”,继续增加 maxRight

情况 2(3)p[mirror] 的数值大于 maxRight - i

在这里插入图片描述
说明:仍然是依据“以 center 为中心的回文子串”的对称性,导致了“以 i 为中心的回文子串”与“以 center 为中心的回文子串”也具有对称性。
下面证明,p[i] = maxRight - i,证明的方法还是利用三个回文子串的对称性。
在这里插入图片描述
① 由于“以 center 为中心的回文子串”的对称性, 黄色箭头对应的字符 ce 一定不相等;

② 由于“以 mirror 为中心的回文子串”的对称性, 绿色箭头对应的字符 cc 一定相等;

③ 又由于“以 center 为中心的回文子串”的对称性, 蓝色箭头对应的字符 cc 一定相等;

推出“以 i 为中心的回文子串”的对称性, 红色箭头对应的字符 ce 一定不相等。

因此,p[i] = maxRight - i,不可能再大。上面是因为我画的图,可能看的朋友会觉得理所当然。事实上,可以使用反证法证明:

如果“以 i 为中心的回文子串” 再向两边扩散的两个字符 ce 相等,就能够推出黄色、绿色、蓝色、红色箭头所指向的 8 个变量的值都相等,此时“以 center 为中心的回文子串” 就可以再同时向左边和右边扩散 1 格,与 maxRight 的最大性矛盾。

综合以上 3 种情况,当 i < maxRight 的时候, p[i] 可以参考 == p[mirror]== 的信息,以 maxRight - i 作为参考标准,p[i] 的值应该是保守的,即二者之中较小的那个值:

p[i] = min(maxRight - i, p[mirror]);

参考代码:

public static void main(String[] args) {

		String string = "abc1234321b";
		System.out.println("原串:" + string);
		System.out.println("最长回文串:" + longestPalindrome(string));

	}

	public static String longestPalindrome(String s) {
		// 特判
		int len = s.length();
		if (len < 2) {
			return s;
		}

		// 得到预处理字符串
		String str = addBoundaries(s, '#');
		// 新字符串的长度
		int sLen = 2 * len + 1;

		// 数组 p 记录了扫描过的回文子串的信息
		int[] p = new int[sLen];

		// 双指针,它们是一一对应的,须同时更新
		int maxRight = 0;
		int center = 0;

		// 当前遍历的中心最大扩散步数,其值等于原始字符串的最长回文子串的长度
		int maxLen = 1;
		// 原始字符串的最长回文子串的起始位置,与 maxLen 必须同时更新
		int start = 0;

		for (int i = 0; i < sLen; i++) {
			if (i < maxRight) {
				int mirror = 2 * center - i;
				// 这一行代码是 Manacher 算法的关键所在,要结合图形来理解
				p[i] = Math.min(maxRight - i, p[mirror]);
			}

			// 下一次尝试扩散的左右起点,能扩散的步数直接加到 p[i] 中
			int left = i - (1 + p[i]);
			int right = i + (1 + p[i]);

			// left >= 0 && right < sLen 保证不越界
			// str.charAt(left) == str.charAt(right) 表示可以扩散 1 次
			while (left >= 0 && right < sLen && str.charAt(left) == str.charAt(right)) {
				p[i]++;
				left--;
				right++;

			}
			// 根据 maxRight 的定义,它是遍历过的 i 的 i + p[i] 的最大者
			// 如果 maxRight 的值越大,进入上面 i < maxRight 的判断的可能性就越大,这样就可以重复利用之前判断过的回文信息了
			if (i + p[i] > maxRight) {
				// maxRight 和 center 需要同时更新
				maxRight = i + p[i];
				center = i;
			}
			if (p[i] > maxLen) {
				// 记录最长回文子串的长度和相应它在原始字符串中的起点
				maxLen = p[i];
				start = (i - maxLen) / 2;
			}
		}
		return s.substring(start, start + maxLen);
	}

	/**
	 * 创建预处理字符串
	 *
	 * @param s      原始字符串
	 * @param divide 分隔字符
	 * @return 使用分隔字符处理以后得到的字符串
	 */
	private static String addBoundaries(String s, char divide) {
		int len = s.length();
		if (len == 0) {
			return "";
		}
		if (s.indexOf(divide) != -1) {
			throw new IllegalArgumentException("参数错误,您传递的分割字符,在输入字符串中存在!");
		}
		StringBuilder stringBuilder = new StringBuilder();
		for (int i = 0; i < len; i++) {
			stringBuilder.append(divide);
			stringBuilder.append(s.charAt(i));
		}
		stringBuilder.append(divide);
		return stringBuilder.toString();
	}

复杂度分析:
● 时间复杂度:O(N),由于Manacher 算法只有在遇到还未匹配的位置时才进行匹配,已经匹配过的位置不再匹配,因此对于字符串 S 的每一个位置,都进行一次匹配,算法的复杂度为O(N)
● 空间复杂度:O(N)

原文地址链接:(本文有删改)原文地址

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述: 给定一个字符串 s,找到 s 中最长的回文子串。 回文串是指正着读和反着读都一样的字符串。 示例: 输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。 输入: "cbbd" 输出: "bb" 解题思路: 1.暴力法 暴力法是最简单直接的方法,遍历所有子串,判断是否为回文串,记录最长回文串。 时间复杂度:O(n^3) 2.中心扩展法 中心扩展法是基于回文串的对称性质,从中心向两边扩展,判断是否为回文串,记录最长回文串。 时间复杂度:O(n^2) 3.动态规划法 动态规划法是基于回文串的子串也是回文串的性质,利用状态转移方程,记录最长回文串。 时间复杂度:O(n^2) 4.马拉车算法 马拉车算法是基于回文串的对称性质和回文串的最大半径,利用中心扩展法的思想,减少重复计算,记录最长回文串。 时间复杂度:O(n) 参考代码: 1.暴力法 class Solution { public: string longestPalindrome(string s) { int n = s.size(); if (n < 2) return s; string res = ""; for (int i = ; i < n; i++) { for (int j = i; j < n; j++) { int len = j - i + 1; if (len > res.size() && isPalindrome(s, i, j)) { res = s.substr(i, len); } } } return res; } bool isPalindrome(string s, int left, int right) { while (left < right) { if (s[left] != s[right]) return false; left++; right--; } return true; } }; 2.中心扩展法 class Solution { public: string longestPalindrome(string s) { int n = s.size(); if (n < 2) return s; string res = ""; for (int i = ; i < n; i++) { string s1 = palindrome(s, i, i); string s2 = palindrome(s, i, i + 1); res = res.size() > s1.size() ? res : s1; res = res.size() > s2.size() ? res : s2; } return res; } string palindrome(string s, int left, int right) { while (left >= && right < s.size() && s[left] == s[right]) { left--; right++; } return s.substr(left + 1, right - left - 1); } }; 3.动态规划法 class Solution { public: string longestPalindrome(string s) { int n = s.size(); if (n < 2) return s; vector<vector<bool>> dp(n, vector<bool>(n, false)); int start = , maxLen = 1; for (int i = ; i < n; i++) { dp[i][i] = true; } for (int j = 1; j < n; j++) { for (int i = ; i < j; i++) { if (s[i] != s[j]) { dp[i][j] = false; } else { if (j - i < 3) { dp[i][j] = true; } else { dp[i][j] = dp[i + 1][j - 1]; } } if (dp[i][j] && j - i + 1 > maxLen) { maxLen = j - i + 1; start = i; } } } return s.substr(start, maxLen); } }; 4.马拉车算法 class Solution { public: string longestPalindrome(string s) { int n = s.size(); if (n < 2) return s; string t = "#"; for (int i = ; i < n; i++) { t += s[i]; t += "#"; } n = t.size(); vector<int> p(n, ); int center = , maxRight = , start = , maxLen = 1; for (int i = ; i < n; i++) { if (i < maxRight) { int mirror = 2 * center - i; p[i] = min(maxRight - i, p[mirror]); } int left = i - (p[i] + 1), right = i + (p[i] + 1); while (left >= && right < n && t[left] == t[right]) { p[i]++; left--; right++; } if (i + p[i] > maxRight) { center = i; maxRight = i + p[i]; } if (p[i] > maxLen) { maxLen = p[i]; start = (i - maxLen) / 2; } } return s.substr(start, maxLen); } };

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值