前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • 前端算法面试

    • 数组基础
    • 栈、队列与链表
    • 树与二叉树
    • 二叉树递归遍历
    • 时间与空间复杂度
    • 数组高频题
      • Map 的妙用——两数求和问题
        • 思路分析:
      • 强大的双指针法
        • 合并两个有序数组
        • 三数求和问题
        • 双指针法中的“对撞指针”法
    • 字符串高频题
    • 链表基础题
    • 链表双指针
    • 环形链表
    • 栈的经典应用
    • 队列与双端队列
    • DFS 与 BFS
    • 递归与回溯
    • 二叉树高频题
    • 二叉搜索树
    • 平衡二叉树
    • 堆与堆排序
    • 基础排序算法
    • 归并与快速排序
    • 动态规划入门
    • 动态规划进阶
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

数组的应用——真题归纳与解读|算法篇

我们现在要开始做题啦!

万里长征第一步,仍然是数组。 单纯针对数组来考察的题目,总体来说,都不算太难——数组题目要想往难了出,基本都要结合排序、二分和动态规划这些相对复杂的算法思想才行。

咱们本节要解决的正是这一类“不算太难”的数组题目——并不是只有难题才拥有成为真题的入场券,一道好题不一定会难,它只要能够反映问题就可以了。

本节所涉及的题目在面试中普遍具有较高的出镜率、同时兼具一定的综合性,对培养大家的通用解题能力大有裨益 。

相信这节你会学得很开心,在轻松中收获自己的第一份算法解题锦囊。

# Map 的妙用——两数求和问题

30 秒速记

  • 暴力枚举两下标是 O(n²);哈希表把“找另一个数”降为期望 O(1),总时间 O(n)、空间 O(n)
  • 每轮先算 need = target - nums[i] 并查询已扫描前缀,再写当前值,才能保证不重复使用同一元素
  • Map 的键可安全表示数字;普通对象会字符串化键,还要处理原型属性,不是更稳的默认选择
  • 重复数字是否合法取决于下标:[3, 3] 可组成 6,只要它们来自两个位置
  • 返回一组、全部组合或不存在时的结果必须先约定,不能让实现细节替题目决定契约

两数之和可以用 Map 记录“已遍历的值到下标”,把暴力枚举的 O(n²) 优化为期望 O(n)。 遍历到 nums[i] 时,先查询补数 target - nums[i],未命中再写入当前值,这样不会重复使用同一个元素。重复数字本身没问题,例如两个不同位置的 3 可以组成 6。额外空间是 O(n);若题目要求返回全部组合,就需要保存每个值的下标列表并另外约定去重规则。

哈希解法的不变量是:进入第 i 轮时,seen 只保存区间 [0, i) 的值及下标。因此命中补数时,它一定来自不同元素;未命中才写入当前值。原文使用对象并用 !== undefined 判断,会把“索引值是否存在”和“属性值是否为 undefined”混在一起,使用 Map.has() 更直接。

function twoSum(nums, target) {
  const seen = new Map()
  for (let i = 0; i < nums.length; i += 1) {
    const need = target - nums[i]
    if (seen.has(need)) return [seen.get(need), i]
    seen.set(nums[i], i)
  }
  return []
}
@前端进阶之旅: 代码已经复制到剪贴板

每个元素最多查询和写入一次,期望时间 O(n)、额外空间 O(n)。若要求全部不重复下标组合,Map<number, number> 不够,需要保存每个值的下标列表或采用另一套去重契约;若输入含浮点数,还要先确认能否直接用精确相等比较。

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

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
@前端进阶之旅: 代码已经复制到剪贴板

# 思路分析:

30 秒速记

  • 两数之和用 Map 保存“已经见过的值 → 下标”,遍历当前值时查找补数 target - value
  • 必须先查补数再写当前值,避免同一个元素被使用两次;命中后返回两个下标而不是两个数值
  • 单次哈希查找期望 O(1),整体时间 O(n)、额外空间 O(n),以空间换掉双重循环

