经典排序算法总结

经典排序算法总结

今日 (2017.10.10) 开始学习数据结构与算法, 边学边总结. 时时对自己的知识进行有效的总结, 可以加深对知识的记忆和理解, 同时方便日后查阅.
本篇博文的主要介绍经典的排序算法, 大致结构为: 按照实现由易到难的顺序依次介绍各排序算法; 在具体介绍一个排序算法前, 首先说明该算法的原理和步骤, 应力求清晰易懂, 对于不方便演示的内容, 应插入对应的参考资料 (方便自己, 造福他人); 之后给出该算法的 C++/Python 实现, 在每个实现中, 应给出相应的测试数据, 算法的运行时间, 以及最后的输出结果(不要偷懒, 现在的疼苦是为了将来的方便)

2017.10.11: 完成数据准备, 冒泡排序, 选择排序, 插入排序的 C++ 代码实现;
2017.10.12: 完成归并排序, 希尔排序, 快速排序的实现 1 等的 C++ 代码, 进度慢;
2017.10.13: 完成快速排序的 C++ 实现 2 与快速排序的分析等, 进度慢 (画图也需要时间的…);
2017.10.14: 完成 Python 数据准备, 快速排序的 Python 实现以及分析等, 无进度;
2017.10.15: 完成快速排序的实现分析等, 剩下的坑一个月之后再来填. 另外, 还有一个堆排序没有考虑.
2018.04.17: 增加归并排序的易于理解的实现.

数据准备

为了方便后面算法程序的测试, 这里先给出测试数据的生成代码.

Cpp 测试数据

#include <iostream>
#include <iomanip>
#include <random>
#include <ctime>

using namespace std;

// 产生一个随机的数组, 数组元素的范围有 min 和 max 控制
template <typename T>
void generate_array(T arr[], size_t len, T min = 0, T max = 10) {

    static default_random_engine e(1234);
    static uniform_int_distribution<int> uniform(min, max);

    T *parr = arr;
    for (size_t i = 0; i != len; ++i)
        *parr++ = uniform(e);
}

// 打印数组
template <typename T>
void print_array(T arr[], size_t len) {
    T *parr = arr;
    for (size_t i = 0; i != len; ++i)
        cout << *parr++ << " ";
    cout << endl;
}

// 确认数组是否已经排序好, 如果没排序好, 通知你 Mission failed!
template <typename T>
void confirm_sorted(T arr[], size_t len) {
    for (size_t i = 0; i != len - 1; ++i) {
        if (arr[i] > arr[i + 1]) {
            cout << "Mission failed!" << endl;
            return;
        }
    }
    cout << "Congratulations! Mission accomplished!" << endl;
}

/**
* 测试排序算法, 参数分别是:
* arr : 数组指针
* len : 数组长度
* SortName : 排序算法名称, 可以是 C 风格字符串, 比如 "Bubble Sort"
* sort_alg : 排序算法函数
* 该函数会随机生成数组, 然后给数组排序, 并判断数组是否排序成功, 
* 同时输出排序花费时间, 当数组的长度太长的话, 就不打印出来, 如果要
* 打印, 数组长度应该小于 20.
*/
template <typename T>
void test_sort(T arr[], size_t len, string SortName, void (*sort_alg)(T[], size_t), T min = 0, T max = 10) {
    generate_array(arr, len, min, max);
    if (len < 20) {
        cout << "Before Sorting: ";
        print_array(arr, len);
    }
    clock_t t = clock();
    sort_alg(arr, len);
    t = clock() - t;
    if (len < 20) {
        cout << "After  Sorting: ";
        print_array(arr, len);
    }
    confirm_sorted(arr, len);
    cout << SortName << ":\n\t" << len << " elements\n\t"
        << "spend " << setprecision(4) << (float)t / CLOCKS_PER_SEC << "s" 
        << endl;
}

Python 测试数据

下面给出 Python 的测试数据:

import random
import functools
import time

# used for testing sorting algorithm
def test_sort(func):
    @functools.wraps(func)
    def test(n=10, a=0, b=10):
        arr = generate_array(n, a, b)
        if (n < 20):
            print "Before Sorting", arr
        t = time.time()
        arr = func(arr)
        if (n < 20):
            print "After  Sorting", arr
        print "Spend: {:.4f}s".format(time.time() - t)
        confirm_sorted(arr)
    return test


