排序 - 快速排序(C++)

前言的前言:

如果看不懂这些干巴巴的理论文字,那就先不用看了,下面代码中有详细的注释,大家可以先跟着代码走几遍,回过头来再看这些文字描述,总之:纸上得来终觉浅,绝知此事要躬行。

前言:

排序算法哪家强,从实际应用的角度上将,快排表现很好。很自然地,人们会觉得短数组比长数组更好处理,因此可能会想到将原始数组分为若干各子部分然后分别进行排序。快速排序就是基于分治法的排序算法,这不过它更多地侧重于“分”,没有明显的“合”过程。

快速排序的要点:

1.快排算法的主要思想:

(1)从待排序序列S中“任意”选择一个记录k作为轴值(Pivot)。
(2)将剩余的记录分割(partition)成左子序列L和右子序列R。
(3)L中所有记录都小于或等于k,R中记录都大于等于k,因此k正好位于正确的位置。
(4)对子序列L和R递归进行快速排序,直到子序列中只含有0或1个元素,退出递归。

2.如何选择轴值pivot,这对快排的时间性能影响很大,轴值的选择应尽量使得序列可以据此划分为均匀的两半。

3.如何实现最关键的分割的过程:最简单的分割方法就是左指针l和右指针r(下标)分别从序列的左端、右端向序列中间扫描;左边越过那些小于等于pivot的值,停在第一个大于pivot的值Kl;右边越过那些大于等于pivot的值,停在第一个小于pivot的值Kr;交换逆置记录Kl和Kr;从交换后的位置,继续从左右向中间扫描,发现并交换逆置记录对,直到l、r交叉而整个序列扫描完毕。这种方法适用于需要处理l、r边界,轴值最后定位等情况。
4. 由于递归开销过大,所以当子序列足够短时,我们采用插入排序来完成对子序列的排序。

5. 快速排序之所以快是因为:它每次选定轴值pivot并进行划分子集(分割交换)后该轴值被一次性的放到了他最终该放到的位置。


下面以一趟分割交换为例:

下标

0

1

2

3

4

5

6

7

初始

25

34

45

32

34`

12

29

64

选基准

25

34

45

29

34`

12

32

64

分割

25

12

45

29

34`

34

32

64

分割

25

12

29

45

34`

34

32

64

基准定位

25

12

29

32

最终位置

34`

34

45

64

完整代码如下:(pivot采用取头、中、尾中位数、分割采用上述介绍的方法)

//#pragma once
//QuickSort.h

#include <iostream>
using namespace std;

const int Cutoff = 28;  //阈值,当子序列元素个数小于Cutoff时,采用简单排序

template <class T>
class Quick
{
private:
	T * A;  //用来存储待排序列的数组
	int N;  //待排序列元素个数

public:
	Quick(int size);  //构造函数
	~Quick() { delete[] A; }  //析构函数
	void QuickSort(int Left, int Right);  //递归进行快排
	T Median3(int Left, int Right);  //选主原(轴值)- 取头、中、尾的中位数
	friend void Swap(T* a, T* b);  //交换两个数 - 友元函数
	void InsertSort(T* B, int Nb);  //当子序列元素个数小于阈值时调用插入排序
	void Print();  //输出结果 
};

//Quick类的实现
//构造函数初始化
template <class T> Quick<T>::Quick(int size)
{
	N = size;
	A = new T[N];

	for (int i = 0; i < N; i++)
		cin >> A[i];
}

//交换两个数
template <class T> Swap(T* a, T* b)
{
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}

//输出结果
template <class T> void Quick<T>::Print()
{
	for (int i = 0; i < N; ++i)
	{
		cout << A[i] << " ";
	}
}

//插入排序
template <class T> void Quick<T>::InsertSort(T* B, int Nb)
{
	int tmp, p, i;
	for (p = 1; p < Nb; ++p)  //总共摸取Nb-1张牌
	{
		tmp = B[p];  //当前摸到手中的一张牌
		for (i = p; i > 0 && A[i - 1] > tmp; i--)  //i很多情况下都是用来控制循环次数的
		{
			A[i] = A[i - 1];
		}
		A[i] = tmp;  //新牌归位
	}
}

//选轴值pivot
template <class T>  T Quick<T>::Median3(int Left, int Right)
{
	int Center = (Left + Right) / 2;

	if (A[Left] > A[Center])
		Swap(&A[Left], &A[Center]);
	if (A[Left] > A[Right])
		Swap(&A[Left], &A[Right]);
	if (A[Center] > A[Right])
		Swap(&A[Center], &A[Right]);
	//此时A[Left] <= A[Center] <= A[Right],为了接下来的分割过程,将轴值藏到右边
	//将轴值藏到子序列的右端只是为了不影响分割过程
	//分割时只需考虑A[Left+1] - A[Right-2]的区间
	Swap(&A[Center], &A[Right - 1]);
	//返回轴值
	return A[Right - 1];
}

//递归分割
template <class T> void Quick<T>::QuickSort(int Left, int Right)
{
	int Pivot, Low, High;  //基准、左、右指针

	if (Cutoff < Right - Left)  //如果序列元素充分多,则进入快排
	{
		Pivot = Median3(Left, Right);
		Low = Left;
		High = Right - 1;

		//分割过程
		while (true)
		{
			while (A[++Low] < Pivot);
			while (A[--High] > Pivot);
			if (Low < High)
				Swap(&A[Low], &A[High]);
			else
				break;
		}
		//将当前子序列的轴值一次性的放到他最终所在的位置上 - Low
		Swap(&A[Low], &A[Right - 1]);
		//当前子序列分割完成,递归进入更小一层子序列的分割
		QuickSort(Left, Low - 1);  //递归解决左边子序列
		QuickSort(Low + 1, Right);  //递归解决右边子序列
	}
	else
		InsertSort(A + Left, Right - Left + 1);  //当子序列足够短时,采用插入排序
}

int main()
{
	int N;  //待排元素个数
	cin >> N;

	//定义Quick对象QuickA
	Quick<int> QuickA(N);
	//快速排序
	QuickA.QuickSort(0, N - 1);
	//输出结果
	QuickA.Print();

	return 0;
}

运行结果:

猜你喜欢

转载自blog.csdn.net/y_16041527/article/details/80460015