常用排序算法

来源:互联网 发布:app软件制作 编辑:程序博客网 时间:2024/05/21 10:59

排序算法按照算法复杂度来分:

简单的有:冒泡排序、简单选择排序、直接插入排序

复杂的改进算法有:

希尔排序(对直接插入排序的改进);

堆排序(对简单选择排序的改进,基本思想是将数据大小比较的结果存储下来,引入了堆这一数据结构的概念,当然此数据结构与完全二叉树关系最为密切);

归并排序(自成一体,并且是四个改进算法中唯一一个稳定的排序方法,但是最大的缺点是需要的存储空间较大);

快速排序(目前排序算法中的王者,速度至上的理念,是对冒泡排序的改进)。


按照类型来分有四种:

1、插入排序类:直接插入排序、希尔排序;

2、选择排序类:简单选择排序、堆排序;

3、交换排序类:冒泡排序、快速排序;

4、归并排序类:归并排序。


排序算法中还涉及到稳定性的问题,所谓稳定性就是对数据元素进行比较时,如果进行比较的项具有相同的值,那么如果比较后相同值元素的顺序未发生变化则此算法是稳定的,若发生了变化那么就是不稳定的。

举个例子来说:5、8、2、5、7、6

其中包括两个相同的元素5,如果排序后这两个5的先后位置未发生变化,那么排序算法稳定,否则就不稳定。

就这一性质而言有以下结论:简单择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、直接插入排序、归并排序是稳定的排序算法。


下边分述一下各个算法的具体实现:

首先设置排序用到的结构与函数

结构:

#define MAXSIZE 10typedef struct{int r[MAXSIZE+1];int length;}SqList;

函数(交换函数):

void swap(SqList *L,int i,int j){int temp=L->r[i];L->r[i]=L->r[j];L->r[j]=temp;}

1、冒泡排序

实现代码如下:

void BubbleSort2(SqList *L){  int i,j; Status flag=TRUE;     /* flag用来作为标记 */ for(i=1;i<L->length && flag;i++)  /*若flag为true则有过数据交换,否则退出循环*/ {  flag=FALSE;     /* 初始为false */  for(j=L->length-1;j>=i;j--)  {   if(L->r[j]>L->r[j+1])   {     swap(L,j,j+1);  /* 交换L->r[j]与L->r[j+1]的值 */     flag=TRUE;   /* 如果有数据交换,则flag为true */   }  } }}

此方法为标准的冒泡排序算法,符合比较两两相邻记录。而且为了减少不必要的比较,即不再对已排序好的序列继续进行排序加入了标志位的干预,防止了这一情况的发生。

2、简单选择排序(基本思想,第n次循环中找到第n小的数据,并置换到相应位置,保证数据次就能够将其排序完毕)

实现代码如下:

/* 对顺序表L作简单选择排序 */void SelectSort(SqList *L){ int i,j,min; for(i=1;i<L->length;i++) {   min = i;       /* 将当前下标定义为最小值下标 */  for (j = i+1;j<=L->length;j++) /* 循环之后的数据 */  {       if (L->r[min]>L->r[j])  /* 如果有小于当前最小值的关键字 */             min = j;     /* 将此关键字的下标赋值给min */  }  if(i!=min)      /* 若min不等于i,说明找到最小值,交换 */       swap(L,i,min);    /* 交换L->r[i]与L->r[min]的值 */ }}

这一算法的时间复杂度稳定为O(n2),因为无论如何都需要对比1+2+……+(n-2)+(n-1)次,但是他的交换次数会降低很多,所以此算法(在数据量不大,但是记录的关键字信息量较大的排序时)也有一定的优势。堆排序将比较后的情况记录下来,形成了堆的数据结构,对此算法进行了相应改进。

3、直接插入排序(用到了数据结构中L的第一个元素也就是L->r[0],此元素也称为哨兵元素,个人理解主要是用于暂存数据的,另外一个功能是占位,方便循环中下标的对应)

实现代码如下:

void InsertSort(SqList *L){   int i,j;  for(i=2;i<=L->length;i++)  {   if (L->r[i]<L->r[i-1])   /* 需将L->r[i]插入有序子表 */   {    L->r[0]=L->r[i];    /* 设置哨兵 */    for(j=i-1;L->r[j]>L->r[0];j--)     L->r[j+1]=L->r[j];  /* 记录后移 */    L->r[j+1]=L->r[0];   /* 插入到正确位置 */   }  }}

此方法是三个简单方法中性能最好的,这也是为什么快排优化中会设置一个阈值来选择继续用快排法还是直接插入法,也就是说当数据量小的时候用此方法是很有优势的。

4、希尔排序(第一批时间复杂度为O(nlogn)的方法,打破了排序算法时间复杂度不能突破O(n2)的历史,是对直接插入排序的一个改进算法,将整个数据集分成数个先排子序,然后再排总的)

实现代码如下:

void ShellSort(SqList *L){    int i,j;    int increment=L->length;    do    {        increment=increment/3+1;            /* 增量序列 */        for(i=increment+1;i<=L->length;i++)        {            if (L->r[i]<L->r[i-increment])    /* 需将L->r[i]插入有序增量子表 */             {                 L->r[0]=L->r[i];             /* 暂存在L->r[0] */                for(j=i-increment;j>0 && L->r[0]<L->r[j];j-=increment)                    L->r[j+increment]=L->r[j]; /* 记录后移,查找插入位置 */                L->r[j+increment]=L->r[0]; /* 插入 */            }        }    }    while(increment>1);}

5、堆排序(对简单选择排序的优化,引入了堆这一新的数据结构,核心思想是将每次比较的结果存储到堆这一结构中,一般为大顶堆,在排序过程中会不断调整堆的结构)

实现代码:

/* 对顺序表L进行堆排序 */void HeapSort(SqList *L){ int i; for(i=L->length/2;i>0;i--)  /* 把L中的r构建成一个大顶堆 */   HeapAdjust(L,i,L->length);//<span style="color:#ff0000;">这里需要注意,堆的构建是由下到上的,也就是从最末一个非叶子结点开始的。这里需要一个循环才能将开始的数据构建成一个标准的堆结构。</span> for(i=L->length;i>1;i--) {    swap(L,1,i);    /*将堆顶记录和当前未经排序子序列的最后一个记录交换*/   HeapAdjust(L,1,i-1);   /* 将L->r[1..i-1]重新调整为大顶堆 */ }}
其中的核心是HeapAdjust函数此函数的代码如下:

/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, *//* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */void HeapAdjust(SqList *L,int s,int m){  int temp,j; temp=L->r[s]; for(j=2*s;j<=m;j*=2)  /* 沿关键字较大的孩子结点向下筛选 */ {  if(j<m && L->r[j]<L->r[j+1])   ++j;   /* j为关键字中较大的记录的下标 */  if(temp>=L->r[j])   break;   /* rc应插入在位置s上 */  L->r[s]=L->r[j];  s=j; } L->r[s]=temp;   /* 插入 */}

6、归并排序(此排序算法自成一体,虽然占用的空间较多,但是算法却很稳定)

void MergeSort(SqList *L){   MSort(L->r,L->r,1,L->length);}
实现代码代码只有一行,其中MSort为调用的其他函数

Msort函数的代码如下:

/* 将SR[s..t]归并排序为TR1[s..t] */void MSort(int SR[],int TR1[],int s, int t){ int m; int TR2[MAXSIZE+1]; if(s==t)  TR1[s]=SR[s]; else {  m=(s+t)/2;   /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */  MSort(SR,TR2,s,m); /* 递归地将SR[s..m]归并为有序的TR2[s..m] */  MSort(SR,TR2,m+1,t); /* 递归地将SR[m+1..t]归并为有序TR2[m+1..t] */  Merge(TR2,TR1,s,m,t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */ }}

此函数中又用到了Merge函数,Merge函数的源代码如下:

/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */void Merge(int SR[],int TR[],int i,int m,int n){ int j,k,l; for(j=m+1,k=i;i<=m && j<=n;k++) /* 将SR中记录由小到大归并入TR */ {  if (SR[i]<SR[j])   TR[k]=SR[i++];  else   TR[k]=SR[j++]; } if(i<=m) {  for(l=0;l<=m-i;l++)   TR[k+l]=SR[i+l];  /* 将剩余的SR[i..m]复制到TR */ } if(j<=n) {  for(l=0;l<=n-j;l++)   TR[k+l]=SR[j+l];  /* 将剩余的SR[j..n]复制到TR */ }}
7、快速排序(现在排序算法中的王者,对于大量数据的排序效率最高)

代码如下:

void QuickSort(SqList *L){  QSort(L,1,L->length);}
也是调用到了外写的函数,QSort的源代码如下:

/* 对顺序表L中的子序列L->r[low..high]作快速排序 */void QSort(SqList *L,int low,int high){ int pivot; if(low<high) {  pivot=Partition(L,low,high);  /* 将L->r[low..high]一分为二,算出枢轴值pivot */  QSort(L,low,pivot-1);   /*  对低子表递归排序 */  QSort(L,pivot+1,high);   /*  对高子表递归排序 */ }}

而次调用函数中也会用到另一个外写函数Partition,Partition函数的源代码如下:


/* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 *//* 此时在它之前(后)的记录均不大(小)于它。 */int Partition(SqList *L,int low,int high){  int pivotkey; pivotkey=L->r[low];  /* 用子表的第一个记录作枢轴记录 */ while(low<high)      /* 从表的两端交替地向中间扫描 */ {    while(low<high&&L->r[high]>=pivotkey)   high--;   swap(L,low,high); /* 将比枢轴记录小的记录交换到低端 */   while(low<high&&L->r[low]<=pivotkey)   low++;   swap(L,low,high); /* 将比枢轴记录大的记录交换到高端 */ } return low;     /* 返回枢轴所在位置 */}

还可以对快速排序进行以下几方面的优化:
1、这里涉及到了一个取枢轴的问题,这个枢轴选取的好坏直接影响了算法的性能,所以取枢轴一般用三数取中的方法进行,方法如下:

int pivotkey; int m = low + (high - low) / 2;  /* 计算数组中间的元素的下标 */   if (L->r[low]>L->r[high])     swap(L,low,high);    /* 交换左端与右端数据,保证左端较小 */ if (L->r[m]>L->r[high])  swap(L,high,m);    /* 交换中间与右端数据,保证中间较小 */ if (L->r[m]>L->r[low])  swap(L,m,low);    /* 交换中间与左端数据,保证左端较小 */  /* 此时L.r[low]已经为整个序列左中右三个关键字的中间值。*/pivotkey=L->r[low];    /* 用子表的第一个记录作枢轴记录 */

2、优化不必要的交换(其实这里L->[0]同样起到了缓存的作用)

/* 快速排序优化算法 */int Partition1(SqList *L,int low,int high){  int pivotkey; //这里省略三数取中代码 pivotkey=L->r[low];  /* 用子表的第一个记录作枢轴记录 */ L->r[0]=pivotkey;   /* 将枢轴关键字备份到L->r[0] */ while(low<high)   /* 从表的两端交替地向中间扫描 */ {    while(low<high&&L->r[high]>=pivotkey)   high--;   L->r[low]=L->r[high]; /* 采用替换而不是交换的方式进行操作 */   while(low<high&&L->r[low]<=pivotkey)   low++;   L->r[high]=L->r[low]; /* 采用替换而不是交换的方式进行操作 */ } L->r[low]=L->r[0];   /* 将枢轴数值替换回L.r[low] */ return low; /* 返回枢轴所在位置 */}

3、优化小数组时的排序方案(就是当要快排的数据量较少时使用直接插入的排序方法,这里需要设定一个阈值)


#define MAX_LENGTH_INSERT_SORT 7  /* 数组长度阀值 *//* 对顺序表L中的子序列L.r[low..high]作快速排序 */void QSort(SqList &L,int low,int high){  int pivot; if((high-low)>MAX_LENGTH_INSERT_SORT) /*当high-low大于常数时快速排序*/ {  pivot=Partition(L,low,high);  /* 将L.r[low..high]一分为二,算出枢轴值pivot */  QSort(L,low,pivot-1);   /* 对低子表递归排序 */  QSort(L,pivot+1,high);   /* 对高子表递归排序 */ } else        /* 当high-low小于等于常数时用直接插入排序 */  InsertSort(L); }



0 0