各类排序总结

常见排序算法归纳

  • 一. 排序和顺序统计量
    • 1. 排序算法的分类
    • 2. 排序算法复杂度总览
  • 二. 排序详解
    • 1.冒泡排序
      • 1.1 算法描述
      • 1.2 代码实现
      • 1.3 算法分析
    • 2. 选择排序
      • 2.1 算法描述
      • 2.2 代码实现
      • 2.3 算法分析
    • 3. 插入排序
      • 3.1 算法描述
      • 3.2 代码实现
      • 3.3 算法分析
    • 4. 希尔排序
      • 4.1 算法描述
      • 4.2 代码实现
      • 4.3 算法分析
    • 5. 归并排序
      • 5.1 算法描述
      • 5.2 代码实现
      • 5.3 算法分析
    • 6. 快速排序
      • 6.1 算法描述
      • 6.3 代码实现
    • 7. 堆排序
      • 7.1 算法描述
      • 7.2 代码实现
    • 8. 后面的非比较排序,请参照
      • 十大经典排序算法(动图演示)
  • 三. 总结
    • C++一道深坑面试题:STL里sort算法用的是什么排序算法?
  • 四. 参考链接
    • 十大经典排序算法(动图演示)
    • 排序算法
    • C++一道深坑面试题:STL里sort算法用的是什么排序算法?

一. 排序和顺序统计量

输入: 一个 n n n个数的序列 [ a 1 , a 2 , . . . , a n ] [a1,a2,...,an] [a1,a2,...,an]
输出: 输出原序列的重排,使得 a 1 < = a 2 < = . . . < = a n a1<=a2<=...<=an a1<=a2<=...<=an
在实际中,待排序的数很少时单独的数值,它们通常是记录的数据集一部分。每个记录包含一个关键字,就是排序问题中的重排的值。记录剩余部分为卫星数据,通常与关键字一同存取。在实际中,当一个排序算法重排关键字时,也必须要重排卫星数据。如果每个记录包含大量卫星数据,我们通常重排记录指针的数组,而不是记录本身,这样可以降低数据移动量。

1. 排序算法的分类

排序按照是否进行元素之间比较分为比较排序和非比较排序。
其中比较排序有常见的如:

  1. 交换排序: 冒泡排序,快速排序
  2. 插入排序: 简单插入排序,希尔排序
  3. 选择排序: 简单选择排序,堆排序
  4. 归并排序: 二路归并排序,多路归并排序

而非比较排序有:计数排序 桶排序 基数排序
特殊的有,按照输入数组是否仅有常数个元素需要在排序过程中存储在数组之外,分为原址排序非原址排序
另外,按照元素 a a a和元素 b b b满足 a = b a=b a=b,排序前的前后关系和排序后是否一样分为稳定排序不稳定排序

2. 排序算法复杂度总览

排序算法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 原址 稳定
插入排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 原址 稳定
希尔排序 O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 原址 不稳定
选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1) 原址 不稳定
堆排序 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( 1 ) O(1) O(1) 原址 不稳定
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 原址 稳定
归并排序 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n ) O(n) O(n) 非原址 稳定
快速排序 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) 非原址 不稳定
计数排序 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 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n + k ) O(n+k) O(n+k) 非原址 稳定
基数排序 O ( n ∗ k ) O(n*k) O(nk) O ( n ∗ k ) O(n*k) O(nk) O ( n ∗ k ) O(n*k) O(nk) O ( n + k ) O(n+k) O(n+k) 非原址 稳定

二. 排序详解

1.冒泡排序

冒泡排序顾名思义就是小的元素就如水泡一样往上冒。重复走过要排序的数列,一次比较两个元素,如果他们打的顺序作物就把它们交换过来。

1.1 算法描述

  1. 比较相邻的元素。如果前者大于后者,进行交换。
  2. 对每一对相邻元素做同样的工作,从开始到结尾,最后的元素一定是最大的数。
  3. 重复以上步骤,除了最后一个元素。直到排序完成。

1.2 代码实现

#include<bits/stdc++.h>
using namespace std;
template<typename comparable>
void Bubblesort(vector<comparable> &a){
    int flag=1; //记录是否比较
    for(int i=0;flag;i++){ //上次冒泡没有比较过,就跳出循环
        flag=0;
        for(int j=a.size()-1;j>i;j--)
            if(a[j]<a[j-1]){ //这里注意是小于,保证稳定排序
                swap(a[j],a[j-1]);
                flag=1;
            }
    }
}

1.3 算法分析

最直观的排序,原址稳定排序,就是复杂度不行,不适合大数组排序。

2. 选择排序

选择排序,是找到未排序数组的最小元素,放到排序好数组的末尾。直到所有元素都是已排序的。

