【每日算法】LeetCode 23. 合并 K 个升序链表

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 23. 合并 K 个升序链表

1. 题目描述

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,并返回合并后的链表。

示例 1:

复制代码
输入: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

示例 2:

复制代码
输入:lists = []
输出:[]

示例 3:

复制代码
输入:lists = [[]]
输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i]升序 排列
  • lists[i].length 的总和不超过 10^4

2. 问题分析

在前端开发中,我们经常需要处理多个有序数据流的合并,例如:

  1. 多来源数据聚合:从多个API接口获取已排序的数据,需要合并展示
  2. 日志合并:多个服务的按时间排序的日志需要合并分析
  3. 虚拟列表优化:多个有序数据源合并后渲染长列表

这个问题本质上是多路归并问题,是归并排序的扩展。对于前端开发者来说,理解此问题有助于掌握分治、优先队列等思想,在处理大数据流、实现高效渲染时非常有用。

3. 解题思路

3.1 思路概览

我们有几种主要解决方案:

  1. 顺序合并法:依次合并每个链表
  2. 分治法:借鉴归并排序思想,两两合并
  3. 优先队列(最小堆)法:维护当前所有链表头节点的最小值
  4. 暴力解法:将所有节点值收集后排序,再构建链表

从复杂度分析看,优先队列法分治法是最优解,时间复杂度都是O(Nlogk),其中N是总节点数,k是链表数量。

3.2 各思路详解

3.2.1 顺序合并法

逐个链表合并,每次合并两个有序链表。简单直观,但效率较低。

3.2.2 分治法

采用归并排序的思想,将k个链表配对并两两合并,重复这一过程直到合并成一个链表。

3.2.3 优先队列法(最优解之一)

维护一个大小为k的最小堆,每次从堆中取出最小节点,将该节点的下一个节点加入堆中,直到堆为空。

3.2.4 暴力解法

收集所有节点值到数组,排序后构建新链表。简单但失去了链表的特性优势。

4. 各思路代码实现

4.1 顺序合并法

javascript 复制代码
/**
 * 合并两个有序链表
 */
const mergeTwoLists = (l1, l2) => {
    const dummy = new ListNode(0);
    let cur = dummy;
    
    while (l1 && l2) {
        if (l1.val < l2.val) {
            cur.next = l1;
            l1 = l1.next;
        } else {
            cur.next = l2;
            l2 = l2.next;
        }
        cur = cur.next;
    }
    
    cur.next = l1 || l2;
    return dummy.next;
};

/**
 * 顺序合并K个链表
 */
const mergeKLists = function(lists) {
    if (lists.length === 0) return null;
    
    let result = lists[0];
    for (let i = 1; i < lists.length; i++) {
        result = mergeTwoLists(result, lists[i]);
    }
    
    return result;
};

4.2 分治法

javascript 复制代码
/**
 * 分治法合并K个链表
 */
const mergeKLists = function(lists) {
    if (lists.length === 0) return null;
    
    const merge = (start, end) => {
        if (start === end) return lists[start];
        if (start > end) return null;
        
        const mid = Math.floor((start + end) / 2);
        const left = merge(start, mid);
        const right = merge(mid + 1, end);
        
        return mergeTwoLists(left, right);
    };
    
    return merge(0, lists.length - 1);
};

/**
 * 迭代版本的分治法
 */
const mergeKListsIterative = function(lists) {
    if (lists.length === 0) return null;
    
    let interval = 1;
    const n = lists.length;
    
    while (interval < n) {
        for (let i = 0; i < n - interval; i += interval * 2) {
            lists[i] = mergeTwoLists(lists[i], lists[i + interval]);
        }
        interval *= 2;
    }
    
    return lists[0];
};

4.3 优先队列法(最小堆)

javascript 复制代码
/**
 * 优先队列法(最小堆实现)
 */
class MinHeap {
    constructor() {
        this.heap = [];
    }
    
    size() {
        return this.heap.length;
    }
    
    push(node) {
        this.heap.push(node);
        this.bubbleUp(this.heap.length - 1);
    }
    
    pop() {
        if (this.size() === 0) return null;
        const min = this.heap[0];
        const last = this.heap.pop();
        
        if (this.size() > 0) {
            this.heap[0] = last;
            this.sinkDown(0);
        }
        
        return min;
    }
    
    bubbleUp(index) {
        const node = this.heap[index];
        while (index > 0) {
            const parentIndex = Math.floor((index - 1) / 2);
            const parent = this.heap[parentIndex];
            
            if (node.val >= parent.val) break;
            
            this.heap[parentIndex] = node;
            this.heap[index] = parent;
            index = parentIndex;
        }
    }
    
