前言
快慢指针是指使用两个指针进行定位,一个指针在前,称之为“快指针”,一个指针在后,称之为“慢指针”。通常的用法是,使用快指针进行定位,使用慢指针记录我们需要的目标。
下面将会介绍快慢指针的一个典型应用。
1. 题目
原题:力扣 剑指 Offer 22. 链表中倒数第k个节点
链表节点的定义:
2. 解题思路
对于一个单向链表而言,如果要定位它的倒数第n个节点,首先就要定位尾节点。
最容易想到也最容易实现的做法就是,先遍历一遍链表,确定链表节点的数量k,那么倒数第n个节点就是正数第 k-n+1 个节点(从1开始计数)。确定好目标节点的正向位置后,再从头遍历并计数,直到遍历到第 k-n+1 个节点,返回这个节点即可。
这个思路没有问题,实现起来也特别简单。问题是,在这个过程中,我们需要遍历两遍链表,整个算法的时间复杂度达到了 O(2n)(n为链表长度)。
1 class Solution {
2 public ListNode getKthFromEnd(ListNode head, int k) {
3 int sum=0;
4 ListNode node=head;
5 while(node!=null){
6 sum++;
7 node=node.next;
8 }
9 for(int i=1;i<sum-k+1;i++){
10 head=head.next;
11 }
12 return head;
13 }
14 }
3. 使用快慢指针优化查找过程
我们可以使用快慢指针记录节点,从而在一次遍历中定位到倒数第n个节点。首先,我们定位一个快指针指向链表的头部,然后移动这个指针,直到该指针移动到链表的第n个节点上(从1开始),然后我们再定义一个慢指针,它指向链表的头节点。这样,这个慢指针指向的节点就是相对快指针指向的节点的前向第n个节点。然后我们开始一起移动两个指针,每次都一起后移一位,直到快指针移动到尾节点,这时,慢指针所指向的节点就是整个链表的倒数第n个节点。
使用快慢指针遍历寻找链表的倒数第n个节点,由于仅需要一次遍历,因此时间复杂度达到了O(n)。
1 class Solution {
2 public ListNode getKthFromEnd(ListNode head, int k) {
3 ListNode node=head;
4 for(int i=1;i<=k;i++){
5 head=head.next;
6 }
7 while(head!=null){
8 head=head.next;
9 node=node.next;
10 }
11 return node;
12 }
13 }
4. 扩展
上面我们介绍了使用快慢指针定位倒数第n个节点的实现方式。但就解决这个问题而言,我们还可以使用栈和队列来实现时间复杂度O(k+n)的算法(其中k为链表长度,n为节点的倒数位置)。
1)使用栈实现
我们知道,栈是先进后出的数据结构,因此,我们可以在一边遍历中,每遍历到一个节点就将这个节点存入一个栈中,直到遍历完整个链表,此时栈中就存储了整个链表的所有节点(而且是倒序的)。接下来我们只需要依次从栈中取出节点就好了,因为栈的特性,因此我们取出节点的顺序其实是从尾节点向头结点反向逐个取出。因此,只要在取节点的过程中进行计数,当计数达到n时,我们取出的这个节点就是链表的倒数第n个节点。
1 class Solution {
2 public ListNode getKthFromEnd(ListNode head, int k) {
3 Stack<ListNode> stack=new Stack<>();
4 ListNode node=head;
5 while(node!=null){
6 stack.push(node);
7 node=node.next;
8 }
9 for(int i=1;i<=k;i++){
10 node=stack.pop();
11 }
12 return node;
13 }
14 }
2)使用队列实现
但是这个说的队列是双向队列,单向队列的做法和二次遍历几乎没有任何区别,还浪费了一份空间来存储节点的引用。所以我们使用Deque来存储节点,因为Deque是一个双向队列,本质上而言,我们就相当于把原本的单向链表变成了一个双向链表,这样当我们定位到尾节点以后就可以向前移动n-1次从而定位到倒数第n个节点。
1 class Solution {
2 public ListNode getKthFromEnd(ListNode head, int k) {
3 Deque<ListNode> queue=new LinkedList<>();
4 ListNode node=head;
5 while(node!=null){
6 queue.push(node);
7 node=node.next;
8 }
9 for(int i=1;i<=k;i++){
10 node=queue.pop();
11 }
12 return node;
13 }
14 }
当链表不大时,使用队列和栈的方式相比两次遍历并没有明显的提升,并且因为要维护一个栈/队列来存储节点的引用反而浪费了性能。但当链表很长时,这种做法带来的收益就是肉眼可见的了。