各種類型排序的實現及比較


插入排序法:

簡單介紹:插入排序在排序過程中會把整個數組分成已排好序和還未排序兩部分。每次從未排序部分的開頭取出一個數字,插入到已排序的部分。

性質:是穩定的排序法。且最壞的情況下一共要移動(1+2+...+N-1)=(N^2-N)/2次,所以基本是O(n^2)復雜度的排序法,當然輸入數據的順序可以極大的影響該排序算法的復雜度。例如數據本來就是升序排列,所有數據都不用移動,此時只需判斷n次就可以結束算法運行。

參考代碼:

void insert_sort(int *a,int n) {
    for (int i = 1; i < n;i++) {
        int v = a[i],j=i-1;
        while (j>=0&&a[j]>v) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = v;
    }
}

冒泡排序法:

簡單介紹:排序過程中也會把數組分成排好序和未排序兩部分。每次從數組末尾開始依次比較相鄰兩個元素的大小,若逆序,則交換兩者順序。

性質:也是穩定的排序。最壞的情況下,也需要進行(N-1+N-2+...+1)=(N^2-N)/2次排序,所以屬於O(n^2)復雜度算法。在冒泡排序中,每次兩個數交換順序,就會減少一個逆序對,所以冒泡法的排序次數就是該數列逆序對的個數。具體實現該算法時加入了一個flag,用於判斷上一次循環時是否還有逆序對,若沒有,說明已經排好序了。不加flag的話不管輸入數據怎么變化,冒泡排序都需要(N^2-N)/2次排序。

參考代碼:

void bubble_sort() {
    bool flag = 1;
    for (int i = 0; flag;i++) {
        flag = 0;//用於判斷是否還有逆序對,有交換說明還有逆序對,若沒有逆序對,直接出循環
        for (int j = n - 1; j >= i + 1;j--) {
            if(a[j]<a[j-1])
            swap(a[j], a[j - 1]),flag = 1;
        }
    }
}

選擇排序法:

簡單介紹:排序過程中是分已排序和未排序兩部分。每次從未派好的元素當中挑一個最小的與未排好數據部分的首位進行交換。

性質:不穩定的排序,因為選擇排序可能會交換兩個位置不相鄰的元素。不管輸入數據怎么變化,利用選擇排序都需要(N^2-N)/2次操作。

參考代碼:

void select_sort() {
    for (int i = 0; i < n; i++) {
        int min_num = INF, k;
        for (int j = i; j < n; j++) {
            if (min_num > a[j]) min_num = a[j], k = j;
        }
        swap(a[i], a[k]);
    }
}

希爾排序法:

簡單介紹:希爾排序法充分發揮了插入排序可以較快的處理有順序數據的特點。設置一個間隔數組G={1,4,13,...,k},k<n,每次按照間隔進行插入排序。

性質:間隔數組取得好,一般復雜度維持在O(n^1.25)。

參考代碼:

vector<int>G;
void insert_sort(int *a,int n,int g) {//其中g代表間隔
    for (int i = g; i < n;i++) {
        int j = i - g,v=a[i];
        while (j>=0&&a[j]>v) {
            a[j + g] = a[j];
            j -= g;
        }
        a[j + g] = v;
    }
}
void shell_sort() {
    int g = 1;
    while(g<n) {
        G.push_back(g);
        g = 3 * g + 1;
    }
    for (int i = G.size() - 1; i >= 0; i--) {
        insert_sort(a, n, G[i]);
    }
}

歸並排序法:

簡單介紹:分治思想,要處理長度為n的數列,先將數列划分左右兩部分,每部分n/2個元素,每部分分別排好序后,線性時間內就可以完成n個元素的排列。

性質:歸並排序是穩定排序。n個數據會大致分成log(n)層,每層執行的時間復雜度為n,故整體nlog(n)復雜度。復雜度遞推式為T(n)=2T(n/2)+n,計算復雜度也可以利用主定理。不過歸並排序需要額外的使用O(n)的空間。

參考代碼:

void merge_sort(int l,int r) {//[l,r)
    if (r - l <= 1)return;
    int mid = (l + r) >> 1;
    merge_sort(l, mid);
    merge_sort(mid, r);
    int p = l, q = mid,i=l;
    while (p<mid||q<r) {
        if (q>=r||p<mid&&a[p]<a[q])t[i++] = a[p++];
        else  t[i++] = a[q++];
    }
    for (int i = l; i < r;i++) {
        a[i] = t[i];
    }
}

快速排序法:

簡單介紹:分治的思想。對於n個元素的數列,先找到一個基准e,比e大的元素排在e右邊,比e小的元素排在e左邊,接下來以e為分割點,遞歸分治[L,e),[e,R)部分的數列。

性質:由於分割會交換不相鄰的元素,故快排屬於不穩定排序。並且不需要額外的空間,是一種原地算法。基准的選擇是算法快慢的關鍵,若基准每次都能均勻的分出兩部分子列,復雜度為nlog(n),如果極端的情況,仍然可能會有O(n^2)復雜度。

