(初学者!超详细!一篇文章学习9种排序算法)几种内排序算法的比较、总结以及时间复杂度的分析

几种内排序算法的比较和总结

首先我们将各种排序方法进行一个大致的分类

一、插入排序

1、直接插入排序

2、折半插入排序

3、希尔排序

二、交换排序

1、冒泡排序

2、快速排序

三、选择排序

1、简单选择排序

2、堆排序

四、归并排序

五、基数排序

总结

一些结构体变量的申明如下:

typedef struct entry
{
    KeyType key;
    DataType data;
}Entry;

typedef struct list
{
    int n;
    Entry D[MaxSize];
}List;

一、插入排序

我们先来一起康康直接插入排序八!

1、直接插入排序

应该说直接插入排序是相对简单的一种排序方法。
大体上,我们把整个表分成两个区域:有序区和无序区。初始状态有序区是空的,我们把无序区中的第一个元素加入到有序区中。
在第一层循环中,我们从i=1即表中第二个元素开始,依次将无序区中第i个元素先存到一个临时的结构体变量insertItem中,接着进入第二层循环,在有序区中从j=i-1向开始逆序遍历,同时与需要插入的元素进行比较,如果insertItem中的元素即我们将要新插入的元素的关键字值更小,那么我们将有序区中第j个元素向后挪一个位置到j+1,直到我们在有序区中找到第一个比新元素小的元素,将新元素插入到其后,从而完成一次插入操作。这个过程中,有序区在不断扩大,且是一个递增的区域,而无序区在不断缩小。最终,整个无序表就成为了一个有序表。
举个简单的栗子:无序集合{7,3,16,5},首先我们取下标i=1即第二元素3存入临时变量insertIterm中,我们将3与i-1=0到集合首部的元素依次比较,发现7比3小,于是7向后挪,集合变成{7,7,16,5},这时发现已经到集合首部,也就是说3就是最小的元素,用3覆盖第一个元素7即可,集合变为{3,7,16,5},第一次循环结束。第二次循环,将16存入insertIterm,与16之前的元素比较,发现16大于7,那么直接退出了本层循环进入到下一次循环。将5存入insertItem,5比16小,16替代5所在的位置向后挪一位,集合变为{3,7,16,16},接着5比7小,7代替16所在位置向后挪一位,集合变为{3,7,7,16},最后,5比3大,那5就插入在3后面代替前一个7所在的位置,集合变成{3,5,7,16},排序完成。
代码如下:

//直接插入排序
void InsertSort(List *list){
    int i,j;                                 //i标识待插入元素下标
    for(i = 1;i < list->n;i ++){
        Entry insertItem = list -> D[i];     //标记每次第一位元素
        for(j = i-1;j >= 0;j --){
            //不断将有序序列中元素向后移动,为待插入元素空出一个位置
            if(insertItem.key < list->D[j].key){
                list->D[j+1] = list->D[j];
            }
            else break;
        }
        list->D[j+1] = insertItem;          //待插入元素有序存放至有序序列中
    }
}

我们来分析一下直接插入排序算法的时间复杂度。
最好情况:需要排序的表已经有序,那么每次仅需比较1次,不需要移动元素,那么总的比较次数就是n-1,总的移动次数为0。时间复杂度O(n)。
最坏情况:表中元素原来是降序排列的,那么每次我们都需要将第i个元素与其之前所有元素比较直到表的首部,也就是说每次比较的次数就是i,相应地,每次需要移动元素的次数就是i+2次,总的比较次数就是∑(1,n-1)i=n(n-1)/2,总的移动次数就是∑(1,n-1)(i+2)=(n-1)(n+4)/2。时间复杂度O(n²)。
最终总的平均比较和移动次数为:∑(1,n-1)( i/2 + i/2+2)=∑(1,n-1)(i+2)=(n-1)(n+4)/2,时间复杂度即O(n²)。

2、二分(折半)插入排序

