webDevInterviewQuestions/algorithm/03-data-structure.md
2026-01-20 17:05:59 +08:00

424 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数据结构与算法
## 1. 链表
> **场景**操作历史记录浏览器前进后退、LRU 缓存淘汰、撤销重做功能、音乐播放列表。
> **解决**:频繁插入/删除场景下数组性能差的问题O(1) 插入删除。
### 反转链表
```js
function reverseList(head) {
let prev = null, curr = head; // prev 记录前一个节点
while (curr) {
const next = curr.next; // 暂存下一个节点
curr.next = prev; // 将当前节点指向前一个(反转)
prev = curr; // prev 前移
curr = next; // curr 前移
}
return prev; // prev 就是新的头节点
}
```
### 环形链表判断
**什么是环形链表?**
环形链表是指链表中某个节点的 `next` 指针指向了链表中之前的某个节点,从而形成一个闭环。遍历这种链表时会陷入无限循环,永远无法到达 `null`
```
正常链表: 1 -> 2 -> 3 -> 4 -> null
环形链表: 1 -> 2 -> 3 -> 4
↑ ↓
└─────────┘
```
**实际应用场景:**
- **内存泄漏检测**:对象间循环引用导致垃圾回收器无法回收,需要检测环
- **死锁检测**:进程等待资源形成环形依赖时会发生死锁
- **约瑟夫环问题**n 个人围成一圈报数淘汰的经典算法问题
- **循环播放列表**:音乐/视频循环播放时最后一项指向第一项
- **游戏地图边界**:无缝世界地图从一端走到另一端
```js
// 快慢指针法:如果有环,快指针一定会追上慢指针
function hasCycle(head) {
let slow = head, fast = head; // 两指针都从头开始
while (fast?.next) { // fast 能继续走两步
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow === fast) return true; // 相遇说明有环
}
return false; // fast 走到结尾,无环
}
```
### 合并有序链表
```js
function mergeTwoLists(l1, l2) {
const dummy = { next: null }; // 哑节点,简化处理
let curr = dummy;
while (l1 && l2) { // 两个链表都有剩余节点
if (l1.val <= l2.val) {
curr.next = l1; // 取较小的节点
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next; // 移动当前指针
}
curr.next = l1 || l2; // 接上剩余部分
return dummy.next; // 返回哑节点的下一个即真正头节点
}
```
---
## 2. 二叉树
> **场景**DOM 树遍历、文件目录结构、组织架构图、表达式解析、前端路由匹配。
> **解决**:层级数据的存储与高效查找,遍历、搜索与路径问题。
### 遍历(前中后序)
```js
// 前序遍历:根 -> 左 -> 右(先访问根节点)
const preorder = (root, res = []) => {
if (!root) return res;
res.push(root.val); // 访问根
preorder(root.left, res); // 递归左子树
preorder(root.right, res); // 递归右子树
return res;
};
// 中序遍历:左 -> 根 -> 右(二叉搜索树得到有序数组)
const inorder = (root, res = []) => {
if (!root) return res;
inorder(root.left, res); // 先左
res.push(root.val); // 再根
inorder(root.right, res); // 后右
return res;
};
// 后序遍历:左 -> 右 -> 根(常用于删除操作)
const postorder = (root, res = []) => {
if (!root) return res;
postorder(root.left, res); // 先左
postorder(root.right, res); // 再右
res.push(root.val); // 最后根
return res;
};
```
### 求最大深度
```js
// 递归求最大深度:左右子树深度的较大值 + 1
const maxDepth = root => {
if (!root) return 0; // 空节点深度为 0
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
```
### 路径和
```js
// 判断是否存在根到叶子的路径,使得路径上所有节点值之和等于目标值
function hasPathSum(root, targetSum) {
if (!root) return false;
// 叶子节点:检查剩余值是否等于当前节点值
if (!root.left && !root.right) return targetSum === root.val;
// 递归检查左右子树,目标值减去当前节点值
return hasPathSum(root.left, targetSum - root.val)
|| hasPathSum(root.right, targetSum - root.val);
}
```
---
## 3. 栈与队列
> **场景**栈用于括号匹配、撤销操作、函数调用栈队列用于任务调度、消息队列、BFS 广度优先搜索。
> **解决**:先进后出/先进先出的数据操作顺序控制。
### 用栈实现队列
```js
// 使用两个栈实现队列:入栈和出栈
class MyQueue {
constructor() {
this.inStack = []; // 入队栈
this.outStack = []; // 出队栈
}
// 入队:直接压入 inStack
push(x) { this.inStack.push(x); }
// 出队:从 outStack 取,如果空则把 inStack 全部倒入 outStack
pop() {
if (!this.outStack.length) {
while (this.inStack.length) this.outStack.push(this.inStack.pop());
}
return this.outStack.pop();
}
// 查看队首:同 pop 逻辑,但不弹出
peek() {
if (!this.outStack.length) {
while (this.inStack.length) this.outStack.push(this.inStack.pop());
}
return this.outStack[this.outStack.length - 1];
}
// 判断是否为空
empty() { return !this.inStack.length && !this.outStack.length; }
}
```
### 有效的括号
```js
function isValid(s) {
// 右括号到左括号的映射
const map = { ')': '(', ']': '[', '}': '{' };
const stack = [];
for (const c of s) {
if (map[c]) { // 如果是右括号
// 栈顶必须是对应的左括号
if (stack.pop() !== map[c]) return false;
} else {
stack.push(c); // 左括号入栈
}
}
return !stack.length; // 栈必须为空才有效
}
```
---
## 4. 哈希表
> **场景**快速查找用户ID查询、统计词频、数据去重、分组操作。
> **解决**O(1) 时间复杂度的键值对存储和查找,避免线性遍历。
### 两数之和
```js
// 用哈希表存储已遍历的数字及其索引
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const diff = target - nums[i]; // 计算需要的配对数字
if (map.has(diff)) return [map.get(diff), i]; // 找到则返回两个索引
map.set(nums[i], i); // 存储当前数字和索引
}
return [];
}
```
### 字母异位词分组
```js
// 异位词排序后结果相同,以此为 key 分组
function groupAnagrams(strs) {
const map = new Map();
for (const s of strs) {
const key = [...s].sort().join(''); // 排序后作为 key
map.set(key, (map.get(key) || []).concat(s)); // 加入对应分组
}
return [...map.values()]; // 返回所有分组
}
```
---
## 5. 排序算法
> **场景**:商品价格/销量排序、排行榜、数据可视化预处理。
> **解决**:无序数据有序化,理解分治思想(快排、归并)是算法基础。
### 快速排序
```js
// 快速排序:分治思想,选取基准值分区
function quickSort(arr) {
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)];
}
```
### 归并排序
```js
// 归并排序:分治思想,先拆分再合并
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 i = 0, j = 0;
// 双指针比较,取较小的元素
while (i < left.length && j < right.length) {
result.push(left[i] < right[j] ? left[i++] : right[j++]);
}
// 拼接剩余部分
return result.concat(left.slice(i), right.slice(j));
}
```
---
## 6. 二分查找
> **场景**有序列表快速定位分页跳转、版本号查找、IP 地址归属地查询、猜数字游戏。
> **解决**:在有序数据中 O(log n) 时间复杂度快速查找,比线性查找效率高很多。
### 基础二分查找
```js
// 标准二分查找:在有序数组中查找目标值
function binarySearch(arr, target) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2); // 计算中间位置
if (arr[mid] === target) return mid; // 找到目标
// 根据大小关系缩小范围
arr[mid] < target ? (left = mid + 1) : (right = mid - 1);
}
return -1; // 未找到
}
```
### 旋转数组查找
```js
// 旋转数组查找:数组被旋转过,如 [4,5,6,7,0,1,2]
function searchRotated(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return mid;
// 判断哪一半是有序的
if (nums[left] <= nums[mid]) { // 左半边有序
// target 在左半边有序区间内
if (target >= nums[left] && target < nums[mid]) right = mid - 1;
else left = mid + 1;
} else { // 右半边有序
// target 在右半边有序区间内
if (target > nums[mid] && target <= nums[right]) left = mid + 1;
else right = mid - 1;
}
}
return -1;
}
```
---
## 7. 斐波那契数列
> **场景**:算法入门经典题、理解递归与动态规划思想、缓存优化演示、爬楼梯问题变种。
> **解决**:求第 n 位斐波那契数F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2))。
### 方法一:普通递归(不推荐)
```js
// 时间复杂度 O(2^n),存在大量重复计算
function fib(n) {
if (n <= 1) return n; // 基准情况F(0)=0, F(1)=1
return fib(n - 1) + fib(n - 2); // 递归调用
}
```
### 方法二:记忆化递归
```js
// 时间复杂度 O(n),空间复杂度 O(n)
function fib(n, memo = {}) {
if (n <= 1) return n;
if (memo[n]) return memo[n]; // 已计算过,直接返回缓存
return memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // 计算并缓存
}
```
### 方法三:动态规划
```js
// 时间复杂度 O(n),空间复杂度 O(n)
function fib(n) {
if (n <= 1) return n;
const dp = [0, 1]; // dp[i] 表示第 i 位斐波那契数
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
}
```
### 方法四:空间优化(推荐)
```js
// 时间复杂度 O(n),空间复杂度 O(1)
function fib(n) {
if (n <= 1) return n;
let prev = 0, curr = 1; // 只保留前两个值
for (let i = 2; i <= n; i++) {
[prev, curr] = [curr, prev + curr]; // 滚动更新
}
return curr;
}
```
### 方法五:矩阵快速幂(大数优化)
```js
// 时间复杂度 O(log n),适合求极大位数
// 原理:|F(n) | |1 1|^(n-1) |F(1)|
// |F(n-1)| = |1 0| * |F(0)|
function fib(n) {
if (n <= 1) return n;
// 2x2 矩阵乘法
const multiply = (a, b) => [
[a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]],
[a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]]
];
// 快速幂:计算 m^p
const power = (m, p) => {
let result = [[1, 0], [0, 1]]; // 单位矩阵
while (p > 0) {
if (p & 1) result = multiply(result, m); // 奇数幂则乘一次
m = multiply(m, m); // 底数平方
p >>= 1; // 指数减半
}
return result;
};
const matrix = [[1, 1], [1, 0]];
return power(matrix, n - 1)[0][0]; // 结果在左上角
}
```
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|------|-----------|-----------|----------|
| 普通递归 | O(2^n) | O(n) | 仅理解思想 |
| 记忆化递归 | O(n) | O(n) | 面试常考 |
| 动态规划 | O(n) | O(n) | 面试常考 |
| 空间优化 | O(n) | O(1) | **推荐** |
| 矩阵快速幂 | O(log n) | O(1) | 大数/竞赛 |