科目一考试和算法学习的思考和体会(下篇)

摘要: 这篇文章主要是接着上一篇中谈到的科目一算法考试中,我认为可能常考的一些知识点。如果有说得不全的欢迎大家补充。同样,如果提到的有错误,也欢迎大家批评指正。另外补充下,我这里介绍的内容都是针对科目一LeetCode的,OJ方面的题目没有涉及。

这是我写的针对咱们的科目一算法考试的思考和体会系列文章的最后一篇,其他的链接:

科目一考试和算法学习的思考和体会(上篇)

科目一考试和算法学习的思考和体会(中篇)

什么时候考滑动窗口?

上一篇中我说,如果考试中出了关于二叉树或链表的问题,是有些“不公平”(具体原因参加中篇)的,那么相对来说,考察字符串类的问题,就非常的“公平”,不涉及复杂的数据结构,易于调试,就是纯靠大家对题目的理解和编程技巧了,当然,因此字符串类的问题难以预测,这里仅仅提两个我觉得给我自己带来思考的题目,建议大家可以看下,可能大家理解了这类问题的思路后,会觉得豁然开朗。

上一篇中也提到,递归是一种编程技巧,很值得学习,动态规划、记忆化搜索都是一种带优化的递归,它们的出题特点是,往往有一个“最”字,比如最短路径,最大子序和,最长回文子串等等,所以大家可以通过上一篇中分享的那篇文章记住这几类问题的处理方法,遇到带“最”字的问题先想想是不是能不能套用这几种模型去解决,如果不行,而这道题还是关于字符串的问题,那么就可以考虑是不是滑动窗口类型的问题。以这两个题解为例:

LeetCode 209

LeetCode 219

通过这两道题相信大家可以感觉到滑动窗口这类问题的特点,发现这两点就可以考虑是不是滑动窗口类问题了:

  1. 需要遍历字符串,但最终关注的往往是一段数据(也就是滑动窗口)的长度,而不需要关注具体的值,以第一个题目为例,它关注的是连续的几个数组值之和大于等于target时的下标长度,而不是关心具体这些值,第二个题同样是声明了长度不超过k;
  2. 遍历时可能有很多重复子问题,比如以第一题为例,当你遍历了从第1个到第3个元素的时候,实际就已经包含了从第一个到第二个元素的和,或者说,从第1个到第3个元素,可以由第1个到第2个元素的和转化而来,这种想法其实是来自对暴力法的观察,发现暴力法会有很多重复的计算时,就可以考虑用滑动窗口,因此,建议大家可以认真理解下LeetCode 209这道题,体会滑动窗口的思路和最佳实践。

歧路亡羊,如何是好?

虽然关于字符串类型的问题多种多样,这里还是有一类问题想给大家介绍下,就是和IP相关的问题,这之中包含了剪枝的思想,什么叫剪枝?以一个成语“歧路亡羊”为例:

从前杨朱的邻居请他派仆人帮他去找丢失的一只羊,杨子问他为什么要那么多的人去找,邻居说丢失羊的路上有很多岔道,所以要很多人去找。过了很久,派出去找羊的人回来说没有找到,邻居说岔道上又分许多岔道,根本没法找了。

但是假如,找羊的人在每个岔路都有技巧判断羊唯一能走的路线,按理来说,这个羊就能找到。这就是剪枝的思想,你可以通过

LeetCode 93

这道题的题解再去理解下剪枝的思想。多看一些题目,你会发现很多人都是这样分析算法问题的:如果遇到的不是自己能不假思索就说出解题思路的问题,先用暴力法去分析,然后结合数据规模再去想怎么剪枝,甚至直接把暴力法的代码提交,不能通过的话再优化,优化的思路就是剪枝,甚至你会发现,前面介绍到的动态规划,也可以说是一种剪枝,有个著名的算法——二分搜索,它背后有这么个故事:二分搜索的思想,是在1946年左右被提出的,然而,第一个没有bug版本的二分搜索代码,居然是在16年后才完成的。相信很多同学会说,为什么考试的时候,我的代码明明是对的,能通过它题目中给的那几个用例,但是提交上去总是显示解答错误/超时,其实你可以理解成算法考试就是你和用例的博弈,如果遇到提交代码显示解答错误,你可以根据题目给你的用例稍作改动变成新的用例看看程序是否正常,如果遇到提交代码显示超时,可以从剪枝的角度考虑下,是否那里可以剪掉,有时你会发现,拦住你的往往就是那一个用例(练习时可以看到自己的某次提交有多少个用例没通过),而这个用例就对应着一次剪枝。

有没有必考题?

前面给大家介绍的很多知识,都是我觉得很有必要掌握的,但是要说他们一定会考吗,不一定。那么有没有必考题?哈哈,还真有。就是系统设计题。

相比于很多时候我们自嘲自己是”码农“,”搬砖的“,甚至最近大火的”农民工“,”架构师“似乎是个更好听的名字?。那怎么成为架构师呢?推荐大家看看这篇文章,相信它会为你打下架构师的根基。另外这篇文章真的很赞,很多人都推荐过,比如某知乎答主

