(完结)王道视频-数据结构-笔记8:排序


0 笔记说明

来源于2020 王道考研 数据结构,博客内容是对自己笔记的书面整理,根据自身学习需要,我可能会增加必要内容。


1 排序的基本概念

排序是重新排列表中的元素,使表中的元素满足按关键字有序的过程。定义如下:

1、输入:n个记录R1,R2,…,Rn对应的关键字为k1,k2,…,kn

2、输出:输入序列的一个重排R’1,R’2,…,R’n,使得k’1≤k’2≤…≤k’n,其中“≤”可以换成其他的比较大小的符号)。

什么是排序算法的稳定性呢?

若待排序表中有两个元素Ri和Rj,其对应的关键字相同即keyi=keyj,且在排序前Ri在Rj的前面,若使用某一排序算法排序后,Ri仍然在Rj的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要。

在排序过程中,根据数据元素是否完全在内存中,可将排序算法分为两类:

1、内部排序,是指在排序期间元素全部存放在内存中的排序

2、外部排序,是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序

一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。当然,并非所有的内部排序算法都要基于比较操作,如基数排序就不基于比较。

每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。

可将排序算法分为:

1、插入排序;

2、交换排序;

3、选择排序;

4、归并排序;

5、基数排序。

内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。


2 插入排序

插入排序的基本思想是:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法:

1、直接插入排序;

2、折半插入排序;

3、希尔排序(我不进行整理)。

2.1 直接插入排序

直接插入排序算法假设在排序过程中,待排序表L[1…n]在某次排序过程中的某一时刻状态如下:
在这里插入图片描述
要将元素L(i)插入到已有序的子序列L[1…i-1]中,需要执行以下操作(为避免混淆,下面用L[]表示一个表,而用L()表示一个元素):

1)查找出L(i)在L[1…i-1]中的插入位置k。

2)将L[k…i-1]中的所有元素依次后移一个位置。

3)将L(i)复制到L(k)。

为了实现对L[1…n]的排序,可以将L(2)-L(n)依次插入到前面已排好序的子序列中,初始L[1]可以视为是一个已排好序的子序列。上述操作执行n-1次就能得到一个有序的表。插入排序在实现上通常采用就地排序,即空间复杂度为O(1),因而在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。

下面是直接插入排序的代码,其中再次用到了“哨兵”:

void InsertSort(ElemType A[],int n){
    int i,j;
    for(i=2;i<=n;i++) //依次将A[2]~A[n]插入到前面已排序序列
        if(A[i]<A[i-1]){ //若A[i]的关键字小于前面的关键字,则将A[i]插入到有序表
            A[0]=A[i]; //A[0]存放哨兵
            for(j=i-1;A[0]<A[j];--j) //从后往前查找待插入位置
                A[j+1]=A[j]; //向后挪1位
            A[j+1]=A[0]; //复制到待插入位置
        }
}

假定初始序列为[49,38,65,97,76,13,27,49],初始时49可以视为一个已排好序的子序列,按照上述算法进行直接插入排序的过程如图8.1所示,括号内是已排好序的子序列:
在这里插入图片描述
直接插入排序算法的性能分析如下:

1、空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。

2、时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了n-1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。

3、时间复杂度

  • 在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为O(n)。
  • 在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反,即整个待排序表是逆序的,因此总的比较次数达到最大,为:
    在这里插入图片描述
    总的移动次数也达到最大,为:
    在这里插入图片描述
  • 平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为n2/4。

因此,直接插入排序算法的时间复杂度为O(n2)。

4、稳定性:由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。

5、适用性:直接插入排序算法适用于顺序存储和链式存储的线性表。

注意:大部分排序算法都仅适用于顺序存储的线性表

2.2 折半插入排序

直接插入排序算法中,每趟插入的过程中都进行了两项工作:

1、从前面的有序子表中查找出待插入元素应该被插入的位置;

2、给插入位置腾出空间,将待插入元素复制到表中的插入位置。

在直接插入排序算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。算法代码如下:

void InsertSort(ElemType A[],int n){
    int i,j,low,high,mid;
    for(i=2;i<=n;i++){ //依次将A[2]~A[n]插入到前面已排序序列
        A[0]=A[i]; //将A[i]暂存到A[0]
        low=1;high=i-1; //设置折半查找的范围
        while(low<=high){
            mid=(low+high)/2; //取中间位置
            if(A[mid]>A[0])
                high=mid-1; //查找左半子表
            else
                low=mid+1; //查找右半子表
        }
        for(j=i-1;j>=high+1;--j)
            A[j+1]=A[j]; //统一进行后移,从而空出插入位置
        A[high+1]=A[0]; //将元素插入到对应位置
    }
}

