剑指Offer-16-链表中倒数第k个结点及其相关变形题目

题目

输入一个链表,输出该链表中倒数第k个结点。为了不引起歧义,我们规定最后一个节点为倒数第1个结点。

解析

预备知识

最简单想法肯定就是链表遍历了,分为2次遍历,第一遍历统计出链表的所有节点数,那么倒数第k个结点位于n - k + 1位置处(注意此处说的位置从1开始计数),那么我们第二次遍历则从头结点开始,走n - k步即可到达倒数第k个结点。
由于思路很简单,此处代码省略,感兴趣可以自行尝试,也可与我交流。

思路一

我们知道如果用一个指针,那么必须遍历2次才可得到答案。那么如何利用1次遍历就可以找到目标节点呢?
这里我们采用2个指针,初始化2个指针都指向头结点,之后让第二个指针先走k步,然后同时让2个指针前进,直到第二个指针指向空为止,也就是第二个指针指向了n + 1的位置,这是我们第一个指针正好指向n - k + 1的位置,也就是倒数第k个结点位置。
代码的实现要注意:

  1. 处理头结点为空的时候
  2. 可能k的值大于链表总结点数
    我们这里图例展示求倒数第二个节点的过程:
    这里写图片描述
    /**
     * 我的思路,用2个指针,一个先走k步,然后再一起走,当第二指针走到n位时,
     * 第一个指针正好指向(n - k)位置
     * @param head
     * @param k
     * @return
     */
    public static ListNode FindKthToTail1(ListNode head, int k) {
        if(head == null || k <= 0) {
            return null;
        }
        ListNode q = head;
        ListNode p = head;
        for(int i = 1; i <= k; i++) {
            if(p != null) {
                p = p.next;
            }else { // 说明k大于结点总数量了
                return null;
            }
        }
        while(p != null) {
            q = q.next;
            p = p.next;
        }
        return q;
    }

其实上述代码不够简明,看了某大神的简明代码,如下:

    /**
     * 牛逼的写法
     * @param head
     * @param k
     * @return
     */
    public static ListNode FindKthToTail2(ListNode head, int k) {
        if(head == null || k <= 0) {
            return null;
        }
        ListNode q = head;
        ListNode p = head;
        int i = 0;
        for(; p != null; i++) {
            if(i > k) {
                q = q.next;
            }
            p = p.next;
        }
        return i < k ? null : q;
    }

note:需特别注意当k大于链表本身长度时的情况。

拓展

利用2个指针的变形的链表题目有很多,此处列举几个。

题目1

求链表的中间节点。当节点数量为奇数时,则返回中间节点。当节点数量为偶数时,则返回中间两个节点的第一个节点。

思路

这里虽然看起来跟上面倒数第k个结点类似,但是这里我们无法确定中间位置位于何处。所以要变换一下思路。
我们采取2个步长不一致的指针,第一个指针步长为1,第二个指针步长为2。当第二个指针指向链表末尾时,第一个指针正好指向链表中间位置。下面我具体说下过程:

  • 我们保证第一个指针q永远指向奇数位置,第二个指针p用于指向偶数位置(2,4,6…)
  • 当头指针为空,直接返回空
  • 当链表总数为奇数时,假如是5,如果想结束的时候,第一个指针正好指向3位置,那么第二指针此时应该指向6位置(为空)。所以循环结束条件为 p == null
  • 当链表总数为偶数时,假如是6,如果想结束的时候,第一个指针正好指向3位置,那么第二指针此时应该指向6位置(链表最后一个位置,它的下一个节点为空)。所以循环结束条件为 p.next == null

图示如下:
这里写图片描述
综上,代码就出来:

    /**
     * 寻找链表的中间节点
     * 当节点数量为奇数时,则返回中间节点
     * 当节点数量为偶数时,则返回中间两个节点的第一个节点
     * @param head
     * @return
     */
    public static ListNode findMiddle(ListNode head) {
        if(head == null) {
            return null;
        }
        ListNode q = head;
        ListNode p = head.next;
        while(p != null && p.next != null) {
            q = q.next;
            p = p.next.next;
        }
        return q;
    }
题目2

判断一个单向链表是否有环。

思路

同样此处的思路也是采用步长不一致的指针,第一个指针步长为1,第二个指针步长为2。如果存在环,速度快的指针必然会在某一刻追上慢的指针。如果不存在环, 速度快指针必然率先走到链表末尾。下面我具体说下过程:

  • 因为可能给定链表无环,所以循环结束条件可以参照题目1的结束条件,可以有效避免空指针异常。
  • 因为可能给定链表有环,所以需要额外添加循环结束条件,即第一个指针与第二个指针相等时,说明第二个指针追上了第一个指针,表明链表有环。
  • 因为存在2种情况导致循环结束,所以需要额外的判断语句来判断是否是因为有环结束的。

综上,代码随之而出:

    /**
     * 判断单链表是否有环
     * @param head
     * @return
     */
    public static boolean isCircle(ListNode head) {
        if(head == null) {
            return false;
        }
        ListNode q = head;
        ListNode p = head.next;
        while(p != null && p.next != null && p != q) {
            q = q.next;
            p = p.next.next;
        }
        return p != q ? false : true;
    }

其实链表有环问题还有很多需要了解的,参照这篇博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值