# generate random array
def generate_array(n, a=0, b=10):
    arr = []
    for _ in range(n):
        arr.append(round(random.uniform(a, b)))
    return arr

# confirm the array has been sorted
def confirm_sorted(arr):
    for i in range(len(arr) - 1):
        if (arr[i] > arr[i + 1]):
            print "Mission Failed!\n"
    print "Congratulations! Mission Accomplished!\n"

冒泡排序 (Bubble Sort)

原理

冒泡排序的原理非常简单, 它重复遍历要排序的序列, 一次比较两个元素的大小, 不断的将较大的元素向序列的尾部移动, 直到没有一对元素需要比较. 详情见维基百科 – 冒泡排序

步骤

冒泡排序算法的流程如下:

  1. 比较相邻的两个元素. 如果第一个元素比第二个元素大, 便进行交换;
  2. 对每一对相邻元素重复以上步骤, 从序列的第一对到最后一对. 完成这个步骤之后, 序列的最后一个元素会是整个序列的最大值.
  3. 针对每个元素重复以上步骤, 除了最后一个元素 (a[0] ~ a[N - 2]);
  4. 对持续减少的序列 (a[0] ~ a[N - m], 其中 m > 2) 重复以上步骤, 直到没有元素可以交换.

Cpp 实现

下面给出 Bubble Sort 的 C++ 实现, 思路比较简单.
外层循环控制遍历数组的次数, 对于长度为 N 的数组, 每次遍历都能确定一个最大的元素并将其放在数组最后的位置上, 那么只需要 N - 1 次遍历 (最后剩下的一个元素就不必再和其他元素交换了) 便能将数组排好序. 因此外层中 i 的范围为 0 ~ len - 2;
而对于内层循环, 在每次遍历过程中需要不断交换相邻的元素, 对于第 0 次遍历, 最后一组相邻元素为 (arr[len - 2], arr[len - 1], 对于第 i 次遍历, 最后一组相邻的元素为 arr[len - i - 2], arr[len - 1 - i].

template <typename T>
void bubble_sort(T arr[], size_t len) {
    for (size_t i = 0; i != len - 1; ++i) {
        for (size_t j = 0; j != len - 1 - i; ++j) {
            if (arr[j] > arr[j + 1])
                swap(arr[j], arr[j + 1]);
        }
    }
}
测试结果

下面是测试代码. 第一个实验元素个数较少, 方便打印; 第二个实验元素有 100000 个, 在我的电脑上需要运行 38s 左右, 可以改小一些. 具体看结果.

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Bubble Sort", bubble_sort<int>);

    const size_t LEN = 100000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Bubble Sort", bubble_sort<int>);
    return 0;
}

下面是测试结果:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Bubble Sort:
        10 elements
        spend 1e-06s
Congratulations! Mission accomplished!
Bubble Sort:
        100000 elements
        spend 38.28s

Python 实现

选择排序 (Selection Sort)

原理

选择排序的原理也非常直观, 它首先找到序列中的最小元素, 将其放在排序序列的起始位置; 然后在剩下未排序的元素中寻找最小的元素, 并放在已排序好的序列的末尾, 重复该步骤直到序列排序完成. 详情见维基百科 – 选择排序

步骤

  1. 寻找要排序序列中的最小元素, 然后和序列的首位元素交换;
  2. 在剩下未排序的元素中继续寻找最小元素, 然后和未排序序列的首位元素进行交换;
  3. 重复以上步骤, 直到序列排序完成.

Cpp 实现

下面给出 Selection Sort 的 C++ 实现. 和 Bubble Sort 类似, 外层循环控制遍历的次数, 但内层循环负责查找最最小的元素. 对于长度为 N 的数组来说, 只需要将其中的 N - 1 个元素排序好, 剩下的一个元素就会在正确的位置. 因此只需要遍历 N - 1 次, 所以 i 最大为 len - 2.
在每次遍历开始前, 首先将未排序序列的第一个元素设置为 min (此处用的是元素的索引 min_index), 然后查找未排序序列的最小元素, 只要将剩下的元素依次和 min 比较, 并将 min 设置为最小的那个值即可, 因此内层循环中 j 的变化是从 i + 1len - 1(未排序序列的最后一个元素).

