数据结构与算法26-排序

排序

最简单的排序实现

冒泡排序(Bubble   Sort)一种交换排序,它的基本思想是:两两比较相邻记录的的关键字,如果反序则交换,直至没有反序记录为止

如下:

/*对顺序表L作交换排序(冒泡排序初级版)*/

void    BubbleSort0(SqList   *L)

{

         int    i,j;

         for(i=1;i<L->length;i++)

         {

                  for(j=i+1;j<L->length;j++)

                 {

                        if(L->r[i]>L->r[j])

                          {

                                   swap(L,i,j); //交换 L->r[i]与L->r[j]的值

                           }

                  }

          }

}

这的思路就是让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后,一定变成最小值。

它应该算是最最容易写出的排序代码了,不过这个简单易懂的代码,却是有缺陷的。观察后发现,在排序好1和2的位置后,对其余关键字的排序没有什么帮助。也就是说,这个算法的效率是非常低。

来看一个正宗的冒泡排序算法

冒泡排序算法

/*对顺序表L作冒泡排序*/

void    BubbleSort(SqList    *L)

{

        int   i,j;

        for(i=1;i<L->length;i++)

        {

              for(j=L->length-1;j>=i;j--)

               {

                    if(L->r[j]>L->r[j+1]){

                             swap(L,j,j+1);

                   }

               }

        }

}

 

当i=2时,变量j由8反向循环到2,逐个比较

 

冒泡排序优化

如我们要排序序列是{2,1,3,4,5,6,7,8,9}。也就是说,除了第一和第二的关键字需要交换外,别的都已经是正常顺序。当i=1时,交换2和1,此时序列已经有序,但算法仍然不依不饶地将i=2到9以及每个循环中的j循环都执行一遍,尽管并没有交换数据,但是之后的大量比较还是大大地多余了,

当i=2时,我们已经对9和8,8与7,….3与2作了比较,没有任何数据交换,这就说明此序列已经有序,不需要再继续后面的循环判断了,为了实现这个想法,我们需要改进一下代码,增加一个标记变量flag来实现这一算法的。

/*对顺序表L作改进冒泡算法*/

void    BubbleSort2(SqList   *L)

{

        int   i,j;

        Status  flag = TRUE;

        for(i=1;i<L->length &&flag;i++)

       {

               flag=FALSE;

              for(j=L->length-1;j>=i;j--)

               {

                     if(L->r[j]>L->r[j+1]){

                             swap(L,j,j+1);

                          flag = TRUE;

                   }

               }

        }

}

简单选择排序

简单选择排序(Simple  Selection  Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1  i    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;

                 }

                  if(i!=min)

                      swap(L,i,min);

          }

}

针对待排序的关键字序列是{9,1,5,8,3,7,4,6,2},对i从1循环到8。当i=1时,L.r[i]=9,min开始是1,然后与j=2到9比较L.r[min]与L.r[j]的大小,因为j=2时最小,所以min=2。最终交换了L.r[2]与L.r[1]的值。如图,注意,这里比较了8次却只交换数据操作一次。

当i=2时,L.r[i]=9,min开始是2,经过比较后,min=9,交换L.r[min]与L.r[i]的值。这样就找到了第二个位置

当i=3

之后数据比较和交换完全雷同,最多经过8次,就可以完成排序工作。

尽管于冒泡排序同为O(n2),但简单选择排序的性能上还是要略优于冒泡排序的

直接插入排序

