对于快速排序和快速选择我之前的文章已经有详细的说明,需要了解的同学可以移步
传送门:快速排序|快速选择(BFPTR)
所谓随机化其实就是选择枢纽的时候使用随机数选择而已,实现起来很简单。但是我们使用随机数如何保证复杂度呢?
首先,我们假定随机数的确是随机的,即我们选取任何一个元素作为枢纽都是有可能的。
我们使用指示器随机变量 x i x_i xi,如果选择第 i i i个元素作为枢纽则 x i = 1 x_i=1 xi=1,否则等于0。如果还不了解什么是指示器随机变量可以看一下这篇文章,觉得讲的很好:传送门
简单来讲,利用期望的线性性质,巧妙利用指示器随机变量可以将一个复杂的问题分解为许多容易处理的简单的问题。而且它有一个很好的性质就是他的期望就是他的概率。
因为是随机算法,所以我们求解的是复杂度的期望。
随机快速排序
由快速排序算法,我们可以得到递归式为:
E
(
T
(
n
)
)
=
E
∑
i
=
1
n
x
i
(
T
(
i
−
1
)
+
T
(
n
−
i
)
+
Θ
(
n
)
)
E(T(n))=E\sum_{i=1}^n{x_i(T(i-1)+T(n-i)+\Theta(n))}
E(T(n))=Ei=1∑nxi(T(i−1)+T(n−i)+Θ(n))
由期望的线性性质:
E
(
T
(
n
)
)
=
∑
i
=
1
n
E
(
x
i
(
T
(
i
−
1
)
+
T
(
n
−
i
)
+
Θ
(
n
)
)
)
E(T(n))=\sum_{i=1}^nE({x_i(T(i-1)+T(n-i)+\Theta(n)))}
E(T(n))=i=1∑nE(xi(T(i−1)+T(n−i)+Θ(n)))
对于一个确定的
x
i
x_i
xi,就确定了当前的划分,但是对于进一步的递归和
x
i
x_i
xi没有关系,因此两者是独立的,由期望的独立性性质:
E
(
T
(
n
)
)
=
∑
i
=
1
n
E
(
x
i
)
∗
E
(
(
T
(
i
−
1
)
+
T
(
n
−
i
)
+
Θ
(
n
)
)
)
E(T(n))=\sum_{i=1}^nE(x_i)*E((T(i-1)+T(n-i)+\Theta(n)))
E(T(n))=i=1∑nE(xi)∗E((T(i−1)+T(n−i)+Θ(n)))
因为我们假定是完全随机的,所以
E
(
x
i
)
=
1
n
E(x_i)=\frac{1}{n}
E(xi)=n1,是一个常量,然后再根据期望的线性性质:
E
(
T
(
n
)
)
=
E
(
x
i
)
∗
(
∑
i
=
1
n
E
(
T
(
i
−
1
)
)
+
∑
i
=
1
n
E
(
T
(
n
−
i
)
)
+
∑
i
=
1
n
Θ
(
n
)
)
E(T(n))=E(x_i)*(\sum_{i=1}^nE(T(i-1))+\sum_{i=1}^nE(T(n-i))+\sum_{i=1}^n\Theta(n))
E(T(n))=E(xi)∗(i=1∑nE(T(i−1))+i=1∑nE(T(n−i))+i=1∑nΘ(n))
仔细观察发现两个求和式是一样的,因此我们可以合并
E
(
T
(
n
)
)
=
2
n
∗
∑
i
=
0
n
−
1
E
(
T
(
i
)
)
+
Θ
(
n
)
E(T(n))=\frac{2}{n}*\sum_{i=0}^{n-1}E(T(i))+\Theta(n)
E(T(n))=n2∗i=0∑n−1E(T(i))+Θ(n)
我们预期的时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),因此我们用代入法证明:
我们假设 E ( T ( n ) ) < = c n l o g n E(T(n))<=cnlogn E(T(n))<=cnlogn
因为当i=0和i=1的时候复杂度都是常数,所以我们将他们从式子中移去加入常数项(这样才可以将log带入)
E ( T ( n ) ) = 2 c n ∗ ∑ i = 2 n − 1 i l o g i + Θ ( n ) E(T(n))=\frac{2c}{n}*\sum_{i=2}^{n-1}ilogi+\Theta(n) E(T(n))=n2c∗i=2∑n−1ilogi+Θ(n)
因为我们想要证明的是 E ( T ( n ) ) < a n l o g n E(T(n))<anlogn E(T(n))<anlogn,我们需要想办法消去后面的 Θ ( n ) \Theta(n) Θ(n),所以我们必须在求和式上动些手脚。
下面证明:
∑
i
=
2
n
−
1
i
l
o
g
i
<
1
2
n
2
l
o
g
n
−
1
8
n
2
\sum_{i=2}^{n-1}ilogi<\frac{1}{2}n^2logn-\frac{1}{8}n^2
i=2∑n−1ilogi<21n2logn−81n2
算法导论中这里提示说可以将式子分成两半
可是愚昧的我并没有想到怎么计算(哪位大佬知道烦请告知)。但是我尝试了一下积分,发现了一个更加紧凑的上界。
我们可以将式子变成
∑
k
=
2
n
−
1
k
l
o
g
k
<
∫
2
n
−
1
k
l
o
g
k
d
k
\sum_{k=2}^{n-1}klogk<\int_{2}^{n-1}klogk\,{\rm d}k
k=2∑n−1klogk<∫2n−1klogkdk
然后我掏出了多年没有用过的高数课本,对这个式子积分,然后会得到
∑
k
=
2
n
−
1
k
l
o
g
k
<
1
2
n
2
l
o
g
n
−
1
4
l
n
2
n
2
\sum_{k=2}^{n-1}klogk<\frac{1}{2}n^2logn-\frac{1}{4ln2}n^2
k=2∑n−1klogk<21n2logn−4ln21n2
而这个式子是比上面小的,所以算是证明了吧。。。
然后将式子带入得到:
E
(
T
(
n
)
)
<
c
n
l
o
g
n
+
Θ
(
n
)
−
c
n
4
E(T(n))<cnlogn+\Theta(n)-\frac{cn}{4}
E(T(n))<cnlogn+Θ(n)−4cn
对于足够大的
c
c
c,后面的为负,即
E
(
T
(
n
)
)
<
c
n
l
o
g
n
E(T(n))<cnlogn
E(T(n))<cnlogn
对于基本情况,如果
c
c
c足够大,上式也满足,证毕。
随机快速选择
这个复杂度的证明和上面的类似,而且比上面的简答。
由快速选择算法,有递归式:
E
(
T
(
n
)
)
=
E
∑
i
=
1
n
x
i
(
T
(
m
a
x
(
i
−
1
,
n
−
i
)
)
+
Θ
(
n
)
)
E(T(n))=E\sum_{i=1}^n{x_i(T(max(i-1,n-i))+\Theta(n))}
E(T(n))=Ei=1∑nxi(T(max(i−1,n−i))+Θ(n))
之所以有
m
a
x
max
max是因为我们求取的是上界,所以我们总选取大区间
然后我们同上进行化简:
E
(
T
(
n
)
)
=
∑
i
=
1
n
E
(
x
i
(
T
(
m
a
x
(
i
−
1
,
n
−
i
)
)
+
Θ
(
n
)
)
)
E(T(n))=\sum_{i=1}^nE({x_i(T(max(i-1,n-i))+\Theta(n)))}
E(T(n))=i=1∑nE(xi(T(max(i−1,n−i))+Θ(n)))
E
(
T
(
n
)
)
=
∑
i
=
1
n
E
(
x
i
)
∗
E
(
(
T
(
m
a
x
(
i
−
1
,
n
−
i
)
)
+
Θ
(
n
)
)
)
E(T(n))=\sum_{i=1}^nE(x_i)*E((T(max(i-1,n-i))+\Theta(n)))
E(T(n))=i=1∑nE(xi)∗E((T(max(i−1,n−i))+Θ(n)))
E
(
T
(
n
)
)
=
1
n
∑
i
=
1
n
E
(
T
(
m
a
x
(
i
−
1
,
n
−
i
)
)
)
+
Θ
(
n
)
E(T(n))=\frac{1}{n}\sum_{i=1}^nE(T(max(i-1,n-i)))+\Theta(n)
E(T(n))=n1i=1∑nE(T(max(i−1,n−i)))+Θ(n)
然后我们去掉
m
a
x
max
max,即从
┌
n
/
2
┐
\ulcorner n/2 \urcorner
┌n/2┐到
n
n
n计算两边。
E
(
T
(
n
)
)
=
2
n
∑
i
=
┌
n
/
2
┐
n
E
(
T
(
i
)
)
+
Θ
(
n
)
E(T(n))=\frac{2}{n}\sum_{i=\ulcorner n/2 \urcorner}^nE(T(i))+\Theta(n)
E(T(n))=n2i=┌n/2┐∑nE(T(i))+Θ(n)
我们假设复杂度为
O
(
n
)
O(n)
O(n),即
E
(
T
(
k
)
)
<
=
c
k
E(T(k))<=ck
E(T(k))<=ck,再带入:
E
(
T
(
n
)
)
=
2
n
∑
i
=
┌
n
/
2
┐
n
c
i
+
Θ
(
n
)
E(T(n))=\frac{2}{n}\sum_{i=\ulcorner n/2 \urcorner}^nci+\Theta(n)
E(T(n))=n2i=┌n/2┐∑nci+Θ(n)
求和式是一个简单的等差数列,因此
E
(
T
(
n
)
)
=
3
c
4
n
+
Θ
(
n
)
<
=
c
n
E(T(n))=\frac{3c}{4}n+\Theta(n)<=cn
E(T(n))=43cn+Θ(n)<=cn
当
c
c
c足够大的时候上式成立。
对于基本情况,当
c
c
c足够大的时候成立,证毕。
测试
既然复杂度差不多,那么是否随机化的性能差别大吗?怀着这个疑问,我自己手动进行了测试。
快速排序
数据规模 | 1e5 | 1e6 | 1e7 |
---|---|---|---|
三者取中 | 0.020247 | 0.232556 | 2.641669 |
随机化 | 0.131858 | 1.344317 | ten thousand yearslater |
可以看出,快速选择排序我们使用三者取中的方法是比随机化快很多的。
快速选择
1e5 | 1e6 | 1e7 | |
---|---|---|---|
模拟随机化 | 0.005681 | 0.058796 | 0.560251 |
随机化 | 0.001348 | 0.016457 | 0.159499 |
BFPTR | 0.014120 | 0.147033 | 1.438184 |
可以看出,当我们进行快速选择的时候随机化的选择枢纽是最快的。也验证了我在专门介绍BFPTR算法的文章中的分析。
测试代码
因为快速排序算法的测试代码我在专门介绍快速排序的时候已经写过了,这里就不再贴了,如果需要的话加单修改一下就可以。这里贴一下快速选择算法的测试代码:
#include <iostream>
#include <ctime>
#include <cstdio>
#include <fstream>
#include <cstdlib>
using namespace std;
typedef double T;
typedef int (*FP)(T*,int,int,int); //定义函数指针数组类型
void CreatData()
{
int n=10;
FILE* file=fopen("TestFile","w");
fprintf(file,"%d\n",n);
int t;
srand(t);
for(int i=0;i<n;++i)
{
t=rand();
fprintf(file,"%d ",rand()%10);
}
fclose(file);
return ;
}
T* CreatList(int &n)
{
//printf("n=");
//CreatData();
ifstream in("TestFile");
in >> n;
T* ret = new T[n];
for(int i=0;i<n;++i)
{
in>>ret[i];
}
in.close();
return ret;
}
void Init(T* a,int l,int r)
{
srand((int)time(NULL));
int idx = rand()%(l-r)+l;
swap(a[idx],a[l]);
return;
}
void InsertSort(T* a,int l,int r)
{//插入排序
int mid=(l+r)>>1; //获得中位数就足够了
for(int i=l+1;i<=mid;++i)
{
T x=a[i]; int j=i-1;
while(j>=l && a[j]>x)
{
a[j+1]=a[j]; --j;
}
a[j+1]=x;
}
}
void InsertSort1(T* a,int l,int r)
{//插入排序
for(int i=l+1;i<r;++i)
{
T x=a[i]; int j=i-1;
while(j>=l && a[j]>x)
{
a[j+1]=a[j]; --j;
}
a[j+1]=x;
}
}
void GetPovit1(T* a,int l,int r)
{
int x; //将区间分割为[x,x+5)
int cnt=0; //有多少个中位数
for(x=l; x+5<r; x+=5)
{
InsertSort1(a,x,x+5);
swap(a[l+cnt],a[x+2]); //将当前区间的中位数放在最前面
++cnt;
}
if(x<r)
{
InsertSort1(a,x,r);
swap(a[l+cnt],a[(x+r)>>1]);
++cnt;
}
if(1 == cnt) return;
GetPovit1(a,l,l+cnt);
}
int BFPTR1(T* a,int l,int r,int k)
{
if(r-l == 1) return l; //返回找到的数字
GetPovit1(a,l,r); //五个一组递归求取中位数
T povit=a[l];
int i=l-1,j=r;
while(i<j)
{
do ++i; while(a[i]<povit);
do --j; while(a[j]>povit);
if(i<j) swap(a[i],a[j]);
}
if(j-l+1>=k) return BFPTR1(a,l,j+1,k);
else return BFPTR1(a,j+1,r,k-j+l-1);
}
int BFPTR2(T* a,int l,int r,int k)
{
if(r-l == 1) return l; //返回找到的数字
Init(a,l,r);
T povit=a[l];
int i=l,j=r;
while(i<j)
{
do ++i; while(i+1 < r && a[i]<povit);
do --j; while(a[j]>povit);
if(i<j) swap(a[i],a[j]);
}
swap(a[l],a[j]);
int num=j-l+1; //povit在当前序列中排第几
if(k == num) return j;
else if(num > k) return BFPTR2(a,l,j,k);
else return BFPTR2(a,j+1,r,k-num);
}
int BFPTR3(T* a,int l,int r,int k);
T GetPovit(T* a,int l,int r)
{
int x; //将区间分割为[x,x+5)
int cnt=0; //有多少个中位数
for(x=l; x+5<r; x+=5)
{
InsertSort(a,x,x+5);
swap(a[l+cnt],a[x+2]); //将当前区间的中位数放在最前面
++cnt;
}
if(x<r)
{
InsertSort(a,x,r);
swap(a[l+cnt],a[(x+r)>>1]);
++cnt;
}
if(1 == cnt) return l;
return BFPTR3(a,l,l+cnt,cnt/2);
}
int BFPTR3(T* a,int l,int r,int k)
{
if(r-l == 1) return l; //返回找到的数字
//五个一组递归求取中位数
int idx = GetPovit(a,l,r);
T povit = a[idx];
swap(a[l],a[idx]);
int i=l,j=r;
while(i<j)
{
do ++i; while(i+1 < r && a[i]<povit);
do --j; while(a[j]>povit);
if(i<j) swap(a[i],a[j]);
}
swap(a[l],a[j]);
int num=j-l+1; //povit在当前序列中排第几
if(k == num) return j;
else if(num > k) return BFPTR3(a,l,j,k);
else return BFPTR3(a,j+1,r,k-num);
}
void Show(T* a,int n)
{
for(int i=0;i<n;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
void Test(FP fp[])
{
for(int i=0;i<3;++i)
{
clock_t S,E;
int Time = 10;
double sum=0;
for(int j=0;j<Time;++j)
{
int n;
T* a=CreatList(n);
S=clock();
int x=fp[i](a,0,n,9);
//0 0 1 4 4 4 6 6 8 9
E=clock();
//cout<<x<<":"<<a[x]<<" ";
sum+=(double)(E-S)/CLOCKS_PER_SEC;
//cout<<"经过排序之后:"<<endl;
//Show(a,n);
delete[] a;
}
cout<<endl;
printf("BFPTR%d's times=%f\n",i+1,sum/Time);
}
}
int main()
{
FP fp[3] = {BFPTR1,BFPTR2,BFPTR3};
Test(fp);
return 0;
}