[前端]_一起刷leetcode 23. 合并K个升序链表

140 阅读5分钟

大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。

题目

23. 合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

 

示例 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

暴力解法

思路

  1. lists中的数据进行过滤,过滤掉空节点;
  2. 找出循环结束条件 -- lists.length <= 1, 当只剩下一个链表直接插入末尾即可;
  3. 创建一个新链表result用来存储排序好的结果, 同时添加变量prev指向排序好的末尾节点;
  4. 每一轮相当于对数组做一个取最小值的操作,然后把最小值插到prev后面;
  5. 找出最小值后,还需要判断最小值所在的链表还有没有剩余元素,有的话就执行抬走,下一位。没有的话就把空链表踢出数组。

实现

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    let result = { val: 0, next: null };
    let prev = result;

    lists = lists.filter(item => item);

    while (lists.length > 1) {
        const n = lists.length;
        // 找最小值所在的链表
        let minIndex = 0, minValue = lists[0].val;
        for (let i = 1; i < n; i++) {
            if (lists[i].val < minValue) {
                minValue = lists[i].val;
                minIndex = i;
            }
        }

        const cur = lists[minIndex];
        const next = cur.next;
        
        // 抬走,下一位。没有下一位就整个链表抬走
        if (next) {
            lists[minIndex] = next;
        } else {
            lists.splice(minIndex, 1);
        }

        // 每一轮把最小值插入最后面,就实现了排序
        prev.next = cur;
        prev = prev.next;
    }

    // 有可能一开始就是空数组,lists[0]为undefined节点执行Null
    prev.next = lists[0] || null;

    return result.next;
};

耗时

image.png

优化

在上述解法过程中, 我们发现我们每次都要拿列表中的所有元素出来比较,取最大值。这样子做了很多重复的比较,所以我做了如下优化:

  1. 把所有链表根据头节点的值大小做个排序,放到数组arr中;
  2. 每次取出arr中的第一个元素就是最小值,同时把第一个元素的链表指针向后传递;
  3. 如果链表为空就踢出数组中,否则的话就做一轮插入排序,把当前值放到合适的位置;
  4. 这样子一来没轮我们只需要进行O(logn)次遍历即可,而数组的第一位始终是最小值。

优化代码

/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    let result = { val: 0, next: null };
    let prev = result;

    lists = lists.filter(item => item);
    // 这里先做好排序
    lists.sort((a, b) => a.val - b.val);

    while (lists.length > 1) {
        const n = lists.length;
        // 数组已经排序好了, 第一个就是最小值

        let cur = lists[0];
        const next = cur.next;

        // 抬走,下一位。没有下一位就整个链表抬走
        if (next) {
            lists[0] = next;
            // 插入排序
            for (let i = 1; i < n; i++) {
                if (lists[i].val < lists[i - 1].val) {
                    // 交换位置
                    [lists[i], lists[i - 1]] = [lists[i - 1], lists[i]]
                } else {
                    break;
                }
            }
            
        } else {
            lists.splice(0, 1);
        }

        // 每一轮把最小值插入最后面,就实现了排序
        prev.next = cur;
        prev = prev.next;
    }

    // 剩余的接在尾部,有可能一开始就是空数组,lists[0]为undefined节点赋值Null
    prev.next = lists[0] || null;

    return result.next;
};

耗时

image.png

进阶

堆排序

我们可以构建一个最小堆,每次取出堆顶的一个元素来构建链表。然后用完了如果链表还有后继节点再塞回去堆里面。一直循环到拿完链表的所有元素为止。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    let result = new ListNode(0);
    let prev = result;
    let minHeap = new MinPriorityQueue({ priority: (bid) => bid.val });

    // 构建最小堆
    for (let i = 0; i < lists.length; i++) {
        if (lists[i]) {
            minHeap.enqueue(lists[i]);
        }
    }

    // 如果长度不为0的时候,每次拿最小值出来构建新链表
    while (minHeap.size()) {
        let cur = minHeap.dequeue().element;
        const next = cur.next;
        cur.next = null;
        prev.next = cur;
        prev = prev.next;
        // 如果还有值,塞回去继续排
        if (next) {
           minHeap.enqueue(next); 
        }
    }

    return result.next;
};

堆排序结果

image.png

归并排序

对链表两两进行排序,直到最后剩下一个链表直接返回即可。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    // 归并排序两两合并
    while (lists.length > 1) {
        const len = Math.ceil(lists.length / 2);
        for (let i = 0; i < len; i++) {
            lists[i] = mergeTwoList(lists[2 * i], lists[2 * i + 1]);
        }
        lists.length = len;
    }

    return lists[0] || null;
};

// 合并两个有序链表
function mergeTwoList(list1, list2) {
    if (!list2) return list1;

    let result = new ListNode(0);
    let prev = result;
    
    while (list1 && list2) {
        if (list2.val < list1.val) {
            const next = list2.next;
            list2.next = null;
            prev.next = list2;
            list2 = next;
        } else {
            const next = list1.next;
            list1.next = null;
            prev.next = list1;
            list1 = next;
        }
        prev = prev.next;
    }

    prev.next = list1 || list2;
    return result.next;
}

归并排序结果

image.png

总结

合并有序链表题目用归并排序性能最高,以后遇到这种题目我就直接归并排序梭哈了。

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。