前端面试-leetcode力扣hot100算法题Day1

LeetCode Hot 100

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

示例:给定

ini 复制代码
nums = [2, 7, 11, 15]
target = 9

要求:找出数组中两个数,使它们的和为 target,并返回对应的下标。

核心思路

使用一个 哈希表 Map 来存储遍历过的数字和它们的下标,在每次遍历当前元素时: 查找是否有另一个数 target - 当前数 已经在哈希表中出现过。

哈希表本质是:把"值"映射为"键",然后通过键快速访问值。

在 JavaScript 中

  • Map / Object 都是哈希表结构
  • 查找 map.has(key) 的时间复杂度是 O(1)
  • 因此每次查找是否存在目标差值 target - num 的时间是常数级

步骤:

  1. 初始化一个空的哈希表:map = new Map()

  2. 遍历数组:

    第一次遍历(i = 0):

    • 当前数是 2=

    • 计算目标差值:target - 2 = 7

    • 哈希表中还没有 7,所以:

      把当前数字和下标存入 map:map.set(2, 0)

    第二次遍历(i = 1):

    • 当前数是 7
    • 计算差值:target - 7 = 2
    • 查找 map 中是否有 2
      • 有说明我们已经遍历过了一个可以和 7 组成 9 的数
      • 所以返回 [map.get(2), 1][0, 1]找到了答案
    javascript 复制代码
    function twoSum(nums, target) {
      const map = new Map(); // 创建一个哈希表用于存储数值到索引的映射
    
      for (let i = 0; i < nums.length; i++) {
        const num = nums[i];               // 当前数
        const complement = target - num;   // 当前数需要的"配对数"
    
        if (map.has(complement)) {
          // 如果哈希表中已有配对数,说明找到了两个数相加等于 target
          return [map.get(complement), i];
        }
    
        // 否则,将当前数加入哈希表
        map.set(num, i);
      }
    
      return []; // 如果没有找到,返回空数组(题目保证有解时可省略)
    }

2. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

示例 :

scss 复制代码
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

问题拆解思路 :转换为 Two Sum 问题

我们可以将问题转化为:

  • 对于数组中的每一个元素 a = nums[i]
  • 在剩下的元素中查找两个数 b + c = -a

也就是把问题从 a + b + c = 0 转为 b + c = -a,这其实就是我们前面讲的 Two Sum 问题。

解题算法(排序 + 双指针)

1.先排序:方便跳过重复值,并使用双指针优化查找

遍历排序后的数组,用一个指针 i 固定第一个数 nums[i]

2.固定一个数 a ,再用左右指针在 a 右侧查找 b + c = -a

对于每一个固定的 nums[i],我们需要在 i 之后的子数组中找到两个数 nums[left]nums[right],使得 nums[i] + nums[left] + nums[right] == 0。这等价于找到 nums[left] + nums[right] == -nums[i]

为了找到这两个数,我们使用两个指针:left 指针从 i + 1 开始,right 指针从数组末尾开始。

  • 双指针移动规则:

    计算当前三数之和 sum = nums[i] + nums[left] + nums[right]

    • 如果 sum == 0:找到了一个有效的三元组 [nums[i], nums[left], nums[right]]。将其添加到结果列表中。
      • 然后,为了避免重复,需要移动 left 指针向右,跳过所有与 nums[left] 相同的元素。
      • 同时,移动 right 指针向左,跳过所有与 nums[right] 相同的元素。
      • 最后,leftright 指针各自再向前/后移动一步,继续寻找。
    • 如果 sum < 0:说明三数之和太小了,需要增大和。由于数组已排序,增大和的唯一方法是增加 left 指针,使其指向更大的数。因此,left++
    • 如果 sum > 0:说明三数之和太大了,需要减小和。减小和的方法是减小 right 指针,使其指向更小的数。因此,right--
  • 循环条件: 双指针循环直到 left >= right

3.跳过重复元素,避免结果重复

  • 对 i 的去重: 在遍历 i 时,如果 i > 0nums[i] == nums[i-1],则说明当前的 nums[i] 与上一个已经处理过的 nums[i-1] 是重复的。跳过当前 i 的循环,直接进入下一个 i
  • 对 left 和 right 的去重: 当找到一个有效三元组后,需要移动 left 指针跳过所有与 nums[left] 相同的元素,right 指针跳过所有与 nums[right] 相同的元素。这是因为 nums[i] 已经固定,如果 nums[left]nums[right] 重复,它们组成的三元组也会重复。

步骤:

