数组类技巧
- 滑动窗口;
- 双指针;
- 巧用排序;
- 巧用数据结构(Hash Map/Set,Stack,Priority Queue)
- 动态规划;
- 回溯;
字符串类技巧
和数组类似,唯一不同的一点是巧用各类字符串API和正则表达式;
二叉树框架
递归更为简单,但吃内存;
function func(root) {
// 先序
func(root.left);
// 中序
func(root.right);
// 后序
}
迭代较为复杂,但一般效率较高;
先序
function preorder(root) {
if (!root) return [];
const res = [];
const stack = [root];
while (stack.length) {
const node = stack.pop();
res.push(node.val);
// 栈是后进先出,先压右再压左,出栈顺序才是根、左、右
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return res;
}
中序
function inorder(root) {
const res = [];
const stack = [];
let node = root;
while (node || stack.length) {
// 一路向左,把路径上的节点压栈
while (node) {
stack.push(node);
node = node.left;
}
node = stack.pop();
res.push(node.val);
node = node.right;
}
return res;
}
后序
function postorder(root) {
if (!root) return [];
const res = [];
const stack = [root];
// 按根、右、左遍历,最后整体反转,得到左、右、根
while (stack.length) {
const node = stack.pop();
res.push(node.val);
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return res.reverse();
}
层序
function levelOrder(root) {
if (!root) return [];
const res = [];
const queue = [root];
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;
}
树框架
二叉树的题,90% 都能套进同一个递归骨架:
function traverse(root) {
if (!root) return;
// 先序:进入节点时做点什么
traverse(root.left);
// 中序:左子树处理完做点什么
traverse(root.right);
// 后序:离开节点时做点什么
}
写题时只需要回答三个问题:
- 进入节点时要做什么:记录路径、累加和、传下界/上界;
- 子节点要返回什么:深度、是否平衡、子树的和、找到的节点;
- 离开节点时要做什么:合并左右子树的结果、更新全局答案。
几个典型映射:
- 求最大深度 / 直径:后序,
1 + max(left, right),直径在“离开节点”时用left + right更新全局最大值; - 路径总和:先序,把剩余目标值传给子节点,到叶子时判断是否为 0;
- 验证 BST:先序传
(min, max)上下界,或中序遍历判断是否严格递增; - 最近公共祖先:后序,左右子树各找到一个就返回当前节点;
- 层序遍历:BFS + 队列,用
size固定当前层节点数。
// 最大深度:后序的典型
function maxDepth(root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// 最近公共祖先:后序,左右都找到就返回自己
function lowestCommonAncestor(root, p, q) {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root;
return left || right;
}
两个实践建议:
- 递归先想清楚返回值语义,再写 base case,不要在人脑里模拟整个递归栈;
- 树退化成链表时递归深度是
O(n),节点数到10^5量级要改成显式栈或 Morris 遍历,否则会爆栈; - 层序遍历用
shift()出队是O(n),数据量大时用索引指针或双端队列,别在循环里反复移动数组。
End of entry