23. 合并K个升序链表【堆】【归并】
归并
思想如下图,两两合并。
- 时间复杂度分析:假设链表长度为 n n n,共有 k k k个链表,可以看到每一层都共有 k ∗ n k*n k∗n个节点进行合并,合并的次数为递归深度 l o g k logk logk。因此时间复杂度为: O ( k × n × l o g k ) O(k×n×logk) O(k×n×logk)
- 空间复杂度为递归深度: O ( l o g k ) O(logk) O(logk)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
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 low, int high) {
if (low == high) {
return lists[low];
}
int mid = (low + high) / 2;
ListNode l1 = merge(lists, low, mid);
ListNode l2 = merge(lists, mid + 1, high);
return merge2Lists(l1, l2);
}
// 合并两个有序链表
private ListNode merge2Lists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode();
ListNode tail = dummy;
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 ? l1 : l2;
return dummy.next;
}
}
方法二:小根堆(优先队列)
将链表都存到小根堆中,实质上,堆中存的的是链表的头节点,并且会按照增序进行排列。每次从堆中取出头节点最小的链表,然后将该链表的头节点插入到结果链表的末尾,最后再将去点头节点的链表加回到堆中。
- 时间复杂度:堆中元素不超过 k k k个,那么插入/删除的时间复杂度为 O ( l o g k ) O(logk) O(logk),而一共最多有 k n kn kn个节点,因此时间复杂度为 O ( k × n × l o g k ) O(k×n×logk) O(k×n×logk)
- 空间复杂度:堆的大小 O ( k ) O(k) O(k)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 小根堆
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
for (ListNode elem : lists) {
if (elem != null) {
pq.offer(elem);
}
}
ListNode dummy = new ListNode();
ListNode tail = dummy;
while (!pq.isEmpty()) {
ListNode minElem = pq.poll(); // 取出头节点最小的链表
tail.next = minElem; // 将头节点元素加入结果链表
tail = tail.next;
if (minElem.next != null) { // 将去除头节点后的链表加回堆
pq.offer(minElem.next);
}
}
return dummy.next;
}
}
24. 两两交换链表中的节点
设置一个虚头节点。
将p的前驱pre后插到p的后面。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 空链表,返回null
if (head == null) {
return null;
}
// 虚头节点
ListNode dummy = new ListNode();
dummy.next = head;
// pre : 两个待交换节点中的前一个
// p : 两个待交换节点中的后一个
// tmp : pre交换前的前驱节点
ListNode pre = head, p, tmpHead = dummy;
while (pre != null && pre.next != null) {
p = pre.next;
tmpHead.next = p;
pre.next = p.next;
p.next = pre;
tmpHead = pre;
pre = pre.next;
}
return dummy.next;
}
}
25. K 个一组翻转链表
先求链表长度,以计算出需要反转的子段有几个。
设置虚头节点,即实际需要反转的子段的前一个节点。
反转函数,将长度为k的链表反转,并返回下一个需要反转的链表的虚头节点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(); // 要返回的链表的虚拟头节点
dummy.next = head;
int len = 0; // 链表长度
ListNode q = head;
while (q != null) {
++len;
q = q.next;
}
int maxCnt = len / k; // 需要反转的段数
// 进行分段反转
ListNode nextHead = dummy;
for (int i = 0; i < maxCnt; i++) {
if (nextHead.next != null) {
nextHead = reverse(nextHead, k);
}
}
return dummy.next;
}
// 反转k个节点,并返回下一段的虚头节点
private ListNode reverse(ListNode head, int k) {
ListNode p = head.next, q = head;
ListNode nextHead = head.next;
head.next = null;
for (int i = 0; i < k; i++) {
q = p.next;
p.next = head.next;
head.next = p;
p = q;
}
nextHead.next = q;
return nextHead;
}
}