Go 刷题清单
数组
综合常考
#560 和为 K 的子数组
本题可用暴力枚举和前缀和两种方法求解。以 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
}
哈希表
基础必会
综合常考
128 最长连续序列 #49 字母异位词分组
在本题中,要求\(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
}
双指针
基础必会
#11 盛水最多的容器 #88 合并两个有序数组 #283 移动零 #26 删除有序数组中的重复项
对撞指针计算面积,只移动较矮的一边。
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 --
}
}
注意移动零时还要保持非零元素的相对位置,因此不能使用对撞指针,而应该用快慢指针不断交换零和非零元素
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++
}
综合常考
#3 无重复字符的最长子串 #15 三数之和 #5 最长回文子串 #31 下一个排列 #165 比较版本号
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]
解题思路:
从右往左,找到第一个满足 nums[k] < nums[k+1] 的位置 k(即第一个"下降点")。若不存在,说明已是最大排列,直接翻转整个数组即可。(可以通过折线图可视化查找过程)
再从右往左,找到第一个满足 nums[l] > nums[k] 的位置 l。
交换 nums[k] 与 nums[l]。
翻转 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
}
链表
基础必会
#206 反转链表 #92 局部反转链表 #876 链表的中间节点 #141 判断环形链表 #142 找到环形链表的入口 #160 相交链表 #21 合并两个有序链表
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
}
本题使用快慢指针求解,但要注意链表奇偶长度的处理
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
}
综合常考
#25 K个一组反转链表 #23 合并K个升序链表 #148 排序链表 #143 重排链表 #146 LRU缓存
分组+局部反转(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
}
二叉树
遍历方式
DFS 递归解法 #102 层序遍历 #144 前序遍历(最简单) #94 中序遍历(仅限于二叉树) #145 后序遍历(最复杂)
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
/ \ / \
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
}
后序遍历题目
当节点需要依赖左右子树的信息时,使用后序遍历
#104 最大深度 #110 平衡树 #543 树的直径 #124 最大路径和 #236 最近公共祖先 #235 二叉搜索树的最近公共祖先
与递归翻转二叉树类似,只是这个需要后序遍历计算最大深度。
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 判断逻辑
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
}
中序遍历题目
前序遍历题目
当节点需要依赖左右子树的信息时,使用前序遍历,这样不仅代码简单,而且高效
层序遍历题目
#103 锯齿形层序遍历 #199 右视图 #111 最小深度
本题的解题关键在于水平方向开关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
#200 岛屿数量
可用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 ) // 右
}
堆/优先队列
栈
队列
二分查找
#704 二分查找 #35 搜索插入位置 #69 x的平方根 #153 寻找旋转排序数组中的最小值 #33 搜索旋转排序数组 #34 在排序数组中查找元素的第一个和最后一个位置
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
}
如图所示,这道题目的本质是在有序数组中查找最值问题,只不过通过旋转数组来增加难度。
本题中,最小值就是断点,也就是「右侧有序段」的起点。
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) 即可获取到最大值。
如果数组中包含重复元素,则
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
}
动态规划
基础必会
#509 斐波那契数 #70 爬楼梯
斐波那契数作为递归和动态规划的入门问题,必须掌握。
注意:斐波那契数有从 0 和从 1 开始两个版本,其判断条件也分别是 n>=2 和 n>=3(n 指索引),本题采用的是 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
最简单的递归解法:
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
}
综合常考
#746 最小花费爬楼梯 #53 最大子数组和 #300 最长递增子序列 #1143 最长公共子序列 #64 最小路径和 #62 不同路径 #198 打家劫舍 #322 零钱兑换 #42 接雨水 编辑距离 最大正方形 最长有效括号
状态转移方程:
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)\)
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种解法:动态规划、单调栈、双指针。
由于要有坑洼才能接雨水,因此需要找到下一个更高的柱子,即使用递减栈。
回溯
基础必会
综合常考
贪心
基础必会
综合常考
递归
编写递归步骤:
明确输入输出
明确递归终止条件(什么时候触底反弹?)
明确递归处理逻辑(每层要做什么事?)
明确递归过程(下楼做还是返回做?)
图论