笔记: 排序算法——快速排序(C++实现)

  1. 快速排序介绍
    简单介绍:快排与归并排序一样,也使用分治思想。快排通常是实际排序应用中最好的选择,因为它的平均性能非常好,而且还能够进行原址排序.
    时间复杂度:最坏情况是 Θ ( n 2 ) \Theta(n^2) Θ(n2);期望时间复杂度是 Θ ( n lg ⁡ n ) \Theta(n\lg n) Θ(nlgn)
    分治过程:
    分:将数组A[p…r]划分为两个子数组A[p…q-1]和A[q+1…r],使得A[p…q-1]中的每一个元素都小于等于A[q],而A[q+1…r]中的每一个元素都大于A[q]。其中下标q也是划分过程的一部分.
    治:递归调用快排,对两个子数组进行排序.
    合:因为是原址排序,所以不需要再进行合并.

  2. 快速排序的伪代码(截图于算法导论)
    a)quicksort: 对输入数组进行排序
    笔记: 排序算法——快速排序(C++实现)_第1张图片
    b)partition: 对输入数组A[p…r]进行原址重排.
    笔记: 排序算法——快速排序(C++实现)_第2张图片
    c)randomized_partition: 随机选取主元
    randomized_parttion
    标注:这里采用随机选取主元的方式,以使得算法实现随机化,从而使得对所有输入都能获得较好的结果,尽量避免最坏情况出现.

  3. C++实现:需要注意的是,C++的数组起始是0,而上述算法的起始是1
    a) 递归实现

#include 
#include    // 后面循环实现需要用到stack
using namespace std;
/**
快速排序  2020.04.20
Author: 豆奶
Reference: 算法导论第三版
  **/


/**
定义partition函数实现对子数组A[p, r]的原址重排.
Input:
	A[]:int[],是需要排序的数组. 指针传递,函数内部对A的修改能导致外部A的变化
	p: int,是待排序数组的起始位置.
	r: int,是待排序数组的结尾位置.
Output:
	i+1: int
  **/
int partition(int A[], int p, int r){
	int x = A[r];  // 选取主元
	int i = p-1;
	for(int j=p; j<r; j++){
		if( A[j]<= x ){
			i = i+1;
			swap(A[i], A[j]);
		}
	}
	swap(A[i+1], A[r]);
	return i+1;
}

/*
定义randdomized_partition函数实现随机选取主元.
Input:
	A[]:int[],是需要排序的数组. 指针传递,函数内部对A的修改能导致外部A的变化
	p: int,是待排序数组的起始位置.
	r: int,是待排序数组的结尾位置.
Output:
	i+1: int
  */
int randdomized_partition(int A[], int p, int r){
	int i = rand() % (r-p+1) + p; // 随机选取主元,表示p~r 之间的一个随机整数。
	swap(A[r], A[i]);
	return partition(A, p, r);
}

/**
定义mid_partition函数实现取中间值为主元. 当然不是全部元素的中间值, 而是首元素, 尾元素和中间元素的中间值
**/
int mid_partition(int A[], int p, int r){
	int c = (p+r)/2; // 取中间值为主元,表示p~r 之间的一个随机整数。
	if( A[c] < A[p] )
        swap( A[p], A[c] );
    if( A[r] < A[p] )
        swap( A[p], A[r] );
    if( A[r] < A[c] )
        swap( A[c], A[r] );

    swap( A[c], A[r] );
	return partition(A, p, r);
}

/**
定义quicksort函数实现
快速排序
Input:
	A[]:int[],是需要排序的数组. 指针传递,函数内部对A的修改能导致外部A的变化
	p: int,是待排序数组的起始位置.
	r: int,是待排序数组的结尾位置.
Output:
	A: int*, 排序后的数组.
  **/
void quicksort(int A[], int p, int r){
	if(p<r){
		int q = randdomized_partition(A, p, r); //采用随机选主元策略.
		// int q = partition(A, p, r);  // 采用选尾元素作为主元.
		quicksort(A, p, q-1);
		quicksort(A, q+1, r);
	}

}

b) 循环实现

/**
定义quicksort2函数用循环方法实现快速排序
Input:
	A[]:int[],是需要排序的数组. 指针传递,函数内部对A的修改能导致外部A的变化
	p: int,是待排序数组的起始位置.
	r: int,是待排序数组的结尾位置.
Output:
	A: int*, 排序后的数组.
  **/
void quicksort2(int A[], int p, int r){
	if(p<r){
		stack<int> mystack;
		mystack.push(p);
		mystack.push(r);
		int high, low, mid;
		while(mystack.size()!=0)
		{
			high = mystack.top(); mystack.pop(); // 下标大的先出栈
			low = mystack.top(); mystack.pop();  // 下标小的后出栈
			
			mid = randdomized_partition(A, low, high);  //随机选主元
			if(mid+1<high)            // 保证每次partition至少有两个元素
			{
				mystack.push(mid+1);  // 下标小的先入栈
				mystack.push(high);   // 下标大的后入栈
			}
			if(low<mid-1)
			{
				mystack.push(low);
				mystack.push(mid-1);
			}

		}		
	}
}

标注:
a) 为什么要用循环的方法再实现一次快排呢? 因为循环的方式更快, 消耗内存更少, 具体可以查看 循环与迭代的区别.
b) 为什么上面有三种选主元的方式呢(选取尾元素为主元, 随机选主元, 三元素中值选主元), 后面两种方式都是为了提高partition的速度, 尽量避免最坏情况出现. 可以通过给一个排好序的数组排序并且计时, 验证后两种方法是比较快速的…
c) 循环和部分选主元的程序就不在这里写测试了…

  1. 测试
int main(){
	int A[15]={3, 19, 13, 5, 6, 28, 33, 38, 43, 12, 53, 58, 63, 2, 73};
	int i;

	cout<<"A[i]的值为:  ";
	for(i=0; i<15; i++){
		cout<<A[i]<<",  ";
	}
	cout<<endl<<endl;

	int length = sizeof(A)/sizeof(A[0]); 
	//sizeof()函数可以返回数组所占的内存,而sizeof(a[0])返回的是数组第一个元素所占的内存。

	quicksort(A, 0, length-1);

	cout<<"排序后A[i]的值为:  ";
	for(i=0; i<15; i++){
		cout<<A[i]<<",  ";
	}
	cout<<endl<<endl;
	return 0;
}

/**
测试结果:
A[i]的值为:  3,  19,  13,  5,  6,  28,  33,  38,  43,  12,  53,  58,  63,  2,  73,

排序后A[i]的值为:  2,  3,  5,  6,  12,  13,  19,  28,  33,  38,  43,  53,  58,  63,  73,
**/
  1. 参考文献
  • 算法导论(第三版)

如有不对之处,还望指出。

你可能感兴趣的:(数据结构)