前端面试常见算法题系列:第23题,巧用归并法,轻松合并多个有序链表!
2023-12-30 12:02:10
1. 引言
在前端开发中,我们经常需要处理各种数据结构,其中链表是一种非常常见的线性数据结构。链表由一系列节点组成,每个节点包含一个数据元素和指向下一个节点的指针。链表具有插入、删除和查找操作的优势,但由于其结构特点,在某些场景下可能不如数组高效。
在前端面试中,算法题是经常被问到的考点之一。其中,链表相关算法题是比较常见的,比如合并链表、反转链表、删除链表节点等。这些题目旨在考察应聘者对链表数据结构的理解以及对算法的掌握程度。
2. 题目
给你一个链表数组 lists
,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
例如,以下是一个链表数组 lists
:
lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
合并后的链表应该是:
[1, 1, 2, 3, 4, 4, 5, 6]
3. 算法分析
为了解决这个问题,我们可以采用归并排序的思想。归并排序是一种经典的排序算法,它将一个待排序的数组分成两半,然后递归地对每一半进行排序,最后再将排好序的子数组合并成一个有序的数组。
对于链表的合并问题,我们可以采用类似的思路。首先,我们将链表数组 lists
中的所有链表分成两半,然后递归地对每一半中的链表进行合并。最后,我们将排好序的子链表合并成一个有序的链表。
具体来说,我们可以按照以下步骤来实现链表的合并:
- 如果链表数组
lists
中只有一个链表,则直接返回该链表。 - 否则,将链表数组
lists
分成两半,分别记为left
和right
。 - 递归地对
left
和right
中的链表进行合并。 - 将排好序的子链表合并成一个有序的链表。
在合并两个有序链表时,我们可以使用以下步骤:
- 初始化两个指针
p1
和p2
,分别指向两个有序链表的头部。 - 循环比较
p1
和p2
指向的节点的值,将较小的节点添加到合并后的链表中。 - 将
p1
或p2
指针移动到下一个节点。 - 重复步骤 2 和步骤 3,直到两个有序链表都遍历完毕。
- 将剩余的节点添加到合并后的链表中。
4. 代码实现
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* 合并多个已经按升序排列的链表
*
* @param {ListNode[]} lists
* @return {ListNode}
*/
const mergeKLists = (lists) => {
if (lists.length === 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
};
const merge = (lists, left, right) => {
if (left === right) {
return lists[left];
}
const mid = Math.floor((left + right) / 2);
const leftList = merge(lists, left, mid);
const rightList = merge(lists, mid + 1, right);
return mergeTwoLists(leftList, rightList);
};
const mergeTwoLists = (l1, l2) => {
let dummy = new ListNode(0);
let curr = dummy;
while (l1 && l2) {
if (l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = l1 || l2;
return dummy.next;
};
5. 时间复杂度分析
合并 k
个链表的时间复杂度主要取决于合并两个链表的操作。在最坏的情况下,每次合并两个链表都需要遍历其中一个链表的所有节点。因此,合并 k
个链表的时间复杂度为 O(n * k)
,其中 n
是所有链表中节点总数。
6. 结语
在本篇文章中,我们一起学习了如何使用归并排序的思想来合并多个已经按升序排列的链表。这种方法可以高效地将链表合并成一个有序的链表。我们还分析了算法的时间复杂度,并提供了代码实现。希望这篇教程对您有所帮助,也祝您在前端面试中取得佳绩!