【Data Structure】高级排序问题 [part 2]

高级排序问题 [part 2]

●快速排序法

●随机化快速排序法

●双路快速排序法

●三路快速排序法

●归并排序和快速排序的衍生问题

 

1   快速排序法

每次从当前排序的数组中选择一个元素,以该元素为基点,考虑将其移动到排好序情况下应该处在的位置(该元素之前的元素都小于该元素,该元素之后的元素都大于该元素),再对左右两个子数组递归使用快速排序

//对arr[l...r]部分进行partition操作
//返回p,使得arr[l...p-1] < arr[p; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){
    T v = arr[l];//partition的标准简单取待排序数组的第一个元素
    
    //arr[l+1...j] < v; arr[j+1...i) > v  后面为开区间的原因是i元素为当前待考察的元素
    int j = l;//初始化为l,即l < l+1,满足初始的时候第一个区间为空
    for(int i = l+1; i <= r; i++){//满足初始的时候第二个区间为空
        if(arr[i] < v){
            swap(arr[j+1], arr[i]);//将第一个大于v的元素arr[j+1]与当前考察的元素arr[i]进行交换
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;//此时j位置存储的就是v的值
}

//对arr[l...r]部分进行快速排序
template<typename T>
void  __quickSort(T arr[], int l, int r){
    if(l >= r)
        return;
    int p = __partition(arr, l, r);
    __quickSort(arr, l, p-1);
    __quickSort(arr, p+1, r);
    
}

template<typename T>
void quickSort(T arr[], int n){
    __quickSort(arr, 0, n-1);
}

2  随机化快速排序法(对近乎有序数组排序的性能优化

1、高级的排序算法在底层的时候,都可以采用插入排序方法来优化
2、对近乎有序数组排序的性能优化

归并排序可以保证每次都平均地将数组一分为二,而对于快速排序来说,却没有这个保证,换言之,快速排序分出来的两个子数组可能是一大一小的 ,容易看出,快速排序调用递归的过程所生成的递归树,其平衡性就比归并排序生成的递归树差。并且这棵递归树的高度并不一定是logn,很可能会比logn要差,其中最坏的情况就是待排序数组近乎有序,此时每次分割的左子树都为空,右子树为剩余的所有元素,二叉树的高度为n,快速排序算法的时间复杂度退化为O(n^2)


在无法准确确定中间元素的情况下,随机选择中间元素,此时快速排序时间复杂度的期望值为O(nlogn)(数学方法可证),此

时快速排序算法退化成O(n^2)的概率是极低的(每次都正好选到最小的数组元素)

//对arr[l...r]部分进行partition操作
//返回p,使得arr[l...p-1] < arr[p; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){
    
    swap(arr[l], arr[rand()%(r-l+1)+l]);//生成一个在区间[r...l]的随机数,并将其与第一个元素交换位置
    
    T v = arr[l];//partition的标准简单取待排序数组的第一个元素
    ...
}
...
template<typename T>
void quickSort(T arr[], int n){
    srand(time(NULL));
    __quickSort(arr, 0, n-1);
}

3  双路快速排序法(对于较多重复数值数组排序的性能优化

归并排序和上述快速排序时间对比:

之前的算法将小于v和大于v的两部分都放在了数组的一头,指针i从左到右直到遍历完所有的数,现在将小于v和大于v的两部分放在数组的两端,这样需要多出一个索引j记录大于v的部分下一个需要扫描的元素位置(注意此时的i和j的作用是相似的)。首先从左边的i指针开始向右扫描,记录第一个大于v的元素位置,接着右边的j指针开始向左扫描,记录第一个小于v的元素位置。此时交换i,j位置,重复以上步骤,直到i,j两个指针重合


需要注意的是,与之前算法将所有等于v的元素全部移到左边/右边相比,该算法中的左右部分都可以包含等于v的元素,即将等于v的元素较为均匀地分散在左右两个数组中,当面临大量重复键值的情况下,算法依然可以保持数组相对较好的平衡性

//对arr[l...r]部分进行partition操作
//返回p,使得arr[l...p-1] < arr[p; arr[p+1...r] > arr[p]
template<typename T>
int __partition2(T arr[], int l, int r){

    swap(arr[l], arr[rand()%(r-l+1)+l]);//生成一个在区间[r...l]的随机数,并将其与第一个元素交换位置
    T v = arr[l];//partition的标准简单取待排序数组的第一个元素
    
    //arr[l+1...i) <= v; arr(j...r] >= v  注意开区间
    int i = l + 1, j = r;//由于都是开区间,所以初始化的时候可以确保两个区间都为空
    while(true){
        while(i <= r && arr[i] < v) i++;
        while(j >= l+1 && arr[j] > v) j--;
        if(i > j) break;
        swap(arr[i], arr[j]);
        i++;
        j--;
    }
    swap(arr[l], arr[j]);//将第一个元素(标定元素)与j指针指向的元素互换位置(数组中最后一个小于等于v的位置)
    return j;
}

//对arr[l...r]部分进行快速排序
template<typename T>
void  __quickSort2(T arr[], int l, int r){
    if(l >= r)
        return;
    int p = __partition2(arr, l, r);
    __quickSort2(arr, l, p-1);
    __quickSort2(arr, p+1, r);

}

template<typename T>
void quickSort2(T arr[], int n){
    srand(time(NULL));
    __quickSort2(arr, 0, n-1);
}

二路快排与归并排序、优化前快速排序的速度对比:

4  三路快速排序法(对于较多重复数值数组排序的性能再优化

使用快速排序方法给具有大量重复键值的数组进行排序还有一个更加经典的实现方法:三路快排

之前对数组进行快速排序操作时,都是将数组分为两部分——小于v和大于v,而三路快排将整个数组分成了三个部分——小于v、等于v和大于v。通过这样的分割,在递归的过程中,不需要对等于v的部分进行递归排序,只需要对小于v和大于v的部分进行排序。

template <typename T>
void __quickSort3Ways(T arr[], int l, int r){

    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }

    swap( arr[l], arr[rand()%(r-l+1)+l ] );

    T v = arr[l];

    //分成三种情况讨论
    int lt = l;     // arr[l+1...lt] < v
    int gt = r + 1; // arr[gt...r] > v
    int i = l+1;    // arr[lt+1...i) == v
    while( i < gt ){
        if( arr[i] < v ){
            swap( arr[i], arr[lt+1]);
            i ++;
            lt ++;
        }
        else if( arr[i] > v ){//注意交换操作后的i元素(gt-1)依然是一个待确定的元素,所以i不需要++
            swap( arr[i], arr[gt-1]);
            gt --;
        }
        else{ // arr[i] == v
            i ++;
        }
    }

    swap( arr[l] , arr[lt] );//将标定元素v与指向小于v部分最后一个元素的指针lt交换,lt此时指向新的等于v部分的第一个元素

    __quickSort3Ways(arr, l, lt-1);//只需要对左右两部分进行递归,等于v的部分就不需要递归了
    __quickSort3Ways(arr, gt, r);
}

template <typename T>
void quickSort3Ways(T arr[], int n){

    srand(time(NULL));
    __quickSort3Ways( arr, 0, n-1);
}

三路快排与归并排序、二路快排的速度对比:

5  归并排序和快速排序的衍生问题

归并排序和快速排序都使用了分治算法,将原问题分割成同等结构的子问题,之后将子问题逐一解决后,原问题也得到了解决
两种排序方法使用了不同的解决思路:归并排序的采用了简单的二分思想,重点在于之后的merge操作;快速排序则将重点放在如何切分数据上,采用了选取一个标定点,并使用partition操作将该标定点放在了正确的位置上,此时才将数组分成了两部分,这样数组就不需要过多考虑之后的合并问题,只需要一步步递归下去即可

衍生问题:

1、求数组中的逆序对

使用归并排序方法可以解决求解逆序对的问题,使得算法的时间复杂度为O(nlogn),注意不是一个个比,而是一组组比,利用左右两边数组有序的特性。eg:只要小于其中一个元素,就小于该元素后面的所有元素,直接count加上相应的元素个数(待填坑... ... )

2、取出一个数组中第n大的元素

使用快速排序方法,在O(n)级别的时间复杂度下查找数组中第n大的元素(待填坑... ... )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值