排序总结

 

一 排序的基本概念与分类

 

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等十大排序算法。用一张图概括:

对前7中排序算法而言,只看平均时间复杂度,归并/快速/堆排序要胜过希尔排序,远胜于冒泡,选择和插入排序。

从最好的情况下,冒泡和直接插入排序更快,也就是说,如果待排序序列总是基本有序,反而不用考虑改进的方法。

从最坏情况下,归并和堆排序又好过快速排序和其他简单排序。

堆排序和归并排序发挥较为稳定,而快速排序则不然,最坏情况下差强人意。

从空间复杂度上来看,归并排序强调马跑的快,就要给马喂得饱。快速排序也有相应的空间要求,反而堆排序占用空间比较少。如果在意所占用的空间,那么归并排序和快速排序就不是一个好的方法。

从稳定性上,归并排序独占鳌头。

从待排序记录的个数上来说,待排序的个数越少,采用简单排序越合适,反之,n越大,采用改进排序越合适。

名词解释

表中的n表示数据规模,

k表示桶的个数 

In-place表示占用常数内存不占用额外内存。

Out-place表示占用额外内存。

稳定性表示2个相同键值的记录在排序前和排序后的顺序一致。

 

 

时间复杂度

1 平方阶排序(各类简单排序):冒泡排序,选择排序,插入排序。

2 线性对数阶排序O(nlogn):希尔排序,归并排序,快速排序,堆排序。

3 线性阶排序O(n):计数/桶排序。

 

二 排序算法

1 冒泡排序

冒泡排序是一种交换排序,基本思想是:两两比较相邻记录的关键字,如果反序就交换,直到没有反序的记录为止。

初级版

#include <cstdio>
#define LEN 10

int a[LEN]={5,7,3,9,0,1,4,6,8,2};

void swap_ele(int *a,int *b)
{
    int temp=*a;
    *a=*b;
    *b=temp;
}

void bubblesort(int a[])
{
    int i,j;
    for(i=0;i<LEN-1;i++)
    {
        for(j=i+1;j<LEN;j++)
        {
            if(a[i]>a[j])
                swap_ele(&a[i],&a[j]);
        }
    }
}

int main()
{
    bubblesort(a);
    for(int i=0;i<LEN;i++)
    {
        printf("%d ",a[i]);
    }
    return 0;
}

这段代码其实不是严格意义上的冒泡排序,因为不满足22交换,它是让每一个关键字都和它后面的每一个关键字比较,如果反序就交换。同时每一次排序后都对其他的排序没有帮助。

正宗版

void bubblesort(int a[])
{
    int i,j;
    for(i=0;i<LEN-1;i++)
    {
        for(j=LEN-2;j>=i;j--)
        {
            if(a[j]>a[j+1])
            {
                swap_ele(&a[j],&a[j+1]);
            }
        }
    }
}
void bubblesort(int a[],int num)
{
    for(int i=0;i<num-1;i++)
    {
        for(int j=0;j<num-i-1;j++)
        {
            if(a[j]>a[j+1])
                swap(a[j],a[j+1]);
        }
    }
    return ;
}

这样每次都是22比较,每次都把最小的放到前方/最大的放在后面。而且每一次排序都会对之后的排序有所帮助。

改进版

如果待排序的数据本来就已经基本有序,那么其实前几次就可以排序完成,但是还是会执行后面的比较操作,其实这是没有必要的。我们可以设置一个标志,如果该趟排序没有交换元素,那么就说明数据已经基本有序,就不用再排了。

void bubblesort(int a[])
{
    int i,j;
    bool flag=true;
    for(i=0;i<LEN-1&&flag;i++)   //如果上一次交换了,就再继续。
    {
        flag=false;
        for(j=LEN-2;j>=i;j--)
        {
            if(a[j]>a[j+1])
            {
                swap_ele(&a[j],&a[j+1]);
                flag=true;
            }
        }
    }
}

复杂度分析

最好的情况是,数据有序,我们进行n-1次的比较,没有数据交换。复杂度位O(n)。

