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()。