leetcode-hot-前端 js 版(持续更新中)

330 阅读22分钟

做题策略

最近面了 几个一线厂,发现前端算法基本属于 leetcode hot 100 中的题 且有以下特点

  • 字符串、数组、对象,堆栈等 相关操作题几乎占据 99%
  • 对前端来说几乎都是简单、中等 的题,当然如出困难级别的 自动放弃了
  • 链表、二叉树几乎不考 自动放弃 如准备时间充足可准备
  • 前端对象相关操作 (将树铺平 将铺平数组改为树)
  • 题目描述太多的一般不会出,因为笔试时间限制一般 15min 以内 长题干 需要的时间很长除非专门流出笔试时间
  • 如果拿到题后 3-5 分钟一点头绪都没 则主动咨询面试官能否换题

我们拿到 leetcode hot 100 之后

先按照 “出题频率排序” 从最高的频率开始往下刷 遇到“困难”、链表、二叉树等直接跳过,这么算下大概还剩 80 题 如下是详细题解

列表转成树形结构

[
  {
    id: 1,
    text: '节点1',
    parentId: 0 //这里用0表示为顶级节点
  },
  {
    id: 2,
    text: '节点1_1',
    parentId: 1 //通过这个字段来确定子父级
  }
  ...
]

转成
[
  {
    id: 1,
    text: '节点1',
    parentId: 0,
    children: [
      {
        id:2,
        text: '节点1_1',
        parentId:1
      }
    ]
  }
]
function filterArray(data, pid) {
  let tree = [];
  for (let i = 0; i < data.length; i++) {
    if (data[i].pid == pid) {
      tree.push({...data[i],children : filterArray(data, data[i].id)});
    }
  }
  return tree;
 }

树形结构转成列表

[
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点1_1',
                parentId:1
            }
        ]
    }
]
转成
[
    {
        id: 1,
        text: '节点1',
        parentId: 0 //这里用0表示为顶级节点
    },
    {
        id: 2,
        text: '节点1_1',
        parentId: 1 //通过这个字段来确定子父级
    }
    ...
]

复制代码

1、两数之和

  • 利用 hash (可用对象模拟)然后求另一个值
var twoSum = function(nums, target) {
  const numsMap = {};
  for(let i = 0;i<nums.length;i++){
      numsMap[nums[i]] = i
  }
   for(let i = 0;i<nums.length;i++){
      const left = target - nums[i] ;
      if(numsMap[left] && numsMap[left] !== i){
          return [i,numsMap[left]]
      }
  }
};

5. 最长回文子串

  • 该返回回文, 不是奇数就是偶数,那么根据奇偶数 两问题拆分细化,合并结果就是最终答案
var longestPalindrome = function (s) {
    let res = ""
    for (let i = 0; i < s.length; i++) {
        // 处理奇数回文串
        const s1 = palindrome(s, i, i)
        // 处理偶数回文串
        const s2 = palindrome(s, i, i + 1)
        res = res.length <= s1.length ? s1 : res
        res = res.length <= s2.length ? s2 : res
    }
    return res
};

// 返回以l,r为中心点扩散的最长回文串
function palindrome(s, l, r) {
    while (l >= 0 && r < s.length && s[l] === s[r]) {
        l-- // 左扩张
        r++ // 右扩张
    }
    return s.slice(l + 1, r)
}

3 无重复字符的最长子串

  • 潜在子串 字符存到 hash 来验证 如果存在重复 则从出事位置重复的那个值 index 举行往下走
  • 类似双指针概念,默认左右指针 右指针向右走当发现走的值在左右指针之间有重复,则先存下当前子串长度,然后左指针改为该重复值的 index 右指针重新从左指针的位置向右走
var lengthOfLongestSubstring = function(s) {
    let maxLeng = 1;
    if(!s) return 0
    for(let i = 0;i<s.length;i++){  
        const strMap = {[s[i]]:true}
        for(let j = i+1;j<= s.length;j++){
            if(!strMap[s[j]]){
                strMap[s[j]] = true
            } else {
                if(maxLeng<(j-i)){
                    maxLeng = j-i
                }
                break; 
            }
            if(j === s.length){
                if(maxLeng<(j-i)){
                    maxLeng = j-i
                }
            }
        }
    }
    return maxLeng
};

146.LRU 缓存

用map保存key
1.get 有key 缓存value 删掉key 再set一遍
2.put 有key 删掉 重新set 超出内存 删掉第一个key


var LRUCache = function(capacity) {
    this.capacity = capacity;
    this.map = new Map();
};

LRUCache.prototype.get = function(key) {
    if(this.map.has(key)){
        let temp=this.map.get(key)
         this.map.delete(key);
         this.map.set(key, temp);
         return temp
    }else{
        return -1
    }
};
LRUCache.prototype.put = function(key, value) {
    if(this.map.has(key)){
        this.map.delete(key);
    }
    this.map.set(key,value);
    if(this.map.size > this.capacity){
     
        this.map.delete(this.map.keys().next().value);
    }
};

70.爬楼梯

  • 从答案反向思维,第一步 只存在 一步或者 2 步的情况
  • 枚举发现符合公式
    • n=1 result=1
    • n=2 result=2
    • n=3 result=3
    • n=4 result=5 为 result(n=2)+result(n=3)之和
    • n=5 result=8 为 result(n=3)+result(n=4)之和
    • 该数列为斐波那契数列,数列的定义是从第三位开始,每一位的数为前两位之和。
var climbStairs = function(n) {
    step[1] = 1;//爬一步方法
    step[2] = 2;//爬两步方法
    const step = []
    for(let i = 3;i<=n;i++){
        step[i] = step[i-2] + step[i-1];
    }
    return step[n]
};

