zoukankan      html  css  js  c++  java
  • 面试 14:合并两个排序链表

    终于又回到了我们的算法习题讲解了。南尘发现最近文章阅读量明显比以前少了不少,就上门请教小伙伴原因。他们都说作为一名 Android 应用开发工程师,实在是在工作中没有接触到算法。做技术这个东西,学习了还是得练,不练过几天一定会忘掉。

    不过想必大家读南尘的文章也是深有所感,基本都是站在一个极其普通的程序员角度思考的,层次感也不会突如其来。所以,大家还望多多思考呀,算法这个东西是练出来的,不是看出来的。

    不过呢,不喜欢算法系列推文的小伙伴也大可不必担心,在算法之后的板块就是 Java 基础啦。

    有句话说的好,面试造航母,入职拧螺丝。实际上也是这样,面试官极难通过简单的面试了解到你这个人的能力,而手写算法却是最适合看出一个人写代码的习惯和程序思维的,这也是大公司以及不少小公司慢慢转向算法面试的一个原因吧~

    好了,话不多说,我们直接来看今天的面试题。

    面试题:输入两个递增排序的单链表,data 域为 int 型值。合并这两个链表,并使新链表中的结点也是按照递增排序的。例如链表 A:1->3->5,链表 B:2->4,那它们合并后就是 1->2->3->4->5

    看到这样的题,我们一定要学会先在心中想好测试用例,再思考程序逻辑。写完程序逻辑后,再把自己事先想好的测试用例测试通过后再交给面试官。事实上,面试官也是事先准备了测试用例的。

    而对于测试用例,就一定要注意好之前南尘给大家讲的边界值。只有能通过边界值、错误值的程序,才拥有足够的健壮性。

    我们回到题干,输入的是两个递增排序的单链表,我们需要合并它,得到的新链表也是递增排序的。在心中不难拥有这样的思路。

    1. 假设单链表 A:1->3,单链表 B:2->4->5;
    2. 先比较 A、B 链表的头结点,这里 1 < 2,所以把 1 作为新链表的头结点;
    3. 1 从 A 链表脱离,3 成为了 A 链表的头结点;
    4. 再进行第二步的比较,3 > 2,把 B 链表的头结点值 2 接到新链表上,得到 1->2;
    5. 一直执行类似 2~4 的步骤,直到 A 链表脱离完,新链表是 1->2->3 ,B 链表是 4->5,A 链表已经为 null;
    6. 此时直接把 B 链表全部接到新链表上,得到最后结构 1->2->3->4->5;
    7. 我们不得不想到边界值和错误值,假设输入的 A 链表为 null,则新链表直接为 B。B 链表为 null,新链表为 A。 A、B 都为 null,则新链表也为 null。

    我们既然是重复的步骤,那我们一定首先能想到递归的思路。我们极容易得到下面的代码。

    public class Test14 {
        public static class Node {
            int data;
            Node next;
    
            Node(int data) {
                this.data = data;
            }
        }
    
        private static Node merge(Node head1, Node head2) {
            // 如果链表 1 为 null ,新链表直接为 2 链表;
            if (head1 == null)
                return head2;
            // 如果链表 2 为 null,则新链表直接为 1 链表
            if (head2 == null)
                return head1;
            Node head = null;
            // 假设链表 1 的头结点值小于等于链表 2;则直接把链表 1 的头结点赋值为新链表,并递归新的 1 链表
            if (head1.data <= head2.data) {
                head = head1;
                head.next = merge(head1.next, head2);
            } else {
                // 否则,对链表 2 执行同样的操作,并把脱离的值赋值上去
                head = head2;
                head.next = merge(head1, head2.next);
            }
            return head;
        }
    
        public static void main(String[] args) {
            Node head1 = new Node(1);
            head1.next = new Node(3);
            head1.next.next = new Node(5);
            head1.next.next.next = new Node((7));
    
            Node head2 = new Node(2);
            head2.next = new Node(4);
            head2.next.next = new Node(6);
            head2.next.next.next = new Node(8);
    
            Node head = merge(head1, head2);
            while (head != null) {
                System.out.print(head.data + "->");
                head = head.next;
            }
            System.out.print("null");
        }
    }
    

    写出了代码后,自然不能忘了用事先想好的测试用例去测试一遍流程,这里测试是没有问题的。

    事实上,这也是《剑指 Offer》的标准解法。递归解法有个好处,就是比较容易想到,但这应该是建立在建模能力比较强的基础之上。但往往不少人会觉得递归特别绕,容易把人绕晕,而且在空间利用率上一直都表现不好,有的面试官就是不能接受递归的代码,所以本题我们更加推荐的是迭代的方式处理。

    迭代处理

    回到我们前面的思路中,上面我们用的方式是不断「脱落」,然后把暴露出来的结点当做一个新的头结点来处理,相信不少小伙伴不怎么能接受这个思路。那我们换个更好理解的方式,我们就直接用我们链表常用的指针移动的方式来处理,我们看行不行。

    所以我们开始直接编写。

    private static Node merge(Node head1, Node head2) {
        if (head1 == null)
            return head2;
        if (head2 == null)
            return head1;
        // 上面的不用说,就是处理传入值为 null 的情况
        Node head = null;
        // 当两个链表都不为空就可以比较大小来确定接哪个
        while (head1 != null && head2 != null) {
            if (head1.data <= head2.data) {
                head = head1;
                head1 = head1.next;
            } else {
                head = head2;
                head2 = head2.next;
            }
        }
        // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后
        if (head1 != null)
            head.next = head1;
        // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点
        if (head2 != null)
            head.next = head2;
        return head;
    }
    

    同样写完后,拿出我们的测试用例开始测试,首先肯定是功能测试。

    1. 假定我们的链表 A 为:1->3,链表 B 为: 2->4->5;
    2. 都不为空进入 while 循环,因为 1 < 2,执行 head = head1,所以新链表为:1->3->null;head1 = head1.next,指针后移;
    3. 依然满足 whild 循环条件,因为 3 > 2,所以新链表为,head = head2 ;等等,这里出了问题,我根本没接到前面放的 head1 的后面,所以这样的赋值明显是不对的。

    功能测试就出了问题,我们当然得思考如何修改,正常来说,我们希望新链表的 1.next = 2,所以我们肯定不能直接用 head = head2 这样的表达式来进行赋值。

    我们一定的有个类似 head.next = head2,然后用类似 head = head.next 这样的方式向后遍历才是正确的。所以我们修改一下代码:

    private static Node merge(Node head1, Node head2) {
        if (head1 == null)
            return head2;
        if (head2 == null)
            return head1;
        // 上面的不用说,就是处理传入值为 null 的情况
        // 为了下面的 head.next,所以我们首先肯定得初始化一个 Node
        Node head = new Node();
        // 当两个链表都不为空就可以比较大小来确定接哪个
        while (head1 != null && head2 != null) {
            if (head1.data <= head2.data) {
                head.next = head1;
                head1 = head1.next;
            } else {
                head.next = head2;
                head2 = head2.next;
            }
            head = head.next;
        }
        // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后
        if (head1 != null)
            head.next = head1;
        // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点
        if (head2 != null)
            head.next = head2;
        return head;
    }
    

    再次进行功能测试:

    1. 假定 A:1->3,B:2->4->5,先初始化 head,最开始肯定两个都不为空的,所以直接比较;
    2. 因为 1 < 2 , 所以新链表为 0->1->3->null,链表 A 指针后移;head = head.next,所以新的 head 值为 1;
    3. 继续循环,因为 3 > 2,所以得到新的 head.next = head2, 即有 1->2;
    4. 以此类推,最终得到 head 一直得到的历史为 1,2,3。此时链表 A 已经遍历完,链表 B 的指针指向 4;
    5. 退出 while 循环,满足 head2 != null,接到 head 后面, head.next = head2,此时的 head2 里面还剩下 4->5->null;
    6. 返回 head。等等,这里还是有问题,我们一直在让新链表的指针后移,这样返回出来的就是尾结点了。但我们想要的答案必须是头结点才可以遍历;

    所以我们必须在 while 循环前面放头结点的值。需要注意的是,由于我们要用到 head.next,所以我们最后真正的头结点实际上是 head.next。

    完整代码如下:

    public class Test14 {
        public static class Node {
            int data;
            Node next;
    
            Node(int data) {
                this.data = data;
            }
    
            Node() {
            }
        }
    
        private static Node merge(Node head1, Node head2) {
            if (head1 == null)
                return head2;
            if (head2 == null)
                return head1;
            // 上面的不用说,就是处理传入值为 null 的情况
            // 为了下面的 head.next,所以我们首先肯定得初始化一个 Node
            Node head = new Node();
            Node temp = head;
            // 当两个链表都不为空就可以比较大小来确定接哪个
            while (head1 != null && head2 != null) {
                if (head1.data <= head2.data) {
                    head.next = head1;
                    head1 = head1.next;
                } else {
                    head.next = head2;
                    head2 = head2.next;
                }
                head = head.next;
            }
            // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后
            if (head1 != null)
                head.next = head1;
            // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点
            if (head2 != null)
                head.next = head2;
            return temp.next;
        }
    
        public static void main(String[] args) {
            Node head1 = new Node(1);
            head1.next = new Node(3);
            head1.next.next = new Node(5);
            head1.next.next.next = new Node((7));
    
            Node head2 = new Node(2);
            head2.next = new Node(4);
            head2.next.next = new Node(6);
            head2.next.next.next = new Node(8);
    
            Node head = merge(head1, head2);
            while (head != null) {
                System.out.print(head.data + "->");
                head = head.next;
            }
            System.out.print("null");
        }
    }
    

    写完后,还是得过一遍测试用例。

    1. 当传入的 head1 或者 head2 为 null 的时候,直接返回另外一条链表,都为 null ,就返回 null;
    2. 当传入 A:1->3,B:2->4->5,上面已经例证,符合条件;
    3. 当传入相等值:A:1->3->5,B:1->2->4,符合条件;
    4. 当传入两个链表均只有一个结点的,A:1,B:2,符合条件。

    基本还是没毛病,这时候我们就可以交上自己的答卷啦~

    基本步骤很清晰:思考测试用例 => 思考程序逻辑 => 拿测试用例进去验证 => 发现问题直接更改 => 测试用例验证,注意边界值 => 测试通过。

    如果保持上面这样的步骤,实在想不明白,在纸上画画思路,相信即使你没有做到 100% 正确,你给面试官的感觉也是足够靠谱的。

    好啦,今天的面试题就先到这里,不知道大家有没有被南尘绕晕呀,绕晕了没事儿,多体验几次就好了。

    还是要提醒大家:千万要自己去练习,看懂不思考,这样的学习效率是极低的!

    好啦,明天的习题我们先放一下,别忘了先思考思考,并动手练练~

    题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入:
    {{1,2,3},
    {4,5,6},
    {7,8,9}}
    则依次打印数字为 1、2、3、6、9、8、7、4、5

  • 相关阅读:
    信息安全学习笔记1
    Linux学习笔记4------磁盘分区(问答形式)
    Linux学习笔记3-------添加磁盘并分区
    用for循环打印三角形
    包机制
    运算符
    变量、常量、作用域
    数据类型转换
    数据的基本类型
    Java运行原理的简单理解
  • 原文地址:https://www.cnblogs.com/liushilin/p/9364096.html
Copyright © 2011-2022 走看看