【算法面试速成计划】:21天攻克LeetCode Top 50高频题(附代码模板)

21天攻克LeetCode Top 50高频题

第一章:LeetCode高频题型总览

在准备技术面试的过程中,LeetCode 已成为全球开发者提升算法能力的核心平台。通过对近年企业面试题目的统计分析,可以发现部分题型出现频率显著高于其他题目,掌握这些高频题型有助于高效备考。

常见高频题型分类

  • 数组与字符串操作:如两数之和、最长无重复子串
  • 链表处理:反转链表、环形检测、合并有序链表
  • 树的遍历与递归:二叉树的最大深度、路径总和、验证BST
  • 动态规划:爬楼梯、背包问题、最长递增子序列
  • 滑动窗口与双指针:最小覆盖子串、盛最多水的容器

典型题目示例(两数之和)

// 使用哈希表实现O(n)时间复杂度
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 // 将当前数值和索引加入map
    }
    return nil // 未找到解时返回nil
}
该代码通过一次遍历完成查找,利用哈希表将查找补数的时间降为O(1),整体效率优于暴力双重循环。

高频题型分布统计

题型占比(近一年)代表题目数量
数组/字符串35%45
动态规划20%26
树相关18%23
链表12%15
graph TD A[开始刷题] --> B{选择题型} B --> C[数组与字符串] B --> D[动态规划] B --> E[树与图] C --> F[掌握模板] D --> F E --> F F --> G[模拟面试]

第二章:数组与字符串经典题解

2.1 数组双指针技巧与实战应用

在处理数组问题时,双指针技巧能显著提升效率,尤其适用于有序数组的查找、去重和子数组问题。通过维护两个移动指针,避免使用额外空间或嵌套循环。
基本思想与常见模式
双指针主要分为同向指针和对向指针。对向指针常用于两数之和问题,而同向指针多用于移除元素或合并操作。

// 对向指针:在有序数组中寻找两数之和
func twoSum(nums []int, target int) []int {
    left, right := 0, len(nums)-1
    for left < right {
        sum := nums[left] + nums[right]
        if sum == target {
            return []int{left, right}
        } else if sum < target {
            left++
        } else {
            right--
        }
    }
    return []int{}
}
该代码利用数组有序特性,left 从起始位置右移,right 从末尾左移。当和小于目标值时,增大 left;反之减小 right,时间复杂度为 O(n)。
实际应用场景
  • 有序数组中查找满足条件的数对
  • 原地修改数组,如删除重复元素
  • 合并两个有序数组

2.2 滑动窗口算法原理与高频变种

滑动窗口是一种用于优化区间查询问题的经典技巧,特别适用于数组或字符串中连续子序列的处理。其核心思想是通过维护一个可变或固定大小的窗口,动态调整左右边界,避免重复计算。
基本原理
使用双指针模拟窗口滑动:左指针控制窗口起始位置,右指针扩展窗口范围。当窗口内状态不满足条件时收缩左边界,从而在线性时间内完成遍历。
常见变种与应用场景
  • 定长窗口:求固定长度子数组的最大和
  • 可变窗口(最短/最长满足条件的子串):如最小覆盖子串
  • 前缀和 + 哈希优化:用于子数组和为目标值等问题
// 示例:可变滑动窗口模板
func slidingWindow(s string, t string) string {
    left, right := 0, 0
    valid := 0
    window := make(map[byte]int)
    need := make(map[byte]int)
    for i := range t {
        need[t[i]]++
    }

    start, length := 0, math.MaxInt32
    for right < len(s) {
        c := s[right]
        right++
        if need[c] > 0 {
            window[c]++
            if window[c] == need[c] {
                valid++
            }
        }

        for valid == len(need) {
            if right-left < length {
                start = left
                length = right - left
            }
            d := s[left]
            left++
            if need[d] > 0 {
                if window[d] == need[d] {
                    valid--
                }
                window[d]--
            }
        }
    }
    if length == math.MaxInt32 {
        return ""
    }
    return s[start : start+length]
}
该代码实现“最小覆盖子串”问题,利用两个哈希表记录目标字符频次与当前窗口匹配情况,通过移动右指针扩展、左指针收缩实现最优解搜索。变量 valid 表示已完全匹配的字符种类数,是判断窗口合法性的关键。

