leetcode hot100:用Go刷完hot100

130 阅读32分钟

以下解法基本均是官方解法,且均用Go语言。hot100链接:leetcode.cn/studyplan/t… 持续更新...

ps:用Go去刷算法,感觉比用Java写简洁方便了,即使Go学习没多久,也可以很容易把Java的题解改成Go的代码。

Go语法相关

index, value := range nums // nums := []int
value, ok := hashTable[x] // hashTable := map[int]int{},value为哈希值,ok为hash表是否有该key
index,value := range str // str := "12345" index是字符下标,value为每个字符。
key, value := range mp // mp := map[[26]int][]string{}
num := range numSet // numSet := map[int]bool{}, num是key,只接收key
make([][]string, 0, len(mp)) // 创建初始值为0的数组,数组值为string数组,数组长度为len(mp)
  • go数组直接 == 比较,比较的是两个数组值是否相等。

思考

  • 做题一开始要想到暴力解,然后想一下暴力解中,有哪些步骤比较复杂,想到暴力解的冗余,可以用什么数据结构方法去进行优化。因此,自己的脑子里必须要有各种数据结构使用场景的方法论,这样才能活学活用。然后写出低复杂度的代码。解题的思路就是如此。

  • 因此,首先要有秒想到暴力解的能力。其次,要有发现暴力解冗余的能力。最后,要有优化暴力解的能力

  • 培养看到一段代码,能够快速看懂它的逻辑的能力。

  • 一定要知道思路后,能快速代码实现的能力。

  • 快速把题目归类的能力(用什么方法去做)。

  • 实际开发中用复杂的、可读性差的代码去换取空间,长期来看是不利的行为。

哈希表

  • 查找:把所有要查找的值作为hash的key,可以在O(1)时间里找到有没有该值。
  • 分组:以某种规则进行分组,分组就是分到hash的某个key下。注意构造正确的map。

两数之和

  • 本题很容易想到使用两层暴力循环,但这样复杂度太高。为了降低寻找每一个值x,数组中是否有对应的target - x,我们可以使用哈希表。循环查找target - x,需要O(n),而哈希表只需要O(1),因而降低了复杂度。
  • 但要注意哈希表存储什么,因为是寻找有没有target - x,所以key必须是数组中的每个值,因为返回的是下标,所以value是index。
  • 总结:当需要遍历寻找数组中是否存在某一值的时候,我们可以先把数组中的数放到一个哈希表中,从而可以在O(1)时间内完成查找。(一般常用于需要配对的场景)
// 方法一:暴力循环 O(n^2) O(1)
func twoSum(nums []int, target int) []int {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i] + nums[j] == target {
                return []int{i, j}
            }
        }
    }
    return nil
}

// 方法二:哈希表 O(n) O(n)
func twoSum(nums []int, target int) []int {
    hashTable := map[int]int{}
    for i, x := range nums {
        if p, ok := hashTable[target - x]; ok {
            return []int{p, i}
        }
        hashTable[x] = i
    }
    return nil
}

字母异位词分组

  • 如何想到基于计数分组的:因为异位词肯定是每个字母出现的次数相同,于是想到字母计数。然后每个异位词进行分组的话,就可以用哈希表进行分组。
  • 由于已经确定了所有的词都是由小写英文字母组成,因此哈希表的key很容易确定为26个字母的计数的数组。
// 时间:O(n(k+∣Σ∣)) n是strs长度,k为最长字符串长度,∣Σ∣是26
// 空间:O(n(k+∣Σ∣))
func groupAnagrams(strs []string) [][]string {
    mp := map[[26]int][]string{}
    for _, str := range strs {
        cnt := [26]int{}
        for _, b := range str {
            cnt[b - 'a']++
        }
        mp[cnt] = append(mp[cnt], str)
    }
    ans := make([][]string, 0, len(mp))
    for _, v := range mp {
        ans = append(ans, v)
    }
    return ans
}

最长连续序列

  • 对于数组中的每个数x,我们都去寻找有无x + 1,x + 2...
  • 冗余:每次找x + 1都要遍历整个数组;对x找完了连续序列,还要对x + 1, x+ 2等去找连续序列。
  • 优化:把数组所有数提前放到哈希表,从哈希表寻找x + 1;寻找之前,从哈希表判断是否有x - 1。
  • 总结:哈希表可以极大降低查找的时间复杂度。哈希优化查找!
// 时间:O(n),数组每个值智慧遍历一次。
// 空间:O(n),哈希表存储数组的值。
func longestConsecutive(nums []int) int {
    numSet := map[int]bool{}
    for _, num := range nums{
        numSet[num] = true
    }
    longestStread := 0
    for num := range numSet {
        if !numSet[num - 1] {
            currentStreak := 1
            for currentNum := num; numSet[currentNum + 1]; currentNum++{
                currentStreak++
            }
            if longestStread < currentStreak {
                longestStread = currentStreak
            }
        }
    }
    return longestStread
}

双指针

  • 不同类型:都从左往右;都从右往左;一左一右往中间遍历;
  • 移动规则:注意移动规则。
  • 原地移动数组元素:按照某种规则把数组中的元素移动,把数组分为两部分。
  • 遍历 数组

移动零

  • 通过left指针记录前面非0的数的index,right是遍历指针。
  • 这种题要注意边界情况!!
func moveZeroes(nums []int)  {
    for left, right := 0, 0; right < len(nums); {
        if nums[right] != 0 {
            nums[left], nums[right] = nums[right], nums[left] // 优雅的写法
            left++
        }
        right++
    }
}

盛水最多的容器

  • 注意什么时候移动左指针,什么时候移动右指针。为了找最高,所以每次移动低的那个指针。
  • 为什么要这么遍历:只有一左一右才能找到最大的区间。
// 时间:O(n)
// 空间:O(1)
func maxArea(height []int) int {
    l, r, ans := 0, len(height) - 1, 0
    for l < r {
        area := min(height[l], height[r]) * (r - l)
        ans = max(ans, area)
        if height[l] <= height[r] {
            l++
        } else {
            r--
        }
    }
    return ans
}

三数之和

  • 注意各种边界情况和去重。