直接插入排序(Straight    Insertion  Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表

顾名思义,从名称上也可以知道它是一种插入排序的方法。我们来看直接插入排序法的代码:

/*对顺序表L作直接插入排序*/

void   InsertSort(SqList   *L)

{

        int    i,j;

        for(i=2;i<L->length;i++)

        {

                if(L->r[i]<L->r[i-1])

                {

                       L->r[0] = L->r[i];//设置哨兵

                     for(j=i-1;L->r[j]>L->L->r[0];j--)

                           L->r[j+1]=L->r[j];

                      L->r[j+1] = L->r[0];    //插入到正确位置

                }

        }

}

1.    程序开始运行,此时我们传入的SqList参数的值为length=6,r[6]={0,5,3,4,6,2},其中r[0]=0将用于后面起到哨兵的作用。

2.    第4~13行就是排序的主循环。i从2开始的意思是我r[1]=5已经放好了位置,后面的数据其实就是插入到它的左侧还是右侧的问题。

3.    第6行,此时i=2,L.r[i]=3比L.r[i-1]=5要小,因此执行第8~11行的操作。第8行,我们将L.r[0]赋值为L.r[i]=3的目的是为了起到9~10的循环终止判断依据。下图就是L.r[j+1]=L.r[j]的过程

             

4.    此时,第10行就是在移动完成后,突出了空位,然后第11行L.r[j+1]=L.r[0],将哨兵的值3赋值给j=0时的L.r[j+1],也就是说,将扑克牌3放置到L.r[1]的位置

5.    继续循环,第6行,因为此时i=3,L.r[i]=4比L.r[i-1]=5要小,因此执行第8~11行代码操作,将5再右移一位,将4放置到当前5所在位置,

6.    再次循环,此时i=4。因为L.r[i]=6比L.r[i-1]=5要大,于是第8~11行代码不执行,此时前三张牌的位置没变化,

7.    再次循环,此时i=5,因为L.r[i]=2比L.r[i-1]=6要小,因此执行第8~11行的操作。由于6、5、4、3都比2小,它们都将右移一位,将2放置到当前3所在位置。

 

希尔排序

所谓基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间。

跳跃分割策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到结果是基本有序而不是局部有序的

希尔排序代码如下:

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->[i];

                             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)

}

1.    程序开始运行,此时我们传入的SqList参数的值为length=9,r[10]={0,9,1,5,8,3,7,4,6,2}。这就是我们需要等待排序的序列,

2.    第4行,变量increment就是那个“增量”,我们初始值让它等于等待排序的记录数。

3.    第5~19行是一个do循环,终止的条件是increment不大于1时,其实也就是增量为1时,就停止循环了。

4.    第7行,这一句很关键,但也是难以理解的地方,我们后面还要谈到它,先放一放。这里执行完后,increment = 9/3+1=4;

5.    第8~17行是一个for循环,i从4+1=5开始到9结束。

6.    第10行,判断L.r[i]与L.r[i-increment]大小,L.r[5]=3小于L.r[i-increment]=L.r[1]=9,满足条件,第12行,将L.r[5]=3暂存入L.r[0]。第13~14行的循环只是为了将L.r[1]=9的值赋给L.r[5],由于循环的增量是j-=increment,其实它就循环了一次,此时j=-3。第15行,再将L.r[0]=3赋值给L.r[j+increment]=L.r[-3+4]=L.r[1]=3。事实上,这一段代码就干了一件事,就是将第5位的3和第1位的9交换了位置。

7.    循环继续,i=6,L.r[6]=7> L.r[i-increment]=L.r[2]=1,因此不交换两者数据。如图

8.    循环继续,i=7,L.r[7]=4<L.r[i-increment]=L.r[3]=5,交换两者数据

9.    循环继续,i=8,L.r[8]=6<L.r[i-increment]=L.r[4]=8,交换两者数据。注意,第13~14行是循环,此时还要继续比较L.r[5]与L.r[1]的大小,因为2<3,所以还要交换L.r[4]=8,交换两者数据

10.  循环继续,i=9,L.r[9]=2<L.r[i-increment]=L.r[5]=9,交换两者数据。注意,第13~14行是循环,此时还要继续比较L.r[5]与L.r[1]的大小,因为2<3,所以还要交换L.r[5]与L.r[1]的数据,如下图

最终第一轮循环后,数组的排序结果为下图所示,细心的同学会发现,我们的数字1、2等小数字已经在前两位,而8、9等大数已经在后两位,也就是说,通过这样的排序,我们已经让整个序列基本有序了。这其实就是希尔排序的精华所在,它将关键字较小的记录,不是一步一步地往前挪动,而是跳跃式地往前移,从而使得每次完成一轮循环后,整个序列就朝着有序坚实地迈进一步。

11.  我们继续,在完成一轮do循环后,此时由于increment=4>1因此我们需要继续do循环。第7行得到increment=4/3+1=2。第8~17行for循环,i从2+1=3开始到9结束。当i=3、4时,不用交换,当i=5时,需要交换数据,如图

12.  此后,i=6,7,8,9均不交换,

13.  再次完成一轮do循环,increment=2>1,再次do循环,第7行得到increment=2/3+1,此时这就是最后一轮do循环了。尽管第8~17行,for循环,i从1+1=2开始到9结束,但由于当前序列已经基本有序,可交换数据的情况在为减少,效率其实很高。如图,箭头连线为需要交换的关键字如下:

