文章目录
- 前言
- 一、常见的排序算法
- 二、快速排序的基本思想
- 三、快速排序的不同实现
- 1.hoare版本
- 2. 挖坑法
- 3. 前后指针法
- 4.三种版本单趟排序结果
- 5.快速排序三数取中优化
- 6.小区间优化
- 四、快速排序的特性总结
前言
手撕排序算法第六篇:快速排序!
从本篇文章开始,我会介绍并分析常见的几种排序,例如像插入排序,冒泡排序,希尔排序,选择排序,快速排序,堆排序,归并排序等等!
这篇文章我先来给大家手撕一下快速排序!
大家可以点下面的链接去阅读其他的排序算法:
C语言手撕排序算法
正文开始!
一、常见的排序算法
二、快速排序的基本思想
快速排序时Hoare于1962年提出的一种二叉树的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,有子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
三、快速排序的不同实现
1.hoare版本
我们先来看看动图简单了解一下!
如果右边做key的话,要让左边先走,这样能保证相遇位置比key的值大。
代码如下
//单趟快速排序
int PartSort(int* a,int left,int right)
{
int keyi = left;
while (left<right)
{
//加上=的判断是因为left和right的值相同就会导致死循环(如 5 2 1 5)
while (left < right&&a[keyi] <= a[right])
{
right--;
}
while (left < right && a[keyi] >= a[left])
{
left++;
}
Swap(&a[left],&a[right]);
}
Swap(&a[left],&a[keyi]);
return left;
}
void QuickSort(int* a,int begin,int end)
{
if (begin >= end)
return;
int keyi = PartSort(a,begin,end);
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
2. 挖坑法
先来看动图演示
简单实例:
同样的道理我就不给大家演示了哈。
相比第一个版本,挖坑法更好理解
- 不需要理解为什么最终相遇位置比key小
- 不需要理解为什么左边做key,右边先走!
本质上没有什么区别!
//挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int pit = left;
while (left < right)
{
//右边先走找小
while (left < right &&a[right] >=key)
{
--right;
}
a[pit] = a[right];
pit = right;
while (left < right && a[left] <= key)
{
++left;
}
a[pit] = a[left];
pit = left;
}
a[left] = key;
return left;
}
void QuickSort(int* a,int begin,int end)
{
if (begin >= end)
return;
int keyi = PartSort2(a,begin,end);
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
void TestQuickSort()
{
int a[] = { 4,2,7,8,5,1,0,6 };
printf("排序前:");
PrintArray(a, sizeof(a) / sizeof(a[0]));
QuickSort(a, 0,sizeof(a) / sizeof(a[0])-1);
printf("排序后:");
PrintArray(a, sizeof(a) / sizeof(a[0]));
}
int main()
{
TestQuickSort();
return 0;
}
3. 前后指针法
先来看一下动态图简单理解一下
(2条消息) 前后指针法 -- 快速排序-CSDN直播
int PartSort3(int* a, int left, int right)
{
int key = a[left];
int prev = left;
int cur = prev + 1;
while (cur<=right)
{
if (a[cur] < key&&a[++prev]!=a[cur])
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[left],&a[prev]);
return prev;
}
void QuickSort(int* a,int begin,int end)
{
if (begin >= end)
return;
//int keyi = PartSort1(a,begin,end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a,begin,end);
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
void TestQuickSort()
{
int a[] = { 4,2,7,8,5,1,0,6 };
printf("排序前:");
PrintArray(a, sizeof(a) / sizeof(a[0]));
QuickSort(a, 0,sizeof(a) / sizeof(a[0])-1);
printf("排序后:");
PrintArray(a, sizeof(a) / sizeof(a[0]));
}
int main()
{
TestQuickSort();
return 0;
}
选右边做key值。
4.三种版本单趟排序结果
5.快速排序三数取中优化
如果是有序或者接近有序的话,对于快排的时间复杂度就是O(N^2).这个时候还存在栈溢出的问题。
针对key进行优化即可
- 随机选key
- 三数取中(key选最左边,最右边,中间,选不是最大的,也不是最小的)
代码如下:
int GetMidIndex(int* a,int left,int right)
{
int mid = (left + right) >> 1;
if (a[left] <a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else //a[left] >a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right]>a[left])
{
return left;
}
else
{
return right;
}
}
}
6.小区间优化
根据快速排序的递归展开图我们可以知道到最后几层每个区间只有几个数据的时候,递归展开图所消耗的栈帧也很多。
所以对于这种情况,我们对于小区间进行优化,因为经过前面的快速排序,小区间已经有序或者接近有序,这样的话就可以使用直接插入排序进行优化!
减少区间很小时,不再使用递归划分的思想让他有序,而是直接使用插入排序对小区进行排序,减少递归调用。
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
return;
//小区间直接插入排序控制有序
if (end - begin +1<= 10)
{
InsertSort(a+begin,end-begin+1);
}
int keyi = PartSort3(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
四、快速排序的特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序。
- 时间复杂度为O(N*logN)
- 空间复杂度为O(N)
- 稳定性:不稳定。
(本章完!)