前言
兄弟,想一起学习算法吗?想一起变强吗?想毕业的时候在算法方面吊打面试官吗?想成为刷题狂人吗?
快来联系我,一起互相监督,一起征服力扣~~
我的伙伴,刷题四天王(自己封的):
那还等啥!算法那还不得赶紧学起来!这一激动我这包整的河南话都彪出来了。
一、什么是算法
什么是算法?我们为什么要学算法?算法是为了什么才出现的?算法是怎么从解决一个问题慢慢到解决一个体系的问题的?
想要学习算法,如果你要求不高,你可以不学这个,你只需要知道怎么用,如何用,什么地方用就可以了~
但是如果你对算法想有深入的了解,那么这些你是需要知道的。
1.1 绪论
想必大家都听过这样一句话:数据结构+算法=程序
你可能会说:屁嘞,我不会数据结构与算法,我也照样写程序。我只能说:能说出来这话,一看你就是个新手!
你了解什么是数据结构吗?你了解什么是算法吗?不要把它们看得那么高大上,他们的原本定义其实很简单的~~
数据结构:
提一嘴基础知识,在编程语言中,数据是信息的载体,而数据又分为值类型和引用类型。值类型无非就是整数,实数,复数等,而引用类型就是那些数组,字符串,集合图片,音乐等。
有位老哥说过:任何数据都可以使用数据结构表现出来。其实意思就是,数据结构就是存储数据的结构体。
其实数据结构的定义就是这么简单,不要看的太高
算法:
一个程序,有一种书本上的描述:解决一个问题除了要有一个数据结构来表示和组织信息外,还需要一系列步骤来完成问题的解决方案。
当然了,第一个逗号前面的那当然就是数据结构了,那后面的一系列步骤其实说的就是算法。
也可以称为:操作数据结构的方法;或者称为:解决问题的方案。
那么看完这两个概念,你是否对数据结构与算法有一个了解了呢?你是否还会说,我不会数据结构与算法也能写程序了呢?
有点模糊也没关系,我来带你看几个例子,你就明白了~~
案例:
例子1 HelloWorld:
HelloWorld也能当数据结构与算法的例子??!没错,没看错,他能!他能!!
java代码:
public class Hello{ /* 在这里Hello和World就是两个数据结构 将两个数据结构用+拼接起来,就是算法 */ public static void main(String[] a){ System.out.println("Hello"+"World"); } }
Python代码:
print("Hello"+"World")
同理
例子2 两个数字相加:
输入两个数字,输出两个数字的和
Java代码:
public class Hello{ public static void main(String[] a){ System.out.println(sumTwo(5,6)); } public static int sumTwo(int a, int b){ return a+b; } }
这里面没有数据结构,只有时间,也有算法,算法就是a+b
Python代码:
def sumTwo(a:int,b:int)->int: return a+b
例子3 两数之和(来真的了啊!):
给你一个非空数组,和一个目标值,从数组中找出两个元素使其的和与目标值相同,并返回一个长度为2的数组装这两个值对应的下标,只允许一个元素使用一次
————源于力扣第一题,简单难度
Java代码:
public class Hello{ public static void main(String[] a){ System.out.println(); } public static TwoSum(int[] nums1, int[] nums2, int target){ /* 最容易想到的,也是没学过系统学习过算法的人的方法 嵌套循环: 遍历nums1,再遍历nums2,然后求和,与目标值做判断 当然,想法很简单,却会出错,因为还要考虑元素只用到一遍 */ for (int i=0;i<nums.length;i++){ for (int j=0;j<nums.length;j++){ if (i!=j){ if (nums[i]+nums[j]==target){ return new int[]{i, j}; } } } } return null; } }
这里面就用到了一个数据结构,叫做数组,这里的算法就是将新数组求出来的过程!
Python代码:
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: for i in range(len(nums)): for j in range(len(nums)): if i==j: continue elif nums[i]+nums[j]==target: return [i, j]
当然,例3的方法是很简陋的,更简单的方法还有哈希法和二分法,在这道题中,哈希法的时间复杂度是O(n),二分法的时间复杂度为O(nlogn)
感兴趣的话,可以看看我的这一篇博客:零基础的我刷力扣一周后,总结了点东西
这三个例子大概也能让你对什么是数据结构与算法有一个大致的了解了。那么接着看吧~
二、为什么要学算法
既然知道了算法是什么,那么你知道为什么要学习算法吗?
有人可能会说:因为我想在找工作的时候更有竞争力,更有话语权,所以我来学算法!这个当然没错,面向加薪学习~~
当然了,除了这个,也没什么别的办法能够令人疯狂了吧~~兴趣?有,很少的~
那么为什么公司喜欢会算法的人呢?换句话说,为什么会算法的人会更吃香呢?
因为公司运行你的代码是需要机器的,而机器也是分价钱的,如果你的算法很糟糕,那就会让公司的机器运行起来占用CPU资源多,拟人化一下那就是会很吃力,而且运行你得代码会更耗费时间。
而如果你会算法,甚至算法很好,那么你的算法就会让机器如释重负,甚至跑的更快!
想一想,机器同样的价钱,你会算法能够让机器干更多事,那公司肯定要你啊!别说工资,那一个机器都好贵的。
而算法的出现也是因为先有了问题,之前也说过算法是什么了,就是一个问题的解决方案,所以我们的算法是为了解决问题才出现的。
三、算法的特性
当然一个问题都是可以由多个算法解决的。俗话说,无规矩不成方圆。
那么不同的算法是不是得有相同的规则,或者说相同的特性,才能约束算法,不能任意一种算法就可以解决问题。
打个比方,就用曹冲称象这个例子吧。为了不浪费大家的脑子,我决定就说的简单一点。
现有大象一头,需要知道大象有多重。但是秤太小,无法精准测量。
请你设计一个算法来计算大象的重量。
曹冲的方法:把象放到大船上,在水面所达到的地方做上记号,再让船装载其它东西,称一下这些东西,那么比较下就能知道了。
方法1:我把大象切碎,一块一块量
方法2:造一个超大的秤,专门用来秤大象的重量
你觉得方法1和方法2怎么样?先不急着回答,我们先看看算法的这几个性质。
1. 有穷性
一个算法必须要有一个明确的终止条件来控制处理过程的终止,或者说结束。
什么意思呢?就是你的算法至少要有一个终止条件,或者说能够在特定时间内终止,并计算出结果。要不然你的方法有什么意义呢?
2. 确定性
你的算法的每个步骤都是有明确的目的,不能有别的意思。
就拿上面的例子,你说要把大象切碎,不能切着切着,我还是再造个秤吧。
3. 可行性
你的算法的没一个步骤都是可以被机器或者人执行的。
就像曹冲的方法一样,是切实可行的,而像方法12,则多少有点不切实际了。
4. 正确性
你的算法所计算的结果是能够用来解决这个问题的。
不能说你的算法无法解决问题,那样你的算法也是毫无意义的。
那么,在看完这四个特性后你怎么回答上面的问题呢?
当然,答案我也在特性里面说了出来,还不知道答案的,面壁思过去!
四、算法的分析
那么讲了这么多,我们怎么判断我们的程序是好是坏呢?那就是时间复杂度和空间复杂度。
是不是感觉这两个名词好难理解?没事,他其实也不难。
1. 时间复杂度
所谓时间复杂度就是算法执行的时间。而时间复杂度可以由两种方式:
- 计算程序实际运行时间
- 计算程序相对运行时间
我们知道,即使是同一种算法,在不同的编程语言,不同的机器,不同的环境索引行的实际时间也是不一样的。
所以第一种方式,就不是很银杏化!那么就只能是第二种方法了来判断我们的程序是否得到了优化~~
而时间复杂度又分为三种,最优情况,最劣情况,平均情况。而这个情况是由问题来决定的~
这三种情况我就不多说啦~看名字就知道了
时间复杂度统一使用O(x)来表示,也是俗称的大O表示法,而时间复杂度的大小与问题有关。
比方说:
- 我给你三个数,你把他们三个从小到大排序,时间复杂度是O(3),也可以说是 O(1)
- 我给你10**100个数,你把他们从小到大排序,时间复杂度是O(10**100),也可以近似取到O(1)
- 我给你几个数,你把他们从小到大排序,时间复杂度为O(n)
那么为什么改了一下题目,时间复杂度就不一样了呢?因为1,2是确定的值,不会受到外部影响而改变,所以只需要循环那么多次,不会改变所以时间复杂度是O(1);而3不一样,他给的值是不确定的,所以需要循环n次,时间复杂度就是O(n)
当然这个也是可以根据问题改变的,那就是对特定的值使用特定的算法。比如二分法,递归的时间复杂度就是O(logn),而O(n)是大于O(logn)的
而如果需要嵌套循环,那么可能是O(n**2)或者O(nlogn)。
常见的时间复杂度按照从小到大的顺序排列有:O(1),O(logn),O(n),O(nlogn),O(n**2),O(2**n),O(n!)
2. 空间复杂度
根据算法在执行过程所使用的存储空间,分为堆区和栈区,当然还有别的区。
而栈区中存放着变量名,堆区中存放着变量名指向的值。这么说可能不好理解,那就画个图吧~~
诶😱😨叭叭了这么多,好像还没到点上!
稍安勿躁嘛~~
这就来空间复杂度啦~
其实和时间复杂度差不多,我们使用常数,实数,或者对数组,字符串进行原地修改的时候,空间复杂度就是O(1),因为没有开辟新的空间
而如果是新建一个字符串,数组就是需要新开辟一个数组或者字符串空间,所以空间复杂度为O(n)
五、算法
关于一些生活中常见的算法…
这里不讲代码,只讲原理!
1. 贪婪算法
在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法 。
他也叫贪心算法。
举个栗子:
你去超市买东西,买了十三块钱的东西
你拿了一百块钱,给收银员,收银员会给你找零钱
贪婪算法就是:
先找你一张50的,再找你一张20的,再10的,再5的,再1的,再1的
当然这也是一般收银员会选择的方法,通过找给你最少张的纸币,找给你所需要的钱
这就是贪婪算法
贪婪算法所能解决的问题的特性:
- 该问题至少有一个最优的解决方案。只有这样,才可能为解决问题构造候选方案集合。在前面的例子中候选方案集合就是不同面值的纸币组成的纸币集。
- 随着问题的求解将建立另外两个集合,-一个集合(假设为集合a)由已经被选出并符合问题要求的候选解组成,另一个集合(假设为集合b)由已经被选出但并不符合问题要求的候选解组成。
- 集合a最终要包含问题的解答。在集合a中未出现解答时,问题的候选解集合中至少还要剩余一个候选解。
贪婪算法的有点事简单,直观,容易实现,但是缺点也很明显,那就是他会认为当前选择是满足条件的就进行下一个,而不管当前操作是否会对下一操作产生影响。
2. 分治法
- 分治法就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
- 说简单点就是:把一个复杂的问题分成几个简单的问题,如果分的还不够简单,那就接着分,然后再把各个简单的问题解答,最终的答案就是所有简单问题答案的并集
举个栗子:
给你一个有序数组和一个目标值,请找出这个元素的下标
我们使用二分查找法
Java代码:
public class ErFen{ public static void main(String[] a){ } public static int Defg(int[] nums, int target){ int start = 0; int end = 0; int mid = 0; for (int i=0;i<nums.lenrth;i++){ mid = (start+end)/2; if (nums[mid]>target) end = mid; else start = mid+1; } return mid; } }
Python代码:
def Defg(nums:list, target:int)->int: start = 0 end = 0 mid = 0 for i in range(len(nums)): mid = (start + end) //2 if nums[mid]>target: end = mid else: start = mid return mid
看不懂?没关系,看这是什么!!!大宝贝!——LeetCode刷题278-简单-第一个错误版本
分治法所能解决的问题的特性:
- 整个问题可以分解为两个或者多个较小的问题
- 对每个子问题的求解类似于对整个问题的求解
- 如果子问题的规模或者难度仍旧很大,那就继续使用分治法
分治法的难度较高,技巧性较高,需要使用者对问题有着透彻的理解,效率也不高。
但是能解决很多复杂的问题。
3. 动态规划
在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法
这个算法是极其难以理解的,就算理解,代码也很难敲出来。
而也可以这么理解,动态规划的塑像是与分治法的思想相反的,如果说分治法这种吧一个问题分解成多个简单问题的方法叫做自上而下的方法,那么动态规划就是一种自下而上的解决方法。
而动态规划也不会出现贪婪算法那种当前操作最优而不再以下一操作的情况。
举个栗子:
计算二项式Cnm的值
使用杨辉三角可以根据当前二项式生成更简单的二项式
而不会重复生成更简单的相同的二项式
所以才叫动态规划
动态规划解决的问题的特性:
- 算法要解决大量重复的工作,计算大量重复的数值。
- 至少有一 些新的数值可以通过已经计算出的数值组合出来。
- 问题要具有一定的规模,这样动态规划算法的好处才可凸显出来。
- 使用分治法划分 子问题时,子问题之间的关系界限不清,相互交叉。一个子问题的求解与多个已经解决的子问题有关。
优点是效率高,缺点是太难了~~
有好多人都不会用,包括某菜鸡博主~~
4. 回溯法
回溯法是一种优化的穷举法。 所谓穷举法就是穷举问题的所有可能性,直到发现解决问题的最佳解为止。当然利用穷举法可以解决很大一部分的实际问题,但由穷举法的实现过程可知它所花费的资源是相当多的。使用穷举法进行彻底的搜索彻底的比较,要以大量的运算为基础,而大量的运算势必要花费大量的时间,有时这种消耗是计算机也承担不起的。所以用穷举法解决问题往往是不实际的。
说白了还是穷举法~~
举个栗子:
你站在一颗形状类似于一个思维导图的头结点
你想查看某个尾结点,但是计算机可看不到,所以使用回溯法
回溯法就是让你先沿着最左边的一直走
一直沿着最左边,直到尾结点,你发现不是自己想要的
返回上一个父节点,再往右走,不是,回到父节点的父节点
继续往右,然后再往左…
循环往复,直到找到你想要的尾结点才停止。
回溯法解决的问题的特性:
- 问题可以划分为几个不相关的子问题
- 几个字问题有类似的解决方案
- 如果子问题还可以划分,就继续划分
缺点:由于是穷举法,所以效率极低
5. 概率算法
概率算法允许算法在执行过程中随机地选择下一个计算步骤。在很多情况下,算法在执行过程中面临选择时,随机性选择比最优选择省时,因此概率算法可以在很大程度上降低算法的复杂度。
他也叫,随机算法。
说直白了他就是碰运气,但是总能碰到正确的答案,但是得不到精准的答案。
只有在某些场合才会使得效率增大。
举个栗子:
比如说某国的总统选举
不让所有人进行投票选举
而是随机找一些人进行民意调查
最后统计结果
支持率搞得就是新的国家总统
效率是不是提高了!
你可以说结果是正确的,但不能说结果是精准的
优点是在某种情况下能使得解决问题的效率大大提高,缺点是不是所有的问题都适用。
结语
希望阅读完我这篇文章,能够让你对数据结构与算法打下基础。
我是布小禅,一位初入IT界的萌新,希望多多关照~~
兴趣是最好的老师,坚持是不变的真理。
学习不要急躁,一步一个脚印,踏踏实实的往前走。
每天进步一点点,日积月累之下,再回头看看,你就会发现自己已经变得很厉害了。