本文主要记录了博主的 Leetcode 算法刷题记录,方便以后查询复习。
哈希表
两数之和
这道题首先考虑的是通过两层for循环遍历查询,但这样有很多无意义的查找。因此我们可以采用哈希表来减少查找的时间复杂度,此时查找顺序是往后而不是往前。注意使用Map对象来减少查询之前数据的时间复杂度,这里不能使用WeakMap,因为WeakMap只能使用对象作为键。
const twoSum = (nums, target) => {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
const twoSum = (nums, target) => {
for (let i = 0; i < nums.length; i++) {
if (map.has(target - nums[i])) {
return [map.get(target - nums[i]), i];
console.log(twoSum([2, 7, 11, 15], 9));
字母异位词分组
建立一个判断某一字符串是否是同一组的函数,再遍历数组,填充键-数组的Map对象,然后利用Map对象的相关方法返回实例。关键在于如何判断是同一种类型的字符串,一种是将字符串转为数组后排序,然后生成唯一的key,排序算法时间复杂度 O(nlogn) 另一种方法是使用一个26位的数组来构造一个字符串的唯一键,来实现匹配的目的,注意使用的是charCodeAt方法,此种方法的时间复杂度为 O(n),n 为字符串的长度。
const groupAnagrams = (strs) => {
const valToKey = (str) => {
const strArr = Array.from(str);
return strArr.sort().toString();
for (const str of strs) {
const key = valToKey(str);
return Array.from(map.values());
const groupAnagrams = (strs) => {
const valToKey = (str) => {
const keyArr = new Array(26).fill(0);
for (const char of str) {
keyArr[char.charCodeAt() - "a".charCodeAt()]++;
return keyArr.toString();
for (const str of strs) {
const key = valToKey(str);
return Array.from(map.values());
console.log(groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]));
// [ [ 'eat', 'tea', 'ate' ], [ 'tan', 'nat' ], [ 'bat' ] ]
最长连续序列
首先采用以数组为参数构建set,在建立每个元素哈希的同时去重,后续遍历在set集合中展开,然后找到每段区间的起始位置并求其最大值,即存在比起小的数字即set.has(num - 1)这种情况需要跳过,因为必然会从比num - 1开始遍历的区间的长度要小。
const longestConsecutive = (nums) => {
const set = new Set(nums);
maxLength = Math.max(maxLength, count);
console.log(longestConsecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1]));
总结
在 JS 中,哈希表主要采用Map和Set这两种内置的数据结构。合理使用这两种数据结构可以极大地方便查找元素,从而降低算法的时间复杂度。对于Map对象来说更是可以把键与值关联起来存储,方便算法的改进,还可以用于实际需求,如分组的运用。
双指针
移动零
代码中left指针指向已处理好的数组的末尾后一个元素,即指向现数组的第一个0,当right指针指向非零数字的时候就将非零数字提到前面然后进行交换。核心思路为left指针始终指向第一个0元素,当right指针指向遇到非零元素时left指针才会变化。
* @return {void} Do not return anything, modify nums in-place instead.
const moveZeroes = (nums) => {
while (right < nums.length) {
[nums[left], nums[right]] = [nums[right], nums[left]];
const nums = [0, 1, 0, 3, 12];
盛水最多的容器
双指针向内遍历,由于横向长度的变小,想要大于之前的水量,只能遇到更高的容器壁,所以小的容器壁一边需要往内收缩,不断更新最大值,最后得到相应的结果。感觉核心的思想还是贪心。🤔
* @param {number[]} height
const maxArea = (height) => {
right = height.length - 1;
height[left] > height[right]
? Math.max(max, (right - left) * height[right--])
: Math.max(max, (right - left) * height[left++]);
console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]));
三数之和
这道题目的难点在于去重。将数组进行排序是一个很巧妙的做法,一方面可以便于第一层i使用一次后去重,第二个是便于l和r双指针向内遍历去重,同时在nums[i] > 0 的时候可以退出循环,避免无用的计算。做这道题重点在于两次去重的过程,值得仔细理解。
const threeSum = (nums) => {
if (!nums || nums.length < 3) {
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
while (i > 0 && nums[i] === nums[i - 1]) {
const sum = nums[i] + nums[left] + nums[right];
res.push([nums[i], nums[left], nums[right]]);
while (nums[left] === nums[left + 1]) {
while (nums[right] === nums[right - 1]) {
console.log(threeSum([-1, 0, 1, 2, -1, -4]));
// [ [ -1, -1, 2 ], [ -1, 0, 1 ] ]
接雨水
接雨水似乎是写烂了的动态规划?🤔 下面也列出了三种方法,分别是:
- 动态规划双向遍历得到左右数组,然后逐个元素求解。
- 单调栈求解。注意栈中存储的是数组的索引。
- 动态规划 + 双指针,两边取最小求解,
leftMax和rightMax那个最小就确定了那个最小的值。
* @param {number[]} height
const trap = (height) => {
const leftMaxArr = new Array(n);
leftMaxArr[0] = height[0];
leftMaxArr[n - 1] = height[n - 1];
for (let i = 1; i < n - 1; i++) {
leftMaxArr[i] = Math.max(leftMaxArr[i - 1], height[i - 1]);
const rightMaxArr = new Array(n);
rightMaxArr[0] = height[0];
rightMaxArr[n - 1] = height[n - 1];
for (let i = n - 2; i > 0; i--) {
rightMaxArr[i] = Math.max(rightMaxArr[i + 1], height[i + 1]);
for (let i = 1; i < n - 1; i++) {
const num = Math.min(leftMaxArr[i], rightMaxArr[i]);
res += Math.abs(num - height[i]);
* @param {number[]} height
const trap = (height) => {
for (let i = 0; i < height.length; i++) {
while (stack.length && height[i] > height[stack[stack.length - 1]]) {
const left = stack[stack.length - 1];
const curHeight = Math.min(height[left], height[i]) - height[top];
const curWidth = i - left - 1;
res += curWidth * curHeight;
* @param {number[]} height
const trap = (height) => {
right = height.length - 1;
leftMax = Math.max(height[left], leftMax);
rightMax = Math.max(height[right], rightMax);
if (leftMax < rightMax) {
res += leftMax - height[left];
res += rightMax - height[right];
console.log(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]));
总结
双指针向内收缩对于特殊情况的元素遍历有很大的帮助,对于两个或两个以上元素的遍历,都可以考虑双指针的做法。
滑动窗口
无重复字符的最长字串
利用set集合与滑动窗口解决,右扩张,左收缩,遍历数组获得最长长度。有点像滑动窗口 + 哈希表 + 双指针的结合。🤩
const lengthOfLongestSubstring = (s) => {
maxLength = Math.max(maxLength, set.size);
console.log(lengthOfLongestSubstring("abcabcbb"));
找到字符串中所有字母的异位词
主要采取长度为26位的数组生成唯一的key值,用来滑动窗口进行匹配。注意对一个元素进行处理或者对最后增减做一个条件判断。
const findAnagrams = (s, p) => {
if (p.length > s.length) {
const pKeyArr = new Array(26).fill(0);
pKeyArr[char.charCodeAt() - "a".charCodeAt()]++;
const pKey = pKeyArr.toString();
const winArr = new Array(26).fill(0);
for (let i = 0; i < p.length; i++) {
winArr[s[i].charCodeAt() - "a".charCodeAt()]++;
for (let i = 0; i < s.length - p.length + 1; i++) {
if (winArr.toString() === pKey) {
if (i + p.length < s.length) {
winArr[s[i].charCodeAt() - "a".charCodeAt()]--;
winArr[s[i + p.length].charCodeAt() - "a".charCodeAt()]++;
const findAnagrams = (s, p) => {
if (s.length < p.length) {
const keyArr = new Array(26).fill(0);
const valArr = new Array(26).fill(0);
for (let i = 0; i < p.length; i++) {
keyArr[p[i].charCodeAt() - "a".charCodeAt()]++;
valArr[s[i].charCodeAt() - "a".charCodeAt()]++;
const key = keyArr.toString();
if (valArr.toString() === key) {
for (let i = p.length; i < s.length; i++) {
valArr[s[i].charCodeAt() - "a".charCodeAt()]++;
valArr[s[i - p.length].charCodeAt() - "a".charCodeAt()]--;
if (valArr.toString() === key) {
res.push(i - p.length + 1);
console.log("cbaebabacd", "abc");
总结
滑动窗口分为静态大小的窗口和动态大小的窗口,前者每次都需要变化,大小固定;后者一般为右扩张左收缩,大小不固定。
子串
和为 k 的子数组
注意是连续的子数组,可以采用两层for循环,内层for循环往回遍历。或者使用哈希表,注意哈希表的思路比较巧妙,遍历到当前元素的时候,采取看之前的Map对象中是否存在当前和减去目标值,即map.has(pre - k),存在即计数增加,注意需要设置0的初始值const map = new Map([[0, 1]])。暴力做法可帮助理解第二种做法 🦀。
const subarraySum = (nums, k) => {
for (let i = 0; i < nums.length; i++) {
for (let j = i; j >= 0; j--) {
const subarraySum = (nums, k) => {
const map = new Map([[0, 1]]);
for (const num of nums) {
count += map.get(pre - k);
map.set(pre, map.get(pre) + 1);
console.log(subarraySum([1, 1, 1], 2));
滑动窗口最大值
有点类似单调栈?🤔 采用单调队列求解,但需要注意无论是队列还是栈,数组中存放的都是下标,推入的元素需要根据题目条件来做处理。
const maxSlidingWindow = (nums, k) => {
for (let i = 0; i < k; i++) {
while (queue.length && nums[i] >= nums[queue[queue.length - 1]]) {
const res = [nums[queue[0]]];
for (let i = k; i < nums.length; i++) {
while (queue.length && nums[i] >= nums[queue[queue.length - 1]]) {
while (queue[0] <= i - k) {
res.push(nums[queue[0]]);
console.log(maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3));
最小覆盖字串
采用哈希表计数,然后采用不定长度滑动窗口求解,右扩张,左收缩,哈希表计数是关键中的关键。
const minWindow = (s, t) => {
map.set(char, map.has(char) ? map.get(char) + 1 : 1);
// 注意 count 不是 t 的长度而是不同字母的个数,即 Map 对象的大小
map.set(s[j], map.get(s[j]) - 1);
if (map.get(s[j]) === 0) {
if (map.get(s[i]) === 0) {
map.set(s[i], map.get(s[i]) + 1);
if (j - i < res.length || res === "") {
map.set(s[i], map.get(s[i]) + 1);
console.log(minWindow("ADOBECODEBANC", "ABC"));
总结
子串很多都与哈希表相关,注意总结题目经验求解,很多情况下可能用到滑动窗口,单调栈与单调队列的思想也很重要。
普通数组
最大子数组和
比较简单的动态规划题目,若前面的pre小于 0 则直接舍去,防止拖累变小,感觉也有点贪心的思想在里面。🤔
const maxSubArray = (nums) => {
for (const num of nums) {
pre = Math.max(pre + num, num);
maxAns = Math.max(pre, maxAns);
console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]));
合并区间
简单的模拟策略但需要注意排序的时候,在前面的右侧值不一定最大,所以需要取二者的最大值。
* @param {number[][]} intervals
const merge = (intervals) => {
intervals.sort((a, b) => a[0] - b[0]);
for (const interval of intervals) {
if (res.length === 0 || interval[0] > res[res.length - 1][1]) {
res[res.length - 1][1] = Math.max(res[res.length - 1][1], interval[1]);
// [ [ 1, 6 ], [ 8, 10 ], [ 15, 18 ] ]
轮转数组
可采用额外数组存储,或者数组反转进行解决。第一种方法比较常规,第二种方法技巧性强但便于记忆与理解。😊
| 操作 |
结果 |
| 原始数组 |
1 2 3 4 5 6 7 |
| 翻转所有元素 |
7 6 5 4 3 2 1 |
| 翻转 [0,kmodn−1] 区间的元素 |
5 6 7 4 3 2 1 |
| 翻转 [kmodn,n−1] 区间的元素 |
5 6 7 1 2 3 4 |
* @return {void} Do not return anything, modify nums in-place instead.
const rotate = (nums, k) => {
const newArr = new Array(nums.length);
for (let i = 0; i < nums.length; i++) {
newArr[(i + k) % nums.length] = nums[i];
for (let i = 0; i < nums.length; i++) {
* @return {void} Do not return anything, modify nums in-place instead.
const rotate = (nums, k) => {
const reverse = (i, j) => {
reverse(0, nums.length - 1);
reverse(k, nums.length - 1);
console.log(rotate([1, 2, 3, 4, 5, 6, 7], 3));
除自身以外数组的乘积
类似接雨水双向数组,可采取遍历的方式进行反向计算。可以最大程度上节省空间。
const productExceptSelf = (nums) => {
const leftArr = new Array(nums.length).fill(1);
const rightArr = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
leftArr[i] = leftArr[i - 1] * nums[i - 1];
for (let i = nums.length - 2; i >= 0; i--) {
rightArr[i] = rightArr[i + 1] * nums[i + 1];
const res = new Array(nums.length);
for (let i = 0; i < nums.length; i++) {
res[i] = leftArr[i] * rightArr[i];
const productExceptSelf = (nums) => {
const ansArr = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
ansArr[i] = ansArr[i - 1] * nums[i - 1];
for (let i = nums.length - 1; i >= 0; i--) {
ansArr[i] = right * ansArr[i];
console.log(productExceptSelf([1, 2, 3, 4]));
缺失的第一个正数
原地哈希,将数组的值i与下标i - 1相对应,没有对应的元素证明是第一个缺失的正整数。注意第三个判断条件nums[i] !== nums[nums[i] - 1]添加一个nums判断可以应对数组中重复值的情况。
const firstMissingPositive = (nums) => {
for (let i = 0; i < n; i++) {
for (let i = 0; i < n; i++) {
let num = Math.abs(nums[i]);
nums[num - 1] = -Math.abs(nums[num - 1]);
for (let i = 0; i < n; i++) {
const firstMissingPositive = (nums) => {
for (let i = 0; i < nums.length; i++) {
nums[i] <= nums.length &&
nums[i] !== nums[nums[i] - 1]
const temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
for (let i = 0; i < nums.length; i++) {
console.log(firstMissingPositive([3, 4, -1, 1]));
总结
数组的技巧性比较强,一般会与模拟,动态规划,哈希表结合,需要灵活使用。
矩阵
矩阵置零
常规的遍历标记然后取值,可以优化一下空间复杂度
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
const setZeroes = (matrix) => {
const rowSet = new Set();
const colSet = new Set();
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] === 0) {
rowSet.forEach((item) => {
for (let i = 0; i < matrix[0].length; i++) {
colSet.forEach((item) => {
for (let i = 0; i < matrix.length; i++) {
螺旋矩阵
核心思路为模拟,设置上下左右 4 个变量便于理解,for循环的时候采用变量命名的方式提醒自己是行还是列,同时注意left <= right && top <= bottom与left < right && top < bottom这两个判断条件的使用。
* @param {number[][]} matrix
const spiralOrder = (matrix) => {
if (!matrix.length || !matrix[0].length) {
right = matrix[0].length - 1,
bottom = matrix.length - 1;
while (left <= right && top <= bottom) {
for (let col = left; col <= right; col++) {
res.push(matrix[top][col]);
for (let row = top + 1; row <= bottom; row++) {
res.push(matrix[row][right]);
if (left < right && top < bottom) {
for (let col = right - 1; col > left; col--) {
res.push(matrix[bottom][col]);
for (let row = bottom; row > top; row--) {
res.push(matrix[row][left]);
[left, right, top, bottom] = [left + 1, right - 1, top + 1, bottom - 1];
console.log(spiralOrder(matrix));
旋转图像
可以采用额外空间来一一对应,或者采用特殊技巧,上下反转后再对角线反转。
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
const rotate = (matrix) => {
const matrix_new = new Array(n).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix_new[j][n - i - 1] = matrix[i][j];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix[i][j] = matrix_new[i][j];
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
const rotate = (matrix) => {
const length = matrix.length;
const halfLen = Math.floor(length / 2);
for (let i = 0; i < halfLen; i++) {
for (let j = 0; j < length; j++) {
const temp = matrix[i][j];
matrix[i][j] = matrix[length - i - 1][j];
matrix[length - i - 1][j] = temp;
for (let i = 0; i < length; i++) {
for (let j = 0; j < i; j++) {
const temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
搜索二维矩阵
定位到右上角,利用矩阵递增的性质,实现 Z 字形查找
* @param {number[][]} matrix
const searchMatrix = (matrix, target) => {
y = matrix[0].length - 1;
while (x < matrix.length && y >= 0) {
if (target < matrix[x][y]) {
} else if (target > matrix[x][y]) {
总结
矩阵的题目比较考验技巧与空间思维,需要严格分清行坐标与列坐标,写的时候需要思路清晰,一步一步来。