前端 2024 算法题库(未完待续)

709 阅读34分钟

leetcode hot 100 是必须要过一遍的,其次 codetop 勾选公司字节、按照频率排序,前 50 必刷的

刷题三条主线

  • top100 排查困难之外的题
  • codetop 中 字节+前端+高频题 前 50
  • codetop 标记的题

刷题需要背的东西

  • js 数组 字符串、Math 等基础 api
  • 二分查找、链表、二叉树、回溯、递归、动态规划等 通用公式

top 38 ,如果你面试没时间准备全部,那至少这38题 必须刷

  1. 2.两数相加
  2. 3. 无重复字符的最长子串
  3. 88. 合并两个有序数组
  4. 165. 比较版本号
  5. 53. 最大子数组和
  6. 112. 路径总和
  7. 20. 有效的括号
  8. 46. 全排列
  9. 415. 字符串相加
  10. 129. 求根到叶子节点数字
  11. 54. 螺旋矩阵
  12. 121. 买卖股票的最佳时机 122. 买卖股票的最佳时机 II
  13. 15. 三数之和
  14. 200. 岛屿数量
  15. 141. 环形链表
  16. 102. 二叉树的层序遍历
  17. 215. 数组中的第K个最大
  18. 5. 最长回文子串
  19. 206. 反转链表
  20. 209. 长度最小的子数组
  21. 695. 岛屿的最大面积
  22. 70. 爬楼梯
  23. 42.接雨水
  24. 4.寻找两个正序数组的中位数
  25. 14.最长公共前缀
    1. 交替合并字符串
  26. 27.移除元素
  27. 128.最长连续序列
  28. 28.找出字符串中第一个匹配项
  29. 11.盛最多水的容器
  30. 49.字母异位词分组
    1. 合并两个有序链表
  31. 704.二分查找
  32. 26.删除有序数组中的重复项
  33. 56.合并区间
  34. 18.四数之和
  35. 22.括号生成
  36. 560. 和为 K 的子数组

哈希

  • 对每个值进行 hash 存储
  • 记住 .split('') 是字符转数组 .join() 是数组转字符
/**
 * @param {string[]} strs
 * @return {string[][]}
 */
var groupAnagrams = function(strs) {
    if(strs.length===0){
        return [[""]]
    }
     if(strs.length===1){
        return [[strs[0]]]
    }
    let map = {}
    for(let i = 0;i<strs.length;i++){
        const key  = strs[i].split('').sort()
        if(map[key]){
            map[key] = [...map[key],strs[i]]
        }else{
            map[key] = [strs[i]]
        }
    }
    return Object.values(map)
};

1.1. 128. 最长连续序列

key存数字,value存什么?

  • 新存入的数字,如果它找到相邻的数,它希望从邻居数那里获取什么信息?
  • 很显然它希望,左邻居告诉它左边能提供的连续长度,右邻居告诉它右边能提供的连续长度
  • 加上它自己的长度,就有了自己处在的连续序列的长度
  • 同处一个连续序列的数字的value理应都相同,这是它们共同特征
  • 但没有必要每个的value都是序列长度,只需要两端的数存序列的长度就好
  • 因为靠的是两端和新数对接,序列是连续的,中间没有空位
  • 序列的一端找到邻居后,将另一端对应的value更新为最新的序列长度
var longestConsecutive = (nums) => {
  let map = new Map()
  let max = 0
  for (const num of nums) { // 遍历nums数组
    if (!map.has(num)) { // 重复的数字不考察,跳过
      let preLen = map.get(num - 1) || 0  // 获取左邻居所在序列的长度 
      let nextLen = map.get(num + 1) || 0 // 获取右邻居所在序列的长度 
      let curLen = preLen + 1 + nextLen   // 新序列的长度
      map.set(num, curLen) // 将自己存入 map
      max = Math.max(max, curLen) // 和 max 比较,试图刷新max
      map.set(num - preLen, curLen)  // 更新新序列的左端数字的value
      map.set(num + nextLen, curLen) // 更新新序列的右端数字的value
    }
  }
  return max
}

法二

只有当前节点是无左邻居 即是起点时才开始进行向右循环判断 查找

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
}

1.2. 560. 和为 K 的子数组

const subarraySum = (nums, k) => {
  const map = { 0: 1 };
  let prefixSum = 0;
  let count = 0;
  for (let i = 0; i < nums.length; i++) {
    prefixSum += nums[i];
    if (map[prefixSum - k]) {  // 这个前缀和已经有了
      count += map[prefixSum - k];
    }
    map[prefixSum] = map[prefixSum] ? map[prefixSum]+1 : 1;
  }
  return count;
};

1.3. 49. 字母异位词分组

var groupAnagrams = function(strs) {
    if(strs.length===0){
        return [[""]]
    }
     if(strs.length===1){
        return [[strs[0]]]
    }
    let map = {}
    for(let i = 0;i<strs.length;i++){
        const key  = strs[i].split('').sort()
        if(map[key]){
            map[key] = [...map[key],strs[i]]
        }else{
            map[key] = [strs[i]]
        }
    }
    return Object.values(map)
};

1. 滑动窗口

1.1. 3. 无重复字符的最长子串 🔥

var lengthOfLongestSubstring = function(s) {
    let strMap = {[s[0]]:0};
    let max= 1
    if(!s){
        return 0
    }
    if(s.length === 1){
        return 1
    }
    for(let i = 1;i<s.length;i++){
        if(strMap[s[i]] && strMap[s[i]]!== 0){
            i = strMap[s[i]];
            const length = Object.keys(strMap).length
            max = max<length?length:max
            strMap= {}
        }else if(!strMap[s[i]]){
           strMap[s[i]] = i;
           if(i === s.length-1){
            const length = Object.keys(strMap).length
             max = max<length?length:max
           }
        }
    }
    return max
};

2. 普通数组

2.1. 704. 二分查找

var search = function(nums, target) {
    if(nums.length === 1){
        return nums[0] ===target ? 0 :-1
    }
    let left = 0;
    let right= nums.length-1;
    while(left<=right){
        const sum = left +right;
       const index = sum %2 === 0 ? sum/2 :sum/2+0.5;
       if(nums[index] === target){
         return index;
       }
       if(nums[index] > target){
          right = index-1
       }
       if(nums[index] < target){
          left = index+1
       }
    }
    return -1
};

2.2. 27. 移除元素

var removeElement = (nums, val) => {
    let k = 0;
    for(let i = 0;i < nums.length;i++){
        if(nums[i] != val){
            nums[k] = nums[i];
            k++
        }
    }
    return k;
};

2.3. 977. 有序数组的平方

var sortedSquares = function(nums) {
    let n = nums.length;
    let res = new Array(n).fill(0);
    let i = 0, j = n - 1, k = n - 1;
    while (i <= j) {
        let left = nums[i] * nums[i],
            right = nums[j] * nums[j];
        if (left < right) {
            res[k--] = right;
            j--;
        } else {
            res[k--] = left;
            i++;
        }
    }
    return res;
};

