文章目录
- 21. 螺旋矩阵
- 题目描述
- 代码与解题思路
- 22. 反转链表 II
- 题目描述
- 代码与解题思路
- 23. 相交链表
- 题目描述
- 代码与解题思路
- 24. 合并 K 个升序链表
- 题目描述
- 代码与解题思路
- 25. 字符串相加
- 题目描述
- 代码与解题思路
- 26. 最长递增子序列
- 题目描述
- 代码与解题思路
- 27. 重排链表
- 题目描述
- 代码与解题思路
- 28. 环形链表 II
- 题目描述
- 代码与解题思路
- 29. 接雨水
- 题目描述
- 代码与解题思路
- 30. 删除链表的倒数第 N 个结点
- 题目描述
- 代码与解题思路
21. 螺旋矩阵
题目链接:54. 螺旋矩阵
题目描述
代码与解题思路
func spiralOrder(matrix [][]int) (res []int) {
if len(matrix) == 0 {
return res
}
top, bottom, left, right := 0, len(matrix)-1, 0, len(matrix[0])-1
for top < bottom && left < right {
for i := left; i < right; i++ { res = append(res, matrix[top][i]) }
for i := top; i < bottom; i++ { res = append(res, matrix[i][right]) }
for i := right; i > left; i-- { res = append(res, matrix[bottom][i]) }
for i := bottom; i > top; i-- { res = append(res, matrix[i][left]) }
left++
top++
right--
bottom--
}
if top == bottom { // 剩余一行
for i := left; i <= right; i++ { res = append(res, matrix[top][i]) }
} else if left == right { // 剩余一列
for i := top; i <= bottom; i++ { res = append(res, matrix[i][right]) }
}
return res
}
这个方法是最好理解且最好记忆的,直接根据题意来模拟这样一个过程,一圈一圈的遍历来实现,最后再处理一下剩余的行/列即可。
22. 反转链表 II
题目链接:92. 反转链表 II
题目描述
代码与解题思路
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func reverseBetween(head *ListNode, left int, right int) *ListNode {
phead := &ListNode{Next: head}
prev := phead
for i := 0; i < left-1; i++ { // 找到起始位置
prev = prev.Next
}
cur := prev.Next
for i := 0; i < right-left; i++ { // 反转链表
tmp := cur.Next
cur.Next = tmp.Next
tmp.Next = prev.Next
prev.Next = tmp
}
return phead.Next
}
核心就在链表反转的逻辑,如果忘记了就画图再复习一遍。
23. 相交链表
题目链接:160. 相交链表
题目描述
代码与解题思路
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func getIntersectionNode(headA, headB *ListNode) *ListNode {
curA, curB := headA, headB
for curA != curB {
if curA == nil {
curA = headB
} else {
curA = curA.Next
}
if curB == nil {
curB = headA
} else {
curB = curB.Next
}
}
return curA
}
题解区很多用三目运算符的,没必要,if else 就很清晰了,两个指针每次遍历各走一步,如果为空就从对方的链表从头开始,最后相等只有两种情况:
- 他们在相交的地方相遇了
- 他们都走到各自的 nil 了
24. 合并 K 个升序链表
题目链接:23. 合并 K 个升序链表
题目描述
代码与解题思路
func mergeKLists(lists []*ListNode) *ListNode {
n := len(lists)
if n == 0 {
return nil
}
if n == 1 {
return lists[0]
}
left := mergeKLists(lists[:n/2])
right := mergeKLists(lists[n/2:])
return mergeTwoLists(left, right)
}
func mergeTwoLists(list1, list2 *ListNode) *ListNode {
phead := &ListNode{}
cur := phead
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
cur.Next = list1
list1 = list1.Next
} else {
cur.Next = list2
list2 = list2.Next
}
cur = cur.Next
}
if list1 != nil {
cur.Next = list1
} else {
cur.Next = list2
}
return phead.Next
}
这道题我曾经的做法是暴力全部遍历一遍,塞进数组中,然后用库函数排序,然后再将数组转化成链表的形式,采用的是暴力匹配的方式,之后也可以和面试官聊聊这个思路
而现在新学习到的解法是使用分治的解法,通过递归分治,将链表数组对半拆分,直到拆分成两个子数组后调用合并两个链表的方法(这个方法实现起来也不难)将每一个子链表合并成升序链表,最后组合成一个大的升序链表,非常巧妙的分治算法
多学,多用,多思考。
25. 字符串相加
题目链接:415. 字符串相加
题目描述
代码与解题思路
func addStrings(num1 string, num2 string) (ans string) {
len1, len2 := len(num1)-1, len(num2)-1
carry := 0
for len1 >= 0 || len2 >= 0 {
x, y := 0, 0
if len1 >= 0 {
x = int(num1[len1] - byte('0'))
}
if len2 >= 0 {
y = int(num2[len2] - byte('0'))
}
sum := x+y+carry
ans += strconv.Itoa(sum%10)
carry = sum/10
len1--
len2--
}
if carry != 0 {
ans += "1"
}
return reverse(ans)
}
func reverse(ans string) string {
tmp := []byte(ans)
l, r := 0, len(tmp)-1
for l < r {
tmp[l], tmp[r] = tmp[r], tmp[l]
l++
r--
}
return string(tmp)
}
这道题不难,但是有很多需要注意的地方
- 字符串相加这类题型的思路,从后往前遍历,累加成一个字符串,最后反转
- reverse 函数的编写需要熟练,用 go 的小缺陷,有些方法需要手撕
- strconv.Itoa(int),这个 int 转 string 的方法必须得记住,不然这道题铁定做不出来,go 只能借助调用来 int 转 string
26. 最长递增子序列
题目链接:300. 最长递增子序列
题目描述
代码与解题思路
func lengthOfLIS(nums []int) int {
ans := 1
dp := make([]int, len(nums))
for i, _ := range nums { dp[i]=1 }
for i := 1; i < len(nums); i++ {
for j := 0; j < i; j++ {
if nums[i] > nums[j] { // 只要遇到, 对应的 dp 数组位置的值就+1
dp[i] = max(dp[j]+1, dp[i])
}
}
ans = max(ans, dp[i])
}
return ans
}
能写出动态规划的方法就行了,如果忘了就去看题解区的图解,二分法就不要想了,能做出来就不错了。。。
27. 重排链表
题目链接:143. 重排链表
题目描述
代码与解题思路
第一种方法,用数组存节点,然后通过数组的随机访问性质来重排
func reorderList(head *ListNode) {
tmp := []*ListNode{}
cur := head
for cur != nil {
tmp = append(tmp, cur)
cur = cur.Next
}
i, j := 0, len(tmp)-1
for i < j {
tmp[i].Next = tmp[j]
i++
if i == j {
break
}
tmp[j].Next = tmp[i]
j--
}
tmp[i].Next = nil
}
第二种方法:
- 取中点(快慢指针,获得了中点的指针)
- 反转右半部分链表(这样就能 O(N) 的进行从后往前的遍历了)
- 再通过起点指针和中点指针的配合切换节点位置
func reorderList(head *ListNode) {
// 取中点
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 反转右半链表
var pre *ListNode = nil
var cur = slow
for cur != nil {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
// 完成链表重排
left, right := head, pre
for right.Next != nil {
tmpl := left.Next
tmpr := right.Next
left.Next = right
right.Next = tmpl
left = tmpl
right = tmpr
}
}
到时候如果面试遇到这道题目,可以把第一种思路讲出来,然后再讲第二种思路,最后实现第二种思路,最需要注意的是链表的反转,我突然链表反转老是写不出来,好难受,之后多刷几遍链表反转怎么写,一定要把链表的反转给练熟了
28. 环形链表 II
题目链接:142. 环形链表 II
题目描述
代码与解题思路
func detectCycle(head *ListNode) *ListNode {
slow, fast := head, head
for {
if fast == nil || fast.Next == nil { // 走到尾了,证明没环
return nil
}
slow = slow.Next
fast = fast.Next.Next
if slow == fast { // 相遇了,证明有环
break
}
}
fast = head // 让 fast 回头重新遍历
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return fast
}
这道题很操蛋,是一个数学问题,是需要证明的,我是证明不出来这东西,如果真在面试遇到了,我也就只能背代码了。。没办法,平等的对每一个数学问题白痴。。。
29. 接雨水
题目链接:42. 接雨水
题目描述
代码与解题思路
func trap(height []int) (ans int) {
st := []int{}
for i, v := range height {
for len(st) > 0 && height[st[len(st)-1]] < v {
cur := st[len(st)-1]
st = st[:len(st)-1]
if len(st) == 0 {
break
}
l := st[len(st)-1]
r := i
h := min(height[l], height[r])-height[cur] // 用矮的柱子去减最低位的值
ans += (r-l-1)*h
}
st = append(st, i)
}
return ans
}
经典单调栈题目,多刷几遍就熟练了,非常经典的单调栈思路,栈中的值是不断递减的,如果遇到比栈顶大的值,就证明柱子把这片区域围上了。
30. 删除链表的倒数第 N 个结点
题目链接:19. 删除链表的倒数第 N 个结点
题目描述
代码与解题思路
func removeNthFromEnd(head *ListNode, n int) *ListNode {
phead := &ListNode{Next: head}
slow, fast := phead, head
for n > 0 { // 让 fast 指针先走 n 步
fast = fast.Next
n--
}
for fast != nil { // 同时遍历,这样 slow 指针就会离链表尾 n 个身位
slow = slow.Next
fast = fast.Next
}
slow.Next = slow.Next.Next // 删除节点
return phead.Next
}
非常经典的快慢指针思路,让快指针先走 n 步,快慢指针同时往后走,那慢指针就能停在结尾倒数第 n 个节点上,然后执行删除操作即可。