第一章:JavaScript算法刷题路线图(从入门到Offer收割)
明确学习目标与阶段划分
刷题不是盲目地做题,而是有策略地提升编程思维和问题解决能力。整个路线可分为四个核心阶段:基础语法与数据结构掌握、经典算法理解、专题突破、真题模拟与优化。每个阶段都应设定明确目标,例如在第一阶段能熟练使用数组、字符串、哈希表等基本结构解决问题。推荐学习路径与资源搭配
- 初学者优先完成 LeetCode 热题 HOT 100 中的简单题
- 掌握双指针、滑动窗口、递归回溯等高频技巧
- 配合《代码随想录》系统梳理知识点
- 进阶阶段挑战 LeetCode Top Interview 150 和周赛题目
高频数据结构操作示例
以下是一个使用 JavaScript 实现哈希表统计字符频次的通用模板:// 统计字符串中每个字符出现次数
function countChars(str) {
const map = new Map();
for (const char of str) {
map.set(char, (map.get(char) || 0) + 1); // 若不存在则初始化为0
}
return map;
}
// 调用示例
console.log(countChars("hello"));
// 输出: Map { 'h' => 1, 'e' => 1, 'l' => 2, 'o' => 1 }
刷题进度跟踪建议
| 阶段 | 目标 | 建议题量 |
|---|---|---|
| 入门 | 熟悉语法与基础结构 | 30题 |
| 提升 | 掌握主流算法思想 | 80题 |
| 冲刺 | 应对大厂真题 | 100+题 |
graph TD
A[开始刷题] --> B{掌握JS基础?}
B -->|否| C[补基础语法]
B -->|是| D[刷热题HOT 100]
D --> E[分类攻克专题]
E --> F[模拟面试真题]
F --> G[Offer收割]
第二章:基础语法与数据结构夯实
2.1 JavaScript核心语法精要与常见陷阱
变量提升与作用域陷阱
JavaScript中的变量提升(hoisting)常引发意料之外的行为。使用var声明的变量会被提升至函数或全局作用域顶部,但赋值仍保留在原位。
console.log(a); // undefined
var a = 5;
上述代码中,a的声明被提升,但赋值未提升,导致输出undefined而非报错。推荐使用let和const以避免此类问题。
相等运算符的隐式类型转换
==会进行强制类型转换,而===则严格比较类型与值。常见陷阱如下:
| 表达式 | 结果 |
|---|---|
| '0' == false | true |
| '0' === false | false |
===以确保可预测性。
2.2 数组与字符串操作的高频技巧实战
在算法题和系统设计中,数组与字符串的高效操作是性能优化的关键。掌握原地修改、双指针与滑动窗口等技巧,能显著降低时间与空间复杂度。双指针处理有序数组
使用双指针可在不增加额外空间的情况下完成数组去重或查找操作:func removeDuplicates(nums []int) int {
if len(nums) == 0 {
return 0
}
slow := 0
for fast := 1; fast < len(nums); fast++ {
if nums[slow] != nums[fast] {
slow++
nums[slow] = nums[fast]
}
}
return slow + 1
}
该函数通过快慢指针将非重复元素前移,slow 指向最终结果的末尾,时间复杂度为 O(n),空间复杂度 O(1)。
滑动窗口解决子串问题
- 用于查找满足条件的最短/最长子串
- 典型应用:最小覆盖子串、无重复字符的最长子串
2.3 对象与哈希表在算法中的灵活应用
在算法设计中,对象与哈希表的结合能显著提升数据查找与组织效率。通过将对象属性映射为哈希键,可实现常量时间内的数据访问。哈希表优化对象检索
例如,在寻找数组中两数之和等于目标值的问题中,使用哈希表存储已遍历的数值与索引,避免嵌套循环:
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
}
上述代码中,Map 以数值为键、索引为值,将时间复杂度从 O(n²) 降至 O(n)。
对象属性的哈希化处理
- 利用对象的唯一标识作为哈希键,实现去重逻辑
- 在图算法中,用哈希表存储邻接关系,提升遍历效率
2.4 递归与迭代的思维转换训练
在算法设计中,递归与迭代是两种核心的实现方式。理解二者之间的转换逻辑,有助于提升代码效率与可读性。递归的本质
递归通过函数自调用分解问题规模,典型如阶乘计算:def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
该实现简洁直观,但存在重复调用和栈溢出风险。
向迭代转换
将递归转化为迭代,关键在于用循环和变量替代隐式调用栈:def factorial_iter(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
参数 n 控制循环次数,result 累积中间状态,避免了函数调用开销。
- 递归适合分治、树遍历等天然递归结构
- 迭代在性能敏感场景更具优势
- 思维转换需识别“递归出口”与“状态更新”
2.5 时间与空间复杂度分析入门与实践
在算法设计中,时间与空间复杂度是衡量性能的核心指标。时间复杂度反映算法执行时间随输入规模增长的变化趋势,而空间复杂度描述所需内存资源的增长情况。常见复杂度级别
- O(1):常数时间,如数组访问
- O(log n):对数时间,如二分查找
- O(n):线性时间,如遍历数组
- O(n²):平方时间,如嵌套循环
代码示例与分析
// 计算数组元素之和
func sumArray(arr []int) int {
sum := 0
for _, v := range arr { // 循环n次
sum += v
}
return sum
}
该函数时间复杂度为 O(n),因循环体执行次数与输入数组长度成正比;空间复杂度为 O(1),仅使用固定额外变量。
复杂度对比表
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 冒泡排序 | O(n²) | O(1) |
| 归并排序 | O(n log n) | O(n) |
第三章:经典算法思想深度解析
3.1 双指针技术在数组与链表中的高效应用
双指针技术通过两个指针协同移动,显著提升处理数组和链表问题的效率,尤其适用于避免暴力遍历的场景。快慢指针判断链表环
使用快指针(每次走两步)和慢指针(每次走一步)可高效检测链表中是否存在环。func hasCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true // 环存在
}
}
return false
}
该算法时间复杂度为 O(n),空间复杂度 O(1)。当链表无环时,快指针率先到达末尾;若有环,则快慢指针终将相遇。
左右指针实现数组原地调整
在有序数组中,左右指针从两端向中间逼近,常用于两数之和或三数之和问题,减少不必要的比较。3.2 滑动窗口与前缀和解题模式剖析
滑动窗口的基本思想
滑动窗口常用于解决数组或字符串的子区间问题,通过维护一个可变窗口来降低时间复杂度。适用于求解最长/最短子串、满足条件的子数组个数等问题。func maxSubArraySum(nums []int, k int) int {
sum := 0
for i := 0; i < k; i++ {
sum += nums[i]
}
maxSum := sum
for i := k; i < len(nums); i++ {
sum = sum - nums[i-k] + nums[i] // 滑动:移出左端,加入右端
if sum > maxSum {
maxSum = sum
}
}
return maxSum
}
该代码计算长度为 k 的连续子数组的最大和。初始计算前 k 个元素之和,随后窗口向右滑动,每次更新总和并比较最大值。时间复杂度从 O(nk) 优化至 O(n)。
前缀和的应用场景
前缀和适用于频繁查询区间和的场景。通过预处理构造前缀数组,实现 O(1) 查询任意区间的累加和。- 滑动窗口适合动态调整边界的问题
- 前缀和适合静态数组的区间求和
- 两者结合可解决如“和为 K 的子数组”类问题
3.3 分治与回溯思想在真实面试题中的落地
分治法解决数组最大子序和
分治思想常用于将大问题拆解为子问题。以“最大子数组和”为例,可通过递归处理左半、右半及跨中点的子数组。
public int maxSubArray(int[] nums, int left, int right) {
if (left == right) return nums[left];
int mid = (left + right) / 2;
int leftSum = maxSubArray(nums, left, mid);
int rightSum = maxSubArray(nums, mid + 1, right);
int crossSum = calCrossSum(nums, left, mid, right);
return Math.max(Math.max(leftSum, rightSum), crossSum);
}
该方法将原问题分解为三个子问题,时间复杂度稳定在 O(n log n),适用于大规模数据场景。
回溯法生成全排列
回溯本质是递归中的路径选择与撤销。求解无重复元素的全排列可用如下方式:
- 选择未使用的数字加入当前路径
- 递归进入下一层
- 回溯时移除当前数字,尝试其他分支
这种“试错”机制在组合类问题中极为高效。
第四章:高频题型分类突破
4.1 链表类题目全解:反转、环检测与合并
链表反转:基础但关键的操作
反转链表是理解指针操作的核心题型。通过迭代方式,逐个调整节点的指针方向。
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next
curr.Next = prev
prev = curr
curr = next
}
return prev
}
代码中 prev 指向已反转部分的头节点,curr 遍历原链表,每次将当前节点指向前驱,实现原地反转。
环检测:Floyd 判圈算法
使用快慢指针判断链表是否存在环。快指针每次走两步,慢指针走一步,若相遇则存在环。
合并两个有序链表
典型分治思想应用,通过哨兵节点简化边界处理:
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
curr := dummy
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
curr.Next = l1
l1 = l1.Next
} else {
curr.Next = l2
l2 = l2.Next
}
curr = curr.Next
}
if l1 != nil {
curr.Next = l1
} else {
curr.Next = l2
}
return dummy.Next
}
该方法时间复杂度为 O(m+n),空间复杂度 O(1),适用于有序链表的归并场景。
4.2 二叉树遍历与递归套路系统梳理
三种基础遍历方式的递归实现
二叉树的深度优先遍历可分为前序、中序和后序三种,其核心逻辑均可通过递归简洁表达:
def inorder(root):
if not root:
return
inorder(root.left) # 左
print(root.val) # 根
inorder(root.right) # 右
上述为中序遍历,执行顺序为“左-根-右”。改变打印语句位置即可转换为前序或后序遍历。
递归四步法模板
- 终止条件:节点为空时返回
- 递归方向:向左右子树深入
- 当前处理:访问当前节点值
- 返回值:根据题意决定
4.3 动态规划入门:从斐波那契到背包问题
动态规划(Dynamic Programming, DP)是一种通过将复杂问题分解为子问题来优化求解过程的算法设计思想。其核心在于**记忆化**和**最优子结构**,避免重复计算。斐波那契数列的递推本质
斐波那契数列是理解DP的起点。朴素递归时间复杂度高达 O(2^n),而使用动态规划可优化至 O(n)。def fib(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
代码中 dp[i] 表示第 i 个斐波那契数,通过自底向上填充数组避免重复计算,空间复杂度可进一步优化至 O(1)。
0-1 背包问题的状态转移
给定物品重量与价值,求在承重限制下的最大价值。定义dp[i][w] 为前 i 个物品在容量 w 下的最大价值。
| i | weight | value |
|---|---|---|
| 1 | 2 | 3 |
| 2 | 3 | 4 |
| 3 | 4 | 5 |
dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1])
表示“不选”或“选第 i 个物品”的最优决策。
4.4 贪心算法与排序配合的典型场景实战
在解决最优化问题时,贪心算法常与排序结合使用,以提升决策效率。典型应用场景包括区间调度、任务安排和最小延迟调度等。区间调度问题
目标是选择最多不重叠的区间。关键策略是按结束时间排序,优先选择最早结束的区间。def interval_scheduling(intervals):
intervals.sort(key=lambda x: x[1]) # 按结束时间排序
count = 0
end = float('-inf')
for s, e in intervals:
if s >= end: # 当前开始时间不早于上一个结束时间
count += 1
end = e
return count
该算法时间复杂度为 O(n log n),主要开销在排序。贪心选择性质确保每一步局部最优能导向全局最优。
适用场景归纳
- 任务安排:按截止时间排序,优先处理临近任务
- 加油站问题:按位置排序,贪心选择最远可达油站
- 分发饼干:对孩子和饼干分别排序,实现最大满足数
第五章:高效刷题策略与Offer收割心得
制定科学的刷题节奏
- 每天固定2小时专注刷题,优先完成LeetCode Hot 100和Top Interview Questions
- 采用“三遍法”:第一遍理解思路,第二遍手写代码,第三遍限时完成
- 每周进行一次模拟面试,使用CoderPad或白板还原真实场景
分类突破高频考点
| 题型 | 出现频率 | 推荐练习量 |
|---|---|---|
| 动态规划 | 38% | 50+ |
| 二叉树遍历 | 32% | 30+ |
| 双指针 | 25% | 20+ |
代码实现与边界处理
// Go语言实现两数之和,注意边界条件和哈希表优化
func twoSum(nums []int, target int) []int {
numMap := make(map[int]int)
for i, num := range nums {
complement := target - num
if idx, found := numMap[complement]; found {
return []int{idx, i}
}
numMap[num] = i // 后插入避免重复使用同一元素
}
return nil
}
Offer选择与谈判技巧
薪资谈判流程图:
收到口头offer → 调研市场薪资水平 → 对比其他公司报价 → 提出合理期望 → 协商签字奖金与RSU归属周期 → 确认入职细节
在字节跳动和美团同时发放offer时,通过展示竞争性报价,成功将总包提升18%。
收到口头offer → 调研市场薪资水平 → 对比其他公司报价 → 提出合理期望 → 协商签字奖金与RSU归属周期 → 确认入职细节
2830

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



