整理所有排序算法

分类

排序算法一般指内部排序算法 , 即数据记录在内存中进行排序。

  • 一种是比较排序,时间复杂度O(nlogn) ~ O(n2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等;
  • 另一种是非比较排序,时间复杂度可以达到 O(n),主要有:计数排序,基数排序,桶排序等。
排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 O(n2) O(n) O(n2) O(1) 稳定
简单选择排序 O(n2) O(n2) O(n2) O(1) 不稳定
直接插入排序 O(n2) O(n) O(n2) O(1) 稳定
希尔排序 O(nlogn) ~ O(n2) O(n1.3) O(n2) O(1) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
快速排序 O(nlogn) O(nlogn) O(n2) O(logn)~ O(n) 不稳定

冒泡排序 BubbleSort

越大(或越小)的元素会经由交换慢慢 “浮到” 数列的顶端

步骤

从小到大进行排序:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。
    这步做完后,最后的元素会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

C语言实现 : 冒泡排序

采用冒泡法实现对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }排序

  1. #include <stdio.h>
  2. // 分类 -------------- 内部比较排序
  3. // 数据结构 ---------- 数组
  4. // 最差时间复杂度 ---- O(n^2)
  5. // 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
  6. // 平均时间复杂度 ---- O(n^2)
  7. // 所需辅助空间 ------ O(1)
  8. // 稳定性 ------------ 稳定
  9. void Swap(int A[], int i, int j)
  10. {
  11. int temp = A[i];
  12. A[i] = A[j];
  13. A[j] = temp;
  14. }
  15. void BubbleSort(int A[], int n)
  16. {
  17. for (int j = 0; j < n - 1; j++) // 每次最大元素就像气泡一样"浮"到数组的最后
  18. {
  19. for (int i = 0; i < n - 1 - j; i++) // 依次比较相邻的两个元素,使较大的那个向后移
  20. {
  21. if (A[i] > A[i + 1]) // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法
  22. {
  23. Swap(A, i, i + 1);
  24. }
  25. }
  26. }
  27. }
  28. int main()
  29. {
  30. int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大冒泡排序
  31. int n = sizeof(A) / sizeof(int);
  32. BubbleSort(A, n);
  33. printf("冒泡排序结果:");
  34. for (int i = 0; i < n; i++)
  35. {
  36. printf("%d ", A[i]);
  37. }
  38. printf("\n");
  39. return 0;
  40. }

冒泡排序gif

排序算法 - 图1

优缺点

优点 : 最容易了解和实现的算法之一
缺点 : 效率低

时间复杂度

  • 最好情况 : 若数据初始状态是正序 , 一次扫描即可完成排序 , 比较次数为 C**min** = n-1 , 移动次数为 M**min** = 0 , 初始排序就是最终需要的序列 , 时间复杂度为 O(n)
  • 最差情况 : 数据初始状态是反序 , 需要进行 n-1 趟排序 , 每次排序需要进行 n-i ( 1 ≤ i ≤ n-1 ) 次比较 , 每次比较必须移动记录三次来达到交换记录位置 , 比较和移动次数为 :

排序算法 - 图2

鸡尾酒排序 : 冒泡的改进

也叫定向冒泡排序 , 从低到高然后从高到低

C语言实现

  1. #include <stdio.h>
  2. // 分类 -------------- 内部比较排序
  3. // 数据结构 ---------- 数组
  4. // 最差时间复杂度 ---- O(n^2)
  5. // 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
  6. // 平均时间复杂度 ---- O(n^2)
  7. // 所需辅助空间 ------ O(1)
  8. // 稳定性 ------------ 稳定
  9. void Swap(int A[], int i, int j)
  10. {
  11. int temp = A[i];
  12. A[i] = A[j];
  13. A[j] = temp;
  14. }
  15. void CocktailSort(int A[], int n)
  16. {
  17. int left = 0; // 初始化左起点
  18. int right = n - 1; // 初始化右起点
  19. while (left < right)
  20. {
  21. //从左往右开始
  22. for (int i = left; i < right; i++) // 前半轮,将最大元素放到后面
  23. {
  24. if (A[i] > A[i + 1])
  25. {
  26. Swap(A, i, i + 1);
  27. }
  28. }
  29. right--;
  30. //从右往左开始
  31. for (int i = right; i > left; i--) // 后半轮,将最小元素放到前面
  32. {
  33. if (A[i - 1] > A[i])
  34. {
  35. Swap(A, i - 1, i);
  36. }
  37. }
  38. left++;
  39. }
  40. }
  41. int main()
  42. {
  43. int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大定向冒泡排序
  44. int n = sizeof(A) / sizeof(int);
  45. CocktailSort(A, n);
  46. printf("鸡尾酒排序结果:");
  47. for (int i = 0; i < n; i++)
  48. {
  49. printf("%d ", A[i]);
  50. }
  51. printf("\n");
  52. return 0;
  53. }

