Golang 最大回文子串(Manacher算法)

本文介绍了一种寻找字符串中最长回文子串的算法实现。通过在每个字符间插入相同字符并使用动态规划思想,算法能高效地找出最长回文子串。代码示例使用Go语言编写。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于该算法的原理其他博客写的很清楚,我就不再重复了,此处直接贴代码


package main

import "fmt"

//用于存储最后长度最长的子串中间字符的信息
type str struct {
	i int    //位置
	num int   //数值
}

func longestPalindrome(s string) string {

    
	length := len(s)
	btyestr := []byte(s)

    if length<2 {
        return s
    }
    
	bytestr2 := make([]byte, 2*length+1)
	p := make([]int, 2*length+1)

	for m := 0; m < length; m++ {

		bytestr2[2*m] = 1
		bytestr2[2*m+1] = btyestr[m]

	}
	bytestr2[2*length] = 1

	fmt.Println(bytestr2)

    //在每个字符间插入相同的字符
	for i := 0; i < len(bytestr2); i++ {

		p[i] = 1
		if i != 0 && i != 2*length {
			for j := 1; ; j++ {
				if bytestr2[i-j] == bytestr2[i+j] {
					p[i] = p[i] + 1
				} else {
					break
				}
				if i-j == 0 || i+j == 2*length {
					break
				}
			}
		}
	}

	maxdata := str{0, 1}

	for j := 0; j < 2*length+1; j++ {

		if p[j] > maxdata.num {

			maxdata.i = j
			maxdata.num = p[j]
		}
	}

	bytestr3 := make([]byte, maxdata.num-1)
   //找出最长子串,跳过之前我自己添加的字符
	for h := 0; h < maxdata.num-1; h++ {
		if (maxdata.num-1)%2 == 0 {
		    //跳过之前我自己添加的字符,所以+2*h
			bytestr3[h] = bytestr2[maxdata.i+2-maxdata.num+2*h]
		} else {
			bytestr3[h] = bytestr2[maxdata.i+2-maxdata.num+2*h]
		}

	}

	ss := string(bytestr3)
	return ss
}

