交換ソート(バブルソートとクイックソート)

序文

スワッピングとは、2 つのキーワード値の比較結果に基づいて、順序要件が満たされない場合に位置を交換することを意味します。バブル ソートとクイック ソートは代表的な交換ソート アルゴリズムであり、その中でクイック ソートが現在最も高速なソート アルゴリズムです。


バブルソート

バブル ソートは最も単純な交換ソート アルゴリズムであり、キーワードをペアごとに比較し、逆の順序である場合はそれらを交換するため、より大きなキーワードを含むレコードがバブルのようにポップアップして最後に配置されます。バブルソートを数回繰り返して、最終的に順序付けられたシーケンスを取得します。

アルゴリズムのステップ

1) ソート対象のレコードが配列 r[1...n] に格納されていると仮定し、まず 1 番目のレコードと 2 番目のレコードのキーワードを比較し、順序が逆であれば入れ替え、次に、レコードのキーワードを比較します。最初のレコードと 2 番目のレコードのキーワード...というように、n-1 番目のレコードと n 番目のレコードのキーワードが比較されるまで続きます。最初の並べ替えが完了し、最大のキーワードを持つレコードが最後の位置になります。

2) 2 回目のソートでは、最初の n-1 個の要素がバブルソートされ、2 番目に大きいキーを持つキーが n-1 の位置に記録されます。

3) 特定の並べ替えパスでレコードが交換されなくなり、順序が正しいことが示されるまで、上記のプロセスを繰り返します。

たとえば、バブル ソート アルゴリズムを使用して、シーケンス {12, 2, 16, 30, 28, 10, 16, 6, 20, 18} に対して非減少ソートを実行します。

1) 最初のソート パスでは、図 9-27 に示すように、ペアを比較し、順序が逆の場合は交換します。

ここに画像の説明を挿入します

最初のソート パスの後、最大記録は最後尾に浮上しており、2 番目のソート パスに参加する必要はありません。

2) 2 番目のソートでは、図 9-28 に示すように、各ペアを比較し、順序が逆の場合は交換します。

ここに画像の説明を挿入します

3) バブルソートを継続し、特定のソートパスで交換がなくなったら停止します。すべてのバブルソート結果は図 9-29 に示されています。

ここに画像の説明を挿入します


コード
public class Bubble {
    
    

    /**
     * 对数组元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){
    
    

        for (int i = a.length - 1; i > 0; i--) {
    
    
            for (int j = 0; j <i ; j++) {
    
    
                // 最坏情况 {6,5,4,3,2,1}
                // 比较索引 j 和 j+1 处的值
                if (greater(a[j],a[j+1])){
    
    
                    exch(a,j,j+1);
                }
            }
        }
    }

    /**
     *  比较  v 元素是否大于  W 元素
     * @param v
     * @param w
     * @return
     */
    private static boolean greater(Comparable v,Comparable w){
    
    

        return v.compareTo(w)>0;
    }

