快速排序及两种优化与思考

快速排序

随机化快速排序

  1. 基本思想:

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  1. 运行效率分析:

快速排序是一种比较快速的排序算法,它的平均运行时间是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);
    }
}

双路快速排序

  1. 基本思想:

双路快速排序算法是随机化快速排序的改进版本,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);

    }
}

三路排序算法

最后,进入这种排序的最优方案

  1. 基本思想

三路快速排序是双路快速排序的进一步改进版本,三路快排将排序的数据分为三部分,分别小于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, &lt, &gt);

   

    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);
    }
}
相关推荐
Ritsu栗子8 分钟前
代码随想录算法训练营day35
c++·算法
Tubishu17 分钟前
数据结构——实验五·图
数据结构
好一点,更好一点18 分钟前
systemC示例
开发语言·c++·算法
卷卷的小趴菜学编程39 分钟前
c++之List容器的模拟实现
服务器·c语言·开发语言·数据结构·c++·算法·list
言之。1 小时前
【Java】面试中遇到的两个排序
java·面试·排序算法
林开落L1 小时前
模拟算法习题篇
算法
玉蜉蝣1 小时前
PAT甲级-1014 Waiting in Line
c++·算法·队列·pat甲·银行排队问题
我真不会起名字啊1 小时前
“深入浅出”系列之算法篇:(2)openCV、openMV、openGL
算法
南宫生1 小时前
力扣动态规划-7【算法学习day.101】
java·数据结构·算法·leetcode·动态规划
spssau1 小时前
2025美赛倒计时,数学建模五类模型40+常用算法及算法手册汇总
算法·数学建模·数据分析·spssau