鸡尾酒排序Gif

排序算法 - 图3

为什么鸡尾酒排序比冒泡排序好一点?

以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问一次序列就可以完成排序,但如果使用冒泡排序则需要四次。但是在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。

选择排序 SelectionSort

原理 : 初始时在序列中找到最大或最小元素 , 放到序列的起始位置作为已排序序列;然后再从剩余未排序元素中继续寻找最大或最小元素 , 放在已排序序列的末尾。以此类推 , 知道所有元素均排序完毕。

与冒泡排序的区别

冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

C语言实现

  1. #include <stdio.h>
  2. // 分类 -------------- 内部比较排序
  3. // 数据结构 ---------- 数组
  4. // 最差时间复杂度 ---- O(n^2)
  5. // 最优时间复杂度 ---- O(n^2)
  6. // 平均时间复杂度 ---- O(n^2)
  7. // 所需辅助空间 ------ O(1)
  8. // 稳定性 ------------ 不稳定
  9. void Swap(int A[], int i, int j)
  10. {
  11. int temp = A[i];
  12. A[i] = A[j];
  13. A[j] = temp;
  14. }
  15. void SelectionSort(int A[], int n)
  16. {
  17. for (int i = 0; i < n - 1; i++) // i为已排序序列的末尾
  18. {
  19. int min = i;
  20. for (int j = i + 1; j < n; j++) // 未排序序列
  21. {
  22. if (A[j] < A[min]) // 找出未排序序列中的最小值
  23. {
  24. min = j;
  25. }
  26. }
  27. if (min != i)
  28. {
  29. Swap(A, min, i);
  30. // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
  31. }
  32. }
  33. }
  34. int main()
  35. {
  36. int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序
  37. int n = sizeof(A) / sizeof(int);
  38. SelectionSort(A, n);
  39. printf("选择排序结果:");
  40. for (int i = 0; i < n; i++)
  41. {
  42. printf("%d ", A[i]);
  43. }
  44. printf("\n");
  45. return 0;
  46. }

实现过程Gif

①序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }
②宏观实现过程
排序算法 - 图4 排序算法 - 图5

插入排序 InsertionSort

简单直观 , 原理类似与抓牌

排序算法 - 图6
对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
插入排序在实现上,通常采用in-place排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

C语言实现

  1. #include <stdio.h>
  2. // 分类 ------------- 内部比较排序
  3. // 数据结构 ---------- 数组
  4. // 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
  5. // 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
  6. // 平均时间复杂度 ---- O(n^2)
  7. // 所需辅助空间 ------ O(1)
  8. // 稳定性 ------------ 稳定
  9. void InsertionSort(int A[], int n)
  10. {
  11. for (int i = 1; i < n; i++) // 类似抓扑克牌排序
  12. {
  13. int get = A[i]; // 右手抓到一张扑克牌
  14. int j = i - 1; // 拿在左手上的牌总是排序好的
  15. while (j >= 0 && A[j] > get) // 将抓到的牌与手牌从右向左进行比较
  16. {
  17. A[j + 1] = A[j]; // 如果该手牌比抓到的牌大,就将其右移
  18. j--;
  19. }
  20. A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
  21. }
  22. }
  23. int main()
  24. {
  25. int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序
  26. int n = sizeof(A) / sizeof(int);
  27. InsertionSort(A, n);
  28. printf("插入排序结果:");
  29. for (int i = 0; i < n; i++)
  30. {
  31. printf("%d ", A[i]);
  32. }
  33. printf("\n");
  34. return 0;
  35. }

实现过程Gif

①序列{ 6, 5, 3, 1, 8, 7, 2, 4 }
②宏观实现过程
排序算法 - 图7 排序算法 - 图8

应用范围

插入排序不适合数据量较大的排序。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

二分插入排序 : 插入的改进

对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法减少比较操作的次数,我们称为二分插入排序。