最终排序如下

 

堆排序

如果可以做到每次在选择到最小记录的同时,并根据比较结果对其他记录做出相应的调整,那样排序的总体效率就会非常高了。而堆排序,就是对简单选排序进行的一种改进。

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

这里需要注意从堆的定义可知,根结点一定是堆中所有结点最大(小)者。较大(小)的结点靠近根结点(但也不绝对,比如右图小顶堆,60、40均小于70,但它们并没有70靠近根结点)。

如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:

这里为什么i要小于等于呢?这个是二叉树的性质5,性质5第一条就说一棵完全二叉树,如果i=1,则结点是二叉树的根,无双亲;如果i>1,则其双亲结点是[i/2]。那么对于n个结点的二叉树而言,它的i值自然就是小于等于[i/2]了。性质5的第二、三条,也是说明下标i和2i和2i+1的双亲子女关系。

如果将上图的大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的关系表达式如图:

 

堆排序算法

堆排序(Heap   Sort)就是览胜堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的未必元素交换,此时未尾元素就是最大值),然后将剩余的n-1个序列重新构造一个堆,这样就会得到n个元素中次大值,如此反复执行,便能得到一个有序序列了

例如图所示,图①是一个大顶堆,90为最大值,将90与20(末尾元素)互换,如图②所示,此时90就成了整个堆序列的最后一个元素,将20经过调整,使得90以外的结点继续满足大顶堆定义(所有结点都大于等于其子孩子)见图③,然后再考虑将30与80互换….

相信大家有些明白堆排序的基本思想了,不过要实现它还需要解决两个问题:

1.    如何由一个无序序列构建成一个堆?

2.    如果在输出堆顶元素后,调整剩余元素成为一个新的堆?

/*对顺序表L进行堆排序*/

void    HeapSort(SqList    *L)

{

        int   i;

       for(i=L->length/2;i>0;i--)

                 HeapAdjust(L,i,L->length);

       for(i=L->length;i>1;i--)

        {

                   swap(L,1,i); //将堆顶记录和当前未经排序子序列的最后一个记录交换

                   HeapAdjust(L,1,i-1);//将L->r[i…i-1]重新调整为大顶堆

        }



}

从代码也可以看出,整个排序过程分为两个for循环。第一个循环要完成的就是将现在的待排序序列构建成一个大顶堆。第二个循环要完成的就是逐步将每个最大值的根结点与末尾元素交换,并且再调整其成为大顶堆。

假设我们要排序的序列是{50,10,90,30,70,40,80,60,20},那么L.length=9,第一个for循环,代码第4行,i是从[9/2]=4开始,4->3->2->1的变量变化。为什么不是从1到9或者从9到1,而是从4到1呢?其实我们看了下图就明白了,它们都有什么规律?它们都是有孩子的结点。注意灰色结点的下标编号就是1,2,3,4。

我们所谓的将待排序的序列构建成为一个大顶堆,其实就是从下往上、从右到左,将每个非终端结点(非叶结点)当作根结点,将其和其子树调整成大顶堆。i的4->3->2->1的变量,其实也就是30,90,10,50的结点调整过程。既然已经弄清楚i的变化是在调整如些元素了,现在我们来看关键的HeapAdjust(堆调整)函数是如何实现的。

/*已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义*/

/*本函数调整L->r[s]的关键字,使L->[s..m]成为一个大顶堆*/

void      HeadAdjust(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; //插入

}

1.    函数第一次调用时,s=4,m=9,传入的Sqlist参数的值为length=9,r[10]={0,50,10,90,30,70,40,80,60,20}。

2.    第4行,将L.r[s]=L.r[4]=30,赋值给temp,如图

3.    第5~13行,循环遍历其结点的孩子。这里j变量为什么是从2*s开始呢?又为什么是j*=2递增呢?原因还是二叉树的性质5,因为我们这棵是完全二叉树,当前结点序号是s,其左孩子的序号一定是2s,右孩子的序号一定是2s+1,它们的孩子当然也是以2的位数序号增加,因此j变量才这样循环。

4.    第7~8行,此时j=2*4=8,j<m说明它不是最后一个结点,如果L.r[j]<L.r[j+1],则说明左孩子小于右孩子。我们的目的是要找到较大值,当然需要让j+1以便变成指向右孩子的下标。当前30的左右孩子是60和20,并不满足此条件,因此j还是8。

