面试 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

你可能感兴趣的:(面试 14:合并两个排序链表)