这里是我的 leetcode 做题笔记,以前是用写一篇文章的方式发布 leetcode 做题笔记的,现在觉得,或许开个专栏更好,因为有每日一题的打算,就不用水那么多篇文章了。自从我开始以时间为分类的方式用专栏来记录自己的每日活动,我发现自己表达的欲望也变强了,记录和回过头来检索这些信息的效率也都提高了,真是不错的方法。
2024-02-18
树的前序遍历
1 | /** |
2023-12-11
二分查找+BFS
有种以前做过类似题目的感觉
1 | function minimumEffortPath(heights: number[][]): number { |
2023-11-19
这题是我在整理前端面试题的时候,为了测试我的代码是否正确找出来的。
1 | type MultiDimensionalArray = (number | MultiDimensionalArray)[]; |
2023-11-15
这题太简单了,用到了等差数列和
1 | function maximizeSum(nums: number[], k: number): number { |
2023-11-14
虽说这是道中等题,但是我是第一次接触线段树,而且离谱的是暴力法居然也能通过(那这题就失去意义了)
线段树是用树的方式存储下区间的一些信息,比如区间和,以空间换时间的方式来优化求区间信息的时间复杂度。构造线段树时间复杂度是 O(n),更新线段树是 O(logN),查询是 O(logN)
举个具体的例子来展示一下线段树的原理:
例如我们有个数组:[10, 11, 12, 13, 14]
,构造成线段树就是如下:
用堆来表示线段树那就是如下数组:[60, 33, 27, 21, 12, 13, 14, 10, 11]
此 leetcode 题解代码如下:
1 | class NumArray { |
2023-11-09
这道题稍微有点复杂,主要设计到两个算法,广度优先遍历和二分搜索
我封装出了一个扩散方法:spread
1 | function maximumMinutes(grid: number[][]): number { |
2023-11-08
比较简单
1 | function findTheLongestBalancedSubstring(s: string): number { |
2023-11-07
简单题
1 | function vowelStrings(words: string[], left: number, right: number): number { |
2023-11-06
主要优化点在比较两个字符串,如何在 O(1)时间内完成比较呢?
如果是字符乱序直接比较,那就是 O(n^2)
如果用个 hash 表存下一个字符,那就是 O(n)
用位运算。那就是 O(1)。位运算不仅仅是相当于把字符码好了位置,而且是批量对比(按位与,与一个位和与 N 个位是一个速度),而不是挨个比较。
1 | function maxProduct(words: string[]): number { |
2023-10-26
简单题
1 | function countDigits(num: number): number { |
2023-10-24
这道题一开始没有思路,但简单看了一下答案,发现确实不难,动态规划拿下:
1 | function numRollsToTarget(n: number, k: number, target: number): number { |
2023-10-08
这道题一开始是打算用遍历的方式求 max 和 min,可惜会超时,看了答案知道了要用堆来做。堆更新的时间复杂度是 O(logN)
1 | class PriorityQueue1<T> { |
2023-10-09
这道题的思路是把小的数放高位,把大的数放低位。可以先排序,然后再把数对半分配,分配规则就是:把小的数放高位,把大的数放低位。
代码:
1 | function splitNum(num: number): number { |
2023-05-09
最近朋友提醒我 leetcode 开了 javascript 专栏,有一道典型的深度判断对象相等的题目,我试了一下
代码:
1 | function areDeeplyEqual(o1: any, o2: any): boolean { |
2023-04-10
几乎是简单题
1 | function nextLargerNodes(head: ListNode | null): number[] { |
2023-04-06
这题可以用通用的进制转换方法,先求余数,然后减去余数,然后除以除数,如此循环往复,得出的余数就是每一位上的值。
1 | function baseNeg2(n: number): string { |
2023-04-05
简单题,时间复杂度 O(n)
1 | function commonFactors(a: number, b: number): number { |
2023-04-03
今天这题很简单,我用的快排来找最大元素,速度居然击败了 100%
1 | function prevPermOpt1(arr: number[]): number[] { |
2023-04-02
很简单的字符串处理
1 | function maskPII(s: string): string { |
2023-03-06
随便在一个数组坐标处划一个分界线,左边的 b 的个数+右边的 a 的个数之和最小,即为要删除的最少数目。而这个 a 和 b 的个数,最好是用前后缀和记下来,不然每次都要遍历一遍数组搜集。那就是 O(n^2)的复杂度了。用前缀和记下来,则是 O(n)时间复杂度。
2023-01-13
这题很简单,不过题目描述和举例不是很清楚。就是我不知道,是不是要连续的字符串去配对。试了一下,结果并不需要连续。
1 | function rearrangeCharacters(s: string, target: string): number { |
2023-01-09
这题很简单,按照题目描述进行运算即可,没有任何技术可言。
1 | function reinitializePermutation(n: number): number { |
2023-01-08
这是昨天的每日一题,用了前缀和+滑动窗口(双指针)
1 | function minOperations(nums: number[], x: number): number { |
2021-06-06
474. 一和零
一道动态规划题目。
首先要明白这是一道背包问题,而且是双维度的,可以装 0 和 1。
那么我们就需要一个三维数组 dp 来记录动态规划的子过程的结果,第一个维度代表遍历到第 i 个字符串,第二个维度代表第 j 个 0 的问题规模,第三个维度代表第 k 个 1 的问题规模,依次扩展到我们的目标字符串个数,目标问题规模。
状态转移方程:
- 如果加入当前字符串,导致背包溢出,则不加:dp[i][j][k] = dp[i-1][j][k]
- 如果不溢出,则有两种可能,取最优解:dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-zeros][k-ones]+1)
这里还需要考虑一些边界问题,比如 i=0 的时候,dp[0][any][any]应该是 0,同理 m=0 和 n=0 也是如此。所以我们的数组空间需要每个维度上都加 1 来存放这些初始值。
JavaScript 代码:
1 | /** |
2021-06-05
203. 移除链表元素
很简单的一道删除单链表节点题
JavaScript 代码:
1 | /** |
2021-06-04
160. 相交链表
这题有两种解法:
- 哈希表记录指针
- 双指针
哈希表记录指针
JavaScript 代码:
1 | /** |
双指针
链表总共分为三部分:
- headA 到公共节点
- headB 到公共节点
- 公共部分
所以如果我们利用双指针,把这三个部分走一遍,就能让双指针碰上。
- index1 走 A 链,走完 A 链,走 B 链
- index2 走 B 链,走完 B 链,走 A 链
两个指针同时等于 null 只有一种情况,就是两个链不相交。如果相交,想要都在链尾碰上,则两个链长度相等,若两个链长度相等且相交,则非公共部分长度一定相等,那么他们早就在第一次遍历的时候在公共节点遇上了。
1 | /** |
2021-06-03
凡是涉及到连续子数组的,都可以用前缀和+哈希表来解
525. 连续数组
这题要注意的就是,因为要统计元素个数,所以要使用 i+1。
哈希表的 key 的含义是:当前遍历到的 1 与符合标准(一半是 1)之间的差距,而记录的位置则必须是最小位置。所以只在初始化的时候赋值。
JavaScript 代码:
1 | /** |
2021-05-31
这个问题看起来是个简单题,其实可以从中学到位运算和一些数学知识。
342. 4 的幂
我自己的解法很简单易懂,但是不够高效:
JavaScript 代码:
1 | /** |
这个解法的效率很差,只战胜了 55%的选手。说明肯定有更优解,我翻了一下答案。主要是 2 种角度的解法:
- 位运算
- 数学
位运算
如果是 2 的幂,那么位中只能出现一个 1。如果是 4 的,那么肯定也只有一个 1,且出现的位置是每隔一位出现。那么问题来了,怎么判断位上只有一个 1 呢?操作是:减去 1,然后与。得到的结果必然应该是 0。那如何判断 1 在哪一位上呢?好像只能遍历了。但其实我们不需要知道具体是哪一位,只需要知道是否分布在正确的位上,可以通过 mask 解决:mask=$(01010101010101010101010101010101)_2$
,因为 1 分布在奇数位。也可以写成更简短的 16 进制形式:mask=$(55555555)_16$
JavaScript 代码:
1 | /** |
奇怪的是这个代码的运行时间居然比上面那个还长,感觉不科学。
数学角度
首先依然是按照上面的两个条件:
- n>0
- n 只有一个 0
我们观察到所有偶数分为:$4^x \times 2 \times 2$也就是$4^x$,和$4^x \times 2 \times 1$。而 4 的幂次除以 3 的余数必然是 1,而$4^x \times 2$这种除以 3 的余数必然是 2。
我们增加这个条件筛选出$4^x$
JavaScript 代码:
1 | /** |
2021-05-29
这个问题需要拆分出子问题才好解决,要不然没有思路。它的子问题是:560. 和为 K 的子数组
1074. 元素和为目标值的子矩阵数量
当你理解了子问题之后,我们来想想,怎么把这个问题转换到子问题上呢?也就是如何把二维问题变一维问题呢?
我们想象把一个矩阵的列上的元素全部加起来,不就是一个一维数组了吗。这个一维数组可以等效的应用在这个问题上。
那这样的组合有哪些呢?通过简单的二次遍历,就能得出我们想要的组合:
JavaScript 代码:
1 | for (let i = 0; i < n; i++) { |
每次 i 到 j 之间的数就是我们想要的组合,拿这些数的和,组成新的一维数组,然后用一维数组的解法去解。这里有个小技巧是这个和也要避免重复计算,所以要把每次计算所得存下来,下次在这个基础上算,这样可以省下从头开始求和的时间。
JavaScript 代码:
1 | /** |
简化问题的办法有很多,比如降低问题规模,降低维度,二维 -> 一维。
2021-05-29
这题是在做每日一题中遇到的问题的子问题:1074. 元素和为目标值的子矩阵数量
560. 和为 K 的子数组
遇到这类问题,首先想的是复杂度,然后复杂度天然是跟问题规模有关的。遍历一遍肯定是必要的,当我们遍历到第 n 这个位置,我们怎么判断从 0 到 n 中有多少个解,进一步的,我们还只要增量数据,n-1 的解不应该去重复计算。第 n 这个位置上的数是一定要考虑进去的,所以我们从后往前寻找。具体代码如下:
JavaScript 代码:
1 | /** |
这样的话,算法的时间复杂度是 O(n^2)。有没有重复计算的问题呢,似乎不太好说,但结果是:有,像此类问题有统一的规律,就是我们可以记录前缀和。如果我们知道前缀和,那么我们只需要用当前和减去 k,看是否等于某个前缀和,如果有,我们不就正好找到一个子数组的和等于 k 了吗?所以基于前缀和,我们一次遍历即可解决问题。
JavaScript 代码:
1 | /** |
前缀和对过往的遍历总结提取了信息,使我们不用再去进行重复的计算,是非常重要的技巧。
2021-05-28
这题初看上去特别简单,就是一个 O(n^2)的遍历(组合),对每一组求汉明距离累加起来。不过我一开始就觉得可能会超时,提交后果然超时了。更优的做法是按位遍历,每一位上所有的数要么是 0 要么是 1,把 0 和 1 的个数统计出来,相乘,就是这一位的汉明距离总和。
JavaScript 代码:
1 | /** |
2021-05-27
这道题一看就知道用栈来解决,但具体到怎么做却依旧不容易想通。直到看过答案后,才发现,实际上真的只需要遍历一遍就能解决问题。
思路如下:
每遇到一个括号块,就需要把里面的字符串翻转(这是单步操作),然后递归翻转每一层。这是我们人的思维,但机器是看不到这种宏观信息的,我们需要安排具体到每一步的任务。代码在遍历的时候只会遇到左括号或者右括号,假如我们遇到左括号的时候开始记录字符串,那么在遇到右括号的时候,就有翻转的目标对象了。但如果连续遇到两个左括号呢?我们将记录的信息先入栈,然后继续上面的步骤即可。
具体步骤(单步):
- 遇到左括号:入栈已记录的字符串,清空我们用于记录的变量
- 遇到普通字符:记录
- 遇到右括号:翻转记录的字符串,将栈顶字符串 pop 出来拼接上翻转好的字符串
JavaScript 代码:
1 | /** |