// 时间:O(n^2),两重循环。
// 空间:O(logn)
func threeSum(nums []int) [][]int {
    n := len(nums)
    sort.Ints(nums)
    ans := make([][]int, 0)

    for first := 0; first < n; first++ {
        // 第一个去重
        if first > 0 && nums[first] == nums[first - 1] {
            continue
        }
        third := n - 1
        target := -1 * nums[first]
        
        for second := first + 1; second < n; second++ {
            // 第二个去重
            if second > first + 1 && nums[second] == nums[second - 1] {
                continue
            }
            for second < third && nums[second] + nums[third] > target {
                third--
            }
            if second == third {
                break
            }
            if nums[second] + nums[third] == target {
                ans = append(ans, []int{nums[first], nums[second], nums[third]})
            }
        }
    }
    return ans
}

接雨水

  • 本题有三种解法:动态规划、单调栈、双指针。其中双指针为最优解。(正常人很难想出来)
  • 双指针思路:两个指针left、right分别往中间遍历。并且记录左右最高的柱子。每次ans加的部分,比如在左边的,当前是left,如果right比left高,则left比leftMax矮的这一部分一定会被接住。所以计算的时候,并不是每次都去加每个连通的区域。而是每个柱子上方可以被接到的区域。
func trap(height []int) int {
    var ans int
    left, right := 0, len(height) - 1
    leftMax, rightMax := 0, 0
    for left < right {
        leftMax = max(leftMax, height[left])
        rightMax = max(rightMax, height[right])
        if height[left] < height[right] {
            ans += leftMax - height[left]
            left++
        } else {
            ans += rightMax - height[right]
            right--
        }
    }
    return ans
}

滑动窗口

  • 使用关键词:连续、子;或者是寻找某一窗口中的某个数

无重复字符的最长子串

  • 暴力解:找出所有的子串,并且判断这些子串是否无重复字符。
  • 冗余:某一趟如果确定当前有重复字符,就无需往下遍历了,就可以开始下一趟了;开始下一趟也不用从头遍历,因为那样还可能会有冗余。基于这两种情况,分别使用左右指针进行优化。
// 时间:O(n)
// 空间:O(|m|), m为字符最多的个数。
func lengthOfLongestSubstring(s string) int {
    // 标记每个字符是否出现
    m := map[byte]bool{}
    n := len(s)
    // rk为右指针,初始化为-1
    rk, ans := -1, 0
    //i为左指针
    for i := 0; i < n; i++ {
        if i != 0 {
            // 左指针移动了就删除
            m[s[i - 1]] = false
        }
        for rk + 1 < n && m[s[rk + 1]] == false {
            //不断移动右指针
            m[s[rk + 1]] = true
            rk++
        }
        ans = max(ans, rk - i + 1)
    }
    return ans
}

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

  • 判断是否是异位词,使用哈希的方法,因为确定了是26个字母,所以使用[26]int
  • 字母异位词,所以是固定的窗口长度,然后一直往下遍历即可。
func findAnagrams(s string, p string) []int {
    var ans []int
    sLen, pLen := len(s), len(p)
    if sLen < pLen{
        return nil
    }

    var sCount, pCount [26]int
    for i, ch := range p {
        sCount[s[i] - 'a']++
        pCount[ch - 'a']++
    }
    if sCount == pCount {
        ans = append(ans, 0)
    }
    
    for i, ch := range s[:sLen - pLen] {
        sCount[ch - 'a']--
        sCount[s[i+pLen] - 'a']++
        if sCount == pCount {
            ans = append(ans, i + 1)
        }
    }
    return ans
}

子串

  • 前缀和:子串、子路径。我们可以转化为有多少个相应的前缀和来计算。这是因为子串和子路径不好求。

和为k的子数组(前缀和)

  • 暴力解就是双层循环
  • 前缀和+哈希表
  • 最主要的思想:我们考虑以 i 结尾的和为 k 的连续子数组个数时只要统计有多少个前缀和为 pre[i]−kpre[j] 即可。
  • 为了计算有多少和为k的子数组,那么只需要计算有多少个等于pre-k的前缀和即可。pre是当前遍历位置的前缀和。遍历顺序保证了,所有统计的前缀和,其下标一定比当前下标小。pre的计算只和前一个值有关,所以只需要一个变量来累加就可以。
  • 如果该题限制k>0,数组元素大于0,就可用滑动窗口来解。
func subarraySum(nums []int, k int) int {
    count, pre := 0, 0
    m := map[int]int{}
    m[0] = 1
    for i := 0; i < len(nums); i++ {
       pre += nums[i] 
       if _, ok := m[pre - k]; ok {
            count += m[pre - k]
       }
       m[pre] += 1
    }
    return count
}

滑动窗口最大值(单调队列)

  • 目标在于不要每次移动窗口之后都重新遍历窗口内数据去找最大值。但难点在于,我们无法用一个变量维护窗口内的最大值,当最大值排除之后,为了找新的最大值,还是要重新遍历去寻找。因为窗口内的数据没什么规律。
  • 这时用单调队列可以解决上述问题。队首始终是当前最大值,保证队列中下标对应的元素是单调递减的,才能保证队首是最大值的性质。
func maxSlidingWindow(nums []int, k int) []int {
    // 存放下标的队列(数组形式实现),且下标对应的数,单调递减。
    q := []int{}
    push := func(i int) {
        for len(q) > 0 && nums[i] >= nums[q[len(q)-1]] {
            q = q[:len(q) - 1]  // 新的数如果大于队尾元素,队尾一直出队。
        }
        q = append(q, i)
    }
    
    for i := 0; i < k; i++ {
        push(i)
    }

    n := len(nums)
    ans := make([]int, 1, n - k + 1)
    ans[0] = nums[q[0]]
    for i := k; i < n; i++ {
        push(i)
        for q[0] <= i - k {
            q = q[1:]
        }
        ans = append(ans, nums[q[0]])
    }
    return ans
}

最小覆盖子串

  • 考验自己根据思路能写出代码的能力。
func minWindow(s string, t string) string {
    // ori是t的字符映射,cnt是s子串的映射
    ori, cnt := map[byte]int{}, map[byte]int{}
    for i := 0; i < len(t); i++ {
        ori[t[i]]++
    }

    sLen := len(s)
    len := math.MaxInt32
    ansL, ansR := -1, -1

    checko := func() bool {
        for k, v := range ori {
            if cnt[k] < v {
                return false
            }
        }
        return true
    }

    for l, r := 0, 0; r < sLen; r++ {
        if r < sLen && ori[s[r]] > 0 {
            cnt[s[r]]++
        }
        for check() && l <= r {
            if r - l + 1 < len {
                len = r - l + 1
                ansL, ansR = l, l + len
            }
            if _, ok := ori[s[l]]; ok {
                cnt[s[l]] -= 1
            }
            l++
        }
    }
    if ansL == -1 {
        return ""
    }
    return s[ansL:ansR]
}