    /**
     * 对数组的 i 和 j  交换位置
     * @param a
     * @param i
     * @param j
     */
    private static  void  exch(Comparable[] a,int i,int j){
    
    
        Comparable temp;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}
テスト
import java.util.Arrays;

public class BubbleTest {
    
    
    public static void main(String[] args) {
    
    
        Integer[] arr = {
    
    12, 2, 16, 30, 28, 10, 16, 6, 20, 18};
        Bubble.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}
===========================
结果:[2, 6, 10, 12, 16, 16, 18, 20, 28, 30]
複雑さ

(1) 時間計算量

バブル ソートの時間計算量は初期シーケンスに関連しており、最良のケース、最悪のケース、平均的なケースに分類できます。

最良の場合、ソートされるシーケンス自体は正の順序です (たとえば、ソートされるシーケンスは非降順であり、質問では非降順のソートが必要です)。ソート パスは 1 回のみ、n-1 個の比較、および記録の交換は必要ありません。最良の場合、バブル ソートの時間計算量は O(n) です。

最悪のケースでは、ソートされるシーケンス自体が逆順になっており (たとえば、ソートされるシーケンスが非昇順であり、質問では非降順のソートが必要です)、n-1 回のソートと i-1 が必要になります。並べ替えごとの比較。比較の合計数は次のとおりです。

ここに画像の説明を挿入します

最悪の場合、バブルソートの時間計算量は O(n2) になります。· 平均的なケースでは、ソートされるシーケンス内のさまざまな状況の確率が等しい場合、最良のケースと最悪のケースの平均を取ることができます。平均的な場合、バブルソートの時間計算量も O(n2) です。

(2) 空間複雑度 バブルソートではいくつかの補助空間、つまり i、j、temp が使用され、空間複雑度は O(1) です。

(3) 安定性 バブルソーティングは安定した選別方法です。

安定性: 同じサイズの 2 つの値の順序がソートの前後で変わらないことを意味し、これは安定しています。バブルソートはソート前後で順番が変わらないので安定しています。

クイックソート

バブル ソートの欠点は、レコードの移動回数が増えるため、アルゴリズムのパフォーマンスが低下することです。誰かが実験したことがありますが、105 個のデータをソートする場合、バブル ソートには 8174 ミリ秒かかりますが、クイック ソートには 3.634 ミリ秒しかかかりません。

クイックソートは比較的高速な並べ替え方法です。クイック ソートは 1962 年に CAR Hoare によって提案されました。その基本的な考え方は、一連の並べ替えを通じて、並べ替え対象のデータを 2 つの独立した部分に分割することです。一方の部分のすべてのデータは、もう一方の部分のすべてのデータよりも小さいです。その後、データの 2 つの部分は、以下に従ってすばやく個別に並べ替えられます。並べ替えプロセス全体を再帰的に実行して、すべてのデータを順序付けられたシーケンスに変換できます。

クイック ソート アルゴリズムは分割統治戦略に基づいており、そのアルゴリズムの考え方は次のとおりです。

1) 分解: まずシーケンスから基本要素として要素を取り出します。基本要素を基準として、基本要素以下の部分列が左側、基本要素より大きい部分列が右側になるように、シーケンスを 2 つの部分列に分解します。

2) ガバナンス: 2 つのサブシーケンスをクイックソートします。

3) マージ: 2 つのソートされたサブシーケンスをマージして、元の問題の解を取得します。ソート対象の現在のシーケンスが R[low:high] であるとします (low ≤ high)。シーケンスのサイズが十分に小さい (要素が 1 つだけ) 場合、ソートは完了します。それ以外の場合、ソートは 3 つのステップで処理されます。加工手順は以下の通りです。

1) 分解: R[low:high] 内の要素 R[pivot] を選択し、これを基準としてソート対象のシーケンスを 2 つのシーケンス R[low:pivot-1] と R[pivot+1] に分割します。 : high] を実行し、シーケンス R[low:pivot-1] のすべての要素を R[pivot] 以下にし、シーケンス R[pivot+1:high] のすべての要素を R[pivot] より大きくします。 。この時点で、参照要素はすでに正しい位置にあるため、図 9-30 に示すように、後続の並べ替えに参加する必要はありません。

ここに画像の説明を挿入します

2) ガバナンス: 2 つのサブシーケンス R[low:pivot-1] と R[pivot+1:high] について、それぞれ再帰呼び出しを通じてクイック ソートが実行されます。

3) Merge : R[low:pivot-1] と R[pivot+1:high] のソートがその場で実行されるため、R[low:pivot-1] と R[pivot+1:high] Afterすべてがソートされているため、マージステップでは何もする必要はありません。シーケンス R[low:high] はすでにソートされています。

どのように分解するかは難しい問題です。参照要素が適切に選択されていない場合、サイズが 0 と n-1 の 2 つの部分列に分解され、クイック ソートがバブル ソートに堕してしまう可能性があるためです。

アルゴリズムのステップ

1) まず、配列の最初の要素を参照要素 pivot=R[low]、i=low、j=high とします。

2)右から左にスキャンしてピボット以下の数値を見つけます。見つかった場合は、R[i] と R[j] が交換されます (i++)。

3)左から右にスキャンしてピボットより大きい数値を見つけます。見つかった場合は、R[i] と R[j] が交換されます (j–)。

4) i と j が一致するまでステップ 2 と 3 を繰り返し、位置 Mid=i を返します。この位置の数値がピボット要素になります。

