快速排序(Quick Sort)是一种高效的排序算法,由英国计算机科学家霍尔(C.A.R. Hoare)在1960年提出。它的基本思想是,通过一次排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
算法步骤
选择基准值(pivot):
从数列中挑出一个元素,称为“基准”(pivot)。通常选择第一个元素、最后一个元素、中间元素或者随机选择一个元素作为基准值。
分区(Partition)操作:
重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区操作(Partition operation)。注意,在分区操作中,并不要求将小于基准的元素按照某种顺序排列,同样也不要求将大于基准的元素按照某种顺序排列。
递归排序子序列:
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法示例
假设有一个无序序列如下:[6, 2, 8, 5, 1, 4, 9, 3, 7]。
选择第一个元素6作为基准值。
进行分区操作,将小于6的元素放在左边,大于6的元素放在右边。分区后的序列可能如下:
[3, 2, 1, 4, 5, 6, 9, 8, 7]。
对基准值左边和右边的子序列分别进行快速排序。比如先对左边的子序列[3, 2, 1, 4, 5]进行快速排序,选择3为基准值,分区后得到[2, 1, 3, 4, 5],然后再对左右两边的子序列进行排序,直到整个序列有序。同样,对右边的子序列也进行类似的排序操作。
算法性能
时间复杂度:快速排序的平均时间复杂度为O(n log n),最坏情况下的时间复杂度为O(n^2)(当输入序列已经有序或逆序时)。但通过随机化选择基准值,可以期望得到O(n log n)的平均性能。
空间复杂度:快速排序的空间复杂度主要取决于递归调用的深度。平均情况下,递归树的深度为O(log n),因此空间复杂度为O(log n)。但在最坏情况下,递归树的深度可能达到O(n),导致空间复杂度为O(n)。
优化技巧
随机化基准值选择:为了避免最坏情况的发生,可以在每次分区前随机选择一个元素作为基准值。
三数取中法:在选择基准值时,可以考虑选择头、中、尾三个元素中的中位数作为基准值,以提高排序的稳定性。
插入排序处理小数组:对于非常小的数组(如长度小于10),插入排序可能比快速排序更快。因此,在递归到很小数组时,可以切换到插入排序。
迭代法实现:虽然快速排序通常使用递归实现,但也可以通过迭代法(使用栈)来实现,以减少递归调用的开销。
// 快速排序函数,传入一个整数数组a,以及要排序部分的左右边界left和right
void QuickSort1(int* a, int left, int right) {
// 如果左边界大于等于右边界,说明排序区间无效或只有一个元素,直接返回
if (left >= right) {
return;
}
// 初始化变量,begin和end用于记录整个排序区间的边界,因为下次用的时候left和right的值会被改变
//keyi用于记录基准元素的位置
int begin = left, end = right;
int keyi = left;
// 开始进行分区操作,将数组分为小于基准和大于基准的两部分
while (left < right) {
// 从右向左查找,找到第一个小于基准的元素
while (left < right && a[right] >= a[keyi]) {
right--;
}
// 从左向右查找,找到第一个大于基准的元素
while (left < right && a[left] < a[keyi]) {
left++;
}
// 交换找到的两个元素,使得小于基准的元素在基准的左边,大于基准的元素在基准的右边
Swap(&a[right], &a[left]);
}
// 将基准元素交换到它应该在的位置,此时基准左边的元素都小于基准,右边的元素都大于基准
Swap(&a[left], &a[keyi]);
// 更新基准元素的位置
keyi = left; // 注意不要忘了这一步,因为基准元素的位置可能已经变化了
// 递归地对基准左边的子数组进行快速排序
QuickSort(a, begin, keyi - 1);
// 递归地对基准右边的子数组进行快速排序
QuickSort(a, keyi + 1, end);
}
这段代码实现的是快速排序算法的一个变种,其核心思想是通过分区操作将数组划分为两个子数组,然后递归地对这两个子数组进行排序。这个特定的实现采用了一种“单边循环法”来进行分区。
大概是以下两个步骤,建议自己先在草稿纸上演示一遍
void QuickSort2(int* a, int left, int right) {
// 如果左边界大于或等于右边界,说明区间无效或已排序完成,直接返回。
if (left >= right) {
return;
}
// keyi是基准元素的索引,初始化为左边界。
int keyi = left;
// prev指向小于基准元素的最后一个位置,初始化为左边界。
int prev = left;
// cur用于遍历数组,初始化为左边界的下一个位置。
int cur = left + 1;
// 当cur没有超过右边界时,执行循环。
while (cur <= right) {
// 如果当前元素小于基准元素,并且prev不等于cur(避免自己与自己交换),
// 则交换prev和cur位置的元素,并将prev向前移动一位。
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[cur], &a[prev]); // 这里假设Swap函数能够正确交换两个元素的值。
}
// cur向后移动一位,继续遍历数组。
cur++;
}
// 将基准元素交换到prev指向的位置,此时prev左边的所有元素都小于基准元素,
// prev右边的所有元素都大于或等于基准元素。
Swap(&a[prev], &a[keyi]);
keyi = prev; // 更新keyi为基准元素的新位置。
// 递归地对基准元素左边的子数组进行快速排序。
QuickSort2(a, left, keyi - 1);
// 递归地对基准元素右边的子数组进行快速排序。
QuickSort2(a, keyi + 1, right);
}
#include
void PrintArray(int* a, int n)//打印
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void Swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
//快速排序
void QuickSort1(int* a, int left, int right) {
if (left >= right) {
return;
}
int begin = left, end = right;
int keyi = left;
while (left < right) {
while (left < right && a[right] >= a[keyi]) {
right--;
}
while (left < right && a[left] < a[keyi]) {
left++;
}
Swap(&a[right], &a[left]);
}
Swap(&a[left], &a[keyi]);
keyi = left;//注意不要忘了
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
void QuickSort2(int* a, int left, int right) {
if (left >= right) {
return;
}
int 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;
QuickSort2(a, left, keyi - 1);
QuickSort2(a, keyi + 1, right);
}
void TestSort()
{
int a[] = { 6, 3, 9, 1, 5, 8, 2, 4, 7};
PrintArray(a, sizeof(a) / sizeof(int));//计算数组元素个数并打印
QuickSort2(a, sizeof(a) / sizeof(int));
PrintArray(a, sizeof(a) / sizeof(int));
}
int main() {
TestSort();
return 0;
}