一文搞定排序算法

对于排序算法,网上有很多文章,也有很多种不同风格的实现,这里就所有的排序算法做一个汇总,部分代码参考了网上的博客,精选出一些优质答案,方便理解和记忆,并且所有的代码均在OJ平台上测试通过

这里的所有排序算法的实现,均统一了方法调用的接口:...Sort(array,n)...Sort(array, n) 其中arrayarray为待排序数组,nn为数组长度


排序动图可参考十大经典排序算法


排序算法复杂度


排序方法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度
冒泡排序 O(n2)O(n^2) O(n2)O(n^2) O(n)O(n) O(1)O(1)
插入排序 O(n2)O(n^2) O(n2)O(n^2) O(n)O(n) O(1)O(1)
选择排序 O(n2)O(n^2) O(n2)O(n^2) O(n2)O(n^2) O(1)O(1)
希尔排序 O(n1.3)O(n^{1.3}) O(n2)O(n^2) O(n)O(n) O(1)O(1)
归并排序 O(nlog2n)O(nlog_{2}n) O(nlog2n)O(nlog_{2}n) O(nlog2n)O(nlog_{2}n) O(n+k)O(n + k)
快速排序 O(nlog2n)O(nlog_{2}n) O(n2)O(n^2) O(nlog2n)O(nlog_{2}n) O(nlog2n)O(nlog_{2}n)
堆排序 O(nlog2n)O(nlog_{2}n) O(nlog2n)O(nlog_{2}n) O(nlog2n)O(nlog_{2}n) O(1)O(1)
计数排序 O(n+k)O(n + k) O(n+k)O(n + k) O(n+k)O(n + k) O(n+k)O(n + k)
基数排序 O(n×k)O(n × k) O(n×k)O(n × k) O(n×k)O(n × k) O(n+k)O(n + k)
桶排序 O(n+k)O(n + k) O(n2)O(n^2) O(n)O(n) O(n+k)O(n + k)

基数排序、基数排序、桶排序中:n表示数据个数,k表示数据位数

  • 稳定:如果aa原本在bb前面,而a=ba = b,排序之后aa仍然在bb的前面。
  • 不稳定:如果aa原本在bb的前面,而a=ba=b,排序之后aa可能会出现在 bb 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当nn变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模nn的函数。

辅助记忆

  • 时间复杂度记忆
    • 冒泡、选择、直接 排序需要两个forfor循环,每次只关注一个元素,平均时间复杂度为(一遍找元素,一遍找位置)
    • 快速、归并、希尔、堆基于二分思想,loglog22为底,平均时间复杂度为(一遍找元素,一遍找位置)
  • 稳定性记忆
    • “快希选堆”(快牺牲稳定性)

冒泡排序

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
  • 针对所有的元素重复以上的步骤,除了最后一个
  • 重复步骤1~3,直到排序完成
#include <iostream>
using namespace std;
/**
 * @brief 冒泡排序(从小到大)
 * @param array 
 * @param n 
 */
void bubbleSort (int array[], int n) {
    // 开始进行遍历 ==>控制判断的次数
    for (int i = 0; i < n; i++) {
        bool flag = false;  // 记录有无数据交换
        // 内循环 ==> 相邻数字的比较和移动
        for (int j = 0; j < n - 1 - i; j++) {
            // j < n - 1 - i这里 -i 是因为后面已经排好
            if (array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
                flag = true;
            }
        }
        if (!flag)
            break;
    }
}

选择排序

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

nn个记录的直接选择排序可经过 n1n - 1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n]R[1..n],有序区为空
  • ii趟排序(i=1,2,3n1)(i = 1,2,3…n - 1)开始时,当前有序区和无序区分别为R[1..i1]R(i..n)R[1..i - 1] 和 R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k]R[k],将它与无序区的第1个记录R交换,使R[1..i]R[i+1..n)R[1..i] 和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区
  • n1n - 1趟结束,数组有序
#include <iostream>
using namespace std;
/**
 * @brief 选择排序
 * @param array 
 * @param n 
 */
