1. JavaScript基础与进阶
基础语法
-
数据类型判断
console.log(typeof null); console.log(typeof undefined); console.log(typeof []); console.log(typeof {}); console.log(typeof NaN);
答案:
"object" "undefined" "object" "object" "number"
判断数组最佳方法:
Array.isArray([])
或Object.prototype.toString.call([]) === '[object Array]'
-
隐式类型转换
console.log(1 + "2"); console.log("2" - 1); console.log(1 == "1"); console.log(1 === "1"); console.log([] == false); console.log({} + []);
答案:
"12" (+运算符有字符串则进行字符串拼接) 1 (-运算符将字符串转为数字) true (==比较会进行类型转换) false (===比较不会进行类型转换) true ([]转为空字符串,然后转为0,false也转为0) "[object Object]" ({}被当作代码块,+[]触发字符串转换)
-
变量提升
console.log(a); var a = 2; foo(); function foo() { console.log("foo"); } bar(); var bar = function() { console.log("bar"); }
答案:
undefined (变量a声明提升但赋值不提升) "foo" (函数声明整体提升) TypeError: bar is not a function (函数表达式只提升变量名,此时bar是undefined)
作用域与闭包
-
作用域链
var name = "全局"; function outer() { var name = "外层"; function inner() { console.log(name); } inner(); } outer();
答案:
"外层"
- inner函数在查找变量时,首先在自身作用域找,没找到则向上级作用域查找。 -
闭包陷阱
var fns = []; for (var i = 0; i < 5; i++) { fns[i] = function() { console.log(i); }; } fns[2]();
答案:
5
- var声明没有块级作用域,循环结束后i为5。修改方案:
var fns = []; for (var i = 0; i < 5; i++) { fns[i] = (function(j) { return function() { console.log(j); }; })(i); } fns[2](); // 输出2 // 或使用let(更简洁) let fns = []; for (let i = 0; i < 5; i++) { fns[i] = function() { console.log(i); }; } fns[2](); // 输出2
-
this指向
const user = { name: "张三", sayName: function() { console.log(this.name); }, sayNameArrow: () => { console.log(this.name); }, friends: { name: "李四", sayName: function() { console.log(this.name); } } }; user.sayName(); user.sayNameArrow(); user.friends.sayName(); const fn = user.sayName; fn();
答案:
"张三" (this指向调用者user) undefined (箭头函数this指向定义时的外部作用域,此处为全局) "李四" (this指向调用者friends) undefined (非严格模式下全局对象没有name属性)
原型与继承
-
原型链判断
function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log(this.name); }; const p1 = new Person("张三"); console.log(p1.__proto__ === Person.prototype); console.log(Person.prototype.__proto__ === Object.prototype); console.log(Person.__proto__ === Function.prototype); console.log(Object.__proto__ === Function.prototype);
答案:
true (实例的__proto__指向构造函数的prototype) true (构造函数的prototype是对象,其__proto__指向Object.prototype) true (Person是函数,函数的__proto__指向Function.prototype) true (Object作为函数,其__proto__也指向Function.prototype)
-
继承实现
function inherit(Child, Parent) { // Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }
-
Class实现
// class Person { constructor(name, age) { this.name = name; this.age = age; } sayHello() { console.log(`你好,我是${this.name}`); } static create(name, age) { return new Person(name, age); } }
异步编程
-
事件循环
console.log(1); setTimeout(() => { console.log(2); Promise.resolve().then(() => console.log(3)); }, 0); Promise.resolve().then(() => { console.log(4); setTimeout(() => console.log(5), 0); }); console.log(6);
答案:
1 (同步代码) 6 (同步代码) 4 (微任务队列中的Promise) 2 (宏任务队列中的setTimeout) 3 (2之后产生的微任务) 5 (4之后产生的宏任务)
-
Promise链式调用
Promise.resolve() .then(() => { console.log(1); throw new Error("error"); }) .then(() => { console.log(2); }) .catch(() => { console.log(3); }) .then(() => { console.log(4); });
答案:
1 3 (捕获到前面抛出的错误) 4 (catch后的then仍会执行)
-
async/await实现
// function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 使用示例 async function test() { console.log("start"); await sleep(1000); console.log("end after 1s"); }
ES6+特性
-
解构赋值
const obj = { a: 1, b: { c: 2, d: [3, 4] } }; // const { a, b: { c = 5 } } = obj; console.log(a, c); // 1, 2
-
Set与Map
// 使用Set实现数组去重 const arr = [1, 2, 2, 3, 3, 4]; // const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, 3, 4] // 使用Map实现缓存函数 function cached(fn) { // const cache = new Map(); return function(arg) { if (cache.has(arg)) { return cache.get(arg); } const result = fn(arg); cache.set(arg, result); return result; }; }
-
Proxy应用
// const obj = new Proxy({}, { get(target, property) { return property in target ? target[property] : "属性不存在"; }, set(target, property, value) { if (typeof value === 'number' && value < 0) { throw new Error("不能设置负数"); } target[property] = value; return true; } });
函数式编程
-
纯函数
// 答案分析 function add(a, b) { return a + b; } // 纯函数:输入相同则输出相同,无副作用 function random(min, max) { return Math.floor(Math.random() * (max - min)) + min; } // 非纯函数:输入相同输出不同,依赖外部Math.random let count = 0; function increment() { return count++; } // 非纯函数:修改了外部变量,有副作用
-
柯里化实现
function add(a, b) { return a + b; } // function curry(fn) { return function(a) { return function(b) { return fn(a, b); }; }; } // 通用版本 function curriedAdd(a) { return function(b) { return a + b; }; } console.log(curry(add)(1)(2)); // 3 console.log(curriedAdd(1)(2)); // 3
-
函数组合
// function compose(...fns) { return function(x) { return fns.reduceRight((value, fn) => fn(value), x); }; } // 使用示例 const add1 = x => x + 1; const mul2 = x => x * 2; const div3 = x => x / 3; const compute = compose(div3, mul2, add1); console.log(compute(3)); // ((3 + 1) * 2) / 3 = 2.67
设计模式
-
单例模式
// class Storage { static instance = null; constructor() { if (Storage.instance) { return Storage.instance; } this.data = {}; Storage.instance = this; } setItem(key, value) { this.data[key] = value; } getItem(key) { return this.data[key]; } } // 测试 const s1 = new Storage(); const s2 = new Storage(); console.log(s1 === s2); // true
-
观察者模式
// class EventEmitter { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); return this; } emit(event, ...args) { if (this.events[event]) { this.events[event].forEach(cb => cb(...args)); } return this; } off(event, callback) { if (this.events[event]) { if (callback) { this.events[event] = this.events[event].filter(cb => cb !== callback); } else { delete this.events[event]; } } return this; } }
-
工厂模式
// class ProductFactory { createProduct(type) { switch (type) { case 'A': return new ProductA(); case 'B': return new ProductB(); case 'C': return new ProductC(); default: throw new Error('无效产品类型'); } } } class ProductA { constructor() { this.name = '产品A'; } } class ProductB { constructor() { this.name = '产品B'; } } class ProductC { constructor() { this.name = '产品C'; } }
模块化
-
CommonJS与ESM
// CommonJS // module.js const data = { name: '张三', age: 25 }; module.exports = data; // 导入 const data = require('./module.js'); // ESM // module.js const data = { name: '张三', age: 25 }; export default data; // 导入 import data from './module.js';
-
循环依赖
// 答案分析 // CommonJS模块循环引用时,导出的是模块的不完整副本 // a.js输出:b: {}(空对象,因为b.js的导出尚未完成) // b.js输出:a: {name: 'a', say: [Function: say]}(a模块已部分加载) // 解决方法:重构代码避免循环依赖,或将公共部分提取到第三个模块
-
动态导入
// - 使用ESM动态导入 function loadModule(condition) { if (condition) { return import('./moduleA.js') .then(module => module.default); } else { return import('./moduleB.js') .then(module => module.default); } } // async/await写法 async function loadModule(condition) { if (condition) { const module = await import('./moduleA.js'); return module.default; } else { const module = await import('./moduleB.js'); return module.default; } }
2. TypeScript
基础类型系统
-
接口与类型别名区别
// 问:以下代码是否正确?为什么? interface User { name: string; } interface User { age: number; } type Animal = { species: string; } type Animal = { age: number; }
答案:
第一部分正确,第二部分错误。 接口(interface)可以合并声明,相同名称的接口会自动合并。 类型别名(type)不可重复声明,会报错"重复标识符"。
-
泛型应用
// 问:编写一个泛型函数getProperty,从对象中安全地获取属性值 function getProperty<T, K>(obj: T, key: K): _______ { return obj[key]; }
答案:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
-
枚举特性
// 问:以下代码输出什么? enum Direction { Up, Down, Left, Right } enum Status { Success = "SUCCESS", Fail = "FAIL" } console.log(Direction.Up, Direction[0], Status.Success);
答案:
0 "Up" "SUCCESS" 数字枚举是双向映射的,可通过值获取键。 字符串枚举只能从键到值单向映射。
高级类型
-
联合类型与交叉类型
// 问:以下代码中T1和T2的类型分别是什么? interface A { a: string; c: boolean; } interface B { b: number; c: string; } type T1 = A | B; type T2 = A & B; const x: T1 = { a: "hello", c: true }; const y: T2 = { a: "hello", b: 42, c: _______ };
答案:
T1是联合类型,表示要么是A类型要么是B类型,必须满足其中一个接口的所有要求 T2是交叉类型,表示同时是A类型和B类型,必须同时满足两个接口的所有要求 y中的c不能被赋值,因为在A中c是boolean,在B中c是string,交叉后产生never类型
-
映射类型应用
// 问:实现一个类型Readonly,使对象所有属性变为只读 type MyReadonly<T> = _______ interface Person { name: string; age: number; } const p: MyReadonly<Person> = { name: "张三", age: 30 }; p.name = "李四"; // 这行应该报错
答案:
type MyReadonly<T> = { readonly [K in keyof T]: T[K]; }
-
索引类型查询
// 问:以下代码输出什么? interface Person { name: string; age: number; address: { city: string; code: number; } } type T1 = keyof Person; type T2 = Person["address"]; type T3 = Person["address"]["city"]; // T1: _______ // T2: _______ // T3: _______
答案:
T1: "name" | "age" | "address" T2: { city: string; code: number; } T3: string
类型守卫
-
类型守卫应用
// 问:完善函数,对不同类型做不同处理 interface Fish { swim(): void; } interface Bird { fly(): void; } function move(animal: Fish | Bird) { if (_______ animal _______) { animal.swim(); } else { animal.fly(); } }
答案:
if ("swim" in animal) { animal.swim(); } // 或使用自定义类型守卫 function isFish(animal: Fish | Bird): animal is Fish { return (animal as Fish).swim !== undefined; } if (isFish(animal)) { animal.swim(); }
-
类型断言
// 问:下面的代码有什么问题?如何修复? const value: unknown = "Hello World"; const length: number = value.length;
答案:
// 问题:unknown类型不能直接访问属性 // 修复方式1:类型断言 const length: number = (value as string).length; // 修复方式2:类型守卫 if (typeof value === "string") { const length: number = value.length; }
装饰器
-
类装饰器
// 问:完成一个日志类装饰器,记录类的创建 function _______(constructor: Function) { console.log(`${constructor.name}类已创建`); } @_______ class Person { constructor(public name: string) {} } new Person("张三");
答案:
function Logger(constructor: Function) { console.log(`${constructor.name}类已创建`); }
-
方法装饰器
// 问:实现一个计时装饰器,记录方法执行时间 function measure(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function(...args: any[]) { _______ const result = original.apply(this, args); _______ return result; } return descriptor; }
答案:
function measure(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function(...args: any[]) { const start = performance.now(); const result = original.apply(this, args); console.log(`${key} 执行时间: ${performance.now() - start}ms`); return result; } return descriptor; }
类型推断与兼容性
-
结构类型系统
// 问:以下代码能否正常运行?为什么? interface Named { name: string; } class Person { name: string; constructor(name: string) { this.name = name; } } let p: Named; p = new Person("张三");
答案:
可以正常运行。 TypeScript使用结构类型系统,只要对象包含接口要求的所有属性且类型兼容, 不管实际类型是什么,都认为该对象实现了该接口。
-
协变与逆变
// 问:以下代码是否有类型错误? // 假设Cat是Animal的子类型 class Animal { eat(): void {} } class Cat extends Animal { meow(): void {} } // 例1 let animals: Animal[] = []; let cats: Cat[] = []; animals = cats; // 例2 type AnimalFn = (a: Animal) => void; type CatFn = (c: Cat) => void; let animalFunc: AnimalFn = (a) => {}; let catFunc: CatFn = (c) => {}; catFunc = animalFunc;
答案:
例1: 正确,数组类型是协变的,Cat[]可赋值给Animal[] 例2: 错误,函数参数是逆变的,AnimalFn不能赋值给CatFn 协变: 子类型关系保持一致 (如果Cat是Animal的子类,那么Cat[]是Animal[]的子类) 逆变: 子类型关系反转 (如果Cat是Animal的子类,那么(Animal)=>void是(Cat)=>void的子类)
tsconfig配置
-
编译选项
// 问:如何配置tsconfig.json使项目支持以下特性: // 1. 严格空值检查 // 2. 支持装饰器 // 3. 生成sourcemap
答案:
{ "compilerOptions": { "strictNullChecks": true, "experimentalDecorators": true, "sourceMap": true } }
-
模块解析策略
// 问:什么是模块解析?如何在tsconfig.json中设置Node模块解析策略?
答案:
模块解析是TypeScript确定import语句实际引用什么模块的过程。 在tsconfig.json中设置Node模块解析策略: { "compilerOptions": { "moduleResolution": "node" } } 这允许TypeScript使用Node.js方式解析模块(检查node_modules等)。
声明文件
-
声明文件编写
// 问:为以下JavaScript库编写一个声明文件 // calculator.js // const calculator = { // add(a, b) { return a + b; }, // subtract(a, b) { return a - b; } // }; // export default calculator;
答案:
// calculator.d.ts declare namespace calculator { function add(a: number, b: number): number; function subtract(a: number, b: number): number; } export default calculator;
-
第三方库声明
// 问:如何安装和使用第三方库jQuery的类型声明?
答案:
安装jQuery类型声明: npm install --save-dev @types/jquery 使用方式: import * as $ from 'jquery'; // 现在可以使用带有类型提示的jQuery API $(document).ready(() => { $('#element').text('Hello'); });
3. 算法与数据结构
一、JavaScript实现常见数据结构
1. 链表
题目: 实现单链表及其基本操作(增删查)
答案:
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
// 添加节点到末尾
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
}
// 在指定位置插入
insert(value, index) {
if (index < 0 || index > this.size) return false;
const newNode = new Node(value);
if (index === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let current = this.head;
let previous = null;
let count = 0;
while (count < index) {
previous = current;
current = current.next;
count++;
}
newNode.next = current;
previous.next = newNode;
}
this.size++;
return true;
}
// 删除指定位置节点
removeAt(index) {
if (index < 0 || index >= this.size) return null;
let current = this.head;
if (index === 0) {
this.head = current.next;
} else {
let previous = null;
let count = 0;
while (count < index) {
previous = current;
current = current.next;
count++;
}
previous.next = current.next;
}
this.size--;
return current.value;
}
// 获取指定位置节点的值
getAt(index) {
if (index < 0 || index >= this.size) return null;
let current = this.head;
let count = 0;
while (count < index) {
current = current.next;
count++;
}
return current.value;
}
}
思路:
- 链表由节点组成,每个节点包含值和指向下一节点的指针
- 基本操作围绕维护节点间的引用关系
- 时间复杂度:查找O(n),头部插入/删除O(1),尾部操作O(n)
2. 栈
题目: 用数组实现栈结构及其操作
答案:
class Stack {
constructor() {
this.items = [];
}
// 入栈
push(element) {
this.items.push(element);
}
// 出栈
pop() {
if (this.isEmpty()) return null;
return this.items.pop();
}
// 查看栈顶元素
peek() {
if (this.isEmpty()) return null;
return this.items[this.items.length - 1];
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 获取栈的大小
size() {
return this.items.length;
}
// 清空栈
clear() {
this.items = [];
}
}
思路:
- 栈遵循后进先出(LIFO)原则
- 使用数组实现最为简单,利用push和pop方法
- 所有操作时间复杂度均为O(1)
3. 队列
题目: 实现队列及其操作
答案:
class Queue {
constructor() {
this.items = {};
this.frontIndex = 0;
this.backIndex = 0;
}
// 入队
enqueue(element) {
this.items[this.backIndex] = element;
this.backIndex++;
}
// 出队
dequeue() {
if (this.isEmpty()) return null;
const item = this.items[this.frontIndex];
delete this.items[this.frontIndex];
this.frontIndex++;
return item;
}
// 查看队首元素
front() {
if (this.isEmpty()) return null;
return this.items[this.frontIndex];
}
// 判断队列是否为空
isEmpty() {
return this.backIndex - this.frontIndex === 0;
}
// 获取队列长度
size() {
return this.backIndex - this.frontIndex;
}
}
思路:
- 队列遵循先进先出(FIFO)原则
- 使用对象而非数组实现,避免出队时的数组位移开销
- 维护前后指针,确保O(1)时间复杂度
4. 二叉树
题目: 实现二叉树的创建和遍历
答案:
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BinaryTree {
constructor() {
this.root = null;
}
// 前序遍历
preOrderTraversal(node = this.root) {
if (!node) return [];
const result = [];
result.push(node.value);
result.push(...this.preOrderTraversal(node.left));
result.push(...this.preOrderTraversal(node.right));
return result;
}
// 中序遍历
inOrderTraversal(node = this.root) {
if (!node) return [];
const result = [];
result.push(...this.inOrderTraversal(node.left));
result.push(node.value);
result.push(...this.inOrderTraversal(node.right));
return result;
}
// 后序遍历
postOrderTraversal(node = this.root) {
if (!node) return [];
const result = [];
result.push(...this.postOrderTraversal(node.left));
result.push(...this.postOrderTraversal(node.right));
result.push(node.value);
return result;
}
// 层序遍历
levelOrderTraversal() {
if (!this.root) return [];
const result = [];
const queue = [this.root];
while (queue.length) {
const node = queue.shift();
result.push(node.value);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return result;
}
}
思路:
- 二叉树由节点组成,每个节点最多有两个子节点
- 遍历方式:前序(根-左-右),中序(左-根-右),后序(左-右-根),层序(按层)
- 递归实现简洁优雅,层序遍历使用队列辅助实现
5. 图
题目: 实现图结构及其遍历
答案:
class Graph {
constructor() {
this.vertices = {};
}
// 添加顶点
addVertex(vertex) {
if (!this.vertices[vertex]) {
this.vertices[vertex] = [];
}
}
// 添加边
addEdge(v1, v2) {
if (!this.vertices[v1]) this.addVertex(v1);
if (!this.vertices[v2]) this.addVertex(v2);
this.vertices[v1].push(v2);
this.vertices[v2].push(v1); // 无向图
}
// 深度优先遍历
dfs(startVertex) {
const result = [];
const visited = {};
const dfsHelper = (vertex) => {
if (!vertex) return null;
visited[vertex] = true;
result.push(vertex);
this.vertices[vertex].forEach(neighbor => {
if (!visited[neighbor]) {
dfsHelper(neighbor);
}
});
};
dfsHelper(startVertex);
return result;
}
// 广度优先遍历
bfs(startVertex) {
const result = [];
const visited = {};
const queue = [startVertex];
visited[startVertex] = true;
while (queue.length) {
const currentVertex = queue.shift();
result.push(currentVertex);
this.vertices[currentVertex].forEach(neighbor => {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.push(neighbor);
}
});
}
return result;
}
}
思路:
- 图由顶点和边组成,可用邻接表(对象+数组)实现
- DFS使用递归或栈实现,探索尽可能深的路径
- BFS使用队列实现,按层次探索,适合寻找最短路径
二、排序算法
1. 快速排序
题目: 实现快速排序算法
答案:
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const middle = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else if (arr[i] > pivot) {
right.push(arr[i]);
} else {
middle.push(arr[i]);
}
}
return [...quickSort(left), ...middle, ...quickSort(right)];
}
思路:
- 选择基准值(pivot),将数组分为三部分:小于、等于和大于基准值
- 递归地对左右两部分进行快速排序
- 平均时间复杂度O(nlogn),最坏情况O(n²)
2. 归并排序
题目: 实现归并排序算法
答案:
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const result = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] < right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
思路:
- 分治思想:将数组分为两半,分别排序,再合并
- 合并过程比较两个有序数组的元素,按序合并
- 时间复杂度稳定在O(nlogn),空间复杂度O(n)
3. 堆排序
题目: 实现堆排序算法
答案:
function heapSort(arr) {
const n = arr.length;
// 构建最大堆
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 从堆顶取出最大元素,放到数组末尾
for (let i = n - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]]; // 交换
heapify(arr, i, 0); // 重新调整堆
}
return arr;
}
function heapify(arr, n, i) {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
// 如果左子节点大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点大于当前最大值
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点,交换并继续堆化
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest);
}
}
思路:
- 将数组视为完全二叉树,构建最大堆
- 不断取出堆顶(最大值),调整剩余元素为新堆
- 时间复杂度O(nlogn),空间复杂度O(1)
三、搜索算法
1. 二分查找
题目: 实现二分查找算法
答案:
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
思路:
- 要求数组已排序
- 每次比较中间元素,缩小查找范围一半
- 时间复杂度O(logn),空间复杂度O(1)
2. 深度优先搜索(DFS)
题目: 使用DFS遍历二叉树
答案:
function dfs(root) {
const result = [];
function traverse(node) {
if (!node) return;
// 前序位置
result.push(node.value);
traverse(node.left);
// 中序位置
traverse(node.right);
// 后序位置
}
traverse(root);
return result;
}
// 非递归实现
function dfsIterative(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop();
result.push(node.value);
// 先放右节点,后放左节点,确保左节点先被访问
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return result;
}
思路:
- 沿着一个方向探索到底,然后回溯
- 可递归实现(简洁)或使用栈实现(节省调用栈)
- 适合搜索所有可能解,寻找连通性
3. 广度优先搜索(BFS)
题目: 使用BFS遍历二叉树
答案:
function bfs(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length) {
const node = queue.shift();
result.push(node.value);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return result;
}
思路:
- 按层次访问节点,使用队列实现
- 先访问邻近节点,再访问较远节点
- 适合查找最短路径,层次遍历
四、动态规划与贪心
1. 斐波那契数列(动态规划)
题目: 求斐波那契数列的第n项
答案:
// 递归(效率低)
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// 动态规划
function fibDP(n) {
if (n <= 1) return n;
let dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// 空间优化
function fibOptimized(n) {
if (n <= 1) return n;
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
const next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
思路:
- 递归解法有大量重复计算,时间复杂度O(2^n)
- 动态规划自下而上计算,避免重复计算,时间复杂度O(n)
- 空间优化版本只存储必要状态,空间复杂度O(1)
2. 背包问题(动态规划)
题目: 01背包问题:有n个物品,每个物品有重量w和价值v,背包容量为W,求最大价值
答案:
function knapsack(weights, values, capacity) {
const n = weights.length;
// dp[i][j]表示前i个物品,容量为j的最大价值
const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= capacity; j++) {
if (weights[i - 1] <= j) {
// 可以放入第i个物品
dp[i][j] = Math.max(
dp[i - 1][j], // 不放入
dp[i - 1][j - weights[i - 1]] + values[i - 1] // 放入
);
} else {
// 不能放入第i个物品
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][capacity];
}
思路:
- 定义状态:dp[i][j]表示前i个物品,容量为j时的最大价值
- 状态转移:每个物品有放与不放两种选择
- 时间复杂度O(nW),空间复杂度O(nW)
3. 找零钱问题(贪心)
题目: 给定不同面额的硬币和一个总金额,求所需最少的硬币个数
答案:
// 贪心算法(仅适用于特定币值系统,如美元)
function coinChangeGreedy(coins, amount) {
// 确保coins按降序排列
coins.sort((a, b) => b - a);
let count = 0;
let remaining = amount;
for (const coin of coins) {
const numCoins = Math.floor(remaining / coin);
count += numCoins;
remaining -= numCoins * coin;
}
return remaining === 0 ? count : -1;
}
// 动态规划(通用解)
function coinChangeDP(coins, amount) {
// dp[i]表示金额i所需的最少硬币数
const dp = Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
思路:
- 贪心:每次选择最大面额的硬币(只适用于某些特定币值系统)
- 动态规划:dp[i]表示金额i所需的最少硬币数
- 贪心时间复杂度O(n),动态规划时间复杂度O(n*amount)
五、字符串处理
1. 正则表达式匹配
题目: 验证邮箱格式是否合法
答案:
function validateEmail(email) {
const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return regex.test(email);
}
思路:
- 正则表达式描述字符串的模式
^
和$
表示开始和结束[]
表示字符集,+
表示一个或多个{2,}
表示至少2个字符
2. KMP字符串匹配算法
题目: 实现KMP算法查找模式串在文本串中的位置
答案:
function kmpSearch(text, pattern) {
if (pattern.length === 0) return 0;
// 计算前缀表(部分匹配表)
const lps = computeLPSArray(pattern);
let i = 0; // 文本指针
let j = 0; // 模式指针
const results = []; // 存储所有匹配位置
while (i < text.length) {
if (pattern[j] === text[i]) {
i++;
j++;
}
if (j === pattern.length) {
// 找到一个匹配
results.push(i - j);
j = lps[j - 1]; // 寻找下一个匹配
} else if (i < text.length && pattern[j] !== text[i]) {
if (j !== 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
return results;
}
function computeLPSArray(pattern) {
const lps = [0]; // 首字符的最长公共前后缀长度为0
let len = 0; // 当前已匹配的长度
let i = 1;
while (i < pattern.length) {
if (pattern[i] === pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len !== 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
思路:
- KMP算法避免不必要的字符比较,利用已知信息
- 构建部分匹配表(lps数组),记录最长公共前后缀长度
- 失配时,根据lps数组回退到合适位置继续匹配
- 时间复杂度O(n+m),其中n和m分别是文本串和模式串长度
六、前端算法应用
1. 虚拟DOM的Diff算法
题目: 简化版虚拟DOM的Diff算法实现
答案:
function diff(oldVNode, newVNode) {
// 如果节点类型不同,直接替换
if (oldVNode.type !== newVNode.type) {
return {
type: 'REPLACE',
newVNode
};
}
// 如果是文本节点且内容不同
if (!oldVNode.children && !newVNode.children) {
if (oldVNode.text !== newVNode.text) {
return {
type: 'TEXT',
text: newVNode.text
};
}
return null; // 无变化
}
// 如果老节点无子节点,新节点有子节点
if (!oldVNode.children) {
return {
type: 'REPLACE',
newVNode
};
}
// 如果新节点无子节点,老节点有子节点
if (!newVNode.children) {
return {
type: 'REPLACE',
newVNode
};
}
// 子节点对比
const patches = [];
const len = Math.max(oldVNode.children.length, newVNode.children.length);
for (let i = 0; i < len; i++) {
// 新增节点
if (i >= oldVNode.children.length) {
patches.push({
type: 'ADD',
node: newVNode.children[i],
index: i
});
continue;
}
// 删除节点
if (i >= newVNode.children.length) {
patches.push({
type: 'REMOVE',
index: i
});
continue;
}
// 递归对比
const childPatch = diff(oldVNode.children[i], newVNode.children[i]);
if (childPatch) {
patches.push({
type: 'PATCH',
patch: childPatch,
index: i
});
}
}
if (patches.length === 0) return null;
return {
type: 'CHILDREN',
patches
};
}
思路:
- 比较两棵树的差异,生成最小操作集(补丁)
- 节点类型不同时整体替换,相同时递归比较子节点
- 采用深度优先遍历,对比过程自顶向下
- 优化策略:列表节点使用key进行优化(简化版未实现)
2. 状态管理设计
题目: 实现简易版发布-订阅模式(类似Redux)
答案:
class Store {
constructor(reducer, initialState = {}) {
this.reducer = reducer;
this.state = initialState;
this.listeners = [];
}
// 获取状态
getState() {
return this.state;
}
// 订阅状态变化
subscribe(listener) {
this.listeners.push(listener);
// 返回取消订阅函数
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
// 发送action更新状态
dispatch(action) {
this.state = this.reducer(this.state, action);
// 通知所有监听器
this.listeners.forEach(listener => listener());
return action;
}
}
// 使用示例
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
const store = new Store(counterReducer, { count: 0 });
// 订阅状态变化
const unsubscribe = store.subscribe(() => {
console.log('状态更新:', store.getState());
});
// 发送action
store.dispatch({ type: 'INCREMENT' }); // 状态更新: { count: 1 }
store.dispatch({ type: 'INCREMENT' }); // 状态更新: { count: 2 }
store.dispatch({ type: 'DECREMENT' }); // 状态更新: { count: 1 }
// 取消订阅
unsubscribe();
思路:
- 发布-订阅模式:组件订阅状态变化,状态变化时通知组件更新
- 单一数据源:整个应用的状态存储在单个对象中
- 状态只读:唯一改变状态的方式是发送action
- 使用纯函数reducer来执行状态更新