从直接插入排序中,我们发现有序区中的元素是有序的,那么对有序区中元素查找,可以采用二分查找的方式,提高查找效率。
与直接插入唯一的区别就是查找插入的位置使用的是二分查找的方法。

//二分插入排序
void BinInsertSort(List *list){
    int i,j,low,high,mid;
    for(i = 1;i < list->n;i ++){
        if(list->D[i].key<list->D[i-1].key) //反序时
        {
        Entry insertItem = list -> D[i];    //保存
        low=0;high=i-1;
        while(low<=high)                    //在[low,high]中查找
        {
            mid=(low+high)/2;               //更新中间位置
            if(insertItem.key<list->D[mid].key)
                high=mid-1;                 //插入点在左半区
            else
                low=mid+1;                  //插入点在右半区
        }                                   //找位置high即第一个小于新插入元素的位置
        for(j = i-1;j >= high+1;j --)       //记录后移
            list->D[j+1]=list->D[j];
        list->D[high+1]=insertItem;         //插入insertIterm
        
    }
}
}

时间复杂度分析如下:
二分插入与直接插入唯一的区别在于查找效率得到了优化。我们知道,二分查找的平均查找次数为log2(i+1)-1,于是得到二分插入排序的平均时间复杂度为∑(1,n-1)(log2(i+1)-1+i/2+2),是**O(n²)**级的。

3、希尔排序

希尔排序是一种不稳定的排序方法,是直接插入排序的一种优化。
对于希尔排序,我们的基本思路如下:

  1. 将元素数量为n的序列分成d组,每间隔为5的元素分为一组,对各组内进行直接插入排序,其中d=n/2。
  2. 每次递减d=d/2,重复操作,直至d=1。

因为到最后一趟d=1时,即对所有元素进行直接插入排序,所以结果一定正确。

举个简单的栗子八!
若n=10,即10个元素的序列{9,8,7,6,5,4,3,2,1,0}
第一趟,d=5,9和序列中与9间隔为5的元素4分为一组,8和3一组,7和2一组,以此类推。一共5组,对每组进行小组内排序,9>4,于是9与4交换了位置,于是8与3交换了位置,以此类推,得到序列{4,3,2,1,0,9,8,7,6,5}。
第二趟,d=5/2=2,即将第一趟得到的序列分为2组,4、2、0、8、6五个数为一组,3、1、9、7、5为一组,直接插入排序后,得到新序列{0,1,2,3,4,5,6,7,8,9}。
第三趟,d=2/2=1,即对第二趟得到的新序列整个的做直接插入排序。事实上,d=1时,整个序列已经接近正序,接近或者就已经是直接插入排序的最好情况,时间复杂度是O(n),这是希尔排序更快的一个重要原因
代码如下:

void ShellSort(List *list)
{
    int i,j,d;
    Entry tmp;
    d=list->n/2;              //增量置初值
    while(d>0)
    {
        for(i=d;i<list->n;i++)//对间隔d位置的元素组进行直接插入排序操作
        {
            tmp=list->D[i];
            j=i-d;
            while(j>=0&&tmp.key<list->D[j].key)
            {
                list->D[j+d]=list->D[j];
                j=j-d;
            }
            list->D[j+d]=tmp;
        }
        d=d/2;                //更新减小分量
    }
}

因为数据的随机性,希尔排序时间复杂度的分析非常复杂,直接给出结论约为O(n1.3)。

二、交换排序

1、冒泡排序

与直接插入元素类似,我们将一个序列前后分为有序区和无序区两个部分。对于n个元素的序列,我们将进行n-1趟处理,每一趟处理都是依次把相邻的两个元素进行比较然后排序。在这个过程中,无序区中的较小的元素与其前一个相邻的元素通过一趟又一趟的交换“冒泡”到有序区中(所以我们形象地称其为“冒泡排序”),有序区不断扩大,直到排序完成。

