Leetcode专项学习计划:LeetCode热题100

题目来自:Leetcode专项学习计划——LeetCode热题100,共 100 道题。

本文章只记录当前(2024.05.25)掌握得不太好的题目,完整的题目请参考上面的链接。

基础篇

最长连续序列

128. 最长连续序列

1
2
3
4
5
6
7
8
func main() {
    nums := []int{100, 4, 200, 1, 3, 2}
    r := longestConsecutive(nums)
    fmt.Println(r)
}
func longestConsecutive(nums []int) int {
    // ......
}
分析

本题要求时间复杂度为O(n)。

自己的一些思考:

自己的思路1:开辟容量为 maxVal-minVal+1 的数组,将每个位置出现的次数记录下来,然后统计最长的连续序列。

这样写存在的问题是,num 可能是负数,就不好使用数组的下标来表示 num 了。

自己的思路2:从大到小,使用 map 记录当前数值连续的数量。

这里有一种 map 和动态规划相结合的思想。

但这样也存在问题:元素出现的顺序不是从大到小的,元素是可以交换顺序的。

参考答案的方法:既然元素之间的顺序不重要,那么我们就可以直接将所有的元素存入 set(numSet)中,

遍历数组,如果 num-1 不在 numSet 集合中,那么就可以从 num 开始往右搜索。(这里还是比较巧妙的

如果 num+1 在 numSet 集合中,则当前连续的长度加 1(循环这个过程,获得最大的长度)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func longestConsecutive(nums []int) int {
    numSet := map[int]bool{}
    for _, num := range nums {
       numSet[num] = true
    }
    maxLen := 0
    for num := range numSet {
       if !numSet[num-1] {
          curNum := num
          curLen := 1
          for numSet[curNum+1] {  // 一直找从num开始的所有连续数字
             curNum++
             curLen++
          }
          if maxLen < curLen {
             maxLen = curLen
          }
       }
    }
    return maxLen
}

字母异位词分组

49. 字母异位词分组

1
2
3
4
5
6
7
8
func main() {
    strs := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
    r := groupAnagrams(strs)
    fmt.Println(r) // [[eat tea ate] [tan nat] [bat]]
}
func groupAnagrams(strs []string) [][]string {
    // ......
}
分析

参考答案1:对字符串中的字母进排序,如果是字母异位词,则排序之后得到的字符序列一定是相同的。

把排序之后的字符串作为 key 存入 map 中,map 的 value 存相同 key 对应的字符串集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func groupAnagrams(strs []string) [][]string {
    strMap := make(map[string][]string) // 存字母异位词
    for _, str := range strs {
       s := []byte(str)
       sort.Slice(s, func(i, j int) bool {
          return s[i] < s[j]
       })
       sortedS := string(s)
       strMap[sortedS] = append(strMap[sortedS], str)
    }
    res := make([][]string, len(strMap)) // 最后的返回值
    i := 0
    for _, strSlice := range strMap {
       res[i] = strSlice
       i++
    }
    return res
}

参考答案2:开辟一个长度为 26 的数组 byteCount,将 strs 数组的每个元素 str 中每个字母出现的次数进行统计。

如果是字母异位词,那么每个字母出现次数就是相等的,也就是 byteCount 是相等的。

所以我们可以构建一个以 byteCount 为 key 的 map,如果 byteCount 相同,则将其添加到 byteCount 对应的 map 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func groupAnagrams(strs []string) [][]string {
    map01 := make(map[[26]int][]string)
    for _, str := range strs {
       byteCount := [26]int{}
       for _, byte := range str {
          byteCount[byte-'a']++ // 存放每个字母出现的次数
       }
       map01[byteCount] = append(map01[byteCount], str)
    }
    res := make([][]string, len(map01))
    i := 0
    for _, strSlice := range map01 { // 将map中的value转移到res切片中
       res[i] = strSlice
       i++
    }
    return res
}

盛最多水的容器

11. 盛最多水的容器

1
2
3
4
5
6
7
8
func main() {
    nums := []int{1, 8, 6, 2, 5, 4, 8, 3, 7}
    r := maxArea(nums)
    fmt.Println(r) // 49
}
func maxArea(height []int) int {
    // ......
}
分析

自己的解法:双重循环,第 55 个测试用例超时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func maxArea(height []int) int {
	// 左右双指针,取左右高度较矮的作为高度
	n := len(height)
	realH, largestArea := 0, 0
	for l := 0; l < n; l++ {
		for r := n - 1; r >= 0; r-- {
			realH = min(height[l], height[r])
			largestArea = max(largestArea, realH*(r-l))
		}
	}
	return largestArea
}

贪心算法 + 双指针:

参考:对O(n)的算法写一下自己的理解

最开始 l 和 r 分别指向 0 位置和 n-1 位置,现在我们需要需要移动 l 和 r,无论我们怎么移动,都会使容器的宽度减 1。

这时候我们就应该尽量让高度 h 更大,所以我们应该移动高度更低的指针,以获得更大的容量。

还有一个比较有哲学意味的回答:感觉这个移动有点博弈论的味了,感兴趣可以看看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func maxArea(height []int) int {
    maxVal, realH := 0, 0
    n := len(height)
    l, r := 0, n-1
    for l < r {
       realH = min(height[l], height[r])
       if (r-l)*realH > maxVal {
          maxVal = (r - l) * realH
       }
       if height[l] <= height[r] {
          l++
       } else {
          r--
       }
    }
    return maxVal
}

找到字符串中所有字母异位词

438. 找到字符串中所有字母异位词

1
2
3
4
5
6
7
8
func main() {
    s, p := "cbaebabacd", "abc"
    r := findAnagrams(s, p)
    fmt.Println(r) // [0,6]
}
func findAnagrams(s string, p string) []int {
    // ......
}
分析

方法1:

自己写的版本,每个位置都需要去判断滑动窗口内的字符串是否合法。

滑动窗口的大小为 len(p),判断滑动窗口内每个字符出现的次数与 p 中每个字符出现的次数是否相同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func findAnagrams(s string, p string) []int {
    res := make([]int, 0)
    lenP := len(p)
    pChars := [26]byte{}
    for _, pStr := range p {
       pChars[pStr-'a']++
    }
    for i := range s {
       sChars := [26]byte{}
       if i+lenP > len(s) {
          break
       }
       for _, sStr := range s[i : i+lenP] { // 判断滑动窗口中的字符串是否合法
          sChars[sStr-'a']++
       }
       if sChars == pChars {
          res = append(res, i)
       }
    }
    return res
}

上面这种写法在每个滑动窗口中都需要统计每个字母出现的次数,时间复杂度较高。

方法2:

参考了答案,先一起遍历 s 和 p,判断 s[:lenP] 部分是否是和 p 是字母异位词,如果是,则将下标 0 添加到 res 中

然后就可以移动滑动窗口了,在移动滑动窗口时,移出去的元素个数应该减 1,新添加入滑动窗口的元素个数应该加 1,即:sChars[s[i]-‘a’]–,sChars[s[i+lenP]-‘a’]++。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func findAnagrams(s string, p string) []int {
    if len(s) < len(p) { // 剪枝
       return nil
    }
    res := make([]int, 0)
    lenP := len(p)
    pChars, sChars := [26]byte{}, [26]byte{}
    for i, pStr := range p {
       pChars[pStr-'a']++
       sChars[s[i]-'a']++
    }
    if sChars == pChars {
       res = append(res, 0)
    }
    for i := 0; i < len(s)-lenP; i++ { // 滑动窗口的移动过程,i是移除滑动窗口的位置
       sChars[s[i]-'a']--      // 移出
       sChars[s[i+lenP]-'a']++ // 移入
       if sChars == pChars {
          res = append(res, i+1) // 加入到res的应该是i+1,而不是i
       }
    }
    return res
}

合并区间

56. 合并区间

1
2
3
4
5
6
7
8
9
func main() {
    //intervals := [][]int{{1, 3}, {2, 6}, {8, 10}, {15, 18}}
    intervals := [][]int{{2, 3}, {4, 5}, {6, 7}, {8, 9}, {1, 10}}
    r := merge(intervals)
    fmt.Println(r)
}
func merge(intervals [][]int) [][]int {
    // ......
}
分析

计算重叠区间。

先将数组按照左区间值进行排序,然后计算当前区间的最右侧位置,如果大于等于下一个元素的左区间位置,那么就说明存在区间的重叠。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func merge(intervals [][]int) [][]int {
    sort.Slice(intervals, func(i, j int) bool {
       return intervals[i][0] < intervals[j][0]
    })
    res := make([][]int, 1)
    res[0] = intervals[0]
    for i := 1; i < len(intervals); i++ {
       if intervals[i][0] <= res[len(res)-1][1] { // 存在重叠。注意这里比较的右区间位置是res[len(res)-1][1],而不是intervals[i-1][0](这里很关键)
          rightIndex := max(intervals[i][1], res[len(res)-1][1])  
          res[len(res)-1][1] = rightIndex // 更新右区间的值
       } else {
          res = append(res, intervals[i])
       }
    }
    return res
}

轮转数组

189. 轮转数组

1
2
3
4
5
6
7
8
func main() {
    nums, k := []int{1, 2, 3, 4, 5, 6, 7}, 3
    rotate(nums, k)
    fmt.Println(nums) // [5 6 7 1 2 3 4]
}
func rotate(nums []int, k int) {
	// ......
}
分析

方法1:

自己写的方法,每轮都将元素后移一个位置,时间复杂度为 O(n*k),当k较大时,时间复杂度很高。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func rotate(nums []int, k int) {
	n := len(nums)
	k = k % n
	for k > 0 {
		temp := nums[n-1]
		for i := n - 1; i >= 1; i-- {
			nums[i] = nums[i-1]
		}
		nums[0] = temp
		k--
	}
}

方法2:

参考别人的代码:先将 nums 整体翻转,再将 0 到 k 之间的元素进行翻转,最后将 k 到 n 之间的元素进行翻转,时间复杂度为 O(n)。

这样处理是比较巧妙的,反正我自己想是想不到这种方法的,哈哈哈。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func rotate(nums []int, k int) {
    reverse := func(nums []int) {
       l, r := 0, len(nums)-1
       for l < r {
          nums[l], nums[r] = nums[r], nums[l]
          l++
          r--
       }
    }
    reverse(nums)
    reverse(nums[:k])
    reverse(nums[k:])
}

除自身以外数组的乘积

238. 除自身以外数组的乘积

1
2
3
4
5
6
7
8
func main() {
    nums := []int{1, 2, 3, 4}
    r := productExceptSelf(nums)
    fmt.Println(r) // [24 12 8 6]
}
func productExceptSelf(nums []int) []int {
    // ......
}
分析

参考题解:前缀积&后缀积

先从前往后遍历,计算前缀积(不算自己);然后从后往前遍历,计算后缀积(也不算自己)。

使用一个数组 res 存放前缀积的值,最后乘上后缀积之后返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func productExceptSelf(nums []int) []int {
    pre, post := 1, 1
    res := make([]int, len(nums))
    for i := 0; i < len(nums); i++ {
       res[i] = pre
       pre *= nums[i]
    }
    for j := len(nums) - 1; j >= 0; j-- {
       res[j] *= post
       post *= nums[j]
    }
    return res
}

螺旋矩阵

54. 螺旋矩阵

1
2
3
4
5
6
7
8
9
func main() {
    matrix := [][]int{{7}, {9}, {6}}
    //matrix := [][]int{{2, 5}, {8, 4}, {0, 1}}
    r := spiralOrder(matrix)
    fmt.Println(r) // [7,9,6]
}
func spiralOrder(matrix [][]int) []int {
    // ......
}
分析

本题是 59. 螺旋矩阵 II 的进阶版本,行数和列数不一定相等。

我们需要使用一个变量 count 统计已经遍历过的元素数量,如果 count 等于了 m*n,那么就应该直接返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func spiralOrder(matrix [][]int) []int {
    // 遍历matrix数组的每一条边
    m := len(matrix)
    n := len(matrix[0])
    res := make([]int, m*n)
    count := 0 // 遍历过的元素数量
    i, j, startX, offset := 0, 0, 0, 1
    for c := 0; c < max(m, n)/2; c++ { // c表示圈数
       i, j = startX, startX
       for ; j < n-offset; j++ { // 每条边不包含最后一个元素
          if count == m*n {
             return res
          }
          res[count] = matrix[i][j]
          count++
       }
       for ; i < m-offset; i++ {
          if count == m*n {
             return res
          }
          res[count] = matrix[i][j]
          count++

       }
       for ; j > startX; j-- {
          if count == m*n {
             return res
          }
          res[count] = matrix[i][j]
          count++
       }
       for ; i > startX; i-- {
          if count == m*n {
             return res
          }
          res[count] = matrix[i][j]
          count++
       }
       offset++
       startX++
    }
    if count == m*n-1 { // 最中心的位置如果没有遍历到,则最后再赋值
       res[count] = matrix[m/2][n/2]
    }
    return res
}

搜索二维矩阵II

240. 搜索二维矩阵 II

本题较为简单,可以减少练习次数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
	matrix := [][]int{
		{1, 4, 7, 11, 15},
		{2, 5, 8, 12, 19},
		{3, 6, 9, 16, 22},
		{10, 13, 14, 17, 24},
		{18, 21, 23, 26, 30}}
	target := 5
	r := searchMatrix(matrix, target)
	fmt.Println(r) // true
}
func searchMatrix(matrix [][]int, target int) bool {
	// ......
}
分析