2.3 前缀和与差分数组的优化策略

在处理高频区间更新与查询问题时,前缀和与差分数组是两种高效的技术手段。通过预处理数据结构,显著降低时间复杂度。
前缀和优化区间求和
对于静态数组的多次区间求和查询,使用前缀和可将每次查询从 O(n) 降至 O(1):
// 构建前缀和数组
prefix[i] = prefix[i-1] + arr[i-1]
// 查询 [l, r] 区间和
sum = prefix[r+1] - prefix[l]
该方法适用于无更新或更新极少的场景,预处理时间 O(n),空间开销 O(n)。
差分数组处理频繁更新
当面临大量区间增减操作时,差分数组更优:
  • 在区间 [l, r] 增加 val:diff[l] += val, diff[r+1] -= val
  • 最终通过前缀还原原数组
此策略将每次更新压缩为 O(1) 操作,适合批量更新后统一输出结果的场景。

2.4 字符串匹配与回文判断模板

基础字符串匹配方法
在处理字符串问题时,朴素匹配法是最直观的起点。其核心思想是逐位比较主串与模式串。
func match(s, pattern string) int {
    n, m := len(s), len(pattern)
    for i := 0; i <= n-m; i++ {
        j := 0
        for j < m && s[i+j] == pattern[j] {
            j++
        }
        if j == m {
            return i
        }
    }
    return -1
}
该函数返回模式串在主串中的起始索引。外层循环控制主串的匹配起点,内层循环逐字符比对,时间复杂度为 O(n×m)。
回文串高效判定
使用双指针从两端向中心逼近,可在线性时间内判断回文。
  • 初始化左指针为0,右指针为len(s)-1
  • 循环直至两指针相遇
  • 若对应字符不等,则非回文

2.5 贪心思想在区间问题中的体现

在处理区间调度与覆盖类问题时,贪心策略常能提供高效且最优的解决方案。其核心思想是:每一步都选择当前最优的局部解,期望最终得到全局最优。
经典问题:区间调度
给定一组闭区间,目标是选出最多不重叠的区间。贪心策略为按结束时间升序排序,优先选择最早结束的区间。
func maxNonOverlapping(intervals [][]int) int {
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][1] < intervals[j][1]
    })
    count := 0
    end := -1
    for _, interval := range intervals {
        if interval[0] >= end {
            count++
            end = interval[1]
        }
    }
    return count
}
该代码通过排序后遍历实现,时间复杂度为 O(n log n),主要开销在排序。参数说明:intervals 为输入区间数组,每个元素 [start, end] 表示一个区间;count 记录选中区间数,end 维护上一个选中区间的结束位置。
适用条件
  • 问题具有最优子结构
  • 贪心选择性质成立:局部最优可导向全局最优

第三章:链表与树的核心套路

3.1 链表反转与环检测统一模型

在链表操作中,反转与环检测看似无关,实则可通过双指针技术构建统一模型。通过快慢指针或前后指针的协同移动,可同时解决两类问题。
核心思想:双指针范式
使用两个指针以不同速度遍历链表,不仅能检测环(如Floyd算法),也能在反转过程中维护前驱关系。
链表反转实现

func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next // 临时保存下一节点
        curr.Next = prev  // 反转当前指针
        prev = curr       // 前移prev
        curr = next       // 前移curr
    }
    return prev // 新头节点
}
该代码通过迭代将每个节点的Next指向其前驱,最终prev指向原尾部,完成反转。
环检测扩展
当快指针与慢指针相遇时,即存在环。结合反转逻辑,可在检测后逆向验证环的起点,形成统一处理框架。

3.2 二叉树遍历递归与迭代实现

递归实现:简洁直观的遍历方式