最差的情况是,数据逆序,需要比较1+2+3+4+5+......+(n-1)=n(n-1)/2次。并做等量级的数据交换。复杂度为O(n^2)。

因此总的时间复杂度为O(n^2)

2 选择排序

冒泡排序的思想就是不断交换,通过交换完成最终的排序。

而选择排序则是每一趟在n-i+1(i=1,2,3...n-1)个记录中选取关键字最小的记录作为有序序列的第i记录。

步骤如下:

(1)在未排序序列中找到最小元素,存放在序列的起始位置。

(2)从剩余未排序序列中继续寻找最小元素,然后放到以排序序列末尾。

(3)重复第二步骤,直至所有元素都排列完全。

void selectsort(int a[])
{
    int i,j,min_;
    for(int i=0;i<LEN-1;i++)
    {
        min_=i;    //用当前未排序序列的首个元素下标初始化最小值的下标。
        for(j=i+1;j<LEN;j++)
        {
            if(a[min_]>a[j])
                min_=j;
        }
        if(min_!=i)
        swap_ele(&a[min_],&a[i]);
    }

}

复杂度分析

从思路来看,其最大的特点就是交换移动数据次数相当少,节约了相应时间。但无论好坏,都需要比较n(n-1)/2次。

对于交换而言,最好的时候,交换0次,最差的时候,交换n-1次。时间复杂度总是O(n^2)。在性能上稍优于冒泡排序。

3 插入排序

直接插入排序的基本操作就是将一个记录插入到已经排序的有序表中,从而得到一个新的,记录增1的有序表。

步骤如下:

(1)将第一个待排序序列第一个元素看作一个有序序列,把第二个元素到最后一个元素看作是未排序序列。