先列二分,再行二分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func searchMatrix(matrix [][]int, target int) bool {
	m := len(matrix)
	n := len(matrix[0])
	colLeft, colRight := 0, m-1
	for colLeft <= colRight {
		colMid := colLeft + (colRight-colLeft)>>1
		if target == matrix[colMid][0] {
			return true
		} else if target < matrix[colMid][0] {  // 只有target小的时候才能二分
			colRight = colMid - 1
		} else {
			break
		}
	}
	for i := colLeft; i <= colRight; i++ {
		rowLeft, rowRight := 0, n-1
		for rowLeft <= rowRight {
			rowMid := rowLeft + (rowRight-rowLeft)>>1
			if target == matrix[i][rowMid] {
				return true
			} else if target > matrix[i][rowMid] {
				rowLeft = rowMid + 1
			} else {
				rowRight = rowMid - 1
			}
		}
	}
	return false
}

最小栈

本题较为简单,可以减少练习的次数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type MinStack struct {

}

func Constructor() MinStack {

}
func (this *MinStack) Push(val int)  {

}
func (this *MinStack) Pop()  {

}
func (this *MinStack) Top() int {

}
func (this *MinStack) GetMin() int {

}
分析

参考了答案:

维护一个正常栈和一个最小栈(二者长度是相同的),正常栈的元素正常添加,最小栈始终添加当前元素的最小值。也就是在最小栈中加入 min(num, minStack[len(minStack)-1])。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type MinStack struct {
    stack    []int
    minStack []int
}
func Constructor() MinStack {
    return MinStack{
       stack:    []int{},
       minStack: []int{math.MaxInt},
    }
}
func (this *MinStack) Push(val int) {
    this.stack = append(this.stack, val)
    minTop := this.minStack[len(this.minStack)-1]
    this.minStack = append(this.minStack, min(minTop, val))
}
func (this *MinStack) Pop() {
    this.stack = this.stack[:len(this.stack)-1]
    this.minStack = this.minStack[:len(this.minStack)-1]
}
func (this *MinStack) Top() int {
    return this.stack[len(this.stack)-1]
}
func (this *MinStack) GetMin() int {
    return this.minStack[len(this.minStack)-1]
}

在排序数组中查找元素的第一个和最后一个位置

1
2
3
4
5
6
7
8
func main() {
    nums, target := []int{5, 7, 7, 8, 8, 10}, 8
    r := searchRange(nums, target)
    fmt.Println(r) // [3,4]
}
func searchRange(nums []int, target int) []int {
    // ......
}
分析

两次二分查找,这个二分查找必须返回 target 最左侧的下标值。

先查找左边的位置 left,在查找右边的位置 right(查找 target+1 的位置,然后减 1)。

最后需要检查返回的 left 是否合法,如果不合法,则返回 []int{-1, -1}。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func searchRange(nums []int, target int) []int {
    // 返回target最左侧的位置
    binarySearch := func(nums []int, target int) int {
       l, r := 0, len(nums)-1
       for l <= r {
          mid := l + (r-l)>>1
          if target > nums[mid] {
             l = mid + 1
          } else {
             r = mid - 1
          }
       }
       return l
    }
    left := binarySearch(nums, target)
    right := binarySearch(nums, target+1) - 1
    if left == len(nums) || nums[left] != target { // 可能存在数组中没有找到target的情况
       return []int{-1, -1}
    }
    return []int{left, right}
}

多数元素

169. 多数元素

1
2
3
4
5
6
7
8
func main() {
    nums := []int{2, 2, 1, 1, 1, 2, 2}
    r := majorityElement(nums)
    fmt.Println(r) // 2
}
func majorityElement(nums []int) int {
    // ......
}
分析

参考官方题解:方法五:Boyer-Moore 投票算法

Boyer-Moore投票算法,使用两个变量,一个保存候选众数candidate,一个记录它出现的次数count。

遍历数组,如果元素值与candidate不相等,则count–。

如果元素值与candidate相等,则count++。

如果count等0了,就换一个候选数组为当前元素值。

遍历完成后,candidate中保存的就是众数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func majorityElement(nums []int) int {
    candidate, count := 0, 0
    for _, num := range nums {
       if count == 0 {
          candidate = num
       }
       if num == candidate {
          count++
       } else {
          count--
       }
    }
    return candidate
}

颜色分类

75. 颜色分类

1
2
3
4
5
6
7
8
func main() {
    nums := []int{2, 0, 2, 1, 1, 0}
    sortColors(nums)
    fmt.Println(nums) // [0,0,1,1,2,2]
}
func sortColors(nums []int) {
    // ......
}
分析

方法一:单指针。 参考官方题解:方法一:单指针

遍历两遍数组,第1遍将全部的0放到最前面,并使用count0统计0的个数。

再次遍历数组(从count0开始),将所有的1放到最前面。

这样所有的2自然也就在最后面了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func sortColors(nums []int) {
    countTarget := 0
    var swap func(nums []int, target int)
    swap = func(nums []int, target int) { // 需要把target放到数组最前面
       countTarget = 0
       for i := 0; i < len(nums); i++ {
          if nums[i] == target {
             nums[countTarget], nums[i] = nums[i], nums[countTarget]
             countTarget++
          }
       }
    }
    swap(nums, 0)
    swap(nums[countTarget:], 1)
}

进阶篇

接雨水

【困难】42. 接雨水

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    height := []int{0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1}
    r := trap(height)
    fmt.Println(r) // 6
    // 010210132121
    //        o
    //    o×××oo×o
    //  o×oo×oooooo
    // ×位置就是可以接雨水的位置,总共可以接6个单位的雨水。
}
func trap(height []int) int {
    // ......
}
分析

