【栈】
示例:'{()[()]}'
特点:可能层层叠叠,但是类似于消消乐,需要从里面最小的分子消掉,然后向外继续消灭相同的,这种就需要用到先进后出的栈,因为越晚进来的越早配对,配对完就出栈去掉
解法:先进后出的栈,遍历过程中和栈顶匹配就出栈,否则入栈,遍历完成如果站内还有元素,说明没有匹配完整
【dfs】
起点分几种
1.确定从一个点开始 dfs(0) 比如 从左上角开始一共有多少路径 左上角是一个确定的点
2.起点不确定,需要寻找符合要求的起点,那就要for循环寻找,不满足就跳过
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if board[i][j] == word[0] {
tmp[i][j] = true
if dfs(i, j, 1, tmp) {
return true
}
tmp[i][j] = false
}
}
}
空间复杂度,主要有两个来源 辅助空间 递归栈 取较大值 时间复杂度 看dfs调用的次数
数组
【有序数组剔除重复项,原地操作】
有个模板,采用快慢指针,记住模板即可,根据不同题目,判断条件稍有不同
1.剔除所有重复 leetcode.cn/problems/re…
2.允许两个重复项 leetcode.cn/problems/re…
这里面关键信息是 有序 ,所以就不用每次遍历了,和前面一个元素比较即可
模板如下,一共6小步
func removeDuplicates(nums []int) int {
//第一步 特殊情况,不满足遍历条件,直接返回
n := len(nums)
if n < 2 {
return n
}
//第二步 设置快慢指针的初始值 slow是结果数组里下一个要填充的位置 fast是当前待检查的元素位置
slow, fast := 2, 2
//第三步 慢指针每次循环,后移一位
for ; slow < n; slow++ {
//第四步 快指针 找满足题目要求的下一个元素
for fast < n && nums[fast] == nums[slow-2] {
fast++
}
//第五步 到头,直接跳出循环
if fast == n {
break
}
//第六步 nums[fast]即为结果数组里的下一个元素,赋值给slow,fast后移
nums[slow] = nums[fast]
fast++
}
return slow
}
上面示例中判断条件 是找 最多重复两次的元素 如果 nums[fast] == nums[slow-2] 成立 ,说明 nums[fast] = nums[slow-1] = nums[slow-2] 就是三个连续了,不满足,所以继续找,直到 不等于nums[slow-2] 最多有两个重复的,即为所求
如果是26题,删除所有重复,判断条件应该是 nums[fast] == nums[slow-1] 直到这个条件不成立,说明不是重复元素,nums[fast]才是下一个要加入结果数组的元素
快慢指针的初始位置 也不同
删除所有重复元素,结果只放一个元素绝对能满足 不重复的条件,所以 初始值为1
允许两个重复元素,结果跳过两个元素也满足这个条件,所以初始值为2 ,前面的0,1位置不需要检查肯定满足
【有序数组查找某个值】
有序肯定采用二分法查找 有序数组可能经过变换,比如从某一个位置旋转数组,从位置i开始,后半段和前半段调换位置,这种场景二分的时候要考虑一些特殊情况,有模板可用
func search(nums []int, target int) int {
第一步 处理特殊情况直接返回
n := len(nums)
if n < 1 {
return -1
}
if n == 1 {
if nums[0] == target {
return 0
}
return -1
}
第二步 设置左右指针位置
l, r := 0, n-1
第三步 for循环条件
for l <= r {
第四步 找中间值,满足条件直接返回
mid := (l + r) / 2
if nums[mid] == target {
return mid
}
第五步 判断 如果前半部分 是有序的
if nums[l] <= nums[mid] {
第六步 target在前半部分中 更新右指针
if nums[l] <= target && target < nums[mid] {
r = mid - 1
} else {
第七步 前半部分有序且target不在范围中
l = mid + 1
}
} else {
第八步 后半部分有序 并且target在其中 更新左指针
if nums[mid] < target && target <= nums[n-1] {
l = mid + 1
} else {
//第九步 后半部分有序且 target不在其中 更新右指针
r = mid - 1
}
}
}
return -1
}
上面的模板是在数组升序没有重复元素的情况下,如果有重复元素,用第五步的判断证明不了是有序的,举个例子 111111121111111111,中间值和左边界都是1,但左边不是有序的,所以需要在第五步前面判断特殊case,这种特殊case 就是 左右中对应的值都是一样的可没有办法判定哪边是有序的,所以只能两个边界往里缩一位(因为去掉这两个数其实对数组的顺序和最终的结果都不会产生影响,所以去重一直到可以判断哪一个区间是有序的),在第五步前面添加如下代码,具体看leetcode.cn/problems/se…
if nums[l] == nums[mid] && nums[mid] == nums[r] { l++ r-- continue }
非降序数组
leetcode.cn/problems/se…
【深度优先遍历】
【路径问题】
遍历路径,在遍历的过程中不断更新结果值(比如这棵树的最小值等),dfs方法不需要返回值,在内部更新迭代结果值
leetcode.cn/problems/cl… (路径走一遍) leetcode.cn/problems/se… (遍历树找到比根值大的最小值)
leetcode.cn/problems/bi… (数的中序遍历)
leetcode.cn/problems/va… (遍历二叉树,找到不满足条件的值更新结果)
遍历路径找出所有满足条件的路径,这种情况需要注意路径回溯
leetcode.cn/problems/pa… (有多少种满足条件路径)
leetcode.cn/problems/su… (剪枝去重)
【修改链表结构】
修改链表结构的感觉难度比较大,不太好统一规律
leetcode.cn/problems/fl… (dfs每次要返回连接的node)
leetcode.cn/problems/po… (思路比较清晰但是难以第一时间想到,不适合统计规律)
leetcode.cn/problems/po… (补充next节点,用层序遍历,出入栈,已经不是深度优先遍历的问题)
【翻转链表】
思想就是 设立3个节点,分别是
1.done(已经翻转完成的链表末端)
2.p(当前要翻转的节点)
3.next(即将翻转的节点p的下一个节点),3个节点遍历一直走到末尾
代码模板
func reverseList(head *ListNode) *ListNode {
//第一步 特殊情况直接返回
if head == nil {
return nil
}
//第二步 给三个节点赋值,done初始是nil
var done *ListNode
p, next := head, head.Next
//第三步 设立for循环结束条件,这是整个链表翻转的结束条件
for p != nil {
//第四步 3个节点赋值后移
p.Next = done
done = p
p = next
//注意判断边界条件
if next != nil {
next = next.Next
}
}
//第五步 done是翻转后的第一个节点,直接返回
return done
}
以上示例中对于链表部分翻转(left到right)的情况,上面的方法for循环的判断应该是p!= right作为翻转的终点,部分翻转的找到断裂处的4个节点,翻转之后重新连接即可,代码如下 leetcode.cn/problems/re…
func reverseBetween(head *ListNode, left int, right int) *ListNode {
//tmp从prehead开始,防止left是1的情况
tmp := &ListNode{-1, head}
prehead := tmp
var p1, p2, p3, p4 *ListNode//分别是left的前一位,left,right,right的后一位
i := 0
//找断裂处p1,p3
for tmp != nil {
if i == left-1 {
p1 = tmp
}
if i == right {
p3 = tmp
break
}
tmp = tmp.Next
i++
}
p2, p4 = p1.Next, p3.Next //4个断裂处的节点都保存了
//这个方法翻转l-r的节点
var reverse = func(l, r *ListNode) (*ListNode, *ListNode) {
//第一步
var done *ListNode
p := l
next := p.Next
//第二步
for done != r {
//第三步
p.Next = done
done = p
p = next
if next != nil {
next = next.Next
}
}
//第四步
return r, l //返回反转顺序即可
}
l, r := reverse(p2, p3)
p1.Next, r.Next = l, p4//重新连接
return prehead.Next
}
岛屿区域问题
核心思路就是从边界下手,找到符合条件的元素(比如是特定的值‘O';比前一个元素大的值等),同化这个元素或者标记出来
leetcode.cn/problems/su… (被包围的区域同化,反过来找没有被包围的)
leetcode.cn/problems/pa… (找从边界开始逐渐增高的区域,本质还是从边界开始着手遍历)
还有一些是求有多少独立分隔的区域,这种每次从一个位置入手消除区域元素,不是边界思路了,消除到碰壁就返回,一次循环消除一个区域
leetcode.cn/problems/nu… (求岛屿数量,一边消灭岛屿,一遍更新结果值)
leetcode.cn/problems/ba… (甲板上的战舰就是一个独立的区域,求区域个数)
【动态规划】
适合的场景
1.结果求多少种可能性,极值(比如最大最小值)
2.后面位置的结果跟前面位置有关联
leetcode.cn/problems/wo… (死记吧,我想不出来)
一维数组的案例
二维数组案例
还可以使用滚动数组优化空间复杂度
树 顺序遍历
【回文字符串】
回文字符串 可以考的的场景一般有
1.一个字符串划分子字符串,一共可以有多少种组合方式(路径问题,用回溯遍历)
2.一个字符串分割最少回文子字符串的个数(极值问题,动态规划)
需要判断s【i,j】是否为回文串,动态规划的思路是这样的
代码实现为
n := len(s)
g := make([][]bool, n)
//先全部设为true
for i := range g {
g[i] = make([]bool, n)
for j := range g[i] {
g[i][j] = true
}
}
for i := n - 1; i >= 0; i-- {
for j := i + 1; j < n; j++ {
g[i][j] = s[i] == s[j] && g[i+1][j-1]
}
}
【两个线程交替打印ab】
用go实现非常的优雅,两个channel,用select监听,执行到一个case给另一个case的channel放数据,交替执行,注意sleep后面要显式的写明时间是秒,否则默认是纳秒会提前退出,最后一个打印协程可能不执行
func main() {
flag1 := make(chan struct{}, 1)
flag2 := make(chan struct{}, 1)
flag1 <- struct{}{}
for i := 0; i < 10; i++ {
select {
case <-flag1:
go func() {
fmt.Print("A")
flag2 <- struct{}{}
}()
case <-flag2:
go func() {
fmt.Print("B")
flag1 <- struct{}{}
}()
}
}
time.Sleep(5 * time.Second)
}
【IP和数字相互转换】
解释:IP转换数字就是按照.分割分别拿出来4个数,前三个分别位移相应的位数即可; 数字转换成IP,先把原始数据的后8位置0,与原始数据相减就是最后8位二进制的数值,循环操作得到4个数值用.串起来就是IP地址了
func ipToNum(ip string) int {
strs := strings.Split(ip, ".")
if len(strs) != 4 {
return 0
}
nums := make([]int, 4)
for i := 0; i < 4; i++ {
nums[i], _ = strconv.Atoi(strs[i])
}
return nums[0]<<24 + nums[1]<<16 + nums[2]<<8 + nums[3]
}
func numToIp(num int) string {
nums := make([]int, 4)
for i := 0; i < 4; i++ {
src := (num >> 8) << 8 //先左移再右移,后面8位置零
nums[i] = num - src
num = num >> 8
}
return fmt.Sprintf("%d.%d.%d.%d", nums[3], nums[2], nums[1], nums[0])
}
【three letters】
在其他平台的编程题上做的,一个多小时,还是记录下下来,本来用深度优先遍历,但是时间上太长了,题目让返回一个满足的就行
大意就是给几个ab,让你安排顺序不能有连续三个的重复字母在一起,思路就是以min作为分隔字母去均匀的隔开max字母,分为两个区间,一个是区间为2个元素的,一个是1个元素的,确定两个区间个数,就能填充得到结果了,比深度遍历要节省时间,返回的格式是 XXYXXYXXYXYXYX的形式
func Solution(A int, B int) string {
var minMax = func(i, j int) (min, max int) {
if i < j {
return i, j
}
return j, i
}
//获取字母的最大最小值
min, max := minMax(A, B)
//因为最小值一共可以划分min+1个缝隙,每个缝隙最多能容纳2个max对应字母,再多就满足不了条件直接返回,节省时间
if (min+1)*2 < max {
return ""
}
//原始公式是假设min+1个缝隙有x个装2个元素的,装一个元素的就是 总的减去2个的就是 min+1 -x 所有区间加载一起就是max 这样能得到x(two)的值
two := max - min - 1
res := ""
//两者元素一样或者只相差一个的情况
if two <= 0 {
for i := 0; i < min; i++ {
res += "ab"
}
c := min + max - len(res)
//元素较多的补齐其实也就1个
if A > B {
for c != 0 {
res += "a"
c--
}
} else {
for c != 0 {
res += "b"
c--
}
}
return res
}
//先装填2个元素的区域
for i := 0; i < two; i++ {
if A > B {
res += "baa"
} else {
res += "abb"
}
}
//因为上面为了保证最后是max对应的字母,前面多写了一个min对应的去掉即可,这样就能保证第一个和最后一个区间都被max元素填充
res = res[1:]
//还剩下的1区间个数
d := min + 1 - two
if A > B {
for d != 0 {
res += "ba"
d--
}
} else {
for d != 0 {
res += "ab"
d--
}
}
return res
}