ソートアルゴリズム-ソートシリーズとパフォーマンステストを挿入

挿入ソートの最初の知り合い

挿入ソートも比較的単純なソートアルゴリズムです。配列の最初の要素を並べ替えられたシーケンスとして取得し、配列の残りの要素を左から右に1つずつ取得して、左側の順序付けられたシーケンスに挿入するという考え方です。続行配列全体が順序付けられるまで、左側の順序付けられたシーケンスの長さを拡張します。

回路図は以下の通りです

次のような配列を挿入するとします。

最初に最初の要素を順序付けられたシーケンスに追加します

次にソートされていないパーツの最初の要素を取得し、左側の順序付けられた順序で要素を適切な位置に挿入します

ソートされていない部分の最初の要素は3であり、9の前に3を挿入する必要があることがわかります。

すると、1回目の挿入の結果は次のようになります。

次のラウンドに進みソートされていない部分の最初の要素を取ります、6

6を左側の順序付けられた順序で適切な位置、つまり3から9の間に挿入します。

次のラウンドに進み、配列のソートされていない部分の最初の要素1を取得し、1を3の左側に挿入する必要があることを確認します。

次のラウンドに進み、4が3と6の間に挿入されます

次のラウンドでは、2が1と3の間に挿入されます

…最後に、アレイ全体が順序付けられます

注:番号を順序付けられた順序で適切な位置に挿入します。このプロセスは、複数の方法で実装できます。最も簡単な方法は、バブリングと同様のプロセスを使用して、2つの隣接する番号を常に比較および交換し、挿入する要素を常に適切な位置に交換することです。この考えによると、挿入ソートのコードは次のとおりです。

public void insertSort(int[] array) {
    
    
		/*不断地将数字插入到合适的位置,扩大有序序列的边界 */
		for (int sortedSize = 1; sortedSize < array.length; sortedSize++) {
    
    
			for (int i = sortedSize - 1; i >= 0 ; i--) {
    
    
				if (array[i + 1] < array[i]) {
    
    
					swap(array, i + 1, i);
				} else {
    
    
					break;
				}
			}
		}
	}

最適化のアイデア

アイデア1

比較と交換を一方向の割り当てに変更します

上記の挿入ソートには、実際には最適化の余地があります。なぜなら、ソートの各ラウンドでは、実際には適切な位置を見つけるだけでよいからです。上記の実装で、適切な挿入位置見つけるプロセスは、要素の絶え間ない交換によって達成されます。2つの番号を交換するたびに、3つの割り当て操作が必要です別の考えを言えば、実際に挿入する要素を最初から最初に配置してから、要素の順序が要素よりも順序付けられている限り、比較のために要素要素に右から左の順序で挿入することができます。ラージに挿入されている場合は、順序付けられた順序でその位置を1ビット右に移動します。これにより、交換が必要な場所が一方向割り当てに変更されるたびに、1回の割り当て操作のみが必要になります。最後に、適切な位置を見つけたら、一時的に保存されている要素を直接割り当てて挿入します。回路図は以下の通りです

挿入ソートプロセス中の特定の時点での配列の状態は次のとおりであると想定します。

次に挿入される要素は5です。一時的に保存し、順序付けられた順序で右端の要素から開始して、順番に5と比較し、9が5より大きいことを確認してから、9を右に一方向に割り当てて上書きします。 5.論理的に言えば、9は1つ右に移動しており、一時的に5を格納しているため、9の元の位置は実際には役に立たないので、失う心配はありません。下の図では、役に立たない要素が紫色でマークされています

次に、左に移動して、順序付けられたシーケンスの次の要素8を確認します。

8と5を比較すると、8が5より大きいことがわかります。次に、8も右側に割り当てられ、紫色の9をカバーしています。

左に進み、

2と5を比較すると、2は5未満であり、適切な挿入位置が見つかったことを示し、以前に一時的に保存された5が紫色の要素の一方向の位置に割り当てられていることがわかります。

これで、このラウンドの挿入ソートは完了です。