方法1:找到每个位置左侧的最小高度 lHeight 和右侧的最小高度 rHeight,每个位置能接的雨水量就等于 min(lHeight, rHeight) - height[i]。

lHeight 可以根据一个单调的数组 left 来得到,也就是从左到右遍历 height 数组,left[i]=max(left[i-1], height[i])。

同理,rHeight 也可以根据一个单调的数组 right 来得到,right 数组的计算需要从右到左遍历 height 数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func trap(height []int) int {
	n := len(height)
	left, right := make([]int, n), make([]int, n)
	left[0], right[n-1] = height[0], height[n-1]
	for i := 1; i < n; i++ {
		left[i] = max(left[i-1], height[i])
	}
	for i := n - 2; i >= 0; i-- {
		right[i] = max(right[i+1], height[i])
	}
	sum := 0
	for i, h := range height {
		sum += min(left[i], right[i]) - h
	}
	return sum
}

对上面方法在空间上的优化:使用 l 和 r 双指针,在遍历数组的同时更新 lMax 和 rMax 的值(也就是使用两个变量来代替前面的两个数组)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func trap(height []int) int {
    n := len(height)
    l, r := 0, n-1
    lMax, rMax, sum := 0, 0, 0
    for l < r {
       lMax = max(lMax, height[l]) // 遍历过程中,更新lMax和rMax的值
       rMax = max(rMax, height[r])
       if lMax < rMax { // 如果lMax更小,则决定桶高度的就是lMax,计算lMax与height[l]的差值,加入到sum中,并右移l指针
          sum += lMax - height[l]
          l++
       } else { // 反之,rMax更小,决定桶高度的就是rMax
          sum += rMax - height[r]
          r--
       }
    }
    return sum
}

和为K的子数组

560. 和为 K 的子数组

1
2
3
4
5
6
7
8
func main() {
    nums, k := []int{1, -1, 0}, 0
    r := subarraySum(nums, k)
    fmt.Println(r) // 3
}
func subarraySum(nums []int, k int) int {
    // ......
}
分析

方法1:自己写的版本,利用前缀和数组就行求解,时间复杂度较大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func subarraySum(nums []int, k int) int {
	count := 0
	preSum := make([]int, len(nums)+1)
	preSum[0] = 0
	for i, num := range nums { // 构建前缀和数组
		preSum[i+1] = preSum[i] + num
	}
	for i := 0; i < len(preSum); i++ { // i遍历preSum的每个位置,j遍历i后面的每个位置
		for j := i + 1; j < len(preSum); j++ {
			if preSum[j]-preSum[i] == k {
				count++
			}
		}
	}
	return count
}

方法2:参考了答案,使用前缀和+map

preSum[i] = preSum[i−1] + nums[i]。

我们需要在 0 到 i-1 之间找到一个位置 j,使得:pre[i]−pre[j−1]==k,然后统计满足这样等式的 j 有多少个。

上面等式移项之后为:pre[j−1]==pre[i]−k,也就是说,我们要计算有多少个前缀和满足上面这个等式。

我们可以使用 map 来存已经存在的前缀和,map 的 key 为前缀和,map 的 value 为该前缀和出现的次数。

遍历 nums 数组,使用 pre 变量保存当前的前缀和,如果 map 中存在 key 为 pre−k 的元素,说明从某个位置到当前位置的连续子数组的和为 k,我们将对应的次数累加到结果中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func subarraySum(nums []int, k int) int {
    preSum := 0
    count := 0
    preSumMap := make(map[int]int, 1) // key为前缀和,value为该前缀和出现的次数
    preSumMap[0] = 1                  // 前缀和为0至少出现了1次(这里的初始化很重要)
    for _, num := range nums {
       // 计算当前的前缀和,由于后一个位置的前缀和可以由前一个位置得到,而所有的前缀和都保存到了map中,
       // 所以,这里只需要一个不断迭代的变量就可以了,不需要一个前缀和数组
       preSum += num
       if preSumCount, exist := preSumMap[preSum-k]; exist { // 找到了满足条件的前缀和
          count += preSumCount
       }
       preSumMap[preSum]++
    }
    return count
}

滑动窗口最大值

【困难】239. 滑动窗口最大值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
    //nums, k := []int{1, 3, -1, -3, 5, 3, 6, 7}, 3 // [3 3 5 5 6 7]
    //nums, k := []int{7, 2, 4}, 2          // [7 4]
    nums, k := []int{1, 3, 1, 2, 0, 5}, 3 // [3,3,2,5]
    r := maxSlidingWindow(nums, k)
    fmt.Println(r)
}
func maxSlidingWindow(nums []int, k int) []int {
    // ......
}
分析

维护一个单调不增(可以相等)队列,在元素加入队列时,把前面比它小的元素全部删除,再加入队列。

想清楚维护什么样的队列很重要,不然会存在各种问题。

在元素出队时,如果出队的元素和队首元素相等,才真正地出队,否则就什么也不做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func maxSlidingWindow(nums []int, k int) []int {
	queue := make([]int, 1)
	queue[0] = nums[0]
	push := func(num int) {
		if len(queue) == 0 || num < queue[len(queue)-1] {
			queue = append(queue, num)
		} else {
			for len(queue) > 0 && num > queue[len(queue)-1] { // 把前面小于num的元素全部删除
				queue = queue[:len(queue)-1]
			}
			queue = append(queue, num)
		}
	}
	pop := func(num int) {
		if num == queue[0] {
			queue = queue[1:]
		}
	}
	res := make([]int, len(nums)-k+1)
	for i := 1; i <= k-1; i++ { // 先把前k个元素加入到队列中
		push(nums[i])
	}
	res[0] = queue[0]
	for i := k; i < len(nums); i++ {
		pop(nums[i-k])
		push(nums[i])
		res[i-k+1] = queue[0]
	}
	return res
}

最小覆盖子串

【困难】76. 最小覆盖子串

本题比较复杂,建议多加理解和练习。

1
2
3
4
5
6
7
8
func main() {
    s, t := "ADOBECODEBANC", "ABC"
    r := minWindow(s, t)
    fmt.Println(r) // "BANC"
}
func minWindow(s string, t string) string {
    // ......
}
分析

参考了答案:使用滑动窗口+哈希表。

参考题解:最小覆盖子串-超级无敌最小白式整理

两个指针 l 和 r,最初都指向 0 位置,然后移动 r 位置,直到t中所有的字符都在滑动窗口中出现,然后就可以移动l了。

然后构建两个哈希表,一个存 t 中所有出现的字符和出现的次数(tMap),另一个存现在滑动窗口中有的 t 中出现的字符的次数(sMap)。

具体如下:将 t 中所有表的字符添加到哈希 tMap 中,然后判断r位置的字符是否在 tMap 中存在,如果不存在,则直接将 r 位置右移,然后继续下一次循环

如果 sMap[rStr] 小于 tMap[rStr],则使用 count 统计 s 中出现 t 中相同元素的次数,count++。然后将sMap[rStr]++、r++。

当 count 等于 len(t) 时,说明 t 中所有的字符都在滑动窗口中出现了,就可以移动 l 了。(这是一个循环的过程,循环条件为:count==len(t))。

在移动之前,先记录最小长度 minLen 和当前的 left 位置作为起始位置 start。

判断滑动窗口最左侧位置的元素是否在 tMap 中,如果不在,则直接右移 l,并继续下一次循环。

如果sMap(l)==tMap(l),说明这个是需要的元素,则将 sMap[s[l]]–、l++、count–,这样之后前面循环条件就不成立了,退出移动 l 的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func minWindow(s string, t string) string {
    if len(s) < len(t) {
       return ""
    }
    tMap, sMap := make(map[byte]int), make(map[byte]int)
    for i := 0; i < len(t); i++ {
       tMap[t[i]]++
    }
    l, r, start, minLen, count := 0, 0, 0, len(s)+1, 0
    for r < len(s) {
       if tMap[s[r]] == 0 { // r位置的元素在tMap中不存在
          r++
          continue
       }
       if sMap[s[r]] < tMap[s[r]] {
          count++
       }
       sMap[s[r]]++
       r++
       for count == len(t) { // 这是一个循环的过程
          if r-l < minLen {
             minLen = r - l // 记录start后面到r的距离,不用加1
             start = l
          }
          if tMap[s[l]] == 0 { // 滑动窗口最左侧的元素不在t中
             l++
             continue
          }
          if sMap[s[l]] == tMap[s[l]] {
             count--
          }
          sMap[s[l]]--
          l++
       }
    }
    if minLen == len(s)+1 { // 如果最小长度还为初始值,说明没有符合条件的子串
       return ""
    }
    return s[start : start+minLen]
}

缺失的第一个正数

【困难】41. 缺失的第一个正数

1
2
3
4
5
6
7
8
func main() {
    nums := []int{3, 4, -1, 1}
    r := firstMissingPositive(nums)
    fmt.Println(r) // 2
}
func firstMissingPositive(nums []int) int {
    // ......
}
分析

如果 nums 数组的长度为 n,那么缺失的第 1 个正数只可能在 [1, n+1] 的范围内。

因为如果 1 到 n 的每个数都出现了,那么缺失的就是 n+1,,否则缺失的就是 [1, n] 范围内的某个数。

我们可以遍历 nums 数组,把合法的 nums[i] 放到它应该在的位置上。

什么是应该在的位置呢?比如:1 应该放在 0 位置,2 应该放在 1 位置……

所以遇到 nums[i] 应该放在 nums[nums[i]-1] 位置。

