快速排序曾被评为20世纪十大算法之一,最很多情况来说都是名副其实的最快的排序算法。其实思路并不难,但在具体的代码实现时会有很多细节容易出错。也有一些细节如果不加注意会使排序效率意外降低。
大致思路是:从待排序数组中选出一个值(一般是随即选的),对数组进行分割,把小于轴值的放在左侧,大于轴值的放在右侧。然后分别对左侧数组和右侧数组继续进行排序(递归进行)。一直进行下去,最后可以得到一个有序数组。
快速排序本质上是一种交换排序。我们最熟悉的交换排序可能应该是冒泡排序,但快速排序与冒泡排序最根本的不同是:它采用了分而治之的思想,不再是相邻的去进行交换。这样可以提高一些效率。
几个需要注意的细节:
1.轴值的选择
最简单的选择当然是选择第一个或最后一个值,但这样有一个风险,如果数组原来是正序或逆序的,这样选择效率可能会比较低,因为有一侧的数组元素个数是0,这样最终其实是一个一个元素排的。当然可以用随机数确定下标,但比较麻烦。一般可以选择中间下标值。
2.怎么分割数组
最直接的方法是:确定轴值后,从左侧开始找到第一个大于轴值的元素,下标为l。从右侧开始找到第一个小于轴值的元素,下标为r。然后,直接交换两个值(l处和r处的)。交换后继续向前寻找。
个人认为这样做比较麻烦,对边界处理比较麻烦,对轴值也要做处理。很容易出错。
最常用的方法是:确定轴值后(一般是中间下标处的元素),设置一个遍历T parval=arr[mid](即是把轴值存起来)。然后把轴值和数组最后一个元素进行交换,这时相当于数组最后一个位置是空着的,等着放左侧符合条件的元素。然后从数组左侧开始遍历,找到第一个大于(也可以找大于等于的,但那样交换次数会多一些)轴值的元素,其下标为l,把其放在下标为r处(有的人实现时会判读一下l和r的大小关系,如果l<r,就r--。其实没有必要,不需要对r做处理,知识多使arr[r]和parval多比较了一次而已)。这时再从下标为r处往左寻找,找到第一个小于轴值parval的值,放在下标l处。接着继续即可。知道l和r相遇,返回l。几位轴值parval应该在的位置。
代码如下:
//快速排序
#include <iostream>
using namespace std;
#define N 5
遇到两个值直接交换的
分割函数,得到分界点,左侧数据都小于分界点值,右侧的均大于
若使用arr[l]<=parval,arr[r]>parval这时对于分割值是最大值或最小值时会排序错误。比如5,4,10,3,9
此方法可能会陷入死循环,比如5,4,9,5
总之,这种方法需要对分割值做额外的处理,以下这种方式写肯定是有问题的
//template <class T>
//int Partion(T arr[],int left,int right)
//{
// int l=left,r=right;
// T parval=arr[(l+r)/2];//把中间位置的元素作为分界值,可以防止数组正序或逆序时的低效率
// while(l<r)
// {
// while(l<r && arr[l]<parval)//找到左侧第一个不小于分界值的元素
// {
// l++;
// }
// while(l<r && arr[r]>parval)//找到右侧第一个不大于分界值的元素
// {
// r--;
// }
// //交换两个值
// T temp=arr[l];
// arr[l]=arr[r];
// arr[r]=temp;
// cout<<"haha"<<endl;
// }
// cout<<l<<endl;
// return l;
//}
不直接进行交换的
template <class T>
int Partion(T arr[],int left,int right)
{
int l=left,r=right;
int mid=(l+r)/2; //数组的中间下标
//把中间位置的元素作为分界值,可以防止数组正序或逆序时的低效率
//分割前先交换arr[mid](分割值)和arr[r](最右侧元素)
T temp=arr[mid];
arr[mid]=arr[r];
arr[r]=temp;
//最右侧元素(即交换前的中间位置的元素值)
//如果是把最左侧元素作为中间值(即之前把arr[l]与arr[mid]交换),那么while循环内从右侧开始即可。效果是一样的。
T parval=arr[r];
while(l<r)
{
while(l<r && arr[l]<=parval)//找到左侧第一个大于分界值的元素
{
l++;
}
//if(l<r)
//{
// arr[r]=arr[l]; //把arr[l](即是左侧第一个大于分界值的元素)放置arr[r]处,这是arr[l]空了下来
// r--; //r左移一位
//}
//或者是不进行判定l和r的大小,直接不移动r,充其量是多把arr[r]与parval比较了一次
arr[r]=arr[l];
while(l<r && arr[r]>=parval)//找到右侧第一个小于分界值的元素
{
r--;
}
//if(l<r)
//{
// arr[l]=arr[r]; //把arr[r](即是右侧第一个大于分界值的元素)放置arr[l]处,这是arr[r]空了下来
// l++; //l右移一位
//}
//或者是不进行判定l和r的大小,直接不移动l,充其量是多把arr[l]与parval比较了一次
arr[l]=arr[r];
}
//非常关键的一步,要把分界值放置最后空闲的位置处(arr[l])
arr[l]=parval;
//返回分界值的最终下标
return l;
}
//快速排序(归并)
template <class T>
void QuickSort(T arr[],int left,int right)
{
if(left>=right)return;//递归出口
int pivot=Partion(arr,left,right);
QuickSort(arr,left,pivot-1);
QuickSort(arr,pivot+1,right);
}
//输出数组内容
template <class T>
void print(T arr[],int size)
{
for(int i=0;i<size;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
//主函数
int main()
{
//int arr[N]={5,4,1,3,9,12,7,22,0,-10,12,33,-22,0,88,123,-45,345,-98,-666};
int arr[N]={5,4,10,5,9};
//int arr[N]={5,4,0,3,9,22,7,22,0,-10};
cout<<"原始数据为:"<<endl;
print(arr,N);
QuickSort(arr,0,N-1);
cout<<"排序后:"<<endl;
print(arr,N);
return 0;
}
第一个分割方法(即直接交换的,一直没有通过所有测试用例),希望能有高手给知道一下。
注意测试用例的全面性:比如一定要测试那些含有重复元素的数组、分割值即是最大值或最小值的数组。很容易发现bug。