49. 字母异位词分组

  • 这道题 最难的是题目意思很模糊,题目没读懂肯定写不出来
  • 题目说人话:将相同字母组成的单词放在一个数组,如果只有一个那就放一个

如下 由b\a\t 三个字母组成的单词 在输入中只有一个;n\a\t三个字母组成的单词有 2 个

e\a\t三个字母组成的单词有 3 个

  • 互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
var groupAnagrams = function(strs) {
    const map = new Map();
    for (let str of strs) {
        let array = Array.from(str);
        array.sort();
        let key = array.toString();
        let list = map.get(key) ? map.get(key) : new Array();
        list.push(str);
        map.set(key, list);
    }
    return Array.from(map.values());
};

121. 买卖股票的最佳时机

  • 贪心算法 卖出那天肯定是最大的股价,买入的价格肯定是 max 之前的 min
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
  const len = prices.length;
  let maxProfit = 0,maxPrice = prices[len-1];
  for(let i=len-2;i>=0;i--){
      maxProfit = Math.max(maxProfit,maxPrice-prices[i]);
      maxPrice = Math.max(maxPrice,prices[i]);
  } 
  return maxProfit;
};

15. 三数之和

  • 先排序,锁定一个值 然后双指针靠拢,由于不能重复 所以遇到相同值跳指针
var threeSum = function (nums) {
    let i, L, R, sum = 0, store = []
    let newNums = nums.sort((a, b) => a - b)
    for (i = 0; i < newNums.length; i++) {
        if (newNums[i] > 0) break
        if(newNums[i] === newNums[i - 1]) continue
        L = i + 1
        R = newNums.length - 1
        while (L < R) {
            sum = newNums[i] + newNums[L] + newNums[R]
            if (sum === 0) {
                store.push([newNums[i], newNums[L], newNums[R]])
                while (newNums[L] === newNums[L + 1]) L++
                L++
            } else if (sum < 0) {
                L++
            } else if (sum > 0) {
                R--
            }
        }
    }
    return store
}

11. 盛最多水的容器

  • 双指针法。从左右两边开始计算面积,应用较高的线来寻找较长的范围,从而获得较大的面积。因此当左值较小时,左指针增加,右值较小时,右指针减小。
var maxArea = function(height) {
  let l = 0,r = height.length-1;
  let sum = 0;
  let maxSum = 0
  while (l<r) {
    sum = (r-l)* Math.min(height[l], height[r])
    if(height[l] >height[r]){
      r--;
    }else{
      l++;
    }
     maxSum = Math.max(maxSum, sum);

  }
  return maxSum;
};

20. 有效的括号

虽然看题目就知道是堆栈,但是一时还在很不知道怎么写

  • 不使用堆栈也可直接贪心解决:该字符一定至少存在一对[]、{}、()
var isValid = function (s) {
    while (s.length) {
        var temp = s;
        s = s.replace('()', '');
        s = s.replace('[]', '');
        s = s.replace('{}', '');
        if (s == temp) return false
    }
    return true;
};

堆栈版

var isValid = function(s) {
    const stack = []; 
    const map = {
            "(":")",
            "{":"}",
            "[":"]"
        };
    for(const x of s) {
        if(x in map) {
            stack.push(x);
            continue;
        };
        if(map[stack.pop()] !== x) return false;
    }
    return !stack.length;
};

200. 岛屿数量

注意题意 连续大陆就是一个岛屿

遍历二维数组,每当遇到1开启搜索模式,从当前节点向左/右/上/下,每次分别移动一步,如果是1则替换为0


var numIslands = function(grid) {
  function dfs(grid,i,j){
    // 递归终止条件
    if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==='0'){
       return  
    }
    grid[i][j]='0' // 走过的标记为0
    dfs(grid, i + 1, j)
    dfs(grid, i, j + 1)
    dfs(grid, i - 1, j)
    dfs(grid, i, j - 1)
  }
  let count=0
  for(let i=0;i<grid.length;i++){
      for(let j=0;j<grid[0].length;j++){
          if(grid[i][j]==='1'){
              dfs(grid,i,j)
              count++
          }
      }
  }
 return count
};

128. 最长连续序列

  • 本身很简单 但是多了一个条件“时间复杂度为 O(n)”
  • 查找 Set 中的元素的时间复杂度是 O(1),JS 的 Set 能给数组去掉重复元素

将数组元素存入 set 中,遍历数组 nums

如果 当前项 - 1 存在于 set ,说明当前项不是连续序列的起点,跳过,继续遍历

当前项没有“左邻居”,它就是连续序列的起点

不断在 set 中查看 cur + 1 是否存在,存在,则 count +1

cur 不再有 “右邻居” 了,就算出了一段连续序列的长度

var longestConsecutive = (nums) => {
  const set = new Set(nums) // set存放数组的全部数字
  let max = 0
  for (let i = 0; i < nums.length; i++) {
    if (!set.has(nums[i] - 1)) { // nums[i]没有左邻居,是序列的起点
      let cur = nums[i]
      let count = 1
      while (set.has(cur + 1)) { // cur有右邻居cur+1
        cur++ // 更新cur
        count++ 
      }
      max = Math.max(max, count) // cur不再有右邻居,检查count是否最大
    }
  }
  return max
}

215. 数组中的第K个最大元素

最常用以下解法

但是 复杂度不符合

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
   return nums.sort((a,b)=>b-a)[k-1]
};

279. 完全平方数

  • 动态规划
  • 思路:dp[i] 表示i的完全平方和的最少数量,dp[i - j * j] + 1表示减去一个完全平方数j的完全平方之后的数量加1就等于dp[i],只要在dp[i], dp[i - j * j] + 1中寻找一个较少的就是最后dp[i]的值。

