【LeetCode专项】递归、排序、贪心

223 阅读6分钟

88. 合并两个有序数组

代码

func merge(nums1 []int, m int, nums2 []int, n int)  {
    i, j := m-1, n-1
    tail := len(nums1)-1

    for i>=0 || j>=0 {
        if i<0 || (j>=0 && nums2[j]>=nums1[i]) {
            nums1[tail]=nums2[j]
            j--
        } else {
            nums1[tail]=nums1[i]
            i--
        }
        tail--
    }
}

思路

因为后边是空的,所以从后往前排

性能

image.png

75. 颜色分类

代码

func sortColors(nums []int)  {
    quickSort(&nums, 0, len(nums)-1)
}

func quickSort(nums *[]int, low int, high int) {
    if low>=high {
        return
    }

    value := (*nums)[low]
    i, j := low, high
    for i<j {
        for i<j && (*nums)[j]>value {
            j--
        }
        (*nums)[i], (*nums)[j] = (*nums)[j], (*nums)[i]
        for i<j && (*nums)[i]<=value {
            i++
        }
        (*nums)[i], (*nums)[j] = (*nums)[j], (*nums)[i]
    }

    quickSort(nums,low,  i-1)
    quickSort(nums, i+1, high)
}

思路

对于这道题,有两种思路。
第一种:由于只有012,所以可以记住0,1,2的个数,然后直接把数组刷成结果。
第二种:原地排序,锻炼一下自己,选择了快排。

  • 快速排序的核心思想
    • 随便选一个值,让小于等于(小于等于而不是小于,重要)这个值的都在左边,让大于这个值的都在右边
    • 分治,分别对左边和右边排序
  • 原地排序:golang
    • 需要对int数组取地址(&nums)来获取指针
    • 使用数组指针的时候,要解指针来使用具体的值:(*nums)[i],别忘了括号(重要)
  • 选择值value,随便选一个就可以,在这里选择了(*nums)[low],目标是获得一个下标(这里是i),让在i左边的都小于等于value,在i右边的都大于等于value
    • 先从右往左走(重要),获得右边第一个比value小的值
    • 交换,然后找到左边第一个比value大的,交换。一直循环
  • 循环后,保证了下标i左边的小于等于value,下标i右边的大于value
  • 分治,让左右两边有序,注意是i-1和i+1,不能像归并一样直接用i(重要)

性能

image.png

面试题 16.16. 部分排序

代码

func subSort(a []int) []int {
    n := len(a)
    min, max := math.MaxInt64, math.MinInt64

    left, right := -1, -1

    for i, v := range a {
        if v<max {
            right = i
        } else {
            max = v
        }
    }
    for i:=n-1; i>=0; i-- {
        if a[i]>min {
            left = i
        } else {
            min = a[i]
        }
    }

    return []int{left, right}
}

思路

  • 核心思路:
    • 待排序的左边界:
      • 右边存在小于它的值
    • 待排序的右边界:
      • 左边存在大于它的值
  • 做法
    • 先从左往右遍历,目的是找到右边界
      • 期望是找到一个下标right,a[right]小于左边最大的值,并且这个right尽量大。
        • 做法就是逐渐记录左边最大的值:max,
        • 然后让a[right]和max比较,
        • 如果a[right]小于max,则right是目前的右边界。
    • 再从右往左遍历,目的是找到左边界,同理
      • 期望是找到一个下标left,a[left]大于右边最小的值,并且这个left尽量小。
        • 做法就是逐渐记录右边最小的值:min,
        • 然后让a[left]和min比较,
        • 如果a[left]大于min,则left是目前的右边界。
  • 注意
        if v>=max {
            max = v
        } else {
            right = i
        }
        if a[i]<=min {
            min = a[i]
        } else {
            left = i
        }

这么写了,就是小于等于,和大于等于,要带上等于号。
不然值虽然没赋,但是会走到else里,是错误的

性能

性能不准 image.png

剑指 Offer 51. 数组中的逆序对

代码

func reversePairs(nums []int) int {
    ans, result := mergeSort(nums)
    fmt.Println(ans)
    return result
}

func mergeSort(nums []int) ([]int, int) {
    n := len(nums)
    if n<=1 {
        return nums, 0
    }
    left, leftResult := mergeSort(nums[:n/2])
    right, rightResult := mergeSort(nums[n/2:])
    
    ans, result := merge(left, right)
    result += leftResult+rightResult
    return ans, result
}