2.4. 209. 长度最小的子数组🔥

var minSubArrayLen = function(target, nums) {
    let start, end
    start = end = 0
    let sum = 0
    let len = nums.length
    let ans = Infinity
    // 两层循环外层负责 后指针的移动,第二层负责前指针移动
    while(end < len){
        sum += nums[end];
        while (sum >= target) {  // 当滑动窗口符合条件时
            ans = Math.min(ans, end - start + 1); // 取最小 answer
            sum = sum - nums[start];   // 前指针向前移动
            start++;
        }
        end++; // 后指针向后移动
    }
    return ans === Infinity ? 0 : ans
};

2.5. 59. 螺旋矩阵 II 🔥

var generateMatrix = function(n) {
    let startX = startY = 0;   // 起始位置
    let loop = Math.floor(n/2);   // 旋转圈数
    let mid = Math.floor(n/2);    // 中间位置
    let offset = 1;    // 控制每一层填充元素个数
    let count = 1;     // 更新填充数字
    let res = new Array(n).fill(0).map(() => new Array(n).fill(0));

    while (loop--) {
        let row = startX, col = startY;
        // 上行从左到右(左闭右开)
        for (; col < n - offset; col++) {
            res[row][col] = count++;
        }
        // 右列从上到下(左闭右开)
        for (; row < n - offset; row++) {
            res[row][col] = count++;
        }
        // 下行从右到左(左闭右开)
        for (; col > startY; col--) {
            res[row][col] = count++;
        }
        // 左列做下到上(左闭右开)
        for (; row > startX; row--) {
            res[row][col] = count++;
        }

        // 更新起始位置
        startX++;
        startY++;

        // 更新offset
        offset += 1;
    }
    // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
    if (n % 2 === 1) {
        res[mid][mid] = count;
    }
    return res;
};

2.6. 41. 缺失的第一个正数

思路

  • 题目不让用额外的空间,时间复杂度又要是 O(n)
  • 我们可以利用 nums 数组这个空间本身,不影响 nums 数组的原信息的情况下,利用它存储一些额外的信息

缺失的最小正整数 的范围

  • 假设它为 n,n >= 1 是肯定的,还意味着 1 、2、 3、 …… 、n-1 肯定在数组里存在
  • 如果排好序的话,元素 1 ~ n-1 排在数组的前面,然后 n 缺失,比 n 大的元素在不在数组里都不影响 n 是缺失的最小正整数
  • 所以 nums 数组的长度最短可以是 n-1,nums.length >= n-1 ,即 n >= nums.length+1
  • 比方说,数组有 5 个元素,n 肯定是在 [1,6] 中。n 为 6 ,就是 1~5 正好占满了数组

交换元素 重排数组

  • 我们希望数组中尽量小的正整数放在前面,以便更快地找到目标 n
  • 1 出现在 位置 0 ,2 出现在位置 1…… 遍历时看哪个位置没有出现该出现的元素
  • 即,nums[i] 从 位置 i 交换到 位置 nums[i]-1 。[1,nums.length+1] 以外的数不用交换

题目把 nums 看作一个集合

  • 题意把 nums 数组当做一个存放元素的集合,找出没有出现在集合里的最小正整数
  • 对元素进行位置的交换,元素继续存在于集合中,没有改变原有的信息
  • 将部分的数安排到合适的位置,让 nums 数组承载一些额外信息,帮助解决问题
/**
 * @param {number[]} nums
 * @return {number}
 */
const firstMissingPositive = (nums) => {
  for (let i = 0; i < nums.length; i++) {
    while (
      nums[i] >= 1 &&              
      nums[i] <= nums.length &&     // 对1~nums.length范围内的元素进行安排
      nums[nums[i] - 1] !== nums[i] // 已经出现在理想位置的,就不用交换
    ) {
      const temp = nums[nums[i] - 1]; // 交换
      nums[nums[i] - 1] = nums[i];
      nums[i] = temp;
    }
  }
  // 现在期待的是 [1,2,3,...],如果遍历到不是放着该放的元素
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] != i + 1) { 
      return i + 1;        
    }
  }
  return nums.length + 1; // 发现元素 1~nums.length 占满了数组,一个没缺
};

2.7. 238. 除自身以外数组的乘积

思路:从左往右遍历,记录从左到当前位置前一位的乘积,然后从右往左遍历,从左到当前位置前一位的乘积乘上右边元素的积。

复杂度:时间复杂度O(n),空间复杂度O(1)

  • a\b\c\d
  • 1\a\ab\abc(前面数的乘积list1)
  • bcd\cd\d\1(后面数的乘积list2)
  • list1 与 list2 相乘
var productExceptSelf = function (nums) {
    const res = [];
    res[0] = 1;
  	//从左往右遍历
  	//记录从左到当前位置前一位的乘积
    for (let i = 1; i < nums.length; i++) {
        res[i] = res[i - 1] * nums[i - 1];
    }

    let right = 1;
  	//从右往左遍历
  	//从左到当前位置前一位的乘积 乘上 右边元素的积
    for (let j = nums.length - 1; j >= 0; j--) {
        res[j] *= right;
        right *= nums[j];
    }

    return res;
};

2.8. 合并区间

/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
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
};

3. 字符串

3.1. 344. 反转字符串

/**
 * @param {character[]} s
 * @return {void} Do not return anything, modify s in-place instead.
 */
var reverseString = function(s) {

    let left = 0 ;
    let right = s.length-1;
    while(left<=right){
        [s[left],s[right]] = [s[right],s[left]]
        left++
        right--
    }
    return s
};

3.2. 541. 反转字符串 II

/**
 * @param {string} s
 * @param {number} k
 * @return {string} 每计数至 2k 个字符  这个是循环,题目没读懂
 */
var reverseStr = function(s, k) {
 const len = s.length;
    let resArr = s.split(""); 
    for(let i = 0; i < len; i += 2 * k) {  // 每隔 2k 个字符的前 k 个字符进行反转
        let l = i - 1;
        r = i + k > len ? len : i + k;
        while(++l < --r) {
            [resArr[l], resArr[r]] = [resArr[r], resArr[l]];
        }
    }
    return resArr.join("");
};

3.3. 151. 反转字符串中的单词

/**
 * @param {string} s
 * @return {string}
 */
/**
 * @param {string} s
 * @return {string}
 */
 var reverseWords = function(s) {
   // 字符串转数组
   const strArr = Array.from(s);
   // 移除多余空格
   removeExtraSpaces(strArr);
   // 翻转
   reverse(strArr, 0, strArr.length - 1);

   let start = 0;

   for(let i = 0; i <= strArr.length; i++) {
     if (strArr[i] === ' ' || i === strArr.length) {
       // 翻转单词
       reverse(strArr, start, i - 1);
       start = i + 1;
     }
   }

   return strArr.join('');
};

