跳转至

Go 刷题清单

数组

综合常考

本题可用暴力枚举和前缀和两种方法求解。以 nums=[1,-1,2,3,-2,4], k=3 为例,满足条件的子数组共有 3 个,分别是 [1,-1,2,3,-2][2,3,-2][3]。其前缀和如下表所示。由于 prefixSum[5]-prefixSum[0]=3,说明区间 [1,-1,2,3,-2] 的元素和恰好为 3。因此,只要在遍历过程中查询哈希表中是否存在 prefixSum-k,并累加它的出现次数,就能统计出和为 k 的子数组个数。

nums 1 -1 2 3 -2 4
prefixSum 0 1 0 2 5 3 7
freq 2 1 1 1 1 1
func subarraySum(nums []int, k int) int {
    count, prefixSum := 0, 0
    prefixSumMap := map[int]int{0:1} // 兼容 nums=[3], k=3
    for i := range nums {
        currentSum += nums[i] // 计算前缀和
        if _, ok := prefixSumMap[currentSum-k]; ok { // 也可以利用 Go Map 的零值机制优化为一行代码
            count += prefixSumMap[currentSum-k] // 累计频率
        }
        prefixSumMap[currentSum]++ // 对前缀和值的出现次数统计
    }

    return count
}

哈希表

基础必会

func twoSum(nums []int, target int) []int {
    // 哈希表记录出现的元素及索引
    // 遍历 nums,如果 target - num 在哈希表中,返回索引
    // 否则将 num 加入哈希表

    seen := map[int]int{}
    for i, num := range nums {
        if j, ok := seen[target-num]; ok {
            return []int{i, j}
        }
        seen[num] = i
    }

    return []int{}
}

综合常考

在本题中,要求\(O(n)\)求解,排序后复杂度为\(O(logn)\),不符合要求。因此需要把输入存入哈希表,然后遍历哈希表,如果取值的前一位不在哈希表,则当前值作为序列起始值;如果下一位在哈希表,则认为是连续序列。

func longestConsecutive(nums []int) int {
    has := map[int]bool{} // 直接初始化为 bool 可以简化哈希表判断
    for _, num := range nums {
        has[num] = true
    }

    ans := 0
    for num := range has {
        prev, next := num-1, num+1
        if has[prev] { // 上一个数存在于哈希表,说明 num 不是序列的起点
            continue
        }

        for has[next] { // 不断查找下一个序列是否在哈希表中
            next++
        }

        ans = max(ans, next-num)
    }

    return ans
}
  • 排序解法
    排序解法
  • 计数解法
    计数解法
// 排序解法
// Time: O(m*nlogn), Space: O(m*n)
func groupAnagrams(strs []string) [][]string {
    groups := map[string][]string{}
    for _, str := range strs {
        bytes := []byte(str)
        slices.Sort(bytes)
        key := string(bytes)
        groups[key] = append(groups[key], str)
    }

    ans := make([][]string, 0, len(groups))
    for _, group := range groups {
        ans = append(ans, group)
    }

    return ans
}
// 计数解法
// Time: O(m*n), Space: O(m*n)
func groupAnagrams(strs []string) [][]string {
    groups := map[[26]int][]string{}
    for _, str := range strs {
        count := [26]int{}
        for _, ch := range str {
            count[ch-'a']++
        }
        groups[count] = append(groups[count], str)
    }

    ans := make([][]string, 0, len(groups))
    for _, group := range groups {
        ans = append(ans, group)
    }

    return ans
}

双指针

基础必会

对撞指针计算面积,只移动较矮的一边。

func maxArea(height []int) int {
    maxArea, left, right := 0, 0, len(height)-1
    for left < right {
        area := (right - left) * min(height[left], height[right]) // 以较矮的作为高计算面积
        maxArea = max(maxArea, area)
        // 只移动较矮的一边
        if height[left] < height[right] {
            left++
        } else {
            right--
        }
    }

    return maxArea
}

双指针倒序填充

// Time: O(m+n), Space: O(1)
func merge(nums1 []int, m int, nums2 []int, n int) {
    p1, p2, tail := m-1, n-1, m+n-1

    // 只需检查 p2 >= 0,因为 nums2 处理完后,nums1 剩余元素已在正确位置
    for p2 >= 0 {
        if p1 >= 0 && nums1[p1] > nums2[p2] {
            nums1[tail] = nums1[p1]
            p1--
        } else {
            nums1[tail] = nums2[p2]
            p2--
        }
        tail--
    }
}

注意移动零时还要保持非零元素的相对位置,因此不能使用对撞指针,而应该用快慢指针不断交换零和非零元素

1
2
3
4
5
6
7
8
9
func moveZeroes(nums []int)  {
    slow := 0 // 并不能确定 nums[0] 是否为非零值
    for fast := range nums { // fast 要从0开始遍历,否则可能遗漏首位的零值交换。首位是非零值会发生自己和自己交换
        if nums[fast] != 0 {
            nums[slow], nums[fast] = nums[fast], nums[slow]
            slow++
        }
    }
}

题目描述不清楚,本意是想把nums中不重复的元素移动到数组左边,然后返回数组中不重复元素的个数,因此不能使用哈希表求解。

注意本题是有序数组,也就意味着重复项是相邻的,因此可以类似#283通过快慢指针把重复元素交换到尾部。注意#283为了避免遗漏首位的零值交换,fast 要从 0 开始,本题是判断重复元素,fast 可以从 1 开始

func removeDuplicates(nums []int) int {
    slow := 0
    for fast := 1; fast < len(nums); fast++ {
        if nums[slow] != nums[fast] {
            slow++ // // 注意要交换到 slow 的下一个位置
            nums[slow] = nums[fast]
        }
    }

    return slow+1 // 注意差1问题。Go 中 i++ 是语句,不是表达式,不能返回 i++
}

综合常考

