[データ構造] この記事では、ソート (以下) - バブル ソート、クイック ソート、マージ ソート、カウンティング ソートについて包括的に理解することができます。

 

目次

1. 共通のソートアルゴリズムの実装 

 1.1 交換ソート

1.1.1 基本的な考え方

1.1.2 バブルソート 

1.1.3 クイックソート

1.2 マージソート

1.3 非比較ソート

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


 過去の怠惰の代償を支払わなければなりません。


1. 共通のソートアルゴリズムの実装 

 1.1 交換ソート

1.1.1 基本的な考え方

基本的な考え方: いわゆる交換とは、シーケンス内の 2 つのレコードのキー値の比較結果に従って、シーケンス内の 2 つのレコードの位置を交換することです。キー値が小さいレコードは、シーケンス内の 2 つのレコードの位置を交換します。シーケンスの先頭。

1.1.2 バブルソート 

詳細については、「バブルソートリンク」を参照してください。

バブルソート:

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//趟数
	{
		int end = n - i - 1;
		for (int j = 0; j < end; ++j)//交换次数
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

バブルソートの最適化: [最初の交換が実行されるとき、交換は実行されず、配列が正しいことを示すため、後続のバブリング操作を実行する必要はありません] 

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//趟数
	{
		int exchange = 0;
		int end = n - i - 1;
		for (int j = 0; j < end; ++j)//交换次数
		{
			if (a[j] > a[j + 1])
			{
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

直接挿入ソートと最適化されたバブル ソートを比較します。順序が順序付けされている場合、両者は同じですが、部分的に順序付けされているか、順序付けに近い場合は、挿入の適応性と比較の数が少なくなります。

1. バブルソートはとてもわかりやすいソートです

2. 時間計算量: O(N^2)

3. 空間の複雑さ: O(1)

4.安定性: 安定しています

1.1.3 クイックソート

クイックソートは、1962 年に Hoare によって提案された 2 分木構造交換ソート手法です。その基本的な考え方は、ソートされる要素のシーケンス内の任意の要素が基準値とみなされ、ソートされる集合が基準値に従って 2 つの部分列に分割されるというものです。ソート コードに従って、左側のサブシーケンスのすべての要素が基準値より小さく、右側のサブシーケンスのすべての要素が基準値より大きい場合、すべての要素が対応する位置に配置されるまで、最も左のサブシーケンスでプロセスが繰り返されます


 シングルパス並べ替え: キー (通常は最初の番号または最後の番号) を選択します。並べ替え後、左側の値はキーより小さい必要があり、右側の値はキーより大きい必要があります。

基準値に従って間隔を左半分と右半分に分割する一般的な方法は次のとおりです (シングルパス ソート)。

(1)ホアバージョン(ホール)

左側のキー (最初の数字)

まず、右側の右側はキーより小さいデータを探し、見つかったら停止します。次に、左側の左側はキーより大きいデータを探し、見つかったら停止します。そして、左側とキーが一致するまでデータを交換します。右に一致し、位置の値とキー交換を比較します。

[[左が小さい]交換後は右側が先になるので、待ち合わせ位置はキーより小さい数字でなければなりません]

[ミートの状況は左が主導権を握って右をミートする場合と、右が主導権を持って左をミートする場合の2通りしかなく、どちらもキーより小さい位置で止まる]

キーは右側にあります (最後の番号)

まず、左側の左側はキーより大きいデータを探し、見つかったら停止します。次に右側の右側はキーより小さいデータを探し、見つかったら停止し、左側とキーが一致するまでデータを交換します。右に一致し、位置の値とキー交換を比較します。

[[左が小さい]交換後は左側が先になるので、ミート位置はキーより大きい数字にする必要があります]

[ミートの場面は左が主導権を握って右をミートする場合と、右が主導権を持って左をミートする場合の2通りしかなく、どちらもキーより大きい位置で止まる]

コード 1 は次を示します

void PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
}

詳細: (1) [キーが左側にある] 右が歩いているとき、左側の数字が常にキー以下である場合、常に -- になり、境界を越えてしまうので、左を追加します。 <右

(2) 掘削方法

左側のキー (最初の数字)

まずキーの値を保存し、この位置がピットとなり、次に右側の右側がキーより小さいデータを探し、見つかったらピットを埋めます。は穴を形成し、左側の左側はキーよりもデータを見つける必要があります。大きなデータの場合は、右側の穴を見つけた後、左右が出会うまで埋め、キーが出会ったときに穴を埋めます [左が穴、右が先、右が穴、左が先]

右側のキー (最後の数字)についても同様です。

hoareと比べるとコードはほぼ同じですが、 (1) ミーティング位置がキーより小さい理由を理解する必要はありません (2) 左側がキーであることを理解する必要はありません。側面がキーで、右側が最初になります

コード 2 は次を示します

int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int pit = left;
	while (left < right)
	{

		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];
		pit = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[pit] = a[left];
		pit = left;
	}
	a[pit] = key;
	return pit;
}

(3) 前後ポインタ方式

左側のキー (最初の数字)

2 つのポインター、1 つは前へ、もう 1 つは現在。最初に、prev は keyi の位置にあり、cur は keyi の次の位置にあります; cur が prev++ を見つけた場合、prev と cur の値を交換します; [cur と prev++ が同じ位置にある場合、データを交換する必要はありません: if cur の最初の位置がキーより小さいため、prev++ が必要で、その後位置が交換されますが、同じ位置では必要ありません]【while(cur <= right)】

prev と cur の関係: (1) cur は key より大きい値に遭遇していない、prev は次々と cur に従う (2) cur は key より大きい値に遭遇する、prev と cur の間には距離がある主要な値。

コード 3 は次を示します。

int PartSort3(int* a, int left, int right)
{
	int key = a[left];
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key && a[cur] != a[++prev])//这个条件,只有前面条件符合才会走后面的条件
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[left], &a[prev]);//这里不能写&key,因为并不能改变a[left]的值,不要和局部变量交换
	return prev;
}

