1 - 常见算法种类梳理
-
算法种类
一、枚举算法
1、本质
枚举所有的可能,从所有候选答案中搜索正确的解
2、前提条件
- 候选答案的数量可以预先确定
- 候选答案的范围在求解之前必须有一个确定的集合
3、核心思路
一般使用while循环或者if语句实现
- 确定枚举对象、枚举范围和判定条件
- 逐一列举可能的解,验证每个解是不是问题的解
4、解题步骤
- 确定解的可能范围,不遗漏任何一个真正的解,也要避免有重复(解的可能范围缩至最小以便提高解决问题的效率)
- 判断是否是真正的解
5、特点
最大特点:在面对问题时会去尝试每一种解决方法
6、适用场景
- 便于理解和方便编写代码,建议处理不是很复杂的问题时使用
- 如果要处理的问题很复杂或者很大,那么遍历所有可能性时会消耗很长的时间,这时候就需要考虑更高效的方法
7、实例
- 找出符合条件的5位数: 算法描述题*算=题题题题题题
- 24点游戏:给定4个整数,数字范围在1-13之间,任意使用+-*/() ,构造出一个表达式,使得最终结果为24 例如(9-8)*8*3=24
- 解决熄灯问题
- 讨厌的青蛙
二、递归算法
1、本质
重复性的将问题分解为同类的子问题,然后解决这些子问题,最终完整解决问题
和平常用到的递归函数类似,是一种直接或者间接的调用自身函数或者方法的算法
2、前提条件
- 明确知道你这个函数想要干什么、要完成什么样的一件事
- 然后寻找出递归结束的条件(必须能根据递归结束的条件知道递归结果是什么)
3、适用场景
- 代码的结构清晰,可读性强,非常适合初学者使用
- 运行效率低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多
4、递归和递推的差异
递推像是多米诺骨牌,根据前面几个得到后面的
递归是大事化小,将复杂的问题层层转化为同类简单子问题
一个问题既可以用递推解决也可以用递归解决的时候,选择递推算法,因为递推的效率比递归高
5、实例
- 斐波那契数列(又称兔子数列)
- 汉诺塔问题
- 阶乘问题
- 进制转换器:将输入的十进制数字转换为二进制
- 分解数字:分解出给定多位数中每个位数的数字并按照顺序存放到列表中
- 二叉树遍历问题:先序遍历、中序遍历、后序遍历 及 层次遍历
- 最大公约数和最小公倍数
三、分治算法
1、本质
分而治之,把一个复杂的问题分解成两个或者更多的相同或者相似的问题,再把子问题分成更小的子问题,直到最后子问题可以简单的直接求解,原问题的解就是子问题的解的合并
2、前提条件
处理数据相当多、求解过程比较复杂、直接求解法会比较耗时
3、核心思想
将一个难以直接解决的大问题,分割成一些规模较小的相同或相似问题,以便各个击破,分而治之
- 对于一个规模为N的问题
- 若该问题可以比较容易的解决(比如说规模N较小)则直接解决
- 否则将其分解为K个规模较小的子问题,这些子问题相互独立且与原问题形式相同,递归的去解决这些子问题,然后将各个子问题的解合并得到原问题的解
4、解题步骤
- 将问题分解为多个子问题:将要解决的问题划分成若干个规模较小的同类子问题
- 求解子问题:当将子问题划分的足够小时,用较简单的方法即可解决
- 合并问题的解:按照原问题的要求,将子问题的解逐层合并构成原问题的解
重点解决以下2个问题:
- 找出分割问题的基线条件,这种条件必须尽可能简单
编写涉及数组的递归函数时,基线条件通常情况下都是数组为空或者只包含一个元素。 编程陷入困境时,可以检查基线条件设置的是否正确
2. 确定如何缩小问题的规模,然后不断将问题分解为更小的问题,直到符合基线条件
5、适用场景
- 特征1: 当问题的规模缩小到一定的程度就可以容易的解决问题:此特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般随着问题规模的增加而增加
- 特征2: 问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
- 特征3: 利用该问题分解出的子问题的解可以合并为该问题的解 ---- 能否利用分治法完全取决于问题是否具有特征3,如果具备特征1和2、而不具备特征3,则可以考虑使用贪心算法或者迭代算法
- 特征4: 该问题分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题 ----此特征涉及分治算法的效率问题 如果不满足特征4,可以考虑使用迭代算法
6、实例
- 二分法找出有序列表中的指定值
- 求顺序表中数据的最大值
- 查找列表中元素的最大值和最小值
- 找出一组序列中的第K小(大)的元素
- 快速排序
- 归并排序
- 整数划分
四、贪心算法
1、本质
在求解一个问题时,总是做出在当前看来是最好的选择(即只是得出一个在局部某种意义上的最优解),不从整体最优方面考虑问题
2、前提条件
核心是贪心策略的选择,必须确保选择的贪心策略无后效性,即某个状态以前的过程不会影响以后的状态,只是与当前的状态有关
3、解题思路
从问题的某一个初始解出发逐渐逼近给定的目标值,这样可以尽快求出更好的解。当达到算法中的某一步不能再继续前进时,就停止运行并给出一个近似解
- 从问题的某一个初始解出发,然后按照顺序依次进行,根据某个决策确保每一步都能获得局部最优解
- 在每一步只考虑一个数据,这个被选取的数据应该是局部最优解的条件
- 如果下一个数据和局部最优解连在一起不再是可行的解时,就不把该数据添加到部分解中
- 直到把所有数据枚举完,或者不能再添加算法停止
4、缺点
- 不能保证最后的解是最优的
- 不能用来求最大或最小解问题
- 只能求满足某些约束条件的可行解的范围
5、特点
- 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征
- 贪心算法的每一次操作都对结果产生直接影响,而动态规划算法则不是
- 贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能
- 贪心算法一般是解决一维问题,动态规划主要用于解决二维或者三维问题
6、适用场景
- 当需要实时解决方案且近似答案足够好时,贪心算法最适用
- 贪心算法在确保产生局部最佳解决方案的同时最大程度的减少时间,因此更适用于需要较少时间的情况
- 尽管贪心算法给出了接近最优的解决方案,但是未能找到解决问题的最优方案
7、实例
- 找零问题:给定需要找的零钱数目,如何求得最少的硬币数呢
- 汽车加油问题:对于给定的距离N和加油站的个数K,编程计算最少加油次数
- 求最大子元素之和:给定一个有正有负的整数列表,编写程序计算整数列表内连续子元素之和的最大值
- 幼儿园分糖果问题
- 圣诞节的礼物
- 活动安排问题(贪心算法可以求得此问题的整体最优解,即它最终所确定的相容活动集合A的规模最大)
- 摇摆序列问题:一个整数序列,如果两个相邻元素的差恰好是正负(负正)交替出现,则该序列被称为摇摆序列
- 移除K个数字
- 霍夫曼编码
- Kruskal问题
- Prim算法
五、回溯算法
1、本质
是一种通过暴力穷举的方式解决问题的算法,是深度优先搜索的一种具体应用
2、要素
- 解空间树:是要解决的问题的范围,不知道范围的搜索是不可能找到结果的
- 约束条件:包括隐性的和显性的,题目中的要求以及题目描述隐含的约束条件,是搜索有解的保证
适合解决没有要求求最优解的问题,如果采用,一定要注意跳出条件及搜索完成的标志,否则会陷入泥潭不可自拔
3、核心思想
先暂时放弃关于问题规模大小的限制,并将问题的候选解按照某种顺序逐一进行枚举和检验:
- 如果当前的候选解不可能是正确的解时,就选择下一个候选解(在回溯算法中,放弃当前候选解,并继续寻找下一个候选解的过程,称之为回溯)
- 如果当前的候选解能够满足除了对问题规模的要求外的所有其它要求时,则继续扩大当前候选解的规模,并继续回溯(扩大当前候选解的规模并继续试探的过程称之为向前试探)
- 如果当前候选解能够满足所有要求(包括问题规模)时,这个候选解就是问题的一个解
4、解题步骤
- 定义一个解空间树(子集树、排列树 二选一)
- 利用适于搜索的方法组织解空间树
- 利用深度优先法搜索解空间
- 利用剪枝函数避免移动到不可能产生解的子空间
5、特点
- 在包含问题的所有解的解空间树中,按照深度优先的策略,从根节点出发搜索解空间树
- 算法搜索至解空间树的任意结点时,总是先判断该结点是否肯定不包含问题的解
- 如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯
- 否则进入该子树,继续按照深度优先的策略进行搜索
回溯算法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已经被搜索一遍才结束
回溯算法在用来求问题的任意解时,只要搜索到问题的一个解就可以结束
6、适用场景
- 适用于解一些组合数较大的问题
- 因为要对每一步可能的情况都进行枚举测试,所以在数据量较大的情况下,回溯算法的效率特别低
- 但是它是我们最容易想到的一种算法,在某些小数据量的情况下比较实用,而且结果可靠
7、缺点
回溯算法为了求得问题的正确解,会先委婉地试探某一种可能的情况,在进行试探的过程中一旦发现原来选择的假设情况是不正确的,立即会自觉的退回一步重新选择,然后继续向前试探,如此这般反复进行,直到得到解或证明无解时才放弃
回溯算法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案
- 在尝试了所有可能的分步方法后宣告该问题没有答案
在最坏的情况下,回溯算法会导致一次复杂度为指数时间的计算
8、影响算法效率的因素
- 搜索树的结构、解的分步、约束条件的判断
- 改进回溯算法的途径
- 搜索顺序
- 结点少的分支优先、解多的分支优先
- 让回溯尽早发生
9、实例
- 破解解空间
- 全排列问题:从N个不同元素中任取M个元素,按照一定的顺序排列起来,当M=N时,所有的排列情况叫全排列
- 选排列问题:从N个元素中挑选M个元素进行排列,每个元素最多可重复R次
- 最长公共子序列(LCS)问题
- 排课问题
- 最佳作业调度
- 图的遍历
- 爬楼梯问题:楼梯有N层台阶,每步只能走1级或者2级台阶,从下往上爬,可以有多少种爬法
- M着色问题:无向连通图G和M种不同的颜色,用这M种颜色为图G中的各个顶点着色,要求每个顶点着一种颜色,是否有一种着色法使任意相邻的2个顶点着不同的颜色
- 取物搭配问题:5件不同的上衣、3条不同的裤子、4顶不同的帽子,从中取出一顶帽子、一件上衣和一条裤子作为一种搭配,请问有多少种不同的搭配方式
- 旅行商问题:旅行商要到若干个城市旅游,各城市之间的费用是已知的,为了节省费用,旅行商决定从所在城市出发,到每个城市旅行一次后返回初始城市,应该选择什么路线才能使所走的总费用最短
- 野人与传教士问题:河的左岸有n个传教士、n个野人和一条船,传教士们想用这条船把所有人都运过河去,但有条件限制,请规划出一个确保传教士安全过河的计划
- 骑士巡逻问题
- 八皇后问题
- 迷宫问题
六、迭代算法
1、本质
是一种不断用变量的旧值递推新值的过程,可以将迭代算法分为两种:精确迭代和近似迭代(比如二分法、牛顿迭代法)
2、前提条件
- 确定迭代的变量:这个问题中至少存在一个迭代变量,可以直接或者间接的不断由旧值递推出新值
- 建立迭代关系表达式:这个是关键,含义是描述如何从变量的前一个值推出下一个值的公式或者关系。 在现实应用中,通常使用递推或者倒推等方法建立迭代关系式
- 控制迭代过程:在编写程序时,必须确定在什么时候结束迭代过程,不能让迭代过程无休止的重复执行下去,通常可分为以下2种情况来控制迭代过程:
1)所需的迭代次数是个确定的值,可以计算出来,可以构建一个固定次数的循环来实现对迭代过程的控制
2)所需的迭代次数无法确定,需要进一步分析出用来结束迭代过程的条件
3、特点
递归是自顶向下逐步拓展需求,最后自下向顶运算,即由f(n)拓展到f(1),再由f(1)逐步算回f(n);递归是在函数内调用本身
迭代是直接自下向顶运算,由f(1)逐步算回f(n);迭代是循环求值
5、实例
- 斐波纳切数列
- 角谷猜想
- 使用牛顿迭代法计算方程的根
- 使用牛顿迭代法求极值
- 使用牛顿迭代法求平方根
- 使用牛顿迭代法求极值并绘制曲线
- 使用牛顿迭代法求解输入的方程
- 求方程在X附近的一个实根
- 解决非线性方程组问题
七、查找算法
1、本质
在大量的信息中寻找一个特定的信息元素
基本运算是在关键字之间进行比较
可用平均查找长度来衡量查找算法的性能
2、前提条件
- 列表
- 关键字
- 查找
- 平均查找长度
3、分类
- 比较式查找法:基于线性表的查找法和基于树的查找法
- 计算式查找法:又称哈希查找法
4、适用场景
现实场景中,用到查找算法的效率非常高
解决问题时遵循具体情况具体对待的原则,不要一味追求使用最高效率的算法
建议初学者尽量用多种查找算法来解决同一个问题,这样可以比较不同查找算法的效率,并能深入体会不同算法的思想精髓
5、实例
- 线性表查找:顺序查找
- 线性表查找:折半查找算法
- 线性表查找:插值查找算法
- 线性表查找:分块查找算法
- 基于树的查找法:二叉排序树算法
- 基于树的查找法:平衡二叉排序树算法
- 哈希查找算法
- 斐波那契查找算法
- 红黑树查找算法
八、排序算法
1、本质
针对一连串数据,按照其中的某个或者某些关键字的大小,以递增或者递减的样式排列起来的操作
目的是将一组无序的记录序列调整为有序的记录序列,可分为内部排序和外部排序
2、前提条件
首先要求其具有一定的稳定性,即当2个相同的元素同时出现于某个序列之中,经过一定的排序算法之后,两者在排序前后的相对位置不发生变化
3、核心思路
通过特定的算法因式将一组或者多组数据按照既定模式进行重新排序
4、分类实例
- 插入排序算法
- 希尔排序
- 交换类排序:冒泡排序算法
- 交换类排序:快速排序算法
- 选择排序算法
- 堆排序算法
- 归并排序算法
- 基数排序算法
-
算法应用
一、使用算法解决数据结构问题
- 约瑟夫环
- 操作顺序表
- 操作链表
- 带有尾结点引用的单链表
- 使用多叉树寻找最短路径