0
点赞
收藏
分享

微信扫一扫

数据结构之排序

胡桑_b06e 2022-05-03 阅读 63

数据结构之排序

排序的定义

       排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。为了查找方便,通常希望计算机中的表是按关键字有序的。排序的确切定义如下:

       输入:n个记录Rt, R…,Rn,对应的关键字为k, k,…",kn

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

       在排序过程中,根据数据元素是否完全在内存中,可将排序算法分为两类:①内部排序,是指在排序期间元素全部存放在内存中的排序:②外部排序,是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。

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

       每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类,后面几节会分别进行详细介绍。内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。

1.插入排序

       插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法:直接插入排序、折半插入排序和希尔排序。

1.1 直接插入排序

       根据上面的插入排序思想,不难得出一种最简单也最直观的直接插入排序算法。假设在排序过程中,待排序表L[1…n]在某次排序过程中的某一时刻状态如下:

有序序列L[ 1…i-1]L(i)无序序列L[i+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)~(n)依次插入前面已排好序的子序列,初始工[1]可以视为是一个已排好序的子序列。上述操作执行n-1次就能得到一个有序的表。插入排序在实现上通常采用就地排序(空间复杂度为O(1)),因而在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。
下面是直接插入排序的代码。

void InsertSort(ElemType A[],int n){
    int i,j;
    for(i=2;i<=n;i++){
        if(A[i]<A[i-1]){
            A[0]=A[i];
            for(j=i-1;A[0]<A[j];j--){
                A[j+1]=A[j];
            }
            A[j+1]=A[0];
        }
    }
}

直接插入排序算法的性能分析如下:

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

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

       在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为O(n)。

       在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序)·总的比较次数达到最大,为 ∑ i = 1 n i \sum_{i=1}^{n}i i=1ni,总的移动次数也达到最大,为 ∑ i = 1 n ( i + 1 ) \sum_{i=1}^{n}(i+1) i=1n(i+1)

       平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为n^2/4。

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

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

       适用性:直接插入排序算法适用于顺序存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。

1.2 折半插入排序

       从直接插入排序算法中,不难看出每趟插入的过程中都进行了两项工作:①从前面的有序子表中查找出待插入元素应该被插入的位置;②给插入位置腾出空间,将待插入元素复制到表中的插入位置。注意到在该算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。算法代码如下:

void InsertSort(ElemType A[],int n){
    int i,j,low.high,mid;
    for(i=2;i<=n;i++){
        A[0]=A[i];
        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(nlogan),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为O(n),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序方法。

1.3希尔排序

       希尔排序的基本思想是:先将待排序表分割成若干形如L[i,i+d,i+2d,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。

       希尔排序的过程如下:先取一个小于n的步长d1,把表中的全部记录分成d1组,所有距离为d1的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个步长d2<d1,重复上述过程,直到所取到的dt=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。

希尔排序算法的代码如下:

void shellSort (ElemType A[],int n){
    for(dk=n/2;dk>-1;dk=dk/2)
        for(i=dk+l;i<=n;++i)
            if(A[i]<A[i-dk]){
                A[0]=A[i];
            for(j=i-dk;j>0&&A[0]<A[j];j-=dk)
                A[j+dk] =A[j];
            A[j+dk]=A[0];
}

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

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

       时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当n在某个特定范围时,希尔排序的时间复杂度约为O(n1.3)。在最坏情况下希尔排序的时间复杂度为O(n2)。

       稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相互次序,因此希尔排序是一种不稳定的排序方法。

       适用性:希尔排序算法仅适用于线性表为顺序存储的情况。

交换排序

冒泡排序:

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

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

image.png

冒泡排序代码如下:

void BubbleSort(ElemType A[],int n){
    for(i=0;i<n-1;i++){
        flag=false;
        for(j=n-1;j>i;i++){
            if(A[j-1]>A[j]){
                swap(A[j-1],A[j]);
                flag=true;
            }
        }
        if(flag==false)
            return;
    }
}

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

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

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

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

快速排序

       快速排序的基本思想是基于分治法的:在待排序表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)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

       一趟快速排序的过程是一个交替搜索和交换的过程,下面通过实例来介绍,附设两个指针i和j,初值分别为low和 high,取第一个元素49为枢轴赋值到变量pivot。

       指针j从high往前搜索找到第一个小于枢轴的元素27,将27交换到i所指位置。

image.png

       指针i从low往后搜索找到第一个大于枢轴的元素65,将65交换到j所指位置。

image.png

       指针j继续往前搜索找到小于枢轴的元素13,将13交换到i所指位置。

image.png

       指针i继续往后搜索找到大于枢轴的元素97,将97交换到j所指位置。

image.png

       指针j继续往前搜索小于枢轴的元素,直至i==j。

image.png

       此时,指针i(==j)之前的元素均小于49,指针i之后的元素均大于等于49,将49放在i所指位置即其最终位置,经过一趟划分,将原序列分割成了前后两个子序列。

image.png

       按照同样的方法对各子序列进行快速排序,若待排序列中只有一个元素,显然已有序。

image.png

       对算法的最好理解方式是手动地模拟一遍这些算法。

       假设划分算法已知,记为 Partition(),返回的是上述的k,注意到L(k)已在最终的位置,因此可以先对表进行划分,而后对两个表调用同样的排序操作。因此可以递归地调用快速排序算法进行排序,具体的程序结构如下:

void QuickSort(ElemType A[],int low,int high){
    if(low<high){
        int pivotpos=Partition(A,low,high);
        QuickSort(A,low,pivotpos-1);
        QuickSort(A,pivotpos+1,high);
    }
}

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

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

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

       稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序方法。例如,表L={3,2,2},经过一趟排序后L={2,2,3},最终排序序列也是L={2,2,3},显然,2与2的相对次序已发生了变化。

举报

相关推荐

0 条评论