【每日算法】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. 系统设计

    • 多个有序数据源的流式处理
    • 分布式系统中的有序日志合并
    • 时间序列数据库的多查询结果合并
相关推荐
白兰地空瓶3 小时前
一行 npm init vite,前端工程化的世界就此展开
前端·vue.js·vite
xiaoxue..3 小时前
LeetCode 第 15 题:三数之和
前端·javascript·算法·leetcode·面试
yaoh.wang3 小时前
力扣(LeetCode) 28: 找出字符串中第一个匹配项的下标 - 解法思
python·程序人生·算法·leetcode·面试·职场和发展·跳槽
flashlight_hi3 小时前
LeetCode 分类刷题:101. 对称二叉树
javascript·算法·leetcode
yaoh.wang3 小时前
力扣(LeetCode) 35: 搜索插入位置 - 解法思路
程序人生·算法·leetcode·面试·职场和发展·跳槽·二分搜索
狂炫冰美式3 小时前
《预言市场进化论:从罗马斗兽场,到 Polymarket 的 K 线图》
前端·后端
码力巨能编3 小时前
Markdown 作为 Vue 组件导入
前端·javascript·vue.js
私人珍藏库3 小时前
[吾爱大神原创工具] FlowMouse - 心流鼠标手势 v1.0【Chrome浏览器插件】
前端·chrome·计算机外设
唯唯qwe-3 小时前
Day20:贪心算法,跳跃游戏
python·算法·贪心算法