快速排序的优化

三种快速排序以及快速排序的优化:

一:快速排序的基本思想

快排使用分治的思想: 通过一趟排序将待排序序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

二:快速排序的三个步骤

1.选择基准 在待排序列中,按照某种方式挑出一个元素,作为“基准”(pivot)
2.分割操作 以该基准在序列中的位置,把序列分成两个子序列,此时基准左边的比基准小,基准右边的都比基准大
3.递归操作 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

三:选择基准的方式

  对于分治算法,当每次划分时,算法若能都划分成两个子序列时,那么分治算法效率会达到最大。也就是说,基准的选择将会决定算法的效率。选择基准的方式决定了两个分割后子序列的长度,进而对整个算法的效率产生决定性的影响。

  最理想的方法:选择基准恰好能把待排序序列分成两个等长的子序列。

下面我们将介绍3种选择基准的方法:

方法(1):固定位置(基础)

①思想:取序列的第一个或最后一个元素作为基准。基本快速排序

int SelectPivot(int arr[] , int low , int high)
{
    return arr[low];//选择序列的第一个元素作为基准。
    //return arr[high];
}

//注意:基本快排选取第一个或者最后一个元素作为基准。

②测试数据:

③测试数据分析:

  如果输入的序列是随机的,处理时间可以接受。

  但如果数组已经有序了,此时的分割方法是一个非常不好的分割,因为每次分割只能使待排序序列减一,此时为最坏的情况,导致快速排序沦为冒泡排序。时间复杂度为O(n^2)

  因此,使用第一个元素作为枢纽元素是非常糟糕的,为了避免这种情况,就引入了下面两个获取基准的方法。

方法(2):随机选取基准

①思想:随机取带排序元素中的元素作为基准。

/*随机选择枢轴的位置,区间在low和high之间*/
int SelectPivotRandom(int arr[],int low , int high)
{
    srand((unsigned)time(NULL));
    int pivotPos = rand()%(high - low) + low;

    //把枢轴位置的元素和low位置的元素互换,此时可以和普通的快排一样调用换分函数
    swap(arr[pivotPos],arr[low]);

    return arr[low];
}

②测试数据:

③:测试数据分析

  这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是出现劣质分割。

但是在整个数组数字全相等时,认识最坏的情况,时间复杂度为O(n^2)。实际上:随机化快排得到的理论上最坏的情况可能性仅仅为

1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。

一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”

方法(3):三数取中(median-of-three)(优化有序的数据)

引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是最坏的情况下还是O(n^2),要缓解这种情况,就引入了三数取中的选取枢轴。

①:具体思想

  对待排序序列中low,mid,high三个位置上数据进行排序,取他们中间的那个数据作为枢轴,并且用0下标元素储存枢轴。

即:三数取中,并且0下标元素储存枢轴。

/*函数作用:取待排序序列中low,mid,high三个位置上数据,选取他们中间的那个数据作为枢轴*/
int SelectPivotMedianOfThree( int arr[],int low,int high)
{
    int mid = low + ((low + high)>>1);//计算数组中间元素的下标。

    
    if(arr[mid] > arr[high])//目标:arr[mid] <= arr[high]
    {
        swap(arr[mid] , arr[high]);

    }
    if(arr[low] > arr[high])
    {
        swap(arr[low] , arr[high]);

    }
    if(arr[mid] > arr[low])
    {
        swap(arr[mid] , arr[low]);

    }
    //此时,arr[mid]<=arr[low]<=arr[high]    
    return arr[low];
    //low位置上保存这三个位置中间的值,分割时可以直接使用low位置的元素作为枢轴,而不改用分割函数了
}

②:测试数据

③:测试数据分析:使用三数取中选择枢轴的优势还是很明显的,但是还是处理不了重复数组

优化:

优化1:对于很小和部分有序的数组。快排不如插入排序好。当待排序序列的长度分割到一定大小之后,继续分割的效率比插入排序要差。此时可以使用插排而不是快排。

截至范围:待排序序列长度N  =  10.虽然在2 ~ 20之间任意截至范围都有可能产生类似大的结果,这种做法也避免了一些有害的退化情形。摘自《数据结构与算法分析》Mark Allen Weiness著。

