• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
夏弈
沿着河边的白沙滩行走,希望能找到桃花源。
博客园    首页    新随笔    联系   管理    订阅  订阅
【夏弈的解题记录】快慢指针的典型应用——定位给定单向链表的倒数第n的节点

前言

  快慢指针是指使用两个指针进行定位,一个指针在前,称之为“快指针”,一个指针在后,称之为“慢指针”。通常的用法是,使用快指针进行定位,使用慢指针记录我们需要的目标。

  下面将会介绍快慢指针的一个典型应用。

 

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 }

 

  当链表不大时,使用队列和栈的方式相比两次遍历并没有明显的提升,并且因为要维护一个栈/队列来存储节点的引用反而浪费了性能。但当链表很长时,这种做法带来的收益就是肉眼可见的了。

posted on 2020-08-19 11:45  夏弈  阅读(200)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3