func lengthOfLongestSubstring(s string) int {
    // 哈希表记录出现的元素及索引
    // 哈希表出现的元素,left 移动到哈希表中索引 +1 位置,否则右指针右移扩大窗口

    seen := map[byte]int{}
    left, right, maxWin := 0, 0, 0
    for right < len(s) {
        if prevIndex, ok := seen[s[right]]; ok {
            left = prevIndex + 1
        }

        seen[s[right]] = right
        right++

        maxWin = max(maxWin, right-left)
    }

    return maxWin
}
func threeSum(nums []int) [][]int {
    if len(nums) < 3 {
        return nil
    }

    sort.Ints(nums)

    ans := [][]int{}
    // 先固定第一个数
    for i := 0; i < len(nums)-2; i++ {
        // 提前剪枝:如果第一个数已经大于0,后面都是正数,不可能和为0
        if nums[i] > 0 {
            break
        }

        if i > 0 && nums[i] == nums[i-1] {
            continue // 跳过重复的第一个数
        }

        // 用双指针让剩余两数之和与第一个数的和为0
        left, right := i+1, len(nums)-1
        for left < right {
            sum := nums[i] + nums[left] + nums[right]

            // 注意此时已经排序
            if sum < 0 { // 和太小,需要右移靠近较大数
                left++
            } else if sum > 0 { // 和太大,需要左移靠近较小数
                right--
            } else { // 和为 0
                ans = append(ans, []int{nums[i], nums[left], nums[right]})

                // 跳过重复的第二个数
                for left < right && nums[left] == nums[left+1] {
                    left++
                }

                // 跳过重复的第三个数
                for left < right && nums[right] == nums[right-1] {
                    right--
                }

                left++
                right--
            }
        }
    }

    return ans
}

由于本题查找最长回文子串,我们并不清楚回文边界,因此 无法使用对撞指针,只能用回文中心对称的特点,从中心向两侧扩展求解。

// Time: O(n²), Space: O(1)
func longestPalindrome(s string) string {
    start, maxLen := 0, 0

    // 从中心向两侧扩展,返回回文长度
    expand := func(l, r int) {
        for l >= 0 && r < len(s) && s[l] == s[r] {
            l--
            r++
        }
        // 此时 [l+1, r-1] 是回文
        if r-l-1 > maxLen {
            start = l + 1
            maxLen = r - l - 1
        }
    }

    for i := range s {
        // 无法预知以某个位置为中心的最长回文是奇数还是偶数长度,因此需要同时遍历两种情况,取最长的那个
        expand(i, i)   // 奇数长度回文(如 "aba")
        expand(i, i+1) // 偶数长度回文(如 "abba")
    }

    return s[start : start+maxLen]
}

题目解释:将数组视为一个数字,在所有排列中找到恰好比当前排列大的下一个排列;若已是最大排列,则回绕到最小排列。

  • nums = [1,2,3]:对应数字 123,所有排列按大小排序为 123 → 132 → 213 → …,下一个排列为 [1,3,2]
  • nums = [3,2,1]:对应数字 321,已是最大排列,回绕到最小排列 [1,2,3]

解题思路:

  1. 从右往左,找到第一个满足 nums[k] < nums[k+1] 的位置 k(即第一个"下降点")。若不存在,说明已是最大排列,直接翻转整个数组即可。(可以通过折线图可视化查找过程)
  2. 再从右往左,找到第一个满足 nums[l] > nums[k] 的位置 l
  3. 交换 nums[k]nums[l]
  4. 翻转 nums[k+1:],使其从降序变为升序,得到恰好大一点的排列。

nums = [1,2,7,4,3,1] 为例:

  • 找下降点:从右往左扫描,nums[1]=2 < nums[2]=7,故 k=1
  • 找交换点:从右往左找第一个大于 2 的数,nums[4]=3,故 l=4
  • 交换:nums = [1,3,7,4,2,1]
  • 翻转 nums[2:]nums = [1,3,1,2,4,7]
// Time: O(n), Space: O(1)
func nextPermutation(nums []int) {
    // 从右往左找到第一个下降点 i
    i := len(nums) - 2
    for i >= 0 && nums[i] >= nums[i+1] {
        i--
    }

    if i >= 0 {
        // 从右往左找到第一个大于 nums[i] 的数 j
        j := len(nums) - 1
        for nums[j] <= nums[i] {
            j--
        }
        // 交换 i 和 j
        nums[i], nums[j] = nums[j], nums[i]
    }

    // 反转 i 之后的部分
    left, right := i+1, len(nums)-1
    for left < right {
        nums[left], nums[right] = nums[right], nums[left]
        left++
        right--
    }
}
// Time: O(n+m), Space: O(1)
func compareVersion(version1, version2 string) int {
    n, m := len(version1), len(version2)
    i, j := 0, 0
    for i < n || j < m {
        x := 0
        for i < n && version1[i] != '.' {
            x = x*10 + int(version1[i]-'0')
            i++
        }
        i++ // 跳过点号

        y := 0
        for j < m && version2[j] != '.' {
            y = y*10 + int(version2[j]-'0')
            j++
        }
        j++ // 跳过点号

        if x > y {
            return 1
        }
        if x < y {
            return -1
        }
    }

    return 0
}

链表

基础必会

func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    // 暂存、反转、移动
    for head != nil {
        next := head.next
        head.Next = prev
        prev = head
        head = next
    }

    return prev
}

头插法,把 curr.Next 节点插入区间头部(即 prev 后)。

func reverseBetween(head *ListNode, left, right int) *ListNode {
    dummy := &ListNode{Next: head} // 可能从头结点开始反转

    prev := dummy
    for i := 1; i < left; i++ {
        prev = prev.Next
    }

    curr := prev.Next
    for ; left < right; left++ { // 这段代码画个步骤图就出来了
        next := curr.Next     // 暂存操作节点的移动路径
        curr.Next = next.Next // 摘下节点
        next.Next = prev.Next // 插到区间头部
        prev.Next = next      // 移动操作的节点
    }

    return dummy.Next
}

reverse_by_head_insert

本题使用快慢指针求解,但要注意链表奇偶长度的处理

func middleNode(head *ListNode) *ListNode {
    if head == nil {
        return nil
    }

    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow, fast = slow.Next, fast.Next.Next
    }

    return slow
}

快慢指针解法:快指针走两步,慢指针走一步,相遇则有环

func hasCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow, fast = slow.Next, fast.Next.Next
        if slow == fast {
            return true
        }
    }

    return false
}
func detectCycle(head *ListNode) *ListNode {
    seen := map[*ListNode]bool{}
    for head != nil {
        if seen[head] {
            return head
        }
        seen[head] = true
        head = head.Next
    }

    return nil
}

本题可以使用哈希表来简单求解,也可以用双指针把空间复杂度优化到 \(O(1)\)

func getIntersectionNode(headA, headB *ListNode) *ListNode {
    pA, pB := headA, headB
    for pA != pB {
        if pA == nil {
            pA = headB
        } else {
            pA = pA.Next
        }

        if pB == nil {
            pB = headA
        } else {
            pB = pB.Next
        }
    }

    return pA // 没有相交时,遍历结束的链表最终指向 nil
}
func mergeSortedTwoLists(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    current := dummy

    // 注意该遍历需同时操作3个链表
    for l1 != nil && l2 != nil { // 注意是 &&
        if l1.Val < l2.Val {     // 将较小的值挂载在新链表上
            current.Next = l1
            l1 = l1.Next         // 移动原链表
        } else {
            current.Next = l2
            l2 = l2.Next
        }
        current = current.Next   // 移动新链表
    }

    // 将链表剩余部分挂载,同时处理原链表为空
    if l1 != nil {
        current.Next = l1
    } else {
        current.Next = l2
    }

    // 注意返回的是 dummy.Next
    return dummy.Next
}

综合常考

分组+局部反转(k=3):

// 反转前
// dummy -> A -> B -> C -> D -> E
//   ↑      ↑         ↑    ↑
// prev    cur      tail  nextGroup

// 反转后(第一组)
// dummy -> C -> B -> A -> D -> E
//          ↑         ↑    ↑
//         tail      cur  nextGroup
//                   prev
func reverseKGroup(head *ListNode, k int) *ListNode {
    dummy := &ListNode{Next: head}
    prev := dummy

    for {
        tail := prev
        for range k {
            tail = tail.Next
            if tail == nil {
                return dummy.Next // 不足 k 个时结束反转
            }
        }

        nextGroup := tail.Next // 下一组起始位置

        // 头插法局部反转链表
        cur := prev.Next
        for range k - 1 {
            next := cur.Next
            cur.Next = next.Next
            next.Next = prev.Next
            prev.Next = next
        }

        // 区间反转完成后,准备下一组的反转,此时 curr 移动到了区间末尾
        cur.Next = nextGroup
        prev = cur
    }
}

本题有归并和最小堆两种解法,并且复杂度相同

归并解法:类似于锦标赛两两合并快速收敛

func mergeKListsByDivideAndConquer(lists []*utils.ListNode) *utils.ListNode {
    if len(lists) == 0 {
        return nil
    }

    return mergeRange(lists, 0, len(lists)-1)
}

func mergeRange(lists []*utils.ListNode, left, right int) *utils.ListNode {
    if left == right {
        return lists[left]
    }

    mid := left + (right-left)/2
    l1 := mergeRange(lists, left, mid)
    l2 := mergeRange(lists, mid+1, right)

    return mergeTwoLists(l1, l2)
}

func mergeTwoLists(l1, l2 *utils.ListNode) *utils.ListNode {
    dummy := &utils.ListNode{}
    cur := dummy

    for l1 != nil && l2 != nil {
        if l1.Val <= l2.Val {
            cur.Next = l1
            l1 = l1.Next
        } else {
            cur.Next = l2
            l2 = l2.Next
        }
        cur = cur.Next
    }

    if l1 != nil {
        cur.Next = l1
    } else {
        cur.Next = l2
    }

    return dummy.Next
}

最小堆:利用小顶堆排序

// 注意 Go 中需要实现 heap 接口
func mergeKListsByMinHeap(lists []*utils.ListNode) *utils.ListNode {
    h := &listNodeHeap{}
    heap.Init(h)

    for _, node := range lists {
        if node != nil {
            heap.Push(h, node)
        }
    }

    dummy := &utils.ListNode{}
    cur := dummy

    for h.Len() > 0 {
        node := heap.Pop(h).(*utils.ListNode)
        cur.Next = node
        cur = cur.Next

        if node.Next != nil {
            heap.Push(h, node.Next)
        }
    }

    return dummy.Next
}

本题与 #23 类似,采用归并排序,其中又用到了 #876 和 #21 来查找链表中点和进行链表排序

func sortList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }

    mid := split(head)
    left := sortList(head)
    right := sortList(mid)

    return mergeTwoLists(left, right)
}

func split(head *ListNode) *ListNode {
    prev, slow, fast := head, head, head
    for fast != nil && fast.Next != nil {
        prev, slow, fast = slow, slow.Next, fast.Next.Next
    }

    prev.Next = nil // 注意需要切断链表

    return slow
}

func mergeTwoLists(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    for l1 != nil && l2 != nil {
        if l1.Val < l2.Val {
            curr.Next = l1
            l1 = l1.Next
        } else {
            curr.Next = l2
            l2 = l2.Next
        }
        curr = curr.Next
    }

    if l1 != nil {
        curr.Next = l1
    }
    if l2 != nil {
        curr.Next = l2
    }

    return dummy.Next
}

利用快慢指针找中点并反转后半链表进行重排。

func reorderList(head *ListNode)  {
    mid := middleNode(head)
    reversedHead := reverseList(mid)
    p1, p2 := head, reversedHead
    for p2.Next != nil {                    // 注意结束条件
        p1Next, p2Next := p1.Next, p2.Next  // 保存指针避免断链
        p1.Next, p2.Next = p2, p1Next       // 交叉连接
        p1, p2 = p1Next, p2Next             // 移动节点
    }
}

func middleNode(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow, fast = slow.Next, fast.Next.Next
    }

    return slow
}

func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    for head != nil {
        next := head.Next
        head.Next = prev
        prev = head
        head = next
    }

    return prev
}

LRU 模型如下,哈希表中的Key和双向链表中的Key是完全一样的,这主要提供了淘汰链表数据时同步更新哈希表,避免哈希表的内存泄露。

  哈希表 (O(1) 查找)     双向链表 (O(1) 移动/删除)