普通数组

技巧性比较强

最大子数组和

  • 动态规划
// 时间:O(n)
// 空间:O(1)
func maxSubArray(nums []int) int {
    max := nums[0]
    for i := 1; i < len(nums); i++ {
        // 状态转移
        if nums[i] + nums[i -1] > nums[i] {
            nums[i] += nums[i - 1]
        }
        if nums[i] > max {
            max = nums[i]
        }
    }
    return max
}

合并区间

  • 基于排序的思想。
// 时间:O(nlogn)
// 空间:O(logn)
func merge(intervals [][]int) [][]int {
    if len(intervals) == 0 {
        return [][]int{}
    }
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][0] < intervals[j][0]
    })
    merged := [][]int{}
    for i := 0; i < len(intervals); i++ {
        L, R := intervals[i][0], intervals[i][1]
        if len(merged) == 0 || merged[len(merged) - 1][1] < L {
            merged = append(merged, []int{L, R})
        } else {
            merged[len(merged) - 1][1] = max(merged[len(merged) - 1][1], R)
        }
    }
    return merged
}

轮转数组

  • 三次旋转法
// 时间:O(n)
// 空间:O(1)
func rotate(nums []int, k int)  {
    reverse := func(a []int) {
        for i, n := 0, len(a); i < n / 2; i++ {
            a[i], a[n - i - 1] = a[n - 1 -i], a[i]
        }
    }
    k %= len(nums)
    reverse(nums)
    reverse(nums[:k])
    reverse(nums[k:])
}

除自身以外数组的乘积(以空间换时间)

  • 不能使用除法。如果对于每个数,都去遍历求乘积,复杂度为O(n^2)。由于每次遍历都是类似的,所以可以把遍历的结果都存储起来,所以就有了空间换时间的思想。分别保存index = i 的左边和右边所有元素的乘积。于是answer[i] = L[i] * R[i]
  • 空间换时间:找到重复的计算的地方,可以把这些结果存储。
// 时间:O(n)
// 空间:O(n)
func productExceptSelf(nums []int) []int {
    length := len(nums)
    L, R, answer := make([]int, length), make([]int, length), make([]int, length)

    L[0] = 1
    for i := 1; i <length; i++ {
        L[i] = L[i - 1] * nums[i - 1]
    }
    R[length - 1] = 1
    for i := length - 2; i >= 0; i-- {
        R[i] = R[i + 1] * nums[i + 1]
    }

    for i := 0; i < length; i++ {
        answer[i] = L[i] * R[i]
    }
    return answer
}

缺失的第一个正数

  • 交换位置,x去到x - 1的位置。(不太容易想到)
// 时间:O(n)
// 空间:O(1)
func firstMissingPositive(nums []int) int {
    n := len(nums)
    for i := 0; i < n; i++ {
        for nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i] {
            nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
        }
    }
    for i := 0; i < n; i++ {
        if nums[i] != i + 1 {
            return i + 1
        }
    }
    return n + 1
}

矩阵

矩阵置零

  • 方法一:使用两个分别长为m、n的数组,记录每一行和列是否有0出现。
  • 方法二:使用原矩阵的第一行第一列代替方法一中的两个数组,然后再用两个int变量记录第一行、列是否出现0。
// 时间:O(mn)
// 空间:O(m+n)
func setZeroes(matrix [][]int)  {
    row := make([]bool, len(matrix))
    col := make([]bool, len(matrix[0]))
    for i, r := range matrix {
        for j, v := range r {
            if v == 0 {
                row[i] = true
                col[j] = true
            }
        }
    }
    for i, r := range matrix {
        for j := range r {
            if row[i] || col[j] {
                r[j] = 0
            }
        }
    }
}

螺旋矩阵

  • 按层去遍历。第k层就是到最近边界距离为k的所有顶点。
func spiralOrder(matrix [][]int) []int {
    if len(matrix) == 0 || len(matrix[0]) == 0 {
        return []int{}
    }
    var (
        rows, columns = len(matrix), len(matrix[0])
        order = make([]int, rows * columns)
        index = 0
        left, right, top, bottom = 0, columns - 1, 0, rows - 1
    )

    for left <= right && top <= bottom {
        for column := left; column <= right; column++ {
            order[index] = matrix[top][column]
            index++
        }
        for row := top + 1; row <= bottom; row++ {
            order[index] = matrix[row][right]
            index++
        }
        if left < right && top < bottom {
            for column := right - 1; column > left; column-- {
                order[index] = matrix[bottom][column]
                index++
            }
            for row := bottom; row > top; row-- {
                order[index] = matrix[row][left]
                index++
            }
        }
        left++
        right--
        top++
        bottom--
    }
    return order
}

旋转图像

  • 水平翻转+对角线翻转
func rotate(matrix [][]int)  {
    n := len(matrix)
    // 水平翻转
    for i := 0; i < n / 2; i++ {
        matrix[i], matrix[n - 1 - i] = matrix[n - 1 - i], matrix[i]
    }
    // 主对角线翻转
    for i := 0; i < n; i++ {
        for j := 0; j < i; j++ {
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        }
    }
}

搜索二维矩阵2️⃣

  • Z字查找
func searchMatrix(matrix [][]int, target int) bool {
    m, n := len(matrix), len(matrix[0])
    x, y := 0, n - 1
    for x < m && y >= 0 {
        if matrix[x][y] == target {
            return true
        }
        if matrix[x][y] > target {
            y--
        } else {
            x++
        }
    }
    return false
}

链表

  1. 拷贝:
dummyHead := &ListNode{0, head}
temp := dummyHead
  1. dummyHead := &ListNode{0, head} 的作用

分解操作:

  • ListNode{0, head}:这是一个结构体字面量,表示创建一个新的 ListNode 实例:

    • Val 字段被初始化为 0
    • Next 字段被初始化为 head(即原链表的头节点)。
  • & 操作符:取结构体的内存地址,返回一个指向该结构体的指针(类型为 *ListNode)。

关键结果:

  • 创建了一个全新的节点对象,存储在内存中。
  • dummyHead 是指向这个新节点的指针,而非节点本身。
  1. temp := dummyHead 的作用

分解操作:

  • dummyHead 是一个指针(类型为 *ListNode)。
  • temp := dummyHeaddummyHead 的值(即内存地址)赋给 temp

关键结果:

  • 没有创建新的节点对象,只是复制了指针的值。
  • tempdummyHead 现在指向同一个节点(内存中的同一块地址)。