最后再次遍历 nums 数组,如果 nums[i]和 i+1 不相等,说明 i+1 位置就是第一个缺失的正整数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func firstMissingPositive(nums []int) int {
    n := len(nums)
    for i := 0; i < n; i++ {
       for nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i]-1] {
          nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i] // 交换两个位置的值
       }
    }
    for i := 0; i < len(nums); i++ {
       if nums[i] != i+1 {
          return i + 1
       }
    }
    return n + 1
}

矩阵置零

73. 矩阵置零

1
2
3
4
5
6
7
8
func main() {
    matrix := [][]int{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}}
    setZeroes(matrix)
    fmt.Println(matrix) // [[1 0 1] [0 0 0] [1 0 1]]
}
func setZeroes(matrix [][]int) {
    // ......
}
分析

参考:O(1)空间

使用数组的第 0 行和第 0 列标记每列和每行是否存在 0。

但是对于第 0 行和第 0 列要设置一个标志位,为了防止第 0 行和第 0 列也有 0的情况。

先遍历第 0 行和第 0 列,使用两个标识变量记录它们是否存在 0。

后面遍历其他元素时,i 和 j 从 1 开始。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func setZeroes(matrix [][]int) {
    m := len(matrix)
    n := len(matrix[0])
    col_zero, row_zero := false, false // 记录第0列个第0行是否存在0
    for i := 0; i < m; i++ {
       if matrix[i][0] == 0 {
          col_zero = true
          break
       }
    }
    for j := 0; j < n; j++ {
       if matrix[0][j] == 0 {
          row_zero = true
          break
       }
    }
    for i := 1; i < m; i++ {
       for j := 1; j < n; j++ {
          if matrix[i][j] == 0 {
             matrix[i][0] = 0
             matrix[0][j] = 0
          }
       }
    }
    for i := 1; i < m; i++ {
       for j := 1; j < n; j++ {
          if matrix[i][0] == 0 || matrix[0][j] == 0 {
             matrix[i][j] = 0
          }
       }
    }
    if col_zero {
       for i := 0; i < m; i++ {
          matrix[i][0] = 0
       }
    }
    if row_zero {
       for j := 0; j < n; j++ {
          matrix[0][j] = 0
       }
    }
}

两数相加

2. 两数相加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type ListNode struct {
    Val  int
    Next *ListNode
}
func main() {
    node13 := &ListNode{9, nil}
    node12 := &ListNode{9, node13}
    node11 := &ListNode{9, node12}

    node23 := &ListNode{9, nil}
    node22 := &ListNode{9, node23}
    node21 := &ListNode{9, node22}

    r := addTwoNumbers(node11, node21)
    for r != nil {
       fmt.Printf("%d ", r.Val)
       r = r.Next
    }
}
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    // ......
}
分析

始终以 l1 为基准,将 l2 的值加到 l1 中,如果 l1 为空了,使用 pre1 指向 l1 的最后一个位置,让 pre1 的 next 等于 l2。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    oldL1 := l1  // 指向最初l1的头结点,用于最后返回
    addFlag := 0 // 进位值
    var pre1 *ListNode
    for l1 != nil && l2 != nil {
       temp := l1.Val + l2.Val + addFlag // 注意这里要用temp临时保存计算的值,因为l1.Val的值在这里会被修改,所以后面求进位值时,不能使用l1.Val + l2.Val + addFlag(此时l1.Val的值已经发生了变化)
       l1.Val = temp % 10
       addFlag = temp / 10
       pre1 = l1
       l1 = l1.Next
       l2 = l2.Next
    }
    if l1 == nil {
       pre1.Next = l2
       l1 = l2
    }
    for l1 != nil {
       temp := l1.Val + addFlag
       l1.Val = temp % 10
       addFlag = temp / 10
       if addFlag == 0 { // 如果没有进位值了,就可以不用再进行计算了
          break
       }
       pre1 = l1
       l1 = l1.Next
    }
    if addFlag == 1 {
       pre1.Next = &ListNode{1, nil}
    }
    return oldL1
}

K个一组翻转链表

【困难】25. K 个一组翻转链表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type ListNode struct {
    Val  int
    Next *ListNode
}
func main() {
    node5 := &ListNode{5, nil}
    node4 := &ListNode{4, node5}
    node3 := &ListNode{3, node4}
    node2 := &ListNode{2, node3}
    node1 := &ListNode{1, node2}
    k := 2
    newHead := reverseKGroup(node1, k)
    for newHead != nil {
       fmt.Printf("%d ", newHead.Val) // 2 1 4 3 5
       newHead = newHead.Next
    }
}
func reverseKGroup(head *ListNode, k int) *ListNode {
    // ......
}
分析

参考了答案:这种递归的写法很简洁,也很好理解。

定义一个辅助函数,传入需要翻转链表的头结点和k,翻转该链表的前k个结点,

该辅助函数有两个返回值,一个是翻转之后的头结点(用于链表连接),一个是该翻转的头结点(用于下一次翻转)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func reverseKGroup(head *ListNode, k int) *ListNode {
    if head == nil || k == 1 {
       return head
    }
    curLen := 0
    curHead := head
    for curHead != nil {
       curLen++
       curHead = curHead.Next
    }
    if curLen < k { // 判断当前链表长度是否大于等于k
       return head
    }
    reversedHead, nextHead := reverseK(head, k)
    head.Next = reverseKGroup(nextHead, k) // 递归地处理剩余的链表,head的next为翻转之后的头结点,这样就把链表连接起来了
    return reversedHead                    // 向上层返回翻转之后的头结点
}
func reverseK(head *ListNode, k int) (*ListNode, *ListNode) {
    var pre *ListNode
    for i := 0; i < k; i++ {
       temp := head.Next
       head.Next = pre
       pre = head
       head = temp
    }
    return pre, head // 返回翻转之后的头结点和下一个需要翻转的头结点
}

随机链表的复制

138. 随机链表的复制

1
2
3
4
5
6
7
8
type Node struct {
    Val    int
    Next   *Node
    Random *Node
}
func copyRandomList(head *Node) *Node {
    // ......
}
分析

参考了答案,使用map+递归。

定义一个辅助函数deepCopy,帮我们完成结点的拷贝,该函数返回拷贝后的新结点。

由于有随机指针的存在,我们需要在拷贝结点时,将该结点的随机指针指向的结点也创建出来,不让随机结点的指向就是空了。

那么我们可以递归地调用deepCopy函数来完成随机指针指向节点的拷贝。

为了防止创建多个相同的结点,我们可以使用map来存原结点和新结点的映射关系。

map的key为原结点,value为新的结点,如果存在某结点的key,说明其对应的新结点已经创建了,可以直接存map中拿出,不用再次创建。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func copyRandomList(head *Node) *Node {
    existNode := make(map[*Node]*Node) // key为原结点,value为新结点
    var deepCopy func(head *Node) *Node
    deepCopy = func(head *Node) *Node {
       if head == nil {
          return nil
       }
       if newNode, exist := existNode[head]; exist { // 要复制的结点已经创建
          return newNode
       }
       newNode := &Node{Val: head.Val}
       existNode[head] = newNode
       newNode.Next = deepCopy(head.Next) // 递归调用
       newNode.Random = deepCopy(head.Random)
       return newNode
    }
    return deepCopy(head)
}

排序链表

148. 排序链表

本题较为复杂,建议多加练习。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type ListNode struct {
    Val  int
    Next *ListNode
}
func main() {
    node5 := &ListNode{0, nil}
    node4 := &ListNode{4, node5}
    node3 := &ListNode{3, node4}
    node2 := &ListNode{5, node3}
    node1 := &ListNode{-1, node2}
    newHead := sortList(node1)
    for newHead != nil {
       fmt.Printf("%d ", newHead.Val) // -1 0 3 4 5
       newHead = newHead.Next
    }
}
func sortList(head *ListNode) *ListNode {
    // ......
}
分析

使用归并排序的思想:将链表分成两半,递归地对两半进行排序,然后将已排序的两半合并成一个有序链表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func sortList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
       return head
    }
    var merge func(head1, head2 *ListNode) *ListNode
    merge = func(head1, head2 *ListNode) *ListNode {
       pre := &ListNode{} // 虚拟头结点
       oldPre := pre
       temp1, temp2 := head1, head2
       for temp1 != nil && temp2 != nil {
          if temp1.Val <= temp2.Val { // 链表1的值更小,选择链表1的值
             pre.Next = temp1
             temp1 = temp1.Next
          } else {
             pre.Next = temp2
             temp2 = temp2.Next
          }
          pre = pre.Next
       }
       if temp1 != nil { // 将 链表1 和 链表2 中不为空的那个接在pre指针后面
          pre.Next = temp1
       } else if temp2 != nil {
          pre.Next = temp2
       }
       return oldPre.Next
    }
    var sort func(head, tail *ListNode) *ListNode
    sort = func(head, tail *ListNode) *ListNode {
       if head == nil {
          return nil
       }
       if head.Next == tail { // 链表只有两个元素,而且tail位置是不包含的,所以这里要让head.Next置空,然后返回head
          head.Next = nil
          return head
       }
       slow, fast := head, head                // 使用快慢指针找到链表的中点
       for fast != tail && fast.Next != tail { // 注意这里是tail,而不是nil
          slow = slow.Next
          fast = fast.Next.Next
       }
       mid := slow
       return merge(sort(head, mid), sort(mid, tail))
    }
    return sort(head, nil)
}

合并K个升序链表

