链表
采用指针访问元素,而且指针只向一个方向移动。
- 巧妙的构造虚拟头结点,可以使遍历处理逻辑更加统一;
- 灵活使用递归(递归深度可能会导致超时和栈溢出);
- 链表区间逆序。第 92 题。
- 链表寻找中间节点。第 876 题(
快慢指针
)。链表寻找倒数第 n 个节点。第 19 题(双指针
)。 - 合并 K 个有序链表。第 21 题,第 23 题(分治算法)。
- 链表归类。第 86 题-分隔链表,第 328 题-奇偶链表。
- 链表排序,时间复杂度要求 O ( n × l o g n ) O(n \times log n) O(n×logn), 空间复杂度 O ( 1 ) O(1) O(1)。只有一种做法,归并排序,自顶向上递归调用,第 148 题。
- 判断链表是否存在环,如果有环,输出环的交叉点的下标;判断 2 个链表是否有交叉点,如果有
交叉点,输出交叉点。第 141 题(哈希表
、快慢指针
),第 142 题(哈希表
、快慢指针
a = c + ( n − 1 ) ( b + c ) a=c+(n−1)(b+c) a=c+(n−1)(b+c) ),第 160 题。
合并两个有序链表(第21题)
leetcode 第21题(简单)
方法一:递归
public class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// l1 为空 返回 l2
if (l1 == null) {
return l2;
// l2 为空 返回 l1
} else if (l2 == null) {
return l1;
// 链表中保留 val 值小的节点,递归 之后的部分
} else if (l1.val < l2.val){
l1.next = mergeTwoLists (l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
public static void main(String[] args) {
// l1: 1 -> 2 -> 4
ListNode l1 = new ListNode(1, new ListNode(2, new ListNode(4, null)));
// l2: 1 -> 3 -> 4
ListNode l2 = new ListNode(1, new ListNode(3, new ListNode(4, null)));
ListNode result = new Solution().mergeTwoLists(l1, l2);
ListNode ptr = result;
while (ptr != null) {
System.out.print(ptr.val + " -> ");
ptr = ptr.next;
}
System.out.println("null");
}
}
class ListNode {
int val;
ListNode next;
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
复杂度分析:
- 时间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉
l1
或l2
的头节点(直到至少一个链表为空),mergeTwoList
至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O ( n + m ) O(n+m) O(n+m)。 - 空间复杂度: O ( n + m ) O(n+m) O(n+m),其中 n 和 m 分别为两个链表的长度。递归调用
mergeTwoLists
函数时需要消耗栈空间(占空间的大小取决于递归调用的深度)。结束递归调用时mergeTwoLists
函数最多调用n+m
次,因此空间复杂度为 O ( n + m ) O(n+m) O(n+m)。
方法二: 迭代
当 l1
和 l2
都不为空链表时,判断 哪个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移动一位。
class Solution {
public ListNode mergeTwoLists (ListNode a, ListNode b) {
// 1、创建虚拟头节点 dummyHead,保存合并之后的链表
// 声明指针 tail 来记录下一个插入元素的在哪个节点之后挂载新的节点
ListNode dummyHead = new ListNode(0, null), tail = dummyHead;
// 5、循环终止条件,当有一个链表为空时,结束循环
while (a != null && b != null) {
if (a.val < b.val) {
// 2、将值较小的节点拼接到尾部节点tail 之后;
tail.next = a;
// 3、同时将 对应指针后移;
a = a.next;
} else {
tail.next = b;
b = b.next;
}
// 4、尾部指针后移
tail = tail.next;
}
// 6、跳出循环后,至多有一个链表非空,将非空的链表拼接到尾部节点
tail.next = a == null ? b : a;
return dummyHead.next;
}
}
复杂度分析:
- 时间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代只有
l1
或l2
中的一个元素会被放进合并链表中,因此while
循环次数不会超过两个链表的长度之和,其他操作时间复杂度都是常数级别,因此总的时间复杂度为 O ( n + m ) O(n+m) O(n+m)。 - 空间复杂度: O ( 1 ) O(1) O(1)。只需要常数的空间存放若干指针变量。
合并 k 个有序链表(第23题)
LeetCode 第23题 (困难)
给定一个链表数组,每个链表都已经按照升序排列,将所有链表合并到一个升序链表中,返回合并后的链表;
方法一:顺序合并
最朴素的方法是,用 一个变量 ans
来维护合并后的链表,第i 次循环,把第 i 个链表和 ans
合并;
class Solution{
public ListNode mergerKLists(ListNode[] lists) {
ListNode ans = null;
for (int i = 0; i < lists.length; ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
return ans;
}
}
复杂度分析:
- 时间复杂度:假设每个链表长度为
n
,在第一次合并后,ans
的长度为n
;第二次合并后,ans
的长度为2 x n
;第i
次合并后,ans
的长度为i x n
。第i
次合并的时间代价是 O ( n + ( i − 1 ) × n ) = O ( i × n ) O(n+(i-1)\times n) = O(i \times n) O(n+(i−1)×n)=O(i×n),那么总的时间代价为 O ( ∑ i = 1 k ( i × n ) ) = O ( ( 1 + k ) k 2 × n ) = O ( k 2 n ) O(\sum_{i=1}^{k}(i\times n)) = O(\frac{(1+k)k}{2} \times n) = O(k^2 n) O(∑i=1k(i×n))=O(2(1+k)k×n)=O(k2n),故渐进时间复杂度为 O ( K 2 n ) O(K^2 n) O(K2n)。 - 空间复杂度: 没有用到 与 k 和 n 规模相关的辅助空间,故渐进空间复杂度为 O(1)。
*方法二: 分治合并
- 将 k 个链表配对,并将同一对中的链表合并;
- 第一轮合并以后,k 个链表被合并成 k 2 \frac{k}{2} 2k 个链表,平均长度 为 2 n k \frac{2n}{k} k2n, 然后是 k 4 \frac{k}{4} 4k 个链表, k 8 \frac{k}{8} 8k 个链表等等;
- 重复这一过程,直到得到最终的有序链表;
class Solution {
public ListNode mergeKLists (ListNode[] lists) {
return merge(lists, 0, lists.length - 1)