Bootstrap

手撕快排——三种实现方法(附动图及源码)

🤖💻👨‍💻👩‍💻🌟🚀

🤖🌟 欢迎降临张有志的未来科技实验室🤖🌟

专栏:数据结构      

👨‍💻👩‍💻 先赞后看,已成习惯👨‍💻👩‍💻

👨‍💻👩‍💻 创作不易,多多支持👨‍💻👩‍💻

🚀 启动创新引擎,揭秘数据结构的密码🚀


快速排序(Quicksort)是一种高效的排序算法,广泛应用于各种场景。它采用分治法(Divide and Conquer)策略,将一个数组分成两个子数组,然后递归地对这两个子数组进行排序。本文将介绍三种快速排序的实现方法,并附上相应的源码。

目录

⚙️一、快速排序的基本原理

🧠二、算法性能

📚三、实现

1.霍尔方法(hoare)

实现步骤:

1. 随机化基准值选择

2. 三数取中法

3.插入排序小数组

4. 尾递归优化

2. 挖坑法 (Digging Hole Method) - 优化版

实现步骤:

3. 前后指针法 (Two-Pointer Method) - 优化版

实现步骤:

💡优化要点回顾


⚙️一、快速排序的基本原理


快速排序的基本步骤如下:

  1. 从数组中选择一个基准元素(pivot)
  2. 将数组分成两个子数组:小于基准元素的部分和大于基准元素的部分。
  3. 递归地对这两个子数组进行排序。
  4. 合并两个已排序的子数组和基准元素,形成最终的排序结果。

🧠二、算法性能


  1. 时间复杂度:

    • 最好情况: 当输入数组已经接近有序或者每次分区都能均匀地将数组分成大小相近的两部分时,快速排序的时间复杂度为 O(n log n)
    • 平均情况: 在大多数情况下,快速排序的时间复杂度为 O(n log n)
    • 最坏情况: 当输入数组完全逆序或者每次都选择最小或最大值作为基准时,时间复杂度退化为 O(n^2)后文我们将给出优化方案
  2. 空间复杂度:

    • 快速排序是一种原地排序算法,这意味着它不需要额外的存储空间来保存数据。但是,由于使用了递归,递归栈会占用一定的空间,因此它的空间复杂度为 O(log n)(递归调用的深度)。

📚三、实现


1.霍尔方法(hoare)

霍尔方法是快速排序原始版本中使用的分区方法。这种方法使用两个指针,一个从左向右移动,另一个从右向左移动,直到它们相遇。霍尔方法的主要优点是不需要额外的存储空间,并且通常比其他方法更高效。

实现步骤:
  1. 选择基准值:通常选择数组中的最后一个元素。
  2. 初始化指针:设置 i 为左端点减一,j 为右端点加一。
  3. 移动指针
    • 从左向右移动 i,直到找到一个大于或等于基准值的元素。
    • 从右向左移动 j,直到找到一个小于或等于基准值的元素。
  4. 交换元素:如果 i 小于等于 j,则交换 i 和 j 指向的元素,并继续移动指针。
  5. 重复步骤 3 和 4,直到 i 大于 j
int hoare_partition(int arr[], int low, int high) {
    int pivot = arr[low];
    int i = low - 1;
    int j = high + 1;

    while (true) {
        do {
            i++;
        } while (arr[i] < pivot);

        do {
            j--;
        } while (arr[j] > pivot);

        if (i >= j)
            return j;

        swap(&arr[i], &arr[j]);
    }
}

这并不完美,接下来我将给出优化方案

1. 随机化基准值选择

问题1:如果基准值总是选择数组的第一个或最后一个元素,那么在最坏情况下(例如数组已经排序好或几乎排序好)时间复杂度会退化为 O(n^2)。

解决方案:通过随机选择基准值来减少最坏情况出现的概率。这可以通过在分区前随机选择数组中的一个元素作为基准值来实现。

int random_partition(int arr[], int low, int high) {
    srand(time(0));  // 种子时间
    int rand_pivot = low + rand() % (high - low + 1);
    swap(&arr[rand_pivot], &arr[high]);  // 交换随机基准值到数组末尾
    return partition(arr, low, high);
}
2. 三数取中法

问题:随机化虽然可以减少最坏情况的概率,但在某些情况下仍然可能选择到极端的基准值。

解决方案:三数取中法选择数组的第一个元素、中间元素和最后一个元素的中位数作为基准值,这样可以进一步减少最坏情况出现的概率。

int median_of_three(int arr[], int low, int high) {
    int mid = (low + high) / 2;
    if (arr[low] > arr[mid]) swap(&arr[low], &arr[mid]);
    if (arr[low] > arr[high]) swap(&arr[low], &arr[high]);
    if (arr[mid] > arr[high]) swap(&arr[mid], &arr[high]);

    // 中间元素作为基准值
    swap(&arr[mid], &arr[high]);
    return partition(arr, low, high);
}
3.插入排序小数组

问题:对于小规模数组,快速排序的递归开销较大。

解决方案:当数组的大小低于某个阈值(例如 10 或 15)时,改用插入排序。插入排序在小规模数组中效率更高。

