发布于 

Go 数据结构与算法、SQL 刷题 ⌨️

刷题记录和总结

数组

题号 思路 备注
704. 二分查找 有序无重是前提,左右指针,不断缩小查找范围,注意边界处理 1️⃣
27. 移除元素 快慢指针替换位置 1️⃣
977. 有序数组的平方 两头对比选择,从后往前填充 2️⃣
209. 长度最小的子数组 双指针滑动收缩窗口 2️⃣
59. 螺旋矩阵 II 模拟过程,一圈圈往里缩,注意统一 [i, j) 填充范围 2️⃣
15. 三数之和 排序 + 双指针 + 剪枝
18. 四数之和 排序 + 双指针 + 剪枝(上一题多加一个 for)

链表

题号 思路 备注
203. 移除链表元素 设置虚拟头节点,统一处理 3️⃣
707. 设计链表 注意 for 后 cur 的定位 3️⃣
206. 反转链表 双指针同步移动,注意循环终止条件 3️⃣
24. 两两交换链表中的节点 虚拟头节点 + 两个记录节点 4️⃣
19. 删除链表的倒数第 N 个结点 虚拟头节点,快慢指针 4️⃣
面试题 02.07. 链表相交 双指针走完互换起点,结尾的 nil 也可看做是交点 4️⃣
142. 环形链表 II 快慢指针环内相遇后,起点位置和慢指针同步移动一定会相遇 4️⃣

哈希表

题号 思路 备注
242. 有效的字母异位词 分别遍历,先 ++ 后 –,最后判断 5️⃣
349. 两个数组的交集 分别遍历,判断收集去重 5️⃣
202. 快乐数 模拟,map 判断是否出现过 5️⃣
1. 两数之和 遍历,一边判断一边存 [val] index 5️⃣
454. 四数相加 II 两个双重循环,用 map 优化 6️⃣
383. 赎金信 ++ 后 –,– 时判断是否 < 0 6️⃣

字符串

题号 思路 备注
344. 反转字符串 双指针,往里夹 7️⃣
541. 反转字符串 II 局部的反转 + 最后的边界判断 7️⃣
151. 反转字符串中的单词 双指针删除冗余空格/定位单词 + 反转整体再反转每个单词 8️⃣
28. 找出字符串中第一个匹配项的下标 Knuth-Morris-Pratt 字符串查找算法 🤡 $O(n * m)$ → $O(n + m)$
459. 重复的子字符串 KMP 基础上,首尾拼接查中间 🤡

栈与队列

题号 思路 备注
225. 用队列实现栈 使用两个 slice ,每添加一个元素都需要倒腾 9️⃣
232. 用栈实现队列 输入栈 + 输出栈,出的时候触发倒腾 9️⃣
20. 有效的括号 slice 模拟栈,遍历时添加或消除括号 9️⃣
1047. 删除字符串中的所有相邻重复项 在栈中玩消消乐 9️⃣
150. 逆波兰表达式求值 “后缀表达式” 求解,用栈模拟 1️⃣0️⃣
239. 滑动窗口最大值 实现 “单调队列”,记录窗口内的最大值 1️⃣0️⃣
347. 前 K 个高频元素 “优先队列”(添加和移除元素的时候维护单调性);统计后排序(偷懒做法) 1️⃣0️⃣

单调栈

题号 思路 备注
739. 每日温度 栈顶到栈底递增(即遇到 第一个 更高温,才开始更新等待的天数) 4️⃣1️⃣
496. 下一个更大元素 I 上一题套了一个哈希映射 4️⃣1️⃣
503. 下一个更大元素 II 在遍历的过程中使用 % 模拟走两遍 nums 4️⃣1️⃣
42. 接雨水 凹,横着计算面积,算完一个面积后记得更新栈顶元素 4️⃣2️⃣
84. 柱状图中最大的矩形 凸,数组头尾加个 0 避免数组原来就有序不走计算逻辑和拿不到 left 4️⃣2️⃣

二叉树

