数据结构与算法分析 C++描述(第3版) 习题2.8 详尽分析

本文通过数学归纳法详尽分析并证明了《数据结构与算法分析C++描述》一书中习题2.8的算法3能产生等概率置换序列。通过对n=1,2,3的具体情况分析归纳至一般情况,最终证明该算法的有效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据结构与算法分析 C++描述(第3版) 习题2.8 详尽分析

Data Structures and Algorithm Analysis In C++ Third Edition By Mark Allen Weiss 转载请注明出处及作者:九天雁翎

a:因为12很明显,所以不证明了。

至于算法3,可以用数学归纳法证明,x详细证明如下:

1.n=1时,a[0]=1,都是100%,成立;

2.n=2时,

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

swap (a[i],a[ randInt(0,i)]);

第一次循环,当i=1时,此时a[0]=1,a[i]=a[1]

randInt(0,1)50%机会为050%机会为1,所以,swap(a[1],a[0])的机会为50%,即,序列为0,1; 1,0几率都为50%。 成立

3. n=3时,第二次循环时,当i=2时,此时有两种可能,原序列为0,1; 1,0的几率各为50%randInt(0,2)可能为0,1,2的几率各位1/3.此时,原序列为0,1,randInt(0,2)0,那么此序列经过swap(a[2],a[0]),最后序列为2,1,0,此序列的可能性为(1/2)*(1/3)=1/6; 同理易得,最后此序列为

210, 021, 012, 201, 120, 102的几率各位1/6,而此序列的所有排列可能为<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 15pt; HEIGHT: 17.25pt" type="#_x0000_t75"><imagedata chromakey="white" o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtmlclip1/01/clip_image001.png"></imagedata></shape>= 3 * 2 = 6,所以 此时成立.

4.假设此命题在n = k时成立,那么就是说,k前面序列共有序列<shape id="_x0000_i1025" style="WIDTH: 20.25pt; HEIGHT: 26.25pt" type="#_x0000_t75"><imagedata chromakey="white" o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtmlclip1/01/clip_image003.png"></imagedata></shape>种,且所有序列的几率为1/<shape id="_x0000_i1025" style="WIDTH: 20.25pt; HEIGHT: 26.25pt" type="#_x0000_t75"><imagedata chromakey="white" o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtmlclip1/01/clip_image003.png"></imagedata></shape>.

5.n=k+1,k+1次循环时,前面序列正好为n=k时的情况,此时i = k. randInt(0,k)共可能为0~k,k+1种可能。所以以前的每个可能序列在置换后有k+1种可能,而以前共有k种等可能序列,那么最后可能的序列为k*(k+1)种可能,并且,因为原序列为等可能的,每个等可能序列产生的序列都是k+1种,所以最后的序列也是等可能的。而当n=k+1时,应该共有<shape id="_x0000_i1025" style="WIDTH: 33.75pt; HEIGHT: 26.25pt" type="#_x0000_t75"><imagedata chromakey="white" o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtmlclip1/01/clip_image005.png"></imagedata></shape>=(k+1)*k种,所以,此命题得证。

即算法3产生的也是等置换序列。

这里最后要说明的是,证明时默认地利用了一个命题,

当原序列为互不相等的等可能序列时,新加入一个与原来序列任何数值都不相等的数值,无论这个数值放在原序列的哪个位置,都不可能使原不相等的序列相等,说的好复杂啊-_-!

基本就是这样,210, 021, 012, 201, 120, 102,这是你加入一个新的数值3,无论你把3插入序列的哪个位置,因为原来序列的排列不相等,所以,他们还是不会相等,这样,才保证了最后k个序列的k+1种可能都不相等,不会重复。

