跳转至

Go 刷题清单

哈希表

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

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

    return []int{}
}

排序解法
排序解法
计数解法
计数解法

// 排序解法
// 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)
    }

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

    return result
}
// 计数解法
// 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)
    }

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

    return result
}

双指针

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(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--
    }
}
// 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
}

头插法,将每个要反转的节点连接到 prev

func reverseBetween(head *ListNode, left, right int) *ListNode {
    dummy := &ListNode{Next: head}

    prev := dummy
    for range left - 1 {
        prev = prev.Next
    }

    cur := prev.Next
    for range right - left {
        next := cur.Next
        cur.Next = next.Next
        next.Next = prev.Next
        prev.Next = next
    }

    return dummy.Next
}

reverse_by_head_insert

分组+局部反转

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

        nextGroup := tail.Next

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

        cur.Next = nextGroup
        prev = cur
    }
}

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

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]struct{}{}
    for head != nil {
        if _, ok := seen[head]; ok {
            return head
        }
        seen[head] = struct{}{}
        head = head.Next
    }

    return 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
}

归并排序

双向链表 + 哈希表

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)
        }
    }
}

// 辅助方法:将节点添加到头部
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 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
}

栈:先压右再压左

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 指针用于记录历史状态,防止右子树被重复访问。这一机制类似于回溯算法中利用 used[] 数组来标记路径是否已被使用。

Note

[1,2,3,4,5] 为例,

1
2
3
4
5
    1
   / \
  2   3
 / \
4   5

第 1 次遇到节点 2(检查阶段):

// 一路向左:1 → 2 → 4
stack = [1, 2, 4]

// 访问完4后,回到节点2
curr = stack[1] = 2  // ← 第1次遇到节点2

// 判断
if curr.Right == nil || curr.Right == prev {
    // 2.Right = 5, prev = 4
    // 5 != 4 不满足条件
} else {
    curr = curr.Right  // ← 转向右子树5,暂时不访问2
}

第 2 次遇到节点 2(访问阶段):

// 访问完5后,再次回到节点2
stack = [1, 2]
curr = stack[1] = 2  // ← 第2次遇到节点2

// 判断
if curr.Right == nil || curr.Right == prev {
    // 2.Right = 5, prev = 5
    // 5 == 5 ✅ 满足条件(右子树已访问)

    stack = [1]
    nums = append(nums, 2)  // ← 现在才真正访问节点2
    prev = 2
    curr = nil
}

每个节点可能被访问两次:

  • 检查右子树

  • 右子树访问完后,才真正访问当前节点

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

后序遍历题目

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

后序遍历的递归解法:先钻到最底下,在返回时再做处理

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 checkHeight(root) != -1
}

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

    // 后序遍历:先检查左子树
    leftHeight := checkHeight(root.Left)
    if leftHeight == -1 {
        return -1 // 左子树不平衡,提前终止
    }

    // 后序遍历:再检查右子树
    rightHeight := checkHeight(root.Right)
    if rightHeight == -1 {
        return -1 // 右子树不平衡,提前终止
    }

    // 返回时处理:检查当前节点是否平衡
    if abs(leftHeight-rightHeight) > 1 {
        return -1 // 当前节点不平衡
    }

    // 返回当前节点的高度(自底向上汇总信息)
    return max(leftHeight, rightHeight) + 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)
}

层序遍历题目

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

    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:]

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

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

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

    return result
}
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
}
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
}
// Time: O(n), Space: O(n)
func findBottomLeftValue(root *TreeNode) int {
    firstNode := 0
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        for i := range len(queue) {
            node := queue[0]
            queue = queue[1:]

            // 只记录每层的第一个节点
            if i == 0 {
                firstNode = node.Val
            }

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

    return firstNode
}

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 searchIterative(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 searchInsertLowerBound(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 的位置
}

动态规划

// Time: O(n), Space: O(1)
func climbStairsIterative(n int) int {
    if n <= 2 {
        return n
    }

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

    return y
}

回溯

贪心

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
}

递归

编写递归步骤:

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

图论

评论