问题描述:
给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素,即如果将这n个元素依其线性序排列时,排在第k个的元素即为要找到元素。
细节须知:(与之前的随笔相比)
(1)设置了对于程序运行次数的手动输入设定
(2)取消了文件的读入,直接生成随机数进行排序查找
(3)扩大了随机数的范围、数组的可申请大小
(4)时间统计精确到了微秒级
(5)运行结束后一次性写入提升了程序稳定性,写入的数据可用于数据分析图表
算法原理:
将n个输入元素划分成⌈n/5⌉个组,每组5个元素,只可能有一个组不是5个元素。用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共⌈n/5⌉个。递归调用算法Select来找出⌈n/5⌉个元素的中位数。如果⌈n/5⌉是偶数,就找它的两个中位数中较大的一个。以这个元素作为划分基准。以此递归排序找到所需的第k项。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstdlib>
4 #include<cstring>
5 #include<fstream>
6 #include<algorithm>
7 #include<windows.h>
8 #include<ctime>
9 using namespace std;
10 LARGE_INTEGER nFreq;//LARGE_INTEGER在64位系统中是LONGLONG,在32位系统中是高低两个32位的LONG,在windows.h中通过预编译宏作定义
11 LARGE_INTEGER nBeginTime;//记录开始时的计数器的值
12 LARGE_INTEGER nEndTime;//记录停止时的计数器的值
13
14 //一次快排
15 int Partition(int nums[],int p,int r,int x)
16 {
17 if(p>r) return -1;
18 //找出基准x的位置并与第一位交换
19 for(int i=p;i<=r;i++)
20 {
21 if(nums[i]==x)
22 {
23 swap(x,nums[p]);
24 break;
25 }
26 }
27 int left=p,right=r;
28 while(left<right)
29 {
30 while(left<right && nums[right]>=x) right--;
31 nums[left]=nums[right];
32 while(left<right && nums[left]<x) left++;
33 nums[right]=nums[left];
34 }
35 nums[left]=x;
36 return left;
37 }
38
39 //快速排序
40 void QuickSort(int nums[],int low,int high)
41 {
42 if(low>high) return;
43 int key=nums[low];
44 int left=low,right=high;
45 while(left<right)
46 {
47 while(left<right && nums[right]>=key) right--;
48 nums[left]=nums[right];
49 while(left<right && nums[left]<key) left++;
50 nums[right]=nums[left];
51 }
52 nums[left]=key;
53 QuickSort(nums,low,left-1);
54 QuickSort(nums,left+1,high);
55 }
56
57 int Select(int nums[],int p,int r,int k)
58 {
59 if(r-p<75)
60 {
61 QuickSort(nums,p,r);
62 return nums[p+k-1];
63 }
64 //每5个为一组,找到各组的中位数,并存储在前(r-p-4)/5个位置里
65 for(int i=0;i<=(r-p-4)/5;i++)
66 {
67 QuickSort(nums,p+5*i,p+5*i+4);
68 swap(nums[p+i],nums[p+5*i+2]);
69 }
70 //找所有中位数的中位数
71 int x=Select(nums,p,p+(r-p-4)/5,(r-p-4)/10);
72 //以x为基准做一次快排
73 int i=Partition(nums,p,r,x);
74 int j=i-p+1;
75 //判断k属于那个部分
76 if(k<=j)
77 return Select(nums,p,i,k);
78 else
79 return Select(nums,i+1,r,k-j);
80 }
81 int main(){
82 ofstream fout;
83 double cost;
84 int i = 0,m = 0,n = 0,key = 0;
85 cout<<"Please enter the number of times you want to run the program:"; //输入程序运行次数
86 cin>>m;
87 int data_amount[m];
88 double runtime[m];
89 srand((unsigned int)time(NULL)); //设置随机数种子
90 for(i=0;i<m;i++)
91 {
92 n=10000+RAND_MAX*(rand()%300)+rand(); //RAND_MAX=32767,随机生成数据量
93 data_amount[i]=n; //限定数据规模为10000~9872867
94 cout<<"☆The "<<i+1<<"th test Data amount is:"<<n<<endl;
95 int j=0;
96 int *a=(int *)malloc(n*sizeof(int));
97 for(j=0;j<n;j++){
98 a[j]=RAND_MAX*(rand()%400)+rand(); //随机生成0~13139567的随机数
99 }
100 key=rand()%10000+1; //随机生成1~10000作为所要选择的次序
101 QueryPerformanceFrequency(&nFreq);//获取系统时钟频率
102 QueryPerformanceCounter(&nBeginTime);//获取开始时刻计数值
103 int t=Select(a,0,n-1,key);
104 QueryPerformanceCounter(&nEndTime);//获取停止时刻计数值
105 cost=(double)(nEndTime.QuadPart - nBeginTime.QuadPart) / (double)nFreq.QuadPart;
106 runtime[i]=cost;
107 cout<<"The "<<key<<"th number is:"<<t<<endl;
108 cout<<"The running time is:"<<cost<<" s"<<endl;
109 free(a);
110 }
111 fout.open("data.txt");
112 if(!fout){
113 cerr<<"Can not open file 'data.txt' "<<endl;
114 return -1;
115 }
116 for(i=0;i<m;i++){
117 fout<<data_amount[i]<<","<<runtime[i]<<endl;
118 }
119 fout.close();
120 cout<<"Success!"<<endl;
121 return 0;
122 }
程序设计思路:
假设输入的数据规模为n,要查找的数据次序为k。
(1)判断数组长度,若小于75则直接进行快速排序,否则进行之后的算法。
(2)将n个输入元素划分成⌈n/5⌉个组,每组五个元素。用快速排序将每组中的元素排好序,并确定每组的中位数,共⌈n/5⌉个。
(3)递归调用算法Select来找出这⌈n/5⌉个元素的中位数。如果⌈n/5⌉是偶数,就找它的两个中位数中较大的一个。以这个元素作为划分基准。
(4)以此递归调用进行排序,最终搜索得到要找的第k项。
时间复杂性分析:
为了分析算法Select的计算时间复杂性,设n=r-p+1,即n为输入数组的长度。算法的递归调用只有在n≥75时才执行。因此,当n<75是算法Select所用的计算时间不超过一个常数C1。找到中位数的中位数x后,算法Select以x为划分基准调用Partition对数组a[p:r]进行划分,这需要O(n)时间。算法Select的for循环体行共执行n/5次,每一次需要O(1)时间。因此,执行for循环共需O(n)时间。
设对n个元素的数组调用算法Select需要T(n)时间,那么找中位数的中位数x至多用了T(n/5)的时间。已经证明了,按照算法所选的基准x进行划分所得到的2个子数组分别至多有3n/4个元素。所以,无论对哪一个子数组调用,Select都至多用了T(3n/4)的时间。
总之,可以得到关于T(n)的递归式

解此递归式可得T(n)=O(n)。
经过5000次不同规模数据的实验并统计运行时间得到如下算法效率分析图:
