常见排序算法

排序方法有多种分类方式:例如,根据在排序过程中待排序的所有记录是否全部被放置在内存中,可以将排序方法分为内排序和外排序两大类;根据排序方法是否建立在关键字比较的基础上,可以将排序方法分为基于比较的排序和不基于比较的排序,等等。

外部归并排序

外排序(External sorting)能够处理极大量数据,通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。

step1. 把原始数据分成M段,每段都排好序,分别存入M个文件中。
step2. 从M个临时文件读出头条记录,进行M路归并排序,最小的放到输出文件,同时删除对应的临时文件中的记录。
这个过程不就是MapReduce吗,step1就是Map过程,step2是Reduce过程,大量数据排序用MapReduce来做正好啊!

内部排序

排序稳定:如果两个数相同,对他们进行排序后,他们的相对顺序不变。
原地排序:不占用额外内存或占用常数的内存,就是在原来的数据中比较和交换的排序。

非基于比较的排序

基于比较的排序算法是不能突破O(NlogN)的。简单证明如下:
N个数有N!个可能的排列情况,也就是说基于比较的排序算法的判定树有N!个叶子结点,比较次数至少为log(N!)=O(NlogN)(斯特林公式)。
而非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。

1 计数排序

特性:stable sort、out-place sort
最坏情况运行时间:O(n+k)
最好情况运行时间:O(n+k)

计数排序的基本思想是对每一个输入元素x,确定出不大于x的元素个数,有了这一信息,就可以把x直接放在它最终的位置上,例如,如果有17个元素不大于x,则x就应放在第18个输出位置上。
当输入的元素是n个0到k-1之间的整数时,计数排序的运行时间是O(n+k)。由于用来计数的数组c的长度取决于待排序数组中数据的范围,这使得计数排序对于数据范围很大的数组,需要大量内存,而且 n << k 时,也很不划算。例如,计数排序是用来排序0到100之间的数字的非常好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

算法的步骤如下:
1.找出待排序的数组中最大和最小的元素
2.统计待排序数组中每个值为i的元素出现的次数,存入数组c的第i项
3.对所有的计数累加,获得不大于元素i的元素的个数(从c中的第一个元素开始,每一项和前一项相加)
4.反向填充目标数组,将每个元素i放在新数组的第c(i)项,每放一个元素就将c(i)减去1

当k不是很大时,这是一个很有效的线性排序算法。更重要的是,它是一种稳定排序算法,这是计数排序很重要的一个性质,就是根据这个性质,我们才能把它应用到基数排序。

#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;

/** input为输入数组, n表示input的大小,
    output为输出数组, k表示有所输入数字都介于[0,k-1]之间
*/
void counting_sort(int *input, int n, int *output, int k)
{
    int *c = new int[k];//临时存储区
    memset(c, 0, k*sizeof(int));//置0
    // 下面的操作完成后, c[i]中存放了input中值为i的元素的个数
    for (int i=0; i<n; ++i) ++c[input[i]];
    // 下面的操作完成后, c[i]中存放了input中值不大于i的元素的个数
    for (int i=1; i<k; ++i) c[i]+=c[i-1];
    // 把input中的元素放在output中适当的位置上
    // 逻辑是: 如果不大于m的元素个数有5个, 那m就应该被放在第5的位置上
    // 从后往前遍历input, 以保证排序稳定
    for (int i=n-1; i>=0; --i)
    {
        output[c[input[i]]-1] = input[i];
        //下面的操作使得input中下一个值为input[i]的元素被放置在output中input[i]的前一个位置, 保证排序稳定
        --c[input[i]];
    }
    delete[] c;
}

int main(int argc, char *argv[])
{
    const int LEN = 120;
    const int MAX = 90;
    const int TMAX = MAX + 1;

    int a[LEN];
    int b[LEN];

    for (int i=0; i<LEN; ++i) a[i] = rand() % TMAX;
    for (int i=0; i<LEN; ++i) cout << a[i] << " ";
    counting_sort(a, LEN, b, TMAX);
    cout << endl;
    for (int i=0; i<LEN; ++i) cout << b[i] << " ";

    return 0;
}