复杂度:时间复杂度O(n* sqrt(n)),n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n),空间复杂度O(n)

var numSquares = function(n) {
    // 创建一个数组,直接在for里面填充好一点,不用fill(),这样节省时间
    let dp = []
    dp[0] = 0
    for(let i = 1; i <= n; i++){
        dp[i] = i
        for(let j = 1; i - j * j >= 0; j++){
            dp[i] = Math.min( dp[i] ,dp[i - j * j] +1 )
        }
    }
    return dp[n]
};

739. 每日温度

栈中记录还没算出「下一个更大元素」的那些数(的下标)。

var dailyTemperatures = function (temperatures) {
    const n = temperatures.length;
    const ans = new Array(n).fill(0);
    const st = [];
    for (let i = 0; i < n; i++) {
        const t = temperatures[i];
        while (st.length && t > temperatures[st[st.length - 1]]) {
            const j = st.pop();
            ans[j] = i - j;
        }
        st.push(i);
    }
    return ans;
};

22. 括号生成

  • n 意味着 n个左括号, n个右括号 递归配对,第一个一定是左括号,且剩下的一定是左括号少于右括号
  • 每一个树状递归结尾都会走到 终止路线
/**
 * @param {number} n
 * @return {string[]}
 */

 var generateParenthesis = function (n) {
    var list = []
    function dfs(str, left, right) {
        if (left == 0 && right == 0) { // 当前左右都用光了则一次拼接完成
            list.push(str)
            return;
        }
        if (left > 0) {
            dfs(str + '(', left - 1, right)
        }

        if (right > 0 && left < right) {
            dfs(str + ')', left, right - 1)
        }
    }
    dfs('', n, n)
    return list
};

39. 组合总和

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
const combinationSum = (candidates, target) => {
  const res = [];
  
  function dfs(start, temp, sum){ // start是当前选择的起点索引 temp是当前的集合 sum是当前求和
    if (sum > target) {
        return;   // 结束当前递归
    }
    if (sum === target) {
        res.push(temp);
        return;
    }
    for (let i = start; i < candidates.length; i++) {
        dfs(i, [...temp,candidates[i]], sum + candidates[i]);
    }
  };
  dfs(0, [], 0); // 最开始可选的数是从第0项开始的,传入一个空集合,sum也为0
  return res;
};

46. 全排列

/**
 * @param {number[]} nums
 * @return {number[][]}  认真读题 《不含重复数字的数组》
 */
var permute = function(nums) {
    let result = []
  function fn(arr){

     if(arr.length === nums.length){
          result.push(arr)
          return;
     }
      nums.forEach(v=>{
         if(!arr.includes(v)){
             fn([...arr,v])
         }
      })
  }
  fn([])
  return result
};

53. 最大子数组和

var maxSubArray = function(nums) {
    let ans = nums[0];
    let sum = 0;
    for(const num of nums) {
        if(sum > 0) {
            sum += num;
        } else {
            sum = num;
        }
        ans = Math.max(ans, sum);
    }
    return ans;
};

56. 合并区间

var merge = function (intervals) {
  intervals.sort((a, b) => a[0] - b[0])
  for (let i = 1; i < intervals.length; i++) {
    if (intervals[i][0] <= intervals[i - 1][1]) {
      intervals[i - 1][1] = Math.max(intervals[i][1], intervals[i - 1][1])
      intervals.splice(i, 1)
      i--
    }
  }
  return intervals
};

78. 子集

var subsets = function (nums) {
    let target = [[]];
    function fn(oldArr,j){
        if(j>  nums.length){
            return;
        }
        for(let k = j+1;k < nums.length;k++) {
            const newArr = [...oldArr,nums[k]]
            target.push(newArr);
            fn(newArr,k);
        }
    }
    for(let i = 0;i<nums.length;i++) {
        target.push([nums[i]]);

        fn([nums[i]],i)
    }
    return target
};

581 最短无序连续子数组

var findUnsortedSubarray = function(nums) {
    //浅复制原数组,并排序做为参考
    const temp = nums.slice();
    temp.sort((a,b) => a - b);
    //定义左右指针,找到符合题意的子数组的左右边界
    let i = 0, j = temp.length - 1;
    while (i <= j) {
        if (temp[i] == nums[i]) {//如果相同,左指针一直右移
            i++;
        } else {//如果不同(此时子数组的左边界已经确定),则从右开始确定子数组的右边界
            if (temp[j] == nums[j]) {//如果相同,右指针一直左移
                j--;
            } else {//如果不同,此时左右边界都确定了,就可以得出最短子数组的长度返回了
                return j - i + 1;
            }
        }
    }
    //如果前面的循环里没有返回,说明原数组nums是有序的,最短子数组的长度为0
    return 0;
};

560. 和为 K 的子数组

var subarraySum = function(nums, k) {
  if (nums.length <= 0) return 0;
  let map = new Map([[0, 1]]);
  let sum = 0,
    count = 0;
  for (let i = 0; i < nums.length; i++) {
    sum += nums[i];
    // 注意顺序,要先判断存不存在sum - k
    // 然后再设置map
    if (map.has(sum - k)) {
      count += map.get(sum - k);
    }
    if (!map.has(sum)) {
      map.set(sum, 1);
    } else {
      map.set(sum, map.get(sum) + 1);
    }
  }
  return count;
};

647. 回文子串

var countSubstrings = function(s) {   
    var res = [];

    for (var i = 0; i  < s.length; i ++) {
        for (var j = 0; j < 2; j++) {
            var left = i;
            var right = left + j;

            while(s[left] && s[left] === s[right]) {
                var subString = s.substring(left, right + 1);
                res.push(subString);

                left--;
                right++;
            }
        }
    }
    return res.length;

};

