本文承接​​排序算法总结1​

1 快速排序---版本1

算法思想:

分治算法思想的典型应用

将数组中第一个元素arr[0]作为标志元素,通过一趟快速排序将待排数组分成两部分,其中左侧部分的元素值都比标志元素小,而右侧部分的元素值都大于等于标志元素;然后接着对两部分数组递归应用快速排序,直至全部有序。

排序算法总结(下)——快速排序_排序算法

从上图中我们可以看出,第一趟快速排序结束后,标志元素所处的位置即为它的最终位置;同时我们也可以看出,partition(将数组进行分组)是快速排序的核心操作,这一点和归并算法不同,后者仅仅根据数组下标,简单地将元素平均分成两部分。

接下来我们对partition过程进行分析,如果理解了下面一张图的含义,该版本的快速排序算法也就迎刃而解了。

排序算法总结(下)——快速排序_数组_02

我们用指针i来遍历除首元素之外的其他元素,用指针j表示 < v 的最后一个元素的位置,假设我们此时正在访问元素e(指针i即为元素e的下标),显然会有下面两种情况:

//伪代码表示如下
if (e < v){
swap(arr[j + 1],arr[i]);
j++;
i++;
}else{
i++;
}

进行到最后,我们会得到如下所示的结果 排序算法总结(下)——快速排序_排序算法_03 此时只需要交换v和arr[j]的元素即可,第一趟快速排序便完成了。剩下的就是对两部分数组继续递归处理即可。

代码如下:

#include <iostream>
#include <algorithm>


template <typename T>
int __partition(T arr[],int l,int r){
T pivot = arr[l];
int j = l;
for (int i = l + 1;i <= r;i++){
if (arr[i] < pivot){
std::swap(arr[i],arr[j + 1]);
j++;
}
}
std::swap(arr[l],arr[j]);
return j;
}

template <typename T>
void __quickSort(T arr[],int l ,int r){
if (l >= r)
return ;
//partition过程,返回标志元素在数组中的最终位置下标
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);
}

对该本版的快速排序测试如下

排序算法总结(下)——快速排序_数组_04

接下来我们用该算法对有100000个近乎有序的元素的数组进行排序,测试效果如下:

排序算法总结(下)——快速排序_排序算法_05

程序之前崩溃掉了。。。。。。。

造成这种现象的原因是,系统会为我们的递归调用维护一个调用栈,当我们选用近乎有序的数组中的第一个作为pivot(基准元素/中枢元素)时,剩余的元素几乎都大于该pivot而继续被递归调用,因此系统维护的调用栈的深度过大,以至于这种情况下该版本的快速排序的时间复杂度会退化为O(n2)O(n^2)O(n2)级别,效果可想而知。接下来我们针对这种情形对该版本进行改进。

1.1 快速排序---版本1的改进

我们对pivot进行随机选取,以期望获得较为平衡的partition,具体代码具体改动如下:

//快速排序1改进版
#include <iostream>
#include <algorithm>

template <typename T>
int __partition1_enhance(T arr[],int l,int r) {
//优化:arr[l]与随机位置的数值交换
std::swap(arr[rand() % (r - l + 1) + l],arr[l]);

T flag = arr[l];

int j = l;
for (int i = l + 1; i <= r; i++) {
if (arr[i] > flag) {
//do nothing, just i++
} else {
std::swap(arr[j + 1],arr[i]);
j++;

//可以简写为
// std::swap(arr[++j],arr[i]);
}
}
std::swap(arr[l],arr[j]);
return j;
}


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

if (l >= r)
return;

int p = __partition1_enhance(arr,l,r);
__quickSort1_enhance(arr,l,p-1);
__quickSort1_enhance(arr,p+1,r);
}


template <typename T>
void quickSort1_enhance(T arr[],int n) {
//设置随机数种子
srand(time(NULL));
__quickSort1_enhance(arr,0,n-1);
}

测试结果:

排序算法总结(下)——快速排序_数组_06

可以看出,随机选取pivot对于排序几乎有序数组有很大的改善。

更进一步,我们利用优化好的算法,对含有大量重复元素的数组进行排序(100000个数据,元素取值范围为[0,5]),测试结果如下:

排序算法总结(下)——快速排序_i++_07