2 基数排序

假定每位的排序是计数排序。
特性:stable sort、Out-place sort
最坏情况运行时间:O((n+k)d)
最好情况运行时间:O((n+k)d)
当d为常数、k=O(n)时,效率为O(n)

基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

我们也不一定要一位一位排序,我们可以多位多位排序,比如一共10位,我们可以先对低5位排序,再对高5位排序。

基数排序的例子:
基数排序
先低位后高位的原理在于:如果高位相等,低位的顺序就是整体的顺序,如果高位不等,高位的排序会修正低位的错序。

3 桶排序

这篇关于桶排序的文章讲的不错。
我理解桶排序其实是分治的思想,将数据分散到若干个桶里,所有桶的数据范围不相交,对每个桶分别进行排序,最后组合在一起。
它和归并排序的区别在于,每个桶的范围不交叉,最后组合数据的时候,归并排序需要用败者树或堆来进行归并,而桶排序直接合并即可。

基于比较的排序算法

根据排序过程中依据的原则,基于比较的内排序大致可以分为插入类排序、交换类排序、选择类排序、分配类排序和归并排序等。

  • 插入类排序:直接插入排序、折半插入排序、二路插入排序、希尔排序
  • 交换类排序:冒泡排序、快速排序
  • 选择类排序:简单选择排序、树形选择排序、堆排序
  • 分配类排序
  • 归并排序

1 直接插入排序

每次从无序表中取出第一个元素,把它插入到有序表中的合适位置,使有序表仍然有序。
适合于记录基本有序且记录数不是很多的情形。

//直接插入排序, n为数组a的元素个数
void insert_sort(int *a, int n)
{
    int picket;
    int i,j;
    for (i=1; i<n; ++i)
    {
        picket = a[i]; //将待排序记录暂存入监视哨
        for (j=i-1; j>=0&&a[j]>picket; --j)
            a[j+1] = a[j]; //比待排序记录大的记录后移
        a[j+1] = picket; //将待排序记录插入到正确位置
    }
}

2 折半插入排序

直接插入排序在查找待排序记录正确位置时使用的是顺序查找。折半查找的性能要比顺序查找好得多,所以在有序序列中查找待插入记录的位置时可以使用折半查找法。

//折半插入排序
void half_sort(int *a, int n)
{
    int picket;
    int i,j,low,high,m;
    for (i=1; i<n; ++i)
    {
        picket = a[i]; //将待排序记录暂存入监视哨
        low = 0;
        high = i-1;
        while (low <= high) //折半查找插入位置
        {
            m = (low+high) / 2;
            if (picket<a[m]) high = m-1; //插入点在低半区
            else low = m+1; //插入点在高半区
        }
        for (j=i-1; j>high; --j)
            a[j+1] = a[j]; //插入点及其后的记录顺序后移
        a[j+1] = picket; //待排序的记录存入插入点
    }
}

注:这段折半插入排序写的有问题,没有提前判断是不是有序。

3 冒泡排序

重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来,每走访一次,较大的元素就浮到数列的顶端。

//简单冒泡排序
void bubble(int *a, int n)
{
    int i,j,t;
    for (i=0; i<n-1; ++i) //比较的趟数
    {
        for (j=1; j<n-i; ++j) //每趟比较的次数
            if (a[j-1] > a[j]) //相邻记录进行比较
            {
                t = a[j-1];
                a[j-1] = a[j];
                a[j] = t;
            }
    }
}
//若初始序列已有序,则没有必要进行剩下的n-1趟扫描
void bubble2(int *a, int n)
{
    int i,j,t;
    int flag=1; //flag为数组是否正序的标志
    //flag=0时意味着未排序区已经有序,提前结束循环
    for (i=0; flag&&i<n-1; ++i)
    {
        for (flag=0,j=1; j<n-i; ++j)
            if (a[j-1] > a[j])
            {
                t = a[j-1];
                a[j-1] = a[j];
                a[j] = t;
                flag = 1;//有交换意味着未排序区无序,需要进行下趟排序
            }
    }
}

4 快速排序