题号 思路 备注
144. 二叉树的前序遍历94. 中145. 后 递归(注意 slice 是一个值类型);迭代用个栈,注意中序的访问和处理节点不一致;后序为【左右中】即【中右左】的反转 1️⃣1️⃣
102. 二叉树的层序遍历 使用 []*TreeNode 保存每一层节点 1️⃣1️⃣
226. 翻转二叉树 每个节点的左右都交换一下即整棵树翻转(中序遍历不行) 1️⃣2️⃣
101. 对称二叉树 只能后序遍历(左右中 + 右左中) 1️⃣2️⃣
104. 二叉树的最大深度 根节点的高度就是二叉树的最大深度(后序) 1️⃣2️⃣
111. 二叉树的最小深度 和上一题对比,区别在于处理非叶子节点的逻辑(后序) 1️⃣2️⃣
110.平衡二叉树 计算以每个节点为根节点的二叉树的高度(后序) 1️⃣3️⃣
257. 二叉树的所有路径 前序遍历,字符串的值传递隐含了回溯(先序) 1️⃣3️⃣
404. 左叶子之和 通过节点的父节点来判断其左孩子是不是左叶子 1️⃣3️⃣
222. 完全二叉树的节点个数 可以利用完全二叉树的特性,公式求解(后序) 1️⃣4️⃣
513. 找树左下角的值 递归:深度最大的叶子节点是最后一行(先序);迭代:模板 1️⃣4️⃣
112. 路径总和 函数的值传递隐含了回溯,递归函数需要返回值 1️⃣4️⃣
113. 路径总和 II 递归函数不需要返回值,注意指针类型会被修改要 copy 一下 1️⃣4️⃣
106. 从中序与后序遍历序列构造二叉树 用散列表记录元素再前序中的索引,根据子树元素个数切割数组,[l, r] 1️⃣4️⃣
654. 最大二叉树 前序遍历递归构建 1️⃣5️⃣
617. 合并二叉树 同时遍历两棵树,递归的优雅表现 1️⃣5️⃣
700.二叉搜索树中的搜索 利用二叉搜索树的性质 1️⃣5️⃣
98.验证二叉搜索树 注意,不能陷入只判断局部(先序) 1️⃣5️⃣
236. 二叉树的最近公共祖先 遍历整棵树,判断两种情况(后序) 1️⃣6️⃣
501. 二叉搜索树中的众数 计数法不使用额外空间, prev 指针(中序) 1️⃣6️⃣
530. 二叉搜索树的最小绝对差 在一个有序数组上求差值(中序) 1️⃣6️⃣
235. 二叉搜索树的最近公共祖先 遇到 cur 节点数值在 [q, p] 区间中,即是最近公共祖先(往一边走就行,后序) 1️⃣7️⃣
701.二叉搜索树中的插入操作 通过递归返回值来加入新节点,不涉及结构的调整(后序查找目标位置) 1️⃣7️⃣
450.删除二叉搜索树中的节点 删除节点操作涉及到结构的调整(后序查找目标位置) 1️⃣7️⃣
669. 修剪二叉搜索树 不需要删除,只需要做结构的调整 1️⃣8️⃣
108. 将有序数组转换为二叉搜索树 寻找分割点 len(nums) / 2,递归左区间和右区间 1️⃣8️⃣
538. 把二叉搜索树转换为累加树 即有序数组求 从后到前 的累加数组,反中序(右左中)遍历顺序累加 1️⃣8️⃣

回溯

题号 思路 备注
77. 组合 注意切片里面是指针不能直接放,要 copy 一下(组合不考虑顺序) 1️⃣9️⃣
216.组合总和 III 比上一题多了一个判断的条件,注意剪枝 1️⃣9️⃣
17. 电话号码的字母组合 构建映射,不同集合,所以 i 都是从 0 开始 1️⃣9️⃣
39. 组合总和 可重复选取,不用控制遍历的起始位置+1;排序剪枝 2️⃣0️⃣
40. 组合总和 II 只能选取一次,在回溯中对同一层的元素去重,而不对同一枝去重 2️⃣0️⃣
131. 分割回文串 切割线,切割问题抽象为组合问题 2️⃣0️⃣
93. 复原 IP 地址 也是切割问题,终止条件判断不一样 2️⃣1️⃣
78. 子集 集合无重复元素,无条件收集所有节点,无需剪枝 2️⃣1️⃣
90. 子集 II 集合有重复元素,排序、同层去重、同枝不去 2️⃣1️⃣
491. 非递减子序列 不能排序,对同枝元素去重,注意 used 数组的位置,只负责本层 2️⃣2️⃣
46. 全排列 不包含重复元素,不需要 startIndex,需要标记一个排列里使用过的元素 2️⃣2️⃣
47. 全排列 II 包含重复元素,排序、同层去重、同枝不去 2️⃣2️⃣
332. 重新安排行程 🤡
51. N 皇后 🤡
37. 解数独 🤡

贪心

题号 思路 备注
455. 分发饼干 两者都排序后,胃口主动遍历,饼干的被动(优先满足胃口大的) 2️⃣3️⃣
376. 摆动序列 “删除”单调坡度上的中间节点(统计峰值节点) 2️⃣3️⃣
53. 最大子数组和 当前“连续和”为负数的时候立刻放弃 2️⃣3️⃣
122. 买卖股票的最佳时机 II 只收集相邻的每个正利润 2️⃣4️⃣
55. 跳跃游戏 判断跳跃覆盖范围能不能覆盖到终点 2️⃣4️⃣
45. 跳跃游戏 II 每超过上一次可达最大范围,需要跳跃次数 +1 后扩大范围 2️⃣4️⃣
1005. K 次取反后最大化的数组和 按绝对值进行排序 2️⃣4️⃣
134. 加油站 当前累加 rest[i] 的和 curSum 一旦小于 0,起始位置至少要是 i+1 2️⃣5️⃣
135. 分发糖果 先 → 比 右 > 左,再 ← 比 左 > 右 2️⃣5️⃣
860. 柠檬水找零 简单模拟 2️⃣5️⃣
406. 根据身高重建队列 按照身高大到小、人数小到大排序,优先按身高高的 k 来插入 2️⃣5️⃣
452. 用最少数量的箭引爆气球 出现重叠的气球只需要一个箭;排序后,判断重叠区间,统计箭数 2️⃣6️⃣
435. 无重叠区间 按右界排序,用总数减去非重叠区间的个数;修改范围模拟移除 2️⃣6️⃣
763. 划分字母区间 字符最远出现位置下标和当前下标相等时即为分割点 2️⃣6️⃣
56. 合并区间 排序、遍历、比较(拿结果集的最后一个元素)、合并 2️⃣7️⃣
738. 单调递增的数字 后往前遍历,-1 时触发后面范围都置 9 2️⃣7️⃣
968. 监控二叉树 🤡

动态规划

