力扣刷题[5] Go代码实现

代码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
}

一点点笔记,以便以后翻阅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小印丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值