(2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入到有序序列的适当位置。

 

void insertsort(int a[])
{
    int i,j;
    for(i=0;i<LEN-1;i++)
    {
        for(j=i+1;j>0;j--)
        {
            if(a[j]<a[j-1])
                swap_ele(&a[j],&a[j-1]);
            else
                break;
        }

    }
}

复杂度分析

最好的情况是本身有序,只比较不交换,时间复杂度为O(n)

最坏的情况是逆序。移动和交换次数都是n(n-1)/2  因此根据概率原则,平均比较次数和交换次数约为n^2/4.

同样的O(n^2)时间复杂度,插入排序比冒泡和选择排序性能好点。

4 希尔排序

之前的三种排序方式(冒泡排序,选择排序,直接插入排序)时间复杂度都是O(n^2),希尔排序首次改进了排序的时间复杂度,改进为O(nlogn)

上一个排序方式直接插入排序,其效率在某些时候是很高的。

当(1)记录本身是基本有序的,我们只需要少量的插入操作,便可以完成整个记录的排序操作,此时很高效。

(2)记录数比较少时,也很高效。

可问题在于,2个条件本身就很苛刻,现实中记录少或者基本有序都属于特殊情况。

但是,可以创造记录少且基本有序的条件:

把大量待排序数据进行分组,分割成若干子序列,此时每个子序列中数据个数就比较少了。然后再这些子序列内进行直接插入排序,当整个序列都基本有序时,再对全体记录进行一次直接插入排序操作。

所谓基本有序:小的关键字基本在前,大的关键字基本在后,不大不小基本在中间。

快速理解希尔排序

 

void shellsort(int a[])
{
    int i,j,temp=0,increment=LEN;
    do
    {
       increment=increment/3+1;
       for(i=increment;i<LEN;i++)   //对每个划分进行直接插入排序
       {
           if(a[i]<a[i-increment])
           {
               temp=a[i];
               j=i-increment;
               do   //移动元素并寻找位置
               {
                   a[j+increment]=a[j];
                   j-=increment;
               }while(j>=0&&a[j]>temp);
               a[j+increment]=temp;  //插入元素
           }
       }
    }while(increment>1);
}

时间复杂度为小于O(n^2),且不是一个稳定的排序算法,因为记录跳动。是简单插入排序的一种改进。

5 堆排序

具体理解见本人博客关于堆的讲解

堆排序其实是简单选择排序的一种改进:简单选择排序在待排序的n个记录中选择一个较小的记录需要比较n-1次,但是这样的操作没有把每一趟的比较结果保存下来,在后一趟的比较中,其实很多比较在之前已经做过了。堆排序就是在选择最小或最大元素的同时,也对其他记录做了相应的调整。

算法步骤:

1 创建一个堆(大顶堆)

2 把堆首元素与堆尾互换;现在堆尾为最大元素。

3 将堆的尺寸减1,并调用向下调整,把最大元素调至堆顶。

4 重复步骤2,直至堆的尺寸为1.

void downadjust(int a[],int low,int high)
{
    int i=low;  //i为欲调整节点
    int j=(i+1)*2-1;   //j为左孩子
    while(j<=high)    //存在左孩子
    {
        if(j+1<=high&&a[j+1]>a[j])  //存在右孩子
            j=j+1;
        if(a[j]>a[i])
        {
            swap_ele(&a[j],&a[i]);
            i=j;
            j=(i+1)*2-1;
        }
        else
            break;
    }
}

void createheap(int a[])
{
    for(int i=LEN/2-1;i>=0;i--)
        downadjust(a,i,LEN-1);
}

void heapsort(int a[])
{
    createheap(a);
    for(int i=LEN-1;i>0;i--)
    {
        swap_ele(&a[0],&a[i]);
        downadjust(a,0,i-1);
    }
}

复杂度分析

由过程很容易知道堆排序的复杂度为O(nlogn)。由于堆排序对原始记录的排序状态不敏感。因此它无论好坏,平均时间复杂度都是O(nlogn)这在性能上远远好于冒泡,简单选择,直接插入了。

空间复杂度上,只用到了一个交换单元,也不错。不过由于记录的 比较与交换总是跳跃式进行,因此堆排序也是一种不稳定的排序方式。

由于初始构建堆所需的比较次数较多,因此他不适合待排序个数比较少的情况。

 

6  归并排序

归并排序就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看作是n个有序子序列,每个子序列的长度为1,然后22归并,得到【n/2】个长度为2或1的有序子序列;再归并,如此重复,直到得到一个长度为n的有序序列为止,这种方法也成为2路归并排序。

void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
    int i = startIndex, j=midIndex+1, k = startIndex;
    while(i!=midIndex+1 && j!=endIndex+1)
    {
        if(sourceArr[i] > sourceArr[j])
            tempArr[k++] = sourceArr[j++];
        else
            tempArr[k++] = sourceArr[i++];
    }
    while(i != midIndex+1)
        tempArr[k++] = sourceArr[i++];
    while(j != endIndex+1)
        tempArr[k++] = sourceArr[j++];
    for(i=startIndex; i<=endIndex; i++)
        sourceArr[i] = tempArr[i];

}

 //内部使用递归
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
    int midIndex;
    if(startIndex < endIndex)
    {
        midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
        MergeSort(sourceArr, tempArr, startIndex, midIndex);
        MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
        Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
    }
}

复杂度分析

由于合并时要进行扫描与归并,因此最终会将所有待排序记录都扫描一遍,O(n)。而由完全二叉树的深度可知,整个归并排序需要进行logn次,因此总的时间为O(nlogn),这是归并排序中最好,最坏,平均的时间性能。

由于归并排序再归并时需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为logn的栈空间,因此时间复杂度为O(n+logn)。排序中22比较,不存在跳跃,因此归并排序是一种稳定的排序算法。

归并排序是一种比较占用内存,但效率高而且稳定的排序算法。

 

 

7 快速排序

希尔排序相当于直接插入排序的升级,他们同属于插入排序类。

堆排序相当于简单选择排序的升级,他们同属于选择排序类。

快速排序相当于最慢的冒泡排序的升级,他也是通过不断比较和移动交换来实现排序的,只不过它的出现,增大了记录的比较和移动的距离,将关键字大的记录直接移到后面,关键字小的记录直接移到前面,从而减少总的移动和比较次数。

