Skip to content

LeetCode 经典算法题目整理

1. 两数之和

javascript
function twoSum(nums, target) {
  // 如果数组长度小于等于 1,则无法找到两数之和,直接返回空数组
  if (nums.length <= 1) return [];
  // 创建一个 Map 用来存储数组中的值及其对应的索引
  const map = new Map();
  // 遍历数组,查找是否存在符合条件的两个数
  for (let i = 0; i < nums.length; i++) {
    // 计算目标值与当前值的差
    const complement = target - nums[i];
    // 检查 Map 中是否存在这个差值
    if (map.has(complement)) {
      // 如果存在,返回该差值对应的索引和当前索引
      return [map.get(complement), i];
    } else {
      // 如果不存在,将当前值和索引存入 Map
      map.set(nums[i], i);
    }
  }
  // 如果遍历结束仍未找到符合条件的两个数,返回空数组
  return [];
}

2. 最大子序和

javascript
function maxSubArray(nums) {
  // 初始化 prev 为 0,表示当前子数组的最大和
  let prev = 0;
  // 初始化 max 为数组的第一个元素,表示全局最大子数组的和
  let max = nums[0];
  // 遍历数组的每个元素,逐步更新最大子数组的和
  nums.forEach((value) => {
    // 更新当前子数组的最大和,选择加上当前值或从当前值重新开始
    prev = Math.max(prev + value, value);
    // 更新全局最大子数组的和
    max = Math.max(prev, max);
  });
  // 返回全局最大子数组的和
  return max;
}

3. 反转链表

javascript
function reverseList(head) {
  // 初始化 prev 为 null,表示反转后的链表的初始节点
  let prev = null;
  // 初始化 current 为 head,表示当前处理的节点
  let current = head;
  // 遍历链表,直到所有节点被反转
  while (current) {
    // 保存当前节点的下一个节点,防止后续操作导致链表断裂
    const next = current.next;
    // 将当前节点的 next 指向前一个节点,实现反转
    current.next = prev;
    // 更新 prev 为当前节点,用于下一次迭代
    prev = current;
    // 更新 current 为下一个节点,继续迭代
    current = next;
  }
  // 返回反转后的链表头节点
  return prev;
}

4. 比较版本号

javascript
function compareVersion(version1, version2) {
  // 将版本号通过 '.' 分隔为数组
  const arr1 = version1.split(".");
  const arr2 = version2.split(".");
  // 计算两个版本号数组的最大长度,确保比较过程中不会漏掉任何部分
  let maxLength = Math.max(arr1.length, arr2.length);
  // 遍历版本号数组的每一部分进行比较
  for (let i = 0; i < maxLength; i++) {
    // 取出当前版本号的第 i 部分,若超出长度则视为 0
    const num1 = Number(arr1[i] || 0);
    const num2 = Number(arr2[i] || 0);
    // 如果第 i 部分的数字 num1 大于 num2,返回 1,表示 version1 大
    if (num1 > num2) {
      return 1;
    }
    // 如果第 i 部分的数字 num1 小于 num2,返回 -1,表示 version2 大
    if (num1 < num2) {
      return -1;
    }
    // 如果两个数字相等,继续比较下一部分
    // 在循环结束后,如果所有部分相等,返回 0 表示两个版本号相同
  }
  // 如果遍历完所有部分未提前返回,说明两个版本号完全相等
  return 0;
}

5. 合并两个有序数组

javascript
function mergeSortArray(nums1, m, nums2, n) {
  // 初始化三个指针
  let i = m - 1; // 指向 nums1 有效数字的最后一个位置
  let j = n - 1; // 指向 nums2 的最后一个位置
  let k = m + n - 1; // 指向 nums1 最后一个位置(包括预留的空位)

  // 主循环:将 nums2 和 nums1 中的数字从大到小依次放到 nums1 的末尾
  while (i >= 0 || j >= 0) {
    if (i < 0) {
      // 如果 nums1 用完了,只需将 nums2 的剩余部分填入 nums1
      nums1[k--] = nums2[j--];
    } else if (j < 0) {
      // 如果 nums2 用完了,只需将 nums1 的剩余部分保持原位
      nums1[k--] = nums1[i--];
    } else if (nums1[i] < nums2[j]) {
      // 如果 nums1 当前数字小于 nums2 当前数字,将 nums2 的数字填入
      nums1[k--] = nums2[j--];
    } else {
      // 如果 nums1 当前数字大于等于 nums2 当前数字,将 nums1 的数字填入
      nums1[k--] = nums1[i--];
    }
  }
}

6. 无重复字符的最长子串

javascript
function lengthOfLongestSubstring(s) {
  // 初始化 max,用来记录最长无重复子串的长度
  let max = 0;
  // 初始化 startIndex,用来记录当前无重复子串的起始索引
  let startIndex = 0;
  // 初始化 map,用来存储字符及其最近出现的位置
  let map = new Map();

  // 遍历字符串的每一个字符
  for (let i = 0; i < s.length; i++) {
    // 如果当前字符已经在 map 中存在,说明出现重复
    if (map.has(s[i])) {
      // 更新起始索引为重复字符上次出现位置的下一位
      startIndex = Math.max(map.get(s[i]) + 1, startIndex);
    }
    // 更新最长无重复子串的长度
    max = Math.max(i - startIndex + 1, max);
    // 将当前字符及其索引存入 map
    map.set(s[i], i);
  }
  // 返回最长无重复子串的长度
  return max;
}

