算法分析:合并 K 个升序链表

69 阅读7分钟

【题目】 对于一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
示例: 输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ]
将它们合并到一个有序链表中得到
1->1->2->3->4->4->5->6
【方法一】 分治合并
思路是用分治的方法进行合并。
将 k 个链表配对并将同一对中的链表合并;
第一轮合并以后, k 个链表被合并成了k/2个链表,平均长度为2n/k,然后是k/4个链表,k/8个链表等;
重复这一过程,直到我们得到了最终的有序链表。

// 用 Java 实现的合并 K 个有序链表的解决方案,采用分治法策略,将问题逐步分解为合并两个有序链表。
class Solution {
	// 参数:lists 是一个包含 K 个有序链表头节点的数组。
    public ListNode mergeKLists(ListNode[] lists) {
		// 调用递归合并方法 merge,初始范围为整个数组(从索引 0 到 lists.length-1)。
        return merge(lists, 0, lists.length - 1);
    }

	// 功能:递归地将链表数组两两合并。
    public ListNode merge(ListNode[] lists, int l, int r) {
        if (l == r) {
            return lists[l];
        }
        if (l > r) {
            return null;
        }
		// 分治逻辑:计算中间索引 mid(使用右移运算符 >> 1 等同于 (l + r) / 2)。
		// 递归合并左半部分(l 到 mid)和右半部分(mid+1 到 r),最后用 mergeTwoLists 合并结果。
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

	// 功能:合并两个有序链表 a 和 b。
    public ListNode mergeTwoLists(ListNode a, ListNode b) {
		// 终止条件:若任一链表为空,直接返回另一个链表。
        if (a == null || b == null) {
            return a != null ? a : b;
        }
		// 虚拟头节点:创建 head 作为虚拟头节点,便于操作。
        ListNode head = new ListNode(0);
		// 双指针遍历:用 aPtr 和 bPtr 分别遍历两个链表,tail 指向当前合并链表的末尾。
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
			// 比较与连接:每次比较 aPtr 和 bPtr 的值,将较小节点连接到 tail 后,并移动相应指针。
            if (aPtr.val < bPtr.val) {
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
		// 处理剩余节点:遍历结束后,将非空链表的剩余部分直接连接到 tail。
        tail.next = (aPtr != null ? aPtr : bPtr);
		// 返回结果:head.next 即为合并后的真实头节点。
        return head.next;
    }
}

复杂度分析

时间复杂度:考虑递归「向上回升」的过程——第一轮合并k/2组链表,每一组的时间代价是 O(2n);第二轮合并k/4组链表,每一组的时间代价是 O(4n)...... 所以总的时间代价是

O(∑ k/2^i * 2^i * n
i=1 )
= O(kn×logk),故渐进时间复杂度为 O(kn×logk)。
空间复杂度:递归会使用到 O(logk) 空间代价的栈空间。
用C++代码实现如下:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }

    ListNode* merge(vector <ListNode*> &lists, int l, int r) {
        if (l == r) return lists[l];
        if (l > r) return nullptr;
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};

【方法二】 使用优先队列合并

class Solution {
    // 定义内部类Status,封装链表节点的值和指针
    // 实现Comparable接口以便在优先队列中排序
    class Status implements Comparable<Status> {
        int val;        // 链表节点的值
        ListNode ptr;   // 链表节点的指针

        // 构造函数,初始化节点值和指针
        Status(int val, ListNode ptr) {
            this.val = val;
            this.ptr = ptr;
        }

        // 实现比较方法,定义排序规则(按节点值升序)
        public int compareTo(Status status2) {
            return this.val - status2.val;
        }
    }

    // 创建优先队列(最小堆),用于动态维护当前最小值节点
    PriorityQueue<Status> queue = new PriorityQueue<Status>();