在 Go 语言中,指针本质上存储的就是一个内存地址值,这个地址指向了它所引用的变量(或结构体实例)在内存中的位置。

  1. 指针的解引用
  • 解引用基本语法

    • 在 Go 中,使用*操作符进行指针的解引用:获取指针指向的值:*ptr;修改指针指向的值:*ptr = newValue
func main() {// 创建一个整型变量
    num := 42// 创建一个指向num的指针
    ptr := &num // &操作符获取变量的地址// 解引用指针,获取指向的值
    fmt.Println("指针ptr存储的地址:", ptr)      // 输出内存地址,例如0xc00001a098
    fmt.Println("指针ptr指向的值:", *ptr)     // 输出42// 通过指针修改指向的值*ptr = 99
    fmt.Println("修改后num的值:", num)        // 输出99}
}
  • 结构体的解引用

在处理链表、树等数据结构时,结构体指针的解引用尤为常见。Go 语言提供了一种简化的语法,可以直接通过指针访问结构体的字段,而无需显式解引用。

最大的区别,访问指针指向的结构体的字段,是 node1.Val,而不是 (*node1).Val

type ListNode struct {
    Val  int
    Next *ListNode
}
func main() {// 创建链表节点
    node1 := &ListNode{Val: 1}
    node2 := &ListNode{Val: 2}
    node1.Next = node2 // 连接节点// 通过指针访问结构体字段(无需显式解引用)
    fmt.Println("node1的值:", node1.Val)      // 直接访问Val字段,无需写成(*node1).Val
    fmt.Println("node1的下一个节点的值:", node1.Next.Val)// 修改字段值
    node1.Next.Val = 200
    fmt.Println("修改后node2的值:", node2.Val) // 输出200}
}

相交链表

  • 方法一:先计算长度的diff,移动长的链表
  • 方法二:双指针
// 方法一:获取长度并计算长度diff
func getIntersectionNode(headA, headB *ListNode) *ListNode {
    // 1. 正确计算两个链表的长度
    lengthA, lengthB := 0, 0
    for p := headA; p != nil; p = p.Next {
        lengthA++
    }
    for p := headB; p != nil; p = p.Next {
        lengthB++
    }
   
    // 2. 让较长的链表指针先走差值步数
    diff := lengthA - lengthB
    if diff > 0 {
        for diff > 0 {
            headA = headA.Next
            diff--
        }
    } else {
        for diff < 0 {
            headB = headB.Next
            diff++
        }
    }
    
    // 3. 同步遍历找交点
    for headA != nil && headB != nil {
        if headA == headB {
            return headA
        }
        headA = headA.Next  // 原代码缺少这两行
        headB = headB.Next
    }
    
    return nil
}

// 方法二:双指针
func getIntersectionNode(headA, headB *ListNode) *ListNode {
    if headA == nil || headB == nil {
        return nil
    }
    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。var prev *ListNode
  • 原地反转!
// 反转链表的函数,牢记
func reverseList(head *ListNode) *ListNode {
    var prev, cur *ListNode = nil, head
    for cur != nil {
        nextTmp := cur.Next
        cur.Next = prev
        prev = cur
        cur = nextTmp
    }
    return prev
}
// 方法一:头插法
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode  // 前一个节点初始为nil
    curr := head        // 当前节点从head开始
    
    for curr != nil {
        next := curr.Next  // 保存下一个节点
        curr.Next = prev   // 当前节点指向前一个节点
        prev = curr        // 前一个节点后移
        curr = next        // 当前节点后移
    }
    
    return prev  // 最后prev指向新的头节点

}
// 方法二:原地反转
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode  // 前一个节点初始为nil
    curr := head        // 当前节点从head开始
    
    for curr != nil {
        next := curr.Next  // 保存下一个节点
        curr.Next = prev   // 当前节点指向前一个节点
        prev = curr        // 前一个节点后移
        curr = next        // 当前节点后移
    }
    
    return prev  // 最后prev指向新的头节点
}

回文链表

  • 方法一:也就是反转链表后,和原链表是同一个链表。空间复杂度O(n)
  • 方法二:原地反转后半部分的链表。然后将前后两部分进行比较。空间复杂度O(1)(比较考验对于链表的操作以及边界条件的判定!)
  • 还有就是将链表的值复制到数组用双指针法,时间和空间都是O(n)
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func isPalindrome(head *ListNode) bool {
    if head == nil {
        return true
    }

    // 找前半部分的为节点,并且反转后半部分
    firstHalfEnd := endOfFirstHalf(head)
    secondHalfStart := reverseList(firstHalfEnd.Next) // 注意此处必须是Next才是后半部分第一个节点

    // 判断是否回文
    p1 := head
    p2 := secondHalfStart
    for p2 != nil {
        if p1.Val != p2.Val {
            return false
        }
        p1 = p1.Next
        p2 = p2.Next
    }
    firstHalfEnd.Next = reverseList(secondHalfStart)
    return true
}

// 返回的是原先的尾节点,也就是新的起始节点
func reverseList(head *ListNode) *ListNode {
    var prev, cur *ListNode = nil, head
    for cur != nil {
        nextTmp := cur.Next
        cur.Next = prev
        prev = cur
        cur = nextTmp
    }
    return prev
}

// 找到的slow的下一个就是后半部分第一个节点
func endOfFirstHalf(head *ListNode) *ListNode {
    fast := head
    slow := head
    for fast.Next != nil && fast.Next.Next != nil {
        fast = fast.Next.Next
        slow = slow.Next
    }
    return slow
}

环形链表

  • 快慢 指针:找链表的中间节点;判断链表是否有环;求链表倒数第n个节点
// 时间:O(n)
// 空间:O(1)
func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil {
        return false
    }
    slow, fast := head, head.Next
    for fast != nil && fast.Next != nil { // fast.Next判断,防止空指针异常
        if slow == fast {
            return true
        }
        slow = slow.Next
        fast = fast.Next.Next
    }
    return false
}

环形链表2️⃣

  • 官方解法通过数学推导演算出来:slowfast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
// 时间:O(n)
// 空间:O(1)
func detectCycle(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil {
        slow = slow.Next
        if fast.Next == nil {
            return nil
        }
        fast = fast.Next.Next
        if fast == slow {
            p := head
            for p != slow {
                p = p.Next
                slow = slow.Next
            }
            return p
        }
    }
    return nil
}