    sinkDown(index) {
        const length = this.size();
        const node = this.heap[index];
        
        while (true) {
            let leftChildIndex = 2 * index + 1;
            let rightChildIndex = 2 * index + 2;
            let swap = null;
            let leftChild, rightChild;
            
            if (leftChildIndex < length) {
                leftChild = this.heap[leftChildIndex];
                if (leftChild.val < node.val) {
                    swap = leftChildIndex;
                }
            }
            
            if (rightChildIndex < length) {
                rightChild = this.heap[rightChildIndex];
                if (
                    (swap === null && rightChild.val < node.val) ||
                    (swap !== null && rightChild.val < leftChild.val)
                ) {
                    swap = rightChildIndex;
                }
            }
            
            if (swap === null) break;
            
            this.heap[index] = this.heap[swap];
            this.heap[swap] = node;
            index = swap;
        }
    }
}

const mergeKLists = function(lists) {
    if (lists.length === 0) return null;
    
    const minHeap = new MinHeap();
    const dummy = new ListNode(0);
    let cur = dummy;
    
    // 将所有链表的头节点加入最小堆
    for (let list of lists) {
        if (list) {
            minHeap.push(list);
        }
    }
    
    // 不断从堆中取出最小节点
    while (minHeap.size() > 0) {
        const node = minHeap.pop();
        cur.next = node;
        cur = cur.next;
        
        // 如果该节点还有下一个节点,加入堆中
        if (node.next) {
            minHeap.push(node.next);
        }
    }
    
    return dummy.next;
};

/**
 * 使用JavaScript内置的优先队列(如果环境支持)
 */
const mergeKListsWithPriorityQueue = function(lists) {
    if (lists.length === 0) return null;
    
    const dummy = new ListNode(0);
    let cur = dummy;
    
    // 使用优先队列,按节点值排序
    const pq = new PriorityQueue({
        compare: (a, b) => a.val - b.val
    });
    
    // 初始化优先队列
    for (let list of lists) {
        if (list) {
            pq.enqueue(list);
        }
    }
    
    // 处理队列
    while (!pq.isEmpty()) {
        const node = pq.dequeue();
        cur.next = node;
        cur = cur.next;
        
        if (node.next) {
            pq.enqueue(node.next);
        }
    }
    
    return dummy.next;
};

4.4 暴力解法

javascript 复制代码
/**
 * 暴力解法
 */
const mergeKLists = function(lists) {
    const nodes = [];
    
    // 收集所有节点值
    for (let list of lists) {
        while (list) {
            nodes.push(list.val);
            list = list.next;
        }
    }
    
    // 排序
    nodes.sort((a, b) => a - b);
    
    // 构建新链表
    const dummy = new ListNode(0);
    let cur = dummy;
    
    for (let val of nodes) {
        cur.next = new ListNode(val);
        cur = cur.next;
    }
    
    return dummy.next;
};

5. 各实现思路的复杂度、优缺点对比

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
顺序合并 O(kN) O(1) 实现简单,无需额外空间 效率低,重复遍历节点多 k较小或链表长度较短
分治法 O(Nlogk) O(logk) 效率高,递归思路清晰 递归栈空间开销 通用场景,尤其适合链表数量多
优先队列 O(Nlogk) O(k) 效率高,逻辑清晰 需要维护堆结构 实时数据流处理,k较大
暴力解法 O(NlogN) O(N) 实现极其简单 破坏链表结构,额外空间大 快速实现,不关心性能

说明:

  • N:所有链表的总节点数
  • k:链表数量
  • 最优解:分治法和优先队列法都是最优时间复杂度,优先选择
  • 前端场景推荐:优先队列法,逻辑清晰且易于理解和维护

6. 总结

6.1 核心要点

  1. 问题本质:多路归并问题,是归并排序从两路到多路的扩展
  2. 关键思想:分治与优先队列(堆)是解决此类问题的核心思想
  3. 最优复杂度:O(Nlogk),无法再优化,因为每个节点都需要处理,且每次选择最小值需要logk时间

6.2 实际应用场景

  1. 前端数据处理

    • 多个API返回的有序数据合并展示
    • 多来源日志的时间线合并
    • 搜索引擎的多索引结果合并
  2. 性能优化

    • 虚拟滚动列表的数据流合并
    • 大文件分片下载后的有序合并
    • 实时聊天消息的多会话合并
  3. 系统设计

    • 多个有序数据源的流式处理
    • 分布式系统中的有序日志合并
    • 时间序列数据库的多查询结果合并
相关推荐
renhongxia17 分钟前
如何基于知识图谱进行故障原因、事故原因推理,需要用到哪些算法
人工智能·深度学习·算法·机器学习·自然语言处理·transformer·知识图谱
坚持就完事了7 分钟前
数据结构之树(Java实现)
java·算法
算法备案代理10 分钟前
大模型备案与算法备案,企业该如何选择?
人工智能·算法·大模型·算法备案
赛姐在努力.34 分钟前
【拓扑排序】-- 算法原理讲解,及实现拓扑排序,附赠热门例题
java·算法·图论
木斯佳42 分钟前
前端八股文面经大全:26届秋招滴滴校招前端一面面经-事件循环题解析
前端·状态模式
我能坚持多久1 小时前
【初阶数据结构01】——顺序表专题
数据结构
光影少年1 小时前
react状态管理都有哪些及优缺点和应用场景
前端·react.js·前端框架
野犬寒鸦2 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总2 小时前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
rainbow68892 小时前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法