22. 链表中倒数第k个节点

本文介绍了解决链表问题的双指针技巧,包括求解链表中倒数第K个节点、删除链表的倒数第N个节点及找到链表中间节点等问题,并提供了详细的解题思路与Java代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剑指offer 22 链表中倒数第K个节点

面试题 02.02. 返回倒数第 k 个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2. k = 2. k=2.
返回链表 4->5.

解题思路:

假设链表共有m个节点,则从头节点走到尾节点需要m-1步(如五个手指间有四个空隙),如果走到尾结点的后一个null节点,则是走m步。倒数第k个,说明前面还有m-k个,那倒数第k个就是顺数的第m-k+1个,需要走m-k步才能到它。用快慢指针,让快指针先走k步,随后让快慢指针一起走,当快指针走到尾部的后一节点null的时候,慢指针刚好走了m-k步,也就是刚好到倒数第k个节点的位置。

Java代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        if(head == null || k < 0) return head;
        ListNode fast = head;
        ListNode slow = head;

        for(int i = 0;i < k;i++){//快指针先走k步
            fast = fast.next;
        }
        //现在快慢指针一起走,快指针指向null时,慢指针刚好指向倒数第k个节点
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

在这里插入图片描述

也可以先计算出链表总共有多少节点,然后从头节点走m - k步即可,不过相对快慢指针遍历了两遍链表

Go代码

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func kthToLast(head *ListNode, k int) int {
    // 假设共m个节点,则倒数第k个,前面还有m - k个节点,则倒数第k个是顺数的第m - k + 1个节点
    // 从头节点走m - k 步可以走到它
    if head == nil || k <= 0 {
        return -1
    }
    cur := head
    m := 0
    for cur != nil {
        m++
        cur = cur.Next
    }
    cur = head
    for i := 0;i < m - k;i++ {
        cur = cur.Next
    }
    return cur.Val
}

在这里插入图片描述


扩展题一:LeetCode 19 删除链表的倒数第N个节点

19. 删除链表的倒数第 N 个结点

LCR 021. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

在这里插入图片描述

输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]

示例 2:

输入: head = [1], n = 1
输出: []

示例 3:

输入: head = [1,2], n = 1
输出: [1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

解题思路:

首先需要明确,对于单链表的删除或者新增操作,我们需要找到待处理节点的前一个节点才好操作。

由于有可能要删除的是倒数最后一个结点,也即第一个结点,故我们可以建立一个虚拟头结点dummy,然后设定快慢指针,fastslow,让fast先走n步,slow不动,然后fastslow同时走,直到fast走到尽头(注意这里是最后一个不为空的节点,而不是null,和上面那题有点区别,因为是要找到要删除的节点的前一个节点,所以少走一步)。

假设加上虚拟头结点共有m个结点(则从虚拟头结点走到尾节点需要m-1步,如5根手指,中间只有4个间隙),倒数第n个,说明前面还有m-n个,那倒数第k个就是顺数的第m-n+1个,需要走m-n步才能到它,但是删除节点,我们要找的是待删除节点的前一节点,所以应该是走m-1-n步。而fastn步后,还剩下m-1-n步走到尾结点,此时fastslow再都走m-1-n步,fast到达尽头尾节点,而slow 走了m-1-n步,刚好是待删除节点的前一个节点,所以这个位置刚好是我们所要删除结点的前一位置。如图所示:
在这里插入图片描述

现在两个指针同时走,fast走到尾结点走了m-1-n步,故慢指针也是走了m-1-n步,还差n步才到尾结点,因此慢指针刚好指着我们需要的倒数第n个结点的前一结点,因为还差n步才到尾节点,说明当前节点到尾节点的节点个数总和是n+1,也即当前节点是倒数第n+1个节点,也即倒数第n个节点的前一节点。
在这里插入图片描述
注意:上述走法即使没有虚拟头节点,也是同样的走法,因为我们是按总节点个数m中夹着m-1步操作的,即有没有虚拟头结点,不影响走法,也是快指针也走n步,然后快慢指针同时走,只是走到尾节点的时候,快慢两个指针同时都比有虚拟头节点的少走一步而已,这里加了虚拟头节点,是因为可能要找的是真实头节点的前一节点。

Java代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //由于可能要删除的是头结点,故建立虚拟头结点,用于统一操作
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode fast = dummy,slow = dummy;
        //快指针先走n步
        for(int i = 0;i < n;i++){
            fast = fast.next;
        }
        //现在快慢指针一起走
        while(fast.next != null){
            slow = slow.next;
            fast = fast.next;
        }
        slow.next = slow.next.next;
        return dummy.next;

    }
}

在这里插入图片描述

