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--
}
}
思路
因为后边是空的,所以从后往前排
性能
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(重要)
性能
面试题 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是目前的右边界。
- 期望是找到一个下标right,
- 再从右往左遍历,目的是找到左边界,同理
- 期望是找到一个下标left,
a[left]大于右边最小的值,并且这个left尽量小。- 做法就是逐渐记录右边最小的值:min,
- 然后让
a[left]和min比较, - 如果
a[left]大于min,则left是目前的右边界。
- 期望是找到一个下标left,
- 先从左往右遍历,目的是找到右边界
- 注意
if v>=max {
max = v
} else {
right = i
}
if a[i]<=min {
min = a[i]
} else {
left = i
}
这么写了,就是小于等于,和大于等于,要带上等于号。
不然值虽然没赋,但是会走到else里,是错误的
性能
性能不准
剑指 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个。
- 归并的时候,如果本次选择了数组left(左边的数组)的第i个数,证明左边
- 举例
- 例如
3,1,4,2,- 最底层
3,1和4,2,排完序之后是1,3和2,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,1和4,2,排完序之后是1,3和2,4,同时result变成了2(3>1,4>2)。
- 在最底层归并的时候,已经把一些问题解决了,例如上边提到的:
- 问题1:归并的时候能把所有的考虑到吗
性能
时间复杂度: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+1,k=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中
- 根据1,2,可以总结
性能
- 时间复杂度:O(nlogn),有排序
- 空间复杂度:O(n)