面试:面试常见的链表类算法捷径(一)

链表是我们数据结构面试中比较容易出错的问题,所以很多面试官总喜欢在这个上面下功夫,为了避免出错,我们最好先进行全面分析。在实际软件开发周期中,设计的时间通常不会比编码时间短,在面试的时候我们不要着急于写代码,而是一开始仔细分析和设计,这将给面试官留下一个很好的印象。

与其很快写出一段千疮百孔的代码,不如仔细分析后再写出健壮性无敌的程序。

面试题:输入一个单链表的头结点,返回它的中间元素。为了方便,元素值用整型标识。

当应聘者看到这道题的时候,内心一阵狂喜,怎么给自己遇到这么简单的题。拿起笔开始写,先遍历整个链表,拿到链表的长度len,再次遍历链表,位于len/2的元素就是链表中的中间元素。

所以这个题最重要的点就是拿到链表的长度len。而拿到这个len也比较简单,只需要遍历前设定一个count值,遍历的时候count++,第一次遍历结束,就拿到单链表的长度len了。

于是我们很快写出了这样的代码:

/**
 * Describe:
 * <p>
 * Author: Lixm
 * Date: 2018/9/17
 */
public class LinkFindDemo {
    public static class LinkNode {
        int data;
        LinkNode next;

        public LinkNode(int data) {
            this.data = data;
        }
    }

    private static int getTheMid(LinkNode head) {
        int count = 0;
        LinkNode node = head;
        while (head != null) {
            head = head.next;
            count++;
        }
        for (int i = 0; i < count / 2; i++) {
            node = node.next;
        }
        return node.data;
    }

    public static void main(String[] args) {
        LinkNode head=new LinkNode(1);
        head.next=new LinkNode(2);
        head.next.next=new LinkNode(3);
        head.next.next.next=new LinkNode(4);
        head.next.next.next.next=new LinkNode(5);
        System.out.println(getTheMid(head));
    }
}

面试官看到这个代码的时候,他告诉我们上面代码循环了两次,但他期待只有一次。

于是我们绞尽脑汁,突然想到了网上介绍过的一个概念:快慢指针法

假设我们设置两个变量slow,fast其实都指向单链表的头结点当中,然后依次向后面移动,fast的移动速度是slow的2倍。这样当fast指向末尾结点的时候,slow就正好在正中间了。

想清楚这个思路后,我们很快就能写出如下代码:

 private static int getTheMid2(LinkNode head) {
        LinkNode slow = head;
        LinkNode fast = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow.data;
    }

快慢指针法举一反三

快慢指针法 确实是在链表类的面试中特别好用,我们不妨在这里举一反三,对原题稍微修改一下,其实也可以实现。

面试题:给定一个单链表的头结点,判断这个链表是否是循环链表。

和前面的问题一样,我们只需要定义两个变量slow,fast,同时从链表的头结点出发,fast每次走链表,而slow每次只走一步。如果走得快的指针追上了走得慢的指针,那么链表就是环形(循环)链表。如果走得快的指针走到了链表的末尾(fast.next指向了null)都没有追上走得慢的指针,那么链表就不是环形链表了。

有了这样的思路,实现代码那还不是分分钟的事儿么。

     private static boolean isRingLink(LinkNode head) {

        LinkNode slow = head;
        LinkNode fast = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (slow == fast || fast.next == slow) {
                return true;
            }
        }
//简化后可以如下写法,省略slow
//        LinkNode fast = head;
//        while (fast != null && fast.next != null) {
//            fast = fast.next.next;
//            head = head.next;
//            if (head == fast || fast.next == head) {
//                return true;
//            }
//        }
        
        return false;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
        System.out.println(getTheMid2(head));
        head.next.next.next.next.next = head;
        System.out.println(isRingLink(head));
    }

确实有意思,快慢指针法再一次利用它的优势巧妙的解决了我们的问题。

快慢指针法的延展

我们上面讲解的【快慢指针法】均是一个变量走1步,一个变量走n步。我们其实还可以扩展它。这个【快慢】并不是说一定要同时遍历。

比如说《剑指Offer》中第15道面试题,就运用到了【快慢指针法】的延展。

面试题:输入一个单链表的头结点,输出该链表中倒数第k个节点的值

初一看这个似乎并不像我们前面学习到的【快慢指针法】的考察。所以大多数人就迷糊了,进入到常规化思考。依然还是设置一个整型变量count,然后每次循环的时候count++,拿到链表的长度n。那么倒数第k节点也就是顺数第n-k+1个节点。所以我们只需要在拿到长度n后,在进行n-k+1次循环就可以拿到这个倒数第k个节点的值了。

但面试官显然不会太满意这个臃肿的解法,他依然希望我们一次循环就能搞定这个事。

为了实现只遍历一次链表就能找到倒数第k个结点,我们依然可以定义两个遍历slow和fast。我们让fast的变量先往前遍历k-1步,slow保持不动。从第k步开始,slow变量也跟着fast变量从链表的头结点开始遍历。由于两个变量指向的结点距离始终保持在k-1,那么当fast变量达到链表的尾结点时,slow变量指向的结点正好是我们所需要的倒数第k个结点。

我们依然可以在心中默认一遍代码:

  1. 假设输入的链表是1->2->3->4->5;
  2. 现在我们要求倒数第三个结点的值,即顺数第3个结点,它的值为3;
  3. 定义两个变量slow、fast,它们均指向结点1;
  4. 先让fast向前走k-1即2步,这时候fast指向了第3个结点,它的值是3;
  5. 现在fast和slow同步向右移动;
  6. fast在经过2步就达到了链表的尾结点;slow正好指向了第三个结点,这显然是符合我们的猜想的;

在心中默走了一遍代码后,我们显然很容易写出下面的代码。

 private static int getSpecifiedNodeReverse(LinkNode head,int k){
        LinkNode slow=head;
        LinkNode fast=head;
        if (fast==null){
            throw new RuntimeException("your linkNode is null");
        }
        //先让fast先走k-1步
        for (int i=0;i<k-1;i++){
            if (fast.next==null){
                //说明输入的k已经超过了链表的长度,直接报错
                throw new RuntimeException("the value k is too large");
            }
            fast=fast.next;
        }
        while (fast.next!=null){
            slow=slow.next;
            fast=fast.next;
        }
        return slow.data;
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode(1);
        head.next = new LinkNode(2);
        head.next.next = new LinkNode(3);
        head.next.next.next = new LinkNode(4);
        head.next.next.next.next = new LinkNode(5);
//        System.out.println("getTheMid2:"+getTheMid2(head));
//        head.next.next.next.next.next = head;
//        System.out.println("isRingLink:"+isRingLink(head));
        System.out.println("getSpecifiedNodeReverse:"+getSpecifiedNodeReverse(head, 3));
        System.out.println("getSpecifiedNodeReverse:"+getSpecifiedNodeReverse(null, 1));
    }

输出结果为:

getSpecifiedNodeReverse:3
your linkNode is null
Exception in thread “main” java.lang.RuntimeException: your linkNode is null
    at com.lixm.animationdemo.Other.LinkFindDemo.getSpecifiedNodeReverse(LinkFindDemo.java:71)
    at com.lixm.animationdemo.Other.LinkFindDemo.main(LinkFindDemo.java:99)

总结

链表类面试题,真是可以玩出五花八门,当我们用一个变量遍历链表不能解决问题的时候,我们可以尝试用两个变量来遍历链表,可以让其中一个变量遍历的时候速度快一些,比如一次走两步,或者是走若干步。我们在遇到这类面试的时候,千万不要自乱阵脚,学会理性分析问题。

原本是想给我的小伙伴说再见了,但唯恐大家还没学到真本事,所以在这里再留一个扩展题。

面试题:给定一个单链表的头结点,删除倒数第k个结点。

哈哈,和上面的题目仅仅只是把获得它的值变成了删除,不少小伙伴肯定都偷着乐坏了,但在这里还是提醒大家,不要太得意忘形呦。

本文从nanchen的公众号学习而来,感兴趣的朋友可以去关注一下大牛,文章链接

 

    原文作者:L竹轩沐雨
    原文地址: https://blog.csdn.net/lxm20819/article/details/82744786
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