我会把求和转成求差:遍历数组时,用 Map 保存已经出现的值及其下标,并查找 target - value。 补数命中后,直接返回它的历史下标和当前下标;必须先查询再写入,才能保证同一个位置不会被使用两次。相比双重循环,这种做法把期望时间从 O(n²) 降到 O(n),代价是增加 O(n) 空间。实现时应使用 Map.has() 判断键是否存在,避免把索引值和不存在混为一谈。

一个“淳朴”的解法

这道题相信很多同学看一眼就很快能得出一个最基本的思路:两层循环来遍历同一个数组;第一层循环遍历的值记为 a,第二层循环时遍历的值记为 b;若 a+b = 目标值,那么 a 和 b 对应的数组下标就是我们想要的答案。

对“淳朴”解法的反思

大家以后做算法题的时候,要有这样的一种本能:当发现自己的代码里有两层循环时,先反思一下,能不能用空间换时间,把它优化成一层循环。

因为两层循环很多情况下都意味着 O(n^2) 的复杂度,这个复杂度非常容易导致你的算法超时。即便没有超时,在明明有一层遍历解法的情况下,你写了两层遍历,面试官对你的印象分会大打折扣。

空间换时间,Map 来帮忙

拿我们这道题来说,其实二层遍历是完全不必要的。

大家记住一个结论:几乎所有的求和问题,都可以转化为求差问题。 这道题就是一个典型的例子,通过把求和问题转化为求差问题,事情会变得更加简单。

我们可以在遍历数组的过程中,增加一个 Map 来记录已经遍历过的数字及其对应的索引值。然后每遍历到一个新数字的时候,都回到 Map 里去查询 targetNum 与该数的差值是否已经在前面的数字中出现过了。若出现过,那么答案已然显现,我们就不必再往下走了。

我们以 nums = [2, 7, 11, 15] 这个数组为例,来模拟一下这个思路:

第一次遍历到 2,此时 Map 为空:

以 2 为 key,索引 0 为 value 作存储,继续往下走;遇到了 7:

计算 targetNum 和 7 的差值为2,去 Map 中检索 2 这个 key,发现是之前出现过的值:

那么 2 和 7 的索引组合就是这道题的答案啦。

键值对存储我们可以用 ES6 里的 Map 来做,如果图省事,直接用对象字面量来定义也没什么问题

编码实现

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
const twoSum = function(nums, target) {
    // 这里我用对象来模拟 map 的能力
    const diffs = {}
    // 缓存数组长度
    const len = nums.length
    // 遍历数组
    for(let i=0;i<len;i++) {
        // 判断当前值对应的 target 差值是否存在(是否已遍历过)
        if(diffs[target-nums[i]]!==undefined) {
            // 若有对应差值,那么答案get!
            return [diffs[target - nums[i]], i]
        }
        // 若没有对应差值,则记录当前值
        diffs[nums[i]]=i
    }
};
@前端进阶之旅: 代码已经复制到剪贴板

tips:这道题也可以用 ES6 中的 Map 来做,你试试呢?

面试官追问

追问 1候选人在白板上写了双层循环处理 nums = [2,7,11,15]、target = 9,并说数据量不大就不用优化,你会怎样追问它的复杂度风险?
参考回答

双层循环的时间复杂度是 O(n²),数组增长后比较次数会快速增加,即使样例能通过也不是理想方案。应把求和转成查找补数,用 Map 保存已遍历值及下标,将平均查找和整体遍历收敛到线性级别,代价是 O(n) 额外空间。

追问 2结算页面传入 nums = [3,3]、target = 6,代码把当前值写进 Map 后再查补数,为什么可能返回同一个下标两次?
参考回答

先写后查会让当前的 3 立即成为自己的候选补数,首轮就可能拼出 [0,0],违反必须使用两个元素的约束。实现应先查询 target - nums[i],未命中再写入当前值,使容器始终只保存当前下标之前的数据。

追问 3搜索服务要求返回所有满足目标值的下标组合,但现有实现用 Map<number, number> 且首次命中就 return,你会改哪两处?
参考回答

