一句话概括:掌握「三指针」、「哨兵节点」、「快慢指针」、「头插法」和「递归遍历」五大核心技巧,轻松拿下数组合并、链表操作、二叉树遍历等高频面试题!
📌 前言:为什么这些题反复考?
在大厂算法面试中,数组和链表是最基础的数据结构,而 “合并”、“删除”、“反转”、“检测环”、“遍历” 是最常考察的操作。
它们看似简单,却能深度考察你对 指针移动、边界处理、空间优化、递归思维 的理解。
本文将结合 真实可运行的 JavaScript 代码,带你逐题拆解,掌握底层逻辑。
🔢 一、合并两个有序数组 —— 三指针 + 原地操作
💡 题目要求
给定两个升序数组 nums1(长度为 m+n,后 n 位为 0)和 nums2(长度为 n),将 nums2 合并到 nums1 中,使其仍有序。
✅ 关键洞察
- 如果从前向后合并,会覆盖
nums1中未处理的元素。 - 从后往前合并,利用
nums1末尾的空位,实现 O(1) 额外空间。
🧩 三指针策略
i:指向nums1有效元素末尾(m-1)j:指向nums2末尾(n-1)k:指向nums1最终位置(m+n-1)
✅ JS 实现
js
编辑
function merge(nums1, m, nums2, n) {
let i = m - 1;
let j = n - 1;
let k = m + n - 1;
// 从后往前比较
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
// 若 nums2 还有剩余,直接复制(nums1 剩余无需处理)
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
⏱️ 复杂度
- 时间:O(m + n)
- 空间:O(1) —— 原地合并,极致优化!
🔗 二、删除链表的倒数第 N 个节点 —— 快慢指针 + 哨兵节点
💡 为什么需要哨兵节点(dummy)?
- 删除头节点时,常规方法需特殊处理。
- 哨兵节点统一所有情况:
dummy.next = head,返回dummy.next即可。
🧩 快慢指针思路
- 快指针先走
N步 - 快慢指针同步走,当快指针到末尾(
fast.next === null),慢指针正好在 倒数第 N+1 个节点 - 执行
slow.next = slow.next.next删除目标节点
✅ JS 实现
js
编辑
const removeNthFromEnd = function (head, n) {
const dummy = new ListNode(0);
dummy.next = head;
let fast = dummy;
let slow = dummy;
// 快指针先走 n 步
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// 同步移动,直到 fast 到达最后一个节点
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
// 删除倒数第 n 个节点
slow.next = slow.next.next;
return dummy.next;
};
✅ 注意:快指针停在 最后一个节点(不是 null),这样慢指针才指向 前驱节点。
🔁 三、判断链表是否有环 —— 快慢指针(Floyd 判圈算法)
🧠 核心思想
- 快指针每次走 2 步,慢指针走 1 步
- 若有环,快指针必会追上慢指针(就像操场跑步)
- 若无环,快指针先到达
null
❌ 你代码中的错误修正
js
编辑
// 原代码有笔误:
// fast - fast.next.next; → 应为 =
// 缺少 slow 初始化
function hasCycle(head) {
if (!head) return false;
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next; // 慢指针走 1 步
fast = fast.next.next; // 快指针走 2 步 ← 修正赋值
if (slow === fast) {
return true; // 相遇即有环
}
}
return false;
}
🗑️ 四、删除链表中等于 val 的节点 —— 哨兵节点简化边界
✅ 优势
- 无需判断是否删除头节点
- 统一处理所有节点
✅ JS 实现(修正拼写)
js
编辑
function removeElements(head, val) {
const dummy = new ListNode(0); // ← 修正:ListNode 拼写
dummy.next = head;
let cur = dummy;
while (cur.next) {
if (cur.next.val === val) { // ← 修正:vall → val
cur.next = cur.next.next;
} else {
cur = cur.next; // 只有不删除时才前进
}
}
return dummy.next;
}
⚠️ 注意:不要加
break!题目要求删除所有等于val的节点。
🔄 五、反转链表 —— 哨兵节点 + 头插法
🧠 头插法三步曲
- 保存当前节点的下一个节点:
next = cur.next - 当前节点插入到已反转部分的头部:
cur.next = dummy.next - 更新新头:
dummy.next = cur - 移动到原链表下一个节点:
cur = next
✅ JS 实现(修正拼写)
js
编辑
function reverseList(head) {
const dummy = new ListNode(null);
let cur = head;
while (cur) {
const next = cur.next;
cur.next = dummy.next; // 插入到反转链表头部
dummy.next = cur; // 更新头
cur = next; // 移动
}
return dummy.next;
}
✅ 虽然比“三指针法”多一个 dummy 节点,但逻辑更直观,适合理解。
🌲 六、二叉树的四种遍历 —— 递归是灵魂
📌 二叉树节点定义
js
编辑
class TreeNode {
constructor(val, left, right) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
🔄 四种遍历的本质区别:根节点的访问时机
| 遍历方式 | 访问顺序 | 应用场景 |
|---|---|---|
| 前序 | 根 → 左 → 右 | 复制树、序列化 |
| 中序 | 左 → 根 → 右 | 二叉搜索树 → 有序输出 |
| 后序 | 左 → 右 → 根 | 删除节点、计算目录大小 |
| 层序 | 从上到下,从左到右 | 广度优先、求宽度/深度 |
✅ 递归实现(模板)
js
编辑
// 前序
function preorder(root, res = []) {
if (!root) return res;
res.push(root.val); // 根
preorder(root.left, res); // 左
preorder(root.right, res); // 右
return res;
}
// 中序
function inorder(root, res = []) {
if (!root) return res;
inorder(root.left, res);
res.push(root.val); // 根
inorder(root.right, res);
return res;
}
// 后序
function postorder(root, res = []) {
if (!root) return res;
postorder(root.left, res);
postorder(root.right, res);
res.push(root.val); // 根
return res;
}
🌐 层序遍历(BFS,用队列)
js
编辑
function levelOrder(root) {
if (!root) return [];
const queue = [root];
const res = [];
while (queue.length) {
const level = [];
const size = queue.length;
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
}
return res;
}
🎯 总结:五大核心技巧
| 技巧 | 应用场景 | 关键点 |
|---|---|---|
| 三指针 | 合并有序数组 | 从后往前,避免覆盖 |
| 哨兵节点 | 删除/反转链表 | 统一边界,无需特判头节点 |
| 快慢指针 | 判环、找中点、倒数第 N 个节点 | 速度差制造位置关系 |
| 头插法 | 链表反转 | 新节点插入到已反转部分头部 |
| 递归遍历 | 二叉树 | 根的访问时机决定遍历类型 |
💼 面试高频问题延伸
- Q:合并数组为什么不能从前向后?
A:会覆盖nums1中尚未处理的元素,导致数据丢失。 - Q:快慢指针为什么一定能相遇?
A:设环长为 L,当慢指针进入环时,快指针已在环内。相对速度为 1,最多 L 步必相遇。 - Q:中序遍历 BST 为什么是有序的?
A:BST 性质:左子树 < 根 < 右子树,中序(左→根→右)自然升序。 - Q:哨兵节点算额外空间吗?
A:算 O(1),因为只创建一个节点,与输入规模无关。
🌟 结语
这些题目看似独立,实则共享同一套 “指针思维” 和 “边界处理哲学” 。
掌握它们,不仅是为刷题,更是为了培养 清晰的逻辑抽象能力。
GitHub 地址:欢迎 Star 我的算法仓库!
在线练习:LeetCode #88, #19, #141, #206, #144/94/145
希望这篇文章助你在面试中游刃有余!如有疑问,欢迎评论区交流 😊
作者:无敌的拖拉斯旋风
标签:#JavaScript #LeetCode #算法 #链表 #二叉树 #前端面试