排序 之 快速排序
快速排序是冒泡排序的升级,属于交换排序
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字ixoa,则可分别对这两部分记录继续进行排序,以达到整个排序有序的目的。
/* 快速排序******************************** */ /* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 */ /* 此时在它之前(后)的记录均不大(小)于它。 */ int Partition(SqList *L,int low,int high) { int pivotkey; pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */ while(low<high) /* 从表的两端交替地向中间扫描 */ { while(low<high&&L->r[high]>=pivotkey) high--; swap(L,low,high);/* 将比枢轴记录小的记录交换到低端 */ while(low<high&&L->r[low]<=pivotkey) low++; swap(L,low,high);/* 将比枢轴记录大的记录交换到高端 */ } return low; /* 返回枢轴所在位置 */ } /* 对顺序表L中的子序列L->r[low..high]作快速排序 */ void QSort(SqList *L,int low,int high) { int pivot; if(low<high) { pivot=Partition(L,low,high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */ QSort(L,low,pivot-1); /* 对低子表递归排序 */ QSort(L,pivot+1,high); /* 对高子表递归排序 */ } } /* 对顺序表L作快速排序 */ void QuickSort(SqList *L) { QSort(L,1,L->length); }
Partition函数要做的,就是先选取当中的一个关键字,然后想尽办法将它放到一个位置,使得它左边的值都比它小,右边的值比它大,我们将这样的关键字称为枢轴(pivot)。
Partition函数其实就是将选取的pivotkey不断交换,将比它小的换到它的左边,比它大的换到它的右边,它也在交换中不断更改自己的位置,知道完全满足这个要求为止。
复杂度
快速排序的时间复杂度为O(nlogn).
空间复杂度为O(logn),(递归造成的栈空间的使用。)
由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。
快速排序算法的优化
1.优化选取枢轴
三数取中(median-of-three)法,即取三个关键字先进行排序,将中间数做为枢轴,一般是取左端、右端和中间三个数,也可以随机选取。
2.优化不必要的交换
在partition函数中,其中的有些交换是不需要的,可以采用替换的方式进行操作。
改进后的partition函数代码
/* 快速排序优化算法 */ int Partition1(SqList *L,int low,int high) { int pivotkey; int m = low + (high - low) / 2; /* 计算数组中间的元素的下标 */ if (L->r[low]>L->r[high]) swap(L,low,high); /* 交换左端与右端数据,保证左端较小 */ if (L->r[m]>L->r[high]) swap(L,high,m); /* 交换中间与右端数据,保证中间较小 */ if (L->r[m]>L->r[low]) swap(L,m,low); /* 交换中间与左端数据,保证左端较小 */ pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */ L->r[0]=pivotkey; /* 将枢轴关键字备份到L->r[0] */ while(low<high) /* 从表的两端交替地向中间扫描 */ { while(low<high&&L->r[high]>=pivotkey) high--; L->r[low]=L->r[high]; while(low<high&&L->r[low]<=pivotkey) low++; L->r[high]=L->r[low]; } L->r[low]=L->r[0]; return low; /* 返回枢轴所在位置 */ }
3.优化小数组时的排序方案
对于元素个数较小的排序要求,快速排序反而不如直接插入排序来的更好,(直接插入是简单排序中性能最好的).其原因在于快速排序用到了递归操作,在大量数据排序时,这点性能影响可以忽略,当数组只有几个记录需要排序时,就成了一个大炮打蚊子的大问题。可以增加一个判断,当元素个数不大于某个常数时,(有资料认为7比较合适,也有认为50更合理),就用直接插入排序。
4.优化递归操作
尾递归:在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这种情形下称该调用位置为尾位置。若这个函数在尾位置调用本身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归
尾递归优化:与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化1便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。
一般来说对于尾递归形式的调用,大部分编译器会自动做出优化,当然也可以主动以循环的方式来优化尾递归。
QSort1(L,low,pivot
-1); /* 对低子表递归排序 */ /* QSort(L,pivot+1,high); /* 对高子表递归排序 */
low=pivot+1;/* 尾递归 */
} } else InsertSort(L); }
排序算法总结
排序分类 |
排序方法 |
平均情况 |
最好情况 |
最坏情况 |
辅助空间 |
稳定性 |
插入排序 |
直接插入排序 |
O(n2) |
O(n) |
O(n2) |
O(1) |
稳定 |
希尔排序 |
O(nlogn)~O(n2) |
O(n1.3) |
O(n2) |
O(1) |
不稳定 |
|
选择排序 |
简单选择排序 |
O(n2) |
O(n2) |
O(n2) |
O(1) |
不稳定 |
堆排序 |
O(nlogn) |
O(nlogn) |
O(nlogn) |
O(1) |
不稳定 |
|
交换排序 |
冒泡排序 |
O(n2) |
O(n) |
O(n2) |
O(1) |
稳定 |
快速排序 |
O(nlogn) |
O(nlogn) |
O(n2) |
O(logn)~O(n) |
不稳定 |
|
归并排序 |
归并排序 |
O(nlogn) |
O(nlogn) |
O(nlogn) |
O(n) |
稳定 |