一法解多题是为了看透本质,一题多解是为了融会贯通。
前一篇,我们从一法解多题的思路搞定了leetcode 的21、23和1669题。今天我们来看看一题多解的情况。
LeetCode23:合并K个升序链表:给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
这道题有多少种解法呢?
策略一:K 个指针分别指向 K 条链表
方法1:直接合并
1.每次O(N)比较K个指针求最小的那个,时间复杂度为O(NK)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
ListNode dummyHead = new ListNode(0);
ListNode newList = dummyHead;
while (true) {
ListNode minNode = null;
int minPointer = -1;
for (int i = 0; i < k; i++) {
if (lists[i] == null) {
continue;
}
if (minNode == null || lists[i].val < minNode.val) {
minNode = lists[i];
minPointer = i;
}
}
if (minPointer == -1) {
break;
}
newList.next = minNode;
newList = newList.next;
lists[minPointer] = lists[minPointer].next;
}
return dummyHead.next;
}
}
这种方式容易超时,不推荐。不过呢,这种思想还是值得梳理一下的,如果面试时实在想不出更好的方式,就只能通过这种方式硬写了。
K个数组合并和K个链表合并有相同也有不同。K个链表更容易一些,因为不需要为每个链表都定义一个当前指针的标记,为啥呢?因为链表只要知道头结点就行了。所以我们将某个链表的结点合并到新链表的时候,两个指针都向前移动一下就行了,也就是这两行的意思:
newList = newList.next;
lists[minPointer] = lists[minPointer].next;
另外一点就是newList我们必须保留其头结点,这里通过定义虚拟结点dummyHead的方式来进行的。
其他部分按照常规设计做即可。
方法2. 使用小根堆对 1 进行优化
堆的问题我们后面也会详细讨论,这里我们只说一下含义。
堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值的结构就叫小根堆。也就说小根堆每个结点的值总是比其左右子树的值小或者相等。
在应用中,我们往往会限制整个堆的大小,比如元素最多为K,那么每次我们都取走最小值,然后再放一个元素进来重新构造小根堆,这样是不是就可以不断得到最小,第二小,第三小的值了呢?拿到一个就添加一个到新的链表里就行了。
所以用小根堆解决这个问题的思路就是:
1.我们就定义小根堆的大小为K,然后从每个链表拿一个元素进来构造最小堆。
2.取走根元素(一定是最小值)插入到新链表的表尾,然后将该元素所在的链表再拿一个元素进来,重新构造小根堆。
重复执行上面的步骤,直到所有链表都为空。
代码如下:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
for (ListNode node: lists) {
if (node != null) {
pq.offer(node);
}
}
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (!pq.isEmpty()) {
ListNode minNode = pq.poll();
tail.next = minNode;
tail = minNode;
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummyHead.next;
}
}
时间复杂度:O(NlogK)O(NlogK)
策略二:逐一合并两条链表
方法3. 递归合并两条链表
也就是上一节介绍过的,先合并两个链表,然后将其他链表逐个合并进来。
事实上,两个链表合并又可以有递归和迭代两种方式,下面分别贴出来:
递归:
private ListNode merge2Lists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = merge2Lists(l1.next, l2);
return l1;
}
l2.next = merge2Lists(l1, l2.next);
return l2;
}
方法4:迭代合并两条链表
private ListNode merge2Lists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 == null? l2: l1;
return dummyHead.next;
}
合并K个链表的代码就是:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list: lists) {
res = merge2Lists(res, list);
}
return res;
}
}
策略三:归并法合并
方法5.归并法两两合并
既然上面的方法三中可以逐步合并,那是否可以利用归并的思想,先两两合并成新的,然后将新的再次两个合并呢?当然可以,因为归并排序就是这么做的。
当然,不管怎么样,还是需要合并两个链表,此时我们可以复用merge2Lists()方法的。
下面这个代码是将所有元素合并到第一个链表list[0]里了:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
int k = lists.length;
while (k > 1) {
int idx = 0;
for (int i = 0; i < k; i += 2) {
if (i == k - 1) {
lists[idx++] = lists[i];
} else {
lists[idx++] = merge2Lists(lists[i], lists[i + 1]);
}
}
k = idx;
}
return lists[0];
}
}
方法6.递归+二分法进行归并合并
在网上看到一个大神给了更牛的方法,将上面的过程再次使用递归完成,而且将谁和谁合并采用了二分的策略,含金量超高,一起来欣赏一下:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int lo, int hi) {
if (lo == hi) {
return lists[lo];
}
int mid = lo + (hi - lo) / 2;
ListNode l1 = merge(lists, lo, mid);
ListNode l2 = merge(lists, mid + 1, hi);
return merge2Lists(l1, l2);
}
}
链表合并的题目,我们用了两节来介绍,本节的不少内容是综合查阅了很多网上资料总结的,一个题至少有6种解法,也就是6种解题思路。如果我们将其都搞清楚,比简单的刷6道题要强很多。