┌─────────────────┐    ┌─────────────────────┐
│    cache map    │    │  head (dummy)       │
│                 │    │    ↕                │
│ key=A → *Node1 ←┼────┼→ [Node1: k=A, v=10] │
│                 │    │    ↕ (prev/next)    │
│ key=B → *Node2 ←┼────┼→ [Node2: k=B, v=20] │
│                 │    │    ↕                │
│ key=C → *Node3 ←┼────┼→ [Node3: k=C, v=30] │
│                 │    │    ↕                │
│                 │    │  tail (dummy)       │
└─────────────────┘    └─────────────────────┘
type Node struct { // 内部实现,不允许外部访问
    key, value int
    prev, next *Node
}

type LRUCache struct {
    capacity int
    cache    map[int]*Node // 哈希表:key -> 链表节点
    head     *Node         // 虚拟头节点(最近使用)
    tail     *Node         // 虚拟尾节点(最久未使用)
}

func Constructor(capacity int) LRUCache {
    lru := LRUCache{
        capacity: capacity,
        cache:    make(map[int]*Node),
        head:     &Node{},
        tail:     &Node{},
    }

    lru.head.next = lru.tail
    lru.tail.prev = lru.head

    return lru
}

func (lc *LRUCache) Get(key int) int {
    if node, exists := lc.cache[key]; exists {
        // 将节点移到头部(标记为最近使用)
        lc.moveToHead(node)
        return node.value
    }
    return -1
}

func (lc *LRUCache) Put(key, value int) {
    if node, exists := lc.cache[key]; exists {
        // 更新已存在的节点
        node.value = value
        lc.moveToHead(node)
    } else {
        // 创建新节点
        newNode := &Node{key: key, value: value}
        lc.cache[key] = newNode
        lc.addToHead(newNode)

        // 检查容量,必要时删除最久未使用的节点
        if len(lc.cache) > lc.capacity {
            removed := lc.removeTail()
            delete(lc.cache, removed.key)
        }
    }
}

// 辅助方法:将节点添加到头部
// 画一个三角插入图形理解分析:先把新节点与左右节点相连,再把右侧节点指向新节点,最后把head指向新节点。插入头部的节点操作要基于 head。
//               ┌──────┐          node.prev        ┌──────┐
//               │ head │◀╌╌╌╌╌╌╌╌╌╳╌╌╌╌╌╌╌╳╌╌╌╌╌╌╌╌│ node │
//               └─▲─┬──┘╌╌╌╌╌╌╌╌╌╌╳╌╌╌╌╌╌╌╳╌╌╌╌╌╌╌▶└──┬─▲─┘
//                 │ │             head.next           │ │
//                 │ │                                 │ │
// 1. newNode.prev │ │               3. head.next.prev │ │ 2. newNode.next
//                 │ │ 4. head.next                    │ │
//                 │ │                                 │ │
//                 │ ╰──────────▶ ┌─────────┐ ◀────────╯ │
//                 ╰───────────── │ newNode │ ───────────╯
//                                └─────────┘
func (lc *LRUCache) addToHead(node *Node) {
    node.prev = lc.head
    node.next = lc.head.next
    lc.head.next.prev = node
    lc.head.next = node
}