參考代碼:

int partition(int L,int R) {//用基准對區間[L,R]進行划分
    int i = L - 1,e=a[R];
    for (int j = L; j < R;j++) {
        if (a[j] < e) {
            i++; swap(a[i], a[j]);
        }
    }
    swap(a[i + 1], a[R]);
    return i + 1;
}
void quick_sort(int l, int r) {//[l,r]
    if (l >= r)return;
        int e = partition(l, r);
        quick_sort(l, e - 1);
        quick_sort(e + 1, r);
}

計數排序法:

簡單介紹:開辟一個數組C,將所有出現的元素以下標的形式記錄於C中,譬如有一個元素x,則C[x]++。之后求C中各元素的累計和,這樣某一個C[k]=p,即代表小於等於k的元素還有p個,這樣k就是第p小的元素,直接就能知道其所在位置了。

性質:穩定的排序。復雜度為O(n+k),其中k代表所有元素中最大的那個元素值。

參考代碼:

int c[N_MAX], b[N_MAX];
void count_sort() {
    for (int i = 0; i < n; i++)c[a[i]]++;
    for (int i = 1; i < N_MAX; i++)c[i] = c[i] + c[i - 1];
    for (int i = 0; i<n; i++) {
        b[--c[a[i]]] = a[i];
    }
}

 

堆排序法:

首先需要介紹一下堆:

堆首先是完全二叉樹,即最下面一層節點全部集中在該層的左邊若干位置,其余層的節點數全滿。其次,最大堆的性質在於堆中任何節點的鍵值小於其父節點的鍵值,故根節點是最大堆中的鍵值最大的節點。依據堆的這些性質,可以用一個下標從1開始的數組來存儲堆。

接下來分三個塊內容:堆的構造,堆的元素插入以及堆的元素拋出。

1:堆的構造

對於一個n元素的數組,如何將其調整為堆:從堆中最后一個有兒子節點的節點(即編號為n/2的節點)開始進行調整,一直調整到根節點為止,每次調整一個節點后,以該節點為根的子樹就是一個堆了。故可以知道,每次調整一個節點,它的左右兒子處已經滿足堆的定義了。現在要將當前節點的鍵值設置為左右兒子以及它本身鍵值中的最大值,譬如原本左兒子鍵值最大,就將左兒子的鍵值與當前節點的鍵值進行交換,再遞歸的調整左兒子所在的樹。若將無序數組調整為堆,一共得調整n/2次,總復雜為O(n)。復雜度簡單說明:假如是完美二叉樹,一共n個節點,那么我們首先要對高度為1的n/2個子樹進行調整操作,再對高度為2的n/4個子樹進行調整。。所以整體要進行的操作是:n*Σ1<=k<=logn(k/2^k)=O(n)

參考代碼:

void heap_adjust(int *a,int i) {//調整以節點i為根節點的子樹
    int left = (i << 1), right = (i << 1) + 1,largest;
    if (left<=n&&a[left]>a[i])largest = left;
    else largest = i;
    if (right<=n&&a[right]>a[largest])largest = right;
    if (largest != i) {
        swap(a[largest], a[i]);
        heap_adjust(a,largest);
    }
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)cin >> a[i];
    for (int i = n / 2; i >= 1; i--)heap_adjust(a, i);//從最后一個有兒子節點的節點開始調整
    for (int i = 1; i <= n; i++)cout << a[i] << " ";
    return 0;
}

2:堆的元素插入:

有元素插入,先將節點數n++,元素插入到數組的最后一個位置,然后查看插入節點的鍵值是否大於其父節點的值,若大於,則和父節點交換鍵值,再遞歸調整其父節點的值。log(n)復雜度

參考代碼:

void insert(int key) {
    n++;//堆中元素個數加1
    a[n] = key;
    int tmp_n = n;
    while (tmp_n>1&&a[tmp_n]>a[tmp_n/2]) {
        swap(a[tmp_n], a[tmp_n / 2]);
        tmp_n /= 2;
    }
}

3:堆頂元素拋出:

堆頂元素拋出以后,把末尾元素放置到堆頂,然后進行一次head_adjust堆調整即可。log(n)復雜度

參考代碼:

int pop() {
    if (n < 1)return -1;//堆中沒有元素
    int maxv = a[1];
    swap(a[1], a[n--]);//為了與堆排序進行銜接,這里把堆頂元素與堆最后一個元素交換位置
    heap_adjust(a, 1);
    return maxv;
}

對於堆的基本操作都以給出,堆排序也就能簡單實現了。首先對於n個無序的數字組成的數組,調整為堆,這個過程O(n)復雜度。之后不斷的拋出堆頂元素,並將堆頂元素與堆尾元素進行交換,迭代n次即可。直接利用上述的pop()操作。

參考代碼:

int N;//N用於記錄數組元素個數
void heap_sort() {
    for (int i = n / 2; i >= 1; i--)heap_adjust(a,i);
    int t = n;
    N = n;
    while(t--){
        pop();
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM