动态规划(Dynamic Programming)
适用条件:问题具有重叠子问题和最优子结构的性质。大问题可以分解为若干小问题,而这些小问题有很多重复计算的部分。
比方:想象你在制作拼图。每个拼图碎片可以独立处理,但有些碎片可能需要重复放置。动态规划就像是记下已经拼好的碎片,以免重复工作。
实现步骤:
- 确定子问题:将原问题分解成更小的子问题。
- 定义状态:用一个或多个变量表示子问题的状态。
- 确定状态转移方程:找出子问题之间的关系,并写出状态转移方程。
- 初始化状态:为最简单的子问题初始化状态。
- 计算结果:按照状态转移方程,从最小子问题开始,逐步计算出原问题的解。
注意事项:
- 确保问题具有重叠子问题和最优子结构的性质。
- 状态转移方程需要正确反映子问题之间的关系。
常见的例子:斐波那契数列、爬楼梯问题、背包问题。
分类或变体:
- 线性动态规划:用于一维问题,如斐波那契数列。
- 比方:想象你在计算一个月的收入,你可以将每天的收入累加起来。
- 区间动态规划:用于区间问题,如矩阵链乘法。
- 比方:想象你在计划一个长途旅行,你可以将每段旅程的时间累加起来。
- 背包动态规划:用于背包问题,如0-1背包问题。
- 比方:想象你在打包行李,你需要在有限的行李箱空间内选择最重要的物品。
- 树形动态规划:用于树形结构问题,如树的最大路径和。
- 比方:想象你在决定家族企业的继承顺序,每个节点代表一个家庭成员。
- 状态压缩动态规划:用于状态较多的问题,如旅行商问题。
- 比方:想象你在处理一张复杂的行程表,你需要简化每个状态以便处理。
广度优先搜索(BFS,Breadth-First Search)
适用条件:遍历或搜索图中的所有节点,适用于寻找最短路径等问题。
比方:想象你在城市中寻找最短的路线。城市中的每条街道和路口就是图中的边和节点。
实现步骤:
- 初始化队列:将起始节点加入队列,并标记为已访问。
- 迭代处理队列:从队列中取出一个节点,访问其所有未被访问的邻居节点,并将它们加入队列。
- 继续迭代:重复步骤2,直到队列为空。
注意事项:
- 确保不重复访问已访问的节点,避免无限循环。
- 适用于寻找最短路径等问题。
常见的例子:最短路径问题、连通分量。
分类或变体:
- 普通BFS:用于一般图的遍历。
- 比方:就像你在超市找商品,从入口开始一排排地找。
- 双向BFS:用于寻找两个节点之间的最短路径。
- 比方:就像你和朋友从两个不同的地点出发,在中间碰头。
- 多源BFS:用于同时从多个起点开始搜索。
- 比方:就像多个消防员从不同的地点同时出发,寻找火源。
深度优先搜索(DFS,Depth-First Search)
适用条件:遍历或搜索图中的所有节点,适用于寻找路径、检测环等问题。
比方:想象你在森林中探险,每次都先走尽可能远的路,碰到障碍时再返回。
实现步骤:
- 初始化栈(或递归):将起始节点加入栈,并标记为已访问。
- 迭代处理栈:从栈中取出一个节点,访问其所有未被访问的邻居节点,并将它们加入栈。
- 继续迭代:重复步骤2,直到栈为空。
注意事项:
- 确保不重复访问已访问的节点,避免无限循环。
- 适用于路径搜索、检测环等问题。
常见的例子:拓扑排序、检测图中的环。
分类或变体:
- 普通DFS:用于一般图的遍历。
- 比方:就像你在书架上找书,每次先查看最上面的书。
- 递归DFS:通过递归实现的DFS。
- 比方:就像你逐层打开套娃,每个套娃里都有一个更小的套娃。
- 非递归DFS:通过显式栈实现的DFS。
- 比方:就像你用一摞书记录你的探险进程,每次都从最上面的一本开始。
二分查找(Binary Search)
适用条件:在已排序的数组中高效查找某个元素的位置,利用分治法缩小搜索范围。
比方:想象你在一本电话簿中找一个名字,你不会从第一页开始逐一查找,而是从中间开始,如果没找到,再继续在前半部分或后半部分查找。
实现步骤:
- 初始化左右指针:将左右指针分别指向数组的起始和结束位置。
- 计算中间位置:计算中间位置的索引。
- 比较中间元素:如果中间元素等于目标值,返回索引;如果小于目标值,移动左指针;如果大于目标值,移动右指针。
- 继续迭代:重复步骤2和3,直到找到目标值或左右指针交错。
注意事项:
- 数组必须是有序的。
- 适用于在已排序数组中高效查找元素的位置。
常见的例子:搜索旋转排序数组、查找峰值元素。
分类或变体:
- 标准二分查找:在已排序数组中查找某个元素的位置。
- 比方:就像在排序的列表中找到特定的名字。
- 浮点数二分查找:在连续函数中查找某个值。
- 比方:就像在图表上查找一个特定的点。
- 变形二分查找:在有重复元素的数组中查找某个元素的位置。
- 比方:就像在有重复名字的电话簿中找到第一个出现的位置。
双指针(Two Pointers)
适用条件:在有序数组或链表上进行高效查找和遍历的方法。
比方:想象你和你的朋友从街道的两端向中间走来,你们会在某个点相遇。双指针就是这样一种方法,从两个方向同时进行查找,以加快搜索速度。
实现步骤:
- 初始化两个指针:将两个指针分别初始化为数组或链表的两端。
- 迭代移动指针:根据具体问题的要求,移动一个或两个指针。
- 检查条件:在每一步迭代中,检查两个指针的位置是否满足目标条件。
注意事项:
- 通常适用于有序数组或链表。
- 可用于解决涉及两个元素组合的问题。
常见的例子:两数之和、三数之和、反转链表。
分类或变体:
- 对撞指针:用于排序数组中的两数之和问题。
- 比方:就像你和朋友在街道两端寻找特定的标志,并在中间碰头。
- 快慢指针:用于检测链表中的环。
- 比方:就像你和朋友在操场上跑步,一个人跑得快,一个人跑得慢,如果操场有环,你们会在某处相遇。
哈希算法(Hashing)
实现步骤:
- 初始化哈希表:创建一个哈希表来存储元素及其索引。
- 插入元素:遍历数组,将每个元素插入到哈希表中,并记录其索引。
- 查找元素:根据键值快速查找目标元素。
注意事项:
- 确保哈希函数能有效分配元素,避免冲突。
- 适用于需要快速查找和插入操作的场景。
常见的例子:两数之和、查找重复元素、有效的字母异位词。
分类或变体:
- 哈希表:用于快速查找、插入和删除。
- 比方:就像你在图书馆按书的编号找到书的位置。
- 哈希集合:用于唯一性检查。
- 比方:就像你在派对上检查来宾名单,每个名字只出现一次。
- 布隆过滤器:用于快速查找的概率数据结构。
- 比方:就像快速筛查一大群人中是否有某些特定人物,虽然有可能出错,但速度快。
贪心算法(Greedy Algorithm)
适用条件:每一步都可以做出局部最优选择,从而希望最终达到全局最优结果的问题。
比方:想象你在爬山,每次你都选择目前看到的最陡峭的上坡路,因为它能最快地让你达到更高的海拔。贪心算法就是这样一种策略,通过选择当前最好的选择,希望最终达到最高的山峰。
实现步骤:
- 排序或选择策略:根据具体问题,确定一个贪心策略,通常需要对元素进行排序。
- 选择局部最优:按策略选择当前的局部最优解。
- 迭代选择:重复步骤2,直到满足问题的要求或所有元素都被处理。
注意事项:
- 每一步选择局部最优不一定能保证全局最优。
- 适用于需要快速找到可行解或近似最优解的问题。
常见的例子:跳跃游戏、区间调度问题、分发糖果。
分类或变体:
- 单调贪心:每一步选择当前最优解。
- 比方:就像你每次都选择最短的路线,试图快速到达目的地。
- 加权贪心:考虑权重因素,每一步选择加权最优解。
- 比方:就像你不仅考虑到达目的地的时间,还考虑费用,选择综合最优的路线。
回溯算法(Backtracking)
适用条件:通过递归尝试所有可能的解,搜索一个或多个满足条件的解的问题。
比方:想象你在迷宫中寻找出口。你沿着一条路走,遇到死胡同时回退到前一个分叉点,选择另一条路继续探索。
实现步骤:
- 选择路径:从起始点开始,选择一条路径进行探索。
- 递归探索:沿着选择的路径递归探索,直到找到一个解或无法继续。
- 回溯:如果当前路径无法找到解,返回上一步选择其他路径继续探索。
- 重复:重复步骤2和3,直到找到所有满足条件的解。
注意事项:
- 适用于需要尝试所有可能的解的问题。
- 回溯算法的效率通常较低,但可以保证找到所有解。
常见的例子:数独求解、全排列生成、八皇后问题。
分类或变体:
- 全排列回溯:生成所有可能的排列。
- 比方:就像你尝试所有可能的钥匙组合来打开一个锁。
- 组合回溯:生成所有可能的组合。
- 比方:就像你选择一个礼盒,尝试所有可能的物品组合。
- 子集回溯:生成所有可能的子集。
- 比方:就像你选择一组人,尝试所有可能的团队组合。
图的广度/深度搜索和二叉树的广度/深度搜索
广度优先搜索(BFS)
图中的BFS:
-
目的:遍历或搜索图中的所有节点。
-
数据结构:队列(Queue)。
-
过程:
- 从起始节点开始,将其标记为已访问并加入队列。
- 从队列中取出一个节点,访问其所有未被访问过的邻居节点,并将它们加入队列。
- 重复步骤2,直到队列为空。
-
特点:图中的BFS适用于寻找最短路径等问题,因为它按层次逐层遍历节点。
二叉树中的BFS:
-
目的:按层次遍历二叉树的所有节点。
-
数据结构:队列(Queue)。
-
过程:
- 从根节点开始,将其加入队列。
- 从队列中取出一个节点,访问其左子节点和右子节点(如果存在),并将它们加入队列。
- 重复步骤2,直到队列为空。
-
特点:二叉树中的BFS也称为层序遍历(Level Order Traversal),适用于按层次处理节点的问题。
深度优先搜索(DFS)
图中的DFS:
-
目的:遍历或搜索图中的所有节点。
-
数据结构:栈(Stack),通常使用递归实现。
-
过程:
- 从起始节点开始,将其标记为已访问。
- 递归访问每个未被访问过的邻居节点。
- 直到所有节点都被访问。
-
特点:图中的DFS适用于寻找路径、检测环等问题,因为它深入到每一个分支。
二叉树中的DFS:
-
目的:遍历二叉树的所有节点。
-
数据结构:栈(Stack),通常使用递归实现。
-
过程:
- 先序遍历(Preorder Traversal):访问根节点 -> 访问左子树 -> 访问右子树
- 中序遍历(Inorder Traversal):访问左子树 -> 访问根节点 -> 访问右子树
- 后序遍历(Postorder Traversal):访问左子树 -> 访问右子树 -> 访问根节点
-
特点:二叉树中的DFS适用于按特定顺序处理节点的问题。
主要区别
-
数据结构:
- 图是任意连接的节点集合,可以有环。
- 二叉树是一种特殊的树结构,每个节点最多有两个子节点,没有环。
-
遍历方式:
- 图的BFS和DFS需要考虑节点是否已被访问,避免陷入无限循环。
- 二叉树的BFS和DFS不需要显式检查节点是否已访问,因为二叉树中不存在环。
-
应用场景:
- 图的BFS适用于寻找最短路径、最小生成树等问题。
- 图的DFS适用于路径搜索、拓扑排序、检测环等问题。
- 二叉树的BFS适用于按层次处理节点的问题。
- 二叉树的DFS适用于按特定顺序(先序、中序、后序)处理节点的问题。
总结
- 动态规划:像拼图,避免重复放置。
- 广度优先搜索(BFS):像城市中找路线,逐层遍历。
- 深度优先搜索(DFS):像森林探险,深入探索。
- 二分查找:像在电话簿中折半查找。
- 双指针:像两端向中间走来,双向查找。
- 哈希算法:像在图书馆按编号找书,快速定位。
- 贪心算法:像爬山,每次选择最好的上坡路。
- 回溯算法:像迷宫中找出口,逐步尝试并回退。