5) これで仕分け作業は完了です。このとき、mid を境界として、元のシーケンスを 2 つのサブシーケンスに分割し、左側のサブシーケンスの要素が pivot 以下、右側のサブシーケンスの要素が pivot 以上であるとすると、2 つのサブシーケンスは次のようになります。それぞれすぐに並べ替えられます。

グラフィカルな方法 1 (毎回ベース要素と交換)

ソートされる現在のシーケンスが R[low:high] (low≤high) であると仮定します。クイックソートプロセスを示すために、シーケンス (30、24、5、58、18、36、12、42、39) を例として取り上げます。

1) 初期化。図 9-33 に示すように、i=low、j=high、pivot=R[low]=30。

ここに画像の説明を挿入します
2) 左に進みます。図 9-34 に示すように、配列の右の位置から左に向かって検索し、ピボット以下の数値を探し続け、R[j]=12 を見つけます。

ここに画像の説明を挿入します
図 9-35 に示すように、R[i] と R[j] は i++ のように交換されます。

ここに画像の説明を挿入します
3) 右に進みます。図 9-36 に示すように、配列の左の位置から右に向かって検索し、ピボットより大きい数値を探し続け、R[i]=58 を見つけます。

ここに画像の説明を挿入します
図 9-37 に示すように、R[i] と R[j] は j– のように交換されます。

ここに画像の説明を挿入します
4) 左に進みます。図 9-38 に示すように、配列の右の位置から左に向かって検索し、ピボット以下の数値を探し続け、R[j]=18 を見つけます。

ここに画像の説明を挿入します
図 9-39 に示すように、R[i] と R[j] は i++ のように交換されます。
ここに画像の説明を挿入します

5) 右に進みます。ピボットより大きい数値が見つかるまで、配列の左の位置から右に検索します。このとき、i=j です。最初のソートのラウンドが終了し、図に示すように、i の位置が返されます (mid=i) 9-40。

ここに画像の説明を挿入します
これで仕分け作業は完了です。このとき、mid を境界として、元のシーケンスは 2 つのサブシーケンスに分割され、左側のサブシーケンスは pivot 以下、右側のサブシーケンスは pivot 以上になります。次に、これら 2 つのサブシーケンス (12、24、5、18) と (36、58、42、39) に対してクイック ソートを実行します。

グラフィカルな方法 2 (アルゴリズムの改善)

上記のアルゴリズムからわかるように、各交換は参照要素と行われます。実際には、これを行う必要はありません。目的は、元のシーケンスを参照要素で囲まれた 2 つのサブシーケンスに分割することです。左側のサブシーケンスは小さいです。 than は基本要素と等しく、右側のサブシーケンスは基本要素より大きくなります。これを実現するには多くの方法があります。右から左にスキャンしてピボット以下の数値 R[j] を見つけ、次に左から右にスキャンしてピボットより大きい数値 R[i] を見つけます。 R[i] と R[j ] を交換し、i と j が出会うまで交互に続け、その後は参照要素を R[i] と交換するだけです。このようにして分割処理が完了するが、交換要素の数はさらに少なくなる。

ソートされる現在のシーケンスが R[low: high] (low<high) であると仮定します。

1) まず、配列の最初の要素を参照要素 pivot=R[low]、i=low、j=high とします。

2) 右から左にスキャンして、ピボット以下の数値 R[i] を見つけます。

3) 左から右にスキャンして、ピボットより大きい数値 R[j] を見つけます。

4) R[i] と R[j] が交換されます (i+ +、j- -)。

5) i と j が等しくなるまでステップ 2 ~ 4 を繰り返します。R[i] が pivot より大きい場合、R[i-1] と参照要素 R[low] が交換され、位置 Mid=i-1 が返されます。それ以外の場合、R[i] と参照要素 R [low]を入れ替えて位置を返します、positionmid=i、この位置の番号がまさにベース要素になります。

これで仕分け作業は完了です。このとき、元のデータは、mid を境界として 2 つの部分列に分割され、左側の部分列の要素は pivot 以下、右側の部分列の要素は pivot 以上になります。

次に、2 つのサブシーケンスを別々にすばやく並べ替えます。

シーケンス (30、24、5、58、18、36、12、42、39) を例に挙げます。

1) 初期化。図 9-46 に示すように、i=low、j=high、pivot=R[low]=30。

2) 左に進みます。図 9-47 に示すように、配列の右の位置から左に向かって検索し、ピボット以下の数値を探し続け、R[j]=12 を見つけます。

ここに画像の説明を挿入します

3) 右に進みます。図 9-48 に示すように、配列の左の位置から右に向かって検索し、ピボットより大きい数値を探し続け、R[i]=58 を見つけます。

ここに画像の説明を挿入します

4) 図 9-49 に示すように、R[i] と R[j] が交換されます (i+ +、j- -)。

ここに画像の説明を挿入します

5) 左に進みます。図 9-50 に示すように、配列の右の位置から左に向かって検索し、ピボット以下の数値を探し続け、R[j]=18 を見つけます。

ここに画像の説明を挿入します

6) 右に進みます。図 9-51 に示すように、配列の左の位置から右に向かって、ピボット t より大きい数値が見つかるまで検索します。この時点で、i=j になり、停止します。

ここに画像の説明を挿入します

7) 図 9-52 に示すように、R[i] と R[low] が交換され、i の位置が返され (mid=i)、最初のソートが完了します。

ここに画像の説明を挿入します

これで1回の仕分けが完了します。このとき、元のデータは、図 9-53 に示すように、mid を境界として 2 つの部分列に分割され、左側の部分列は pivot より小さく、右側の部分列は pivot より大きくなります。

ここに画像の説明を挿入します

8) 2 つのサブシーケンス (18、24、5、12) と (36、58、42、39) をそれぞれすばやく並べ替えます。対照的に、上記の方法は、毎回参照要素と交換する従来の方法よりも高速かつ効率的です。

アルゴリズムコードの実装を改善する
  /**
     * 数组元素 i 和 j 交换位置
     * @param a
     * @param i
     * @param j
     */
    private static void exch(Comparable[] a,int i,int j){
    
    
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    /**
     *  比较v元素是否小于w元素
     * @param v
     * @param w
     * @return
     */
    public static boolean less(Comparable v,Comparable w){
    
    
        return v.compareTo(w)<0;
    }

    /**
     * 对数组内的元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){
    
    
        int low = 0;
        int high = a.length - 1;
        // 进行分治
        sort(a,low,high);
    }

    /**
     * 对数组a中从索引lo到索引hi之间的元素进行排序
     * @param a
     * @param lo
     * @param hi
     */
    private static void sort(Comparable[] a, int lo, int hi) {
    
    
        if (hi<=lo){
    
     return; }
        //对a数组中,从lo到hi的元素进行切分 i
        int partition = partition(a, lo, hi);
        // 对左边分组中的元素进行排序
        sort(a,lo,partition-1);
        // 对右边分组中的元素进行排序
        sort(a,partition+1,hi);
    }

    public static int i = 1;

    /**
     * 对数组a中,从索引 lo到索引 hi之间的元素进行分组,并返回分组界限对应的索引
     * @param a
     * @param lo
     * @param hi
     * @return
     */
    public static int partition(Comparable[] a, int lo, int hi) {
    
    

        //把最左边的元素当做基准值
        Comparable key = a[lo];
        //定义一个左侧指针,初始指向最左边的元素
        int left=lo;
        //定义一个右侧指针,初始指向左右侧的元素下一个位置
        int right=hi+1;

        //进行切分
        while(true){
    
    

            //先从右往左扫描,找到一个比基准值小的元素
            while(less(key,a[--right])){
    
    
                //循环停止,证明找到了一个比基准值小的元素
                if (right==lo){
    
    
                    break;//已经扫描到最左边了,无需继续扫描
                }
            }
            //再从左往右扫描,找一个比基准值大的元素
            while(less(a[++left],key)){
    
    
                //循环停止,证明找到了一个比基准值大的元素
                if (left==hi){
    
    
                    break;//已经扫描到了最右边了,无需继续扫描
                }
            }
            if (left>=right){
    
    
                //扫描完了所有元素,结束循环
                break;
            }else{
    
    
                //交换left和right索引处的元素
                exch(a,left,right);
                System.out.println("第"+ (i++) + "轮交换后:"+Arrays.toString(a));
            }

        }
        //交换最后 right 索引处和基准值所在的索引处的值
        exch(a,lo,right);
        return right;//right就是切分的界限

    }
テスト
public class QuickTest {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Integer[] arr = {
    
    30, 24, 5, 58, 18, 36, 12, 42, 39};
        System.out.println("初始化数据:"+Arrays.toString(arr));
        Test.sort(arr);
        System.out.println("排序后数据:"+Arrays.toString(arr));
    }
}
===================
结果:[5, 12, 18, 24, 30, 36, 39, 42, 58]
複雑さ
(1) 最良のシナリオ

