「ソートアルゴリズム」クイックソートの再帰と非再帰

1.この章の焦点

  1. クイックソート思考

  2. 単一のクイック行を実現する3つの方法(ホア、掘削、フロントおよびリアポインター)

  3. クイックソートの再帰的実装

  4. クイックソート再帰アルゴリズムの時間計算量の計算

  5. クイックソートを最適化する(3つの数値の中央、セル間の最適化)

  6. クイックキューの非再帰的実装(スタックまたはキューの実装)

2.クイックキュー

2.1クイックローのアイデア

クイックソートは本質的に交換ソートです。シングルパスの観点から始めましょう。クイックソートのシングルパスソートでは、配列の正しい位置に配置するキーを選択できます。正しい位置は何ですか。つまり、1回のパスで並べ替えた後、番号(Key)はすでに配置されており、後で変更する必要はありません。正しい位置にあることを確認するにはどうすればよいですか。左側のすべての数字がそれ以下であり、右側のすべての数字がそれ以上である限り、それは正しい位置にあります。(昇順)。

クイックソートシングルパスステップ:配列からキー番号を選択し、通常は左端または右端の番号を選択します。ここでは、配列の左端の番号を選択します。例:5 3 2 8 6 1 10 9 3 4 7、ここでKeyiは0、a[keyi]は5

5が正しい位置になるように配列要素を交換するにはどうすればよいですか?

2.23種類のシングルパスソーティング

クイックソートには、次の3つのシングルパスソートアルゴリズムがあります。

最初のもの:クイックソートアルゴリズムの最初の発明者によって書かれたhoare -----Tony Hoare(Tony  Hoare

この方法は次のとおりです。最初にKeyiを選択し、左右に2つの整数変数を取ります。これらの2つの整数変数は配列の添え字を表し、最初はそれぞれ0とn-1を指します。次に、最初に右に移動し、a [keyi]より大きい数を見つけ、次に右に停止し、左に移動し、a [keyi]より小さい数を見つけ、停止してから、a[左]とa[右]を入れ替えます。右または左の移動の方法で、right == left、つまり、右と左が出会うとき、右と左はa[keyi]よりも小さい数を指している必要があります。次に、a [keyi]とこの小さい数が交換され(ミーティングポイント)、最終的にa [keyi]が正しい位置になります。つまり、左側のすべての数がそれ以下になり、すべて右側の数字はそれ以上です。

アイコン:

 注意する必要があります:最初に右を放し、次に左を放す必要があります。そうでなければ、それらが出会う点はa[keyi]よりも小さい数ではないかもしれません。

参照コード:

int Q_PartSort1(int* a, int begin, int end)//hoare
{
	int keyi = begin;
	int left = begin;
	int right = end;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])//右边找小于a[keyi]的数
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])//左边找大于a[keyi]的数
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//将a[keyi]与相遇点交换(要保证相遇点比a[keyi]小,需要让right先走)
	Swap(&a[keyi], &a[right]);
	return right;
}

二つ目:掘り方

手順:最初にa[keyi]の値をinttempに保存し、次にkeyiを最初に作成します。intleft= 0、int right = n-1

最初に右に進み、a [keyi]未満の数を見つけて、それをa [hole]に置き、hole=rightを更新します。次に、左に戻り、a [keyi]より大きい数を見つけて、それをa [hole]に置き、hole = leftを再度更新してから右に移動し、左が右に等しくなるまで左に移動します。この時点で、待ち合わせ場所はピットでなければならず、最後に臨時雇用者が[穴]に配置されます。

アイコン:

 ホアとは異なり、遭遇点の値が温度よりも小さいことを保証する必要はありません。

参照コード:

int Q_PartSort2(int* a, int begin, int end)//挖坑法
{
	int key = a[begin];
	int hole = begin;
	int left = begin;
	int right = end;
	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;
}

3番目のタイプ:フロントおよびリアポインター方式

左端の添え字左keyi、prev = begin、next = begin+1を取る

次に、小さい数を見つけます。a[keyi]未満の数を見つけた場合は、prev ++にしてから、a[prev]とa[next]を入れ替えます。

nextがnより大きくなるまで終了します。

最後に、a[keyi]とa[prev]を入れ替えます。

アイコン:

 参照コード:

int Q_PartSort3(int* a, int begin, int end)//前后指针法
{
	int keyi = begin;
	int prev = begin;
	int next = begin + 1;
	while (next <= end)
	{
		if (a[next] < a[keyi] && ++prev != next)
		{
			Swap(&a[prev], &a[next]);
		}
		next++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

私たちは皆、3つのシングルパスソーティングをマスターする必要があり、時には次の質問を検討します

上記のデータセットの結果は、3回のシングルパスソート後も同じですが、これは偶然の一致であり、さらにデータを追加すると、シングルパス後の結果が異なる場合があることに注意してください。

初期レコードのキーワードシーケンスのセットが(65、56、72、99、86、25、34、66)であるとすると、最初のキーワード65に基づくクイックソートの結果は()です。
A 34、56、25、65、86、99、72、66
B 25、34、56、65、99、86、72、66
C 34、56、25、65、66、99、86、72
D 34、56、25、65、99、86、72、66
この質問では、使用するシングルパスクイックソートを指定していません。このような質問の場合は、3つのシングルパスすべてを試す必要があります。

2.3クイックソートの再帰的実装

最初に参照コードを配置してから、再帰プロセスの図を描きます。

 参照コード:

void _QuickSort1(int* a,int begin,int end)//递归
{
	if (begin >= end)
	{
		return;
	}
	int keyi = Q_PartSort2(a, begin, end);

	_QuickSort1(a, begin, keyi - 1);
	_QuickSort1(a, keyi + 1, end);
}

 スペース上の都合により、右半分は描かれていません。

2.4クイックソート再帰アルゴリズムの時間計算量

最悪の場合:注文

 おおよその実行回数はT(N)= N + N-1 + N-2 + ..... + 3 + 2+1です。

時間計算量はO(N * N)です

最良の場合:毎回取られるキーは中央値です

logNの高さの完全な二分木に相当します

時間計算量はN*logNです

2.5クイックソートを最適化する

2.5.1最適化1:3つの数値の真ん中を取る

順序付けられた配列は迅速な並べ替えには適していないことがわかっています。この観点から、3つの数値の中央をとる最適化方法があります。

つまり、mid =(left + right)/2を選択します

a [左]、a [右]、a [中央]の3つの数値のうち、値は中央値であり、a[keyi]と交換されます。

 参照コード:

int GetMidIndex(int* a, int begin, int end)
{
	int mid = begin + ((end - begin)>>1);
	if (
		(a[mid] >= a[begin] && a[mid] <= a[end])
		|| 
		(a[mid]>=a[end] && a[mid] <= a[begin])
		)
	{
		return mid;
	}
	if (
		(a[begin]<=a[mid] && a[begin]>=a[end])
		||
		(a[begin] >= a[mid] && a[begin] <= a[end])
		)
	{
		return begin;
	}
	return end;
}

int Q_PartSort3(int* a, int begin, int end)//前后指针法
{
	//三数取中优化
	int ki = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[ki]);
	int keyi = begin;
	int prev = begin;
	int next = begin + 1;
	while (next <= end)
	{
		if (a[next] < a[keyi] && ++prev != next)
		{
			Swap(&a[prev], &a[next]);
		}
		next++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

2.5.2セル間最適化

間隔が非常に小さい場合、挿入ソートが直接使用され、再帰を続行する必要はありません。

 参照コード:

void _QuickSort1(int* a,int begin,int end)//递归
{
	//小区间优化
	if (end - begin + 1 <= 12)
	{
		InsertSort(a, end - begin + 1);
	}
	if (begin >= end)
	{
		return;
	}
	int keyi = Q_PartSort2(a, begin, end);

	_QuickSort1(a, begin, keyi - 1);
	_QuickSort1(a, keyi + 1, end);
}

最後に、一般的な並べ替えはaとnを渡すことです。間隔を渡さないために、カプセル化のレイヤーがここに追加されます。

void QuickSort(int* a, int n)
{
	_QuickSort1(a, 0, n - 1);//递归
}

2.6非再帰的クイックソート

ソートする数が多い場合、スタックオーバーフローが発生する可能性があるため、非再帰的なクイックソートアルゴリズムが必要です。

ここでは、スタック+ループを使用して再帰呼び出しプロセスをシミュレートし、時間効率は再帰呼び出しとそれほど変わりません。

再帰を呼び出すのと本質的に同じ手順

 参照コード:

void _QuickSort2(int* a, int begin, int end)//非递归
{
	ST st;
	STInit(&st);
    //检查传递的end和begin
	if (end > begin)
	{
		STPush(&st, begin);
		STPush(&st, end);
	}
	while (!STEmpty(&st))
	{
		int right = STRear(&st);
		STPop(&st);
		int left = STRear(&st);
		STPop(&st);
		int mid = Q_PartSort2(a, left, right);
        if(left<mid-1)
        {
            STPush(&st, left);
	    	STPush(&st, mid - 1);
        }    

		if(mid+1<right)
        {
            STPush(&st, mid + 1);
			STPush(&st, right);
        }
	}
	STDestroy(&st);
}

キューは、クイックソートの非再帰的アルゴリズムを実装します。

 参照コード:

void _QuickSort3(int* a, int begin, int end)//非递归
{
	Queue q;
	QueueInit(&q);
	if (end > begin)
	{
		QueuePush(&q, begin);
		QueuePush(&q, end);
	}
	while (!QueueEmpty(&q))
	{
		int left = QueueFront(&q);
		QueuePop(&q);
		int right = QueueFront(&q);
		QueuePop(&q);
		int keyi = Q_PartSort1(a, left, right);
		if (left < keyi-1)
		{
			QueuePush(&q, left);
			QueuePush(&q, keyi-1);
		}
		if (keyi + 1 < right)
		{
			QueuePush(&q, keyi+1);
			QueuePush(&q, right);
		}
	}
	QueueDestroy(&q);
}

おすすめ

転載: blog.csdn.net/m0_62171658/article/details/124313424