//冒泡排序
void BubbleSort(List *list){
    int i,j;                    //i标识每趟排序范围最后一个元素下标,每趟排序元素下标范围是0~i
    for(i = list->n-1;i > 0;i --){
        int isSwap = 0;    //教材这里错了,应该放到第二层循环前
        for(j = 0;j < i;j ++){
            if(list->D[j].key > list->D[j+1].key){
                swap(list->D[j],list->D[j+1]);
                isSwap=1;
            }
        }
        if(!isSwap) break;     //如果本趟排序没有发生元素交换,排序完成
    }
}

根据以上代码举个简单的栗子:对于序列{3,1,2,4,5},第一趟,相邻的元素从前到后依次进行两两间的排序,首先下标j=0位置处3与1排序,得到{1,3,2,4,5},同时isSwap置为1,接着j=1,3与2之间排序,得到{1,2,3,4,5}。这时我们发现,事实上排序已经完成,程序没有必要继续进行下去,这就需要isSwap变量发挥作用。在第二趟循环中,在j=0到j=2的3次循环中始终没有发生元素的交换,也就是说还未操作完的“无序区”已经有序,那么在最后加上if语句特判,防止程序继续对剩下的元素继续排序。
时间复杂度分析:
最好情况:关键字在序列中正序。比较次数为n-1,移动次数为0,时间复杂度为O(n)。
最坏情况:关键字在序列中逆序。那么对于第i趟冒泡,我们需要在无序区中进行n-i-1次比较才能得出排在第i位置的元素,总的比较次数为∑(0,n-1)(n-i-1)=n(n-1)/2。同时,由于逆序关系,每次比较都需要进行一次元素的交换,且每次交换伴随着3次交换操作,故总的移动次数为∑(0,n-2)3(n-i-1)=3n(n-1)/2。时间复杂度为O(n²)。
所以冒泡排序的平均时间复杂度为O(n²)。

2、快速排序

快速排序是应用较多的一种不稳定的排序算法。我们的思路如下:对于一个无序序列,我们将其首部元素取出作为“基准”,将序列表一分为二。每一趟操作,我们先从序列的末尾向前遍历,用j来记录当前位置,将找到的第一个比基准小的元素移动到“基准”原本所在的序列的首部,接着从序列的首部开始遍历,用i来记录当前位置,找到第一个比“基准”大的元素移动到末尾,继续向前移动j,直到i=j时,将基准放置到i=j所在的位置,完成一趟操作。然后对“基准”左右的子表按递归的方式继续以同样的方式进行划分,直到划分的子表长度为0或1(递归结束)。最终“基准”左侧的所有元素均小于它,右侧元素均大于它。
举一个栗子来理解一下序列划分方法:对序列{3,1,5,4,2},取3为基准存入临时变量tmp中,接着从末尾向前遍历,j指向2(j=4),2<3,所以2移至首部,然后i从1开始向后遍历,到5>3,5到末尾,此时i=2为5的初始位置,得到表{2,1, ,4,5},j继续前移到i=2=j时,将基准3放置到i=2的位置,完成一趟操作。

//快速排序
//序列划分方法
int Partition(List *list,int low,int high){
    int i = low,j = high + 1;
    Entry pivot = list->D[low];                 //pivot是基准元素
    do{
        do i++;
        while(list->D[i].key < pivot.key);      //i向后移动
        do j--;
        while(list->D[j].key > pivot.key);      //j向前移动
        if(i < j) swap(list->D[i],list->D[j]);
    }while(i < j);
    swap(list->D[low],list->D[j]);
    return j;                                   //此时j是基准元素下标
}

//快速排序
void QuickSort(List *list,int low,int high){   //快速排序的递归函数
    int k;
    if(low < high){                            //当前待排序序列至少包含2个元素
        k = Partition(list,low,high);
        QuickSort(list,low,k-1);
        QuickSort(list,k+1,high);
    }
}
//函数重载
void QuickSort(List *list){                   //快速排序算法的主调用函数
    QuickSort(list,0,list->n-1);
}