7. 有效的括号

javascript
function validParenthesis(s) {
  // 创建一个映射,用于匹配右括号对应的左括号
  const map = {
    "}": "{",
    "]": "[",
    ")": "(",
  };
  // 用栈来存储遇到的左括号
  const stack = [];

  // 遍历字符串的每个字符
  for (let i = 0; i < s.length; i++) {
    // 如果栈不为空,并且栈顶元素是与当前字符对应的左括号
    if (stack.length && stack[stack.length - 1] === map[s[i]]) {
      // 如果匹配成功,弹出栈顶元素
      stack.pop();
    } else {
      // 如果不匹配,当前字符是一个左括号,将其入栈
      stack.push(s[i]);
    }
  }
  // 如果栈为空,说明所有括号都有匹配,返回 true;否则返回 false
  return !stack.length;
}

8. LRU 缓存机制

javascript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity; // 缓存的最大容量
    this.cache = new Map(); // 使用 Map 存储缓存数据
  }

  // 获取缓存数据
  get(key) {
    if (!this.cache.has(key)) {
      return -1; // 如果缓存中没有该数据,则返回 -1
    }
    // 如果数据存在,先删除再重新插入(表示该数据被访问过,更新为最近使用)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  // 设置缓存数据
  put(key, value) {
    // 如果缓存已满,删除最旧的数据
    if (this.cache.size >= this.capacity) {
      // Map 的 keys() 返回插入顺序,删除第一个元素
      this.cache.delete(this.cache.keys().next().value);
    }
    // 如果已经存在这个 key,删除旧值并插入新值
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    // 插入新数据到缓存
    this.cache.set(key, value);
  }
}

// 测试代码
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存 {1=1}
lru.put(2, 2); // 缓存 {1=1, 2=2}
console.log(lru.get(1)); // 返回 1,缓存 {2=2, 1=1}
lru.put(3, 3); // 缓存已满,删除最久未使用的 2,缓存 {1=1, 3=3}
console.log(lru.get(1)); // 返回 -1 (未找到)
lru.put(4, 4); // 缓存 {3=3, 4=4}
console.log(lru.get(2)); // 返回 -1 (未找到)
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4

9. 买卖股票的最佳时机

javascript
function maxProfit(prices) {
  // 初始化最小价格为第一个价格,最大利润为 0
  let minPrice = prices[0];
  let maxProfit = 0;

  // 从第二个价格开始遍历
  for (let i = 1; i < prices.length; i++) {
    // 更新最小价格为当前价格和已有的最小价格之间的较小者
    minPrice = Math.min(prices[i], minPrice);
    // 计算当前利润并更新最大利润
    maxProfit = Math.max(maxProfit, prices[i] - minPrice);
  }

  // 返回最大利润
  return maxProfit;
}

10. 最长回文子串

javascript
function longestPalindrome(s) {
  // 初始化左右边界,表示当前找到的最长回文子串的起始和结束位置
  let l = 0;
  let r = 0;
  // 遍历字符串,以每个字符为中心尝试扩展回文
  for (let i = 0; i < s.length; i++) {
    helper(i, i); // 情况 1:回文子串长度为奇数(以 i 为中心)
    helper(i, i + 1); // 情况 2:回文子串长度为偶数(以 i 和 i+1 为中心)
  }
  // 辅助函数,用于扩展回文子串的左右边界
  function helper(m, n) {
    // 当左右指针范围内的字符相等时,继续向外扩展
    while (m >= 0 && n < s.length && s[m] === s[n]) {
      m--; // 左指针向左移动
      n++; // 右指针向右移动
    }
    // 如果当前回文子串的长度大于之前记录的最长回文子串长度
    if (n - m > r - l) {
      r = n; // 更新右边界
      l = m; // 更新左边界
    }
  }
  // 返回最长回文子串,通过 slice 提取范围 [l+1, r)
  return s.slice(l + 1, r);
}

11. 爬楼梯

1. 正常递归求解,但是时间空间复杂度很差

javascript
function climbStairs(n) {
  if (n <= 2) return n;
  return climbStairs(n - 1) + climbStairs(n - 2);
}

2. 利用数组缓存值

javascript
function climbStairs(n) {
  let result = [1, 2];
  for (let i = 2; i < n; i++) {
    result[i] = result[i - 1] + result[i - 2];
  }
  return result.pop();
}

3. 动态规划求解

javascript
function climbStairs(n) {
  if (n <= 2) return n;
  let a = 1;
  let b = 2;
  let sum = 0;
  for (let i = 2; i < n; i++) {
    sum = a + b;
    a = b;
    b = sum;
  }
  return sum;
}

12. 三数之和