func merge(left []int, right []int) ([]int, int) {
    i, j := 0, 0
    m, n := len(left), len(right)
    ans := make([]int,0, m+n)
    result := 0

    for i<m || j<n {
        if i==m || (j<n && left[i]>right[j]) {
            ans = append(ans, right[j])
            j++
        } else {
            ans = append(ans, left[i])
            i++
            result+=j
        }
    }
    return ans, result
}

思路

  • 逆序对的数量,就是归并排序中,合并的顺序。
  • 解析:逆序指的是左边比右边大
    • 归并的时候,如果本次选择了数组left(左边的数组)的第i个数,证明左边left[i]比右边right[j]要小,但同时left[i]right[0]到right[j-1]都要大,大了j个。
  • 举例
    • 例如3,1,4,2
      • 最底层3,14,2,排完序之后是1,32,4,同时result变成了2(3>1,4>2)
      • []——[1,3],[2,4]
      • [1]——[3],[2,4],左边比右边小,1进ans数组,1比j个数大(比0个数大)
      • [1,2]——[3],[4],右边比左边小,2进ans数组,
      • [1,2,3]——[],[4],左边比又变小,3进ans数组,同时3比j个数大(比1一个数大)
  • 问题
    • 问题1:归并的时候能把所有的考虑到吗
      • 在最底层归并的时候,已经把一些问题解决了,例如上边提到的:3,1,4,2,最底层是3,14,2,排完序之后是1,32,4,同时result变成了2(3>1,4>2)。

性能

时间复杂度:O(NLogN)
空间复杂度:O(N)

315. 计算右侧小于当前元素的个数

代码

type node struct{
    index int
    val int
    count int
}

func countSmaller(nums []int) []int {
    n := len(nums)
    if n==0 {
        return []int{}
    }

    array := make([]node, n)
    for i:=0; i<n; i++ {
        array[i].index=i
        array[i].val=nums[i]
    }

    ans := mergeSort(array)
    result := make([]int, n)
    for i:=0; i<n; i++ {
        result[ans[i].index]=ans[i].count
    }
    return result
}

func mergeSort(nums []node) []node {
    n := len(nums)
    if n<=1 {
        return nums
    }

    left := mergeSort(nums[:n/2])
    right := mergeSort(nums[n/2:])

    return merge(left, right)
}
func merge(left []node, right []node) []node {
    m, n := len(left), len(right)
    i, j := 0, 0
    ans := make([]node, 0, m+n)

    for i<m || j<n {
        if i==m || (j<n && right[j].val<left[i].val) {
            ans = append(ans, right[j])
            j++
        } else {
            left[i].count+=j
            ans = append(ans, left[i])
            i++
        }
    }
    return ans
}

思路

剑指 Offer 51. 数组中的逆序对一样,通过归并排序的分治来计算逆序对(左比右大就是逆序)。
不一样的是需要记录每个值到底排了几次,需要记录之前它在哪,究竟多少个。
选择一个结构体来辅助,index是之前的下标,用于最后的时候构建result数组,val是值的大小,count是逆序对数量,通过交换顺序来控制

性能

时间复杂度:O(NLogN)
空间复杂度:O(N)

23. 合并 K 个升序链表

代码

import (
    "container/heap"
    "fmt"
)
// 堆
type IntHeap []*ListNode
func (h IntHeap) Len() int {
    return len(h)
}
func (h IntHeap) Less(i, j int) bool {
    return h[i].Val<h[j].Val
}
func (h IntHeap) Swap(i, j int) {
    h[i], h[j] = h[j], h[i]
}
func (h *IntHeap) Push(x interface{}) {
    *h = append(*h, x.(*ListNode))
}
func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}


func mergeKLists(lists []*ListNode) *ListNode {
    if lists==nil || len(lists) ==0 {
        return nil
    }

    var h IntHeap
    heap.Init(&h)

    for _, list := range lists {
        if list == nil {
            continue
        }
        heap.Push(&h, list)
    }

    ans := &ListNode{}
    tmp := ans
    for h.Len() > 0 {
        t := heap.Pop(&h).(*ListNode)
        if t.Next != nil {
            heap.Push(&h, t.Next)
        }
        tmp.Next=t
        tmp = tmp.Next
    }
    return ans.Next
}

思路