题号 思路 备注
509. 斐波那契数 感受一下 2️⃣8️⃣
70. 爬楼梯 上一题套了一个壳 2️⃣8️⃣
746. 使用最小花费爬楼梯 到达第 i 台阶所花费的最少体力为 dp[i] 2️⃣8️⃣
62.不同路径 从 (0, 0) 出发,到 (i, j) 有 dp[i][j] 条不同的路径 2️⃣9️⃣
63. 不同路径 II 障碍标记对应的 dp[i][j] 保持初始值(0) 2️⃣9️⃣
343. 整数拆分 分拆正整数 i 可以得到的最大乘积为 dp[i]max((i - j) * j), dp[i - j] * j) 2️⃣9️⃣
96. 不同的二叉搜索树 1 - i 为节点组成的二叉搜索树的个数为 dp [i],递推公式不好找 🤡
01 背包(二维/一维) 每个物品只有一个,一维需要从大小遍历背包 3️⃣0️⃣
416. 分割等和子集 背包大小为 sum / 2,商品大小和价值一样,若 dp[target] == target 则存在 (装满背包的最大价值/重量) 3️⃣0️⃣
1049. 最后一块石头的重量 II 让石头分成重量最相同的两堆,相撞之后剩下的石头最小 (装满背包的最大价值/重量) 3️⃣1️⃣
494. 目标和 dp[j] 表示填满 j(包括 j)这么大容积的包,有 dp[j] 种方法 (装满背包的组合数) 🤨
474. 一和零 物品的重量/背包 有两个维度 (背包可以装的最大物品数量) 🔢 3️⃣1️⃣
完全背包 每件物品都有无限个,一维需要从小到大遍历背包 3️⃣1️⃣
518. 零钱兑换 II 凑成总金额 j 的货币组合数为 dp[j] (装满背包的组合数) 3️⃣2️⃣
377. 组合总和 Ⅳ 注意是“排列”数,需要先遍历背包再遍历物品 (装满背包的排列数) 3️⃣2️⃣
322. 零钱兑换 凑足总额为 j 所需钱币的最少个数为 dp[j] (装满背包的最小物品数量,加是否装满的判断)🔢 3️⃣3️⃣
279. 完全平方数 和为 j 的完全平方数的最少数量为 dp [j],注意初始化 (装满背包的最小物品数量)🔢 3️⃣3️⃣
139. 单词拆分 wordDict 是物品,s 是背包,物品能不能装入是看能不能和背包后缀匹配 (装满背包的排列数) 3️⃣3️⃣
打家劫舍
198. 打家劫舍 考虑下标 i(包括 i)以内的房屋,最多可以偷窃的金额为 dp [i] 3️⃣4️⃣
213. 打家劫舍 II 包含首元素,不包含尾元素;包含尾元素,不包含首元素;两种情况取最大值 3️⃣4️⃣
337. 打家劫舍 III dp 数组下标为 0 记录不偷该节点所得到的的最大金钱,下标为 1 记录偷该节点所得到的的最大金钱 (后序遍历) 3️⃣4️⃣
买卖股票 买入卖出,但是同时只能持有一支股票
121. 买卖股票的最佳时机 dp[i][0] 表示第 i 天不持有,dp[i][1] 表示第 i 天持有股票所得最多现金 (只能买卖一次) 3️⃣5️⃣
122.买卖股票的最佳时机 II 可以完成多笔交易,最多只能持有一股股票 (可以买卖多次) 3️⃣5️⃣
123.买卖股票的最佳时机 III 可以完成最多两笔交易,不能同时 (可以买卖最多两次) 3️⃣5️⃣
188. 买卖股票的最佳时机 IV 可以完成最多 k 笔交易,,除了 0 以外,偶数就是卖出,奇数就是买入 (可以买卖最多 k 次) 3️⃣6️⃣
309. 买卖股票的最佳时机含冷冻期 卖出股票后,你无法在第二天买入股票,即冷冻期为 1 天 (可以买卖最多次) 3️⃣6️⃣
714. 买卖股票的最佳时机含手续费 卖出的时候加个手续费即可 (可以买卖多次) 3️⃣6️⃣
子序列问题
300.最长递增子序列 dp[i] 表示 i 之前包括 i 的以 nums[i] 结尾的最长递增子序列的长度 3️⃣7️⃣
674. 最长连续递增序列 只要比较 nums[i]nums[i - 1],而不用去比较 nums[j]nums[i](j 是在 0 到 i 之间遍历) 3️⃣7️⃣
718. 最长重复子数组 子数组就是连续子序列,以下标 i 为结尾的 A,和以下标 j 为结尾的 B,最长重复子数组长度为 dp[i][j],注意初始化 3️⃣7️⃣
1143.最长公共子序列 较上一题就是少了“连续”的要求,如果不等取各自前一个状态的最大值,注意初始化和上面有不同,两者都可以删除元素 3️⃣8️⃣
1035.不相交的线 与上一题相同,只要相对顺序不变就行(就是求最长公共子序列) 3️⃣8️⃣
53. 最大子数组和 (连续)子数组,dp[i] 表示以 i 为结尾的子数组的最大和 3️⃣8️⃣
392. 判断子序列 以下标 i 为结尾的 s,和以下标 j 为结尾的 t,相同子序列的长度为 dp[i][j]删除元素只能是 t(判断最长公共子序列长度是否和 t 长度一样即可) 3️⃣8️⃣
115. 不同的子序列 以下标 i 为结尾的 s 子序列中出现以 j 为结尾的 t 的个数为 dp[i][j],较上一题初始化和递推公式不同 3️⃣9️⃣
583. 两个字符串的删除操作 找出最长公共子序列,用两个字符串的总长度减去两个最长公共子序列的长度 3️⃣9️⃣
72. 编辑距离 3️⃣9️⃣
647. 回文子串 4️⃣0️⃣
516. 最长回文子序列 4️⃣0️⃣

