【C言語】クイックソート


ここに画像の説明を挿入します

1.ホアバージョン

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

アルゴリズムのアイデア:

  1. keyi を定義し、乱数を格納します。キーの添字は配列の最初の要素に変更されます。ここでは、キーは配列の最初の要素に直接デフォルト設定されます。
  2. left と right を定義し、移動と交換のために配列の最初と最後の要素の添字をそれぞれ保存します。
  3. 昇順で右側から左に移動させ、キーの値より小さい要素が見つかったら停止して左に移動します。
  4. left は右に移動し、キー値より大きい要素が見つかったときに停止します。
  5. 添字付きの要素を左右に入れ替えます
  6. 左右が一致する(等しくなる)まで上記の操作を繰り返します。
  7. キーと要素を下付き文字のまま入れ替えます
  8. このとき、キーの左側はそれより小さい数字、右側はそれより大きい数字になります。
  9. 次に、左と右のシーケンスに対してそれぞれ上記のシングル パス ソートを実行し、左と右のシーケンスに要素が 1 つだけ含まれるか要素がなくなるまで操作を繰り返し、操作を停止すると、シーケンスが順序付けできるようになります。

hoare バージョンのシングルパスソート図:

ここに画像の説明を挿入します

ホーアのバージョンコード:

//交换
void Swap(int* a, int* b)
{
    
    
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//hoare版本
void QuickSort1(int* a, int begin, int end)
{
    
    
	//递归结束条件
	if (begin >= end)
	{
    
    
		return;
	}
	int keyi = begin;
	int left = begin;
	int right = end;
	//每趟排序直到左右相遇
	while (left < right)
	{
    
    
		//右边先走,找到比key值小的
		while (left < right && a[right] >= a[keyi])
		{
    
    
			right--;
		}
		//right找到比key值小的之后换到left走,找到比key值大的
		while (left < right && a[left] <= a[keyi])
		{
    
    
			left++;
		}
		//交换
		Swap(&a[left], &a[right]);
	}
	//将key值换到中间
	Swap(&a[keyi], &a[left]);
	//更新key
	keyi = left;
	//对左右序列继续排序
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

全体的なフローチャート:

ここに画像の説明を挿入します

2. 掘削方法

穴を掘るというアイデア:

  1. まず最初のデータを変数キーに格納し、これを初期ピット位置として使用し、添字ホールで記録します。
  2. 次に、右が前進を開始し、キーの値より小さい要素を見つけて停止し、その要素を穴 (下付き文字は穴) に入れると、この場所が穴になり、この時点で穴が右になります。
  3. 次に、左が逆方向に移動し始め、キー値より大きい要素を見つけて停止し、この要素を穴 (下付き文字は穴) に入れると、その場所は穴になり、このとき穴は左になります。
  4. 次に、右の動きに戻り、左右が交わるまで繰り返します(左右が交わる場所は穴でなければなりません)。
  5. 次に、左右が交わる位置、つまり穴の位置にキーを差し込みます このとき、穴の左側がそれ以下、右側がそれ以上になります。
  6. このようにして、シングルパスソートは終了し、穴の左右のシーケンスに対して上記の操作を繰り返し実行し続けます。左右のシーケンスに要素が 1 つしかない場合、または要素がまったくない場合、操作は停止し、シーケンスは次のようになります。命令される。

ピット掘削法のシングルトリップ選別の図:

ここに画像の説明を挿入します

掘削方法のコード:

//挖坑法
void QuickSort2(int* a, int begin, int end)
{
    
    
	//递归结束条件
	if (begin >= end)
	{
    
    
		return;
	}
	int left = begin;
	int right = end;
	int key = a[left];
	//坑最初与left一样在开始位置
	int hole = left;
	//每趟排序直到左右相遇
	while (left < right)
	{
    
    
		//右边先走,找到比key值小的
		while (left < right && a[right] >= key)
		{
    
    
			right--;
		}
		//将right找到的比key小的元素放进坑中
		a[hole] = a[right];
		//更新坑的位置
		hole = right;

		//然后左边走找到比key值大的元素停下来
		while (left < right && a[left] <= key)
		{
    
    
			left++;
		}
		//将left找到的比key大的元素放进坑中
		a[hole] = a[left];
		//更新坑的位置
		hole = left;
	}
	//将key放入坑中
	a[hole] = key;
	//对左右序列继续排序
	QuickSort2(a, begin, hole - 1);
	QuickSort2(a, hole+1, end);
}

3. 前後ポインタ方式

前後ポインタ方式の考え方:

  1. keyi を定義し、乱数を格納します。キーの添字は配列の最初の要素に変更されます。ここでは、キーは配列の最初の要素に直接デフォルト設定されます。
  2. prev を最初の要素の添え字として定義し、cur を prev の次の要素の添え字として定義します。
  3. cur 添字の値がキーと比較され、cur はキーより小さい値が見つかるまで停止します。
  4. prev 添字は 1 つ後ろに移動し、cur 添字の値と交換され、その後 cur は 1 つ後ろに移動します (prev は、キーより小さい前の数値の最後の添字に相当するため、次のようにシフトする必要があります) 1 つを交換してから交換します)
  5. Cur は key より小さい値を検索し続け、cur の値が n より大きくなるまで繰り返し実行します。
  6. キーを prev の添字の値と交換します。このとき、キーの左側はキー以下、キーの右側はキー以上になります。
  7. このようにして、シングルパスソートが終了し、キーの左右のシーケンスに対して上記の操作を繰り返し実行し続けます。左右のシーケンスに要素が 1 つしかない、または要素がまったくない場合、操作は停止し、シーケンスは次のようになります。命令される。

前後ポインタ方式によるシングルパスソートの図:

ここに画像の説明を挿入します

ポインター メソッド コードの前後:

//交换
void Swap(int* a, int* b)
{
    
    
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//前后指针
void QuickSort3(int* a, int begin, int end)
{
    
    
	//递归结束条件
	if (begin >= end)
	{
    
    
		return;
	}

	int keyi = begin;
	int prev = begin;
	int cur = begin + 1;
	//每趟排序直到cur下标大于end
	while (cur <= end)
	{
    
    
		//cur找比key小的值
		if (a[cur] < a[keyi] && ++prev != cur)
		{
    
    
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	//将key换到中间
	Swap(&a[keyi], &a[prev]);
	//更新key的下标
	keyi = prev;
	//对左右序列继续排序
	QuickSort3(a, begin, keyi - 1);
	QuickSort3(a, keyi + 1, end);
}

クイック ソートは不安定なソートであり、時間計算量は O(N*logN) ですが、最悪の場合は O(N 2 ) に達する可能性があり、空間計算量は O(logN) です。

4. 非再帰的なクイックソート

上記 3 つの方法は、分割統治法を使用して再帰的に実装されたクイック ソートですが、実際には非再帰的に実装することもでき、非再帰的なクイック ソートはスタックを使用して実装する必要があります。

アイデア:

配列の最初と最後の添字をスタックに格納し、ループ内で左と右として取り出して配列をソートし、取得したキーの左と右のシーケンスに対して同じ操作を実行します。ここで、左側は左が keyi-1 で、右側が keyi+1 から右である場合、これらの添字がスタックにプッシュされる順序は、取り出される順序によって異なります。たとえば、次のコードでは、後の要素が最初に取り出されるため、スタックにプッシュするときは、後の要素を最初に入れる必要があります。これは、スタックの特性が先入れ後出しであるためです。

ここに画像の説明を挿入します

非再帰的なクイックソートコード:

(このコードで使用するスタックは自分で実装する必要があります。C言語でのスタック実装については、スタック実装を参照してください。)

//非递归快速排序
void QuickSortNonR(int* a, int begin, int end)
{
    
    
	//创建一个栈
	ST st;
	//初始化栈
	STInit(&st);
	//插入尾元素下标
	STPush(&st, end);
	//插入首元素下标
	STPush(&st, begin);
	//栈为空停下
	while (!STEmpty(&st))
	{
    
    
		//取出栈顶元素作为left
		int left = STTop(&st);
		//取出后在栈中删除
		STPop(&st);

		//取出栈顶元素作为right
		int right = STTop(&st);
		//取出后在栈中删除
		STPop(&st);

		int keyi = begin;
		//每趟排序直到左右相遇
		while (left < right)
		{
    
    
			//右边先走,找到比key值小的
			while (left < right && a[right] >= a[keyi])
			{
    
    
				right--;
			}
			//right找到比key值小的之后换到left走,找到比key值大的
			while (left < right && a[left] <= a[keyi])
			{
    
    
				left++;
			}
			//交换
			Swap(&a[left], &a[right]);
		}
		//将key值换到中间
		Swap(&a[keyi], &a[left]);
		//更新key的下标
		keyi = left;
		// 当前数组下标样子  [left,keyi-1] keyi [keyi+1, right]

		//右边还有元素,按顺序插入right和keyi+1
		if (keyi + 1 < right)
		{
    
    
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}
		//左边还有元素,按顺序插入keyi-1和left
		if (left < keyi - 1)
		{
    
    
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
	}

	STDestroy(&st);
}

5. クイックソートの最適化

1. 3 つの数字からキーの値を選択します

最初の 3 つのクイック ソート メソッドはすべて、最初にキーとして値をランダムに選択する必要があります。以前は配列の最初の要素を直接デフォルトとして使用していました。これは十分にランダムではなく、最悪のシナリオが発生する傾向があり、時間が複雑になります。 O(N 2 ) に近いため、最初の要素を直接選択するのではなく、よりランダムになるようにこのキーを選択する関数を作成できます。

3 つの数字の中から正しい数字を当ててください。

配列の先頭、最後、中央の 3 つの位置の中央にある番号を選択します

// 三数取中
int GetMidi(int* a, int left, int right)
{
    
    
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
    
    
		if (a[right] > a[mid])
		{
    
    
			return right;
		}
		else if(a[mid]>a[right]&&a[mid]<a[left])
		{
    
    
			return mid;
		}
		else
		{
    
    
			return left;
		}
	}
	else
	{
    
    
		if (a[left] > a[mid])
		{
    
    
			return left;
		}
		else if (a[mid] > a[left] && a[mid] < a[right])
		{
    
    
			return mid;
		}
		else
		{
    
    
			return right;
		}
	}
}

クイック ソート中に、3 つの中間の方法を使用してキー値を選択し、それを配列の先頭で置き換えます。これにより、最悪のシナリオを効果的に回避し、アルゴリズムの効率を大幅に向上させることができます。

2. セル間の最適化

再帰データが小さい場合、挿入ソートを使用すると、セルが再帰的に分割されるのを防ぎ、再帰の回数を減らすことができます。

6. コードのテスト

//打印数组
void PrintArray(int* a, int n)
{
    
    
	for (int i = 0; i < n; i++)
	{
    
    
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestQuickSort1()
{
    
    
	int a[] = {
    
     9,1,2,5,7,4,8,6,3,5,1,2,3,5,1,8,3 };
	QuickSort1(a, 0, sizeof(a) / sizeof(int) - 1);
	printf("hoare版本快速排序:\n");
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestQuickSort2()
{
    
    
	int a[] = {
    
     9,1,2,5,7,4,8,6,3,5,1,2,3,5,1,8,3 };
	QuickSort2(a, 0, sizeof(a) / sizeof(int) - 1);
	printf("挖坑法快速排序:\n");
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestQuickSort3()
{
    
    
	int a[] = {
    
     9,1,2,5,7,4,8,6,3,5,1,2,3,5,1,8,3 };
	QuickSort3(a, 0, sizeof(a) / sizeof(int) - 1);
	printf("前后指针法快速排序:\n");
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
    
    
	TestQuickSort1();
	TestQuickSort2();
	TestQuickSort3();
	return 0;
}

ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/zcxyywd/article/details/133270717