一.快速排序的基本思想
关于快速排序,它的基本思想就是选取一个基准,一趟排序确定两个区间,一个区间全部比基准值小,另一个区间全部比基准值大,接着再选取一个基准值来进行排序,以此类推,最后得到一个有序的数列。
二.快速排序的步骤
- 1.选取基准值,通过不同的方式挑选出基准值。
- 2.用分治的思想进行分割,通过该基准值在序列中的位置,将序列分成两个区间,在准值左边的区间里的数都比基准值小(默认以升序排序),在基准值右边的区间里的数都比基准值大。
- 3.递归调用快速排序的函数对两个区间再进行上两步操作,直到调用的区间为空或是只有一个数。
三.关于选取基准值的方式
1.固定位置选取基准值
基本思想:选取第一个或最后一个元素作为基准值。
如上是以第一个数作为选取基准的方式的第一趟排序的结果,接着就是对分好的两个区间再进行递归的快排。
但是,这种选取基准值的方法在整个数列已经趋于有序的情况下,效率很低。比如有序序列(0,1,2,3,4,5,6,7,8,9),当我们选取0为基准值的时候,需要将后面的元素每个都交换一遍,效率很低。所以这种以固定位置选取基准值的方式,只适用于该序列本身并不是趋于有序的情况下,比如一串随机数列,此时的效率还能够差强人意。
为了避免这种已经有序的情况,于是有了下面两种选取基准值的方式
下面是关于选取固定基准值快排的代码
int SelectPivot(int* a, int left, int right)//选取基准值函数
{
return a[left];
}
void QuickSort(int* a, int left, int right)
{
assert(a);
int i, j;
int pivot = SelectPivot(a, left, right);//确定基准值
if (left < right)
{
i = left + 1;//以第一个数left作为基准数,从left+1开始作比较
j = right;
while (i < j)
{
if (a[i] > pivot)//如果比较的数比基准数大
{
swap(a[i], a[j]);//把该比较数放到数组尾部,并让j--,比较过的数就不再比较了
j--;
}
else
{
i++;//如果比较的数比基准数小,则让i++,让下一个比较数进行比较
}
}
//跳出while循环后,i==j
//此时数组被分成两个部分,a[left+1]-a[i-1]都是小于a[left],a[i+1]-a[right]都是大于a[left]
//将a[i]与a[left]比较,确定a[i]的位置在哪
//再对两个分割好的部分进行排序,以此类推,直到i==j不满足条件
if (a[i] >= a[left]) //这里必须要用>=,否则相同时会出现错误
{
i--;
}
swap(a[i], a[left]);
QuickSort(a, left, i);
QuickSort(a, j, right);
}
}
//测试函数
int main()
{
int a[] = { 2,5,4,9,3,6,8,7,1,0};
const size_t n = sizeof(a) / sizeof(a[0]);
QuickSort(a, 0, n - 1);
Print(a, n);
system("pause");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
2.随机选取基准
基本思想:选取待排序列中任意一个数作为基准值。
因为快排函数部分的代码是一样的,只是选取基准值部分的函数不相同,下面只附上选取基准值函数的代码
int SelectPivot(int* a, int left, int right)//选取基准值函数
{
srand((unsigned)time(NULL));
int pivotPos;
if (left < right)//这里需要保证传进来的left必须小于left
{
pivotPos = rand() % (right - left) + left;
}
else
{
pivotPos = left;//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
}
swap(a[pivotPos], a[left]);
return a[left];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
引入随机化快速排序的作用,就是当该序列趋于有序时,能够让效率提高,大量的测试结果证明,该方法确实能够提高效率。但在整个序列数全部相等的时候,随机快排的效率依然很低,它的时间复杂度为O(N^2),但出现这种最坏情况的概率非常的低,所以它还是一种效率比较好的方法,一般情况下都能够达到O(N*lgN)。
3.三数取中法选取基准值
基本思想:取第一个数,最后一个数,第(N/2)个数即中间数,三个数中数值中间的那个数作为基准值。举个例子,对于int a[] = { 2,5,4,9,3,6,8,7,1,0};
,‘2’、‘3’、‘0’,分别是第一个数,第(N/2)个是数以及最后一个数,三个数中3最大,0最小,2在中间,所以取2为基准值。
下面也是附上三数取中法的代码
int SelectPivot(int* a, int left, int right)//选取基准值函数
{
int mid;
if (left < right)
{
mid = (right - left) / 2;
}
else
{
return a[left];//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
}
if (a[mid] > a[right])
{
swap(a[mid], a[right]);
}
if (a[left] > a[right])
{
swap(a[left], a[right]);
}
if (a[mid] > a[left])
{
swap(a[mid], a[left]);
}
//上面三步完成之后,a[left]就是三个数中最小的那个数
return a[left];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
采用三数取中法很好的解决了很多特殊的问题,但对于很多重复的序列,效果依然不好。于是在这三种选取基准值的方法下,另外地还有三种优化方法。
四.快速排序的优化
优化一:当待排序序列的长度分割到一定大小后,使用插入排序。
优化原因:对于待排序的序列长度很小或是基本趋于有序时,快排的效率还是插排好。
自定义截止范围:序列长度N=10。当待排序的序列长度被分割到10时,采用快排而不是插排。
if (n <= 10)//当整个序列的大小n<=10时,采用插排
{
InsertSort(a, n);
}
- 1
- 2
- 3
- 4
优化二:在一次排序后,可以将与基准值相等的数放在一起,在下次分割时可以不考虑这些数。
因为这一次改动的代码比较多,所以再继续把所有的代码全部拿出来看一下
int SelectPivot(int* a, int left, int right)//选取基准值函数
{
int mid;
if (left < right)
{
mid = (right - left) / 2;
}
else
{
return a[left];//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
}
if (a[mid] > a[right])
{
swap(a[mid], a[right]);
}
if (a[left] > a[right])
{
swap(a[left], a[right]);
}
if (a[mid] > a[left])
{
swap(a[mid], a[left]);
}
//上面三步完成之后,a[left]就是三个数中最小的那个数
return a[left];
}
void QuickSort(int* a, int left, int right)
{
assert(a);
if (n <= 10)//当整个序列的大小n<=10时,采用插排
{
InsertSort(a, n);
return;
}
int i, j;
int pivot = SelectPivot(a, left, right);//确定基准值
if (left < right)
{
i = left + 1;//以第一个数left作为基准数,从left+1开始作比较
j = right;
while (i < j)
{
if (a[i] == pivot)//处理与基准值相等的数,都放到数组末尾
{
swap(a[i], a[j]);
--j;
}
else if (a[i] > pivot)//如果比较的数比基准数大
{
while (1)
{
if (a[j] == pivot)//如果要换的数值等于基准值,让j--,与前一个交换
{
--j;
}
else
{
break;
}
}
swap(a[i], a[j]);//把该比较数放到数组尾部,并让j--,比较过的数就不再比较了
--j;
}
else
{
++i;//如果比较的数比基准数小,则让i++,让下一个比较数进行比较
}
}
//跳出while循环后,i==j
//此时数组被分成两个部分,a[left+1]-a[i-1]都是小于a[left],a[i+1]-a[right]都是大于a[left]
//将a[i]与a[left]比较,确定a[i]的位置在哪
//再对两个分割好的部分进行排序,以此类推,直到i==j不满足条件
if (a[i] >= a[left]) //这里必须要用>=,否则相同时会出现错误
{
i--;
}
swap(a[i], a[left]);
int tmp = right;//用tmp表示从后往前第一个不是基准值的数
while (tmp > i)
{
if (a[tmp] == pivot)
{
--tmp;
}
else//else表示没有与基准值重复的值
{
QuickSort(a, left, i - 1);
QuickSort(a, i + 1, right);
return;
}
}
int pos = tmp;//因为后面要用到tmp,所以运算的话用一个pos来代替tmp进行运算
int r = right;//因为后面要保证right还是先前的右边界,所以运算的话用另外一个变量来表示
int count = 0;
while (pos > i&&r > tmp)
{
swap(a[pos], a[r]);
--r;
--pos;
++count;//换了一次让count++
}
QuickSort(a, left, i-1);//对左区间快排,i-1是左区间的最后一个数
QuickSort(a, right-count+1, right);//对右区间快排,right-count+1是右区间的第一个数
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
这种聚集与基准值相等的值的优化方法,在解决数据冗余的情况下非常有用,提高的效率也是非常多。
优化三:优化递归操作
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。
只是在尾部的递归调用的时候做了以下改变
while (left < right)
{
QuickSort(a, left, i - 1);//对左区间快排,i-1是左区间的最后一个数
left = right - count + 1;
}
- 1
- 2
- 3
- 4
- 5
其实这种优化编译器会自己优化,相比不使用优化的方法,时间几乎没有减少。
所以到这里,总结一下,对于快速排序(一组随机数组),效率最快的优化方案应该是三数取中法+插排+聚集相等元素,对于尾递归可以有也可以没有,对于效率的变化改变不大。