算法分析答案说的很详细,可以参考答案,即第一个算法为O(<shape id="_x0000_i1025" style="WIDTH: 44.25pt; HEIGHT: 16.5pt" type="#_x0000_t75"><imagedata chromakey="white" o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtmlclip1/01/clip_image007.png"></imagedata></shape>),第二个算法为ONlogN,第三个为线性。

实际算法的实现如下:

#include "jtianling/jtianling.h"

#include "boost/dynamic_bitset.hpp"

using namespace std;

//2.8题目中假设存在的随机数生成器

//根据其定义,此算法生成从ij的随机数,而且包括边界i,j.i=<n<=j

int randInt(int i, int j)

{

//确保i<j,不然就交换

if(i > j)

{

int temp = i;

i = j;

j = temp;

}

//确保范围正确

return rand()%(j-i+1) + i;

}

//算法,算法描述如题目.8所示,根据习惯,不对arr[]的大小做任何检验

//调用此函数的人应该确保这一点,arr的大小大于等于n

void RandArrayCreate1(int arr[], int n)

{

int temp;

for(int i=0; i<n; ++i)

{

while(true)

{

temp = randInt(1,n);

int j = 0;

while(j<i)

{

if(arr[j] == temp){ break;}

++j;

}

if(j == i)

{

arr[j] = temp;

break;

}

}

}

}

//算法,算法描述如题目.8所示,根据习惯,不对arr[]的大小做任何检验

//调用此函数的人应该确保这一点,arr的大小大于等于n

void RandArrayCreate2(int arr[], int n)

{

//为了正好与数值一一对应,浪费位算了,默认就都为flase

bool *used = new bool[n+1];

memset(used, 0, sizeof(bool) * (n+1));

int temp;

for(int i=0; i<n; ++i)

{

while(true)

{

temp = randInt(1,n);

if(used[temp] == false)

{

arr[i] = temp;

used[temp] = true;

break;

}

else

{

continue;

}

}

}

delete []used;

}

//算法,算法描述如题目.8所示,根据习惯,不对arr[]的大小做任何检验

//调用此函数的人应该确保这一点,arr的大小大于等于n

void RandArrayCreate3(int arr[], int n)

{

for(int i=0; i<n; ++i)

{

arr[i] = i + 1;

}

for(int i=0; i<n; ++i)

{

swap(arr[i], arr[randInt(0, i)] );

}

}

void RandArrayCreate2a(int arr[], int n)

{

//为了正好与数值一一对应,浪费位算了,默认就都为flase

boost::dynamic_bitset<> used(n+1);

int temp;

for(int i=0; i<n; ++i)

{

while(true)

{

temp = randInt(1,n);

if(used[temp] == false)

{

arr[i] = temp;

used.set(temp);

break;

}

else

{

continue;

}

}

}

}

void RandArrayCreate2b(int arr[], int n)

{

//为了正好与数值一一对应,浪费位算了,默认就都为flase

vector<bool> used(n+1);

int temp;

for(int i=0; i<n; ++i)

{

while(true)

{

temp = randInt(1,n);

if(used[temp] == false)

{

arr[i] = temp;

used[temp] = true;

break;

}

else

{

continue;

}

}

}

}

这里对算法的测试用另外一个测试函数来进行:

//因为这里主要是比较不同算法的实现,所以这里不区分算法及其实现,都称为算法

//比较算法的函数,其中算法函数的参数为数组(array)和整数(int)

//简写为AI,为了防止混乱,这里我没有使用函数的重载

//准备以后有新的不同参数的算法时再加入新的比较函数

//比较函数的参数解释如下:

//第一参数:各算法的用vector<>表示的函数指针数组

//第二参数:vector<int>表示的一系列算法函数第二参数,用以做算法增长比较

//主要注意的是,这里的第二参数实际也是算法函数第一参数数组的大小

void CompareAlgorithmAI(const std::vector<pfAI> &pfAIVec,const std::vector<int>& vec)

{

double timeInAll = GetTime(); //这里保存所有比较花费的时间

std::ofstream out("CompareAlogrithmAIResult.txt");//将结果保存在这里

//在文件中输入第一行

out <<"parameter" <<'/t';

for(int i=0; i<pfAIVec.size(); ++i)

{

out <<"Algorithm " <<i+1 <<'/t';

}

out <<std::endl <<std::setprecision(4) <<std::scientific;

double timeTaken; //每个算法一次消耗的时间

//以算法参数的数组个数为循环条件,每个数值让各个算法分别调用

for(int i=0; i<vec.size(); ++i)

{

out <<vec[i] <<'/t';

int *arr = new int[ vec[i] ]; //每次动态生成一个数组

for(int j=0; j<pfAIVec.size(); ++j)

{

timeTaken = GetTime();

pfAIVec[j](arr, vec[i]); //实际调用不同算法

timeTaken = GetTime() - timeTaken;

out <<timeTaken <<'/t';

}

delete []arr; //删除arr,因为只考察算法的效率,所以根本不关心结果

//至于各算法是否正确由调用者保证

out<<std::endl;

}

timeInAll = GetTime() - timeInAll;

out <<"Compared time in all: " <<timeInAll <<std::endl;

}

最后得到的结果用excel打开,实在是更加赏心悦目而且更好的可以保存下来,可以只测试一个,也可以一次测试一组,以后的算法比较中还可以重复的利用,这里利用了vector来传递参数,有利于减少参数的数量。关于GetTime的含义,请参考置顶文章

parameter

RandArrayCreate1

parameter

RandArrayCreate3

250

6.62E-04

100000

1.85E-02

500

4.61E-03

200000

3.78E-02

1000

1.59E-02

400000

5.83E-02

2000

6.70E-02

800000

1.17E-01

Compared time in all: 8.9593e-002

1600000

2.30E-01

3200000

4.61E-01

6400000

9.22E-01

Compared time in all: 1.9196e+000

parameter

RandArrayCreate2

RandArrayCreate2a

RandArrayCreate2b

250

1.30E-04

7.51E-04

1.03E-02

500

2.72E-04

1.64E-03

1.88E-02

1000

6.78E-04

3.49E-03

5.65E-02

2000

1.41E-03

9.19E-03

1.11E-01

4000

2.70E-03

1.47E-02

2.63E-01

8000

5.97E-03

3.84E-02

4.66E-01

16000

1.31E-02

7.39E-02

1.33E+00

32000

3.60E-02

1.91E-01

2.39E+00

Compared time in all: 5.0447e+000

很显然的结果,不多说了,不过书中给出要我运行的第2算法的参数实在是太大了,估计一天都运行不完,这里也可以看到,在动态bool位使用上,直接New最快,用Boost库的动态bit是其次,用标准库的vector<bool>只能用慢的出奇来形容了,当然,其实很多时候都是同样的结论,因为用new毕竟不是那么好管理,但是不需要建构析构,而用了类来管理,自然需要更多的时间资源,但是更好管理并且动态更改起来更加方便。这需要自己去权衡利弊了。

至于最E,最坏情况的分析,前两个在一直随机到重复数字的时候可以到无限大。。。汗!第三个算法是线性的,也无所谓最坏情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值