首页 > 代码库 > 内部排序算法(一):交换排序(冒泡排序,快速排序)

内部排序算法(一):交换排序(冒泡排序,快速排序)

        这是我的博文系列《内部排序算法》的第一篇。所谓排序,就是要整理文件中的记录,使之按关键字递增(或递减)次序排列起来。所谓内部排序,是指在排序过程中,若整个文件都是放在内存中处理,排序时不涉及数据的内、外存交换(外排序的定义则相反)。

        内部排序法按照策略可以划分为五类:插入排序选择排序交换排序归并排序分配排序。待排文件的存储方式采用顺序表(或直接用向量)作为存储结构(其他的存储结构还有以链表作为存储结构等)。

        在这个系列的博文中,我按照排序算法的给出,排序算法的分析(包括算法的时空复杂度分析)来展开。对于排序算法的分析包括:对于算法思路的分析,对于算法时间复杂度的分析,对于算法空间复杂度的分析,对于排序算法稳定性的分析(待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的)。

        本文来介绍交换排序。交换排序的基本思想是:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。应用交换排序基本思想的主要排序方法有:冒泡排序和快速排序。

        首先来介绍冒泡排序。给出排序算法:

        Step1:初始化。初始化文件记录R,R[1..n]为无序区。

        Step2:扫描。从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换二者的位置。即依次比较(R[n],R[n-1]),(R[n-1],R[n-2]),…,(R[2],R[1]);对于每对气泡(R[j+1],R[j]),若R[j+1].key<R[j].key,则交换R[j+1]和R[j]的内容。

        Step3:判断。判断文件是否有序,如果有序,则结束算法,否则返回Step2,继续进行下一趟扫描。

        冒泡算法特征

  • 第i趟扫描时,R[1..i-1]和R[i..n]分别为当前的有序区和无序区。扫描仍是从无序区底部向上直至该区顶部。扫描完毕时,该区中最轻气泡飘浮到顶部位置R[i]上,结果是R[1..i]变为新的有序区。比如说,第一趟扫描完毕时,"最轻"的气泡就飘浮到该区间的顶部。
  • 每趟扫描仅能使最重气泡"下沉"一个位置。
  • 整个冒泡排序过程至多需要进行n-1趟排序。因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量。
  • 若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。为此,在下面给出的算法中,引入一个布尔量exchange,在每趟排序开始前,先将其置为FALSE。若排序过程中发生了交换,则将其置为TRUE。各趟排序结束时检查exchange,若未曾发生过交换则终止算法,不再进行下一趟排序。
        下面给出代码:
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;

#define MaxSize 100
typedef int KeyType;

void BubbleSort(KeyType R[],int n){
    //冒泡排序算法
    //R(l..n)是待排序的文件,采用自下向上扫描,对R做冒泡排序
    bool exchange;          //交换标志
    for(int i=1;i<n;i++){   //最多做n-1趟排序
        exchange=false;     //本趟排序开始前,交换标志应为假
        for(int j=n-1;j>=i;j--){//对当前无序区R[i..n]自下向上扫描
            if(R[j+1]<R[j])
                swap(R[j],R[j+1]);//交换记录
            exchange=true;//发生了交换,故将交换标志置为真
        }
        if(!exchange)//本趟排序未发生交换,提前终止算法
            return;
    }
}

//*****************************测试函数***************************
int main(){
    int n,R[MaxSize];
    printf("请输入数组大小:\n");
    scanf("%d",&n);
    printf("请输入数组元素:\n");
    for(int i=1;i<=n;i++)
        scanf("%d",&R[i]);
    printf("Before sorting:\n");
    for(int i=1;i<=n;i++)
        printf("%d  ",R[i]);
    printf("\n");
    BubbleSort(R,n);
    printf("After sorting:\n");
    for(int i=1;i<=n;i++)
        printf("%d  ",R[i]);
    printf("\n");
    return 0;
}

冒泡排序的具体过程举例就不给出了,读者不难在纸上模拟出。对冒泡算法进行分析:
  • 空间复杂度分析:所需要的辅助空间为O(1),即就地排序(若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间是O(1),则称之为就地排序)。
  • 算法的最坏时间复杂度:若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),在这种情况下,比较次数均达到最大值:n(n-1)/2=O(n^2)。
  • 算法稳定性:显然冒泡排序是稳定的。
下面来介绍快速排序法。给出排序算法:(设当前待排序的无序区为R[low..high])

        Step1:分解。在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot,右边的子区间中所有记录的关键字均大于等于pivot,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。

       Step2:求解。通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。

       Step3:组合。因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。

       算法特征:

  • 算法采用了一种分治的策略,将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解(这种方法也成为分治法)。
  • 快速排序执行的全过程可用递归树来描述,快速排序的执行过程相当于是先序遍历递归树。以下举例说明:

