排序算法之快速排序
前言
在C语言阶段,我们学习过如何使用C语言库中提供的快速排序
在进行更深层次的学习以后,本篇博客主要讲述如何实现快速排序
包括快排递归版本与非递归版本
提示:以下是本篇文章正文内容,下面案例可供参考
一、快排的基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
什么?没看懂? 我们可以详看hoare版本的单趟操作动图
二、递归实现快排
注意:基准值key,可以取所有待排元素的任意一个,根据本人习惯此篇文章基准值都是最左边元素
也就是 keyi = left 基准值的位置不同代码可能略有不同 但大体的思路是相同的,同时基准值key与快排的效率息息相关,我们尽量使key的值接近与该区间的中间大小,使得效率更高,本篇博客利用三数取中法获取基准值,并且会手动将基准值放在最左边
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
else // a[left] >= a[mid]
{
if (a[right] > a[left])
{
return left;
}
else if (a[mid] > a[right])
{
return mid;
}
else
{
return right;
}
}
}
1、hoare版本
hoare版本一趟排序的基本思想如下:
我们在所有元素内随机找一个元素作为key 在走了一趟排序之后
我们可以得到key左边元素都比key小或者与key相等,key右边元素都比key大或者与key相等
同时将key放到left与right相遇的地方
实现代码
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = GetMidIndex(a, left, right);
Swap(&a[keyi], &a[left]);
keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
2、挖坑法
这个方法原理与hoare原理相似,但更好理解,左边找大右边找小,在不断挖坑的同时也在填坑
实现代码
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int keyi = GetMidIndex(a, left, right);
Swap(&a[keyi], &a[left]);
keyi = left;
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= key)
{
right--;
}
//Swap(&a[hole], &a[right]);
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3、前后指针法
前后指针法就比较巧妙,总结来说只在做以下几件事
cur找小,a[cur] < key,++prev,交换prev与cur位置的值
1、最开始prev与cur相邻
2、当cur遇到比key大的值之后,cur++,prev不动,此时他们之间的值都是比key大的值
3、cur找小,找到小的之后,跟++prev位置的值交换,这个过程相等于把比key大的翻滚式往右边推同时把小的换到左边
实现代码
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = GetMidIndex(a, left, right);
Swap(&a[keyi], &a[left]);
keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
以上是单趟排序的三种方法,这三种方法都实现的结果都相同 key左边元素都比key小或者与key相等,key右边元素都比key大或者与key相等
这时可以将得到的区间为 [ 0 , key-1 ] key [ key+1 , n-1 ] ,继续将左区间和右区间 分别进行单趟排序操作,每个区间完成之后可以继续分为三段区间,继续重复以上操作
直到区间内只有一个元素,那么这个元素必定有序
当这个区间左边有序,右边也有序时,整体就有序了
这个过程可以近似看作二叉树的前序遍历,所以整个快排需要用递归来实现
而递归的结束条件就是这个区间只有一个元素,或者没有元素
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
看到这里有的小伙伴可能会问一个问题,left与right相遇时,会将a[left] 与 a[keyi]交换也就是说a[left]是比key小的,那么这里如何保证a[left]是比基准值key小的呢
结论:
1、左边做key,右边先走;保障了相遇位置的值比key小
2、右边做key,左边先走;保障了相遇位置的值比key大
我们以左边做key为例进行解释:
三、非递归实现快排
关于快排的非递归实现,其实本质上是利用栈的性质,也就是先进后出的性质来模拟递归的过程,这个过程很像利用栈实现二叉树的前序遍历
在实现的时候,left与right作为一组数据入栈和出栈
代码如下
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
left = STTop(&st);
STPop(&st);
right = STTop(&st);
STPop(&st);
int keyi = PartSort1(a, left, right);
if (right > keyi + 1)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
四、快速排序的特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定