当然,上述截图是他对另一个问题的回答,如果你对这位笔耕不辍的答主很感兴趣,或者只是单纯的觉得他很傲娇想跟他掰掰手腕(没错,我说的就是坐在一起扳扳手腕那种),都欢迎你联系我,加入我们,你会尽快实现心愿。

其实上面介绍的系统设计比咱们考试的系统设计要难,如果大家想往更高处攀登,可以考虑看下。下面我们来说说我们科目一中的系统设计题。

不知道从什么时候开始,咱们公司的算法考试都至少有一道系统设计题,但是考的难度,大多是类似于让大家实现一个系统包含增删改查的功能等。如果大家对这种类型的题目有疑问的话,可以参考下这道考过的真题感受下它想考什么:

0521上机编程“租房信息查询系统”

说下我的看法:

  1. 一般增删改查,都是针对数据库说的,那么这种题目里没有数据库,往往就会利用另一种数据结构来代替数据库表,比如map,或者list,所以大家一定要对常见的map,list比较了解,以Java为例,HashMap跟TreeMap有什么分别,各自的使用场景是什么,考试前大家很有必要了解一下;
  2. 我们用到数据库时,经常会对数据排序,这样就好看了,那么,怎么对一个对象排序,也应该是大家重点练习的点,我举个例子,假如有个学生对象Student,它有属性学号id,姓名name, 身高height, 希望你对若干名学生进行排序,规则是个头最高的排最前面,如果一样高,按字典序降序排,这种代码怎么写,大家应该烂熟于胸,当然,如果大家不熟悉的话,可以仔细看看上面真题中推荐的写法,甚至可以背下来,考试大概率用得上;
  3. 这道系统设计题我认为是性价比最高的题目了,建议大家一定做好准备拿下它,这样考试通过的概率也就大大增加了。为什么这么说?因为首先,这种题目很好调试,假如你写的代码有问题,你也可以通过调试发现问题(按题意创建对象,调用对象方法,就能在本地调试。这比起上一篇中介绍的那种让大家没有棒子的情况下练打狗棒法的题目温柔多了),另外,就是这种题易于考到上述两点,list,map,对象排序方法,以及集合的筛选方法等,稍作准备即可;

有没有对我们“格外公平”的考点?

我上一篇中提到有可能考“不公平”的题目,这一篇提到了考字符串的题目就很“公平”,那么有没有对我们“格外公平”,甚至过于公平,显得有些偏向我们的考点呢?其实,也是有的。举个例子,大家看下这道题

你会发现它简单到不可思议,甚至只需要写这么一行代码就够了:

但实际上,看看题解,你会发现,原来这道题考的是让你自己实现这个开根号的方法。也就是说有些时候算法考试往往是要求你实现某些算法的,比如你去面试,你这样写代码面试官一定会说你能不用这个方法去做这道题吗,所幸考试是机器判题,没人管你是不是用了已有工具类方法。

当然,咱们的算法考试中也不会考这么简单的题,但是我想通过这个提醒大家,可以重视一些基本的已有数据结构的运用。同样以Java为例,举个例子:

LeetCode 20

大家都知道这道题应该用栈实现,你知道Java中有这么个类吗?——Stack

再比如说,

LeetCode 347

大家应该很容易看出,这道题应该用堆实现,那你知道可以用Java中的这个类吗?—— PriorityQueue

另外347题还考了一个基本功:比如我想把这个数组:[1, 2, 3, 1, 2, 1]放在一个map中,map的键是数组中出现过的各个数值,值是它们每个值出现的频次,Java写这个功能只需要3行,应该怎么写?

再比如说,有道比较难的题目,需要你实现LRU算法,LRU算法简言之就是HashMap+双向链表,但假如你知道,Java中LinkedHashMap就是HashMap+双向链表。。。我猜你做梦也会笑醒的?。

这里想告诉大家的是,不管你使用哪门语言,熟悉这门语言中自带(注意,是自带,比如你使用Java,然后你引入Guava中的数据结构,这个不行的哈)的数据结构,对你的上机编程是大有裨益的。比如上面这两道题,记住它们的解法,这类题目有可以做为你的“武器”将它们用于其它堆、栈类题目上。

考纲中剩下的内容

第一篇中我有分析过咱们的考纲,写到这里主要介绍了我之前说的递归,带优化的递归,滑动窗口(双指针中的一种),系统设计题(多是排序加数据结构的考察)这几类问题的解法。相信大家也能看出,一定程度上,对于某些基本的题目或者称为模型的题目,我是建议大家烂熟于胸甚至背下来的,那么如果你复习完了这几类题目,还想看看考纲中其他类型的题目,我建议你可以结合我之前推荐的这本电子书去了解下,你会发现,很多算法问题都是有模板的。比如回溯问题,大家可以看下LeetCode 17LeetCode 200这两道题的回溯法的题解,你会发现,模板简直是一模一样,这两道题都有一个“往下一步”的概念,而们唯一的区别就是这个“往下一步”的代码,17题是用一行代码实现的,而200题使用一个for循环实现的。就像我们请别人吃饭,假如原则是“吃好喝好”,那按这个原则,你请别人吃饭时只给别人点一个肉夹馍,就不行,因为没喝的。而17题就像是你请别人吃路边的干脆面喝饮料(有吃有喝哦),200题就像是你点了四菜一汤给别人(虽然这两种方式请人吃饭给人的观感大不相同,但都遵循了有吃有喝这个我们规定的原则)。建议大家有余力的话,可以多刷几道不同类型的题,找找你喜欢用的模板。而且很多题目可以多题一解或者一题多解,只要找到了你擅长的模板,很多问题都可以迎刃而解,比如上面举例的LeetCode 200这道题,我的同事李云蛟因为过于擅长使用并查集,因此他就用并查集解了这道题,还写了篇博客,是的,当你手里有了几把趁手的工具(比如扳手)的时候,可能看哪个算法题都想修理,矫正一下它?,修理完之后,再去踢个球也是极好的。

