148. 排序链表
在O(nlogn)时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
思路一:排序链表-bottom-to-up O(1) 空间
由于题目要求空间复杂度是 O(1),因此不能使用递归。因此这里使用 bottom-to-up 的算法来解决。
bottom-to-up 的归并思路是这样的:先两个两个的 merge,完成一趟后,再 4 个4个的 merge,直到结束。举个简单的例子:[4,3,1,7,8,9,2,11,5,6].
链表里操作最难掌握的应该就是各种断链啊,然后再挂接啊。在这里,我们主要用到链表操作的两个技术:
1.merge(l1, l2),双路归并,我相信这个操作大家已经非常熟练的,就不做介绍了。
2.cut(l, n),可能有些同学没有听说过,它其实就是一种 split 操作,即断链操作。不过我感觉使用 cut 更准确一些,它表示,将链表 l 切掉前 n 个节点,并返回后半部分的链表头。
3.额外再补充一个 dummyHead 大法,已经讲过无数次了,仔细体会吧。
希望同学们能把双路归并和 cut 断链的代码烂记于心,以后看到类似的题目能够刷到手软。
掌握了这三大神器后,我们的 bottom-to-up 算法伪代码就十分清晰了:
好了,下面是比较正式的代码。
class Solution {
public:
ListNode* sortList(ListNode* head) {
ListNode dummyHead(0);
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) {
p = p->next;
}
if (!p) return nullptr;
auto next = p->next;
p->next = nullptr;
return next;
}
ListNode* merge(ListNode* l1, ListNode* l2) {
ListNode dummyHead(0);
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;
}
};
作者:ivan_allen
链接:https://leetcode-cn.com/problems/sort-list/solution/148-pai-xu-lian-biao-bottom-to-up-o1-kong-jian-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路二:
解答一:归并排序(递归法)
题目要求时间空间复杂度分别为O(nlogn)和O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;
对数组做归并排序的空间复杂度为 O(n),分别由新开辟数组O(n)和递归函数调用O(logn)组成,而根据链表特性:
数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
递归额外空间:递归调用函数将带来O(logn)的空间复杂度,因此若希望达到O(1)空间复杂度,则不能使用递归。
通过递归实现链表归并排序,有以下两个环节:
分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
找到中点 slow 后,执行 slow.next = None 将链表切断。
递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。
合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。双指针法合并,建立辅助ListNode h 作为头部。
设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
返回辅助ListNode h 作为头部的下个节点 h.next。
时间复杂度 O(l + r),l, r 分别代表两个链表长度。
当题目输入的 head == None 时,直接返回None。
作者:jyd
链接:https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路三:
解:由于复杂度的限定,其实就是用归并排序求解
归并排序三部曲:
• fast-slow找中点:直到快的走到了末尾,然后慢的所在位置就是中间位置,这样就分成了两段
• 将链表分成两部分
• 合并两个有序链表
class Solution {
public:
ListNode* sortList(ListNode* head) {
return mergesort(head);
}
ListNode* mergesort(ListNode* node)
{
if(!node || !node->next) return node;
ListNode *fast=node;//快指针走两步
ListNode *slow=node;//慢指针走一步
ListNode *brek=node;//断点
while(fast && fast->next)
{
fast=fast->next->next;
brek=slow;
slow=slow->next;
}
brek->next=nullptr;
ListNode *l1=mergesort(node);
ListNode *l2=mergesort(slow);
return merge(l1,l2);
}
ListNode* merge(ListNode* l1,ListNode* l2)
{
if(l1==NULL)
{
return l2;
}
if(l2==NULL)
{
return l1;
}
if(l1->val < l2->val)
{
l1->next=merge(l1->next,l2);
return l1;
}
else
{
l2->next=merge(l2->next,l1);
return l2;
}
}
};
作者:chenlele
链接:https://leetcode-cn.com/problems/sort-list/solution/pai-xu-lian-biao-by-gpe3dbjds1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我的:
/**
* 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)
{
return mergesort(head);
}
ListNode* mergesort(ListNode* node)
{
if(!node || !node -> next)
{
return node;
}
ListNode *fast = node;
ListNode *slow = node;
ListNode *brek = node;
while(fast && fast -> next)
{
fast = fast -> next -> next;
brek = slow;
slow = slow -> next;
}
brek -> next = nullptr;
ListNode *l1 = mergesort(node);
ListNode *l2 = mergesort(slow);
return merge(l1,l2);
}
ListNode* merge(ListNode* l1,ListNode* l2)
{
if(l1 == NULL)
{
return l2;
}
if(l2 == NULL)
{
return l1;
}
if(l1-> val < l2->val)
{
l1->next=merge(l1->next,l2);
return l1;
}
else
{
l2 -> next = merge(l2 -> next,l1);
return l2;
}
}
};
328. 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 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
说明:
• 应当保持奇数节点和偶数节点的相对顺序。
• 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
思路一:奇偶链表
解法
想法
将奇节点放在一个链表里,偶链表放在另一个链表里。然后把偶链表接在奇链表的尾部。
算法
一个LinkedList 需要一个头指针和一个尾指针来支持双端操作。我们用变量 head 和 odd 保存奇链表的头和尾指针。 evenHead 和 even 保存偶链表的头和尾指针。算法会遍历原链表一次并把奇节点放到奇链表里去、偶节点放到偶链表里去。遍历整个链表我们至少需要一个指针作为迭代器。这里 odd 指针和 even 指针不仅仅是尾指针,也可以扮演原链表迭代器的角色。
解决链表问题最好的办法是在脑中或者纸上把链表画出来。比方说:
图片 1. 奇偶链表的例子
复杂度分析
• 时间复杂度: O(n)。总共有 n个节点,我们每个遍历一次。
• 空间复杂度: O(1)。我们只需要 4 个指针。
作者:LeetCode
链接:https://leetcode-cn.com/problems/odd-even-linked-list/solution/qi-ou-lian-biao-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我的:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* oddEvenList(ListNode* head)
{
if(head == NULL)
{
return NULL;
}
ListNode *odd = head;
ListNode *even = head -> next;
ListNode *evenHead = even;
while(even != NULL && even -> next != NULL)
{
odd -> next = even -> next;
odd = odd -> next;
even -> next = odd -> next;
even = even -> next;
}
odd -> next = evenHead;
return head;
}
};