banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

數據結構學習筆記<8> 排序

一、拓撲排序#

1. 概念定義#

AOV 網絡#

例如,假定一個計算機專業的學生必須完成圖 3-4 所列出的全部課程。從圖中可以清楚地看出各課程之間的先修和後續的關係。如課程 C5 的先修課為 C2,後續課程為 C4 和 C6。通常,我們把這種頂點表示活動、邊表示活動間先後關係的有向圖稱做頂點活動網 (Activity On Vertex network),簡稱 AOV 網。
以計算機

拓撲序、DAG#

  • 若圖中從 V 到 W 有一條有向路徑,則 V 一定排在 W 之前。滿足該條件的頂點序列稱為一個拓撲序
  • 獲得一個拓撲序的過程就是拓撲排序
  • AOV 若有合理的拓撲序,則必定是有向無環圖(Directed Acyclic Graph,DAG

2. 拓撲排序思路#

拓撲排序的思路是每次都找一個入度為 0 的頂點並輸出,並且將該頂點所有鄰接點入度減 1。
可以看出,找入度為 0 的頂點是關鍵,若每次都要遍歷那必定會耗費大量時間空間,所以更聰明的算法是,隨時將入度變為 0 的頂點放入一個容器中。
伪碼描述如下

void TopSort() {
	int cnt = 0;
    for(圖中的每個頂點V) 
		if( Indegree[W] == 0)
			Enqueue(V,Q);
	while(!isEmpty(Q)) {
		V = Dequeue(Q);
		輸出V,或記錄V的輸出序號,cnt++;
        for(V的每個鄰接點W)
        	if(--Indegree[W] == 0)
	            Enqueue(V,Q);
	}
	if(cnt != |V|)
		Error("圖中有回路");
}

模板代碼:

const int maxn = 1005;
int N,M;//頂點數、邊數(活動數)
int edge[maxn][maxn];
int mint[maxn];//到每個活動檢查點的最短時間
int In[maxn];//每個活動檢查點的入度
void init() {
    memset(edge, -1, sizeof(edge));
    memset(mint, 0, sizeof(mint));
    memset(In, 0, sizeof(In));
}
bool Topsort() {//拓撲排序
    queue<int> q;
    for(int i = 0; i < N; ++i) {
        if(In[i] == 0) 
            q.push(i);
    }
    int cnt = 0;
    while(!q.empty()) {
        int v = q.front();
        q.pop();
        cnt++;
        for(int i = 0; i < N; ++i) {
            if(v == i || edge[v][i] == -1) continue;//檢查以v為起點的所有邊
            In[i]--;
            //其他操作
            if(In[i] == 0) q.push(i);
        }
    }
    if(cnt != N) return false;
    else return true;
}

例題#

08 - 圖 8 How Long Does It Take (25 分)
是一道拓撲排序的變形,程序不算複雜,建議嘗試;
題意、代碼及思路指路博客:

3. 解決實際問題#

關鍵路徑問題#

AOE 網絡 (Activity On Edge) 網絡#

  • 一般用於安排項目的工序
  • 在 AOE 網絡中,活動是表示在邊上的,頂點被分為三個部分:頂點編號、最早完成時間和最晚完成時間
    https://www.icourse163.org/learn/ZJU-93001?tid=1459700443#/learn/content?type=detail&id=1235254066&cid=1254945243&replay=true
先推出最早完成時間 —— mint [j] = max ( mint [ j ], mint [ i ]+edge [ i ][ j ])#

在這裡插入圖片描述

再由後往前推出最晚完工時間 —— maxt [i] = min ( maxt [ j ], maxt [ j ]-edge [ i ][ j ])#
即可得機動時間 —— D [i][ j ] = maxt [ j ] - mint [ i ] - edge [ i ][ j ]#

在這裡插入圖片描述
而關鍵路徑,是由絕對不允許延誤的活動組成的路徑,即沒有機動時間的路徑。

例題#

08 - 圖 8 How Long Does It Take (25 分)
是一道拓撲排序的變形,求最早完成時間
08 - 圖 9 關鍵活動 (30 分)
求關鍵路徑
代碼及思路指路博客:PTA 數據結構題目集 第八周 —— 圖(下)

二、簡單排序#

1. 前提#

void X_Sort(ElementType A[], int N);
  • 為簡單起見,討論整數的從小到大排序
  • N 為整數
  • 只討論基於比較的排序(> = < 有定義)
  • 只討論內部排序
  • 穩定性: 任意兩個相等的數據排序前後的相對位置不發生改變
  • 沒有哪一種排序是任何情況下都表現最好的!

2. 排序算法#

測試題目:09 - 排序 1 排序 (25 分)

冒泡排序#

它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果順序(如從大到小、首字母從 Z 到 A)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素列已經排序完成。
這個算法的名字由來是因為越小的元素會經由交換慢慢 “浮” 到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端一樣,故名 “冒泡排序”。

——摘自百度百科

void Bubble_Sort(ll a[], int N) {
    for(int P = N-1; P >= 0; P--) {
        bool flag = false;
        //一趟冒泡 從上往下比較,上邊大於下邊則交換
        for(int i = 0; i < P; ++i) {    
            if(a[i] > a[i+1]) {
                swap(a[i], a[i+1]);
                flag = true;
            }
        }
        if(!flag) break; //一趟下來已經有序了,未發生交換
    }
}

時間複雜度#

最好情況:順序,時間複雜度 T = O (N)
最壞情況:整個逆序,時間複雜度 T = O (N^2^)

優缺點#

優點:簡單易寫,只需交換相鄰元素,即使是單向鏈表也可直接排序,穩定(交換前後相等元素的位置不變)
缺點:時間複雜度較大,慢!

測試結果#

測試結果如下,有 3 個樣例沒過
在這裡插入圖片描述

插入排序#

插入排序,一般也被稱為直接插入排序。對於少量元素的排序,它是一個有效的算法。插入排序是一種最簡單的排序方法,它的基本思想是將一個記錄插入到已經排好序的有序表中,從而一個新的、記錄數增 1 的有序表。在其實現過程使用雙層循環,外層循環對除了第一個元素之外的所有元素,內層循環對當前元素前面有序表進行待插入位置查找,並進行移動。

——摘自百度百科

void Insertion_Sort(ll a[], int N) {
    for(int P = 1; P < N; P++) {
        ll t = a[P];//摸下一張牌
        int i;
        for(i = P; i > 0 && a[i-1] > t; --i) 
            a[i] = a[i-1];  //移出空位 直到前面那個這個元素小於當前元素
        a[i] = t;   //新牌落位
    }
}

時間複雜度#

最好情況:順序,時間複雜度 T = O (N)
最壞情況:整個逆序,時間複雜度 T = O (N^2^)
一般情況下時間複雜度下界計算:
交換兩個相鄰元素正好消去 1 個逆序對
設有 I 個逆序對
則 T (N,I) = O (N+I)

優缺點#

優點:穩定
缺點:比較次數不一定,比較次數越少,插入點後的數據移動越多,尤其是當數據總量龐大的時候

測試結果#

測試結果如下,挺給力的
在這裡插入圖片描述

如何提高效率#

有定理如下

  • 任意 N 個不同元素組成的序列平均具有 N (N-1)/4 個逆序對
  • 任何僅以交換相鄰兩元素來排序的算法,其平均時間複雜度為 O (N^2^)

所以要提高算法效率,我們必須

  • 每次消去不止 1 個逆序對!
  • 每次交換相隔較遠的 2 個元素!

希爾排序#

利用了插入排序的簡單,克服插入排序只能交換相鄰兩元素的缺點。

希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至 1 時,整個文件恰被分成一組,算法便終止。

——摘自百度百科

定義增量序列 D~M~ >D~M-1~>…>D~1~ = 1
對每個 D~k~ 進行 “D~k~ 間隔” 排序(k=M,M-1,……,1)
注意:“D~k~ 間隔” 有序的序列,在執行 “D~k-1~ 間隔” 排序後,仍然是 “D~k~ 間隔” 有序的!

希爾增量序列選取#

  • 原始希爾排序增量序列 D~M~ = N/2, D~k~ = D~k+1~ / 2
    • 增量元素不互質,則小增量可能根本不起作用!
      在這裡插入圖片描述
void Shell_Sort(ll a[], int N) {
    for(int D = N/2; D > 0; D /= 2) { //希爾增量序列
        for(int P = D; P < N; ++P) { //插入排序
            ll t = a[P];
            int i;
            for(i = P; i >= D && a[i-D] > t; i -= D) 
                a[i] = a[i-D];
            a[i] = t;
        }
    }
}
  • Hibbard 增量序列
    • D~k~ = 2^k^-1 —— 相鄰元素互質
  • Sedgewick 增量序列等

優缺點#

優點:快,數據移動少!適用於數據量較大的情況
缺點:不同的增量序列選取會導致算法複雜度差異,如何選取增量序列只能根據經驗,不穩定

測試結果#

可以看到耗時都沒超過 100ms,在這些測試樣例里的速度還是很理想的
在這裡插入圖片描述

選擇排序#

在介紹堆排序前,先介紹選擇排序,老朋友了

void Selection_Sort(ll a[], int N) {
    for(int i = 0; i < N; ++i) {
        int mini = 0;
        ll ans = inf;
        //找i後邊的最小元 並將其位置賦給mini
        for(int j = i; j <= N-1; ++j) {
            if(a[j] < ans) {
                ans = a[j];
                mini = j;
            }
        }
        //將未排序部分的最小元換到有序部分的最後位置
        swap(a[i], a[mini]);
    }
}

時間複雜度#

無論如何複雜度都為 O (N^2^)

測試結果#

測試結果如下,雖然都能過,但後幾個樣例耗時都很大
在這裡插入圖片描述

堆排序#

這裡以排成升序為例,我們需要將其原始數組調整成下標從 0 開始的最大堆,再將最大堆頂與當前最後的元素交換(相當於刪除最大堆頂)後調整

void swap(ll& x, ll& y) {
    ll t = x;
    x = y;
    y = t;
}
void PercDown(ll a[], int N, int rt) {
    //將N個元素的數組中以a[now]為根的子堆調整為最大堆
    int father, son;
    ll tmp = a[rt];
    for(father = rt; (father*2+1) < N; father = son) {
        son = father * 2 + 1;//左兒子
        if(son != N-1 && a[son] < a[son+1]) //右兒子存在且比左兒子大
            son++;
        if(tmp >= a[son]) break;//找到該放的地方
        else a[father] = a[son];//下濾
    }
    a[father] = tmp;
}
inline void BuildHeap(ll a[], int N) {
    for(int i = N/2-1; i >= 0; --i) {
        PercDown(a, N, i);
    }
}
void Heap_Sort(ll a[], int N) {
    BuildHeap(a, N);
    for(int i = N-1; i > 0; --i) {
        swap(a[0], a[i]);//最大堆頂a[0]與a[i]交換
        PercDown(a, i, 0);//刪除後進行調整
    }
}

時間複雜度#

堆排序給出了最佳的平均時間複雜度
最好情況 O (nlogn)
最壞情況 O (nlogn)
平均時間複雜度 O (nlogn)

優缺點#

優點:快!即使是最壞情況下性能也很優越,使用的輔助空間少
缺點:不穩定,不適合對象的排序。

測試結果#

測試結果如下,好像比希爾排序給力些哦~
在這裡插入圖片描述

归并排序#

核心是有序子列的合并,這裡給出遞歸實現的版本~非遞歸實現看這裡哦归并排序循環實現

void Merge(ll a[], int s, int m, int e, ll tmp[]) {
    //將數組a的局部a[s,m]和a[m+1,e]合併到數組tmp,並保證tmp有序
    //然後再拷貝回a[s,m]  時間複雜度O(e-m+1),即O(n);
    int pb = s;//pb為tmp數組的下標
    int p1 = s, p2 = m+1;//p1指向前一半p2指向後一半
    while (p1 <= m && p2 <= e) {
        if (a[p1] < a[p2])
            tmp[pb++] = a[p1++];
        else 
            tmp[pb++] = a[p2++];
    }
    while(p1 <= m) 
        tmp[pb++] = a[p1++];
    while(p2 <= e) 
        tmp[pb++] = a[p2++];
    for (int i = 0; i < e-s+1; ++i)
        a[s+i] = tmp[i];
}
void MergeSort(ll a[], int s, int e, ll tmp[]) {
    if (s < e) {//若s>=e則不做任何事情
        int m = s + (e-s)/2;
        MergeSort(a, s, m, tmp);//前一半排序
        MergeSort(a, m+1, e, tmp);//后一半排序
        Merge(a, s, m, e, tmp);//合併 將a中s到m和m+1到e的兩個數組有序的合併
    }
}

時間複雜度#

最好情況 O (nlogn)
最壞情況 O (nlogn)
平均時間複雜度 O (nlogn)

優缺點#

優點:穩定、快
缺點:較佔用空間

測試結果#

測試結果如下,你品,你細品
在這裡插入圖片描述

快速排序#

1. 設 k = a [0], 將 k 挪到適當位置,使得比 k 小的元素都在 k 左邊,比 k 大的元素都在 k 右邊 (在 O (n) 時間完成)
2. 把 k 左邊的部分快速排序
3. 把 k 右邊的部分快速排序
k 為主元

void QuickSort(ll a[], int s, int e){//將a[s,e]快排
    if (s >= e)
        return;
    int k = a[s];
    int i = s,j = e;
    while (i != j) {
        while (j > i && a[j] >= k)  --j;
        swap(a[i],a[j]);
        while (i < j && a[i] <= k)  ++i;
        swap(a[i],a[j]);
    }//處理完後a[i] = k;
    QuickSort(a, s, i-1);//快排左邊部分
    QuickSort(a, i+1, e);//快排右邊部分
}

時間複雜度#

最好情況每次正好中分 O (nlogn)
最壞情況 O (N^2^)
平均時間複雜度 O (nlogn)

優缺點#

優點:是所有內部排序的最快的算法
缺點:不穩定,最壞情況下效率較慢!

測試結果#

測試結果如下
在這裡插入圖片描述

表排序#

當數據量大且待排序的元素為對象、移動所需時間特別高時,我們需要間接排序
定義一個指針數組作為 “表”(table),記錄待排元素
在這裡插入圖片描述

時間複雜度#

最好情況初始即有序
最壞情況有 N/2 個環,每個環包含 2 個元素,交換兩個元素需要走三步,需要 3N/2 次元素移動
T = O (m N),m 是每個 A 元素的複製時間

基數排序(桶排序的推廣)#

之前講的算法都需要比較,最壞情況下也都有 Nlogn,還能更快嗎?
假設我們有 N 個學生,他們的成績是 0~100 之間的整數(於是有 M = 101 個不同的成績值),如何在線性時間內將學生按成績排序
LSD 主位優先 MSD 次位優先在這裡插入圖片描述
在這裡插入圖片描述

基數排序的代碼我參考了這篇博客的,用的方法非常巧妙基數排序

ll getMax(ll a[], int n) {//找n個元素的a數組中最大數
    int maxx = a[0];
    for(int i = 1; i < n; ++i) {
        if(a[i] > maxx) maxx = a[i];
    }
    return maxx;
}
void radixsort(ll a[], int n, int exp) { //對n個元素的數組a按照"某位數"進行排序(桶排序),基數為10
    ll tmp[maxn];
    ll T[20] = {0}; //有負數的十進制 二十個桶
    for(int i = 0; i < n; ++i) //T存儲該桶里有多少個數
        T[(a[i]/exp)%10 + 10]++;
    for(int i = 1; i < 20; ++i) //讓T的值是在tmp中的位置
        T[i] += T[i-1];
    for(int i = n - 1; i >= 0; --i) {
        int now = T[(a[i]/exp)%10 + 10];//當前這個數所應在的位置
        tmp[now-1] = a[i];
        T[(a[i]/exp)%10 + 10]--;
    }
    for(int i = 0; i < n; ++i)
        a[i] = tmp[i]; //將排好序的tmp賦給a
}
void Radix_Sort(ll a[], int n) {
    ll maxnum = getMax(a, n);
    for(int exp = 1; maxnum/exp > 0; exp *= 10) 
        radixsort(a, n, exp);
}

時間複雜度#

N 為待排序元素個數,而 B 是桶數
O (P (N+B)) 一趟分配時間為 O (N),一趟收集時間複雜度為 O (B),共進行 P 趟分配和收集

優缺點#

優點:適用於位數不多,待排序列最大位數不是特別大的情況,快
缺點:空間換時間

測試結果#

測試結果如下,超快的說~
在這裡插入圖片描述

三、排序算法的比較#

在這裡插入圖片描述

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。