返回倒数第k个结点——双指针

本文介绍了如何使用快慢指针、栈和递归三种方法解决查找单链表中倒数第k个节点的问题,通过实例和代码演示了经典算法和递归技巧在实际问题中的应用。

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

注意:本题相对原题稍作改动

示例:

输入: 1->2->3->4->5 和 k = 2
输出: 4
说明:

给定的 k 保证是有效的。


这个题是非常简单的,大家看到题目可以先自己想想怎么做,然后再看题解!

这题其实是非常经典的用快慢指针解决的问题,大概思路是: 快指针先走k步,然后快慢指针同时往前走,直到快指针到结尾的时候,返回慢指针节点的值。

看代码实现:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int kthToLast(ListNode head, int k) {
        ListNode fast=head;
        ListNode slow=head;
        for(int i=1;i<k;i++){
            fast=fast.next;
        }
        while(fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow.val;
    }
}

我以为自己已经很牛了,但是看了力友的解题我又跪了!

以下解题来自:lc大佬

方法二:使用栈解决

把原链表的结点全部压栈,然后再把栈中最上面的k个节点出栈,出栈的结点重新串成一个新的链表即可,直接看下代码:

    public int kthToLast(ListNode head, int k) {
        Stack<ListNode> stack = new Stack<>();
        //链表节点压栈
        while (head != null) {
            stack.push(head);
            head = head.next;
        }
        //在出栈串成新的链表
        ListNode firstNode = stack.pop();
        while (--k > 0) {
            ListNode temp = stack.pop();
            temp.next = firstNode;
            firstNode = temp;
        }
        return firstNode.val;
    }

方法三:递归求解

终止条件很明显就是当节点head为空的时候,就没法递归了,这里主要看的是逻辑处理部分,当递归往下传递到最底端的时候,就会触底反弹往回走,在往回走的过程中记录下走过的节点,当达到k的时候,说明到达的那个节点就是倒数第k个节点,直接返回即可。接下来直接看代码:

    //全局变量,记录递归往回走的时候访问的结点数量
    int size;

    public int kthToLast(ListNode head, int k) {
        //边界条件判断
        if (head == null)
            return 0;
        int val = kthToLast(head.next, k);
        ++size;
        //从后面数结点数小于k,返回空
        if (size < k) {
            return 0;
        } else if (size == k) {
            //从后面数访问结点等于k,直接返回传递的结点k即可
            return head.val;
        } else {
            //从后面数访问的结点大于k,说明我们已经找到了,
            //直接返回node即可
            return val;
        }
    }

这里在总结一下递归算法吧!!

递归算法就是一个先递后归的过程,在这个题中图解如下:
在这里插入图片描述
把这个题的递归方法抽取一下精华就是:

public ListNode getKthFromEnd(ListNode head, int k) {
    //终止条件
    if (head == null)
        return head;
    //递归调用
    ListNode node = getKthFromEnd(head.next, k);
    //逻辑处理
    ……
}

1.有递归调用
2.有递归出口
3.使用递归的逻辑的处理

大家有什么问题欢迎私聊探讨!


算法对程序员来说及其重要,语言和开发平台不断变化,但是万变不离其宗的是那些算法和理论,刷算法最最最直白的原因就是找一个好的工作,那刷题一定是必不可少的就是一个好的刷题平台。现在算法刷题平台鱼龙混杂,非常多。如何选择一个正确的刷题平台呢?

这里我推荐牛客网如果你对相应语言的基础语法已经比较自信了那你也可以刷算法题,这里牛客网对算法题的难度分类也是很棒的,有入门题,面试必刷题,面试高频题等等。如果基础比较差那么也不用担心,牛客网上面也有基础语法题来帮助你更好地学习基础。

在这里插入图片描述

为了大家方便刷题我直接把牛客网的链接放在下面,大家点击蓝色字体就可以直接进行跳转刷题了!

传送门:刷题入口

刷题这件事,大家一定要认真起来,不可懈怠!

日积月累,方成大器!