用golang的原生堆
堆的写法:

  • 堆的包
    "container/heap"
  • 堆的方法重写(注意类的成员方法是不是用的对象的指针)
func (h IntHeap) Len() int {
    return len(h)
}
func (h IntHeap) Less(i, j int) bool {
    return h[i].Val<h[j].Val
}
func (h IntHeap) Swap(i, j int) {
    h[i], h[j] = h[j], h[i]
}
func (h *IntHeap) Push(x interface{}) {
    *h = append(*h, x.(*ListNode))
}
func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}
  • 堆的用法
    type IntHeap []*ListNode
    
    var h IntHeap
    heap.Init(&h)
    
    heap.Push(&h, list)
    
    heap.Pop(&h).(*ListNode)

为了降低空间复杂度,只在堆中保存lists里每个的第一个node

性能

时间复杂度:O(NLogN)
空间复杂度:小于O(N)

977. 有序数组的平方

代码

func sortedSquares(nums []int) []int {
    n := len(nums)
    ans := make([]int,0, n)
    right := 0
    for right<n && nums[right]<0 {
        right++
    }

    left := right-1
    
    for left>=0 || right<n {
        if left<0 || (right<n && nums[right]<(-nums[left])) {
            ans = append(ans, nums[right]*nums[right])
            right++
        } else {
            ans = append(ans, nums[left]*nums[left])
            left--
        }
    }

    return ans
}

思路

以正负为分界,用双指针,right指向非负数,left指向负数。
比较-nums[left]nums[right]大小,把小的append到数组中。

性能

时间复杂度:O(n)
空间复杂度:O(1)

11. 盛最多水的容器

代码

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

func maxArea(height []int) int {
    left, right := 0, len(height)-1
    max := 0
    tmp := 0

    for left<right {
        tmp = (right-left)*MIN(height[left],height[right])
        if tmp>max {
            max = tmp
        }

        if height[right]<height[left] {
            right--
        } else {
            left++
        }
    }
    return max
}

思路

面积=(右下标-左下标)*MIN(右边高度,左边高度)
最开始的时候(右下标-左下标)最大,所以这个一定越调越小,
只有动相对更小的那一边,面积才可能更大(让MIN(右边高度,左边高度)更大)

性能

时间复杂度:O(n)
空间复杂度:O(1)

1. 两数之和

代码

func twoSum(nums []int, target int) []int {
    n := len(nums)
    m := make(map[int]int, 0)

    i := 0
    for i<n {
        if v, ok := m[target-nums[i]]; ok {
            return []int{i, v}
        } else {
            m[nums[i]]=i
        }
        i++
    }
    return []int{}
}

思路

在拿到一个值的时候,查看和它加和是target的值是否在map中,
如果不在的话就塞进map中。

性能

时间复杂度:O(1)
空间复杂度:O(n)

455. 分发饼干

代码

func findContentChildren(g []int, s []int) int {
    sort.Ints(g)
    sort.Ints(s)

    i, j := len(g)-1, len(s)-1
    ans := 0

    for i>=0 && j>=0 {
        if s[j]>=g[i] {
            ans++
            j--
            i--
        } else {
            i--
        }
    }

    return ans
}

思路

先排序,然后从需求最大的到需求最小的遍历,如果s[j]>=g[i]结果++,否则找需求更小的那个人。

性能

时间复杂度:排序O(nlogn)+遍历O(m+n)
空间复杂度:O(1)

860. 柠檬水找零

代码

func lemonadeChange(bills []int) bool {
    a, b := 0, 0
    for _, v := range bills {
        if v==5 {
            a++
        } else if v==10 {
            a--
            b++
        } else {
            if b>=1 && a>=1 {
                b--
                a--
            } else {
                a-=3
            }
        }

        if a<0 || b<0 {
            return false
        }
    }
    return true
}

思路

用a来存储5美元零钱的个数,用b来存储10美元的个数

性能

空间复杂度:O(1)
时间复杂度:O(n)

452. 用最少数量的箭引爆气球

代码

func findMinArrowShots(points [][]int) int {
    sort.Slice(points, func(i, j int)bool {
        return points[i][1]<points[j][1]
    })

    ans := 1
    tmp := points[0][1]

    for _, v := range points {
        if v[0]>tmp {
            ans++
            tmp=v[1]
        }
    }
    return ans
}

