ソートアルゴリズム - 選択ソート
評価:
最も単純: 人々の自然な思考と最も一致する
最も役に立たない: 基本的にエンジニアリングの実践では使用されない
時間計算量: O(n^2)
空間計算量: O(1)
このセクションの内容:
時間と空間の複雑さを計算する方法
アルゴリズムを検証する方法 - 乱数生成器、対数
アルゴリズム プログラムを作成する哲学
選択ソートアルゴリズムのアイデア
人間の思考に最も沿ったアルゴリズムで、
走査するたびに最小の値を見つけて、すべての値が見つかるまでその値を前に置きます (入れ替えます)。順序は小さいものから大きいものへとソートされていますが、
なぜここで「挿入」ではなく「交換」なのでしょうか? 私の個人的な理解: メモリ配置では、配列要素の「交換」はより小さなメモリ領域を移動しますが、「挿入」にはより多くのメモリが必要ですエリアが移動される
アルゴリズムプログラムはどのように書くのでしょうか? アルゴリズムを書く哲学
- 単純なものから複雑なものまで
- 段階的に検証する
- 複数の中間結果を出力する
- 最初に部分、次に全体
- わからないときは、まず分解してみましょう。
- 最初は荒くて、その後は細かくなります
- 変数の名前変更
- ステートメントのマージ
- 境界処理
選択ソートアルゴリズムに特有の、それをどのように記述するか?
単純なものから複雑なものまで: 最初に 2 層ループを無視し、最も内側のループのみを記述し、最初のコアなことのみを実行します: ループは配列内の最小値の添字を見つけます。より緊密な分割: 具体的には Record という変数があります
。最小値が位置する添字 最初に添字を0とします 1の位置から選択を開始します ある要素まで比較した結果、添字が0の値よりも小さい値があれば更新します最小値。value 添字変数の値は新しい添字です。
コードの大まかなバージョン:
private static void sort1(int[] arr) {
int minPos = 0;
for (int i = 1; i < arr.length; i++) {
if (arr[minPos] > arr[i]) {
minPos = i;
}
}
// 此时下标为 minPos 的值就是我们挑选出来的最小值
System.out.println("minPos = " + minPos);
System.out.println("arr = " + Arrays.toString(arr));
// 交换: 将最小值与下标为0的值做交换
int tmp = arr[0];
arr[0] = arr[minPos];
arr[minPos] = tmp;
// 再次打印
System.out.println("after arr1 = " + Arrays.toString(arr));
// 第一轮执行完毕,准备第二轮
minPos = 1;
for (int i = 2; i < arr.length; i++) {
if (arr[minPos] > arr[i]) {
minPos = i;
}
}
// 此时下标为 minPos 的值就是挑选出来的最小值
System.out.println("minPos = " + minPos);
System.out.println("arr = " + Arrays.toString(arr));
// 交换: 将最小值与下标为1的值做交换
tmp = arr[1];
arr[1] = arr[minPos];
arr[minPos] = tmp;
// 交换后,下标为1的位置就是本轮挑选出来的最小值
System.out.println("arr2 = " + Arrays.toString(arr));
// ...
// 发现规律:每一轮的结果就是挑选出一个最小值,并与指定下标的值交换
// 待交换的下标是从0开始递增,直到length - 1
}
合理化する
ルールを確認する: 各ラウンドの結果は、最小値を選択し、それを指定された添字の値と交換することです。
交換される添字は 0 から始まり、長さ - 1 まで増加します。
private static void sort2(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minPos = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minPos] > arr[j]) {
minPos = j;
}
}
System.out.println("第 " + i + " 轮外层循环,找到的最小值下标: " + minPos);
int tmp = arr[i];
arr[i] = arr[minPos];
arr[minPos] = tmp;
System.out.println("交换后的数组 = " + Arrays.toString(arr));
}
System.out.println("arr = " + Arrays.toString(arr));
}
時間と空間の複雑さを計算する
最初に時間計算量を分析し、次に空間計算量を分析します
sort2 メソッドを例として時間と空間の計算量を計算し、各文の時間計算量を分析します。表現の便宜上、Java コードの文は時間計算量の単位として表現されます。
時間計算量の分析
private static void sort2(int[] arr) {
// 方法名,不计入复杂度,假设参数重的arr长度为n,以下的代码将以n为基础
for (int i = 0; // 代码 i=0 只会执行一次,计为1
i < arr.length - 1; // 代码 i<arr.length-1 将执行 length-1 次,计为n-1,又因n很大的时候与n-1相差无几,所以可直接计为n
i++) {
// 代码 i++ 将执行n次,计为n
int minPos = i; // 执行 n-1 次,计为 n
for (int j = i + 1; // 该语句将执行 (n-1) 次,与外层循环的次数一致
j < arr.length; // 该语句将执行 (n-1)+(n-2)+(n-3)+...+2+1 次,这是个等差数列,换算一下为: n(n-1)/2 = n^2/2 - n/2
j++) {
// 该语句是上句话的次数+1,同理当n很大的时候,与n相关的+1相差无几,所以为了方便计算,+1就不在单独加一,即该语句与上句话的时间复杂度相同
if (arr[minPos] > arr[j]) {
// 该语句与等差数列的次数相同
minPos = j; // 同上
}
}
System.out.println("第 " + i + " 轮外层循环,找到的最小值下标: " + minPos); // 该语句是调试打印语句,不计入时间复杂度
int tmp = arr[i]; // 该语句将执行 (n-1) 次
arr[i] = arr[minPos]; // 同上
arr[minPos] = tmp; // 同上
System.out.println("交换后的数组 = " + Arrays.toString(arr)); // 调试打印语句,忽略
}
System.out.println("arr = " + Arrays.toString(arr)); // 调试打印语句,忽略
}
上記のソースコードのコメントの時間計算量分析から、最も高いべき乗の時間計算量は算術シーケンス、つまり、アルゴリズムn^2/2 - n/2
全体の時間計算量は、アルゴリズム全体として最も高いべき乗の文を見つけるだけで済みます。時間計算量は十分です。
次に、複雑な式を単純化し、定数項を無視し、低電力項を無視します - 最も高い電力のみを保持します - 値は 、つまり、n^2
アルゴリズム全体の時間計算量は次のようになります。O(n^2)
2 レベル ループの時間計算量は であることがわかりますn^2
。将来的には、マルチレベル ループを直接検索できるようになります。基本レベル ループの基本時間計算量は n 乗されます。
空間の複雑さの分析
空间复杂度
アルゴリズムに必要な追加スペースを指します。原則: 主にデータ サイズに関連するスペースを検索し、for ループ内のインデックス変数や内部ループ内のインデックス変数
などの個々の基本型変数を無視し、交換に使用される変数選択およびソート アルゴリズムも無視します。データ量に相当するスペースなので、スペースの複雑さは最も基本的なものになります。i
j
int tmp
O(1)
選択ソートの「不安定さ」
不安定とは何ですか? つまり、配列内に同じ値を持つ 2 つの要素があり、選択ソート後に 2 つの要素が交換される可能性があります。これら 2 つの要素の相対位置が不安定です
たとえば、次のような配列が並べ替えられると仮定します。
int[] arr = {5, 4, 2, 5, 1, 3};
メソッドsort2()
内で交換が行われる前に、デバッグ用の出力コードを追加します。
System.out.printf("即将发生交换arr[%s]=%s <-> arr[%s]=%s\n", i, arr[i], minPos, arr[minPos]);
次の出力が表示されます。
即将发生交换arr[0]=5 <-> arr[4]=1
即将发生交换arr[1]=4 <-> arr[2]=2
即将发生交换arr[2]=4 <-> arr[5]=3
即将发生交换arr[3]=5 <-> arr[5]=4
即将发生交换arr[4]=5 <-> arr[4]=5
arr = [1, 2, 3, 4, 5, 5]
結果の最後の行は、並べ替え結果が実際に小さいものから大きいものへの順序で配置されていることを示しています。これは正しい順序ですが、心配しないでください。値 5 の要素が前後に入れ替わっているかどうかを確認してください。 0に添字が付いた値
5 の交換パス: [0] -> [4]
添字が 3 の値 5 の交換パス: [3] -> [5]
2 つの値の相対的な順序であると言う人もいます。ここの 5 は変更されていません。最初の 5 は位置 0 から位置 4 に交換され、2 番目の 5 は位置 3 から位置 5 に交換されます。交換後も、相対的な順序は維持されます。
心配しないでください。上記の例は私が何気なく書いたもので、たまたま不安定という特別なシーンにヒットしませんでした。次に、この例を使用して、元の配列の順序を少し変更して、不安定なシーンをトリガーします。最後の 2 つの数値を変更します
。1 と 3 が交換されて、次の配列が得られます。
int[] arr = {5, 4, 2, 5, 3, 1};
選択ソート アルゴリズムを再度実行して、出力を取得します。
即将发生交换arr[0]=5 <-> arr[5]=1
即将发生交换arr[1]=4 <-> arr[2]=2
即将发生交换arr[2]=4 <-> arr[4]=3
即将发生交换arr[3]=5 <-> arr[4]=4
即将发生交换arr[4]=5 <-> arr[4]=5
arr = [1, 2, 3, 4, 5, 5]
インデックス 0 の値 5 の交換経路: [0] -> [5]
インデックス 3 の値 5 の交換経路: [3] -> [4]
このとき、最初の 5 は次から交換されます。位置 0 位置番号 5 では、2 番目の 5 が位置 3 から位置 4 に交換されたため、それぞれの交換後に 2 つの 5 の相対位置が変化し、不安定なシーンがトリガーされました。
不安定な仕分けが実際の業務に与える影響の例
銀行の預金者は預金額の多い順に分類されていますが、同額の預金者が多数いるため、同額の預金者の順位が不安定です もしかしたら、何かのイベントで上位1,000名にプレゼントが送られるだけかもしれません振り分けが不安定なため、N人目に割り当てられる金額は50万で、ちょうど50万の入金者が100名います。100名全員がプレゼント対象範囲に含まれる場合、合計人数は1,001名になりますが、ルールでは、最初の 1,000 人のみを含めることが厳密に要求されているため、除外するには 500,000 の預金を持つ預金者が必要です。
選択ソート アルゴリズムは、ソート前の預金額が 500,000 の預金者の相対的な順序が変更されないことを保証できません。これが選択ソート アルゴリズムの「不安定性」です。
アルゴリズムを検証するにはどうすればよいですか?
検証アルゴリズム – Logarithmizer (配列を比較するための手段)
- 視覚的観察 – エラーが発生しやすいため、サンプルが大きい場合は肉眼で見ることができないため、エンジニアリング コードではなくテスト コードにのみ使用されます。したがって、手順は項目 2 から始まります。
- 十分なランダムサンプルを生成する
- 正しいアルゴリズムを使用してサンプル結果を計算する
- 検証されたアルゴリズムの結果を比較する
十分なランダムサンプルを生成する
配列を生成し、その数値はパラメーターで指定され、次にランダム オブジェクトを作成し、配列の各添字に対してランダムな整数を生成します。ランダムな整数の最大値はパラメーターで指定されます。コードは次のとおりです。
private int[] generateRandomArray(int len, int max) {
int[] arr = new int[len];
Random r = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = r.nextInt(max);
}
return arr;
}
2 つの配列が等しいかどうかを比較します
長さが等しくない場合、配列は等しくない必要があります。そうでない場合は、2 つの配列を走査し、2 つの配列の対応する添字の値を取り出します。比較のために、等しくない場合は、等しくないことが返されます。トラバーサルの最後に不等が見つからない場合、戻り値は等しくなります。コードは次のようになります。
private boolean checkArrayEquals(int[] arr1, int[] arr2) {
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
大きな配列の最初の N 要素の補助出力
大きな配列には値が多すぎるため、すべてを出力するのは不便です。並べ替えシナリオでは、最初の N 要素が順番に並んでいるかどうかをチェックするだけで、配列全体の順序を大まかに反映できます。コードは次のとおりです。以下に続きます:
private void printFirstN(int[] arr, int n) {
System.out.print("first-" + n + ": ");
for (int i = 0; i < n; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
検証アルゴリズムの主な機能
補助関数を先に完成させた後、メイン関数を書くことができます。まず 10,000 要素の配列をランダムに生成し、次に 1 つをコピーして 2 つのコピーを取得します。1 つは独自のソート アルゴリズムによるソートに使用され、もう 1 つは標準のソート関数によるソートに使用されます。次に、2 つのソートの結果を比較します。アルゴリズム、コードは以下のように表示されます。
public void test_data_checker() {
for (int i = 0; i < 10; i++) {
int[] arr = generateRandomArray(10000, 10000);
int[] arr2 = new int[arr.length];
System.arraycopy(arr, 0, arr2, 0, arr.length);
printFirstN(arr, 10);
SelectionSort.sort2(arr);
Arrays.sort(arr2);
printFirstN(arr, 10);
printFirstN(arr2, 10);
boolean b = checkArrayEquals(arr, arr2);
if (!b) {
throw new RuntimeException("排序结果与标准排序算法不一致");
}
}
}
上記のロジックをループにラップして複数回ループし、一度で結果が異なる場合は例外メッセージがスローされます。