快速排序介绍
简单介绍:快排与归并排序一样,也使用分治思想。快排通常是实际排序应用中最好的选择,因为它的平均性能非常好,而且还能够进行原址排序.
时间复杂度:最坏情况是 Θ ( 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也是划分过程的一部分.
治:递归调用快排,对两个子数组进行排序.
合:因为是原址排序,所以不需要再进行合并.
快速排序的伪代码(截图于算法导论)
a)quicksort: 对输入数组进行排序
b)partition: 对输入数组A[p…r]进行原址重排.
c)randomized_partition: 随机选取主元
标注:这里采用随机选取主元的方式,以使得算法实现随机化,从而使得对所有输入都能获得较好的结果,尽量避免最坏情况出现.
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) 循环和部分选主元的程序就不在这里写测试了…
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,
**/
如有不对之处,还望指出。