2.1 算法描述

n个记录经过选择排序n-1趟可以得到有序结果。

  1. 初始状态:无序区为 a [ 1 , 2... n ] a[1,2...n] a[1,2...n],有序区为空。
  2. 第i趟排序开始时,当前有序区分别为 a [ 1 , 2... i − 1 ] a[1,2...i-1] a[1,2...i1] a [ i . . . n ] a[i...n] a[i...n]。该趟排序从当前无序区中 选出最小值,加入到有序区。
  3. n-1趟结束,数组有序化了。

2.2 代码实现

#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Selectsort(vector<comparable> &a){
    for(int i=0;i<a.size();i++){
        int minid=i; //记录最小值的位置
        for(int j=i+1;j<a.size();j++)
            if(a[j]<a[minid]){
                minid=j;
            }
        swap(a[i],a[minid]); //交换最小值的值到第i个
    }
}

2.3 算法分析

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

3. 插入排序

插入排序构建有序序列,对后面的元素插入到正确的位置。也是非常直观呢!

3.1 算法描述

插入排序是原址的。

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

3.2 代码实现

#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Insertsort(vector<comparable> &a){
    for(int j=1;j<a.size();j++){
        auto key = a[j]; //待插元素
        int i=j-1;
        while(i>=0 && a[i]>key){ //这里用>保证稳定性
            a[i+1]=a[i]; //比a[j]大的后面凉快去!
            i--;
        }
        a[i+1]=key; //最后把key换到正确的位置
    }
}

3.3 算法分析

插入排序是原址排序,经常作为其他方法收尾时的选择。

4. 希尔排序

第一个突破 O ( n 2 ) O(n^2) O(n2)的排序,是插入排序的改进版。它会优先比较距离较远的元素,希尔排序又名缩小增量排序

4.1 算法描述

  1. 选择递增序列 t 1 , t 2 , . . . , t k t_1,t_2,...,t_k t1,t2,...,tk,其中 t i > t j , t k = 1 t_i>t_j, t_k=1 ti>tj,tk=1
  2. 按增量序列个数k,对序列进行k趟排序。
  3. 每趟排序,根据对应的增量 t i t_i ti,将待排序列分割成若干长度为 m m m的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 1 1时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 代码实现

#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Shellsort(vector<comparable> &a){ //其实就是间隔版的插入排序
    //利用性质,大间隔排好序,小间隔排序时不会影响大间隔排序,牛!时间也快起来了
    for(int gap=a.size()/2;gap>0;gap/=2)
        for(int j=gap;j<a.size();j++){ //下面就是插入排序套路
            auto key=a[j];
            int i=j-gap; //跳gap排序,比如a[0] a[gap]
            while(i>=0 && a[i]>key){
                a[i+gap]=a[i]; //a[j]=a[0]
                i-=gap; //每次减少gap i=-gap;
            }
            a[i+gap]=key; //最后插入key,a[0]=a[j]
        }
}

4.3 算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。

5. 归并排序

前面所讨论的排序算法都是复杂度为 O ( N 2 ) O(N^2) O(N2)的低效率排序算法。下面的算法都是时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)的高级算法。
归并算法是基于分治策略。归并算法的基础是合并两个已经有序的子数组,将两个已经有序的子数组进行合并是容易的。比如两个有序子数组 A A A B B B,然后有一个输出数组 C C C。此时你需要三个位置索引 i 、 j i、j ij k k k,每次比较 A [ i ] A[i] A[i] B [ j ] B[j] B[j],然后将最小者复制到 C [ k ] C[k] C[k],同时递增相应的位置索引。重复上述过程知道某一个子数组遍历完,未遍历完的子数组剩余部分直接复制到输出数组就完成整个合并过程。

5.1 算法描述

利用合并,归并排序算法的步骤为:
(1)将数组分为两个大小相等的子数组;
(2)对每个子数组进行排序,除非子数组比较小,否则利用递归方式完成排序;
(3)合并两个有序的子数组,完成排序。

5.2 代码实现

#include<bits/stdc++.h>
using namespace std;

template <typename comparable>
void merge(vector<comparable> &a,vector<comparable> &tmp,int left,int right,int end){
    int tmpPos = left;
    int leftEnd = right-1;
    int num = end-left+1;
    //O(n)合并两个子数组直到一个子数组遍历完
    while(left<=leftEnd && right<=end){
        if(a[left]<=a[right]) tmp[tmpPos++]=a[left++];
        else tmp[tmpPos++]=a[right++];
    }
    //处理剩余数
    while(left<=leftEnd) tmp[tmpPos++]=a[left++];
    while(right<=end) tmp[tmpPos++]=a[right++];
    //结果返回给原数组
    while(num--) a[end]=tmp[end],end--;
}
//下面是不停二分数组,递归调用环节O(lgn)
template <typename comparable>
void Mergesort(vector<comparable> &a,vector<comparable> &tmp,int left,int right){
    if(left<right){
        int mid = left+right>>1;
        Mergesort(a,tmp,left,mid);
        Mergesort(a,tmp,mid+1,right);
        merge(a,tmp,left,mid+1,right);
    }
}

