算法课堂——初识算法
这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
一直想在博客平台上分享一些技术类的文章,可惜每次一有点想法就因为各种事情耽搁,导致至今都没有什么完整的知识输出,更没有对自己所学的知识进行系统的总结,新的一年到了,正好借掘金的更文挑战,激励自己,坚持输出一些对大家有用的知识。(我可不是想白嫖一台空气炸锅)
好了,言归正传,开始我们算法课堂的第一课:初识算法。
首先,我先说一下我个人对于算法的想法,希望大家能通过这个观点思考一下,我们为什么要学习算法。好,我们先看百度百科对于算法的解释:
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令算法代表着用系统的方法描述解决问题的策略机制。
没错,算法是解决问题准确而完整的描述。但是,这是我们在学习算法过程中的唯一追求吗?如果说获得准确完整的描述就是我们学习算法的目的,那么我们大可去找到一道算法题的优质解答,直接背好就行了。如果这样的话,你大概最终会和大部分在舒适圈徜徉的朋友一样,最终感慨一句:学习算法有什么用啊,都是面试造火箭,工作拧螺丝。 然后躺到床上开了一把王者荣耀。这正是我想告诉你的,学习算法,我们学习的是解决问题的思路,我们培养的是面对复杂问题的解决能力。 这才是我们学习算法的目的,这远比你撸一道leetcode算法题直接提交通过有意义的多。
所以在算法课堂专栏中,我会和大家分享我对于算法题的思考过程以及优化思路。
话不多少我们先来看一道经典的入门算法题,我们大多数人梦开始的地方:leetcode算法题库第一题:两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
看到题目的一瞬间,我们便可以通过题干寻找到我们的第一种解法:既然想知道哪两个数加起来等于给的数,我把所有可能枚举出来不就行了吗?好,说写就写。
第一步我们从数组中拿到一个数;
第二步我们从数组中拿到另一数;
第三步如果两数的和等于目标数,则返回。
第一步和第二步旨在将数组中所有的两两组合都列出来,所以我们采用双重循环,即循环两次数组,拿到所有组合。
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[0];
}
好的,兴高采烈提交代码,完成了人生第一次。
什么?结果只超过了35% 的用户,这你能受的了吗?
没关系,我们分析一下,为啥我们下意识的方法会在耗时上落后其他人这么多呢?或者说多重循环为什么效率这么低呢?
我们拿示例1来看一下双重循环的执行过程:
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
第一步:我们拿到数组的第一个数:nums[0]:2(ps:下标是0,这个都不知道的话,立即推,放弃算法)
第二步:我们依次取下标1,2,3的数,nums[1]:7,2+7=9,符合条件,解题结束,返回[0, 1]。
这个时候,你会说,这不是挺快的嘛?好,我们把题目稍作改动。
示例 1(改):
输入:nums = [0,1,2,7], target = 9
我们再次看下双重循环的执行过程:
第一步:我们拿到数组的第一个数:nums[0]:0
第二步:我们依次取下标1,2,3的数,nums[1]=1,0+1=9,不符合条件;nums[2]=2,0+2=2,不符合条件; nums[3]=7,0+2=7,不符合条件;
第三步:我们拿到数组的第二个数:nums[1]:1
第四步:我们依次取下标2,3的数,nums[2]=2,1+2=3,不符合条件;nums[3]=7,1+7=8,不符合条件;
第五步:我们拿到数组的第三个数:nums[2]:2
第六步:我们还剩下最后一个数,nums[3]=7,2+7=9,符合条件;解题结束,返回[2,3]
那如果我们题目再稍作改变呢?
示例 1(改):
输入:nums = [0,1,3,4,5,6,7,8,9,10,11,12,13,14,15], target = 29
是不是用这种双重循环法(暴力枚举),就会力不从心了呢?
也就是说,给的数组的不同,会严重影响双重循环算法的执行效率,具体用什么来衡量这个执行效率,先不急,咱们接着往下看。
上面说到,双重循环法在我们解题中出现了瓶颈,我们首先分析下这种方法它的劣势在哪?
很明显,我们每此都是从数组里面拿出两个数相加,直到我们找到答案或者枚举出所有可能性,也就是说我们每次都要从数组里面那两个数这个操作太繁琐了,我们可不可以每次多比较一些?举个例子
存在一个数组[0,1,2,3,4,5] 和一个数:6,问数组中有没有一个数与6的和为11?
你会脱口而出有,但我我不信你是循环数组,0+6=6,不符合;1+6=7,不符合.......,5+6=11,符合。所以有
因为正常人思考是这样的,很简单,11-6等于5,一看里面有5,然后回答,有
好,那么我们把这种思想运用到这道题中,还是第一个例子
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
第一步:我们拿到2,只有一个数,不符合,但是我们把它放到一个容器里,你可以理解为一个糖果罐,
第二步:我们拿到7,这个时候我们只需要看糖果罐里有没有2,如果有那么问题就解决了。
那么我们看下示例1(改)呢?
示例 1(改):
输入:nums = [0,1,2,7], target = 9
第一步,我们拿到0,只有一个数,不符合,我们把0放进糖果罐里
第二步,我们拿到1,我们只需要看糖果罐里有没有8,没有,我们把1放进糖果罐里
第三步,我们拿到2,我们只需要看糖果罐里有没有7,没有,我们把2放进糖果罐里
第四步:我们拿到7,我们只需要看糖果罐里有没有7,有,问题解决了
可以看到无论给出的题目怎么样,我们的步骤数最多是数组的长度,通过几次比较,就能解决问题。
那么留给我们的问题就剩一个了,就是这个糖果罐怎么来的。仔细思考下,我们最终要知道的是,数组里面的哪两个位置的数的和等于我们给出的目标数,所以糖果罐里面放的“糖”我不仅要知道“糖”的数值是多少,还要知道“糖”在数组的位置。
这里我们引出一个新的数据结构--哈希表,这种糖果罐,它里面可以存放好多糖果,并且每颗糖都有包装和里面的糖果,用在我们的题目里,我们不仅可以把数组里面的数放进去,还可以一同把它们的位置也放进去。 具体代码如下:
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);
}
return new int[0];
}
}
我们再次提交,看看有什么表现:
嗯。。。。。。。。时间是快了许多,但是内存消耗怎么变多了,很显然嘛,我们引出了新的数据结构, 所以我们的内存消耗就比之前多了
那么这个内存消耗又是用什么衡量呢?
结合leetcode的提交详情我们可以得出:执行用时&内存消耗是用来表示算法的执行效率。
那么在算法中,我们是用什么来用准确的标准来衡量这两点呢?
那就是算法的时间复杂度和空间复杂度
这两点也正是我们优化算法过程中所着重注意的地方。
那么针对一个算法,到底如何确定它的时间复杂度和空间复杂度?
我么又如何具体的观察出我们所写的算法的时间复杂度呢?
且看下一篇文章: