算法导论-顺序统计-快速求第i小的元素
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
算法导论-顺序统计-快速求第i⼩的元素
⽬录
1、问题的引出-求第i个顺序统计量
2、⽅法⼀:以期望线性时间做选择
3、⽅法⼆(改进):最坏情况线性时间的选择
4、完整测试代码(c++)
5、参考资料
内容
1、问题的引出-求第i个顺序统计量
什么是顺序统计量?及中位数概念
在⼀个由元素组成的集合⾥,第i个顺序统计量(order statistic)是该集合第i⼩的元素。
例如,最⼩值是第1个顺序统计量(i=1),最⼤值是第n个顺序统计量(i=n)。
⼀个中位数(median)是它所在集合的“中点元素”。
当n为奇数时,中位数是唯⼀的;当n为偶数时,中位数有两个。
问题简单的说就是:求数组中第i⼩的元素。
那么问题来了:如何求⼀个数组⾥第i⼩的元素呢?
常规⽅法:可以⾸先进⾏排序,然后取出中位数。
由于排序算法(快排,堆排序,归并排序)效率能做到Θ(nlogn),所以,效率达不到线性;在本⽂中将介绍两种线性的算法,第⼀种期望效率是线性的,第⼆种效率较好,是在最坏情况下能做到线性效率。
见下⾯两个⼩节;
2、⽅法⼀:以期望线性时间做选择
这是⼀种分治算法:以为模型:随机选取⼀个主元,把数组划分为两部分,A[p...q-1]的元素⽐A[q]⼩,A[q+1...r]的元素⽐A[q]⼤。
与快速排序不同,如果i=q,则A[q]就是要找的第i⼩的元素,返回这个值;如果i < q,则说明第i⼩的元素在A[p...q-1]⾥;如果i > q,则说明第i⼩的元素在A[q+1...r]⾥;然后在上⾯得到的⾼区间或者低区间⾥进⾏递归求取,直到找到第i⼩的元素。
下⾯是在A[p...q]中找到第i⼩元素的伪码:
1 RandomSelect(A,p, q,k)//随机选择统计,以期望线性时间做选择
2 {
3if (p==q) return A[p];
4int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进⾏划分为两部分
5int i=pivot-p+1;
6if (i==k )return A[pivot];
7else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k⼩的数不在主元左边,则在右边递归选择
8else return RandomSelect(A,p,pivot-1,k);//第k⼩的数不在主元右边,则在左边递归选择
9 }
在最坏情况下,数组被划分为n-1和0两部分,⽽第i个元素总是落在n-1的那部分⾥,运⾏时间为Ө(n^2);但是,除了上述很⼩的概率情况,其他情况都能达到线性;在平均情况下,任何顺序统计量都可以在线性时间Θ(n)内得到。
实现代码(c++):
1//template<typename T>使⽤模板,可处理任意类型的数据
2 template<typename T>//交换数据
3void Swap(T &m,T &n)
4 {
5 T tmp;
6 tmp=m;
7 m=n;
8 n=tmp;
9 }
10
11/***********随机快速排序分划程序*************/
12 template<typename T>
13int Random_Partition(vector<T> &A,int p,int q)
14 {
15//随机选择主元,与第⼀个元素交换
16 srand(time(NULL));
17int m=rand()%(q-p+1)+p;
18 Swap(A[m],A[p]);
19//下⾯与常规快排划分⼀样
20 T x=A[p];
21int i=p;
22for (int j=p+1;j<=q;j++)
23 {
24if (A[j]<x)
25 {
26 i=i+1;
27 Swap(A[i],A[j]);
28 }
29 }
30 Swap(A[p],A[i]);
31return i;
32 }
33/***********随机选择统计函数*************/
34 template<typename T>
35 T RandomSelect(vector<T> &A,int p,int q,int k)//随机选择统计,以期望线性时间做选择
36 {
37if (p==q) return A[p];
38int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进⾏划分为两部分
39int i=pivot-p+1;
40if (i==k )return A[pivot];
41else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k⼩的数不在主元左边,则在右边递归选择
42else return RandomSelect(A,p,pivot-1,k);//第k⼩的数不在主元右边,则在左边递归选择
43 }
View Code
3、⽅法⼆(改进):最坏情况线性时间的选择
相⽐于上⾯的随机选择,我们有另⼀种类似的算法,它在最坏情况下也能达到O(n)。
它也是基于数组的划分操作,⽽且利⽤特殊的⼿段保证每次划分两边的⼦数组都⽐较平衡;与上⾯算法不同之处是:本算法不是随机选择主元,⽽是采取⼀种特殊的⽅法选择“中位数”,这样能使⼦数组⽐较平衡,避免了上述的最坏情况(Ө(n^2))。
选出主元后,后⾯的处理和上述算法⼀致。
那么问题⼜来了,这种特殊的⼿段是什么呢?
如上图所⽰:
1)将输⼊数组的n个元素划分为n/5组,每组(上图中的每列为⼀组)5个元素,且⾄多只有⼀个组有剩下的n%5个元素组成
2)⾸先对每组中的元素(5个)进⾏,然后从排序后的序列中选择出中位数(图中黄⾊数)。
3)对第2步中找出的n/5个中位数,递归调⽤SELECT以找出其中位数x(图中红⾊数)。
(如果有偶数个中位数取较⼩的中位数)
这三个步骤就可以选出⼀个很好的主元,下⾯的处理和⽅法⼀⼀致(递归)
OK! 下⾯是完整的算法步骤:
1)将输⼊数组的n个元素划分为n/5组,每组(上图中的每列为⼀组)5个元素,且⾄多只有⼀个组有剩下的n%5个元素组成
2)⾸先对每组中的元素(5个)进⾏,然后从排序后的序列中选择出中位数(图中黄⾊数)。
3)对第2步中找出的n/5个中位数,递归调⽤SELECT以找出其中位数x(图中红⾊数)。
(如果有偶数个中位数取较⼩的中位数)
4)调⽤PARTITION过程,按照中位数x对输⼊数组进⾏划分。
确定中位数x的位置k。
5)如果i=k,则返回x。
否则,如果i<k,则在地区间递归调⽤SELECT以找出第i⼩的元素,若⼲i>k,则在⾼区找第(i-k)个最⼩元素。
⼤致伪码:
1 WorseLinearSelect(vector<T> &A,int p,int q,int k)
2 {
3// 将输⼊数组的n个元素划分为n/5(上取整)组,每组5个元素,
4// 且⾄多只有⼀个组有剩下的n%5个元素组成。
5if (p==q) return A[p];
6
7int len=q-p+1;
8int medianCount=1;
9if (len>5)
10 medianCount = len%5 >0 ? len/5 + 1 : len/5;
11 vector<T> medians(medianCount);//存放每组的中位数
12
13// 寻找每个组的中位数。
⾸先对每组中的元素(⾄多为5个)进⾏插⼊排序,
14// 然后从排序后的序列中选择出中位数。
15int m=p;
16for (int j=0,m=p;j<medianCount-1;j++)
17 {
18 medians[j] = GetMedian(A,m,m+4);
19 m+=5;
20 }
21 medians[medianCount-1] = GetMedian(A,m,q);
22//对第2步中找出的n/5(上取整)个中位数,递归调⽤SELECT以找出其中位数pivot。
23//(如果是偶数去下中位数)
24int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2);
25//调⽤PARTITION过程,按照中位数pivot对输⼊数组进⾏划分。
确定中位数pivot的位置r。
26int r = partitionWithPivot(A,p,q,pivot);
27int num = r-p+1;
28//如果num=k,则返回pivot。
否则,如果k<num,则在地区间递归调⽤SELECT以找出第k⼩的元素,
29//若⼲k>num,则在⾼区找第(k-num)个最⼩元素。
30if(num==k) return pivot;
31else if (num>k) return WorseLinearSelect(A,p,r-1,k);
32else return WorseLinearSelect(A,r+1,q,k-num);
33 }
该算法在最坏情况下运⾏时间为Θ(n)
代码实现(c++):
1 template<typename T>//插⼊排序
2void insertion_sort(vector<T> &A,int p,int q)
3 {
4int i,j;
5 T key;
6int len=q-p+1;
7for (j=p+1;j<=q;j++)
8 {
9 i=j-1;
10 key=A[j];
11while (i>=p&&A[i]>key)
12 {
13 A[i+1]=A[i];
14 i--;
15 }
16 A[i+1]=key;
17 }
18 }
19/*
20 * 利⽤插⼊排序选择中位数
21*/
22 template<typename T>
23 T GetMedian(vector<T> &A,int p,int q)
24 {
25 insertion_sort(A,p,q);//插⼊排序
26return A[(q-p)/2 + p];//返回中位数,有两个中位数的话返回较⼩的那个
27 }
28/*
29 * 根据指定的划分主元pivot来划分数组
30 * 并返回主元的顺序位置
31*/
32 template<typename T>
33int partitionWithPivot(vector<T> &A,int p,int q,T piovt)
34 {
35//先把主元交换到数组⾸元素
36for (int i=p;i<q;i++)
37 {
38if (A[i] == piovt)
39 {
40 Swap(A[i],A[p]);
41break;
42 }
43 }
44//常规的快速排序划分程序
45//
46 T x=A[p];
47int i=p;
48for (int j=p+1;j<=q;j++)
49 {
50if (A[j]<x)
51 {
52 i=i+1;
53 Swap(A[i],A[j]);
54 }
55 }
56 Swap(A[p],A[i]);
57return i;
58 }
59/*
60 * 最坏情况下线性时间选择算法
61 * 此算法依然是建⽴在快速排序的划分算法基础之上的
62 * 但是与randomizedSelect算法的不同指之处,就是次算法的本质
63 * 是保证了每次划分选择的划分主元⼀定是⼀个较好的主元,算法先对数组5个⼀组进⾏分组
64 * 然后选择每组的中位数,再递归的选择各组中位数中的中位数作为数组的划分主元,以此保证划分的平衡性
65 * 选择中位数的时候必须使⽤递归调⽤的⽅法才能降低时间复杂度
66 * 从⽽保证在最坏情况下都得到⼀个好的划分
67 * 最坏情况下时间复杂度为O(n)
68*/
69 template<typename T>
70 T WorseLinearSelect(vector<T> &A,int p,int q,int k)
71 {
72// 将输⼊数组的n个元素划分为n/5(上取整)组,每组5个元素,
73// 且⾄多只有⼀个组有剩下的n%5个元素组成。
74if (p==q) return A[p];
75
76int len=q-p+1;
77int medianCount=1;
78if (len>5)
79 medianCount = len%5 >0 ? len/5 + 1 : len/5;
80 vector<T> medians(medianCount);//存放每组的中位数
81
82// 寻找每个组的中位数。
⾸先对每组中的元素(⾄多为5个)进⾏插⼊排序,
83// 然后从排序后的序列中选择出中位数。
84int m=p;
85for (int j=0,m=p;j<medianCount-1;j++)
86 {
87 medians[j] = GetMedian(A,m,m+4);
88 m+=5;
89 }
90 medians[medianCount-1] = GetMedian(A,m,q);
91//对第2步中找出的n/5(上取整)个中位数,递归调⽤SELECT以找出其中位数pivot。
92//(如果是偶数去下中位数)
93int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2);
94//调⽤PARTITION过程,按照中位数pivot对输⼊数组进⾏划分。
确定中位数pivot的位置r。
95int r = partitionWithPivot(A,p,q,pivot);
96int num = r-p+1;
97//如果num=k,则返回pivot。
否则,如果k<num,则在地区间递归调⽤SELECT以找出第k⼩的元素,
98//若⼲k>num,则在⾼区找第(k-num)个最⼩元素。
99if(num==k) return pivot;
100else if (num>k) return WorseLinearSelect(A,p,r-1,k);
101else return WorseLinearSelect(A,r+1,q,k-num);
102 }
View Code
4、完整测试代码(c++)
Select.h
1 #ifndef SELECT_HH
2#define SELECT_HH
3 template<typename T>
4class Select
5 {
6public:
7 T RandomSelect(vector<T> &A,int p,int q,int k);//期望线性时间做选择
8 T WorseLinearSelect(vector<T> &A,int p,int q,int k);//最坏情况线性时间的选择
9private:
10void Swap(T &m,T &n);//交换数据
11int Random_Partition(vector<T> &A,int p,int q);//随机快排分划
12void insertion_sort(vector<T> &A,int p,int q);//插⼊排序
13 T GetMedian(vector<T> &A,int p,int q);
14int partitionWithPivot(vector<T> &A,int p,int q,T piovt);//根据指定主元pivot来划分数据并返回主元的顺序位置 15 };
16
17 template<typename T>//交换数据
18void Select<T>::Swap(T &m,T &n)
19 {
20 T tmp;
21 tmp=m;
22 m=n;
23 n=tmp;
24 }
25
26/***********随机快速排序分划程序*************/
27 template<typename T>
28int Select<T>::Random_Partition(vector<T> &A,int p,int q)
29 {
30//随机选择主元,与第⼀个元素交换
31 srand(time(NULL));
32int m=rand()%(q-p+1)+p;
33 Swap(A[m],A[p]);
34//下⾯与常规快排划分⼀样
35 T x=A[p];
36int i=p;
37for (int j=p+1;j<=q;j++)
38 {
39if (A[j]<x)
40 {
41 i=i+1;
42 Swap(A[i],A[j]);
43 }
44 }
45 Swap(A[p],A[i]);
46return i;
47 }
48/***********随机选择统计函数*************/
49 template<typename T>
50 T Select<T>::RandomSelect(vector<T> &A,int p,int q,int k)//随机选择统计,以期望线性时间做选择
51 {
52if (p==q) return A[p];
53int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进⾏划分为两部分
54int i=pivot-p+1;
55if (i==k )return A[pivot];
56else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k⼩的数不在主元左边,则在右边递归选择
57else return RandomSelect(A,p,pivot-1,k);//第k⼩的数不在主元右边,则在左边递归选择
58 }
59
60 template<typename T>//插⼊排序
61void Select<T>::insertion_sort(vector<T> &A,int p,int q)
62 {
63int i,j;
64 T key;
65int len=q-p+1;
66for (j=p+1;j<=q;j++)
67 {
68 i=j-1;
69 key=A[j];
70while (i>=p&&A[i]>key)
71 {
72 A[i+1]=A[i];
73 i--;
74 }
75 A[i+1]=key;
76 }
77 }
78/*
79 * 利⽤插⼊排序选择中位数
80*/
81 template<typename T>
82 T Select<T>::GetMedian(vector<T> &A,int p,int q)
83 {
84 insertion_sort(A,p,q);//插⼊排序
85return A[(q-p)/2 + p];//返回中位数,有两个中位数的话返回较⼩的那个
86 }
87/*
88 * 根据指定的划分主元pivot来划分数组
89 * 并返回主元的顺序位置
90*/
91 template<typename T>
92int Select<T>::partitionWithPivot(vector<T> &A,int p,int q,T piovt)
93 {
94//先把主元交换到数组⾸元素
95for (int i=p;i<q;i++)
96 {
97if (A[i] == piovt)
98 {
99 Swap(A[i],A[p]);
100break;
101 }
102 }
103//常规的快速排序划分程序
104//
105 T x=A[p];
106int i=p;
107for (int j=p+1;j<=q;j++)
108 {
109if (A[j]<x)
110 {
111 i=i+1;
112 Swap(A[i],A[j]);
113 }
114 }
115 Swap(A[p],A[i]);
116return i;
117 }
118/*
119 * 最坏情况下线性时间选择算法
120 * 此算法依然是建⽴在快速排序的划分算法基础之上的
121 * 但是与randomizedSelect算法的不同指之处,就是次算法的本质
122 * 是保证了每次划分选择的划分主元⼀定是⼀个较好的主元,算法先对数组5个⼀组进⾏分组
123 * 然后选择每组的中位数,再递归的选择各组中位数中的中位数作为数组的划分主元,以此保证划分的平衡性
124 * 选择中位数的时候必须使⽤递归调⽤的⽅法才能降低时间复杂度
125 * 从⽽保证在最坏情况下都得到⼀个好的划分
126 * 最坏情况下时间复杂度为O(n)
127*/
128 template<typename T>
129 T Select<T>::WorseLinearSelect(vector<T> &A,int p,int q,int k)
130 {
131// 将输⼊数组的n个元素划分为n/5(上取整)组,每组5个元素,
132// 且⾄多只有⼀个组有剩下的n%5个元素组成。
133if (p==q) return A[p];
134
135int len=q-p+1;
136int medianCount=1;
137if (len>5)
138 medianCount = len%5 >0 ? len/5 + 1 : len/5;
139 vector<T> medians(medianCount);//存放每组的中位数
140
141// 寻找每个组的中位数。
⾸先对每组中的元素(⾄多为5个)进⾏插⼊排序,
142// 然后从排序后的序列中选择出中位数。
143int m=p;
144for (int j=0,m=p;j<medianCount-1;j++)
145 {
146 medians[j] = GetMedian(A,m,m+4);
147 m+=5;
148 }
149 medians[medianCount-1] = GetMedian(A,m,q);
150//对第2步中找出的n/5(上取整)个中位数,递归调⽤SELECT以找出其中位数pivot。
151//(如果是偶数去下中位数)
152int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2);
153//调⽤PARTITION过程,按照中位数pivot对输⼊数组进⾏划分。
确定中位数pivot的位置r。
154int r = partitionWithPivot(A,p,q,pivot);
155int num = r-p+1;
156//如果num=k,则返回pivot。
否则,如果k<num,则在地区间递归调⽤SELECT以找出第k⼩的元素,157//若⼲k>num,则在⾼区找第(k-num)个最⼩元素。
158if(num==k) return pivot;
159else if (num>k) return WorseLinearSelect(A,p,r-1,k);
160else return WorseLinearSelect(A,r+1,q,k-num);
161 }
162#endif
View Code
main.cpp
1 #include <iostream>
2 #include <vector>
3 #include <time.h>
4using namespace std;
5 #include "Select.h"
6#define N 10 //排序数组⼤⼩
7#define K 100 //排序数组范围0~K
8////打印数组
9void print_element(vector<int> A)
10 {
11int len=A.size();
12for (int i=0;i<len;i++)
13 {
14 std::cout<<A[i]<<"";
15 }
16 std::cout<<std::endl;
17 }
18int main()
19 {
20 Select <int> s1;
21int a[10]={23,4,34,345,3,21,45,246,98,50};
22 vector<int> vec_int(a,a+10);
23 cout<<"原始数组"<<endl;
24 print_element(vec_int);
25// 期望线性时间做选择测试
26 cout<<"期望线性时间做选择测试"<<endl;
27for(int i=1;i<=N;i++)
28 {
29int kMin=s1.RandomSelect(vec_int,0,N-1,i);
30 cout<<"第"<<i<<"⼩的数是:"<<kMin<<endl;
31 }
32//最坏情况线性时间的选择测试
33 cout<<"最坏情况线性时间的选择测试"<<endl;
34for(int i=1;i<=N;i++)
35 {
36int kMin=s1.WorseLinearSelect(vec_int,0,N-1,i);
37 cout<<"第"<<i<<"⼩的数是:"<<kMin<<endl;
38 }
39 system("PAUSE");
40return0;
41 }
View Code 5、参考资料。