【困难】23. 合并 K 个升序链表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type ListNode struct {
    Val  int
    Next *ListNode
}
func main() {
    list14 := &ListNode{2, nil}
    list13 := &ListNode{0, list14}
    list12 := &ListNode{-1, list13}
    list11 := &ListNode{-2, list12}

    list23 := &ListNode{4, nil}
    list22 := &ListNode{1, list23}
    list21 := &ListNode{-3, list22}

    list32 := &ListNode{-1, nil}
    list31 := &ListNode{-1, list32}

    // lists:[[-1,1],[-3,1,4],[-2,-1,0,2]]
    lists := []*ListNode{list11, list21, list31}
    newHead := mergeKLists(lists)
    for newHead != nil {
       fmt.Printf("%d ", newHead.Val) // -3 -2 -1 -1 -1 0 1 2 4
       newHead = newHead.Next
    }
}
func mergeKLists(lists []*ListNode) *ListNode {
    // ......
}
分析

利用堆的自动有序性,将所有的结点都加入到小根堆中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func mergeKLists(lists []*ListNode) *ListNode {
    minHeap := make([]*ListNode, 0)
    for _, list := range lists {
       for list != nil {
          minHeap = append(minHeap, list)
          list = list.Next
       }
    }
    n := len(minHeap)
    for i := n/2 - 1; i >= 0; i-- {
       heapify(minHeap, n, i)
    }
    pre := &ListNode{} // 虚拟头结点
    oldPre := pre
    for i := 0; i < n; i++ {
       pre.Next = &ListNode{minHeap[0].Val, nil}
       pre = pre.Next
       minHeap[0], minHeap[n-i-1] = minHeap[n-i-1], minHeap[0] // 把0位置和堆中最后一个位置交换。注意:不能简单粗暴地把第0位置的元素删除。
       heapify(minHeap, n-i-1, 0)
    }
    return oldPre.Next
}
func heapify(minHeap []*ListNode, n, i int) {
    minimum := i
    l := i*2 + 1
    r := i*2 + 2
    if l < n && minHeap[l].Val < minHeap[minimum].Val {
       minimum = l
    }
    if r < n && minHeap[r].Val < minHeap[minimum].Val {
       minimum = r
    }
    if minimum != i {
       minHeap[minimum], minHeap[i] = minHeap[i], minHeap[minimum]
       heapify(minHeap, n, minimum)
    }
}

这种方法也可以合并k个无序链表,对于每个链表的有序性没有利用到,应该还有更好的办法来实现。

LRU缓存

146. LRU 缓存

本题暂时跳过。

二叉树展开为链表

114. 二叉树展开为链表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
    node4 := &TreeNode{2, nil, nil}
    node3 := &TreeNode{4, nil, nil}
    node2 := &TreeNode{1, nil, node4}
    node1 := &TreeNode{3, node2, node3}
    flatten(node1)
    //    3
    //  1   4
    //    2
    for node1 != nil {
       fmt.Printf("%d ", node1.Val) // 3 1 2 4
       node1 = node1.Right
    }
}
func flatten(root *TreeNode) {
    // ......
}
分析

参考:详细通俗的思路分析,多解法——解法2

通过修改每个结点的右指针来达到目的。但如果使用前序遍历,会导致遍历过的结点之前的右结点丢失,使用我们需要把前序遍历逆过来。

也就是遍历顺序为:右左中,这样遍历过的结点的右孩子结点就不会丢失了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func flatten(root *TreeNode) {
    var pre *TreeNode
    var traversal func(root *TreeNode)
    traversal = func(root *TreeNode) {
       if root == nil {
          return
       }
       traversal(root.Right)
       traversal(root.Left)
       root.Right = pre
       root.Left = nil // 注意:还需要把左孩子置空
       pre = root      // 记录前一个遍历过的结点
    }
    traversal(root)
}

从前序与中序遍历序列构造二叉树

105. 从前序与中序遍历序列构造二叉树

1
2
3
4
5
6
7
8
func main() {
    preorder, inorder := []int{3, 9, 20, 15, 7}, []int{9, 3, 15, 20, 7}
    root := buildTree(preorder, inorder)
    fmt.Println(root.Val, root.Left.Val, root.Right.Val) // 3, 9, 20
}
func buildTree(preorder []int, inorder []int) *TreeNode {
    // ......
}
分析

前序:中左右;

中序:左中右。

先根据前序数组的第0个元素找到根结点的值rootVal,然后根据rootVal去切割中序数组,分成 左中序 和 右中序 两个数组。

然后根据 左中序 和 右中序 两个数组的长度去切割前序数组,得到 左前序 和 右前序 两个数组。

最后递归调用buildTree去构造二叉树

递归的出口是前序数组为空,说明二叉树为空了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
    preorder, inorder := []int{3, 9, 20, 15, 7}, []int{9, 3, 15, 20, 7}
    root := buildTree(preorder, inorder)
    fmt.Println(root.Val, root.Left.Val, root.Right.Val) // 3, 9, 20
}
func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 {
       return nil
    }
    rootVal := preorder[0]
    root := &TreeNode{rootVal, nil, nil}
    if len(preorder) == 1 { // 前缀数组只有1个结点,说明是叶子结点了,直接返回
       return root
    }
    i := 0                             // i为分割位置
    for i = 0; i < len(inorder); i++ { // 对中序数组进行分割,找到分割位置
       if inorder[i] == rootVal {
          break
       }
    }
    leftInorder := inorder[:i]
    rightInorder := inorder[i+1:]
    leftPreOrder := preorder[1 : len(leftInorder)+1] // 不包含前序数组的第0个元素
    rightPreOrder := preorder[len(leftInorder)+1:]
    root.Left = buildTree(leftPreOrder, leftInorder)
    root.Right = buildTree(rightPreOrder, rightInorder)
    return root
}

路径总和III

437. 路径总和 III

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
    node6 := &TreeNode{3, nil, nil}
    node5 := &TreeNode{5, nil, nil}
    node4 := &TreeNode{4, nil, nil}
    node3 := &TreeNode{3, node6, nil}
    node2 := &TreeNode{2, node4, node5}
    node1 := &TreeNode{1, node2, node3}
    target := 6
    r := pathSum(node1, target)
    fmt.Println(r) // 2
    //      1
    //   2     3
    // 4   5  3
}
func pathSum(root *TreeNode, targetSum int) int {
    // ......
}
分析

参考了答案:使用前缀和+回溯算法。

使用map存放当前遍历过的结点的前缀和(key为前缀和,value为该前缀和出现的次数)。

然后使用前序遍历,将当前结点的值加到前缀和curPreSum中,判断map中是否存在curPreSum-target的键,

如果存在,则统计该键出现的次数,并加到res中。

将key为curPreSum前缀和哈希表的值加1,然后遍历左子树和右子树。

遍历之后,需要将key为curPreSum前缀和哈希表的值再减1(回溯),因为本子树遍历完之后,遍历父结点的另一个子树时,当前的curPreSum值是不存在的(这点很重要)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func pathSum(root *TreeNode, targetSum int) int {
    resCount := 0
    preSumMap := make(map[int]int, 1) // key为前缀和,value为次数
    preSumMap[0] = 1                  // 初始化map,0出现了1次
    var traversal func(root *TreeNode, curPreSum int)
    traversal = func(root *TreeNode, curPreSum int) {
       if root == nil {
          return
       }
       curPreSum += root.Val
       resCount += preSumMap[curPreSum-targetSum]
       preSumMap[curPreSum]++
       traversal(root.Left, curPreSum)
       traversal(root.Right, curPreSum)
       preSumMap[curPreSum]-- // 回溯
    }
    traversal(root, 0)
    return resCount
}

二叉树的最近公共祖先

236. 二叉树的最近公共祖先

1
2
3
4
5
6
7
8
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
    // ......
}
分析

如果 root 等于 p 或 q,直接返回 root。

如果 root 的左孩子返回 p,右孩子返回 q,则直接返回 root。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
	if root == nil || root == p || root == q {
		return root
	}
	left := lowestCommonAncestor(root.Left, p, q)
	right := lowestCommonAncestor(root.Right, p, q)
	if left != nil && right != nil {
		return root
	} else if left != nil {
		return left
	} else {
		return right
	}
}

二叉树中的最大路径和

【困难】124. 二叉树中的最大路径和

1
2
3
4
5
6
7
8
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}
func maxPathSum(root *TreeNode) int {
    // ......
}
分析
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func maxPathSum(root *TreeNode) int {
    // 使用一个全局变量maxSum记录最大路径和,maxGain函数返回的是最大贡献值
    if root == nil {
       return 0
    }
    maxSum := root.Val
    var maxGain func(root *TreeNode) int
    maxGain = func(root *TreeNode) int {
       if root == nil {
          return 0
       }
       leftGain := max(maxGain(root.Left), 0)
       rightGain := max(maxGain(root.Right), 0)
       maxSum = max(maxSum, root.Val+leftGain+rightGain)
       return root.Val + max(leftGain, rightGain)
    }
    maxGain(root)
    return maxSum
}

岛屿数量

腐烂的橘子

课程表

实现Trie(前缀树)

电话号码的字母组合

17. 电话号码的字母组合

1
2
3
4
5
6
7
8
func main() {
    digits := "23"
    r := letterCombinations(digits)
    fmt.Println(r) // ["ad","ae","af","bd","be","bf","cd","ce","cf"]
}
func letterCombinations(digits string) []string {
    // ......
}
分析

电话号码的字母组合做题记录

好久没写回溯的题了,思路有点混乱,参考了答案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func letterCombinations(digits string) []string {
    // 先将映射关系存入map中,然后获得数字对应的字母的全排列
    arr := make([]byte, 0)
    res := make([]string, 0)
    mapping := map[byte]string{
       '2': "abc",
       '3': "def",
       '4': "ghi",
       '5': "jkl",
       '6': "mno",
       '7': "pqrs",
       '8': "tuv",
       '9': "wxyz",
    }
    var backtracking func(digits string, start int)
    backtracking = func(digits string, start int) {
       if len(arr) == len(digits) {
          res = append(res, string(arr))
          return
       }
       word := mapping[digits[start]]
       for i := 0; i < len(word); i++ { // 遍历这个数字对于的字符组合
          arr = append(arr, word[i])
          backtracking(digits, start+1) // 每层递归选择一个数字
          arr = arr[:len(arr)-1]
       }
    }
    backtracking(digits, 0)
    return res
}