template <typename T>
void selection_sort(T arr[], size_t len) {
    for (size_t i = 0; i != len - 1; ++i) {
        size_t min_index = i;
        for (size_t j = i + 1; j != len; ++j) {
            if (arr[j] < arr[min_index])
                min_index = j;
        }
        if (min_index != i)
            swap(arr[i], arr[min_index]);
    }
}
测试结果

测试代码如下:

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Selection Sort", selection_sort<int>);

    const size_t LEN = 100000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Selection Sort", selection_sort<int>);
    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Selection Sort:
        10 elements
        spend 1e-05s
Congratulations! Mission accomplished!
Selection Sort:
        100000 elements
        spend 9.908s

可以看到, 选择排序要比冒泡排序快很多.

Python 实现

插入排序 (Insertion Sort)

原理

插入排序直观理解就是平常我们玩牌时, 将牌整理成有序的过程. 当我们拿起第一张牌时, 由于手上原本是没有牌的, 所以并不需要任何操作. 接着我们拿起第二张牌, 就需要和第一张牌进行比较, 如果数值小于手上的那张牌, 那么就需要将第二张牌插入到第一张牌的前面;
否则, 只需要放在第一张牌的后面即可. 此时, 我们手上的牌就是已经排序好的, 而桌上剩下的牌是未被排序的. 对于之后从桌上拿起的任何牌, 我们都需要和手上的牌依次比较, 直到找到合适的插入位置. 专业解释见维基百科 – 插入排序

步骤

插入排序的步骤如下:

  1. 从第 1 个元素开始, 该元素可以认为是已经排好序的;
  2. 取出下一个元素, 在已排好序的序列中从后向前扫描;
  3. 如果该元素小于它前一个元素, 那么将它前一个元素移到下一个位置;
  4. 重复步骤 3, 直到该元素大于或等于已排好序的序列中的某个元素, 比如说 A;
  5. 将该元素插入到 A 元素的后面;
  6. 重复步骤 2 ~ 5.

Cpp 实现

下面是插入排序算法的一种实现方法, 内层循环使用的是 while 语句, 可以改为 for 循环.
对于外层循环, 我们认为第一个元素是已经排序好的, 而后面的元素是未排序的, 因此 i 的变化范围为 1 ~ len - 1; 之后我们从未排序的元素中选取第一个元素 current = arr[i], 要将其与它前面已排序的序列中的元素进行比较, 并从已排序序列中的最后一个元素开始, 即 j = i - 1;
对于内层循环, 则是当前元素和已排序序列的比较过程, 不断的比较直到两种情况发生:

  1. 当前元素比所有的元素都要小, 此时要完成整个已排序数组的比较, 需要满足 j >= 0
  2. 当前元素遇到了比它更小的元素 A, 那么只要插入到 A 后面即可, 即 arr[j + 1] = current
template <typename T>
void insertion_sort(T arr[], size_t len) {
    for (size_t i = 1; i != len; ++i) {
        T current = arr[i];
        int j = i - 1;
        while (j > -1 && current < arr[j]) {
           arr[j + 1] = arr[j];
           j--;
        }
        arr[j + 1] = current;
    }
}
测试结果

测试代码如下:

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Insertion Sort", insertion_sort<int>);

    const size_t LEN = 100000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Insertion Sort", insertion_sort<int>);
    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Insertion Sort:
        10 elements
        spend 0s
Congratulations! Mission accomplished!
Insertion Sort:
        100000 elements
        spend 5.831s

可以看到, 插入排序要比选择排序快.

Python 实现

归并排序 (Merge Sort)

原理

归并算法主要采用的是分治(Divide and Conquer)策略, 它是将两个有序的序列合并为一个有序序列的过程. 详情见维基百科 – 归并算法

步骤

维基百科 – 归并算法 上面介绍了两种方法, 分别是迭代法和递归法, 我这里的实现使用的是递归方法. 其中,

迭代法的步骤如下:

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