func main() {
	s := "aac123caa"
	longestPalindrome(s)
}
<think>嗯,用户让我用中文介绍一个Golang最长回文子串算法题的完整代码。首先,我需要回忆一下这个问题。最长回文子串,我记得常见的解法有动态规划和中心扩展法。动态规划的时间复杂度是O(),空间复杂度也是O(),而中心扩展法的时间虽然也是O(),但空间是O(1),可能更高效一些。所以可能用户需要的是中心扩展法的实现? 用户要的是完整的代码,所以不能只讲思路,必须给出完整的可运行的代码。然后可能需要解释代码的结构和关键部分。比如,如何在每个可能的中心点向两边扩展,检查是否是回文。 然后,我要考虑Golang实现方式。比如,主函数可能是longestPalindrome,输入是字符串,返回字符串。然后需要处理空字符串的情况吗?比如输入长度为0或者1的话,直接返回。 接下来,中心扩展的函数部分。每个中心点可能是一个字符或者两个字符之间,比如对于奇数长度和偶数长度的回文。所以可能需要遍历每个可能的中心点,然后调用expandAroundCenter函数两次,分别处理单字符中心和双字符中心的情况。 然后,每次扩展的时候,比较左右字符是否相等,直到不相等为止,记录此时的左右索引,然后计算长度,看是否比当前最长的大。如果是,更新最长回文的起始位置和长度。 最后,根据起始位置和长度,用切片操作返回子串。 另外,代码的注释是否必要?用户可能需要注释来解释每一步的作用,但如果是完整版代码的话,可能用户希望代码本身足够清晰,或者注释可以帮助理解。 然后测试一下示例情况,比如输入"babad",输出应该是"bab"或者"aba",但算法会返回第一个找到的最长的。或者输入"cbbd",应该返回"bb"。 现在,代码的大致结构应该是这样的: 定义一个函数longestPalindrome,参数s string,返回值string。 初始化start和maxLength变量,初始值分别为0和0。 遍历字符串中的每个字符的位置i,作为中心点: 对于每个i,计算expandAroundCenter(s, i, i)得到长度len1,处理奇数长度的回文。 再计算expandAroundCenter(s, i, i+1)得到长度len2,处理偶数长度的回文。 比较len1和len2中的较大者,如果超过当前maxLength,则更新maxLength,并计算对应的start位置。 最后返回s[start:start+maxLength] 然后,expandAroundCenter函数的实现是,给定左右起点,向两边扩展,直到字符不相等。返回此时回文的长度,或者左右的位置差减一? 或者,该函数返回回文的长度,比如每次循环当左右字符相等时,左减一,右加一,直到超出边界或者不相等。最后回文的长度是right - left - 1,或者类似的? 比如,初始left和right是给定的,比如对于奇数情况,left和right都是i,对于偶数情况,left是i,right是i+1。然后循环条件为left >=0 && right < len(s) && s[left] == s[right],然后left--,right++。循环结束后,此时的最大回文长度是right - left -1。或者需要计算正确的长度。 比如,当退出循环时,left和right的位置是不满足条件的,所以实际回文的范围是left+1到right-1。因此,回文的长度是 (right -1) - (left+1) +1 = right - left -1。所以,当返回这个长度之后,需要比较哪一个更大。 然后,比较当前的长度是否大于maxLength,如果是的话,需要计算start的位置。比如,对于奇数的情况,扩展后的长度是len1,此时起始位置是i - (len1-1)/2,长度是 len1。同理,偶数的情况,起始位置是i - (len2)/2 + 1? 或者,可能在expand函数中返回最大的回文长度后,如何确定起始位置? 或者,更好的办法是在expand函数中返回回文的最左和最右的位置,或者在主函数中根据扩展后的结果来更新start和maxLength。或者,直接每次计算可能的start和长度,然后更新。 比如,每次扩展后,计算可能的长度,比如当前扩展后的长度是 right - left -1。然后,如果这个长度大于maxLength,那么maxLength等于这个长度,start等于left+1,因为此时回文的起始位置是left+1,结束位置是right-1,所以长度是 (right-1)-(left+1)+1 = right-left-1,即maxLength。然后,子串就是s[start : start+maxLength]。 所以,在循环中,对于每个i,进行两次expand,然后比较两次的结果,取最大的那个,然后看是否超过全局最大值,然后更新start和maxLength。 或者,是否在expand函数中直接处理这些计算?或者让主函数来处理? 可能更高效的方式是,每次expand返回最大的扩展后的长度,然后根据这个长度计算起始位置。 例如,当处理奇数的扩展时,得到长度len1,此时起始位置是i - len1/2(因为len1是奇数,比如长度3,i是中间的位置,那么起始位置是i-1,结束是i+1)。而偶数的扩展得到len2,起始位置是i - (len2/2 -1),比如长度2,i是左边那个的位置,起始位置是i -0,i+1 -1 =i+0。 但可能更好的方式是,当expand函数返回长度后,起始位置可以通过i - (length-1)/2,而结束位置是i + length/2。这样,无论奇偶都可以适用? 比如,假设len是扩展后的长度,比如对于奇数情况,假设len是3,那么起始位置是i -1,结束是i+1。而如果是偶数情况,假设长度是2,那么起始位置是i -0(因为 (2-1)/2=0.5,取整的话可能有问题),所以可能需要用不同的方式计算。 或者,在扩展之后,left和right已经被扩展到了不符合条件的点,所以回文实际的左右是left+1和right-1。因此,长度是 right-1 - (left+1) +1 = right - left -1. 此时,当完成扩展之后,得到left和 right,那么此时回文的起始位置是 left+1,长度是 right - left -1。然后,比较这个长度是否大于当前maxLength,如果是的话,更新start和maxLength。 所以,可能应该让expand函数返回最大的扩展后的left和 right,或者返回长度? 或者,直接在循环中处理,每次扩展后,计算可能的长度,并更新最大值和起始位置。 所以,修改后的思路: 对于每个i: 左1,右1 = expand(s, i, i) → 得到的是扩展后的左右,但此时实际回文是左1+1到右1-1,长度是 (right1 - left1 -1) 左2,右2 = expand(s, i, i+1) → 同理,回文是左2+1到右2-1,长度是 (right2 - left2 -1) 然后比较这两个的长度,如果哪个更大,就看看是否超过当前的maxLength,如果超过,则更新maxLength,并记录对应的起始位置。 比如,假设最大的长度是L,对应的起始位置是 left+1,结束是 right-1,所以起始索引是 left+1,长度是 L = right - left -1。所以子串是 s[left+1 : right](因为Go的切片是左闭右开的)。 所以,在expand函数中,我们只需要进行扩展,返回最终的left和 right的值。然后主函数中根据这两个值计算长度和起始位置。 所以,代码的大致结构可能是: func longestPalindrome(s string) string { if len(s) < 1 { return "" } start, end := 0, 0 for i := 0; i < len(s); i++ { left1, right1 := expand(s, i, i) left2, right2 := expand(s, i, i+1) if right1 - left1 > end - start { start, end = left1, right1 } if right2 - left2 > end - start { start, end = left2, right2 } } return s[start:end+1] // 因为end是right-1,切片需要到end+1? } func expand(s string, left, right int) (int, int) { for left >=0 && right < len(s) && s[left] == s[right] { left-- right++ } return left+1, right-1 } 等等,或者需要再仔细检查。 比如,在expand函数中,当跳出循环时,left和 right已经是不满足条件的位置,所以正确的回文起始是left+1,结束是right-1。所以,回文的长度是 (right-1) - (left+1) +1 = right - left -1。而start和 end的索引是left+1和 right-1。所以,当比较长度时,可以用 (right-1 - (left+1) +1) = right - left -1。而当前保存的start和 end是之前最大的情况的start和 end。所以,如果新的长度是 (right- left -1) > (end - start),则更新。 或者,在代码中,直接比较新的长度是否更大,如果是,就更新start和 end。例如,每次扩展之后,计算可能的长度,然后比较。 或者,在expand函数返回left+1和 right-1,这样主函数中直接计算长度是 (right-1) - (left+1) +1 = right - left -1。例如: 假设expand返回新的left和 right是有效的范围,例如,left和 right是回文的最左和最右索引。所以,长度是 right - left +1。 比如,在expand函数中,返回的是 left+1和 right-1,这样这两个之间的子串是回文的,并且包含这两个端点。例如,初始传入i, i的话,如果s[i]是字符,则left和 right在循环中会扩展,直到无法继续。所以,例如,对于单字符的情况,初始时left=i, right=i,进入循环的条件是s[left]==s[right],即true。然后left--变成i-1,right++变成i+1。如果此时i-1或者i+1越界,或者字符不等,循环结束。返回的left+1是i-1+1 =i,right-1是 i+1 -1 =i。所以,返回的left和 right是i和i,对应的子串是s[i:i+1],即正确。例如,当i=0,字符串是"a",那么返回left=0, right=0,子串长度1。 那么,主函数中的处理是,当调用expand返回left和 right时,计算长度是 right - left +1。例如,对于上述例子,right - left +1 =0-0+1=1。 所以,正确的逻辑应该是: 在expand函数中,返回的是回文的最左和最右的索引。所以,长度是 right - left +1。 这样,主函数中比较各个可能的长度,记录最大的情况下的start和 end。 所以,修改后的代码: func longestPalindrome(s string) string { if len(s) == 0 { return "" } start, end := 0, 0 for i := 0; i < len(s); i++ { // 处理奇数长度 left1, right1 := expand(s, i, i) // 处理偶数长度 left2, right2 := expand(s, i, i+1) // 比较两种情况的长度 if right1 - left1 > end - start { start, end = left1, right1 } if right2 - left2 > end - start { start, end = left2, right2 } } return s[start:end+1] } func expand(s string, left, right int) (int, int) { // 当左右在边界内且字符相等时,扩展 for left >=0 && right < len(s) && s[left] == s[right] { left-- right++ } // 返回有效的最左和右 return left+1, right-1 } 这里,每次调用expand之后,返回的left+1和 right-1是回文的最左和最右索引。例如,对于单字符的情况,初始i=0,调用expand(s,0,0),循环条件满足,left减到-1,right增到1。退出循环,返回0,0。此时长度为1,对应的子串是s[0:1],即正确。 当处理偶数情况时,例如i=0,i+1=1。如果s[0]==s[1],则left减到-1,right增到2。返回0和1,此时子串是s[0:2],即两个字符。例如"aa",得到正确的回文。 这样,主函数中的start和 end是保存最大回文的左右索引,最后返回s[start:end+1],因为切片是左闭右开的。 需要测试几个案例: 案例1:输入"babad",可能的回文是"bab"或者"aba"。假设代码会找到第一个最长的。例如,当i=1时,奇数扩展得到left=0,right=2,所以子串长度3。此时start=0, end=2。后续其他i可能无法找到更长的,所以最终返回s[0:3]即"bab"。 案例2:输入"cbbd",调用i=1时,偶数扩展,得到left=1, right=2。子串是"bb",长度2。此时start=1, end=2,返回s[1:3]即"bb"。 这样,代码应该能正确处理这些情况。 综上所述,完整的Golang代码应该如上面所述。在代码中,expand函数负责从中心扩展,并返回有效的最左和最右索引,主函数遍历每个可能的中心点,比较奇数长度和偶数长度的回文,保存最大的情况,最后返回对应的子串。</think>以下是使用中心扩展法实现Golang最长回文子串完整代码: ```go func longestPalindrome(s string) string { if len(s) == 0 { return "" } start, end := 0, 0 for i := 0; i < len(s); i++ { // 处理奇数长度回文(如 "aba") l1, r1 := expandAroundCenter(s, i, i) // 处理偶数长度回文(如 "abba") l2, r2 := expandAroundCenter(s, i, i+1) // 更新最大回文范围 if r1 - l1 > end - start { start, end = l1, r1 } if r2 - l2 > end - start { start, end = l2, r2 } } return s[start : end+1] } // 从中心向两边扩展,返回回文的最左/右边界 func expandAroundCenter(s string, left, right int) (int, int) { for left >= 0 && right < len(s) && s[left] == s[right] { left-- right++ } // 回退到最后一次满足条件的边界 return left + 1, right - 1 } ``` ### 算法说明: 1. **中心扩展思想** 遍历每个可能的回文中心(每个字符和每两个字符中间的位置),向两侧扩展检测回文。 2. **双中心处理** - 奇数长度:以单个字符为中心(如 "aba" 以 'b' 为中心) - 偶数长度:以两个字符中间为中心(如 "abba" 以中间两个 'b' 为中心) 3. **复杂度分析** - 时间复杂度:O(),遍历每个中心点 O(n),每个中心扩展 O(n) - 空间复杂度:O(1),仅使用常数级额外空间 ### 使用示例: ```go fmt.Println(longestPalindrome("babad")) // 输出 "bab" 或 "aba" fmt.Println(longestPalindrome("cbbd")) // 输出 "bb" fmt.Println(longestPalindrome("a")) // 输出 "a" ``` ### 代码特点: - 处理空字符串的边界情况 - 统一处理奇偶两种回文类型 - 直接返回字符串切片,避免额外内存分配 - 清晰标记回文中心扩展过程 该实现是解决最长回文子串问题的经典方法,在时间和空间复杂度之间取得了较好平衡。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值