時間の複雑さ

1) 分解: 分割関数 Partition は各要素をスキャンする必要があり、毎回スキャンされる要素の数は n を超えないため、時間計算量は O(n) です。

1) 分解: 分割関数 Partition は各要素をスキャンする必要があり、毎回スキャンされる要素の数は n を超えないため、時間計算量は O(n) です。

ここに画像の説明を挿入します

3) マージ: 図 9-42 に示すように、その場でソートされるため、マージ操作に時間がかかりません。

ここに画像の説明を挿入します

したがって、合計実行時間は次のようになります。

ここに画像の説明を挿入します

n>1 の場合、再帰的に解くことができます。

ここに画像の説明を挿入します

空間の複雑さ

プログラム内の変数は補助空間を占有します。これらの補助空間は順序が一定です。再帰呼び出しで使用されるスタック空間は再帰ツリーの深さ logn であり、空間複雑度は O(logn) です。

(2) 最悪のシナリオ

時間の複雑さ

1) 分解: 分割関数 Partition は各要素をスキャンする必要があり、毎回スキャンされる要素の数は n を超えないため、時間計算量は O(n) です。

2) ガバナンス: 最悪の場合、問題を分割するたびに、ベンチマーク要素の左側 (または右側) には要素がなくなり、ベンチマーク要素の反対側はサイズのサブ問題になります。 n-1. 図 9-43 に示すように、サイズ n-1 のこの副問題を再帰的に解くのに必要な時間は T(n-1) です。

ここに画像の説明を挿入します

3) マージ: 図 9-44 に示すように、マージ操作はその場でソートされるため、複雑な時間は必要ありません。

ここに画像の説明を挿入します

したがって、合計実行時間は

ここに画像の説明を挿入します

n>1 の場合、再帰的に解くことができます。

ここに画像の説明を挿入します

空間の複雑さ

プログラム内の変数は補助空間を占有します。これらの補助空間は順序が一定です。再帰呼び出しで使用されるスタック空間は再帰ツリーの深さ n で、空間複雑度は O(n) です。

(3) 平均的な状況

時間の複雑さ

図 9-45 に示すように、分割後の参照要素の位置が k 番目 (k=1,2, …, n) であると仮定します。

ここに画像の説明を挿入します

ここに画像の説明を挿入します

帰納法により、T(n) の大きさのオーダーも O(nlogn) であると結論付けることができます。平均すると、クイック ソート アルゴリズムの時間計算量は O(nlogn) です。

空間の複雑さ

プログラム内の変数は補助空間を占有します。これらの補助空間の順序は一定です。再帰呼び出しで使用されるスタック空間は O(logn) で、空間複雑度は O(logn) です。

(4) 安定性

2 つの等しい要素が両方向にスキャンされて交換されるため、ソート前後の位置が一致しない可能性があるため、クイック ソートは不安定なソート方法です。

要約する

為替ソートにおけるバブルソートは、トップ10のソートの中では比較的単純なソートであり、個人的には最も理解しやすいソートアルゴリズムでもあり、インタビューの過程でもテストされました。クイックソートは比較的難しいですが、パフォーマンスははるかに優れています。学習を始めたばかりの生徒は、中断して追跡して確認できます。何度か読むと、原理が理解できます。間違いがあれば、ご指摘ください。お知らせ下さい。

ブログソース [Funxue データ構造]

おすすめ

転載: blog.csdn.net/weixin_45833112/article/details/125113821