快速排序是对冒泡排序的改进,是英国牛津大学计算机科学家查尔斯•霍尔于1962年提出的一种划分交换排序,因此又称霍尔排序。
快速排序是一种不稳定的排序方法,适用于待排序记录个数很大且原始记录随机排列的情况,其平均性能是迄今为止所有内排序算法中最好的一种。快速排序应用广泛,典型的应用是C标准库函数qsort函数。

// 数组a的区间[low,high]
void q(int *a, int low, int high)
{
    int i = low;
    int j = high;
    int t = a[i];
    if(i>=j) return;
    while(i<j)
    {
        while(i<j && a[j]>=t) --j;
        a[i] = a[j];
        while(i<j && a[i]<=t) ++i;
        a[j] = a[i];
    }
    a[i] = t;
    q(a, low, i-1);
    q(a, i+1, high);
}

void q(int *nums, int n)
{
    if(n<=1) return;
    int i=0,j=n-1,t=*nums;
    while(i<j)
    {
        while(i<j && nums[j]>=t) --j;
        nums[i]=nums[j];
        while(i<j && nums[i]<=t) ++i;
        nums[j]=nums[i];
    }
    nums[i]=t;
    q(nums, i);
    q(nums+i+1, n-i-1);
}

快排空间复杂度是O(logn),但最坏情况下需要线性空间,可通过划分元素三者取中来避免最坏情况。
快速排序 快速搞定

5 简单选择排序

重复地走访要排序的数列,每走访一次,获得一个较小的数字放在数列首。

//简单选择排序
void select(int *a, int n)
{
    int i,j;
    int t;
    for (i=0; i<n-1; ++i) //比较的趟数
        for (j=i+1; j<n; ++j) //每趟比较的次数
            if (a[i] > a[j])
            {
                t = a[j];
                a[j] = a[i];
                a[i] = t;
            }
}

为避免过多的交换记录, 可以设一指针指示最小值, 再将该记录交换到指定位置, 改进如下:

void select(int *a, int n)
{
    int i,j,t;
    int k; //标记最小值
    for (i=0; i<n-1; ++i)
    {
        k = i; 
        for (j=i+1; j<n; ++j)
            if (a[k] > a[j])
                k = j;
        t = a[i];
        a[i] = a[k];
        a[k] = t;
    }
}

6 堆排序

堆与堆排序

7 归并排序

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

//将有序序列a,b合并为有序序列c
void merge(int *a, int m, int *b, int n, int *c)
{
    int i=0,j=0,k=0;
    while (i<m && j<n)
        if (a[i] < b[j]) c[k++] = a[i++];
        else c[k++] = b[j++];
    while (i<m) c[k++] = a[i++];
    while (j<n) c[k++] = b[j++];
}

这个函数将有序序列a,b归并为c,很明显,序列c不能与a或b发生重叠,否则可能会发生错误。二路归并排序算法要求对同一序列的两部分进行归并,而且结果还是存储到原序列中。二路归并排序算法如下。

void merge(int* a, int m, int* b, int n, int* c)
{
    int i=0,j=0,k=0;
    while (i<m && j<n)
        if (a[i] < b[j]) c[k++] = a[i++];
        else c[k++] = b[j++];
    while (i<m) c[k++] = a[i++];
    while (j<n) c[k++] = b[j++];
}

//将序列a的有序区间[low,m]和[m+1,high]归并到区间[low,high]
void merge1(int *a, int low, int m, int high)
{
    int *t = (int*)malloc((high-low+1)*sizeof(int));
    merge(a+low, m-low+1, a+m+1, high-m, t);
    memcpy(a+low, t, (high-low+1)*sizeof(int));
    free(t);
}
void msort(int *a, int low, int high)
{
    //low<high时继续二分,low等于high时直接返回
    if(low < high)
    {
        int m = (low+high) / 2;
        msort(a, low, m);
        msort(a, m+1, high);
        merge1(a, low, m, high);
    }
}
//二路归并排序
void merge_sort(int *a, int n)
{
    msort(a, 0, n-1);
}

几种排序方法的性能

性能

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值