再举个简单的栗子来理解快速排序八!
序列{6,8,7,9,0,1,3,2,4,5}
在这里插入图片描述
这样看来,这个算法的过程实际上是一个三叉递归树,每个分支非叶结点对应一次递归调用,总共调用7次。显然,优先递归左右任何一个区间都不会影响最终结果。
时间复杂度分析:
最好情况:序列首部元素即“基准”将序列划分成两个长度相等或差1的子序列,那么树的高度最低为[log2n]取上界层,对于每层子序列需要划分的时间复杂度为O(n),所以时间复杂度为O(nlog2n),空间复杂度即树的高度为O(log2n)。
最坏情况:每次递归划分出的两个子序列均由空序列和长度-1的序列组成,那么这棵递归树共有n层,每层划分时间复杂度为O(n),从而时间复杂度为O(n²),空间复杂度为O(n)。
平均情况(略微有些复杂):设含n个元素的递归树的计算次数为T(n),其两个子序列分别由n-k个元素和k-1个元素组成,则T(n)=Cn+1/n*∑(1,n)[T(k-1)+T(n-k)],其中Cn为一次划分的时间,k:1~n 共有n种情况,得到结果T(n)=Cnlog2n。由此可见,虽然快速排序的最坏情况时间复杂度是n²级别的,但是它的平均时间复杂度是接近最好情况的,这也是快速排序是比较常用的排序算法的原因。结论:快速排序的时间复杂度为O(nlog2n),平均所需栈空间为O(log2n)。

三、选择排序

1、简单选择排序

与直接插入排序类似,序列分为有序区和无序区,初始状态有序区没有元素,每次从无序区中找到最小的元素加入有序区,直到无序区元素均被取完。
思路和代码简单易懂,如下:

void SelectSort(List *list)
{
    int i,j,k;
    Entry tmp;
    for(i=0;i<list->n-1;i++)                        //做第i趟排序
    {
        k=i;
        for(j=i+1;j<list->n;j++)                     //找到i+1到末尾的最小元素
            if(list->D[j].key<list->D[k].key)
               k=j;
        if(k!=i)                               //i之后存在元素更小,那么交换位置
        {
            tmp=list->D[i];
            list->D[i]=list->D[k];
            list->D[k]=tmp;
        }
    }
}

时间复杂度分析:
比较次数:从i个元素中挑选最小元素需要i-1次比较,第i个元素到末尾的n-i个元素挑选最小元素需要n-i-1次比较,总的比较次数为∑(0,n-2)(n-i-1)=n(n-1)/2。
移动次数:正序最小为0次,反序最大为3(n-1)次。
故简单排序的最好、最坏和平均时间复杂度均为O(n²)。

2、堆排序

与简单选择排序类似,只是再选择最小元素时采用堆方法,堆排序是一种不稳定的排序算法。
由于在排序过程中需要多次选择最小元素,所以堆方法查找相比于遍历查找可以很好的提高查找效率。
堆定义:序列:k1、k2、…、kn。
任意i,ki<=k2i且ki<=k(2i+1),称为“小根堆”,
ki>=k2i且ki>=k(2i+1),称为“大根堆”。
事实上,可将堆看作是一个完全二叉树,小根堆即任意父节点小于孩子,大根堆即任意父节点大于孩子。对于大根堆,其最小元素一定是在某叶子节点中。
那么对于堆排序,最重要的就是用于调整序列成为大根堆的筛选(调整)算法。
筛选算法思路如下:对区间[low,high]筛选,从low结点开始筛选,若该结点不满足大根堆定义,则每次将其左右孩子中最大的替代根节点的位置,直到调整到序列末尾满足条件时结束。

//堆排序,选择排序,用堆选出最大值
void sift(List* list,int low,int high)//筛选或调整算法
{
    int i=low,j=2*i;                 //list.D[j]是list.D[i]的左孩子
    Entry temp=list->D[i];
    while(j<=high)
    {
        if(j<high && list->D[j].key<list->D[j+1].key) j++; //j始终指向较大的孩子
        if(temp.key<list->D[j].key)                       //双亲小
        {
            list->D[i]=list->D[j];                         //将list.D[i]调整到双亲结点位置上
            i=j;                                         //修改i和j的值,以便继续向下筛选
            j=2*i;
        }
        else break;                                      //双亲大不必调整
    }
    list->D[i]=temp;                                   //最终temp存储的小元素替代最后一个被调整元素所在的位置
}