合并两个有序链表

  • 多注重细节,多刷
  • 创建一个新的头节点,然后分别合并两个链表。一定一定注意指针的操作。
// 时间:O(n)
// 空间:O(1)
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
    prehead := ListNode{Val: -1}
    prev := &prehead    // 必须是引用拷贝,否则无法对prehead产生修改。
    for l1 != nil && l2 != nil {
        if l1.Val <= l2.Val {
            prev.Next = l1
            l1 = l1.Next
        } else {
            prev.Next = l2
            l2 = l2.Next
        }
        prev = prev.Next
    }
    if l1 == nil {
        prev.Next = l2
    } else {
        prev.Next = l1
    }
    return prehead.Next
}

两数相加

  • 一道模拟题,思路不难,难的是实现,以及想得全面。
// 时间:O(max(m,n))
// 空间:O(1)
func addTwoNumbers(l1 *ListNode, l2 *ListNode) (head *ListNode) {
    var tail * ListNode
    carry := 0
    for l1 != nil || l2 != nil {
        n1, n2 := 0, 0
        if l1 != nil {
            n1 = l1.Val
            l1 = l1.Next
        }
        if l2 != nil {
            n2 = l2.Val
            l2 = l2.Next
        }
        sum := n1 + n2 + carry
        sum, carry = sum % 10, sum / 10
        if head == nil {
            head = &ListNode{Val: sum}
            tail = head
        } else {
            tail.Next = &ListNode{Val: sum}
            tail = tail.Next
        }
    }
    if carry > 0 {
        tail.Next = &ListNode{Val: carry}
    }
    return 
}

删除链表的第n个节点

  • 方法一:求链表长度,然后求是删除正数第几个。
  • 方法二:快慢指针,快指针先走n个。快指针到末尾,慢指针是倒数第n个。
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummy := &ListNode{0, head}
    first, second := head, dummy
    for i := 0; i < n; i++ {
        first = first.Next
    }
    for ; first != nil; first = first.Next {
        second = second.Next
    }
    second.Next = second.Next.Next
    return dummy.Next
}

两两交换链表中的节点

  • 练习链表操作,以及理解拷贝的好题!
  • node1 := temp.Next 就像相当于把temp.Next的地址赋值给了node1。首先是因为temp.Next是个指针类型,然后赋值的时候就是把地址给了node1,但因为go的引用指针对应的值时不用加*,所以有点迷惑。因此修改node1,就会修改temp.Next。temp、node1、node2都是指针,是他们指向节点的地址,如果取其值Val的话,就是其指向节点的Val。
// 时间:O(n)
// 空间:O(1)
func swapPairs(head *ListNode) *ListNode {
    dummyHead := &ListNode{0, head}    // 创建新的节点
    temp := dummyHead    // 创建temp指针,地址赋值给了temp,并不生成新的节点。
    for temp.Next != nil && temp.Next.Next != nil {
        node1 := temp.Next
        node2 := temp.Next.Next
        temp.Next = node2
        node1.Next = node2.Next
        node2.Next = node1
        temp = node1
    }
    return dummyHead.Next
}

K个一组反转链表

  • 考验对于链表的操作
  • Go链表中的拷贝:见链表一开始的介绍。
  • 本题难点,如何防止断链、头尾节点分别要记录它们之前和之后的一个节点。
// 时间:O(n)
// 空间:O(1)
func reverseKGroup(head *ListNode, k int) *ListNode {
    hair := &ListNode{Next: head}
    pre := hair

    for head != nil {
        tail := pre
        for i := 0; i < k; i++ {
            tail = tail.Next
            if tail == nil {    // 说明剩下节点的长度不够一组了
                return hair.Next
            }
        }
        nex := tail.Next
        head, tail = myReverse(head, tail) // 反转并且返回反转后的新的头尾节点
        // 反转后的链表进行连接
        pre.Next = head 
        tail.Next = nex
        // pre移动到新的头节点
        pre = tail
        head = tail.Next
    }
    return hair.Next
}
// 反转每一组的链表
func myReverse(head, tail *ListNode) (*ListNode, *ListNode) {
    prev := tail.next   // 防止断链
    p := head
    for prev != tail {
        nex := p.Next
        p.Next = prev
        prev = p
        p = nex
    }
    return tail, head
}

随机链表的复制

  • 方法一:使用一个哈希表存储新旧节点的映射。
  • 方法二:复制节点构成 AA′→BB′→CC′,之后在单独获取A′→B′→C
func copyRandomList(head *Node) *Node {
    if head == nil {
        return nil
    }
    // 因为要创建新的复制节点,所以是node.Next.Next
    for node := head; node != nil; node = node.Next.Next { 
        node.Next = &Node{Val: node.Val, Next: node.Next}
    }
    for node := head; node != nil; node = node.Next.Next { 
        if node.Random != nil {
            node.Next.Random = node.Random.Next
        }
    }
    headNew := head.Next
    for node := head; node != nil; node = node.Next {
        nodeNew := node.Next
        node.Next = node.Next.Next
        if nodeNew.Next != nil {
            nodeNew.Next = nodeNew.Next.Next
        }
    }
    return headNew
}

排序链表

  • 方法一:复制到数组,对数组进行排序
  • 方法二:归并排序,自底向上,每次都采用合并两个有序链表里的方式。
// 时间:O(nlogn)
// 空间:O(1)
func merge(head1, head2 *ListNode) *ListNode {
    dummyHead := &ListNode{}
    temp, temp1, temp2 := dummyHead, head1, head2
    for temp1 != nil && temp2 != nil {
        if temp1.Val <= temp2.Val {
            temp.Next = temp1
            temp1 = temp1.Next
        } else {
            temp.Next = temp2
            temp2 = temp2.Next
        }
        temp = temp.Next
    }
    if temp1 != nil {
        temp.Next = temp1
    } else if temp2 != nil {
        temp.Next = temp2
    }
    return dummyHead.Next
}

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

    length := 0
    for node := head; node != nil; node = node.Next {
        length++
    }

    dummyHead := &ListNode{Next: head}
    for subLength := 1; subLength < length; subLength <<= 1 {
        prev, cur := dummyHead, dummyHead.Next
        for cur != nil {
            head1 := cur
            for i := 1; i < subLength && cur.Next != nil; i++ {
                cur = cur.Next
            }

            head2 := cur.Next
            cur.Next = nil
            cur = head2
            for i := 1; i < subLength && cur != nil && cur.Next != nil; i++ {
                cur = cur.Next
            }

            var next *ListNode
            if cur != nil {
                next = cur.Next
                cur.Next = nil
            }

            prev.Next = merge(head1, head2)

            for prev.Next != nil {
                prev = prev.Next
            }
            cur = next
        }
    }
    return dummyHead.Next
}

