想要用一个“万能模板”直接套用 LeetCode Hot 100 的所有题目是不切实际的,因为算法的本质是针对不同数据结构和业务场景的时间/空间复杂度优化。图的搜索、数组的排序、树的遍历在物理内存和逻辑推演上是完全不同的维度。
不过,如果我们将 Hot 100 的题目进行解构,会发现它们高度集中在几个核心的逻辑模式上。掌握这几个模式的“骨架”,然后根据具体题目填充血肉,才是通关的最优解。
以下是涵盖 Hot 100 中近 80% 题目的 5 大核心套路模板(JavaScript 实现)及其底层思路:
1. 数组/字符串:滑动窗口与双指针模板
核心思路:这类题目通常要求在连续区间内寻找最值(最长子串、最小覆盖子串等)。与其使用 的暴力双重循环,不如维护一个像毛毛虫一样爬行的窗口。右指针主动扩张寻找可行解,左指针被动收缩优化可行解。
适用题目:无重复字符的最长子串、最小覆盖子串、盛最多水的容器、三数之和。
JavaScript
function slidingWindow(s) {
// 1. 初始化双指针和状态记录容器(如哈希表、计数器)
let left = 0, right = 0;
let windowState = new Map();
let res = 0; // 或具体字符串
// 2. 右指针主动扩张
while (right < s.length) {
let charRight = s[right];
// 更新窗口状态...
// windowState.set(charRight, ...)
right++;
// 3. 判断当前窗口是否满足“收缩”条件(比如出现了重复字符、长度超标等)
while (/* 窗口需要收缩的条件 */) {
// 在收缩前或收缩后更新最终结果 res
let charLeft = s[left];
// 更新窗口状态,剔除左边元素...
// windowState.set(charLeft, ...)
left++; // 左指针被动收缩
}
// 有些题目的 res 更新放在收缩循环外(如求最大窗口)
// res = Math.max(res, right - left);
}
return res;
}
2. 穷举与搜索:回溯算法(DFS)模板
核心思路:解决“全排列”、“组合”、“子集”问题的本质是遍历一棵决策树。回溯的核心就三步:做选择 -> 递归进入下一层 -> 撤销选择。它穷尽了所有可能性。
适用题目:全排列、子集、组合总和、电话号码的字母组合、括号生成。
JavaScript
function backtrackGenerator(nums) {
const res = [];
// path: 当前走过的路径(已做的选择)
// startIndex: 防止组合重复(排列问题通常不需要,或者用 visited 数组)
function backtrack(path, startIndex) {
// 1. 触发结束条件(到达树的叶子节点)
if (/* 满足某个条件,如 path.length === nums.length */) {
res.push([...path]); // 必须深拷贝,否则后续的 pop 会清空结果
return;
}
// 2. 遍历当前层的所有选择
for (let i = startIndex; i < nums.length; i++) {
// 剪枝逻辑:跳过不合法的选择
// if (!isValid(nums[i])) continue;
// 3. 做选择
path.push(nums[i]);
// 4. 进入下一层决策树
backtrack(path, i + 1); // 组合问题传 i+1,如果是可以重复选的传 i
// 5. 撤销选择(回溯,恢复状态)
path.pop();
}
}
backtrack([], 0);
return res;
}
3. 树结构:二叉树递归遍历模板
核心思路:二叉树题目的本质是降维打击。把一个庞大的树结构问题,分解为一个节点与其左右子节点的问题。关键在于明确当前节点需要做什么,以及这个动作应该发生在遍历前(前序)、遍历中(中序)还是遍历后(后序)。
适用题目:翻转二叉树、二叉树的最大深度、二叉树的最近公共祖先、二叉树展开为链表。
JavaScript
function treeTraverse(root) {
// 1. Base Case: 触底反弹
if (root === null) {
return /* 基础值,如 null, 0, [] 等 */;
}
// --- 前序位置:刚进入该节点时执行 ---
// (常用于自顶向下的逻辑,如传递深度)
// 2. 递归获取左右子树的信息
let leftResult = treeTraverse(root.left);
let rightResult = treeTraverse(root.right);
// --- 后序位置:即将离开该节点时执行 ---
// (常用于自底向上的逻辑,利用 leftResult 和 rightResult 组装当前节点的结果,这是最常用的位置)
// 3. 整合结果并返回给父节点
return /* 基于当前节点、左结果、右结果的整合 */;
}
4. 有序空间:二分查找模板
核心思路:二分查找不仅用于“在有序数组中找数字”,更强大的是用来寻找“满足某种条件的边界”。只要发现问题具备“单调性”(比如:某个值可行,比它小的值一定可行,比它大的值一定不可行),就可以用二分。
适用题目:搜索旋转排序数组、寻找两个正序数组的中位数、在排序数组中查找元素的第一个和最后一个位置。
JavaScript
function binarySearch(nums, target) {
// 1. 定义搜索区间 [left, right]
let left = 0;
let right = nums.length - 1;
// 2. 当 left <= right 时区间不为空
while (left <= right) {
// 防止 (left + right) 溢出,并向下取整
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) {
// 找到目标值
return mid;
// 如果是寻找左边界,这里改为 right = mid - 1;
// 如果是寻找右边界,这里改为 left = mid + 1;
} else if (nums[mid] < target) {
// 目标在右侧,收缩左边界
left = mid + 1;
} else if (nums[mid] > target) {
// 目标在左侧,收缩右边界
right = mid - 1;
}
}
return -1; // 或返回 left 作为插入位置
}
5. 链表结构:虚拟头节点(Dummy Head)与快慢指针模板
核心思路:链表题的难点往往在于处理头节点被删除或修改的边界情况,以及无法预知链表长度。引入一个指向原始头节点的 dummy 节点可以抹平所有边界差异;快慢指针则可以优雅地解决环形检测、寻找中点等问题。
适用题目:反转链表、合并两个有序链表、删除链表的倒数第 N 个结点、环形链表。
JavaScript
function processLinkedList(head) {
// 1. 设置虚拟头节点,避免讨论原本 head 被删掉或移动的边界情况
let dummy = new ListNode(-1);
dummy.next = head;
// 2. 根据需求设置双指针(这里以找倒数第 n 个节点或找中点为例)
let slow = dummy;
let fast = dummy;
// 3. 快指针先走 / 共同走
while (fast !== null && fast.next !== null) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
}
// 4. 处理指针停下后的逻辑(此时 slow 通常在中点,或者用于其他截断操作)
// ...
// 5. 返回时忽略 dummy 本身
return dummy.next;
}
理解这五个结构的运作机制,就可以覆盖很大一部分的高频考点。至于动态规划(DP),它不是一个代码模板,而是一种“状态定义”与“状态转移方程”的数学建模,必须具体问题具体分析。