我们知道计算机中各类应用程序随处可见的,而支撑这些应用程序运行的就是各类数据结构以及各类算法,这就是经典等式程序=数据结构+算法,上一篇幅中我们列举了一些常用的数据结构,那么今天我们来捋一捋日常开发中常见的一些算法思想以及具体算法各自的特性及相关指标
文章目录
常见算法思想
我们首先介绍下几种常见的算法思想,日常中一些具体的解题算法的思想都依赖于它们,文中列举一些具体算法问题可以自行通过搜索引擎了解
穷举法(Brute Force)
核心思想
顾名思义就是列举出所有可能出现的情况,也称枚举法
特性
- 最简单、最直接、但很暴力的思想(简单粗暴)
- 时间复杂度上也最高(时间耗费高)
- 仅限于数据约束有限的情况(小范围中)
- 能得问题的全部答案(全部解)
适用问题或算法
- 列举100以内所有的素数
- 找出鸡兔同笼问题中所有的鸡和兔的个数
- 找出上海房价在2万-3万的所有房源
分治法(Divide and Conquer)
核心思想
顾名思义分而治之就是该算法的核心思想,递归求解子问题
特性
- 适用于规模大问题求解,将问题拆分成足够小的问题(复杂问题简单化)
- 满足拆分后的小问题和大问题是属于同类情况(处理1条数据和处理n条数据逻辑一样)
- 每个小问题之间不相互依赖(你搞你的,我弄我的)
- 对小问题依次计算后求解后即大问题得到解决(大事化小,小事化了)
- 通常可以通过递归方式来实现分治类的问题(简化代码及逻辑)
适用问题或算法
- 在有序的数列中查找某一个数(二分查找算法)
- 归并排序算法
- 快速排序算法
- 堆排序
贪心算法(Greedy)
核心思想
顾名思义在当前情况下处理问题时都往利益最大化(最优解)的方向考虑
特性
- 可以拆分成多个步骤求解问题(饭要一口一口吃,一口吃不成胖子)
- 求解出每一个步骤中的最优结果及最终的最优结果(利益最大化)
适用问题或算法
- 背包问题(最大价值)
- Dijkstra算法(单源最短路径算法)
- Huffman编码算法(最小生成树算法)
- 理财平台中资产-资金撮合(寻找最合适资产产品和资金人进行交易撮配)
动态规划法(Dynamic Programming)
核心思想
顾名思义就是核心思想就是随机应变,每次遇到问题都会动态调整当前状态,为后续步骤做铺垫
特性
- 将问题拆分成多个子问题(和分治法类似)
- 子问题之间存在依赖关系(下个问题处理依赖于上一个问题的结果)
- 子问题确定后不受后续其他子问题影响(当前做完的事已成定局,后续事项无法影响)
- 分解后会有重复的子问题(记录重复子问题,避免无效计算)
适用问题或算法
- 背包问题(最大重量,最大价值)
- 青蛙上台阶问题
- 两个字符串中最长公共子串问题
回溯法(Backtracking)
核心思想
回溯中文释义为向上推导,当问题从上往下搜索时,发现不满足条件时,尝试原路返回原节点后继续搜索直到找到最终结果(月光宝盒时光倒流)
特性
- 确定问题的结果范围(结果必须存在于一个有限的范围)
- 以深度优先遍历的方式进行搜索
- 基于穷举法思想
适用问题或算法
- 走出迷宫问题
- 八皇后问题
分枝界限法(Branch and Bound)
建设中
关于递归(画外音)
其实在上面几种算法思想中对于具体算法实现时我们都可以通过递归的方式来进行操作,那对于递归我们也可以单独称之为递归算法
初识递归的时候你是否会有这两个感觉:代码原来还可以这么写?思路跟着递归后怎么绕都绕不出来?是的!递归它就是这么神奇!
可能大多数人慢慢对于递归类代码能读的懂,但是对于实际场景进行运用时似乎还是有点无从下手,所以我们先从递归本身出发来看看,我们什么时候可以进行递归处理我们逻辑
线上实际业务场景用到递归场景也很普遍,例如:多级代理商之间的分润处理、裂变推销的个人分享推广业务处理等等
递归函数三部曲
- 问题能否拆分成多个子问题(定位递归函数功能,分而治之)
- 每个子问题的处理逻辑和问题本身处理逻辑是否一致(除了数据范围不一致,100万和1万的区别)
- 是否存在终止条件(死循环可是很可怕的哦)
示例剖析
从示例中我们可以看出整个递归过程完全按照上述的三部曲中所列出的步骤进行
贪心和动态规划(画外音)
这里我们顺便来谈一谈关于贪心算法和动态规划这两者之间共识和区别之处,我们在面对具体问题时是选择贪心算法还是动态规划的方式来处理,需要先分析出最终我们要达到的结果是什么
-
联系
- 都是将问题拆分成若干子问题
- 都是通过逐步推导
- 都运用分治核心思想(划小)
-
区别
- 贪心算法只保证每次子问题求解时是最优结果,每一步完成后不可逆(上一步作为下一步的决策依据),不能保证最终结果是最优结果
- 动态规划记录每次局部最优解(运用了穷举思想),保证全局最终最优结果
-
案例
-
贪心算法
从n个数中进行重新组合是之组合后成为一个最大整数值(比如4个数:21、188、317、4)
-
动态规划
有n个物品,每个物品价值不一,重量也不一,从这n个物品中挑选装进背包中后使背包中所装物品的总价值最高(背包容量有限)
-
常见的算法分类
这里我们先梳理出每种算法的核心思想,以及他们各自的特性
在这之前我们来理解几个重要的概念即衡量一个算法好坏或者说性能高低的指标点
- 时间复杂度
往简单了说就是执行一个算法所需要花费的时间,时间花的越少说明你这个算法在时间性能上越高
但是算法本身执行花费的时间是根据具体要处理的数据量以及数据的分布情况有关,所以在时间复杂度下又进行了一层划分:
- 最好情况时间复杂度
* 比如说要排序的数据恰好全部是已经有序的- 最坏情况时间复杂度
* 比如说要排序的数据恰好全部是已经倒序的- 平均情况时间复杂度
* 不管是最好还是最坏,我们取一个折中点作为该算法的平均值- 渐进时间复杂度
但是一般来说因为要满足所有场景下的需求,所以我们对算法所描述的时间复杂是指的它的最坏情况时间复杂度
- 空间复杂度
往简单了说就是执行一个算法所需要占用的内存空间
排序算法
排序其实我们抽象出来就是对数据进行先比较再交换彼此位置,我们在说排序算法前先来弄清楚几个知识点:
- 算法的复杂度
- 按照时间和空间两个维度去看待
- 算法的稳定性
- 概念:是指待排序的数据中重复元素之间在排序过程中是否进行位置顺序的变换
- 意义:保证数据排序的正确性,如果一个算法不具备稳定性则在进行排序的时候可能会造成数据排序后的结果和预期的结果不一致
- 说明:比如一个电影业务场景中,我们将所有影片按上市时间已经初步排好序后,这时我们可能需要对这些排好序的电影再按归属地区进行重排序,这时如果选择的排序算不稳定则会导致按地区规则排好序后他们上线时间顺序也会被打乱,这样排序后的结果则会达不到预期
- 原地排序
- 概念:这个是站在空间复杂度的维度来看待,指算法在进行排序时除了本身待排序的数据所需的内存空间外无序再额外申请内存空间来辅助处理排序
- 意义:内存空间占用量也是我们衡量算法是否高效的一个重要指标
- 说明:比如一个排序算法时间再快但是他可能需要很高的内存空间来辅助,这样可能在内存空间比较吃紧的环境中是无法运行的
- 有序度
-
概念:是指一组数据中是否具有有序(升序前提下)关系的元素对的个数(如果arr[i]<arr[j]则有i<j)
-
又比如:[1,2,3,4,5,6]这组数据中有序度为15,[6,5,4,3,2,1]这组数据中有序度则为0
-
满有序度:全部有序的情况下我们称之为满有序度,计算规则(n*(n-1)/2)
-
逆序度:其定义与有序度恰好相反,且逆序度=满有序度-有序度(数据初始的有序度)
-
结论:我们对一组数据进行排序的过程其实就是增加其有序度,降低其逆序度的过程,我们可以通过上面的这个等式来计算出我们对一组数据排序时需要进行多少次的比较,比如上图中给出的一组数据我们通过排序还需要对他进行4次排序操作
-
冒泡排序
插入排序
希尔排序
选择排序
归并排序
快速排序
堆排序
计数排序
桶排序
基数排序
排序算法总结
在日常开发中我们在遇到具体问题时到底应该如何选择使用哪种算法呢,这其实我们是需要根据实际的业务场景来决定:比如要排序数据量比较小,我们可以基于用空间换时间的思路选择归并排序,如果待排序的数据量大则可以选则原地排序的快速排序;又比如我们待排数据分布式有规律且范围不是很大则可以选择用桶排序,那如果对于排序数据有多维度多次排序需要考虑排序后的稳定性则可以选择稳定的排序算法;实际场景我们应该掌握每种排序算法的思想以便我们能够灵活运用组合穿插来使用它们
我们总结了最常用、最基础的的10种排序,接来下我们通过一个表格大概列举下这10种算法各个指标以及各种特性以便我们能够基于它们的特性来解决实际的业务问题
算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 原地 | 稳定 | 模式 | 涉及概念 |
---|---|---|---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | ✔️ | ✔️ | 比较 | 数组 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | ✔️ | ✘ | 比较 | 数组 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | ✔️ | ✔️ | 比较 | 数组 |
希尔排序 | O(n) | O(n^2) | O(n^1.*) | O(1) | ✔️ | ✘ | 比较 | 数组 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | ✔️ | ✘ | 比较 | 数组、分治、递归 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | ✘ | ✔️ | 比较 | 数组、分治、递归 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | ✔️ | ✘ | 比较 | 数组、递归、二叉树 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | ✘ | ✔️ | 分配 | 数组 |
桶排序 | O(n+k) | O(n^2) | O(n) | O(n+k) | ✘ | ✘✔️ | 分配 | 数组、散列 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n*k) | ✘ | ✔️ | 分配 | 数组、散列 |
除了上述常用的排序算法外比如还有:快速选择排序、中值排序等等
查找算法
查找是针对在大量数据源中寻找某一特定目标数据的过程
日常生活中我们在图书馆中寻找某一本我们需要看的书籍时,通常通过图书馆里已经建立好的指引(分类)进行找书,其实这类似的场景映射到计算机中则是在一堆存储在计算机硬盘文件或者内存中的数据里找出某些我们需要用的数据的过程在目标数据中进行查找时,目标数据量越大那我们查找所需要花费的时间则越久,如果在查找之前不对目标数据存储制定一定的规则,那我们查找一个数据就像在大海里捞针一样;就像图书馆中如果不对所有的书籍分门别类的建立进行划分,那我们想要找一本自己的想看书那还是有点困难的;那么我们在计算机中想要找一个数据就更加难上加难了,所以我们不管是在日常生活中还是在计算机中对于存储的数据或者物品我们在事先都会维护或者建立一套规则(可以称之为索引),那么后续我们在进行查找时就是根据事先建立好的规则去进行寻找我们所需要的数据(这个过程就是我们要讲的查找算法)
根据数据分布形态维度,我们面对的查找场景可以划分成下列几种
- 静态/动态查找
- 静态查找(目标数据集数据基本上不会有变动)
- 动态查找(目标数据集数据经常有变动)
- 有序/无序查找
- 有序查找(目标数据集已经排好序)
- 无序查找(目标数据集没有任何顺序)
根据目标数据集承载的结构维度,我们可以将查找分为
- 线性表查找
- 线性表是基于数组或者链表这种线性存储结构进行查找
- 树形结构查找
- 树形结构是基于树这种非线性存储结构进行查找
- 散列表查找
- 散列表是基于散列函数计算索引key然后定位到数据元素
- 需要解决散列冲突带来的问题
我们下面总结几种基础常用的查找算法思想,深入了解几种基础的查找算法思想后可以帮助我们在日常业务处理时或者学习各类底层api或者框架时更加得心应手
顺序查找
二分查找
散列查找
二叉树查找
搜索算法
搜索和查找不同的地方在于,查找是在大量数据源中找出某一个具体的目标数据,而搜索是有目的针对某一问题找出该问题对应的部分或者全部的解(答案),这个找出部分和全部解的过程就是搜索
而对于树结构我们通常用到(层次遍历/前序遍历/中序遍历/后序遍历),而对于图结构而言我们则使用广度优先搜索和深度优先搜索,其实对于这两者的遍历搜索算法何其相似,图的广度优先则在树上体现为层次遍历,而图的深度优先在树上则体现为中序遍历
下面我们分别来看看这两种数据结构对应的搜索遍历算法的特性及实现
树的层次遍历
树的(前/中/后)序遍历
来源:CSDN
作者:goblog
链接:https://blog.csdn.net/a164753752/article/details/89359636