【算法】合并k个有序的链表-基于最小堆的思想

题目大意

合并k个已经排序好了的链表。

大致思路

可以借助堆排序的思想,使用小顶堆来实现。
先将K个链表的头元素构建成一个最小堆,之后,取堆顶元素,这个结点就是最小的,放入保存结果的集合,接着将原堆顶元素所在链表的下一元素入堆并重新构建最小堆,之后,再取堆顶元素,这个结点就是第二小的……如此往复操作,知道将所有链表的所有元素都取完。
但是,在将原堆顶元素对应的链表中的下一元素入堆时,会遇到一种情况——不存在下一个元素,即原堆顶元素所在链表都取完值了,此时则将最小堆的末尾元素换置换置堆顶,此时真是的最小堆大小将减一。

具体实现如下:

public class MergeKList {
    static class Node implements Comparable<Node> {
        int val;//元素的值
        int indexForLists;//该元素所在的链表处于K个链表的位置
        int indexInList;//该元素处在对应链表的位置

        public Node(int val, int indexForLists, int indexInList) {
            this.val = val;
            this.indexForLists = indexForLists;
            this.indexInList = indexInList;
        }

        @Override
        public int compareTo(Node o) {
            if (val == o.val) return 0;
            else if (val > o.val) return 1;
            else return -1;
        }
    }

    public static List<Integer> mergeKList(ArrayList<ArrayList<Integer>> lists) {
        if (lists == null || lists.size() == 0) return null;
        List<Integer> list = new LinkedList<>();

        //首先将k个链表的首个元素取出构成最小堆
        Node[] minHeap = new Node[lists.size()];
        int trueLen = 0;
        Iterator<ArrayList<Integer>> iterator = lists.iterator();
        while (iterator.hasNext()) {
            ArrayList<Integer> arrayList = iterator.next();
            if (arrayList != null && arrayList.size() > 0) {
                minHeap[trueLen] = new Node(arrayList.get(0), trueLen, 0);
                trueLen++;
            } else {
                //去除无效的链表
                iterator.remove();
            }
        }
        for (int i = trueLen / 2 - 1; i >= 0; i--) {
            buildMinHeap(minHeap, i, trueLen);
        }

        while (true) {
            //取出最小堆堆堆顶元素
            Node heapTop = minHeap[0];
            list.add(heapTop.val);

            //取原堆顶元素所在链表的下一元素
            ArrayList<Integer> arrayList = lists.get(heapTop.indexForLists);
            if (heapTop.indexInList < arrayList.size() - 1) {//如果还有下一个元素,则把它对应的Node节点放到堆定
                int indexInList = heapTop.indexInList + 1;
                minHeap[0] = new Node(arrayList.get(indexInList), heapTop.indexForLists, indexInList);
                //重新构建最小堆
                buildMinHeap(minHeap, 0, trueLen);
            } else {//如果不存在下一个元素,即原堆顶元素所在链表都取完值了,则将最小堆的末尾元素换置换置堆顶,trueLen--
                minHeap[0] = minHeap[trueLen - 1];
                minHeap[trueLen - 1] = null;
                trueLen--;

                //如果最小堆真实的大小只剩为1,则表示之剩下一个链表的剩下元素没有合并了,且直接储存结果的集合末尾添加即可
                if (trueLen==1) {
                    heapTop = minHeap[0];
                    list.add(heapTop.val);
                    ArrayList<Integer> lastArrayList = lists.get(heapTop.indexForLists);
                    for (int i = heapTop.indexInList+1; i < lastArrayList.size(); i++) {
                        list.add(lastArrayList.get(i));
                    }

                    break;
                } else {
                    //重新构建最小堆
                    buildMinHeap(minHeap, 0, trueLen);
                }
            }
        }

        return list;
    }

    public static void buildMinHeap(Node[] nodes, int i, int heapSize) {
        int min, left, right;
        while (true) {
            min = i;
            left = 2 * i + 1;
            right = left + 1;

            if (left < heapSize && nodes[left].compareTo(nodes[min]) < 0) {
                min = left;
            }

            if (right < heapSize && nodes[right].compareTo(nodes[min]) < 0) {
                min = right;
            }

            if (min != i) {
                Node temp = nodes[min];
                nodes[min] = nodes[i];
                nodes[i] = temp;

                i = min;
            } else {
                break;
            }
        }
    }
}

假设总共有k个list,每个list的最大长度是n
维护一个大小为k的堆,每次取堆顶的最小元素放到结果中,然后读取该元素的下一个元素放入堆中,重新维护好。因为每个链表是有序的,每次又是去当前k个元素中最小的,所以当所有链表都读完时结束,这个时候所有元素按从小到大放在结果链表中。这个算法每个元素要读取一次,即是k*n次,然后每次读取元素要把新元素插入堆中要log k的复杂度,所以总时间复杂度是O(nk logk)。空间复杂度是堆的大小,即为O(k)

点赞