在互联网行业的算法面试中经常会被考到数据结构的知识,它与算法相辅相成,没有扎实的数据结构基础,学好算法几乎不太可能。
这里精心整理了 Google 资深工程师的学习笔记和解题技巧,总结出6大数据结构必考知识点,同时以力扣 LeetCode 经典题辅助讲解,帮助你更好的理解数据结构要点。
一、数据结构、字符串
数组和字符串是最基本的数据结构,在很多编程语言中都有着十分相似的性质,这部分的算法面试题也是最多的。
很多时候,在分析字符串相关面试题的过程中,要针对字符串当中的每一个字符进行分析和处理,甚至有时候需要先把给定的字符串转换成字符数组之后再进行分析和处理。举个最简单的例子:翻转一个字符串。
一种比较快速和直观的方法是用两个指针,一个指向字符串的第一个字符a,一个指向它的最后一个字符m,然后互相交换。交换之后,两个指针向中央一步步地靠拢并相互交换字符,直到两个指针相遇。由于无法直接修改字符串里的字符,所以必须先把字符串变换为数组,然后再运用这个算法。
采用数据的优缺点
a.优点:
构建一个数组非常简单;
能让我们在 O(1)的时间里根据数组的下标(index)查询某个元素。
b.缺点:
构建时必须分配一段连续的空间;
查询某个元素是否存在时需要遍历整个数组,耗费O(n)的时间(其中,n是元素的个数);
删除和添加某个元素时,同样需要耗费O(n)的时间。
所以,在考虑是否应当采用数组去辅助所用算法时,务必需要考虑它的优缺点,看看它的缺点是否会阻碍所用算法的复杂度以及空间复杂度。
真题:力扣(LeetCode)第242题.Valid Anagram 判断两个字符串是否互为字谜
解题思路:
所谓字谜,也就是两个字符串中的相同字符的数量要对应相等。例如:s 等于 “anagram”,t等于 “nagaram”,s和t就互为字谜,因为它们都包含有三个字符a,一个字符g,一个字符m,一个字符n以及一个字符r。而当s为“rat”,t为 “car”的时候,s和t不互为字谜。
题目里有一个重要的前提:假设两个字符串只包含小写字母。小写字母一共26个,这意味着,可以利用两个个长度都为26的字符数组来统计每个字符串中小写字母出现的次数,然后再对比看看是否相等即可。
或者,也可以只利用一个长度为26的字符数组,将出现在字符串s里的字符个数加一,而出现在字符串t里的字符个数减一,最后判断每个小写字母的个数是否都为零就可以了。
二、链表
链表的出现在某种程度上是为了避免数组的一大缺陷,即分配数组的时候需要开辟一段连续的内存空间,但鱼和熊掌不可兼得,链表也牺牲了数组的一些优点,链表不能通过下标进行快速查询。所以在考虑是否需要运用链表的时候,务必搞懂所用算法是否需要经常进行查询和遍历。
1.链表的优点和缺点
a.优点:
链表能灵活地分配内存空间
能在O(1)时间内删除或者添加元素,前提是该元素的前一个元素已知,当然也取决于是单链表还是双链表,在双链表中,如果已知该元素的后一个元素,同样可以在O(1)时间内删除或者添加该元素。
b.缺点:
查询第k个元素需要O(k)时间
很显然,如果要解决的问题里面需要很多快速的查询,链表可能并不适合。一般而言,如果数据的元素个数不确定,而且需要经常进行数据的添加和删除,那么链表会比较合适,而如果数据元素大小确定,删除插入的操作并不多,那么数组可能更适合。
2.链表的经典解题方法
a.利用快慢指针(有时候需要用到三个指针):
例如,链表的翻转,寻找倒数第k个元素,或者寻找链表中间位置的元素,判断链表是否有环等等。
b.构建一个虚假的链表头:
这个方法一般用在要返回新的链表的题目中,例如:
给定两个排好序的链表,要求将它们整合在一起并排好序
将一个链表中的奇数和偶数按照原定的顺序分开后重新组合成一个新的链表,链表的头一半是奇数,后一半是偶数。
在这类问题里,如果不用一个虚假的链表头,那么在创建新链表的第一个元素时,都虚要判断一下链表的头指针是否为空,也就是要多写一条if else语句,比较简洁的写法是创建一个空的链表头,直接往其后面添加元素即可,最后返回这个空的链表头的下一个节点即可。
另外,链表有单链表和双链表,它们是实现很多复杂数据结构的基础,在解决链表的题目时,建议在纸上或者白板上画出节点之间的相互关系,然后画出修改的方法,这样可以有效地分析问题,因为凭空想象是比较困难的,而且在面试的时候,如果能把方法画在白板上,还能帮助面试官清楚地看到你的思路。
真题:力扣(LeetCode)第25题.Reverse Nodes in k-Group在链表中对每k个节点所组成的部分进行翻转
clipboard.png
这道题是力扣(LeetCode)第24题.Swap Node in Paris(在链表中每两个节点进行翻转)的扩展,在这道题里,当k等于2时,第25题就变成了第24题。
那么这道题考察了两个知识点:
你对链表翻转算法是否熟悉
你对递归算法的理解是否清晰
在翻转链表的时候,我们可以借助三个指针:prev、curr、next,分别代表了前一个节点、当前节点和下一个节点。
最为重要的是,当完成了局部的翻转后,prev就是最终的新的链表头,curr指向了下一个要被处理的局部,而原来的头指针head成为了链表的尾巴。
三、栈
栈是许多力扣中等难度偏上的题目里面经常需要用到的数据结构。掌握好它是十分必要的。
1.栈的特点
栈的特点就是后进先出(LIFO),对于栈中的数据来说,所有操作都是在栈的顶部完成的,只可以查看栈顶部的数据,只能够向栈的顶部压⼊入数据,也只能从栈的顶部弹出数据。
因此,可以利用一个单链表来实现栈的数据结构,而且,因为只针对栈顶元素进行操作,所以借用单链表的头就能让所有栈的操作在O(1)的时间内完成。虽然可以用一个数组外加一个指针也能实现相似的效果,但是,一旦数组的长度发生了改变,哪怕只是在最后添加一个新的元素,时间复杂度不再是O(1),而且,空间复杂度也得不到优化。
2.什么时候需要用到栈呢?
围绕栈的算法面试题很多,基本的核心思想就是:当解决某个问题的时候,只关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作。
例如,给出了一串由左括号和右括号组成的字符串,需要判断这些括号的组成是否合法。方法就是可以利用一个栈,不断地往里压左括号,一旦遇上了一个右括号,就把栈顶的左括号弹出来,表示这是一个合法的组合,以此类推,直到最后判断栈里还有没有左括号剩余。
真题:力扣(LeetCode)第739题.Dailey Temperatures气温变化
解题思路:
给定一个数组 T 代表了未来几天里每天的温度值,要求返回一个新的数组D,D中的每个元素表示需要经过多少天才能等来温度的升高。例如:
给定T:[23,25,21,19,22,26,23]
返回D:[1,4,2,1,1,0,0]
最直观的做法就是针对每个温度值向后进行以此搜索,找到比当前温度更高的值,这样的计算复杂度就是O(n^2)。
在这样的搜索过程中,产生了很多重复的对比,从25度开始往后面寻找一个比25度更高的温度的过程中,先后经历了21度,19度和22度,这是一个温度由低到高的过程,也就是在这个过程中已经找到了19度以及21度的答案了,就是22度。
可以运用一个堆栈Stack来快速地知道需要经过多少天就能等到温度升高。基本的思想是从头到尾扫描一遍给定的数组T,如果当天的温度比堆栈Stack顶端所记录的那天温度还要高,那么就能知道结果了。
这是一道比较有意思的题目,建议到力扣(LeetCode)上试试。
利用堆栈,还可以帮助解决如下常见的问题:
判断一系列括号的组合是否合法 【力扣(LeetCode)20】
求解算术表达式的结果【力扣(LeetCode)224,227,772,770】
求解直方图里最大的矩形区域【力扣(LeetCode)84】
四、队列
1.队列的特点
队列的最大特点是先进先出(FIFO),就好像按顺序排队一样。对于队列的数据来说,只允许在队尾查看和添加数据,在队头查看和删除数据。
2.如何实现一个队列
可以借助双链表实现队列,双链表的头指针允许在队头查看和删除数据,而双链表的尾指针允许在队尾查看和添加数据。当需要按照一定的顺序来处理数据,而要处理的数据量在不断地变化的时候,就需要使用队列。在算法面试题当中,广度优先搜索(Breadth-First Search)是运用队列最多的地方。
五、双端队列
双端队列和普通队列最大的不同在于,它允许在队列的头尾两端都能在O(1)的时间内进行数据的查看、添加和删除。
与队列相似,可以利用一个双链表来实现双端队列。
双端队列最常用的地方就是实现一个长度动态变化的窗口或者连续区间,而动态窗口这种数据结构在很多题目里都有运用。下面通过一道经典的例题来分析它的用法。
真题:力扣(LeetCode)第239题.Sliding Window Maximum 寻找滑动窗口中的最大值
解题思路:
给定一个数组以及一个窗口的长度 k,现在移动这个窗口,要求打印出一个数组,数组里的每个元素是当前窗口当中最大的那个数。例如:
输入:nums=[1,3,-1,-3,5,3,6,7],k=3
输出:[3,3,5,5,6,7]
可以利用一个双端队列来保存当前窗口中最大那个数在数组里的下标,有了这个下标,就能很快地知道新的窗口是否已经不再包含原来那个最大的数,如果不再包含,就把旧的数从双端队列的头删除,而双端队列新的头就是当前窗口中最大的那个数。按照这样的操作,我们可以在O(n)的时间里完成所有任务。
在这道题当中,我们需要频繁地进行两个操作:
1、将新的数据加入到窗口的尾部
2、将旧的数据从窗口头部删除
双端队列,它能让上面的这两种操作都能在O(1)的时间里完成,使整个算法的复杂度能控制在O(n)。
六、树
树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归,也就是说,一棵树要满足某种性质,往往要求每个节点都必须满足。例如,在定义一棵二叉搜索树时,每个节点也都必须是一棵二叉搜索树。正因为树有这样的性质,大部分关于树的面试题都与递归有关,换句话说,面试官希望通过一道关于树的问题来你对于递归算法掌握的熟练程度。
在面试中常考的树的形状有:普通二叉树、平衡二叉树、完全二叉树、二叉搜索树、四叉树(Quadtree)、多叉树(N-ary Tree)。
对于一些特殊的树,例如红黑树(Red-Black Tree)、自平衡二叉搜索树(AVL Tree),大家不必花费太多时间去准备,一般在面试中不会被问到,除非你所涉及的研究领域跟它们相关或者你十分感兴趣。
关于树的考题,无非就是要考查树的遍历以及序列化(serialization)。树的基本遍历有三种:前序遍历(Preorder Traversal)、中序遍历(Inorder Traversal)以及后序遍历(Postorder Traversal)。掌握好这三种遍历的递归写法和非递归写法是非常重要的,同时,懂得分析各种写法的时间复杂度和空间复杂度同样重要。
无论你是前端工程师,还是后端工程师,在准备面试的时候,树这个数据结构可以说是最应该花时间学习的。掌握好树,能证明你对递归有很好的认识,能帮助你学习图论。另外,树的许多性质都是面试的热门考点,尤其是二叉搜索树(BST)。
下面可以通过一道例题来看看树的考察点。
真题:力扣(LeetCode)第230题.Kth Smallest Element Element in a BST在一棵二叉搜索树中寻找第k小的元素
解题思路:
这道题考察了两个知识点:
1、二叉搜索树的性质
2、二叉搜索树的遍历
二叉搜索树的中序遍历可以说是二叉搜索树性质里最喜欢被考的知识点,因为节点被遍历到的顺序是按照节点数值大小的顺序排列好的。也就意味着,按照中序遍历一次这个二叉搜索树,遍历当中遇到的元素都是按照从小到大的顺序出现。采用这个知识点,只需要对这棵树进行中序遍历的操作,当访问到第k个元素的时候返回结果就好。
另外,这道题可以变成求解第k大的元素,方法就是对这个二叉搜索树进行反向的中序遍历,那么数据的被访问顺序就是由大到小了。
内容摘取自《300分钟搞定算法面试》,戳此查看更多
第01讲:常用数据结构
主讲人:苏勇,谷歌资深技术工程师
添加amy好友:lagouandy,回复【算法】,可领取电子书等求职礼包