《剑指 Offer》学习记录:题 22:链表中倒数第 k 个结点

题 22:链表中倒数第 k 个结点

题干

输入一个链表,输出该链表中倒数第 k 个结点。为了符合大多数人的习惯,本题从 1 开始计数,即链表的尾结点是倒数第 1 个结点。例如一个链表有 6 个结点,从头结点开始它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个结点是值为 4 的结点。——《剑指 Offer》P134

测试样例

链表的数据结构定义如下(Python):

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

若传入的头结点 head 指向的链表结构如图所示:

要求返回倒数第 4 个结点,也就是结点 2:

蛮力法

方法思路

蛮力法的思路比较简单,设表长为 n,为了得到第 n - k 个结点,就需要先获得表长。首先先遍历一遍链表求出表长,然后再遍历一次链表得到第 n - k 个结点。

题解代码

class Solution:

    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        count = 0

        ptr = head
        while ptr:    #遍历链表获得表长
            ptr = ptr.next
            count = count + 1

        ptr = head    #遍历到 n - k 个结点
        for i in range(count - k):
            ptr = ptr.next
        
        return ptr

时空复杂度

设表长为 n,第一次遍历需要遍历整个链表,第二次遍历 n - k 次。得出 T(n) = 2n - k,进而得到 O(n) = n。
由于不需要任何辅助空间,蛮力法的空间复杂度为 O(1)。

快慢指针

方法思路

快慢指针的想法比较巧妙,也就是可以定义 2 个指针。先使用第一个指针遍历链表到表尾,此时若第二个指针和第一个指针差距 k 个结点,就可以直接得到第 n - k 个结点。例如对于链表 [1,2,3,4,5] 求倒数第 2 个结点,可以先让快指针先出发遍历 2 个结点,慢指针先不动。

接下来同时移动 2 个指针,当快指针遍历完毕时,由于慢指针晚出发 2 个结点,所以慢指针此时指向的就是倒数第 2 个结点。

题解代码

class Solution:
    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        fast_ptr = head
        slow_ptr = head

        for i in range(k):    #快指针先出发 k 个结点
            ptr = ptr.next

        while ptr:    #快慢指针同时向前遍历
            ptr = ptr.next
            slow_ptr = slow_ptr.next

        return slow_ptr

时空复杂度

设表长为 n,遍历整个链表得到时空复杂度 O(n) = n。注意此时虽然时间复杂度和蛮力法一样,T(n) = n 有可能比蛮力法低。
由于不需要任何辅助空间,空间复杂度为 O(1)。

栈辅助法

方法思路

获得倒数第 k 个结点,可以认为是逆序链表后取到第 k 个元素。此时可以借助一个栈结构来让链表逆序,例如对于链表 [1,2,3,4,5] 求倒数第 2 个结点,可以先遍历一遍链表,将所有的数据元素入栈。此时就得到了链表的逆序序列,出栈栈顶的 k 个元素就能得到需要的结点。

题解代码

class Solution:
    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        a_stack = []
        ptr = head

        while ptr:    #链表所有元素入栈
            a_stack.append(ptr)
            ptr = ptr.next

        for i in range(k):    #栈顶 k 个元素出栈
            ptr = a_stack.pop()

        return ptr

时空复杂度


设表长为 n,遍历整个链表得到时空复杂度 O(n) = n。注意此时虽然时间复杂度和蛮力法一样,但是 T(n) = n + k 比蛮力法低。
由于需要一个栈结构作为辅助空间,空间复杂度为 O(n)。

递归法

方法思路

获得倒数第 k 个结点,可以认为是遍历链表后通过回溯取到第 k 个元素。此时可以使用递归遍历完整个链表,然后回溯 k 次得到倒数第 k 个结点,接着再将这个结点传递回去。

题解代码

class Solution:
    num = 0    #全局变量记录回溯了几次

    def fun(self, head: ListNode, k: int) -> ListNode:
        if head.next == None:    #遍历完链表,开始回溯
            Solution.num = Solution.num + 1
            return head

        ptr = self.fun(head.next, k)
        Solution.num = Solution.num + 1
        if Solution.num > k:    #回溯次数 k 次
            return ptr    #传递倒数第 k 个结点
        else:
            return head    #返回递归层次所在结点

    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        Solution.num = 0    #力扣需要手动初始化全局变量
        return self.fun(head, k)

时空复杂度

设表长为 n,需要遍历整个链表,然后回溯 n 次。得出 T(n) = 2n,进而得到 O(n) = n。
由于递归需要额外的辅助空间,空间复杂度为 O(n)。

参考资料

《剑指 Offer(第2版)》,何海涛 著,电子工业出版社
双指针,栈,递归3种解决方式,有两种击败了100%的用户

原文地址:https://www.cnblogs.com/linfangnan/p/14670483.html