Go代码

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    // 假设共有m个节点,倒数第n个,说明前面还有m-n个,则它是顺数的第m - n + 1个
    // 要删除,需要找他的前一个节点,是顺数第m - n 个
    // 从第一个节点走到尾部需要m - 1步,走m - n - 1步可以到待删除节点的前一节点
    // 所以 快指针先走n步,剩下m - n - 1步快慢指针一起走
    // 这样的话,快指针到尾节点的时候,慢指针能刚好到待删除节点的前一节点
    if head == nil || n <= 0 {
        return head
    }
    // 要删除的可能是头节点,故建立一个虚拟头节点
    dummy := &ListNode{}
    dummy.Next = head
    slow,fast := dummy,dummy
    // 因为要用到fast.Next,所以还是判断一下fast!=nil比较好,能避免空指针
    // 不写fast != nil的判断也能过,是因为题目保证了输入数据的合理性,但是工作中取值时,尽量先进行非空判断
    for i:=0;i < n && fast != nil;i++ {
        fast = fast.Next
    }
    for fast != nil && fast.Next != nil {
        fast = fast.Next
        slow = slow.Next
    }
    slow.Next = slow.Next.Next
    return dummy.Next
}

在这里插入图片描述


扩展题二:LeetCode 876 求链表的中间节点

876. 链表的中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:
在这里插入图片描述

输入:[1,2,3,4,5]
输出: 此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例 2:
在这里插入图片描述

输入: [1,2,3,4,5,6]
输出: 此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

提示:

给定链表的结点数介于 1100 之间。

解题思路:

同样是用快慢指针,这里由于是求中间节点,所以可以让快指针每次走两步,慢指针每次走一步,当链表长度是奇数时,快指针走到尾结点时,慢指针刚好指着中间节点。当链表长度是偶数时,快指针指着链表尾结点下一节点的null时,慢指针指着中间节点的第二个节点。
在这里插入图片描述

Java代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode fast = head;
        ListNode slow = head;
        //由于要用到fast.next.next,所以一定要判断一下fast.next!=null,否则出现空指针异常
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}

在这里插入图片描述

Go代码

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func middleNode(head *ListNode) *ListNode {
    // 快慢指针,快的一次走两步,慢的一次走一步
    // m个节点,中间有m-1步,所以m是奇数时,快指针走到尾节点时,慢指针刚好在中间节点
    // m是偶数时,快指针是走到尾节点后面的nil时,慢节点在中间节点,可以对照图模拟一下
    if head == nil {
        return head
    }
    slow,fast := head,head
    for fast != nil  {
        // 奇数情况
        if fast.Next == nil {
            break
        }
        slow = slow.Next
        fast = fast.Next.Next
    }

    return slow
}

在这里插入图片描述

扩展题三:2095. 删除链表的中间节点

2095. 删除链表的中间节点

给你一个链表的头节点 head 。删除 链表的 中间节点 ,并返回修改后的链表的头节点 head

长度为 n 链表的中间节点是从头数起第⌊n / 2⌋个节点(下标从0开始),其中 ⌊x⌋ 表示小于或等于x的最大整数。

对于 n = 1、2、3、4 5 的情况,中间节点的下标分别是 0、1、1、22

示例 1:

在这里插入图片描述

输入:head = [1,3,4,7,1,2,6]
输出:[1,3,4,1,2,6]
解释:
上图表示给出的链表。节点的下标分别标注在每个节点的下方。
由于 n = 7 ,值为 7 的节点 3 是中间节点,用红色标注。
返回结果为移除节点后的新链表。 

示例 2:

在这里插入图片描述

输入:head = [1,2,3,4]
输出:[1,2,4]
解释:
上图表示给出的链表。
对于 n = 4 ,值为 3 的节点 2 是中间节点,用红色标注。

示例 3:
在这里插入图片描述

输入:head = [2,1]
输出:[2]
解释:
上图表示给出的链表。
对于 n = 2 ,值为 1 的节点 1 是中间节点,用红色标注。
值为 2 的节点 0 是移除节点 1 后剩下的唯一一个节点。

提示:

  • 链表中节点的数目在范围 [1, 10^5] 内
  • 1 <= Node.val <= 10^5

解题思路:

相对上题找中间节点,多了一个要求,就是删除。要删除节点,所以需要一个指针记录前一个节点。然后要删除的可能是头节点,所以用一个虚拟头结点的套路。

Go代码

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func deleteMiddle(head *ListNode) *ListNode {
    // 快慢指针,快的一次走两步,慢的一次走一步
    // m个节点,中间有m-1步,所以m是奇数时,快指针走到尾节点时,慢指针刚好在中间节点
    // m是偶数时,快指针是走到尾节点后面的nil时,慢节点在中间节点,可以对照图模拟一下
    // 要删除节点,所以需要一个指针记录前一个节点
    if head == nil {
        return head
    }
    dummy := &ListNode{}
    dummy.Next = head
    pre := dummy
    slow,fast := head,head
    for fast != nil  {
        // 奇数情况
        if fast.Next == nil {
            break
        }
        pre = pre.Next
        slow = slow.Next
        fast = fast.Next.Next
    }
    pre.Next = pre.Next.Next

    return dummy.Next
}

在这里插入图片描述

举一反三

当我们用一个指针遍历链表不能解决问题的时候,可以尝试用两个指针来遍历链表。可以让其中一个指针遍历的速度快一些,比如一次在链表中走两步,或者让它先在链表上走若干步。

双指针、快慢指针两个思想很重要,数组和链表问题经常用到。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值