5.    第9~10行,temp=30,L.r[j]=60,并不满足条件。

6.    第11~12行,将60赋值给L.r[4],并令s=j=8。也就是说,当前算出,以30为根结点的子二叉树,当前最大值是60,在第8的位置。注意此时L.r[4]和L.r[8]的值均为60

7.    再循环因为j=2*j=16,m=9,j>m,因此跳出循环。

8.    第14行,将temp=30赋值给L.r[s]=L.r[8],完成30与60的交换工作。本次函数调用完成。

9.    再次调用HeapAdjust,此时s=3,m=9。第4行,temp=L.r[3]=90,第7~8行,由于40<80得到j+1=2*s+1=7。9~10行,由于90>80,因此退出循环,最终本次调用,整个序列未发生什么改变。

10.  两次调用HeapAdjust,此时s=2,m=9。第4行,temp=L.r[2]=10,第7~8行,60<70,使得j=5。最终本次调用使得10与70进行了互换,如下图

 

11.  再次调用HeapAdjust,此时s=1,m=9。第4行,temp=L.r[1]=50,第7~8行,70<90,使得j=3。第11~12行,L.r[1]被赋值了90,并且s=3,再循环,由于2j=6并未大于m,因此再次执行循环体,使得L.r[3]被赋值了80,完成循环后,L.r[7]被赋值为50,最终本次调用使得50、90、80进行了轮换,如图

到此为止,我们构建大顶堆的过程算是完成了,也就是HeapSort函数的第4~5行循环执行完毕。

接下来HeapSort函数的第6~11行就是正式的排序过程,由于有了前面充分准备,其实这个排序就比较轻松了。如下

for(i=L->length;i>1;i--)

{

       swap(L,1,i);      //将堆顶记录和当前未排序子序列的最后一个记录交换

        HeapAdjust(L,1,i-1);  //将L->r[1..i-1]重新调整为大顶堆

}

1.    当i=9时,第8行,交换20与90,第9行,将当前的根结点20进行大顶堆调整,调整过程和刚才流程一样,找到它左右子结点的较大值,互换,再找到其子结点的较大值互换。此时序列变为{80,70,50,60,10,40,20,30,90}

2.    当i=8时,交换30与80,并将30与70交换,再与60交换,此时序列变为{70,60,50,30,10,40,20,80,90}

如图

3.    后面的变化完全类似,不解释了如图

 

归并排序

如图我们将本是无序的数组序列{16,713,10,9,15,3,2,5,8,12,1,11,4,6,14},通过两两合并排序后再合并,最终获得一个有序的数组。注意仔细观察它的形状,你会发现,它像极了一棵倒置的完全二叉树,通常涉及到完全二叉树结构的排序算法,效率一般都不低的。

 

兼并排序:就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,….,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序

代码如下:

void   MergeSort(SqList   *L)

{

          MSort(L->r,L->r,1,L->length);

}

由于我们要讲解的归并排实现需要用到递归调用,因此我们外封装一个函数。假设现在要对数组{50,10,90,30,70,40,80,60,20}进行排序,L.length=9,我们来看一下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;

                    MSort(SR,TR2,s,m);

                    MSort(SR,TR2,m+1,t);

                    Merge(TR2,TR1,s,m,t)

          }

}

1.    MSort被调用时,SR与TR1都是{50,10,90,30,70,40,80,60,20},s=1,t=9,最终我们的目的就是要将TR1中的数组排好序。

2.    第5行,显然s不等于t,执行第8~13行语句块。

3.    第9行,m=(1+9)/2=5。m就是序列的正中间下标。

4.    此时第10行,调用MSort(ST,TR2,1,5);的目标就是将数组SR中的第1~5的关键字归并到有序TR2(调用前TR2为空数组),第11行,调用MSort(SR,TR2,6,9)调用的目标就是将数组SR中的第6~9的关键字归并到有序TR2。也就说,在调用这两句代码之前,代码已经准备将数组分成两组了

5.    第12行,函数Merge代码细节一会再讲,调用Merge(TR2,TR1,1,5,9)目标其实就是将第10和11行代码获得的数组TR2(注意它是下标为1~5和6~9的关键字分别有序)归并为TR1,此时相当于整个排序就已经完成了