补充:147 对链表进行插入排序

合并K个升序链表

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        return merge(lists, 0, lists.length - 1);
    }

    public ListNode merge(ListNode[] lists, int l, int r) {
        if (l == r) {
            return lists[l];
        }
        if (l > r) {
            return null;
        }
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    public ListNode mergeTwoLists(ListNode a, ListNode b) {
        if (a == null || b == null) {
            return a != null ? a : b;
        }
        ListNode head = new ListNode(0);
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
            if (aPtr.val < bPtr.val) {
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
        tail.next = (aPtr != null ? aPtr : bPtr);
        return head.next;
    }
}

LRU缓存

type LRUCache struct {
    size int
    capacity int
    cache map[int]*DLinkedNode
    head, tail *DLinkedNode
}

type DLinkedNode struct {
    key, value int
    prev, next *DLinkedNode
}

func initDLinkedNode(key, value int) *DLinkedNode {
    return &DLinkedNode{
        key: key,
        value: value,
    }
}

func Constructor(capacity int) LRUCache {
    l := LRUCache{
        cache: map[int]*DLinkedNode{},
        head: initDLinkedNode(0, 0),
        tail: initDLinkedNode(0, 0),
        capacity: capacity,
    }
    l.head.next = l.tail
    l.tail.prev = l.head
    return l
}

func (this *LRUCache) Get(key int) int {
    if _, ok := this.cache[key]; !ok {
        return -1
    }
    node := this.cache[key]
    this.moveToHead(node)
    return node.value
}


func (this *LRUCache) Put(key int, value int)  {
    if _, ok := this.cache[key]; !ok {
        node := initDLinkedNode(key, value)
        this.cache[key] = node
        this.addToHead(node)
        this.size++
        if this.size > this.capacity {
            removed := this.removeTail()
            delete(this.cache, removed.key)
            this.size--
        }
    } else {
        node := this.cache[key]
        node.value = value
        this.moveToHead(node)
    }
}

func (this *LRUCache) addToHead(node *DLinkedNode) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
}

func (this *LRUCache) removeNode(node *DLinkedNode) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (this *LRUCache) moveToHead(node *DLinkedNode) {
    this.removeNode(node)
    this.addToHead(node)
}

func (this *LRUCache) removeTail() *DLinkedNode {
    node := this.tail.prev
    this.removeNode(node)
    return node
}

二叉树

  • 牢记二叉树的四种遍历顺序的写法

  • 各种遍历方式的一些特性:

    • 后序遍历:遍历时,栈中存放从根节点到该节点路径上的所有节点。

补充: 深度优先搜索 广度优先搜索

二叉树的中序遍历

  • 方法一:递归。注意Go如何使用匿名函数。

  • 方法二:迭代。注意Go怎么使用栈和队列,因为Go并没有内置Stack和Queue类型(都是用切片类型来实现,实现起来也很简单和优雅):

    • stack := []*type                // 定义栈
      stack = append(stack, *type)    // 入栈
      stack = stack[:len(stack)-1]    // 出栈
      
      queue := []*type                // 定义队列
      queue = append(queue, *type)    // 入队
      queue = queue[1:]               // 出队
      
// 方法一:递归
// 时间:O(n)
// 空间:O(n)
func inorderTraversal(root *TreeNode) []int {
    var inorder func(node *TreeNode)    // 声明匿名函数
    var res []int
    inorder = func(node *TreeNode) {    // 定义匿名函数
        if node == nil {
            return
        }
        inorder(node.Left)
        res = append(res, node.Val)
        inorder(node.Right)
    }
    inorder(root)
    return res
}
// 不使用匿名函数
func inorderTraversal(root *TreeNode) []int {
    var res []int
    inorder(root, &res) // 传递切片指针
    return res
}

func inorder(root *TreeNode, res *[]int) {
    if root == nil {
        return
    }
    inorder(root.Left, res)
    *res = append(*res, root.Val) // 通过指针直接修改原始切片
    inorder(root.Right, res)
}

// 方法二:迭代(使用栈)
func inorderTraversal(root *TreeNode) []int {
    var res []int
    stack := []*TreeNode{}
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            root = root.Left
        }
        root = stack[len(stack)-1]      // root赋值最左下节点
        stack = stack[:len(stack)-1]    // 出栈,弹出最左下节点
        res = append(res, root.Val)     // 访问并保存节点
        root = root.Right               // 遍历右子树
    }
    return res
}

补充:二叉树迭代法的先序、后序 遍历 、层序遍历

  • 后序遍历:后序遍历的迭代写法:额外维护一个prev。prev 指向上一次访问的节点,用于判断右子树是否已被访问。相比于中序和前序,最大的区别就是出栈的时候,并不能直接访问该节点,必须先判断右子树是否为空或者已经访问。如果为空,当然可以直接访问该节点。prev上一次访问的节点,如果root.Right == prev,说明右子树已经被完全访问完了。则可以直接访问该节点。否则的话,该节点不可直接访问,要重新入栈,并且处理右子树。
// 先序遍历的迭代写法:和中序区别仅在于访问节点数值的位置不同
func preorderTraversal(root *TreeNode) []int {
    var res []int
    stack := []*TreeNode{}
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            res = append(res, root.Val)
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        root = root.Right
    }
    return res
}

// 后序遍历的迭代写法:额外维护一个prev
// prev:指向上一次访问的节点,用于判断右子树是否已被访问。
// 相比于中序和前序,最大的区别就是出栈的时候,并不能直接访问该节点,必须先判断右子树是否为空或者已经访问。
// 如果为空,当然可以直接访问该节点。prev上一次访问的节点,如果root.Right == prev,说明右子树已经被完全访问完了。则可以直接访问该节点。
func postorderTraversal(root *TreeNode) []int {
    stack := []*TreeNode{}
    var res []int
    var prev *TreeNode
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        if root.Right == nil || root.Right == prev {
            res = append(res, root.Val)
            prev = root
            root = nil    // 若该节点已经访问了,那么接下来肯定访问这个节点的父节点了。于是要让root = nil,否则会继续让该节点的左子树入栈。root = nil,下一次循环会直接弹出父节点。
        } else {
            stack = append(stack, root)
            root = root.Right
        }
    }
    return res
}