void selectSort (int array[], int n) {
    // 进行 n - 1轮比较
    for (int i = 0;i < n - 1; i++) {
        int min = i;
        // 找最小的元素
        for (int j = i + 1; j < n; j++) {
            // 记录目前能找最小元素值的下标
            if (array[j] < array[min]) 
                min = j;
        }
        // 将找到的最小元素和i下标的最小元素进行交换
        if (i != min) {
            int temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
}

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。


插入排序

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入

  • 从第一个元素开始,该元素可以认为已经被排序
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  • 将新元素插入到该位置后
  • 重复步骤2~5
#include <iostream>
using namespace std;
/**
 * @brief 插入排序
 * @param array 
 * @param n 
 */
void insertSort (int array[], int n) {
    for (int i = 1; i < n; i++) {
        // 记录当前元素
        int temp = array[i];
        int j = i;
        // 在有序序列中从后往前找到空位
        while (j >= 1 && array[j - 1] > temp) {
            array[j] = array[j - 1];
            j--;
        }
        // 插入
        array[j] = temp;
    }
}

希尔排序

1959年Shell发明,第一个突破O(n2)O(n^2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于:它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1t2tkt1,t2,…,tk,其中ti>tjtk=1ti>tj,tk=1
  • 按增量序列个数kk,对序列进行kk趟排序;
  • 每趟排序,根据对应的增量titi,将待排序列分割成若干长度为mm的子序列,分别对各子表进行直接插入排序。仅增量因子为11 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
#include <iostream>
using namespace std;
/**
 * @brief 希尔排序 ==> 增量序列 + 插入排序(插入排序的高效改进算法)
 * @param array 
 * @param n 
 */
void shellSort(int array[], int n) {
    int d = n / 2;
    int j, temp;
    // 引入增量 d
    while (d > 0) {
        // 每一趟相当于以 d 为步数的插入排序
        for (int i = d; i < n; i++) {
            temp = array[i];
            j = i;
            // 查找插入的位置,这里j >= d放在前面,短路运算会直接跳出循环
            while (j >= d && array[j - d] > temp) {
                array[j] = array[j - d];
                j -= d;
            }
            // 插入排序
            array[j] = temp;
        }
        d /= 2;
    }
}

归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

  • 把长度为nn的输入序列分成两个长度为n/2n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列
#include <iostream>
using namespace std;
/**
 * @brief 一次归并
 * @param array 一次需要归并的数组
 * @param temp 临时数组
 * @param start 数组起始位置
 * @param mid 数组中间位置
 * @param end 数组末尾位置
 */
void _merge(int array[], int temp[], int start, int mid, int end) {
    int i = start;
    int j = mid + 1;
    int k = 0;
    // 归并操作
    while (i <= mid && j <= end) {
        if (array[i] <= array[j])
            temp[k++] = array[i++];
        else
            temp[k++] = array[j++];
    }
    while (i <= mid) 
        temp[k++] = array[i++];
    while (j <= end)
        temp[k++] = array[j++];
    // 将排好序的存回 array 中 start 到 end 这区间
    for (int i = start, k = 0; i <= end; i++, k++) {
        array[i] = temp[k];
    }
}
/**
 * @brief 归并排序核心递归函数 ==> 分类比武,不断进行归并 
 * @param array 
 * @param temp 
 * @param start 
 * @param end 
 */
void mSort (int array[], int temp[], int start, int end) {
    if (start < end) {
        int mid = (start + end) / 2;
        mSort(array, temp, start, mid);
        mSort(array, temp, mid + 1, end);
        _merge(array, temp, start, mid, end);
    }
}
/**
 * @brief 归并排序
 * @param array 
 * @param n 
 */
void mergeSort (int array[], int n) {
    // 将temp数组声明在外部是为了防止在merge函数里面频繁申请释放空间带来的开销
    int* temp = new int[n];
    if (temp != NULL) {
        mSort(array, temp, 0, n - 1);
        delete []temp;
    }
}

快速排序

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
#include <iostream>
using namespace std;
/**
 * @brief 快速排序核心递归函数
 * @param array 
 * @param left 
 * @param right 
 */
void qSort (int array[], int left, int right) {
    if (left >= right)
        return;
    // 一般我们选用中间的元素为基准
    int pivot = array[left + right >> 1];
    // 指针都先从边界开始
    int low = left - 1, high = right + 1;
    // 将序列中比基准小的移到基准左边,大的移到右边
    while (low < high) {
        while (array[++low] < pivot);
        while (array[--high] > pivot);
        if (low < high)
            swap(array[low], array[high]);
    }
    qSort(array, left, high);
    qSort(array, high + 1, right);
}
/**
 * @brief 快速排序 ==> 时间复杂度O(nlog n), 空间复杂度O(log n)
 * @param array 
 * @param n 
 */
void quickSort (int array[], int n) {
    qSort(array, 0, n - 1);
}

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点

  • 将初始待排序关键字序列(R1,R2.Rn)(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区
  • 将堆顶元素R[1]R[1]与最后一个元素R[n]R[n]交换,此时得到新的无序区(R1,R2,Rn1)(R1,R2,……Rn-1)和新的有序区(Rn)(Rn),且满足R[1,2n1]<=R[n]R[1,2…n-1]<=R[n]
  • 由于交换后新的堆顶R[1]R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,Rn1)(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2.Rn2)(R1,R2….Rn-2)和新的有序区(Rn1,Rn)(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n1n-1,则整个排序过程完成
/**
 * @brief 向下过滤,将N个元素的数组中以array[p]为根的子堆调整为最大堆
 * @param array 待过滤数组
 * @param p 根节点编号
 * @param N 数组长度
 */
void PercDown(int array[], int p, int N) {
    int parent, child;
    // 取出根结点存放的值
    int X = array[p];
    for (parent = p; (parent * 2 + 1) < N; parent = child) {
        // 从0开始,不同之前堆是从 1 开始
        child = parent * 2 + 1;
        // child指向左右子结点的较大者
        if ((child != N - 1) && (array[child] < array[child + 1]))
            child++;
        // 找到了合适位置,跳出循环
        if (X >= array[child])
            break; 
        else       /* 下滤X */
            array[parent] = array[child];
    }
    array[parent] = X;
}

/**
 * @brief 堆排序 时间复杂度 O(nlog n) 空间复杂度
 * @param array 待排序数组
 * @param N 数组长度
 */
void HeapSort(int array[], int N) {
    int i;
    // 从第一个非叶子节点开始,从右至左调整结构,建立最大堆
    for (i = N / 2; i >= 0; i--) 
        PercDown(array, i, N);
    // 调整堆结构 + 交换堆顶元素与末尾元素
    for (i = N - 1; i > 0; i--) {
        // 不断删除堆顶元素
        swap(array[0], array[i]);
        PercDown(array, 0, i);
    }
}

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)

  • 设置一个定量的数组当作空桶,桶排序的思想近乎彻底的分治思想。桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去
  • 对每个不是空的桶进行排序
  • 从不是空的桶里把排好序的数据拼接起来
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 桶的默认数量
int DEFAULT_BUCKET_SIZE = 5;

void bucketSort(int array[], int n) {
    if (n < 1)
        return;
    int max_element = array[0];
    int min_element = array[0];

    // 找出数组的最大值和最小值
    for (int i = 0; i < n; i++) {
        if (array[i] > max_element)
            max_element = array[i];
        if (array[i] < min_element)
            min_element = array[i];
    }
    int bucketSize = DEFAULT_BUCKET_SIZE;
    int bucketCount = ((max_element - min_element) / bucketSize) + 1;

    vector<vector<int>> buckets(bucketCount);
    vector<int> res;
    // 利用映射函数将数据分配到各个桶中
    for (int i = 0; i < n; i++)
        buckets[(array[i] - min_element) / bucketSize].push_back(array[i]);
    
    for (int i = 0; i < buckets.size(); i++) {
        // 每个桶内部进行排序
        sort(buckets[i].begin(), buckets[i].end());
        for (int j = 0; j < buckets[i].size(); j++)
            res.push_back(buckets[i][j]);
    }

    // 将vector中数据覆盖到原数组
    for (int i = 0; i < res.size(); i++)
        array[i] = res[i];
}

计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数,计数排序其实是特殊的桶排序

  • 找出待排序的数组中最大和最小的元素
  • 统计数组中每个值为ii的元素出现的次数,存入数组CC的第ii
  • 对所有的计数累加(从CC中的第一个元素开始,每一项和前一项相加,为了使排序稳定的一种策略)
  • 反向填充目标数组:将每个元素ii放在新数组的第C(i)C(i)项,每放一个元素就将C(i)C(i)减去11
#include <iostream>
#include <algorithm>
using namespace std;
/**
 * @brief 计数排序
 * @param array 
 * @param n 
 */
void countSort(int array[], int n) {
    if (n < 1)
        return;
    // 1. 得到数组的最大值和最小值,算出差值
    int max_element = array[0];
    int min_element = array[0];
    for (int i = 1; i < n; i++) {
        max_element = max(max_element, array[i]);
        min_element = min(min_element, array[i]);
    }
    int d = max_element - min_element;
    // 2. 创建统计数组并计算统计对应元素个数
    int* count = new int[d + 1];
    for (int i = 0; i < n; i++)
        count[array[i] - min_element]++;
    // 3. 统计数组变形,后面的元素等于前面的元素之和 ==> 使排序稳定的一种策略
    int sum = 0;
    for (int i = 0; i < d + 1; i++) {
        sum += count[i];
        count[i] = sum;
    }
    // 4. 倒序遍历原始数组,从统计数组找到正确位置,输出到结果数组
    int *sortedArray = new int[n];
    for (int i = n - 1; i >= 0; i--)
        sortedArray[--count[array[i] - min_element]] = array[i];

    for (int i = 0; i < n; i++)
        array[i] = sortedArray[i];
    delete[] sortedArray;
}

计数排序是一个稳定的排序算法。当输入的元素是 nn00kk 之间的整数时,时间复杂度是O(n+k)O(n+k),空间复杂度也是O(n+k)O(n+k),其排序速度快于任何比较排序算法。当kk不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。


基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前

  • 取得数组中的最大数,并取得位数
  • arrayarray为原始数组,从最低位开始取每个位组成radixradix数组
  • radixradix进行计数排序(利用计数排序适用于小范围数的特点)
#include <iostream>
#include <algorithm>
using namespace std;

/**
 * @brief 获取数组array中数据最大位数
 * @param array 
 * @param n 
 * @return int 
 */
int maxbit(int array[], int n) {
    if (n < 1)
        return 0;
    int max_element = array[0];
    for (int i = 1; i < n; i++) 
        max_element = max(max_element, array[i]);
    int d = 0;
    while(max_element > 0) {
        max_element /= 10;
        d++;
    }
    return d;
}

/**
 * @brief 基数排序
 * @param array 
 * @param n 
 */
void radixSort(int array[], int n) {
    int d = maxbit(array, n);
    int* temp = new int[n];
    // 计数器
    int* count = new int[10];
    int radix = 1;
    // 进行d次排序
    for (int i = 0; i < d; i++) {
        // 每次分配前清空计数器
        for (int j = 0; j < 10; j++) 
            count[j] = 0;
        for (int j = 0; j < n; j++) {
            int k = (array[j] / radix) % 10;
            count[k]++;
        }
        // 将temp中位置依次分配给每个桶
        for (int j = 1; j < 10; j++)
            count[j] = count[j] + count[j - 1];
        // 将所有桶中记录依次收集到temp中
        for (int j = n - 1; j >= 0; j--) {
            int k = (array[j] / radix) % 10;
            temp[count[k] - 1] = array[j];
            count[k]--;
        }
        // 将临时数组的内容复制到原数组
        for (int j = 0; j < n; j++)
            array[j] = temp[j];
        
        radix *= 10;
    }
}

桶排序&基数排序&计数排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每个桶只存储单一键值
  • 桶排序:每个桶存储一定范围的数值
赞赏