// 删除多余空格
function removeExtraSpaces(strArr) {
  let slowIndex = 0;
  let fastIndex = 0;

  while(fastIndex < strArr.length) {
    // 移除开始位置和重复的空格
    if (strArr[fastIndex] === ' ' && (fastIndex === 0 || strArr[fastIndex - 1] === ' ')) {
      fastIndex++;
    } else {
      strArr[slowIndex++] = strArr[fastIndex++];
    }
  }

  // 移除末尾空格
  strArr.length = strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex;
}

// 翻转从 start 到 end 的字符
function reverse(strArr, start, end) {
  let left = start;
  let right = end;

  while(left < right) {
    // 交换
    [strArr[left], strArr[right]] = [strArr[right], strArr[left]];
    left++;
    right--;
  }
}

3.4. 右旋字符串

3.5. 28. 找出字符串中第一个匹配项的下标

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    const next = [];
    let j = 0;
    next.push(j);
    for (let i = 1; i < needle.length; i++) {
        while (j > 0 && needle[i] !== needle[j]){
            j = next[j - 1];
        }
        if (needle[i] === needle[j]){
            j++;
        }
        next.push(j);
    }

    console.log(next)
   let k = 0;
    for (let i = 0; i < haystack.length; ++i) {
        while (k > 0 && haystack[i] !== needle[k]){
            k = next[k - 1];
        }
        if (haystack[i] === needle[k]){
             k++;
        }
        if (k === needle.length){
            return (i - needle.length + 1);
        }
    }

    return -1;
};

3.6. 459. 重复的子字符串

var repeatedSubstringPattern = function (s) {
    if (s.length === 0)
        return false;

    const getNext = (s) => {
        let next = [];
        let j = 0;

        next.push(j);

        for (let i = 1; i < s.length; ++i) {
            while (j > 0 && s[i] !== s[j])
                j = next[j - 1];
            if (s[i] === s[j])
                j++;
            next.push(j);
        }

        return next;
    }

    let next = getNext(s);
    console.log(next)
    return next[next.length - 1] >0 && s.length % (s.length - next[next.length - 1]) === 0;

};

4. 哈希表

4.1. 242. 有效的字母异位词

var isAnagram = function(s, t) {
  if(s.length !== t.length) return false;
  let char_count = new Map();
  for(let item of s) {
    char_count.set(item, (char_count.get(item) || 0) + 1) ;
  }
  for(let item of t) {
    if(!char_count.get(item)) return false;
    char_count.set(item, char_count.get(item)-1);
  }
  return true;
};

4.2. 349. 两个数组的交集

var intersection = function (nums1, nums2) {
    if(nums1.length > nums2.length) {
        [nums1,nums2] = [nums2,nums1]
    }
  const set = new Set([])
  const res = [];
  nums1.forEach((num) => {
    if (!set.has(num)) {
      set.add(num)
    }
  });
  nums2.forEach((num) => {
 if (set.has(num)) {
      res.push(num)
    }
  });
  return [... new Set(res)];
};

4.3. 202. 快乐数

var isHappy = function(n) {
  const set = new Set([]);
  function fn(str){
      let sum = 0
      for(let i = 0;i<str.length;i++){
          sum = sum + Number(str[i])* Number(str[i])
      }
      if(set.has(sum)){
          return false
      }
      set.add(sum)
      if(sum === 1){
          return true
      }
      return fn(String(sum))
  }
  return fn(String(n)) || false
};

4.4. 1. 两数之和

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
  const numsMap = new Map();
  for(let i = 0;i<nums.length;i++){
      numsMap.set(nums[i],i)
  }
   for(let i = 0;i<nums.length;i++){
      const left = target - nums[i] ;
      if(numsMap.has(left) && numsMap.get(left) !== i){
          return [i,numsMap.get(left)]
      }
  }
};

4.5. 454. 四数相加 II

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
};

4.6. 383. 赎金信

    if(ransomNote.length>magazine.length){
        return false
    }
    let objMap = {}
    for(let i = 0;i<magazine.length;i++){
        objMap[magazine[i]] = objMap[magazine[i]] ? objMap[magazine[i]]+1:2;
    }
    for(let i = 0;i<ransomNote.length;i++){
       
        if(objMap[ransomNote[i]]){
            objMap[ransomNote[i]]--
        }
         if(objMap[ransomNote[i]] <0 || !objMap[ransomNote[i]]){
            return false
        }
    }
    return true
};

列表转成树形结构