快速排序的基本思路:通过一趟排序将待排序记录分割为独立的2部分,其中一部分记录的关键字均比另一部分的记录的关键字小,可以再分别对这2部分记录进行排序,以达到整个序列有序的目的。

归并排序&快速排序

int Partition(int a[],int low,int high)    //返回枢纽位置
{
    int key=a[low];   //把第一个元素作为枢纽元素
    while(low<high)
    {
        while(low<high&&a[high]>=key) --high;     //从后面循环,若元素大于枢纽,继续循环
        //否则把低位元素赋值为小于枢纽元素的值,已经保存过低位元素,不用交换,直接替换即可
        a[low]=a[high];
        //从前面循环,若小于枢纽,继续往前走,不然把该值赋给高位元素
        while(low<high&&a[low]<=key) ++low;
        a[high]=a[low];
    }
    //当low为high时,退出循环
    a[low]=key;     //把枢纽值还给它
    return low;
}

void QSort(int a[],int low,int high)
{
    if(low<high)
    {
        int pivotkey=Partition(a,low,high);
        QSort(a,low,pivotkey-1);
        QSort(a,pivotkey+1,high);
    }
}

复杂度分析

较好的情况下,当递归树较为平衡时,时间复杂度为O(nlogn)

较差情况下,当递归树是一条斜树时,时间复杂度为O(n^2),即如果枢纽值选择的不好,每次都是把数组分为2部分,但其中的一部分元素个数是1。平均复杂度为O(nlogn)。

空间复杂度主要是递归造成的栈空间的使用。最好是logn,最差时为n。平均为logn

但由于关键字的比较和交换都是跳跃式的,也是一种不稳定的排序方式。

三种快排以及四种优化方式

三数取中和插入排序的优化代码

#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

void insert_sort(int a[],int left,int right)
{
    if(left>=right)
        return;
    for(int i=left;i<right;i++)
    {
        for(int j=i+1;j>0;j--)
        {
            if(a[j]<a[j-1])
                swap(a[j],a[j-1]);
            else
                break;
        }
    }
}

int find_pivot(int a[],int left,int right)
{
    int mid=left+(right-left)/2;
    int key=min(max(a[left],a[right]),a[mid]);   //三数取中,得枢纽值
    while(left<right)
    {
        while(a[right]>=key&&left<right)
            --right;
        while(left<right&&a[left]<=key)
            ++left;
        swap(a[left],a[right]);
    }
    a[left]=key;
    return left;
}

void quick_sort(int a[],int left,int right)
{
    if(left<right)
    {
        if(right-left+1<10)
        {
            insert_sort(a,left,right);
        }

        else
        {
            int pivotkey=find_pivot(a,left,right);
            quick_sort(a,left,pivotkey-1);
            quick_sort(a,pivotkey+1,right);
        }
    }
}

int main()
{
    int num;
    cin>>num;
    int a[num];
    for(int i=0;i<num;i++)
    {
        cin>>a[i];
    }
    quick_sort(a,0,num-1);
    for(int i=0;i<num;i++)
    {
        cout<<a[i]<<endl;
    }
    return 0;
}

优化也可以采用把数据分为n组,取出每组的中位数,得到n个中位数,再对n个中位数取中位数的方法取得枢纽值。

8 计数排序

计数排序是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)

1.计数排序是一种非常快捷的稳定性强的排序方法,时间复杂度O(n+k),其中n为要排序的数的个数,k为要排序的数的最大值。计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。计数排序是消耗空间复杂度来获取快捷的排序方法,其空间复杂度为O(K)同理K为要排序的最大值。

2.计数排序的基本思想为一组数在排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置。例如要排序的数为 7 4 2 1 5 3 1 5;则比7小的有7个数,所有7应该在排序好的数列的第八位,同理3在第四位,对于重复的数字,1在1位和2位(暂且认为第一个1比第二个1小),5和1一样位于6位和7位。

