快速排序与选择

1  快速排序

1. 排序概述

排序就是将一组数据按需要排列成一个有序序列,是数据处理中一种重要的运算。排序分为升序与降序。通常把待排序的n个数据存放在一个数组中,排序后的n个数据仍存放在这n个数组元素中。最简单的排序是把存放在数组的n个数据逐个比较,必要时进行数据交换。

当i=1时,r[1]分别与其余n-1个数据r[j](j=2,3,…,n)比较,若r[i]>r[j],借助变量t实施交换,确保r[1]最小。

然后,i=2时,r[2]分别与其余n-2个数据r[j](j=3,4,…,n)比较,若r[i]>r[j],借助变量t实施交换,确保r[2]次小。

依此类推,最后当i=n-1时,r[n-1]与r[n]比较,若r[n-1]>r[n]实施交换,确保r[n]最大。

逐个比较排序进行升序排序的算法描述如下:

  for(i=1;i<=n-1;i++)

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

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

       {t=r[i];r[i]=r[j];r[j]=t;}

显然,数据比较的次数为

    s=1+2+...+(n-1)=n(n-1)/2

可见逐个比较排序的时间复杂度为O(n2)。当n很大时,排序所需时间会很长。因为逐个比较排序最简单,当n不是很大时也常使用。

为了缩减排序的时间,降低排序的时间复杂度,出现了很多新颖而有特色的排序算法,下面介绍的快速排序法就是其中之一。

2. 快速排序设计要点

快速排序又称为分区交换排序,其基本思想是分治,即分而治之:在待排序的n个数据r[1,2,…,n]中任取一个数(例如r[1])作为基准,把其余n-1个数据分为两个区,小于基准的数放在左边,大于基准的数放在右边。

这样分成的两个区实际上是待排序数据的两个子列。然后对这两个子列分别重复上述分区过程,直到所有子列只有一个元素,即所有元素排到位后,输出排序结果。

分区交换描述如下:

while(i!=j)

      { while(r[j]>=r[0] && j>i)   // 从左至右逐个检查是否大于基准  

          j=j-1;

        if(i<j) {r[i]=r[j];i=i+1;} // 把小于基准的一个数赋给r(i)  

        while(r[i]<=r[0] && j>i)   // 从右至左逐个检查是否小于基准  

          i=i+1;

        if(i<j) {r[j]=r[i];j=j-1;} // 把大于基准的一个数赋给r(j)  

      }

为了解分区交换的实施,以具体数据稍加剖析如下。

设n=12,参与排序的12个整数为:

r[1]=25,45,40,13,30,27,56,23,34,41,46,r[12]=52

  调用qk(r,1,12):

i=1,j=12,选用r[1]=25为基准,并赋给r[0],即r[0]=25,进入1——12实施分区交换的while循环:

  从左至右逐个检查大于基准25的数,至j=8,r[8]=23小于基准,则r[1]=23,i=2;

  从右至左逐个检查小于基准25的数,至i=2,r[2]=45大于基准,则r[8]=45,j=7;

  i=2,j=7,i≠j,继续while循环:

  从左至右逐个检查大于基准25的数,至j=4,r[4]=13小于基准,则r[2]=13,i=3;

  从右至左逐个检查小于基准25的数,至i=3,r[3]=40大于基准,则r[4]=40,j=3;

  i=3,j=3,i=j,结束while循环,由r[i]=r[0]定位基准为r[3]=25。

  至此,完成qk(r,1,12)的分区,即为:

r[1]=23,13,25,40,30,27,56,45,34,41,46,r[12]=52

  进一步调用qk(r,1,2)与qk(r,4,12),继续细化分区。

  例如调用qk(r,1,2):

i=1,j=2,选用r[1]=23为基准,并赋给r[0],即r[0]=23,进入1——2实施分区交换的while循环:

从左至右逐个检查大于基准23的数,至j=2,r[2]=13小于基准,则r[1]=13,i=2;

从右至左未检查出小于基准23的数;

  i=2,j=2,i=j,结束while循环,由r[i]=r[0]定位基准为r[2]=23。

  至此,完成qk(r,1,2)的分区,即为:

r[1]=13,r[2]=23

  而调用qk(r,4,12),还需作多次分区。

  所有分区完成,即升序排序完成,返回调用qk(r,1,12)处,输出排序结果。

3. 快速排序程序实现

// 递归实现快速排序  

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

void main()

{ int i,n,t,r[20001];

  void qk(int r[],int m1,int m2);   // 函数声明  

  t=time(0)%1000;srand(t);          //  随机数发生器初始化  

  printf("  input n:");

  scanf("%d",&n);

  printf("  参与排序的%d个整数为:\n",n);

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

    {r[i]=rand()%(4*n)+10;          // 随机产生并输出n个整数  

     printf("%d ",r[i]);

     }

  qk(r,1,n);

  printf("  \n  以上%d个整数从小到大排序为:\n",n);

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

    printf("%d ",r[i]);            // 输出排序结果  

  printf("\n");

}

void qk(int r[],int m1,int m2)    // 快速排序递归函数  

