1. 程式人生 > >連結串列之排序(插入、選擇、歸併、快速、冒泡)

連結串列之排序(插入、選擇、歸併、快速、冒泡)

已知連結串列節點結構如下:

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為希爾排序的當前步長,這種操作不適合連結串列。

對於堆排序,一般是用陣列來實現二叉堆,當然可以用二叉樹來實現,但是這麼做太麻煩,還得花費額外的空間構建二叉樹