括号生成

22. 括号生成

1
2
3
4
5
6
7
8
func main() {
    n := 3
    r := generateParenthesis(n)
    fmt.Println(r) // ["((()))","(()())","(())()","()(())","()()()"]
}
func generateParenthesis(n int) []string {
    // ......
}
分析

参考了题解:递归——剩余左括号总数要小于等于右括号

编写一个辅助函数,用来生成目标结果,它有3个参数:

  • str:当前已经生成的括号组合字符串。
  • left:还可以添加的左括号数量。
  • right:还可以添加的右括号数量。

如果left==right==0,则说明已经添加完所有的括号了,直接将结果添加到res数组中。

如果left和right相等,说明剩余的左右括号数量相同,下一个只能添加左括号(因为不能先添加右括号,否则它将没有匹配的左括号)。

如果left小于right,下一个可以添加左括号或右括号,但添加左括号的前提是还有剩余的左括号可用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func generateParenthesis(n int) []string {
    res := make([]string, 0)
    var genParenthesis func(str string, left, right int)
    genParenthesis = func(str string, left, right int) {
       if left == 0 && right == 0 {
          res = append(res, str)
          return
       } else if left == right { // 只能先添加左括号
          genParenthesis(str+"(", left-1, right) // 其实这里也隐含了回溯的过程,因为str值本身没有变化,就相当与回溯了
       } else if left < right { // 即可以添加左括号,也可以添加右括号
          if left > 0 {
             genParenthesis(str+"(", left-1, right)
          }
          genParenthesis(str+")", left, right-1)
       }
    }
    genParenthesis("", n, n) // 初始值,left和right都为n
    return res
}

单词搜索

79. 单词搜索

1
2
3
4
5
6
7
8
9
func main() {
    board := [][]byte{{'A', 'B', 'C', 'E'}, {'S', 'F', 'C', 'S'}, {'A', 'D', 'E', 'E'}}
    word := "ABCCED"
    r := exist(board, word)
    fmt.Println(r)
}
func exist(board [][]byte, word string) bool {
    // ......
}
分析

参考题解:深度优先搜索(DFS)+ 剪枝

对每个位置进行递归的搜索,在每个位置可以向上、下、左、右四个方向进行搜索,在搜索时需要判断该位置是否合法

使用过的位置我们要进行标记,下次不能再使用

定义一个辅助函数来实现每个位置开始的搜索过程,它有3个参数:i、j、k,分别表示开始搜索的横坐标、纵坐标、下一个应该匹配的word下标。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func exist(board [][]byte, word string) bool {
    m := len(board)
    n := len(board[0])
    var backtracking func(i, j, k int) bool
    backtracking = func(i, j, k int) bool {
       if i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word[k] { // 非法,直接返回false
          return false
       }
       if k == len(word)-1 { // k和len(word)-1相等(k位置已经匹配才会执行到这里),说明已经找到完全匹配的了,直接返回true
          return true
       }
       board[i][j] = ' '                   // 标记(i,j)位置为不可使用
       res := backtracking(i-1, j, k+1) || // 可以往4个方向搜索
          backtracking(i+1, j, k+1) ||
          backtracking(i, j-1, k+1) ||
          backtracking(i, j+1, k+1)
       board[i][j] = word[k] // 回溯,让该位置在下一次递归中可用
       return res
    }
    for i := 0; i < m; i++ {
       for j := 0; j < n; j++ {
          if backtracking(i, j, 0) { // 只要找到一个满足条件的,直接返回true
             return true
          }
       }
    }
    return false
}

分割回文串

131. 分割回文串

1
2
3
4
5
6
7
8
func main() {
    s := "aab"
    r := partition(s)
    fmt.Println(r)
}
func partition(s string) [][]string {
    // ......
}
分析

上次练习是 2024.05.01,这次练习是 2024.05.25。本题写得有点磕绊,还是要多加练习。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func partition(s string) [][]string {
    // start表示分割的位置
    arr := make([]string, 0)
    res := make([][]string, 0)
    var backtracking func(s string, start int)
    backtracking = func(s string, start int) {
       if start == len(s) {
          temp := make([]string, len(arr))
          copy(temp, arr)
          res = append(res, temp)
          return
       }
       for i := start; i < len(s); i++ {
          if isPalindrome(s[start : i+1]) {
             arr = append(arr, s[start:i+1])
             backtracking(s, i+1)
             arr = arr[:len(arr)-1]
          }
       }
    }
    backtracking(s, 0)
    return res
}
func isPalindrome(s string) bool {
    l, r := 0, len(s)-1
    for l < r {
       if s[l] != s[r] {
          return false
       }
       l++
       r--
    }
    return true
}

N皇后

【困难】51. N 皇后

本题过于复杂,先暂时跳过。

搜索旋转排序数组

33. 搜索旋转排序数组

1
2
3
4
5
6
7
8
9
func main() {
    nums, target := []int{4, 5, 6, 7, 0, 1, 2}, 0 // 4
    //nums, target := []int{1, 2, 3}, 3 // 2
    r := search(nums, target)
    fmt.Println(r) // 4
}
func search(nums []int, target int) int {
    // ......
}
分析

二分查找,使用mid将数组分为两个部分,其中一个数组必然是升序数组,另一个数字是循环数组。

判断左右数组的属性,是升序数组还是循环数组。

然后判断target在升序数组里面,还是循环数组里面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func search(nums []int, target int) int {
    n := len(nums)
    l, r := 0, n-1
    for l <= r {
       mid := l + (r-l)>>1
       if nums[mid] == target {
          return mid
       }
       // 注意这里区分升序数组和循环数组的条件,如果mid值大于r的值,说明r的左侧是升序数组,那么右侧就是循环数组了
       // 反之,r的右侧就是升序数组,左侧是循环数组
       if nums[mid] > nums[r] { // 左侧升序数组,右侧循环数组
          // 判断target在升序数组里面,还是循环数组里面
          if nums[l] <= target && target < nums[mid] { // 在升序数组里面
             r = mid - 1
          } else { // 在循环数组里面
             l = mid + 1
          }
       } else { // 左侧循环数组,右侧升序数组
          if nums[mid] < target && target <= nums[r] { // 在升序数组里面
             l = mid + 1
          } else { // 在循环数组里面
             r = mid - 1
          }
       }
    }
    return -1
}

寻找旋转排序数组中的最小值

153. 寻找旋转排序数组中的最小值

寻找两个正序数组的中位数

【困难】4. 寻找两个正序数组的中位数

1
2
3
4
5
6
7
8
9
func main() {
    nums1 := []int{1, 2, 3}
    nums2 := []int{4, 5, 6}
    r := findMedianSortedArrays(nums1, nums2)
    fmt.Println(r) // 3.5
}
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
    // ......
}
分析

二分查找+递归。 本题比较复杂,建议多加练习和理解。

把本问题转换为求两个数组第k小的元素。

比较两个数组最中间元素的大小关系:

  • 如果nums1[mid]更小,说明nums1[mid]前面的元素都不可能是第k小的元素,则前面的元素就都可以删除。
  • 如果nums2[mid]更小,则nums2[mid]前面的元素都可以删除。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
    var getKth func(i, j, k int) int // i为nums1的开始位置,j为nums2的开始位置,k为第k小的元素(k从1开始)。
    getKth = func(i, j, k int) int {
       if i == len(nums1) { // nums1数组已经遍历完了,那么就从nums2数组中找到j后面第k大的元素。
          return nums2[j+k-1]
       }
       if j == len(nums2) { // 反之,从nums1数组中找到第k大的元素。
          return nums1[i+k-1]
       }
       if k == 1 { // 收获结果的时候
          return min(nums1[i], nums2[j])
       }
       n := min(i+k/2, len(nums1)) - 1 // nums1剩余数组的中间位置
       m := min(j+k/2, len(nums2)) - 1 // nums2剩余数组的中间位置
       if nums1[n] > nums2[m] {        // nums1中间位置的元素更大,则删除nums2[m]前面的元素(也不是真的删除,就是开始搜索位置变成了m+1)
          return getKth(i, m+1, k-(m-j+1))
       } else { // 反之,i的搜索位置变成n+1
          return getKth(n+1, j, k-(n-i+1))
       }
    }
    l := len(nums1) + len(nums2) // 计算两个数组的长度
    // 由于k从1开始,所以这里要找第(l+1)/2小和第(l+1)/2小元素的平均值
    return float64(getKth(0, 0, (l+1)/2)+getKth(0, 0, (l+2)/2)) * 0.5
}

字符串解码

394. 字符串解码

1
2
3
4
5
6
7
8
9
func main() {
    //s := "3[a2[c]]"
    s := "100[leetcode]"
    r := decodeString(s)
    fmt.Println(r) // accaccacc
}
func decodeString(s string) string {
    // ......
}
分析

参考:双栈法

本题的关键是处理括号嵌套的问题。

我们使用 curStr 来保存当前括号中的字符串内容,方便向上层返回。

我们使用两个栈,一个栈(kStack)记录重复次数 k,另一个栈(strStack)记录字符串