思路

  • 按气球的右边界排序。

  • 第一箭射在第一个气球的右边界,然后计算,射这一支箭能同时射爆几个气球。(每个气球都要射爆,所以第一支箭要射在右边界最小的那个气球上才能保证)。

  • 假设第三个气球射不到,那第二支箭就射在这个气球的右边界。再去统计

  • tmp存放射的最新一支箭是哪个边界,ans记录一共射了几箭

  • 有一个case是这样的:

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

结果是2,能正常输出的原因是,计算当中,第一支箭不是射的1,3气球,而是只射了第一气球,第二支箭射在4上,射爆了第2,3气球。不管第三个气球的左边界是1还是4,都可以被第二支箭射爆。

性能

时间复杂度:O(nlogn),有排序
空间复杂度:O(1)

402. 移掉 K 位数字

代码

func removeKdigits(num string, k int) string {
    n := len(num)
    stack := make([]byte, 0, n-k)

    i := 0
    for ; i<n; i++ {
        for k>0 && len(stack) > 0 && stack[len(stack)-1]>num[i] {
            stack = stack[:len(stack)-1]
            k--
        }
        
        stack = append(stack, num[i])
    }

    stack = stack[:len(stack)-k]
    for {
        if len(stack)==0 {
            return "0"
        }
        if stack[0]!='0' {
            break
        }
        stack = stack[1:]
    }
    return string(stack)
}

思路

  • 先考虑,只有两个字符,丢弃一个的情况下,选择哪个进行丢弃,有几种情况。
    • 01,10,12,21,11。这五种,分别包含了:带零零开头,带零零不开头,递增,递减,相同这五种情况。
    • 最优回答分别为:0,0,1,1,1。做法如下
      • 01,看到1的时候发现1比0大,直接丢1.
      • 10,看到0的时候发现0比1小,将临时结果里的1丢掉,留0.
      • 12,和之前一致.
      • 21,11不同,它发现1比2小或者1和1相同,进入临时结果,遍历结束留下len为2。直接截取n-k位即可。
  • 用后边的和前边的相比,如果前边比后边大,则丢弃前边的。
  • 如果到最后是单调递增的,中间不会扔,直接截取n-k位
  • 最后结果删除前导0

性能

时间复杂度:O(n)
空间复杂度:O(n)

55. 跳跃游戏

代码

func canJump(nums []int) bool {
    n := len(nums)
    ans := 0

    i:=0
    for ; i<n; i++ {
        if ans<i {
            return false
        }
        if nums[i]+i>ans {
            ans=nums[i]+i
        }
    }

    return true
}

思路

因为,在每一个点,可以跳到i到i+nums[i]任意位置,只需要记录能跳跃的最大位置即可。

性能

空间复杂度:O(1)
时间复杂度:O(n)

376. 摆动序列

代码

func wiggleMaxLength(nums []int) int {
    n := len(nums)
    if n==0 {
        return 0
    } else if n==1 {
        return 1
    }
    
    ans := 1
    i:=1
    now := true
    for ; i<n; i++ {
        if nums[i]-nums[i-1]<0 {
            now = true
            ans++
            break
        } else if nums[i]-nums[i-1]>0 {
            now=false
            ans++
            break
        }
    }

    i++
    for ; i<n; i++ {
        if now && nums[i]-nums[i-1]>0 {
            now=!now
            ans++
        } else if !now && nums[i]-nums[i-1]<0 {
            now=!now
            ans++
        }
    }

    return ans
}

思路

  • 从前往后遍历,先找到第一对递增/递减的数。
  • now为true证明上一个是递减的,now位false证明上一个是递增的。
  • 继续遍历。
    • 如果上一个是递增的,遇到递减的,now取反,ans++。
    • 如果上一个是递减的,遇到递增的,now取反,ans++。

性能

空间复杂度:O(1)
时间复杂度:O(n)

122. 买卖股票的最佳时机 II

代码

func threeSum(nums []int) [][]int {
    sort.Ints(nums)
    n := len(nums)

    ans := [][]int{}

    i, j, k := 0, 0, 0
    tmp := 0
    for ; i<n-2; i++ {
        if i!=0 && nums[i]==nums[i-1] {
            continue
        }
        if nums[i]>0 {
            break
        }

        j=i+1
        k=n-1
    
        for j<k {
            if j!=i+1 && nums[j]==nums[j-1] {
                j++
                continue
            } else if k!=n-1 && nums[k]==nums[k+1] {
                k--
                continue
            }

            tmp = nums[i]+nums[j]+nums[k]
            if tmp==0 {
                ans = append(ans, []int{nums[i], nums[j], nums[k]})
                j++
                k--
            } else if tmp<0 {
                j++
            } else {
                k--
            }
        }
    }
    return ans
}

