最近这段时间都在忙活校招,在面试的过程中遇到了很多有关链表的问题,由于以前没怎么搞过ACM,数据结构也忘的七七八八了,所以总体感觉回答得不是很好。昨天把链表的基础回顾了下,见这里单链表的基础操作,于是今天把面试过程中经常遇到的链表相关的问题总结下。
翻转
单链表的反转是面试的常用考点,所以必须掌握。思路是把当前节点拿过来作为已经翻转的表头,成为一个已翻转的子链,用result指向其头部(也就是当前节点)。代码如下:
懒删除(在O(1)时间删除链表节点)
有一种情况,当我们的now引用指向某一个节点的时候,我们需要删除该节点。然而如果不知道now节点的前驱节点,一般方法是无法删除now这个节点的。
这个时候,我们可以使用“懒”删除,这种方法的思想是:把now节点后驱节点的值赋给now节点,然后now节点的next指向的它后驱节点的下一个节点。因为不用遍历,所以这个算法的时间复杂度是O(1)。然而要注意的是尾节点,当now节点就是尾节点的时候,这种办法就行不通了。
代码如下:
求链表倒数第k个节点
经常会遇到求单链表倒数第几个节点的问题,如果按常规方法的话,我们需要先遍历整个链表,记录下节点个数,假设为n,然后再遍历一遍,第n-k+1个就是所求节点了。可是这个方法效率太低,需要两次遍历,有没有更加高效的方法呢?答案是肯定的。我们可以用 快慢指针 来解决这个问题,这个方法的思想是这样的:
- 假设有引用fast、slow,开始都指向首节点;
- fast先走k次,这样fast和slow就相隔了k个节点;
- 然后fast、slow一起走,当fast走到链表末尾为null的时候,slow就是所求节点了。
代码如下:
求链表的中间节点
如果要求一个链表的中间节点,常规的方法是先遍历一次获取链表的长度n,如果n是偶数就n/2、n/2+1都可以,如果n是奇数,那就第n/2+1个。可是这个方法的效率也是非常低。
其实通过上面找用快慢指针的方法来找倒数第k个数,我们可以衍生到这里来。这个方法的思路是这样的:
- 假设有引用fast、slow,开始都指向首节点;
- fast、slow同时走,但是fast每次走2步,slow每次走1步;
- 当fast走到链表末尾时,slow就是中间节点;
代码如下:
单链表是否存在环(leetcode 141)
其实判断单链表是否有环有2种方法,第一种是用Set集合的方法,第二种是用快慢指针的方法。
利用HashSet集合
这个方法的思想很简单,就是遍历该链表,每走一次,就判断HashSet中是否存在该节点,如果存在则说明有环,结束;如果不存在,说明还没有环,把该节点放到HashSet中去,直到链表的末尾。如果利用的是HashSet,则该算法的时间复杂度也是O(n),就是空间复杂度大了点。
代码如下:
利用快慢指针
上面的方法实现起来很简单高效,但是却需要额外的空间,空间复杂度是O(n),而快慢指针是一个更高效的方法。让快指针每次走2步,慢指针每次走1步,两个指针的速度不一样,如果存在环的话,那么最后快慢指针肯定会相遇。
代码如下:
单链表中环的起点(leetcode 142)
假设链表中存在环,我们先设定几个参数:n(环的长度=7)、a(链表起点到环起点的距离=3)、x(fast指针到环起点的时候,slow指针在环中的位置0 <= x < n),由此我们可以推出下面几个性质:
- slow到起点(s3)的时候,fast在环中走了x步(f3),那么fast和slow相差 n - x 步,也就是说 n - x 步后,fast会追上slow;
- 经过 n - x 步后(4步),fast和slow在环中M点相遇(f7与s7);
- 假设这时候相遇点M与环起点的距离为b(=4),则slow走过的距离是 a + b ,而fast走过的距离为 a + b + k n 。显然fast走的长度是slow的2倍,所以2 ( a + b ) = a + b + k n,则 **a + b = k n**;
- 把fast指针拉回链表的起始点,这时候fast和slow每次都走一步,经过a步后,fast和slow在环的起始点相遇。
代码如下:
单链表中环的长度
在上面,我们找到环的起点后,再用slow指针走一遍,就可以算出环的长度了。
代码如下:
两个链表是否相交并求出交点(leetcode 160)
给定2个链表,判断这两个链表是否相交,如果相交的话,就类似于一个向左翻转90°的Y。见下图:
这个问题相比环来说简单很多,我们可以用Set集合方法、链长先走方法、成环方法来解决。下面我们逐一来看。
Set集合方法
这个方法的思路最简单:
- 先遍历a链,依次把所有节点放入到Set中去;
- 然后遍历b链,每到一个节点,就去Set看是否存在该节点,如果存在,则说明有交点,而交点就是该节点(第一个相等的肯定是交点);
- 如果直到b链的末尾都没有相等节点,则说明没有交点;
这个方法其实比较笨,效率也比较低。
代码如下:
链长先走方法
这个方法的思路是这样的:
- 遍历一次,求出链a的长度x,链b的长度y,求出|x-y|=k,这个k是链a和链b的长度差;
- 让长的链先走k步,这样a、b链剩下的长度都是一样的;
- 然后a、b一起走,遇到相等的节点,就是它们的交点;如果没相遇,说明没有交点;
这个方法比Set的方法好很多,因为不用用到Set集合,所以快很多。
代码如下:
成环方法
我们把a链的首尾连接起来,这样a链就形成了一个环。如果a、b链相交,那么从b链来看,链b这个时候就会出现环路,我们可以用上面找环的方法来解决。如下图:
这个方法的思路如下:
- 把a链的尾部指向a链头部,让其形成一个环;
- 从b链头部开始遍历,如果b中能找到环,说明ab相交,环的起点即为交点;如果不能找到环,说明无交点;
- 把a链中的环断开,恢复链a;
这个方法的代码和找环的代码差不多,这里就不写了。
复制带有随机指针的链表(leetcode 138)
一个单链表除了next指针外,还有一个random指针,random指针随机指向任何一个元素(可能为null),然后我们的任务是复制它。
这个复制其实next指针直接用常规方法就能解决,难点在于random指针,因为我们不知道random指针在复制后的地址-复制元素的地址变了,而且random指向的元素可能还没生成。
要解决这个问题,我们有2种方法:map集合方法、“副本”方法。
map集合方法
这个方法的思路是这样的:
- 先遍历一次原链,用常规方法复制一个新链,使其next指针指向正确的位置,random指针为null;而且在这个过程中,每生成一个新节点,用map来保存旧节点到新节点的映射,类似与map(oldNode, newNode);
- 再同时遍历原链和新链,在原链中得到节点的random指向的oldNode,然后利用map.get(oldNode)获取newCode,把新链节点的random指向这个newNode;
结合下图:
容易知道,这个方法的时间复杂度是O(n),空间复杂度也是O(n),效率比较低。
代码如下:
“副本”方法
这个方法的思路主要是:
- new副本:在每个旧节点后插入一个当前节点的副本,重新拉成链;
- 复制random:新节点的random=旧节点random的next;
- 拆分:奇数项都是旧节点,偶数项都是新节点,提取偶数项成链;
如下图:
代码如下:
链表partition过程(leetcode 86)
链表里存放整数,给定x,把x小的节点放在>=x之前,这就是一个partition过程,数组在partition的过程就是这样做的。然而链表和数组不同的是,在patition的过程中,链表没必要赋值来赋值去,而是重新起一个头,把比x小的连起来,把>=x的连成另外一条链,最后再把这2条链连起来就是了。
代码如下:
总结
从上面可以看出,涉及链表的问题,大部分都可以用快慢指针或集合的方式来解决,所以遇到这种问题可以往这方面考虑。由于链表实在非常灵活,所以具体的问题还是得灵活处理。