{ int i,j;

  if(m1<m2)

   { i=m1;j=m2;r[0]=r[i];          // 定义第i个数作为分区基准  

     while(i!=j)

      { while(r[j]>=r[0] && j>i)   // 从左至右逐个检查是否大于基准  

          j=j-1;

        if(i<j) {r[i]=r[j];i=i+1;} // 把小于基准的一个数赋给r(i)  

        while(r[i]<=r[0] && j>i)   // 从右至左逐个检查是否小于基准  

          i=i+1;

        if(i<j) {r[j]=r[i];j=j-1;} // 把大于基准的一个数赋给r(j)  

      }                            // 通过循环完成分区  

    r[i]=r[0];                     // 分区的基准为r(i)  

    qk(r,m1,i-1); qk(r,i+1,m2);    // 在两个区中继续分区  

   }

 return;

}

4. 快速排序运行示例

input n:20

参与排序的20个整数为:

78 81 25 88 32 59 19 30 72 57 52 27 34 56 69 54 61 42 43 44

以上20个整数从小到大排序为:

19 25 27 30 32 34 42 43 44 52 54 56 57 59 61 69 72 78 81 88

5. 快速排序的时间复杂度分析

设T(n)为对n 个元素快速排序进行的时间,每次分区正好把待分区间分为长度相等的两个子区间。注意到每一次分区时对每一个元素者要扫描一遍,所需时间为O(n),于是以上分区按每个区数的个数相等计算。如果每次分区时各区数的个数不一定相等,平均时间性能为O()。因而快速排序的时间复杂度O()低于逐个比较排序的时间复杂度O()。

2  选择问题

1. 问题提出

        在一个无序序列r(1),r(2),…,r(n)中,寻找第k小元素的问题称为选择。这里第k小元素是序列按升序排列后的第k个元素。特别地,当k=n/2时,即寻找位于n个元素中的中间元素,称为中值问题。

2. 算法设计

        很自然的想法是把序列实施升序排列,第k个元素即为所寻找的第k小元素。上面的快速排序算法的时间是O(),寻求比O()更省时的选择算法是我们的目标。参照上述分区交换的快速排序算法,在待选择的n个数据r[1,2,…,n]中任取一个数(例如r[1])作为基准,把其余n-1个数据分为两个区,小于基准的数放在左边,大于基准的数放在右边,基准定位在s,则

  (1) 若s=k,可知左边小于该基准的数为s-1,即基准数即为所寻求的第k小元素。

  (2) 若s>k,可知左边小于该基准的数s-1≥k,则在左边的子区继续分区。

  (3) 若s<k,可知所寻求的第k小元素在右边子区,则在右边的子区继续分区。

    依此(2)(3)继续分区,直到出现(1)结束分区,输出结果。

3. 程序实现

// 递归实现选择  

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

void main()

{ int i,j,k,n,t,r[20001];

  int s(int r[],int m1,int m2,int m);   // 函数声明  

  t=time(0)%1000;srand(t);              //  随机数发生器初始化  

  printf("  参与选择的有n个整数,请确定n: ");

  scanf("%d",&n);

  printf("  选择第k小整数,请确定k: ");

  scanf("%d",&k);

  printf("  参与选择的%d个整数为:\n",n);

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

    { t=rand()%(4*n)+10;          // 随机产生并输出n个整数 

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

 if(t==r[j]) break;

  if(j==i)

    {r[i]=t; printf("  %d",r[i]);}

  else {i--; continue;}

    }

  s(r,1,n,k);

  printf("  \n  以上%d个整数中第%d小整数为%d.\n",n,k,r[k]);

  }

int s(int r[],int m1,int m2,int m) // 快速选择递归函数  

{ int i,j;

  if(m1<m2)

   { i=m1;j=m2;r[0]=r[i];          // 定义第i个数作为分区基准  

     while(i!=j)

      { while(r[j]>=r[0] && j>i)   // 从左至右逐个检查是否大于基准  

          j=j-1;

        if(i<j) {r[i]=r[j];i=i+1;} // 把小于基准的一个数赋给r(i)  

        while(r[i]<=r[0] && j>i)   // 从右至左逐个检查是否小于基准  

       i=i+1;

        if(i<j) {r[j]=r[i];j=j-1;} // 把大于基准的一个数赋给r(j)  

      }                            // 通过循环完成分区  

    r[i]=r[0];                     // 分区的基准为r(i)

if(i==m)  return r[m];

else if(i>m) return s(r,m1,i-1,m); 

else return s(r,i+1,m2,m);     // 选择继续分区  

   }

 }


4. 程序运行示例

参与选择的有n个整数,请确定n: 15

 选择第k小整数,请确定k: 3

 参与选择的15个整数为:

 26  41  57  30  50  45  25  53  68  60  46  32  59  61  52

 以上15个整数中第3小整数为30.

5.快速选择的时间复杂度分析

设T(n)为对n 个元素分区选择所进行的时间,每次分区正好把待分区间分为长度相等的两个子区间。注意到每一次分区时对每一个元素者要扫描一遍,所需时间为O(n),于是

T(n)=2T(n/2)+n;

        =2(2T(n/4)+n/2)+n

      =4T(n/4)+2n

      ...

     =nT(1)+nlog2(n)

    =O(nlog2(n)

以上分区按每个区数的个数相等计算。如果每次分区时各区数的个数不一定相等,平均时间性能为O(),低于排序的时间复杂度O()。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值