連結串列之排序(插入、選擇、歸併、快速、冒泡)
已知連結串列節點結構如下:
struct ListNode {
int val; //數值
ListNode *next; //後繼指標
ListNode(int x) : val(x), next(NULL) {}
};
插入排序演算法交換節點,思想是先構造一個fakeHead讓其指向head,以便於接下來搜尋待插入節點應該插入的地方。cur指標指向待插入節點,pre為cur指標的前驅,next為cur指標的後繼。時間複雜度為O(N^2),空間複雜度為O(1)。更詳細的演算法思想,見程式碼註釋:
class Solution { public: ListNode* insertionSortList(ListNode* head) { if(head==NULL||head->next==NULL) return head; //cur初始化為指向第二個元素,pre初始化為指向head, next初始化為NULL ListNode* fakeHead=new ListNode(0), *p, *cur=head->next, *next=NULL, *pre=head; fakeHead->next=head; //使fakeHead指向head,以便於接下來搜尋待插入節點應該插入的位置 while (cur) { next=cur->next; //儲存cur的後繼next以便於接下來的操作 if (cur->val>=pre->val) { //如果cur節點的數值>=pre節點的數值,則說明cur節點,不需要移動。 pre=cur; //更新pre指標 } else { pre->next=next; //移動cur節點之前,讓pre和next連線起來 p = fakeHead; while (p->next->val<cur->val) { //搜尋cur節點插入的位置,cur應該插入p節點之後 p = p->next; } cur->next=p->next; //連線cur和p指標後面的節點 p->next=cur; //連線p和cur指標 } cur=next; //更新cur指標 } p = fakeHead->next; delete fakeHead; //delete fakeHead return p; } };
選擇排序演算法交換節點的值,演算法複雜度為O(N^2),空間複雜度為O(1),思想是先構造一個偽頭結點,讓其指向head,以便於操作。另外,用sortedTail指標指向有序部分的末尾,剩下的工作就是在無序部分找數值最小的節點minNode,若minNode不等於sortedTail->next,則交換sortedTail->next節點和minNode節點的數值。更詳細的思想,見程式碼註釋:
ListNode* selectSortList(ListNode *head) { if(head==NULL) return head; ListNode* fakeHead = new ListNode(0); fakeHead->next = head; //為了操作方便,新增偽頭節點 ListNode* sortedTail = fakeHead; //sortedTail指向已排序部分的尾部,注意在連結串列中的這種使用方法 while(sortedTail->next != NULL) { //minNode記錄最小節點 ListNode* minNode = sortedTail->next, *p = sortedTail->next->next; //尋找未排序部分的最小節點 while(p != NULL) { if(p->val < minNode->val) minNode = p; p = p->next; } if(minNode != sortedTail->next) swap(minNode->val, sortedTail->next->val); //交換節點數值 sortedTail = sortedTail->next; } head = fakeHead->next; delete fakeHead; return head; }
歸併排序演算法交換連結串列節點,時間複雜度為O(NlogN),不考慮遞迴棧空間的話空間複雜度是O(1)) ,演算法思想是首先用快慢指標的方法找到連結串列中間節點,然後遞迴地對兩個子連結串列進行排序,把兩個排好序的子連結串列合併成一條有序的連結串列。歸併排序算是連結串列排序中的最好選擇,保證了最好和最壞時間複雜度都是NlogN,而且它在陣列排序中廣受詬病的空間複雜度在連結串列排序中也從O(N)降到了O(1)。
ListNode* merge(ListNode* left, ListNode* right) { if(left==NULL) { //如果一個為空,則直接返回另外一個 return right; } else if (right==NULL) { return left; } ListNode* res, *tmpPos; if (left->val<right->val) { //確定頭指標 res=left; left=left->next; } else { res = right; right=right->next; } tmpPos=res; while (left!=NULL&&right!=NULL) { if(left->val<right->val) { tmpPos->next=left; left=left->next; } else { tmpPos->next=right; right=right->next; } tmpPos=tmpPos->next; } if (left!=NULL) { tmpPos->next=left; } if (right!=NULL) { tmpPos->next=right; } return res; //返回已經合併好的連結串列的頭指標 } ListNode* mergeSortList(ListNode *head) { if (head==NULL||head->next==NULL) { //遞迴終止條件 return head; } ListNode* fast=head, *slow=head; //通過快慢指標,尋找連結串列中點 while (fast->next!=NULL&&fast->next->next!=NULL) { fast=fast->next->next; slow=slow->next; } fast=slow->next; //fast為右半部分連結串列的起始 slow->next=NULL; //slow為為左半部分的結尾 slow=mergeSortList(head); //對左半部分排序 fast=mergeSortList(fast); //對右半部分排序 return merge(slow, fast); //合併左右部分 }
快速排序1(演算法只交換節點的val值,平均時間複雜度O(nlogn),不考慮遞迴棧空間的話空間複雜度是O(1))
這裡的partition我們參考陣列快排partition的第二種寫法(選取第一個元素作為樞紐元的版本,因為連結串列選擇最後一元素需要遍歷一遍),具體可以參考here。這裡我們還需要注意的一點是陣列的partition兩個引數分別代表陣列的起始位置,兩邊都是閉區間,這樣在排序的主函式中:
void quicksort(vector<int>&arr, int low, int high)
{
if(low < high)
{
int middle = mypartition(arr, low, high);
quicksort(arr, low, middle-1);
quicksort(arr, middle+1, high);
}
}
對左邊子陣列排序時,子陣列右邊界是middle-1,如果連結串列也按這種兩邊都是閉區間的話,找到分割後樞紐元middle,找到middle-1還得再次遍歷陣列,因此連結串列的partition採用前閉後開的區間(這樣排序主函式也需要前閉後開區間),這樣就可以避免上述問題。
ListNode* partition(ListNode* low, ListNode* high) {
int key = low->val;
ListNode* loc = low; //loc為小於key節點序列的最後一個節點
for (ListNode* i=low->next; i!=high; i=i->next) {
if (i->val<key) {
loc=loc->next;
swap(i->val, loc->val);
}
}
swap(loc->val, low->val);
return loc; //loc就是樞紐節點
}
void qSortList(ListNode* head, ListNode* tail) {
if(head!=tail&&head->next!=tail) { //如果子連結串列中至少有兩個元素
ListNode* mid = partition(head, tail); //得到樞紐節點
qSortList(head, mid);
qSortList(mid->next, tail);
}
}
ListNode* quickSortList(ListNode* head) {
if (head==NULL||head->next==NULL) {
return head;
}
qSortList(head, NULL);
return head;
}
快速排序2(演算法交換連結串列節點,平均時間複雜度O(nlogn),不考慮遞迴棧空間的話空間複雜度是O(1))
這裡的partition,我們選取第一個節點作為樞紐元,然後把小於樞紐的節點放到一個鏈中,把不小於樞紐的及節點放到另一個鏈中,最後把兩條鏈以及樞紐連線成一條鏈。
這裡我們需要注意的是,1.在對一條子鏈進行partition時,由於節點的順序都打亂了,所以得保正重新組合成一條新連結串列時,要和該子連結串列的前後部分連線起來,因此我們的partition傳入三個引數,除了子連結串列的範圍(也是前閉後開區間),還要傳入子連結串列頭結點的前驅;2.partition後連結串列的頭結點可能已經改變
ListNode* partition(ListNode* lowPre, ListNode* low, ListNode* high) {
int key = low->val;
ListNode node0(0), node1(0);
ListNode* little=&node0, *big=&node1;
for (ListNode* i=low->next; i!=high; i=i->next) {
if (i->val<key) {
little->next=i;
little=little->next;
} else {
big->next=i;
big=big->next;
}
}
big->next=high; //保證子連結串列[low, high)和後面的部分連線
little->next=low;
low->next=node1.next;
lowPre->next=node0.next; //保證子連結串列[low, high)和前面的部分連線
return low;
}
void qSortList(ListNode* headPre, ListNode* head, ListNode* tail) {
if(head!=tail&&head->next!=tail) { //如果子連結串列中至少有兩個元素
ListNode* mid = partition(headPre, head, tail); //注意這裡的head可能不再指向表頭了
qSortList(headPre, headPre->next, mid);
qSortList(mid, mid->next, tail);
}
}
ListNode* quickSortList(ListNode* head) {
if(head==NULL||head->next==NULL)
return head;
ListNode* headPre = new ListNode(0);
headPre->next=head;
qSortList(headPre, head, NULL);
return headPre->next;
}
氣泡排序(演算法交換連結串列節點val值,時間複雜度O(n^2),空間複雜度O(1))
class Solution {
public:
ListNode *bubbleSortList(ListNode *head) {
// IMPORTANT: Please reset any member data you declared, as
// the same Solution instance will be reused for each test case.
//連結串列快速排序
if(head == NULL || head->next == NULL)return head;
ListNode *p = NULL;
bool isChange = true;
while(p != head->next && isChange)
{
ListNode *q = head;
isChange = false;//標誌當前這一輪中又沒有發生元素交換,如果沒有則表示陣列已經有序
for(; q->next && q->next != p; q = q->next)
{
if(q->val > q->next->val)
{
swap(q->val, q->next->val);
isChange = true;
}
}
p = q;
}
return head;
}
};
對於希爾排序,因為排序過程中經常涉及到arr[i+increment]操作,其中increment為希爾排序的當前步長,這種操作不適合連結串列。
對於堆排序,一般是用陣列來實現二叉堆,當然可以用二叉樹來實現,但是這麼做太麻煩,還得花費額外的空間構建二叉樹