二叉树的三种主要遍历方式——前序、中序和后序,递归实现最为直观。以下为前序遍历的示例:

def preorder_recursive(root):
    if not root:
        return
    print(root.val)           # 访问根
    preorder_recursive(root.left)   # 遍历左子树
    preorder_recursive(root.right)  # 遍历右子树

该方法利用函数调用栈隐式维护访问顺序,逻辑清晰,但深度过大时可能引发栈溢出。

迭代实现:显式栈控制流程

使用栈模拟递归过程可避免系统栈限制。以下是前序遍历的迭代版本:

def preorder_iterative(root):
    stack, result = [], []
    while root or stack:
        if root:
            result.append(root.val)
            stack.append(root)
            root = root.left
        else:
            root = stack.pop()
            root = root.right
    return result

通过手动管理栈结构,程序在时间和空间效率上更具可控性,适用于大规模树结构处理。

3.3 BST性质在路径问题中的运用

BST路径搜索优化原理
二叉搜索树(BST)的中序遍历具有单调递增特性,这一性质可用于加速从根到叶节点的路径查找。当寻找特定值路径时,可依据当前节点值与目标的大小关系,决定仅递归左子树或右子树。
  • 若当前节点值大于目标,路径必存在于左子树
  • 若当前节点值小于目标,路径必存在于右子树
  • 显著降低无效递归调用,提升查询效率
def find_path(root, target, path):
    if not root:
        return False
    path.append(root.val)
    if root.val == target:
        return True
    if (root.left and root.val > target and find_path(root.left, target, path)) or \
       (root.right and root.val < target and find_path(root.right, target, path)):
        return True
    path.pop()
    return False
上述代码利用BST性质剪枝:每次递归前判断目标方向,避免无意义的子树遍历。参数 path 记录当前路径,回溯时弹出节点,确保空间效率。

第四章:搜索与动态规划精讲

4.1 DFS与BFS框架对比及剪枝技巧

核心框架差异
DFS基于栈结构实现回溯,适合路径探索;BFS使用队列逐层扩展,适用于最短路径问题。
特性DFSBFS
数据结构递归/栈队列
空间复杂度O(h)O(w)
剪枝优化策略
通过提前终止无效分支显著提升效率。常见剪枝包括可行性剪枝、最优性剪枝。
// DFS剪枝示例:N皇后问题
func dfs(row int, cols, diag1, diag2 map[int]bool) {
    if row == n {
        count++
        return
    }
    for col := 0; col < n; col++ {
        if cols[col] || diag1[row-col] || diag2[row+col] {
            continue // 剪枝:冲突位置跳过
        }
        // 标记并递归
        cols[col], diag1[row-col], diag2[row+col] = true, true, true
        dfs(row+1, cols, diag1, diag2)
        // 回溯
        cols[col], diag1[row-col], diag2[row+col] = false, false, false
    }
}
上述代码中,利用三个哈希表记录已占用列和对角线,避免非法状态搜索,大幅减少递归深度。

4.2 回溯法解决排列组合类题目

回溯法是解决排列、组合与子集问题的核心算法思想,通过尝试所有可能的分支并及时剪枝来高效遍历解空间。
基本框架
回溯法通常采用递归实现,其核心在于“做选择”与“撤销选择”:
def backtrack(path, options, result):
    if not options:
        result.append(path[:])
        return
    for item in options:
        path.append(item)
        next_options = options - {item}
        backtrack(path, next_options, result)
        path.pop()  # 撤销选择
其中 path 记录当前路径,options 表示可选列表,result 收集最终解。
经典应用场景
  • 全排列问题(Permutations)
  • 组合总和(Combination Sum)
  • 子集生成(Subsets)
通过合理设计状态变量与剪枝条件,可显著提升搜索效率。

4.3 一维与二维动态规划状态设计

在动态规划问题中,状态设计是核心环节。一维DP通常用于线性结构的问题,如斐波那契数列或爬楼梯问题,其状态转移仅依赖前几个已计算的状态。
一维动态规划示例
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
    dp[i] = dp[i-1] + dp[i-2]