javascript
function threeSum(nums) {
  // 如果数组为空或长度小于等于2,不可能组成三元组,直接返回
  if (!nums || nums.length <= 2) return nums;
  // 将数组排序,便于后续双指针查找
  nums.sort((a, b) => a - b);
  // 定义结果数组,用于存放满足条件的三元组
  let result = [];
  // 遍历数组,固定第一个数 nums[i]
  for (let i = 0; i < nums.length; i++) {
    // 如果 nums[i] > 0,后面的数都大于 0,三数之和不可能为 0,直接退出循环
    if (nums[i] > 0) break;
    // 如果当前数和前一个数相同,跳过,避免重复三元组
    if (i > 0 && nums[i] === nums[i - 1]) continue;
    // 定义左右指针
    let L = i + 1; // 左指针初始化为当前数之后的第一个数
    let R = nums.length - 1; // 右指针初始化为数组最后一个数
    // 开始双指针查找
    while (L < R) {
      // 计算三数之和
      const sum = nums[i] + nums[L] + nums[R];
      if (sum === 0) {
        // 如果三数之和为 0,将三元组加入结果数组
        result.push([nums[i], nums[L], nums[R]]);
        // 跳过重复的右指针值,避免重复三元组
        while (L < R && nums[R] === nums[R - 1]) {
          R--;
        }
        // 跳过重复的左指针值,避免重复三元组
        while (L < R && nums[L] === nums[L + 1]) {
          L++;
        }
        // 左指针右移,右指针左移,继续检查其他可能的三元组
        L++;
        R--;
      } else if (sum > 0) {
        // 如果三数之和大于 0,说明右指针的值过大,右指针左移
        R--;
      } else {
        // 如果三数之和小于 0,说明左指针的值过小,左指针右移
        L++;
      }
    }
  }
  // 返回结果数组
  return result;
}

13. 环形链表

javascript
function hasCycle(head) {
  // 定义两个指针,slow 和 fast,均初始化为链表的头节点
  let slow = head;
  let fast = head;
  // 当快指针和快指针的下一个节点不为空时,继续循环
  while (fast && fast.next) {
    // 慢指针每次移动一步
    slow = slow.next;
    // 快指针每次移动两步
    fast = fast.next.next;
    // 如果快慢指针相遇,说明链表中存在环
    if (slow === fast) return true;
  }
  // 如果循环结束,说明快指针到达链表末尾,没有环
  return false;
}

14. 二叉树的层序遍历

javascript
function levelOrder(root) {
  // 如果根节点为空,返回空数组,表示没有任何层级
  if (!root) return [];
  // 初始化结果数组,用于存储每一层的节点值
  let result = [];
  // 使用队列实现层序遍历,初始队列包含根节点
  let queue = [root];
  // 当队列不为空时,持续处理队列中的节点
  while (queue.length) {
    // 用于存储当前层的节点值
    let levelNodes = [];
    // 获取当前层的节点数量
    let levelNodeSize = queue.length;
    // 遍历当前层的所有节点
    for (let i = 0; i < levelNodeSize; i++) {
      // 从队列中取出一个节点
      const node = queue.shift();
      // 将该节点的值加入当前层的结果中
      levelNodes.push(node.val);
      // 如果左子节点存在,将其加入队列
      if (node.left) queue.push(node.left);
      // 如果右子节点存在,将其加入队列
      if (node.right) queue.push(node.right);
    }
    // 将当前层的节点值加入最终结果中
    result.push(levelNodes);
  }
  // 返回结果数组,包含所有层的节点值
  return result;
}

15. 路径总和

1. 通过递归

javascript
function hasPathSum(root, targetSum) {
  // 如果根节点为空,直接返回 false,因为没有路径
  if (!root) return false;

  // 如果当前节点是叶子节点,并且路径和等于目标和,返回 true
  if (!root.left && !root.right && root.val === targetSum) return true;

  // 更新目标和,减去当前节点的值
  const nextTargetSum = targetSum - root.val;

  // 递归调用,分别检查左子树和右子树
  return (
    hasPathSum(root.left, nextTargetSum) || // 检查左子树
    hasPathSum(root.right, nextTargetSum) // 检查右子树
  );
}

2. 通过 BFS

javascript
function hasPathSum(root, targetSum) {
  // 如果根节点为空,直接返回 false,因为没有路径
  if (!root) return false;
  // 初始化队列,队列中的元素为 [节点, 当前路径和]
  let queue = [[root, root.val]];
  // 遍历队列,直到队列为空
  while (queue.length) {
    // 从队列中取出当前节点和当前路径和
    const [node, currentSum] = queue.shift();
    // 如果当前节点是叶子节点,并且路径和等于目标和,返回 true
    if (!node.left && !node.right && currentSum === targetSum) return true;
    // 如果左子节点存在,将左子节点和更新后的路径和加入队列
    if (node.left) queue.push([node.left, node.left.val + currentSum]);
    // 如果右子节点存在,将右子节点和更新后的路径和加入队列
    if (node.right) queue.push([node.right, node.right.val + currentSum]);
  }
  // 如果队列遍历完成,仍未找到符合条件的路径,返回 false
  return false;
}

16. 全排列

javascript
function permute(nums) {
  // 用来存储所有的排列结果
  const result = [];
  // 用来记录已访问的元素,防止重复选择
  const visited = new Set();

  // 深度优先搜索递归函数
  function dfs(paths) {
    // 当路径长度等于输入数组的长度时,说明找到了一个排列
    if (paths.length === nums.length) {
      result.push([...paths]); // 将当前路径加入结果
      return;
    }

    // 遍历 nums 中的每个元素
    for (let i = 0; i < nums.length; i++) {
      // 如果当前元素已经被访问过,则跳过
      if (visited.has(nums[i])) continue;

      // 标记当前元素为已访问
      visited.add(nums[i]);
      // 将当前元素添加到路径中
      paths.push(nums[i]);
      // 递归调用,继续生成下一个元素
      dfs(paths);
      // 回溯:撤销选择
      visited.delete(nums[i]);
      paths.pop(); // 移除最后一个元素
    }
  }

  // 从空路径开始进行深度优先搜索
  dfs([]);
  // 返回所有的排列结果
  return result;
}

17. 搜索二维矩阵 II

