快速排序:
基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
性能:
-
快速排序的时间复杂度:
平均为O(N*logN)
最坏为 O(N^2) -
快速排序的稳定性:不稳定
优化:
-
三数取中法
上面的代码思想都是直接拿序列的最后一个值作为枢轴,如果最后这个值刚好是整段序列最大或者最小的值,那么这次划分就是没意义的。
所以当序列是正序或者逆序时,每次选到的枢轴都是没有起到划分的作用。快排的效率会极速退化。
所以可以每次在选枢轴时,在序列的第一,中间,最后三个值里面选一个中间值出来作为枢轴,保证每次划分接近均等。 -
直接插入
由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。
下面介绍一下三数取中的优化代码:
int GetMidIndex(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid = left + ((right - left) >> 1); //防止溢出,并且效率也比除法要高
if (a[left] < a[mid])
{
if (a[mid] < a[right]) //left mid right
{
return mid;
}
else if (a[left] > a[right]) // right left mid
{
return left;
}
else //left right mid
{
return right;
}
}
else //a[left] > a[mid]
{
if (a[mid] > a[right]) //left mid right
{
return mid;
}
else if (a[left] > a[right]) // mid left right
{
return left;
}
else // mid right left
{
return right;
}
}
}
将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法
- 前后指针版本
动态演示:
方法:
版本1:hoare法
//单趟排序
int Partion1(int* a, int left, int right)
{
//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int keyi = left;
while (left < right)
{
//左边做key,右边先走,找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//左边再走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
return left;
}
版本2:挖坑法
//挖坑法
void Partion2(int* a, int left, int right)
{
//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int key = a[left];
int pivot = left;
while (left < right)
{
//右边找小,放到左边的坑里面
while (left < right && a[right] >= key)
{
--right;
}
a[pivot] = a[right];
pivot = right;
//左边找大,放到右边的坑里面
while (left < right && a[left] >= key)
{
++left;
}
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
}
版本3:前后指针法
//前后指针
int Partion3(int* a, int left, int right)
{
//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
//选左边做key
int keyi = left;
int cur = left+1;
int prev = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[++prev]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
总代码:
递归版本:
//快速排序
//O(N*logN)
void QuickSort(int* a, int left,int right)
{
if (left >= right)
{
return;
}
//小区间优化,当分割到小区间时,不再用递归分割思路让这段子区间有序
//对于递归快排,减少递归的次数
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = Partion1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
非递归版本:
当递归程序的递归深度太深,容易造成stack overflow,因此只能考虑非递归。
当我们要用非递归时,要立刻想到栈,因为递归的原理符合栈的先进后出的规则,递归和栈在面试题和算法中总是联系在一起的
void QuickSortNonR(int* a, int left, int right)
{
ST st;
StackInit(&st);
//将区间压入栈中
//先入的left,后入的right
//先出right,后出left
//先处理右区间
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
int end = StackTop(&st);//弹出right
StackPop(&st);
int begin = StackTop(&st);//弹出left
StackPop(&st);
int keyi = Partion3(a, begin, end);
//[begin, keyi-1] keyi [keyi+1,end]
//先压入右区间
if (keyi + 1 < end)
{
StackPush(&st, keyi+1);
StackPush(&st, end);
}
//再压入左区间
if (begin < keyi - 1)
{
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
}
StackDestory(&st);
}