一、快速排序算法简介
快速排序(Quick Sort)是一种分治的排序算法,由 C. A. R. Hoare 于 1962 年提出。
特点:
- 平均性能出色,时间复杂度通常为 O (nlogn)。
- 空间复杂度相对较低,通常为 O (logn)。
- 是一种不稳定的排序算法,可能会改变相同元素的相对顺序。
应用场景:
- 适用于处理大规模、杂乱无章的数据排序。
- 在各种需要排序的场景中,如数据库查询结果的排序、大规模数据的预处理等,都能发挥较好的性能。
二、快速排序算法原理
(一)基本思想
快速排序的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。具体来说,首先选择一个基准值,然后通过比较将小于基准值的元素放到基准值的左边,大于基准值的元素放到基准值的右边,接着对左右两个子序列重复上述过程,直到所有子序列都只有一个元素,排序完成。
(二)分区操作
- 挖洞法:第一趟操作中,定义洞为最开始的位置以及以第一个元素为基准,然后从最右边开始找比基准小的数放到左边,从最左边开始找比基准大的数放到右边。通过不断利用洞的位置转换,一步步将小的数放到左区间,大的数放到右区间,最后将一开始定的基准元素转到其真实的大小位置上。
- 左右指针法:以第一个元素为基准,从右边开始找比基准小的数据,从左边开始找比基准大的数据,然后将从两边分别找的比基准小的数据和比基准大的数据进行交换,最后将基准元素转到其真实大小的位置。
- 前后指针法:以第一个元素为基准,定义prev 与cur 两个变量,让prev一开始指向第一个元素,cur指向第二个元素,cur从当前位置开始向后遍历到末尾,期间cur找比基准小的数,每次遇到比基准小的值就停下来,++prev,交换prev和cur位置的值,最后将基准元素与prev位置的值进行交换,让基准的数转到其真实大小的位置。
三、快速排序算法实现
(一)普通实现代码
1、挖洞法
#include <stdio.h>
// 交换两个数的值
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 挖洞法快速排序的分区函数
int partition(int arr[], int low, int high)
{
int pivot = arr[low]; // 选择第一个元素作为枢轴
int holeIndex = low; // 初始洞的位置为枢轴位置
while (low < high)
{
while (low < high && arr[high] > pivot) // 从右向左找小于等于枢轴的元素
{
high--;
}
if (low!= high)
{
arr[holeIndex] = arr[high];// 将找到的元素填入洞,更新洞的位置为 high
holeIndex = high;
}
while (low < high && arr[low] <= pivot) // 从左向右找大于枢轴的元素
{
low++;
}
if (low!= high)
{
arr[holeIndex] = arr[low];// 将找到的元素填入洞,更新洞的位置为 low
holeIndex = low;
}
}
arr[holeIndex] = pivot;// 将枢轴填入最后的洞
return low;
}
// 快速排序函数
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 打印数组函数
void printArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
// 测试示例
int main()
{
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组为:\n");
printArray(arr, n);
quickSort(arr, 0, n - 1);
printf("排序后的数组为:\n");
printArray(arr, n);
return 0;
}
在这个实现中,“挖洞法” 通过不断移动洞的位置,将合适的元素填入洞中,最终将枢轴填入正确的位置来实现分区。快速排序函数通过递归地对分区后的子数组进行排序,最终实现整个数组的排序。
2、左右指针法
#include <stdio.h>
// 交换两个数的值
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 普通快速排序的分区函数
int partition(int arr[], int low, int high)
{
int pivot = arr[low]; // 选择第一个元素作为枢轴(pivot)
int i = low + 1; // i 从 low + 1 开始,用于遍历比枢轴大的元素
int j = high; // j 从 high 开始,用于遍历比枢轴小的元素
while (1)
{
while (i <= j && arr[i] <= pivot) // 找到第一个大于等于枢轴的元素
{
i++;
}
while (i <= j && arr[j] > pivot) // 找到第一个小于等于枢轴的元素
{
j--;
}
if (i > j) // 如果 i 和 j 交叉了,说明分区完成
{
break;
}
// 交换 arr[i] 和 arr[j],使得小于枢轴的元素在左边,大于枢轴的元素在右边
swap(&arr[i], &arr[j]);
}
swap(&arr[low], &arr[j]);// 将枢轴放到正确的位置上,即 j 的位置
return j;
}
// 普通快速排序函数
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
int pi = partition(arr, low, high); // 分区,得到枢轴的位置
quickSort(arr, low, pi - 1); // 对左半部分递归排序
quickSort(arr, pi + 1, high);// 对右半部分递归排序
}
}
// 打印数组函数
void printArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
// 测试示例
int main()
{
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组为:\n");
printArray(arr, n);
quickSort(arr, 0, n - 1);
printf("排序后的数组为:\n");
printArray(arr, n);
return 0;
}
上述代码使用左右指针法实现了普通的快速排序算法。首先,swap函数用于交换两个数的值。partition函数选择第一个元素作为基准,通过两个指针i和j从两端向中间移动,将小于基准的元素放在左边,大于基准的元素放在右边,并最终确定基准的正确位置。quickSort函数通过递归调用对数组进行排序。
3、前后指针法
#include <stdio.h>
// 交换两个数的值
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 前后指针法快速排序的分区函数
int partition(int arr[], int low, int high)
{
int pivot = arr[low]; // 选择第一个元素作为枢轴(pivot)
int prev = low; // 前指针,初始指向枢轴位置
int curr = low + 1; // 当前指针,从枢轴下一个位置开始
while (curr <= high)
{
if (arr[curr] < pivot) // 如果当前元素小于枢轴,前指针向后移动一位并与当前元素交换
{
prev++;
swap(&arr[prev], &arr[curr]);
}
curr++; // 当前指针继续向后移动
}
swap(&arr[low], &arr[prev]); // 将枢轴与前指针指向的元素交换,使得枢轴处于正确位置
return prev; // 返回枢轴的最终位置
}
// 快速排序函数
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
int pi = partition(arr, low, high); // 进行分区操作,得到枢轴位置
quickSort(arr, low, pi - 1); // 对左半部分递归排序
quickSort(arr, pi + 1, high); // 对右半部分递归排序
}
}
// 打印数组函数
void printArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
// 测试示例
int main()
{
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组为:\n");
printArray(arr, n);
quickSort(arr, 0, n - 1);
printf("排序后的数组为:\n");
printArray(arr, n);
return 0;
}
前后指针法通过一个前指针prev和一个当前指针curr遍历数组。当前指针找到比枢轴小的元素时,与前指针的下一个位置交换,从而将小于枢轴的元素移动到左边,大于枢轴的元素移动到右边。最后将枢轴与前指针指向的元素交换,得到分区点,再递归地对左右子数组进行排序。
(二)优化策略
1. 三数取中
通常选取数组的左、中、右三个数,将中间数作为基准,这样可以减少基准值选取不当导致的最坏情况。
2. 优化递归操作
当数组规模较小时,采用插入排序等更高效的方法替代快速排序,以减少递归调用的开销。
3. 优化小数组排序
对于较小的数组,可以使用直接插入排序等简单排序方法,避免快速排序在小数组上的性能损失。
四、快速排序算法分析
(一)时间复杂度
快速排序的时间复杂度取决于每次划分时基准元素的选取。
- 最好情况:每次选取的基准元素都能将数组平分为两个长度相近的子数组。此时,时间复杂度为 O (nlogn)。
- 最坏情况:每次选取的基准元素恰好是数组中的最大值或最小值,导致划分极不均衡,每次划分只减少一个元素。此时,时间复杂度为 O (n^2)。
- 平均情况:在随机情况下,快速排序的平均时间复杂度为 O (nlogn)。
(二)空间复杂度
快速排序的空间复杂度主要取决于递归调用时栈的使用。
- 最优情况:递归树的深度为 logn,空间复杂度为 O (logn)。
- 最坏情况:递归树退化为链表,需要进行 n-1 次递归调用,空间复杂度为 O (n)。
- 平均情况:空间复杂度通常为 O (logn)。
五、快速排序算法应用实例
(一)在海量数据排序中的应用
假设我们有一个包含数百万个随机整数的数据集,需要对其进行排序。快速排序能够快速地将这些数据分割成较小的子序列,并通过递归调用不断地细化排序,从而有效地完成整个数据集的排序任务。
(二)查找数组中的第 K 小元素
给定一个整数数组和一个整数 K,要求找出数组中的第 K 小元素。可以通过修改快速排序的分区过程来实现。在每次分区后,判断基准元素的位置与 K 的关系,如果基准元素的位置正好是 K - 1,那么基准元素就是第 K 小的元素;如果基准元素的位置大于 K - 1,则在左子数组中继续查找;如果基准元素的位置小于 K - 1,则在右子数组中继续查找。
(三)快速排序在排序算法比较中的优势
在与其他常见排序算法(如冒泡排序、插入排序等)的比较中,快速排序通常在大规模数据的排序上表现出更高的效率。通过实际的实验数据和分析,可以清晰地展示快速排序在时间复杂度和空间复杂度方面的优势。
例如,对于相同规模的随机整数数组,分别使用不同的排序算法进行排序,并记录它们的运行时间和内存使用情况。比较结果可以直观地说明快速排序在处理大规模数据时的出色性能。