26. 删除有序数组中的重复项

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function (nums) {
  let slow = 0;
  let fast = 0;
  while(fast < nums.length) {
    if (nums[slow] !== nums[fast]) {
      slow++;
      nums[slow] = nums[fast];
    } 
    fast += 1;
  }
  return slow + 1;
};

33. 搜索旋转排序数组

var search = function(nums, target) {
     let map = new Map;
     for(let i = 0;i < nums.length;i++)
        map.set(nums[i],i);
     if(map.has(target)) return map.get(target);
     else return -1;
 };

34. 在排序数组中查找元素的第一个和最后一个位置

var searchRange = function(nums, target) {
//查找左边界的函数,是左边界二分查找模版
    const findLeft = (nums, target) => {
        let left = 0, right = nums.length - 1
        while (left <= right) {
            let mid = Math.floor((right - left) / 2) + left  // 当前做左和右的 中间值
            if (nums[mid] > target)// 在中间值的左边 则更新右角标
                right = mid - 1
            else if (nums[mid] < target)   // 在中间值的右边 则更新左角标
                left = mid + 1
            else if (nums[mid] == target)
                right = mid - 1
        }
//注意不要在此处进行条件判断返回-1
        return left
    }

    let result = new Array(2)
//而是在此处进行条件判断
    const resultLeft = findLeft(nums, target)
    if (resultLeft >= nums.length || nums[resultLeft] != target)// 出界或者不存在则返回  -1 -1 
        result = [-1, -1]
    else
        result = [resultLeft, findLeft(nums, target + 1) - 1]  // 否则利用 目标值+1 其左边界就是期望的右边界
    return result
    
};

48. 旋转图像

解题思路:找规律

  1. 首先,对于矩阵中第 i行的第 j个元素,在旋转后,它出现在倒数第 i列的第 j行个位置。推导出:

Matrix[j][n - 1 - i] = Matrix[i][j]

  1. 由于题目要求原地旋转,所以需要一次性同时交换四个位置:按照1中的公式,找出这四个位置的旋转关系:

Matrix[j][n - 1 - i] = Matrix[i][j]

Matrix[n - 1 - i][n - 1 - j] = Matrix[j][n - 1 - i]

Matrix[n - 1 - j][i] = Matrix[n - 1 - i][n - 1 - j]

Matrix[i][j] = Matrix[n - 1 - j][i]

  1. 确定旋转的次数:
    • 当n为偶数时:需要旋转 n^2 / 4 = n / 2 * n / 2次。即:双层循环,每层次数为 n / 2 次
    • 当n为奇数时:需要旋转 (n^2 − 1) / 4 = ((n−1)/2) * ((n+1)/2)次。即:双层循环,第一层次数为n - 1 / 2次,第二层次数为 n + 1 / 2次(其实也可以反过来)。
    • 综合以上两种情况:第一层循环的次数为 Math.floor(n / 2) 次,第二层循环的次数为Math.floor((n + 1) / 2)次(其实也可以反过来)
var rotate = function(matrix) {
  const n = matrix.length;
  for (let row = 0; row < Math.floor(n / 2); row++) {
    for (let col = 0; col < Math.floor((n + 1) / 2); col++) {
      const temp  = matrix[row][col] // 需要引入一个临时变量来中转一下
      matrix[row][col] = matrix[n - 1 - col][row];
      matrix[n - 1 - col][row] = matrix[n - 1 - row][n - 1 - col];
      matrix[n - 1 - row][n - 1 - col] = matrix[col][n - 1 - row];
      matrix[col][n - 1 - row] = temp;
    }
  }
};

56. 合并区间

解题思路: 先排序,再合并

  1. 先将区间集合根据左区间的进行升序排列。原因:
    • 其实用一端就能判断出两个区间有没有重叠
    • 只需要关心合并后的右区间
  1. 将默认的重叠区间设为intervals[0]。然后从intervals[1]开始遍历,如果当前区间的左区间 <= 重叠区间的右区间。说明有重叠。将两个区间进行合并,并且更新重叠区间:overLappingIntervals[1] = max(overLappingIntervals[1], cur[1])。
  2. 如果当前区间没和当前的重叠区间发生重叠,那么将重叠区间加入res。并且更新最新的重叠区间为cur。
  3. 记得遍历完区间后,需要将最后一个重叠区间加入到res中。
var merge = function (intervals) {
  const res = [];
  // 先根据左区间进行排序,好处: 1.判断一端就能保证区间有没有重叠 2.只需要确定合并后的右区间
  intervals = intervals.sort((a, b) => a[0] - b[0]);
  // overLappingIntervals 为最新的重叠区间,默认的第一个重叠区间为 intervals[0]
  let overLappingIntervals = intervals[0];

  for (let i = 1; i < intervals.length; i++) {
    let cur = intervals[i];
     // 有重合:    |------|
     //       |------|
    if (overLappingIntervals[1] >= cur[0]) {
      overLappingIntervals[1] = Math.max(cur[1], overLappingIntervals[1]); 
    } else {      
       // 不重合,overLappingIntervals推入res数组 
      res.push(overLappingIntervals);
      overLappingIntervals = cur;  // 更新 overLappingIntervals
    }
  }
  // 重点!! 记得将最后一个重叠区间push
  res.push(overLappingIntervals);
  return res;
};

75. 颜色分类

  • 第一个循环把 0 放在最前面
  • 第二个循环把 1 放在 最后一个 0开始的位置