该代码实现斐波那契数列的动态规划求解。dp[i] 表示到达第i级台阶的方法数,状态仅依赖于前两个状态,空间复杂度可优化至O(1)。
二维动态规划场景
当问题涉及两个变量维度时(如字符串匹配、矩阵路径),需采用二维DP。例如在网格中从左上到右下移动,状态定义为 dp[i][j] 表示到达位置(i,j)的路径数。
i\j012
0111
1123

4.4 背包模型在面试题中的变形应用

在实际面试中,背包问题常以隐式状态或复合约束形式出现,需识别其本质并灵活转化。
常见变形类型
  • 分组背包:每组物品中至多选一个
  • 多重背包:物品数量有限制
  • 依赖背包:选择物品存在先后依赖
典型代码实现(0-1背包空间优化)

// dp[j] 表示容量为 j 时的最大价值
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
    for (int j = W; j >= weight[i]; j--) {
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
该代码通过逆序遍历实现空间压缩,避免重复选择。外层循环物品,内层倒序更新状态,确保每个物品仅被使用一次。
应用场景对比
类型状态转移特点复杂度
0-1背包倒序更新O(nW)
完全背包正序更新O(nW)

第五章:高频题冲刺策略与模板总结

常见算法模式归纳
在高频面试题中,滑动窗口、双指针、DFS/BFS 和动态规划出现频率极高。掌握其通用模板可大幅提升解题效率。
  • 滑动窗口适用于子数组/子串问题,如“最长无重复字符子串”
  • 双指针常用于有序数组中的两数之和、合并区间等问题
  • 树的遍历优先考虑递归DFS或迭代BFS
动态规划状态转移模板
// 典型背包问题状态转移
dp[i][w] = max(
    dp[i-1][w],                    // 不选第i个物品
    dp[i-1][w-weight[i]] + value[i] // 选第i个物品
)
高频题优化技巧
使用哈希表预处理数据可将O(n²)降为O(n),例如两数之和问题中用map存储target - nums[i]。
题型推荐方法时间复杂度
子数组最大和Kadane算法O(n)
合并K个有序链表最小堆O(N log k)
实战调试建议
输入测试用例时优先覆盖: - 空输入 [] - 单元素 [1] - 边界情况 如溢出INT_MAX 逐步打印中间状态,验证状态转移正确性
提供了基于BP(Back Propagation)神经网络结合PID(比例-积分-微分)控制策略的Simulink仿真模型。该模型旨在实现对杨艺所著论文《基于S函数的BP神经网络PID控制器及Simulink仿真》中的理论进行实践验证。在Matlab 2016b环境下开发,经过测试,确保能够正常运行,适合学习研究神经网络在控制系统中的应用。 特点 集成BP神经网络:模型中集成了BP神经网络用于提升PID控制器的性能,使之能更好地适应复杂控制环境。 PID控制优化:利用神经网络的自学习能力,对传统的PID控制算法进行了智能调整,提高控制精度稳定性。 S函数应用:展示了如何在Simulink中通过S函数嵌入MATLAB代码,实现BP神经网络的定制化逻辑。 兼容性说明:虽然开发于Matlab 2016b,但理论上兼容后续版本,可能会需要调整少量配置以适配不同版本的Matlab。 使用指南 环境要求:确保你的电脑上安装有Matlab 2016b或更高版本。 模型加载: 下载本仓库到本地。 在Matlab中打开.slx文件。 运行仿真: 调整模型参数前,请先熟悉各模块功能输入输出设置。 运行整个模型,观察控制效果。 参数调整: 用户可以自由调节神经网络的层数、节点数以及PID控制器的参数,探索不同的控制性能。 学习修改: 通过阅读模型中的注释查阅相关文献,加深对BP神经网络与PID控制结合的理解。 如需修改S函数内的MATLAB代码,建议有一定的MATLAB编程基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值