1.序言
快速排序网上有很多优秀的博客,此篇文章也有参考,但对于我来说,看过很多次,总觉得这个算法还是别人的,没有成为自己的,本着遇到问题多问几个为什么,我经过思考,发现主要问题在于,如何实现将数据分割成独立的两部分?为什么要进行从后往前遍历寻找小值?为什又要从前往后寻找大值?
2.问题描述
参见 冒泡排序 问题描述
3.问题分析
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
4.前提条件
a.此处我们选择使用标准C语言。
b.此处选择的哨兵元素为数组第一个元素。
c.此处假设我们需要排序的n个数均为整型。
5.实现步骤
a. 数据存储,采用整型数组,用int array[]表示,并且长度用int length表示。
b.分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小。
<1>既然要分开,就得有参考数据,选取数组的第一个元素为哨兵元素,用 int value = array[0];
<2>要比较哪个元素比标准大,或者小,就需要循环遍历数组,假设数据的开始位置为int start = 0, 结束位置为 int end = length - 1,则<1>中的哨兵元素一般化为value = array[start],循环条件自然是 start < end。
<3>完成一次分割
我们目前打算依赖和哨兵元素比较来获取这个元素所处的分组,但如何在不产生额外空间复杂度的情况下,即直接利用在原数组上操作,来存储分割后的两组数据。
现在我们假设数据array前边放置比哨兵元素小的数据,后边放置比哨兵元素大的元素,而且哨兵元素已经被value进行标记,所以目前原数组中哨兵元素是一个空位置,可以进行覆盖赋值。
这个位置,依据假设,要么放置的是比哨兵小的元素,要么放置哨兵元素本身(数组中哨兵元素最小),目前我们需要找到比哨兵元素小的元素,怎么找呢。当然是循环遍历数组,循环条件依然是start < end,那是从前往后循环,还是从后往前循环呢?当然是从后往前,通过end--的方式,因为这样可以保证数组后边剩下的元素都是比哨兵元素大的元素,而采用从前往后循环,无法保证数组前边的元素都是比哨兵小的元素。循环结束的条件,就是找到end所指向的元素比哨兵元素小,然后将此值赋值给start所在位置。
此时空余的元素是end所在的位置,目前end所在后边都是比哨兵元素大的元素,start左侧都是比哨兵元素小的元素,而start与end之间还是没有比较过的,所以end这里需要填充的要么是比哨兵元素大的,或者是哨兵元素本身,同样面临如何循环?这次是从前往后,通过start++的方式实现,同样从后往前,无法达到预期效果。循环结束条件,就是找到start所指向的元素比哨兵元素大,然后将此值赋值给end所在的位置。
最后不要忘了放置哨兵元素呦。即array[start] = value。
<4>递归实现
递归结束条件,当array数组元素长度为1,即 length < 1时。
递归赋值,前后为两个数组,前边为array,长度为start;后边为array[start + 1],长度为length - start - 1。
6.代码
#include<stdio.h>
// 打印数组
void print_array(int *array, int length)
{
int index = 0;
printf("array:\n");
for(; index < length; index++){
printf(" %d,", *(array+index));
}
printf("\n\n");
}
void quickSort(int array[], int length)
{
int start = 0;
int end = length-1;
int value = array[start];// 得到哨兵元素
if (1 > length) return;// 递归出口
while(start < end){// 以哨兵元素为标准,分成大于它和小于它的两列元素
while(start < end){// 从数组尾部往前循环得到小于哨兵元素的一个元素
if ( array[end--] < value ){
array[start++] = array[++end];
break;
}
}
while( start < end ){// 从数组头部往后循环得到大于哨兵元素的一个元素
if( array[start++] > value){
array[end--] = array[--start];
break;
}
}
}
array[start] = value;// 放置哨兵元素
printf("\nstart:%d, end:%d\n", start, end);// 这个是测试下start和end是否一样
quickSort(array, start);// 递归排序小于哨兵元素的那一列元素
quickSort(array + start + 1, length - start - 1);// 递归排序大于哨兵元素的那一列
}
int main(void)
{
int array[12] = {1,11,12,4,2,6,9,0,3,7,8,2};
print_array(array, 12);// 开始前打印下
quickSort(array, 12);// 快速排序
print_array(array, 12);// 排序后打印下
return 0;
}
7.运行结果
8.时间复杂度
a.递归方程的一般形式为:
b.最优时间复杂度:
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组,此时时间复杂度公式为:
T[n] = 2T[n/2] + f(n)
下面来利用迭代法,计算最优情况下快排时间复杂度:
c.最差情况下时间复杂度
最差情况就是每次取到的数组就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次只排好一个元素的顺序),冒泡排序的时间复杂度为 T[n] = n * (n - 1) = n^2 + n;即快排最差情况下的时间复杂度为O(n^2)。
d.平均时间复杂度
快排的平均时间复杂度也是: O(nlogn)
9.空间复杂度
由于我们用的是就地排序,所以快排本身的空间是O(1),就是个常数级,而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据。
最优的情况下空间复杂度为 O(logn),每一次都平分数组的情况。
最差的情况下空间复杂度为 O(n),退化为冒泡排序的情况。
10.参考
d.《算法竞赛入门经典》