論理的には、順序付けられたシーケンス内の大きい要素を順番に1つ右に移動し、要素を挿入するためのスペースを残すことと同じです。これは一方向の割り当てであるため、以前の比較交換よりも少なくなります。多くの割り当て操作が実行されているため、パフォーマンスが向上します。ただし、挿入する要素を一時的に保存するには、余分なスペースが必要です。この考えによると、コードは次のように書かれています

	public void insertSortV1(int[] array) {
    
    
		for (int sortedSize = 1; sortedSize < array.length; sortedSize++) {
    
    
			int temp = array[sortedSize];
			int i = sortedSize;
			while (i > 0 && array[i - 1] > temp) {
    
    
				array[i] = array[i - 1];
				i--;
			}
			array[i] = temp;
		}
	}

アイデア2

線形検索を二分検索に変更します

挿入ソートの各ラウンドのため、重要なのは適切な挿入位置見つけることです。これは実際には検索プロセスです。上記の実装では、線形検索を使用します。つまり、順序付けられたシーケンスの右端から、適切な位置が見つかるまで左端と比較します。この検索の時間計算量は線形です。つまり、O(n)です。この検索プロセスは、線形検索をバイナリ検索に置き換えることで最適化できます。バイナリ検索はジャンプ検索です。検索するシーケンスの中間位置が取得され、ターゲット値と比較されるたびに、ターゲット値よりも小さい場合は、右半分で検索を続行します。目標値より大きい場合は、左半分で検索を続行します。二分探索は毎回探索空間を半分に減らすことができ、その時間計算量はO(log(n))です。

二分探索を使用して最適化されたコードは次のとおりです

	/**
	 * 二分查找
	 * @param left 左边界(inclusive)
	 * @param right 有边界(inclusive)
	 * */
	private int binarySearch(int[] array, int left, int right, int target) {
    
    
		while (left <= right) {
    
    
            /* 取中间位置 */
			int mid = (left + right) >> 1;
			/*
			* 临界情况有2种
			* 1. 待查找区间还剩2个数 ->  此时 right = left + 1 , mid = left
			*  1.1 若判断 arr[mid] > target, 则该查找左半部分,此时应该插入的位置是mid,也就是left
			*  1.2 若判断 arr[mid] < target, 则该查找右半部分,此时应该插入的位置是mid + 1,也就是更新后的left
			* 2. 待查找区间还剩3个数 -> 此时 right = left + 2 , mid = left + 1
			*  2.1 若判断 arr[mid] > target, 则查找左半部分,回到情况1
			*  2.2 若判断 arr[mid] < target,则查找右半部分,更新完后下一轮循环 left = right = mid,
			*      若arr[mid] > target,则应插入的位置是left,若arr[mid] < target,则更新完后的left是应插入的位置
			* */
			if (array[mid] > target) {
    
    
				/* 往左半边查找 */
				right = mid - 1;
			} else if (array[mid] < target) {
    
    
				/* 往右半边查找 */
				left = mid + 1;
			} else {
    
    
				/* 相等了,返回待插入位置为 mid + 1 */
				return mid + 1;
			}
		}
		return left;
	}

	@Override
	public void insertSortBinarySearch(int[] array) {
    
    
		for (int sortedSize = 1; sortedSize < array.length; sortedSize++) {
    
    
			int temp = array[sortedSize];
			/* 获得待插入的位置 */
			int insertPos = binarySearch(array, 0, sortedSize - 1, temp);
			/* 将待插入位置之后的有序序列,全部往后移一位 */
			for (int i = sortedSize; i > insertPos ; i--) {
    
    
				array[i] = array[i - 1];
			}
			array[insertPos] = temp;
		}
	}

アイデア3

ヒルソート

ヒルソートは挿入ソートの変形です。その中心的な考え方はステップサイズ概念を導入することです。ヒルソートは、ステップサイズを使用して配列を多数の小さな配列分割し、各小さな配列で挿入ソートを使用してから、ステップを徐々に減らします。長い場合、最後のステップは1つに減り、1ステップのヒルソートは上記の単純な挿入ソートです。単純な挿入ソートと比較すると、ヒルソートの利点は、要素を複数の位置に移動できることです。一方、元の単純な挿入ソートを1つずつ比較して、適切な挿入位置を見つける必要があります。

ヒルソートの図は次のとおりです。初期ステップサイズは通常、配列の長さの半分に設定されます。