递归法的步骤如下:

  1. 将长度为 N 的序列不断进行二分, 这样可以形成大量的短序列, 直到短序列的长度为 1, 这样, 得到 N 个长度为 1 的短序列;
  2. 然后依次对相邻的两个长度为 1 的短序列进行合并, 使得合并后的序列为长度为 2 的有序序列; 这样可以得到 floor(N / 2) 个长度为 2 的有序序列;
  3. 重复步骤 2, 继续对短序列进行合并, 但是短序列的长度在不断的增大
  4. 直到最后得到长度分别为 floor(N / 2)N - floor(N / 2) 的两个短序列, 在将它们合并为一个长度为 N 的有序序列.

Cpp 实现 1

在下面的实现中, mergeSort 通过递归调用实现对序列不断的二分, 当递归到底之后, 将会对序列进行归并操作, 这由 merge 函数实现.

template <typename T>
void merge(T arr[], int l, int mid, int r) {
    T newarr[r - l + 1];

    // 对 arr[l...mid] 和 arr[mid+1...r] 数组归并
    // i 用来索引 arr[l...mid], j 用来索引 arr[mid+1...r]
    // k 用来索引 newarr[0...r-l];
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r) {
        if (arr[i] < arr[j])
            newarr[k++] = arr[i++];
        else
            newarr[k++] = arr[j++];
    }
    while (i <= mid)
        newarr[k++] = arr[i++];
    while (j <= r)
        newarr[k++] = arr[j++];

    for (int m = l; m <= r; ++m)
        arr[m] = newarr[m - l];
}