图论

题号 思路 备注

LC🔥100

LeetCode 热题 100

题号 思路 备注
哈希
1. 两数之和 哈希(key: val,val: index)
49. 字母异位词分组 map[string][]string、排序每个元素后作为 key
128. 最长连续序列 map 用来快速查询某个元素是否存在并去重优化
双指针
283. 移动零 快慢指针,移动非 0,末尾置 0
11. 盛最多水的容器 左右指针往里夹,短板效应
15. 三数之和 排序去重,一数外循环,两数左右指针往里夹
18. 四数之和 套个 for + 三数之和
42. 接雨水 维护单调栈(栈底到栈顶递减),识别凹槽,持续按照行计算 ⬅️
滑动窗口
3. 无重复字符的最长子串 一边遍历一边使用 map 记录字符的最后位置,滑动窗口
438. 找到字符串中所有字母异位词 滑动窗口 + [] int 记录判断是否为异位词
子串
560. 和为 K 的子数组 前缀和 + 哈希表优化(记录每个前缀和出现的次数)
239. 滑动窗口最大值 实现 “单调队列”,记录窗口内的最大值
76. 最小覆盖子串 哈希表记录,覆盖到后缩小范围,移除无用元素
普通数组
53. 最大子数组和 贪心:遇到负数直接置零;动规:取考虑 num [i] 后的最大值
56. 合并区间 排序后遍历,判断修改右边界 or 直接添加
189. 轮转数组 整体翻转后再翻转两个局部
238. 除自身以外数组的乘积 遍历两次,计算每个元素左侧/右侧乘积
41. 缺失的第一个正数 把每个数放在 nums[nums[i] - 1] 的位置上,再遍历检查
矩阵
73. 矩阵置零 使用第一行/列记录该行/列是否有 0,再遍历一次判断置 0
54. 螺旋矩阵 定义好四个边界,模拟
48. 旋转图像 先转置(行列交换)再左右对称交换位置
240. 搜索二维矩阵 II 初始定位到第一行的最后一个元素,之后遍历判断移动行/列
链表
160. 相交链表 到头了就走一遍对方的路
206. 反转链表 pre cur temp
234. 回文链表 快慢指针找到链表中点,反转后半部分后逐个与前半部分比较 $O(1)$ 空间
141. 环形链表 快慢指针,如果有环一定回相遇,有一个为 nil 就算没环
142. 环形链表 II 快慢相遇后,头节点和慢指针同步移动,返回相遇点;或者哈希
21. 合并两个有序链表 模拟合并,判断处理剩余部分
2. 两数相加 模拟相加,记录进位,节点为 nil 给个默认值 0
19. 删除链表的倒数第 N 个结点 快慢指针,fast 先移动,到位后一起移动 slow 定位到待删除节点前一个位置
24. 两两交换链表中的节点 设置虚拟头节点加两个临时记录的指针
25. K 个一组翻转链表 反转链表(head, tail → tail, head),上一个子链的 tail 是下一个的 hair
138. 随机链表的复制 map[*Node]*Node 建立新旧节点的映射关系,map [旧] → 新
148. 排序链表 归并排序,快慢指针找到中点,合并两个有序链表
23. 合并 K 个升序链表
146. LRU 缓存 双向链表 + 哈希表
二叉树
94. 二叉树的中序遍历 递归/迭代(用个栈优先向左子树方向保存节点)
104. 二叉树的最大深度 后序递归,depth := max(leftDepth, rightDepth) + 1
226. 翻转二叉树 后序递归,翻转每一个节点的左右孩子
101. 对称二叉树 dfs(left.Left, right.Right) && dfs(left.Right, right.Left)
543. 二叉树的直径 后序递归,和二叉树最大深度是一样的,中间多了一个判断收集结果
102. 二叉树的层序遍历 用两个队列,curLevelnextLevel 轮换,两个 for
108. 将有序数组转换为二叉搜索树 前序遍历,idx := len(nums)/2
98. 验证二叉搜索树 前序遍历,检查的时候需要传递节点的
230. 二叉搜索树中第 K 小的元素 中序遍历是有序的,一边遍历一遍计算是否到第 K 个数
199. 二叉树的右视图 层序列遍历只收集每一层的最后一个节点
114. 二叉树展开为链表 前序遍历后构建链表/前序遍历和展开同步进行
105. 从前序与中序遍历序列构造二叉树 查找划分、递归构造
437. 路径总和 III 递归遍历每个节点,并在每个节点上去查找路径
236. 二叉树的最近公共祖先 后序遍历,使用左右子树的递归结果来决定当前节点是否是最近公共祖先
124. 二叉树中的最大路径和 后序遍历,注意向上传递的额时候只能传递左右的一边
图论
200. 岛屿数量 遇到一个岛屿则 DFS/BFS(单源) 进行标记
994. 腐烂的橘子 多源 BFS(队列里面同时有多个元素),烂 🍊 入队并统计新 🍊 数量
207. 课程表 检测课程之间的依赖关系是否存在环(DFS 构建邻接表 /BFS 移除入度为 0 的节点)
208. 实现 Trie (前缀树) 注意结构体的定义,其余都是遍历判断每个字符
回溯
46. 全排列 排列问题,可回头,不需要 start,使用 used 避免重复选择一个数字
78. 子集 组合问题,不可回头,需要遍历下标 start
17. 电话号码的字母组合 组合问题,构建映射表,需要遍历下标 start
39. 组合总和 用 start 避免重复组合和支持无限次取,可排序剪枝
22. 括号生成
79. 单词搜索
131. 分割回文串
51. N 皇后
二分查找
35. 搜索插入位置
74. 搜索二维矩阵
34. 在排序数组中查找元素的第一个和最后一个位置
33. 搜索旋转排序数组
153. 寻找旋转排序数组中的最小值
4. 寻找两个正序数组的中位数
20. 有效的括号 左括号入栈,右括号优先匹配或入栈,最后判断栈是否为空
155. 最小栈 维护两个栈,一个用于存储所有的元素,另一个用于维护历史最小值
394. 字符串解码 一个栈用于存储重复次数 k,另一个栈用于存储当前累积的字符串部分
739. 每日温度 维护一个单调递减栈(栈底到栈顶是递减的,栈顶是最小的), 栈中存储的是索引 $O(n)$
84. 柱状图中最大的矩形 维护一个单调递增栈(栈底到栈顶是递增的,栈顶是最大的)
215. 数组中的第 K 个最大元素
347. 前 K 个高频元素
295. 数据流的中位数
贪心算法
121. 买卖股票的最佳时机 一次遍历模拟时间流逝,不断交易取最值
55. 跳跃游戏 维护最大覆盖范围,遍历判断
45. 跳跃游戏 II 维护当前遍历位置最远可达,如果遍历位置到达最远可达则 jump++
763. 划分字母区间 遍历字符串并记录每个字符的最后出现位置,注意遍历的时候 end 需要取 max
动态规划
70. 爬楼梯 注意循环的结束条件 $f(n) = f(n-1) + f(n-2)$
118. 杨辉三角 创建模拟逐层填充
198. 打家劫舍 dp[i] 表示偷窃到 i 号房屋所获取的最高金额 $dp[i]=max(dp[i−1],nums[i]+dp[i−2])$
279. 完全平方数 dp[i] 表示数字 i 最少可以由多少个完全平方数组成 $dp[i]=min(dp[i],dp[i−j^2]+1)$
322. 零钱兑换 dp[i] 表示凑成金额 i 所需的最少硬币数 $dp[i]=min(dp[i],dp[i−coin]+1)$
139. 单词拆分 dp[i] 表示字符串 s 的前 i 个字符是否可以用字典中的单词拼接出来 $dp[i] = dp[j]$ && $s[j:i] ∈ wordDict$
300. 最长递增子序列 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度 $dp[i]=max(dp[i],dp[j]+1)$
152. 乘积最大子数组 通过维护 maxProdminProd 同时处理了包含当前元素的最大子数组乘积和最小子数组乘积的情况,而不需要回溯或进行额外的遍历(一次遍历即可)
416. 分割等和子集 0/1 背包问题(先物后背,背倒序),dp[i] 表示是否能找到和为 i 的子集 $dp[i] = dp[i]$
32. 最长有效括号 用栈来做更方便,先把 -1 压入栈作为哨兵
多维动态规划
62. 不同路径 dp[i][j] 表示从左上角 (0, 0) 走到位置 (i, j) 的路径数 $dp[i][j]=dp[i−1][j]+dp[i][j−1]$
64. 最小路径和 dp[i][j] 表示从左上角 (0, 0) 到达位置 (i, j) 的最小路径和 $grid[i][j]+min(dp[i−1][j],dp[i][j−1])$
5. 最长回文子串 dp[i][j] 表示子串 s[i:j+1] 是否是回文 $dp[i][j]=(s[i]==s[j])∧dp[i+1][j−1]$
1143. 最长公共子序列 dp[i][j] 表示字符串 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列的长度 $dp[i][j]=dp[i−1][j−1]+1$$dp[i][j]=max(dp[i−1][j],dp[i][j−1])$
72. 编辑距离 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数
技巧
136. 只出现一次的数字 全部元素的异或运算结果即为数组中只出现一次的数字
169. 多数元素 摩尔投票法,不通的元素数量 – 抵消、相同的 ++ $O(1)$ 空间
75. 颜色分类 分别遍历判断交换元素,确定 0 和 1 的位置
31. 下一个排列
287. 寻找重复数 数字都在 [1, n] 范围内,快慢指针,即 142. 环形链表 II

