代码Gitee:https://gitee.com/xiaoyinhui/golang-code/tree/develop/test-beego/tests
5. 最长回文子串
题目:
// 给你一个字符串 s,找到 s 中最长的回文子串。
// 示例1:
// 输入:s = "babad"
// 输出:"bab"
// 解释:"aba" 同样是符合题意的答案。
// 示例2:
// 输入:s = "cbbd"
// 输出:"bb"
// 提示:
// 1 <= s.length <= 1000
// s 仅由数字和英文字母组成
// 来源:力扣(LeetCode)
// 链接:https://leetcode.cn/problems/longest-palindromic-substring
// 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
// 提示:回文的意思是正着念和倒着念一样,如:上海自来水来自海上
mStr := "civilwartestingwhetherthatnaptionoranynartionsoconceivedandsodedicatedcanlongendureWeareqmetonagreatbattlefiemldoftzhatwarWehavecometodedicpateaportionofthatfieldasafinalrestingplaceforthosewhoheregavetheirlivesthatthatnationmightliveItisaltogetherfangandproperthatweshoulddothisButinalargersensewecannotdedicatewecannotconsecratewecannothallowthisgroundThebravelmenlivinganddeadwhostruggledherehaveconsecrateditfaraboveourpoorponwertoaddordetractTgheworldadswfilllittlenotlenorlongrememberwhatwesayherebutitcanneverforgetwhattheydidhereItisforusthelivingrathertobededicatedheretotheulnfinishedworkwhichtheywhofoughtherehavethusfarsonoblyadvancedItisratherforustobeherededicatedtothegreattdafskremainingbeforeusthatfromthesehonoreddeadwetakeincreaseddevotiontothatcauseforwhichtheygavethelastpfullmeasureofdevotionthatweherehighlyresolvethatthesedeadshallnothavediedinvainthatthisnationunsderGodshallhaveanewbirthoffreedomandthatgovernmentofthepeoplebythepeopleforthepeopleshallnotperishfromtheearth"
fmt.Println("自己的想法-执行效率有点拉胯了 ", longestPalindrome(mStr))
fmt.Println("动态规划结果 ", longestPalindrome2(mStr))
fmt.Println("中心扩展算法 ", longestPalindrome3(mStr))
mStr = "babad"
fmt.Println("Manacher算法 ", longestPalindrome4(mStr))
// 从上面集中结果来看,中心扩展算发相对来说比较快和节省内存
方法一:自己思路-执行效率有点拉胯了
// 5. 最长回文子串(自己思路-执行效率有点拉胯了)
func longestPalindrome(s string) string {
// 首先需要找到两个相同的字符
// 然后判断他们是不是符合回文
// 判断是不是最长的回文,如果是就存起来
// 每一个字符都要计算在内
mRetStr := string(s[0])
mLeft := 0
var mTempL, mTempR int
var mIsOk bool
mMap := make(map[byte][]int)
for mRight := 0; mRight < len(s); mRight++ {
mValue, ok := mMap[s[mRight]]
if ok {
// 这个字符已经存在,判断他们之间的字符是否满足回文
// 这个字符可能不止一个,存在切片中,从切片中取出来
for _, mLeft = range mValue {
mTempL = mLeft + 1
mTempR = mRight - 1
mIsOk = true
// 两个指针开始网中间靠拢,直到相遇或不满足回文的时候结束
for mTempL < mTempR {
if s[mTempL] != s[mTempR] {
// 不满足回文跳出本次循环
mIsOk = false
break
}
mTempL++
mTempR--
}
if mIsOk {
// 当前子串满足回文,且当前回文长度比之前的长,可以将之前的子串替换
if mRight-mLeft+1 > len(mRetStr) {
mRetStr = s[mLeft : mRight+1]
}
}
}
// 将本次的位置加入到切片中
mValue = append(mValue, mRight)
mMap[s[mRight]] = mValue
} else {
// map 中还没有这个字符
mSlice := make([]int, 0)
mSlice = append(mSlice, mRight)
mMap[s[mRight]] = mSlice
}
}
return mRetStr
}
方法二:动态规划
// 5. 最长回文子串(动态规划)
func longestPalindrome2(s string) string {
mLen := len(s)
if mLen < 1 {
return ""
}
if mLen < 2 {
return string(s[0])
}
// 主要有长度,单个字符可以看成回文,这里最大长度默认给1
mMaxLen := 1
// 回文开始下标
mBeginIndex := 0
// 是否为回文的切片
mIsPalindromeSlice := make([][]bool, mLen)
// 先将长度为1的回文设为true
for i := 0; i < mLen; i++ {
// 同时给二维切片添加一维切片在其内部
mSlice := make([]bool, mLen)
mIsPalindromeSlice[i] = mSlice
mIsPalindromeSlice[i][i] = true
}
// 前面回文长度=1的已经弄完,所以这里从回文长度=2的开始遍历
for mCurrLen := 2; mCurrLen <= len(s); mCurrLen++ {
// 从最左边界的字符开始遍历
for mLeft := 0; mLeft < mLen; mLeft++ {
// 可知 有边界字符的位置 = 左边界字符的位置 + 当前回文长度 - 1
mRight := mLeft + mCurrLen - 1
// 如果有边界越界了直接跳出本次循环
if mRight >= mLen {
break
}
if s[mLeft] == s[mRight] {
// 如果 左边界字符 == 右边界字符
if mCurrLen == 2 {
// 且 当前回文长度是按照2来找的,可以直接确定当前的子串就是回文
mIsPalindromeSlice[mLeft][mRight] = true
} else {
// 如果当前查找的回文长度 > 2,就看其左右边界往中间的隔壁的位置是否属于回文
// 注意这里的回文长度判断是从短到长的,往中间找就相当于是把长度减少了,然后减少长度的子串已经知道是否为回文
mIsPalindromeSlice[mLeft][mRight] = mIsPalindromeSlice[mLeft+1][mRight-1]
}
} else {
// 左边界字符 != 有边界字符 的时候可以直接确定这个子串不属于回文
mIsPalindromeSlice[mLeft][mRight] = false
}
// 判断当前的子串是否为回文
if mIsPalindromeSlice[mLeft][mRight] && (mRight-mLeft+1 > mMaxLen) {
// 如果当前的子串为回文 且 回文长度大于现在已经发现的回文长度
mMaxLen = mRight - mLeft + 1
mBeginIndex = mLeft
}
}
}
return s[mBeginIndex : mBeginIndex+mMaxLen]
}
方法三:中心扩展算法
相对比较清晰易懂
// 5. 最长回文子串(中心扩展算法)
func longestPalindrome3(s string) string {
// 这个算法的核心点就是,从回文的中心(最中心的字符)开始往外部进行扩散计算,找到最长子串且满足是回文
// 遍历整个字符串进行检查
// 记录最长满足回文子串的起始点下标
mStrBegin := 0
mStrEnd := 0
for i := 0; i < len(s); i++ {
mLeft1, mRight1 := expandAroundCenter(s, i, i+1)
mLeft2, mRight2 := expandAroundCenter(s, i, i+2)
if mRight1-mLeft1 > mStrEnd-mStrBegin {
mStrBegin = mLeft1
mStrEnd = mRight1
}
if mRight2-mLeft2 > mStrEnd-mStrBegin {
mStrBegin = mLeft2
mStrEnd = mRight2
}
}
return s[mStrBegin : mStrEnd+1]
}
// 从中心开始往两端检查是否满足回文
func expandAroundCenter(aStr string, aLeft, aRight int) (int, int) {
// 左指针下标 < 右指针下标 且 左指针下标 >= 0 且 右指针下标 < 字符串长度 且 左指针指向的值 == 右指针指向的值
for (aLeft < aRight) && (aLeft >= 0) && (aRight < len(aStr)) && (aStr[aLeft] == aStr[aRight]) {
// 左指针下标开始往左移动继续扩大子串长度
aLeft--
// 右指针下标开始往右移动继续扩大子串长度
aRight++
}
// 因为在循环中不满足的时候左右指针的下标已经进行了加减,所以返回的时候要对其进行还原
return aLeft + 1, aRight - 1
}
方法四:Manacher算法
// 5. 最长回文子串(Manacher算法)
func longestPalindrome4(s string) string {
// 在开始之前先弄明白几个关键字的意思
// 中心位置:上面中心扩展算法的那个中心 例如:abcba 中心位置为 c 所在的位置
// 臂长:中心位置到左右边界的距离(字符串长度为奇数的情况下) 例如:abcba 其臂长=2(ab / ba)
// 总体思路:
// 1、将原字符串进行处理 给其中插入一个字符,可以为任意字符
// 例如:abcba 处理后=*a*b*c*b*a* 处理后的字符串的长度一定是奇数,至于为啥咱就不解释了
// 2、利用中心扩散算法的方式进行计算,其中需要注意的是我们从左往右遍历的时候,可以利用回文的对称性
// 在计算中心位置右边臂长内的范围可知其对称的左边的最小臂长,然后右边直接跳过这部分的遍历,进行后续的遍历
// 例如:xabacabay 上一个中心位置=c 当前中心位置=b(c右边的) 此时可知当前中心位置还在上一个中心位置+其臂长 的范围内
// 可以通过计算得到其相对上一个中心的位置的对称点b(c左边的) 最小臂长=1 在当前中心就可跳过这个最小臂长的内容进行计算剩余的
// 就是从 c和y开始计算是否满足回文
// 3、在上述的描述中,我们还要同时判断其其是否在上一中心位置+其臂长的范围内,如果当前中心位置已经超过了范围,就不能进行省略部分遍历
// 例如:xabacabay 上一个中心位置=b(c左边的) 当前中心位置=y 已经超过了之前的辐射范围,直接全部遍历
// 4、前面不管是在范围内也好不在范围内也好都会计算出当前的臂长,如果当前中心位置+当前臂长 > right(之前的终点位置+之前的臂长)
// 满足前面条件就要开始替换最新的数据,将上次的中心位置更新为本次的以及之前的臂长或者说是right
// 例如:xabacabay 上一个中心位置=b(c左边的) 当前中心位置=y 已经超过了之前的辐射范围,直接全部遍历
// 5、开始检查目前满足回文子串的长度是否比之前记录的大,然后做相应的调整
// 例如:xabacabay 在前半段的 aba 中心位置下标=2 + 臂长=1 < 遍历到c的位置 当前中心位置=4 + 臂长=3 将其数据进行更新
// 6、最后就是去掉我们我之前加的特殊字符,从前面的顺序可以看出来,我们只需要将处理后的字符串遍历,取其中偶数位的字符即可,就是是回文子串也是同理
// 例如:*a*b*c*b*a* 处理后=abcba
begin, end := 0, -1
t := "#"
for i := 0; i < len(s); i++ {
t += string(s[i]) + "#"
}
s = t
// 存放臂长的切片
var armLenArr []int
// 右指针下标、旧的中心位置下标
right, oldCenter := -1, -1
for currCenter := 0; currCenter < len(s); currCenter++ {
var currArmLen int
if currCenter > right {
// 计算当前遍历的中心点位置的臂长 无省略遍历
currArmLen = expand(s, currCenter, currCenter)
} else {
// 获取当前中心位置 相对于 上一中心位置(oldCenter) 的对称位置
// 对称位置 = 旧的中心位置 - (当前中心位置 - 旧的中心位置) ==> 旧的中心位置 - 当前中心位置 + 旧的中心位置 ==> 旧的中心位置*2 - 当前中心位置
currCenterSymmetry := oldCenter*2 - currCenter
// 通过对称的点可以知道这个点的最小臂长
// 最小臂长长度 = 从 对称点的臂长 和 (右指针下标 - 当前中心位置下标) 中取一个最小值
minArmLen := min(armLenArr[currCenterSymmetry], right-currCenter)
// 计算当前遍历的中心点位置的臂长 有省略遍历 跳过重复计算最小臂长的部分
currArmLen = expand(s, currCenter-minArmLen, currCenter+minArmLen)
}
// 切片中记录每个位置的臂长
armLenArr = append(armLenArr, currArmLen)
// 如果 当前中心位置下标 + 臂长 > 右指针下标
if currCenter+currArmLen > right {
// 旧的中心位置的下标 = 当前中心位置下标
oldCenter = currCenter
// 右指针下标 = 当前中间位置下标 + 当前臂长
right = currCenter + currArmLen
}
// 如果当前臂长 * 2 > 结束点下标 - 开始点下标
if currArmLen*2 > end-begin {
// 设置开始点位下标 = 中间位置下标 - 当前臂长长度
begin = currCenter - currArmLen
// 设置结束点位下标 = 中间位置下标 + 当前臂长长度
end = currCenter + currArmLen
}
}
fmt.Println("打印结果s[begin+1:end]=", s[begin+1:end])
fmt.Println("打印结果s[begin:end]=", s[begin:end])
fmt.Println("打印结果s[begin:end+1]=", s[begin:end+1])
// 打印结果s[begin+1:end]= a#b#a#c#a#b#a
// 打印结果s[begin:end]= #a#b#a#c#a#b#a
// 打印结果s[begin:end+1]= #a#b#a#c#a#b#a#
subStr := ""
// 我们的开始和结束位置肯定都是我们添加的特殊符号,这里我们可以将其掐头去尾的不去遍历
i := begin + 1
for i < end {
// 这里的i是下标,所以这里取余==1是我们要的
if i%2 == 1 {
subStr += string(s[i])
i += 2
} else {
i++
}
}
return subStr
}
// 求臂长 最后加一个# 也是为了让其变成偶数 然后方便这里求臂长吧
func expand(s string, left, right int) int {
for ; left >= 0 && right < len(s) && s[left] == s[right]; left, right = left-1, right+1 {
}
// 这里是上面计算出来的结果不满足了,这里进行还原 ((right - 1) - (left + 1)) / 2
return (right-left)/2 - 1
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
一点点笔记,以便以后翻阅。