var sortColors = function(nums) {
   let ptr = 0;
    for (let i = 0; i < nums.length; ++i) {
        if (nums[i] === 0) {
            [nums[i], nums[ptr]] = [nums[ptr], nums[i]];
            ++ptr;
        }
    }
    for (let i = ptr; i < nums.length; ++i) {
        if (nums[i] === 1) {
            [nums[i], nums[ptr]] = [nums[ptr], nums[i]];
            ++ptr;
        }
    }
};

88 合并两个有序数组

var merge = function(nums1, m, nums2, n) {
    let k = m + n - 1, i = m - 1, j = n - 1;
    while(j >= 0){
        if(nums1[i] >= nums2[j] ){
            nums1[k--] = nums1[i--]
        }else{
            nums1[k--] = nums2[j--]
        }
    }
};

189. 轮转数组

215. 数组中的第K个最大元素

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
   return nums.sort((a,b)=>b-a)[k-1]
};

283. 移动零

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function(nums) {
    nums.sort((a,b) => b? 0: -1)
};

14、最长公共前缀

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    strs.sort()//按编码排序
    if (strs.length === 0) return ''//空数组返回''
    var first = strs[0],
        end = strs[strs.length - 1]
    if(first === end || end.match(eval('/^' + first + '/'))){
        return first//first包含于end返回first
    }
    for(var i=0;i<first.length;i++){
        if(first[i] !== end[i]){
            return first.substring(0,i)//匹配失败时返回相应字符串
        }
    }
};

136. 只出现一次的数字

var singleNumber = (nums) => {
  let res = nums[0]
  for (let i = 1; i < nums.length; i++) {
    res = res ^ nums[i]
  }
  return res
}

169. 多数元素

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
  nums.sort((a,b) => a - b)
  return nums[Math.floor(nums.length / 2)]
};

242. 有效的字母异位词

287. 寻找重复数

快慢指针:题目说数组必存在重复数,所以 nums 数组肯定可以抽象为有环链表。

首先,如果有环的话,那么快慢指针一定会在环内相遇。

  1. 相遇时,慢指针走的距离:D+S1D+S1
  2. 假设相遇时快指针已经绕环 n 次,它走的距离:D+n(S1+S2)+S1D+n(S1+S2)+S1
  3. 因为快指针的速度是 2 倍,所以相同时间走的距离也是 2 倍:D+n(S1+S2)+S1 = 2(D+S1)D+n(S1+S2)+S1=2(D+S1)即 (n-1)S1+ nS2=D(n−1)S1+nS2=D
  4. 我们不关心绕了几次环,取 n = 1 这种特定情况,消掉 S1: D=S2

所以此时,让快指针回到原点,当下次慢指针和快指针相遇的时候,就是在环的入口处。

1 3 4 2 2

0 1 2 3 4

var findDuplicate = function(nums) {
  let fast = 0;
  let slow = 0;

  while (1) {
    fast = nums[nums[fast]];
    slow = nums[slow];

    if (slow === fast) {
      fast = 0;
      while(nums[slow] != nums[fast]) {
        fast = nums[fast];
        slow = nums[slow];
      }
      return nums[slow];
    }
  }
};

454. 四数相加 II

解题思路:Hash Map: 简单的说,将四数之和转化为两数之和。

  1. 列举出nums1和nums2的所有组合放入mapGroup1中。
  2. 将nums3和nums4进行组合,统计nums3和nums4的和与mapGroup1相加结果为0的个数
/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @param {number[]} nums3
 * @param {number[]} nums4
 * @return {number}
 */
var fourSumCount = function(nums1, nums2, nums3, nums4) {
    const numMap = new Map()
    let res = 0;
    for(let n1 of nums1) {
        for(let n2 of nums2) {
            let cnt = numMap.get(n1+n2) || 0
            numMap.set(n1+n2, cnt+1)
        }
    }
    for(let n3 of nums3) {
        for(let n4 of nums4) {
            if (numMap.has(0-(n3+n4))) {
                res += numMap.get(0-(n3+n4))
            }
        }
    }
    return res
};

1021. 删除最外层的括号

核心还是堆栈,思维方式要反

输入:s = "(()())(())"

每次输入)时 前面移除一位,如果移除空了则加入字符中记录

/**
 * @param {string} S
 * @return {string}
 */
var removeOuterParentheses = function(S) {
    
    if (!S || S === '') {
        return '';
    }
    let res = '';
    let stack = [];
    let start = 0;
    for (let i = 0; i < S.length; i++) {
        if (S[i] === '(') {
            stack.push('(');
        } else {
            stack.pop();
            if (stack.length === 0) {
                res += S.substring(start + 1, i);
                start = i + 1;
            }
        }
    }
    return res;
    
};

1047. 删除字符串中的所有相邻重复项

  • 消消乐策略
  • 堆栈模型
/**
 * @param {string} s
 * @return {string}
 */
var removeDuplicates = function(s) {
  const stack = [s[0]];
  let topIndex = 0;
  for (let i = 1; i < s.length; i++) {
    // 1. 长度为0的情况,直接进入队列
    if (topIndex === -1) {
      stack.push(s[i]);
      topIndex += 1;
      // 2. 判断栈顶和当前的s[i]是否相等,相等的话则出栈
    } else if (stack[topIndex] === s[i]) {
      topIndex -= 1;
      stack.pop(); 
    } else {
      // 不相等的话,入栈,topIndex + 1
      topIndex += 1;
      stack.push(s[i]);
    }
  }
  return stack.join('');
};

55. 跳跃游戏

  • 实质是判断数组是否为环形链表
/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function(nums) {
    let max = 0;// 计算每个元素能跳最远的位置,所有元素最远的位置小于数组长度 则肯定跳不到最后
    for(var i = 0; i <= max; i++){
      max = Math.max(nums[i] + i, max);
    }
    return i >= nums.length 
};

回溯算法

常用模板

  • 大函数包裹小函数
  • 小函数一般两个传参,
    • 第一个参 是解合计 首次传递默认是空
    • 第二个参数是子问题的项 首次传递大函数的默认值
    • 首先是终止 if 内包裹最后一条时 做的操作 如 push 等
    • 下面是循环 符合条件则继续递归小函数
    • 每次递归小函数时 参数变小 类似回溯
  • 回溯算法最终解一般在小函数外定义的变量进行收集

如下是常见的标准模板

function backtrack(path, options) {  
    if (/* 终止条件 */) {  
        // 存储/打印结果  
        result.push(path.slice());  // 注意要拷贝一份,不要直接 push path  
        return;  
    }  
  
    for (let i = 0; i < options.length; i++) {  
        // 做选择  
        path.push(options[i]);  
        // 继续递归探索  
        backtrack(path, options);  
        // 撤销选择  
        path.pop();  
    }  
}  
  
// 使用示例  
let result = [];  
backtrack([], [1, 2, 3]);  // 这里的例子是求 [1,2,3] 的所有子集  
console.log(result);

17. 电话号码的字母组合

var letterCombinations = function(digits) {
    if(!digits){
        return []
    }
  const map = {
    2: ["a", "b", "c"],
    3: ["d", "e", "f"],
    4: ["g", "h", "i"],
    5: ["j", "k", "l"],
    6: ["m", "n", "o"],
    7: ["p", "q", "r", "s"],
    8: ["t", "u", "v"],
    9: ["w", "x", "y", "z"],
  };
  let arr = []
  function dfs(startArr,strs){
    if(!strs.length){
        arr.push(startArr)
        return ;
    }else{
        const  str = map[strs[0]];
       for(let i = 0;i< str.length;i++){
        dfs(startArr+str[i],strs.slice(1,strs.length))
       }
    }
  }
  dfs("",digits);
  return arr
};

22. 括号生成

var generateParenthesis = function(n) {
  const res = [];
  // 1. 第一个关键点: '所有可能' 确定用递归的方式实现。
  const dfs  = function(lRemain , rRemain, str) {
    // 2. 第二个关键点: 递归结束的条件:str的长度为 n 的两倍
    if (str.length === n * 2) {
      res.push(str);
      return;
    }
    // 3. 第三个关键点: 有效的字符串必须是 左括号优先进入。且左括号的个数永远小于等于右括号
    // 选择左括号的前提条件:只要左括号还有的剩,可以直接选择左括号
    if (lRemain > 0) {
      dfs(lRemain - 1, rRemain, str + '(');
    }
    // 选择右括号的前提条件: 剩余的右括号要大于左括号时,才可以让右括号进入,才能保证有效性
    if (rRemain > lRemain) {
      dfs(lRemain, rRemain - 1, str + ')');
    }
  }
  dfs(n , n, '');
  return res;
}

39. 组合总和

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
  const res = [];
  const dfs = (canStartIndex, currentSum, currentArr) => {
    if (currentSum > target) {
      return;
    }
    if (currentSum === target) {
      res.push([...currentArr])
      return;
    }
    for (let i = canStartIndex; i < candidates.length; i++) {
      dfs(i, currentSum + candidates[i], [...currentArr, candidates[i]]);
    }
  }
  dfs(0, 0, []);
  return res;
}

46. 全排列

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
  const res = [];
    const dfs = (usedIndexs, currentArr) => {
        if (currentArr.length === nums.length) {
          res.push([...currentArr]);
          return;
        }
        for (let i = 0; i < nums.length; i++) {
          // 如果已经选择过当前下标,那么就不做处理
          if (usedIndexs[i]) {
            continue;
          }
          dfs(
            {...usedIndexs, [i]: true}, 
            [...currentArr, nums[i]]
          );
        }
    }
    dfs({}, []);
    return res;
};

78. 子集

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
 var subsets = function(nums) {
  const res = [];
  const dfs = (startIndex, list) => {
    res.push(list);
    // 由于必须从选择过了的元素右侧开始选择
    for (let i = startIndex; i < nums.length; i++) {
      dfs(i + 1, [...list, nums[i]]);
    }
  };
  dfs(0, []);
  return res;
};
//                          []
//                    /      |      \
//              [1]         [2]     [3]
//          /      \            |            
//       [1, 2]   [1, 3]   [2, 3]             
//       /                   
//     [1, 2, 3]   

79. 单词搜索

var exist = function (board, word) {
  const row = board.length;
  const columns = board[0].length;
  let used = new Array(row).fill(false).map(() => new Array(columns).fill(false))
  // 该函数计算从 i,j 这个点开始找能否找到
  const canFind = (i, j, currentWordIndex) => {
    if (currentWordIndex === word.length) {
      return true;
    }

    if (i < 0 || j < 0 || i >= row || j >= columns) {
      return false; // 1.下标越界的场景,不能走出框外 (下标越界的情况要最先判断、提前return。防止return)
    }

    if (used[i][j]) {
      return false // 3. 可能上一步往上走、下一步又往下走了,不行。所以记录一下(不能走走过的点)
    }

    // 在dfs函数中,判断false场景
    if (board[i][j] !== word[currentWordIndex]) {
      return false // 2. 下一步的字符对不上
    }

    
    used[i][j] = true;
    // 上、下、左、右都走走一下,试一试
    const canFindRes = 
       canFind(i + 1, j, currentWordIndex + 1)
    || canFind(i, j + 1, currentWordIndex + 1)
    || canFind(i - 1, j, currentWordIndex + 1)
    || canFind(i, j - 1, currentWordIndex + 1)
    if (canFindRes) {
      return true;
    } else {
      // 重点!!!: 如果上下左右都走不通,这个点行不通,回撤路径
      used[i][j] = false;
      return false;
    }
  }

  for (let i = 0; i < row; i++) {
    for (let j = 0; j < columns; j++) {
      // 先找到 '头'。这个总没错的
      if (board[i][j] === word[0]) {
        // 每到一个点做的事情是一样的。DFS 往下选点,构建路径。
        if (canFind(i, j, 0)) {
          // 一旦找到一个就立马return
          return true;
        }
      }
    }
  }
  return false;
};