[-1, 0, 1, 2, -1, -4] 为例

  1. 排序后:[-4, -1, -1, 0, 1, 2]
  2. 从 i = 0 开始,固定 nums[i] = -4,左右指针在 [i+1, n-1] 上找满足 -4 + b + c = 0
  3. 没找到后移动到 i = 1nums[i] = -1,继续双指针查找。
  4. 找到 [-1, -1, 2][-1, 0, 1]
  5. 跳过重复的 -1
  6. 最终返回去重后的结果
javascript 复制代码
function threeSum(nums) {
  const result = [];
  const n = nums.length;

  // 第一步:排序
  nums.sort((a, b) => a - b);

  // 第二步:固定第一个数 nums[i]
  for (let i = 0; i < n - 2; i++) {
    // 如果当前值大于0,后面不可能存在三数之和为0
    if (nums[i] > 0) break;

    // 跳过重复值
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = n - 1;

    // 双指针查找
    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);

        // 跳过重复
        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;
	    //移动指针,继续寻找
        left++;
        right--;
      } else if (sum < 0) {
        left++; // sum太小,左指针右移
      } else {
        right--; // sum太大,右指针左移
      }
    }
  }

  return result;
}
3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

示例 :

ini 复制代码
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
  • 核心思想(滑动窗口 + 哈希集合):

    用两个指针 leftright 维护一个动态窗口 ,这个窗口中始终不包含重复字符。如果遇到重复字符,就收缩左边界;否则扩大右边界。

    我们使用一个集合(Set)来记录当前窗口中的字符,判断是否重复。

    滑动窗口:

    • 想象有一个"窗口"在字符串上滑动。窗口的左边界 left 和右边界 right 定义了当前子串的范围。
    • 我们的目标是维护一个窗口,使得窗口内的字符始终是不重复的。
    • 当窗口内的字符出现重复时,我们就需要收缩左边界来消除重复,直到窗口内再次没有重复字符。

    哈希集合/映射 :

    • 作用: 快速判断一个字符是否已经在当前窗口内存在。哈希表的查找、插入和删除操作的平均时间复杂度都是 O(1)。
    • 存储内容:
      • 可以存储窗口内出现的字符本身 (使用 Set)。
      • 也可以存储字符及其在字符串中的最新索引 (使用 Map),这在某些变体问题中很有用,但对于本问题,Set 足够。
  • 步骤:

    1. 初始化:
      • maxLength = 0: 用于存储迄今为止找到的最长无重复子串的长度。
      • left = 0: 滑动窗口的左边界。
      • charSet = new Set(): 用于存储当前窗口内的字符,以便快速检查重复。
    2. 遍历 字符串(右指针 right):
      • 如果 s[right] 不在 set 中:加入 set,更新最大长度,right++
      • 如果 s[right]set 中:说明出现重复,移除 s[left],然后左指针右移,直到窗口中不包含重复字符。
    3. 检查重复 并调整窗口 (收缩左指针 left):
    4. 更新最大长度并返回,循环结束:
      • 在每次 right 指针移动(且窗口内无重复)后,当前窗口的长度就是 right - left + 1
      • 用当前长度更新 maxLength = Math.max(maxLength, right - left + 1)
      • right 指针遍历完整个字符串时,循环结束,maxLength 存储的就是最终结果。
    javascript 复制代码
    var lengthOfLongestSubstring = function(s) {
        // 如果字符串为空或只有一个字符,直接返回其长度
        if (s.length <= 1) {
            return s.length;
        }
    
        let maxLength = 0; // 存储最长无重复子串的长度
        let left = 0;      // 滑动窗口的左边界
        const charSet = new Set(); // 用于存储当前窗口内的字符,快速判断重复
    
        // right 指针遍历字符串
        for (let right = 0; right < s.length; right++) {
            // 如果当前字符 s[right] 已经在 Set 中存在,说明有重复
            // 需要收缩左边界,直到 Set 中不再包含 s[right]
            while (charSet.has(s[right])) {
                charSet.delete(s[left]); // 从 Set 中移除左边界的字符
                left++;                   // 左边界向右移动
            }
    
            // 当前字符 s[right] 不在 Set 中,可以安全地加入窗口
            charSet.add(s[right]);
    
            // 更新最长子串的长度
            // 当前窗口的长度是 right - left + 1
            maxLength = Math.max(maxLength, right - left + 1);
        }
    
        return maxLength;
    };
4. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

示例 :

