Leetcode Hot100刷题记 JavaScript基础 –待开荒–
Hot100 哈希 1. 两数之和 https://leetcode.cn/problems/two-sum?envType=study-plan-v2&envId=top-100-liked
思考:
最简单的想法是直接暴力遍历,结合题干来看,时间复杂度会做到O(n^2),即最多时要遍历n(n-1)次。
为了压低时间复杂度,可以考虑用Map(哈希表)以空间换时间。(想到哈希表的原因是如果可以把数列进行排序,两数之和就可以相当简单的,根据sums和当前遍历的nums[i]的大小比较确认区间,从而快速地的缩小排查范围,但由于题目要求锁死了数组的item和index一一对应的关系,所以要考虑用其他的数据类型来拓展,哈希表则满足这个需要)
除此之外,本体的Map处理还可以继续优化,考虑到可能出现的相同数字的key覆盖原有的哈希表的键值对的情况,可以采取“先has看有没有,有就直接掉,没有再set”的策略,可以把遍历次数压到一次以内。
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 var twoSum = function (nums, target ) { let arrayMap = new Map () let res = [] nums.forEach ((item,index )=> { arrayMap.set (item,index) }) for (let i=0 ;i<nums.length ;i++){ res.push (i) let remain = target - nums[i] if (arrayMap.has (remain) && arrayMap.get (remain) != i){ res.push (arrayMap.get (remain)) break } res = [] } return res };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var twoSum = function (nums, target ) { const map = new Map (); for (let i = 0 ; i < nums.length ; i++) { const complement = target - nums[i]; if (map.has (complement)) { return [map.get (complement), i]; } map.set (nums[i], i); } return []; };
49. 字母异位词分组 https://leetcode.cn/problems/group-anagrams?envType=study-plan-v2&envId=top-100-liked
思考:
这道题要处理的问题主要是提取出每个字母的“共同之处”作为key,value则为对应同属一种“类型”的数组,返回结果要把这些value数组给拼起来。
最初的设想是将每个字符串str[i]的每个字符统计进一个Map,再以这个Map作为key去归类。
但JavaScript中的Map(引用类型 )是按值引用的地址比较 ,故,即便两个Map的内容可能完全一致,但由于每次创建Map对象时分配的地址 不一样,所以怎么找也找不到相同的key,因此无论如何也不能将Map对象作为键去查找Map。
此时转而考虑使用基本类型(字符串)作为Map的键,但要保证每一个字符串都最终可以化归 成一个大家都一致的“标志”,想到了可以对字符串进行“排序”,每个符合相同异位词格式的字符串,排序处理之后都最后必然满足“同种异位词”的格式,比如abc和bac最后都可以排序变成abc。由此发现了可以作为Map的键的参数。对每个strs[i]遍历,使之排序后纳入Map哈希表中,最后返回这个Map对象的values()。
特别注意,Map.values()返回的是一个迭代器而非数组,需要通过Array.from、展开运算符...或for of方法自行迭代一下才能得到结果。
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 var groupAnagrams = function (strs ) { let wordsMap = new Map () strs.forEach ((item )=> { const sortedWord = item.split ('' ).sort ().join ('' ) if (wordsMap.has (sortedWord)){ const newArr = wordsMap.get (sortedWord) newArr.push (item) wordsMap.set (sortedWord,newArr) }else { wordsMap.set (sortedWord,[item]) } }) return [...wordsMap.values ()] };
128.最长连续序列 https://leetcode.cn/problems/longest-consecutive-sequence?envType=study-plan-v2&envId=top-100-liked
思考:
核心还是找关系,连续序列要考虑两个子问题 :
如何判断开始元素
如何确保该序列连续
要解答这个问题,必须回答好两个子问题。对于开始元素来说,假设它是一个连续序列的一部分,那么这个元素的item-1必然不能存在于这个连续序列,如果存在则说明这个元素并非最小的那个开始元素。
第二个问题是,要确保这个序列连续,那么从开始的第一个元素到这个连续序列的最后一个元素,总是应该满足arr[-1]-arr[0] = arr.length-1的条件。毕竟是一个个加1的,开始元素加了多少,最后一定也连续的“跳”了多少步。
综上这两个点,应该考虑使用Set()对象先对nums做去重的处理,然后依次遍历去重后的numsSet,判断Set中的每一个元素是否满足item-1不存在于numsSet的条件,满足这个条件说明它一定是一个存在的连续序列的那个开始元素,随后以这个开始元素为基准,递增直到这个虚构的连续序列的元素在去重后的numsSet不存在,说明:item存在一个以它自身为开始元素,一直有一个直到item + x为止的连续序列,取这个数组的长度,与1一直比较,最后得出那个最大值。
关于JavaScript内置对象的Set()有必要后续加以补充……
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 var longestConsecutive = function (nums ) { let numsSet = new Set (nums) let startArray = [] let max = 1 numsSet.forEach ((item )=> { if (!numsSet.has (item-1 )){ startArray.push (item) } }) for (const i of startArray){ let flag = true let acc = i let time = 1 while (flag){ if (numsSet.has (acc)){ acc += 1 time += 1 continue }else { flag = false break } } max = Math .max (max,time) } return max-1 };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var longestConsecutive = function (nums ) { const st = new Set (nums); let ans = 0 ; for (const x of t) { if (st.has (x - 1 )) { continue ; } let y = x + 1 ; while (st.has (y)) { y++; } ans = Math .max (ans, y - x); } return ans; };
双指针 283. 移动零 https://leetcode.cn/problems/move-zeroes?envType=study-plan-v2&envId=top-100-liked
思考: 利用双指针来缓存遍历的情况,每当遇到一个非0的数字,就让它和上一个数字交换位置,直到所有非0的数字都被“推”到前面,最后末尾只剩下了0。
注意: 不返回新对象,对原对象的修改不能使用map,concat等方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var moveZeroes = function (nums ) { i0 = 0 for (let i=0 ;i<nums.length ;i++){ if (nums[i]!==0 ){ [nums[i],nums[i0]]=[nums[i0],nums[i]] i0 += 1 } } }
11. 承最多水的容器 https://leetcode.cn/problems/container-with-most-water?envType=study-plan-v2&envId=top-100-liked
思考:
双指针,左右代表水桶两端,桶的最大高度取决于两个水桶的长度中较短的一方,容积=长x宽,宽度即right-left。
为了确保水桶两端都尽可能的长,可以相互比较,如果右端长于左端,左端向右移动;如果左端更长,那么较短的右端就向左走。
疑问: 为什么移动的总是短端?
想象两个人在一条线上,分别从两端向中间走。每次都是较矮的那个人向前走一步,因为:
如果让较高的人向前走,容器的高度上限不会增加(因为容器的有效高度总是受较矮者的限制),但宽度减少,面积必然减小。
只有让较矮的人向前走,才有可能遇到更高的人,从而提升容器的高度上限。
这个策略保证了我们不会错过任何可能提升面积的机会,符合双指针的算法目标 淘汰那些确定不可能成为最优解的情况
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 var maxArea = function (height ) { let n = height.length let left = 0 let right = n - 1 const getArea = (left,right )=>{ return Math .min (height[left],height[right]) * (right-left) } let curArea = getArea (left,right) while (left<right){ curArea = Math .max (curArea,getArea (left,right)) if (height[left] < height[right]){ left++ }else { right-- } } return curArea };
15. 三数之和 https://leetcode.cn/problems/3sum?envType=study-plan-v2&envId=top-100-liked
思考: 第一反应是暴力破解,但问题也是非常显然的,暴力破解ijk的三层遍历会把时间复杂度拉到O(n^3)的程度。所以要思考其他方案。
联想到之前做的两数之和,想到可以先确认一个数字nums[i]作为第一个数字,剩下的两个数字在nums里面找,剩下的两个数字-应该满足相关的约束条件,使得最终三个数字相加得到0。
最初的设想是参考两数之和,构建numsMap,遍历后两个数字时判断这个搭配是否重复出现,从而避免结果重复的情况。但数组在JavaScript中是按引用的比较方法,这就意味着即便[1,0,-1]和[-1,0,1]在数组的形式上是一致的,但由于地址不同,不能使用in和Array.prototype.includes()方法。
尝试使用lodash的严格比较方法!_.some(res, item => _.isEqual(item, part))发现会导致运行结果超时,此时时间复杂度来到了更加夸张的O(n^4),因为深度递归比较会占用大量的时间,因此仍需进一步优化。
为了进一步压缩时间,尝试使用双指针的方法来扫描数组nums从i+1到n-1的组合,此处要考虑使用其他方法来跳过可能的重复,尽可能的避免任何可能触发深度遍历比较的方法。
对数组按照相对大小进行排序(注意:不能对包含0和负数的数组直接使用sort 方法排序,因为这啥子方法是把数列中的元素转换成字符串 比较,这会导致-1<-2<0的奇观,需要使用sort((a, b) => a - b))
对i来说,如果nums[i]和nums[i-1]是一回事(当然应该确保i>0),直接跳过
对双指针的left和right说,如果nums[left]和nums[left-1]相等,说明左指针要向右移动直到出现不一样的;右指针同理。
考虑完毕上述点之后,得到一个优化的双指针做法,此时可以A出来。
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 var threeSum = function (nums ) { const n = nums.length const snums = nums.sort ((a, b ) => a - b) let res = [] for (let i=0 ;i<n-2 ;i++){ if (i > 0 && snums[i] === snums[i-1 ]) continue let first = snums[i] let left = i+1 let right = n-1 while (left<right){ const sum = first + snums[left] + snums[right] if (sum === 0 ){ res.push ([first, snums[left], snums[right]]) left++ right-- while (left < right && snums[left] === snums[left-1 ]){ left++ } while (left < right && snums[right] === snums[right+1 ]){ right-- } }else if (sum < 0 ){ left++ }else { right-- } } } return res };
42.接雨水 https://leetcode.cn/problems/trapping-rain-water?envType=study-plan-v2&envId=top-100-liked
思考:
尝试拆分列,纵向比对不易发现规律,转而注意每一个单独的列的雨水高度,再逐个累加得到总的雨水量。
首先,如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。
可以看出每一列雨水的高度,取决于,该列左侧最高的柱子 和右侧最高的柱子 中最矮的那个柱子的高度。
water = Math.min(leftH,rightH) - height[i]
一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。这里可以使用双指针。
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 var trap = function (height ) { let sum = 0 let n = height.length for (let i =0 ;i<n;i++){ let right = i + 1 let left = i - 1 if (i==0 || i== n-1 ){ continue } let leftH = height[i] let rightH = height[i] for (left;left>=0 ;left--){ if (height[left]>leftH){ leftH = height[left] } } for (right;right<n;right++){ if (height[right]>rightH){ rightH = height[right] } } let water = Math .min (leftH,rightH) - height[i] if (water > 0 ){ sum += water } } return sum };
但是发现这种解法会超时,思考其他的解决方案。
在暴力解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算。
当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。
为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
得到优化后的解放,A出答案。
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 var trap = function (height ) { let n = height.length let leftHeight = new Array (n).fill (0 ) let rightHeight = new Array (n).fill (0 ) leftHeight[0 ] = height[0 ] for (let i=1 ;i<n;i++){ leftHeight[i] = Math .max (leftHeight[i-1 ],height[i]) } rightHeight[n-1 ] = height[n-1 ] for (let i=n-2 ;i>=0 ;i--){ rightHeight[i] = Math .max (rightHeight[i+1 ],height[i]) } let sum = 0 for (let i=0 ;i<n;i++){ let water = Math .min (leftHeight[i],rightHeight[i]) - height[i] if (water>0 ){ sum += water } } return sum };
滑动窗口 3. 无重复字符的最长子串 https://leetcode.cn/problems/longest-substring-without-repeating-characters?envType=study-plan-v2&envId=top-100-liked
思考: 要体现无重复的题意,说明在动态的框选子字符串的时候,应该确保每当扫到一个新的字符的时候,应该确保这个新扫入的字符既不能出现两次,同时要更新起始段,考虑使用滑动窗口来解决问题。
为了统计每个字符,使用Map来储存字符和出现次数。
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 var lengthOfLongestSubstring = function (s ) { let ans = 0 let left = 0 let charMap = new Map () for (let right = 0 ;right<s.length ;right++){ let char = s[right] if (charMap.has (char)){ charMap.set (char,charMap.get (char)+1 ) }else { charMap.set (char,1 ) } while (charMap.get (char)>1 ){ charMap.set (s[left],charMap.get (s[left])-1 ) left++ } ans = Math .max (right-left+1 ,ans) } return ans };
438.找到字符串中所有字母异位词 https://leetcode.cn/problems/find-all-anagrams-in-a-string?envType=study-plan-v2&envId=top-100-liked
思考: 首先想到的是异位词,参考49题,但这道题中的条件不允许使用排序后字符串作为键,因为对字符串的排序的时间复杂度太高:
假设 s 长度 n,p 长度 m
你大约需要检查 n-m+1 个窗口
每个窗口:获取子串 O(m) + 拆分为数组 O(m) + 排序 O(m log m) + 合并 O(m)
总复杂度 ≈ O(n × m log m) ,在最大数据规模下可能达到 10^8 量级操作
转而考虑用其他方式来统计异位词,联想到可以使用频率数组,使用26个长度的的数组来记录一个字符串包含的全部字符频率。将字符映射到数组的方法是String.prototype.charCodeAt(),这个方法会返回传入字符的在utf-16内对应的码元(数字索引),用相对值减去便可以得到其在数组内的索引,例如char.charCodeAt(0)-'a'.charCodeAt(0)。
首先扫描一遍p字符串,得到其频率数组pArr。
问题的关键在于滑动窗口的确认,数组已经定长,不便于直接用数字再记录当前滑动窗口的长度,可以先遍历前p个的字符串长度的s串,判断是否符合要求,然后再从s[p.length]开始遍历剩余部分,每遍历一次,代表right向右移动一格,添加进sArr的频率数组,与此同时,就把维护的sArr中的第left号元素自减,表示滑动窗口的左侧移出一位,然后检查新的窗口JSON.stringify(sArr)是否与参考的频率数值pArr一致,如果一致,说明right - p.length + 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 var findAnagrams = function (s, p ) { const sp = p.split ('' ).sort ().join ('' ) let left = 0 let res = [] for (let right = 0 ;right<s.length ;right++){ let window = s.slice (left,right+1 ) if (window .length <p.length ){ continue } if (window .length ===p.length ){ if (window .split ('' ).sort ().join ('' )===sp){ res.push (left) left++ }else { left++ } } } return res };
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 var findAnagrams = function (s, p ) { if (s.length < p.length ) return []; let pArr = new Array (26 ).fill (0 ); for (let i = 0 ; i < p.length ; i++) { let charI = p.charCodeAt (i) - 97 ; pArr[charI]++; } let left = 0 ; let res = []; let wArr = new Array (26 ).fill (0 ); for (let i = 0 ; i < p.length ; i++) { let charI = s.charCodeAt (i) - 97 ; wArr[charI]++; } if (JSON .stringify (wArr) === JSON .stringify (pArr)) { res.push (0 ); } for (let right = p.length ; right < s.length ; right++) { let leftCharI = s.charCodeAt (right - p.length ) - 97 ; wArr[leftCharI]--; let rightCharI = s.charCodeAt (right) - 97 ; wArr[rightCharI]++; if (JSON .stringify (wArr) === JSON .stringify (pArr)) { res.push (right - p.length + 1 ); } } return res; };
560. 和为K的子数组 https://leetcode.cn/problems/subarray-sum-equals-k?envType=study-plan-v2&envId=top-100-liked
本题要使用前缀和来解决,应该先做303 https://leetcode.cn/problems/range-sum-query-immutable
前缀和: 比如 nums=[1,2,3,4,5,6],要想计算子数组 [3,4,5] 的元素和,可以用前缀 [1,2,3,4,5] 的元素和,减去另一个前缀 [1,2] 的元素和,就得到了子数组 [3,4,5] 的元素和,即
3+4+5=(1+2+3+4+5)−(1+2) 换句话说,把前缀 [1,2,3,4,5] 的前缀 [1,2] 去掉,就得到了子数组 [3,4,5]。
一般地,任意子数组都是一个前缀去掉前缀后的结果。所以任意子数组的和,都可以表示为两个前缀和的差。
nums 的子数组有 O(n^2) 个,但只有 O(n) 个前缀。那么,预处理 nums 的所有前缀和,就可以 O(1) 计算任意子数组的元素和。
通过前缀和,我们可以把子数组的元素和转化成两个前缀和的差,下标区间 [left,right] 的元素和等于前缀 [0,right] 的元素和减去另一个前缀 [0,left−1] 的元素和,即s[right+1]−s[left]。
该问题可以转化为前缀和,即查找nums中是否存在i,k满足s[j]-s[i]=k,为了便于使用Map加速查找,可以转化为s[i]=s[j]−k。
问题转化
子数组 nums[i...j] 的和 = prefix[j] - prefix[i-1]
我们要找 prefix[j] - prefix[i-1] = k
即:prefix[j] - k = prefix[i-1]
哈希表的作用
为什么要初始化 prefixMap.set(0,1)?
考虑这种情况:从数组开头到当前位置的和正好等于 k。 这时 s - k = 0,我们需要知道前缀和为 0 的情况(即空数组),所以初始化为 1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var subarraySum = function (nums, k ) { const prefixMap = new Map () prefixMap.set (0 ,1 ) let sum = 0 let s = 0 for (const i of nums){ s += i sum += (prefixMap.get (s-k) ?? 0 ) prefixMap.set (s,(prefixMap.get (s) ?? 0 ) + 1 ) } return sum };
数组 53. 最大子数组和 https://leetcode.cn/problems/maximum-subarray?envType=study-plan-v2&envId=top-100-liked
思考: 使用动态规划,维护数组dp,根据题意可知,第i位的最大和的连续子数组的和总是Math.max(dp[i-1]+nums[i],nums[i])
,得到了状态转移方程,注意同步维护最大值max,因为我们最终要返回的结果dp[nums.length-1]可能并非是这个数组的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var maxSubArray = function (nums ) { let dp = new Array (nums.length ).fill (0 ) dp[0 ] = nums[0 ] let max = dp[0 ] for (let i=1 ;i<nums.length ;i++){ dp[i] = Math .max (dp[i-1 ]+nums[i],nums[i]) max = Math .max (max,dp[i]) } return max };
56. 合并区间 https://leetcode.cn/problems/merge-intervals?envType=study-plan-v2&envId=top-100-liked
思考: 使用贪心思想,关键在于判断[a,b] [c,d]的合并条件,简单画图可知其关键条件应该是b>c,但考虑到包含的情况,应该确保合并后的结果[x,y],x总是在a,c中的最小值;y总是在b,d中的最大值。
为了确保每次合并比较的过程总是单调,需要先对intervals先做一次排序,确保a<c的单调递增,这里使用到了Array.prototype.sort()方法。在不断比较的过程中,对于满足合并条件的cur不断合并,如果条件不满足,说明在这个区间内的cur可合并区间已经达到了最大,将其更新到res,并更新cur为当前正在遍历的intervals,遍历结束后再把剩下的cur塞进res,得到最终结果。
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 var merge = function (intervals ) { const sortedInterval = intervals.sort ((a,b )=> a[0 ]-b[0 ]) let cur = sortedInterval[0 ] let res = [] if (sortedInterval.length ===1 ){ return [cur] } for (let i=1 ;i<sortedInterval.length ;i++){ if (cur[1 ]>=sortedInterval[i][0 ]){ let right = Math .max (cur[1 ],sortedInterval[i][1 ]) cur = [cur[0 ],right] }else { res.push (cur) cur = sortedInterval[i] } } res.push (cur) return res };
189.轮转数组 思考: 题干要求只能在数组内部完成修改,不能返回新的数组。想到Array.prototype下在内部修改的方法有slice,push,pop。
其中Array.splice方法可以在数组内部指定一个开始索引,就地 移除或者替换已存在的元素和/或添加新的元素。
1 2 3 4 5 6 7 8 9 10 11 12 const months = ["Jan" , "March" , "April" , "June" ]; months.splice (1 , 0 , "Feb" );console .log (months); months.splice (4 , 1 , "May" );console .log (months);
本题的难绷点在于可能出现k>nums.length的情况,此时需要使用%取模。
1 2 3 4 5 6 7 8 9 var rotate = function (nums, k ) { let spliced = nums.splice (nums.length - (k % nums.length )) nums.splice (0 , 0 , ...spliced) };
238. 除了自身以外数组的乘积 https://leetcode.cn/problems/product-of-array-except-self?envType=study-plan-v2&envId=top-100-liked
思考: 联想到我们之前T560 中的做法,且题目要求不允许使用除法,故从前缀和联想到前缀积,再由前缀积联想到后缀积。那么,可以列出式子answer[i] = prefixAcc[i]*suffixAcc[i],从而得出答案。
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 var productExceptSelf = function (nums ) { const n = nums.length let prefixAcc = new Array (n+1 ).fill (1 ) let suffixAcc = new Array (n+1 ).fill (1 ) let answer = [] for (let i = 1 ;i<n;i++){ prefixAcc[i] = prefixAcc[i-1 ] * nums[i-1 ] } for (let i = n-2 ;i>=0 ;i--){ suffixAcc[i] = suffixAcc[i+1 ] * nums[i+1 ] } for (let i = 0 ;i<n;i++){ answer.push (prefixAcc[i]*suffixAcc[i]) } return answer };
41. 缺失的第一个正数 https://leetcode.cn/problems/first-missing-positive?envType=study-plan-v2&envId=top-100-liked
思考: 题目要求时间复杂度为O(n)且不允许使用额外的空间来,故只能从题目要求和数组本身下手。考虑到传统的排序方法(O(nlogn))和哈希表都会产生额外的时间和空间,故采用原地哈希 的做法,通过即时的在数组内交换位置,使得数组的索引即元素的哈希,而题目恰好要求我们得出缺失的第一个正整数,故可以忽略负数的情况,通过条件遍历使得数组内的数字k应该出现在k-1的位置上,如果不满足,就说明正在遍历的索引i+1即是对应缺失的最小正数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function firstMissingPositive (nums : number [] ): number { const n : number = nums.length ; for (let i = 0 ; i < n; i++) { while (nums[i] >= 1 && nums[i] <= n && nums[i] !== nums[nums[i] - 1 ]) { const index : number = nums[i] - 1 ; [nums[i], nums[index]] = [nums[index], nums[i]]; } } for (let i = 0 ; i < n; i++) { if (nums[i] !== i + 1 ) { return i + 1 ; } } return n + 1 ; };
矩阵 73. 矩阵置零 思考: 本体不便于直接修改,因为每次遇到0后立即修改会导致无法区分下一个遍历的数字到底是自身为0还是被替换为0的情况。
可以记录把matrix[i][j]记录进zeroArray,再遍历zeroArray,对每一个item,在x方向上的就替换为对应长度m的0数组(new Array(m).fill(0)),在y方向就遍历n次,使得matrix[i][item[1]] = 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var setZeroes = function (matrix ) { const m = matrix[0 ].length const n = matrix.length let zeroArray = [] for (let i = 0 ;i<n;i++){ for (let j=0 ;j<m;j++){ if (matrix[i][j] === 0 ){ zeroArray.push ([i,j]) } } } zeroArray.forEach ((item )=> { matrix[item[0 ]] = new Array (m).fill (0 ) for (let i=0 ;i<n;i++){ matrix[i][item[1 ]] = 0 } }) };
链表 160. 相交链表 https://leetcode.cn/problems/intersection-of-two-linked-lists?envType=study-plan-v2&envId=top-100-liked
思考: 同时遍历访问不同列表的节点,如果它们存在一个共同的节点,最后总是能相遇,因此在A链的全部节点遍历完毕后,就将p.next设置为headB,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 26 27 28 29 30 var getIntersectionNode = function (headA, headB ) { let p = headA let q = headB while (p!==q){ if (p !== null ){ p = p.next }else { p = headB } if (q !== null ){ q = q.next }else { q = headA } } return p };
206. 反转链表 https://leetcode.cn/problems/reverse-linked-list?envType=study-plan-v2&envId=top-100-liked
思考: 可以创建一个新的链表,在这个链表中,把每个遍历的元素加到新列表节点的头部,然后更新新的待处理头部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var reverseList = function (head ) { let pre = null let cur = head while (cur != null ) { const curN = cur.next cur.next = pre pre = cur cur = curN } return pre }
234. 回文链表 https://leetcode.cn/problems/palindrome-linked-list?envType=study-plan-v2&envId=top-100-liked
思考: 单向链表不能直接回溯,且单次链表遍历完毕前不能确认该链表的长度,故考虑转化 为已知问题,在链表遍历时记录cur.val
,存入arr,再在arr中使用双指针的做法求得数组是否为回文。
注意: 遍历数组的条件是while(cur!=null),因为在更新cur.next的时候已经挂上了下一个节点,即便下一个节点不存在,也应该以cur是否为空为判断条件,否则总是会少一个。
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 var isPalindrome = function (head ) { let arr = [] let cur = head while (cur != null ){ arr.push (cur.val ) cur = cur.next } let left = 0 let right = arr.length - 1 while (left<right){ if (arr[left]!=arr[right]){ return false } left++ right-- } return true };
141. 环形链表 https://leetcode.cn/problems/linked-list-cycle?envType=study-plan-v2&envId=top-100-liked
思考: 成环的条件意味着有一个节点的next 指针会指向这个链表中已经存在的链表节点,考虑到不需要使用额外的信息来记录重复多少次,Set比Map 用于存储节点的引用信息更合适,由此想到使用哈希表来判断,注意存的是节点引用而非节点值,因为后者可能重复,但引用地址总是不变的。
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 var hasCycle = function (head ) { let set = new Set () let cur = head while (cur != null ){ if (set.has (cur)){ return true }else { set.add (cur) } cur = cur.next } return false };
142. 链表节点II https://leetcode.cn/problems/linked-list-cycle-ii?envType=study-plan-v2&envId=top-100-liked
思考: 本体要求返回链表开始入环的第一个节点,再度祭出Set大法,如果存在就返回该节点,否则就加入集合对象中,利用Set实现链表节点遍历情况的记录。
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 var detectCycle = function (head ) { let set = new Set () let cur = head while (head != null ){ if (set.has (head)){ return head }else { set.add (head) } head = head.next } return null };
21. 合并两个有序链表 https://leetcode.cn/problems/merge-two-sorted-lists?envType=study-plan-v2&envId=top-100-liked
思考: 本题的关键在于正确的处理两类情况:如果是公共部分的长度,就要比较大小,将较短值所在的节点拼接到正在遍历的指针节点p上,继续比较;如果公共长度比较完毕,就看谁还剩的多,就把剩余链表的节点挂到p.next上。
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 var mergeTwoLists = function (list1, list2 ) { const dummy = new ListNode (); let cur = dummy; while (list1 && list2) { if (list1.val < list2.val ) { cur.next = list1; list1 = list1.next ; } else { cur.next = list2; list2 = list2.next ; } cur = cur.next ; } cur.next = list1 ?? list2; return dummy.next ; };
2. 两数相加 https://leetcode.cn/problems/add-two-numbers?envType=study-plan-v2&envId=top-100-liked
思考: 考察对链表的理解,本体的难点在于考量进位的情况,使用链表模拟实现进位加法,需要分配一个变量用于存储进位的情况,无论l1,l2链表是否遍历完毕,都要确保最后进位一定是0,创建对应的链表节点,从而实现最终结果与预期一致。本题依然采用哨兵节点+指针遍历的思路,需要牢牢掌握。
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 var addTwoNumbers = function (l1, l2 ) { let upper = 0 let dummy = new ListNode (0 ) let cur = dummy while (l1 != null || l2!=null || upper!== 0 ){ const val1 = l1 ? l1.val : 0 ; const val2 = l2 ? l2.val : 0 ; let sum = upper + val1 + val2 cur.next = new ListNode (sum % 10 ) cur = cur.next upper = Math .floor (sum/10 ) if (l1) l1 = l1.next ; if (l2) l2 = l2.next ; } return dummy.next };
19. 删除链表中的倒数第N个节点 https://leetcode.cn/problems/remove-nth-node-from-end-of-list?envType=study-plan-v2&envId=top-100-liked
思考: 两次遍历法解决,首次遍历时记录节点顺序和节点值。第二次遍历前,先计算得到待删除的节点顺序,然后利用伪节点从头开始创建新的节点,最后返回伪头结点dummy.next。要注意遍历的次数与链表长度一致。
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 var removeNthFromEnd = function (head, n ) { let map = new Map () let p = head let i = 0 while (p != null ) { map.set (i, p.val ) p = p.next i++ } let target = i - n let dummy = new ListNode (0 ) let q = dummy let j = 0 while (j < i) { if (j == target) { j++ continue } else { let node = new ListNode (map.get (j)) q.next = node } q = q.next j++ } return dummy.next };
24. 两两交换链表中的节点 思考: 两两交换节点的前提条件是不可少于两个节点,可以先从最简单的模型开始,同时,鉴于题目要求就地修改,为了便于操作需要创建一个伪头节点。
假定一个含有四个节点的列表分别命名为node0123,如果要交换node1和node2那么对链表的操作应该如下:
node0指向node2
node2指向node1
node1指向node3
此时完成了node1和node2链表节点的交换,拓展后,对于下一轮交换,原本的node0更新为node1,原本的node1更新为node3。
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 var removeNthFromEnd = function (head, n ) { let map = new Map () let p = head let i = 0 while (p != null ) { map.set (i, p.val ) p = p.next i++ } let target = i - n let dummy = new ListNode (0 ) let q = dummy let j = 0 while (j < i) { if (j == target) { j++ continue } else { let node = new ListNode (map.get (j)) q.next = node } q = q.next j++ } return dummy.next };
二叉树 理论基础 https://www.bilibili.com/video/BV1Hy4y1t7ij/?share_source=copy_web&vd_source=75123c5d6ddf7add04ab0e96d5ddb1f7
种类:
储存方式:
二叉树遍历:
DFS/BFS
前/中/后序遍历都是深度优先遍历:递归/迭代法
层序遍历是广度优先遍历.
前序遍历: 中左右
中序遍历: 左中右
后序遍历: 左右中
102. 二叉树的层序遍历(BFS) 【二叉树的层序遍历【基础算法精讲 13】】 https://www.bilibili.com/video/BV1hG4y1277i/?share_source=copy_web&vd_source=75123c5d6ddf7add04ab0e96d5ddb1f7
双数组做法
创建一个数组cur
将根节点放入cur,开始遍历cur
创建nxt数组
将左右儿子记录到下一层节点数组nxt,把节点值记录到数组vals中
遍历结束后,把cur替换成nxt,开启下一轮循环
重复循环直到cur为空,此时可终止循环。
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 var levelOrder = function (root ) { if (root == null ){ return [] } let ans = [] let cur = [root] while (cur.length != 0 ){ let nxt = [] let vals = [] for (const node of cur){ vals.push (node.val ) if (node.left ){ nxt.push (node.left ) } if (node.right ){ nxt.push (node.right ) } } cur = nxt ans.push (vals) } return ans };
108. 将有序数组转换为二叉搜索树 https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree?envType=study-plan-v2&envId=top-100-liked
思考: 使用递归处理,问题的关键在于选择正确的数组元素和处理左儿子和右儿子。解决这个问题的方法是使用递归,将二叉树拆解成子问题,即子二叉树的dfs函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var sortedArrayToBST = function (nums ) { let dfs = (left,right ) => { if (left === right){ return null } const mid = Math .floor ((left+right) / 2 ) return new TreeNode (nums[mid],dfs (left,mid),dfs (mid+1 ,right)) } return dfs (0 ,nums.length ) };
94. 二叉树的中序遍历 https://leetcode.cn/problems/binary-tree-inorder-traversal?envType=study-plan-v2&envId=top-100-liked
思考: DFS+左中右,得补一下DFS和BFS的功课。
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 var inorderTraversal = function (root ) { const ans = []; const dfs = (root ) => { if (!root) { return ; } dfs (root.left ); ans.push (root.val ); dfs (root.right ); }; dfs (root); return ans; };
二分查找 35. 搜索插入位置 https://leetcode.cn/problems/search-insert-position?envType=study-plan-v2&envId=top-100-liked
思考: 二分查找,如果target落在中间值mid左侧,移动right=mid-1,如果target落在中间值mid右侧,移动left=mid+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 var searchInsert = function (nums, target ) { const n = nums.length let left = 0 let right = n-1 while (left <= right){ let mid = Math .floor ((left+right)/2 ) if (nums[mid]<target){ left = mid+1 } if (nums[mid]>target){ right = mid-1 } if (nums[mid]==target){ return mid } } return left };
74. 搜索二维矩阵 思考: 二维矩阵也可以转换为一个长长的数组,问题的关键在于如何获取matrix的matrix[mid],根据二维数组的特性,应该如matrix[Math.floor(mid/2)][mid%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 27 var searchMatrix = function (matrix, target ) { const m = matrix.length , n = matrix[0 ].length ; let left = -1 , right = m * n; while (left + 1 < right) { const mid = Math .floor ((left + right) / 2 ); const x = matrix[Math .floor (mid / n)][mid % n]; if (x === target) { return true ; } if (x < target) { left = mid; } else { right = mid; } } return false ; };
104. 二叉树的最大深度 https://leetcode.cn/problems/maximum-depth-of-binary-tree?envType=study-plan-v2&envId=top-100-liked
思考: 要解决这个问题,仍然需要掌握DFS和递归的思想。问题的关键在于:
深度信息如何传递和更新?
对子树的处理(左子树和右子树)
解决方法是使用闭包,dfs(root,cur),root表示传入的子树节点,cur表示当前递归得到的最大深度。但要获得最大的深度,应该将最大值与cur比较并更新,从而确保最后得到的结果总是最大值。
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 var maxDepth = function (root ) { let maxdepth = 0 let dfs = (root,cur ) =>{ if (!root){ return } maxdepth = Math .max (maxdepth,cur) dfs (root.left ,cur+1 ) dfs (root.right ,cur+1 ) } dfs (root,1 ) return maxdepth };
226. 翻转二叉树 https://leetcode.cn/problems/invert-binary-tree?envType=study-plan-v2&envId=top-100-liked
思考: 为了翻转二叉树,需要交换左子树与右子树,然后执行dfs深度优先遍历。
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 var invertTree = function (root ) { const dfs = (root ) =>{ if (!root){ return } let tmp = root.right root.right = root.left root.left = tmp dfs (root.left ) dfs (root.right ) } dfs (root) return root };
101. 对称二叉树 思考: 将对称二叉树的判定拆分为子问题:左子树与右子树是否对称 ,构造子函数通过递归方法解决。
转化为子树问题 :一颗树对称,意味着它的左子树 和右子树 是镜像关系。
镜像的定义 :对于两个节点(比如左子树的根 A 和右子树的根 B),它们互为镜像需要满足什么条件?
首先,它们的值必须相等。
其次,A 的左子树必须和 B 的右子树镜像。
最后,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 26 27 var isSymmetric = function (root ) { const isSameTree = (left,right ) =>{ if (left==null || right==null ){ return left == right } return left.val === right.val && isSameTree (left.left ,right.right ) && isSameTree (left.right ,right.left ) } return isSameTree (root.left ,root.right ) };
543. 二叉树的直径 思考: 子问题是求得子树的最大链长,子树的最大链长是左子树最大链长与右子树最大链长的最大值。
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 var diameterOfBinaryTree = function (root ) { let ans = 0 const dfs = (node ) =>{ if (!node){ return -1 } const left = dfs (node.left ) + 1 const right = dfs (node.right )+1 ans = Math .max (ans,left+right) return Math .max (left,right) } dfs (root) return ans };
动态规划 题单: https://leetcode.cn/discuss/post/3581838/fen-xiang-gun-ti-dan-dong-tai-gui-hua-ru-007o/
时间紧任务重,先做Hot100…
思路:
明确状态定义
推导状态转移方程
确认边界条件
确定计算顺序
返回计算结果
70. 爬楼梯 https://leetcode.cn/problems/climbing-stairs?envType=study-plan-v2&envId=top-100-liked
思考: 观察可知道这道题实际上是在考察斐波那契数列,易推得状态转移方程dp[i]=dp[i-1]+dp[i-2](i>=2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var climbStairs = function (n ) { if (n==1 ){ return 1 } let dp = new Array (n+1 ).fill (1 ) dp[0 ] = 1 dp[1 ] = 1 for (let i=2 ;i<=n;i++){ dp[i] = dp[i-1 ] + dp[i-2 ] } return dp[n] };
118. 杨辉三角 https://leetcode.cn/problems/pascals-triangle?envType=study-plan-v2&envId=top-100-liked
思考: 三角形不便于观察,可以将每一行从左侧开始对齐,此时更容易观察规律,可见到中间数组由上一排数组内的相邻两个数组相加dp[i-1][j-1]+dp[i-1][j-1] 得到,由此发现了状态转移方程:part[j] = dp[i-1][j-1] + dp[i-1][j] (i>=2,j>=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 var generate = function (numRows ) { if (numRows === 1 ){ return [[1 ]] } if (numRows === 2 ){ return [[1 ],[1 ,1 ]] } let dp = new Array (numRows).fill ([]) dp[0 ] = [1 ] dp[1 ] = [1 ,1 ] for (let i=2 ;i<numRows;i++){ let part = new Array (i+1 ).fill (1 ) for (let j = 1 ;j<i;j++){ part[j] = dp[i-1 ][j-1 ] + dp[i-1 ][j] } dp[i] = part } return dp };
198. 打家劫舍 https://leetcode.cn/problems/house-robber?envType=study-plan-v2&envId=top-100-liked
思考: 状态转移方程dp[i] = Math.max(nums[i-1]+dp[i-2],dp[i-1]) (i>=2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var rob = function (nums ) { if (nums.length ===1 ){ return nums[0 ] } if (nums.length ===2 ){ return Math .max (nums[0 ],nums[1 ]) } const n = nums.length let dp = new Array (n).fill (0 ) dp[1 ] = nums[0 ] for (let i=2 ;i<=n;i++){ dp[i] = Math .max (nums[i-1 ]+dp[i-2 ],dp[i-1 ]) } return dp[n] };