1. 题目来源
链接:lc143. 重排链表
相关题目:
题单:
-
- 链表、二叉树与一般树(前后指针/快慢指针/DFS/BFS/直径/LCA)
- §1.6 快慢指针
2. 题目说明
3. 题目解析
可以采用线性表 vector<ListNode*>
记录所有的节点,然后下标直接访问进行两两之间的逆置,也是官方题解方法一。空间复杂度较高,不谈了。
采用题解中的方法二:寻找链表中点 + 链表逆序 + 合并链表
通过观察能够发现,链表的后半段各个节点会插入到链表前半段两个节点的中间,但是链表没办法逆序访问,所以我们可以找到链表中点,并将后半段链表进行逆置,然后采用双指针一个指向头节点,一个指向中点进行合并。
思路有了,紧接着考虑边界情况,链表节点个数奇偶的情况,记链表节点数为 n
,中间节点为 mid
:
mid
节点求解,如果是奇数个节点时,可以将中间的节点归结到前半段,则为 ⌊ n + 1 2 ⌋ \lfloor \frac {n + 1}{2} \rfloor ⌊2n+1⌋ 等价于 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉- 奇数情况:很好说明,
mid->next = NULL;
即可 - 偶数情况:由于参考示例 1,能够发现是
mid->next->next = NULL
。偶数节点的边界情况,若为1->2->NULL
,则 mid 会指向 1,同时进行链表反转后,1, 2 的next
会互相指向对方(可以自行在lc
中cout
观察),然后mid->next->next = NULL
就可以顺利使得 2 的next
指针指向空。
至此分析完毕了,边界情况了考虑完毕了,我们再来考虑一种情况:
- 在合并前、中、后处理尾指针为空的情况?
- 例如
1->2->3->4->NULL
我们求取了mid = 2
,然后进行后半段链表反转,反转完毕后为1->2<->3<-4
注意2<->3
在此已经形成了环。 - 然后,设置
p = 1, a = 4
来开始合并,合并的次数就是n / 2
次 - 第一次合并,将 4 插入 1、2 之间,变成
1->4->2<->3
,此时p = 2, a = 3,i = 1
- 如果在合并后处理尾指针为空的情况,偶数节点链表的
mid->next
节点会形成一个自环。然后mid->next->next = NULL
刚好可以处理 - 如果在合并前处理尾指针为空的情况,则在最后更新的时候,依旧会形成 3 的自环,但是最后又没有处理,就死循环了。可以在
lc
上将代码放到中间看看,可以得到死循环的代码,然后copy
到尾部再处理一遍就能够通过了 - 当为奇数节点时,在中间处理尾指针是可行的,可自己画图分析~
- 所以将尾指针的空处理直接放到最后即可,可以统一起来
处理尾指针代码:
if (n % 2) mid->next = NULL;
else mid->next->next = NULL;
很综合的一道练习题,上述的坑基本都才过一遍了,链表的题目十分建议画图理解,各个指针之间的关系容易把自己绕进去。
建议可以直接参考灵神的 环形链表II【基础算法精讲 07】 中的写法。
- 找链表中点
- 反转链表
- 两侧 head 指针相对移动即可
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
参见代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
void reorderList(ListNode* head) {
if (!head) return ;
int n = 0;
for (auto e = head; e; e = e->next) ++n;
auto mid = head;
// 在此将奇数节点归结到前半段区间
for (int i = 0; i < (n + 1) / 2 - 1; ++i) mid = mid->next;
auto a = mid, b = a->next;
// 后半段链表反转,1->2->3->4->NULL变成1->2<->3<-4
for (int i = 0; i < n / 2; ++i) {
auto c = b->next;
b->next = a;
a = b;
b = c;
}
// 最后处理尾指针统一奇偶情况
// 偶数节点时,mid节点的下一个节点会形成自环,则mid->next->next就可以让其为null
// 奇数节点时,仅在中间处理是可以的
// 为了统一,统一在尾部进行处理,也可以双处理,验证下奇数节点的处理位置
// if (n % 2) mid->next = NULL;
// else mid->next->next = NULL;
auto p = head, q = a;
for (int i = 0; i < n / 2; ++i) {
auto o = q->next;
q->next = p->next;
p->next = q;
p = q->next, q = o;
}
if (n % 2) mid->next = NULL;
else mid->next->next = NULL;
}
};
更新:2021年7月8日
本题直接转化为后半段的链表反转,然后两个独立链表合并即可。
那么,针对 a->next
及 b->next
不需要最后置为空,导致一系列边界情况,成环情况的发生,直接在确定 a
b
c
指针的时候,直接置为空即可,然后直接使用链表合并,舍弃判断奇偶的奇奇怪怪的情况了!
更新后的简洁代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void rearrangedList(ListNode* head) {
if (!head) return ;
int n = 0;
for (auto p = head; p; p = p->next) n ++ ;
// 找终点
auto a = head;
for (int i = 0; i < (n + 1) / 2 - 1; i ++ ) a = a->next;
// 反转后半段
auto b = a->next, c = b->next;
a->next = NULL, b->next = NULL; // 重要优化,直接将其分成两个链表,防止偶数环出现
while (c) {
auto d = c->next;
c->next = b;
b = c;
c = d;
}
// 合并两个链表,等分且后半段较少,将 q 节点插入到 p 节点的后面即可
for (auto p = head, q = b; q; ) {
auto t = q->next;
q->next = p->next;
p->next = q;
p = q->next, q = t;
}
}
};
更新:2024年08月29日
建议参考灵神的写法吧,感觉更容易被人所接受一点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
void reorderList(ListNode* head) {
// lc876 找链表中间节点
// 注意,这是找右侧中点的写法。需要和下面链表合并配合起来使用。
auto a = head, b = head;
while (b && b->next) {
a = a->next;
b = b->next->next;
}
auto mid = a;
// lc206 反转链表
a = nullptr;
b = mid;
while (b) {
auto c = b->next;
b->next = a;
a = b, b = c;
}
// 本题的特性,两个指针对向改变
// 如果上面找的是右侧中点,那么应该用这个当where条件。
// 如果上面找的是左侧中点,那么应该用 head && head2 当where条件
auto head2 = a;
while (head2->next) {
auto n1 = head->next, n2 = head2->next;
head->next = head2;
head2->next = n1;
head = n1;
head2 = n2;
}
}
};
具体的,可以查看灵神的题解,举了例子,或者自己举一下 奇偶 链表长度的例子即可。
左侧中点写法:
class Solution {
public:
void reorderList(ListNode* head) {
if (!head || !head->next) {
return ;
}
// lc876 找链表中间节点
// 注意,这是找右侧中点的写法。需要和下面链表合并配合起来使用。
auto a = head, b = head;
while (b->next && b->next->next) {
a = a->next;
b = b->next->next;
}
auto mid = a;
// lc206 反转链表
a = nullptr;
b = mid;
while (b) {
auto c = b->next;
b->next = a;
a = b, b = c;
}
// 本题的特性,两个指针对向改变
// 如果上面找的是右侧中点,那么应该用这个当where条件。
// 如果上面找的是左侧中点,那么应该用 head && head2 当where条件
auto head2 = a;
while (head && head2) {
auto n1 = head->next, n2 = head2->next;
head->next = head2;
head2->next = n1;
head = n1;
head2 = n2;
}
}
};