遍历s:

  • 如果我们遇到的是数字,则将其添加到kStack中;
  • 如果遇到的是[,则将curStr添加到strStack中,并将curStr置空
  • 如果遇到的是],说明我们该解码了,将curStr重复k次,并存入到curStr中。
  • 如果遇到的是字母,则将其加到curStr即可。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func decodeString(s string) string {
    k, kStack, strStack := 0, make([]int, 0), make([]string, 0)
    curStr := ""
    for _, str := range s {
       if str >= '0' && str <= '9' {
          k = k*10 + int(str-'0') // 处理多位数字的情况
       } else if str == '[' { // 遇到的是[,将k和curStr压入到各自的栈中,并重置k和curStr
          kStack = append(kStack, k)
          strStack = append(strStack, curStr)
          curStr = ""
          k = 0
       } else if str == ']' { // 遇到的是[,进行解码
          tempStr := make([]string, 1)
          tempStr[0] = strStack[len(strStack)-1]
          strStack = strStack[:len(strStack)-1]
          tempK := kStack[len(kStack)-1]
          kStack = kStack[:len(kStack)-1]
          for i := 0; i < tempK; i++ {
             tempStr = append(tempStr, curStr) // 将curStr重复tempK次
          }
          curStr = strings.Join(tempStr, "")
       } else { // 遇到的是字母,直接加到curStr中
          curStr += string(str)
       }
    }
    return curStr
}

每日温度

739. 每日温度

1
2
3
4
5
6
7
8
func main() {
    temperatures := []int{73, 74, 75, 71, 69, 72, 76, 73}
    r := dailyTemperatures(temperatures)
    fmt.Println(r) // [1,1,4,2,1,1,0,0]
}
func dailyTemperatures(temperatures []int) []int {
    // ......
}
分析

参考了官解:方法二:单调栈

维护一个温度单调递减的栈(使用队列模拟),栈中不直接存温度,而是温度的下标 i(这样方便计算两个温度之间的天数间隔)

遍历数组:

  • 如果当前temper[i]值小于栈顶元素,则直接将i加入到栈中。

  • 如果当前temper[i]值大于栈顶元素,则将其前面小于temper[i]的元素全部弹出栈。

    在元素弹出栈时,需要更新每个弹出元素outIndex的res数组值(也就是说,如果某个元素弹出栈了,说明找到了比它温度高的i,则更新res[outIndex]=i-outIndex)。

  • 如果栈为空,则直接将i加入到栈中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func dailyTemperatures(temperatures []int) []int {
    n := len(temperatures)
    res := make([]int, n)
    minStack := make([]int, 0)
    for i, temper := range temperatures {
       for len(minStack) > 0 && temper > temperatures[minStack[len(minStack)-1]] { // 新加入元素比栈顶元素大
          outIndex := minStack[len(minStack)-1] // 需要出栈的下标
          minStack = minStack[:len(minStack)-1] // 将栈顶元素出栈
          res[outIndex] = i - outIndex          // i位置就是outIndex位置的后一个更高温度的下标
       }
       minStack = append(minStack, i) // 栈大小为空 或者 添加的元素小于栈顶元素,直接加入到栈中
    }
    return res
}

柱状图中最大的矩形

【困难】84. 柱状图中最大的矩形

1
2
3
4
5
6
7
8
func main() {
    heights := []int{2, 1, 5, 6, 2, 3}
    r := largestRectangleArea(heights)
    fmt.Println(r) // 10
}
func largestRectangleArea(heights []int) int {
    // ......
}
分析

使用单调栈来实现。

在i位置,我们需要找到左边第1个比它小的位置leftIndex和右边第1个比它小的位置rightIndex,i位置的最大宽度就是rightIndex-leftIndex-1。

我们需要维护一个最大栈,也就是栈顶元素最大,栈顶到栈底依次递减(可以相等)。具体如下:

  • 如果遇到的元素比栈顶元素大(或者等),则直接将其添加到栈中(添加的是元素的下标);
  • 如果遇到的元素比栈顶元素小(右边就不能继续往后延展了),就是我们需要收获结果的时候。

我们计算以栈顶元素stackTop的值为高度,能形成的最大矩形。

矩形的左侧位置就是栈顶元素stackTop在栈中的前一个位置(比栈顶位置值大);

矩形的右侧位置就是我们遇到的元素位置。

这样计算一个面积值之后,更新maxArea的值。

然后将栈中比添加元素大的元素全部弹出,将遇到的元素加入到栈中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func largestRectangleArea(heights []int) int {
    newHeights := make([]int, len(heights)+2) // 为了能够正常收获结果,在原数组的首尾加上0
    newHeights[0], newHeights[len(newHeights)-1] = 0, 0
    for i := 1; i < len(newHeights)-1; i++ {
       newHeights[i] = heights[i-1]
    }
    maxStack := make([]int, 0)
    curArea, maxArea := 0, 0
    for i, h := range newHeights {
       for len(maxStack) > 0 && h < newHeights[maxStack[len(maxStack)-1]] { // h比栈顶元素小
          stackTop := maxStack[len(maxStack)-1]
          maxStack = maxStack[:len(maxStack)-1] // 将栈顶元素弹出
          lIndex := maxStack[len(maxStack)-1]   // 左侧的位置就是弹出之后的栈顶值
          curArea = (i - lIndex - 1) * newHeights[stackTop]
          maxArea = max(maxArea, curArea)
       }
       maxStack = append(maxStack, i)
    }
    return maxArea
}

数组中的第K个最大元素

215. 数组中的第K个最大元素

分析

方法1: 使用堆排序。

维护一个容量为k的小顶堆,当全部元素都加入到堆中后,堆顶元素就是第k大的元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func findKthLargest(nums []int, k int) int {
	heap := make([]int, k)
	for i, num := range nums {
		if i < k {
			heap[i] = num
			if i == k-1 {
				for i := k/2 - 1; i >= 0; i-- {
					heapify(heap, k, i)
				}
			}
		} else {
			if num > heap[0] {
				heap[0] = num
				heapify(heap, k, 0)
			}
		}
	}
	return heap[0]
}
func heapify(nums []int, n, i int) {
	l := i*2 + 1
	r := i*2 + 2
	min := i
	if l < n && nums[l] < nums[min] {
		min = l
	}
	if r < n && nums[r] < nums[min] {
		min = r
	}
	if min != i {
		nums[i], nums[min] = nums[min], nums[i]
		heapify(nums, n, min)
	}
}

上面这种方法在极端情况下,时间复杂度为$O(nlogn)$,而本题要求的时间复杂度为$O(n)$,所以应该还有更好的方法。

前K个高频元素

347. 前 K 个高频元素

1
2
3
4
5
6
7
8
func main() {
    nums, k := []int{2, 2, 3, 1, 1, 1}, 2
    r := topKFrequent(nums, k)
    fmt.Println(r) // 1,2
}
func topKFrequent(nums []int, k int) []int {
    // ......
}
分析

上次做这道题是 2024.04.30,这次做这道题是 2024.05.25。

好久没写了,中间有一点小问题,参考了答案,当堆中元素个数为 k 时,堆化的过程是循环的,而不是只执行一次,这点需要多加注意。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func topKFrequent(nums []int, k int) []int {
    // 构建能够容纳k个元素的小根堆,根据元素出现的次数进行排序
    numCount := make(map[int]int) // 统计每个元素出现的次数
    for _, num := range nums {
       numCount[num]++
    }
    var minHeap []heapItem
    heapCount := 0
    for num, count := range numCount {
       if heapCount < k {
          minHeap = append(minHeap, heapItem{num, count})
          heapCount++
          if heapCount == k {
             for i := k / 2; i >= 0; i-- { // 注意:这里的堆化是一个循环的过程
                heapify(minHeap, k, i)
             }
          }
       } else {
          if count > minHeap[0].count { // 新加入的元素次数超过了堆顶元素的次数,则将对推替换为新加入的元素
             minHeap[0] = heapItem{num, count}
             heapify(minHeap, k, 0)
          }
       }
    }
    res := make([]int, k)
    for i := k - 1; i >= 0; i-- {
       res[i] = minHeap[0].num
       minHeap = minHeap[1:]
       heapify(minHeap, i, 0)
    }
    return res
}
type heapItem struct {
    num   int
    count int
}
func heapify(minHeap []heapItem, n, i int) {
    left := i*2 + 1
    right := i*2 + 2
    min := i
    if left < n && minHeap[left].count < minHeap[min].count {
       min = left
    }
    if right < n && minHeap[right].count < minHeap[min].count {
       min = right
    }
    if min != i {
       minHeap[i], minHeap[min] = minHeap[min], minHeap[i]
       heapify(minHeap, n, min)
    }
}

数据流的中位数

【困难】295. 数据流的中位数

本题暂时跳过。

跳跃游戏II

45. 跳跃游戏 II

1
2
3
4
5
6
7
8
func main() {
    nums := []int{2, 3, 1, 1, 4}
    r := jump(nums)
    fmt.Println(r) // 2
}
func jump(nums []int) int {
    // ......
}
分析

我们只关心当前覆盖的最大范围。

我们还要在当前的覆盖范围内,找到下一步能够覆盖的最大范围,因为如果这一步不能到达末尾,我们需要再跳一步,所以在遍历当前覆盖范围时,就要求出下一步能够覆盖的最大范围。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func jump(nums []int) int {
    cur, next, stepCount := 0, 0, 0
    for i := 0; i < len(nums); i++ {
       next = max(next, i+nums[i]) // 下一步能覆盖的范围
       if cur >= len(nums)-1 {     // 覆盖了最后位置,返回最少步数
          return stepCount
       }
       if i == cur { // 到达了当前最大覆盖范围,需要增加步数了
          stepCount++
          cur = next
       }
    }
    return stepCount
}

划分字母区间

763. 划分字母区间