// 辅助方法:移除节点
func (lc *LRUCache) removeNode(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

// 辅助方法:将节点移到头部
func (lc *LRUCache) moveToHead(node *Node) {
    lc.removeNode(node)
    lc.addToHead(node)
}

// 辅助方法:移除尾部节点
func (lc *LRUCache) removeTail() *Node {
    node := lc.tail.prev
    lc.removeNode(node)
    return node
}

二叉树

遍历方式

func traversal(root *TreeNode) []int {
    if root == nil {
        return []int{}
    }

    // Preorder: 根 -> 左 -> 右
    // nums := []int{root.Val}
    // nums = append(nums, traversal(root.Left)...)
    // nums = append(nums, traversal(root.Right)...)

    // Inorder: 左 -> 根 -> 右
    nums := traversal(root.Left)
    nums = append(nums, root.Val)
    nums = append(nums, traversal(root.Right)...)

    // Postorder: 左 -> 右 -> 根
    // nums := traversal(root.Left)
    // nums = append(nums, traversal(root.Right)...)
    // nums = append(nums, root.Val)

    return nums
}

队列+双层循环(外循环控制深度,内循环控制宽度),以 [1, 2, 3, 4, 5, 6, 7] 为例,其层序遍历的执行过程如下:

层数 操作 queue level ans
0 初始 [1] [] []
1 出队 1 [] [1]
加子节点 [2,3]
层结束
2 出队 2 [3] [2] [[1]]
加子节点 [3,4,5]
出队 3 [4,5] [2,3]
加子节点 [4,5,6,7]
层结束
3 出队 4 [5,6,7] [4] [[1],[2,3]]
出队 5 [6,7] [4,5]
出队 6 [7] [4,5,6]
出队 7 [] [4,5,6,7]
层结束 [[1],[2,3],[4,5,6,7]]

可以看出,queue 中存放着每层的节点,通过遍历 queue 来使节点入队和出队,level 负责收集每层的节点,然后再交给 ans

func levelOrder(root *TreeNode) [][]int {
    ans := [][]int{}
    if root == nil {
        return ans
    }

    queue := []*TreeNode{root} // 初始化队列,放入根节点
    for len(queue) > 0 { // 遍历树的深度
        level := make([]int, 0, len(queue))
        for range len(queue) { // 遍历当前层的宽度
            // 从队列头部弹出节点
            node := queue[0]
            queue = queue[1:]

            // 收集当前层的值
            level = append(level, node.Val)

            // 将子节点加入队列,为下层遍历准备
            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        ans = append(ans, level)
    }

    return ans
}

与层序遍历很像,只不过是把queue换为stack。注意stack后进先出的特性需要先压右再压左

func preOrderTraversal(root *TreeNode) []int {
  ans := []int}{}
  if root == nil {
    return ans
  }

  stack := []*TreeNode{root}
  for len(stack) > 0 {
    node := stack[len(stack)-1]
    stack = stack[:len(stack)-1]

    ans = append(ans, node.Val)
    // 因为栈是先进后出,所以先压右节点
    if node.Right != nil {
      stack = append(stack, node.Right)
    }
    if node.Left != nil {
      stack = append(stack, node.Left)
    }
  }

  return ans
}

栈:一路向左,先处理完左子树再处理右子树

func inOrderTraversal(root *TreeNode) []int {
  ans := []int{}
  stack := []*TreeNode{}

  curr := root
  for curr != nil || len(stack) > 0 {
    // 把从根节点到叶节点的左节点都压入栈
    for curr != nil {
      stack = append(stack, curr)
      curr = curr.Left
    }

    // 弹出栈中的节点(即从下往上遍历树)
    curr = stack[len(stack)-1]
    stack = stack[:len(stack)-1]

    ans = append(ans, curr.Val)

    // 左子树处理完处理右子树
    curr = curr.Right
  }

  return ans
}

后序遍历的迭代实现由中序遍历演化而来,而区别在于,中序的左 → 根 → 右可以在每次弹出节点就直接访问,而后序的左 → 右 → 根则需要先访问右节点才能弹出根节点。

注意需要引入 prev 来记录前驱节点。

[1,2,3,4,5] 为例,其执行过程如下表:

1
2
3
4
5
6
7
       1
      / \
     2   3
    / \
   4   5
  / \ / \
nil nil nil

步骤 操作 curr stack
(底→顶)
prev nums
0 初始 1 [] nil []
1 向左压栈 1 2 [1]
2 向左压栈 2 4 [1,2]
3 向左压栈 4 nil [1,2,4]
4 看栈顶=4,右为空,弹出并访问 [1,2] 4 [4]
5 看栈顶=2,右=5 且 5≠prev(4),转去右子树 5
6 向左压栈 5 nil [1,2,5]
7 看栈顶=5,右为空,弹出并访问 [1,2] 5 [4,5]
8 看栈顶=2,右=5 且 5=prev,弹出并访问 [1] 2 [4,5,2]
9 看栈顶=1,右=3 且 3≠prev(2),转去右子树 3
10 向左压栈 3 nil [1,3]
11 看栈顶=3,右为空,弹出并访问 [1] 3 [4,5,2,3]
12 看栈顶=1,右=3 且 3=prev,弹出并访问 [] 1 [4,5,2,3,1]
13 结束(curr=nil 且 stack 空)

代码实现如下:

func postOrderTraversal(root *TreeNode) []int {
  ans := []int{}
  stack := []*TreeNode{}
  var prev *TreeNode

  curr := root
  for curr != nil || len(stack) > 0 {
    for curr != nil {
      stack = append(stack, curr)
      curr = curr.Left
    }

    // 查看栈顶不弹出
    curr = stack[len(stack)-1]

    if curr.Right == nil || curr.Right == prev {
      stack = stack[:len(stack)-1]
      ans = append(ans, curr.Val)
      prev = curr
      curr = nil // 重置,避免重复访问左子树
    } else {
      curr = curr.Right
    }
  }

  return ans
}

后序遍历题目

当节点需要依赖左右子树的信息时,使用后序遍历

与递归翻转二叉树类似,只是这个需要后序遍历计算最大深度。

func maxDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }

    left := maxDepth(root.Left)
    right := maxDepth(root.Right)

    return max(left, right) + 1
}

平衡树就是左右子树高度差不超过 1,所以要基于后序遍历的树深度来求解

func abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

func isBalanced(root *TreeNode) bool {
    return dfs(root) != -1
}

// 返回树的高度,如果不平衡则返回 -1
// 后序遍历:先递归左右子树,返回时处理当前节点
func dfs(root *TreeNode) int {
    if root == nil {
        return 0
    }

    // 先检查左、右子树
    left := dfs(root.Left)
    right := dfs(root.Right)
    if left == -1 || right == -1 {
        return -1 // 左子树或右子树不平衡,提前终止
    }

    // 检查当前节点是否平衡
    if abs(left-right) > 1 {
        return -1 // 当前节点不平衡
    }

    // 返回当前节点的高度(自底向上汇总信息)
    return max(left, right) + 1
}
func diameterOfBinaryTree(root *TreeNode) int {
    maxDiameter := 0

    var depth func(*TreeNode) int
    depth = func(node *TreeNode) int {
        if node == nil {
            return 0
        }

        // 后序遍历:先递归计算左右子树的深度
        leftDepth := depth(node.Left)
        rightDepth := depth(node.Right)

        // 当前节点的直径 = 左子树深度 + 右子树深度
        maxDiameter = max(maxDiameter, leftDepth+rightDepth)

        // 计算当前节点的深度
        return max(leftDepth, rightDepth) + 1
    }

    depth(root)

    return maxDiameter
}

与树的直径类似,对于每个节点,路径和 = 左子树贡献 + 右子树贡献 + 当前节点值

func maxPathSum(root *TreeNode) int {
    maxSum := math.MinInt32 // 初始化为最小值,因为节点值可能为负

    var maxGain func(*TreeNode) int
    maxGain = func(node *TreeNode) int {
        if node == nil {
            return 0
        }

        // 后序遍历:先递归计算左右子树的最大贡献
        // 如果子树贡献为负,则不选择该子树(取 0)
        leftGain := max(maxGain(node.Left), 0)
        rightGain := max(maxGain(node.Right), 0)

        // 以当前节点为"拐点"的路径和
        // 路径和 = 左子树贡献 + 右子树贡献 + 当前节点值
        currentPathSum := leftGain + rightGain + node.Val
        maxSum = max(maxSum, currentPathSum)

        // 返回给父节点的最大贡献:只能选择左或右其中一条路径
        // 贡献 = 当前节点值 + max(左子树贡献, 右子树贡献)
        return node.Val + max(leftGain, rightGain)
    }

    maxGain(root)
    return maxSum
}
  • LCA 示例
    最近公共祖先示例
  • LCA 逻辑
    LCA 判断逻辑
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
    // 递归终止条件:
    // 1. 搜到底了(nil)
    // 2. 找到目标节点(p 或 q)
    if root == nil || root == p || root == q {
        return root
    }

    // 后序遍历:先递归左右子树
    left := lowestCommonAncestor(root.Left, p, q)
    right := lowestCommonAncestor(root.Right, p, q)

    // 根据左右子树的返回值判断 LCA 位置
    // 情况1:p 和 q 分散在左右两侧 → 当前节点就是 LCA
    if left != nil && right != nil {
        return root
    }

    // 情况2:p 和 q 都在左子树 → 返回左子树的结果
    if left != nil {
        return left
    }

    // 情况3:p 和 q 都在右子树(或右子树找到一个)→ 返回右子树的结果
    return right
}

