leetcode
leetcode 例题
记录下 leetcode 值得记录的例题
kmp 算法
kmp 算法的原始实现方法在另一篇里面已经写过,实际上就是通过记录模式串的相同的前后缀长度来跳过模式串重新匹配的距离。
1 | public int[] getNext(String pattern) { |
最短回文串
给定一个字符串,问在字符串左边添加一些字母后,形成一个回文串,问形成的回文串中,最短的回文串什么。
- 超时解法
最开始的暴力思维,想得就是遍历这个字符串,找到中间可以作为分隔线的地方(因为回文串其实可以看成在一个分隔点的左右镜像),然后遍历所有的分割线,比较生成的最短回文串。
1 | class Solution { |
- 前后缀思维
其实考虑一个最长的回文串的,肯定是把输入串的逆向输入串拼接到原始字符串的左边。
那么,如果这个逆向的字符串有一部分跟输入串的前一部分是重合的就可以缩短整个长度。
比如
- abaaa 与其逆 aaaba 在红色的地方重合
- 即 abaaa 的前缀 a 与 aaaba 的后缀 a 重合
所以 只需要遍历得到这个相同的前后缀即可。
下述算法,由于需要判断 equals 因此其执行效率趋近 o(n^2)
1 | public String shortestPalindrome(String s) { |
- kmp 思维
上面采用前后缀的方式其实已经接近了 kmp 的思维方式,不过是比较朴素的解法,因为这个时候还可以进一步得到,实际上就是求原串的最长回文前缀,因为这样逆串,反转过来后,与其回文前缀相等的部分,可以直接消去。
那么可以直接拼接成一个最长的字符串,即 s + "#" + reverse
那么只需要找到这个字符串结尾的部分相等的前后缀长度
,即可以在返回结果的时候删除重复的部分。所以可用 kmp 的 next 数组求法找到最长的前后缀,既可以将时间复杂度进一步降低。
1 | public class ShortestPalindromeNew_214 { |
单调栈
找出最具竞争力的子序列
找到其中的一个子序列,满足其字符串比较是最小的结果。
- brute-force
其实查看题意的话,最主要的就是不停的找到最小的数字
- 如果最小的数字 右侧的数字数量大于等于剩下要找的数量,说明继续向右侧寻找最小数
- 如果小于要找的数量,说明现在最小的数及其右侧所有的数字已经组成答案的一部分,因为这个最小数字形成的这个子序列一定是当前这个最小的
- 重复寻找上述过程,直到所有的数字被填充
因此上述过程是一个递归的过程(递归超时,实际上如果能够保存 i -> j 的最小值的位置的话,就不用在递归中每次寻找了,应该能节约很多时间)
1 | // 同一个位置上的数字更小的 int[] 更具竞争力 |
- 单调栈
实际上题目寻找的是最小的数字形成的结果,那么可以用一个单调栈来保存前面的访问的数组,如果当前访问的数字比之前的数字小的话,说明应该从这个数字之后开始寻找,其结果也就是把之前的数组弹出栈
1 | public int[] mostCompetitive(int[] nums, int k) { |
下一个更大元素 II
示例 输入: [1,2,1] 输出: [2,-1,2] 解释: 第一个 1 的下一个更大的数是 2; 数字 2 找不到下一个更大的数; 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
用单调栈来保存之前遍历过的路径,之后访问的数字如果比路径上的数字大的话,说明对于栈中保存的路径上的数字下一个更大的数是当前访问的数。
1 | public int[] nextGreaterElements(int[] nums) { |
移调 k 位数字
问如果在原始字符串中,移掉 k 位数字,最后形成的最小字符串是什么
- 如果 k 大于等于原来字符串的长度,那么就相当于删除了所有的数字,直接返回 “0” 即可
- 考虑 k 小于的情况
- 既然要删除数字,先考虑删除一个数字的情况,如 45 中删除一个数字,那么结果就是删除5 保留4,考虑 54 也是删除5 保留4
- 那既然如此,也就是说,从字符串中删除一个数字的话相当于
删除的数字之前的一个数替代当前这位数
,那么要使结果更小,替代的这个数一定要小于之前的删除的那个数
,即 Dk < D(k-1) 删除 Dk
- 每次遍历删除一个数字
1 | public String removeKdigitsDeleteOne(String num, int k) { |
- 单调栈
每次去删除的一个数字的时间效率太低,因此可以考虑用一个单调栈来保存小于当前数的数字
,当遍历到的下标大于单调栈的尾时,说明前面的数字该删除了
1 | public String removeKdigits(String num, int k) { |
滑动窗口
最小覆盖子串
滑动窗口
- 当前遍历的字符串没有包含所有字符的时候,右移右游标
- 然后左移左游标,直到不再包含该字符串
- 在移动窗口的时候不听比较即可
1 | func checkEqualMap(mapForT, window map[int32]int) bool { |
1 | class Solution { |
K 个不同整数的子数组
找到 A 里面的连续子数组,其中子数组里面的数据的 distinct 只有 K 个
这个题目一想就是滑动窗口
但是 很不好计算 等于 K 的时候 数组有多少个
但是计算 小于等于 K 的比较好计算,可以依据以下规则
以 [1,2,1,2,3] 为例,左边界固定的时候,恰好存在 2 个不同整数的子区间为 [1,2],[1,2,1],[1,2,1,2],总数为 3。其值为下标 3 - 1 + 1,即区间 [1..3] 的长度。
因为,left, right 同时圈定了一组满足 <= k 的题意的长度范围
那么,包含 left 的子数组数量肯定是 right - left + 1,因为相当于每次给数组里面添加一个数([1,2] [1,2,1] [1,2,1,2]) 所以 right 比 left 多几个数 就能形成几个子数组
1 | func subarraysWithKDistinct(A []int, K int) int { |
1 | import java.util.HashMap; |
最大连续 1 的个数 III
最大连续 1 的个数,A 中只有 0 和 1,其中可以变换最多 K 个 0 成为 1,问最长的连续 1 的长度为多少
滑动窗口,窗口中最多含有 K 个 0 即可
1 | func longestOnes(A []int, K int) int { |
绝对差不超过限制的最长连续子数组
给定一个数组 nums 和 limit,找到最长的连续数组,其中任意两个数的差值不超过 limit
上面这句话换个说法说的就是 最大值和最小值 之差不超过 limit,因此如果能够 o(1) 的拿到窗口的 最大最小值,那么就比较方便
1 | package classic |
爱生气的书店老板
给定一个 grumpy 以及 custormers 在 grumpy == 0 的时候 可以加上 custormers 的对应值,问如果有连续的 X 个 gurmpy 可以为 0 最大 customers 的和为多少
- 自己的做法
维护一个前缀和数组 sum,表示前 i 个的和为多少,那么就可以用滑动窗口将数组分为三段
[0->l](可以用 sum 数组求得) [l->r](全部变为 0 所以是直接求和的) [r->len](可以用 sum 数组求得)
1 | public int maxSatisfiedWithSumArray(int[] customers, int[] grumpy, int X) { |
- 题解
题解更进一步,将 customers 数组根据 grumpy 的取值分为两类,一类是 grumpy 等于 1 那么是可以直接加上的,一类是 grumpy == 0,可以在长度为 X 的滑动窗口中 increase 到 第一类的
1 | public int maxSatisfied(int[] customers, int[] grumpy, int X) { |
二进制题目
连接连续二进制数字
题目要求的是将 1 -> n 的二进制的字符拼接起来表示一个超大的二进制数,然后将 二进制 数转换成为 十进制 数,并模上 1000000007。
如果直接把每个数字转换成为二进制数,然后拼接转换,因为会超时,因为 n 的返回达到了 10^5。
所以考虑在遍历的时候直接对每一位数进行处理。
观察事例,可以看到其实相当于 1(1) << 4 位数 10(2) << 2 11(3) 不变,所以只需要在每个数字遍历的时候,将上一个数字形成的结果左移当前数字对应的二进制的位数的长度,加上该数即可
n = 3, res = 27 二进制表示为 1 -> 1, 2 -> 10, 3 -> 11 27 二进制表示为 11011
计算二进制数的长度的时候,可以简单的采用遍历的方式进行。
- [1] 一位数长度
- [2,3] 二位数长度
- [4,……,7] 三位数长度
- [8,……,15] 四位数长度
也就是说,长度也是一个可以从上一个长度推断来的,每次需要新增长度的时候,都是形成 2 的幂 的形成,可以用 i & (i - 1) 来快速的判断 2 的幂。
1 | private static int mod = 1000000007; |
模拟除法
不能使用乘法、除法和 mod 运算符。
除法的本质,以 10 / 3 为例
10 / 3 = 3 …… 1 (即为 3 个 3 相乘 余 1)
即为 10 - (3 _ 2) - (3 _ 1) = 1 其结果为 2 + 1 为 3
也就是说任意一种除法可以用一组除数的 2 的次方的乘积的结果来表示。
如 100 / 15 = 6
100 - (15 _ 4) - (15 _ 2)
所以可以采用二进制的方法来做,每次用被除数减去最大的一个除数的 2 次方的乘积,循环,直到剩下余数或者 0
1 | func divide(dividend int, divisor int) int { |
1 | class Solution { |
只出现一次的数字 II
数组中的数组只有出现 1 次(一个数字)的和 3 次的数字,找到只出现一次的那个数字
其实就是计算每一位数字出现的次数 % 3
注意 goland 默认的 int 可能值得是 int64 所以强制指定为 32 为长度的 int32 不然没办法处理负数的情况
1 | func singleNumber(nums []int) int { |
只出现一次的数字 III
一组数字 其中只有两个数字 出现一次 其余出现两次
1 | func singleNumberIII(nums []int) []int { |
数字按位与
要求求 m -> n 的范围内的所有数字的 按位与 的结果,因为范围比较大,直接 & 会超时
考虑 3 -> 11 这个范围的数字,红色的 就是相同的二进制前缀部分 实际上就是找到这部分前缀
001011 11 001010 10 001001 09 001000 08 000111 07 000110 06 000101 05 000100 04 000011 03
1 | func rangeBitwiseAnd(m int, n int) int { |
动态规划
最后一块石头的重量 II
其实就是问是否能够形成相等的两部分, 用一个 dp[i][j] 表示前 i 个的能否形成和为 j 的数值,在遍历的时候就可以找到最大的和为多少,之后就减去最大的和即可
1 | package classic |
最长子序列套题
最长上升子序列
找到非连续的递增子序列,那么我就只需要知道 在我之前的小于我的数字的上升子序列长度为多少
即实际上只需要在访问数组的时候 0 ≤ i < j < nums.length,只需要知道 i 下标对应的最长的子序列是多少即可。
这样就变成了一个 dp 问题,小问题就是解决的以 nums[i] 结尾的最长的上升子序列的长度
1 | func lengthOfLIS(nums []int) int { |
最长上升子序列数量
与上面那个类似 也是一个 dp 问题 只是需要在 dp 遍历的时候 知道 对应最长长度 对应的 LIS 有多少个
1 | // 找到 LIS 对应的长度的子序列有多少个 |
摆动序列
找到摆动序列(摆动序列是一升一降的序列,即前后相减为一正一负)参考注释即可 (这个题目不要求连续 所以还需要不停的保存前一个状态 不用初始化)
1 | // 两个数组分别代表上升和下降序列的最大长度 |
因为只依赖前一个状态 因此可以压缩状态
1 | // 两个数组分别代表上升和下降序列的最大长度 |
类似摆动序列的题目 978. 最长湍流子数组
找到一个连续的子数组
能够满足
当 A 的子数组 A[i], A[i+1], …, A[j] 满足下列条件时,我们称其为湍流子数组:
若 i <= k < j,当 k 为奇数时, A[k] > A[k+1],且当 k 为偶数时,A[k] < A[k+1];
或 若 i <= k < j,当 k 为偶数时,A[k] > A[k+1] ,且当 k 为奇数时, A[k] < A[k+1]。
也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。
1 | func maxTurbulenceSize(arr []int) int { |
俄罗斯套娃信封问题
根据信封的宽度和高度 判断能够装下的信封的最大长度有多少
高度和宽度均小于另外一个信封的 可以装进去
实际上是一个 找到最长递增序列的问题
- 按照宽度进行排序,这样从一个维度上看 所有的信封都是宽度有序的
- 再次基础上 如果要前一个信封能够装在后一个信封里面 说明长度是一个逆序的
- 最后只需要在这个排序的数组里面 找到长度的一个最长递增序列即可
1 | func maxEnvelopes(envelopes [][]int) int { |
解码方法
这道题是入门的动态规划 只要知道 前一个和前前个的状态,就可以转移到下一个状态
1 | func numDecodings(s string) int { |
子序列
不同的子序列
1 | func numDistinct(s string, t string) int { |
打家劫舍系列题
打家劫舍 I
这道题是经典的 dp 问题。题目要求的是不能抢劫相邻的位置,那么这种条件下的最大和是多少。
- 一个位置会有两个状态,拿当前这个地方的值 或者 不拿
- 下个位置的状态就会由上一个位置决定
- 如果当前位置拿了值的话,上一个位置只能不拿
- 如果当前位置没有拿,上一个位置只需要取拿 or 不拿的 较大值
优化下 dp 数组 其实可以用一对值表示前面一个循环中拿了的最大值即可
1 | func rob(nums []int) int { |
打家劫舍 II
这个是打劫的循环数组,因为 rob 了第一个 就不能 rob 最后一个
所以分别访问从 [1:len(nums)] 和 [0:len(nums)-1] 然后比较大小即可
1 | func rob(nums []int) int { |
打家劫舍 III
这次是树,实际上还是要知道子节点上的话 rob 和 notRob 的状态即可,然后递推到当前的状态
1 | func rob(root *TreeNode) int { |
贪心算法
转换罗马字
1 | package classic |
jumpGame
1 | func canJump(nums []int) bool { |
递归
执行乘法运算的最大分数
给你两个长度分别 n 和 m 的整数数组 nums 和 multipliers ,其中 n >= m ,数组下标 从 1 开始 计数。
初始时,你的分数为 0 。你需要执行恰好 m 步操作。在第 i 步操作(从 1 开始 计数)中,需要:
选择数组 nums 开头处或者末尾处 的整数 x 。
你获得 multipliers[i] * x 分,并累加到你的分数中。
将 x 从数组 nums 中移除。
在执行 m 步操作后,返回 最大 分数。
- 暴力解法
暴力解法就是直接根据每次取的不同字符生成一颗二叉树,然后在二叉树上进行遍历得到结果
1 | class Solution { |
- 带 memo
观察上述的结果的话,可以首先进行的优化是去除 dequeue 的使用,直接使用一个范围框定 nums 的选取
1 | public int maximumScore(int[] nums, int[] multipliers) { |
但是上述的方法仍然超时,因为遍历这颗形成的二叉树的时候,会有重复的访问情况,可以观察到的是 left + n - 1 - right == index
,因为从左边选取的数字数量和右边选取的数字的数量,肯定是 multipliers 选取的数量。
1 | public int maximumScore(int[] nums, int[] multipliers) { |
括号生成
1 | // generateParenthesis 入口函数 |
1 | public List<String> generateParenthesis(int n) { |
正则表达式匹配
这道题可以用递归的思想去做,也可以采用 dp 的方法。实际上递归就是从上向下的 dp
1 | func isMatch(s string, p string) bool { |
1 | public boolean isMatch(String s, String p) { |
数据结构
栈和队列
队列-滑动窗口的最大值
最大 queue 的队列
1 | type MaxQueue struct { |
使用代码
1 | func maxSlidingWindow(nums []int, k int) []int { |
栈-计算器
中值表达式转波兰表达式(实际上是)
链表
删除倒数的第 N 个节点
1 | func removeNthFromEnd(head *ListNode, n int) *ListNode { |
合并 k 个已经排序的链表
类似归并排序
1 | package classic |
翻转链表一系列
反转链表
最简单的反转链表的思路肯定是直接用一个 stack,FILO 的机制来反转,而不采用额外的空间可以用一下的方法
1 | func reverseList(head *ListNode) *ListNode { |
反转链表 II
反转链表 II 是反转链表下表从 m -> n 的一个链表,实际上采用上述的反转的操作,即可反转 m -> n 之间的节点
1 | // 主函数 |
reverse K group
reverse K group 的更进一步,在上面一题的基础上,每 K 个节点反转一次
1 |
|
树
树的遍历
中序遍历
1 | func inorderTraversal(root *TreeNode) []int { |
前序遍历
1 | package classic |
后序遍历
1 | func postorderTraversal(root *TreeNode) []int { |
层次遍历
- 普通层次遍历
1 | func levelOrder(root *TreeNode) [][]int { |
- zigzag 的层次遍历
1 | package classic |
前缀树
实现用字符串的前缀来索引的结构树
1 | // Trie 之间通过 字符 关联 上一个 trie 会通过字符作为边连接下一个节点 |
图
并查集的数据结构
并查集,表示的是一个树形的结构
如图所示,针对图的一个极大连通分量,会形成一个对应的树结构(并查集只关注一个连通分量有多少连接点,不关注内部的其他的细节)
所以针对查找连通分量有哪些,以及连同量间的关系有作用
并查集存储数据的结构
1 | type Union struct { |
并查集的操作
- union (联合,关联两个点)
- find (查找,找到当前点的最终的父节点)
所以,实际上 如果 r1 r2 之间有连接线的话,要关联 r1 r2 的操作就是。
- 就是通过
find
找到分别的根节点 r1Root r2Root - 在通过
union
方法关联两个根节点,实际上就是将 r2Root 作为一个子节点,挂载到 r1Root 下
所以整体的数据结构为
1 | type unionFind struct { |
执行交换操作后的最小汉明距离
这道题实际上是找连通分量,对比两个数组中相应的连通区域不等的部分,所以可以用无向图连通分量 dfs
or 并查集
得到连通分量后进行处理。
1 | import java.util.Arrays; |
连通网络的操作次数
题目所述,给定一个图,找到将其所有最大连通分量连通所需更改的最少的边的数量为多少。
图的所有最小连通为一个树,即需要 n 个节点有 n - 1 条边。
所以题目其实是要找到这个图里面有多少独立的连通分量,然后判断其是否可以连接
- 第一种思路就是直接 dfs 遍历,找到所有的连通分量。首先判断边的数量是否足够 n - 1 这个时候,如果有多个连通分量,说明某个连通分量一定有多的边,随意选取其中的边即可
1 | package classic |
- 并查集,找到每个群组的数据的一个代表点
1 | // findRoot 找到根节点 |
由斜杠划分区域
采用并查集,但是这道题有特殊的地方。
题目中所示,针对一个方格有 / 和 \ 两种,如下
---- ---- |\ | | /| | \| |/ | ---- ----
总之,针对一个 方格 ,可以把他看成四个部分
那么,也就是说,
- 如果当前 char == ‘ ‘ 表示 0 1 2 3 都是联通的
- 如果 char == ‘\‘ 表示 01 23 分别连通
- char == ‘/‘ 表示 03 12 分别连通
内部的连通完毕后,
- 还可以知道 1 一定跟下一个 3 连通
- 2 一定跟下一行的 0 连通
1 | // regionsBySlashes 通过斜杠划分 |
水位上升的泳池中游泳
这道题没想明白最开始,肯定是明白要知道到什么时候 [0,0] 跟 [n - 1, n - 1] 的右下角相连
相连的判断可以通过并查集
实现
那么就要解决几个问题:
- 怎么遍历
- 并查集连接的条件是什么
- 怎么把二维数组的位置抽象到一维
从以下几个方面入手
- 题目中所述 grid 中的数值从 [0, nn-1] 的唯一数值,也就是说每个格子的高度都是独立的,因此只需要遍历高度,在遍历高度中如果 [0, nn-1] 相连,即完成连接
- 由于题目中 当遍历的位置达到一个高度的时候,他可以直接和上下左右上的相连,也就是说遍历到高度更高的地方能够直接比高度更低的地方连接
- 而题目中的 grid 的棋盘的二维数组可以通过简单的
n*x+y
抽象到一维
1 | package classic |
拓扑排序
- 逆后续排列
- 遍历出度为 0 的点
课程表
这道题本质上就是拓扑排序
简单的做法就是用 dfs 去判断是否成环
1 | func canFinish(numCourses int, prerequisites [][]int) bool { |
拓扑排序 遍历入度为 0 的点
1 | func canFinish(numCourses int, prerequisites [][]int) bool { |
最短路径
地图分析
找到多源最短路,改造了一下 dijkstra 算法
1 | public int maxDistance(int[][] grid) { |
1786. 从第一个节点出发到最后一个节点的受限路径数
说实话 我是看不懂受限路径到底是什么意思,所以看了下他们的思路,自己实现了一下。现在贴上原题
现有一个加权无向连通图。给你一个正整数 n ,表示图中有 n 个节点,并按从 1 到 n 给节点编号;另给你一个数组 edges ,其中每个 edges[i] = [ui, vi, weighti] 表示存在一条位于节点 ui 和 vi 之间的边,这条边的权重为 weighti 。
从节点 start 出发到节点 end 的路径是一个形如 [z0, z1, z2, …, zk] 的节点序列,满足 z0 = start 、zk = end 且在所有符合 0 <= i <= k-1 的节点 zi 和 zi+1 之间存在一条边。
路径的距离定义为这条路径上所有边的权重总和。用 distanceToLastNode(x) 表示节点 n 和 x 之间路径的最短距离。受限路径 为满足 distanceToLastNode(zi) > distanceToLastNode(zi+1) 的一条路径,其中 0 <= i <= k-1 。
返回从节点 1 出发到节点 n 的 受限路径数 。由于数字可能很大,请返回对 109 + 7 取余 的结果。
模仿的解法
超时,怀疑是 构建图 花费太多时间
1 | import microsoft.PlusOne; |
博弈问题
预测赢家
分别从数组的两端取值,问最后谁获胜。
模拟取值的过程即可
1 | public boolean PredictTheWinner(int[] nums) { |
石子游戏
石子游戏 VII
杂题
递增的三元子序列
- 首先想到嘛,用两个数组分别存储从左到右的最小值和最右到左的最大值,那么如果 nums 中一个数 num 大于这个最小值小于这个最大值,是一定可以的
1 | func increasingTriplet(nums []int) bool { |
- 还可以进一步优化
他实际上是找这么一组 3 个数 num1 < num2 < num3
那么如果我在 num3 之前找到了这两个数字 num1 num2 即可
所以用 一个 min mid 来记录之前找到的 num1 num2。
其中 min 保存之前遇到的最小值, mid 保存之前 大于 min 的最小值,那么 如果碰到 同时大于 min、mid 的数 就可以直接返回 true 了。
但是可能遇到这种情况,在访问找到 num3 的时候,min 对应的数字的下标在 mid 之后。
但是考虑这种情况的话,一定有一个小于 mid 的 历史 min 值在 mid 之前,所以其实还是一个完整的三元组。
1 | class Solution { |
二分
袋子里最少数目的球
给你一个整数数组 nums ,其中 nums[i] 表示第 i 个袋子里球的数目。同时给你一个整数 maxOperations 。
你可以进行如下操作至多 maxOperations 次:
选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。
你的开销是单个袋子里球数目的 最大值 ,你想要 最小化 开销。
请你返回进行上述操作后的最小开销。
- bruteforce
直接的做法就是不停的找到数组中最大的数,然后在 maxOperations 的次数限制内进行分隔,找到分隔中最小的数据。
为了 o(1) 的找到最大的数,所以使用的 pq 来保存中间数据
1 | // bruteforce 模拟的方法 通过对递归的方法对最大数进行不停的分隔 得到最后的结果 |
- 二分查找
二分查找可以用于查找最小的最大值,最大的最小值等情况
。
那么可以以结果作为区间,每次判断这个最小开销是否能够实现,就可以去缩短遍历的范围。
但是需要知道如何找到能否实现这个函数:
- 当用 mid 去规定最小开销的时候,意味着所有大于 mid 的数字都需要被拆分到最小开销中
- 拆分的时候,如 num = 8, mid = 4, 那么只需要拆分一次即可,如 num = 17, mid = 7,那么需要拆分成[7,7,3] 需要拆分两次。所以拆分的代价是 num / mid,在 num % mid == 0 时要减一
1 | // 二分查找 二分的范围是返回的最小结果 |
找出第 k 小的距离对
距离差定义为 数组中 任意一对数之间的差的绝对值
- 因此找到第 K 个最小距离,一个直观的解法就是,遍历所有的差,放入到只有 k 个数的大顶堆中,那么堆顶都是结果。(memory 爆了)
1 | type IntHeap []int |
- 另外一个想法就是二分
- 二分的范围是从差值的范围出发,即 0 -> max(nums) - min(nums),那么 max min 可以直接排序取首尾即可
- 但是如何统计,二分中小于 mid 的差值的数量 以 [1,2,2,3,4] 为例,固定右边界为 4 的时候 1 -> 4 中间可能小于 k 的数字组合为 [1,4] [2,4] [2,4] [3,4] 其结果为 j - i = 4 - 0 (4 的下标 4 1 的下标 1)
1 | func smallestDistancePair(nums []int, k int) int { |
字典树
猜字谜
暴力解法,用 word 去匹配 puzzle 的 set 超时了。
而原题目中 words 的数量比 puzzles 的数量高一个数量级,因此可以使用 字典树 压缩 word 的数量,用 puzzle 进行比较
1 | private class TrieTree { |
计算器
基本计算器
其本质是一个 中值表达式 求值。实际上只需要注意 符号的 优先级即可。(PS:·· 好多细节没注意到 就会 gg)
TODO: 中值表达式 转成 逆波兰表达式
1 | import java.util.Deque; |
数学问题
1802. 有界数组中指定下标处的最大值
给你三个正整数 n、index 和 maxSum 。你需要构造一个同时满足下述所有条件的数组 nums(下标 从 0 开始 计数):
nums.length == n
nums[i] 是 正整数 ,其中 0 <= i < n
abs(nums[i] - nums[i+1]) <= 1 ,其中 0 <= i < n-1
nums 中所有元素之和不超过 maxSum
nums[index] 的值被 最大化
返回你所构造的数组中的 nums[index] 。
这个问题就是说构建一个只有正整数的数组,且相邻数字之间差值不能超过 1,问 如何构建才能使 index 下标位置的数最大。
其实偏向于贪心的策略,既然要 index 最大,那么每次遍历的时候,我都在 index 上 +1,看在没有打到 maxSum 的时候能够给这个地方添加几次。最后其实际的生长过程可以看做下面的一个过程。
例输入:n = 4, index = 2, maxSum = 6
- 构建基础数组,因为要求每个数字都为正整数,因此最小为 1
1 1 1 1 _ | index
- 从 index 开始生长
2 1 1 1 1 _ | index
这个时候 已经不能再加 1 了 所以直接返回 2
所以其实就是构建一个题型的台状结构,每层比下一层只会高 1 个,最后到 index 的位置最高即可。
1 | class Solution { |
132 模式
在一个数组中找到下标 i < j < k 满足 nums[i] < nums[k] < nums[j] 也就是说 j 对应的数值 是三个中最大的 k 次之,最小的是 i
那么很容易知道 i 的值其实需要越小越好,越小的话,后面 k、j 的条件就最好满足,因此第一步就是求出从左向右的最小值数组
- brute force 的方法(o(n^2))
既然已经知道 i 取从左向右的最小值,那么只想就需要确定 j < k 且 nums[i] < nums[k] < nums[j],也就是在后续的数组中找到一对逆序的数组,那么 o(n^2) 的算法就很好写了
1 | // 找到 1 3 2 模式 |
- 优化的算法(o(n))
之前的算法寻找逆序的时候,是在确定了 j 值 的情况下从前向后寻找 k 值,如果能够知道之前访问的过的 j 值,作为当前 k 值,那么就可以进一步的降低复杂度。
因为,在满足 nums[j] > leftMin[j] (即 nums[i]) 时,如果 k 值正好取到小于 nums[j] 的值 且 大于 nums[i] 时满足条件。所以 nums[k] 的值 在取小于 nums[j] 的值的时候 越大越好,因为这样才可能更大程度的满足 nums[k] > nums[i] 的条件。
所以使用一个单调栈来保存 j 之后遍历的历史情况,越靠近栈底的值越大,只需要取到栈中需要的满足小于 nums[j] 的最大值即可。
这样 栈中还保存着较大的值,之后再遍历的时候,还以用这个比 nums[j] 大的值,与 j 之前更大的值匹配成 132 组合。 而 j 之后的较小值,如何满足题设条件,会在第一次访问的时候就返回了,所以也不会存在漏的结果。
1 | public boolean find132pattern(int[] nums) { |
最大子序和
简单的题型
简单的题型如 剑指offer 上所述,只需要用一个数组保存以当前结尾的最大子序和即可。转移的时候,如果之前的最大子序和小于 0,说明应该重新开始计数。
1 | class Solution { |
删除一次得到子数组最大和
- 直觉想法
拿到这个题目的第一个直觉就是跟上面那个基本一致,但是需要删除一个数字,那么删除的这个数字一定存在这样的性质。
删除这个数字后,有可能左右的最大子序和加起来更大,所以这个数字一定是负数。
那么,只需要知道这个数字左边和右边的分别的最大子序和即可,那么就可以用两次上面的算法,得到以 i 结尾的从左向右最大子序和 和 以 i 结尾的从右想左的最大子序和即可
1 | // 最大子序和的变种 如果中间可以删除一个数字 问能够形成的最大子序和为多少 |
- 两个 dp 数组保存状态
那么可以使用一个循环得到结果。
- 仍然需要一个数组保存以 arr[i] 结尾时的最大子序和
- 需要一个数组保存以 arr[i] 结尾时删除一个数字的最大子序和
那么删除的这个数字可能是遍历的 arr[i] 或者 之前就已经删除了一个数字,arr[i] 不能被删除。所以第2个数组的更新策略即deleteOne[i] = Math.max(deleteOne[i - 1] + arr[i], dp[i - 1])
。
即保留当前的 arr[i] 那么只能取之前删除了一次的最大子序和 和 删除当前的 arr[i],那么就要去之前没有删除数字的最大子序和 dp[i - 1]。
1 | public int maximumSum(int[] arr) { |