1、算法思想
快排算法的基本思想是,选取待排序数组中的任意一个元素作为基准值,遍历数组中的元素。把小于基准值的元素放到基准值的左边,大于基准值的元素放到基准值的右边,基准值放到两者之间,此时,基准值到达最终的位置。然后对基准值左边的子数组和右边的子数组采用同样的方式处理,直到子数组区间缩小为1时,说明数组有序。
快排的递归实现:
template<typename T>
void quickSort_OneWay(T* array,int left, int right)
{
if (left == right){
return;
}
index = partition(T, left, right);
quickSort_OneWay(T, left, index - 1);
quickSort_OneWay(T, index + 1, right);
}
单路快排、随机快排、双路快排、三路快排的主要区别在于partion部分,下面分别介绍一下这几类partition的具体细节。
2、单路快排
在单路快排中的partion算法中,每次选取的基准值是待排序数组中最左边的那个元素,并且是把数组划分成大于基准值和小于等于基准值的两部分(注意下图,右边这部分是>=v)
如上所示,
(1)每次我们选取待排序数组的第一个元素作为基准值;
(2)从第1个元素开始向后遍历,i指向当前正在遍历的元素;
(3)[l+1,j]区间的元素小于V,[j+1,i-1]区间的元素大于等于V。
(4)每当i碰到比基准值小的元素,就将i位置的元素与j+1位置的元素进行交换,橙色区域就扩大一个长度,如下图:
(5)最后只需将位置i的元素和位置j的元素交换即可。
单路快排partition算法:
template<typename T>
int partition(T* arrry, int left, int right){
int base = array[left];
int j = left;
for (int i = left+1; i <= right; i++){
if (array[i] < base){
swap(array, j + 1, i);
j += 1;
}
}
swap(array, left, j);
return j;
}
这段代码存在以下两个问题:
问题1:近乎有序:当待排序数组近乎有序时,由于默认选择待排序数组的第一个元素作为基准值,会导致根据基准值划分的两个子数组严重不均衡(不均衡就会导致递归的深度变得很深),极可能出现一个数组只有一个元素,而另一个数组n-1个元素的情况,此时递归调用的深度为n,快排会退化成时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)的排序算法。
问题2:大量重复:当待排序数组含有大量重复元素时,如果刚好选择了重复元素作为基准值,由于代码会将等于基准值的元素划分到右边这个子数组中,使得根据基准值划分的左右子数组严重不均衡,快排同样会退化成时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
3、随机快排
为了解决问题1,基准值不再默认选择待排序数组的第一个元素,而是从待排序数组中随机选取一个元素作为基准值,因此有了随机快排,随机快排的partion算法如下:
#include<cstdlib>
#include<ctime>
#define random(a,b) ((a)+rand()%((b)+1-(a)))
template<typename T>
int partition_random(T* arrry, int left, int right){
int base_index = random(left,right);
swap(array,left,base_index);
int base = array[left];
int j = left;
for (int i = left+1; i <= right; i++){
if (array[i] < base){
swap(array, j + 1, i);
j += 1;
}
}
swap(array, left, j);
return j;
}
4、双路快排
为了解决问题2,一种思路是将重复的元素均匀地分布在两边的数组中,双路快排可以实现这个想法,双路快排的过程如下所示:
(1)i索引不断向后扫描,当i的元素小于v时,i++;
(2)j索引不断向前扫描,当j的元素大于v时,j- - ;
(3)当i碰到一个>= v的元素以及j碰到一个<= v的元素,交换i与j的元素,i++,j- -;
双路快排实现代码:
template<typename T>
int partition_TwoWay(T* arrry, int left, int right){
int base_index = random(left, right);
swap(array, left, base_index);
int base = array[left];
i = left + 1;
j = right;
while (true){
while (i <= right && array[i] < base) i++;
while (j >= left + 1 && array[j]>base)j--;
if (i > j) break;
swap(array, i,j);
i++;
j--;
}
swap(array, left, j);
return j;
}
注意: 在判定条件中,边界情况只能是 < 或 >,而不是 <= 或 >=。
- 对于arr[i]< v和arr[j]>v的方式,第一次partition得到的分点是数组中间;
- 对于arr[i]<=v和arr[j]>=v的方式,第一次partition得到的分点是数组的倒数第二个。
因为我们的目的就是要让重复的均匀在两边的数组中,而第二种方式还是会将连续出现相等的值归为一方,这样还是会导致两颗子树的不平衡,还是会出现导致O(n^2)的情况出现。
5、三路快排
二路排序只是提升了一下效率,当有大量重复值排序的时候,还是会沦为O(n^2)的排序算法,之前的二路,将数组分成两部分,小于v,大于v,两部分是都含有等于v的,只是说尽可能的均匀分布,当存在大量的重复可能效率还是不好,而三路快排则是多加了一部分等于v。
三路排序的过程如下所示:
(1)[left+1,lt]维持小于v的元素,[lt+1,i-1]维持等于v的元素,[gt,r]维持大于1的元素;
(2)当i小于gt时,重复(3)-(5)
(3)如果i索引元素小于v,则将位置i和位置lt+1的元素互换,i++,lt++;
(4)如果i索引元素大于v,则将位置i和位置gt-1的元素互换,gt–;
(5)如果i索引元素等于v,则i++;
(6)最后将位置left和位置lt的元素进行互换。
三路快排代码实现:
template<typename T>
int* partition_ThreeWay(T* arrry, int left, int right){
int base_index = random(left, right);
swap(array, left, base_index);
int base = array[left];
int lt = left;
int i = left+1;
int gt = right + 1;
while (i < gt){
if (array[i] < base){
swap(array, i, lt + 1);
i++;
lt++;
}
else if(array[i] > base){
swap(array, i, gt - 1);
gt--;
}
else{
i++;
}
}
swap(array, left, lt);
int* reslut = new int[2];
reslut[0] = lt;
result[1] = gt;
return result;
}
6、快排算法分析
-
时间复杂度: 最好 O ( n l o g n ) O(nlogn) O(nlogn),最坏 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度:递归调用本质在不断压栈,因此快排的空间复杂度与递归的深度有关,最好的情况是 O ( l o g n ) O(logn) O(logn),最坏 O ( n ) O(n) O(n)。
-
稳定性:不稳定
-
使用情况:数据规模较大和数组无序。
7、排序算法的稳定性
(1)什么是排序算法的稳定性?
排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
(2)稳定性的好处
其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。