template <typename T>
void mergeSort(T arr[], int l, int r) {
    if (l >= r)
        return;
    int mid = l + (r - l) / 2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

template <typename T>
void merge_sort(T arr[], size_t n) {
    mergeSort(arr, 0, n - 1);
}
测试结果

测试代码如下, 注意第二个实验的数据量改成了 1,000,000.

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Merge Sort", merge_sort<int>);

    const size_t LEN = 1000000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Merge Sort", merge_sort<int>);

    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Merge Sort:
        10 elements
        spend 5e-06s
Congratulations! Mission accomplished!
Merge Sort:
        1000000 elements
        spend 0.1477s

可以看到, 归并排序的性能要远优于前面介绍的三种算法. 下面的 Cpp 实现 2 中使用的是指针操作, 所以速度会快上那么一些. 当然也有可能是 Cpp 实现 1 是在笔记本上测试而 Cpp 实现 2 是在台式机上测试的缘故.

Cpp 实现 2

在下面的实现中, 注意完成实际工作的是 part_sort, 该函数处理数组 arr[0, len - 1] 范围内的数据. 使用数组 brr[N] 来保存归并后的有序序列; 在 while 循环中进行归并操作, 最后要将 brr[N] 中的有序序列拷贝到数组 arr 中.

template <typename T>
void part_sort(T arr[], int left, int right) {
    if (left >= right)
        return;
    int mid = left + (right - left) / 2;
    part_sort(arr, left, mid);
    part_sort(arr, mid + 1, right);
    const int N = right - left + 1;
    T brr[N];
    T *bp = brr;
    T *p = &arr[left], *q = &arr[mid+1];
    T *endp = &arr[mid+1], *endq = &arr[right+1];
    while (p != endp && q != endq) {
        if (*p < *q)
            *bp++ = *p++;
        else
            *bp++ = *q++;
    }
    while (p != endp) {
        *bp++ = *p++;
    }
    while (q != endq) {
        *bp++ = *q++;
    }
    bp = brr;
    for (T *beg = &arr[left]; beg != &arr[right+1]; )
        *beg++ = *bp++;
}

template <typename T>
void merge_sort(T arr[], size_t len) {
    int left = 0, right = len - 1;
    part_sort(arr, left, right);
}
测试结果

测试代码如下, 注意第二个实验的数据量改成了 1,000,000.

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Merge Sort", merge_sort<int>);

    const size_t LEN = 1000000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Merge Sort", merge_sort<int>);

    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Merge Sort:
        10 elements
        spend 1e-06s
Congratulations! Mission accomplished!
Merge Sort:
        1000000 elements
        spend 0.1261s

可以看到, 归并排序的性能要远优于前面介绍的三种算法.

Python 实现

希尔排序 (Shell Sort)

原理

希尔排序是插入排序的一种高效的改进版本. 希尔排序是非稳定排序算法.
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位.
因此, 希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。详情见维基百科 – 希尔排序

用一个例子来解释希尔排序可能更直观一些:

例如,假设有这样一组数 [ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为 5 开始进行排序,我们可以通过将这列表放在有 5 列的表中来更好地描述算法,这样他们就应该看起来是这样:

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然后我们对每列进行排序:

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时 10 已经移至正确位置了,然后再以 3 为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后变为:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最后以1步长进行排序(此时就是简单的插入排序了)。

步骤

希尔排序的步骤如下:

  1. 选取合适的步长, (此时将数组想象成上面的二维结构), 然后对每一列进行插入排序;
  2. 减小步长, 重复步骤 1;
  3. 直到步长大小为 1, 此时对序列就是进行简单的插入排序.

C 实现

Cpp 实现

首先选择合适的步长, 一般为 gap = len / 2; while 循环中的判断条件会一直持续到 gap = 1 时, 使用 gap /= 2 不断的减小步长.
然后看 for 循环, for 循环实现对每一列的插入排序, 变量 igap 开始, 那么(此时回忆序列二维结构的样子), 也就是序列的第一行不去管, 而是从序列的第二行开始, 依次对每一列进行插入排序. 若当前的值为 current, 它要和前一行的对应位置的元素进行比较, 即与 arr[i - gap], 如果小于前一行的对应位置的元素, 那么 arr[i - gap] 就要向后移动一位; 这个比较过程一直持续到 j = 0, 注意 j 的变化是每次减小 gap. 最后如果 current 大于它前面的那个元素, 那么只要将其放在该元素的后面即可: arr[j + gap] = current.
另外提醒一点, 要验证算法是否写对, 可以令 gap = 1, 可以看到 while (gap > 0) 循环中的代码和插入排序是一模一样的.

template <typename T>
void shell_sort(T arr[], size_t len) {
    int gap = len / 2;
    while (gap > 0) {
        for (int i = gap; i != len; ++i) { // 对每一列进行排序
            T current = arr[i];
            int j = i - gap;
            while (j >= 0 && current < arr[j]) {
                arr[j + gap] = arr[j];
                j -= gap;
            }
            arr[j + gap] = current;
        }
        gap /= 2;
    }
}
测试结果

测试代码如下, 注意第二个实验中数据量改成了 1,000,000 了.

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Shell Sort", shell_sort<int>);

    const size_t LEN = 1000000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Shell Sort", shell_sort<int>);

    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Shell Sort:
        10 elements
        spend 1e-06s
Congratulations! Mission accomplished!
Shell Sort:
        1000000 elements
        spend 0.1396s

从结果可以看出, 希尔排序对插入排序的改进大大提升了算法的性能.

Python 实现

快速排序 (Quick Sort)

原理

快速排序算法是一种基于交换的高效的排序算法, 它采用分治法的思想, 首先选出一个基准, 将一个序列分成两个子序列, 使得基准前面的序列小于基准, 基准后面的序列大于基准, 这样基准的位置就是它在最终排序好的序列中的位置. 之后对两个子序列重复快速排序的步骤即可. 详情见维基百科 – 快速排序

步骤

快速排序的步骤如下:

  1. 从序列中选出一个元素作为基准 (pivot);
  2. 分区过程(partition),将比基准数大的放到右边,小于或等于它的数都放到左边;
  3. 再对左右区间递归执行第 2 步,直至各区间只有一个数。

C 实现

Cpp 实现 1

这个实现相对比较直观, 但必须说明, 从测试结果可以看出, 它对含有大量重复元素的序列的排序性能显然是不行的. 下面简述它的实现过程

  1. 选取序列的第一个元素为基准 pivot = arr[start], 使用 storeIndex 来保存基准的位置, 一般分区操作 (Partition) 返回的就是这个索引.
  2. i 表示当前访问元素的索引, istart + 1 开始一直到序列的末尾 end, 访问序列中的每个元素. 如果说当前访问的元素比基准数要大, 那么只需要访问下一个元素即可, 如下图:
  3. 但是若当前访问的元素 arr[i] 要小于基准数, 那么:

    注意步骤, 第 1 步, 当前的访问元素 arr[i] 是小于 pivot 的(注意 i 指向红色), 那么第 2 步, 先将 storeIndex 指向下一个大于 pivot 的元素, 然后在第 3 步将 arr[i]arr[storeIndex] 进行交换.
  4. 持续进行这样的操作, 直到 i 访问到 end; 注意到当完成 arr[i]arr[storeIndex] 的交换时, 此时 storeIndex 指向的是一个小于 pivot 的元素.
  5. 那么当这个操作结束时(也就是 i 访问完了所有元素), 分区(Partition)操作完成, storeIndex 保存的是基准数应该存放的位置. 此时只要将基准数 arr[start]arr[storeIndex] 交换即可(由于 arr[storeIndex] 保存的是小于 pivot 的元素, 所以交换后终于可以满足小于 pivot 的元素在 pivot 的左边, 而大于它的元素全都在 pivot 的右边)
  6. 之后根据分治法的思想, 迭代处理两个子序列.
template <typename T>
void quick_sort_recursive_1(T arr[], int start, int end) {
    if (start >= end) return;
    T pivot = arr[start];
    int storeIndex = start; // 存储 pivot 放置的位置
    for (int i = start + 1; i != end + 1; ++i) {
        if (arr[i] < pivot) {
            storeIndex++; // 当存在小于 pivot 的, 说明要后移一位
            swap(arr[i], arr[storeIndex]);
        }
    }
    swap(arr[storeIndex], arr[start]);
    quick_sort_recursive_1(arr, start, storeIndex - 1);
    quick_sort_recursive_1(arr, storeIndex + 1, end);

}

template <typename T>
void quick_sort_1(T arr[], size_t len) {
    int left = 0, right = len - 1;
    quick_sort_recursive_1(arr, left, right);
}
测试结果

测试代码如下:

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Quick Sort", quick_sort_1<int>);

    const size_t LEN = 1000000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Quick Sort", quick_sort_1<int>);

    return 0;
}

测试结果如下, 注意第二个实验用的数据量是 1,000,000:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Quick Sort:
        10 elements
        spend 2e-06s
Congratulations! Mission accomplished!
Quick Sort:
        1000000 elements
        spend 94.79s

可以看到, 耗时非常久, 需要进一步改进.

Cpp 实现 2

由于序列中可能包含大量的重复元素, 如果使用 Cpp 实现 1 中的方法, 可能会使得快速排序退化为 O(n^2) 的算法. 因此需要采取一定的措施, 如下图:

我们引入两个索引 ij, 分别从序列的左边和右边来对序列进行调整, 我们的目的是使得在 arr[start + 1, i) 范围内的序列始终 <= pivot, 而 arr(j, end] 范围内的序列始终 >= pivot.

索引 istart + 1 开始计数, 当当前访问的数值小于基准时, 则继续访问下一个元素, 直到遇到大于或等于基准的元素; 索引 jend 开始计数, 当当前访问的元素大于基准时, 则继续向左移动访问下一个元素, 直到遇到小于或等于基准的元素. 如图:

此时进行第 3 个步骤, 将 arr[i]arr[j] 进行交换即可.

在下面的实现中要特别注意 3 点:

  • 随机选取序列中的一个元素作为基准, 这样可以在一定程度上避免快速排序性能恶化;
  • while 循环中对于 arr[i] 以及 arr[j] 与基准的比较, 注意不需要写 =, 也就是说, 当 arr[i]arr[j] 等于基准时, 要对它们进行交换操作.
  • 最后循环跳出的条件是 if (i > j), 原因是:

    i == j 时(注意到 ij 始终指向未知的元素), 此时应该判断当前访问的元素与基准的关系.

  • 如果 arr[i] < pivot, 那么也说明了 arr[j] < pivot, 此时第 2 个 while 循环中 i 要自增 1, 同时对于 j 来说, 第 3 个 while 循环不需要判断, 直接跳出, 由于满足了 i > j 的条件, 最大的 while 循环跳出, 最后交换 arr[start]arr[j] 即可.

  • 如果 arr[i] > pivot, 那么也说明了 arr[j] > pivot, 此时第 2 个 while 循环跳出, 同时对于 j 来说, 第 3 个 while 循环满足, j 需要自减 1, 使得此时 j 指向的是一个小于基准的元素. 这时也满足了 i > j 的条件, 最大的 while 循环跳出, 最后交换 arr[start]arr[j] 即可.

template <typename T>
void quick_sort_recursive_2(T arr[], int start, int end) {
    if (start >= end) return;
    // 随机选取基准
    static default_random_engine e(1234);
    swap(arr[e() % (end - start + 1) + start], arr[start]);
    T pivot = arr[start];
    // 使得 arr[start + 1, i) <= pivot, arr(j, end] >= pivot
    int i = start + 1, j = end;
    while (true) {
        // 注意这里是 arr[i] < pivot 而不是 arr[i] <= pivot
        // 当 arr[i] 或 arr[j] 等于 pivot 时, 
        // 跳出循环, 直接进行交换操作
        while (i <= end && arr[i] < pivot) ++i;
        while (j >= start + 1 && arr[j] > pivot) --j;

        // 不能在 i = j 时跳出循环, 否则由于此时 arr[start + 1, i)
        // 与 arr(i, end] 并没有交集, 还有 arr[i](或 arr[j])这个值
        // 并没有和 pivot 进行比较
        if (i > j) break;
        swap(arr[i], arr[j]);
        ++i;
        --j;
    }
    // 当跳出循环时, 此时 arr[start + 1, end] 范围内的数据都和 pivot 比较完,
    // arr[j] 是最后一个小于或等于 pivot 的值, 而 arr[i] 是第一个大于或等于 pivot
    // 的值, 因此 pivot 要和 arr[j] 交换
    swap(arr[start], arr[j]);
    quick_sort_recursive_2(arr, start, j - 1);
    quick_sort_recursive_2(arr, j + 1, end);
}

template <typename T>
void quick_sort_2(T arr[], size_t len) {
    int start = 0, end = len - 1;
    quick_sort_recursive_2(arr, start, end);
}
测试结果

首先测试代码如下:

int main() {

    const size_t N = 10;
    int arr[N];
    test_sort(arr, N, "Quick Sort", quick_sort_2<int>);

    const size_t LEN = 1000000;
    int arr2[LEN];
    test_sort(arr2, LEN, "Quick Sort", quick_sort_2<int>);

    return 0;
}

测试结果如下:

Before Sorting: 0 3 4 10 4 2 0 7 2 4 
After  Sorting: 0 0 2 2 3 4 4 4 7 10 
Congratulations! Mission accomplished!
Quick Sort:
        10 elements
        spend 2e-06s
Congratulations! Mission accomplished!
Quick Sort:
        1000000 elements
        spend 0.1491s

这个结果才有点快速排序的样子!

Python 实现

@test_sort
def quick_sort(arr):
    return quick_sort_recursive(arr, 0, len(arr) - 1)


def quick_sort_recursive(arr, start, end):
    if (start >= end):
        return arr
    n = random.randint(start, end)
    arr[start], arr[n] = arr[n], arr[start]
    pivot = arr[start]

    # arr[start+1, left) <= pivot, arr(right, end] >= pivot
    left = start + 1
    right = end
    while True:
        while (left <= end and arr[left] < pivot): left += 1
        while (right >= start + 1 and arr[right] > pivot): right -= 1
        if (left > right): break
        arr[left], arr[right] = arr[right], arr[left]
        left += 1
        right -= 1
    arr[start], arr[right] = arr[right], arr[start]
    quick_sort_recursive(arr, start, right - 1)
    quick_sort_recursive(arr, right + 1, end)
    return arr
测试结果

测试代码为:

if __name__ == '__main__':
    quick_sort(10)
    quick_sort(1000000)

测试结果:

Before Sorting [4.0, 9.0, 4.0, 1.0, 8.0, 6.0, 2.0, 7.0, 0.0, 1.0]
After  Sorting [0.0, 1.0, 1.0, 2.0, 4.0, 4.0, 6.0, 7.0, 8.0, 9.0]
Spend: 0.0000s
Congratulations! Mission Accomplished!

Spend: 2.9296s
Congratulations! Mission Accomplished!

复杂度分析

参考资料

  1. 维基百科 - 快速排序
  2. 常见排序算法 - 快速排序 (Quick Sort)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值