#include <stdio.h>

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void InsertionSortDichotomy(int A[], int n)
{
    for (int i = 1; i < n; i++)
    {
        int get = A[i];                    // 右手抓到一张扑克牌
        int left = 0;                    // 拿在左手上的牌总是排序好的,所以可以用二分法
        int right = i - 1;                // 手牌左右边界进行初始化
        while (left <= right)            // 采用二分法定位新牌的位置
        {
            int mid = (left + right) / 2;
            if (A[mid] > get)
                right = mid - 1;
            else
                left = mid + 1;
        }
        for (int j = i - 1; j >= left; j--)    // 将欲插入新牌位置右边的牌整体向右移动一个单位
        {
            A[j + 1] = A[j];
        }
        A[left] = get;                    // 将抓到的牌插入手牌
    }
}


int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大二分插入排序
    int n = sizeof(A) / sizeof(int);
    InsertionSortDichotomy(A, n);
    printf("二分插入排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

希尔排序 ShellSort : 插入的高效改进

希尔排序 , 也叫递减增量排序 , 是不稳定的排序算法

希尔排序是基于插入排序的以下两点性质而提出改进方案的 :

  • 插入排序在对几乎已经排好序的数据操作时 , 效率高 , 即可以达到线性排序的效率
  • 但插入排序一般来说是低效的 , 因为插入排序每次只能将数据移动一位

简介

希尔排序通过将比较的全部元素分为几个区域 , 来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序 , 算法的最后一步就是普通的插入排序 , 但是到了这步 , 需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为 O(n) 的排序 (冒泡排序或直接插入排序) , 可能会进行 n 次比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据 , 所以小数据只需进行少数比较和交换即可到正确位置。

C语言实现

#include <stdio.h>  

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
// 最优时间复杂度 ---- O(n)
// 平均时间复杂度 ---- 根据步长序列的不同而不同。
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定

void ShellSort(int A[], int n)
{
    int h = 0;
    while (h <= n)                          // 生成初始增量
    {
        h = 3 * h + 1;
    }
    while (h >= 1)
    {
        for (int i = h; i < n; i++)
        {
            int j = i - h;
            int get = A[i];
            while (j >= 0 && A[j] > get)
            {
                A[j + h] = A[j];
                j = j - h;
            }
            A[j + h] = get;
        }
        h = (h - 1) / 3;                    // 递减增量
    }
}

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大希尔排序
    int n = sizeof(A) / sizeof(int);
    ShellSort(A, n);
    printf("希尔排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

实现过程Gif

以 23, 10 , 4 , 1 的步长序列进行希尔排序 :

ShellSort.gif

应用范围

希尔排序是不稳定的排序算法。虽然一次插入排序是稳定的 , 不会改变相同元素的相对顺序 , 但在不同的插入排序过程中 , 相同的元素可能在各自的插入排序中移动 , 最后其稳定性就会被打乱。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2 时分成两个子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。

归并排序 MergeSort

归并排序是创建在归并操作上的一种有效的排序算法,效率为 O(nlogn)


简介

归并排序的实现分为递归实现非递归(迭代)实现
递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。
非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

步骤

归并排序算法主要依赖归并(Merge)操作。
归并操作指的是将两个已经排序的序列合并成一个序列的操作归并操作步骤如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

C语言实现

#include <stdio.h>
#include <limits.h>

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定


void Merge(int A[], int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
{
    int len = right - left + 1;
    int *temp = new int[len];       // 辅助空间O(n)
    int index = 0;
    int i = left;                   // 前一数组的起始元素
    int j = mid + 1;                // 后一数组的起始元素
    while (i <= mid && j <= right)
    {
        temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];  // 带等号保证归并排序的稳定性
    }
    while (i <= mid)
    {
        temp[index++] = A[i++];
    }
    while (j <= right)
    {
        temp[index++] = A[j++];
    }
    for (int k = 0; k < len; k++)
    {
        A[left++] = temp[k];
    }
}

void MergeSortRecursion(int A[], int left, int right)    // 递归实现的归并排序(自顶向下)
{
    if (left == right)    // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
        return;
    int mid = (left + right) / 2;
    MergeSortRecursion(A, left, mid);
    MergeSortRecursion(A, mid + 1, right);
    Merge(A, left, mid, right);
}

void MergeSortIteration(int A[], int len)    // 非递归(迭代)实现的归并排序(自底向上)
{
    int left, mid, right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
    for (int i = 1; i < len; i *= 2)        // 子数组的大小i初始为1,每轮翻倍
    {
        left = 0;
        while (left + i < len)              // 后一个子数组存在(需要归并)
        {
            mid = left + i - 1;
            right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够
            Merge(A, left, mid, right);
            left = right + 1;               // 前一个子数组索引向后移动
        }
    }
}

int main()
{
    int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 };      // 从小到大归并排序
    int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
    int n1 = sizeof(A1) / sizeof(int);
    int n2 = sizeof(A2) / sizeof(int);
    MergeSortRecursion(A1, 0, n1 - 1);          // 递归实现
    MergeSortIteration(A2, n2);                 // 非递归实现
    printf("递归实现的归并排序结果:");
    for (int i = 0; i < n1; i++)
    {
        printf("%d ", A1[i]);
    }
    printf("\n");
    printf("非递归实现的归并排序结果:");
    for (int i = 0; i < n2; i++)
    {
        printf("%d ", A2[i]);
    }
    printf("\n");
    return 0;
}

实现过程Gif

①对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下
②归并排序宏观过程
 
MergeSort1.gif MergeSort2.gif

应用范围

归并排序除了可以对数组进行排序 , 还可以高效地求出数组小和 (即单调和) 以及数组中的逆序对。详见此处

堆排序 HeapSort

堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构 (通常堆是通过一维数组来实现的) , 并满足性质 : 以最大堆为例 , 其中父结点的值总是大于它的孩子结点

步骤

  1. 由输入的无序数组构造一个最大堆,作为初始的无序区
  2. 把堆顶元素(最大值)和堆尾元素互换
  3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
  4. 重复步骤2,直到堆的尺寸为1

C语言实现

#include <stdio.h>

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定


void Swap(int A[], int i, int j)
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

void Heapify(int A[], int i, int size)  // 从A[i]向下进行堆调整
{
    int left_child = 2 * i + 1;         // 左孩子索引
    int right_child = 2 * i + 2;        // 右孩子索引
    int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
    if (left_child < size && A[left_child] > A[max])
        max = left_child;
    if (right_child < size && A[right_child] > A[max])
        max = right_child;
    if (max != i)
    {
        Swap(A, i, max);                // 把当前结点和它的最大(直接)子节点进行交换
        Heapify(A, max, size);          // 递归调用,继续从当前结点向下进行堆调整
    }
}

int BuildHeap(int A[], int n)           // 建堆,时间复杂度O(n)
{
    int heap_size = n;
    for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
        Heapify(A, i, heap_size);
    return heap_size;
}

void HeapSort(int A[], int n)
{
    int heap_size = BuildHeap(A, n);    // 建立一个最大堆
    while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序
    {
        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        Swap(A, 0, --heap_size);
        Heapify(A, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
    }
}

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大堆排序
    int n = sizeof(A) / sizeof(int);
    HeapSort(A, n);
    printf("堆排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

快速排序 QuickSort

在平均状况下,排序n个元素要 O(nlogn) 次比较。在最坏状况下则需要 O(n) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 O(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

简介

快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。

步骤

  1. 从序列中挑出一个元素,作为”基准”(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

C语言实现

#include <stdio.h>

// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)       
// 稳定性 ---------- 不稳定

void Swap(int A[], int i, int j)
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int Partition(int A[], int left, int right)  // 划分函数
{
    int pivot = A[right];               // 这里每次都选择最后一个元素作为基准
    int tail = left - 1;                // tail为小于基准的子数组最后一个元素的索引
    for (int i = left; i < right; i++)  // 遍历基准以外的其他元素
    {
        if (A[i] <= pivot)              // 把小于等于基准的元素放到前一个子数组末尾
        {
            Swap(A, ++tail, i);
        }
    }
    Swap(A, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
                                        // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    return tail + 1;                    // 返回基准的索引
}

void QuickSort(int A[], int left, int right)
{
    if (left >= right)
        return;
    int pivot_index = Partition(A, left, right); // 基准的索引
    QuickSort(A, left, pivot_index - 1);
    QuickSort(A, pivot_index + 1, right);
}

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 从小到大快速排序
    int n = sizeof(A) / sizeof(int);
    QuickSort(A, 0, n - 1);
    printf("快速排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

实现过程Gif

QuickSort.gif

应用范围

快排是不稳定的排序算法 , 不稳定发生在 基准元素A[tail+1] 交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

总结

Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?

答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。

常用的非比较排序算法总结