// 层序遍历
// 返回二维数组,保存层的信息
func levelOrder(root *TreeNode) [][]int {
    res := [][]int{}
    if root == nil {
        return res
    }
    queue := []*TreeNode{root}  // queue存储某一层所有的节点
    for i := 0; len(queue) > 0; i++ {
        res = append(res, []int{})
        p := []*TreeNode{}      // 下一层所有的节点
        for j := 0; j < len(queue); j++ {
            node := queue[j]
            res[i] = append(res[i], node.Val)
            if node.Left != nil {
                p = append(p, node.Left)
            }
            if node.Right != nil {
                p = append(p, node.Right)
            }
        }
        queue = p
    }
    return res
}
// 返回一维数组
func levelOrder(root *TreeNode) []int {
    res := []int{}
    if root == nil {
        return res
    }
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        node := queue[0]    // // 先取出队列头部节点
        queue = queue[1:]  // 出队    
        res = append(res, node.Val)        // 处理当前节点
        if node.Left != nil {        // 将左右子节点入队(如果存在)
            queue = append(queue, node.Left)
        }
        if node.Right != nil {
            queue = append(queue, node.Right)
        }
    }
    return res
}

二叉树的最大深度

  • 不能使用中序遍历:中序遍历遍历到某一节点时,栈中的所有节点并不是该节点的路径上的所有节点,比如右节点的父节点肯定出栈了。
  • 利用后序 遍历的性质:后序遍历某一个节点时,栈中的节点为从根节点到该节点路径上的所有节点。所以只需要在后序遍历中加上唯一一个判断,就是当入栈时,判断栈的深度是否大于max并且赋值即可!
// 时间:O(n)
// 空间:O(height)
func maxDepth(root *TreeNode) int {
    stack := []*TreeNode{}
    var prev *TreeNode
    max := 0
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            if len(stack) > max {
                max = len(stack)
            }
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        if root.Right == nil || root.Right == prev {
            prev = root
            root = nil
        } else {
            stack = append(stack, root)    // 该处永远不会入栈新节点,所以无需进行长度判断
            root = root.Right
        }
    }
    return max
}

反转二叉树

  • 层序遍历
// 时间:O(n)
// 空间:O(n)
func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]
        node.Left, node.Right = node.Right, node.Left
        if node.Left != nil {
            queue = append(queue, node.Left)
        }
        if node.Right != nil {
            queue = append(queue, node.Right)
        }
    }
    return root
}

对称二叉树

  • 方法一:先反转然后再判断反转后的和原先是否为同一个二叉树
  • 方法二:迭代。每次都插入两个节点,并且根节点也要入队两次。每次提取两个节点比较它们的值。两个节点的左右子节点按相反的方向插入到队列中。
// 时间:O(n)
// 空间:O(n)
func isSymmetric(root *TreeNode) bool {
    u, v := root, root
    q := []*TreeNode{}
    q = append(q, v)
    q = append(q, u)
    for len(q) > 0 {
        u, v = q[0], q[1]
        q = q[2:]
        if u == nil && v == nil {    // 队列中可能会包含nil节点
            continue
        }
        if u == nil || v == nil {
            return false
        }
        if u.Val != v.Val {
            return false
        }
        q = append(q, u.Left)
        q = append(q, v.Right)

        q = append(q, u.Right)
        q = append(q, v.Left)
    }
    return true
}

二叉树的直径

  • 递归遍历每个节点,每个节点左右经过的节点数为L、R,该路径上的总节点数为L+R+1。然后求的最大的那个即可。
  • 学会写递归:
func diameterOfBinaryTree(root *TreeNode) int {
    var depth func(node *TreeNode) int
    var ans int
    depth = func(node *TreeNode) int {
        if node == nil {
            return 0;
        }
        L := depth(node.Left)
        R := depth(node.Right)
        ans = max(ans, L+R+1)
        return max(L, R) + 1
    }
    ans = 1
    depth(root)
    return ans - 1
}

二叉树的层序遍历

  • 注意这道题会返回层次的信息,所以会比不返回层次信息的复杂一些。
func levelOrder(root *TreeNode) [][]int {
    res := [][]int{}
    if root == nil {
        return res
    }
    queue := []*TreeNode{root}  // queue存储某一层所有的节点
    for i := 0; len(queue) > 0; i++ {
        res = append(res, []int{})
        p := []*TreeNode{}      // 下一层所有的节点
        for j := 0; j < len(queue); j++ {
            node := queue[j]
            res[i] = append(res[i], node.Val)
            if node.Left != nil {
                p = append(p, node.Left)
            }
            if node.Right != nil {
                p = append(p, node.Right)
            }
        }
        queue = p
    }
    return res
}

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

  • nums[start:end] 会返回原切片从索引 startend-1 的元素
  • mid>0说明左边还有元素,mid+1<len(nums)说明右边还有元素。
func sortedArrayToBST(nums []int) *TreeNode {
    if len(nums) == 0 {
        return nil
    }
    mid := len(nums) / 2
    node := &TreeNode{
        Val: nums[mid],
    }
    if mid > 0 {
        node.Left = sortedArrayToBST(nums[:mid])
    }
    
    if mid + 1 < len(nums) {
        node.Right = sortedArrayToBST(nums[mid+1:])
    }
    return node
}

验证二叉搜索树

  • 本质就是中序遍历
func isValidBST(root *TreeNode) bool {
    if root == nil {
        return false
    }
    var res []int
    var stack []*TreeNode
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        res = append(res, root.Val)
        root = root.Right
    }
    for i := 0; i < len(res) - 1; i++ {
        if res[i] >= res[i+1] {
            return false
        }
    }
    return true
}

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

  • 直接中序遍历,i 用来记录遍历到第几个节点了