javascript
function searchMatrix(matrix, target) {
  // 获取矩阵的行数和列数
  let m = matrix.length;
  let n = matrix[0].length;

  // 从矩阵的右上角开始搜索,初始化行索引和列索引
  let i = 0; // 行索引,从第一行开始
  let j = n - 1; // 列索引,从最后一列开始

  // 循环条件:行索引不越界且列索引不越界
  while (i < m && j >= 0) {
    // 如果当前元素等于目标值,返回 true
    if (matrix[i][j] === target) {
      return true;
    }
    // 如果当前元素小于目标值,说明目标值在当前元素的下方,移动到下一行
    else if (matrix[i][j] < target) {
      i++;
    }
    // 如果当前元素大于目标值,说明目标值在当前元素的左边,移动到前一列
    else {
      j--;
    }
  }

  // 如果循环结束仍未找到目标值,返回 false
  return false;
}

18. 旋转图像

1. 先水平翻转,在沿对角线翻转

javascript
function rotate(matrix) {
  const n = matrix.length; // 矩阵的大小(n x n)

  // 第一步:上下翻转矩阵
  for (let i = 0; i < Math.floor(n / 2); i++) {
    // 遍历矩阵的前一半行(上下对称行)
    for (let j = 0; j < n; j++) {
      // 遍历每一行中的所有列
      // 交换当前行 `i` 和对称行 `n - i - 1` 的元素
      [matrix[i][j], matrix[n - i - 1][j]] = [
        matrix[n - i - 1][j],
        matrix[i][j],
      ];
    }
  }

  // 第二步:沿主对角线(左上到右下)翻转矩阵
  for (let i = 0; i < n; i++) {
    // 遍历矩阵的每一行
    for (let j = 0; j < i; j++) {
      // 遍历主对角线左下方的元素
      // 交换对称位置的元素,使矩阵沿主对角线翻转
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }

  return matrix; // 返回翻转后的矩阵
}

2. 原地旋转

javascript
function rotate(matrix) {
  const n = matrix.length; // 矩阵的边长(n x n)

  // 遍历矩阵的每个"块"并进行旋转
  // 外层循环:控制行的遍历范围(只需遍历前一半的行)
  for (let i = 0; i < Math.floor(n / 2); ++i) {
    // 内层循环:控制列的遍历范围(每行只需遍历左半部分或包含中轴)
    for (let j = 0; j < Math.floor((n + 1) / 2); ++j) {
      const temp = matrix[i][j]; // 暂存当前元素(左上角)

      // 四元素旋转,依次将对应位置的值赋到目标位置
      matrix[i][j] = matrix[n - j - 1][i]; // 左下角 -> 左上角
      matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1]; // 右下角 -> 左下角
      matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1]; // 右上角 -> 右下角
      matrix[j][n - i - 1] = temp; // 左上角(暂存值) -> 右上角
    }
  }

  return matrix; // 返回旋转后的矩阵
}

19.螺旋矩阵

javascript
function spiralOrder(matrix) {
  if (matrix.length == 0) return []; // 如果矩阵为空,直接返回空数组
  let result = []; // 用于存储按螺旋顺序的结果
  let left = 0; // 左边界的初始值
  let bottom = matrix.length - 1; // 底边界的初始值
  let top = 0; // 上边界的初始值
  let right = matrix[0].length - 1; // 右边界的初始值

  // 开始遍历,条件是边界范围有效
  while (left <= right && top <= bottom) {
    // 从左到右遍历当前顶边
    for (let i = left; i <= right; i++) result.push(matrix[top][i]);
    top++; // 顶边向下收缩

    // 从上到下遍历当前右边
    for (let i = top; i <= bottom; i++) result.push(matrix[i][right]);
    right--; // 右边向左收缩

    // 如果边界越界,跳出循环
    if (left > right || top > bottom) break;

    // 从右到左遍历当前底边
    for (let i = right; i >= left; i--) result.push(matrix[bottom][i]);
    bottom--; // 底边向上收缩

    // 从下到上遍历当前左边
    for (let i = bottom; i >= top; i--) result.push(matrix[i][left]);
    left++; // 左边向右收缩
  }

  return result; // 返回结果数组
}

20. 颜色转化

javascript
const HexToRgb = (hex) => {
  const hexRegExp = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; // 定义正则表达式,匹配3位或6位的十六进制颜色值
  if (!hexRegExp.test(hex)) return null; // 如果输入的hex格式不正确,返回null

  let hexChar = hex.slice(1); // 去掉颜色字符串中的‘#’符号

  if (hexChar.length === 3) {
    // 如果是3位的简写形式,扩展成6位
    hexChar = hexChar
      .split("") // 将字符串分割为单字符数组
      .map((char) => char + char) // 每个字符重复一次
      .join(""); // 重新拼接成字符串
  }

  const r = parseInt(hexChar.slice(0, 2), 16); // 提取并将前两位转换为红色分量
  const g = parseInt(hexChar.slice(2, 4), 16); // 提取并将中间两位转换为绿色分量
  const b = parseInt(hexChar.slice(4, 6), 16); // 提取并将后两位转换为蓝色分量

  return `rgb(${r},${g},${b})`; // 返回RGB颜色格式的字符串
};

console.log(HexToRgb("#fff")); // 测试短格式十六进制颜色
console.log(HexToRgb("#eeeeee")); // 测试长格式十六进制颜色
console.log(HexToRgb("#ff5733")); // 测试标准长格式十六进制颜色