5.3 算法分析

归并排序是稳定的,但是不是原址的,但表现比选择排序好很多。

6. 快速排序

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

6.1 算法描述

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.3 代码实现

#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
//选择快排主元,一半都小于,一半都大于
const comparable& median(vector<comparable> &a,int left,int right){
    int mid = left+right>>1;
    if(a[mid]<a[left]) swap(a[mid],a[left]);
    if(a[left]>a[right]) swap(a[left],a[right]);
    if(a[mid]>a[right]) swap(a[mid],a[right]);
    // left位置的值小于等于pivot,right位置的值一定大于等于pivot,
    // 要分割的数组变成left+1到right-1
    swap(a[mid],a[right-1]); //将pivot放到right-1位置处
    return a[right-1];
}
template <typename comparable>
void Quicksort(vector<comparable> &a,int left,int right){
    if(left+1<right){
        auto pivot = median(a,left,right);
        int i=left,j=right-1;
        while(1){
            while(a[++i]<pivot){}
            while(a[--j]>pivot){}
            if(i<j) swap(a[i],a[j]);
            else break;
        }
        swap(a[i],a[right-1]);
        //对子数组递归
        Quicksort(a,left,i-1);
        Quicksort(a,i+1,right);
    }else if(left<right) if(a[left]>a[right]) swap(a[left],a[right]); //只有两个元素特殊情况
}

7. 堆排序

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

7.1 算法描述

  1. 将初始待排序关键字序列 ( R 1 , R 2 … . R n ) (R1,R2….Rn) (R1,R2.Rn)构建成大顶堆,此堆为初始的无序区;
  2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区 ( R 1 , R 2 , … … R n − 1 ) (R1,R2,……Rn-1) (R1,R2,Rn1)和新的有序区 ( R n ) (Rn) (Rn),且满足 R [ 1 , 2 … n − 1 ] < = R [ n ] R[1,2…n-1]<=R[n] R[1,2n1]<=R[n]
  3. 由于交换后新的堆顶 R [ 1 ] R[1] R[1]可能违反堆的性质,因此需要对当前无序区 ( R 1 , R 2 , … … R n − 1 ) (R1,R2,……Rn-1) (R1,R2,Rn1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区 ( R 1 , R 2 … . R n − 2 ) (R1,R2….Rn-2) (R1,R2.Rn2)和新的有序区 ( R n − 1 , R n ) (Rn-1,Rn) (Rn1,Rn)。不断重复此过程直到有序区的元素个数为 n − 1 n-1 n1,则整个排序过程完成。

7.2 代码实现

#include<bits/stdc++.h>
using namespace std;
#define lf(root) 2*root+1
//#define rt(root) 2*root+2
template <typename comparable>
// 堆排序辅助函数
// v是存储堆的数组,i是要下沉的节点,n代表当前堆的大小
void siftDown(vector<comparable> &a,int i,int n){
    int child;
    auto tmp=a[i];
    while(lf(i)<n){
        child=lf(i); //左子节点
        //寻找最大子节点
        if(child!=n-1 && a[child]<a[child+1]) child++;
        if(tmp<a[child]) {//子节点上移
            a[i]=a[child];
            i=child;
        }else break;
    }
    a[i]=tmp; //下沉到正确位置
}
template <typename comparable>
void Heapsort(vector<comparable> &a){
    //建堆
    for(int i=a.size()/2-1;i>=0;i--) siftDown(a,i,a.size());
    //删除重复根节点
    for(int i=a.size()-1;i>0;i--){
        swap(a[i],a[0]); //交换根节点与最右子节点
        siftDown(a,0,i); //下沉根节点
    }
}

8. 后面的非比较排序,请参照

十大经典排序算法(动图演示)

三. 总结

一般合并排序和快速排序用的很多,一定要会手撕!当然也可以自己看c++ STL库查看sort实现源码,仅仅只用了快速排序,还结合了插入排序和堆排序。可以参照

C++一道深坑面试题:STL里sort算法用的是什么排序算法?

四. 参考链接

十大经典排序算法(动图演示)

排序算法

C++一道深坑面试题:STL里sort算法用的是什么排序算法?

你可能感兴趣的:(Algorithms,排序算法)