快速排序
随机化快速排序
- 基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 运行效率分析:
快速排序是一种比较快速的排序算法,它的平均运行时间是O(nlogn) ,之所以特别快速,是由于其非常精练和高度优化的内部循环,当然,最坏情形下,性能为O(n^2) 。像归并一样,快速排序也是一种体现分治思想的递归算法,空间性能上来看,快排只需要一个元素的辅助空间,但是弊端却是,需要一个栈空间来实现递归,空间复杂度也是O(nlogn)。
3.图解理解
我们在一个数组中,选择一个基点,比如第一个位置的6,然后将6挪动到正确位置,让在这个基点之前的子数组中数据都小于6,之后的子数组中数据都大于6,然后逐渐递归下去完成整个排序
显而易见的,最关键的就是要将基点数据挪动到正确位置上,这是快速排序的核心,我们称之为Partition。
过程如下所示,我们用 i 来记录当前遍历比较的元素位置:
Partition过程,让我们用代码来实现一下
int类型的一般情形:
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
int partition(int arr[], int l, int r) {
int v = arr[l]; // 选择第一个元素作为基准元素
int j = l; // 初始化左边界指针
for (int i = l + 1; i <= r; i++) { // 从基准元素的下一个位置开始遍历
if (arr[i] < v) { // 如果当前元素小于基准元素
j++; // 左边界指针向右移动
// 交换当前元素和左边界元素
swap(arr, j, i);
}
}
// 将基准元素放到它应该在的位置
swap(arr, l, j);
return j; // 返回基准元素的位置
}
通用情形:
// 通用交换函数
void swap(void* arr, size_t i, size_t j, size_t element_size) {
void* temp = malloc(element_size);
if (temp == NULL) {
perror("Failed to allocate memory for swap");
exit(EXIT_FAILURE);
}
memcpy(temp, (char*)arr + i * element_size, element_size);
memcpy((char*)arr + i * element_size, (char*)arr + j * element_size, element_size);
memcpy((char*)arr + j * element_size, temp, element_size);
free(temp);
}
// 通用比较函数的类型
typedef int (*compare_fn)(const void*, const void*);
// 分区函数
size_t partition(void* arr, size_t l, size_t r, size_t element_size, compare_fn cmp) {
void* pivot = (char*)arr + l * element_size; // 选择第一个元素作为基准元素
size_t j = l; // 初始化左边界指针
for (size_t i = l + 1; i <= r; i++) { // 从基准元素的下一个位置开始遍历
void* current = (char*)arr + i * element_size;
if (cmp(current, pivot) < 0) { // 如果当前元素小于基准元素
j++; // 左边界指针向右移动
// 交换当前元素和左边界元素
swap(arr, j, i, element_size);
}
}
// 将基准元素放到它应该在的位置
swap(arr, l, j, element_size);
return j; // 返回基准元素的位置
}
// 示例比较函数(用于整数数组)
int compare_int(const void* a, const void* b) {
return (*(int*)a - *(int*)b);
}
我们可以发现,如果我们对近乎有序的数组进行快排,每次partition分区后前后两个子数组的大小极其不平衡,容易性能衰减到**O(n^2)**的时间复杂度算法,我们需要对上面代码进行优化,我们可以随机选择一个基点作为比较基点,这种方法被称为随机快速排序算法,我们在选择比较数据的时候做一个随机数处理,随机选择数组中一数据和基点数据进行交换。
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
代码实例
// 交换函数
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 分区函数
int partition(int* arr, int l, int r) {
// 随机选择一个基准元素
int random_index = l + rand() % (r - l + 1);
swap(arr, l, random_index);
int pivot = arr[l];
// 初始化指针
int j = l;
for (int i = l + 1; i <= r; i++) {
if (arr[i] < pivot) {
j++;
swap(arr, j, i);
}
}
swap(arr, l, j);
return j;
}
// 快速排序函数
void quicksort(int* arr, int l, int r) {
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quicksort(arr, l, p - 1);
quicksort(arr, p + 1, r);
}
// 辅助函数:打印数组
void printArray(int* arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 辅助函数:生成随机数组
int* generateRandomArray(int n, int rangeL, int rangeR) {
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++) {
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
}
return arr;
}
// 主函数
int main() {
srand(time(NULL)); // 初始化随机种子
// Quick Sort也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
int* arr = generateRandomArray(N, 0, 100000);
quicksort(arr, 0, N - 1);
printArray(arr, 100); // 打印前100个元素以验证排序结果
free(arr); // 释放内存
return 0;
}
Java 代码:
java
/**
* 随机化快速排序
*/
public class QuickSort {
// 对arr[l...r]部分进行partition操作
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...j] < v ; arr[j+1...i) > v
int j = l;
for( int i = l + 1 ; i <= r ; i ++ )
if( arr[i].compareTo(v) < 0 ){
j ++;
swap(arr, j, i);
}
swap(arr, l, j);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
if (l >= r) {
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 测试 QuickSort
public static void main(String[] args) {
// Quick Sort也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
sort(arr);
SortTestHelper.printArray(arr);
}
}
双路快速排序
- 基本思想:
双路快速排序算法是随机化快速排序的改进版本,partition 过程使用两个索引值(i、j)用来遍历数组,将 <v 的元素放在索引i所指向位置的左边,而将 >v 的元素放在索引j所指向位置的右边,v 代表标定值。
2.运行效率分析:
时间和空间复杂度同随机化快速排序。
对于有大量重复元素的数组,如果使用上一节随机化快速排序效率是非常低的,导致 partition 后大于基点或者小于基点数据的子数组长度会极度不平衡,甚至会退化成 O(n*2) 时间复杂度的算法,对这种情况可以使用双路快速排序算法。
3.过程图解
我们用两个索引值( i , j )来遍历我们的序列,将 <= v 的元素放在索引为 i 所指向位置的左边,而将 >= v 的元素放在索引 j 所指向的位置的右边,以此来平衡左右两边子数组的数据个数。
同样,让我们分别用Java和C来实现一下
先来看C ;
java
// 交换函数
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 分区函数
int partition(int* arr, int l, int r) {
// 随机选择一个基准元素
int random_index = l + rand() % (r - l + 1);
swap(arr, l, random_index);
int v = arr[l];
// 初始化指针
int i = l + 1, j = r;
while (1) {
while (i <= r && arr[i] < v) {
i++;
}
while (j >= l + 1 && arr[j] > v) {
j--;
}
if (i > j) {
break;
}
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}
// 快速排序函数
void quicksort(int* arr, int l, int r) {
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quicksort(arr, l, p - 1);
quicksort(arr, p + 1, r);
}
// 辅助函数:打印数组
void printArray(int* arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 辅助函数:生成随机数组
int* generateRandomArray(int n, int rangeL, int rangeR) {
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++) {
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
}
return arr;
}
// 主函数
int main() {
srand(time(NULL)); // 初始化随机种子
int N = 1000000;
int* arr = generateRandomArray(N, 0, 100000);
quicksort(arr, 0, N - 1);
printArray(arr, 100); // 打印前100个元素以验证排序结果
free(arr); // 释放内存
return 0;
}
再来看Java;
java
/**
* 双路快速排序
*/
public class QuickSort2Ways {
//核心代码---开始
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
if( i > j )
break;
swap( arr, i, j );
i ++;
j --;
}
swap(arr, l, j);
return j;
}
//核心代码---结束
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
if (l >= r) {
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 测试 QuickSort
public static void main(String[] args) {
// 双路快速排序算法也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
// Quick Sort也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
sort(arr);
SortTestHelper.printArray(arr);
}
}
三路排序算法
最后,进入这种排序的最优方案
- 基本思想
三路快速排序是双路快速排序的进一步改进版本,三路快排将排序的数据分为三部分,分别小于v,等于v,大于v,v为标定值,在这样的三部分数据中,等于v的数据在下一次递归中不再需要排序,这样很好的降低了递归的深度,另一方面也减少了栈溢出的可能性,小于v和大于v的数据也不会出现哪一个特别多的情形,此方法可以让三路快速排序算法的性能更优。
2.性能分析
时间和空间复杂度同随机化快速排序。
三路快速排序算法是使用三路划分策略对数组进行划分,对处理大量重复元素的数组有非常高效提高快排 的过程。它添加了 处理等于划分元素值的逻辑,将所有等于划分元素的值集中在一起。
3.过程图解
我们分三种情况来讨论partition过程,i用以表示遍历的当前索引位置;
当前处理的元素e = V,元素e直接纳入蓝色区间,同时 i 向后移一位。
当前处理元素e < v, e和等于V区间的第一个数值进行交换,同时gt索引向前移动一位
当前处理元素 e>v,e 和 gt-1 索引位置的数值进行交换,同时 gt 索引向前移动一位。
最后当 i=gt 时,结束遍历,同时需要把 v 和索引 lt 指向的数值进行交换,这样这个排序过程就完成了,然后对 <V 和 >V 的数组部分用同样的方法再进行递归排序。
最后我们用代码来完整实现一下三路快速排序算法;
C语言:
java
// 交换函数
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 三路分区函数
void threeWayPartition(int* arr, int l, int r, int* lt, int* gt) {
int v = arr[l];
*lt = l;
*gt = r + 1;
int i = l + 1;
while (i < *gt) {
if (arr[i] < v) {
swap(arr, i, *lt + 1);
i++;
(*lt)++;
} else if (arr[i] > v) {
(*gt)--;
swap(arr, i, *gt);
} else {
i++;
}
}
swap(arr, l, *lt);
}
// 快速排序函数
void quicksort(int* arr, int l, int r) {
if (l >= r) {
return;
}
// 随机选择一个基准元素
int random_index = l + rand() % (r - l + 1);
swap(arr, l, random_index);
int lt, gt;
threeWayPartition(arr, l, r, <, >);
quicksort(arr, l, lt - 1);
quicksort(arr, gt, r);
}
// 辅助函数:打印数组
void printArray(int* arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 辅助函数:生成随机数组
int* generateRandomArray(int n, int rangeL, int rangeR) {
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++) {
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
}
return arr;
}
// 主函数
int main() {
srand(time(NULL)); // 初始化随机种子
int N = 1000000;
int* arr = generateRandomArray(N, 0, 100000);
quicksort(arr, 0, N - 1);
printArray(arr, 100); // 打印前100个元素以验证排序结果
free(arr); // 释放内存
return 0;
}
Java:
java
/**
* 三路快速排序
*/
public class QuickSort3Ways {
//核心代码---开始
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
if (l >= r) {
return;
}
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l, (int)(Math.random()*(r-l+1)) + l );
Comparable v = arr[l];
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l+1; // arr[lt+1...i) == v
while( i < gt ){
if( arr[i].compareTo(v) < 0 ){
swap( arr, i, lt+1);
i ++;
lt ++;
}
else if( arr[i].compareTo(v) > 0 ){
swap( arr, i, gt-1);
gt --;
}
else{ // arr[i] == v
i ++;
}
}
swap( arr, l, lt );
sort(arr, l, lt-1);
sort(arr, gt, r);
}
//核心代码---结束
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 测试 QuickSort3Ways
public static void main(String[] args) {
// 三路快速排序算法也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
sort(arr);
SortTestHelper.printArray(arr);
}
}