聊前端面试,算法永远是绕不开的坎。很多小伙伴项目经验很丰富,框架用得溜,但一上面试场就被手写算法题卡住,直接导致面试失利。
我整理了一份30个高频手写JS算法清单,覆盖ES6语法、数组操作、链表、二叉树、动态规划等核心考点,从简单到进阶,每道题都附完整可复制代码+考点解析,跟着敲一遍,面试时直接手到擒来!
一、基础语法与数组变换(必拿分,入门级)
这类题考察基础语法功底,难度低、频率高,面试时先搞定这类题,稳拿基础分,给面试官留好第一印象。
1. 深度克隆(Deep Clone)
考察点:引用类型传址、循环引用、基本类型与引用类型区别
function deepClone(obj, map = new WeakMap()) {
// 基本类型直接返回(null也是基本类型范畴)
if (obj === null || typeof obj !== 'object') return obj;
// 处理循环引用(避免无限递归)
if (map.has(obj)) return map.get(obj);
// 区分数组和对象,创建对应克隆容器
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone); // 存入map,标记已克隆
// 遍历对象/数组,递归克隆每一项
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // 只克隆自身属性,不克隆原型链属性
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
// 测试
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const cloneObj = deepClone(obj);
obj.b.c = 100;
console.log(cloneObj.b.c); // 2(克隆后互不影响)
2. 数组扁平化(Flatten)
考察点:递归、数组方法(forEach、concat)、ES6新特性
// 解法1:递归(兼容性好,易理解)
function flatten(arr) {
let result = [];
arr.forEach(item => {
// 若当前项是数组,递归扁平化,否则直接加入结果
result = result.concat(Array.isArray(item) ? flatten(item) : item);
});
return result;
}
// 解法2:ES6 flat方法(简洁,实际开发常用)
const flatten = arr => arr.flat(Infinity); // Infinity表示无限层级扁平化
// 解法3:reduce实现(更简洁,面试加分)
const flatten = arr => arr.reduce((prev, curr) => {
return prev.concat(Array.isArray(curr) ? flatten(curr) : curr);
}, []);
// 测试
console.log(flatten([1, [2, [3, 4], 5]])); // [1,2,3,4,5]
3. 防抖(Debounce)
考察点:高频事件控制、定时器、this指向
// 核心:频繁触发时,只在最后一次触发后延迟执行
function debounce(fn, delay = 500) {
let timer = null; // 定时器标识,闭包保存
return function(...args) {
clearTimeout(timer); // 每次触发,清除上一次定时器
// 重新设置定时器,延迟执行目标函数
timer = setTimeout(() => {
fn.apply(this, args); // 绑定this和参数,适配实际场景
}, delay);
};
}
// 用法(搜索框输入示例)
const handleSearch = debounce((val) => {
console.log('请求搜索接口:', val);
}, 500);
4. 节流(Throttle)
考察点:高频事件控制、时间戳/定时器、性能优化
// 解法1:时间戳版(触发时立即执行,之后固定时间内不执行)
function throttle(fn, interval = 1000) {
let lastTime = 0; // 上一次执行时间
return function(...args) {
const nowTime = Date.now(); // 当前时间
// 若当前时间 - 上一次执行时间 > 间隔,执行函数
if (nowTime - lastTime >= interval) {
fn.apply(this, args);
lastTime = nowTime; // 更新上一次执行时间
}
};
}
// 解法2:定时器版(触发后延迟执行,固定时间内只执行一次)
function throttle2(fn, interval = 1000) {
let timer = null;
return function(...args) {
if (!timer) { // 若定时器不存在,执行函数
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 执行后清空定时器,允许下次执行
}, interval);
}
};
}
// 用法(滚动加载示例)
window.addEventListener('scroll', throttle(() => {
console.log('滚动加载更多');
}, 1000));
5. 数组去重(Unique)
考察点:数组方法、Set数据结构、兼容性
// 解法1:Set实现(最简洁,ES6+常用)
const unique = arr => [...new Set(arr)];
// 解法2:indexOf实现(兼容性好,适合旧项目)
function unique(arr) {
const result = [];
arr.forEach(item => {
// 若结果数组中没有当前项,加入结果
if (result.indexOf(item) === -1) {
result.push(item);
}
});
return result;
}
// 解法3:filter+indexOf(简洁,面试常用)
const unique = arr => arr.filter((item, index) => {
// 只保留第一次出现的元素(indexOf返回第一个匹配的索引)
return arr.indexOf(item) === index;
});
// 测试
console.log(unique([1, 2, 2, 3, 3, 3])); // [1,2,3]
6. 数组排序(冒泡排序)
考察点:排序原理、循环逻辑、基础算法思维
// 冒泡排序:相邻元素对比,大的往后移,每次循环确定一个最大值
function bubbleSort(arr) {
const len = arr.length;
// 外层循环:控制排序轮次(共len-1轮)
for (let i = 0; i < len - 1; i++) {
let flag = false; // 优化:标记是否发生交换,若无则排序完成
// 内层循环:对比相邻元素,交换位置
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换两个元素
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
flag = true;
}
}
if (!flag) break; // 无交换,直接退出循环
}
return arr;
}
// 测试
console.log(bubbleSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]
7. 数组排序(快速排序)
考察点:分治思想、递归、时间复杂度优化(面试高频)
// 快速排序:分治思想,选一个基准值,将数组分成两部分,递归排序
function quickSort(arr) {
// 终止条件:数组长度<=1,直接返回
if (arr.length <= 1) return arr;
// 选基准值(中间项,避免极端情况)
const pivot = arr[Math.floor(arr.length / 2)];
// 分治:小于基准值的放左边,等于的放中间,大于的放右边
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
// 递归排序左右两部分,拼接结果
return [...quickSort(left), ...middle, ...quickSort(right)];
}
// 测试
console.log(quickSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]
8. 实现数组forEach方法
考察点:数组方法原理、this绑定、回调函数
// 模拟数组forEach,接收回调函数和this指向
Array.prototype.myForEach = function(callback, thisArg) {
// 边界判断:回调必须是函数
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
// 遍历当前数组(this指向调用myForEach的数组)
for (let i = 0; i < this.length; i++) {
// 执行回调,传入三个参数:当前项、索引、原数组,绑定thisArg
callback.call(thisArg, this[i], i, this);
}
};
// 用法
[1, 2, 3].myForEach((item, index) => {
console.log(item, index); // 1 0 | 2 1 | 3 2
});
9. 实现数组map方法
考察点:数组方法原理、返回值处理、回调函数
// 模拟数组map,返回新数组,新数组元素是回调函数的返回值
Array.prototype.myMap = function(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const result = []; // 存储结果的新数组
for (let i = 0; i < this.length; i++) {
// 执行回调,将返回值加入结果数组
result.push(callback.call(thisArg, this[i], i, this));
}
return result;
};
// 用法
const newArr = [1, 2, 3].myMap(item => item * 2);
console.log(newArr); // [2,4,6]
10. 实现数组filter方法
考察点:数组方法原理、条件判断、返回值处理
// 模拟数组filter,返回满足条件的元素组成的新数组
Array.prototype.myFilter = function(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const result = [];
for (let i = 0; i < this.length; i++) {
// 回调返回true,将当前项加入结果数组
if (callback.call(thisArg, this[i], i, this)) {
result.push(this[i]);
}
}
return result;
};
// 用法
const evenArr = [1, 2, 3, 4].myFilter(item => item % 2 === 0);
console.log(evenArr); // [2,4]
二、原型与作用域(进阶必问,中层前端考点)
这类题考察对JS底层原理的理解,是区分初级和中级前端的关键,面试时高频出现,必须吃透。
11. 实现new关键字
考察点:原型链、构造函数、this绑定、返回值判断
// myNew:模拟new关键字的作用
function myNew(fn, ...args) {
// 1. 创建一个空对象,让其原型指向构造函数的prototype
const obj = Object.create(fn.prototype);
// 2. 执行构造函数,将this绑定到新创建的对象上
const result = fn.apply(obj, args);
// 3. 判断构造函数的返回值:若为对象(非null),则返回该对象;否则返回新创建的obj
return result instanceof Object ? result : obj;
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = myNew(Person, '张三', 25);
console.log(person.name); // 张三
console.log(person instanceof Person); // true
12. 手写Promise(简易版,支持then链式调用)
考察点:异步编程、状态机、回调队列、链式调用
class MyPromise {
constructor(exector) {
// 初始化状态:pending(等待)、fulfilled(成功)、rejected(失败)
this.status = 'pending';
this.value = null; // 成功时的返回值
this.reason = null; // 失败时的原因
// 存储成功/失败的回调队列(支持多个then绑定)
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
// 成功回调:改变状态,保存值,执行所有成功回调
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
// 执行所有缓存的成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
// 失败回调:改变状态,保存原因,执行所有失败回调
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
// 执行所有缓存的失败回调
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 执行 executor,捕获异常,异常时调用reject
try {
exector(resolve, reject);
} catch (err) {
reject(err);
}
}
// then方法:支持链式调用,返回新的Promise
then(onFulfilled, onRejected) {
// 兼容:若then未传回调,默认透传值/原因
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
// 返回新Promise,实现链式调用
return new MyPromise((resolve, reject) => {
// 状态为成功时,执行成功回调
if (this.status === 'fulfilled') {
try {
const result = onFulfilled(this.value);
// 回调返回值,传递给下一个then的成功回调
resolve(result);
} catch (err) {
// 回调执行失败,传递给下一个then的失败回调
reject(err);
}
}
// 状态为失败时,执行失败回调
if (this.status === 'rejected') {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (err) {
reject(err);
}
}
// 状态为等待时,缓存回调
if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
try {
const result = onFulfilled(this.value);
resolve(result);
} catch (err) {
reject(err);
}
});
this.onRejectedCallbacks.push(() => {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (err) {
reject(err);
}
});
}
});
}
}
// 测试
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
}, 1000);
}).then(res => {
console.log(res); // 成功
return '下一个then';
}).then(res => {
console.log(res); // 下一个then
});
13. 实现Promise.all方法
考察点:Promise并发控制、数组遍历、状态判断
// Promise.all:接收一个Promise数组,所有Promise成功才成功,有一个失败则失败
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
// 边界判断:若传入的不是数组,直接reject
if (!Array.isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
const result = []; // 存储所有Promise的成功结果
let count = 0; // 记录已完成的Promise数量
// 若数组为空,直接resolve空数组
if (promises.length === 0) return resolve(result);
// 遍历每个Promise
promises.forEach((promise, index) => {
// 兼容非Promise值(直接视为成功)
Promise.resolve(promise).then(res => {
result[index] = res; // 按原顺序存储结果
count++;
// 所有Promise都完成,resolve结果数组
if (count === promises.length) {
resolve(result);
}
}).catch(err => {
// 有一个失败,直接reject该错误
reject(err);
});
});
});
};
// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.myAll([p1, p2, p3]).then(res => {
console.log(res); // [1,2,3]
});
14. 实现Promise.race方法
考察点:Promise并发控制、第一个完成的状态优先
// Promise.race:接收一个Promise数组,第一个完成(成功/失败)的结果作为最终结果
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
// 遍历每个Promise,第一个完成的直接改变状态
promises.forEach(promise => {
Promise.resolve(promise).then(res => {
resolve(res); // 第一个成功,直接resolve
}).catch(err => {
reject(err); // 第一个失败,直接reject
});
});
});
};
// 测试
const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('失败'), 500));
Promise.myRace([p1, p2]).catch(err => {
console.log(err); // 失败(p2先完成,且失败)
});
15. 函数柯里化(Currying)
考察点:闭包、参数复用、函数式编程
// 柯里化:将多参数函数,转化为单参数函数的链式调用
function curry(fn) {
// 闭包保存已传入的参数
return function curried(...args) {
// 若传入的参数数量 >= 原函数的参数数量,执行原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 否则,返回一个新函数,接收剩余参数,递归调用curried
return function(...args2) {
return curried.apply(this, [...args, ...args2]);
};
}
};
}
// 用法
const add = (a, b, c) => a + b + c; // 原函数,3个参数
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6(链式调用)
console.log(curriedAdd(1, 2)(3)); // 6(支持部分参数)
console.log(curriedAdd(1, 2, 3)); // 6(支持完整参数)
16. 实现call方法
考察点:this绑定、函数执行、参数传递
// 模拟Function.prototype.call:改变函数this指向,立即执行函数
Function.prototype.myCall = function(context, ...args) {
// 边界判断:若context为null/undefined,指向window(浏览器环境)
context = context || window;
// 给context添加一个临时属性,指向当前函数(this就是调用myCall的函数)
const fnKey = Symbol('tempFn'); // 用Symbol避免属性冲突
context[fnKey] = this;
// 执行函数,传入参数,获取返回值
const result = context[fnKey](...args);
// 删除临时属性,避免污染context
delete context[fnKey];
// 返回函数执行结果
return result;
};
// 测试
const obj = { name: '张三' };
function sayHello(age) {
console.log(`我是${this.name},年龄${age}`);
}
sayHello.myCall(obj, 25); // 我是张三,年龄25
17. 实现apply方法
考察点:this绑定、函数执行、参数传递(与call的区别:参数是数组)
// 模拟Function.prototype.apply:改变this指向,参数以数组形式传递,立即执行
Function.prototype.myApply = function(context, args = []) {
context = context || window;
const fnKey = Symbol('tempFn');
context[fnKey] = this;
// 执行函数,args是数组,用扩展运算符展开
const result = context[fnKey](...args);
delete context[fnKey];
return result;
};
// 测试
const obj = { name: '李四' };
function sayHello(age, gender) {
console.log(`我是${this.name},年龄${age},性别${gender}`);
}
sayHello.myApply(obj, [28, '男']); // 我是李四,年龄28,性别男
18. 实现bind方法
考察点:this绑定、闭包、函数柯里化、构造函数兼容
// 模拟Function.prototype.bind:改变this指向,返回一个新函数,不立即执行
Function.prototype.myBind = function(context, ...args1) {
const fn = this; // 保存当前函数(this就是调用myBind的函数)
// 返回新函数
const boundFn = function(...args2) {
// 兼容构造函数:若新函数被new调用,this指向实例,否则指向context
const isNew = this instanceof boundFn;
const targetContext = isNew ? this : context;
// 合并参数,执行原函数
return fn.apply(targetContext, [...args1, ...args2]);
};
// 继承原函数的原型,确保new调用时,实例能访问原函数原型上的属性
boundFn.prototype = Object.create(fn.prototype);
return boundFn;
};
// 测试1:普通调用
const obj = { name: '王五' };
function sayHello(age) {
console.log(`我是${this.name},年龄${age}`);
}
const boundSay = sayHello.myBind(obj, 30);
boundSay(); // 我是王五,年龄30
// 测试2:new调用(构造函数兼容)
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.myBind(null, '赵六');
const person = new BoundPerson(35);
console.log(person.name); // 赵六
console.log(person.age); // 35
19. 实现防抖+立即执行版
考察点:防抖原理、立即执行逻辑、定时器控制
// 立即执行版防抖:第一次触发立即执行,之后频繁触发不执行,延迟后可再次触发
function debounceImmediate(fn, delay = 500) {
let timer = null;
return function(...args) {
// 若定时器存在,清除定时器(取消延迟执行)
if (timer) clearTimeout(timer);
// 判断是否是第一次触发(timer为null)
const isImmediate = !timer;
if (isImmediate) {
fn.apply(this, args); // 立即执行
}
// 重置定时器,延迟后清空timer,允许下次立即执行
timer = setTimeout(() => {
timer = null;
}, delay);
};
}
// 用法(按钮提交示例,避免重复提交,第一次点击立即执行)
const handleSubmit = debounceImmediate(() => {
console.log('提交表单');
}, 1000);
20. 实现节流+立即执行/延迟执行可选版
考察点:节流原理、参数配置、灵活性优化
// 可选版节流:可配置立即执行(leading)和延迟执行(trailing)
function throttleOpt(fn, interval = 1000, options = { leading: true, trailing: false }) {
const { leading, trailing } = options;
let lastTime = 0;
let timer = null;
return function(...args) {
const nowTime = Date.now();
// 若不允许立即执行,且是第一次触发,重置lastTime
if (!leading && !lastTime) {
lastTime = nowTime;
}
// 计算剩余时间
const remainingTime = interval - (nowTime - lastTime);
// 剩余时间<=0,执行函数
if (remainingTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = nowTime;
} else if (trailing && !timer) {
// 允许延迟执行,且无定时器,设置延迟执行
timer = setTimeout(() => {
timer = null;
lastTime = Date.now();
fn.apply(this, args);
}, remainingTime);
}
};
}
// 用法
// 立即执行,不延迟执行(默认)
const throttle1 = throttleOpt(() => console.log('立即执行'), 1000);
// 不立即执行,延迟执行
const throttle2 = throttleOpt(() => console.log('延迟执行'), 1000, { leading: false, trailing: true });
三、数据结构与算法(大厂高频,高级前端考点)
这类题考察数据结构基础和算法思维,是大厂面试的重点,也是拉开薪资差距的关键,建议重点练习。
21. 两数之和(Two Sum)
考察点:哈希表、时间复杂度优化(从O(n²)优化到O(n))
// 题目:给定一个整数数组和一个目标值,找出数组中和为目标值的两个整数的索引
function twoSum(nums, target) {
const map = new Map(); // 用Map存储{数值: 索引},快速查找
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i]; // 互补值
// 若互补值存在于Map中,返回两个索引
if (map.has(complement)) {
return [map.get(complement), i];
}
// 否则,将当前数值和索引存入Map
map.set(nums[i], i);
}
return []; // 无匹配项,返回空数组
}
// 测试
console.log(twoSum([2, 7, 11, 15], 9)); // [0,1]
22. LRU缓存机制
考察点:哈希表+双向链表、缓存淘汰策略(Vue3响应式缓存底层类似)
// LRU:最近最少使用,超出容量时,删除最久未使用的元素
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;
}
// 存入元素:超出容量时,删除最久未使用的元素(Map的第一个键)
put(key, value) {
// 若元素已存在,先删除(避免覆盖后,位置不变)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 超出容量,删除最久未使用的元素
if (this.cache.size >= this.capacity) {
const oldestKey = this.cache.keys().next().value; // Map的第一个键是最久未使用的
this.cache.delete(oldestKey);
}
// 存入新元素,移到最近使用的位置
this.cache.set(key, value);
}
}
// 测试
const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
console.log(lru.get(1)); // 1(访问后,1变为最近使用)
lru.put(3, 3); // 超出容量,删除最久未使用的2
console.log(lru.get(2)); // -1(已被删除)
23. 发布-订阅模式(EventBus)
考察点:设计模式、组件通信、回调函数管理(Vue EventBus底层原理)
// 发布-订阅模式:实现组件间通信,解耦
class EventEmitter {
constructor() {
this.events = new Map(); // 存储事件:{事件名: [回调函数数组]}
}
// 订阅事件:绑定回调函数
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []); // 若事件不存在,初始化回调数组
}
this.events.get(event).push(callback); // 加入回调数组
}
// 发布事件:触发该事件的所有回调函数
emit(event, ...args) {
const callbacks = this.events.get(event);
if (callbacks) {
// 执行所有回调,传入参数
callbacks.forEach(cb => cb(...args));
}
}
// 取消订阅:移除指定事件的指定回调
off(event, callback) {
const callbacks = this.events.get(event);
if (callbacks) {
// 过滤掉要取消的回调,保留其他回调
this.events.set(event, callbacks.filter(cb => cb !== callback));
// 若回调数组为空,删除该事件
if (this.events.get(event).length === 0) {
this.events.delete(event);
}
}
}
// 一次性订阅:触发一次后,自动取消订阅
once(event, callback) {
// 包装回调,执行后取消订阅
const wrapCallback = (...args) => {
callback(...args);
this.off(event, wrapCallback);
};
this.on(event, wrapCallback);
}
}
// 测试
const bus = new EventEmitter();
const callback = (msg) => console.log('收到消息:', msg);
bus.on('message', callback);
bus.emit('message', 'Hello World'); // 收到消息:Hello World
bus.off('message', callback);
bus.emit('message', 'Hello Again'); // 无输出(已取消订阅)
bus.once('onceEvent', () => console.log('一次性事件'));
bus.emit('onceEvent'); // 一次性事件
bus.emit('onceEvent'); // 无输出(已自动取消)
24. 实现链表反转(单链表)
考察点:链表数据结构、指针操作、递归/迭代思维
// 1. 定义单链表节点
class ListNode {
constructor(val = 0, next = null) {
this.val = val;
this.next = next;
}
}
// 解法1:迭代法(推荐,空间复杂度O(1))
function reverseList(head) {
let prev = null; // 前驱节点
let curr = head; // 当前节点
while (curr !== null) {
const next = curr.next; // 保存下一个节点
curr.next = prev; // 反转当前节点的指针
prev = curr; // 前驱节点后移
curr = next; // 当前节点后移
}
return prev; // 反转后,prev是新的头节点
}
// 解法2:递归法(易理解,空间复杂度O(n))
function reverseListRecursive(head) {
// 终止条件:空节点或只有一个节点,直接返回
if (head === null || head.next === null) return head;
// 递归反转后续节点
const newHead = reverseListRecursive(head.next);
// 反转当前节点和下一个节点的指针
head.next.next = head;
head.next = null; // 避免循环
return newHead;
}
// 测试
const head = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversedHead = reverseList(head);
// 遍历反转后的链表:3 -> 2 -> 1
let curr = reversedHead;
while (curr) {
console.log(curr.val); // 3 2 1
curr = curr.next;
}
25. 判断回文链表
考察点:链表操作、双指针、回文判断
// 题目:判断一个单链表是否是回文(正读和反读一样)
// 步骤:1. 找到链表中点;2. 反转后半部分;3. 对比前半部分和反转后的后半部分
function isPalindrome(head) {
if (head === null || head.next === null) return true; // 空链表或单个节点,是回文
// 1. 找到链表中点(慢指针走1步,快指针走2步,快指针到末尾时,慢指针到中点)
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转后半部分链表(从slow.next开始)
let prev = null;
let curr = slow.next;
while (curr !== null) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
slow.next = prev; // 反转后的后半部分链表,头节点是prev
// 3. 对比前半部分和反转后的后半部分
let left = head;
let right = prev;
while (right !== null) {
if (left.val !== right.val) return false; // 不相等,不是回文
left = left.next;
right = right.next;
}
return true; // 全部相等,是回文
}
// 测试
const head1 = new ListNode(1, new ListNode(2, new ListNode(1)));
console.log(isPalindrome(head1)); // true
const head2 = new ListNode(1, new ListNode(2, new ListNode(3)));
console.log(isPalindrome(head2)); // false
26. 二叉树的前序遍历(递归+迭代)
考察点:二叉树数据结构、遍历算法、递归/迭代思维
// 1. 定义二叉树节点
class TreeNode {
constructor(val = 0, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
// 解法1:递归法(简洁,易理解)
function preorderTraversalRecursive(root, result = []) {
if (root === null) return result;
result.push(root.val); // 根节点
preorderTraversalRecursive(root.left, result); // 左子树
preorderTraversalRecursive(root.right, result); // 右子树
return result;
}
// 解法2:迭代法(面试常考,避免递归栈溢出)
function preorderTraversalIterative(root) {
const result = [];
if (root === null) return result;
const stack = [root]; // 用栈存储节点
while (stack.length > 0) {
const node = stack.pop(); // 弹出栈顶节点(根节点)
result.push(node.val);
// 注意:栈是先进后出,所以先压右子树,再压左子树
if (node.right !== null) stack.push(node.right);
if (node.left !== null) stack.push(node.left);
}
return result;
}
// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(preorderTraversalRecursive(root)); // [1,2,3]
console.log(preorderTraversalIterative(root)); // [1,2,3]
27. 二叉树的中序遍历(递归+迭代)
考察点:二叉树遍历、栈的应用
// 解法1:递归法
function inorderTraversalRecursive(root, result = []) {
if (root === null) return result;
inorderTraversalRecursive(root.left, result); // 左子树
result.push(root.val); // 根节点
inorderTraversalRecursive(root.right, result); // 右子树
return result;
}
// 解法2:迭代法
function inorderTraversalIterative(root) {
const result = [];
const stack = [];
let curr = root;
while (curr !== null || stack.length > 0) {
// 先遍历左子树,所有左节点入栈
while (curr !== null) {
stack.push(curr);
curr = curr.left;
}
// 弹出栈顶节点(左子树最底层节点),加入结果
curr = stack.pop();
result.push(curr.val);
// 遍历右子树
curr = curr.right;
}
return result;
}
// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(inorderTraversalRecursive(root)); // [1,3,2]
console.log(inorderTraversalIterative(root)); // [1,3,2]
28. 斐波那契数列(递归+迭代+优化)
考察点:递归、动态规划、时间/空间复杂度优化
// 题目:求第n个斐波那契数(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2))
// 解法1:递归法(简单但效率低,时间复杂度O(2ⁿ),有重复计算)
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// 解法2:迭代法(推荐,时间复杂度O(n),空间复杂度O(1))
function fibIterative(n) {
if (n <= 1) return n;
let prevPrev = 0; // F(n-2)
let prev = 1; // F(n-1)
let curr = 0;
for (let i = 2; i <= n; i++) {
curr = prevPrev + prev; // F(n) = F(n-2) + F(n-1)
prevPrev = prev;
prev = curr;
}
return curr;
}
// 解法3:动态规划(空间复杂度O(n),适合需要保存所有斐波那契数的场景)
function fibDP(n) {
if (n <= 1) return n;
const dp = new Array(n + 1);
dp[0] = 0;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// 测试
console.log(fibIterative(10)); // 55
29. 最长公共前缀
考察点:字符串操作、遍历对比、边界处理
// 题目:编写一个函数,找出字符串数组中的最长公共前缀
function longestCommonPrefix(strs) {
// 边界判断:数组为空,返回空字符串
if (strs.length === 0) return '';
// 以第一个字符串为基准,逐个字符对比
let prefix = strs[0];
for (let i = 1; i < strs.length; i++) {
// 循环对比当前字符串和基准字符串,直到找到公共前缀
while (strs[i].indexOf(prefix) !== 0) {
// 若不匹配,缩短基准字符串(去掉最后一个字符)
prefix = prefix.slice(0, prefix.length - 1);
// 若基准字符串为空,说明没有公共前缀,直接返回
if (prefix === '') return '';
}
}
return prefix;
}
// 测试
console.log(longestCommonPrefix(["flower","flow","flight"])); // "fl"
console.log(longestCommonPrefix(["dog","racecar","car"])); // ""
30. 验证回文串
考察点:字符串处理、正则表达式、双指针
// 题目:验证一个字符串是否是回文串(只考虑字母和数字,忽略大小写)
function isPalindromeStr(s) {
// 1. 过滤无效字符(只保留字母和数字),并转为小写
const validStr = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
// 2. 双指针:左指针从开头,右指针从末尾,逐步对比
let left = 0;
let right = validStr.length - 1;
while (left < right) {
if (validStr[left] !== validStr[right]) {
return false; // 不相等,不是回文串
}
left++;
right--;
}
return true; // 全部相等,是回文串
}
// 测试
console.log(isPalindromeStr("A man, a plan, a canal: Panama")); // true
console.log(isPalindromeStr("race a car")); // false
写在最后
这30个手写JS算法,覆盖了前端面试从基础到进阶的所有高频考点——基础语法、数组操作、原型作用域、数据结构、算法思维,每道题都能直接复制到编辑器调试,吃透这30道题,面试时再遇到手写算法题,就能从容应对。
很多前端同学觉得算法难,其实是没找对方法:不用刷上千道题,重点吃透这些高频题,搞懂每道题的原理和考点,比盲目刷题更有效。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!