由于 BST 的有序性,其解法与普通的 LCA 相似:如果 \(p\)\(q\) 都小于当前节点,则 LCA 在左子树;如果 \(p\)\(q\) 都大于当前节点,则 LCA 在右子树;否则当前节点就是 LCA。

// 迭代解法(推荐)
// Time: O(h), Space: O(1)
func lowestCommonAncestorIterative(root, p, q *TreeNode) *TreeNode {
    for root != nil {
        if p.Val < root.Val && q.Val < root.Val {
            root = root.Left
        } else if p.Val > root.Val && q.Val > root.Val {
            root = root.Right
        } else {
            return root
        }
    }

    return nil
}
// 递归解法
// Time: O(h), Space: O(h)
func lowestCommonAncestorRecursive(root, p, q *TreeNode) *TreeNode {
    if p.Val < root.Val && q.Val < root.Val {
        return lowestCommonAncestorRecursive(root.Left, p, q)
    }
    if p.Val > root.Val && q.Val > root.Val {
        return lowestCommonAncestorRecursive(root.Right, p, q)
    }
    return root
}

中序遍历题目

由于需要捕获外部变量,因此可以使用闭包避免二级指针。本题使用递归的系统栈来代替手动维护的栈。

func isValidBST(root *TreeNode) bool {
    // prev 记录中序遍历中上一个访问的节点
    // 放在闭包外部,所有递归调用共享同一个变量
    var prev *TreeNode

    // 定义闭包函数,自动捕获外部的 prev 变量
    var inorder func(*TreeNode) bool
    inorder = func(node *TreeNode) bool {
        // 递归终止条件:空节点视为有效
        if node == nil {
            return true
        }

        // 中序遍历:左 -> 根 -> 右
        // 1. 先递归检查左子树
        if !inorder(node.Left) {
            return false // 左子树不合法,提前终止
        }

        // 2. 检查当前节点:BST 的中序遍历必须严格递增
        if prev != nil && node.Val <= prev.Val {
            return false // 不满足严格递增,不是 BST
        }
        prev = node // 更新 prev 为当前节点,供下一个节点比较

        // 3. 递归检查右子树
        return inorder(node.Right)
    }

    return inorder(root)
}

前序遍历题目

当节点需要依赖左右子树的信息时,使用前序遍历,这样不仅代码简单,而且高效

递归交换左右节点即可,本题前序和后序递归都可以。

func invertTree(root *TreeNode) *TreeNode {
  if root == nil {
      return nil
  }

  // 交换当前节点的左右子树
  root.Left, root.Right = root.Right, root.Left

  // 递归翻转子树
  invertTree(root.Left)
  invertTree(root.Right)

  return root
}

对称树的定义:

  • 左子树的左节点 == 右子树的右节点
  • 左子树的右节点 == 右子树的左节点
对称树示意图

镜像递归

func isSymmetricMirrorRecursive(root *TreeNode) bool {
    if root == nil {
        return true
    }
    return isMirror(root.Left, root.Right)
}

func isMirror(left, right *TreeNode) bool {
    // 递归终止条件
    // 检查节点存在的对称性
    if left == nil && right == nil {
        return true
    }
    if left == nil || right == nil {
        return false
    }

  // 递归处理逻辑
    // 检查节点值的对称性
    if left.Val != right.Val {
        return false
    }

    // 递归处理:交叉比较子树(镜像对称)
    return isMirror(left.Left, right.Right) &&
        isMirror(left.Right, right.Left)
}

判断给定的树中是否有和为 targetSum 的路径存在。

func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil {
        return false
    }

    // 到达叶子节点
    if root.Left == nil && root.Right == nil {
        return root.Val == targetSum
    }

    remainingSum := targetSum - root.Val
    // 只需要左、右子树其中一个满足条件即可
    // 短路求值提前终止
    return hasPathSum(root.Left, remainingSum) || hasPathSum(root.Right, remainingSum)
}

层序遍历题目

本题的解题关键在于水平方向开关leftToRight

// Time: O(n), Space: O(n)
func zigzagLevelOrder(root *utils.TreeNode) [][]int {
    ans := [][]int{}
    if root == nil {
        return ans
    }

    leftToRight := true
    queue := []*utils.TreeNode{root}
    for len(queue) > 0 {
        levelSize := len(queue)
        level := make([]int, levelSize)

        for i := range levelSize {
            node := queue[0]
            queue = queue[1:]

            // 根据方向决定插入位置
            if !leftToRight {
                i = levelSize - 1 - i
            }
            level[i] = node.Val

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        ans = append(ans, level)
        leftToRight = !leftToRight
    }

    return ans
}
func rightSideView(root *TreeNode) []int {
    ans := []int{}
    if root == nil {
        return ans
    }

    queue := []*TreeNode{root}
    for len(queue) > 0 {
        width := len(queue)
        for i := range width {
            node := queue[0]
            queue = queue[1:]

            // 只把每层的最后一个节点加入结果集
            if i == width-1 {
                ans = append(ans, node.Val)
            }

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }
    }

    return ans
}

找到第一个叶子节点(node.Left == nil && node.Right == nil)返回深度即可

func minDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }

    depth := 1 // 非空节点的最小深度为 1
    queue := []*TreeNode{root}

    for len(queue) > 0 {
        for range len(queue) {
            node := queue[0]
            queue = queue[1:]

            // 找到第一个叶子节点,立即返回
            if node.Left == nil && node.Right == nil {
                return depth
            }

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        depth++
    }

    return depth
}

