記事ディレクトリ
序文
スワッピングとは、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 データ構造]