[
  {
    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;
 }

4.7. 15. 三数之和 🔥

  • 先排序
  • 三指针法 初始第一个指针pre在 0 ,第二个指针 left 1 第三个指针right 在最后
    • 大于 0 right--
    • 小于 0 left++
    • left 和 right 走到一起的时候 pre++ left = pre+1 right 在最后
  • 重点是左右指针先判断是否重复才继续(只能在这里去重)
var threeSum = function(nums) {
    const res = [], len = nums.length
    // 将数组排序
    nums.sort((a, b) => a - b)
    for (let i = 0; i < len; i++) {  // i 是第一个数
        let l = i + 1, r = len - 1, iNum = nums[i]
        // 数组排过序,如果第一个数大于0直接返回res
        if (iNum > 0) return res   
        // 去重
        if (iNum !== nums[i - 1]) {  // 只有不相等才进入逻辑
            while(l < r) {
                const lNum = nums[l], rNum = nums[r]
                 const threeSum = iNum + lNum + rNum
                // 三数之和小于0,则左指针向右移动
                if (threeSum < 0) l++ 
                else if (threeSum > 0) r--
                else {
                    res.push([iNum, lNum, rNum])
                    // 去重
                    while(l < r && nums[l] == nums[l + 1]){
                        l++
                    }
                    while(l < r && nums[r] == nums[r - 1]) {
                        r--
                    }
                    l++
                    r--
                }
     
            }
           }
    }
    return res
};

18. 四数之和

  • 数组排序
  • 4 个指针,固定前 2 个指针 移动后面 2 个指针
    • 根据大小值判断活跃指针是否右移动
    • 核心还是去重
var fourSum = function(nums, target) {
    const len = nums.length;
    if(len < 4) return [];
    nums.sort((a, b) => a - b);
    const res = [];
    for(let i = 0; i < len - 3; i++) {
        // 去重i
        if(i > 0 && nums[i] === nums[i - 1]) continue;
        for(let j = i + 1; j < len - 2; j++) {
            // 去重j
            if(j > i + 1 && nums[j] === nums[j - 1]) continue;
            let l = j + 1, r = len - 1;
            while(l < r) {
                const sum = nums[i] + nums[j] + nums[l] + nums[r];
                if(sum < target) { l++; continue}
                if(sum > target) { r--; continue}
                res.push([nums[i], nums[j], nums[l], nums[r]]);
		
		// 对nums[left]和nums[right]去重
                while(l < r && nums[l] === nums[++l]);
                while(l < r && nums[r] === nums[--r]);
            }
        } 
    }
    return res;
};

4.8. 454. 四数相加 II

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

  1. 列举出nums1和nums2的所有组合放入mapGroup1中。
  2. 将nums3和nums4进行组合,统计nums3和nums4的和与mapGroup1相加结果为0的个数
  3. 实际复杂度 n 方
/**
 * @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
};

5. 双指针

双指针大部分情况就是 维护一个读指针(快指针) 一个写指针(慢指针)

  • 双指针的核心思想是找哪个是快指针

5.1. 27. 移除元素

  • 反方向思考 只有不相同的元素进行重新保存 并使用 k 保留顺序
var removeElement = (nums, val) => {
    let k = 0;
    for(let i = 0;i < nums.length;i++){
        if(nums[i] != val){
            nums[k] = nums[i];
            k++;
        }
    }
    return k;
};

5.2. 206. 反转链表🔥

  • cur 是当前节点,最后处理的时候将其改成下一个节点 方便下次使用
  • pre 是当前节点的下一个几点
  • temp 是用于转换的临时变量
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
// 双指针:
 var reverseList = function(head) {
    if(!head || !head.next) return head;

    let temp = null, pre = null, cur = head;
    // 1、 暂存 当前的 next 到最后复制给 cur
    // 2、 将当前的 next 赋值pre (正向的上一个)
    // 3、  pre 给就是当前的 cur
    // 4、  cur 重新赋值为 之前暂存的 cur.next
    while(cur) {
        temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    // temp = cur = null;
    return pre;
};

// 递归:
var reverse = function(pre, head) {
    if(!head) return pre;
    const temp = head.next;
    head.next = pre;
    pre = head
    return reverse(pre, temp);
}

var reverseList = function(head) {
    return reverse(null, head);
};

// 递归2
var reverse = function(head) {
    if(!head || !head.next) return head;
    // 从后往前翻
    const pre = reverse(head.next);
    head.next = pre.next;
    pre.next = head;
    return head;
}

var reverseList = function(head) {
    let cur = head;
    while(cur && cur.next) {
        cur = cur.next;
    }
    reverse(head);
    return cur;
};

5.3. 11. 盛最多水的容器

  • 左右哪个短,哪个先移动
var maxArea = function(height) {
    let l = 0,r = height.length-1;
    let max = 0;
    while(l<r){
        const result = (r-l)*Math.min(height[l],height[r])
        max = Math.max(max,result);
        if(height[l] > height[r]){
            r--
        }else{
            l++
        }
    }
    return max
};

5.4. 283. 移动零

  • l和 r 之间都是 0 要做的就是将 l-r 向右移动,
  • 遇0 则 r+1 非 0 则 r+1 l 与该值互换 并+1
var moveZeroes = function(nums) {
   let l = 0 ,r=0;
   while(r<=nums.length-1){
     if(nums[r]===0){
        if(nums[l] === 0){
            r++
        }
     }else{
         [nums[r],nums[l]] = [nums[l],nums[r]];
            l++;
            r++;
     }
   }
   return nums
};

5.5. 42. 接雨水🔥

// 双指针解法
// 注意到动态规划中
// 对于位置 i 的接水量取决于 leftMax 和 rightMax 中的较小者
// 所以我们不必真的知道较大者是谁
// 只要知道较小者是谁就可以了 

// 初始化 left = 0, right = n-1, leftMax = 0, rightMax = 0
// 注意到对于位置 left 来说, leftMax 是真正意义上的左侧最大值, 而 rightMax 不是真的右侧最大值
// 而对于位置 right 来说, rightMax 是真正意义上的右侧最大值, 而 leftMax 不是真的左侧最大值

// 从左往右扫描
// 1. 使用 height[left]height[right] 更新 leftMax, rightMax
// 2. 若 height[left] < height[right], 则说明对于位置 left 来说, 
//     leftMax 一定小于其右侧真正意义上的最大值
//     因为连当前右侧的局部最大值 rightMax 都比不过, 更比不过右侧真正意义上的最大值
//     而我们又不需要真的知道右侧真正意义上的最大值
// 3. 类似地处理 height[left] >= height[right] 的情况
var trap = function (height) {
    let ans = 0,
     left = 0,
     right = height.length - 1, 
     leftMax = 0, 
     rightMax = 0;
    while (left < right) {
        leftMax = Math.max(leftMax, height[left]);
        rightMax = Math.max(rightMax, height[right]);
        if (leftMax < rightMax) {
            ans = ans +  leftMax - height[left];
            left++
        } else {
            ans = ans + rightMax - height[right];
            right--
        }
    }
    return ans;
};

单调栈解法

相当于「竖着」计算面积,单调栈的做法相当于「横着」计算面积。

这个方法可以总结成 16个字:找上一个更大元素,在找的过程中填坑。

注意 while 中加了等号,这可以让栈中没有重复元素,从而在有很多重复元素的情况下,使用更少的空间。

var trap = function(height) {
    let ans = 0;
    const st = [];
    for (let i = 0; i < height.length; i++) {
        while (st.length && height[i] >= height[st[st.length - 1]]) {
            const bottomH = height[st.pop()];
            if (st.length === 0) {
                break;
            }
            const left = st[st.length - 1];
            const dh = Math.min(height[left], height[i]) - bottomH; // 面积的高
            ans += dh * (i - left - 1);
        }
        st.push(i);
    }
    return ans;
};

6. 动态规划

  • 常见于求某种条件的极限值

通用动态规划公式:

对于一个给定问题 P,其状态可以用一个变量(或一组变量)S 表示。动态规划的核心在于构建一个表格(或数据结构)dp,其中 dp[S] 存储与状态 S 相关的最优解或所需信息。动态规划的计算过程通常遵循以下步骤:

  1. 定义状态:明确描述问题的状态空间,确定状态变量 S。一般是数组 dp,需要考虑初始值的问题,需要考虑 dp[0]的取值问题
  2. 状态转移方程: 每次循环时根据 dp 执行状态转移公式
    • 根据问题的性质,建立状态之间的关系,即状态转移方程 dp[S] = f(dp[S1], dp[S2], ..., S)。
    • f 是一个函数,它根据前一阶段或前几个阶段的状态(如 S1, S2, ...) 和当前状态 S 的相关信息,计算出当前状态 S 的最优解或所需信息。
  1. 初始化:为 dp 表格中的基础状态赋值,这些通常是问题的边界条件或最小粒度的子问题解。
  2. 填充表格:按照一定的顺序(如自底向上、自顶向下等),依据状态转移方程递归地或迭代地计算并填充 dp 表格。
  3. 解答:从 dp 表格中提取最终答案,通常是某个特定状态 S_final 对应的 dp[S_final] 值。

以上步骤适用于各种动态规划问题,尽管具体的 S、f、初始状态和填充顺序会因问题而异。在实际编程中,dp 可能是一维数组、二维数组,甚至是更复杂的结构,取决于问题的维度和状态表示方式。上述示例中的斐波那契数列、最大连续子序列和以及最长公共子序列问题均遵循这一通用框架。

6.1. 118. 杨辉三角

var generate = function(numRows) {

    let list = [[1]]
    if(numRows.length ===1){
        return list
    }
    for(let i= 1 ;i<numRows;i++){
        let k  =i;
        let arr = []
        for(let j = 0;j<=k;j++){
            const left = list[i-1][j-1] || 0
            const right = list[i-1][j] || 0
            arr[j] = left + right
        }
        list.push(arr)
    }
    return list
};

6.2. 152. 乘积最大子数组

var maxProduct = function(nums) {
  let max = nums[0]
  let imax = 1
  let imin = 1
  for(let num of nums) {
    if(num < 0) {
      [imax, imin] = [imin, imax]
    }
    imax = Math.max(num, num * imax)
    imin = Math.min(num, num * imin)
    max = Math.max(imax, max)
  }
  return max
};

/**
  如: nums = [2,3,-2,4]
  循环

  i
  0    num = 2; imax = 2, imin = 1, max = 2
  1    num = 3  imax = 6, imin = 1, max = 6
  2    num = -2 < 0, 交换 => imax = 1, imin = 6 => imax = -2, imin = -12, max = 6
  3    num = 4  imax = 4, imin = -48, max = 6
  
*/

6.3. 70. 爬楼梯🔥

var climbStairs = function(n) {
    const dp = [];
    dp[0] = 1;
    dp[1] = 1;
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};

6.4. 416. 分割等和子集

/**
 * @param {number[]} nums
 * @return {boolean}
 */
 // 背包容量为 sum/2
 // 求是否存在一组数满足 和为sum/2
var canPartition = function(nums) {
    const sum  = nums.reduce((prev, curr)=> prev + curr);

    if(sum%2 !== 0){
      return false
    }
    const target = sum/2;
    const dp = new Array(target + 1).fill(false);
    dp[0] = true
    for(let num of  nums) {
      for(let i = target; i >=num; i--) {
          dp[i] = dp[i] || dp[i - num];
      }
    }

    return dp[target]
};

6.5. 300. 最长递增子序列

类似题目套路

  • dp[i]含义:从0到下标为i的序列的最长子序列长度
  • 根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
  • 假设我们已经知道了 dp[0..4] 的所有结果,我们如何通过这些已知结果推出 dp[5] 呢?
  • nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
// 洗牌策略  第一个默认第一堆  后面每一个和前面每堆最后一个 比较  更小则放入  否则新建一个堆
// 每一堆最后一个数相连就是 最长递增子序列 堆的个数就是答案
var lengthOfLIS = function(nums) {
    let n = nums.length;
    if(n == 0){
        return 0;
    }
    let list =[[nums[0]]]
    for(let i = 1;i< nums.length;i++){
        let isNewNum = true
        for(let j = 0;j<list.length;j++){
            if(nums[i]<=list[j][list[j].length-1]){
                list[j].push(nums[i]);
                isNewNum = false
                break;
            }
        }
        if(isNewNum){
            list.push([nums[i]])
        }

    }
    return list.length

};

// 4 4 3
// 10 8 
// 9

6.6. 32. 最长有效括号

1,括号抵消。有效的括号组合必然能左右相消。(即右括号进来时,栈顶为左括号的话就pop)。
2,记录谁被消除了。(引入一个对象,它一方面需要记录是什么括号。另一方面需要记录它在数组的位置)。
3,得出最终栈后,相邻对象间可能被消除了n对括号。(通过位置差值得出)。
4,特殊情况。开头符号或末尾符号都被pop了。未记录栈中。于是 s = ')' + s + ‘(’;
var longestValidParentheses = function(s) {
    let max = 0
    if (s.length < 1) return max
    let len = s.length
    // 栈顶之所有加入一个-1,纯粹是为了方便计算有效括号的长度
    // 不然就需要手动调整为i-j+1;同时而确保第一个字符为")"时不需要特殊处理
    let stack = [-1]
    for(let i = 0; i < len; i++) {
        let value = s[i]
        if (value === '(') {
          stack.push(i)
        } 
        if (value === ')') {
          stack.pop()
          // 栈顶加入一个pivot字符")",实际上是方便计算有效括号串长度
            if (stack.length < 1) {
                stack.push(i)
            } else {
                max = Math.max(max, i - stack[stack.length - 1])
            }
          }
      }
    return max
};

6.7. 198. 打家劫舍

  • 状态转移公式
    • Math.max(dp[i-1],(dp[i-2] || 0)+nums[i])
var rob = function(nums) {
    let dp = [nums[0]];
    for(let i = 1;i<nums.length;i++){
        dp[i] = Math.max(dp[i-1],(dp[i-2] || 0)+nums[i])
    } 
    return Math.max(...dp)
};

6.8. 279. 完全平方数

  • 默认值思考】dp 默认放最大值
  • Math.min(dp[i], (dp[i - j * j]) + 1);

对数组进行遍历,下标为 i,每次都将当前数字先更新为最大的结果,即 dp[i]=i,比如 i=4,最坏结果为 4=1+1+1+1 即为 4 个数字

动态转移方程为:dp[i] = MIN(dp[i], dp[i - j * j] + 1),i 表示当前数字,j*j 表示平方数

时间复杂度:O(n∗sqrt(n))O(n*sqrt(n))O(n∗sqrt(n)),sqrt 为平方根

var numSquares = function(n) {
    const dp = [0]; // 数组长度为n+1,值均为0
    for(let i= 1;i<=n;i++){
        let j = 1;
        let square = 1
        dp[i] = i
        while(i>= square){
            dp[i] = Math.min(dp[i], (dp[i - j * j]) + 1);
            j++;
            square = j*j
        };
    }
    return dp[n];
};

6.9. 322. 零钱兑换

  • dp 初始值怎么设置
  • 第二次循环怎么设计
  • dp 公式 dp[i] = Math.min(dp[i],dp[i-coins[j]]+1 )
var coinChange = function(coins, amount) {
   let dp = new Array( amount + 1 ).fill( Infinity );
   dp[0] = 0
   for(let i = 1;i<=amount;i++){
      let j = 0;
      while(j<coins.length){
        if(i-coins[j]>=0){
            dp[i] = Math.min(dp[i],dp[i-coins[j]]+1 )  
        }
        j++
      }
   }
    return dp[amount] === Infinity?-1 :dp[amount]
};

6.10. 139. 单词拆分

  • 第二个循环从小从 0 到 i 开始计算
const wordBreak = (s, wordDict) => {
  const wordSet = new Set(wordDict);
  const len = s.length;
  const dp = new Array(len + 1).fill(false);
  dp[0] = true;
  for (let i = 1; i <= len; i++) {
    for (let j = i - 1; j >= 0; j--) {    // j去划分成两部分
      const suffix = s.slice(j, i);       // 后缀部分 s[j: i-1]
      if (wordSet.has(suffix) && dp[j]) { // 后缀部分是单词,且左侧子串[0,j-1]的dp[j]为真
        dp[i] = true;
        break;  // dp[i] = true了,i长度的子串已经可以拆成单词了,不需要j继续划分子串了
      }
    }
  }
  return dp[len];
};

7. 多维动态规划

7.1. 62. 不同路径

回溯算法(超时)

var uniquePaths = function(m, n) {
    let count = 0;
    function dfs(x,y){
        if(y === m && x === n){
            count++
            return;
        }
        if(y+1<=m){
            dfs(x,y+1)
        }
        if(x+1<=n){
            dfs(x+1,y)
        }
    }
    dfs(1,1)
    return count
}

7.2. 5. 最长回文子串

var longestPalindrome = function(s) {
    if(s.length === 1){
        return s
    }
   let maxLength = 0;
   let maxStr = ''
   for(let i = 0;i<s.length;i++){
        if(i+1<s.length){
            fn(i,i+1);

        }
        if(i+2<s.length){
            fn(i,i+2);
        }
   }
   function fn (l,r){
        while(l>=0 && r<s.length && s[l] === s[r]){
             if(maxLength<r-l){
                console.log(l,r)
                maxLength = r-l;
                maxStr = s.slice(l,r+1)
            }
            l--;
            r++;
        }
   }
   return maxStr || s[0]
};

8. 贪心算法

说实话贪心算法并没有固定的套路

所以唯一的难点就是如何通过局部最优,推出整体最优。

那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?

不好意思,也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

有同学问了如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

如何判断要不要用贪心算法

  • 手动模拟部分值
  • 举反例

8.1. 贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

8.2. 121. 买卖股票的最佳时机 🔥

var maxProfit = function(prices) {
    let ans = 0;
    let minPrice = prices[0];
    for (const p of prices) {
        ans = Math.max(ans, p - minPrice);
        minPrice = Math.min(minPrice, p);
    }
    return ans;
};

8.3. 455. 分发饼干

  • 2 个数组都排序
  • 双指针循环匹配
var findContentChildren = function(g, s) {
  let count = 0;
  g.sort((a,b)=>a-b)
  s.sort((a,b)=>a-b)
  let  i =0,j = 0;
  while(i<g.length && j< s.length){
    console.log(g[i], s[j])
        if(g[i] > s[j]){
            j++;
        }else{
            count++
            i++
            j++
        }
  }
    return count
};

8.4. 376. 摆动序列

var wiggleMaxLength = function(nums) {
    if(nums.length <= 1) return nums.length
    let result = 1
    let preDiff = 0
    let curDiff = 0
    for(let i = 0; i < nums.length - 1; i++) {
        curDiff = nums[i + 1] - nums[i]
        if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
            result++
            preDiff = curDiff
        }
    }
    return result
};

8.5. 53. 最大子数组和🔥

  • 当前指针之前的和为 小于等于 则丢弃当前元素之前的数
var maxSubArray = function (nums) {
    let sum = 0;  // 临时求和
    let ans = nums[0];  // 真是答案
    for (let num of nums) {
        if (sum > 0) {
            sum = sum + num
        } else {
            sum = num
        }
        ans = Math.max(ans, sum)
    }
    return ans
};

8.6. 122. 买卖股票的最佳时机 II

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

8.7. 134. 加油站

var canCompleteCircuit = function(gas, cost) {
    let curSum = 0
    let min = Infinity
    for(let i = 0; i < gas.length; i++) {
        let rest = gas[i] - cost[i]
        curSum += rest
        if(curSum < min) {
            min = curSum
        }
    }
    if(curSum < 0) return -1   //1.总油量 小于 总消耗量
    if(min >= 0) return 0      //2. 说明油箱里油没断过
                               //3. 从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点
    for(let i = gas.length -1; i >= 0; i--) {
        let rest = gas[i] - cost[i]
        min += rest
        if(min >= 0) {
            return i
        }
    }
    return -1
}

8.8. 1005. K 次取反后最大化的数组和

var largestSumAfterKNegations = function(nums, k) {
    nums.sort((a, b) => Math.abs(b) - Math.abs(a)); // 排序
    let sum = 0;
    for(let i = 0; i < nums.length; i++) {
        if(nums[i] < 0 && k-- > 0) { // 负数取反(k 数量足够时)
            nums[i] = -nums[i];
        }
        sum += nums[i]; // 求和
    }
    if(k % 2 > 0) { // k 有多余的(k若消耗完则应为 -1)
        sum -= 2 * nums[nums.length - 1]; // 减去两倍的最小值(因为之前加过一次)
    }
    return sum;
};

8.9. 135. 分发糖果

8.10. 860. 柠檬水找零

var lemonadeChange = function(bills) {
    let fiveCount = 0
    let tenCount = 0

    for(let i = 0; i < bills.length; i++) {
        let bill = bills[i]
        if(bill === 5) {
            fiveCount += 1
        } else if (bill === 10) {
            if(fiveCount > 0) {
                fiveCount -=1
                tenCount += 1
            } else {
                return false
            }
        } else {
            if(tenCount > 0 && fiveCount > 0) {
                tenCount -= 1
                fiveCount -= 1 
            } else if(fiveCount >= 3) {
                fiveCount -= 3
            } else {
                return false
            }
        } 
    }
    return true
};

8.11. 406. 根据身高重建队列

  • 遇到两个维度同时考虑的时候 一定要先考虑一个维度
  • 当第一维度相同时 可以开始再按照第二维度进行排序
  • 不知道哪个维度 可以分别尝试
var reconstructQueue = function(people) {
    let queue = []
    people.sort((a, b ) => {
        if(b[0] !== a[0]) {
            return b[0] - a[0]
        } else {
            return a[1] - b[1]
        }
        
    })

    for(let i = 0; i < people.length; i++) {
        queue.splice(people[i][1], 0, people[i])
    }
    return queue
};

8.12. 452. 用最少数量的箭引爆气球

  • 完全没有理解
var findMinArrowShots = function(points) {
    points.sort((a, b) => {
        return a[0] - b[0]
    })
    let result = 1
    for(let i = 1; i < points.length; i++) {
        if(points[i][0] > points[i - 1][1]) {
            result++
        } else {
            points[i][1] = Math.min(points[i - 1][1], points[i][1])
        }
    }

    return result
};

9. 回溯算法

回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”并尝试另一个可能的候选解。

以下是一个简单的回溯算法框架,使用JavaScript编写,可以作为前端实现回溯算法的通用模板:虽然实际编程中不存在严格的数学公式来表示回溯算法,但在编写前端JavaScript或其他语言实现回溯算法时,我们可以抽象出一个通用的逻辑框架或者说模板。以下是一个简化的前端JavaScript风格的回溯算法模板:

什么时候用回溯

  • 计算所有可能的结果时
// 定义一个全局结果容器用于存储所有有效解
let result = [];

// 回溯函数,其中:
// - path 是当前正在构建的解的路径
// - choices 是当前剩余可供选择的元素集合
function backtrack(path = [], choices = [...inputArray]) {
  // 终止条件:如果找到一个解或者满足特定条件时停止递归
  if (/* 达到了结束条件 */) {
    // 将当前路径添加到结果集中
    result.push([...path]);
    return;
  }

  // 遍历当前层的所有可能性
  for (let i = 0; i < choices.length; i++) {
    // 做出一个选择,并将其从剩余的选择中移除(如果是不可重复选择的情况)
    let choice = choices[i];
    let newChoices = [...choices.slice(0, i), ...choices.slice(i + 1)];

    // 将当前选择加入到路径中
    path.push(choice);

    // 递归进入下一层决策树
    backtrack(path, newChoices);

    // 回溯:撤销当前选择
    path.pop();
  }
}

// 初始化并调用回溯函数
const inputArray = [/* 待处理数据 */];
backtrack();

// 输出最终结果
console.log(result);

这个模板涵盖了回溯算法的核心逻辑,包括递归调用、选择、递归前进和回溯。具体应用时,你需要根据实际问题调整终止条件部分,以及如何做出选择和是否允许重复选择等细节。例如,在解决子集生成问题时,“终止条件”可能是所有元素都已经被遍历过,而在解决八皇后问题时,则是当所有皇后都被合法放置时。

  • 1、首次输入什么
  • 2、什么时候第一次 push
  • 3、什么时候开始 return
  • 4、循环中怎么赋值新值

9.1. 22. 括号生成🔥

 // 从左到右的思维  第一个肯定是左括号 最后一个肯定是右括号
 var generateParenthesis = function (n) {
    var list = []
    function dfs(s = '',left,right){
        if(!left && !right){
            list.push(s);
            return;
        }
        if(left>0){
            dfs(`${s}(`,left-1,right)

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

9.2. 78. 子集

  • 根据子集是否去重判断什么时候 push
var subsets = function(nums) {
   let list = []
   function dfs(velueArr,arr){
        list.push(velueArr);
        if(velueArr.length === nums.length){
            return
        }
        for(let i = 0;i<arr.length;i++){
            dfs([...velueArr,arr[i]],arr.slice(i+1,arr.length))
        }
   }
   dfs([],nums)
   return list
};

9.3. 46. 全排列🔥

  • 1、不含重复数字的数组
  • 2、每次都是一个 for 循环 但是只取 当前 arr 没有的值
/**
 * @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
};

9.4. 131. 分割回文串

  • 能手写一个函数实现判断指定区间是否符合回文
const isPalindrome = (s, l, r) => {
    for (let i = l, j = r; i < j; i++, j--) {
        if(s[i] !== s[j]) return false;
    }
    return true;
}

var partition = function(s) {
    const res = [], len = s.length;

    function backtracking(startIndex,arr=[]) {
        if(arr.join('').length >= len) {//  当前收集的数组已饱和
            res.push(arr);
            return;
        }
        for(let i = startIndex; i < len; i++) {
            if(isPalindrome(s, startIndex, i)){ //  如果当前区间是数组
                // 携带当前的结果 并寻找剩下的数据 进行排查
                backtracking(i + 1,[...arr,s.slice(startIndex, i + 1)]);
            }
        }
    }
    backtracking(0);
    return res;
};

9.5. 39. 组合总和

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
   let list = []
   candidates.filter(v=>v<=target)
   const sum=(array) => array.reduce((a,b)=>a+b,0)
    function dfs(start,arr){
        const value = sum(arr)
        if(value === target){
            list.push(arr);
            return;
        }
        if(value>target){
            return
        }
        for(let i =start;i<candidates.length;i++){
            dfs(i,[...arr,candidates[i]])
        }
    }
   dfs(0,[]);

   return list
};

10. 二分查找

10.1. 4. 寻找两个正序数组的中位数

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
var findMedianSortedArrays = function(nums1, nums2) {
    // 保证num1是比较短的数组
    if (nums1.length > nums2.length) {
        [nums1, nums2] = [nums2, nums1];
    }
    const length1 = nums1.length;
    const length2 = nums2.length;
    let min = 0;
    let max = length1;
    let half = Math.floor((length1 + length2 + 1) / 2);
    while (max >= min) {
        const i = Math.floor((max + min) / 2);
        const j = half - i;
        if (i > min && nums1[i - 1] > nums2[j]) {
            max = i - 1;
        } else if (i < max && nums1[i] < nums2[j - 1]) {
            min = i + 1;
        } else {
            let left,right;
            if (i === 0) left = nums2[j - 1];
            else if (j === 0) left = nums1[i - 1];
            else left = Math.max(nums1[i - 1], nums2[j - 1]);
            
            if (i === length1) right = nums2[j];
            else if (j === length2) right = nums1[i];
            else right = Math.min(nums1[i], nums2[j]);
            
            return (length1 + length2) % 2 ? left : (left + right) / 2;
        }
    }
    return 0;
};

11. 技巧题

根据题目的特定限制 找技巧

11.1. 136. 只出现一次的数字

/**
 * @param {number[]} nums
 * @return {number}
 出现 2 次的数字在异或中都抵消了,最后得出只出现 1 次的数

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

11.2. 169. 多数元素

/**
 * @param {number[]} nums
 * @return {number}
 设置nums[0] 为 x   当前值等于 x   m+1  否则 m-1  当 m==0 则说明当前值 
 相同的加1, 不相同的减1, 因为是大于一半, 所以最后肯定剩下大于一半的那个
 m再次等于 0 所以一定是遇到了
 */
var majorityElement = function(nums) {
  let x =  0
  let m = 0
  for(let n of nums){
    x = m === 0 ? n : x
    m = m + (x === n ? 1 : -1)
    console.log(x,m)
  }
  return x
};

11.3. 两个数组中完全独立的数据

就是找到仅在两个数组中出现过一次的数据

var a = [1, 2, 4], b = [1, 3, 8, 4]
const newArr = a.concat(b).filter((item, _, arr) => {
  return arr.indexOf(item) === arr.lastIndexOf(item)
})

11.4. 31. 下一个排列🔥

1、从后向前 找一个小大的场景

2、将小和其后面所有数 差值最小的数互换

3、除了小那个位置后面的数据按照从小到大重新排序

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var nextPermutation = function(nums) {
  let i = nums.length - 2;
  // 从后向前 找一个小大的场景 这个值 index 是 i
  while (i >= 0 && nums[i] >= nums[i+1]){
    i--
  }
  const minNum = nums[i]
  if (i >= 0){
    let j = nums.length - 1;
    // 将小和其后面所有数 差值最小的 那个数   互换
    while (j >= i && nums[j] <= minNum){
      j--
    }
    // 交换两个数
    [nums[j], nums[i]] = [nums[i], nums[j]]
  }
  // 除了小那个位置后面的数据按照从小到大重新排序
  let l = i + 1;
  let r = nums.length - 1;
  while (l < r){
    [nums[l], nums[r]] = [nums[r], nums[l]]
    l++
    r--
  }
}

11.5. 75. 颜色分类

  • 一种颜色丢前面 一种颜色丢后面 遍历完就是排序完
var sortColors = function(nums) {
    for(let i=0,len=nums.length; i<len; i++) {
        nums[i]===0 && (nums.splice(i,1), nums.unshift(0))
        nums[i]===2 && (nums.push(2), nums.splice(i,1),len--,i--)
    }
    return nums
};

12. 栈

12.1. 155. 最小栈🔥

  • 使用一个min_stack 存储最小栈,当发现当前入栈数比 最小栈栈顶还小则也入最小栈
  • 当弹出时判断是否是最小栈栈顶 是的话也弹出
var MinStack = function() {
    this.x_stack = [];
    this.min_stack = [Infinity];
};

MinStack.prototype.push = function(x) {
    this.x_stack.push(x);
    this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], x));
};

MinStack.prototype.pop = function() {
    this.x_stack.pop();
    this.min_stack.pop();
};

MinStack.prototype.top = function() {
    return this.x_stack[this.x_stack.length - 1];
};

MinStack.prototype.getMin = function() {
    return this.min_stack[this.min_stack.length - 1];
};

12.2. 84. 柱状图中最大的矩形

var largestRectangleArea = function(heights) {
    const n = heights.length;
    const left = Array(n).fill(-1);
    const st = [];
    for (let i = 0; i < n; i++) {
        const x = heights[i];
        while (st.length && x <= heights[st[st.length - 1]]) {
            st.pop();
        }
        if (st.length) {
            left[i] = st[st.length - 1];
        }
        st.push(i);
    }

    const right = Array(n).fill(n);
    st.length = 0;
    for (let i = n - 1; i >= 0; i--) {
        const x = heights[i];
        while (st.length && x <= heights[st[st.length - 1]]) {
            st.pop();
        }
        if (st.length) {
            right[i] = st[st.length - 1];
        }
        st.push(i);
    }

    let ans = 0;
    for (let i = 0; i < n; i++) {
        ans = Math.max(ans, heights[i] * (right[i] - left[i] - 1));
    }
    return ans;
};

12.3. 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;
};