单个下标会被后续同值覆盖,首次返回也会截断剩余候选,因此两处都不符合全部组合的契约。应让每个值关联历史下标列表,当前补数命中后逐个生成组合,再追加当前下标;结果规模可能达到 O(n²),无法仍承诺线性输出成本。

追问 4线上日志显示输入里明明有补数,使用普通对象模拟 Map 却没有命中,排查时你先看哪段判断和键值存储?
参考回答

先检查命中条件是否依赖 !== undefined,以及对象键是否被字符串化或碰到继承属性,再用失败输入逐轮记录补数与已存键。数值题更稳妥的实现是使用 ES6 Map 的 has、get 和 set;若输入含非数值或浮点误差,仍需先明确相等语义。

追问 5商品列表已经按价格升序排列,只需判断是否存在一对价格等于预算,你会选 Map 还是左右指针?
参考回答

在有序且不要求原下标的契约下,左右指针能以 O(n) 时间和 O(1) 额外空间完成,比 Map 更节省内存。若接口必须返回排序前下标,就要保留值与原索引的绑定,或直接使用哈希方案;不能把排序后位置冒充原位置。

面试官追问

追问 1代码评审中有人把流程改成“先执行 diffs[nums[i]] = i,再判断补数”,输入 [3,3]、目标 6 时具体会错在哪里?
参考回答

首轮写入 3 → 0 后再查询补数 3,会直接命中当前元素并返回重复下标,而不是等待第二个 3。查询必须发生在写入之前,使 diffs 只代表已遍历的前缀;第二轮才能合法返回 [0,1]。

追问 2支付页面把 0.1、0.2 和目标 0.3 直接交给两数之和,监控显示始终查不到组合,你会如何修复而不是随手加 epsilon?
参考回答

故障来自二进制浮点表示导致补数与已存键不能精确相等,哈希查询不会自动进行近似匹配。金额应按明确币种精度转换为最小货币单位整数,或采用十进制定点方案后再建表;任意 epsilon 会让等价关系不稳定,也难以作为可靠的 Map 键。

追问 3风控接口从“返回任意一组”改为“返回全部下标组合”,输入含大量重复值时,原来的单值映射为什么会漏结果?
参考回答

Map<number, number> 对同值只保留一个下标,且首次命中立即结束,因此既丢失历史重复项,也不会继续扫描后续元素。应保存 Map<number, number[]>,对每个补数的历史下标逐一产出组合;当结果本身很多时,时间和空间会随输出规模上升。

追问 4线上返回了 [undefined, 4],实现仍用对象字面量保存下标并以真假值判断命中,你会怎样定位下标 0 被漏掉的问题?
参考回答

若条件写成 if (diffs[complement]),合法下标 0 会被当作假值,后续取值还可能形成异常结果。应改用 diffs[complement] !== undefined,更推荐使用 Map.has(complement) 区分不存在与合法值;同时回放输入确认键和值没有被非数值数据污染。

追问 5移动端数组规模较大但内存受限,产品又只要任意一组结果;无序输入下你会保留哈希解法还是先排序再双指针?
参考回答

哈希解法保持平均 O(n) 时间,但需要 O(n) 额外空间,并能自然保留扫描时的原下标。排序加双指针减少额外查找结构,却引入 O(n log n) 排序成本;若还要原下标,必须携带索引副本,空间优势可能随之减弱。

# 强大的双指针法

← 时间与空间复杂度字符串高频题 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • 前端算法面试

    • 数组基础
    • 栈、队列与链表
    • 树与二叉树
    • 二叉树递归遍历
    • 时间与空间复杂度
    • 数组高频题
      • Map 的妙用——两数求和问题
        • 思路分析:
      • 强大的双指针法
        • 合并两个有序数组
        • 三数求和问题
        • 双指针法中的“对撞指针”法
    • 字符串高频题
    • 链表基础题
    • 链表双指针
    • 环形链表
    • 栈的经典应用
    • 队列与双端队列
    • DFS 与 BFS
    • 递归与回溯
    • 二叉树高频题
    • 二叉搜索树
    • 平衡二叉树
    • 堆与堆排序
    • 基础排序算法
    • 归并与快速排序
    • 动态规划入门
    • 动态规划进阶