【刷题之路Ⅱ】LeetCode 5.最长回文子串

【刷题之路Ⅱ】LeetCode 5.最长回文子串

  • 一、题目描述
  • 二、解题
    • 1、方法1——暴力法
      • 1.1、思路分析
      • 1.2、代码实现
    • 2、方法2——中心扩散
      • 2.1、思路分析
      • 2.2、代码实现
    • 3、方法3——动态规划
      • 3.1、快速入门动态规划
      • 3.2、思路分析
      • 3.3、代码实现

一、题目描述

原题连接: 5.最长回文子串

题目描述:
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:
输入: s = “babad”
输出: “bab”
解释: “aba” 同样是符合题意的答案。

示例 2:
输入: s = “cbbd”
输出: “bb”

二、解题

1、方法1——暴力法

1.1、思路分析

枚举出所有的子串,看看它们是否是回文子串,从中找出长度最长的那个回文子串进行返回。
但在求得一个回文子串之后,下次只需要判断比它长的字串是否是回文串即可。再判断比他短的就没意义了。

1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 先写一个函数判断一个字符串是否是回文子串
// 如果是,则返回1,否则返回0
int is_palindrome(const char *str, int left, int right) {
	assert(str);
	while (left < right) {
		if (str[left] != str[right]) {
			return 0;
		}
		left++;
		right--;
	}
	return 1;
}

char* longestPalindrome1(char* s) {
	assert(s);
	int len = strlen(s);
	if (1 == len) {
		return s;
	}
	int i = 0;
	int j = 0;
	int index = 0; // 存储最长回文子串的起始下标
	int max_len = 1; // 存储最长回文子串的长度
	for (i = 0; i < len - 1; i++) {
		for (j = i + 1; j < len; j++) {
			if (j - i + 1 > max_len && is_palindrome(s, i, j)) {
				index = i;
				max_len = j - i + 1;
			}
		}
	}
	// 将最长回文子串拷贝到s的最前面;
	for (i = 0; i < max_len; i++) {
		s[i] = s[index + i];
	}
	s[max_len] = '\0';
	return s;
}