ini 复制代码
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
  • 核心思想:动态规划(Kadane 算法)

    我们使用一个变量 currentSum 表示当前子数组的最大和 ,一个变量 maxSum 表示全局最大和

    状态转移方程:

    ​ currentSum = max(nums[i], currentSum + nums[i]) maxSum = max(maxSum, currentSum)

    含义:当前这个数 nums[i] 要么

    • 自己重新开始一个新的子数组(丢弃之前的)

    • 要么继续加入之前的子数组中(currentSum + nums[i])

    • maxSum 保留遍历过程中最大的 currentSum

    动态规划思路:

    假设我们遍历数组,到当前元素 nums[i] 时:dp[i] 表示以 nums[i] 结尾的连续子数组的最大和。那么,dp[i] 的值有两种可能:

    1. nums[i] 自己作为新的子数组的开始:这意味着之前的所有元素都不值得包含在内,因为它们会使和变小。
    2. nums[i] 连接到之前的连续子数组中 :这意味着以 nums[i-1] 结尾的最大子数组和 dp[i-1] 是正的,加上 nums[i] 后仍然能够增大或保持和。

    所以,状态转移方程为:dp[i] = Math.max(nums[i], dp[i-1] + nums[i])

    我们最终的目标是整个数组中所有 dp[i] 中的最大值。

    Kadane's Algorithm (卡丹算法):

    Kadane's Algorithm 是对上述动态规划思想的空间优化。我们不需要存储整个 dp 数组,只需要维护两个变量:

    1. currentMax (或 dp[i] 的当前值): 表示以当前元素 nums[i] 结尾的连续子数组的最大和。
    2. globalMax (或 maxSum): 表示迄今为止遍历过程中发现的整体最大子数组和。
  • 步骤:

    1. 初始化:
      • globalMax = nums[0]:初始化为数组的第一个元素。这是因为子数组至少包含一个元素,所以第一个元素本身就是一个合法的子数组。
      • currentMax = nums[0]:以第一个元素结尾的最大子数组和就是它自己。
    2. 遍历数组: 从第二个元素开始 (i = 1) 遍历数组直到结束。
    3. 更新 currentMax:
      • 对于每个 nums[i],计算以它结尾的最大子数组和:currentMax = Math.max(nums[i], currentMax + nums[i]);
      • 这意味着,如果 currentMax + nums[i] 变得比 nums[i] 本身还小(即 currentMax 变成负数了),那么我们宁愿从 nums[i] 重新开始一个子数组。
    4. 更新 globalMax:
      • 在每次更新 currentMax 之后,比较 currentMaxglobalMax,取较大值:globalMax = Math.max(globalMax, currentMax);
      • 因为 currentMax 只代表以当前元素结尾的最大和,而 globalMax 需要记录整个过程中出现过的最大和。
    5. 返回 globalMax: 循环结束后,globalMax 就是整个数组的最大连续子数组和。
javascript 复制代码
var maxSubArray = function(nums) {
    // 处理空数组或只有一个元素的边界情况(虽然题目说至少包含一个元素,但好的实践是考虑)
    if (nums === null || nums.length === 0) {
        return 0; // 或者抛出错误,根据具体要求
    }
    // 如果只有一个元素,最大和就是它自己
    if (nums.length === 1) {
        return nums[0];
    }

    // 初始化:
    // currentMax: 以当前元素结尾的最大子数组和
    // globalMax: 迄今为止发现的整体最大子数组和
    let currentMax = nums[0];
    let globalMax = nums[0];

    // 从第二个元素开始遍历数组
    for (let i = 1; i < nums.length; i++) {
        // 更新 currentMax:
        // 比较:
        // 1. nums[i] 自己作为新的子数组的开始
        // 2. nums[i] 连接到之前的连续子数组中 (currentMax + nums[i])
        currentMax = Math.max(nums[i], currentMax + nums[i]);

        // 更新 globalMax:
        // 比较当前以 nums[i] 结尾的最大和 (currentMax) 与全局最大和 (globalMax)
        globalMax = Math.max(globalMax, currentMax);
    }

    return globalMax;
};
相关推荐
笔尖的记忆3 小时前
浏览器的观察者
前端·javascript
高热度网3 小时前
初始化electron项目运行后报错 electron uninstall 解决方法
前端·javascript
前端AK君3 小时前
React license 争议
前端·react.js
我的写法有点潮3 小时前
竟然被element-plus背刺了
前端·javascript·vue.js
南村群童欺我老无力3 小时前
100-app-challenge 第二期 GestureGo手势识别
前端·浏览器
unknown不想说话474553 小时前
什么是Module Federation2
前端
岁月宁静3 小时前
AI 聊天消息长列表性能优化:后端分页 + 前端虚拟滚动
前端·vue.js·人工智能
TZOF3 小时前
TypeScript的对象如何进行类型声明
前端·后端·typescript
一只叁木Meow3 小时前
DOM元素尺寸属性详解:offset、client、scroll
前端