第一章:JS算法题库的核心价值与学习路径
JavaScript 算法题库不仅是前端开发者提升逻辑思维的训练场,更是通往高阶编程能力的关键阶梯。掌握算法不仅能提高代码效率,还能在技术面试中脱颖而出。为何算法学习至关重要
- 增强问题拆解能力,快速定位核心逻辑
- 优化程序性能,减少时间与空间复杂度
- 应对技术面试中的高频考点,如动态规划、回溯等
高效学习路径建议
从基础到进阶,循序渐进是关键。建议按以下顺序进行:- 掌握数组、字符串、链表等基本数据结构操作
- 理解递归、双指针、哈希表等常用技巧
- 深入学习树、图、动态规划等复杂主题
- 定期复盘错题,总结模板化解法
典型算法实现示例
以下是使用 JavaScript 实现的二分查找算法,适用于已排序数组:/**
* 二分查找:在有序数组中查找目标值的索引
* 时间复杂度:O(log n)
* @param {number[]} arr - 已排序的数组
* @param {number} target - 目标值
* @return {number} - 目标值的索引,不存在返回 -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; // 未找到
}
推荐练习平台对比
| 平台 | 优势 | 适合人群 |
|---|---|---|
| LeetCode | 题目丰富,面试真题多 | 求职者、进阶开发者 |
| Codewars | 趣味性强,社区活跃 | 初学者、兴趣驱动者 |
| CodeSignal | 自动化测评,企业常用 | 准备笔试者 |
第二章:数组与字符串类问题的解题模板
2.1 双指针技术的理论基础与典型应用场景
双指针技术是一种通过两个指针在数组或链表中协同移动,以优化时间复杂度的经典算法策略。其核心思想是利用指针的相对位置或移动规律,避免暴力遍历。基本原理
双指针常用于有序数据结构中,分为同向指针、相向指针和快慢指针三种模式。相向指针适用于两数之和问题,快慢指针常用于检测环形链表。代码示例:两数之和(有序数组)
func twoSum(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return []int{left + 1, right + 1} // 题目要求1-indexed
} else if sum < target {
left++
} else {
right--
}
}
return nil
}
该函数使用左右指针从数组两端向中间逼近。若当前和小于目标值,左指针右移以增大和;反之则右指针左移。时间复杂度为 O(n),优于暴力解法的 O(n²)。
- left:指向最小元素的索引,初始为0
- right:指向最大元素的索引,初始为len-1
- sum:当前两指针所指元素之和,用于判断移动方向
2.2 滑动窗口思想在子数组问题中的实践应用
滑动窗口是一种高效的双指针技巧,广泛应用于连续子数组或子串的求解问题中,尤其适用于满足特定条件的最短或最长区间查找。基本思想与适用场景
滑动窗口通过维护一个可变的窗口区间,动态调整左右边界,避免暴力枚举带来的重复计算。典型应用场景包括:最大/最小和子数组、不含重复字符的最长子串等。代码实现示例
func maxSubArraySum(nums []int, k int) int {
if len(nums) < k { return 0 }
windowSum := 0
for i := 0; i < k; i++ {
windowSum += nums[i] // 初始化窗口
}
maxSum := windowSum
for i := k; i < len(nums); i++ {
windowSum += nums[i] - nums[i-k] // 滑动窗口:右进左出
if windowSum > maxSum {
maxSum = windowSum
}
}
return maxSum
}
该函数计算长度为 k 的连续子数组的最大和。初始窗口累加前 k 个元素,随后通过减去左侧元素、加入右侧元素实现窗口滑动,时间复杂度从 O(nk) 优化至 O(n)。
- 窗口左边界:由索引 i-k 隐式控制
- 窗口右边界:由当前遍历位置 i 表示
- 核心操作:windowSum += nums[i] - nums[i-k]
2.3 哈希表优化查找效率的实战技巧
在高频查询场景中,合理优化哈希表结构能显著提升性能。首要策略是选择合适的哈希函数,避免冲突集中。负载因子与扩容策略
当哈希表元素数量超过容量与负载因子的乘积时,应触发自动扩容。常见负载因子设置为0.75,平衡空间与性能。- 初始容量建议设为预期数据量的1.5倍
- 扩容时重建哈希表,减少链化概率
开放寻址法优化查找
对于小规模数据集,线性探测虽简单但易聚集。推荐使用双重哈希法:func doubleHash(key string, size int) int {
h1 := hashFunc1(key) % size
h2 := 1 + hashFunc2(key)%(size-1)
for i := 0; ; i++ {
index := (h1 + i*h2) % size
if table[index] == nil || table[index].key == key {
return index
}
}
}
该方法通过第二个哈希函数计算步长,有效分散碰撞位置,降低聚集效应,平均查找时间接近O(1)。
2.4 原地变换法解决空间限制类高频题
在面对数组类问题且要求空间复杂度为 O(1) 时,原地变换法是一种高效策略。该方法通过复用输入数组存储中间状态,避免额外空间开销。核心思想
利用数组元素的数学特性(如取模、符号标记)在不丢失原始信息的前提下编码新信息。经典应用:数组原地标记
例如,在“寻找重复数”问题中,可将访问过的索引位置元素置负,表示已访问:
for _, num := range nums {
index := abs(num) - 1
if nums[index] < 0 {
return index + 1 // 重复数
}
nums[index] = -nums[index]
}
上述代码通过符号变化记录访问状态,时间复杂度 O(n),空间复杂度 O(1)。关键在于恢复原始值时可通过绝对值还原,确保数据完整性。此技巧广泛适用于索引与值映射类问题。
2.5 排序与二分查找结合的经典题目剖析
在算法设计中,排序与二分查找的结合常用于优化搜索效率。典型应用场景包括“寻找旋转排序数组中的最小值”或“在无序数组中查找第K大元素”。经典问题:寻找旋转排序数组中的目标值
此类问题可通过先排序预处理(实际中更优解为直接二分),再执行二分查找实现。// 两步法:排序 + 二分查找
func search(nums []int, target int) int {
sort.Ints(nums) // 升序排列
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
上述代码中,sort.Ints 确保数组有序,二分查找部分时间复杂度为 O(log n),整体受排序影响为 O(n log n)。虽然非最优解,但展示了排序与二分的协同逻辑。
第三章:树与图结构的递归与遍历策略
3.1 深度优先搜索的递归模板与边界处理
深度优先搜索(DFS)是图和树遍历的核心算法之一,其递归实现简洁直观。掌握标准模板与边界条件处理是避免栈溢出和逻辑错误的关键。基础递归模板
def dfs(node, visited):
if node in visited:
return
visited.add(node)
# 处理当前节点
for neighbor in graph[node]:
dfs(neighbor, visited)
该模板中,visited 集合防止重复访问,确保每个节点仅被处理一次。递归调用前判断是否已访问,构成核心边界控制机制。
边界条件的重要性
- 空输入:如根节点为 None 时应立即返回
- 自环边:通过 visited 集合规避无限递归
- 深层递归:Python 默认递归深度限制为 1000,必要时需调整 sys.setrecursionlimit()
3.2 广度优先搜索在层序遍历中的工程化实现
在二叉树的层序遍历中,广度优先搜索(BFS)通过队列结构按层级顺序访问节点,具备良好的可扩展性与稳定性。核心算法逻辑
使用标准队列实现BFS,确保每一层节点被完整处理后再进入下一层:func levelOrder(root *TreeNode) [][]int {
result := [][]int{}
if root == nil {
return result
}
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
currentLevel := []int{}
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
currentLevel = append(currentLevel, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, currentLevel)
}
return result
}
该实现通过 levelSize 快照控制每层遍历边界,避免跨层混淆。入队顺序保证从左到右处理,符合层序要求。
性能优化策略
- 预分配切片容量以减少内存扩容开销
- 复用队列空间,提升GC效率
- 结合并发机制处理大规模树结构
3.3 二叉搜索树的特性利用与验证技巧
二叉搜索树(BST)的核心特性是:对任意节点,其左子树所有节点值均小于该节点值,右子树所有节点值均大于该节点值,且左右子树均为二叉搜索树。这一递归性质为验证和优化操作提供了基础。中序遍历验证法
利用BST中序遍历结果为升序的特性,可高效验证结构正确性:
func isValidBST(root *TreeNode) bool {
var prev *TreeNode
var inorder func(*TreeNode) bool
inorder = func(node *TreeNode) bool {
if node == nil {
return true
}
if !inorder(node.Left) {
return false
}
if prev != nil && prev.Val >= node.Val {
return false
}
prev = node
return inorder(node.Right)
}
return inorder(root)
}
上述代码通过闭包维护前驱节点 prev,在中序遍历时比较当前节点值是否严格大于前驱,确保全局有序性。时间复杂度为 O(n),空间复杂度 O(h),其中 h 为树高。
递归边界检查
另一种方法是在递归过程中传递值域上下界:- 根节点值域为 (-∞, +∞)
- 左子树值域更新为 (min, root.Val)
- 右子树值域更新为 (root.Val, max)
第四章:动态规划与贪心算法的思维突破
4.1 动态规划状态定义与转移方程构建方法
动态规划的核心在于合理定义状态和构建状态转移方程。状态应能完整描述子问题的解空间,通常以数组维度形式体现,如dp[i] 表示前 i 个元素的最优解。
状态设计原则
- 无后效性:当前状态仅依赖于先前状态,不受未来决策影响;
- 可穷尽性:所有可能情况均被状态覆盖;
- 最小粒度:状态划分足够细,避免信息重叠。
经典转移方程示例
dp[i] = max(dp[i-1], dp[i-2] + value[i]);
该方程适用于打家劫舍问题:dp[i] 表示偷到第 i 家时的最大收益。若不偷第 i 家,则继承 dp[i-1];若偷,则加上前两家之前的最大收益 dp[i-2] 与当前价值。
常见模式对比
| 问题类型 | 状态定义 | 转移方式 |
|---|---|---|
| 背包问题 | dp[i][w] | max(dp[i-1][w], dp[i-1][w-weight]+value) |
| 最长递增子序列 | dp[i] | if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j]+1) |
4.2 经典背包问题变种在JS中的编码实现
0-1背包问题基础实现
在JavaScript中,通过动态规划解决0-1背包问题,核心是构建二维DP表:
function knapsack(weights, values, capacity) {
const n = weights.length;
const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
if (weights[i-1] <= w) {
dp[i][w] = Math.max(
dp[i-1][w],
dp[i-1][w-weights[i-1]] + values[i-1]
);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
return dp[n][capacity];
}
该函数接收物品重量数组、价值数组和背包容量。dp[i][w]表示前i个物品在容量w下的最大价值。内层循环逐项更新状态,时间复杂度为O(n*W)。
空间优化:一维数组实现
- 利用滚动数组思想,将二维dp压缩为一维
- 遍历顺序需从右向左,避免状态覆盖错误
function knapsackOptimized(weights, values, capacity) {
const dp = Array(capacity + 1).fill(0);
for (let i = 0; i < weights.length; i++) {
for (let w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
优化后空间复杂度由O(nW)降为O(W),适用于大规模数据场景。
4.3 贪心策略的适用条件与反例分析
贪心算法在每一步选择中都采取当前状态下最优的决策,期望通过局部最优达到全局最优。然而,其正确性依赖于问题是否具备贪心选择性质和最优子结构。适用条件
- 贪心选择性质:局部最优解能导向全局最优解;
- 最优子结构:问题的最优解包含子问题的最优解。
经典反例:0-1背包问题
若按价值密度(价值/重量)贪心选择,可能无法装满背包,导致非最优解。# 物品:(重量, 价值)
items = [(10, 60), (20, 100), (30, 120)]
capacity = 50
# 按价值密度排序后贪心选择前两个:总价值160
# 但最优解为选择后两个:总价值220
该反例说明贪心策略不适用于所有优化问题,需谨慎验证其适用性。
4.4 DP优化技巧:空间压缩与记忆化搜索
在动态规划问题中,空间压缩是一种有效降低内存消耗的技术。通过观察状态转移方程,若当前状态仅依赖前几个状态,可将二维数组压缩为一维。空间压缩示例:背包问题
// 原始二维DP
for (int i = 1; i <= n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
}
}
// 空间压缩后
for (int i = 1; i <= n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]); // 滚动更新
}
}
上述代码通过逆序遍历重量维度,避免状态覆盖错误,将空间复杂度从 O(nW) 降至 O(W)。
记忆化搜索提升效率
- 适用于状态转移路径稀疏的问题
- 递归过程中缓存已计算结果,避免重复求解
- 结合剪枝策略可进一步优化性能
第五章:从刷题到面试通关的系统性复盘与跃迁建议
构建个人知识图谱
将刷题过程中涉及的数据结构与算法归类整理,形成可检索的知识网络。例如,使用思维导图工具标注“二叉树遍历”与“回溯法”的关联场景,提升问题识别速度。高频面试题模式提炼
- 滑动窗口常用于子串匹配(如最小覆盖子串)
- 快慢指针多应用于链表环检测
- 拓扑排序解决依赖调度问题
代码实现与边界处理示例
// 判断链表是否有环
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
if slow == fast {
return true // 快慢指针相遇
}
slow = slow.Next
fast = fast.Next.Next
}
return false
}
模拟面试反馈机制
建立录音复盘流程:每次模拟面试后重听回答,评估表达逻辑是否清晰,是否存在术语误用。某候选人通过此方法将动态规划讲解准确率从60%提升至92%。行为问题应答框架
| 问题类型 | 应答结构 | 案例关键词 |
|---|---|---|
| 项目挑战 | STAR模型 | 性能优化、跨团队协作 |
| 技术选型 | 权衡分析 | 一致性 vs 可用性 |
[准备阶段] → [刷题强化] → [模拟面试] → [反馈迭代] → [正式面试]
1367

被折叠的 条评论
为什么被折叠?



