“你是不是也这样?打开LeetCode,看着题目发呆,脑子里一堆‘暴力解法’,提交后却只换来一个冰冷的‘Time Limit Exceeded’?别慌!《LeetCode 101》 就是你的救星!
这里没有晦涩的理论,只有清晰的套路、高效的代码模板和面试官最爱的优化技巧。不管你是被‘动态规划’虐到怀疑人生,还是看到‘二叉树’就头皮发麻——
哎呀,看到你这问题,我就不得不说,你这卡在算法学习的瓶颈上了啊。咱这行,不就是算法吃饭的嘛。别急,哥来给你掰扯掰扯,怎么学算法才能真练成个高手。
跟着这篇攻略,一路平推LeetCode 101,从此算法面试横着走!”
先说个老实话,理论你是绕不过去的。你看了《数据结构与算法》和《算法图解》这些书,觉得太理论了用不上,那是因为你还没找到对的练法。就像你学武功,光练套路不打实战那肯定不行啊,但没有套路,实战也打不出花样来。所以,理论和实践得结合。
首先打开 LeetCode 网站,如果我们按照题目类型数量分类,最多的几个题型有数组、动态规划、数学、字符串、树、哈希表、深度优先搜索、二分查找、贪心算法、广度优先搜索、双指针等等。
第一个大分类是算法
本书先从最简单的贪心算法讲起,然后逐渐进阶到二分查找、排序算法和搜索算法,最后是难度比较高的动态规划和分治算法
第二个大分类是数学
包括偏向纯数学的数学问题,和偏向计算机知识的位运算问题。这类
问题通常用来测试你是否聪敏,在实际工作中并不常用,笔者建议可以优先把精力放在其它大类
上。
第三个大分类是数据结构
包括 C STL 内包含的常见数据结构、字符串处理、链表、树和
图。其中,链表、树、和图都是用指针表示的数据结构,且前者是后者的子集。最后我们也将介
绍一些更加复杂的数据结构,比如经典的并查集和 LRU。
学算法一定要多思考、多练习!!!
在复习 Java、巩固基础的过程中,每天可以坚持用 Java 做 2 – 3 道算法题目。
不用担心看不懂,直接进入 LeetCode 学习板块 LeetBook,提供了免费的教程,文字、图解、动画讲算法、在线练习应有尽有,从 0 开始,跟着学习基础知识、跟着教程刷一些同类题目,培养算法思路。
目录
先打基础,再搞高深
别急着上来就刷力扣的困难题,先把基础打牢。基础是什么?排序算法、查找算法、基本的数据结构(链表、栈、队列、树、图),这些东西你得扎扎实实掌握。你要知道,复杂的问题,往往都是简单问题的叠加和变种。
1. 排序和查找
先拿排序和查找练手。你别看排序简单,冒泡、插入、选择、快速、归并,每种都敲一遍,自己实现一遍,再理解它们的时间复杂度和空间复杂度。查找算法,二分查找是基础,弄懂了这个,再搞搞其他的。
2. 数据结构
数据结构这块,你就把链表、栈、队列、树、图这些东西搞明白。每种数据结构,自己实现一遍,光看书不敲代码,那是不行的。比如链表,单链表、双链表、循环链表,自己写写插入、删除、查找这些操作,熟练了再说。
好了聊完以上内容,就到了我们实际内容供大家查看
贪心算法
顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最
后得到的结果是全局最优的。
举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹
果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的
贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全
局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的
策略。
在贪心算法下有两个重点问题
分配问题
区间问题
现在到了双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
对于 C 语言,指针还可以玩出很多新的花样。一些常见的关于指针的操作如下。
同样的还是两个经典问题抛给大家,供大家参考
归并两个有序数组
滑动窗口
二分查找
二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一半。于是我们的查找区间变成了 {3,4,5}。(注意,根据具体情况和您的刷题习惯,这里的 5 可以保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4,正好是我们需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍历数组,最坏的情况则需要查找 5 次。
我们也可以用更加数学的方式定义二分查找。给定一个在 [a; b] 区间内的单调函数 f (x),若f (a) 和 f (b) 正负性相反,那么必定存在一个解 c,使得 f (c) = 0。在上个例子中,f (x) 是离散函数f (x) = x 2,查找 4 是否存在等价于求 f (x) −4 = 0 是否有离散解。因为 f (1) −4 = 3−4 = −1 < 0、f (5) − 4 = 7 − 4 = 3 > 0,且函数在区间内单调递增,因此我们可以利用二分查找求解。如果最后二分到了不能再分的情况,如只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在离散解,即 4 不在这个数组内。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C 、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。
二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
同样的还是两个经典问题抛给大家,供大家参考
第一个就是求开方
给定一个非负整数,求它的开方,向下取整。
第二个就是查找区间
现在到了排序算法
以下是一些最基本的排序算法。虽然在 C 里可以通过 std::sort() 快速排序,而且刷题时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解出由这些排序算法引申出来的题目。
有以下5种排序类型他们分别是快速排序、归并排序、插入排序、冒泡排序、选择排序
在这一个经典问题抛给大家,供大家参考
由于中间包含的算法类型实在是太多,我就跳过,直接来描述数据结构
指针三剑客之一:链表
(单)链表是由节点和指针构成的数据结构,每个节点存有一个值,和一个指向下一个节点的指针,因此很多链表问题可以用递归来处理。不同于数组,链表并不能直接获取任意节点的值,必须要通过指针找到该节点后才能获取其值。同理,在未遍历到链表结尾时,我们也无法知道链表的长度,除非依赖其他数据结构储存长度。LeetCode 默认的链表表示方法如下。
struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}};
由于在进行链表操作时,尤其是删除节点时,经常会因为对当前节点进行操作而导致内存或指针出现问题。有两个小技巧可以解决这个问题:一是尽量处理当前节点的下一个节点而非当前节点本身,二是建立一个虚拟节点 (dummy node),使其指向当前链表的头节点,这样即使原链表所有节点全被删除,也会有一个 dummy 存在,返回 dummy->next 即可。
案例题我直接给大家甩出来
以 O(1) 的空间复杂度,判断链表是否回文。
指针三剑客之二:树
作为(单)链表的升级版,我们通常接触的树都是二叉树(binary tree),即每个节点最多有两个子节点;且除非题目说明,默认树中不存在循环结构。LeetCode 默认的树表示方法如下。
struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}};
可以看出,其与链表的主要差别就是多了一个子节点的指针。
数的递归
对于一些简单的递归题,某些 LeetCode 达人喜欢写 one-line code,即用一行代码解决问题,把 if-else 判断语句压缩成问号冒号的形式。我们也会展示一些这样的代码,但是对于新手,笔者仍然建议您使用 if-else 判断语句。在很多时候,树递归的写法与深度优先搜索的递归写法相同,因此本书不会区分二者
案例题我直接给大家甩出来
指针三剑客之三:图
作为指针三剑客之三,图是树的升级版。图通常分为有向(directed)或无向(undirected),有循环(cyclic)或无循环(acyclic),所有节点相连(connected)或不相连(disconnected)。树即是一个相连的无向无环图,而另一种很常见的图是有向无环图(Directed Acyclic Graph,DAG)。
有向无环图样例
图通常有两种表示方法。假设图中一共有 n 个节点、m 条边。第一种表示方法是邻接矩阵(adjacency matrix):我们可以建立一个 n× n 的矩阵 G,如果第 i 个节点连向第 j 个节点,则 G[i][j]= 1,反之为 0;如果图是无向的,则这个矩阵一定是对称矩阵,即 G[i][j] = G[j][i]。第二种表示方法是邻接链表(adjacency list):我们可以建立一个大小为 n 的数组,每个位置 i 储存一个数组或者链表,表示第 i 个节点连向的其它节点。邻接矩阵空间开销比邻接链表大,但是邻接链表不支持快速查找 i 和 j 是否相连,因此两种表示方法可以根据题目需要适当选择。除此之外,我们也可以直接用一个 m × 2 的矩阵储存所有的边。
案例题我直接给大家甩出来
练习可以看着练习,也可直接搜索答案直接查询,对自己的提升也是有好处的
有需要资料的兄弟们可以直接后台私信小编【学习】即可直接免费拿走!