const rgbToHex = (r, g, b) => {
  if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
    // 检查RGB每个分量是否在有效范围内
    throw Error("Invalid RGB values"); // 如果超出范围,抛出错误
  }

  const toHex = (num) => {
    return num.toString(16).padStart(2, "0"); // 将数字转换为两位的十六进制字符串
  };

  return `#${toHex(r)}${toHex(g)}${toHex(b)}`; // 拼接并返回十六进制颜色字符串
};

console.log(rgbToHex(0, 0, 0)); // 测试黑色RGB转十六进制

21. 二叉树的右视图

javascript
function rightSideView(root) {
  let result = []; // 存储右视图的节点值
  const queue = [root]; // 初始化队列,用于层序遍历
  while (queue.length && root) { // 当队列不为空且根节点存在时,继续遍历
    const size = queue.length; // 当前层的节点数量
    for (let i = 0; i < size; i++) { // 遍历当前层的所有节点
      const node = queue.shift(); // 从队列中取出当前节点
      if (node.left) { // 如果当前节点有左子节点,将其加入队列
        queue.push(node.left);
      }
      if (node.right) { // 如果当前节点有右子节点,将其加入队列
        queue.push(node.right);
      }
      if (i === size - 1) { // 如果是当前层的最后一个节点
        result.push(node.val); // 将该节点的值加入结果数组
      }
    }
  }
  return result; // 返回右视图的节点值数组
}
// 递归解法
function rightSideView(root) {
  const result = []; // 存储右视图的节点值
  function dfs(node, deepPath) {
    if (!node) return null; // 如果当前节点为空,直接返回
    if (deepPath === result.length) { // 如果当前深度尚未有值,则记录该节点的值
      result.push(node.val);
    }
    dfs(node.right, deepPath + 1); // 优先递归遍历右子树
    dfs(node.left, deepPath + 1); // 然后递归遍历左子树
  }
  dfs(root, 0); // 从根节点开始深度优先遍

22. 二叉树的最近公共祖先

javascript
function lowestCommonAncestor(root, p, q) {
  if (!root || p === root || q === root) return root; // 如果当前节点为空,或等于p或q,则直接返回当前节点

  const left = lowestCommonAncestor(root.left, p, q); // 在左子树中查找p和q的最近公共祖先
  const right = lowestCommonAncestor(root.right, p, q); // 在右子树中查找p和q的最近公共祖先

  if (left && right) return root; // 如果p和q分别位于当前节点的左右子树,则当前节点为最近公共祖先

  return left || right; // 如果仅在某一侧找到p或q,则返回该侧的节点;否则返回null
}

23. 二叉树最大宽度

javascript
function widthOfBinaryTree(root) {
  if (!root) return 0; // 如果根节点为空,宽度为0

  let maxWidth = 0; // 用于存储二叉树的最大宽度
  let queue = [[root, 0]]; // 初始化队列,存储节点和其对应的索引,根节点索引为0

  while (queue.length) {
    // 当队列不为空时,继续遍历
    const size = queue.length; // 当前层的节点数量
    let startIndex = queue[0][1]; // 当前层的起始索引
    let endIndex = startIndex; // 当前层的终止索引,初始为起始索引
    for (let i = 0; i < size; i++) {
      // 遍历当前层的所有节点
      const [node, index] = queue.shift(); // 从队列中取出当前节点和其索引
      endIndex = index; // 更新终止索引为当前节点的索引

      if (node.left) queue.push([node.left, 2 * index]); // 左子节点的索引为2 * index
      if (node.right) queue.push([node.right, 2 * index + 1]); // 右子节点的索引为2 * index + 1
    }
    const currentWidth = endIndex - startIndex + 1; // 计算当前层的宽度
    maxWidth = Math.max(currentWidth, maxWidth); // 更新最大宽度
  }

  return maxWidth; // 返回二叉树的最大宽度
}

24. 翻转二叉树

javascript
// 1. 递归实现
function invertTree(root) {
  if (!root) return null; // 如果节点为空,返回null

  const temp = root.left; // 暂存左子树
  root.left = root.right; // 将右子树赋值给左子树
  root.right = temp; // 将原来的左子树赋值给右子树

  invertTree(root.left); // 递归翻转左子树
  invertTree(root.right); // 递归翻转右子树

  return root; // 返回翻转后的根节点
}
// 2. 迭代队列实现
function invertTree(root) {
  if (!root) return null; // 如果节点为空,返回null

  let queue = [root]; // 初始化队列,存储待处理节点

  while (queue.length) {
    // 当队列不为空时,继续处理
    const node = queue.shift(); // 从队列中取出当前节点

    const temp = node.left; // 暂存左子树
    node.left = node.right; // 将右子树赋值给左子树
    node.right = temp; // 将原来的左子树赋值给右子树

    if (node.left) queue.push(node.left); // 如果左子树存在,加入队列
    if (node.right) queue.push(node.right); // 如果右子树存在,加入队列
  }

  return root; // 返回翻转后的根节点
}

25. 合并两个有序链表

javascript
// 方法 1:递归实现
function mergeTwoLists(list1, list2) {
  if (!list1) return list2; // 如果 list1 为空,返回 list2
  if (!list2) return list1; // 如果 list2 为空,返回 list1

  if (list1.val < list2.val) {
    // 如果 list1 的节点值小于 list2 的节点值
    list1.next = mergeTwoLists(list1.next, list2); // 递归合并 list1 的下一个节点和 list2
    return list1; // 返回当前的 list1 节点
  } else {
    // 如果 list2 的节点值小于或等于 list1 的节点值
    list2.next = mergeTwoLists(list1, list2.next); // 递归合并 list1 和 list2 的下一个节点
    return list2; // 返回当前的 list2 节点
  }
}

// 方法 2:迭代实现
function mergeTwoLists2(list1, list2) {
  let link = { val: -1, next: null }; // 创建一个虚拟头节点,用于简化操作
  let current = link; // 指针指向当前处理的节点位置

  while (list1 && list2) {
    // 当 list1 和 list2 都不为空时,继续比较
    if (list1.val < list2.val) {
      // 如果 list1 的节点值小于 list2 的节点值
      current.next = list1; // 将 list1 的当前节点接到结果链表中
      list1 = list1.next; // 移动 list1 的指针到下一个节点
    } else {
      // 如果 list2 的节点值小于或等于 list1 的节点值
      current.next = list2; // 将 list2 的当前节点接到结果链表中
      list2 = list2.next; // 移动 list2 的指针到下一个节点
    }
    current = current.next; // 移动结果链表的指针到最新的节点
  }

  current.next = list1 ?? list2; // 将剩余的 list1 或 list2 接到结果链表的末尾
  return link.next; // 返回结果链表的头节点(跳过虚拟头节点)
}

26. 合并 K 个升序链表

javascript
function mergeKLists(lists) {
  if (!lists.length) return null; // 如果链表数组为空,返回 null

  // 合并两个有序链表的递归函数
  const mergeTowList = (list1, list2) => {
    if (!list1) return list2; // 如果第一个链表为空,返回第二个链表
    if (!list2) return list1; // 如果第二个链表为空,返回第一个链表

    if (list1.val < list2.val) {
      // 如果 list1 的当前值小于 list2
      list1.next = mergeTowList(list1.next, list2); // 递归合并 list1 的下一个节点和 list2
      return list1; // 返回 list1 作为结果链表的当前节点
    } else {
      // 如果 list2 的当前值小于或等于 list1
      list2.next = mergeTowList(list1, list2.next); // 递归合并 list1 和 list2 的下一个节点
      return list2; // 返回 list2 作为结果链表的当前节点
    }
  };

  // 分治法合并链表的函数
  const merge = (lists, left, right) => {
    if (left === right) return lists[left]; // 如果左右边界相等,直接返回对应链表
    const mid = Math.floor((left + right) / 2); // 找到中点
    const l1 = merge(lists, left, mid); // 递归合并左半部分
    const l2 = merge(lists, mid + 1, right); // 递归合并右半部分

    return mergeTowList(l1, l2); // 合并左右两部分
  };

  return merge(lists, 0, lists.length - 1); // 调用分治合并函数,范围从 0 到链表数组的长度减 1
}

26. 和为 K 的子数组

javascript
function subarraySum(nums, k) {
  let count = 0; // 用于记录满足条件的子数组数量
  let prefixSums = new Map(); // 用于存储前缀和及其出现的次数
  let prefixSum = 0; // 当前的前缀和

  prefixSums.set(0, 1); // 初始化,前缀和为0出现一次,用于处理子数组本身等于k的情况

  for (const num of nums) {
    // 遍历数组
    prefixSum += num; // 更新当前前缀和

    if (prefixSums.has(prefixSum - k)) {
      // 检查是否存在满足条件的前缀和
      count += prefixSums.get(prefixSum - k); // 累加满足条件的前缀和出现次数
    }

    prefixSums.set(prefixSum, (prefixSums.get(prefixSum) || 0) + 1); // 更新当前前缀和的出现次数
  }

  return count; // 返回满足条件的子数组数量
}

27. 缺失的第一个正数

javascript
// 方法 1:标记法实现

var firstMissingPositive = function (nums) {
  if (nums.indexOf(1) === -1) return 1; // 如果数组中没有1,则返回1

  // 将所有小于等于0或大于数组长度的值替换为1
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] <= 0 || nums[i] > nums.length) {
      nums[i] = 1;
    }
  }

  // 使用负数标记数组中的数是否出现过
  for (let i = 0; i < nums.length; i++) {
    const index = Math.abs(nums[i]) - 1; // 获取对应的索引
    nums[index] = -Math.abs(nums[index]); // 将对应索引的值设为负数,表示出现过
  }

  // 找到第一个值为正数的位置,其索引+1即为结果
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] > 0) {
      return i + 1;
    }
  }

  // 如果所有位置都被标记为负数,返回数组长度+1
  return nums.length + 1;
};

