23. Merge k Sorted Lists

题目描述和难度

  • 题目描述:

合并 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
  1->4->5,
  1->3->4,
  2->6
]
输出: 1->1->2->3->4->4->5->6

思路分析

求解关键:这是一道类似于教科书上例题的问题。想出解决办法一点都不难,由于 k 个链表已经是排序好的链表,那么 k 个排序的链表头结点中 val 最小的结点就是合并以后的链表中最小的结点,即应该排在第 1 个位置的结点。我们拿出这个结点以后,此时 k 个排序的链表头结点中 val 最小的结点就是合并以后的链表中第 2 小的结点,应该放在第 2 个位置,我们按照这个思路,依次拿出结点,完成合并的工作。

这里我们举生活中的例子来理解求解思路。假设有如下生活情境:假设你是一名体育老师,有 3 个班的学生,他们已经按照身高从矮到高排好成了 3 列纵队,现在要把这 3 个班的学生也按照身高从矮到高排列一列纵队。我们可以这么做:

(1)让三个班的学生按列站在你的面前,这时你能看到站在队首的学生的全身,其余同学只能看到比前面同学脑袋高出的那部分;

(2)每一次队首的 3 名同学,请出最矮的同学出列到“队伍4”(即我们最终认为排好序的队列),出列的这一列的后面的所有同学都向前走一步(其实走不走都行,只要你能比较出站在你面前的 3 位在队首的同学同学的高矮即可);

(3)重复第(2)步,直到 3 个班的同学全部出列完毕。

具体实现的时候,“每一次队首的 3 名同学,请出最矮的同学”这件事情可以交给优先队列去完成。在连续的两次出队之间完成“穿针引线”的工作,是不是很酷!下面的图说明了这样的过程。

以上是思路1,对应参考解答1。下面介绍思路2,对应参考解答2: 根据之前处理链表的经验,如果我们不想“穿针引线”,那么递归方法是一个不错的选择,既然是数组的“排序”问题,我们不妨借助归并排序的分治思想来解决,代码结构和归并排序可以说是同出一辙。 1、先一分为二地解决了这个问题; 2、再考虑如何合并,这个合并的过程也是一个递归方法。

两种方法都利用到了常见的算法和基础的数据结构,值得学习和思考。

虽然我只给使用优先队列的解法做了图,但是使用分治思想递归完成的方法也同样很酷!

参考解答

参考解答1

import java.util.Comparator;
import java.util.PriorityQueue;

class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }

    ListNode(Integer[] nums) {
        ListNode currNode = this;
        currNode.val = nums[0];
        for (int i = 1; i < nums.length; i++) {
            currNode.next = new ListNode(nums[i]);
            currNode = currNode.next;
        }
    }

    @Override
    public String toString() {
        ListNode currNode = this;
        StringBuilder s = new StringBuilder();
        while (currNode != null) {
            s.append(currNode.val);
            s.append(" -> ");
            currNode = currNode.next;
        }
        // 最后添加一个 NULL 标志表示添加到末尾了
        s.append("NULL");
        return s.toString();
    }
}

public class Solution {

    public ListNode mergeKLists(ListNode[] lists) {
        int len = lists.length;
        if (len == 0) {
            return null;
        }
        PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>(len, Comparator.comparingInt(a -> a.val));
        ListNode dummyNode = new ListNode(-1);
        ListNode curNode = dummyNode;
        for (ListNode list : lists) {
            if (list != null) {
                // 这一步很关键,不能也没有必要将空对象添加到优先队列中
                priorityQueue.add(list);
            }
        }
        while (!priorityQueue.isEmpty()) {
            // 优先队列非空才能出队
            ListNode node = priorityQueue.poll();
            // 当前节点的 next 指针指向出队元素
            curNode.next = node;
            // 当前指针向前移动一个元素,指向了刚刚出队的那个元素
            curNode = curNode.next;
            if (curNode.next != null) {
                // 只有非空节点才能加入到优先队列中
                priorityQueue.add(curNode.next);
            }
        }
        return dummyNode.next;
    }

    public static void main(String[] args) {
        Integer[] nums1 = {1, 4, 5};
        Integer[] nums2 = {1, 3, 4};
        Integer[] nums3 = {2, 6};
        ListNode head1 = new ListNode(nums1);
        ListNode head2 = new ListNode(nums2);
        ListNode head3 = new ListNode(nums3);
        ListNode[] lists = new ListNode[3];
        lists[0] = head1;
        lists[1] = head2;
        lists[2] = head3;
        Solution solution = new Solution();
        ListNode mergeKLists = solution.mergeKLists(lists);
        System.out.println(mergeKLists);
    }
}

说明:这里创建比较器对象还可以使用下面两种在 Java8 语言中使用的语法:

Comparator<ListNode> comparator = (a, b) -> a.val - b.val;

Comparator<ListNode> comparator = Comparator.comparingInt(a -> a.val);

参考解答2

class Solution2 {
    public ListNode mergeKLists(ListNode[] lists) {
        int len = lists.length;
        if (len == 0) {
            return null;
        }
        return mergeKLists(lists, 0, len - 1);
    }

    private ListNode mergeKLists(ListNode[] lists, int l, int r) {
        // 思考这里为什么取等于?这是因为根据下文对 sort 的递归调用情况,区间最窄的时候,只可能是左右端点重合
        if (l == r) {
            return lists[l];
        }
        int mid = l + (r - l) / 2;
        ListNode listNode1 = mergeKLists(lists, l, mid);
        ListNode listNode2 = mergeKLists(lists, mid + 1, r);
        // 于是问题转化成合并两个有序链表的问题了,我们可以穿针引线,也可以继续递归解决这个子问题,请见 LeetCode 第 21 题,
        // 这里我们使用继续递归解决,
        // 因为使用穿针引线,每一次调用这个方法的时候,都需要创建一个虚拟的头结点,归并次数有些多的时候,是不划算的
        return mergeOfTwoListNode(listNode1, listNode2);
    }

    private ListNode mergeOfTwoListNode(ListNode listNode1, ListNode listNode2) {
        // 先处理递归到底的情况
        if (listNode1 == null) {
            return listNode2;
        }
        if (listNode2 == null) {
            return listNode1;
        }
        if (listNode1.val < listNode2.val) {
            // 把问题转化为一个更小的问题
            listNode1.next = mergeOfTwoListNode(listNode1.next, listNode2);
            return listNode1;
        } else {
            // 把问题转化为一个更小的问题
            listNode2.next = mergeOfTwoListNode(listNode1, listNode2.next);
            return listNode2;
        }
    }
}