题目描述
知识点
链表、归并排序
结果
实现
码前思考
由于题目要求空间复杂度是 O ( 1 ) O(1) O(1),因此不能使用递归。因此这里使用 bottom-to-up(自底向上) 的算法来解决。
bottom-to-up
的归并思路是这样的:先两个两个的 merge,完成一趟后,再 4 个4个的 merge,直到结束。举个简单的例子:[4,3,1,7,8,9,2,11,5,6]
。
step=1: (3->4)->(1->7)->(8->9)->(2->11)->(5->6)
step=2: (1->3->4->7)->(2->8->9->11)->(5->6)
step=4: (1->2->3->4->7->8->9->11)->5->6
step=8: (1->2->3->4->5->6->7->8->9->11)
链表里操作最难掌握的应该就是各种 断链 啊,然后 再挂接 啊。在这里,我们主要用到链表操作的两个技术:
merge(l1, l2)
,双路归并,我相信这个操作大家已经非常熟练的,就不做介绍了。cut(l, n)
,可能有些同学没有听说过,它其实就是一种split
操作,即断链操作。不过我感觉使用cut
更准确一些,它表示,将链表l
切掉前n
个节点,并返回后半部分的链表头。- 额外再补充一个
dummyHead
大法,已经讲过无数次了,仔细体会吧。
希望同学们能把 双路归并 和 cut
断链 的代码烂记于心,以后看到类似的题目能够刷到手软。
掌握了这三大神器后,我们的 bottom-to-up
算法伪代码就十分清晰了:
current = dummy.next;
tail = dummy;
for (step = 1; step < length; step *= 2) {
while (current) {
// left->@->@->@->@->@->@->null
left = current;
// left->@->@->null right->@->@->@->@->null
right = cut(current, step); // 将 current 切掉前 step 个头切下来。
// left->@->@->null right->@->@->null current->@->@->null
current = cut(right, step); // 将 right 切掉前 step 个头切下来。
// dummy.next -> @->@->@->@->null,最后一个节点是 tail,始终记录
// ^
// tail
tail.next = merge(left, right);
while (tail->next) tail = tail->next; // 保持 tail 为尾部
}
}
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
ListNode dummyHead(0); //链表顺序会发生改变,需要使用dummyHead
dummyHead.next = head;
auto p = head;
int length = 0;
//求链表的长度
while(p){
++length;
p = p->next;
}
//遍历每一种归并方案
for(int size = 1;size < length;size <<= 1){
//从起点开始
auto cur = dummyHead.next;
auto tail = &dummyHead; //注意是取地址
while(cur){
auto left = cur;
auto right = cut(left,size); //left->@->@ right->@->@->@...
cur = cut(right,size); //left->@->@ right->@->@ cur->@->...
tail->next = merge(left,right);
while(tail->next){
tail = tail->next;
}
}
}
return dummyHead.next;
}
ListNode* cut(ListNode* head,int n){
auto p = head;
while(--n && p){ //注意这里是--n而不是n--
p = p->next;
}
//如果是因为p为nullptr 而 提前终止的
if(!p){
return nullptr;
}
// int i=5;
// while(--i){
// printf("%d\n",i);
// }
auto next = p->next; //注意next可能为NULL
p->next = nullptr;
return next;
}
ListNode* merge(ListNode* l1,ListNode* l2){
ListNode dummyHead(0); //合并链表,需要使用dummyHead
auto p = &dummyHead;
while(l1 && l2){
if(l1->val < l2->val){
p->next = l1;
p = l1;
l1 = l1->next;
}else{
p->next = l2;
p = l2;
l2 = l2->next;
}
}
p->next = l1 ? l1:l2; //直接拼接,妙啊!!!
return dummyHead.next;
}
};
码后反思
- 注意上述操作对 空指针 的处理(因为有一些子链会没有
size
那么大,只有0~size-1
大小),比如下面的代码片段就考虑了这种情况:while(--n && p){ //注意这里是--n而不是n-- p = p->next; } //如果是因为p为nullptr 而 提前终止的 if(!p){ return nullptr; }
- 这种链表题目,有两种操作需要记住,并且将这两步操作写成单独的函数(写成单独的函数更能够理清代码的结构):
cut()
/split()
:切分链表,并且返回右链表的头指针;merge()
:合并两个链表,并且返回合并后的链表的头指针。
(天下之事分久必合~有cut
就得有merge
)
- 一旦涉及到链表内的元素的位置的交换,然后又要返回调整后的链表的头结点的时候,一定要使用
dummyHead
!上述代码有两处使用了dummyHead
都是这种情况。