标准 I/O

fmt.Scan()fmt.Scanln()bufio.NewScanner(os.Stdin)

多组输入、大批量输请使用 bufio.Reader 来替换 fmt.Scan ❗️ 不然可能会出现超时

OJ 在线编程常见输入输出练习场

输入: 包括两个正整数 a, b (1 <= a, b <= 10^9),输入数据包括多组
输出: a+b 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组数不确定,组内元素数量确定,无结束输入条件
package main
import "fmt"
func main() {
var a, b int
for {
//也可以用 fmt.Scan
n, _ := fmt.Scanln(&a, &b)
if n == 0 {
break
} else {
fmt.Printf("%d\n", a+b)
}
}
}

输入: 第一行包括一个数据组数 t (1 <= t <= 100),接下来每行包括两个正整数 a, b (1 <= a, b <= 10^9)
输出: a+b 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 组数确定,组内元素数量确定
func main() {
var t, a, b int
fmt.Scanln(&t)
for i := 0; i < t; i++ {
fmt.Scanln(&a, &b)
fmt.Printf("%d\n", a + b)
}
}

func main() {
reader := bufio.NewReader(os.Stdin)
// 输入的组数
time, _ := reader.ReadString('\n')
times, _ := strconv.Atoi(strings.TrimSpace(time))
// 每一组两个元素
for i := 0; i < times; i++ {
// 读取 n 和 k
line, _ := reader.ReadString('\n')
params := strings.Fields(line)
n, _ := strconv.Atoi(params[0])
k, _ := strconv.Atoi(params[1])
fmt.Println(n, k)
}
}

