排序

来源:互联网 发布:黑马程序员是什么 编辑:程序博客网 时间:2024/05/17 04:42

      整理自《算法设计与分析基础》和《数据结构C++版》

      排序是数据处理中经常使用的一种操作,其主要目的是便于查找。

一、排序的基本概念

      给定一个记录序列(r1 , r2 , r3 ,..., rn),其相应的关键码分别是(k1 , k2 , k3,...,kn),排序是将这些记录排列成顺序为(rs1 , rs2 , rs3,..., rsn)的一个序列,使得相应的关键码满足 ks1 <= ks2 <=... <= ksn或 ks1 >= ks2 >=... >= ksn。简言之,排序是将一个记录的任意序列重新排列成一个按照关键码有序的序列。
      从操作的角度看,排序是线性结构的一种操作,待排序的记录可以用顺序存储结构或者链式存储结构存储。这里用顺序存储结构,并假定关键码为整型来讨论(一维数组)。
      排序算法的稳定性:假定在待排序的记录序列中,存在多个具有相同关键码的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法稳定。
      排序算法的性能:排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要标志。排序过程中经常要进行两种基本操作:①比较,关键码之间的比较;②移动,记录从一个位置移动到另外一个位置。所以在待排序的记录个数一定的条件下,算法的执行时间主要消耗在关键码之间的比较和记录的移动上。另外一个评价排序算法的标准是执行算法所需要的辅助存储空间。

二、排序的分类

2.1减治法

减治技术利用了一个问题给定实例的解和同样问题较小实例的解之间的某种关系。一旦建立了这种关系,我们既可以自顶向下(递归),也可以自底向上(非递归)地来运用该关系。

2.1.1插入排序

      这里我们用减一技术来对一个数组A[ n ]排序。遵循该方法的思路,假设对较小数组A[ n-1 ] 排序的问题已经解决了,我们得到一个大小为n-1的有序数组,我们需要做的就是在这些有序的元素中找到一个合适的位置插入第n个元素。
      这里只介绍直接插入排序:从左到右(或者从右到左)扫描这个有序的子数组,直到遇到一个大于等于(或者小于等于)A[ n-1 ]的元素,然后就把A[ n-1 ]插在该元素的前面(或者后面)。对于基本有序的数组,从右到左扫描效率会更高。
      虽然插入排序很明显是基于递归思想的,但是自底向上实现这个算法,效率会更高,也就是迭代。

      在直接插入排序中需要解决的关键问题是:
      ①如何构造初始的有序序列?
      ②如何查找待插入记录的插入位置?

      下面看一个直接插入排序的例子,| 之前的是有序区。



      具体的排序过程是:
      ①将整个待排序的序列划分成有序区和无序区,初始时有序区为待排序序列中的第一个元素,无序区包括剩下所有的元素。
      ②将无序区的第一个记录插入到有序区的合适位置,从而使无序区减少一个元素,有序区增加一个元素。
      ③重复执行第二步,直到无序区没有元素为止。
      
      对于问题①的解决:将第一个元素看作是初始有序区,从第二个元素依次插入到这个有序区中,直到将第n个记录插入完毕。
      对于问题②的解决:在一般情况下,在i-1个元素的有序区中插入一个元素,首先要查找正确的插入位置。最简单就是采用顺序查找,为了在寻找过程中避免数组下标越界,在自i-1起往前查找的过程中,同时后移元素。在退出循环时,说明找到了插入位置,因为A[ j ]刚刚比较完毕,所以,j+1就是正确的插入位置。
下面是代码实现:
void insertsort(int a[],int n){int i,j;for(i=1;i<n;i++){int v=a[i];j=i-1;while(j>=0&&a[j]>v){a[j+1]=a[j];j--;}a[j+1]=v;}}

      直接插入排序基本操作是键值比较,由两层嵌套的循环组成,外层循环要执行n-1次,内层循环执行次数取决于第i个元素前有几个元素大于第i个元素。最坏的情况下,a[ j ]>v的执行次数最大,即对 每个j=i-1,i-2,...,0,都有a[ j ]>a[ i ],也就是一个严格递减数组,对于这种输入的键值比较次数是(n+2)*(n-1)/2,元素移动次数是(n+4)*(n-1)/2,时间复杂度是O(n^2)。最好的情况下,在每次外部循环的迭代中,比较a[ j ]>v只执行一次,待排序序列是非递减数组,键值比较次数是n-1,元素移动次数是2*(n-1),时间复杂度是O(n)。平均情况下,总的比较次数是降序数组的一半,约为n^2/4,时间复杂度为O(n^2)。平均性能比最差性能快两倍,以及遇到基本有序的数组时表现出的优异性能,使得插入排序领先于它在基本排序算法领域的主要竞争对手——选择排序和冒泡排序。