//  2:原地哈希实现

function firstMissingPositive(nums) {
  const n = nums.length; // 数组长度

  if (!nums.includes(1)) return 1; // 如果数组中没有1,直接返回1

  // 将所有不合法的数(小于等于0或大于数组长度)替换为1
  for (let i = 0; i < nums.length; i++) {
    while (nums[i] > 0 && nums[i] <= n && nums[i] !== nums[nums[i] - 1]) {
      const temp = nums[i]; // 暂存当前值
      nums[i] = nums[temp - 1]; // 将值交换到正确位置
      nums[temp - 1] = temp; // 完成交换
    }
  }

  // 遍历数组,找到第一个不匹配的值,其索引+1即为结果
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] !== i + 1) {
      return i + 1;
    }
  }

  // 如果所有位置都匹配,返回数组长度+1
  return nums.length + 1;
}

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

javascript
function removeDuplicates(nums) {
  let slow = 0; // 慢指针,用于标记当前没有重复的元素的最后位置
  let fast = 1; // 快指针,用于遍历数组

  // 遍历数组,快指针逐步向前移动
  while (fast <= nums.length - 1) {
    if (nums[slow] !== nums[fast]) {
      // 如果慢指针和快指针指向的值不同
      nums[slow + 1] = nums[fast]; // 将快指针指向的元素移动到慢指针后面
      slow++; // 慢指针向前移动一位
    }
    fast++; // 快指针向前移动一位
  }

  return nums.slice(0, slow + 1); // 返回去重后的数组
}