if(high - low + 1 < 10)
{
    insertSort(arr,low,high);
    return ;
}
//else正常执行快排

②:测试数据

③:测试数据分析

  针对随即数组,使用三数取中选择枢轴+插排,效率还是可以提高一点。

  但是针对已排序数组,是没有作用的。因为待排序序列是已经有序的,那么每次划分只能使得待排序序列减一。此时插入排序是起不了任何作用的,所以这里看不到任何的时间减少。

  同时该方法对于重复数组还是没有任何的办法。

优化2:再一次分割结束后,可以把与key相等的元素聚在一起。继续下次分割时,不再用对于key相等元素分割。

①:具体的处理过程

第一步: 再划分过程中,把与key相等元素放入数组的两端。
第二步: 划分结束后,把与key相等的元素移到枢轴周围。

举例:

待排序序列:1 4 6 7 6 6 7 6 8 6 
三数取中选取枢轴:下标为4 的数 6
转化后待分割序列:6 4 6 7 1 6 7 6 8 6   枢轴key: 6
第一步:再划分过程中,把与key相同的元素放入数组的两端  结果为: 6 4 1 6(枢轴) 7 8 7 6 6 6 。此时与6相等的元素全部放入两端
第二步:划分结束后,把与key相等的元素移到枢轴周围。结果:1 4 6 6(枢轴) 6 6 6  7 8 7.此时与6相等的元素全移到枢轴周围了。
之后,在1 4 和 7 8 7两个子序列中进行快排。
void QSort(int arr[],int low,int high)
{
    int first = low;
    int lase = high;

    int left = low;
    int right = high;

    int leftlen = 0;
    int rightlen = 0;
    
    if(high - low +1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    
    //一次分割;
    int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中选择枢轴

    while(low < high)
    {
        while(high < low && arr[high] >= key)
        {
            if(arr[high] == key)//处理相等元素
            {
                swap(arr[right],arr[high]);
                right--;
                rightlen++;
            }
            high--;
        }
        
        arr[low] = arr[high];
        while(high > low && arr[low] <= key)
        {
            if(arr[low] == key)
            {
                swap(arr[left],arr[low]);
                left++;
                leftlen++;
            }
            low++
        }
        arr[high] = arr[low];
    }
    arr[low] = ley;

    //一次排序结束
    //把与枢轴key相同的元素移到枢轴最终位置周围
    int  i = low - 1;
    int j  = first;
    while(j < left && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i--;          
        j++;
    }
    i = low +1;
    j = last;
    while(j > right && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i++;
        j--;
    }
    QSort(arr,first,low-1-leftlen);
    QSort(arr,low + 1 + rightlen,last);

}

        

②:测试数据

③:测试数据分析:三数取中选择枢轴+插排+聚集相等元素的组合竟然效果好的 出奇。

  原因:在数组中,如果有相等的元素,那么减少不少冗余的划分。这点再重复数组中的体现特别明显。(这里的插排的作用还是不明显)。

优化3:优化递归操作

①:思想:快排函数在函数末尾有两次递归操作,我们可以对其使用尾递归优化

②优点:如果待排序的序列划分的极端不平衡,递归的深度将趋近于n,而栈的大小有限,每次递归调用都会耗费一定 的占空间,函数参数越多,每次递归耗用的空间也就越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(log n)

void Qsrot(int arr[] , int low , int high)
{
    int pivotPos = -1;
    if(high - low + 1 < 10)
    {
        InsertSort(arr , low , high);
        return;
    }
    while(low < high)
    {
        pivotPos = Partition(arr,low,high);
        QSort(arr , low , pivot - 1);
        
        low = pivot + 1;
    }
}

注意:第一次递归后,low就没用了;此时第二次递归可以使用循环代替。

②测试数据

③:测试分析:其实这种优化编译器会自己优化,相比于不用该优化方法,时间几乎没少。

优化4:使用并行或多线程处理子程序(不详细解释)

所有的数据测试:

概括:这里效率最好的快排组合 是:三数取中 + 快排 + 聚合相等元素。他和STL中的Sort函数的效率差不多。

猜你喜欢

转载自blog.csdn.net/genzld/article/details/84103358