思路

  • 三指针遍历循环,n^3,即使剪枝也无法通过时间
  • 先排序
  • 用边界点算值,初始时j=i+1k=n-1
  • 如果加和小于零,j向右移。
  • 如果加和大于零,k向左移。
  • 记得要排除重复值,每次使用用j、k的时候都判断
    • 不是初始值且是上一次的值

性能

  • 空间复杂度:O(n)
  • 时间复杂度:O(n^2)

16. 最接近的三数之和

代码

func threeSumClosest(nums []int, target int) int {
    sort.Ints(nums)
    min := nums[0]+nums[1]+nums[2]-target
    i, j, k := 0, 0, 0
    n := len(nums)
    tmp := 0

    for ; i<n; i++ {
        j=i+1
        k=n-1
        for j<k {
            
            tmp = nums[i]+nums[j]+nums[k]-target
            if abs(tmp)<abs(min) {
                min=tmp
            }

            if tmp>=0 {
                k--
            } else {
                j++
            }
        }
    }

    return min+target
}

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

思路

和三数之后一致,也是排序后,挪动前后两个指针。

性能

  • 空间复杂度:O(n)
  • 时间复杂度:O(n^2)

134. 加油站

代码

func canCompleteCircuit(gas []int, cost []int) int {
    n := len(gas)
    ans := 0
    now := 0
    before := 0

    for i:=0; i<n; i++ {
        now = now+gas[i]-cost[i]

        if now<0 {
            before += now

            ans = i+1
            now = 0
        }
    }
    
    now += before
    if now<0 {
        return -1
    }
    return ans
}

思路

  • 核心思路:
    • 假设从x点到y点,就无法继续了(剩余油量小于0)
    • 那么x到y中间的任何一个点,都无法绕一整圈
  • 核心思路证明(反证法):
    • 假设x+n可以绕圈(1<=n<=y-x),
    • 那x+n一定能到y+1。
    • 因为x能走到x+n(限制条件n<=y-x),所以x+n一定能走到y+1
    • 与实际不符。
  • 解法和存储的量:
    • 如果绕圈失败,不从起点的下一个点开始,而是从最后一个不可以的点的下一个点开始。只遍历一次
    • (从x到y点失败,从y+1开始,而不是从x+1开始)
    • 记录本次起点:ans
    • 记录当前油量:now
    • 记录之前油量:before(不需要再遍历算一次了)
  • 实现
    • 从起点开始,计算油量到now,如果不能继续前进,记录进之前的油量,从下一个点开始遍历。
    • 到最后一个点之后,计算是否能转圈(now=now+before),判断。

性能

  • 空间复杂度:O(1)
  • 时间复杂度:O(n)

56. 合并区间

代码

func merge(intervals [][]int) [][]int {
    sort.Slice(intervals, func(i, j int)bool{
        return intervals[i][0]<intervals[j][0]
    })

    ans := make([][]int, 0)
    n := 0
    for _, now := range intervals {
        n = len(ans)
        if n==0 || now[0]>ans[n-1][1] {
            ans = append(ans, []int{now[0], now[1]})
        } else if now[0]<=ans[n-1][1] && now[1]>ans[n-1][1] {
            ans[n-1][1]=now[1]
        }
    }

    return ans
}

思路

  • 分治思想,先考虑两个区间会遇到哪些情况,如何合并,在考虑如何推广。
  • 先排序,排序之后的case如下
    • (1,3),(1,2)结果(1,3)
    • (1,3),(2,3)结果(1,3)
    • (1,3),(3,4)结果(1,4)
    • (1,3),(2,4)结果(1,4)
    • (1,3),(4,5)结果(1,3),(4,5)
  • 结果放在ans中,遍历到的元素放在now中
    • 根据1,2,可以总结now[1]<=ans[n-1][1]时,不用管。
    • 根据3,4,可以总结now[0]<=ans[n-1][1]且now[1]>ans[n-1][1],要将右边界向右移。
    • 根据5,可以总结now[0]>ans[n-1][1]时,要直接将now放进ans中

性能

  • 时间复杂度:O(nlogn),有排序
  • 空间复杂度:O(n)