6.    再来看第10行递归调用进去后,s=1,t=5,m=(1+5)/2=3。此时相当于将5个记录拆分为三个和两个。继续递归进去,直到细分为一个记录填入TR2,此时s与t相等,递归返回,每次递归返回后都会执行当前递归函数的第12行,将TR2归并到TR1中,最终使得当前序列有序。

 

7.    同样的第11行也是类似方式

8.    此时也就是刚才所讲的最后一次执行第12行代码,将{10,30,50,70,90}与{20,40,60,80}归并为最终有序的序列

 

/*将有序的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++)

               {

                       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+1]=SR[i+1];

              }

               if(j<=n)

              {

                         for(l=0;l<=n-j;l++)

                             TR[k+1]=SR[j+1];

               }

}

1.    假设我们此时调用的Merge就是将{10,30,50,70,90}与{20,40,60,80}归并为最终有序的序列,因此数组SR为{10,30,50,70,90,20,40,60,80},i=1,m=5,n=9。

2.    第4行,for循环,j由m+1=6开始到9,i由1开始到5,k由1开始每次加1,k值用于目标数组TR的下标。

3.    第6行,SR[i]=SR[1]=10,SR[j]=SR[6]=20,SR[i]<SR[j],执行第7行,TR[k]=TR[1]=10,并且i++

4.    再次循环,k++得到k=2,SR[i]=SR[2]=30,SR[j]=SR[6]=20,并且j++,如图

5.    再次循环,k++得到k=3,SR[i]=SR[2]=30,SR[j]=SR[7]=40,SR[i]<SR[j],执行第7行,TR[k]=TR[3]=30,并且i++,如图

6.    接下来完全相同的操作,一直到j++后,j=10,大于9退出循环,如图

7.    第11~20行代码,其实就将归并剩下的数组数据,移动到TR的后面。当前k=9,i=m=5,执行第13~20行代码,for循环l=0,TR[k+1]=90,大功造成。

就这样,我们归并排序就算是完成了一次排序工作,怎么样,和堆排序比,是不是要简单一些呢?

非递归实现归并排序

归并排序大量引用递归,尽管在代码上比较清晰,容易理解,但这会造成时间和空间上的性能损耗。我们排序追求的就是效率,有没有可能将递归转化为迭代呢?结论当然是可以的,而且改动之后性能上进一步提高,代码如下:

void   MergeSort2(SqList   *L)

{

          int*   TR=(int  *)malloc(L->length*sizeof(int));

          int    k=1;

          while(k<L->length)

          {

                   MeragePass(L->r,TR,k,L->length);

                     k=2*k;

                    MergePass(TR,L->r,k,L->length);

                     k=2*k;

          }

}

1.    程序开始执行,数组L为{50,10,90,30,70,40,80,60,20},L.length=9。

2.    第3行,我们事先申请额外的数组内存空间,用来存放归并结果。

3.    第5~11行,是一个while循环,目的是不断地归并有序序列。注意k值变化,第8行与第10行,在不断循环中,它将由1->2->4->8->16,跳出循环。

4.    第7行,此时k=1,MergePass函数将原来的无序数组两两归并入TR

5.    第8行,k=2。

6.    第9行,MergePass函数将TR中已经两两归并的有序序列再次归并回数组L.r中如下图

 

7.    第10行,k=4,因为k<9,所以继续循环,再次归并,最终执行完第7~10行,k=16,结束循环,完成排序工作

从代码中,我们能够感受到,非递归的迭代做法更加直截了当,从最小的序列开始归并直至完成。不需要像归并的递归算法一样,需要先拆分递归,再归并退出递归。代码如下:

void    MergePass(int   SR[],int  TR[],int  s,int   n)

{

      int   i=1;

     int    j=;

      while(i<n-2*s+1)

      {

                 Merge(SR,TR,i,i+s-1,i+2*s-1);

                 i=i+2*s;

       }

        if(i<n-s+1)

                 Merge(SR,TR,i,i+s-1,n);

        else

            for(j=i;j<=n;j++)

                      TR[j] = SR[j];

}

1.    程序执行。我们第一次调用MergePass(L.r,TR,L.length);此时L.r是初始无序状态,TR为新申请的空数组,k=1,L.length=9。

2.    第5~9行,循环的目的就两两归并,因s=1,n-2*s+1=8,为什么循环i从1到8,而不是9呢?就是因为两两归并,最终第9条记录定剩下来,无法归并

3.    第7行,Merge函数我们前面已经详细讲过,此时i=1,i+s-1,i+2*s=2。也就是说,我们将SR(即L.r)中第一个和第二个记录归并到TR中,然后第8行i=i+2*s=3,再循环,我们就是将第三个和第四个记录归并到TR中,一直到第七和第八个记录归并,

4.    第10~14行,主要是处理最后的尾数,第11行是说将最后剩下的多个记录归并到TR中。不过由于i=9,n-s+1=9,因此执行第13~14行。将20放入到TR数组的最后,如图

5.    再次调用MergePass时,s=2,第5~9行的循环,由第8行的i=i+2*s可知,此时i就是以4为增量进行循环了,也就是说,是将两个有两个记录的有序序列进行归并为四个记录的序列。最终再将最后剩下的第9条记录"20"插入TR。如图

6.    后面类似(多推几次就熟啦)。

使用归并排序时,尽量考虑用非递归方法。

快速排序

我们现在要学习的快速排序算法,被列为20世纪十大算法之一。

希尔排序相当于直接排序的升级,它们属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类,而快速排序其产是我们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动后面,关键字较小的记录从后面直接移动前面,从而减少总的比较次数和移动交换次数。

算法的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,则分别对这两部分记录继续进行排序,以达到整个序列有序的目的

假设要对数组{50,10,90,30,70,40,80,60,20}进行排序。我们通过代码的讲解来学习快速排序。我们来看代码:

void    QuickSort(SqList    *L)

{

       QSort(L,1,L->length);

}

又是一句代码,和归并排序一样,由于需要递归调用,因此我们外封装一个函数。现在我们来年Qosrt的实现

/*对顺序表L中的子序列L->r[low ...high]作快速排序*/