console.log('res', exist([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], 'SEE'));

93. 复原 IP 地址

/**
 * @param {string} s
 * @return {string[]}
 */
var restoreIpAddresses = (s) => {
  const res = [];
  const dfs = (canStartIndex, selectArr) => {
    const unCheckedRes = selectArr.join('.');
    if (selectArr.length === 4 && unCheckedRes.length === s.length + 3) {
      res.push(unCheckedRes);
    }

    for (let singleLen = 1; singleLen <= 3; singleLen++) {
      const nextStartIndex = canStartIndex + singleLen;
      // 假设下标越界了,直接返回
      if (nextStartIndex > s.length) {
        return;
      }
      const str = s.substring(canStartIndex, nextStartIndex);
      // 假设当前截断的数超过255 或 02、033 这样的情况直接返回
      if (Number(str) > 255 || (str.length > 1 && str[0] === '0')) {
        return;
      }
      dfs(nextStartIndex, [...selectArr, str]);
    }
  }
  dfs(0, []);
  return res;
}

动态规划

动态规划(Dynamic Programming, DP)是一种解决优化问题的方法,它将问题分解为更小的子问题,并将这些子问题的解存储起来,以便在解决更大问题时重用它们。这种方法避免了重复计算相同的子问题,从而提高了算法的效率。

在JavaScript中,实现动态规划通常涉及创建一个数组(或二维数组)来存储子问题的解,然后按照问题的要求填充这个数组。

下面是一个使用动态规划解决“斐波那契数列”问题的模板和例子:

// 动态规划模板 - 斐波那契数列  
function fibonacci(n) {  
    // 创建一个数组来存储斐波那契数列的值  
    const dp = new Array(n + 1).fill(0);  
  
    // 初始化基本情况  
    dp[0] = 0;  
    if (n > 0) {  
        dp[1] = 1;  
    }  
  
    // 填充剩余的值  
    for (let i = 2; i <= n; i++) {  
        dp[i] = dp[i - 1] + dp[i - 2];  
    }  
  
    // 返回第n个斐波那契数  
    return dp[n];  
}  
  
// 示例:计算第10个斐波那契数  
const n = 10;  
console.log(fibonacci(n)); // 输出: 55

在这个例子中,fibonacci函数接受一个参数n,并返回斐波那契数列中的第n个数。我们使用一个数组dp来存储斐波那契数列的值,其中dp[i]表示第i个斐波那契数。我们从基本情况开始(即dp[0] = 0和dp[1] = 1),然后使用一个循环来计算剩余的值。

53. 最大子数组和

var maxSubArray = function(nums) {
    let ans = nums[0];
    let sum = 0;
    for(const num of nums) {
        if(sum > 0) {
            sum += num;
        } else {
            sum = num;
        }
        ans = Math.max(ans, sum);
    }
    return ans;
};

62. 不同路径

/**
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var uniquePaths = function(m, n) {
  // 特殊情况,单独处理
  if (m * n ===  1 || n * m === 2) {
    return 1;
  }
  // 1. 定义dp[i][j]: 到达dp[i][j]有几种不同的路径 
  // 2. dp[i][j] = dp[i + 1][j] + dp[i][j + 1]
  // 3. Base Case: dp[m - 1][*] = 1; dp[*][n - 1] = 1;
  const dp = new Array(m).fill(false).map(() => new Array(n).fill(false));
  for (let row = 0; row < m; row++) {
    dp[row][0] = 1;
  }
  for (let col = 0; col < n; col++) {
    dp[0][col] = 1;
  }

  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      //  考虑一下下标越界的情况
       dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    }
  }
  console.log(dp);
  return dp[m - 1][n - 1]
};

64. 最小路径和

var minPathSum = (grid) => {
  // 思路:其实这也是一个动态规划的题 要到[m,n]的最小路径,就算到 Math.min([m-1, n],[m. n -1])的最小路径
  const columns = grid[0].length; // 列
  const row = grid.length; // 行
  // 自己维护一个二维数组来保存到各个点的最小路径
  const dp = new Array(row)
    .fill(null)
    .map((item) => new Array(columns).fill(0));
  dp[0][0] = grid[0][0];
  // 向下和向右都只有一种走法,先将第一行和第一列填充好。(已知条件)
  for (let i = 1; i < columns; i++) {
    dp[0][i] = dp[0][i - 1] + grid[0][i];
  }

  for (let i = 1; i < row; i++) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
  }
  // 开始计算到每个点的路径
  for (let i = 1; i < row; i++) {
    for (let j = 1; j < columns; j++) {
      dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
    }
  }
  return dp[row - 1][columns - 1];
};

70. 爬楼梯

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
  const dp = new Array(n + 1).fill(false);
  dp[1] = 1;
  dp[2] = 2;
  for (i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
};

96. 不同的二叉搜索树

/**
 * @param {number} n
 * @return {number}
 */
var numTrees = function(n) {
  // dp[i]=∑dp[j]∗dp[i−j−1],0<=j<=i−1 
  const dp = new Array(n + 1).fill(false);
  dp[0] = 1;
  dp[1] = 1;
  // 从2开始,计算 dp[i]。
  for (let i = 2; i < n + 1; i++) {
    let count = 0;
    for (let j = 0; j < i; j++)  {
      // j 为左边分配的个数,那么右边分配的个数为 i - j - 1(还有一个根元素)
      count += dp[j] * dp[i - j - 1];
    }
    dp[i] = count;
  }
  return dp[n];
};

