[データ構造 - 手引きソートアルゴリズム その6] クイックソートを再帰的に実現 (ホールのバージョン、ディギングメソッド、フロントポインタメソッドとバックポインタメソッドを1つの実装メソッドに統合、タイピングが非常に得意)

目次

1. 一般的な並べ替えアルゴリズム

1.1 為替ソートの基本的な考え方

2. クイックソートの実装方法

2.1 基本的な考え方

3 ホーレ (ホール) バージョン

3.1 実装のアイデア

3.2 概念図

3.3 実現アイデアのステップ 2 とステップ 3 を交換できない理由

3.4 hoare バージョンのコード実装

3.5 hoare バージョン コード テスト

4. 掘削方法

4.1 実装のアイデア

4.2 考え方の図

4.3 掘削メソッドのコード実装

4.4 掘削方法のコードテスト

5. フロントポインターとリアポインターのバージョン

5.1 実装のアイデア

5.2 思考の図式

5.3 フロントポインタメソッドとバックポインタメソッドのコード実装

5.4 ポインタメソッド前後のコードテスト

6. 時間計算量の分析

6.1 最良のケース

6.2 最悪の場合

7. 最適化されたクイックソート

7.1 キー選択の最適化

7.2 セル間の最適化


1. 一般的な並べ替えアルゴリズム

1.1 為替ソートの基本的な考え方

バブルソートはエクスチェンジソートの一つですが、まずは以下のバブルソートの考え方を理解しましょう。

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

2. クイックソートの実装方法

再帰的実装はバイナリ ツリーのプリオーダー トラバーサルと非常によく似ており、間隔を左半分と右半分に分割するには 3 つの一般的な方法があります: 1.ホア バージョン、2. ディギング メソッド、3. 左右のポインタ メソッド。

2.1 基本的な考え方

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

したがって、奥の非常に重要なポイントは、小さいものを見つけるには左に進み、大きいものを見つけるには右に行くことです。

3 ホーレ (ホール) バージョン

3.1 実装のアイデア

昇順でソートするように指定しますソートされた配列の名前は a、参照値 key、参照値の添字 keyi、left left、right right です。

1. キーを選択します。キーには、並べ替える必要がある配列内の任意の要素を指定できます。この記事では、キーは a[left] として選択されています

2. 配列の右端 (末尾) から左に進み、a[right] < key(a[keyi]) の場合、右は停止します。

3. 次に、配列の左端 (先頭) から右に進み、a[left] > key(a[keyi]) のとき、left が停止します。

4. a[左]、a[右]を交換します。

5. 手順2、3、4を繰り返します。左右が同じ位置になったら、その位置でエレメントとキーを交換し、キーの位置を決定します。

6. このとき、キーは配列を左右の区間に分割します。最初の 5 ステップを左右の区間に使用し、次に左右の区間のキーを再度決定し、左の区間を再帰して、正しい間隔で再帰して並べ替えを実行します。

注: ステップ 2 と 3 の順序は変更できません。その理由については、図にしてからお話します。

3.2 概念図

私たちの図は実現アイデアに従って描かれています。

3.3 実現アイデアのステップ 2 とステップ 3 を交換できない理由

思考図では、最終的に 3 が見つかったとき、R が最初に進み、R が最初に 3 に出会うことがわかります。L が最初に行く場合は、大きな左を探します。L は 3 に出会っても止まらず、そしては R の位置に直接行きます、R の位置は 9 です、このとき出会ったら交換します、一度交換すると、左の間隔がすべてキー (6) より小さいわけではなく、エラーが発生します。したがって、左側のキーが選択されている場合は、右側が先に進みます。

Q: hoare バージョンはわかりますが、キーを右側で選択した場合、左側が最初になりますか?

A:答えは「はい」です。これは実際には、L が R と出会うかR と L が出会うかという問題です。この問題を考えるために上記の解決策を使用することもできます. L が R に出会ったとき、それは R が停止したことを意味し、R の停止は a[right]<key です. このとき、L が R に出会ったとき、出会う位置は小さくなりますkey よりも (key, a[left]) を交換します。このとき、 key より小さい最後の要素が左端に配置され、 key は特定の位置にあります。再度調整する必要はありません。 key との左の間隔を中間点はすべて key より小さく、右の間隔はすべて key より大きくなります。

鍵は右側にあります R が L に出会ったとき、L が止まったことを意味します L が止まったとき、[左]>鍵を意味します このとき、R が L に出会ったとき、出会った位置は鍵よりも大きいです 交換(a[left], key) とすると、キーより大きい最後の要素が右端に配置され、キーは一定の位置にあるため、再度調整する必要はありません。中間点はすべてキーより小さく、右の間隔はすべてキーより大きくなります。

したがって、キーが左側で選択されている場合は、最初に右に移動し、キーが右側で選択されている場合は、最初に左に移動します。

3.4 hoare バージョンのコード実装

// 快速排序hoare版本
int 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[keyi], &a[left]);

	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
 
 

3.5 hoare バージョン コード テスト

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
 //快速排序递归实现
 //快速排序hoare版本
int 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]);
	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

4. 掘削方法

4.1 実装のアイデア

昇順でソートするように指定しますソートされた配列の名前は a、参照値キー、左左、右右です。

1. キーを選択します。キーは、並べ替える必要がある配列内の任意の要素にすることができます。キーを a[left] として選択し、保存するキーに a[left] を与えます。選択された要素の位置重要なのはピット位置です

2. 配列の右端(末尾)から左に進み、a[right] < key になったら、右が止まり、ピットに a[right] を入れます。ピットは添字の right に置き換えられます。このとき、右は Do です。動かない;