从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,约为O(n·log2n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。

折半插入排序是一种稳定的排序方法

2.3 希尔排序

不会整理


3 交换排序

所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。

3.1 冒泡排序

冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完。这被称为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置……这样最多做n-1趟冒泡就能把所有元素排好序。

假定初始序列为[49,38,65,97,76,13,27,49],图8.3所示为冒泡排序的过程:
在这里插入图片描述
第一趟冒泡时:27<49,不交换;13<27,不交换;76>13,交换;97>13,交换;65>13,交换;38>13,交换;49>13,交换。通过第一趟冒泡后,最小元素已交换到第一个位置,也是它的最终位置。第二趟冒泡时对剩余子序列采用同样方法进行排序,以此类推,到第六趟结束后没有发生交换,说明表已有序,冒泡排序结束。

冒泡排序算法的代码如下:

void BubbleSort(ElemType A[],int n){
    for(i=0;i<n-1;i++){
        flag=false; //本趟冒泡是否发生交换的标志
        for(j=n-1;j>i;j--) //一趟冒泡过程
            if(A[j-1]>A[j]){
                swap(A[j-1],A[j]); //若为逆序则交换
                flag=true;
            }
        if(flag==false)
            return; //若本趟遍历后没有发生交换,则说明序列已经有序
    }
}

冒泡排序的性能分析如下:

1、空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。

2、时间效率:当初始序列有序时,显然第一趟冒泡后flag依然为false(本趟冒泡没有元素交换),从而直接跳出循环,比较次数为n-1,移动次数为0,从而最好情况下的时间复杂度为O(n);当初始序列为逆序时,需要进行n-1趟排序,第i趟排序要进行n-i次关键字的比较,而且每次比较后都必须移动元素3次来交换元素位置。这种情况下:
在这里插入图片描述
从而,最坏情况下的时间复杂度为O(n2),其平均时间复杂度也为O(n2)。

3、稳定性:由于i>j且A[i]=A[j]时,不会发生交换,因此冒泡排序是一种稳定的排序方法。

注意:冒泡排序中所产生的有序子序列一定是全局有序的——有序子序列中的所有元素的关键字一定小于或大于无序子序列中所有元素的关键字,这样每趟排序都会将一个元素放置到其最终的位置上

3.2 快速排序

快速排序的基本思想是基于分治法的:在待排序表L[1…n]中任取一个元素pivot作为枢轴(通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

一趟快速排序的过程是一个交替搜索和交换的过程,下面通过实例来介绍,假定初始序列为[49,38,65,97,76,13,27,49],附设两个指针i和j,初值分别为low和high,取第一个元素49为枢轴赋值到变量pivot。如下:
在这里插入图片描述
在这里插入图片描述
假设划分算法已知,记为Partition(),返回的是上述的k,注意到L(k)已在最终的位置,因此可以先对表进行划分,而后对两个表调用同样的排序操作。因此可以递归地调用快速排序算法进行排序,具体的程序结构如下:

void QuickSort(ElemType A[],int low,int high){
    if(low<high){
        //Partition()是划分操作
        int pivotpos=Partition(A,low,high);
        QuickSort(A,low,pivotpos-1); //依次对左右两个子表进行递归排序
        QuickSort(A,pivotpos+1,high);
    }
}

从上面的代码不难看出快速排序算法的关键在于划分操作,同时快速排序算法的性能也主要取决于划分操作的好坏。假设每次总以当前表中第一个元素作为枢轴来对表进行划分,则将表中比枢轴大的元素向右移动,将比枢轴小的元素向左移动,使得一趟Partition()操作后,表中的元素被枢轴值一分为二。代码如下:

int Partition(ElemType A[],int low,int high){ //一趟划分操作
    ElemType pivot=A[low]; //以第一个元素为枢轴,对表进行划分
    while(low<high){
        while(low<high&&A[high]>=pivot)
            --high;
        A[low]=A[high]; //将比枢轴小的移到左端
        while(low<high&&A[low]<=pivot)
            ++low;
        A[high]=A[low]; //将比枢轴大的移到右端
    }
    A[low]=pivot; //将枢轴元素放到最终位置
    return low; //返回存放枢轴的最终位置
}

快速排序算法的性能分析如下:

1、空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为O(log2n);最坏情况下,因为要进行n-1次递归调用,所以栈的深度为O(n);平均情况下,栈的深度为O(log2n)。

2、时间效率:快速排序的运行时间与划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含n-1个元素和0个元素时,这种最大程度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为O(n2)。

有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。

在最理想的状态下,即Partition()可能做到最平衡的划分,得到的两个子问题的大小都不可能大于n/2,在这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为O(n·log2n)。好在快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。快速排序是所有内部排序算法中平均性能最优的排序算法

3、稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序方法。

在快速排序算法中,并不产生有序子序列,但每趟排序后会将枢轴元素放到其最终的位置上


4 选择排序

选择排序的基本思想是:每一趟(如第i趟)在后面n-i+1(i=1,2,…,n-1)个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完,待排序元素只剩下1个,就不用再选了。

4.1 简单选择排序

简单选择排序算法的思想:假设排序表为L[1…n],第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可使得整个排序表有序。简单选择排序算法的代码如下:

void SelectSort(ElemType A[],int n){
    for(i=0;i<n-1;i++){ //n-1次循环
        min=i; //初始化最小元素索引下标为i
        for(j=i+1;j<n;j++) //在A[i...n-1]中选择最小元素
            if(A[j]<A[min]) min=j; //更新最小元素的索引下标
        if(min!=i) swap(A[i],A[min]); //交换
    }
}

简单选择排序算法的性能分析如下:

1、空间效率:仅使用常数个辅助单元,故空间效率为O(1)。

2、时间效率:从上述伪码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过3(n-1)次,最好的情况是移动0次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是n(n-1)/2次,因此时间复杂度始终是O(n2)。

3、稳定性:在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变,简单选择排序是一种不稳定的排序方法

4.2 堆排序

堆的定义如下,n个关键字序列L[1…n]称为堆,当且仅当该序列满足(1≤i≤⌊n/2⌋):

①、L(i)>=L(2i)且L(i)>=L(2i+1)

或者

②、L(i)<=L(2i)且L(i)<=L(2i+1)

可将该一维数组视为一棵完全二叉树,满足条件①的堆称为大根堆(或大顶堆),大根堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点值。满足条件②的堆称为小根堆(或小顶堆),小根堆的定义刚好相反,根结点是最小元素。图8.4所示为一个大根堆:
在这里插入图片描述
堆排序的思路是:首先将存放在L[1…n]中的n个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见堆排序需要解决两个问题:①如何将无序序列构造成初始堆?②输出堆顶元素后,如何将剩余元素调整成新的堆?

堆排序的关键是构造初始堆。n个结点的完全二叉树,最后一个结点是第⌊n/2⌋个结点的孩子。对第⌊n/2⌋个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点(⌊n/2⌋-1~1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。如下图8.5所示:
在这里插入图片描述
初始时调整L(4)子树,09<32,交换,交换后满足堆的定义;向前继续调整L(3)子树,78<左右孩子的较大者87,交换,交换后满足堆的定义;向前调整L(2)子树,17<左右孩子的较大者45,交换后满足堆的定义;向前调整至根结点L(1),53<左右孩子的较大者87,交换,交换后破坏了L(3)子树的堆,采用上述方法对L(3)进行调整,53<左右孩子的较大者78,交换,至此该完全二叉树满足堆的定义。

输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将09和左右孩子的较大者78交换,交换后破坏了L(3)子树的堆,继续对L(3)子树向下筛选,将09和左右孩子的较大者65交换,交换后得到了新堆,调整过程如图8.6所示:
在这里插入图片描述
下面是建立大根堆的算法(向上检查以非叶结点为根的子树,在子树中向下筛选结点):

void BuildMaxHeap(ElemType A[],int len){
    for(int i=len/2;i>0;i--)
        HeapAdjust(A,i,len); //从索引下标i=⌊n/2⌋~1,反复调整堆
}

void HeapAdjust(ElemType A[],int k,int len){
    //对下标k为根的子树进行调整
    A[0]=A[k]; //A[0]暂存子树的根结点
    for(i=2*k;i<=len;i*=2){ //沿key较大的子结点向下筛选
        if(i<len&&A[i]<A[i+1])
            i++; //取key较大的子结点的下标
        if(A[0]>=A[i]) 
            break; //筛选结束
        else{
            A[k]=A[i]; //将A[i]调整到双亲结点上
            k=i; //修改k以便继续向下筛选
        }
    }
    A[k]=A[0]; //将被筛选结点的值放入最终位置
}

调整时间与树高有关,为O(h)。在建含n个元素的堆时,关键字的比较总次数不超过4n,时间复杂度为O(n),这说明可以在线性时间内将一个无序数组建成一个堆。下面是堆排序算法:

void HeapSort(ElemType A[],int len){
    BuildMaxHeap(A,len); //建立初始堆
    for(i=len;i>1;i--){ //进行n-1次交换和调整堆
        swap(A[i],A[1]); //堆顶、堆底元素交换
        HeapAdjust(A,1,i-1); //将剩余的i-1个元素整理成为堆
    }
}

堆也支持插入操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。大根堆的插入操作示例如图8.7所示:
在这里插入图片描述
堆排序适合关键字较多的情况(如n>=1000)。例如,在1亿个数中选出前100个最大值:首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。

堆排序算法的性能分析如下:

1、空间效率:仅使用了常数个辅助单元,所以空间复杂度为O(1)。

2、时间效率:建堆时间为O(n),之后有n-1次向下调整操作,每次调整的时间复杂度为O(h),故在最好、最坏和平均情况下,堆排序的时间复杂度均为O(n·log2n)。

3、稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。


5 归并排序和基数排序

5.1 归并排序

归并排序与基于交换、选择等排序的思想不一样,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表

假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到⌈n/2⌉个长度为2或1的有序表:继续两两归并……如此重复,直到合并成一个长度为n的有序表为止,这种排序方法称为2路归并排序

图8.8所示为2路归并排序的一个例子,经过三趟归并后合并成了有序序列:
在这里插入图片描述
Merge()将前后相邻的两个有序表归并为一个有序表。设两段有序表A[low…mid]、A[mid+1…high]存放在同一顺序表中的相邻位置,先将它们复制到辅助数组B中。每次从对应B中的两个段取出一个记录进行关键字的比较,将较小者放入A中,当数组B中有一段的下标超出其对应的表长(即该段的所有元素都已复制到A中)时,将另一段中的剩余部分直接复制到A中。算法如下:

ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType)); //辅助数组B
void Merge(ElemType A[],int low,int mid,int high){
    //A[low...mid]和A[mid+1...high]各自有序,函数将二者合并为一个有序表
    for(int k=low;k<=high;k++)
        B[k]=A[k]; //将A中所有元素复制到B中
    for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
        if(B[i]<=B[j]) //将B中左右两段中较小的那个元素复制到A中
            A[k]=B[i++];
        else
            A[k]=B[j++];
    }
    while(i<=mid)
        A[k++]=B[i++]; //若第一个表未检测完,则将剩余元素复制到A中
    while(j<=high)
        A[k++]=B[j++]; //若第二个表未检测完,则将剩余元素复制到A中
}