void     QSort(SqList   *L,int  low ,int   high)

{

       int  pivot;

       if(low<high)

       {

                      pivot =Partition(L,low,high);

                       QSort(L,low,pivot-1);

                       QSort(L,pivot+1,high);

        }

}

我调用这个函数时我们理解QSort(L,1,L->length)中的1和L->length它就是当前待排序的序列最小下标low和最大下标high。

Partition函数要做的,就是选取当中一个关键字,比如选择第一个关键字50,然后想尽办法将它放到一个位置,使得它左边的值都比它小,右边的值都比它大,我们将这样的关键字称为枢轴(pivot)。

在经过Paitition(L,1,9)的执行之后,数组变成{20,10,40,30,50,70,80,60,90},并返回值5给pivot,数字5表明50放置在数组下标为5的位置。此时,计算机把原来的数组变成了两个位于50左和右的小数组{20,10,40,30,}  {70,80,60,90},而后的递归调用QSort(L,1,5-1);QSort(L,5+1,9);语句其实就是对这两个数组分别进行Partition操作,直到顺序全部正确为止。

我们来看看快速排序的关键函数Partition函数

/*交换顺序表L中的子表记录,使枢轴记录到位,并返回其位置*/

/*此时在它之前的记录均不大于它*/

int     Partition(SqList  *L,int   low,int   high)