输入: 包括两个正整数 a, b (1 <= a, b <= 10^9),输入数据有多组,如果输入为 0 0 则结束输入
输出: a+b 的结果

1
2
3
4
5
6
7
8
9
10
11
12
// 组数不确定,组内元素确定,有结束输入条件
func main() {
var a, b int
for {
fmt.Scanln(&a, &b)
if a == 0 && b == 0 {
break
} else {
fmt.Printf("%d\n", a + b)
}
}
}

输入: 输入数据包括多组。每组数据一行,每行的第一个整数为整数的个数 n (1 <= n <= 100),n 为 0 的时候结束输入。接下来 n 个正整数,即需要求和的每个正整数。
输出: 每组数据输出求和的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 组数不确定,组内元素数量确定并用数组装,有结束输入条件
func main() {
var t int
for {
var sum int
fmt.Scan(&t)
if t == 0 {
break
} else {
a := make([]int, t)
for i := 0; i < t; i++ {
fmt.Scan(&a[i])
sum += a[i]
}
}
fmt.Println(sum)
}
}

输入: 第一行包括一个正整数 t (1 <= t <= 100),表示数据组数。接下来 t 行, 每行一组数据。每行的第一个整数为整数的个数 n (1 <= n <= 100)。接下来 n 个正整数,即需要求和的每个正整数。
输出: 每组数据输出求和的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 组数确定,组内元素数量确定
func main() {
var t int
fmt.Scan(&t)
for i := 0; i < t; i++ {
var num, sum, a int
fmt.Scan(&num)
for j := 0; j < num; j++ {
fmt.Scan(&a)
sum += a
}
fmt.Println(sum)
}
}

输入: 输入数据有多组,每行表示一组输入数据。每行的第一个整数为整数的个数 n(1 <= n <= 100)。接下来 n 个正整数, 即需要求和的每个正整数。
输出: 每组数据输出求和的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 组数不确定,组内元素数量确定并用数组装,无结束输入条件
func main() {
var t int
for {
var sum int
n, _ := fmt.Scan(&t)
if n == 0 {
break
} else {
nums := make([]int, t)
for i := 0; i < t; i++ {
fmt.Scan(&nums[i])
sum += nums[i]
}
}
fmt.Println(sum)
}
}

输入: 输入数据有多组,每行表示一组输入数据。每行不定有 n (1 <= n <= 100) 个整数,空格隔开。
输出: 每组数据输出求和的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 组数不确定,组内元素数量不确定⚠️,无结束输入条件
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)

func main() {
sc := bufio.NewScanner(os.Stdin)
//每次读入一行
for sc.Scan() {
//通过空格将他们分割,并存入一个字符串切片
strs := strings.Split(sc.Text(), " ")
var sum int
for i := range strs {
//将字符串转换为 int
val, _ := strconv.Atoi(strs[i])
sum += val
}
fmt.Println(sum)
}
}

输入: 输入有两行,第一行 n,第二行是 n 个空格隔开的字符串
输出: 输出一行排序后的字符串,空格隔开,无结尾空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 组数确定,组内元素数量确定
package main

import (
"fmt"
"sort"
"strings"
)

func main() {
var t int
fmt.Scanln(&t)
strs := make([]string, t)
for i := 0; i < t; i++ {
fmt.Scan(&strs[i])
}
sort.Strings(strs)
fmt.Println(strings.Join(strs, " "))

// sc := bufio.NewScanner(os.Stdin)
// for sc.Scan() {
// strs := strings.Split(sc.Text(), " ")
// sort.Strings(strs)
// fmt.Println(strings.Join(strs, " "))
// }
}

输入: 多个测试用例,每个测试用例一行。每行通过空格隔开,有 n 个字符串,n<100
输出: 对于每组测试用例,输出一行排序过的字符串,每个字符串通过空格隔开

1
2
3
4
5
6
7
8
9
// 组数不确定,组内元素数量不确定⚠️
func main() {
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
strs := strings.Split(sc.Text(), " ")
sort.Strings(strs)
fmt.Println(strings.Join(strs, " "))
}
}

输入: 多个测试用例,每个测试用例一行。每行通过 , 隔开,有 n 个字符串,n<100
输出: 对于每组用例输出一行排序后的字符串,用 , 隔开,无结尾空格

1
2
3
4
5
6
7
8
9
// 和上面那个一样的
func main(){
sc := bufio.NewScanner(os.Stdin)
for sc.Scan(){
strs := strings.Split(sc.Text(),",")
sort.Strings(strs)
fmt.Println(strings.Join(strs, ","))
}
}

构造二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import "fmt"

type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}

