快排思路:
(Ⅰ)选取主元
(Ⅱ)根据主元,划分子集,一次的排序后,左边元素<主元<右边元素
(Ⅲ)然后在左边元素作为新的集合,对左集合进行再次进行快排,同理,对右集合快排,
这样一直快排下去,如果无法进行3数中值法的快排了,就对小的集合进行插入排序。
选取主元的分割策略是:
选取中位数,交换一下位置,使得:A[Left]<=A[Center]<=A[Right],
并把A[Center]和A[RIght-1]交换位置,目的是把枢纽元放到和子集划分无关的地方,方便划分子集
下面开始排序
意思很明显,当能进行三数中值划分的时候就快排,否则就使用插入排序.
用i,j作为下标当A【i】< Pivot,++i,i往右滑
当A【j】>Pivot , j–,j往左滑
最后有可能是i指向了大元素,j指向了小元素,如果此时i
接下来只需要把A【i】和枢纽元A【right-1】互换,他们互换前,i是指向大元素的(因为i,j交错),
所以交换后A[i]左边全部是小元素,A【i】右边全部是大元素,这符合我们的期望,子集划分很好
把现在的A【i】左边的集合作为新的集合,继续划分,A【i】右边的集合作为大集合继续划分
快排最快的原因是,通过选取枢纽元,它枢纽元左边的元素都小于枢纽元,枢纽元右边的元素都大于枢纽元
所以,枢纽元一次性被防止到了最合适的位置,从此之后不需要再移动,但是,其他普通的算法都有可能需要频繁的移动同一个元素。
快排最好情况就是它进行子集划分的时候,2个子集划分的非常的均匀,切成2半是最好情况,
与之相反,最坏情况:就是子集划分很不均匀,每次都是最左边的元素是枢纽元,右边元素都大于它
没有元素小于它,造成子集划分的非常多,最多划分成n 个子集(每个集合以O(N)遍历),这就使得快排变成了O(n2)算法了
,而最好情况就是,划分很好,每次切成一半,需要log2N次,每个集合遍历的时候又是O(N)时间,共花费
O(N*log2N)时间
总结:
快排 | 最好情况 | 最坏情况 |
---|---|---|
复杂度 | O(N*log2N) | O(N2) |
如果我们总是选取第一个元素作为枢纽元,当一组预排序数据输入进来的时候,我们右边的元素
全部大于你选的第一个元素,也就是枢纽元,然后子集划分的时候左边一个元素作为子集,右边那么多个元素全部划分成为另外一个大集合子集,一次就砍掉了1个元素,共需要划分n个集合,线性n遍历,花费
O(N2时间)
当A[i]或A[j]元素等于pivot时,不停止,极端情况下会导致O(N2),举一种极端情况就是我全部元素都
是相等的,你i或者j开始遍历数组的元素时,就会滑到数组两端去,又造成了划分n个集合的情况,每次
划分都极端的不均匀,根据上面分析原因①得出此时快排是O(N2)复杂度
原理:一般来说,枢纽元选取数组的中位数比较合适,这样在划分子集的时候,最大限度的保证了划分
log2N的个子集,但是对N个数据求中位数开销也比较大,但是我们有另外一种方法可以近似得到中位数
具体做法如下:
选取数组最左边,最右边,以及最中间的元素,求这三个数的中位数,结果比较近似于整个数组的中位数
然后选取该中位数作为我们排序的枢纽元就比较合适了,选取过程中我们为了更进一步优化快排,
还要做另外一些工作,
①使得数组A【Left】<= A【Center】<= A【Right】,
防止预排序输入导致的糟糕情况
②交换A【Center】和A【Right-1】位置,把枢纽元和划分的2个子集分离开,易于后面从操作,
为了更快的排序,可以使得i从Left+1开始,j从Right-2开始
对于小数组,直接简单暴力的插入排序比递归调用的快排要快,无需多层递归,来回跳转,
但是要注意的是,只是对数组在一定范围之内排序,所以传递的数组地址要加上Left个偏移,元素个数
是Right-Left+1个,+1是因为传进来的Right就是经过了-1操作的(N-1==Right)
所以InsertSort(A+Left,Right-Left+1);
// qsort_2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include
#include
#include
template<typename T>
void swap(T& a, T& b) {
T t;
t = a;
a = b;
b = t;
}
template<typename T>
void insert_sort(T* array, unsigned int n) {
unsigned int i , j , Increatement = n / 2;
T tmp;
for (; Increatement >= 1; Increatement /= 2) {
for (i = Increatement; i < n; i++) {
tmp = array[i];
for (j = i; j >= Increatement && array[j - Increatement] > tmp; j -= Increatement) {
array[j] = array[j - Increatement];
}
array[j] = tmp;
}
}
}
template<typename T>
T median(T* array, unsigned int begin, unsigned int end) {
unsigned int Center = (begin + end) / 2;
if (array[begin] > array[Center]) {
swap(array[begin], array[Center]);
}
if (array[begin] > array[end]) {
swap(array[begin], array[end]);
}
if (array[Center] > array[end]) {
swap(array[Center], array[end]);
}//三段if防止预排序数据导致的糟糕情况
swap(array[Center], array[end - 1]);//交换枢纽元是因为把枢纽元和划分的两个子集分离开
return array[end - 1];//返回枢纽元
}
template<typename T>
void qsort(T* array, unsigned int Left, unsigned int Right) {
unsigned int i, j;
if (Right-Left>=3) {
i = Left;//由于++i,所以i实际从Left开始
j = Right-1;//--j从Right-2开始
T privot = median(array, Left, Right);
for (;;) {
while(array[++i]<privot){
}
while(array[--j]>privot){
}
if (i < j) {
swap(array[i], array[j]);
}
else {
break;
}
}
swap(array[i], array[Right- 1]);//与三数中值分割法的操作相反,此时把枢纽元交换回去,枢纽元放到最合适的位置
qsort(array, Left, i - 1);//左边的小集合进行子集划分,右边下标到i-1,也就是枢纽元之前
qsort(array, i+1, Right);//右边的子集合进行子集划分,左边下标i+1,也就是枢纽元A【i】之后
}
else {
insert_sort(array+Left,Right-Left+1);//对数组的部分元素排序,所以从A+Left开始排序
}
}
template<typename T>
void qsort(T* array, unsigned int n) {
qsort(array, 0, n - 1);
}
int main()
{
srand(time(NULL));
const int MAX = 100;
int a[MAX];
for (int i = 0; i < MAX; i++) {
a[i] = rand()%200;
}
long start = clock();
qsort(a, MAX);
long end = clock();
for (auto& i : a) {
std::cout << i << std::endl;
}
std::cout << "using " << double(end - start) << "s" << std::endl;
}
(Ⅰ)枢纽元的选取,不适当的选取容易导致成为O(N2)级别,一般都是围绕枢纽元的选取
(Ⅱ)具体的实现细节:
①和枢纽元相同时,i,j是否应该停止,
②划分字节到很小的数组时,是否采用其他的非递归算法
③三数中位数分割时,对枢纽元的易位处理,防止预排序数据捣乱
当你A【i】或者A【j】等于pivot时就会导致无限循环!!!
for(;;){
while(A[i]<pivot) i++;
while(A[j]>pivot) j--;
if(i<j){
swap(&A[i],&A[j])
}else{
break;
}
}