数据结构之排序(2)

206 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

数据结构之排序(2)

交换排序

冒泡排序:

       冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即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=1n1\sum_{i=1}^{n-1}(n-i)= n(n-1)/2,移动次数=i=1n1\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的相对次序已发生了变化。