解法一:暴力法(居然也能通过...)
算法的本质是穷举,先写一个暴力解做到无遗漏地穷举,最容易想到的解法如下
func findLength(nums1 []int, nums2 []int) int {
res := 0
for i := 0; i<len(nums1); i++{
for j:=0; j<len(nums2); j++{
// 统计以nums1[i]和nums2[j]为起点的重复子数组长度
tmp := 0
p1, p2 := i, j
for p1 < len(nums1) && p2 < len(nums2) && nums1[p1] == nums2[p2]{
tmp++
p1++
p2++
}
// 找到一个重复子数组,更新答案
res = max(res, tmp)
}
}
return res
}
func max(a, b int) int{
if a > b{
return a
}
return b
}
时间复杂度:O(N^3) 空间复杂度:O(1)
解法二:自底向上的递推DP(二维动态规划)
只要遇到公共前缀/后缀这类问题,大概率有冗余计算,要往动态规划的思路上靠。
前面的暴力法中哪里可以进一步优化呢?
最内层的 for 循环可以用空间换时间的思路优化掉,构建一个二维dp数组
dp[i][j] 表示以nums1[i-1]结尾的子数组和以nums2[j-1]结尾的子数组的最大公共子数组长度。长度+1是为了兼容空数组情况,例如,其中一个数组为空,或者两个空数组,都不存在重复子数组。即dp[0][j]和dp[i][0]都为0。
- 如果nums1[i] == nums1[j], dp[i+1][j+1] = dp[i][j] + 1
- 如果nums1[i] != nums1[j], dp[i+1][j+1] = 0 (就是申请数组的初始值,什么都不用改)
func findLength(nums1 []int, nums2 []int) int {
// dp[i][j] 表示以nums1[i-1]结尾的子数组和以nums2[j-1]结尾的子数组的最大公共子数组长度
dp := make([][]int, len(nums1)+1)
for idx := range dp{
dp[idx] = make([]int, len(nums2)+1)
}
res := 0
for i := 1; i<len(dp); i++{
for j := 1; j<len(dp[0]); j++{
if nums1[i-1] == nums2[j-1]{
dp[i][j] = dp[i-1][j-1] + 1
res = max(res, dp[i][j])
}
}
}
return res
}
func max(a, b int) int{
if a > b{
return a
}
return b
}
时间复杂度:O(N^2)
空间复杂度:O(n*m),二维dp数组占用的空间,其中 n 是 nums1的长度,m 是 nums2的长度
解法三:自顶向下的递归dp + 备忘录
动态规划一般都有两种实现方案,我们再尝试用递归的思想实现一下,注意为了避免暴力穷举出现的冗余计算,我们可以借助一个备忘录。
func findLength(nums1 []int, nums2 []int) int {
memo := make([][]int, len(nums1))
// 备忘录初始化为一个和可能答案不冲突的值
for i := range memo{
memo[i] = make([]int, len(nums2))
for j := range memo[i]{
memo[i][j] = -1
}
}
res := 0
for i := 0; i < len(nums1); i++{
for j := 0; j<len(nums2); j++{
res = max(res, dp(nums1, i, nums2, j, memo))
}
}
return res
}
// dp函数返回nums1[...i]和nums2[...j]的最长重复子数组长度
func dp(nums1 []int, i int, nums2 []int, j int, memo [][]int) int{
if i < 0 || j < 0{ // base case,有一个为空数组即无重复子数组了
return 0
}
if memo[i][j] != -1{ // 避免重复计算
return memo[i][j]
}
if nums1[i] == nums2[j]{
memo[i][j] = dp(nums1, i-1, nums2, j-1, memo) + 1
}else{
memo[i][j] = 0
}
return memo[i][j]
}
func max(a, b int) int{
if a > b{
return a
}
return b
}
扩展:百度面试真题
找到两个字符串的最长公共子串
eg: 字符串1"abcdagh", 字符串2"abfagh", 最长公共子串是"agh"
分析
其实字符串也是一个字符数组,因此本质上和寻找重复子数组是类似的,只不过我们需要记录的答案不是长度,而是这个子串。
解法一:暴力法
func main() {
s1 := "abcdagh"
s2 := "abfagh"
res := findCommonStr(s1, s2)
fmt.Println(res)
}
func findCommonStr(s1, s2 string) string {
res := ""
for i := range s1 {
for j := range s2 {
tmp := ""
p1, p2 := i, j
for p1 < len(s1) && p2 < len(s2) && s1[p1] == s2[p2] {
tmp += string(s1[p1])
p1++
p2++
}
if len(tmp) > len(res) {
res = tmp
}
}
}
return res
}
解法二:自底向上的dp
dp数组里头存放的元素改为是公共子串即可
func main() {
s1 := "abcdagh"
s2 := "abfagh"
res := findCommonStr(s1, s2)
fmt.Println(res)
}
func findCommonStr(s1, s2 string) string {
// dp[i][j] 记录s1[:i]和s2[:j]的最长公共子串
// 长度+1是为了兼容空串的情况,dp[0][j]和dp[i][0]一定为0
dp := make([][]string, len(s1)+1)
for i := range dp {
dp[i] = make([]string, len(s2)+1)
}
res := ""
for i := range s1 {
for j := range s2 {
if s1[i] == s2[j] {
dp[i+1][j+1] = dp[i][j] + string(s1[i])
if len(dp[i+1][j+1]) > len(res) {
res = dp[i+1][j+1]
}
}
}
}
return res
}