面试官都在问 | 快速排序C++实现
原创
©著作权归作者所有:来自51CTO博客作者wx58c6a41d9524d的原创作品,请联系作者获取转载授权,否则将追究法律责任
比较类排序系列-快速排序
1. 原理
快排是C. A. R. Hoare在1960提出的一种排序算法,这是一种采用分治思想的排序算法,大致分为三个步骤。
- 定基准——首先选择一个元素作为基准值
- 划分区——所有比基准小的元素置于基准左侧,比基准大的元素置于右侧,构成左右两个子序列
- 递归调用——递归地调用此切分过程,切分其子序列,直到子序列只含有一个值时停止递归
如下图所示
上图只完整的演示了三次划分的过程,第一次选取3为基准值,进行一次划分,得到了第二行的序列,此时得到两个子序列,如第三行和第五行所示。对第三行的子序列继续进行划分,得到第四行的划分结果。对第五行的子序列继续进行划分,得到第六行的划分结果。后面会继续对第四行和第六行划分出的新的子序列继续相同的过程,最后会得到一个完整的有序序列。
1.1基准值
基准值的选取有多种方法。但是不同的选取方法对排序的性能会有比较大的影响。
- 序列的第一个值:最简单的选取方式,但是某些情况下,会导致划分的子序列非常不均衡。比如数据有序时,每次只能划分出一个子序列,而不是两个。
- 几数取中:从几个数中选取一个基准值,选取的基准值是这几个数中的中间值。此种方式可以避免第一种方式中的极端情况出现,会让划分比较均衡。
- 随机选取:随机选一个数作为基准值,此种方式也会避免极端情况的出现。
1.2划分
划分是把数据分成两部分,一部分小于基准值,一部分大于基准值,划分过程大致如下:
- 从当前序列从后往前找到第一个小于基准的数。
- 从当前序列从前往后找到第一个大于基准的数。
- 交换找到的这两个数。继续执行1,2步,直到1,2步遍历的位置相遇则结束。
- 基准值与相遇位置的数据进行交换,完成当前序列的划分,此时所有小于基准的数据在左,大于等于基准的数据在右。
2. 代码
void swap(int* array, int i, int j)
{
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
int partion(int* array, int begin, int end)
{
//第一个数据作为基准值
int key = array[begin];
int start = begin;
while (begin < end)
{
//从后向前找小
while (begin < end && array[end] >= key)
--end;
//从前向后找大
while (begin < end && array[begin] <= key)
++begin;
//可能找到了两个数据, 交换
swap(array, begin, end);
}
//交换基准值和相遇位置的值
swap(array, start, begin);
//返回基准值位置
return begin;
}
void quickSort(int* array, int begin, int end)
{
if (begin >= end)
return;
//划分当前区间
int mid = partion(array, begin, end);
//划分小区间
quickSort(array, begin, mid - 1);
quickSort(array, mid + 1, end);
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static int partion(int[] arr, int left, int right){
//第一个数据作为基准值
int key = arr[left];
int start = left;
while(left < right){
//从后向前找小
while(left < right && arr[right] >= key)
--right;
//从前向后找大
while(left < right &&arr[left] <= key)
++left;
//交换
swap(arr, left, right);
}
//交换基准值和相遇位置的值
swap(arr, left, start);
return left;
}
public static void quickSort(int[] arr, int left, int right){
if(left < right){
//划分当前区间
int mid = partion(arr, left, right);
//划分小区间
quickSort(arr, left, mid - 1);
quickSort(arr, mid + 1, right);
}
}
3. 时间空间复杂度
3.1 时间复杂度
快速排序的过程就是不断的进行子序列的划分,直到子序列中只包含一个值,此时排序也就结束了。
- 最坏时间复杂度:如果数据是有序的,则每一次划分之后,只会得到一个子序列,子序列的元素比其父序列少一个,故每次需要划分子序列大小是一个等差数列,故:1 + 2 + 3 + … + n - 1 = n(n - 1)/2 = O(n^2)。
- 最好时间复杂度:最理想情况下,每次划分都可以得到元素个数相等的两个子序列,设T(n)为n个元素的时间复杂度,则T(n) = T(n/2) + T(n / 2) + O(n) = 2*T(n/2) + O(n),可以看到每次都是二分的关系,递归的层数为logn,每一层需要给n的元素进行划分,故时间复杂度为O(nlogn)。
- 平均时间复杂度:O(nlogn)。
3.2 空间复杂度
快速排序过程中,需要进行递归调用,所以需要进行函数压栈操作,每一个函数栈中占用常数空间,最好情况下,最大的递归调用深度为logn, 最坏情况下,最大递归调用深度为n, 故最好情况下为O(logn), 最坏为O(n)。
总结
实际中,普通的排序场景,都是采用快速排序,因为其实现容易,且时间和空间的消耗都比较小,排序比较快。