前言
链表是一种常见的线性数据结构。对于单向链表,每个节点有一个 n e x t next next 指针指向后一个节点,还有一个成员变量用以存储数值;对于双向链表,还有一个 p r e v prev prev 指针指向前一个节点。与数组类似,搜索链表需要 O ( n ) O(n) O(n) 的时间复杂度,但是链表不能通过常数时间读取第 k k k 个数据。链表的优势在于能够以较高的效率在任意位置插入或删除一个节点。
解题思路
- 当涉及可能改变头结点的操作时(插入、删除、反转等),应该考虑在头结点前建立哑结点(dummy)。
- 当需要在链表中寻找特定位置时,可以用快慢指针实现 O ( n ) O(n) O(n) 时间内完成遍历。
具体问题
结点定义
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
反转链表 II
反转从位置 m m m 到 n n n 的链表。请使用一趟扫描完成反转。 1 ≤ m ≤ n ≤ 1 ≤ m ≤ n ≤ 1≤m≤n≤ 链表长度。
示例
输入:1->2->3->4->5->NULL, m = 2, n = 4
输出:1->4->3->2->5->NULL
解题思路
由于头结点可能发生改变,因此需要一个哑结点指向头结点。首先需要找到第一个开始反转结点的前一个位置。因为题目中
m
,
n
m,n
m,n 是从 1 开始计数的,所以
p
r
e
pre
pre 只需要向后移动
m
−
1
m-1
m−1 个位置,
p
r
e
pre
pre 始终指向位置
m
m
m 的前一个结点。然后要开始交换位置,由于一次只能交换两个结点,示例中交换过程如下:
ListNode* reverseBetween(ListNode* head, int m, int n) {
ListNode *dummy = new ListNode(-1), pre = dummy;
dummy->next = head;
for (int i = 0; i < m - 1; ++i) {
pre = pre->next;
}
ListNode *cur = pre->next;
for (int i = m; i < n; ++i) {
ListNode *t = cur->next;
cur->next = t->next;
t->next = pre->next;
pre->next = t;
}
return dummy->next;
}
类似题目 反转链表
K 个一组翻转链表
给你一个链表,每 k k k 个结点一组进行翻转,请你返回翻转后的链表。 k k k 是一个正整数,它的值小于或等于链表的长度。如果结点总数不是 k k k 的整数倍,那么请将最后剩余的结点保持原有顺序。
示例
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
解题思路
类似于上一题,只不过需要把原链表分成若干段,然后分别对每段进行反转。其中,反转每段链表的过程和上一题一样,注意反转之后 reverseKGroup 函数中
c
u
r
cur
cur 和
p
r
e
pre
pre 的位置发生了改变,cur = pre->next
;不需要反转的话 cur = cur->next
。
/* 迭代 */
ListNode* reverseKGroup(ListNode* head, int k) {
if (!head || k < 2) return head;
ListNode *dummy = new ListNode(-1), *pre = dummy, *cur = head;
dummy->next = head;
for (int i = 1; cur; ++i) {
if (i % k == 0) {
pre = reverse(pre, cur->next);
cur = pre->next;
} else {
cur = cur->next;
}
}
return dummy->next;
}
ListNode* reverse(ListNode* start, ListNode* end) {
ListNode* pre = start->next, *cur = pre->next;
while (cur != end) {
pre->next = cur->next;
cur->next = start->next;
start->next = cur;
cur = pre->next;
}
return pre;
}
/* 递归 */
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* cur = head;
for (int i = 0; i < k; ++i) {
if (!cur) return head;
cur = cur->next;
}
ListNode* newHead = reverse(head, cur);
head->next = reverseKGroup(cur, k);
return newHead;
}
ListNode* reverse(ListNode* start, ListNode* end) {
ListNode* pre = end;
while (start != end) {
ListNode* t = start->next;
start->next = pre;
pre = start;
start = t;
}
return pre;
}
类似题目 两两交换链表中的节点
合并K个排序链表
合并 k k k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例
输入:
[
1->4->5,
1->3->4,
2->6
]
输出:1->1->2->3->4->4->5->6
解题思路
利用归并排序的思想,将
k
k
k 个链表不断分成两份,然后递归调用分段函数,最终问题变成了合并两个有序链表。然后再写一个函数对两个链表进行合并即可。时间复杂度
O
(
N
l
o
g
k
)
O(Nlogk)
O(Nlogk),其中
N
N
N 为链表总结点数;空间复杂度
O
(
1
)
O(1)
O(1)。合并两个链表时,这里采用了递归的写法,迭代也可以,思想是“穿针引线”。当 l1->val > l2->val
时,说明要先从 l2 拿出一个结点排序,因此对 l1 和 l2 剩余部分(即 l2->next)进行递归,最后将递归部分连在 l2 拿出的结点后面,最后返回 l2。反之亦然。
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists, 0, (int) lists.size() - 1);
}
ListNode* merge(vector<ListNode*> &lists, int start, int end) {
if (start > end) return NULL;
if (start == end) return lists[start];
int mid = start + (end - start) / 2;
ListNode *left = merge(lists, start, mid);
ListNode *right = merge(lists, mid + 1, end);
return mergeTwoLists(left, right);
}
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
if (!l1) return l2;
if (!l2) return l1;
if (l1->val > l2->val) {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
} else {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
}
删除排序链表中的重复元素 II
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
示例
输入:1->2->3->3->4->4->5
输出:1->2->5
解题思路
由于链表开头可能会有重复项,被删掉的话头指针会改变,而最终却还需要返回链表的头指针,所以需要定义一个新的节点,然后链上原链表。定义一个前驱指针和一个现指针,每当前驱指针指向新建的节点,现指针从下一个位置开始往下遍历,遇到相同的则继续往下,直到遇到不同项时,把前驱指针的 n e x t next next 指向下面那个不同的元素。如果现指针遍历的第一个元素就不相同,则把前驱指针向下移一位。
ListNode* deleteDuplicates(ListNode* head) {
if (!head || !head.next) return head;
ListNode *dummy = new ListNode(-1), *pre = dummy;
dummy->next = head;
while (pre->next) {
ListNode *cur = pre->next;
while (cur->next && cur->next->val == cur->val) {
cur = cur->next;
}
if (cur != pre->next) {
pre->next = cur->next;
} else {
pre = pre->next;
}
}
return dummy->next;
}
类似题目 删除排序链表中的重复元素,移除链表元素
环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
解题思路
只需要设两个指针,一个每次走一步的慢指针和一个每次走两步的快指针,如果链表里有环的话,两个指针最终肯定会相遇。当两个指针相遇了后,让其中一个指针再次从链表头开始,两个指针一起每次走一步,等到它们再相遇的位置就是链表中环的起始位置。
如图,X 为表头,Y 为环的起始结点。假设两个指针第一次相遇的位置为结点 Z。此时,慢指针走过的距离为
a
+
b
a + b
a+b,快指针走过的距离为
a
+
b
+
c
+
b
=
2
(
a
+
b
)
a + b + c + b = 2(a + b)
a+b+c+b=2(a+b),可得到
a
=
c
a=c
a=c。
由图可知,环的长度
L
=
b
+
c
=
a
+
b
L = b + c = a + b
L=b+c=a+b,也就是说,从一开始到二者第一次相遇,循环的次数就等于环的长度。接着让两个指针分别从 X,Z 处开始每次走一步,那么它们正好会在 Y 处再次相遇。
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) break;
}
if (!fast || !fast->next) return NULL;
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
类似题目 环形链表,链表的中间结点,删除链表的倒数第N个节点,旋转链表,奇偶链表。