时间复杂度:O(n^3),其中n为字符串长度。
空间复杂度:O((1),我们只需要用到常数级的额外空间。

2、方法2——中心扩散

2.1、思路分析

我们可以先遍历字符串的每个字符,对于每个字符,我们都以它为中心,向它的两边扩散看它两边有多少个字符相等(也就是看以它为中心能求出的最长回文串有多长)。
这个操作我们可以用一个函数来完成,这个函数只需要返回以某个字符为中心所求得的最长回文串的长度即可。
但问题来了,如果回文串的长度为奇数,我们是可以以一个单独的字符为它的中心,例如:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第1张图片
但如果长度为偶数呢?那它的中就应该是最中间那两个字符之间的一条虚拟的中线:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第2张图片
这应该怎么办呢?
其实这很简单,我们在设计上面那个求回文串的函数时,可以设计两个下标参数,分别设置为left和right,然后再进行统一操作:往left的左边扫描,往right的右边扫描。
如果两个参数相同那我们实质上也是只以一个下标为中心而已:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第3张图片
(当然,这里的left和right并非是指针,这里只是为了更形象的描述left和right表示的下标相同)
若不同那就是两个:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第4张图片
那么,之后在遍历到每个字符时,我们都分别判断只以它为中心下标的字串和以它和它下一个字符为中心的字串就行了。
这样,我们就用一个统一的方法完处理了两个不同的场景。

2.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 先写一个向两边扩散以求最长回文串长度的函数
int get_max_len(const char* str, int left, int right) {
	assert(str);
	int len = strlen(str);
	int max_len = left == right ? 1 : 0; // 保存最长回文串的长度
	int i = left == right ? 1 : 0;
	while (left - i >= 0 && right + i < len) {
		if (str[left - i] == str[right + i]) {
			max_len += 2;
			i++;
		}
		else {
			break;
		}
	}
	return max_len;
}

char* longestPalindrome2(char* s) {
	assert(s);
	int len = strlen(s);
	if (len < 2) {
		return s;
	}
	int index = 0;
	int max_len = 1;
	int i = 0;
	for (i = 0; i < len - 1; i++) {
		if (i == 0) {
			if (get_max_len(s, i, i + 1) > max_len) {
				max_len = get_max_len(s, i, i + 1);
			}
		}
		else {
			if (get_max_len(s, i, i) > max_len) {
				max_len = get_max_len(s, i, i);
				index = i - max_len / 2;
			}
			if (get_max_len(s, i, i + 1) > max_len) {
				max_len = get_max_len(s, i, i + 1);
				index = i - (max_len - 2) / 2;
			}
		}
	}
	// 将最长回文子串拷贝到s的最前面;
	for (i = 0; i < max_len; i++) {
		s[i] = s[index + i];
	}
	s[max_len] = '\0';
	return s;
}

时间复杂度:O(n2),其中n为字符串的长度,我们其实是进行了两层的遍历,故时间复杂度为O(n2)。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

3、方法3——动态规划

3.1、快速入门动态规划

可能有人对动态规划这种算法思路还不是很了解,下面由我来带着大家快速入门一下:
动态规划算法

1. 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
2. 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3. 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
4. 动态规划可以通过填表的方式来逐步推进,得到最优解.

也可以简单地理解为动态规划就是对分治(递归)做出了改进的一种算法,它避免了,递归中可能出现的大量重复计算。

3.2、思路分析

其实回文串是具有动态转移性质的,也就是说一个回文串去掉两端的字符后,剩下的串依旧是一个回文串,所以我们在获得一个子串后,可以先看它两端的字符是否相等,如果不相等,就不是一个回文子串,如果相等就接着看去掉两段字符后的字符串是否为回文串。
如此反复。
那么,可能有人就会想到可以用递归啊,但是这个题目如果用递归的话就会产生大量的重复计算,而且递归的空间复杂度太大了,很浪费空间。
这时候我们就应该想到要使用动态规划了,我们可以用一个二维表dp来存储每个子串是否为回文串的信息,dp[i][j]中存储的是从字符串第i个字符到第j个字符是否为回文子串的信息。如果是回文串则将dp[i][j]赋值为1,否则就赋值为0,我们就以“ababa”这个字符串举例:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第5张图片
有人可能会疑惑我为什么只填了一半?其实是这样的:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第6张图片
若left和right指向的字符所形成的字符串以某个字符为中心,那么把left和right指向的位置互换过来其实得到的也是同一个回文子串。而这个中心字符就可以理解成对称轴或对角线,当然,对于长度为偶数的回文子串也是一样的。
对应到而二维表中就是:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第7张图片
也就是说dp[i][j]和dp[j][i]在回文子串的判断过程中是等价的。而绿色框框框起来的就可以看成是它们的对称轴或对角线。所以我们只需要填完对角线上的一半的表格即可。
但在这样一个二维表中我们应该怎样判断一个子串是否为回文子串呢?
上面说到在我们知道di个字符和第j个字符相等时,我们还要判断从第i + 1字符到第j - 1个字符是否为回文串。
也就是说dp[i][j]是否为1要通过判断dp[i + 1][j - 1]是否为1。这对应到二维表就是dp[i][j]的判断要借助于它左下角的
dp[i + 1][j - 1]:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第8张图片
所以,想要保证填出来的二维表是正确的,就必须一列一列的从上至下填:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第9张图片
这样,我们才能从分利用每个元素左下角的元素,为我们判断出dp[i][j]。
而且当j - i < 3时,也就是子串的长度小于等于3(子串的实际长度为j - i + 1),我们就并不需要,判断p[i + 1][j - 1]了,只需要判断s[i]和s[j]是否相等即可,因为在是s[i]和是s[j]中间就只有可能剩一个或者零个字符了。
所以我们就能得到一个这样的动态转移方程:
dp[i][j] = (s[i] == s[j]) && (j - i < 3 && dp[i + 1][j - 1])。

从中我们也能够得到一个结论,就是二维表中对角线上的元素其实判不判断都是一样的,因为在判断的dp[i][j]的时候根本就不会借助到对角线上的元素。图释如下:
【刷题之路Ⅱ】LeetCode 5.最长回文子串_第10张图片
比如说我们现在要判断dp[4][2]这个元素,我们知道它对应的子串就是“aba”,它的对角线上的元素是‘b’。但此时的字串长度已经等于3。所以我们只需判断两端即可。
而且,当我们求得一个dp[i][j] == 1时,就可以与已有的最长回文子串的长度进行比较,更新长度和起始下标。
所以,当我们填完表之后,最终的最长回文子串的长度也就确定了。

3.3、代码实现

有了以上思路,那我们写起代码来也就水到渠成了

char* longestPalindrome3(char* s) {
	assert(s);
	int len = strlen(s);
	if (len < 2) {
		return s;
	}
	int index = 0;
	int max_len = 1;
	int i = 0;
	int j = 0;
	// 定义一个存储子串状态的数组dp
	int dp[100][100] = { 0 };
	// 填表
	for (j = 1; j < len; j++) {
		for (i = 0; i < j; i++) {
			if (s[i] != s[j]) {
				dp[i][j] = 0;
			}
			else {
				if (j - i < 3) {
					dp[i][j] = 1;
				}
				else {
					dp[i][j] = dp[i + 1][j - 1];
				}
			}
			if (dp[i][j]) {
				if (j - i + 1 > max_len) {
					max_len = j - i + 1;
					index = i;
				}
			}
		}
	}
	// 将最长回文子串拷贝到s的最前面;
	for (i = 0; i < max_len; i++) {
		s[i] = s[index + i];
	}
	s[max_len] = '\0';
	return s;
}

时间复杂度:O(n2),其中n为字符串的长度。动态规划的总数为O(n2),而对于每个状态,我们需要用来我们转移的时间复杂度为O(1)。
空间复杂度:O(n^2),及动态规划状态需要的空间。

你可能感兴趣的:(刷题之路——中等篇,leetcode,算法,c语言,动态规划)