引言
在计算机科学中,排序算法是最基础且重要的算法之一。快速排序(Quick Sort)作为一种高效的排序算法,在实际应用中被广泛使用。平均时间复杂度为 (O(n log n)),最坏情况下为 (O(n^2))。本文将详细介绍快速排序算法的原理,结合具体代码进行分析,给出两种递归方法与一种非递归方法,并给出两种优化方案。
快速排序原理
快速排序的基本思想是通过选择一个基准元素(key),将数组分为两部分,使得左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素。然后递归地对左右两部分进行排序,最终得到一个有序的数组。
具体步骤如下:
- 选择基准元素:从数组中选择一个元素作为基准元素。
- 分区操作:将数组中的元素重新排列,使得所有小于等于基准元素的元素都在基准元素的左边,所有大于等于基准元素的元素都在基准元素的右边。这个过程称为分区(partition)。
- 递归排序:对左右两部分分别递归地应用快速排序算法。
代码实现
1.hoare法(递归)
/交换
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
if(left >= right)
return;
int key = left;
int begin = left, end = right;
while(begin < end)
{
while(begin < end && a[end] > a[key])
{
end--;
}
while(begin < end && a[begin] <= a[key])
{
begin++;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[key], &a[begin]);
key = begin;
QuickSort(a, left, key - 1);
QuickSort(a, key + 1, right);
}
}
2.双指针法(递归)
//快排递归:双指针
int PartSort2(int* a, int left, int right)
{
//三数取中(优化)
int mid = GetMid(a, left, right);
Swap(&a[mid], &a[left]);
int key = left;
int prev = left;
int cur = prev + 1;
while(cur <= right)
{
if(a[cur] < a[key] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
//快速排序(递归)
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 key = PartSort1(a, left, right);
int key = PartSort2(a, left, right);
QuickSort(a, left, key - 1);
QuickSort(a, key + 1, right);
}
}
代码解释:
Swap
函数:用于交换两个整数的值。QuickSort
函数:- 递归终止条件:当
left >= right
时,说明数组已经有序,直接返回。 - 分区操作:使用双指针法将数组分为两部分,左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。
- 递归排序:对左右两部分分别递归地调用
QuickSort
函数。
- 递归终止条件:当
3.非递归法
递归方法来实现排序终归有栈溢出的风险,所以这里提供一种非递归方式实现快速排序
//快速排序(非递归)
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while(!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int key = PartSort2(a, begin, end);
if(end > key + 1)
{
STPush(&st, end);
STPush(&st, key + 1);
}
if(begin < key - 1)
{
STPush(&st, key - 1);
STPush(&st, begin);
}
}
STDestory(&st);
}
这里使用栈来存储指针begin与end ,栈在堆中存储,能通过malloc不断申请内存空间,有效规避了栈溢出的风险。
测试代码
下面对快速排序做一个简单的测试:
void TestOP()
{
srand((unsigned int)time(NULL));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
for(int i = 0; i < N; i++)
{
a1[i] = rand();
}
int begin1 = clock();
// 此处可调用 QuickSort 进行测试
QuickSort(a1, 0, N - 1);
int end1 = clock();
printf("QuickSort:%d\n", end1 - begin1);
}
int main()
{
TestOP();
return 0;
}
在这个测试文件中,我们生成了一个包含 100000 个随机整数的数组,并调用
QuickSort
函数对其进行排序,最后输出排序所需的时间。
复杂度分析
- 时间复杂度:平均情况下为 (O(n log n)),最坏情况下为 (O(n^2))。
- 空间复杂度:递归调用栈的深度为 (O(log n)),因此空间复杂度为 (O(log n))。
优化方案
1.三数取中
若数组排序前就有序,时间复杂度为O(n^2),那么此时三数取中就是消除这种接近有序带来的大量重复计算的优化方法
代码实现
//三数取中
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
// left midi right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
紧接着再把返回的mid值与left处值交换即可
//三数取中(优化)
int mid = GetMid(a, left, right);
Swap(&a[mid], &a[left]);
2.小区间优化
我们通过前面所学内容可以得知快排的递归是类似与二叉树递归的一种算法,那么高度越高所消耗的空间越大,每个递归函数的调用会建立一个函数栈帧,为了避免出现栈溢出的情况,我们可以采取小区间优化来规避风险并高效实现排序。
代码实现
(可任选一种时间复杂度为O(n^2)的排序,这里选择更为高效的插入排序。)
//插入排序 最好O(N) 最坏O(N ^ 2)
void InsertSort(int* a, int n)
{
for(int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while(end >= 0)
{
if(tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
//小区间优化
if((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);
}
3.其他
除了这些优化方案,可能有人会觉得快速排序难以理解,这里还有一种较为通俗的挖坑法(并没有改变排序效率,只是更为通俗易懂),可以自行查阅资料。
总结
快速排序是一种高效的排序算法,通过分治法和分区操作,将数组不断地分为两部分进行排序。在实际应用中,通过三数取中和小区间优化,可以提高算法的性能。希望本文对你理解快速排序算法有所帮助。