29. Z 字形变换

javascript
function convert(s, numRows) {
  if (numRows <= 1) return s; // 如果行数小于等于1,返回原字符串

  let row = 0; // 当前所在行
  let down = true; // 方向标志,true 表示向下,false 表示向上

  let result = new Array(numRows).fill(""); // 创建一个包含 numRows 个空字符串的数组,用于存储每行的字符

  for (let i = 0; i < s.length; i++) {
    // 遍历输入字符串
    result[row] += s[i]; // 将当前字符添加到当前行的对应位置

    if (down) {
      // 如果是向下遍历
      row++; // 向下移动到下一行
    } else {
      // 如果是向上遍历
      row--; // 向上移动到上一行
    }

    if (row === 0) {
      // 如果到达第一行
      down = true; // 切换为向下遍历
    }
    if (row === numRows - 1) {
      // 如果到达最后一行
      down = false; // 切换为向上遍历
    }
  }

  return result.join(""); // 将所有行的字符串合并成一个结果并返回
}

30. 最长公共前缀

javascript
function longestCommonPrefix(strs) {
  if (!strs.length || !strs) return ""; // 如果输入数组为空或为 null,返回空字符串

  let prefix = strs[0]; // 以第一个字符串作为初始前缀

  for (let i = 1; i < strs.length; i++) {
    // 遍历剩余的字符串
    // 当当前字符串不以当前前缀开始时,缩短前缀
    while (strs[i].indexOf(prefix) !== 0) {
      prefix = prefix.slice(0, -1); // 删除前缀的最后一个字符
      if (!prefix) return ""; // 如果前缀为空,则返回空字符串
    }
  }

  return prefix; // 返回最长公共前缀
}

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

javascript
function removeDuplicates(s) {
  let stack = []; // 创建一个栈,用于存储不重复的字符

  for (let i = 0; i < s.length; i++) {
    // 遍历字符串中的每个字符
    if (s[i] === stack[stack.length - 1]) {
      // 如果当前字符与栈顶字符相同,表示找到了一对相邻的重复字符
      stack.pop(); // 弹出栈顶元素(删除这对重复字符)
    } else {
      stack.push(s[i]); // 如果当前字符不重复,推入栈中
    }
  }

  return stack.join(""); // 将栈中的字符连接成一个新的字符串并返回
}

console.log(removeDuplicates("abbaca")); // 输出 "ca"

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

javascript
function removeDuplicates(s, k) {
  const stack = []; // 用于存储字符
  const countStack = []; // 用于存储字符的计数
  let i = 0;

  while (i < s.length) {
    // 遍历字符串的每个字符
    if (stack[stack.length - 1] === s[i]) {
      // 如果栈顶字符与当前字符相同
      stack.push(s[i]); // 将当前字符压入栈
      countStack[countStack.length - 1] = countStack[countStack.length - 1] + 1; // 更新栈顶字符的计数
      if (countStack[countStack.length - 1] === k) {
        // 如果栈顶字符的计数达到k
        for (let j = 0; j < k; j++) {
          // 移除栈顶k个字符
          stack.pop();
        }
        countStack.pop(); // 同时也移除计数栈的计数
      }
    } else {
      // 如果栈顶字符与当前字符不同
      stack.push(s[i]); // 将当前字符压入栈
      countStack.push(1); // 计数栈中该字符的计数为1
    }
    i++; // 移动到下一个字符
  }

  return stack.join(""); // 返回去除重复字符后的结果字符串
}

33. 盛最多水的容器

javascript
function maxArea(height) {
  let left = 0; // 初始化左指针,指向数组的起始位置
  let right = height.length - 1; // 初始化右指针,指向数组的末尾位置
  let currentMaxArea = 0; // 用于存储当前的最大水量

  while (left < right) {
    // 当左指针小于右指针时,继续计算
    // 计算当前容器的水量,更新最大水量
    currentMaxArea = Math.max(
      currentMaxArea,
      (right - left) * Math.min(height[left], height[right])
    );

    // 如果左边的高度小于右边的高度,则移动左指针,试图找到更大的高度
    if (height[left] < height[right]) {
      left++;
    } else {
      // 否则,移动右指针,试图找到更大的高度
      right--;
    }
  }

  return currentMaxArea; // 返回最终计算得到的最大水量
}

34. 最大子序和

javascript
function maxSubArray(nums) {
  let maxSum = nums[0]; // 初始化最大和为数组的第一个元素
  let currentSum = nums[0]; // 当前子数组的和初始化为第一个元素

  for (let i = 1; i < nums.length; i++) {
    currentSum = Math.max(nums[i], currentSum + nums[i]); // 计算当前子数组的最大和
    maxSum = Math.max(maxSum, currentSum); // 更新全局最大和
  }

  return maxSum; // 返回最大子序和
}

35. 接雨水

