在前端面试中, "反转字符串" 和 "两数之和" 虽看似简单,却是考察候选人 基础扎实度、代码思维、API 熟练度与算法意识 的经典组合拳。本文将带你:
- 用 5 种方式实现字符串反转,对比优劣;
- 深入剖析 两数之和的暴力解 vs 哈希优化;
- 揭秘面试官真正想考察什么;
- 提供可直接复用的高质量代码模板。
🔄 一、字符串反转:不止一种写法
场景
输入 'hello',输出 'olleh'。
💡 面试官关注点:
- 是否熟悉数组/字符串 API(
split,reverse,join)- 能否写出清晰、健壮的循环逻辑
- 是否理解递归思想及其风险
- 是否会用现代语法(如扩展运算符、reduce)
✅ 方法1:经典三连(API 流)
perl
js
编辑
function reverseStr(str) {
return str.split('').reverse().join('');
}
const str = "hello";
const arr = str.split('');
// arr 的值会是:['h', 'e', 'l', 'l', 'o']
const arr = ['h', 'e', 'l', 'l', 'o'];
arr.reverse();
// 调用后,arr 本身被修改了,现在的值是:['o', 'l', 'l', 'e', 'h']
const reversedArr = ['o', 'l', 'l', 'e', 'h'];
const reversedStr = reversedArr.join('');
// reversedStr 的值会是:"olleh"
- 优点:简洁、可读性强,体现对内置方法的掌握。
- 缺点:创建中间数组,内存开销略高(但通常可忽略)。
✅ 方法2:传统 for 循环(从后往前)
ini
js
编辑
function reverseStr(str) {
let reversed = '';
for (let i = str.length - 1; i >= 0; i--) {
reversed += str[i];
}
return reversed;
}
- 优点:逻辑直观,兼容性好(ES3 起支持)。
- 注意:字符串拼接在旧引擎中可能低效(现代 V8 已优化)。
✅ 方法3:for...of 反向拼接
csharp
js
编辑
function reverseStr(str) {
let reversed = '';
for (const char of str) {
reversed = char + reversed; // 每次把新字符放前面
}
return reversed;
}
- 优点:避免索引操作,更符合"遍历字符"语义。
- 缺点:频繁字符串拼接(虽现代 JS 引擎已优化)。
✅ 方法4:扩展运算符 + 数组方法(ES6+)
python
js
编辑
function reverseStr(str) {
return [...str].reverse().join('');
}
const str = "hello"; const arr = [...str];
// arr 的值会是:['h', 'e', 'l', 'l', 'o']
- 优点 :比
split('')更优雅(尤其对 emoji/Unicode 支持更好)。 - 原理 :
[...str]能正确处理 UTF-16 代理对(如'👨💻'.length === 5,但[...'👨💻'].length === 1)。
🌰 对比:
bashjs 编辑 'café'.split('') // ['c','a','f','é'] ✅ '👨💻'.split('') // ['👨', '', '💻'] ❌ [...'👨💻'] // ['👨💻'] ✅
✅ 方法5:递归实现(展示思维)
rust
js
编辑
function reverseStr(str) {
if (str === '') return ''; // 终止条件
return reverseStr(str.substring(1)) + str.charAt(0);
}
const str = "hello";
const subStr = str.substring(1);
// subStr 的值会是:"ello" (从索引1的 'e' 开始到末尾)
const str = "hello";
const firstChar = str.charAt(0);
// firstChar 的值会是:"h" (索引0位置的字符)
1. `reverseStr("hello")` 调用 `reverseStr("ello")`
1. `reverseStr("ello")` 调用 `reverseStr("llo")`
1. `reverseStr("llo")` 调用 `reverseStr("lo")`
1. `reverseStr("lo")` 调用 `reverseStr("o")`
1. `reverseStr("o")` 调用 `reverseStr("")`
1. `reverseStr("")` 触发终止条件,返回 `""`
1. `reverseStr("o")` 得到返回值 `"" + "o"`,即 `"o"`,并返回
1. `reverseStr("lo")` 得到返回值 `"o" + "l"`,即 `"ol"`,并返回
1. `reverseStr("llo")` 得到返回值 `"ol" + "l"`,即 `"oll"`,并返回
1. `reverseStr("ello")` 得到返回值 `"oll" + "e"`,即 `"olle"`,并返回
1. `reverseStr("hello")` 得到返回值 `"olle" + "h"`,即 `"olleh"`,并返回给最初的调用者
-
优点:体现分治思想,代码简洁。
-
⚠️ 风险:
- 爆栈:长字符串(如 >10,000 字符)会导致栈溢出;
- 性能差 :每次
substring都创建新字符串,时间复杂度 O(n²)。
🚫 不推荐生产使用,但面试可作为"展示递归理解"的备选。
✅ 方法6:reduce 高阶函数(函数式风格)
javascript
js
编辑
function reverseStr(str) {
return [...str].reduce((reversed, char) => char + reversed, '');
}
- 优点:无状态、纯函数风格,适合函数式编程场景。
- 缺点:可读性略低于三连 API。
📊 反转方法对比总结
| 方法 | 可读性 | 性能 | Unicode 安全 | 推荐场景 |
|---|---|---|---|---|
split+reverse+join |
⭐⭐⭐⭐ | ⭐⭐⭐ | ❌(部分) | 快速实现 |
for 循环 |
⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ | 兼容老环境 |
for...of |
⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ | 现代项目 |
[...str] |
⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅✅ | 首选推荐 |
| 递归 | ⭐⭐ | ⭐ | ✅ | 面试展示 |
reduce |
⭐⭐⭐ | ⭐⭐ | ✅ | 函数式偏好 |
✅ 最佳实践 :日常开发优先用
[...str].reverse().join('')。
🔢 二、两数之和:从暴力到哈希优化
题目 :给定整数数组
nums和目标值target,找出两个数的索引,使其和等于target。假设每组输入只有一组解。
面试官真实意图:
- 考察 时间复杂度意识
- 是否知道 "空间换时间" 思想
- 对 数据结构选择 的敏感度(Object vs Map)
❌ 解法1:暴力双重循环(O(n²))
ini
js
编辑
function twoSum(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
}
- 问题:n=10⁴ 时,操作次数达 5×10⁷,明显超时。
- 结论:仅用于兜底或教学演示。
✅ 解法2:哈希表优化(O(n))------核心思路
核心思想:
"求和变求差"
遍历时,记录每个数字及其索引;
对当前
num,检查target - num是否已存在。
方案A:使用普通对象(Object)
ini
js
编辑
function twoSum(nums, target) {
const map = {};
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map[complement] !== undefined) {
return [map[complement], i];
}
map[nums[i]] = i;
}
}
- 风险 :键为字符串,若
nums含非数字(如'2'),可能混淆; - 性能:V8 对对象属性访问高度优化,实际很快。
方案B:使用 Map(推荐!)
ini
js
编辑
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
}
-
优势:
- 键类型保持原样(number 还是 number);
has()/get()语义清晰;- 避免原型链污染(如
map.__proto__干扰); - 更符合"键值对容器"语义。
📌 面试加分点 :主动说出 "我选择
Map而非对象,因为......"
🧠 三、面试官到底想考什么?
| 题目 | 考察维度 | 高分回答要点 |
|---|---|---|
| 字符串反转 | - API 熟练度 - 代码风格 - 边界意识 | "我会优先用 [...str] 因为它对 Unicode 更安全" |
| 两数之和 | - 算法思维 - 数据结构选择 - 复杂度分析 | "暴力是 O(n²),我用哈希表降到 O(n),空间换时间" |
💡 避坑提示:
- 不要一上来就写递归(除非被要求);
- 不要说"Object 和 Map 一样"(暴露基础不牢);
- 记得处理边界(如空字符串、无解情况)。
📌 四、总结 & 复习清单
✅ 字符串反转
- 首选写法 :
[...str].reverse().join('') - 慎用递归:有爆栈风险,性能差
- 注意 Unicode :
split('')对 emoji 不友好
✅ 两数之和
- 最优解 :
Map+ 一次遍历,O(n) 时间 - 关键转换 :
a + b = target→b = target - a - 不要用 Object 当哈希表(除非明确知道数据安全)