wang-kai / algorithms Goto Github PK
View Code? Open in Web Editor NEW人活着,就要逾越高墙
人活着,就要逾越高墙
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
贪心算法通过局部最优选择来构造全局最优。
贪心算法的思路是:
针对于该题的上下文,最后再求解下区间总数与选定区间的差即可。
func eraseOverlapIntervals(intervals [][]int) int {
// 剔除长度为 0 的情况
if len(intervals) == 0 {
return 0
}
var resIntervals = [][]int{}
// 按照结束时间排序
startTimes, endTimes := sortIntervals(intervals)
// 排在第一个的,是一定要选定的
resIntervals = append(resIntervals, []int{startTimes[0], endTimes[0]})
currentEndTime := endTimes[0]
for i := 1; i < len(endTimes); i++ {
// 在已排序好的结构中,选择开始时间比当前结束时间大的,作为下一个选定对象
if currentEndTime <= startTimes[i] {
resIntervals = append(resIntervals, []int{startTimes[i], endTimes[i]})
currentEndTime = endTimes[i]
}
}
return len(intervals) - len(resIntervals)
}
func sortIntervals(intervals [][]int) (startTimes []int, endTimes[]int) {
// 这里使用选择排序算法,把 intervals 按照结束时间排序
for i := 1; i < len(intervals); i++ {
key := intervals[i]
var j = i-1
for j >= 0 && key[1] < intervals[j][1] {
intervals[j+1] = intervals[j]
j--
}
intervals[j+1] = key
}
for i := 0; i < len(intervals); i++ {
startTimes = append(startTimes, intervals[i][0])
endTimes = append(endTimes, intervals[i][1])
}
return
}
链表反转题目,大致就是在一个单链表上,全部或局部的反转链表。解题代码很简洁,但理解起来有点困难。
输入一个链表,反转链表后,输出新链表的表头。
输入
{1,2,3}
返回值
{3,2,1}
类似于这样的题目,其实是技巧大于思路。我也是从解题代码中反复看,才理解了代码的思路。
func ReverseList( pHead *ListNode ) *ListNode {
// 排除极端情况
// 当然,这也是递归调用的终止条件
if pHead == nil || pHead.Next == nil {
return pHead
}
// 递归拿到最后一个元素
newHead := ReverseList(pHead.Next)
// 执行反转,让其下一个元素,倒指向其本身
// 此时的 pHead 已经不是原来的 pHead 了,而是倒数第二个元素、倒数第三个元素 ...
pHead.Next.Next = pHead
// 置空本身的 next 属性(会有下一个元素帮其重新指向正确的元素)
pHead.Next = nil
// 新链表的表头在最后一次递归中获取到
// 该变量一直没变,层层往上抛
return newHead
}
反转链表前 N 个节点
在之前反转整条链表的思路上,控制两个变量:
// 没用说使用了递归方法就不能使用全局变量
// 能帮助理清思路
var lastNode *ListNode
func reverseKList(head *ListNode, k int) *ListNode {
// 当 k == 1 的时候,说明递归深度已经到头了
if k <= 1 {
lastNode = head.Next
return head
}
// 拿到第 K 个节点
KthNode := reverseKList(head.Next, k-1)
// 反转链表
head.Next.Next = head
// 设置每轮反转后,元素的默认 next 值
// 如果它不是最初的 head 的话,会有下一轮来修改它的 next
// 如果是最初的 head,则它指向 k+1 元素
head.Next = lastNode
// 返回第 K 个节点,因为它将作为新的 head
return KthNode
}
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度。
这道题要基于上一步的基础上求解。
func reverseBetween(head *ListNode, m int, n int) *ListNode {
// 直到 m == 1,就开始按照反转前 n 个的思路操作
if m == 1 {
reverseKList(head, m-n)
}
// 如果匹配不到 m==1,保持原有序列不变
head.Next = reverseBetween(head.Next, m-1, n-1)
// 返回最初的 head
return head
}
var lastNode *ListNode
func reverseKList(head *ListNode, k int) *ListNode {
if k <= 0 {
lastNode = head.Next
return head
}
KthNode := reverseKList(head.Next, k-1)
head.Next.Next = head
head.Next = lastNode
return KthNode
}
一颗二叉搜索树是以二叉树来组织的,其中每个节点就是一个对象,除了 key
和卫星数据之外,每个节点还包含属性 left
、right
、p
,分别指向左孩子、右孩子和双亲。其满足如下性质:
对于二叉搜索树,任何结点 x,其左子树中关键字最大不超过 x.key (x.L.key <= x.key),其右子树中关键字最小不能低于 x.key (x.R.key >= x.key)。
二叉搜索树的相关操作执行效率如下:
定义数据结构
type Node struct {
Val int
Parent *Node
Left *Node
Right *Node
}
type BST struct {
Root *Node
}
删除二叉搜索树节点,其实考验的是删除一个节点后,如何保证二叉搜索树的性质不变。删除节点的思考点如下:
因为 leet-code 上给出的节点没有 parent
指针,所以解题中使用了递归,为什么在没有前驱指针的时候递归可以实现响应操作?
root.Left =
、root.Right =
就是做的这个事情func deleteNode(root *TreeNode, key int) *TreeNode {
// 排除特殊条件
if root == nil {
return nil
}
if root.Val == key {
// 已找到,开始删除
if root.Left == nil {
// 没有左孩子,右孩子替上
return root.Right
}
if root.Right == nil {
// 没有右孩子,左孩子替上
return root.Left
}
// 找到后驱节点
min := root.Right
for min != nil && min.Left != nil {
min = min.Left
}
// 用后驱节点的值修改本节点
root.Val = min.Val
// 删除后驱节点
root.Right = deleteNode(root.Right, min.Val)
}else if root.Val > key {
// 比节点值小,转向左子树查找
root.Left = deleteNode(root.Left, key)
}else if root.Val < key {
// 比节点值大,转向右子树查找
root.Right = deleteNode(root.Right, key)
}
return root
}
添加节点主要分为两步:
p = root
是有深意的,当 for
循环条件破裂后,p 真正的记录到了要插入的新元素的父节点if p == nil return newNode
这里其实是间接判断了 root == nil
,但判断 p 可以与下面形成一整串 if else
更规整func insertIntoBST(root *TreeNode, val int) *TreeNode {
// 记录原始的根节点
originHead := root
var newNode = &TreeNode{Val: val}
// 找到要插入的位置
var p *TreeNode
for root != nil {
p = root
if root.Val > val {
root = root.Left
}else {
root = root.Right
}
}
if p == nil {
return newNode
}else if p.Val > val {
p.Left = newNode
}else{
p.Right = newNode
}
return originHead
}
根据二叉搜索树固有的性质,查找特定节点相对还是很简单的:
root.Val == val
递归解法
func searchBST(root *Node, val int) *Node {
if root == nil || root.Val == val {
return root
}
if root.Val > val {
return searchBST(root.Left, val)
}else {
return searchBST(root.Right, val)
}
}
迭代解法(对于大多数计算机,迭代版本的效率要高很多)
func searchBST(root *TreeNode, val int) *TreeNode {
for root != nil && root.Val != val {
if root.Val > val {
root = root.Left
}else {
root = root.Right
}
}
return root
}
出于二叉搜索树的性质,可以在无需任何比较的情况下找到最大值和最小值。
此次是做题配合《算法导论》看书在推进,不得不说书上有很多经典的技巧
func (b *BST) Minimum() *Node {
var n = b.Root
for n != nil && n.Left != nil {
n = n.Left
}
return n
}
func (b *BST) Maximun() *Node {
var n = b.Root
for n != nil && n.Right != nil {
n = n.Right
}
return n
}
拿到一道算法题的时候,不要上来就直接看最优解,那些粗鲁的 “笨方法” 中也有一些思维和代码技巧在里面。
求数组的子数组之和的最大值,数组中每个元素是:正整数、负整数 或 0
比如
[-2, 5, 3, -6, 4, -8, 6]
的结果是 8 ([5, 3])
最优解法是动态规划,如果临场想不出来,暴力循环做出来也是一种能力。
依次遍历每个元素,这是第一个游标。然后从每个当前元素依次向后遍历,这是第二个游标。然后计算两个游标区间内的数组和。
设定一个全局变量,每次比对所计算出来的数组和与全局变量,如果更大,就替换掉全局变量。
package main
import "fmt"
func main() {
var arr = []int{-2, 1}
result := maxSum(arr)
fmt.Println(result)
}
func maxSum(arr []int) int {
var sum = arr[0]
// 遍历每个元素
for i := 0; i < len(arr); i++ {
// 从第一层元素(包括它自己,因为可以是一个元素的数组),向后遍历
for j := i; j < len(arr); j++ {
// 求 arr[i, j] 子数组元素的和
var k = i + 1
var tmpSum = arr[i]
for k <= j {
tmpSum += arr[k]
k++
}
if tmpSum > sum {
sum = tmpSum
}
}
}
return sum
}
求两个字符串的公共子串
例如:A
abc123
, Bbc1234
=>bc1
这道题也是有动态规划解法的,这里我们使用暴力求解法训练下思维和代码一致性。
for
关键字做有判断条件的遍历)package main
import "fmt"
func main() {
var a string = "abc123"
var b string = "bc1234"
res := longestCommonStr(a, b)
fmt.Println(res)
}
func longestCommonStr(a, b string) string {
var maxLen int
var from, to int
// 遍历 a 的每一个字符
for i := 0; i < len(a); i++ {
// 遍历 b 的每一个字符
for j := 0; j < len(b); j++ {
if a[i] == b[j] {
fmt.Printf("Match at %c\n", a[i])
var matchLen int = 1
// 把两个变量重新赋值(不要影响到两层循环遍历大局),开启 “携手并进”
var p, q = i, j
// 带判断条件的 for 循环
for p+1 < len(a) && q+1 < len(b) && a[p+1] == b[q+1] {
matchLen++
p++
q++
}
// 记录下每次符合条件的场景
if matchLen > maxLen {
maxLen = matchLen
from = i
to = p
}
fmt.Printf("From %d\tto %d\n", from, to)
}
}
}
return string([]byte(a)[from : to+1])
}
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层序遍历结果:
[
[3],
[9,20],
[15,7]
]
这里主要提到一点,也是卡主我的一点:当一个操作需要递归,而这些递归过程要共享(并且都操作)某个对象的时候,就需要在参数上加指针,该指针指向要共享的对象。
在 levelOrder()
中定义的 tmp Int Slice,然后我们通过 kLevel
函数中的 *[]int
指针把 tmp Slice 的地址传给每一个递归的函数。
每当有 append()
的操作时,都有可能产生新的 Int Slice,因为我们最后加到 resutl 中的是最原始的 tmp 引用的地址。这就需要我们在迭代的过程中去不断的修改 tmp 变量所保存的地址。
# 声明一个 Slice,tmp 的值为 []int 的地址
tmp := []int{}
# 这里相当于先定义了一个指针 *[]int,其值存的是 tmp 的值,也就是最初 []int 的地址
# 然后把指针传给了调用函数
kLevel(root, &tmp, k)
# *items 可以拿到 tmp 的值,然后对其做 append 操作,该操作可能返回一个新的 []int 地址
# 我们修改 *items, 也就是把新的地址再赋予到指针的 value,这个 value 也就是 tmp 所承载的地址
*items = append(*items, root.Val)
# 最后我们把 tmp 指向的 []int 装入到结果数组中
result = append(result, tmp)
for
我们从 0 层开始向下层序遍历是,我们不知道有几层,但我们知道遍历的结束条件,这时可以空缺掉 for
循环的条件项,把条件项通过 break
语句写在 for
函数内
for i := 0; ; i++ {
if (xxx) {
break
}
}
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func levelOrder(root *TreeNode) [][]int {
// 构建结果数组,如果二叉树为空,则直接返回结果
var result = [][]int{}
if root == nil {
return result
}
for k := 0; ; k++ {
// 把第 k 层的元素撞到 tmp slice 中
var tmp = []int{}
kLevel(root, &tmp, k)
// 当这一次元素数量为空,那就停止遍历,结束
if len(tmp) == 0 {
break
}
// 如果不为空,把这一层元素加到结果数组中
result = append(result, tmp)
}
return result
}
// 把以 root 为根的二叉树的第 k 层装到 items 里面
func kLevel(root *TreeNode, items *[]int, k int){
if root == nil {
return
}
if k == 0 {
// 开始装元素
*items = append(*items, root.Val)
} else {
// 不属于这一层,就往下递归
k--
kLevel(root.Left, items, k)
kLevel(root.Right, items, k)
}
}
蚂蚁金服第二轮,出了个我做过的类似题目,但我的第一反应是:这道题比较难,我做过类似的,但这道题我不会。于是心里就开始想着把这道题如何哼哼唧唧过去。其实现在回头再做这道题,我凭自己的能力也可以做个七七八八。
有时候就是有太多的思维惯式和不自信,要打破它。还是高中数学老师说的,为什么做过的题还会错?是因为错的太少。
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
例如 [2,1,6,4], 交换后输出应为 [1,2,4,6]
之前做过依次反转一个链表,现在是两两反转,其实技法差不多,都是基于递归:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func swapPairs(head *ListNode) *ListNode {
// 链表个数为偶数个,最后一个为 nil
// 或者为奇数个的时候,此时已是最后一个,node.Next == nil
// 此种场景不做操作
if head == nil || head.Next == nil {
return head
}
newHead := head.Next
// 指向下一轮的头结点
head.Next = swapPairs(newHead.Next)
// 反转相邻结点
newHead.Next = head
// 每次返回链表头结点,为下一轮递归操作做准备
return newHead
}
给定一个数组,里面有一个数字的个数超过了一半,把这个数找出来。
求众数(超过一半)有专门的算法叫摩尔投票。
摩尔投票算法是基于这个事实:每次从序列里选择两个不相同的数字删除掉(或称为“抵消”),最后剩下一个数字或几个相同的数字,就是出现次数大于总数一半的那个。
算法本质上就是 “对拼消耗”,设定一个计数变量,记录下每个数字的 “生命值”,谁的人数最多,谁活到最后,时间复杂度 O(n)。
package main
func main() {
var arr = []int{2, 3, 13, 2, 2, 45, 2, 2}
result := marjorElement(arr)
println(result)
}
func marjorElement(arr []int) int {
var temp = arr[0]
var mount = 1
for i := 1; i < len(arr); i++ {
if temp == arr[i] {
mount++
} else {
mount--
}
if mount <= 0 {
temp = arr[i]
mount = 1
}
}
return temp
}
如果没有元素超过一半,则返回 -1
func majorityElement(nums []int) int {
tmp := nums[0]
var amount = 1
for i := 1; i < len(nums); i++ {
if tmp == nums[i] {
amount++
}else{
amount--
}
if amount == 0 {
tmp = nums[i]
amount = 1
}
}
var elAmount = 0
for i := 0; i < len(nums); i++ {
if nums[i] == tmp {
elAmount++
}
if elAmount > len(nums) / 2 {
return tmp
}
}
return -1
}
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
确定思路前要通篇考虑一些极端情况:
那么我们如何找到 “直径线路”?“直径线路” 所经过的根节点,该根节点左子树方向的深度 + 右子树方向的深度,一定是最大的。所以我们就遍历每颗子树的左右子树深度之和即可。
我们不能直接知道一个节点的深度,但我们可以根据 “后续遍历” 知道左右节点的高度后,向上累加,最后知道节点深度。
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func diameterOfBinaryTree(root *TreeNode) int {
var max int
var depth func(*TreeNode) int
depth = func(n *TreeNode) int {
// 空节点的深度一定为 0
if n == nil {
return 0
}
ld := depth(n.Left)
rd := depth(n.Right)
// 直径就是左右子树的深度之和
// 辗转求出最大值
distance := ld + rd
if distance > max {
max = distance
}
// 子树的深度,一定是左右子树最大的那个基础上再加 1
if ld > rd {
return ld + 1
}
return rd + 1
}
depth(root)
return max
}
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:
“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
提示:
树中节点数目在范围 [2, 10^5] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
解对一道题,最重要的是什么?答曰:思路,对于一道题获取正确思路最重要的是什么?答曰:审题。获取的信息量越多,越对解题有益。
编程题不像普通的数学题,当你开始实现思路的时候,就开始陷于代码实现。如果思路不对,在规定的时间内基本上不可能有重做的机会了。
“通过一层一层向上找父节点,然后比较” 这种思路是行不通的。审好示例二,这两个节点之间,可能 A 本身 就是 B 的父节点,最近公共祖先可以为节点本身。
二叉树的题,多数都是要通过递归来解决。递归三要素:
二话不说,先写递归框架,看着框架容易有思路
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
lowestCommonAncestor(root.Left, p, q)
lowestCommonAncestor(root.Right, p, q)
}
p, q 一定是不变的,因为找的就是它俩。变的就是不断的去递归左、右节点。两个节点的最近公共祖先,那这两个节点一定是某个节点的左右节点,或者一个在节点的左子树中,一个在节点的右子树中。所以,求两个节点在一颗树上的最近公共祖先问题,可以转化为:求两个节点在左右子树的公共祖先问题,如果左右子树都没找到公共祖先,那公共祖先就是根节点。
上面的分析就解决了 1、2 要素。那么递归的终结条件是什么?
这里要理清楚,我的终止条件是判断节点,还是判断节点的左右子节点?,也就是说要判断直接的后续条件,还是判断当前元素本身。回答这个问题的依据是,直接判断当前元素是否可以直接解决此规模中的子问题,并且产生的结果能为更大规模的问题提供判断依据?
代码逻辑:
如何拼装递归出来的子问题来还原原问题的解?
/**
* Definition for TreeNode.
* type TreeNode struct {
* Val int
* Left *ListNode
* Right *ListNode
* }
*/
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
// 递归结束条件
if root == nil {
return nil
}
if p == root || q == root {
return root
}
// 求解子问题
lRes := lowestCommonAncestor(root.Left, p, q)
rRes := lowestCommonAncestor(root.Right, p, q)
// 用子问题来还原原问题的解
if lRes == nil && rRes == nil {
return nil
}
if lRes != nil && rRes != nil {
return root
}
if lRes == nil {
return rRes
}
return lRes
}
很多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归的调用其自身以解决紧密相关的若干子问题。这些算法典型的遵循 分治**:将原问题分解为几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后合并这些子问题的解来建立原问题的解。
分治模式在每层递归时都有 3 个步骤:
这里练习两道题,深入理解下 “分治” **的精髓。
归并排序的思路大致如下:
/*
归并排序思路:
1. 把原数组递归拆分成仅有一个元素的小数组
2. 不断两两合并
*/
func mergeSort(nums []int, p, r int) {
if r > p {
q := (p + r) / 2
// 把原数组拆分为 [p, q] & [q+1, r]
mergeSort(nums, p, q)
mergeSort(nums, q+1, r)
merge(nums, p, q, r)
}
}
/*
merge 操作
1. 把数组 [p, q] [q+1, r] 合并成一个有序数组
*/
func merge(nums []int, p, q, r int) {
// 声明左右两个数组,分别存放 [p, q] & [q+1, r]
// 长度比需要的长度加一,因为要存入哨兵元素
var leftArr = make([]int, q-p+1+1)
var rightArr = make([]int, r-q+1)
// 填充两个数组
for i := 0; i < len(leftArr)-1; i++ {
leftArr[i] = nums[i+p]
}
leftArr[q-p+1] = math.MaxInt64
for i := 0; i < len(rightArr)-1; i++ {
rightArr[i] = nums[i+1+q]
}
rightArr[r-q] = math.MaxInt64
// 有序合并两个数组,并放入到 nums 数组的指定序列内
var a, b int
for i := p; i <= r; i++ {
if leftArr[a] < rightArr[b] {
nums[i] = leftArr[a]
a++
} else {
nums[i] = rightArr[b]
b++
}
}
}
“最大子数组问题” 是由另外一个问题引申过来的间接问题,直接求解引申问题是重要的,理解原问题是如何引申过来的也是十分重要的,因为这是第一步。
如图是一张股票的波动图,我们要求出如何 “低买高卖” 才能获取最大收益?图下方有一列 “变化” 的统计,原问题可以转化为:寻找 “变化” 数组中和最大的非空连续子数组。
使用分治思路来求解这个问题:
func findMaxSubArray(nums []int, low, high int) (from, to, sum int) {
// base case
if low == high {
return low, high, nums[low]
}
mid := (low + high) / 2
lLow, lHigh, lSum := findMaxSubArray(nums, low, mid)
rLow, rHigh, rSum := findMaxSubArray(nums, mid+1, high)
cLow, cHigh, cSum := findCrossMaxArray(nums, low, mid, high)
if lSum >= rSum && lSum >= cSum {
return lLow, lHigh, lSum
}
if rSum >= lSum && rSum >= cSum {
return rLow, rHigh, rSum
}
return cLow, cHigh, cSum
}
func findCrossMaxArray(nums []int, low, mid, high int) (l, r, sum int) {
var lSum int = math.MinInt64
l = mid
// mid 元素一定要包含在内
for i := mid; i >= low; i-- {
// 这里的 sum 是每次都要加新元素的,因为要是连续子数组
sum += nums[i]
if sum > lSum {
lSum = sum
l = i
}
}
var rSum int = math.MinInt64
sum = 0
for i := mid + 1; i <= high; i++ {
sum += nums[i]
if sum > rSum {
rSum = sum
r = i
}
}
return l, r, lSum + rSum
}
动态规划(dynamic programming
)应用于子问题重叠的情况,即不同的子问题具有公共的子子问题,动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中。从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。
动态规划方法通通常用来求解最优化问题,这类问题可以有很多可能解,每个解都有一个值,我们希望寻找具有最优值(最大值或最小值)的解。
步骤 1~3 是动态规划算法求解的基础。如果我们仅仅需要一个最优解的值,而非解本身,可以忽略第 4 步。如果确实要做第四步,就需要在第三步的过程中维护一些额外信息,以便来构造一个最优解。
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
func maxSubArray(nums []int) int {
// 数组长度为 0 时直接返回
if len(nums) == 0 {
return 0
}
// 因为是连续的,所以要有一个 endWithArr[i] 表示以 arr[i] 结尾的最大子数组和
// 因为已特定数结尾并不一定最大,可能是中间的一段,还需要有一个全局的 all[i] 表示 arr[0:i] 最大子数组和
var endWith = make([]int, len(nums))
var all = make([]int, len(nums))
endWith[0] = nums[0]
all[0] = nums[0]
// 依次找出每个子问题(子数组)的最优解
// 从 nums[0:i] 0<i<len
for i := 1; i < len(nums); i++ {
endWith[i] = max(nums[i], nums[i]+endWith[i-1])
all[i] = max(endWith[i], all[i-1])
}
return all[len(nums)-1]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
func lengthOfLIS(nums []int) int {
// 排除数组长度为 0 & 1 的情况
if len(nums) == 0 {
return 0
}
if len(nums) == 1 {
return 1
}
// 最大长度,辗转比较,得出最大值
var maxLen int = 1
// 最长递增子序列数据记录, LIS[i] 表示截止到 i 元素,最长自增子序列的长度
var LIS = make([]int, len(nums))
// 初始化第 0 个元素
LIS[0] = 1
// 从第 1 个元素开始
for i := 1; i < len(nums); i++ {
// 和 i 之前的每个数都比较一下,如果比它大,就考虑加入它的 LIS
LIS[i] = 1
for j := 0; j < i; j++ {
// 比该数字大,并且加入后 LIS 比以前更长,就加入进入
if nums[i] > nums[j] && LIS[j]+1 > LIS[i] {
LIS[i] = LIS[j] + 1
}
}
// 辗转比较出最大值
if LIS[i] > maxLen {
maxLen = LIS[i]
}
}
return maxLen
}
数组中有若干个无序的数,选出其中最大的 K 个数。
首先想到的思路就是排序,排序之后取出最大的前 K 个数。排序的时间复杂度是 n*lgn
。
但是其实我们只要最大的 K 个数字,并不需要整体都是有序的。
我们可以通过 K * N 次遍历和比较,来把前 K 大的数字找出来。具体使用哪种方法就需要根据 K & N 的大小来确定使用 K * N
复杂度,还是 n * lgn
。
如上的办法还是不够最优,二分法是常用的可以改进效率的办法。如果我们把数组分成两部分 setA、setB,setA 集合内的数字全大于 SetB,然后根据 K 的大小决定是否继续拆分。这样子,理论上我们只需要 O(n * log2k) 复杂度。
package main
import (
"fmt"
)
func main() {
var arr = []int{12, 3, 1, 2, 3, 12, 31, 4, 34}
result := kbig(arr, 2)
fmt.Println(result)
}
func kbig(arr []int, k int) []int {
// 此时说明 arr 数组被弃用,不在结果之列
if k <= 0 {
return []int{}
}
// arr 数组被全部采用,不用再做计算了
if len(arr) <= k {
return arr
}
setA, setB := partition(arr)
fmt.Println(setA, setB)
return append(kbig(setA, k), kbig(setB, k-len(setA))...)
}
func partition(arr []int) (a, b []int) {
var key = arr[len(arr)-1]
for i := 0; i < len(arr)-1; i++ {
if arr[i] > key {
a = append(a, arr[i])
} else {
b = append(b, arr[i])
}
}
/*
二分法如果分组失败,就会陷入无限递归分组,需要避免这种情形
例如对 [1, 2, 3, 99] 做拆分,如果不把 99 放入到长度较小的子数组中,则分组将无限循环进行下去
*/
if len(a) < len(b) {
a = append(a, key)
} else {
b = append(b, key)
}
return
}
从输出结果上看,partition 总共做了 5 次,略高于 log2k
[34] [12 3 1 2 3 12 31 4]
[12 12 31 4] [3 1 2 3]
[12 12 31] [4]
[31] [12 12]
[34 31]
整理这个文章的本意是 “找链表倒数第 n 个节点” 这个思路我竟然需要画图辅助思索了。先上两道「简单」题:
这两道题基本一样,无非是返回节点的值,还是直接返回节点,解题代码如下:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func kthToLast(head *ListNode, k int) int {
var slowP = head
for k > 1 && head.Next != nil {
head = head.Next
k--
}
for head.Next != nil {
head = head.Next
slowP = slowP.Next
}
return slowP.Val
}
这里有两个注意点:
k > 1
?根据 “快慢指针” 的套路,快指针最终会移动到尾节点(也就是倒数第 1 个节点),如果要找倒数第 2 个节点,那么它仅需比慢指针先走 1 步即可。所以这里让它先走 k-1
步。
var slowP = head
的使用疑虑go 语言的指针 & 引用是有点搞,var slowP = head
背后是这样的:
*ListNode
变量slowP
变量中这里对 slowP
的改动完全不会影响到 head,反之同理。预热完成之后,开始上题目。
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
next
指针。next
指针指向下下个节点。这样方式的场景是,该节点不是最后一个节点。next
指针指向 head 节点,这样有几个方便之处:
假使要删除倒数第 n 个节点,那么快指针仅需先跑 n-1 步即可,因为慢指针已经在第一个节点上了。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {
var dummyNode = &ListNode{Next: head}
var slowP = dummyNode
for n > 1 {
head = head.Next
n--
}
// for 循环后,slowP 的下一个节点就是倒数第 n 个节点
for head != nil && head.Next != nil{
head = head.Next
slowP = slowP.Next
}
// delete nth node
slowP.Next = slowP.Next.Next
return dummyNode.Next
}
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
原则上来讲前序、后序、中序出的列表是无法恢复成二叉树的,因为没有记录空值。之前做过题目 重建二叉树 也是基于两个条件:
才能完成。
这里要恢复二叉树,就需要我们把空指针也记录下来(以前序为例)。
type Codec struct {
}
func Constructor() Codec {
return Codec{}
}
// Serializes a tree to a single string.
func (this *Codec) serialize(root *TreeNode) string {
if root == nil {
return "#"
}
lStr := this.serialize(root.Left)
rStr := this.serialize(root.Right)
// 返回前序遍历字符串
return fmt.Sprintf("%d,%s,%s", root.Val, lStr, rStr)
}
// Deserializes your encoded data to tree.
func (this *Codec) deserialize(data string) *TreeNode {
valStrArr := strings.Split(data, ",")
return doDeserialize(&valStrArr)
}
func doDeserialize(valsArr *[]string) *TreeNode {
// 第一个元素即为根元素
vals := *valsArr
val := vals[0]
if val == "#" {
newArr := vals[1:]
*valsArr = newArr
return nil
}
valInt, _ := strconv.Atoi(val)
node := &TreeNode{Val: valInt}
// 去除已经被使用的元素
newArr := vals[1:]
*valsArr = newArr
// 前一部分属于左子树
node.Left = doDeserialize(valsArr)
// 后一部分属于右子树
node.Right = doDeserialize(valsArr)
return node
}
给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。
两棵树重复是指它们具有相同的结构以及相同的结点值。
示例 1:
1
/ \
2 3
/ / \
4 2 4
/
4
下面是两个重复的子树:
2
/
4
和
4
找重复子树,那就要回答两个问题:
此题目的主要思路是:
0
\
0
&
0
/
0
如果使用 [左根右] 的话,序列化结果是一样的。
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func findDuplicateSubtrees(root *TreeNode) []*TreeNode {
var res = []*TreeNode{}
if root == nil {
return res
}
var memo = make(map[string]int)
recordSubtree(root, memo, &res)
return res
}
func recordSubtree(n *TreeNode, memo map[string]int, res *[]*TreeNode) string{
if n == nil {
return "#"
}
lStr := recordSubtree(n.Left, memo, res)
rStr := recordSubtree(n.Right, memo, res)
treeStr := fmt.Sprintf("%s,%s,%d", lStr, rStr, n.Val)
if _, ok := memo[treeStr]; !ok {
memo[treeStr] = 1
}else {
if memo[treeStr] == 1 {
*res = append(*res, n)
memo[treeStr]++
}
}
return treeStr
}
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
在另一篇文章中有提到过这道题目的解法,当时用的是有序队列,这次解题我们使用最大堆。一道题做出来就做出来了嘛,干嘛要找另一种方法?思考的执念吧。
说来 “最大堆” 方法也很简单,就是使用最大堆构建一个优先队列,每次从队列中取出最大值和插入新的值,和这个题目很贴切(哎,所谓的面试算法考试,都是考从没学过算法,或者临时抱佛脚的人的,真正的翻翻算法导论,做做课后题,那些都是小儿科)。
具体步骤:
windowArr
变量来存放当前窗口内的元素索引,只存索引,涉及到元素比较,就通过索引去找具体的值func maxHeapify(originData, indexArr []int, i int) {
var largest = i
l, r := left(i), right(i)
heapsize := len(indexArr)
if l < heapsize && originData[indexArr[l]] > originData[indexArr[largest]] {
largest = l
}
if r < heapsize && originData[indexArr[r]] > originData[indexArr[largest]] {
largest = r
}
if largest != i {
indexArr[i], indexArr[largest] = indexArr[largest], indexArr[i]
maxHeapify(originData, indexArr, largest)
}
}
func buildMaxHeap(originData, indexArr []int) {
for i := len(indexArr)/2 - 1; i >= 0; i-- {
maxHeapify(originData, indexArr, i)
}
}
func maxSlidingWindow(nums []int, k int) []int {
var res = []int{}
// 填入窗口初始值
var windowArr = []int{}
for i := 0; i < k; i++ {
windowArr = append(windowArr, i)
}
// 构建最大堆
buildMaxHeap(nums, windowArr)
// 放入第一个元素
res = append(res, nums[windowArr[0]])
for i := k; i < len(nums); i++ {
// 向堆内插入新元素
windowArr = append(windowArr, i)
elIndex := len(windowArr) - 1
// 如果比其父节点值大,那就替换其父节点,保持最大堆性质
for elIndex >= 0 && nums[windowArr[parent(elIndex)]] < nums[windowArr[elIndex]] {
windowArr[parent(elIndex)], windowArr[elIndex] = windowArr[elIndex], windowArr[parent(elIndex)]
elIndex = parent(elIndex)
}
// 从第 0 个元素开始,剔除不在窗口内的元素,直到最大值属于当前窗口
for windowArr[0] <= i-k {
windowArr[0], windowArr[len(windowArr)-1] = windowArr[len(windowArr)-1], windowArr[0]
windowArr = windowArr[0 : len(windowArr)-1]
// maxHeapfiy 保持最大堆的性质
maxHeapify(nums, windowArr, 0)
}
res = append(res, nums[windowArr[0]])
}
return res
}
func parent(i int) int {
return (i - 1) / 2
}
func left(i int) int {
return (i+1)*2 - 1
}
func right(i int) int {
return (i + 1) * 2
}
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
其实,把单个点各个击破之后,剩下的就是技巧点的结合了。这道题思路不难,需要的是代码准确,不要因为一个点卡主(毕竟做题是有时间限制的)。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func isPalindrome(head *ListNode) bool {
// 处理极端空链表情况
if head == nil {
return true
}
// 记录下 head 指针
var firstNode = head
// 快慢指针找中点
var fast, slow *ListNode = head, head
for fast.Next != nil {
if fast.Next.Next != nil {
fast = fast.Next.Next
slow = slow.Next
} else {
// 链表长度为偶数
break
}
}
// 慢指针之后开始反转
tailNode := reverse(slow.Next)
// 比对是否是回文
var allSame bool = true
for firstNode != nil && tailNode != nil {
if firstNode.Val != tailNode.Val {
allSame = false
break
}
firstNode = firstNode.Next
tailNode = tailNode.Next
}
return allSame
}
func reverse(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
newHead := reverse(head.Next)
head.Next.Next = head
head.Next = nil
return newHead
}
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
1. 节点的左子树只包含小于当前节点的数。
2. 节点的右子树只包含大于当前节点的数。
3. 所有左子树和右子树自身必须也是二叉搜索树。
判断二叉搜索树的合法性,其实是有一个坑的,那就是:不能仅满足 Left < root && Right > root。二叉搜索树的性质是:
这就要求这个合法性的校验,其实是带有上下文的,是一个区段,而不是单纯的和 root 比较大小。
这个上下文通过走 Left 还是 Right 路径向下透传
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isValidBST(root *TreeNode) bool {
return isValidWithContext(root, nil, nil)
}
func isValidWithContext(root, min, max *TreeNode) bool {
if root == nil {
return true
}
if min != nil && root.Val <= min.Val{
return false
}
if max != nil && root.Val >= max.Val{
return false
}
return isValidWithContext(root.Left, min, root) && isValidWithContext(root.Right, root, max)
}
写一个程序,求两个正整数的最大公约数(Greatest Common Divisor, GCD)。
解法来自欧几里得《几何原本》中的辗转相除法,假使要求 x, y 的最大公约数,则设 d 是 x, y 的公约数,r 是 x, y 的余数,则有:
r = x - yk (k 为正整数)
package main
func main() {
maxDivisor := gcd(10000004, 400000)
println(maxDivisor)
}
// 求两个数的最大公约数
func gcd(a, b int) int {
a, b = order(a, b)
if a%b == 0 {
return b
}
var r = a % b
return gcd(b, r)
}
func order(a, b int) (int, int) {
if a > b {
return a, b
}
return b, a
}
数组是最简单的一种数据结构。我们经常碰到的一个基本问题,就是寻找整个数组中最大的数,或者最小的数。我们都会扫描一遍数组,把最大(最小)的数找出来。如果同时找出最大 & 最小的数呢?
对于一个由 N 个整数组成的数组,需要对比多少次才能把最大 & 最小的数找出来呢?
排序需要 n * lgn
复杂度,这里仅找出最大 & 最小值,排序不是一个最优办法。遍历数组,分别比较找出最大 & 最小值,要做 2n
次比较。
这里,我们把两个两个划成一组,第一组做比较后确定 max
、min
的值,接下来每一组的两个值先自己比较,然后相对大的和 max
比较,相对小的和 min
比较,这样比较次数为 1.5n
次。
package main
/*
寻找数组中的最大值和最小值
*/
var arr = []int{90, 23, 4, 23, 5, 4, 234, 6, 65, 47, 89, 89, 6, 56, 0, 8}
func main() {
max, min := findMaxAndMin(arr)
println(max, min)
}
func findMaxAndMin(arr []int) (max, min int) {
// empty input
if len(arr) == 0 {
return 0, 0
}
// only has one element
if len(arr) <= 1 {
return arr[0], arr[0]
}
max, min = compare(arr[0], arr[1])
for i := 2; i < len(arr); i += 2 {
if i+1 < len(arr) {
relativeMax, relativeMin := compare(arr[i], arr[i+1])
max, _ = compare(max, relativeMax)
_, min = compare(min, relativeMin)
} else {
lastEl := arr[i]
max, _ = compare(max, lastEl)
_, min = compare(min, lastEl)
}
}
return
}
func compare(a, b int) (max, min int) {
if a > b {
return a, b
} else {
return b, a
}
}
所谓的滑动窗口算法,通常是用来解决字符串、一维数组相关问题。本质上就是定义两个游标,一左一右,根据条件向右移动,左右游标之间的那一段,我们形象的称之为 “窗口”,整个过程好像推着一个窗户往右走。
“滑动窗口算法” 通常要解决的问题都有好多解,只不过要找的是最优解。在滑动窗口的整个算法过程中,右指针负责 “张”,左指针负责 “收”,以是否不满足条件为左指针的移动条件,右指针会直接干到尽头。确实以最低成本穷举出了所有可能解。
在整个求解过程中,我们辗转比较出最优解。
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: s = ""
输出: 0
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 {
return 0
}
var maxLen int = 1
// 定义左、右两个指针
var left, right int
// 记录当前窗口内的内容
var window = make(map[byte]int)
window[s[0]] = 1
for right < len(s)-1 {
// 右游标一直走到尽头
right++
window[s[right]]++
// 不再满足条件
for window[s[right]] > 1 {
// 左游标向右移动
window[s[left]]--
left++
}
// 辗转求出符合条件的最大长度
if right - left + 1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
这篇文章总结一下如何借助二维数组来求解公共子序列问题。公共子序列问题(LCS),无论是连续的公共部分,还是不连续的公共部分,都是基于二维数组来求解。
类似的问题可以引申为:
公共子序列(不保证连续)
求两个字符串的 LCS 长度
str1 = "Java2blog", str2 = "CoreJava" , 输出: 4
公共子串(一定要是连续的)
String 1: Java2blog
String 2: CoreJava
Longest common subString is: Java
两个题的思路大致都是一样的,首先要把两个字符串标记在二维数组中,这个数组一般称之为 dp (dynamic programming) 数组。dp[i][j] 就表示 str1[1 ... i] & str2[1 ... j] 的公共子串 OR 子序列长度。
· | 0 | J | a | v | a | 2 | b | l | o | g |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
C | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
o | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
r | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
e | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
J | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
v | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
这里以 “Java2blog” & “CoreJava” 做举例
求 公共子串 的方法是找到二维数组中的最长的对角线
求 公共子序列 就是求从横轴 OR 纵轴标记为 1 的格子个数。这个场景下我们通过另一种思路会更好理解:
func longestCommonSubStr(a, b string) string {
var maxLen int
// 初始化 dp 二维数组
// 确定了二维数组的长度和每个元素的长度,基础值为 0
var dp = make([][]int, len(a)+1)
for i := range dp {
dp[i] = make([]int, len(b)+1)
}
var endAt int
// 遍历 a 的每一个字符(纵坐标)
for i := 0; i < len(a); i++ {
// 遍历 b 的每一个字符(横坐标)
for j := 0; j < len(b); j++ {
if a[i] == b[j] {
// 公共子串在二维数组上的表现为连续的并且
// 斜率为 1 的对角线
dp[i+1][j+1] = dp[i][j] + 1
if dp[i+1][j+1] > maxLen {
endAt = i
maxLen = dp[i+1][j+1]
}
}
}
}
return a[endAt-maxLen+1 : endAt+1]
}
func longestCommonSubsequence(str1, str2 string) int {
var maxLen int
m, n := len(str1), len(str2)
// 构建二维数组
var dp = make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// str1 作为纵坐标
for i := 0; i < m; i++ {
// str2 作为横坐标
for j := 0; j < n; j++ {
if str1[i] == str2[j] {
dp[i+1][j+1] = dp[i][j] + 1
} else {
dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
}
if dp[i+1][j+1] > maxLen {
maxLen = dp[i+1][j+1]
}
}
}
return maxLen
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
给定排序数组 arr 和整数 k ,不重复打印 arr 中所有相加和为 k 的不降序二元组
例如, arr = [-8, -4, -3, 0, 1, 2, 4, 5, 8, 9]
, k = 10,打印结果为:
1, 9
2, 8
要求: 时间复杂度为 O(n),空间复杂度为 O(1)
这个前提很好,数组已经是排序好的了。这道题的解法是声明两个下标,一个指向数组的最前,一个指向数组的最后,比较对应数字的和,如果等于 K,则加入到结果 slice,前置下标继续向后移动。如果小于 K,则前置下标向后移动一位,如果大于 K,则后置下标向前移动一位,直到两个下标相遇。
最终可以在 O(n) 时间复杂度上找到满足条件的元素。
package main
import "fmt"
func main() {
var arr = []int{-8, -4, -3, 0, 1, 2, 4, 5, 8, 9}
result := twoSum(arr, 10)
fmt.Println(result)
}
func twoSum(arr []int, k int) [][]int {
var result = [][]int{}
var prePos, endPos = 0, len(arr) - 1
for prePos < endPos {
sum := arr[prePos] + arr[endPos]
if sum == k {
item := []int{arr[prePos], arr[endPos]}
result = append(result, item)
// 当找到满足条件的值后,前置下标加一
// 查询继续向下走
prePos++
} else {
if sum < k {
// 和小于 k,则前置下标向后移动
prePos++
} else {
// 和大于 k, 则后置下标向前移动
endPos--
}
}
}
return result
}
所谓的面试算法,其实就是基础知识加上题海战术后的做题技巧和熟练度。
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
思路很简单,就是你的代码要能跟上你的思路走。
具化递归执行思路
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func reverseKGroup(head *ListNode, k int) *ListNode {
if head == nil {
return head
}
// 找出区间段
startNode := head
var endNode *ListNode = head
// 因为 k 接下来还要用它,所以要定义新的变量
var i = k
for i > 0 {
i--
if endNode == nil {
return head
}
endNode = endNode.Next
}
// 递归执行反转,从 startNode 开始,到 endNode 结束,左开右闭
newHead := reverse(startNode, endNode)
// 反转下一轮
startNode.Next = reverseKGroup(endNode, k)
return newHead
}
// 反转从 a 到 b 的元素
func reverse(a, b *ListNode) *ListNode {
var pre, cur, next *ListNode
pre = nil
cur = a
next = a
for cur != b {
next = cur.Next
cur.Next = pre
pre = cur
cur = next
}
return pre
}
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
要求和为 k 的连续子数据,这里要用到一个技巧:前缀数组。前缀数组第 i 位记录前 i 个元素的和。[1, 2, 3]
的前缀数组为:[1, 3, 6]
。通过前缀数组,我们可以非常方便的计算出数组中第 i 个元素到第 j 个元素的和为多少。
借力前缀数组,这个问题就变成了:在数组中找到合为 k 的两个元素。和 TwoSum 问题很类似,可以用暴力解法,也可以用上 Map 。
这里有一个 go 语言的特性要记住,从 map 中取一个值 val, ok := hash[key]
,如果这个 key 不存在,则会返回其类型的零值。
func subarraySum(nums []int, k int) int {
var res int
// 构建前缀数组
var preArr = make([]int, len(nums)+1)
preArr[0] = 0
for i := 0; i < len(nums); i++ {
preArr[i+1] = preArr[i]+nums[i]
}
for i := 1; i < len(preArr); i++ {
for j := 0; j < i; j++ {
if preArr[i] - preArr[j] == k{
res++
}
}
}
return res
}
暴力求解的过程中,要让前缀数组中的每一个元素和之前的元素求差,这个是否可以用 Map 来填充,检查遍历次数?
func subarraySum(nums []int, k int) int {
var res int
var sumi int
var hash = make(map[int]int)
// 便于求解从第 0 位到元素本身的和
hash[0] = 1
for i := 0; i < len(nums); i++ {
sumi += nums[i]
// 要找的元素
sumj := sumi-k
if count, ok := hash[sumj]; ok {
res += count
}
// 元素本身加入到 map
hash[sumi] = hash[sumi]+1
}
return res
}
堆是一个很有趣的数据结构,其保证父节点都大于等于子节点(最大堆),或者父节点都小于等于子节点(最小堆)。常用于优先队列的场景,堆排序也是一个 n*logN 的排序方法。
堆的操作基于其结构的基本性质:
$\lfloor n/2 \rfloor+1$
,$\lfloor n/2 \rfloor+2$
,... ,n
i
的父节点索引为 (i-1)/2
(从 0 开始计数)i
的左孩子节点索引为 (i+1)*2 - 1
(从 0 开始计数)i
的右孩子节点索引为 (i + 1) * 2
(从 0 开始计数)设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]
这是一个最小堆的典型场景(哎,这么基本的课本示例也算中等难度,可见整体平均水平也不高)。具体步骤如下:
minHeapify
,保持其最小堆属性func smallestK(arr []int, k int) []int {
// 本质上是一个减缩版本的 heap sort
var res = make([]int, k)
// 针对整个数组构建最大堆
buildMinHeap(arr)
var heapSize = len(arr)
for i := 0; i < k; i++ {
res[i] = arr[0]
// 把最大值与最后一个元素交换
arr[0], arr[heapSize-1] = arr[heapSize-1], arr[0]
// 缩减堆的大小
heapSize--
// 重新保持最小堆性质
minHeapify(arr, heapSize, 0)
}
return res
}
func buildMinHeap(nums []int) {
// 因为从数组的 n/2+1 后都是叶节点,所以仅需对 [0, n/2] 做 minHeapify 操作,并且是倒序来的
for i := len(nums) / 2; i >= 0; i-- {
minHeapify(nums, len(nums), i)
}
}
// minHeapify 保持最小堆性质
func minHeapify(nums []int, heapSize, i int) {
var min int = i
l, r := left(i), right(i)
if l < heapSize && nums[min] > nums[l] {
min = l
}
if r < heapSize && nums[min] > nums[r] {
min = r
}
if i != min {
// 如果左孩子 OR 右孩子比父节点小,交换之
nums[i], nums[min] = nums[min], nums[i]
// 继续下沉,保持最小堆性质
minHeapify(nums, heapSize, min)
}
}
// 工具函数,快速定位到父节点、左孩子、右孩子(index from 0)
func parent(i int) int {
return (i - 1) / 2
}
func left(i int) int {
return (i+1)*2 - 1
}
func right(i int) int {
return (i + 1) * 2
}
在今天的两道题中,卡主我的是代码实现。我既要最终返回一个变量最开始的值,又要不断的在过程中修改这个变量,导致做题超时,最后做不出来。
两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
之前已经用递归的方法做过,但往往递归的效率很低,这次用迭代法做一遍。
整体思路如下:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func swapPairs(head *ListNode) *ListNode {
var result = &ListNode{0, head}
tmp := result
for tmp.Next != nil && tmp.Next.Next != nil {
// 把两个节点声明出来,不然思维跟不上,都是 next next next
node1 := tmp.Next
node2 := tmp.Next.Next
// 和上一轮串起来
tmp.Next = node2
// 在链表反转之前,为下轮做准备
// 下一轮会用到 node1, node2,以及上一轮的尾节点
// 如果下一轮为 nil 或 奇数个值,也没有关系,正好满足了 “如不足两个则不作操作”
node1.Next = node2.Next
// 链表反转
node2.Next = node1
// 把这轮的尾元素给到下一轮
tmp = node1
}
return result.Next
}
给定两个用链表表示的整数,每个节点包含一个数位。
这些数位是反向存放的,也就是个位排在链表首部。
编写函数对这两个整数求和,并用链表形式返回结果。
输入:(7 -> 1 -> 6) + (5 -> 9 -> 2),即617 + 295
输出:2 -> 1 -> 9,即912
这道题没有太难的思路问题,卡住我的问题是:
这里就涉及到对指针 & 引用的深刻理解,先定义一个指针,然后用引用去记住这个指针。然后在过程中迭代指针,最后返回引用。
lastLoopNode
?第一是记录头指针用,第二是当我们不知道 head 在哪里时,就自己先声明一个,然后在迭代过程中找到头指针(多应用于多链表操作情形,例如合并两个有序链表)/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
if l1 == nil {
return l2
}
if l2 == nil {
return l1
}
var lastLoopAddVal int
var lastLoopNode = &ListNode{}
var head = lastLoopNode
for l1 != nil || l2 != nil || lastLoopAddVal != 0 {
if l1 != nil {
lastLoopAddVal += l1.Val
l1 = l1.Next
}
if l2 != nil {
lastLoopAddVal += l2.Val
l2 = l2.Next
}
newNode := &ListNode{Val: lastLoopAddVal%10}
lastLoopAddVal = lastLoopAddVal/10
lastLoopNode.Next = newNode
lastLoopNode = newNode
}
return head.Next
}
LRU Cache(Least Recently Used)的淘汰策略是:删除那些最近没有被用到的数据。意即:
功能要求:
算法要求:
最终,LRUCache 是一个链表 & Map 的结合体,Map 中存储 Key & 节点指针的映射关系,可以在 O(1) 时间内定位到链表节点。然后通过双向链表,在 O(1) 时间内完成添加、删除节点。
type LRUCache struct {
Cap int
Cache map[int]*Item
Link DLink
}
// 定义带哨兵的双向链表
type DLink struct {
Nil *Item
}
func newDLink() DLink {
nilItem := &Item{}
nilItem.Pre = nilItem
nilItem.Next = nilItem
return DLink{Nil: nilItem}
}
func (l DLink) addItem(n *Item) {
n.Next = l.Nil.Next
l.Nil.Next.Pre = n
l.Nil.Next = n
n.Pre = l.Nil
}
func (l DLink) delItem(n *Item) {
n.Pre.Next = n.Next
n.Next.Pre = n.Pre
}
// 定义元素结构
type Item struct {
Key int
Val int
Pre, Next *Item
}
func Constructor(capacity int) LRUCache {
lru := LRUCache{
Cap: capacity,
Link: newDLink(),
Cache: make(map[int]*Item),
}
return lru
}
func (this *LRUCache) Get(key int) int {
if item, ok := this.Cache[key]; !ok {
return -1
} else {
this.Link.delItem(item)
this.Link.addItem(item)
return item.Val
}
}
func (this *LRUCache) Put(key int, value int) {
if item, ok := this.Cache[key]; !ok {
// 该节点未存在
newItem := &Item{Key: key, Val: value}
if this.Cap == len(this.Cache) {
// 删除过时元素(一定要先删 Map 再删节点,因为要删哪个 cache 是由链表最后一个元素决定的)
delete(this.Cache, this.Link.Nil.Pre.Key)
this.Link.delItem(this.Link.Nil.Pre)
}
this.Cache[key] = newItem
this.Link.addItem(newItem)
} else {
// 已经存在
item.Val = value
this.Link.delItem(item)
this.Link.addItem(item)
}
}
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
这道题思考起来并不难,我最初的想法是这样的:
我这个思路有个问题:
经过看了题解,有以下两种改进方式:
func maxSlidingWindowPro(nums []int, k int) []int {
var res = make([]int, len(nums)-k+1)
// 装的是元素的 index
var stack = []int{}
push := func(i int) {
// 移除当前窗口内比 nums[i] 小的数值,在新窗口内,比其小的值,已没有用武之地了
for len(stack) > 0 && nums[i] > nums[stack[len(stack)-1]] {
// 移除 stack 上最后一个元素
stack = stack[0 : len(stack)-1]
}
// 最后把元素添加到队列的最后一位
stack = append(stack, i)
}
// 前 k 个元素放入 stack 内
for i := 0; i < k; i++ {
push(i)
}
res[0] = nums[stack[0]]
for i := k; i < len(nums); i++ {
push(i)
// 如果最大元素不在窗口内,则删除掉
for stack[0] <= i-k {
stack = stack[1:]
}
// 将本轮窗口内的最大值,放入到结果数组中
res[i-k+1] = nums[stack[0]]
}
return res
}
双向链表的优点在于可以非常方便的删除、添加结点,添加了哨兵(sentinel)的链表可以使得代码更加干净整洁。
添加哨兵之后,双向链表将变为一个有哨兵的双向循环链表,L.nil
将位于表头 & 表尾之间。属性 L.nil.next
指向表头,L.nil.pre
指向表尾。类似的,表头的 pre
属性 & 表尾的 next
属性均指向 L.nil
。这样子在操作的时候有几个好处:
L.nil.next
) & 表尾(L.nil.pre
),不需要额外的指针type Node struct {
Key int
Val int
Pre *Node
Next *Node
}
type LinkedList struct {
Nil *Node
}
func NewLinkList() LinkedList {
nilNode := &Node{}
// 空链表情形,L.nil.next & L.nil.pre 均指向 L.nil
nilNode.Pre = nilNode
nilNode.Next = nilNode
l := LinkedList{
Nil: nilNode,
}
return l
}
func (l LinkedList) DelNode(n *Node) {
// 代码更加清晰、简洁,无需关心空指针
n.Pre.Next = n.Next
n.Next.Pre = n.Pre
}
func (l LinkedList) AddNode(n *Node) {
// 和头部元素连接
n.Next = l.Nil.Next
l.Nil.Next.Pre = n
// 和 Nil 元素连接
l.Nil.Next = n
n.Pre = l.Nil
}
func (l LinkedList) Search(key int) *Node {
var n *Node = l.Nil.Next
for n != nil && n != l.Nil {
if n.Key == key {
break
}
n = n.Next
}
return n
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.