// 方法一
func kthSmallest(root *TreeNode, k int) int {
    var stack []*TreeNode
    i := 0
    for root != nil || len(stack) > 0 {
        for root != nil {
            stack = append(stack, root)
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        i++
        if i == k {
            return root.Val
        }
        root = root.Right
    }
    return 0
}

二叉树的右视图

  • 本质是层序遍历,并且还是要获取层次信息的。获取了层次信息,以及每层的节点之后,直接返回每一层最右边的节点即可。
func rightSideView(root *TreeNode) []int {
    res := [][]int{}
    if root == nil {
        return nil
    }
    queue := []*TreeNode{root}  // queue存储某一层所有的节点
    for i := 0; len(queue) > 0; i++ {
        res = append(res, []int{})
        p := []*TreeNode{}      // 下一层所有的节点
        for j := 0; j < len(queue); j++ {
            node := queue[j]
            res[i] = append(res[i], node.Val)
            if node.Left != nil {
                p = append(p, node.Left)
            }
            if node.Right != nil {
                p = append(p, node.Right)
            }
        }
        queue = p
    }
    var rst []int
    for i := 0; i < len(res); i++ {
        rst = append(rst, res[i][len(res[i])-1])
    }
    return rst
}

二叉树展开为链表

  • 就是前序遍历二叉树,保存前序遍历到一个数组中,然后再根据该数组构造链表即可。
func flatten(root *TreeNode)  {
    var list []*TreeNode
    var stack []*TreeNode
    for root != nil || len(stack) > 0 {
        for root != nil {
            list = append(list, root)    // 保存前序遍历节点
            stack = append(stack, root)
            root = root.Left
        }
        root = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        root = root.Right
    }
    for i := 1; i < len(list); i++ {            // 根据前序遍历节点构造链表
        prev, cur := list[i-1], list[i]
        prev.Left, prev.Right = nil, cur
    }
}

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

  • 递归的方法来做。
  • 一定不要忘记递归的终止条件判断,也就是preorder和inorder长度为0,说明左子树或者右子树已经为空了。
func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 || len(inorder) == 0 {
        return nil
    }
    i := 0
    for inorder[i] != preorder[0] {
        i++
    }
    root := &TreeNode{
        Val: preorder[0],
        Left: buildTree(preorder[1:i+1], inorder[:i]),
        Right: buildTree(preorder[i+1:], inorder[i+1:]),
    }
    return root
}

路径总和3️⃣

  • 方法一是自己+豆包写的。后序遍历,迭代法,前缀和。
  • 方法二:前缀和,dfs
// 方法一
func pathSum(root *TreeNode, targetSum int) int {
    result := 0
    stack := []*TreeNode{}
    mp := map[int]int{0: 1} // 初始化前缀和为0的情况出现1次
    currentSum := 0
    var prev *TreeNode
    
    for root != nil || len(stack) > 0 {
        // 遍历左子树,计算前缀和
        for root != nil {
            currentSum += root.Val
            stack = append(stack, root)
            // 检查以当前节点为终点的路径是否符合条件
            if count, exists := mp[currentSum - targetSum]; exists {
                result += count
            }
            mp[currentSum]++ // 记录当前前缀和
            root = root.Left
        }
        
        // 处理当前节点
        root = stack[len(stack)-1]
        
        // 如果右子树为空或已访问过,处理当前节点并回溯
        if root.Right == nil || root.Right == prev {
            // 回溯:移除当前路径的影响
            mp[currentSum]--
            currentSum -= root.Val
            
            stack = stack[:len(stack)-1]
            prev = root
            root = nil
        } else {
            // 处理右子树
            root = root.Right
        }
    }
    
    return result
}

// 方法二
func pathSum(root *TreeNode, targetSum int) int {
    var ans int
    preSum := map[int64]int{0: 1}
    var dfs func(*TreeNode, int64)
    dfs = func(node *TreeNode, curr int64) {
        if node == nil {
            return 
        }
        curr += int64(node.Val)
        ans += preSum[curr-int64(targetSum)]
        preSum[curr]++
        dfs(node.Left, curr)
        dfs(node.Right, curr)
        preSum[curr]--
        return 
    }
    dfs(root, 0)
    return ans
}

二叉树的最近公共祖先

  • 递归的写法
 func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
    // 如果 root 是空,或就是 p 或 q,直接返回
    if root == nil {
        return nil
    }
    if root.Val == p.Val || root.Val == q.Val {
        return root
    }
    // 在左子树中找 p 或 q
    left := lowestCommonAncestor(root.Left, p, q)
    // 在右子树中找 p 或 q
    right := lowestCommonAncestor(root.Right, p, q)
    // 情况1:左右都找到 → 当前 root 就是最近公共祖先
    if left != nil && right != nil {
        return root
    }
    // 情况2:只找到一个 → 把那个往上返回
    if left == nil {
        return right
    }
    return left
 }

二叉树的最大路径和

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)

        // 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        priceNewPath := node.Val + leftGain + rightGain

        // 更新答案
        maxSum = max(maxSum, priceNewPath)

        // 返回节点的最大贡献值
        return node.Val + max(leftGain, rightGain)
    }
    maxGain(root)
    return maxSum
}

func max(x, y int) int {
    if x > y {
        return x
    }
    return y
}

回溯

有效的括号

func isValid(s string) bool {
    stack := []byte{}
    mp := map[byte]byte{']': '[', '}': '{', ')': '('}
    for _, r := range s {
        v := byte(r)
        if v == '(' || v == '{' || v == '[' {
            stack = append(stack, v)
            continue
        } 
        if len(stack) == 0 {    //右括号记得先判断栈是否为空
            return false
        }
        ch := stack[len(stack)-1]
        stack = stack[:len(stack)-1]    // 记得出栈
        if ch != mp[v] {        
            return false
        }
    
    }
    return len(stack) == 0    // 最后还要看一下栈是不是空
}

最小栈

type MinStack struct {
    stack []int
}

func Constructor() MinStack {
    return MinStack{
        stack: []int{},
    }
}

func (this *MinStack) Push(val int)  {
    this.stack = append(this.stack, val)
}

func (this *MinStack) Pop()  {
    if len(this.stack) == 0 {
        return 
    }
    this.stack = this.stack[:len(this.stack)-1]
}

func (this *MinStack) Top() int {
    return this.stack[len(this.stack)-1]
}

func (this *MinStack) GetMin() int {
    if this.stack == nil {
        return 0
    }
    min := this.stack[0]
    for i := 1; i < len(this.stack); i++ {
        if this.stack[i] < min {
            min = this.stack[i]
        }
    }
    return min
}

字符串解码

全排列

func permute(nums []int) [][]int {
    var res [][]int
    var output []int
    for _, v := range(nums) {
        output = append(output, v)
    }
    n := len(nums)
    res = backtrack(n, output, res, 0)
    return res
}

func backtrack(n int, output []int, res [][]int, first int) [][]int{
    if first == n {
        temp := make([]int, len(output))
        copy(temp, output)
        res = append(res, temp)
        return res
    }
    for i := first; i < n; i++ {
        output = swap(output, first, i)
        res = backtrack(n, output, res, first + 1)
        output = swap(output, first, i)
    }
    return res
}

func swap(output []int, first, i int) []int{
    output[first], output[i] = output[i], output[first]
    return output
}