13. 链表

13.1. 2. 两数相加

  • 核心代码
  • 每一步都需要 new ListNode()
let result = new ListNode();
let current = result;
return result;
var addTwoNumbers = function (l1, l2) {
    let result = new ListNode();
    let l3 = result;
    let pre = 0;
    let val = 0;
    while (l1 || l2) {
        const l1val = !l1 ? 0 : l1.val;
        const l2val = !l2 ? 0 : l2.val;
        val = l1val + l2val + (pre || 0);
        pre = val > 9 ? 1 : null;
        val = val > 9 ? val - 10 : val;
        l3.val = val
        l1 = l1 ? l1.next : null;
        l2 = l2 ? l2.next : null;
        if (l1 || l2 || pre) {
            l3.next = new ListNode(pre);
            l3 = l3.next
        }
    }
    return result
};

14. 堆

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

/*
 * @lc app=leetcode.cn id=215 lang=javascript
 *
 * [215] 数组中的第K个最大元素
 */
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
  let low = 0
  let high = nums.length - 1
  while (low <= high) {
    const mid = partition(nums, low, high)
    if (mid === k - 1) return nums[mid]
    mid < k - 1 ? low = mid + 1 : high = mid - 1
  }

}

function partition(arr, low, high) {
  let mid = Math.floor(low + (high - low) / 2)
  const pivot = arr[mid]; // 这里记得添加分号 
  // 把pivot放在arr的最后面
  [arr[mid], arr[high]] = [arr[high], arr[mid]]
  let i = low
  // 把pivot排除在外,不对pivot进行排序
  let j = high - 1
  while (i <= j) {
    while (arr[i] > pivot) i++
    while (arr[j] < pivot) j--
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]]
      i++; j--;
    }
  }
  // 因为arr[i]是属于left的,pivot也是属于left的
  // 故我们可以把原本保护起来的pivot和现在数组的中间值交换
  [arr[high], arr[i]] = [arr[i], arr[high]]
  return i
}

