快速排序优化细节分析

快排优化详析

  • 一,快排思路
    • (Ⅰ)选取主元
    • (Ⅱ)子集划分
    • (Ⅲ)递归的划分子集
  • 二,快排分析
    • (Ⅰ)快排最好情况
    • (Ⅱ)导致快排退化成为O(N^2^)原因分析
      • ①总是选取第一个元素作为枢纽元
      • ②算法设计的时候元素等于pivot时,i,j不停下来
  • 三,优化快排的手段
    • ①三数中值分割法
    • ②对于小数组,采用插入排序
  • 四,快排实现源码:
  • 五,总结影响快排的因素
  • 六,不适当的实现细节导致无限循环

一,快排思路

快排思路:
(Ⅰ)选取主元
(Ⅱ)根据主元,划分子集,一次的排序后,左边元素<主元<右边元素
(Ⅲ)然后在左边元素作为新的集合,对左集合进行再次进行快排,同理,对右集合快排,
这样一直快排下去,如果无法进行3数中值法的快排了,就对小的集合进行插入排序。

(Ⅰ)选取主元

选取主元的分割策略是:
选取中位数,交换一下位置,使得:A[Left]<=A[Center]<=A[Right],
并把A[Center]和A[RIght-1]交换位置,目的是把枢纽元放到和子集划分无关的地方,方便划分子集
下面开始排序
快速排序优化细节分析_第1张图片

(Ⅱ)子集划分

意思很明显,当能进行三数中值划分的时候就快排,否则就使用插入排序.
用i,j作为下标当A【i】< Pivot,++i,i往右滑
当A【j】>Pivot , j–,j往左滑
最后有可能是i指向了大元素,j指向了小元素,如果此时i 也有可能是i,j已经交错了,说明此时左边已经全部是小元素,右边全部是大元素,划分已经很好了
接下来只需要把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)

(Ⅱ)导致快排退化成为O(N2)原因分析

①总是选取第一个元素作为枢纽元

如果我们总是选取第一个元素作为枢纽元,当一组预排序数据输入进来的时候,我们右边的元素
全部大于你选的第一个元素,也就是枢纽元,然后子集划分的时候左边一个元素作为子集,右边那么多个元素全部划分成为另外一个大集合子集,一次就砍掉了1个元素,共需要划分n个集合,线性n遍历,花费
O(N2时间)

②算法设计的时候元素等于pivot时,i,j不停下来

当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;
	}
}

你可能感兴趣的:(数据结构,算法,数据结构,快速排序,排序算法)