DFS

可用DFS、BFS、并查集3种解法,但推荐DFS

// Time: O(m×n), Space: O(m×n) 递归栈
func numIslandsDFS(grid [][]byte) int {
    if len(grid) == 0 {
        return 0
    }

    count := 0
    for i := range grid {
        for j := range grid[0] {
            if grid[i][j] == '1' {
                count++
                dfs(grid, i, j)
            }
        }
    }

    return count
}

func dfs(grid [][]byte, i, j int) {
    // 边界检查
    if i < 0 || i >= len(grid) || j < 0 || j >= len(grid[0]) || grid[i][j] == '0' {
        return
    }

    // 标记为已访问
    grid[i][j] = '0'

    // 递归访问四个方向
    dfs(grid, i-1, j) // 上
    dfs(grid, i+1, j) // 下
    dfs(grid, i, j-1) // 左
    dfs(grid, i, j+1) // 右
}

堆/优先队列

使用小顶堆,维护K个最大元素

func findKthLargest(nums []int, k int) int {
    // 使用小顶堆(Go 的 heap 包)
    h := &IntHeap{}
    heap.Init(h)

    for _, num := range nums {
        heap.Push(h, num)
        if h.Len() > k {
            heap.Pop(h)  // 保持堆大小为 k
        }
    }

    return heap.Pop(h).(int)
}

// Time: O(n), Space: O(n)
func isValid(s string) bool {
    pairs := map[byte]byte{
        ')': '(',
        ']': '[',
        '}': '{',
    }

    stack := []byte{}
    for i := 0; i < len(s); i++ { // 注意 range 返回的是 rune,这个返回的是 byte
        if s[i] == '(' || s[i] == '[' || s[i] == '{' {
            stack = append(stack, s[i])
        } else {
            n := len(stack)
            if n == 0 || stack[n-1] != pairs[s[i]] {
                return false
            }
            stack = stack[:n-1]
        }
    }

    return len(stack) == 0
}

单调递减栈,当前温度高于栈顶时不断弹出并更新等待天数(出栈时更新结果)。建议手动模拟栈的变化过程来辅助理解。

温度折线图 单调递减栈

func dailyTemperatures(temperatures []int) []int {
    tLen := len(temperatures)
    ans := make([]int, tLen)

    stack := []int{} // 栈内存放 temperatures 的索引
    for i := range tLen {
        // 如果当前元素 > 栈顶元素,则不断弹出栈中的元素,直至当前元素 < 栈顶元素
        for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]] {
            top := stack[len(stack)-1]   // 取栈顶值,即 temperatures 数组中 i 之前的索引
            ans[top] = i - top           // 将差值更新到对应的位置
            stack = stack[:len(stack)-1] // pop
        }
        stack = append(stack, i) // push
    }

    return ans
}

队列

二分查找

func search(nums []int, target int) int {
    left, right := 0, len(nums)-1

    // 这里的二分查找的核心在于每次搜索都是以 mid 为单位跳跃
    for left <= right {
        mid := left + (right-left)/2
        if target < nums[mid] {
            right = mid - 1 // 左侧区间
        } else if target > nums[mid] {
            left = mid + 1 // 右侧区间
        } else {
            return mid // 找到
        }
    }

    return -1
}
func searchInsert(nums []int, target int) int {
    left, right := 0, len(nums)
    for left < right {
        mid := left + (right-left)/2
        if target > nums[mid] {
            left = mid + 1
        } else {
            right = mid
        }
    }

    return left // 返回第一个 >= target 的位置
}
func mySqrt(x int) int {
    l, r := 0, x
    ans := -1
    for l <= r {
        mid := l + (r-l)/2
        if mid*mid <= x { // 注意被截断的小数也符合要求
            ans = mid
            l = mid + 1
        } else {
            r = mid - 1
        }
    }

    return ans
}

Rotated Sorted Array 如图所示,这道题目的本质是在有序数组中查找最值问题,只不过通过旋转数组来增加难度。

本题中,最小值就是断点,也就是「右侧有序段」的起点。

func findMin(nums []int) int {
    left, right := 0, len(nums)-1
    for left < right {
        mid := left + (right-left)/2
        if nums[mid] < nums[right] {
            right = mid    // 最小值一定在 [left, mid]
        } else {
            left = mid + 1 // 最小值一定在 (mid, right]
        }
    }

    return nums[left]
}

如果要查找最大值,那么与最小值左侧相邻的即为最大值,只需要 (left-1 + len(nums)) % len(nums) 即可获取到最大值。

如果数组中包含重复元素,则

1
2
3
4
5
6
7
if nums[mid] > nums[right] {
    left = mid + 1
} else if nums[mid] < nums[right] {
    right = mid
} else {
    right-- // 无法判断,缩小范围
}

做本题前先做 #153