写在最后的话

本来想单独通过一篇文章介绍下我自己学习算法的感悟,不过上面想写的内容太多,感悟在这里简单说下吧:我觉得概括起来的话,学习算法有两个好处:

  1. 它能帮你打通“任督二脉”:我相信有很多同学(也包括我)都感觉自己遇到了瓶颈(当然,只是说技术,身高方面我相信大家早都遇到了瓶颈),也有很多同学抱怨,“天天让我写增删改查,我被低代码平台淘汰了怎么办?”,当然,有个办法就是加入我们,因为我们团队在做低代码平台,这样就很难被淘汰了,也在做其它有点意思的平台,或者你可以通过算法与数据结构提升下,我举个例子,很多前端同学都知道,第一次打开某个网站可能比较慢,后面再打开就会快很多,原因肯定是浏览器有缓存,不管是通过Nginx配置还是什么别的方法做到的,但你知道,这是因为浏览器端有缓存;很多后端同学都知道,我们经常查询走数据库某些不常变化的数据是不明智的,好的做法是把内存当数据库,把这些不常变化的数据缓存在内存(比如使用Redis)中;那么你是否想过,其实这就是我前面介绍过的算法中的记忆化搜索,很多同学都知道React中的diff算法从O(n^3)优化到O(n),都知道谈Java必谈高并发,谈高并发必谈信号量,CyclicBarrier这些,那么为什么O(n^3)的算法必须要优化,而操作系统诞生的时候就在考虑通过创建栅栏,信号量的概念调度,解决资源抢占问题了。而这些问题的解决,最后都要依托合适的算法与数据结构。大家可以想下假如你去参加一场Java面试,面试官问你,“有了解过J.U.C吗?”,你说,“听说过,没在项目中实战过。不过我之前就对操作系统的调度算法有所了解,要不我结合哲学家就餐这个问题简单谈下?”,或者你去参加一次前端面试,面试官问你,“会写递归吗?”,你说,“我是做组件库开发的,不常写递归,不过我对React的Fiber架构有所了解,要不我结合React中的render阶段的beginWork和completeWork的流程说下?”,我猜这样即使你简历上没写什么太亮眼的工作经历,结果也一定不差(当然,怎么高效的学习算法,避免前学后忘,这就是另一个话题了,如果没有那么多时间去钻研的话,可以考虑了解下liuyubobobo这个人,听他讲讲);
  2. 它能让你明白“舍得”的道理:从我们学习算法中不难发现,大家能学到的算法中,很少有一个算法A既包含了算法B的所有优点,却又没有算法B的所有缺点。如果有,那么算法B一定不存在,在历史长河中被淘汰了。包括很多系统设计题,你会发现它看似是让你“既要又要还要”(比如LRU算法,既要便于添加又要便于删除还要便于查找),但一定程度上它也会做一些舍弃。当然,很多算法设计有意思的点就是,如何根据当前的需求做取舍,保证这个需求的实现(如果大家感兴趣可以去搜下Trie这个数据结构),因此软件行业常谈到这个词:最佳实践。另外就是,假如有个房产中介告诉你这里有套房子全是优点,也不那么贵,还不是万人摇,你现在就能买,你一定要查查,是不是因为他忘了告诉你这小区对面是个说要迁走但迟迟没迁的坟地(这是我真实遇到的情况)。很多技术也是,他们也是经历了时代的“舍·得”的,比如现在大热的Vite,前几年兴起的Docker,16年前的Ajax,你可以说是时代选择了它们,但这些技术都不是当时横空出世的新技术,而是早已有之的技术组合,只是之前的时代不是它们“得到天下”的年代。所以,我觉得很多事,大家都可以带着一些“不以物喜,不以己悲”的态度去面对。比如,对待算法考试这件事,就可以用这样的态度?,刷题,提升自己的能力,收获更多的思考和体会,我觉得都没问题,但也没必要执着于一个分数。就像第一篇中介绍的Max Howell一样。最后,分享一首小诗,与大家共勉。
一只船孤独地航行在海上,
它既不寻求幸福,
也不逃避幸福,
它只是向前航行,
底下是沉静碧蓝的大海,
而头顶是金色的太阳
posted @ 2023-03-17 16:45  易先讯  阅读(23)  评论(0编辑  收藏  举报