一、前言
链表是基本的数据结构之一,它与数组不同,数组在内存中存储,需要一块连续的内容空间来存储,对内存的要求比较高。例如我们需要 100MB 大小的数组,内存中就必须有一段连续的 100MB 的内存空间,否则就会出现 OOM。
而链表则不同,链表不需要一块连续的内存空间。它是通过“指针”,将一组零散的内存块串联起来。所以完全不用担心连续内存空间以及扩容等问题。
数据结构中的链表,一直是面试的常客,尤其是它引申出来的一些面试题,其实只要掌握了其中的技巧,很多同类型的链表题,思路都是相似的。
今天就来聊聊单链表的快慢指针思路,在有些算法书里,也把它称为双指针。很多和单链表相关的算法题,都可以通过快慢指针的方式解决。
在文章中,还会举几个快慢指针相关的算法题的例子,加深大家的理解。例如:求单链表倒数第 K 个节点、求单链表的中间节点、判断链表是否有环。
二、链表
2.1 什么是链表
链表是通过“指针”,将一组零散的内存块串起来,它不需要连续的内存空间,链表中的每个结点,都存储了下一个结点的内存地址的指针。
所以链表在声明时,不需要和数组一样连续的内存空间,但是它需要额外的空间来存储与之关联的结点指针,通常就会认为,链表比数组会消耗更多的内存空间。
但是其实在我们正常的编码过程中,链表中存储的每个结点,其实是远大于指针存储所消耗的空间,所以链表指针的这点消耗,基本上是可以忽略不计的。
链表,根据每个结点存储的指针关系,以及指针的指向,可以分为:
-
单链表:每个结点存在一个 next 指针,称为后续指针, next 指针指向后一个结点,尾结点的 next 指针,指向 NULL。
-
单向循环链表:和单链表类似,但是尾结点的 next 指针,指向头结点。
-
双向链表:在单链表的基础上,为每个结点增加一个 prev 指针,指向该结点在链表中的前驱结点。
-
双向循环链表:和双向链表类似,但是头结点的 prev 指向尾结点,而尾结点的 next 指向头结点,以此形成一个环。
可以看到,单链表是最基本的链表结构,也是受限最严重的链表,其他的链表都是在单链表的基础之上,增加一些功能,让开发者在使用的时候更方便。例如之前讲的单链表反转的问题,如果使用双向链表,其实也就不是问题了。
2.2 单链表的快慢指针
我们很多面试中的算法题,也是根据根据单链表这个受限严重的链表结构出发,考验大家对链表的理解以及思路是否清晰。
对于单链表,每个结点只有一个存储下一结点的 next 指针。当我们在查找某个结点的时候,无法和数组一样通过下标快速定位到待查询的结点,只能从头结点开始,通过每个结点的 next 指针,一个个结点的匹配查找。
这就代表了单链表循环的一个特点,它在遍历上无法后悔,遍历走过了,就是过了,无法回头。除非再用一个数据结构去记录已经遍历的结点,那就不再是一个单纯的单链表了。
这就引发出一种解决方案,就是快慢指针,一个指针无法完成任务,那就再引入一个指针。它通过两个指针 fast 和 slow 指针,让两个指针保持一定的间隔规律,达到我们查找的目的。
只通过概念去描述,有点不好理解,接下来以几个常见的算法题当例子来讲解快慢指针的应用。
三、快慢指针例子
3.1 倒数第 K 个结点
题目:已知一个单链表的头结点,找到该链表中,倒数第 K 个结点。
这个问题,主要的核心点在于,我们不知道单链表的长度,否则在遍历的时候,用一个 index 就可以解决。
我们通过两次遍历,是不是就可以解决这个问题?第一次遍历,找到单链表的长度 length,第二遍遍历找到 K(length - K)个 结点即可。
如果我们想,只通过一次循环遍历,找到倒数第 K 个节点,就需要用到快慢指针了。
首先定义两个指针 fast 和 slow , fast 和 slow 指针都指向链头 head,然后 fast 指针先向前走 K 步,这样 slow 和 fast 中间就间隔了 K 个节点,最后 slow 和 fast 同步向前移动,直到 fast 指针走到链表末尾,此时 slow 指针,就正好指向倒数第 K 个结点。
3.2 求链表的中间结点
题目:求单链表的中间结点,如果链表长度为偶数,返回两个结点的任意一个,若为奇数,则返回中间结点。
这道题当然也可以通过两次遍历的方式解决,但是这必然不是我们想要的。
还是使用快慢指针,fast 指针每次移动两步,而 slow 指针每次移动一步,等到 fast 指针指向链表尾结点时,slow 指针指向的正好是链表的中间结点。
3.3 判断链表是否存在环
题目:已知一个单链表的 head,判断当前链表是否存在环。
循环单链表,从逻辑图上来说,本身也是一个环状结构
这种方式非常简单,只需要在循环的时候,判断尾结点的 next 是否指向头结点即可。
但是我们这里分析的,是单链表的环入口不确定的情况,它可能从任意一个结点开始形成了环。
这依然通过快慢指针的思想来解决,fast 每次走两步而 slow 每次走一步,如果单链表中存在环,fast 以两倍于 slow 的速度前进,那么两个指针,最终一定会进入环,也一定会在环中相遇。