javascript
function trap(height) {
  if (!height || height.length < 3) return 0; // 如果数组为空或长度小于3,无法接水

  let left = 0; // 左指针
  let right = height.length - 1; // 右指针
  let leftMax = height[left]; // 左侧最大高度
  let rightMax = height[right]; // 右侧最大高度
  let waterTrapped = 0; // 初始化接水量

  while (left < right) {
    if (height[left] < height[right]) {
      // 如果左边的柱子较低
      if (height[left] >= leftMax) {
        // 更新左侧最大高度
        leftMax = height[left];
      } else {
        // 否则,计算接水量
        waterTrapped += leftMax - height[left];
      }
      left++; // 移动左指针
    } else {
      // 如果右边的柱子较低
      if (height[right] >= rightMax) {
        // 更新右侧最大高度
        rightMax = height[right];
      } else {
        // 否则,计算接水量
        waterTrapped += rightMax - height[right];
      }
      right--; // 移动右指针
    }
  }

  return waterTrapped; // 返回接到的水量
}

36. N 皇后

javascript
function solveNQueens(n) {
  const result = [];
  const board = Array.from({ length: n }, () => Array(n).fill(".")); // 初始化棋盘
  const cols = new Set(); // 存储列冲突
  const diag1 = new Set(); // 存储主对角线冲突
  const diag2 = new Set(); // 存储副对角线冲突

  function backtrack(row) {
    if (row === n) {
      // 如果所有行都已填充皇后,找到一个解
      result.push(board.map((row) => row.join("")));
      return;
    }

    for (let col = 0; col < n; col++) {
      // 检查当前列和两个对角线是否有冲突
      if (cols.has(col) || diag1.has(row - col) || diag2.has(row + col)) {
        continue;
      }

      // 放置皇后
      board[row][col] = "Q";
      cols.add(col);
      diag1.add(row - col);
      diag2.add(row + col);

      // 递归尝试在下一行放置皇后
      backtrack(row + 1);

      // 回溯,撤销当前选择
      board[row][col] = ".";
      cols.delete(col);
      diag1.delete(row - col);
      diag2.delete(row + col);
    }
  }

  backtrack(0); // 从第0行开始
  return result; // 返回所有解
}

37. 二叉树的最大深度

javascript
function maxDepth(root) {
  if (!root) return 0; // 如果根节点为空,深度为0

  // 递归计算左子树和右子树的深度,取较大的值
  const leftDepth = maxDepth(root.left);
  const rightDepth = maxDepth(root.right);

  // 当前树的最大深度是左子树和右子树深度的最大值加1
  return Math.max(leftDepth, rightDepth) + 1;
}

38. 最长连续序列

javascript
function longestConsecutive(nums) {
  if (nums.length === 0) return 0;

  const numSet = new Set(nums); // 使用集合来存储数组元素,查找复杂度为 O(1)

  let longest = 0;

  for (let num of numSet) {
    // 只有当 `num - 1` 不在集合中时,才是可能的序列的起始元素
    if (!numSet.has(num - 1)) {
      let currentNum = num;
      let currentLength = 1;

      // 尝试扩展序列
      while (numSet.has(currentNum + 1)) {
        currentNum++;
        currentLength++;
      }

      // 更新最长连续序列的长度
      longest = Math.max(longest, currentLength);
    }
  }

  return longest;
}

39. 只出现一次的数字

javascript
function singleNumber(nums) {
  let result = 0;
  for (let num of nums) {
    result ^= num; // 将所有元素进行异或
  }
  return result;
}

40. 复原 IP 地址

javascript
function restoreIpAddresses(s) {
  const result = [];

  // 判断某个子字符串是否是有效的 IP 地址段
  const isValid = (str) => {
    if (str.length > 1 && str[0] === "0") return false; // 不能有前导零
    const num = Number(str);
    return num >= 0 && num <= 255; // 必须在 0 到 255 之间
  };

  // 回溯函数
  const backtrack = (start, path) => {
    // 如果已经分割成 4 段且遍历完所有字符,则是一个有效的 IP 地址
    if (path.length === 4 && start === s.length) {
      result.push(path.join("."));
      return;
    }

    // 如果已经分割成 4 段但还没遍历完字符,说明无效
    if (path.length === 4) return;

    // 尝试每一段长度为 1 到 3 的子串
    for (let len = 1; len <= 3; len++) {
      const segment = s.substring(start, start + len);
      if (start + len > s.length) break;

      if (isValid(segment)) {
        path.push(segment); // 选择当前段
        backtrack(start + len, path); // 继续递归分割剩余部分
        path.pop(); // 撤销选择
      }
    }
  };

  // 如果输入字符串长度不在 4 到 12 之间,直接返回空数组
  if (s.length < 4 || s.length > 12) return result;

  backtrack(0, []); // 从第 0 个字符开始回溯
  return result;
}

41. 二叉树的直径

javascript
function diameterOfBinaryTree(root) {
  let maxDiameter = 0; // 用于保存最大直径

  // 递归计算树的深度并更新最大直径
  function depth(node) {
    if (!node) return 0; // 空节点深度为0

    const leftDepth = depth(node.left); // 左子树的深度
    const rightDepth = depth(node.right); // 右子树的深度

    // 更新最大直径:当前节点的直径为左子树深度 + 右子树深度
    maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);

    // 当前节点的深度是左子树和右子树深度的最大值加1
    return Math.max(leftDepth, rightDepth) + 1;
  }

  depth(root); // 从根节点开始递归计算
  return maxDiameter; // 返回最终的最大直径
}

Released under the MIT License.