2.1.2希尔排序

      希尔排序是对直接对插入排序的一种改进,改进的着眼点是:
      ①若待排序序列基本有序,直接插入排序效率很高;
      ②由于直接插入排序算法简单,则在待排序序列个数很少时效率也很高。

      希尔排序的基本思想:先将整个待排序序列分割成若干个子序列(由相隔某个增量的元素组成),在子序列内分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素(增量足够小)基本有序时,再对全体记录进行一次直接插入排序。

      在希尔排序中,需要解决的问题是:
      ①应该如何分割待排序序列,才能保证整个序列逐步向基本有序发展?
      ②子序列内如何进行直接插入排序?
      下面是一个希尔排序的例子:
      以n=7的一个数组12, 15, 9, 20, 6, 31, 24为例
      第一次 d= 7 / 2 = 3
     
      注:1A,1B等为分组标记,数字相同的表示在同一组,大写字母表示是改组的第几个元素,每次对同一组的数据进行直接插入排序。第一次分成了4组(12,20),(15,6),(9,31),(20,24),下同。
      第二次 d= 3 / 2 = 1
      排序后
      
      第三次 d= 1 / 2 = 0
      排序完成得到数组:
     

      对问题①的解决:子序列的构成不能使简单的逐段分割,而是将相距某个增量的元素组成一个子序列,这样才能有效地保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。接下来的问题是增量如何选取。希尔最早提出的方法是dn = dn-1 / 2, 且没有除1以外的公因子,显然最后一个增量必须是1。开始时增量的取值较大,每个子序列中的记录个数较少,并且提供了记录跳跃移动的可能,但基本有序,所以效率较高。
      对问题②的解决:每个子序列中,待插入元素和同一个子序列中的前一个元素比较,在插入元素a[ i ]时,从a[ i-d ]起往前跳跃d个幅度查找待插入位置,在查找过程中,元素后移也是跳跃d个位置。在整个序列中,前d个元素分别是d 个子序列中的第一个元素,所以从第d+1个元素开始进行插入。
      下面是代码实现:
void shellsort(int a[],int n){int i,j,k;int d;for(d=n/2;d>=1;d=d/2){for(i=d;i<n;i++){//d个子序列轮流进行插入排序int v=a[i];for(j=i-d;j>=0&&a[j]>v;j=j-d){a[j+d]=a[j];}a[j+d]=v;}}}
希尔排序算法的时间性能分析是一个复杂的问题,因为它是所取增量的函数,有人在大量实验的基础上指出,希尔排序的时间性能在O(n^2)和O(nlog2n)之间。希尔排序是一种不稳定的排序。

2.2蛮力法

      蛮力法是一种简单直接地解决问题, 常常直接基于问题的描述和所涉及的概念定义。

2.2.1选择排序

      选择排序的主要思想:开始的时候,我们扫描整个列表,找到它的最小元素然后和第一个元素交换,将最小元素放到它在有序表中的最终位置上。然后我们从第二个元素开始扫描列表,找到最后n-1个元素中的最小元素,再和第二个元素交换位置,把第二小的元素放在它的最终位置上。一般来说,在对该列表做第i遍扫描时,该算法最后n-i个元素中寻找最小元素,然后拿它和ai交换。在n-1遍以后,该列表就被 排序好了。

      在选择排序中,需要解决的关键问题是:
      ①如何在待排序序列中选出最小的元素?
      ②如何确定待排序序列中最小元素在序列中的位置?
      下面看一个选择排序的例子,[ ]中的为无序区:


      具体实现过程:(对问题①、②的解决)
      ①将整个序列划分为 有序区和无序区,初始化时有序区为空,无序区为整个序列;
      ②在无序区中选取最小的元素,将它与无序区中的第一个元素交换,使得有序区扩展一个元素,同时无序区减少一个元素;
      ③不断重复第二步,直到无序区只有一个元素为止。

      下面是代码实现,问题1与2均易解决:
void selectsort(int a[],int n){for(int i=0;i<n-1;i++){for(int j=i+1;j<n;j++){if(a[j]<a[i]){int tmp=a[i];a[i]=a[j];a[j]=tmp;}}}}
对选择排序的分析很简单,输入的规模由元素个数决定,基本操作是键值比较a[ j ] < a[ min ],这个比较的执行次数仅仅依赖于数组的规模。键值比较次数是n*(n-1)/2,而键值交换的次数是n-1,对于任何输入来说,选择排序都是一个O(n^2)的算法。选择排序是一种不稳定的排序方法。

2.2.2冒泡排序

      冒泡排序的基本思想是:两两比较相邻的元素,如果反序则交换,直到没有反序的元素为止。

      在冒泡排序中,需要解决的关键问题是:
      ①在一趟冒泡排序中,若有多个元素位于最终位置,应该如何记载?
      ②如何确定冒泡排序的范围,使得已经位于最终位置的元素不再参与下一趟的排序?
      ③如何判别冒泡排序的结束?
      下面是一个冒泡排序的例子,[ ] 中的是无序区:
      

      具体的排序过程是:
      ①将整个待排序的序列分为有序区和无序区,初始时有序区为空,无序区为整合序列;
      ②对无序区 从前向后依次将相邻记录的元素进行比较,若反序则交换,从而使得大元素向后移;
      ③重复第二步,直到无序区没有反序的元素。

      对于问题①、②的解决:如果在某一趟排序中位于最终位置上的关键码个数大于1,那么在下一趟冒泡排序中这些元素应该避免重复比较。因此将第二层循环条件改为j<n-i-1。在一趟排序后,n-i-1后的元素一定是有序的。
      对于问题③的解决:判别冒泡排序的结束条件应该是在一趟排序过程中没有进行数据交换的操作。为此,设置一个交换标志isExchange,初始值为true,表示第一次要进行序列的扫描。若没有发生元素交换,则isExchange置为false,下一次扫描前判断为已经是有序序列,则循环结束;否则isExchange设置为true,继续循环扫描。
      下面是代码实现:
void bubblesort(int a[],int n){bool isExchange = true;for(int i=0;i<n-1&&isExchange;i++){isExchange = false;for(int j=0;j<n-i-1;j++){if(a[j]>a[j+1]){isExchange = true;int tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;}}}}
      冒泡排序算法的执行时间取决于排序的趟数。最好的情况下,待排序序列是正序,进行n-1次比较,不需要移动元素,时间复杂度为O( n );最坏的情况下,待排序序列为逆序,每趟排序在无序序列中只有一个最大的元素被交换到最终位置,算法执行n-1趟,第i趟执行了n-i次元素大小的比较,因此时间复杂度为O( n^2 )。在平均 情况下,冒泡排序的时间复杂度与最坏情况相同数量级,是一种稳定的排序方法。

2.3分治法

      分治法是按照以下思路进行工作的:
      ①将问题的实例划分为同一个问题的几个较小的实例,最好拥有同样的规模;
      ②对这些较小的实例求解,一般使用递归,但在问题规模足够小的时候,可以用其他的算法。
      ③如果有必要的话,合并这些较小问题的解,以得到原始问题的解。

2.3.1合并排序

      合并排序是成功应用分治技术的一个完美例子,基本思想是:对于一个需要排序的数组a[0,1,...,n-1],合并排序将它一分为二,a[0,1,...,n/2-1]和a[ n/2,n/2+1,...,n-1 ],并对每个子数组递归排序然后把两个排好序的子数组合并为一个有序数组。
      
      在合并排序中,需要解决的关键问题是:
      ①如何进行两个序列的合并?
      ②递归一分为二的结束条件?

      对问题①的解决:初始状态下,两个指针(数组下标)分别指向两个待合并序列的第一个元素,然后比较两个元素的大小,将较小的元素添加到一个新创建的数组中,接着被复制序列中的指针后移,指向该较小元素的后继元素。上述操作一直到两个序列中的一个被处理完为止。然后,在未处理完的数组中,剩下的元素被复制到新数组的尾部。
      对问题②的解决:当划分的序列中只有一个元素时,递归结束。
      下面是一个合并排序的例子:
  

      下面是代码实现:
#define LEN 20void Merge(int b[],int c[],int a[],int n,int m){int i=0,j=0;int k=0;while(i<n&&j<m){if(b[i]<c[j]){a[k]=b[i];i++;}else{a[k]=c[j];j++;}k++;}if(i==n){while(j<m){a[k]=c[j];j++;k++;}}else{while(i<n){a[k]=b[i];i++;k++;}}}void MergeSort(int a[],int n){if(n<=1)return;int b[LEN];int c[LEN];int i=0,j=0;for(i=0;i<n/2;i++){b[i]=a[i];}for(i=n/2;i<n;i++){c[j]=a[i];j++;}MergeSort(b,n/2);MergeSort(c,n-n/2);Merge(b,c,a,n/2,n-n/2);}

      整个合并排序的时间复杂度是O(nlog2n),在合并过程中需要与待排序序列同样数量的存储空间,以便存放合并结果,所以空间复杂度为O(n)。合并排序在最坏情况下的键值比较次数十分接近于任何基于比较的排序算法在理论上能够达到的最少次数。合并排序的主要缺点就是该算法需要线性的额外空间。虽然合并能做到“在位”,但会导致算法过于复杂。而且因为它的增长次数具有一个很大的常系数,所以在位的合并排序算法只具有理论上的意义。

2.3.2快速排序

      快速排序是另外一种基于分治技术的重要排序算法,不像合并排序那样按照元素在数组中的位置对它们进行划分,快速排序是按照元素的值对它们进行划分。基本思想:首先选取一个轴值(比较的基准),将待排序序列分成独立的两部分,左侧序列的元素均小于等于轴值,右侧序列的元素均大于等于轴值,然后分别对这两部分重复上述过程,直到整个序列有序。显然这是一个递归的过程。
      
      在快速排序中,需要解决的关键问题:
      ①如何选择轴值?
      ②在待排序序列中如何进行划分?
      ③如何处理得到的两个待排序子序列?
      ④如何判断快速排序的结束?

      对于问题①的解决:选择轴值的方法很多,最简单的策略是选取第一个元素作为轴值。但是,如果待排序序列是正序或者逆序,就会将除轴值以外的所有记录分到轴值的一边,这是快速排序的最坏情况。研究人员发现,更好的中轴选择方法比如三平均分区法:以序列最左边、最右边和中间的元素的中值作为中轴。
      对于问题②的解决:这里使用一种基于两次扫描子数组的高效方法:一次从左到右,一次从右到左,每次都把子序列的元素和中轴比较。
      (1)初始化:取第一个元素作为中轴,设置两个参数i,j分别用来指示将要与中轴比较的左侧元素和右侧元素;
      (2)从左到右扫描从第二个元素开始,直到遇到第一个大于等于中轴的元素停止;
      (3)从右到左扫描从最后一个元素开始,直到遇到第一个小于等于中轴的元素停止;
      (4)两次扫描停止以后,取决于扫描的指针是否相交,会发生3种不同的情况。如果i和j不想交(i<j),我们就简单地交换a[i]和a[j],再分别对i++,j--,继续扫描;如果指针相交(i>j),把中轴和a[j]交换以后,我们得到一个分区;如果扫描指针停下来指向的是同一个元素,就是说i=j,那么被指向的元素的值一定等的中轴,因此我们建立一盒分区。我们可以把第三种情况和第二种情况结合起来,只要i>=j就交换中轴和a[j]的位置。
      下面是一个快速排序的例子:


      下面代码实现分区:
void swap(int& a,int& b){int tmp=a;a=b;b=tmp;}int Partition(int a[],int l,int r){int i=l,j=r+1;int p=a[l];do{do{i=i+1;}while(a[i]<=p);do{j=j-1;}while(a[j]>p);swap(a[i],a[j]);}while(i<j);swap(a[i],a[j]);//撤销最后一次交换swap(a[l],a[j]);return j;}

      注意:在这种形式下,下标i可能会越过子数组的边界,与其每次对下标i加1的时候检查下标越界,不如给序列加一个限位器。
      下面是快速排序的代码实现:
void QuickSort(int a[],int l,int r){if(l>=r)return;int s=Partition(a,l,r);QuickSort(a,l,s-1);QuickSort(a,s+1,r);}
      快速排序的趟数取决于递归的深度。最好的情况下,每次划分一个元素定位后,该元素左侧子序列和右侧子序列的长度相同。在具有n个元素的序列中,对一个元素定位要对整个带划分序列扫描一遍,则所需时间为O(n),整体的时间复杂度是O(nlog2n)。最坏的情况下,待排序序列正序或者逆序,每次划分只得到一个比上一次划分少一个元素的子序列,另一个子序列为空。此时必须经过n-1次递归调用才能把所有元素定位,而且第i趟划分需要经过n-i次元素的比较大小才能找到第i个元素的轴值位置,因此时间复杂度为O(n^2)。快速排序的平均时间性能可经归纳证明为O(nlog2n)。快速排序是一种不稳定的排序方法,使用与元素个数很大且原始元素随机排列的情况。

2.4变治法

2.4.1堆排序

      堆是一种灵巧的、部分有序的数据结构,它尤其适合用来实现优先队列。优先队列支持下列操作:①找出一个具有最高优先级的元素;②删除一个具有最高优先级的元素;③添加一个元素到集合中。
      【堆的概念】
      定义:堆可以定义为一棵二叉树,树的结点中包含键(每个结点一个键),并满足下面两个条件:
      ①数的形状要求——这棵二叉树是完全二叉树,树的每一层都是满的,除了最后一层最右边的元素有可能缺位;
      ②父母优势要求——每一个结点的键都要大于或者等于它子女的键。
      举例来说,下面三个树中,第一个是堆,第二个和第三个都不是
 
      注意:在堆中,键值是从上到下排序的,在任何从根到某个叶子的路径上,键值的序列是递减的。但是,键值之间并不是从左到右的次序。也就是说,在树的同一层的结点之间,不存在任何关系,更一般地来说,在同一结点的左右子树之间没有任何关系。

      下面是堆的4点特性:
      ①只存在一棵n个结点的完全二叉树,它的高度等于log2n的向下取整;
      ②堆的根总是包含了堆的最大元素;
      ③堆的一个结点以及该结点的子孙也是一个堆;
      ④可以用数组来实现堆,方法是从上到下、从左到右(类似层次遍历)的方式来记录堆的元素。为了方便起见,可以在这种数组从1到n的位置上存放堆元素,留下a[0],要么让它空着,要么在其中存放一个限位器,它的值大于数组中任何一个元素。在这种表示法中:
        (1)父母结点将位于数组前n/2个位置,剩余的是叶子结点;
        (2)在数组中,对于一个位于父母位置i的键来说,它的子女将位于2*i和2*i+1,相应的,对于一个位于i的键来说,它的父母位于i/2。
      
      如上图所示,我们将堆定义为数组a[1...n],对于i=1,2,...,n/2,a[i]>=max{a[2i],a[2i+1]。
      
      在堆排序中,需要解决的关键问题是:
      ①如何构造一个堆?
      ②在建立的堆中如何进行排序?

      对问题①的解决:这里介绍自底向上堆构造法。从最后的父母结点(n/2)开始,到根为止,检查这些结点的键K是否满足父母优势要求。如果该结点不满足,把该结点的键值和它的子女的最大键值进行交换,然后再检查在新的位置上,K是不是满足父母优势要求。这个过程一直继续到对K的父母优势要求满足为止。对于以当前父母结点为根的子树,在完成了它“堆化”以后,对该结点的直接前驱进行同样的操作。在对树的根完成这种操作以后,该算法就停止。
      下面是一个堆构造的例子:


      下面是堆构造的代码实现:
//自底向上void HeapBottom(int a[], int n){int i = n / 2;int k, j;int v;bool heap;for (; i >= 1; i--){k = i;v = a[k];heap = false;while (heap == false && 2 * k <= n){j = 2 * k;if (j < n){if (a[j] < a[j + 1]){j = j + 1;}}if (v >= a[j]){heap = true;}else{a[k] = a[j];k = j;}}a[k] = v;}}

      我们假设n=2^k-1,所以树是满树。在堆构造最坏的情况下,每个位于树的第i曾的键都会移动到叶子层。因为移动到叶子层要进行两次比较—— 一次找出较大的子女,另一次确定是否需要交换——位于第i层的键总会需要2(h-i)次键值比较。所以最坏的情况下,总的键值比较次数是 。使用这个方法,一个规模为n的堆只需要不用2n次比较就能构造完成。

      对于问题②的解决:第一步,为一个给定数组构造一个堆;第二步,(删除最大键)对剩下的堆应用n-1次根删除操作。作为结果,按照降序删除了该数组的元素。但是对于堆的数组实现来说,一个正在被删除的元素是位于最后的,所以结果数组将恰好是按照升序排列的原始数组。

      又遇到新的问题,如何从堆中删除一个元素呢?因为堆排序涉及的是根结点的删除,所以这里仅介绍删除根结点。将要删除的键和最后的键做交换,堆的规模减1,然后堆化这颗较小的树:根中的新键和它子女中较大的键做交换,直到满足父母优势要求。删除的效率取决于在交换和堆的规模减1后,树的堆化所需要的键值比较次数。     
      下面是一个堆排序的例子:

      下面是对排序的代码实现:
void HeapSort(int a [], int n){while (n > 0){swap(a[1], a[n]);n--;HeapBottom(a, n);}}
      实际上,无论是最差情况还是平均情况,堆排序的时间效率都属于O(nlog2n),因此堆排序的时间效率和合并排序的效率属于同一类。而且,与后者不同的是,堆排序是在位的,不需要任何额外的存储空间。
                                                                                                                                                                                                                                                                                                                     

2.5时空权衡

      时空权衡的思想是对问题的部分或全部输入做预处理,然后对获得的额外信息进行存储,以加速后面问题的求解,这个方法叫做输入增强。计数排序就是基于这个思想的。

2.5.1比较计数排序

      针对待排序咧中的每一个元素,算出序列中小于该元素的元素个数,并把结果记录在一张表中,这个个数就是该元素在有序序列中的位置。因此,我们可以简单地把序列中的元素,复制到它在有序的新列表的相应位置上,来对列表进行排序。
      下面是比较计数排序的一个例子:

      下面是比较计数排序的代码实现:
//比较计数排序void ComparisonCountingSort(int a [], int n){int i, j;int* count = new int[n];// (int*) malloc(n);for (i = 0; i < n; i++){count[i] = 0;}for (i = 0; i < n - 1; i++){for (j = i + 1; j < n; j++){if (a[i]>a[j])count[i]++;elsecount[j]++;}}int* s = new int[n];// (int*) malloc(n);for (i = 0; i < n; i++){s[count[i]] = a[i];}for (i = 0; i < n; i++){printf("%d ", s[i]);}printf("\n");delete []count;delete []s;}

      因为该算法执行的键值比较次数和选择排序一样多,并且还占用了线性数量的额外空间,我们几乎不用它来做实际应用。

2.5.2分布计数排序

      如果待排序的元素的值都来自于一个已知的小集合,例如,如果元素的值是位于下届l和上届u之间的整数,我们可以计算每个这样的值出现的频率,然后把他们存储在数组F[0,...,u-l]中。我们可以把元素复制到一个新的数组 S[0,...,n-1]中,A中元素的值如果等于最小的l,就被复制到S的前F[0]个元素中,也就是0到F[0]-1的位置,值等于l+1的元素被复制到F[0]至F[0]+F[1]-1的位置,以此类推。
      分布值指出了在最后的有序数组中,它们的元素最后一次出现的正确位置。如果从0到n-1建立数组下标,那么为了得到相应的元素位置,分布值必须减一。
      下面是分布计数排序的代码实现:
//分布计数排序void DistributionCountingSort(int a [], int n,int l,int u){int i, j;int index, p;int* d = new int[u - l + 1];for (j = 0; j < u - l + 1; j++){d[j] = 0;}for (i = 0; i < n; i++){d[a[i] - l]++;}for (j = 1; j < u - l + 1; j++){d[j] = d[j - 1] + d[j];}int* s = new int[n];for (i = n - 1; i >= 0; i--){j = a[i] - l;s[d[j] - 1] = a[i];d[j] = d[j] - 1;}for (i = 0; i < n; i++){printf("%d ", s[i]);}printf("\n");delete []s;delete []d;}

                        
0 0
原创粉丝点击