上記の配列は、gap = 4に従って要素の4つのグループに分割されます。位置間隔がギャップに等しい要素は、同じグループに分けられます。最初のグループは[1,5,9]であり(5と1は4つの位置で区切られ、9と5は4つの位置で区切られています...)、水色でマークされ、2番目のグループは[4,7]です。 、淡黄色でマークされ、3番目のグループは[2,6]で、薄紫でマークされ、4番目のグループは[8,3]で、オレンジでマークされています。要素のグループごとに、単純な挿入ソートが実行され、ギャップ値が減少し、ギャップが1に減少するまで、グループごとに挿入ソートが続行されます。最後のラウンドは、単純な挿入ソートと同等です。挿入ソートは、配列が基本的に順序付けられている場合に高効率を実現できるため、ヒルソートではステップサイズの概念が導入されており、要素を1つずつ操作しなくても複数の位置に挿入できます。ヒルソートの各ラウンドの後、配列全体が全体としてより順序付けられます(ヒルソートのラウンドの後、配列全体の前の位置にある要素が各グループの最小要素であり、行が次の要素は、各グループの大きな要素です)

ヒルソートの最初のラウンドの後、配列のステータスは次のようになります(3と8の位置のみが交換されます)

ヒルソーティングの第2ラウンドでは、ギャップが半分から2に減少します。

要素の2つのグループをそれぞれ挿入して並べ替えると、結果は次のようになります(最初のグループは水色でマークされ、変更はありません。2番目のグループはオレンジでマークされ、3と4の位置のみが交換されます)

最後に、gap = 1で、ヒルソートの最後のラウンドを実行します(これは単純な挿入ソートと同等です)

肉眼で見ると、最終ラウンドは2と3、6と7を交換するだけでよいことがわかります。2つの交換、つまり完全な並べ替え

上記の配列はヒルでソートされ、合計4回のスワップ操作が実行された後にソートが完了するため、非常に効率的です。単純な挿入ソートを使用する場合、スワップ操作の数は4を超えます。これは、ヒルソーティングの力を示しています。上記の内容に基づいて、ヒルソートの意味をよりよく理解することもできますこれは、縮小増分ソートとも呼ばれますステップサイズ、増分、ギャップはすべて同じです。インクリメントシーケンスの選択は、ヒルソートの効率にも影響します。通常、最初のインクリメントは配列の半分の長さであり、その後、インクリメントシーケンスは毎回半分になります。

ヒルソートのコード実装は次のとおりです

	public void shellSort(int[] array) {
    
    
		for (int gap = array.length / 2; gap > 0; gap /= 2) {
    
    
			for (int i = gap; i < array.length; i++) {
    
    
				int j = i;
				int temp = array[i];
                /* 这里没有采用二分查找,直接使用的线性查找 */
				while (j - gap >= 0 && array[j - gap] > temp) {
    
    
					array[j] = array[j - gap];
					j -= gap;
				}
				array[j] = temp;
			}
		}
	}

コードの実装では、ギャップの後の位置から配列の最後の要素まで直接トラバースすることに注意してください。各要素について、ギャップが増分され、直接挿入ソートが前方に実行されます。これは、図の要素の各グループの挿入ソートの説明とは多少異なります。このように書くことの利点は、ヒルソートコード全体が3層のループしか必要とせず、理解しやすいことです。図の説明によると、要素のグループの挿入ソートがループとして使用されるたびに、記述されたコードは4層のループを必要としますが、これは理解しにくいものです。興味のある読者は、次のコードを参照できます

	public void shellSort(int[] array) {
    
    
		int gap = array.length / 2;
		while (gap >= 1) {
    
    
			for (int i = 0; i < gap; i++) {
    
    
				/* 这一层循环就是对每一组元素进行插入排序 */
				for (int j = i; j + gap < array.length; j += gap) {
    
    
					int pos = j + gap;
					while (pos - gap >= 0) {
    
    
						if (array[pos] < array[pos - gap]) {
    
    
							swap(array, pos, pos - gap);
							pos -= gap;
						} else {
    
    
							break;
						}
					}
				}
			}
			gap /= 2;
		}
	}

性能試験

10,000から500,000の範囲のランダム配列を使用して、各挿入ソートアルゴリズムのパフォーマンステストが実行され、次のように折れ線グラフが描画されます。

ヒルソートのパフォーマンスが他のバージョンで爆発的に増加していることがわかります。2番目はバイナリ検索によって最適化され、2番目は一方向割り当てによって最適化されています。最適化されていないパフォーマンスは最悪です。

おすすめ

転載: blog.csdn.net/vcj1009784814/article/details/109026483