文章目录
LeetCode精选题之链表
参考资料:CyC2018的LeetCode题解
总结:
- 链表中基本的穿针引线,画图即可明白指针变化的过程。
- 使用递归来操作链表,代码简洁,思路清晰,例如反转链表。
- 如果要修改或者删除头节点,可以在头节点前面加一个虚拟头节点dummyHead,这样原链表中每个节点都有前一个节点,就能进行一样的操作,不用对头节点区别对待了。
- 可以使用容器来存放节点,比如需要倒序访问链表中的节点(链表求和–LeetCode445)。
- 链表中的双指针,主要用法有:快慢指针求链表中点,固定长度的双指针一次遍历删除倒数第N个节点,有点类似于固定长度的滑动窗口。
1 相交链表–LeetCode160
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
注意:
- 如果两个链表没有交点,返回 null.
- 在返回结果后,两个链表仍须保持原有的结构。
- 可假定整个链表结构中没有循环。
- 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
思路:
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
如果不存在交点,那么 a + b = b + a,以下实现代码中 l1 和 l2 会同时为 null,从而退出循环。
代码如下:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode curr1 = headA;
ListNode curr2 = headB;
while (curr1 != curr2) {
curr1 = curr1.next;
curr2 = curr2.next;
if (curr1==null && curr2==null) {
return null;
}
if (curr1 == null) {
curr1 = headB;
}
if (curr2 == null) {
curr2 = headA;
}
}
return curr1;
}
}
大佬的代码(真清爽):
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode l1 = headA, l2 = headB;
while (l1 != l2) {
l1 = (l1 == null) ? headB : l1.next;
l2 = (l2 == null) ? headA : l2.next;
}
return l1;
}
2 反转链表–LeetCode206
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
迭代:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode prev = null;
ListNode curr = head;
ListNode next = null;
while (curr != null) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
递归:
class Solution {
public ListNode reverseList(ListNode head) {
// 递归终止条件
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
3 合并两个有序链表–LeetCode21
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
链表中的穿针引线:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 边界条件
if (l1 == null || l2 == null) {
return l1==null ? l2 : l1;
}
// 首先确定新的头结点
ListNode curr1 = l1;
ListNode curr2 = l2;
ListNode newHead = null;
if (curr1.val < curr2.val) {
newHead = curr1;
curr1 = curr1.next;
}else {
newHead = curr2;
curr2 = curr2.next;
}
ListNode prev = newHead;
while (curr1 != null || curr2 != null) {
if (curr1 == null) {
prev.next = curr2;
break;
}
if (curr2 == null) {
prev.next = curr1;
break;
}
if (curr1.val < curr2.val) {
prev.next = curr1;
curr1 = curr1.next;
}else {
prev.next = curr2;
curr2 = curr2.next;
}
prev = prev.next;//这一步千万别忘了
}
return newHead;
}
}
递归解法:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
4 删除排序链表中的重复元素–LeetCode83
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
代码如下:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode prev = head;
while (prev.next != null) {
if (prev.next.val == prev.val) {
prev.next = prev.next.next;
}else {
prev = prev.next;
}
}
return head;
}
}
5 删除链表的倒数第N个节点–LeetCode19
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的 n 保证是有效的。
进阶:你能尝试使用一趟扫描实现吗?
思路:双指针,类似于固定长度的滑动窗口。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) {
return null;
}
ListNode dummyHead = new ListNode(-1);// 虚拟头节点
dummyHead.next = head;
ListNode slow = dummyHead;
ListNode fast = dummyHead;
for (int i = 1; i <= n; i++) {
fast = fast.next;
// 特殊情况,n大于链表长度
// 题目中保证n有效,所以这里可以不用考虑
if (fast == null) {
return head;
}
}
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
// 此时slow指向待删除节点的前一个
slow.next = slow.next.next;
return dummyHead.next;
}
}
6 两两交换链表中的节点–LeetCode24
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
代码如下:
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
ListNode prev = dummyHead;
ListNode curr = head;
ListNode nextNode = null;
while (curr != null && curr.next != null) {
nextNode = curr.next;
prev.next = nextNode;
curr.next = nextNode.next;
nextNode.next = curr;
prev = curr;
curr = curr.next;
}
return dummyHead.next;
}
}
7 链表求和–LeetCode445
给你两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。你可以假设除了数字 0 之外,这两个数字都不会以零开头。
进阶:如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
示例:
输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7
使用了辅助集合来逆序存储元素。
import java.util.Stack;
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Stack<Integer> stack1 = buildStack(l1);
Stack<Integer> stack2 = buildStack(l2);
int carry = 0;
ListNode dummyHead = new ListNode(-1);
while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
int a = stack1.isEmpty() ? 0 : stack1.pop();
int b = stack2.isEmpty() ? 0 : stack2.pop();
int sum = a+b+carry;
ListNode node = new ListNode(sum%10);
node.next = dummyHead.next;
dummyHead.next = node;
carry = sum/10;
}
return dummyHead.next;
}
private Stack<Integer> buildStack(ListNode l) {
Stack<Integer> stack = new Stack<>();
ListNode curr = l;
while (curr != null) {
stack.push(curr.val);
curr = curr.next;
}
return stack;
}
}
8 回文链表–LeetCode234
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
思路:首先利用快慢指针找到中点,将链表切成两半,把后半段反转,然后比较两半是否相等。
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 首先利用快慢指针找到中点,当长度为偶数时,中点指的是前一个
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 将后一半进行反转
ListNode newHead = reverseList(slow.next);
ListNode curr1 = head;
ListNode curr2 = newHead;
while (curr2 != null) {//这里是以后一半链表的结束为循环结束条件,因为当原链表长度为奇数时,前一半链表会多一个节点
if (curr1.val != curr2.val) {
return false;
}
curr1 = curr1.next;
curr2 = curr2.next;
}
return true;
}
private ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
9 分隔链表–LeetCode725
给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。返回一个符合上述规则的链表的列表。
举例: 1->2->3->4, k = 5 // 5 结果 [ [1], [2], [3], [4], null ]
示例 1:
输入:
root = [1, 2, 3], k = 5
输出: [[1],[2],[3],[],[]]
解释:
输入输出各部分都应该是链表,而不是数组。
例如, 输入的结点 root 的 val= 1, root.next.val = 2, \root.next.next.val = 3, 且 root.next.next.next = null。
第一个输出 output[0] 是 output[0].val = 1, output[0].next = null。
最后一个元素 output[4] 为 null, 它代表了最后一个部分为空链表。
示例 2:
输入:
root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3
输出: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过1.前面部分的长度大于等于后面部分的长度。
提示:
- root 的长度范围: [0, 1000].
- 输入的每个节点的大小范围:[0, 999].
- k 的取值范围: [1, 50].
自己写的代码,比较冗长。base表示每个每个小链表至少应该有base个节点,more表示多出来的节点,那么应该平均分给前more个小链表,每个链表分到一个。
class Solution {
public ListNode[] splitListToParts(ListNode root, int k) {
ListNode[] res = new ListNode[k];
if (root == null) {
return res;
}
// 首先计算出链表长度
int n = 1;
ListNode curr = root;
while (curr.next != null) {
curr = curr.next;
n++;
}
int base = n / k;// 表示每个小链表至少有多少个节点
int more = n % k;// 多出来的节点数量,应该前more个小链表每人多分一个
curr = root;
ListNode next = null;
int i = 0;
for (; i < more; i++) {
for (int j = 1; j < (1+base); j++) {
curr = curr.next;
}
next = curr.next;
curr.next = null;
res[i] = root;
root = next;
curr = root;
}
while (curr != null) {
for (int j = 1; j < base; j++) {
curr = curr.next;
}
next = curr.next;
curr.next = null;
res[i++] = root;
root = next;
curr = root;
}
return res;
}
}
大佬的简洁版代码:
class Solution {
public ListNode[] splitListToParts(ListNode root, int k) {
// 首先计算出链表长度
int n = 0;
ListNode curr = root;
while (curr != null) {
n++;
curr = curr.next;
}
ListNode[] res = new ListNode[k];
int size = n / k;// 表示每个小链表至少有多少个节点
int mod = n % k;// 多出来的节点数量,应该前mod个小链表每人多分一个
curr = root;
for (int i = 0; curr != null && i < k; i++) {
res[i] = curr;
int curSize = size + (mod-- > 0 ? 1 : 0);
for (int j = 1; j < curSize; j++) {
curr = curr.next;
}
ListNode next = curr.next;
curr.next = null;
curr = next;
}
return res;
}
}
10 奇偶链表–LeetCode328
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
- 应当保持奇数节点和偶数节点的相对顺序。
- 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
思路:将链表按照奇偶位拆分成两个链表,然后合起来。
代码如下:
class Solution {
public ListNode oddEvenList(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return head;
}
ListNode tempHead = head.next;
ListNode curr1 = head;
ListNode curr2 = tempHead;
ListNode curr = tempHead.next;
while (curr != null) {
// 看curr的前一个节点是谁
if (curr2.next == curr) {
curr1.next = curr;
curr1 = curr;
}else {
curr2.next = curr;
curr2 = curr;
}
curr = curr.next;
}
curr1.next = tempHead;
curr2.next = null;
return head;
}
}