作者| 慕课网精英讲师 liuyubobobobo
LeetCode 按照怎样的顺序来刷题比较好?
我想大多数人的回答都会是直接“应试”地刷题。如果你的时间特别紧,直接刷题当然没有问题。但我的经验是,如果你有相对宽裕的一些时间,除了想准备算法面试以外,还想真正把算法和数据结构的基础打扎实,应该先认真学习经典的算法和数据结构的底层原理。如果你的时间特别紧,可以直接跳到第二部分,“刷题篇”。
1. 认真学习复习经典算法和数据结构的底层原理
什么叫经典的算法和数据结构的底层原理?我简单总结了一下,主要包括以下内容:
1、各种排序算法,所谓的十大经典排序算法;
2、二分搜索法;
3、线性数据结构:动态数组,链表,栈和队列;
4、常用的树形数据结构:二分搜索树,堆,红黑树,B 树,并查集等;
5、哈希表;
6、常用字符串算法,如 Trie 的使用,滚动哈希,KMP 等;
7、基础的图论算法,如图的深度优先遍历,广度优先遍历,及其应用;带权图最短路径的 Dijkstra 算法;最小生成树的 Kruskal 算法;有向图的拓扑排序;哈密尔顿回路(或者路径)等等;
8、一些进阶的数据结构,如线段树,后缀树,跳表,等等;
9、一些进阶的图论算法,如寻找桥或者割点;欧拉回路(或者路径);匈牙利算法;最大流最小割;等等。
10、其他算法,比如随机算法 Knuth Suffle,蓄水池抽样,等等
很多同学看这个列表,会觉得“枯燥”,甚至觉得“轻视”。这看似都是课本上最基础的内容,也好像和见到的大多数面试题没有关系?但是,我的经验是,对这些“基础问题”的掌握,既是刷题的基础,也是继续深入计算机科学其他领域的关键。
首先,这里面的很多内容,是刷题涉及不到的。比如在刷题的时候,基本不会遇到让你实现一个排序算法的情况,如果需要排序,通常是直接调用语言的标准库就好了。但是,我知道很多厂子的面试问题,恰恰就是“讲一讲快速排序是怎么回事儿,归并排序是怎么回事儿,红黑树的基本原理”,等等这类问题。
另一方面,其实对这些基础的算法和数据结构的学习,很多时候并不完全是学习一个算法这么简单,而蕴含着对算法思想的学习。我们可以看到,为了解决一个问题,我们可以如何设计算法。
最典型的例子就是递归。很多同学都觉得递归很绕。但是,在我列的这个列表中,所有的算法如果真的踏踏实实都搞明白了,我相信对递归是不怕的。无论是学习快速排序和归并排序,还是实现各种树结构中的基本操作,都在不停地使用递归。我们在学习这些内容的时候,并非是简单的死记硬背,而是在看一些算法思想的具体应用。
这样的例子数不胜数:
Dijkstra 和 Prim 算法蕴含着动态规划的思想;
Kruskal 算法蕴含着贪心的思想;
滚动哈希蕴含着双指针的思想;
哈希表,滚动哈希,都蕴含着哈希方式的运用;
计数排序,基数排序,桶排序,哈希表,都有对数据元素进行分类(桶)这种思想的应用;
哈密尔顿回路(或者路径)可以使用回溯法求解,进而可以使用记忆化搜索或者动态规划做优化,这背后也包含状态压缩的思想;
同样解决 topK 问题,使用优先队列和使用快排的改进思想,可以让我们理解在线算法和离线算法;
等等等等;
更关键的是,在大多数时候,这些所谓的基础,是进一步学习计算机科学的关键,而不是那些“稀奇古怪”的“刷题”问题。
实际上,这本身也是学习算法和数据结构的核心目标——进一步去学习计算机领域其他更深入的知识。我的经验是,很多同学看似刷了很多题,但其实基础不牢靠;这就像很多同学为了考研,数学做了很多题,但是真的在论文中看到数学公式;或者实际项目中,需要用数学做一个建模的时候,还是会很头疼,一个道理。归根到底,是过早的刷题,只是以应试为导向,对这些基础的内容,理解得并不深入。
关于这些基础的算法和数据结构的学习,最推荐的教材,就是大名鼎鼎的《算法 4》。
不夸张地说,这本书我认为学习计算机的同学,不管是不是科班,都应该通读。当然,这本书在我看来是有一定的缺点的。
比如 Prim 算法和 Dijkstra 算法使用索引堆实现,虽然复杂度更优一些,但是索引堆这种数据结构稍微绕一些,在一般语言的标准库中也不提供。而 Prim 算法和 Dijkstra 算法都可以使用普通的优先队列实现,相关的方式这本书却没有介绍。
再比如对于 KMP 算法,这本书介绍的是使用自动机的方式完成,但其实,KMP 算法有更简洁的实现方式,使用所谓的 LPS 数组(Longest Proper Prefix which is Suffix),《算法4》对此没有提及。
但瑕不掩瑜,《算法4》仍然是学习经典的算法和数据结构底层实现的最佳书籍。而且这本书对新手足够友好,有 Java 语言基础的同学就可以直接看。
如果对视频课程感兴趣的同学,可以参考慕课网上的《算法与数据结构体系课程》和《玩转算法系列–图论精讲 面试升职必备》两门课程:
2. Leetcode 刷题篇
刷题关注的内容,和学习经典算法和数据结构的底层实现不太一样。
整体来说,刷题的核心,是算法设计。所以,在刷题的过程中,大家可能很难遇到诸如实现一个哈希表或者实现一个快速排序这样的问题。想排序,想使用哈希表,大家直接调用语言的标准库就好了。但是,如何使用这些经典的算法和数据结构解决问题,是另一回事儿。
另一方面,算法设计领域涉及的一些问题,也是学习经典算法和数据结构的底层实现不太能系统接触的。最典型的就是动态规划,分支限界,这些算法设计思想。
另一方面,有一些基础的算法或者数据结构的应用,也是在学习底层实现的时候不涉及的。最典型的例子是栈。栈的底层实现非常简单。但是,我们可以使用栈,巧妙地解决一些问题。比如用单调栈的思想,来求解区间的最大最小值问题,或者离线解决 RMQ 问题。
对于算法设计来说,如果大家想系统学习的话,书籍方面,我推荐这本 Algorithm Design Mannual。
这本书在疫情期间,出版社 Springer 可以免费让大家下载其英文原版。如果能看英文原版的同学,大家在网上一搜,应该有很多。想看中文版的同学,这本书国内也有引进,在这里:
但是,因为算法设计是一个太广泛,也没有具体范围的领域,所以,这样的书,也不可能涵盖所有的问题。尤其是以面试为导向的话,刷题还是需要的。
刷题按照什么顺序刷?我个人建议按照 Leetcode 给大家提供的专题顺序刷。大家点击力扣中文站的“学习”标签,就可以看到力扣为大家总结的各个专题。大家可以按照自己感兴趣的(或者薄弱的)专题刷。
3. Leetcode 竞赛
从面试准备的角度看,单纯的按照专题刷题,是有问题的。
因为,一个问题的标签本身,其实是包含巨大信息量的,是一个很重要的提示。而大家在实际的笔试面试中,是没有这个提示信息的。
比如,如果直接告诉你,一个问题是动态规划,那么大家就可以直接去思考:怎么设计状态,状态转移是怎样的。这其实将问题简化了。如果没有这个标签,很多同学可能想不到这是一个 dp 问题。
在算法设计领域,这类问题特别多。另外一个典型的例子是二分搜索。对于一些问题,没有经验的同学,可能很难想到使用二分搜索去解决,但是一旦告诉你这个问题可以用二分搜索解决,其实就没什么难度了。
对此,我的建议是:刷题到一定程度以后,可以开始尝试做力扣(LeetCode)的周赛。甚至,有些同学时间紧,可能只有一个月的时间,没有时间专门系统地对每一类问题进行练习,我都直接建议,做周赛就好了。
大家可以把力扣(LeetCode)的每一次周赛,都看作是一场实际的算法面试的模拟。
力扣(LeetCode)的周赛每期四个问题,问题质量都很高。每场比赛的问题类型不定,没有标签。大家在做的时候,需要从头分析,每个问题需要使用什么算法解决。这在我看来,是非常重要的一个训练。
在这个过程中,如果发现自己对某一个类型的问题不熟悉,可以再回头,有针对性地根据专题进行“特训”。
但是,大家也要注意,现在力扣周赛的一些问题,尤其是 Hard 问题,越来越偏竞赛问题,这种竞赛问题,在面试中出现的概率极低。所以整体,我认为,大家如果能在周赛中,做到稳定做出三道问题,应付大多数算法面试已经够了。
如果对 Hard 级别的问题感兴趣,整体上,在力扣上,题号越靠前的 Hard 问题,会相对“简单”一些,也更“标准”一些,更容易是面试过程中可能考察的问题。而相对比较新的问题,在最近一年,Leetcode 周赛的 Hard 问题,个人认为越来越偏竞赛问题。
当然如果有同学对竞赛问题感兴趣,也可以学习。力扣上每个问题都有大量优秀的同学写的题解,也有源代码大家可以分析,是一个非常好的学习场所。
在我看来,如果想搞竞赛的同学,力扣也是很好的一个入门平台。当然,如何深入学习训练算法竞赛,就完全是另一个话题了。