15. 二叉树

15.1. 102. 二叉树的层序遍历

  • 第一场循环遍历树 这一层搜集每一层的数据
  • 循环遍历节点 手机该层的数据
var levelOrder = function(root) {
    if (root === null) return [];
    let ans = [];
    let cur = [root];
    while (cur.length) {
        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;
};

16. 图论

16.1. 200. 岛屿数量

var numIslands = function (grid) {
	let res = 0;

	function dfs(i, j) {
        console.log(grid)
		grid[i][j] = '0'; // 贪心 先标记为 0 统计一个打沉一个  因为不会影响到下一个计算
		if (grid[i + 1] && grid[i + 1][j] === '1') dfs(i + 1, j);
		if (grid[i - 1] && grid[i - 1][j] === '1') dfs(i - 1, j);
		if (grid[i][j + 1] && grid[i][j + 1] === '1') dfs(i, j + 1);
		if (grid[i][j - 1] && grid[i][j - 1] === '1') dfs(i, j - 1);
	}

	for (let i = 0; i < grid.length; i++) {
		for (let j = 0; j < grid[0].length; j++) {
			if (grid[i][j] === '1') {  // 如果当前是陆地就展开搜索
				dfs(i, j);
				res++;
			}
		}
	}
	return res;
};

17. 矩阵

17.1. 54. 螺旋矩阵

在画图分析后,判断出路线都是有固定方向的 先→再↓再←再↑再→.....一直循环到没数字

因此定义4个方向边界 当触及边界时即按固定方向转向 且其对应的边界值向内收缩1

若没触及边界 即按自身方向继续行走 改变坐标值直到触边界/数字全部遍历过

  1. 在其方向为右 且未触碰边界值时 列向右走(j++)
  2. 当触碰时转向
  • for 循环用来收集数据的
  • 循环中主要来调整 i、j 的指针
/**
 * @param {number[][]} matrix
 * @return {number[]}
 */
var spiralOrder = function (matrix) {
    let count = 0;
    let mLength = matrix.length
    let nLength = matrix[0].length
    const num = mLength * nLength
    let m = 0, n = 0;
    let arr = []
    while (arr.length < num) {
        while (n < nLength - count) {
            arr.push(matrix[m][n]);
            n++
        }
        n--; m++;
        if (arr.length === num) break // 遍历结束
        while (m < mLength - count) {
            arr.push(matrix[m][n]);
            m++
        }
        if (arr.length === num) break // 遍历结束
        m--
        n--
        while (n >= count) {
            arr.push(matrix[m][n]);
            n--
        }
        if (arr.length === num) break // 遍历结束
        n = count;
        m--
        while (m > count) {
            arr.push(matrix[m][n]);
            m--
        }
        count++
        m = count
        n = count
    }
    return arr
};

17.2. 48. 旋转图像

var rotate = function(matrix) {
    let martrixLength = matrix.length
    for(let i=0; i < martrixLength; i++) {
        for(let j=i; j < martrixLength; j++) {
            [matrix[j][i],matrix[i][j]] = [matrix[i][j],    matrix[j][i]]
        }
    }
    return matrix.map(item => item.reverse())
};

17.3. 240. 搜索二维矩阵 II

var searchMatrix = function(matrix, target) {
    if(matrix.length==0) return false // 判空
    let [left, up]=[matrix[0].length-1, 0]; // 初始化位置
    while(left>=0 && up<matrix.length){
        if(matrix[up][left]>target){
            left--;
        }else if(matrix[up][left]<target){
            up++;
        }else{
            return true;
        }
    }
    return false;
};