キーは右側 (最後の数字)

2 つのポインター、1 つは前へ、もう 1 つは現在。最初にprevがleft-1の位置にあり、curが左にある; curがprev++の値を見つけたら、prevとcurの値を交換する; [curとprev++が同じ位置にある場合は、そこにデータを交換する必要はありません: if cur 最初の位置がキーより小さいため、 prev++ が必要で、その後位置が交換されますが、同じ位置では必要ありません] [while (cur <= right - 1 ]

int PartSort4(int* a, int left, int right)
{
	int key = a[right];
	int prev = left - 1;
	int cur = left;
	while (cur <= right - 1)
	{
		if (a[cur] < key && a[cur] != a[++prev])//这个条件,只有前面条件符合才会走后面的条件
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[right], &a[++prev]);//这里不能写&key,因为并不能改变a[left]的值,不要和局部变量交换
	return prev;
}

全体のソート: 1 パスのソート後、キーは正しい位置に配置されているため、移動する必要はありません。この時点で、左側が順序通りで右側が順序通りであれば、全体の順序は決まります。を保証できる [部分問題を解決するための分割統治]

コード表示: (hoare シングルパス ソート + 分割統治全体ソート) 

//快速排序

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

1. クイック ソートの全体的な総合的なパフォーマンスと使用シナリオは比較的良好であるため、あえてクイックソートと呼びます。

2. 時間計算量: O(N*logN) クイックソートを呼び出すたびに、すべての要素を走査する必要があります。

最良の場合: キーが選択されるたびに、中央値は O(N*logN) になります。 最悪の場合: キーが選択されるたびに、最大数または最小数は O(N^2) [順序付けされた、またはシーケンスに近い] になります。

3. 空間複雑度: O(logN)

4. 安定性: 不安定


 クイックソートの最適化 

 最悪のケースのため、キーが最大または最小の数値にならないようにキーを最適化できます。(1) キーをランダムに選択します。(2) 3 つの数値に対して大きすぎず、小さすぎない数値を選択します。すると、この番号と左または右の位置のデータが交換されます。

コード表示:

int GetMinIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

int PartSort1(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

セル間の最適化: 間隔が非常に小さい場合、再帰的パーティショニングを使用する代わりに、挿入ソートを使用してセル間を直接ソートし、再帰呼び出しを減らします。

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	if (end - begin + 1 <= 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort1(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

クイックソート非再帰的(スタック)

void QucikSort(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort(a, left, right);
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi -1);
		}
		if (right > keyi + 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, right);
		}
	}
	StackDestory(&st);
}

 再帰を非再帰に変更します: (1) ループを使用します (2) スタックを使用します [再帰にはスタック爆発の危険があります]

1.2 マージソート

最初に各サブシーケンスを順序付けし、次にサブシーケンス セグメントを順序付けします。2 つの順序付きテーブルを 1 つの順序付きテーブルにマージする場合 [まず左右の間隔を順序どおりにしてから、2 つの間隔をマージします]


