算法题解之最长回文子串

题目描述

最近发现好多童鞋在刷题,偶然看到求最长回文子串的问题,也来尝试下。下面是力扣(LeetCode)上的题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"`

解法一:暴力法

我一开始还真没想到用这个方法,它是比较简单暴力的。思路是遍历每一个子字符串,判断是否是回文串,如果是就将其长度和之前最大的回文串长度比较,从而得到最长的回文子串。

	public String longestPalindrome(String s) {
		if (s == null || s.isEmpty())
			return "";
		String longestPalindrome = null;
		int lpLen = 0;
		for (int i = 0; i < s.length(); i++) {
			for (int j = i + 1; j <= s.length(); j++) {
				String subStr = s.substring(i, j);
				if (isPalindrome(subStr) && subStr.length() > lpLen) {
					longestPalindrome = subStr;
					lpLen = subStr.length();
				}
			}
		}
		return longestPalindrome;
	}

	private boolean isPalindrome(String s) {
		int sLen = s.length();
		for (int i = 0; i < sLen / 2; i++) {
			if (s.charAt(i) != s.charAt(sLen - i - 1)) {
				return false;
			}
		}
		return true;
	}

时间复杂度为O(n³),空间复杂度为O(n)

解法二:中心扩展法

我最开始想到的就是这种方法。因为回文字符串左右是对称的,所以从中间字符向两侧查找,依次比较左右两个字符是否相等,如果相等就继续比较下一个字符,如果不等就停止。需要注意的是长度为奇数的回文串中间是单个字符,长度为偶数的回文串中间是两个相同的字符。

   public String longestPalindrome(String s) {
		if(s == null || s.isEmpty())
			return "";
		int startIndex=0,lastIndex=0;
		for(int i=0; i<s.length(); i++) {
			int lenOdd = catchPalindromeLen(s, i, i);
			int lenEven = catchPalindromeLen(s, i, i+1);
			int len = Math.max(lenOdd, lenEven);
			if(len > (lastIndex+1 - startIndex)) {
				startIndex = i-(len-1)/2;
				lastIndex = i+len/2;
			}
		}
		return s.substring(startIndex, lastIndex+1);
    }
	
	private int catchPalindromeLen(String s, int left, int right) {
		int leftIndex = left,rightIndex = right;
		while(leftIndex > -1 && rightIndex < s.length() && (s.charAt(leftIndex) == s.charAt(rightIndex))) {
			leftIndex--;
			rightIndex++;
		}
		return rightIndex - (leftIndex + 1);
	}

时间复杂度为O(n²),空间复杂度为O(n)

解法三:动态规划法

这也是比较常见的一种方法,核心思想是以空间换时间,将已经计算过的结果保存起来,进行后续计算的时候不必从头开始重新计算,可以直接使用前面步骤中计算过的结果。对于长度为n的字符串s,先定义一个二维数组dp[n][n],dp[i][j]表示起始字符索引为i、结束字符索引为j的字符串是否是回文串,是的话值为1,不是值设为0。按照动态规划的思想,如果s[i] = s[j],并且dp[i+1][j-1]值为1的话,那么dp[i][j]=1。如果觉得公式不好理解,可以去看一下如何理解动态规划?,最高赞的讲解非常通俗易懂。

    public String longestPalindrome(String s) {
		if (s == null || s.isEmpty())
			return "";
		int len = s.length();
		int maxNum = 1;
		int startIndex = 0;
		int[][] isPal = new int[len][len];
		for (int i = 0; i < len; i++) {
			isPal[i][i] = 1;
			if (i < len - 1) {
				if (s.charAt(i) == s.charAt(i + 1)) {
					isPal[i][i + 1] = 1;
					startIndex = i;
					maxNum = 2;
				}
			}
		}
		for (int num = 3; num <= len; num++) {
			for (int i = 0; i + num - 1 < len; i++) {
				int j = num + i - 1;
				if (s.charAt(i) == s.charAt(j) && isPal[i + 1][j - 1] == 1) {
					isPal[i][j] = 1;
					startIndex = i;
					maxNum = num;
				}
			}
		}
		return s.substring(startIndex, startIndex + maxNum);
	}

时间复杂度为O(n²),空间复杂度为O(n²)

解法四:Manacher算法

中文俗称马拉车算法。它可以将时间复杂度降到O(n)。具体讲解可以参考Manacher 算法。这个算法在中心扩展法的基础上,充分利用了回文串的对称性,这里截取关键部分如图所示:
image
设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是mx = id + p[id]。(其中p[id]表示以 id 为中心的最长回文的半径)
假设我们现在求p[i],也就是以 i 为中心的最长回文半径,如果i < mx,如上图,那么:

if (i < mx)  
    p[i] = min(p[2 * id - i], mx - i);

2 * id - i为 i 关于 id 的对称点,即上图的 j 点,而**p[j]表示以 j 为中心的最长回文半径**,因此我们可以利用p[j]来加快查找。

public String longestPalindrome(String s) {
		if (s == null || s.isEmpty()) {
			return "";
		}
		//用#扩充字符串,开始结束分别添加^$
		char[] es = extendStr(s);
		int n = es.length;
		int[] p = new int[n];
		int ci = 0, ri = 0;//ci为中心字符索引,ri为中心字符的回文串最右侧字符索引
		for (int i = 1; i < n - 1; i++) {//去除开始结束符,从索引为1的字符到索引为n-2的字符
			int i_mirror = 2 * ci - i;
			if (ri > i) {
				p[i] = Math.min(ri - i, p[i_mirror]);
			} else {
				p[i] = 0;
			}

			while (es[i + 1 + p[i]] == es[i - 1 - p[i]]) {
				p[i]++;
			}

			if (i + p[i] > ri) {
				ci = i;
				ri = p[i] + i;
			}

		}

		int maxLen = 0;
		int centerIndex = 0;
		for (int i = 1; i < n - 1; i++) {
			if (p[i] > maxLen) {
				maxLen = p[i];
				centerIndex = i;
			}
		}
		int start = (centerIndex - maxLen) / 2; 
		return s.substring(start, start + maxLen);
	}

	private char[] extendStr(String s) {
		int l = s.length();
		char[] ec = new char[l * 2 + 3];//开始和结束分别添加^$,防止越界
		ec[0] = '^';
		int i = 1;
		for (; i < (l * 2);) {
			ec[i] = '#';
			ec[i + 1] = s.charAt(i / 2);
			i = i + 2;
		}
		ec[i] = '#';
		ec[i + 1] = '$';
		return ec;
	}

该方法能理解就行,不需要一定可以手写出来。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值