func constructBinaryTree(array []int) *TreeNode {
var root *TreeNode
nodes := make([]*TreeNode, len(array))
// 初始化二叉树节点
for i := 0; i < len(nodes); i++ {
var node *TreeNode
if array[i] != -1 {
node = &TreeNode{Val: array[i]}
}
nodes[i] = node
if i == 0 {
root = node
}
}
// 串联节点
for i := 0; i*2+2 < len(nodes); i++ {
if nodes[i] != nil {
nodes[i].Left = nodes[i*2+1]
nodes[i].Right = nodes[i*2+2]
}
}
return root
}

// 层序列遍历
func printBinaryTree(root *TreeNode) {
res := make([][]int, 0)
if root == nil {
fmt.Println(res)
return
}
curLevel := []*TreeNode{root}
for len(curLevel) > 0 {
nextLevel := make([]*TreeNode, 0)
vals := make([]int, 0)
for _, node := range curLevel {
vals = append(vals, node.Val)
if node.Left != nil {
nextLevel = append(nextLevel, node.Left)
}
if node.Right != nil {
nextLevel = append(nextLevel, node.Right)
}
}
res = append(res, vals)
curLevel = nextLevel
}
fmt.Println(res)
}

func main() {
// 用 -1 来表示 nil
array := []int{4, 1, 6, 0, 2, 5, 7, -1, -1, -1, 3, -1, -1, -1, 8}
root := constructBinaryTree(array)
printBinaryTree(root)
}

武器库

标准库容器

  • slice(切片)
  • map(散列表)
  • list(双向链表)
  • heap(堆)
  • ring(环)

排序函数

1
2
3
4
5
6
7
8
9
10
11
12
import "sort"

// in ascending order.
func sortExample(exampleSlice []int) {
sort.Slice(exampleSlice, func(i, j int) bool {
return exampleSlice[i] < exampleSlice[j]
})
}

func sortExample(exampleSlice []int) {
sort.Ints(exampleSlice)
}

反转切片

1
2
3
4
5
6
func ReverseSlice(slice []int) {
for i := 0; i < len(slice)/2; i++ {
j := len(slice) - i - 1
slice[i], slice[j] = slice[j], slice[i]
}
}

使用 Map 计数

1
2
3
4
5
6
7
8
// Example of using map for counting occurrences of elements in a slice.
func countOccurrences(nums []int) map[int]int {
counts := make(map[int]int)
for _, num := range nums {
counts[num]++
}
return counts
}

检查 Map 中是否存在某个元素

1
2
3
4
5
6
// Example of using map for checking if an element exists.
func checkElements(numsMap map[int]int, certainValue int) bool {
// Check if the certainValue exists in the numsMap
_, exists := numsMap[certainValue]
return exists
}

栈的 Push 和 Pop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
)

// Stack Define a struct for the Stack
type Stack struct {
data []interface{}
}

// Push adds an element to the top of the stack
func (s *Stack) Push(item interface{}) {
s.data = append(s.data, item)
}

// Pop removes and returns the top element from the stack
func (s *Stack) Pop() interface{} {
if len(s.data) == 0 {
return nil // Stack is empty
}
item := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return item
}

// Peek returns the top element of the stack without removing it
func (s *Stack) Peek() interface{} {
if len(s.data) == 0 {
return nil // Stack is empty
}
return s.data[len(s.data)-1]
}

// isEmpty returns true if the stack is empty, false otherwise
func (s *Stack) isEmpty() bool {
return len(s.data) == 0
}

func main() {
// Create a new stack
stack := Stack{}
// Push elements onto the stack
stack.Push(1)
stack.Push(2)
stack.Push(3)
// Peek at the top element
fmt.Println("Top element:", stack.Peek())
// Pop elements from the stack
fmt.Println("Popped element:", stack.Pop())
fmt.Println("Popped element:", stack.Pop())
// Check if the stack is empty
fmt.Println("Is stack empty?", stack.isEmpty())
}

常见排序

冒泡

1
2
3
4
5
6
7
8
9
10
11
12
13
func bubbleSort(nums []int) []int {
if len(nums) <= 1 {
return nums
}
for i := 0; i < len(nums); i++ {
for j := 0; j < len(nums)-i-1; j++ {
if nums[j] > nums[j+1] {
nums[j], nums[j+1] = nums[j+1], nums[j]
}
}
}
return nums
}

快速

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 快速排序 */
func quickSort(nums []int, left, right int) {
// 子数组长度为 1 时终止递归
if left >= right {
return
}
// 哨兵划分
pivot := partition(nums, left, right)
// 递归左子数组、右子数组
quickSort(nums, left, pivot-1)
quickSort(nums, pivot+1, right)
}

/* 哨兵划分 */
func partition(nums []int, left, right int) int {
// 以 nums[left] 为基准数
i, j := left, right
for i < j {
for i < j && nums[j] >= nums[left] {
j-- // 从右向左找首个小于基准数的元素
}
for i < j && nums[i] <= nums[left] {
i++ // 从左向右找首个大于基准数的元素
}
// 元素交换
nums[i], nums[j] = nums[j], nums[i]
}
// 将基准数交换至两子数组的分界线
// 此时 i 位置上的值并不会比基准数大
nums[i], nums[left] = nums[left], nums[i]
return i // 返回基准数的索引
}

华为笔试回顾

2024.05.22 实习笔试沿用到秋招

2024.10.25 面试没问到笔试复盘

题目:给定两个链表,获取两者中相同节点值的 最大连续片段。如果没有公共片段,返回 -1

解答要求:时间限制: C/C++ 1000ms, 其他语言: 2000ms 内存限制: C/C++ 256MB, 其他语言: 512MB