{

       int   pivotkey;

      pivotkey=L->r[row];

      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.    程序开始执行,此时low=1,high=L.length=9。第4行,我们将L.r[low]=L.r[0]=50赋值给枢轴变量pivotkey,

2.    第5~13行为while循环,目前low=1<high=9执行内部语句

3.    第7行,L.r[high]=L.r[9]=20不大于pivotkey=50,因此不执行第8行

4.    第9行交换数据L.r[1]=20,L.r[9]=50。为什么要交换,就是因为通过第7行的比较知道,L.r[high]是比pivotkey=50还要小的值,因此它应该交换到50左侧。

5.    第10行,当L.r[low]=L.r[1]=20,pivotkey=50,L.r[row]<pivotkey,因此第11行,low++,此时low=2。继续循环,L.r[2]=10<50,low++,此时low=3,L.r[3]=90>50,退出循环。

6.    第12行,交换L.r[low]=L.r[3]与L.r[high]=L.r[9]的值,使得L.r[3]=50,L.r[9]=90。此时相当于将一个比50大的值90交换到50的右边,注意此时low已经指向3,

7.    继续第5行,因为low-3<high=9执行循环体。

8.    第7行,当L.r[high]=L.r[9]=90,pivotkey=50,L.r[high]>pivotkey,因此第8行,high--,此时high=8,继续循环,L.r[8]=60>50,high--,此时high=7。L.r[7]=80>50,high--,此时high=6。L.r[6]=40<50,退出循环。

9.    第9行交换L.r[low]=L.r[3]=50与L.r[high]=L.r[6]=40的值,使得L.r[3]=40,L.r[6]=50。

10.  第10行,当L.r[low]=L.r[3]=40,pivotkey=50,L.r[low]<pivotkey,因此第11行,low++,此时low=4。继续循环L.r[4]=30<50,low++,此时low=5。L.r[5]=70>50退出循环。

11.  第12行交换L.r[row]=L.r[5]=70与L.r[high]=L.r[6]=50的值,使得L.r[5]=50,L.r[6]=70。如图

12.  再次循环。因为low=5<high=6,执行循环体后,low=high=5,退出循环如图

13.  最后第14行,返回low的值5,函数执行完成,接下来就是递归调用QSort(L,1,5-1);和QSort(L,5+1,9)。其实就是对{20,10,40,30}和{70,80,60,90}分别进行同样的Partition操作,直到顺序全部正确为止。

快速排序的优化

优化选取枢轴

如果我们排序数组是{9,1,5,8,3,7,4,6,2}由代码第4行pivotkey=L->r[row] 我们应该选取9作为第一个枢轴pivotkey。此时,经过一轮pivot=Partition(L,1,9)转换后,它只是更换了9的与2的位置,并且返回9给pivot,整个系列并没有实质性的变化如图

固定选择第一个关键字

随机选择枢轴法

三数取中法。即取三个关键字先进行排序,将中间的数作为枢轴,一般是取左端、右端和中间三个数,也可以随机取

我们来看看取左端、右端和中间三个数的实现代码,在Partition函数代码的第3行与第4行之间增加这样一段代码。

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);    //保证左边比中间的小

还有一种叫九数取中法,分三次在数组中分三次取样,三个样品各取出中数,显然这就更加保证了取到的pivotkey是比较接近中间值的关键字

优化不必要的交换

我们发现之前的例子中50这个关键字,其位置变化是1、9、3、6、5,可其实它的最终目标就是5,当中的交换其实是不需要的。因此我们可以对Partition函数的代码进行优化。

int     Partition(SqList  *L,int   low,int   high)

{

       int   pivotkey;

      pivotkey=L->r[row];

      L->r[0] =pivotkey;

      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[row];

       }

          L->r[low]=L->[0];

           return   low;

}

事实上将pivotkey备份到L.r[0]中,然后在之前是swap时,只作替换工作,最终当low和high会合,即到找到枢轴位置时,再将L.r[0]的数值赋值回L.r[low]。因为这当中少了多次交换数据的操作。如图

 

优化小数组时的排序方案

如果对于非常小的数组,其实快速排序反而不如直接插入排序来得更好。其原因在于快速排序用到了递归操作,在大量数据排序时,这点性能影响相对于它的整体算法优势而言可以忽略的,但如果数组只有几具记录需要排序时,这就成了一个大炮打蚊子的问题。因此我们改进一下QSort函数

即我们设置一个阈值,当超过这个阈值用快速排序,否则用插入排序

#define   MAX_LENGTH_INSERT_SORT   7;

void     QSort(SqList   *L,int  low ,int   high)

{

       int  pivot;

    if(high-low)> MAX_LENGTH_INSERT_SORT){

                   if(low<high){

                                   pivot =Partition(L,low,high);

                                  QSort(L,low,pivot-1);

                                   QSort(L,pivot+1,high);

                     }

     }else

            InsertSort(L);

}

优化递归操作

对QSort实施尾递归优化


 

#define   MAX_LENGTH_INSERT_SORT   7;

void     QSort(SqList   *L,int  low ,int   high)

{

       int  pivot;

    if(high-low)> MAX_LENGTH_INSERT_SORT){

                   while(low<high){

                                   pivot =Partition(L,low,high);

                                  QSort(L,low,pivot-1);

                                   low=pivot+1;

                     }

     }else

            InsertSort(L);

}

因为第一次递归以后,变量low就没有用处了,所以可以将pivot+1赋值给low,再循环后,来一次Partition(L,low,hight),其效果等同于QSort(L,pivot+1,hight)结果相同。但因采用迭代而不是递归的方法可以缩减堆栈的尝试,从而提高整体性能

猜你喜欢

转载自blog.csdn.net/lhr434348820/article/details/82901193