前言
本文主要介绍快速排序算法的三大实现方式,时间复杂度的计算,以及简单优化一下快速排序算法的小技巧。
一、快速排序的介绍与基本思想
- 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法.
- 基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
*大致呈现为以下形式:
二、实现快速排序的三大方式(递归版本)
(一).挖洞法
- 第一趟操作:定义洞为最开始的位置以及以第一个元素为基准,然后从最右边开始找比基准小的数放到左边,从最左边开始找比基准大的数放到右边。具体实现上面的操作为不断利用洞的位置转换,一步步将小的数放到左区间,大的数放到右区间,最后将一开始定的基准元素转到其真实的大小位置上。
- 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
- 图形解释:
- 第一趟排序:
- 多趟排序:
该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part1Sort(int* a, int left, int right)
{
//第一趟排序:
int begin = left, end = right;
int pivot = begin;//让洞的位置为最开始位置
int key = a[begin];
while (begin < end)
{
//从最右面开始找比key小的数,放到左面
while (begin < end && a[end] >= key)//注意begin<end要加上防止比的超出界限,a[end]>=key要加上等于防止出现死循环BUG
{
--end;
}
//小的数据放在左边的坑位,自己的位置形成新的坑位
a[pivot] = a[end];
pivot = end;
//从最左面开始找比key大的数,放在右面
while (begin < end && a[begin] <= key)
{
++begin;
}
//大的数据放在右边的坑位,自己的位置形成新的坑位
a[pivot] = a[begin];
pivot = begin;
}
//最后将begin或者end(此两者相等)赋给洞,此时洞的位置即key值自己真实的大小位置
pivot = begin;
a[pivot] = key;
//此时再让左子区间和右子区间分别有序,那么最终就有序了
//实现方法:分治递归:}
return pivot;
}
void QuickSort(int* a, int left, int right)//实现分治递归
{
if (left >= right)
{
return;
}
//将左子区间和右子区间变为有序,实现方法:分治递归;
int keyIndex = Part1Sort(a, left, right);
QuickSort(a, left, keyIndex - 1);
QuickSort(a, keyIndex + 1, right);
}
(二).左右指针法
- 第一趟操作:以第一个元素为基准,**从右边开始找比基准小的数据,从右边开始找比基准大的数据,然后将从两边分别找的比基准小的数据和比基准大的数据进行交换,**最后将基准元素转到其真实大小的位置
- 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
- 图形解释:
- 第一趟排序:
- 多趟排序
该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part2Sort(int* a, int left, int right)//左右指针法来进行快速排序
{
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
//从右边开始找比a[keyi]小的数据
while (begin < end && a[end] >= a[keyi])
{
--end;
}
//从左边开始找比a[keyi]大的数据
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
//从两边分别找到比a[keyi]小的数据和比a[keyi]大的数据,将两数据进行交换
Swap(&a[begin], &a[end]);
}
//将a[keyi]于a[begin]交换或者于a[end]交换,将a[keyi]放在真实大小的位置
Swap(&a[begin], &a[keyi]);
return begin;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//将左子区间和右子区间变为有序,实现方法:分治递归;
int keyIndex = Part2Sort(a, left, right);
QuickSort(a, left, keyIndex - 1);
QuickSort(a, keyIndex + 1, right);
}
(三).前后指针法
- 第一趟排序:以第一个元素为基准,定义prev 与cur 两个变量,让prev一开始指向第一个元素,cur指向第二个元素,cur从当前位置开始向后遍历到末尾,期间
cur找比基准小的数,每次遇到比基准小的值就停下来,++prev,交换prev和cur位置的值,最后将基准元素与prev位置的值进行交换,让基准的数转到其真实大小的位置。 - 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
- 图形解释:
- 第一趟排序
*多趟排序:
该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part3Sort(int* a, int left, int right)//前后指针法来进行快速排序
{
int keyi = left;
int prev = left, cur = left + 1;
//cur找比a[keyi]小的数,每次遇到比key小的值就停下来,++prev,交换prev和cur位置的值
while (cur<=right)
{
if (a[cur] < a[keyi]&&++prev !=cur)//这里不需要等于,不会出现死循环的BUG
{
Swap(&a[prev], &a[cur]);//交换函数最后会给出定义
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//将左子区间和右子区间变为有序,实现方法:分治递归;
int keyIndex = Part3Sort(a, left, right);
QuickSort(a, left, keyIndex - 1);
QuickSort(a, keyIndex + 1, right);
}
三、快速排序的简单优化
(一).三数取中法
- 在实现了快速排序之后,我们发现,keyi的位置,是影响快速排序效率的重大因素。因此有人采用了三数取中的方法解决选keyi不合适的问题。
- 三数取中:即知道这组无序数列的首和尾后,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值(keyi),进行快速排序,即可进一步提高快速排序的效率。
- 算法代码实现:
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;//等效于整除于2
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[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
(二).小区间改造法
- 由于快速排序是递归进行的,当递归到最后几层时,此时数组中的值其实已经接近有序,而且这段区间再递归会极大占用栈(函数栈帧开辟的地方)的空间,
- 接下来,我们对其进行优化,如果区间数据量小于10,我们就不进行递归快速排序了,转而使用插入排序。(插入排序会在最后总的代码中体现)
代码实现:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyIndex = PartSort3(a, left, right);
// 小区间
if (keyIndex - 1 - left > 10)
{
QuickSort(a, left, keyIndex - 1);
}
else
{
InsertSort(a + left, keyIndex - 1 - left + 1);//插入排序
}
if (right - (keyIndex + 1) > 10)
{
QuickSort(a, keyIndex + 1, right);
}
else
{
InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);插入排序
}
}
四、快速排序的时间复杂度计算
- 每一趟的时间复杂度为O(N)
- 递归的时间复杂度为O(logN)
- 故而总的时间复杂度为O(N*logN)
五、快速排序的完整代
#include<stdio.h>
//打印数组函数:
void Print(int* a, int n) {
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//插入排序:
void InsertSort(int* a, int n) {
for (int i = 0;i < n - 1;i++) {
int end=i;
//[0,end]有序,插入后,[0,end+1]有序
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];//数据往后挪动
end--;
}
else {
break;
}
}
a[end + 1] = tmp;//比较到末尾的一刻,或者照顾到在中间的情况:当end比tmp小的时候
}
//交换元素的函数:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//三数取中函数:
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;//等效于整除于2
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[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
//利用挖洞法来进行快速排序:
int Part1Sort(int* a, int left, int right)//实现分治递归//时间复杂度:O(N*log(N))
{
//三数取中法,如果一开始为有序时候,为了降低时间复杂度(将begin赋予整体数据较为靠中数据的下标值)
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
//第一趟排序:
int begin = left, end = right;
int pivot = begin;//让洞的位置为最开始位置
int key = a[begin];
while (begin < end)
{
//从最右面开始找比key小的数,放到左面
while (begin < end && a[end] >= key)//注意begin<end要加上防止比的超出界限,a[end]>=key要加上等于防止出现死循环BUG
{
--end;
}
//小的数据放在左边的坑位,自己的位置形成新的坑位
a[pivot] = a[end];
pivot = end;
//从最左面开始找比key大的数,放在右面
while (begin < end && a[begin] <= key)
{
++begin;
}
//大的数据放在右边的坑位,自己的位置形成新的坑位
a[pivot] = a[begin];
pivot = begin;
}
//最后将begin或者end(此两者相等)赋给洞,此时洞的位置即key值自己真实的大小位置
pivot = begin;
a[pivot] = key;
//此时再让左子区间和右子区间分别有序,那么最终就有序了
//实现方法:分治递归:}
return pivot;
}
//左右指针法来实现快速排序:
int Part2Sort(int* a, int left, int right)//左右指针法来进行快速排序
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
//从右边开始找比a[keyi]小的数据
while (begin < end && a[end] >= a[keyi])
{
--end;
}
//从左边开始找比a[keyi]大的数据
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
//从两边分别找到比a[keyi]小的数据和比a[keyi]大的数据,将两数据进行交换
Swap(&a[begin], &a[end]);
}
//将a[keyi]于a[begin]交换或者于a[end]交换,将a[keyi]放在真实大小的位置
Swap(&a[begin], &a[keyi]);
return begin;
}
//前后指针法来进行快速排序:
int Part3Sort(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int keyi = left;
int prev = left, cur = left + 1;
//cur找比a[keyi]小的数,每次遇到比key小的值就停下来,++prev,交换prev和cur位置的值
while (cur<=right)
{
if (a[cur] < a[keyi]&&++prev !=cur)//这里不需要等于,不会出现死循环的BUG
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
快速排序算法递归:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
//再次可改变Part3Sort的函数名来实现上述三种快排方式的转变
int keyIndex = Part3Sort(a, left, right);
// 小区间优化
if (keyIndex - 1 - left > 10)
{
QuickSort(a, left, keyIndex - 1);
}
else
{
InsertSort(a + left, keyIndex - 1 - left + 1);
}
if (right - (keyIndex + 1) > 10)
{
QuickSort(a, keyIndex + 1, right);
}
else
{
InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
}
}
int main()
{
int a[] = { 3, 5, 2, 7, 8, 6, 1, 9, 4, 0 };
QuickSort(a, 0, sizeof(a) / sizeof(int)-1);
Print(a, sizeof(a) / sizeof(int));
return 0;
}
- 注意可以通过改变Part1Sort,Part2Sort,Part3Sort,的函数名来实现对快排三种方式的转变
总结
- 本文主要呈现了实现快排的三种方式–挖洞法、左右指针法、前后指针法,此过程大家可依据调试过程加深理解,以及优化快排的 两种小技巧–三数取中以及设置小区间的 方法。