void quickSortWithInsertion(int arr[], int low, int high) {
    if (low < high) {
        if (high - low < 10) {  // 使用插入排序
            insertionSort(arr, low, high);
        } else {
            int pi = hoare_partition(arr, low, high);
            quickSortWithInsertion(arr, low, pi - 1);
            quickSortWithInsertion(arr, pi + 1, high);
        }
    }
}

void insertionSort(int arr[], int low, int high) {
    for (int i = low + 1; i <= high; i++) {
        int key = arr[i];
        int j = i - 1;

        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
4. 尾递归优化

问题:快速排序使用递归来实现,递归深度过大可能导致栈溢出。

解决方案:通过递归只处理一边的子数组,而另一边使用迭代处理,可以减少递归调用栈的深度。

void quickSortOptimized(int arr[], int low, int high) {
    while (low < high) {
        int pi = partition(arr, low, high);

        // 只递归处理较短的那一边
        if (pi - low < high - pi) {
            quickSortOptimized(arr, low, pi - 1);
            low = pi + 1;
        } else {
            quickSortOptimized(arr, pi + 1, high);
            high = pi - 1;
        }
    }
}

2. 挖坑法 (Digging Hole Method) - 优化版

挖坑法通过预留一个“坑”来放置基准值,以减少不必要的交换次数。下面是一个结合了随机化基准值选择、尾递归优化以及对于小数组使用插入排序的优化版本。

实现步骤:
  1. 选择基准值:通常选择数组中的最后一个元素。
  2. 初始化“坑”:将基准值移至数组末尾,形成一个“坑”。
  3. 遍历数组:从左向右遍历数组。
    • 如果遇到小于基准值的元素,将其放入“坑”,然后将“坑”向前移动一位。
  4. 重复步骤 3,直到遍历结束。
  5. 基准值就位:将基准值放入“坑”中

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 交换函数
void swap(int *xp, int *yp) {
    int temp = *xp;
    *xp = *yp;
    *yp = temp;
}

// 随机化基准值选择
int randomized_partition(int arr[], int low, int high) {
    srand(time(0));
    int rand_pivot = low + rand() % (high - low + 1);
    swap(&arr[rand_pivot], &arr[high]);

    int pivot = arr[high];
    int hole = low - 1;
    for (int i = low; i < high; i++) {
        if (arr[i] <= pivot) {
            hole++;
            swap(&arr[hole], &arr[i]);
        }
    }
    swap(&arr[hole + 1], &arr[high]);
    return hole + 1;
}

// 快速排序函数
void quickSortOptimized(int arr[], int low, int high) {
    while (low < high) {
        int pi = randomized_partition(arr, low, high);

        if (pi - low < high - pi) {
            quickSortOptimized(arr, low, pi - 1);
            low = pi + 1;
        } else {
            quickSortOptimized(arr, pi + 1, high);
            high = pi - 1;
        }
    }
}

// 插入排序小数组
void insertionSort(int arr[], int low, int high) {
    for (int i = low + 1; i <= high; i++) {
        int key = arr[i];
        int j = i - 1;

        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

// 主函数
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    quickSortOptimized(arr, 0, n - 1);

    printf("Sorted array: \n");
    for (int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

3. 前后指针法 (Two-Pointer Method) - 优化版

前后指针法是一种直观的分区方法,类似于霍尔方法,但使用两个指针来跟踪小于基准值和大于基准值的边界。

实现步骤:
  1. 选择基准值:通常选择数组中的最后一个元素。
  2. 初始化指针:设置 i 为左端点,j 也为左端点。
  3. 遍历数组
    • 从左向右移动 j,直到找到一个大于基准值的元素。
    • 如果 arr[j] 小于等于基准值,将 arr[j] 与 arr[i] 交换,并将 i 向右移动一位。
  4. 重复步骤 3,直到 j 达到右端点。
  5. 基准值就位:将基准值与 arr[i] 交换
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 交换函数
void swap(int *xp, int *yp) {
    int temp = *xp;
    *xp = *yp;
    *yp = temp;
}

// 随机化基准值选择
int randomized_partition(int arr[], int low, int high) {
    srand(time(0));
    int rand_pivot = low + rand() % (high - low + 1);
    swap(&arr[rand_pivot], &arr[high]);

    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return i + 1;
}

// 快速排序函数
void quickSortOptimized(int arr[], int low, int high) {
    while (low < high) {
        int pi = randomized_partition(arr, low, high);

        if (pi - low < high - pi) {
            quickSortOptimized(arr, low, pi - 1);
            low = pi + 1;
        } else {
            quickSortOptimized(arr, pi + 1, high);
            high = pi - 1;
        }
    }
}

// 插入排序小数组
void insertionSort(int arr[], int low, int high) {
    for (int i = low + 1; i <= high; i++) {
        int key = arr[i];
        int j = i - 1;

        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

// 主函数
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    quickSortOptimized(arr, 0, n - 1);

    printf("Sorted array: \n");
    for (int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

💡优化要点回顾

  1. 随机化基准值选择:通过随机选择基准值来减少最坏情况出现的概率,从而提高快速排序的平均性能。
  2. 尾递归优化:通过迭代而非递归来减少递归调用栈的深度,从而降低栈溢出的风险。
  3. 插入排序小数组:对于小规模数组使用插入排序,因为插入排序在这种情况下通常更快。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;