func search(nums []int, target int) int {
    left, right := 0, len(nums)-1

    for left <= right {
        mid := left + (right-left)/2

        if nums[mid] == target {
            return mid
        }

        // 左半部分有序
        if nums[left] <= nums[mid] {
            if nums[left] <= target && target < nums[mid] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        } else {
            // 右半部分有序
            if nums[mid] < target && target <= nums[right] {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
    }

    return -1
}

动态规划

基础必会

斐波那契数作为递归和动态规划的入门问题,必须掌握。 注意:斐波那契数有从 0 和从 1 开始两个版本,其判断条件也分别是 n>=2n>=3n 指索引),本题采用的是 Version 1。

Version 1: 0 1 1 2 3 5 8 13   n >= 2;  F(0)=0, F(1)=1
Version 2: 1 1 2 3 5 8 13 21  n >= 3;  F(1)=1, F(2)=1

最简单的递归解法:

1
2
3
4
5
6
7
func fib(n int) int {
    if n < 2 {
        return n
    }

    return fib(n-1) + fib(n-2)
}

记忆优化的递归解法:

func fibMemo(n int) int {
    memo := map[int]int{}

    var fib func(int) int
    fib = func(n int) int {
        if n < 2 {
            return n
        }

        if val, ok := memo[n]; ok {
            return val
        }

        memo[n] = fib(n-1) + fib(n-2)

        return memo[n]
    }

    return fib(n)
}

动态规划解法:

func fibDP(n int) int {
    if n < 2 {
        return n
    }

    dp := make([]int, n+1) // 注意 n 是从 0 开始,因此要 n+1
    dp[0], dp[1] = 0, 1
    for i := 2; i <= n; i++ {
        dp[i] = dp[i-1] + dp[i-2]
    }

    return dp[n]
}

自底向上的迭代解法:

func fibIterative(n int) int {
    if n < 2 {
        return n
    }

    x, y := 0, 1
    for i := 2; i <= n; i++ { // 注意此处的条件是 <=
        // x, y 分别代表 f(i-2) 和 f(i-1)
        // 计算 f(i) 并更新 x 和 y
        x, y = y, x+y
    }

    return y
}

爬楼梯与斐波那契数类似,这里展示最推荐的写法

// Time: O(n), Space: O(1)
func climbStairs(n int) int {
    if n < 3 {
        return n
    }

    x, y := 1, 2
    for i := 3; i <= n; i++ {
        x, y = y, x+y
    }

    return y
}

综合常考

状态转移方程:

1
2
3
4
dp[i]=min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
              \______\__________/_________/
                      v            v
             可以爬1个或2个台阶 对应台阶所需花费

func minCostClimbingStairs(cost []int) int {
    n := len(cost)
    dp := make([]int, n+1) // 从 0 或 1 开始时,cost 为 0
    for i := 2; i <= n; i++ { // 要爬到楼顶,因此要包含 n
        // dp[i-1]代表累计花费,cost[i-1]代表当前门票
        dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
    }

    return dp[n]
}

上述代码主要依赖最近两个值,因此可以用滚动数组优化空间复杂度到 \(O(1)\)

1
2
3
4
5
6
7
8
func minCostClimbingStairsIterative(cost []int) int {
    x, y := 0, 0
    for i := 2; i <= len(cost); i++ {
        x, y = y, min(y+cost[i-1], x+cost[i-2])
    }

    return y
}

状态转义方程为dp[i] = max(nums[i], dp[i-1] + nums[i]),其中dp[i] 表示以 i 结尾的最大子数组和。

以数组 [-2, 3, -1, 1, -3] 为例,其执行过程如下:

num -2 3 -1 1 -3
sum 0 -2 3 2 3 0
ans -2 -2 3 3 3 3

以下为 Kadane 算法,用 sum 代替 dp[i],上一轮的 sum 代替 dp[i-1]

func maxSubArray(nums []int) int {
    sum, ans := 0, nums[0]
    for _, num := range nums {
        if sum > 0 { // sum 对结果有增益效果
            sum += num
        } else { // sum 对结果是减损效果,需要舍弃并更新为当前遍历数字
            sum = num
        }

        // 以上代码也可以进一步压缩为以下代码,但更难理解思路
        // sum = max(num, sum+num)

        ans = max(ans, sum) // 取最大值
    }

    return ans
}

// Time: O(amount × len(coins)), Space: O(amount)
func coinChange(coins []int, amount int) int {
    // dp[i] 表示凑成金额 i 所需的最少硬币数
    dp := make([]int, amount+1)

    // 初始化:dp[0]=0(凑成0元需要0枚硬币)
    // 其他位置初始化为 amount+1(表示不可能,因为最多需要 amount 枚面值为1的硬币)
    for i := 1; i <= amount; i++ {
        dp[i] = amount + 1

        // 尝试使用每一种硬币
        for _, coin := range coins {
            if i >= coin {
                // 状态转移:dp[i] = min(dp[i], dp[i-coin]+1)
                // 如果使用面值为 coin 的硬币,需要 dp[i-coin]+1 枚
                dp[i] = min(dp[i], dp[i-coin]+1)
            }
        }
    }

    // 如果 dp[amount] 仍然大于 amount,说明无法凑成
    if dp[amount] > amount {
        return -1
    }

    return dp[amount]
}

本题有3种解法:动态规划、单调栈、双指针。

由于要有坑洼才能接雨水,因此需要找到下一个更高的柱子,即使用递减栈。

回溯

基础必会 综合常考

贪心

基础必会

1
2
3
4
5
6
7
8
9
func maxProfit(prices []int) int {
    minPrice, maxProfit := math.MaxInt64, 0
    for _, price := range prices {
        minPrice = min(minPrice, price)
        maxProfit = max(maxProfit, price-minPrice)
    }

    return maxProfit
}

解题思路:可跳跃范围覆盖最后一个元素的索引即可

func canJump(nums []int) bool {
    cover := 0
    for i, jump := range nums {
        // 不可达位置
        if i > cover {
            return false
        }

        // 更新最大覆盖范围
        cover = max(cover, i+jump) // 从 i 跳 jump 步

        // 是否可以覆盖最后一个元素
        if cover >= len(nums)-1 {
            break
        }
    }

    return true
}

综合常考

本题只要以 ans 最后一个区间为基准,并且比较 end 值即可,但注意在比较前需要对输入的二维数组排序

// Time: O(nlogn), Space: O(nlogn)
func merge(intervals [][]int) [][]int {
    ans := [][]int{}
    if len(intervals) < 2 {
        return intervals
    }

    // 先对输入的二维数组排序
    for range intervals {
        slices.SortFunc(intervals, func(a, b []int) int {
            return cmp.Compare(a[0], b[0])
        })
    }

    // 以 ans 为基准比较
    ans = append(ans, intervals[0])
    for i := 1; i < len(intervals); i++ {
        last := ans[len(ans)-1]
        if last[1] < intervals[i][0] { // 不需要合并
            ans = append(ans, intervals[i])
        } else { // 只需考虑合并区间的 end 值即可
            last[1] = max(last[1], intervals[i][1])
        }
    }

    return ans
}

递归

编写递归步骤:

  1. 明确输入输出
  2. 明确递归终止条件(什么时候触底反弹?)
  3. 明确递归处理逻辑(每层要做什么事?)
  4. 明确递归过程(下楼做还是返回做?)

图论

评论