C语言 从m中随机选取n个r,小算法:从N个对象中随机选择M个

小算法:从N个对象中随机选择M个

近日工作中遇到一个问题:在游戏中要求从50道题中随机选9道题出来给玩家。随即进行了一些思考,并将次过程整理成文记录于此。

从N个对象中随机选择M个作为输出是一个很常见的问题,现有的随机数生成器一般都只产生1个随机整数,因此解决这个问题的时候我们一开始可能会考虑这样解决问题:

#include

template

// 输入候选对象数组的头指针和元素个数,以及输出对象内存头指针和元素个数

int RandomSelectMfromN(const T *pSrcDatas, const int N, T *pDstDatas, const int M)

{

if( !pSrcDatas || !pDstDatas || M<=0 ) // 判断输入参数的合法性

return -1;

for( int j=0; j

{

int t = ::rand()%N;

pDstDatas[j] = pSrcDatas[t]; // 将随机选取的数据放入目标内存

}

return 0;

}

时间复杂度O(M),空间复杂性O(1)。

当不要求选出来的M个对象必须是不同的对象时,这样做是没有问题的,即使M>=N,也就是说当要求输出的对象个数,比候选对象都要多的时候,都能正确输出。

首先假定候选对象集合中的对象互不相同,如果要求选出来的对象必须不同时(M<=N,也是必须保证的)。同时,为了保证不重复,需要随机选择之后,判断该对象是否已经选过了,如果选过了,就必须重选:

template

int RandomSelectMfromN(const T *pSrcDatas, const int N, T *pDstDatas, const int M)

{

if( !pSrcDatas || !pDstDatas || M<=0 )

return -1;

if( N

{

memcpy(pDstDatas, pSrcDatas, M*sizeof(T));

return 1;

}

for( int j=0; j

{

int t = ::rand()%(N-j);

int i = 0;

T value = pSrcDatas[t];

bool bFind = false;

for( int i=0; i

{

if( value==pDstDatas[i] )

{

bFind = true;

break;

}

}

if( true==bFind )

{

--j; // 选过了,重选

continue;

}

pDstDatas[j] = value;

}

return 0;

}

时间复杂度O(M*N),空间复杂性O(1)。

这样看起来没什么问题,但当M非常接近N时,选到后期,重选的概率非常大,而且每次都要进行将近M次比较才能。如果随即数产生函数不是太好的话,就像上面代码中用的C语言函数库中的随即数生成函数,从18个数中“随机”选择17个输出这样的工作似乎让程序陷入了死循环,因为到了后期,每次随机出来的数都已经和前面已选择的数据重复了。

鉴于上面的问题,看来必须使用点技巧来解决了。在此,我想到的办法是:每次随机选择一个后,我就将这个已经选择过的对象放到一个“角落”里,并在下一次选择时拒绝跑到那个角落里去选,这样一来,就能保证每次选到的数据都不会重复了:

template

int RandomSelectMfromN(const T *pSrcDatas, const int N, T *pDstDatas, const int M)

{

if( !pSrcDatas || !pDstDatas || M<=0 )

return -1;

if( N

{

memcpy(pDstDatas, pSrcDatas, M*sizeof(T));

return 1;

}

int *pTemp = new int[N]; // 记录候选元素的下标

for( int i=0; i

pTemp[i] = i;

for( int j=0; j

{

int t = ::rand()%(N-j); // 每次都只从前N-j个对象里选

pDstDatas[j] = pSrcDatas[pTemp[t]]; //

swap(pTemp[t], pTemp[N-j-1]); // 选出一个后,就把选出来的元素放到后面去,以保证下次随机选择时不会选择到同样下标的元素。

}

delete []pTemp;

return 0;

}

时间复杂度O(M),空间复杂性O(N)。

创建一个新的数组pTemp,而不直接使用pSrcDatas,是为了不破坏pSrcDatas里的内容;pTemp只保存下标也是为了更好的节省空间,因为类型T可能要占用很大空间。

这种方法的确比上面那种方法好多了,毕竟,不管输入的数据是什么,它都能在固定的时间内完成任务,而且时间复杂度控制得很好。可惜pTemp空间的创建给程序的运行带来了一些不确定性,如果输入的候选对象数量庞大,这里的内存消耗也是很可观的;pTemp内存申请失败的话,程序无法继续运行。

不使用pTemp,就不能让选过的元素放到“选不到的地方”了。不真正进行移动操作,只是进行假想的移动,然后推算出该位置上的元素就可以了:

template

int RandomSelectMfromN(const T *pSrcDatas, const int N, T *pDstDatas, const int M)

{

if( !pSrcDatas || !pDstDatas || M<=0 )

return -1;

if( N

{

memcpy(pDstDatas, pSrcDatas, M*sizeof(T));

return 1;

}

for( int j=0; j

{

int t = ::rand()%(N-j);

int i = 0;

T value = pSrcDatas[t];

for( int i=0; i

{

if( value==pDstDatas[i] )

{

value = pSrcDatas[N-i-1];

i = -1;

}

}

pDstDatas[j] = value;

}

return 0;

}

时间复杂度O(M*M),空间复杂性O(1)。

通常情况下,即使没有发生重复,上述算法也要进行(M-1)*M/2次检测,看是否发生了重复选择,平均每次选择随机数后要检测(M-1)/2次;如果发现选了重复的对象,算法便开始计算该位置上进行完假想的“移动”操作后的真实对象,极端情况下最多(M-1)*(M-1)次检索后找到该位置上进行完假想的“移动”操作后的真实对象。因此,这也引证了一句话:小概率事件发生时,只能通过更多的计算来弥补了。

最后,附上我的测试用例:

int main(int argc, unsigned char* argv[])

{

const int N = 18;

const int M = 15;

int src[N] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19};

int dst[M];

memset( dst, 0, sizeof(int)*M );

RandomSelectMfromN(src, N, dst, M);

cout << "dst =";

for( int i=0; i

{

cout << " " << dst[i];

}

cout << "/n";

return 0;

}

也欢迎就此问题进行深入探讨!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值