上图是对整数序列:49,38,65,97,76,13,27,49进行快速排序的递归树。递归树上每一结点左旁方括号表示当前待排序的区间,结点内的关键字是划分的基准关键字,每个分支结点右旁圆括号中的内容表示对该结点左旁区间的排序过程结束之后返回的结果。算法的执行顺序是递归树中的箭头顺序,实际上当把划分操作视为访问结点的操作时,快速排序的执行过程相当于是先序遍历其递归树。

        Step1中的划分算法Partition描述如下:第一步:(初始化)设置两个指针i和j,它们的初值分别为区间的下界和上界,即i=low,j=high;选取无序区的第一个记录R[i](即R[low])作为基准记录,并将它保存在变量pivot中;第二步:令j自high起向左扫描,直到找到第1个关键字小于pivot的记录R[j],将R[j])移至i所指的位置上,这相当于R[j]和基准R[i](即pivot)进行了交换,使关键字小于基准关键字pivot的记录移到了基准的左边,交换后R[j]中相当于是pivot;然后,令i指针自i+1位置开始向右扫描,直至找到第1个关键字大于pivot的记录R[i],将R[i]移到j所指的位置上,这相当于交换了R[i]和基准R[j],使关键字大于基准关键字的记录移到了基准的右边,交换后R[i]中又相当于存放了pivot;接着令指针j自位置j-1开始向左扫描,如此交替改变扫描方向,从两端各自往中间靠拢,直至i=j时,i便是基准pivot最终的位置,将pivot放在此位置上就完成了一次划分。
        对于划分算法,在当前无序区中选取划分的基准关键字是决定算法性能的关键。一般地,有如下俩种选取基准关键字的方法:
  1. "三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区伺的第1个记录进行交换,此后的划分过程与上面所给的Partition算法完全相同。
  2. 取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准。选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序
       下面给出快速排序算法的代码:
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<time.h>
using namespace std;

#define MaxSize 100
typedef int KeyType;

int Partition(KeyType R[],int i,int j){
    //对R[low..high]做划分,并返回基准记录的位置
    //用区间的第1个记录作为基准
    KeyType pivot=R[i];//用区间的第1个记录作为基准
    while(i<j){//从区间两端交替向中间扫描,直至i=j为止
        while(i<j&&R[j]>=pivot)//pivot相当于在位置i上
            j--;//从右向左扫描,查找第1个关键字小于pivot.key的记录R[j]
        if(i<j) //表示找到的R[j]的关键字<pivot.key
            R[i++]=R[j];//相当于交换R[i]和R[j],交换后i指针加1
        while(i<j&&R[i]<=pivot)//pivot相当于在位置j上
            i++;//从左向右扫描,查找第1个关键字大于pivot.key的记录R[i]
        if(i<j)//表示找到了R[i],使R[i].key>pivot.key
            R[j--]=R[i];//相当于交换R[i]和R[j],交换后j指针减1
    }
    R[i]=pivot;//基准记录已被最后定位
    return i;//基准元素位置
}

int RandomPartition(KeyType R[],int i,int j){
    //对R[low..high]做随机划分,并返回基准记录的位置
    //取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准
    srand((unsigned)time(NULL));
    int k=i+rand()%(j-i+1);
    swap(R[k],R[i]);
    //下面的步骤与Partition函数相同,不再作注释
    KeyType pivot=R[i];
    while(i<j){
        while(i<j&&R[j]>=pivot)
            j--;
        if(i<j)
            R[i++]=R[j];
        while(i<j&&R[i]<=pivot)
            i++;
        if(i<j)
            R[j--]=R[i];
    }
    R[i]=pivot;
    return i;
}

void QuickSort(KeyType R[],int low,int high){
    //对R[low..high]快速排序
    int pivotpos;//划分后的基准记录的位置
    if(low<high){//仅当区间长度大于1时才须排序
        //pivotpos=Partition(R,low,high);
        pivotpos=RandomPartition(R,low,high);//对R[low..high]做划分
        QuickSort(R,low,pivotpos-1);//对左区间递归排序
        QuickSort(R,pivotpos+1,high);//对右区间递归排序
    }
}

//*****************************测试函数***************************
int main(){
    int n,R[MaxSize];
    printf("请输入数组大小:\n");
    scanf("%d",&n);
    printf("请输入数组元素:\n");
    for(int i=1;i<=n;i++)
        scanf("%d",&R[i]);
    printf("Before sorting:\n");
    for(int i=1;i<=n;i++)
        printf("%d  ",R[i]);
    printf("\n");
    QuickSort(R,1,n);
    printf("After sorting:\n");
    for(int i=1;i<=n;i++)
        printf("%d  ",R[i]);
    printf("\n");
    return 0;
}

        快速排序算法分析:
  • 最好时间复杂度:在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)。用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
  • 空间复杂度:快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。
  • 稳定性:快速排序是非稳定的,例如[2,2,1]。


内部排序算法(一):交换排序(冒泡排序,快速排序)