3.计数排序的实现办法

  首先需要三个数组,第一个数组A记录要排序的大小为n的数列,第二个数组B要记录每个数字出现的频数,所以第二个数组的大小应当为K(数列中最大数的大小),第三个数组C为记录排序好了的数列的数组,大小应当为n。

  接着需要确定数组最大值以确定B数组的大小。并对每个数由小到大的记录数列中每个数的出现次数。因为是有小到大,可以通过前面的所有数的出现次数来确定比这个数小的数的个数,从而确定其位置。

  对于重复的数,每排好一个数则对其位置数进行减减操作,以此对完成其余相同的数字进行排位。

void countsort(int a[])
{
    int *c=new int[LEN];
    int maxn=0;
    for(int i=0;i<LEN;i++)   //确定最大值
    {
        if(maxn<a[i])
            maxn=a[i];
    }
    int *b=new int[LEN+1];   //要比最大值多一,暂时先存放下标对应的值出现的频次

    //初始化为0
    memset(c,0,LEN*sizeof(int));
    memset(b,0,(LEN+1)*sizeof(int));

    //记录频次
    for(int i=0;i<LEN;i++)
    {
       b[a[i]]++;
    }

    //这次b[i]存储的是数字i+1在最终排序数组中的下标/索引
    for(int i=1;i<=maxn;i++)
    {
        b[i]=b[i]+b[i-1];
    }

    //根据b[i]数组的值,开始进行排序
    for(int i=0;i<LEN;i++)
    {
        b[a[i]]--;   //当前元素在最终数组中的索引
        c[b[a[i]]]=a[i];
    }

    //把数组C中的数据再赋给数组a;
    for(int i=0;i<LEN;i++)
    {
        a[i]=c[i];
    }

    delete [] c;
    delete [] b;

}

9 桶排序

桶排序的思想:

  • 根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;
  • 将落在特定范围内的所有元素放入对应的桶中;
  • 对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;
  • 按照划分的范围顺序,将桶中的元素依次取出。排序完成。

举个例子,假如被排序的元素在0~99之间,我们可以建立10个桶,每个桶按范围顺序依次是[0, 10)、[10, 20]......[90, 99),注意是左闭右开区间。对于待排序数组[0, 3, 2, 80, 70, 75, 72, 88],[0, 3, 2]会被放到[0, 10)这个桶中,[70 ,75, 72]会被放到[70, 80)这个桶中,[80, 88]会被放到[80, 90)这个桶中,对这三个桶中的元素分别排序。得到

  • [0, 10)桶中的元素: [0, 2, 3]
  • [70, 80)桶中的元素: [70, 72, 75]
  • [80, 90)桶中的元素: [80, 88]

依次取出三个桶中元素,得到序列[0, 2, 3, 70, 72, 75, 80, 88]已经排序完成。

可以用一个数组bucket[]存放各个桶,每个桶用链表表示,用于存放处于同一范围内的元素。上面建立桶的方法是根据输入范围为0~99,建立了10个桶,每个桶可以装入10个元素,这将元素分布得很均匀,在桶排序中保证元素均匀分布到各个桶尤为关键。举个反例,有数组[0, 9, 4, 5, 8, 7, 6, 3, 2, 1]要排序,它们都是10以下的数,如果还按照上面的范围[0, 10)建立桶,全部的元素将进入同一个桶中,此时桶排序就失去了意义。实际情况我们很可能事先就不知道输入数据是什么,为了保证元素均匀分不到各个桶中,需要建立多少个桶,每个桶的范围是多少呢?为此指定一个简单通用的规则:

