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;
}
};