zoukankan      html  css  js  c++  java
  • 面试 7:快慢指针法玩转链表算法面试(一)

    面试 7:面试常见的链表类算法捷径

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

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

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

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

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

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

    public class Test15 {
        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 就正好在正中间了。

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

    public class Test15 {
        public static class LinkNode {
            int data;
            LinkNode next;
    
            public LinkNode(int data) {
                this.data = data;
            }
        }
    
        private static int getTheMid(LinkNode head) {
            LinkNode slow = head;
            LinkNode fast = head;
            while (fast != null && fast.next != null) {
                fast = fast.next.next;
                slow = slow.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(getTheMid(head));
        }
    }
    

    快慢指针法举一反三

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

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

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

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

    public class Test15 {
        public static class LinkNode {
            int data;
            LinkNode next;
    
            public LinkNode(int data) {
                this.data = data;
            }
        }
    
        private static boolean isRingLink(LinkNode head) {
            LinkNode slow = head;
            LinkNode fast = head;
            while (slow != null && fast != null && fast.next != null) {
                if (slow == fast || fast.next = slow) {
                    return true;
                }
                fast = fast.next.next;
                slow = slow.next;
            }
            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(isRingLink(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 步到达了链表尾结点;fast 正好指向了第 3 个结点,这显然是符合我们的猜想的。

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

    public class Test15 {
        public static class LinkNode {
            int data;
            LinkNode next;
    
            public LinkNode(int data) {
                this.data = data;
            }
        }
    
        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(getSpecifiedNodeReverse(head, 3));
            System.out.println(getSpecifiedNodeReverse(null, 1));
        }
    }
    

    总结

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

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

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

    哈哈,和上面的题目仅仅只是把获得它的值变成了删除,不少小伙伴肯定都偷着乐了,但南尘还是先提醒大家,不要太得意忘形哟~

    好啦,咱们明天再见啦~

  • 相关阅读:
    安装VMtools vim编辑器的使用 压缩包命令 Linux下的用户管理 (第三天)
    VM虚拟机安装 常用Linux命令 网卡配置 (第二天)
    数据库的交互模式 常用的dos命令 (第一天)
    Validate US Telephone Numbers FreeCodeCamp
    Arguments Optional FreeCodeCamp
    Everything Be True FreeCodeCamp
    Binary Agents FreeCodeCamp
    Steamroller FreeCodeCamp
    Drop it FreeCodeCamp
    Smallest Common Multiple FreeCodeCamp
  • 原文地址:https://www.cnblogs.com/liushilin/p/9297686.html
Copyright © 2011-2022 走看看