假设待排序数组为arr,长度为arr.length;任意元素用value表示,其中的最大元素为maxValue

  • 建立的桶个数与待排序数组个数相同,这个简单的数字虽然大多数情况下会浪费许多空间(很多桶是空的),但也正因为桶的数量多,也很好地避免了大量元素都装入同一个桶中的情况。
  • 对于待排序数组中每个元素,使用如下映射函数将每个元素放到合适的桶中。这相当于每个桶能装的元素个数为
    (maxvalue+1)/arr.length
    下式中maxValue加1是为了保证最大元素可以存到数组最后一个位置,即arr.length - 1处。
    bucketindex=(value*arr.length)/(maxvalue+1);

    要注意:如何选择桶的个数,以及使用哪个映射函数将元素转换成桶的索引都是不一定的。上面的规则只是一种简单易懂的方法而已。

int compare(const void *a, const void *b)
{
    int *pa = (int*)a;
    int *pb = (int*)b;
    return (*pa )- (*pb);  //从小到大排序
}

void bucketsort(int a[])
{
    int maxn=0;
    int j=0;

    //找寻最大值
    for(int i=0;i<LEN;i++)
    {
        if(a[i]>maxn)
            maxn=a[i];
    }

    int bucket_number=LEN;  //桶的个数与待排序数组中元素个数相同
    int data_number=(maxn+1)/LEN;    //每个桶中数据的个数

    int *b[LEN];   //指针数组,数组元素全为指针,每个元素代表一个指向桶的指针

    for(int i=0;i<LEN;i++)
    {
        b[i]=NULL;    //初始化为NULL
    }

    //对有元素的桶开始创建,并把元素放入桶中
    for(int i=0;i<LEN;i++)
    {
        int bucketindex=(a[i]*LEN)/(maxn+1);
        if(b[bucketindex]==NULL)
        {
            b[bucketindex]=new int[data_number+1];    //数组的b[bucketindex][0]存放数组中数据的个数,即当前桶中数据的个数
            memset(b[bucketindex],0,(data_number+1)*sizeof(int));
        }
        b[bucketindex][b[bucketindex][0]+1]=a[i];
        b[bucketindex][0]++;   //桶中数据的个数增1
    }

    //对桶元素进行排序
    for(int i=0;i<LEN;i++)
    {
        if(b[i]!=NULL)
        {
            qsort(b[i]+sizeof(int),b[i][0],sizeof(int),compare);   //注意排序从下标1开始,因此地址为b[i]+sizeof(int)
        }
        for(int k=1;k<=b[i][0];k++)
        {
            a[j++]=b[i][k];
        }
    }

    for(int i=0;i<LEN;i++)
    {
        if(b[i]!=NULL)
        {
            delete [] b[i];
        }
    }

}

上述桶排序中桶内排序算法是C++中的快速排序算法,已经集成到了头文件<cstdlib>中,当然也可以使用别的排序算法。

桶排序可以是稳定的。这取决于我们对每个桶中的元素采取何种排序方法,比如桶内元素的排序使用快速排序,那么桶排序就是不稳定的;如果使用的是插入排序,桶排序就是稳定的。

桶排序也不能很好地应对元素值跨度很大的数组。比如[3, 2, 1, 0 ,4, 8, 6, 999],按照上面的映射规则,999会放入一个桶中,剩下所有元素都放入同一个桶中,在各个桶中元素分布极不均匀,这就失去了桶排序的意义。

桶排序和计数排序有个共同的缺点:耗费大量空间。

再细看桶排序,其实计数排序可以看作是桶排序的一种特例,计数排序相当于将所有相同的元素放入同一个桶中,而桶排序可以将一定范围内的元素都放入同一个桶中;另外,桶排序的数据结构很像基于拉链法的散列表,只是定义的映射函数不同。桶排序的映射函数将较大值映射成较大的索引,这两者是呈正相关的。而散列表的映射函数得到的哈希值是随意的。

桶排序的时间复杂度为O(N)

10 基数排序

常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位0~9数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,每一轮排序都基于上轮排序后的结果;最后一轮,最左边那位也作为关键字并排序,整个数组就达到有序状态。比如对于数字2985,从右往左就是先以个位为关键字进行排序,然后是十位、百位、千位,总共需要四轮。一定要注意每一轮排序中排序方法必须是稳定的。否则基数排序不能得到正确的结果。