我们看到,对同样大的数据量,不同的数据特征下,同一个快速排序算法的效率几乎是天壤之别,我们继续分析一下改进版的快速排序为什么会对含有大量重复元素的数组排序显得如此无力。 其实原因和之前一样,同样是递归的深度过深导致的。由于含有大量重复元素(假如元素5重复的次数最多),那么根据我们的算法,一旦pivot选择了5,那么必然剩余元素中所有的5都会出现在pivot的右侧(根据实现不同,也可能在左侧,但无论哪种情况对于我们都是不可接受的,因为都会导致递归的数组大小严重失衡),结果可想而知。

因此下一步我们的优化策略就是,对于重复的元素,尽可能地令其随机分布在pivot两侧,而不是全部集中在pivot的一边,由此引出了接下来的 二路快速排序

2 二路快速排序

排序算法总结(下)——快速排序_排序算法_08

我们分别用指针i,j分别从左到右和从右到左进行遍历,伪代码如下:

//伪代码
while(true) {
while(i <= r && arr[i] < pivot) i++;
while(j >l && arr[j] > pivot) j--;
if (i > j) break;
swap(arr[j],arr[i]);
i++;
j--;
}
swap(arr[j],arr[l]);

这样对于i来说,>= v的元素都会到j的位置,同理,对于j来说,<= v的元素都会到i的位置,这也就达到了重复元素较均匀分布在两侧的目的。具体的实现代码如下所示:

#include <iostream>
#include <algorithm>
#include <ctime>
#include "InsertionSort.h"


template <typename T>
int __partition2(T arr[],int l,int r) {
std::swap(arr[rand() % (r - l + 1) + l],arr[l]);

T pivot = arr[l];
int i = l + 1;
int j = r;
while(true) {
while(i <= r && arr[i] < pivot) i++;
while(j >l && arr[j] > pivot) j--;
if (i > j) break;
std::swap(arr[j],arr[i]);
i++;
j--;
}
std::swap(arr[j],arr[l]);
return j;

}

template <typename T>
void __quickSort(T arr[],int l,int r) {
if (r - l >= 15) {
insertionSort(arr,l,r);
return ;
}

int q = __partition2(arr, l ,r);
__quickSort(arr,l,q - 1);
__quickSort(arr,q + 1,r);
}

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

排序算法总结(下)——快速排序_i++_09

经过修改,我们的二路归并排序对含有大量重复元素的数组进行排序的效果已经相当不错了。但是,我们仔细想想,既然含有大量重复元素,如果我恰好选择了其中一个作为pivot,那么在pivot必然有大量等于pivot的元素,如果我们在递归的时候去掉重复元素,那么可以想象,我们会在一定程度上又提高了算法的性能。接下来我们对快速排序做最后一次优化(真的是最后一次了。。。。),很多语言自带的排序算法就是这种思想的实现,就是赫赫有名的 三路快速排序

3 三路快速排序

排序算法总结(下)——快速排序_数组_10

三路快速排序以v作为pivot,将元素分为 < v, = v, > v三部分,partition完成之后,只对<v和>v的部分进行归并排序就可以了。

我们用指针i来遍历元素,用指针lt表示<v的最后一个元素的索引,指针gt表示>v的第一个元素的索引,假如此时我们正在访问索引为i的元素e,那么可能出现的情况有三种

if (e > v){
swap(arr[i],arr[gt - 1]);
gt--;
}else if (e < v){
swap(arr[i],arr[lt + 1]);
lt++;
i++;
}else{
i++;
}

具体代码如下:

#include <iostream>
#include <ctime>
#include <algorithm>

template <typename T>
void __quickSort3(T arr[],int l,int r){
if (l >= r)
return;

//partition过程
std::swap(arr[l],arr[rand() % (r - l + 1) + l]);
T pivot = arr[l];
int lt = l;
int gt = r + 1;
int i = l + 1;
while (i < gt){
if ( arr[i] > pivot){
std::swap(arr[--gt],arr[i]);
}else if (arr[i] < pivot){
std::swap(arr[++lt],arr[i]);
i++;
}else{
i++;
}
}
__quickSort3(arr,l,lt);
__quickSort3(arr,gt,r);
}

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

至此,我们所有版本的快速排序算法都已经完成了,完整的代码以及测试用例参见​​我的github​