<think>根据用户需求,需要设计一个算法返回链表倒数第k个结点的指针,若不存在则返回空指针。同时,需要处理多组输入,每组输入包括链表长度n、倒数序号k以及n个链表元素,以n=0结束输入。对于每组输入,输出倒数第k个结点的值,如果不存在则输出NULL。 参考引用[1][2][3]中的思路,可以使用双指针技巧(快慢指针)来高效地解决这个问题: 1. 初始化两个指针pFast和pSlow,都指向头结点(注意:头结点是带数据的第一个结点还是哨兵结点?根据题目描述,链表带头结点,通常头结点不存储数据,第一个数据结点是头结点的下一个结点)。 2. 先让快指针pFast移动k步(注意:如果链表长度不足k,则返回空)。 3. 然后同时移动快指针和慢指针,直到快指针到达链表末尾(即快指针的下一个为空)。 4. 此时慢指针指向的结点就是倒数第k个结点。 但是,注意引用[3]中的代码: 它使用了两个指针,先让快指针移动k-1步(因为是从头结点开始,头结点通常不计入数据结点?),然后同时移动两个指针直到快指针的下一个为空。然而,这里有一个细节:链表是否带头结点(即头结点不存放数据,第一个数据结点是head->next)?根据题目描述,链表带头结点,所以头结点不存储数据,第一个数据结点是头结点的下一个结点。 因此,我们需要调整: - 算法应该从第一个数据结点开始计数,但题目要求返回倒数第k个结点(从1开始计数),那么倒数第k个结点就是正数第n-k+1个结点(n为链表长度)。 考虑到双指针算法: 步骤: 1. 设置两个指针pFast和pSlow,初始时都指向头结点(头结点是哨兵结点,第一个数据结点是head->next)。 2. 先让快指针pFast移动k步(移动k次,指向第k个结点,注意:这里移动的k步中,每一步都是移动到下一个结点,且要确保在移动过程中没有遇到空指针)。 3. 然后,同时移动pFast和pSlow,直到pFast指向最后一个结点(即pFast->next为空)?实际上,当pFast移动k步后,pSlow和pFast之间相隔k-1个结点,然后同时移动直到pFast到达尾结点,这时pSlow指向的结点就是倒数第k个结点。 但是,我们需要注意的是: 如果链表有头结点,那么实际的数据结点是从head->next开始的。因此,我们初始时将两个指针都指向头结点(即哨兵结点),然后让快指针先走k步(注意:这k步是走到第k个数据结点?还是包括头结点?)——这里需要明确:头结点不计入数据结点。 根据通常的做法,我们让快指针先走k步(每次走一步,走k次),此时快指针指向第k个数据结点(即从头结点的下一个结点开始算起,第一个数据结点就是头结点的next,第k个数据结点就是走了k步后的位置)。然后,慢指针从头结点开始,然后两个指针同时向后移动,直到快指针到达最后一个结点(即快指针的next为空)?不对,应该是直到快指针指向最后一个结点(即它的下一个结点为空)时,慢指针指向的结点就是倒数第k个结点的前一个结点?因为慢指针是从头结点开始,所以实际上慢指针最后指向的结点倒数第k个结点的前一个结点?这显然不对。 重新思考:我们想要得到的是倒数第k个结点,那么当快指针指向最后一个结点(即尾结点)时,慢指针应该指向倒数第k个结点。假设链表有n个数据结点,那么快指针先走k步,此时快指针指向第k个数据结点(注意头结点不算)。然后,慢指针从头结点(即第一个数据结点的前一个)开始,快指针从第k个数据结点开始,同时移动直到快指针到达尾结点。这时,快指针又移动了n-k步,因此慢指针也移动了n-k步,从第一个数据结点的前一个结点(头结点)移动n-k步,到达的位置是第(n-k)个结点(头结点算第0个?),那么下一个结点就是第(n-k+1)个结点,即正数第(n-k+1)个结点,也就是倒数第k个结点。但是,此时慢指针指向的结点是第(n-k)个结点(头结点算第0个),那么倒数第k个结点应该是慢指针的下一个结点?这不对。 为了避免混淆,我们通常的做法是: 让两个指针都指向第一个数据结点(head->next)。然后快指针先走k-1步(这样快指针和慢指针之间相差k-1个结点),然后同时移动,直到快指针到达最后一个结点(即快指针->next为空?不对,当快指针指向最后一个结点时,慢指针指向倒数第k个结点)。但是,如果k=1,那么快指针不需要先走?然后同时移动,当快指针指向最后一个结点时,慢指针指向的就是倒数第1个结点。 步骤: 1. 初始化:pSlow = head->next; pFast = head->next; 2. 先让快指针走k-1步(注意:每走一步都要检查是否为空,因为可能链表长度不足k) for (int i=0; i<k-1; i++) { if (pFast->next == NULL) { // 不足k步就遇到空,说明链表长度小于k return NULL; } pFast = pFast->next; } 3. 然后同时移动两个指针,直到快指针的下一个结点为空(即快指针指向尾结点): while (pFast->next != NULL) { // 当快指针还没有到达尾结点 pSlow = pSlow->next; pFast = pFast->next; } 4. 此时,pSlow指向的就是倒数第k个结点。 但是,这个算法在k=1时,快指针不需要走(k-1=0),然后直接进入while循环,但是此时快指针指向第一个结点,它的next如果不为空,则两个指针同时移动,直到快指针的next为空(即快指针指向最后一个结点),这时慢指针指向倒数第一个结点,正确。 然而,这里有一个边界:如果链表长度为0(即只有头结点,没有数据结点),那么head->next为空,则第一步初始化两个指针都指向NULL,然后k>=1就会导致在第二步中pFast为空,无法进行后续操作。所以我们需要在开始判断链表是否为空(即没有数据结点)。 因此,算法可以这样: 1. 判断链表是否为空(head->next == NULL)或者k小于1,则直接返回NULL。 2. 设置两个指针都指向第一个数据结点(head->next)。 3. 快指针先走k-1步,每走一步都要判断当前快指针是否为空(实际上,在移动过程中,如果某一步pFast已经为空,说明链表长度不足k,返回NULL)。注意:在第3步中,我们走k-1步,要求每次移动前pFast不为空(因为循环条件里已经判断了pFast->next不为空?不,我们移动k-1步,每一步都可能遇到空。所以每一步都需要判断当前pFast是否为空(在移动一步后)?但初始时pFast是第一个数据结点,非空,然后移动第一步后,判断pFast是否为空(即移动后是否为空)。实际上,我们在移动一步之前,需要确保当前pFast不为空,才能移动,否则提前结束返回NULL。 但是,实际上,在第二步之后,pFast指向第一个数据结点(不为空),然后我们让它走k-1步,每一步:移动到下一个结点,然后判断移动后的指针是否为空(如果为空,说明链表长度不足k,返回NULL)。注意:走了k-1步后,pFast指向第k个数据结点(如果存在的话)。 4. 然后同时移动两个指针,直到pFast->next为空。此时pSlow指向倒数第k个结点。 但是,考虑特殊情况:如果k等于链表长度n,那么快指针走了k-1步后指向第k个结点(即最后一个结点),然后进入同时移动的循环,此时pFast->next为空,所以循环不执行,pSlow仍然指向第一个结点(即倒数第n个结点,正确)。 因此,这个算法可行。 然而,根据引用[3]的代码,它使用的是带头结点链表,并且初始时两个指针都指向头结点(pHead),然后让快指针先走k-1步(从0开始计数,所以走k-1步后指向第k-1个结点?这里要注意)。引用[3]的代码: Node* pFast = pHead; Node* pSlow = pHead; //pFast指针后移k-1位 while (--k) { pFast = pFast->Next; } // 循环结束后pFast指向表尾?不对,应该是走了k-1步,所以指向第k-1个结点(头结点算第0个?) while (pHead&&(pFast->Next)) { pSlow = pSlow->Next; pFast = pFast->Next; } return pSlow; 但是,这里有一个问题:如果k=1,那么--k变成0,循环不执行,然后进入while循环,条件是pHead存在且pFast->Next存在(此时pFast指向头结点,pFast->Next是第一个数据结点,如果链表非空则存在),然后进入循环,将pSlow和pFast都后移一次,然后pFast->Next可能是空也可能不是。这样最后返回的pSlow是头结点的下一个结点?也就是第一个数据结点倒数第一个结点,正确。 但是,当k=2时,--k变成1,执行一次循环:pFast指向头结点的下一个结点(第一个数据结点),然后进入while循环,条件为pFast->Next存在(即第二个数据结点存在),然后同时移动两个指针:pSlow变为第一个数据结点,pFast变为第二个数据结点。然后循环条件判断pFast->Next(即第三个结点),如果存在则继续移动,直到pFast->Next为空,这时pSlow指向倒数第2个结点。 所以引用[3]的代码是可行的,但它返回的指针是倒数第k个结点吗?注意在k=1时,它返回的是第一个数据结点倒数第一个),正确。但是,注意在k大于链表长度时,在第一个while循环(--k)中,如果k大于链表长度(包括头结点?)这里头结点算一个结点,然后数据结点有n个,那么链表总共有n+1个结点(带头结点)。而第一个循环中,k的初始值大于1,每次移动k-1次,如果在移动过程中pFast->Next为空(即pFast已经指向尾结点)而k还未减到0,则会出现空指针访问。所以引用[3]的代码没有在循环中判断指针是否为空,这是不安全的。 因此,我们需要在快指针移动时检查是否为空。 总结我们的算法(带头结点链表): 步骤: 1. 如果链表为空(head->next == NULL)或k<1,直接返回NULL。 2. 初始化两个指针pSlow和pFast,都指向头结点(head)。 3. 快指针先走k步(注意:这里走k步,而不是k-1步,因为我们是从头结点开始,头结点不算数据结点,但我们要找的倒数第k个结点是数据结点。如果我们走k步,那么pFast指向第k个结点(头结点算第0个,则第k个结点是数据结点中的第k个)?不对,我们想让快指针和慢指针之间相差k个结点(包括慢指针当前指向的结点?)) 实际上,更常见的做法是: 让快指针先走k步(从头结点开始,走k步,指向第k个结点(从0开始计数,头结点是0,第一个数据结点是1,第k个数据结点就是第k步后的位置)),然后同时移动两个指针,当快指针指向尾结点(最后一个结点)时,慢指针指向倒数第k个结点的前一个结点?因为慢指针从头结点开始,移动的步数与快指针相同(在快指针先走k步后,再同时移动直到快指针到尾结点,此时快指针总共移动了n步(n为数据结点的个数),慢指针也移动了n-k步(因为快指针先走了k步,然后同时移动的步数是n-k步),所以慢指针从head移动了n-k步,指向第n-k个结点(头结点算0,则第n-k个结点就是倒数第k个结点的前一个结点?)。这样,慢指针的下一个结点才是倒数第k个结点。 因此,我们有两种选择: 选择1:两个指针都从第一个数据结点开始,然后快指针先走k-1步(这样它们之间相差k-1),然后同时移动直到快指针到尾结点,慢指针指向倒数第k个结点。 选择2:两个指针都从头结点开始,快指针先走k步(此时快指针指向第k个结点(数据结点中的第k个)),然后同时移动直到快指针到尾结点,此时慢指针指向倒数第k个结点的前一个结点,然后返回慢指针的下一个结点。 用户要求返回倒数第k个结点的指针,所以选择1更直接。但注意,链表带头结点,第一个数据结点是head->next。 我们采用选择1: 1. 初始化:pSlow = head->next, pFast = head->next; 2. 快指针先走k-1步(注意:如果链表长度小于k,则返回NULL) for (int i=0; i<k-1; i++) { if (pFast->next == NULL) { return NULL; // 链表长度小于k } pFast = pFast->next; } 3. 然后同时移动两个指针,直到pFast的next为空(即pFast指向最后一个结点): while (pFast->next != NULL) { pSlow = pSlow->next; pFast = pFast->next; } 4. 返回pSlow。 注意:这个过程中,如果k=0,因为第一步k-1为负数,循环不会执行,但k=0是非法的(倒数第0个结点不存在),所以我们在开始判断k<1时返回NULL。 还有特殊情况:链表为空(head->next==NULL),那么pSlow和pFast都为空,则第一步后,如果k-1>=0,那么循环中访问pFast->next会导致段错误。所以我们在开始就要判断链表为空的情况。 因此,完整的算法: Node* findKthFromEnd(Node* head, int k) { // 链表为空或k小于1 if (head == NULL || head->next == NULL || k < 1) { return NULL; } Node* pSlow = head->next; Node* pFast = head->next; // 快指针先走k-1步 for (int i = 0; i < k-1; i++) { if (pFast->next == NULL) { // 还未走完k-1步就已经到达尾结点,说明链表长度小于k return NULL; } pFast = pFast->next; } // 同时移动两个指针,直到快指针到达尾结点 while (pFast->next != NULL) { pSlow = pSlow->next; pFast = pFast->next; } return pSlow; } 但是,这里还有一个问题:如果k=1,那么循环0次,然后进入while循环,移动pSlow和pFast直到pFast->next为空,此时pSlow指向倒数第一个结点,正确。 然而,考虑k=链表长度n的情况:快指针先走n-1步,此时pFast指向链表的最后一个结点(因为初始在第一个结点,走n-1步就到第n个结点,即尾结点)。然后进入while循环,pFast->next为空,所以不执行循环,直接返回pSlow(此时pSlow仍然指向第一个结点),而第一个结点就是倒数第n个结点,正确。 因此,算法正确。 接下来,我们需要按照题目要求处理多组输入: 输入格式:每行依次为链表长度n和倒数序号k,下行为n个链表元素,当n=0时结束输入。 输出:对于每组数据,输出倒数第k个结点的值,如果不存在则输出NULL。 注意:题目要求链表带头结点,所以我们在构建链表时,需要创建一个头结点,然后后面接n个数据结点。 编程实现: 使用C语言,因为引用[3]是C/C++风格代码。 步骤: 1. 定义链表结点结构: typedef struct Node { int data; struct Node *next; } Node, *LinkList; 2. 创建链表:根据输入的n和n个整数,创建带头结点的单链表。 3. 实现函数:Node* findKthFromEnd(LinkList head, int k) 4. 处理多组输入,直到n==0。 注意:输入中,如果k大于n或者k<=0,则返回NULL。 例如输入: 5 2 1 2 3 4 5 输出:4(因为倒数第2个结点是4) 再如: 5 5 1 2 3 4 5 输出:1(倒数第5个结点) 5 6 1 2 3 4 5 输出:NULL 下面编写代码框架: 注意:每组输入包括: 第一行:n和k 第二行:n个整数 代码结构: while (scanf("%d", &n) != EOF && n != 0) { scanf("%d", &k); // 创建链表,包含n个结点 LinkList head = (LinkList)malloc(sizeof(Node)); // 头结点 head->next = NULL; Node *tail = head; for (int i=0; i<n; i++) { int data; scanf("%d", &data); Node *newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; newNode->next = NULL; tail->next = newNode; tail = newNode; } Node *res = findKthFromEnd(head, k); if (res == NULL) { printf("NULL\n"); } else { printf("%d\n", res->data); } // 释放链表内存,避免内存泄漏 Node *p = head; while (p) { Node *temp = p; p = p->next; free(temp); } } 但是,请注意内存释放:头结点也要释放。 然而,题目要求多组输入,每组都要释放链表。 我们将上述思路转化为代码。 注意:在函数findKthFromEnd中,我们假设链表带头结点,并且head不为空(头结点存在),但数据结点可能为空(即n=0,但此时在创建链表时,head->next为空,所以我们在函数入口判断了head->next为空则直接返回NULL)。所以当n=0时,我们不会进入创建数据结点的循环,链表只有头结点,然后调用函数,返回NULL。 但是,题目输入要求:n=0时结束输入,所以当n=0时,我们跳出循环,不进行后续处理。 因此,我们在循环开始判断n!=0。 代码实现如下: </think>### 设计单链表倒数第k个结点查找算法 针对返回链表倒数第k个结点指针的需求(不存在则返回空指针),结合引用[1][2][3]的思路,使用**双指针(快慢指针)算法**实现高效查找。以下是完整实现: ```c #include <stdio.h> #include <stdlib.h> /* 链表结点结构 */ typedef struct Node { int data; struct Node* next; } Node, *LinkList; /* 查找倒数第k个结点 */ Node* findKthFromEnd(LinkList head, int k) { if (head == NULL || k <= 0) return NULL; // 非法输入检查 Node *fast = head->next; // 快指针(指向第一个数据结点) Node *slow = head->next; // 慢指针(指向第一个数据结点) /* 快指针先前进k-1步 */ for (int i = 0; i < k - 1; i++) { if (fast == NULL) return NULL; // 链表长度不足k fast = fast->next; } if (fast == NULL) return NULL; // 检查k等于链表长度的情况 /* 双指针同步前进 */ while (fast->next != NULL) { slow = slow->next; fast = fast->next; } return slow; } int main() { int n, k; while (scanf("%d", &n) != EOF && n != 0) { scanf("%d", &k); /* 创建带头结点的空链表 */ LinkList head = (Node*)malloc(sizeof(Node)); head->next = NULL; Node* tail = head; /* 构建链表 */ for (int i = 0; i < n; i++) { int data; scanf("%d", &data); Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; newNode->next = NULL; tail->next = newNode; tail = newNode; } /* 查找并输出结果 */ Node* result = findKthFromEnd(head, k); if (result == NULL) { printf("NULL\n"); } else { printf("%d\n", result->data); } /* 释放链表内存 */ Node* p = head; while (p != NULL) { Node* temp = p; p = p->next; free(temp); } } return 0; } ``` ### 算法详解 1. **初始化阶段** - 快慢指针均指向第一个数据结点(`head->next`) - 时间复杂度:$O(1)$ 2. **快指针先行** - 快指针先移动 $k-1$ 步(建立 $k-1$ 的间距) - 若中途遇到 `NULL` ⇒ 链表长度 $<k$ ⇒ 返回 `NULL` - 时间复杂度:$O(k)$ 3. **双指针同步移动** $$\begin{cases} \text{快指针移动到尾结点} & : \text{移动 } n-k \text{ 步} \\ \text{慢指针位置} & : \text{倒数第 } k \text{ 个结点} \end{cases}$$ - 数学原理:$n - (n - k + 1) = k - 1$ - 时间复杂度:$O(n-k)$ 4. **整体时间复杂度** $$O(k) + O(n-k) = O(n)$$ 空间复杂度:$O(1)$(仅用双指针) ### 输入输出示例 ``` 输入: 5 2 1 2 3 4 5 4 3 10 20 30 40 0 输出: 4 // 倒数第2个结点值 30 // 倒数第3个结点值 ``` ### 边界处理 | 特殊情况 | 处理方式 | |-------------------|----------------------------------| | $k \leq 0$ | 直接返回 `NULL` | | 链表长度 $< k$ | 快指针先行时提前终止返回 `NULL` | | $k = 1$ | 直接返回结点 | | 空链表($n=0$) | `head->next==NULL` 返回 `NULL` | ### 性能优化点 1. **输入校验**:提前过滤 $k \leq 0$ 或 $k > n$ 的情况 2. **内存管理**:每组数据结束后释放链表内存 3. **头结点设计**:统一空链表处理逻辑(哨兵结点) 4. **循环终止**:快指针移动时实时检测空指针 > 该算法满足 $O(n)$ 时间复杂度和 $O(1)$ 空间复杂度的要求,符合引用[1][2][3]的高效性设计原则。
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

abcccccccccccccccode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值