举个简单例子,对于数组a[] = {45, 44, 37, 28},先以个位为关键字对数组进行稳定的排序。得到[44, 45, 37, 28],只看个位是有序的;再对十位进行稳定的排序,得到[28, 37, 44 ,45],此时所有位都已作为关键字排序过一次,排序完成。再顺便说说为什么要求每轮排序是稳定的:假设每轮排序不是稳定的,对个位排序还是[44, 45, 37, 28],对十位排序时,44、45十位相同,不稳定的排序有可能改变它们原来的相对位置,排序后可能就变成了[28, 37, 45, 44],这样的结果显然不是我们期望的。

现在来说说基数是什么意思,对于十进制整数,每一位都只可能是0~9中的某一个,总共10种可能。那10就是它的基,同理二进制数字的基为2;对于字符串,如果它使用的是8位的扩展ASCII字符集,那么它的基就是256。既然我们知道每一位的数值范围。那么使用计数排序以关键字对数组进行排序就是个十分明智的选择,原因如下

  • 对于元素的每一位(关键字),计数排序都可以统计其频率,然后直接将整个元素按照该关键字进行分类、排序,实现起来简单。(想想插入排序、归并排序等稳定排序算法要如何按照某一位来将整个元素排序,是不是更复杂?)
  • 因为数据范围确定且都不大(基的大小),因此不会占用多少空间;
  • 而且计数排序不是基于比较,比通常的比较排序方法效率更高;
  • 计数排序是稳定排序,这一点至关重要。

基数排序也适用于字符串,若字符串使用的是8位的ASCII扩展字符集,则基的大小是256。下一篇文章将看到,只需对上面的代码稍作修改,就能实现对字符串的排序,基于基数排序的字符串排序方法称为低位优先的字符串排序(LSD).

基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

int digitat(int data,int d)   //得到数据data的从右往左数第d+1位的数字
{
    return (data/int(pow(10,d)))%10;
}

int count_digit(int data)   //计算数据data的位数
{
    int a=1;
    while(data/10)
    {
        data/=10;
        a++;
    }
    return a;
}

void car_num_sort(int a[])
{
    int R=10;  //基数从0-9共10个
    int *temp=new int[LEN];    //临时数组
    int *count_num=new int[R+1];    //count_num[0]始终为0,count_num[2]表示基数为1出现的频率

    //初始都设置为0
    memset(temp,0,LEN*sizeof(int));
    memset(count_num,0,sizeof(int)*(R+1));

    //以关键字来排序的轮数,由位数最高的数字决定,其余位数少的,在比较高位的时候,自动用0进行比较
    int num=0;   //排序的轮数

    //更新排序的轮数
    for(int i=0;i<LEN;i++)
    {
        int b=count_digit(a[i]);
        if(b>num)
            num=b;
    }

    //共计需要num轮计数排序,从num=0开始,说明是从个位数比较
    for(int d=0;d<num;d++)
    {
        // 1.计算频数,在需要的长度上加1,count_num[i]表示基数i-1出现的频次
        for(int i=0;i<LEN;i++)
        {
            count_num[digitat(a[i],d)+1]++;
        }

        //2.频率转换为元素的开始索引.执行完下面的循环之后,count_num[i]就表示基数i的开始下标
        for(int r=0;r<R;r++)
        {
            count_num[r+1]+=count_num[r];
        }

        //3.元素按照开始索引分类,用到一个和待排序数组一样大的临时数组存放数据
        for(int i=0;i<LEN;i++)
        {
            temp[count_num[digitat(a[i],d)]++]=a[i];  //加1是为了考虑重复的基数,往后顺延
        }

        //4.数据回写
        for(int i=0;i<LEN;i++)
        {
            a[i]=temp[i];
        }

        //5.重置count_num,以便下一轮使用
        for(int i=0;i<=R;i++)
        {
            count_num[i]=0;
        }
    }
}

对于a[10]={45,21,9,456,34,77,80,5,888,1001}

最后三种排序方式参照博客

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值