121. 买卖股票的最佳时机

var maxProfit = function(prices) {
    let isMax = 0;
    for(let i = 1;i<prices.length;i++){
        if(prices[i-1] <prices[i]){
            isMax=1
        }
    }
    if(isMax === 0){
        return 0
    }
    let max = Math.max(...prices.slice(1,prices.length));
    const min = Math.min(...prices.slice(0,prices.length-1));
    return max-min

};

152. 乘积最大子数组

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxProduct = function(nums) {
  let res = nums[0];
  // 存在负负得正的情况,所以dp[i]的最大值、最小值都要记录一下,因为nums[i]可能为正数、也可能为负数
  const dp = new Array(nums.length).fill(false).map(() => new Array(2));
  //  base case
  dp[0][0] = nums[0]; // 以下标为0结尾的子数组的最小值;
  dp[0][1] = nums[0]; // 以下标为0结尾的子数组的最大值;
  for (let i = 1; i < nums.length; i++) {
    dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]);
    dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]);
    res = Math.max(dp[i][1], res);
  }
  return res;
}

198. 打家劫舍

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function (nums) {
  let dp = new Array(nums).fill(false);
  dp[0] = nums[0];
  dp[1] = Math.max(nums[0], nums[1]);
  for (let i = 2; i < nums.length; i++) {
    // 假设,当前的值和前前家的加起来大于偷上一家的最大值。
    if (nums[i] + dp[i - 2] > dp[i - 1]) {
      dp[i] = dp[i - 2] + nums[i]
    } else {
      dp[i] = dp[i - 1]
    }
  }
  return dp[nums.length - 1];
};

221. 最大正方形

279. 完全平方数

/**
 * @param {number} n
 * @return {number}
 */
var numSquares = function(n) {
  // 定义dp[i]:和为 i 的完全平方数的最小数量
  const dp = new Array(n + 1).fill(false);
  // 初始化 Base Case:
  dp[0] = 0;
  dp[1] = 1;
  // 计算dp[i]
  for (let i = 2; i < n + 1; i++) {
    dp[i] = i; // 默认设一个最大值为i(比如4 = 1 + 1 + 1 + 1 就是最坏的情况)
    // 枚举 [1, √i]的所有情况
    for (let j = 1; i - j * j >= 0; j++) {
      dp[i] = Math.min(dp[i - j * j] + 1, dp[i]);
    }
  }
  console.log(dp);
  return dp[n];
};

300. 最长递增子序列

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
  // 含第 i 个元素的最长上升子序列的长度。
  // 在这里顺便做了一个 初始化 Base Case
  const dp = new Array(nums.length).fill(1);
  let res = dp[0];
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      // dp[i] 有个默认的初始化值 为1
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)        
      } else {
        dp[i] = Math.max(dp[i], dp[j]);
      }
    }
    res = Math.max(res, dp[i]);
  }
  return res;
}

322. 零钱兑换

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  if (amount === 0) {
    return 0;
  }
  // 初始化Base Case 1
  const dp = new Array(amount + 1).fill(-1);
  // 初始化Base Case 2
  for (coin of coins) {
    dp[coin] = 1;
  }
  for (i = 1; i < amount + 1; i++) {
    for (coin of coins) {
      // 考虑下标越界的情况 && 上一步的硬币数是可以组成的
      if (i - coin >= 0 && dp[i - coin] > 0) {
        // 先确保 dp[i] 能被组成不为 -1。 再考虑取较小值
        if (dp[i] > 0) {
          dp[i] = Math.min(dp[i], dp[i - coin] + 1)
        } else {
          dp[i] = dp[i - coin] + 1;
        }
      }
    }
  }
  console.log(dp);
  return dp[amount]
};

647. 回文子串

深度优先搜索

深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。在图中,这个算法是用来标记访问过和未访问过的顶点,并保持追踪它当前访问的顶点。

这个例子中,我们有一个简单的无向图,用邻接列表来表示。neighbors对象包含了图中每个节点及其相邻节点的信息。dfs函数是深度优先搜索的实现,它接受当前节点和一个记录访问状态的visited对象。如果当前节点未被访问过,我们就标记它为已访问,并对它的每个邻居递归调用dfs函数。

注意:在实际应用中,你可能需要根据你的具体需求和数据结构来调整这个模板。例如,如果你的图是有向的,或者你需要收集关于遍历路径的额外信息,你可能需要修改这个模板来适应这些需求。

function dfs(node, visited) {  
    if (node == null) {  
        return;  
    }  
      
    if (!visited[node]) {  
        visited[node] = true;  
        console.log(node);  // 或者其他你想对节点进行的操作  
          
        // 遍历当前节点的所有邻居  
        for (let neighbor of neighbors[node]) {  
            dfs(neighbor, visited);  
        }  
    }  
}  
  
// 示例图(以邻接列表形式表示)  
const neighbors = {  
    'A': ['B', 'C'],  
    'B': ['A', 'D', 'E'],  
    'C': ['A', 'F'],  
    'D': ['B'],  
    'E': ['B', 'F'],  
    'F': ['C', 'E'],  
};  
  
// 初始化访问状态  
const visited = {};  
for (let node in neighbors) {  
    visited[node] = false;  
}  
  
// 从节点'A'开始深度优先搜索  
dfs('A', visited);

139. 单词拆分

200. 岛屿数量

207. 课程表

贪心算法

55. 跳跃游戏