输入:第一行表示链表 1,第二行表示链表 2,每条链表长度不超过 20 个元素,链表不会为空

输出:公共链表片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 查找两个链表中最长的公共连续片段
func findMaxCommonSegment(head1 *ListNode, head2 *ListNode) []int {
list1 := linkedListToList(head1)
list2 := linkedListToList(head2)

n := len(list1)
m := len(list2)

// 创建 DP 表
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m+1)
}

maxLen := 0 // 记录最长公共片段的长度
endIdx := -1 // 记录最长公共片段的结束位置

// 动态规划填表
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if list1[i-1] == list2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
if dp[i][j] > maxLen {
maxLen = dp[i][j]
endIdx = i - 1
}
}
}
}

// 如果没有公共片段,返回 -1
if maxLen == 0 {
return []int{-1}
}

// 获取最长公共片段
startIdx := endIdx - maxLen + 1
return list1[startIdx : endIdx+1]
}

// 将链表转换为数组
func linkedListToList(head *ListNode) []int {
var list []int
for head != nil {
list = append(list, head.Val)
head = head.Next
}
return list
}

题目:露天矿采矿作业的特点是规模大,矿石和废料的移动量达到百万吨,运输成本开销较大,需要寻求一种最优的运输路径节省成本。

已知矿场可以划分成 N*M 的网格图,每个网格存在地形的差异,因此通过不同网格时,成本开销存在差异

  1. 标志为’S’的网格为运输起点;
  2. 标志为’E”的网格时运输终点;
  3. 标志为’B’的网格为阻塞点,不允许通行;
  4. 标志为’C’的网格为检查点,矿车在运输路径中,至少需要进入一次检查点。
  5. 标志为‘数字”的网格,其数字表示经过该网格的成本开销。

运输矿车只能上下左右 4 个方向运行,不允许斜对角进入其他网格。必要时可重复进入网格,请根据输入的网格图,寻求一条从 S 网格到 E 网格,并且至少经过一次检查点的最低成本运输路径,并输出其成本开销

解答要求:时间限制: C/C++ 1000ms, 其他语言: 2000ms 内存限制: C/C++ 256MB, 其他语言: 512MB

输入

  • 第一行:网格图的行数 N [3,200] 和网格图的列数 M [3,200],使用空格隔开。
  • 第二行至第 N+1 行:网格图每一行的元素,可以为’S’,E’,’B’,‘C’或者数字 [0,100],并且有且仅有一个’S’和一个’E’,同时存在一个或者多个‘C’,并依次使用空格隔开。

输出:第一行:输出运输最低成本开销。如果不存在可达通路,请输出-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package main

import (
"fmt"
"math"
)

type Node struct {
x, y int // 当前节点的坐标
cost int // 到达该节点的开销
visitedC bool // 是否访问过检查点
}

var directions = [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}

func inBounds(x, y, N, M int) bool {
return x >= 0 && x < N && y >= 0 && y < M
}

// 主函数:最短路径求解
func findMinCostPath(grid [][]string, N, M int) int {
var startX, startY, endX, endY int

// 构建cost图
costGrid := make([][]int, N)
for i := 0; i < N; i++ {
costGrid[i] = make([]int, M)
for j := 0; j < M; j++ {
if grid[i][j] == "S" {
startX, startY = i, j
} else if grid[i][j] == "E" {
endX, endY = i, j
} else if grid[i][j] == "B" {
costGrid[i][j] = math.MaxInt32 // 阻塞点不可达
} else if grid[i][j] == "C" {
costGrid[i][j] = 0 // 检查点本身无需额外成本
} else {
fmt.Sscanf(grid[i][j], "%d", &costGrid[i][j])
}
}
}

// 定义 BFS 队列
queue := []Node{{startX, startY, 0, false}}

// 记录访问状态和最小成本
visited := make([][][]bool, N)
minCost := make([][][]int, N)
for i := range visited {
visited[i] = make([][]bool, M)
minCost[i] = make([][]int, M)
for j := range visited[i] {
visited[i][j] = make([]bool, 2) // 记录是否访问过该点,分为是否经过检查点的两种状态
minCost[i][j] = []int{math.MaxInt32, math.MaxInt32}
}
}
visited[startX][startY][0] = true
minCost[startX][startY][0] = 0

// 开始 BFS
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]

x, y, cost, visitedC := node.x, node.y, node.cost, node.visitedC

// 到达终点并且经过了检查点
if x == endX && y == endY && visitedC {
return cost
}

// 扩展到四个方向
for _, dir := range directions {
nx, ny := x+dir[0], y+dir[1]
if !inBounds(nx, ny, N, M) || grid[nx][ny] == "B" {
continue
}

newCost := cost + costGrid[nx][ny]
newVisitedC := visitedC || grid[nx][ny] == "C"

state := 0
if newVisitedC {
state = 1
}

// 如果新状态的代价小于当前记录的最小代价,则更新并加入队列
if newCost < minCost[nx][ny][state] {
minCost[nx][ny][state] = newCost
queue = append(queue, Node{nx, ny, newCost, newVisitedC})
visited[nx][ny][state] = true
}
}
}

// 无法找到满足条件的路径
return -1
}

func main() {
var N, M int
fmt.Scan(&N, &M)

grid := make([][]string, N)
for i := 0; i < N; i++ {
grid[i] = make([]string, M)
for j := 0; j < M; j++ {
fmt.Scan(&grid[i][j])
}
}

result := findMinCostPath(grid, N, M)
fmt.Println(result)
}

资料