1
2
3
4
5
6
7
8
func main() {
    s := "ababcbacadefegdehijhklij"
    r := partitionLabels(s)
    fmt.Println(r) // [9,7,8]
}
func partitionLabels(s string) []int {
    // ......
}
分析

先遍历s,使用map记录每个元素最后出现的位置。

遍历当前范围内所有元素到其最后出现的位置,然后就可以划分下一个区间了。

使用left和right记录当前区间的左侧位置和右侧位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func partitionLabels(s string) []int {
    lastIndex := make(map[byte]int)
    for i := 0; i < len(s); i++ {
       lastIndex[s[i]] = i
    }
    res := make([]int, 0)
    left, right := 0, 0 // 区间的左右侧位置
    for i := 0; i < len(s); i++ {
       right = max(right, lastIndex[s[i]]) // 更新当前区间的右侧位置
       if i == right {                     // 当前区间结束
          res = append(res, right-left+1)
          left = right + 1
       }
    }
    return res
}

乘积最大子数组

152. 乘积最大子数组

1
2
3
4
5
6
7
8
9
func main() {
    nums := []int{2, 3, -2, 4}
    //nums := []int{-2, 3, -4} // 24
    r := maxProduct(nums)
    fmt.Println(r) // 6
}
func maxProduct(nums []int) int {
    // ......
}
分析

参考了官方题解:方法一:动态规划——考虑优化空间

由于在i位置,获得最大乘积的方式有三种:

  1. 之前的最大乘积 * 当前元素

  2. 之前的最小乘积 * 当前元素

  3. 当前元素值

所以我们需要维护最大乘积和最小乘积两个dp变量(不用数组,压缩空间)。

每遍历一个元素,就更新这两个变量的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func maxProduct(nums []int) int {
    maxProd, minProd, res := nums[0], nums[0], nums[0]
    for i := 1; i < len(nums); i++ {
       // 记录上一个位置的最大乘积和最小乘积。注意:这里要用两个临时变量来存之前的值,
       // 因为在循环中,maxProd的值会先被改变,如果直接使用maxProd会导致计算错误。
       maxP, minP := maxProd, minProd
       maxProd = max(maxP*nums[i], minP*nums[i], nums[i])
       minProd = min(maxP*nums[i], minP*nums[i], nums[i])
       res = max(res, maxProd)
    }
    return res
}

最长有效括号

【困难】32. 最长有效括号

1
2
3
4
5
6
7
8
func main() {
    s := ")()())"
    r := longestValidParentheses(s)
    fmt.Println(r) // 4
}
func longestValidParentheses(s string) int {
    // ......
}
分析

方法1:动态规划

dp[i]表示以s[i]结尾的字符串,最长有效括号的长度。

遍历s:

  • 如果s[i]=’(’,那么一定不能构成有效的括号,则dp[i]=0。

  • 如果s[i]=’)’,则要判断i-dp[i-1]-1位置的元素是否是’(’:

    • 如果不是,则dp[i]=0;

    • 如果是,则dp[i]=dp[i-1]+2+dp[i-dp[i-1]-2]。

递推表达式解释:dp[i-1]是i的前一个位置有效括号的长度,用i减去中间有效的长度,比较i位置和它应该比较的位置是否匹配。

为什么还要加上dp[i-dp[i-1]-2]?因为匹配位置的前面还有可能存在匹配的括号,所以需要加上前面的dp数组值。

初始化dp数组,dp数组的初始值都是0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func longestValidParentheses(s string) int {
    maxLen := 0
    dp := make([]int, len(s))
    for i := 1; i < len(s); i++ { // i从1开始
       if s[i] == '(' { // 左括号,不可能构成有效括号,直接跳过
          continue
       } else { // 右括号,比较i位置和它应该比较的位置
          if i-dp[i-1]-1 < 0 { // 应该匹配的位置不合法,dp[i]为0
             dp[i] = 0
          } else if s[i-dp[i-1]-1] == '(' { // i位置和前面应该比较的位置匹配上了
             if i-dp[i-1]-2 < 0 { // 匹配位置前面没有元素了
                dp[i] = dp[i-1] + 2
             } else {
                dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] // 这里记得加上 dp[i-dp[i-1]-2]
             }
             if dp[i] > maxLen { // 更新最大长度
                maxLen = dp[i]
             }
          }
       }
    }
    return maxLen
}

下一个排列

31. 下一个排列

1
2
3
4
5
6
7
8
func main() {
    nums := []int{3, 2, 1}
    nextPermutation(nums)
    fmt.Println(nums) // 1,2,3
}
func nextPermutation(nums []int) {
    // ......
}
分析

参考官方题解:下一个排列

使用两个指针,先从右往左遍历,找到第1个nums[i]小于nums[i+1]的数(左边的较小值)。

然后再次从右往左遍历,找到第1个大于nums[i]的数(右边的较大值)。

然后让两个元素值进行交换。

交换之后,让之前左边较小值位置后面的元素全部逆序,这样就能得到下一个排列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func nextPermutation(nums []int) {
    i := len(nums) - 2
    for i >= 0 && nums[i] >= nums[i+1] { //找到第1个右到左降序的值
       i--
    }
    if i >= 0 { // 如果i小于0,说明数组从右到左始终是升序的,那么交换元素的过程就不需要了,直接将元素值逆序即可。
       j := len(nums) - 1
       for j >= 0 && nums[j] <= nums[i] {
          j--
       }
       nums[i], nums[j] = nums[j], nums[i]
    }
    reverse := func(nums []int) {
       for i := 0; i < len(nums)/2; i++ {
          nums[i], nums[len(nums)-i-1] = nums[len(nums)-i-1], nums[i]
       }
    }
    reverse(nums[i+1:])
}

寻找重复数

287. 寻找重复数

1
2
3
4
5
6
7
8
func main() {
    nums := []int{1, 3, 4, 2, 2}
    r := findDuplicate(nums)
    fmt.Println(r) // 2
}
func findDuplicate(nums []int) int {
    // ......
}
分析

使用快慢指针来解决本题。让i位置的值作为索引i指向的下一个元素

在上面的例子中,0—>1—>3—>2—>4—>2,当4指向2时,这个链表就形成了一个环,也就是有两个位置的元素值都是2,那么2就是那个重复值。

寻找环入口的方式和 142. 环形链表 II 相同。

快指针每次移动2步,慢指针每次移动1步。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func findDuplicate(nums []int) int {
    slow, fast := nums[0], nums[0]
    for {
       slow = nums[slow]       // slow移1步
       fast = nums[nums[fast]] // fast移2步
       if slow == fast {       // 找到相遇点
          break
       }
    }
    temp := nums[0]    // temp从头开始移动
    for temp != slow { // temp和slow同时移动
       temp = nums[temp]
       slow = nums[slow]
    }
    return temp
}

做题记录

20240523

#1 两数之和

#283 移动零

#3 无重复字符的最长子串

#53 最大子数组和

#160 相交链表

#206 反转链表

#234 回文链表

#141 环形链表

#142 环形链表 II

#21 合并两个有序链表

#2 两数相加

#19 删除链表的倒数第 N 个结点

#24 两两交换链表中的节点

#94 二叉树的中序遍历

#104 二叉树的最大深度

#226 翻转二叉树

#101 对称二叉树

#543 二叉树的直径

#102 二叉树的层序遍历

#108 将有序数组转换为二叉搜索树

#98 验证二叉搜索树

#199 二叉树的右视图

#236 二叉树的最近公共祖先

#124 二叉树中的最大路径和

#121 买卖股票的最佳时机

#55 跳跃游戏

#70 爬楼梯

20240524

#15 三数之和

#239 滑动窗口最大值

#56 合并区间

#189 轮转数组

#46 全排列

#78 子集

#17 电话号码的字母组合

#131 分割回文串

#39 组合总和

#35 搜索插入位置

#34 在排序数组中查找元素的第一个和最后一个位置

#153 寻找旋转排序数组中的最小值

#20 有效的括号

#118 杨辉三角

#198 打家劫舍

#279 完全平方数

#322 零钱兑换

#139 单词拆分

#300 最长递增子序列

20240525

#416 分割等和子集

#62 不同路径

#64 最小路径和

#5 最长回文子串

#1143 最长公共子序列

#72 编辑距离

#48 旋转图像

#215 数组中的第K个最大元素

#347 前 K 个高频元素

从后面的题目开始就基本上是之前没有做过的新题了。

#49 字母异位词分组

#128 最长连续序列

#11 盛最多水的容器

#42 接雨水

#438 找到字符串中所有字母异位词

20240526

#560 和为 K 的子数组

#76 最小覆盖子串

#238 除自身以外数组的乘积

#41 缺失的第一个正数

#73 矩阵置零

#54 螺旋矩阵

#240 搜索二维矩阵 II

20240627

#74 搜索二维矩阵

20240528

#155 最小栈

20240529

#394 字符串解码

#739 每日温度

#84 柱状图中最大的矩形

#25 K 个一组翻转链表

#138 随机链表的复制

#148 排序链表

#23 合并 K 个升序链表

20240530

#230 二叉搜索树中第K小的元素

#114 二叉树展开为链表

#105 从前序与中序遍历序列构造二叉树

#437 路径总和 III

20240531

#22 括号生成

#79 单词搜索

#51 N 皇后

#45 跳跃游戏 II

#763 划分字母区间

#152 乘积最大子数组

#32 最长有效括号

#33 搜索旋转排序数组

#4 寻找两个正序数组的中位数

#136 只出现一次的数字

#169 多数元素

#75 颜色分类

#31 下一个排列

20240601

#287 寻找重复数

#146 LRU 缓存

#295 数据流的中位数

0%