3. 次に配列の左端(先頭)から右に進み、a[left] > キーで左を止め、ピットに a[left] を入れ、ピットを添え字が左の位置に変更します、この時点では左は動きません。

4. 手順 2 と 3 を繰り返します 左右が同じ位置に来たら、その位置が最後のピット位置です このピット位置にキーを置くと、キーの最終的な位置が決まり、何もする必要はありません後で調整します。

5. このとき、キーは配列を左右の区間に分割します。最初の 4 ステップを左右の区間に使用し、次に左右の区間のキーを再度決定し、左の区間を再帰して、正しい間隔で再帰して並べ替えを実行します。

掘削方法とホアバージョンの違い:

1. ピット掘り法では、L と R の最終合流位置の要素がキーよりも小さいと考える必要はありません。最終合流位置はピット位置であるため、キーをピット位置に直接置くだけです。

2. 左側が選択されている場合は右側が最初に、右側が選択されている場合は左側が最初に行われる理由を考えないでください。左側をキーとして選択した場合、左側の穴は必要があります。が埋まるので、右側を先にする必要があります。この文は、左側の小さいものと右側の大きいものを探して理解する方が適切です。

4.2 考え方の図

4.3 掘削メソッドのコード実装

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

4.4 掘削方法のコードテスト

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

5. フロントポインターとリアポインターのバージョン

5.1 実装のアイデア

昇順でソートするように指定しますソートされた配列の名前は a 、参照値は key です

1. キーを選択します。キーは並べ替える配列内の任意の要素にすることができますが、キーを [left] として選択します。

2. prev ポインタと cur ポインタを定義し、配列の先頭位置を指すように prev を初期化し、prev の次の位置を指すように cur を初期化します。Cur が最初に実行され、cur は key より小さい要素を見つけて、それらを見つけた後に停止し、prev++ にしてから (a[cur], a[prev]) を交換します。交換後、戻り続けます。cur が見つけた値はキー以上です。cur は戻り続けます。それを見つけた後、prev++ にして、(a[cur], a[prev]) を交換し、これを繰り返します。ステップ;

3. cur が配列全体を調べ、(a[left], a[prev]) を交換すると、キーの最終位置が決定されます。key は配列を左と右のサブ間隔に分割します。左のサブ間隔はキーより小さく、右のサブ間隔はキーより大きくなります。

4. 左右のサブ区間は最初の 3 ステップを繰り返し続け、配列のソートは再帰的に下に進むことによって実現されます。

5.2 思考の図式

ここでの連続交換とは実際にはキーより小さい値を前方に投げ続け、キーより大きい値を後方に投げ続けることであり、curとprevの間の値は実際には全てキーより大きい値となる。最終的なキーによる除算 左右の音程。左の音程はキーより小さく、右の音程はキーより大きくなります。

5.3 フロントポインタメソッドとバックポインタメソッドのコード実装

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

5.4 ポインタメソッド前後のコードテスト

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

6. 時間計算量の分析

6.1 最良のケース

上記の 3 つのケースでは、最適な時間計算量はO(N*logN)です。

キーが間隔の中央に配置されるたびに、バイナリ ツリーのように logN 回再帰する必要があり、各部分間隔の並べ替えの時間計算量は O(N) であるため、最良のケースは O(N * logN) です。

6.2 最悪の場合

配列がソートされると、キーが左端か右端かに関係なく、時間計算量はO(N^2)になります。

7. 最適化されたクイックソート

迅速なソートの最適化には 2 つのアイデアがあります。

1. キー選択方法を最適化できます。

2. 小さな部分区間まで再帰するには、小さな部分間最適化としても知られる挿入ソートの使用を検討できます。

7.1 キー選択の最適化

キー選択の最適化は、主に順序付けされた、または順序付けに近い配列に対して行われます。

キー選択を最適化するための 2 つのアイデアがあります。

1. キーをランダムに選択します。

2. 3 つの数字に対して選択したキーを取得します。(左、中、右を取り出し、これら 3 つの位置を添字に持つ数値の中間の値をキーとして選択します)。

最初の考え方は制御できないため、2 番目のキーの選択方法が最も適切です。

以下は、3 つの数値を取得するために最適化されたコードです。

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

取得した後、その番号を a[left] と交換します。前方ポインタと後方ポインタの以前の方法を引き続き使用しても問題ありません。ホール版はディギング法と同じ最適化法です。

3 つの数値の選択を最適化しない場合、配列が順序付けされているか、順序付けに近い場合、時間計算量は最悪のケースで O(N^2) になります。3 つの数値の中心を取得した後、配列が順序付けされている場合、時間計算量は依然として O(N * logN) です。

7.2 セル間の最適化

再帰する場合、前に描いた絵でも分かりにくいですが、常に分割していると後からどんどん分割することになり、特にデータ量が多い場合にはスタックの消費が非常に大きくなり、スタックオーバーフローの危険性が生じます。したがって、分割が一定のレベルに達すると、分割を行わずに直接挿入ソートを選択します。通常の状況では、部分間隔データの数が 10 の場合、再帰は行われず、挿入ソートが直接使用されます。

実装コード:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[i + 1];
		
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int left, int right)
{
	//子区间只有一个值,或者子区间不存在的时候递归结束
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

これら 2 つの最適化手法は、時間空間の点である程度の改善が見られますが、クイック ソートの本質は変わっておらず、最適化は元のアイデアに付け加えただけです。

おすすめ

転載: blog.csdn.net/Ljy_cx_21_4_3/article/details/131794152