配列が順序付けされていると仮定して、配列が左と右の配列に分割され、2 つの順序付けされた配列が 1 つの順序付けされた配列にマージされます。(2 つの配列の小さい方のデータを取得し、それに応じて新しい配列に挿入します) ただし、この配列は順序付けされていないため、データが 1 つになるかデータがなくなるまで配列を分割し、順序付けされているとみなしてから、順番にマージします。そして配列は整います。[まず分割してから結合]

コード表示: [再帰]

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;//中间值
	_MergeSort(a, begin, mid, tmp);//左边有序
	_MergeSort(a, mid + 1, end, tmp);//右边有序
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = 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)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

(1)mid は (最初のデータの添字 + 最後のデータの添字)/2 に等しく、分割される 2 つの区間は [left,mid][mid+1,right] でなければなりません。そうしないと無限ループが発生します。

(2) 分割後にマージし、分割後にマージソートを呼び出し、呼び出し後にマージソートを書き込みます。[分割された 2 つの配列が整った後、マージされます] [関数のメインフレーム、左側がソート + 右側がソート + マージ]

(3) 結合後、内容を元の配列にコピーします。[マージ時には新しい配列が必要です]

(4) ポストオーダートラバーサルと同様

1. マージの欠点は、O(N) スペースの複雑さを必要とすることです。マージとソートの考え方は、ディスク内の外部ソートの問題を解決することを目的としています。

2. 時間計算量: O(N*logN) N データ、logN 層 

3. 空間計算量: O(N) 時間計算量は O(N + logN) ですが、logN は無視できます。

4.安定性: 安定しています

コード表示: [非再帰]

//归并排序
//非递归
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (end1 >= n)
				end1 = n - 1;
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			// begin2没有越界, end2越界,修正end2即可
			if (begin2 < n && end2 >= n)
				end2 = n - 1;

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

			while (begin1 <= end1)
				tmp[index++] = a[begin1++];

			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
		}
		memcpy(a, tmp, n * sizeof(int));
		gap *= 2;
	}
	free(tmp);
}

まず、ギャップは 1 でマージします。このとき、2 つのデータのグループが順番に並んでいます。次に、ギャップ = ギャップ*2 = 2 により、ギャップが >= n であるかどうかを判断し、ギャップを 2 としてマージします。このとき、4 つのデータのグループが順序どおりに、ギャップ = ギャップ*2 = 4 でギャップが >= n であるかどうかを判断し、ギャップを 4 としてマージします...最後まで、ギャップは >= n [ギャップはどのくらいか、マージされるグループの数]

範囲外の問題: begin1 は範囲外にはなりません (begin1 は i に等しく、i は n 未満です) end1、begin2、end2 は範囲外になる可能性があります。

(1) end2 のみが範囲外です。修正してください (n-1)

(2) Begin2 が範囲外であり、end2 も範囲外であり、マージされたデータの 2 番目のセットがこの時点で範囲外である場合、begin2 と end2 は修正する必要がなく、2 番目の間隔セットは修正する必要があります。存在しない

(3) end1 が範囲外です。修正すると、2 番目の間隔グループは存在しません。

時間計算量: O(N*logN) 空間計算量: O(N) 

デバッグのヒント:

// 条件断点
			if (begin1 == 8 && end1 == 9 && begin2 == 9 && end2 == 9)
			{
				int x = 0;
			}

ある場所で止めたいけど面倒な場合は、条件付きブレークポイントを直接書いてブレークポイントを叩けばOKです。

1.3 非比較ソート

比較ソート:直接挿入ソート、ヒルソート、選択ソート、ヒープソート、バブルソート、クイックソート、マージソート

非比較ソート: (1) カウンティングソート (2) 基数ソート、バケットソート

カウンティング ソートは、鳩の巣原理としても知られ、ハッシュ ダイレクト アドレス指定方法を修正したものです。

カウントソートコード表示:

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = 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*)malloc(sizeof(int) * range);
	if (countA == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	memset(countA, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = i + min;
		}
	}
}

まずデータの最大値の最小値を見つけ、次に +1 を引いて新しい配列のサイズを計算し、次に新しい配列の値を 0 に割り当てます。次に、前の配列を走査し、走査された数 -minは新しい配列の添字です。この添字の値は +1 です。前の配列を走査した後、新しい配列の値を前の配列に返します。

1. カウントソートはデータ範囲が集中している場合に効率が高くなりますが、適用範囲やシナリオが限定されます。(データ範囲セットに適用) (整数に適用、負の数も許容されます。他の型は使用できません)

2. 時間計算量: O(MAX(N,range))

3. 空間複雑度: O(範囲)

4.安定性: 安定しています

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

おすすめ

転載: blog.csdn.net/m0_57388581/article/details/131875540