注意:上面的代码中,最后两个while循环只有一个会执行。递归形式的2路归并排序算法是基于分治的,其过程如下:

1、分解:将含有n个元素的待排序表分成各含n/2个元素的子表,采用2路归并排序算法对两个子表递归地进行排序。

2、合并:合并两个已排序的子表得到排序结果:

void MergeSort(ElemType A[],int low,int high){
    if(low<high){
        int mid=(low+high)/2; //从中间划分两个子序列
        MergeSort(A,low,mid); //对左侧子序列递归调用归并排序
        MergeSort(A,mid+1,high); //对右侧子序列递归调用归并排序
        Merge(A,low,mid,high); //将两个子序列归并
    }
}

2路归并排序算法的性能分析如下:

1、空间效率:Merge()操作中,辅助空间刚好为n个单元,所以算法的空间复杂度为O(n)。

2、时间效率:每趟归并的时间复杂度为O(n),共需进行⌈log2n⌉趟归并,所以算法的时间复杂度为O(n·log2n)。

3、稳定性:由于Merge()操作不会改变相同关键字记录的相对次序,所以2路归并排序算法是一种稳定的排序方法。

5.2 基数排序

基数排序不基于比较和移动进行排序,而是基于关键字各位的大小进行排序借助多关键字排序的思想来对单逻辑关键字进行排序
在这里插入图片描述
下面描述以r为基数的最低位优先基数排序的过程,在排序过程中,使用r个队列Q0,Q1,…,Qr-1。基数排序对i=0,1,…,d-1,依次做一次“分配”和“收集”(其实是一次稳定的排序过程),其中:

1、分配:开始时,把Q0,Q1,…,Qr-1各个队列置成空队列,然后依次考察线性表中的每个结点aj(j=0,1,…,n-1),若aj的关键字kij=k,就把aj放进Qk队列中。

2、收集:把Q0,Q1,…,Qr-1各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表。

通常采用链式基数排序,假设对如下10个记录进行排序:
在这里插入图片描述
每个关键字是1000以下的正整数,基数r=10,在排序过程中需要借助10个链队列,每个关键字由3位子关键字构成K1K2K3,分别代表百位、十位和个位,一共需要进行三趟“分配”和“收集”操作。第一趟分配用最低位子关键字K3进行,将所有最低位子关键字(个位)相等的记录分配到同一个队列,如图8.9(a)所示,然后进行收集操作,第一趟收集后的结果如图8.9(b)所示:
在这里插入图片描述
第二趟分配用次低位子关键字K2进行,将所有次低位子关键字(十位)相等的记录分配到同一个队列,如图8.10(a)所示,第二趟收集后的结果如图8.10(b)所示:
在这里插入图片描述
第三趟分配用最高位子关键字K1进行,将所有最高位子关键字(百位)相等的记录分配到同一个队列,如图8.11(a)所示,第三趟收集后的结果如图8.11(b)所示,至此整个排序结束:
在这里插入图片描述
基数排序算法的性能分析如下:

1、空间效率:一趟排序需要的辅助存储空间为r(r个队列:r个队头指针和r个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为O(r)

2、时间效率:基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r),所以基数排序的时间复杂度为O(d(n+r)),它与序列的初始状态无关。

3、稳定性:对于基数排序算法而言,很重要一点就是按位排序时必须是稳定的。因此,这也保证了基数排序的稳定性。

基数排序擅长解决的问题

①数据元素的关键字可以方便地拆分为d组,且d较小;

②每组关键字的取值范围不大,即r较小;

③数据元素个数n较大。


6 各种内部排序算法的比较及应用

6.1 内部排序算法的比较

不同的排序算法一般基于三个因素进行对比:时空复杂度算法的稳定性算法的过程特征

1、从时间复杂度看:简单选择排序、直接插入排序和冒泡排序平均情况下的时间复杂度都为O(n2),且实现过程也较简单,但直接插入排序和冒泡排序最好情况下的时间复杂度可以达到O(n),而简单选择排序则与序列的初始状态无关。堆排序利用了一种称为堆的数据结构,可在线性时间内完成建堆,且在O(n·log2n)内完成排序过程。快速排序基于分治的思想,虽然最坏情况下快速排序时间会达到O(n2),但快速排序平均性能可以达到O(n·log2n),在实际应用中常常优于其他排序算法。归并排序同样基于分治的思想,但由于其分割子序列与初始序列的排列无关,因此它的最好、最坏和平均时间复杂度均为O(n·log2n)。

2、从空间复杂度看:简单选择排序、插入排序、冒泡排序和堆排序都仅需要借助常数个辅助空间。快速排序在空间上只使用一个小的辅助栈,用于实现递归,平均情况下大小为O(log2n),当然在最坏情况下可能会增长到O(n)。2路归并排序在合并操作中需要借助较多的辅助空间用于元素复制,大小为O(n),虽然有方法能克服这个缺点,但其代价是算法会很复杂而且时间复杂度会增加。

3、从稳定性看:插入排序、冒泡排序、归并排序和基数排序是稳定的排序方法,而简单选择排序、快速排序和堆排序都是不稳定的排序方法。
在这里插入图片描述

6.2 内部排序算法的应用

对排序算法的比较和应用应考虑以下情况:

1、选取排序方法需要考虑的因素:

①待排序的元素数目n。

②元素本身信息量的大小。

③关键字的结构及其分布情况。

④稳定性的要求。

⑤语言工具的条件,存储结构及辅助空间的大小等。

2、排序算法小结:

①若n较小,可采用直接插入排序或简单选择排序。由于直接插入排序所需的记录移动次数较简单选择排序的多,因而当记录本身信息量较大时,用简单选择排序较好。

②若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。

③若n较大,则应采用时间复杂度为O(n·log2n)的排序方法:快速排序、堆排序或归并排序。快速排序被认为是目前基于比较的内部排序方法中最好的方法,当待排序的关键字随机分布时,快速排序的平均时间最短。堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,这两种排序都是不稳定的。若要求排序稳定且时间复杂度为O(n·log2n),则可选用归并排序。

④在基于比较的排序方法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的n个关键字随机分布时,任何借助于“比较”的排序算法,都至少需要O(n·log2n)的时间。

⑤若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。

⑥当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构,此时仅修改相关结点的指针即可。


7 外部排序

7.1 外部排序的基本概念

不会整理

7.2 外部排序的方法

不会整理

7.3 多路平衡归并与败者树

不会整理

7.4 置换-选择排序(生成初始归并段)

不会整理

7.5 最佳归并树

不会整理


END

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值