一般的なソート アルゴリズム (C 言語で実装)

並べ替えの概要

  並べ替えとは、1 つまたはいくつかのキーワードのサイズに応じて、レコードの文字列を昇順または降順で並べ替える操作です。
  ソートされるレコードの順序に、同じキーワードを持つ複数のレコードがあり、これらのレコードの相対的な順序がソート後に変更されない場合、ソート アルゴリズムは安定していると言われます。たとえば、A = B、A は元のシーケンスで B の前にあり、A はソート後も B の前にあり、安定しています。
  データ要素がすべてメモリに配置される順序付けは、内部順序付けと呼ばれます。
  同時にメモリに格納するにはデータ要素が多すぎます。ソートプロセスの要件に従って、内部メモリと外部メモリの間でデータを継続的に移動する必要があるソートは、外部ソートと呼ばれます。
ここに画像の説明を挿入

挿入ソート

  基本的な考え方: すべてのレコードが挿入され、新しいシーケンスが取得されるまで、それらのキー コード値の委託に従って、並べ替えられるレコードを 1 つずつ既に並べ替えられたシーケンスに挿入します。

直接挿入ソート

  1. 基本的な紹介:
      並べ替える配列では、最初の n-1 要素が既に並べられていると仮定し、n 番目の要素を 1 つずつ比較してから、n 番目の要素を適切な位置に配置します。
      一連の要素の順序が近いほど、直接挿入アルゴリズムの時間効率が向上します。
  2. コード:
void InsertSort(int* a, int n)
{
    
    
	for (int i = 0; i < n - 1; i++)
	{
    
    
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
    
    
			if (tmp < a[end])
			{
    
    
				a[end + 1] = a[end];
				end--;
			}
			else
			{
    
    
				break;
			}
		}
		a[end + 1] = tmp;
	}

}
  1. 時間複雑度と空間複雑度
      挿入ソートの平均時間複雑度も O(n 2 ) であり、空間複雑度は一定次数 O(1) です.特定の時間複雑度は、配列の順序性にも関連しています.
      挿入ソートでは、ソート対象の配列が整っている場合が最適な状況です.現在の数と前の数を比較するだけで済みます.このとき、合計でN-1回比較する必要があり、時間計算量は O(N) です。ワーストケースはソート対象の配列が逆順で、このとき比較回数が最大となり、ワーストケースはO(n 2 )となります。
  2. アニメーションのデモ:ここに画像の説明を挿入

ヒルソート

  1. 基本的な紹介:
      ヒル ソートは、縮小インクリメンタル ソートとも呼ばれる挿入ソートです。ヒルソーティングは、直接挿入ソートに基づくグループ化を導入し、最初に事前ソートのためにグループ化し、次に直接挿入ソートによって最終ソートを完了します。
      最初の増分として数のギャップ (ソートするデータの総数未満) を最初に選択し、要素間にギャップがある要素をグループとして選択し、それらをグループ化して直接挿入ソートします。ソートが完了すると、インクリメントが減少し、ギャップが 1 になるまで上記の操作が繰り返され、最終的な並べ替えが行われます。
  2. コード:
void ShellSort(int* a, int n)
{
    
    
	int gap = n;
	while (gap > 1)
	{
    
    
		gap = gap / 3 + 1;

		for (int i = 0; i < n - gap; i++)
		{
    
    
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
    
    
				if (tmp < a[end])
				{
    
    
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
    
    
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}
  1. 時間計算量と空間計算量:
      ヒル ソートの時間計算量は O(n 1.3-2 ) であり、空間計算量は一定次数 O(1) です。ヒル ソートは、時間計算量が O(n(logn)) のクイック ソート アルゴリズムほど高速ではないため、中規模のスケールではうまく機能しますが、非常に大規模なデータ ソートには最適な選択ではありません。つまり、一般的な O(n 2 )よりも優れています。複雑なアルゴリズムははるかに高速です。
      アルゴリズムの実行中は、いくつかの定義済み一時変数のみが必要なため、スペースの複雑さは一定レベル O(1) です。
  2. グラフィック:
    ここに画像の説明を挿入

選択ソート

  基本的な考え方: 毎回並べ替えるデータ要素から最小 (または最大) の要素を選択し、並べ替えるすべてのデータ要素がなくなるまで、シーケンスの先頭に配置します。

選択ソート

  1. 基本的な紹介:
      直接選択ソートも単純なソート方法です. その基本的な考え方は: 1回目はR[0]~R[n-1]から最小値を選択し、R[0]と交換し、2回目はR[1]~R[n-1]から最小値を選択、R[1]と交換、…、R[i-1]~R[n-1]からi回目の最小値を選択、R[i-1]と交換、…、R[n-2]~R[n-1]の最小値をn-1回選択、R[n-2]と交換、合計n-1回渡して、ソートキーで昇順に並べられた順序付きシーケンスを取得します。
      単純な最適化は、とにかく 1 回トラバースすることです。次に、最大値と最小値を見つけて、それぞれ最後と最初に配置することで、ある程度の効率を向上させることができます。
  2. コード:
void SelectSort(int* a, int n)
{
    
    
	int begin = 0, end = n - 1;

	while (begin < end)
	{
    
    
		int minI = begin, maxI = begin;
		for (int i = begin + 1; i <= end; i++)
		{
    
    
			if (a[i] < a[minI])
				minI = i;
			if (a[i] > a[maxI])
				maxI = i;
		}
		int tmp = a[begin];
		a[begin] = a[minI];
		a[minI] = tmp;
		if (maxI == begin)
			maxI = minI;
		tmp = a[end];
		a[end] = a[maxI];
		a[maxI] = tmp;

		begin++;
		end--;
	}
}
  1. 時間計算量と空間計算量:
      直接選択ソートの時間計算量は O(n 2 ) であるため、レコードが大量のバイトを占める場合、通常、直接挿入ソートの実行速度よりも高速です; 空間
      計算量については、単純選択ソートは、レコード交換の一時記憶単位に 1 つのストレージ スペースしか必要としない、つまり、スペースの複雑度は O(1) です; 直接選択ソートでは隣接しない
      要素間の交換があるため、直接選択ソートは不安定なソート方法です。
  2. グラフィック:
    ここに画像の説明を挿入

ヒープソート

  1. 基本的な紹介:
      ヒープ ソートとは、選択ソートの一種であるヒープのデータ構造を使用して設計されたソート アルゴリズムを指します。データ選択にヒープを使用します。昇順では、大きな山を構築する必要があり、降順では、小さな山を構築する必要があります。
  2. コード:
void Swap(int* p1, int* p2)
{
    
    
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
    
    
	int child = parent * 2 + 1;
	while (child < n)
	{
    
    
		// 确认child指向大的那个孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
    
    
			++child;
		}
		// 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (a[child] > a[parent])
		{
    
    
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
    
    
	// 向下调整建堆 -- O(N)
	// 升序:建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
    
    
		AdjustDown(a, n, i);
	}
	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
  1. 時間の複雑さと空間の複雑さ:
      ヒープ ソートを使用する最悪のケースは、常にノードを交換する必要があり、h - 1 回ループする必要があることです (h はツリーの高さです)。また、h = log(N+1) (N はツリーの要約ポイントの数)、ソートの時間計算量は O(logN) です。ただし、ヒープの並べ替えでヒープを構築する必要がある前に、ヒープを構築する時間の複雑さは O(N) です。
      スペースの複雑さについては、ヒープの並べ替えはいくつかの添字位置を記録するためにいくつかのストレージ スペースしか必要としないため、スペースの複雑さは O(1) です。
  2. グラフィック:
    ここに画像の説明を挿入

スワップソート

  いわゆる交換
とは、シーケンス内の 2 つのレコードのキー値の比較結果に従って、シーケンス内の 2 つのレコードの位置を交換することで、小さいレコードほどシーケンスの先頭に移動します。

バブルソート

  1. 基本的な紹介:
      バブル ソートは、バブルのようにソートされ、要素が 1 つずつポップアップするため、非常に鮮明な説明です。最初の要素から始めて、それらをペアごとに比較し、昇順または降順に従って大きい要素または小さい要素を後方に移動します。
      1回のトラバーサルでやり取りがなければ、トラバーサルの順番が整っているということで、そのまま飛び出せます。これにより効率が少し向上しますが、効率は他のソートアルゴリズムほど良くありません。
  2. コード:
void BubbleSort(int* a, int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		int exchange = 0;
		for (int j = 1; j < n - i; j++)
		{
    
    
			if (a[j - 1] > a[j])
			{
    
    
				int tmp = a[j - 1];
				a[j - 1] = a[j];
				a[j] = tmp;
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}
  1. 時間の複雑さと空間の複雑さ:

  2. グラフィック:
    ここに画像の説明を挿入

クイックソート

  クイックソートは、1962 年に Hoare によって提案された二分木構造の交換ソート方法です。その基本的な考え方は、ソートされる要素のシーケンス内の任意の要素がその参照値 (多くの場合、最初の要素または最後の要素) として取得され、ソートコードに従って左右のサブシーケンスにソートされるように設定されている場合、左のサブシーケンスのすべての要素は基準値よりも小さく、右のサブシーケンスのすべての要素は基準値よりも大きく、左と右のサブシーケンスは次を繰り返しますすべての要素がそれぞれの位置に配置されるまで処理します。

再帰的な実装

ホアレ版
  1. 基本的な紹介:
      キー値を選択し、L と R を定義します。L は右に、R は左に移動します。(キー値が最初の要素の場合、R が最初に移動することに注意してください。キー値が最後の要素の場合、L が最初に移動します) R が最初に移動し、キー値より小さい値に遭遇すると停止します
      。 、そして L が動き始めます. キーの値よりも大きいストップに遭遇し、この時点で L と R の値を交換し、上記の手順を繰り返します. L と R が一致するまで、このときの値はキーの値よりも小さくなければならず、それをキーと交換します。このとき、キー値はあるべき場所に配置され、シーケンスは左右のサブシーケンスに分割されます。
      R と L の値が一致するときにキー値よりも小さくなければならないのはなぜですか? R はキー値よりも小さい値に遭遇すると停止し、L が移動するのを待ちます.L が移動する結果は 2 つあり、1 つはキー値よりも大きい値に遭遇し、2 つが交換され、次に Rキー値が大きい場合、一致するまでは、キー値より小さい値です。L が停止する前に、R が key より小さい値に遭遇したときに R を停止する必要があり、2 つが交換されます。したがって、R と L が交わる値がキー値よりも小さくなければならない理由は、R が最初に来るためです。これは非常に巧妙なステップです。L が最初に行く場合、出会い位置の値はキー値よりも大きくなければならず、このときのキー値は最後の要素でなければなりません。
  2. コード:
void QuickSortHoare(int* a, int begin, int end)
{
    
    
	if (begin >= end)
		return;

	int left = begin, right = end;
	int keyI = left;
	while (left < right)
	{
    
    
		// 右边先走,找小于key值的值
		while (left < right && a[right] >= a[keyI])
			right--;

		// 左边后走,找大于key值的值
		while (right < right && a[left] <= a[keyI])
			left++;

		int tmp = a[left];
		a[left] = a[right];
		a[right] = tmp;
	}

	int tmp = a[left];
	a[left] = a[keyI];
	a[keyI] = a[left];
	keyI = left;
	// 左子序列[begin,keyI),右子序列(keyI,end]
	QuickSortHoare(a, begin, keyI - 1);
	QuickSortHoare(a, keyI + 1, end);
}
  1. 時間計算量と空間計算量:
      迅速な並べ替えのために、比較の数は固定されており、O(n) を超えないため、分割数は非常に重要です。初期シーケンスが順序付けられている場合、この時点でのソート プロセスはバブル ソートと非常に似ており、時間計算量は O(n) であり、最悪の場合の時間計算量は O(n 2 )です
      キー値が毎回真ん中にある場合、それは少し二分法に似ており、時間計算量は O(logn) であり、このときの時間計算量は O(n*logn) です。
      再帰を利用するため, 実行過程で関連情報をスタックに保存する必要がある. 必要な容量は再帰回数に関係し, 再帰は分割数に関係する. つまり, O(logn )、最悪は O( n) です。
  2. グラフィック:
    ここに画像の説明を挿入
穴掘り
  1. 基本的な紹介:
      穴掘り法は Hoare に基づいています. 彼は R と L が出会うときの値が重要な値よりも小さくなければならないという問題を掘る方法で回避します.なぜなら誰もが R の値を理解できるわけではないからです. L が一致する場合、L はキー値よりも小さくなります。しかし、Hoare のように、キー値の選択に応じて、先に行くか L 先に行くかを決定する必要もあります。
      穴掘り法は、最初にキー値を取り出し、Rが移動してキー値よりも小さい値を探し、ここで値をLの位置に埋め、Lが移動を開始し、それよりも大きい値を見つけます。キー値を入力して R の位置に入力すると、R が再び移動を開始し、R と L が出会うまで上記の手順を繰り返し、最後にキー値を入力します。
  2. コード:
void QuickSortPit(int* a, int begin, int end)
{
    
    
	// 当只有一个数据或数列不存在时
	if (begin >= end)
		return;

	int left = begin;
	int right = end;
	int key = a[left];
	int pit = left;
	while (left < right)
	{
    
    
		// 右边先走,找比key值小的值
		while (left < right && a[right] >= key)
		{
    
    
			right--;
		}
		a[pit] = a[right];
		pit = right;

		// 左边再走,找比key值大的值
		while (left < right && a[left] <= key)
		{
    
    
			left++;
		}
		a[pit] = a[left];
		pit = left;
	}

	a[pit] = key;
	QuickSortPit(a, begin, pit - 1);
	QuickSortPit(a, pit + 1, end);
}
  1. 時間複雑度と空間複雑度:
      核となるアイデアは変わっておらず、スープも薬を変えていないため、時間複雑度はホアレ版と同じです。
  2. グラフィック:
    ここに画像の説明を挿入
前面と背面のポインター バージョン
  1. 基本的な導入:
      これは Hoare の変形であり、キー値を取り、次に prev と cur を取り、それぞれ最初の要素と 2 番目の要素を指すようにします。その後、cur は後方に移動し、キーよりも小さい値に遭遇します。cur の値は次のとおりです。 prev の値と交換し、cur の値が key よりも大きい場合は続けて進みます。このように prev は 2 つのケースで cur と同じ位置にあるか、値が key 値よりも大きい位置にとどまり、最後に cur が最後に達した後、prev と key が交換され、タスクが完了します。左右のサブシーケンスを区別する. .
      これは Hoare の変種で、プロセスは理解しにくいですが、コードは簡単に実装できます。
  2. コード:
void QuickSortPoint(int* a, int begin, int end)
{
    
    
	if (begin >= end)
		return;

	int keyI = begin;
	int prev = begin;
	int cur = begin + 1;
	while (cur <= end)
	{
    
    
		// 找到比key小的值时,与prev++位置交换,小的向前移动,大的向后移动
		if (a[cur] < a[keyI] && ++prev != cur)
		{
    
    
			int tmp = a[prev];
			a[prev] = a[cur];
			a[cur] = tmp;
		}
		cur++;
	}

	int tmp = a[prev];
	a[prev] = a[keyI];
	a[keyI] = tmp;

	keyI = prev;

	QuickSortPoint(a, begin, keyI - 1);
	QuickSortPoint(a, keyI + 1, end);
}
  1. 時間複雑度と空間複雑度:
      時間複雑度と空間複雑度は Hoare バージョンと同じです。
  2. グラフィック:
    ここに画像の説明を挿入

非再帰的な実装

  まずポイントを知っておく必要があります.再帰ごとにスタックフレーム空間が開かれます.スタックフレーム空間は最初に開かれた空間が最後に破壊されるという特徴があります.しかし,これも問題を引き起こします.深すぎると、スタックがオーバーフローします。ただし、クイック ソートは、最初にスタックにプッシュし、次に破棄してソートを完了するという機能に依存しています。したがって、非再帰的なクイック ソートを実装したい場合は、この機能を実装する必要があり、この機能を持つデータ構造にたまたまスタック データ構造が存在するため、非再帰的なクイック ソートを実装したい場合は、スタックデータ構造を使用する必要があります。

ホアレ版
int Hoare(int* a, int begin, int end)
{
    
    
	int left = begin, right = end;
	int keyI = left;
	while (left < right)
	{
    
    
		// 右边先走,找小于key值的值
		while (left < right && a[right] >= a[keyI])
			right--;

		// 左边后走,找大于key值的值
		while (right < right && a[left] <= a[keyI])
			left++;

		int tmp = a[left];
		a[left] = a[right];
		a[right] = tmp;
	}

	int tmp = a[left];
	a[left] = a[keyI];
	a[keyI] = a[left];
	keyI = left;

	return keyI;
}

void QuickSortNonR(int* a, int begin, int end)
{
    
    
	// 创建、初始化栈,将begin、end插入栈中
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	// 栈非空就循环
	while (!StackEmpty(&st))
	{
    
    
		int right = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;
		int left = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;

		int keyI = Hoare(a, left, right);

		if (keyI + 1 < right)
		{
    
    
			StackPush(&st, keyI + 1);
			StackPush(&st, right);
		}

		if (left < keyI - 1)
		{
    
    
			StackPush(&st, left);
			StackPush(&st, keyI - 1);
		}
	}

	StackDestroy(&st);
}
穴掘り
int Pit(int* a, int begin, int end)
{
    
    
	int left = begin;
	int right = end;
	int key = a[left];
	int pit = left;
	while (left < right)
	{
    
    
		// 右边先走,找比key值小的值
		while (left < right && a[right] >= key)
		{
    
    
			right--;
		}
		a[pit] = a[right];
		pit = right;

		// 左边再走,找比key值大的值
		while (left < right && a[left] <= key)
		{
    
    
			left++;
		}
		a[pit] = a[left];
		pit = left;
	}

	a[pit] = key;

	return pit;
}
void QuickSortNonR(int* a, int begin, int end)
{
    
    
	// 创建、初始化栈,将begin、end插入栈中
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	// 栈非空就循环
	while (!StackEmpty(&st))
	{
    
    
		int right = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;
		int left = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;

		int keyI = Pit(a, left, right);

		if (keyI + 1 < right)
		{
    
    
			StackPush(&st, keyI + 1);
			StackPush(&st, right);
		}

		if (left < keyI - 1)
		{
    
    
			StackPush(&st, left);
			StackPush(&st, keyI - 1);
		}
	}

	StackDestroy(&st);
}
前面と背面のポインター バージョン
int Point(int* a, int begin, int end)
{
    
    
	int keyI = begin;
	int prev = begin;
	int cur = begin + 1;
	while (cur <= end)
	{
    
    
		// 找到比key小的值时,与prev++位置交换,小的向前移动,大的向后移动
		if (a[cur] < a[keyI] && ++prev != cur)
		{
    
    
			int tmp = a[prev];
			a[prev] = a[cur];
			a[cur] = tmp;
		}
		cur++;
	}

	int tmp = a[prev];
	a[prev] = a[keyI];
	a[keyI] = tmp;

	keyI = prev;

	return keyI;
}
void QuickSortNonR(int* a, int begin, int end)
{
    
    
	// 创建、初始化栈,将begin、end插入栈中
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	// 栈非空就循环
	while (!StackEmpty(&st))
	{
    
    
		int right = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;
		int left = StackTop(&st);
		StackPop(&st);
		if (StackEmpty(&st))
			break;

		int keyI = Point(a, left, right);

		if (keyI + 1 < right)
		{
    
    
			StackPush(&st, keyI + 1);
			StackPush(&st, right);
		}

		if (left < keyI - 1)
		{
    
    
			StackPush(&st, left);
			StackPush(&st, keyI - 1);
		}
	}

	StackDestroy(&st);
}

クイックソートの最適化

3 つの値の中間を取る

前述のように、キー値の位置が毎回最も遠い側にある場合、クイック ソートの時間効率は O(n 2 )  になります.この確率は非常に小さいですが、それでも確率はあります。それが起こること。このとき、3 値法を使用してこのような状況を回避できます。キー値は分割数に影響するキーであるため、3 つの値の中間を取るということは、最初、中間、および最後の値を見つけて値を比較し、中間値をキー値と交換することを意味します。キー値の位置は同じであることが保証されています。

int GetMidIndex(int* a, int begin, int end)
{
    
    
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
    
    
		if (a[mid] < a[end])
		{
    
    
			return mid;
		}
		else if (a[begin] > a[end])
		{
    
    
			return begin;
		}
		else
		{
    
    
			return end;
		}
	}
	else // a[begin] > a[mid]
	{
    
    
		if (a[mid] > a[end])
		{
    
    
			return mid;
		}
		else if (a[begin] < a[end])
		{
    
    
			return begin;
		}
		else
		{
    
    
			return end;
		}
	}
}
セル間の最適化

  各層の再帰は 2 倍、つまり 1、2、4、8、16 で増加します... このシーケンスを通じて、再帰の 1 つの層を減らす限り、その数を論理的に見つけることができます。の再帰を約半分に減らすことができます。したがって、他のソートを組み合わせて判断を下すことができ、数が少ない場合は他のソートを使用して、深すぎる再帰を効果的に回避できます。

void QuickSort(int* a, int begin, int end)
{
    
    
	if (begin >= end)
	{
    
    
		return;
	}

	if ((end - begin + 1) < 15)
	{
    
    
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
    
    
		int keyi = PartSort3(a, begin, end);

		// [begin, keyi-1]  keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

マージソート

再帰的な実装

  1. 基本的な紹介:
      マージ ソートは、分割統治のアイデアを使用するマージ操作に基づく効果的な並べ替えアルゴリズムです。完全にソートされたシーケンスを取得するために、既にソートされたサブシーケンスをマージします。つまり、各サブシーケンスは最初にソートされ、次に順序付きシーケンスにマージされます。2 つのソート済みリストを 1 つのソート済みリストにマージすることを双方向マージと呼びます。マージソートは、最初に分解してからマージする必要があります。
    ここに画像の説明を挿入

  2. コード:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
    
    
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	// [begin, mid] [mid+1, end] 递归让子区间有序
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	// 归并[begin, mid] [mid+1, end]
	int begin1 = begin, end1 = mid;
	int begin2 = mid+1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
    
    
		if (a[begin1] <= a[begin2])
		{
    
    
			tmp[i++] = a[begin1++];
		}
		else
		{
    
    
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
    
    
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
    
    
		tmp[i++] = a[begin2++];
	}

	memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));
}

void MergeSort(int* a, int n)
{
    
    
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);


	free(tmp);
	tmp = NULL;
}

  1. 時間の複雑さと空間の複雑さ:
      マージ ソートは、高さが O(logn) のバイナリ ツリー構造にいくぶん似ており、各層は n 回ループするため、時間の複雑さは O(n*logn) です。マージ ソートはさらに開きます
      。 n 個のスペースに再帰 logn を加えたものなので、スペースの複雑さは O(n+logn) ですが、logn は無視でき、最終的な複雑さは O(n) になります。
  2. グラフィック:
    ここに画像の説明を挿入

非再帰的な実装

  1. 基本的な紹介:
      マージ ソートの非再帰アルゴリズムは、実装するためにスタックのデータ構造を使用する必要はありません. スタックを使用すると、非常に面倒です. マージに参加する要素の数を制御するだけで済みます.毎回、そして最終的にシーケンスを整然とすることができます。
      ただし、マージはペアで行われるため、いくつかの特殊なケースを考慮する必要があります。つまり、マージする要素の数は 1、2、4、8、16 です...要素はそのような標準ではありません倍数はどうですか?このとき、3 つの状況が発生します。
      ①: 最後のグループでは、適切な間隔の要素数が十分ではありません. このとき、シーケンスをマージするときに、この間隔の境界を制御する必要があります. ②: 最後のグループでは、正しい間隔に要素がありません
      .右側の間隔、つまり要素が左側の間隔にちょうど十分である場合、このグループは既に順序付けられているため、マージする必要はありません; ③: 最後のグループでは、左側の間隔の要素の数
      は十分でない場合は、このグループをマージする必要はありません。
    ここに画像の説明を挿入

  2. コード:

void MergeSortNonR(int* a, int n)
{
    
    
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}

	// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
	int rangeN = 1;
	while (rangeN < n)
	{
    
    
		for (int i = 0; i < n; i += 2 * rangeN)
		{
    
    
			// [begin1,end1][begin2,end2] 归并
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
			int j = i;

			// end1 begin2 end2 越界
			// 修正区间  ->拷贝数据 归并完了整体拷贝 or 归并每组拷贝
			if (end1 >= n)
			{
    
    
				end1 = n - 1;
				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
    
    
				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
    
    
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
    
    
				if (a[begin1] <= a[begin2])
				{
    
    
					tmp[j++] = a[begin1++];
				}
				else
				{
    
    
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
    
    
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
    
    
				tmp[j++] = a[begin2++];
			}
		}

		// 也可以整体归并完了再拷贝
		memcpy(a, tmp, sizeof(int)*(n));

		rangeN *= 2;
	}

	free(tmp);
	tmp = NULL;
}

カウントソート

  1. 基本的な紹介:
      ピジョンホール ソートとも呼ばれるカウンティング ソートは、ハッシュ ダイレクト アドレッシング方式の修正されたアプリケーションであり、非比較ソートです。最初に同じ要素の出現回数をカウントし、次に統計結果に従ってシーケンスを元のシーケンスにリサイクルします。
      カウントソートは、データ範囲セット内のシーケンスに適しており、現時点では非常に効率的ですが、適用可能な範囲とシナリオは限られています。
  2. コード:
void CountSort(int* a, int n)
{
    
    
	int max = a[0], min = a[0];
	for (int i = 1; i < n; i++)
	{
    
    
		if (a[i] < min)
			min = a[i];

		if (a[i] > max)
			max = a[i];
	}

	int range = max - min + 1;
	int* countA = (int*)calloc(range, sizeof(int));
	if (NULL == countA)
	{
    
    
		perror("calloc fail\n");
		exit(-1);
	}
	// 统计次数
	for (int i = 0; i < n; i++)
		countA[a[i] - min]++;

	// 排序
	int k = 0;
	for (int j = 0; j < range; j++)
	{
    
    
		while (countA[j]--)
			a[k++] = j + min;
	}

	free(countA);
}
  1. 時間複雑度と空間複雑度:
      時間複雑度と空間複雑度は, それ自体の要素の間隔スパンによって決まります. 時間複雑度は O(MAX(n, range)), 空間複雑度は O(range ).
  2. グラフィック:
    ここに画像の説明を挿入

ソートアルゴリズムの複雑性と安定性の分析

ソートアルゴリズム 平均的なケース 最良の場合 最悪の場合 補助スペース 安定
バブルソート O(n 2 ) の上) O(n 2 ) O(1) 安定させる
単純選択ソート O(n 2 ) O(n 2 ) O(n 2 ) O(1) 不安定
直接挿入ソート O(n 2 ) の上) O(n 2 ) O(1) 安定させる
ヒルソート O(nlogn)~O(n 2 ) O(n 1.3 ) O(n 2 ) O(1) 不安定
ヒープソート O(nlogn) O(nlogn) O(nlogn) O(1) 不安定
マージソート O(nlogn) O(nlogn) O(nlogn) の上) 安定させる
クイックソート O(nlogn) O(nlogn) O(n 2 ) O(nlogn)~O(n) 不安定

さまざまなアルゴリズムの動作効率

  データが大きすぎるとスタック オーバーフローが発生する可能性があるため、再帰的でないクイック ソートとマージ ソートを選択してテストします。

void TestOP()
{
    
    
	srand(time(0));
	const int N = 50000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
    
    
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];

	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	BubbleSort(a5, N);
	int end5 = clock();

	int begin6 = clock();
	QuickSortNonR(a6, 0, N - 1);
	int end6 = clock();

	int begin7 = clock();
	MergeSortNonR(a7, N);
	int end7 = clock();

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end5 - begin5);
	printf("QuickSort:%d\n", end6 - begin6);
	printf("MergeSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/qq_47658735/article/details/129780381