    // 合并K个有序链表的主方法
    public ListNode mergeKLists(ListNode[] lists) {
        // 遍历所有链表,将每个链表的头节点(如果不为空)加入优先队列
        for (ListNode node: lists) {
            if (node != null) {
                queue.offer(new Status(node.val, node));
            }
        }
        
        // 创建虚拟头节点,简化链表操作
        ListNode head = new ListNode(0);
        // tail指针指向当前合并链表的末尾节点
        ListNode tail = head;
        
        // 循环处理优先队列,直到队列为空
        while (!queue.isEmpty()) {
            // 取出队列中当前最小的节点
            Status f = queue.poll();
            // 将该节点连接到结果链表的尾部
            tail.next = f.ptr;
            // 更新tail指针到新连接的节点
            tail = tail.next;
            
            // 如果该节点有下一个节点,将下一个节点加入队列
            if (f.ptr.next != null) {
                queue.offer(new Status(f.ptr.next.val, f.ptr.next));
            }
        }
        
        // 返回虚拟头节点的下一个节点,即合并后链表的头节点
        return head.next;
    }
}

对于上面的代码,其核心逻辑详解如下:
1)Status 类:
封装链表节点的值和指针。compareTo方法定义了比较规则,使优先队列能按节点值升序排列元素。
2)优先队列初始化:
PriorityQueue queue = new PriorityQueue();
创建最小堆,默认按自然顺序(即 Status 类定义的比较规则)排序。
3)初始入队操作:
for (ListNode node: lists) { if (node != null) { queue.offer(new Status(node.val, node)); } }
将每个非空链表的头节点加入队列,队列初始状态包含 K 个链表的头节点,按节点值排序。
4)合并主循环:
while (!queue.isEmpty()) {
Status f = queue.poll(); // 取出当前最小节点
tail.next = f.ptr; // 连接到结果链表
tail = tail.next; // 更新尾指针
if (f.ptr.next != null) { // 如果有后续节点
queue.offer(new Status(f.ptr.next.val, f.ptr.next)); // 后续节点入队
}
}
每次循环从队列取出最小值节点,加入结果链表;若该节点有后续节点,将后续节点入队,队列自动调整顺序;循环结束时,所有节点按值升序加入结果链表。 5)虚拟头节点的使用: ListNode head = new ListNode(0); ListNode tail = head; // ... return head.next; 虚拟头节点简化了链表操作,避免处理空链表的边界情况;最终返回head.next即为合并后的真实头节点。
6)复杂度分析
时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O(logk),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O(kn×logk)。
空间复杂度:这里用了优先队列,优先队列中的元素不超过 k 个,故渐进空间复杂度为 O(k)。
另附:上面的代码用到了PriorityQueue。
PriorityQueue 是 Java 集合框架中的一个重要类,它实现了优先队列的功能。 优先队列是一种特殊的队列,元素出队的顺序由元素的优先级决定,而非元素入队的顺序。

PriorityQueue 类概述
类定义:public class PriorityQueue extends AbstractQueue implements Serializable
实现接口:Queue, Collection, Iterable
数据结构:基于最小堆实现(通过数组表示的完全二叉树)
元素顺序:默认按元素的自然顺序排序(元素需实现 Comparable 接口),也可通过构造函数传入 Comparator 自定义排序规则。
容量特性:动态扩容,初始容量为 11,当元素数量超过容量时自动增长。

PriorityQueue 关键特性
1)排序规则
自然顺序:元素必须实现 Comparable 接口,否则抛出 ClassCastException
自定义顺序:通过构造函数传入 Comparator
// 示例:创建降序排列的优先队列
PriorityQueue maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
2)堆操作原理
插入操作(offer/add):将元素添加到数组末尾;向上调整堆(siftUp),确保堆性质。
删除操作(poll/remove):移除数组首元素;将数组末尾元素移至首位;向下调整堆(siftDown),确保堆性质。
3)线程安全性
PriorityQueue 是非线程安全的,线程安全场景需使用 PriorityBlockingQueue
// 线程安全的优先队列
PriorityBlockingQueue blockingQueue = new PriorityBlockingQueue<>();
4)迭代器行为
iterator() 返回的迭代器不保证元素有序 若需有序遍历,可将元素倒入数组并排序: Object[] sortedElements = queue.toArray(); Arrays.sort(sortedElements)

用C++代码实现的方案:

class Solution {
public:
    struct Status {
        int val;
        ListNode *ptr;
        bool operator < (const Status &rhs) const {
            return val > rhs.val;
        }
    };

    priority_queue <Status> q;

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        for (auto node: lists) {
            if (node) q.push({node->val, node});
        }
        ListNode head, *tail = &head;
        while (!q.empty()) {
            auto f = q.top(); q.pop();
            tail->next = f.ptr; 
            tail = tail->next;
            if (f.ptr->next) q.push({f.ptr->next->val, f.ptr->next});
        }
        return head.next;
    }
};