void HeapSort(List* list,int n)
{
    int i;
    Entry temp;
    for(i=n/2;i>=1;i--)             //循环建立初始堆
        sift(list,i,n-1);
    for(i=n;i>=2;i--)               //进行n-1次循环,完成堆排序
    {
        temp=list->D[1];            //堆顶元素放置到堆底最后一个元素,相当于取堆顶元素,后续操作不予考虑
        list->D[1]=list->D[i];
        list->D[i]=temp;
        sift(list,1,i-1);           //筛选最大结点放到堆顶list.D[0],得到i-1个结点的堆
    }
}

举一个栗子!
序列{4,3,5,2,1,6},n=6,下标1~6。
在这里插入图片描述
for(i=n/2;i>=1;i–)
sift(list,i,n-1);
从n/2=3即最后一个非叶结点开始筛选,5和6先交换。
在这里插入图片描述
接着i–从其兄弟节点继续筛选,发现兄弟节点满足大根堆定义。
在这里插入图片描述
最后到根节点,3<6,将6和4交换,4又比5小,则5与4交换,调用筛选算法3次,建堆完成,得到一个首部为序列最大元素的大根堆。
排序时,我们不断将最大元素的归位到序列末尾,同时不断维护大根堆。如上图将6与4交换位置后,再对除了6以外的序列(下标1~5)进行大根堆的筛选sift(list,0,i-1)。重复操作即可。

时间复杂度分析:
对于高度为h的堆,则从最后一个非叶节点开始判断,每个非叶结点都需要与孩子结点进行比较操作,若结点的孩子依然是非叶结点,还要继续向下比较,直到最后一个非叶节点满足大根堆,这个过程中,第i层的一个元素需要比较的次数为h-i,(例如上图中,原本祖先结点4在第1层,它需要与5比较,再与6比较,共比较3-1=2次),又因为第i层共有2(i-1)个元素,所以对于第i层的一个元素总的比较次数为2(i-1) ×(h-i)次,所有元素总的比较次数S=∑(1,h)2(i-1) ×(h-i)=2h -h+1,又因为h是完全二叉树的深度,可大约认为k=log2n,于是得到S=n-log2n-1,最终我们得到比较的时间复杂度为O(n)
元素排序归位后,更新维护堆的过程中,循环n-1次,每次进行堆的调整都是log2n级,故调整的时间复杂度为O(nlog2n)
综上所述:堆排序的时间复杂度为O(nlog2n)

四、归并排序

归并排序是多次将相邻两个或两个以上的有序表合并成一个新的有序表。若每次将两个有序子表合并,则称为二路归并排序。
对于一次合并,我们的思路如下:将一个序列一分为二,定义两个指针分i,j别指向两个序列,同时定义一个临时数组,每次对两个指针指向的元素进行比较,较小的元素优先加入临时数组,同时将较小指向元素的指针后移一位。当任一子序列已完成遍历,说明另一子序列中剩余元素均较大,则将另一子序列直接拷贝到临时数组中。
二路归并排序,就是不断的进行合并操作,每一趟合并操作过后,扩大合并操作的规模,直到排序完成。
举个栗子:序列{18,2,20,34,12,32,6,16,1,5}。
第一趟:子序列长度从1开始,对于第一次合并,下标i,j分别指向18与2两个元素,将更小的2放到临时数组中,此时第二个子序列已为空,将第一个序列剩余元素复制到临时数组中,得到第一次合并的结果{2,18},类似地,剩余元素两两合并后得到{20,34}、{12,32}、{6,16}、{1,5}。
第二趟:子序列长度为2,{2,18}与{20,34}合并,2<20,2进临时数组,i后移一位,18<20,18进临时数组,此时第一个子序列为空,将第二个子序列复制到临时数组,得到{2,18,20,34},同理得到{6,12,16,32},{1,5}与自己操作未变化。
第三趟:子序列长度为4,{2,18,20,34}与{6,12,16,32}合并得到{2,6,12,16,18,20,32,34}。{1,5}不发生改变。
第四趟:子序列长度为8,特判处理一下长度为{1,5},{2,6,12,16,18,20,32,34}与{1,5}合并得到最终结果:{1,2,5,6,12,16,18,20,32,34}
代码如下:

//二路归并算法
void Merge(List *list,int low,int mid,int high)//一次二路合并,low为整个序列起点,mid是两个子序列的分界位置,high是整个序列末尾
{
    Entry *R;
    int i=low,j=mid+1,k=0;     //k是临时新数组R的下标,i、j分别为两个子序列的下标
    R=(Entry *)malloc((high-low+1)*sizeof(Entry));
    while(i<=mid&&j<=high)
        if(list->D[i].key<=list->D[j].key)  //第一个序列元素小,放入R中
           R[k++].key=list->D[i++].key;
        else
            R[k++].key=list->D[j++].key;      //第二个序列元素小,放入R中
    while(i<=mid)   //剩余部分拷贝
        R[k++].key=list->D[i++].key;
    while(j<=high)
        R[k++].key=list->D[j++].key;
    for(k=0,i=low;i<=high;k++,i++)  //将R复制回原来的list序列中
        list->D[i].key=R[k].key;
    free(R);
}


void MergePass(List *list,int length)//一趟二路归并
{
    int i;
    for(i=0;i+2*length-1<list->n;i=i+2*length)//归并长度为length的两个相邻子表
        Merge(list,i,i+length-1,i+2*length-1);
    if(i+length-1<list->n)                    //剩余两个子表中,右一个子表长度小于length
        Merge(list,i,i+length-1,list->n-1);
}


void MergeSort1(List *list)//二路归并排序算法
{
    int length;
    for(length=1;length<list->n;length=2*length)//做log2n趟,每趟都扩大子序列长度为前一次的两倍
        MergePass(list,length);
}

我们可以从更直观的角度来理解这个算法,两两合并最终得到一个总的结果,可以称它为一颗归并树,如下图:在这里插入图片描述
时间复杂度分析:对每一趟归并,我们对每个元素均进行了常数次的比较和交换,复杂度为O(n),总共需要进行[log2n]趟,因此**二路归并排序的时间复杂度为O(nlog2n)。**容易分析出空间复杂度是O(n)的。

五、基数排序

基数可以理解为进制,对于二进制,基数r=2;对十进制,基数r=10。
基数排序分为两种:最低位优先(LSD)和最高为优先(MSD)。
越重要的位越在后面排序,如对整数序列递增排序,选择最低为优先。
最低为优先排序分为分配、收集两个步骤:
对长度位d的数字或字符,我们进行d趟分配和收集操作。
分配:根据基数r,使用r个队列Q0,Q1,…,Qr-1,然后依次考察线性表中的每一个结点aj,若aj中的关键字等于k,那就把aj放进Qk队列中。
收集:按Q0,Q1,…,Qr-1顺序把各个队列中的结点首尾相连,得到新的顺序的线性表。
(由于数据需要进队、出队等大量元素移动操作,所以排序数据和队列均采用链表存储更好,因为链表存储不需要移动元素,仅需修改相应的指针域即可)
举个栗子:序列{369,367,167,239,237,138,230,139}
首先我们定义指针p指向序列首部,根据基数10,我们建立10个队列,Q0,Q1,…,Q9,f记为队首,r记为队尾。
第一趟排序,从个位开始分配:
f[0]->230 <-r[0]
f[7]->367->167->237 <-r[7]
f[8]->138 <-r[8]
f[9]->369->239->139 <-r[9]
接着我们根据从小到大顺序将以上队列收集起来:
p->230->367->167->237->138->369->239->139,得到了一个新的单链表,第一趟排序完成。
根据第一趟排序得到的新的序列进行第二趟排序,按十位分配:
f[3]->230->237->138->239->139 <-r[3]
f[6]->367->167->369 <-r[6]
收集:p->230->237->138->239->139->367->167->369,第二趟排序完成。
第三趟排序,按百位分配:
f[1]->138->139->167 <-r[1]
f[2]->230->237->239 <-r[2]
f[3]->367->369 <-r[3]
收集:p->138->139->167->230->237->239->367->369,第三趟排序完成,序列已有序。
上述过程我们发现,基数排序是根据元素的分配和收集来完成的,未进行元素关键字大小的比较。
其实由于收集过程的有序性,不难理解基数排序实现的道理。
对应的代码如下:

typedef struct node  //为基数排序定义的结构体
{
    char data[MAXD]; //记录关键字定义的字符串
    struct node *next;
    struct node *head;    //表头结点
	int n;
}RecType1;            //单链表中每个结点的类型

void RadixSort(RecType1 *p,int r,int d)//p为待排序序列链表指针,r为基数,d为关键字位数
{
    p=p->head->next;
    RecType1 *head[MAXR],*tail[MAXR],*t;//定义各链队的首尾指针
    int i,j,k;
    for(i=d-1;i>=0;i--)                    //从低位到高位做d趟排序
    {
        for(j=0;j<r;j++)                //初始化各链队首、尾指针
            head[j]=tail[j]=NULL;
        while(p!=NULL)                  //对于原链表中每个结点循环(分配)
        {
            k=p->data[i]-'0';           //找第k个链队
            if(head[k]==NULL)           //进行分配,即尾插法建立单链表
            {
                head[k]=p;
                tail[k]=p;
            }
            else
            {
                tail[k]->next=p;
                tail[k]=p;
            }
            p=p->next;                  //取下一个待排序的结点
        }
        p=NULL;
        for(j=0;j<r;j++)                //对每一个链队循环进行收集
            if(head[j]!=NULL)
            {
               if(p==NULL)
               {
                   p=head[j];
                   t=tail[j];
               }
               else
               {
                   t->next=head[j];
                   t=tail[j];
               }
            }
        t->next=NULL;                  //最后一个结点的next置为null,t相当于分配过程中的tail
    }
}

时间复杂度分析:分配的时间复杂度位O(n),收集的时间复杂度位O®,其中r为基数,设分配、收集的趟数为d,则基数排序的时间复杂度为O(d(n+r)),空间复杂度为O{r)(因为r个队列)。

最后

我们对以上排序算法做个总结。在这里插入图片描述

1、按算法平均时间复杂度分类

  1. 平方阶O(n²):简单排序方法,如直接插入、简单选择和冒泡排序。
  2. 线性对数阶O(nlog2n):如快速排序、堆排序和归并排序。
  3. 线性阶O(n):如基数排序(假设r、d为常量)。

2、按算法空间复杂度分类

  1. O(n):归并排序,基数排序。
  2. O(log2n):快速排序。
  3. O(1);其他。

3、按算法稳定性分类

  1. 不稳定:希尔排序、快速排序、堆排序、简单选择排序。
  2. 稳定的:其他。
    算法稳定性是排序时需要考虑的一个重要因素。
    如果排序时,需要比较的关键字有两个或两个以上,这里以两个为例,分别为k1,k2。
    若需要首先比较k1,小的在前,若k1相同,则比较k2大小。
    即k1更重要,重要的后排序,那么我们先根据k2进行排序,再对k1进行排序,那么对k1排序必须要选择稳定的算法,否则可能会导致事先根据k2排序的序列内部元素相对次序的改变,出现错误排序结果。

总结:对于排序算法的选择,需要综合考虑时间、空间复杂度、稳定性、问题规模、元素大小等多种因素,具体的选择还是要根据实际情况具体分析。(完~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值