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 どのくらい時間がかかりますか (25 点)
はトポロジカルソートの変形で、プログラムはそれほど複雑ではないので、挑戦することをお勧めします;
問題の意図、コード、考え方についてはブログを参照してください:

3. 実際の問題の解決#

关键路径问题#

AOE ネットワーク(Activity On Edge)ネットワーク#

  • 一般的にプロジェクトの工程を計画するために使用されます
  • AOE ネットワークでは、活動は辺上に表示され、頂点は 3 つの部分に分けられます:頂点番号、最早完了時間、最遅完了時間
    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 どのくらい時間がかかりますか (25 点)
はトポロジカルソートの変形で、最早完了時間を求めます。
08 - 図 9 关键活动 (30 点)
关键路径を求めます。
コードと考え方についてはブログを参照してください:PTA データ構造問題集 第 8 週 —— グラフ(下)

二、簡単なソート#

1. 前提#

void X_Sort(ElementType A[], int N);
  • 簡単のため、整数の昇順ソートについて議論します。
  • N はの整数です。
  • 比較に基づくソート(> = < が定義されています)についてのみ議論します。
  • 内部ソートについてのみ議論します。
  • 安定性: 任意の 2 つの等しいデータのソート前後の相対位置は変わりません。
  • どのソートも、どの状況でも最も良いパフォーマンスを示すわけではありません!

2. ソートアルゴリズム#

テスト問題:09 - ソート 1 ソート (25 点)

バブルソート#

それは、ソートする要素の列を繰り返し訪問し、隣接する 2 つの要素を比較し、順序(例えば、大から小、アルファベット順で 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 つのレコードを既にソートされた順序表に挿入することによって、新しいレコード数が 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^)
一般的な場合の時間計算量下界の計算:
隣接する 2 つの要素を交換することで、ちょうど 1 つの逆順対を消去します。
I 個の逆順対があると仮定します。
T(N,I) = O(N+I)

長所と短所#

長所:安定
短所:比較回数は一定ではなく、比較回数が少ないほど、挿入点以降のデータの移動が多くなり、特にデータの総量が多い場合に顕著です。

テスト結果#

テスト結果は以下の通りで、かなり良好です。
ここに画像の説明を挿入

効率を向上させる方法#

以下の定理があります。

  • 任意の N 個の異なる要素からなる列は、平均して N (N-1)/4 個の逆順対を持ちます。
  • いかなる隣接する 2 つの要素を交換するだけでソートするアルゴリズムの平均時間計算量は O (N^2^) です。

したがって、アルゴリズムの効率を向上させるためには、

  • 毎回 1 つ以上の逆順対を消去する必要があります!
  • 毎回、離れた 2 つの要素を交換する必要があります!

シェルソート#

挿入ソートの簡単さを利用し、挿入ソートが隣接する 2 つの要素しか交換できないという欠点を克服します。

シェルソートは、レコードをインデックスの一定の増分でグループ化し、各グループに直接挿入ソートアルゴリズムを適用します。増分が徐々に減少するにつれて、各グループに含まれるキーワードが増え、増分が 1 になると、全ファイルがちょうど 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の2つの配列をソートされた状態でマージ
    }
}

時間計算量#

最良の場合 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)

長所と短所#

長所:すべての内部ソートの中で最も速いアルゴリズムです。
短所:不安定で、最悪の場合の効率が低下します!

テスト結果#

テスト結果は以下の通りです。
ここに画像の説明を挿入

表ソート#

データ量が多く、ソートする要素がオブジェクトで、移動にかかる時間が特に高い場合、間接ソートが必要です。
ポインタ配列を「表」として定義し、ソート待ちの要素を記録します。
ここに画像の説明を挿入

時間計算量#

最良の場合は初期状態で整列されています。
最悪の場合は N/2 個の環があり、各環には 2 つの要素が含まれ、2 つの要素を交換するには 3 ステップが必要で、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}; //負数のある10進数 二十個のバケット
    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 回の配分と収集を行います。

長所と短所#

長所:桁数が少なく、待ちソート列の最大桁数が特に大きくない場合に適しており、速い。
短所:空間を時間と交換します。

テスト結果#

テスト結果は以下の通りで、非常に速いです。
ここに画像の説明を挿入

三、ソートアルゴリズムの比較#

ここに画像の説明を挿入

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。