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 个结点。

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

    好啦,咱们明天再见啦~

  • 相关阅读:
    Pytest权威教程21-API参考-02-标记(Marks)
    GitLab获取人员参与项目-贡献项目列表
    通过Confulence API统计用户文档贡献量
    Git项目代码统计-Python3版gitstats
    Pytest从测试类外为测试用例动态注入数据
    Python操作Jira
    Selenium操作Chrome模拟手机浏览器
    剑指offer 构建乘积数组
    栈与堆 && char*, char[], char**, char*[], char[][]详解
    vector 容器知识点汇总
  • 原文地址:https://www.cnblogs.com/liushilin/p/9297686.html
Copyright © 2011-2022 走看看