洗牌程序的两种实现方法比较

方法一:随机生成法

首先,我介绍一种很常见的方法:随机生成法(我自己命名的),这方法我在扫雷游戏中随机分布雷的位置时用过(思想是一样的),该方法要点就是从头开始逐个随机生成规定区域的数字,如果新生成随机数之前已经生成过就不保存该数;如果新生成的随机数之前没有生成过就保存该数;直到生成的数字的数量达到所需的数量。

实现代码如下:

size_t shuffle(char s[], int n)
{
    size_t t=0;//计算循环次数
    int c=0;
    while(c<n)
    {
        t++;
        int num = rand()%n;
        if (memchr(s,num,c) == NULL)
        {
            s[c++] = static_cast<char>(num);
        }
    }
    return t;
}


void printCards(char s[], int n)
{
    for (int i=0; i<n; i++)
    {
        cout << static_cast<int>(s[i]) << " ";
    }
    cout << endl;
}

代码中使用了memchr函数(时间复杂度可能是O(n),没找到依据),即使是O(1),它的循环生成随机数的次数是不固定的(大于等于需要生成的个数)。

方法二:交换位置法

这种方法是我昨天在参加腾讯笔试考试时候想到的,今天回到学校后在寝室测试了一番,基本思路是:先初始化一串分布的数字,然后为每个位置依次生成一个与之交换的随机位置,如果生成的随机位置不是它本身就执行交换操作。

实现代码:

void swap(int& a, int& b)
{
    a = a^b;
    b = a^b;
    a = a^b;
}

size_t shuffle2(int s[], int n)
{
    size_t t=0;//计算循环次数
    for (int i=0; i<n; i++)
    {
        t++;
        s[i] = i;
    }
    for (int i=0; i<n; i++)
    {
        t++;
        int num = rand()%n;
        if (num != i)
        {
            swap(s[num],s[i]);
        }
    }
    return t;
}

void printCards2(int s[], int n)
{
    for (int i=0; i<n; i++)
    {
        cout << s[i] << " ";
    }
    cout << endl;
}

比较:在时间上方法二比方法一快好多,因为交换位置的次数的最大值是限定了的(生成随机数的次数是固定的),而且省去了查找新生成数是否在已生成数中的时间。方法一中,当新生成的数在已生成的数中就需要从新生成一个随机数,从而随机生成数的次数是不固定的(有最小值)。

测试代码:



#include <cstdlib>
#include <ctime>
#include <cstring>
#include <iostream>
using namespace std;

const int CARDS_COUNT=54;

int main()
{
    srand((unsigned int)time(NULL));
    char s[CARDS_COUNT];
    int s2[CARDS_COUNT];
    size_t t1,t2;
    
    int n=2000;
    while (--n>0)
    {
        t2 = shuffle2(s2,CARDS_COUNT);
        printCards2(s2,CARDS_COUNT);
//        cout << "方法二循环次数:" << t2 << endl;

        t1 = shuffle(s,CARDS_COUNT);
        printCards(s,CARDS_COUNT);
//        cout << "方法一循环次数:" << t1 << endl;
        if (t1>t2)
        {
            cout << "方法二快" << endl;
        }
        else
        {
            cout << "方法一快" << endl;
        }
    }
    return 0;
}

结果:

image

我还是不能确定第二种方法是不是更好的,因为是自己想到的,我的验证也不是很完善,也许有什么其他的缺点(比如说随机性太弱),也没在其他书上看到过,如果网友们在哪看到过就告诉下我吧,方法一是在《c和c++代码精粹》中文版P97中找到的。

 

后续补充:

谢谢chncwang的回复,方法二不是完全随机的,完全随机的修改如下:

size_t shuffle22(int s[], int n)
{
    size_t t=0;//计算循环次数
    for (int i=0; i<n; i++)
    {
        t++;
        s[i] = i;
    }
    for (int i=n-1; i>0; --i)
    {
        t++;
        int num = rand()%(i+1);
        if (num != i)
        {
            swap(s[num],s[i]);
        }
    }
    return t;
}

因为"第1次移动后,第1个数还在第1个位置的概率是1/n,后续移动只会减少这个概率。所以这个算法不是完全随机的"。修改后的算法其实就是使用C++的STL<algorithm>库中的random_shuffle()函数的实现方法。取随机数的时候的范围每次都随着i的改变而变动,这样就不会减少之前的位置的数还是原来的数的概率了(即后续交换操作不会影响到已经交换过的位置)。

使用标准库<algorithm>中的random_shuffle()函数实现很简单,代码如下:

int main()
{
    vector<int> s_stl;
    for (int i=0; i<CARDS_COUNT; ++i) s_stl.push_back(i);
    random_shuffle(s_stl.begin(),s_stl.end());
    cout << "使用C++算法库:";
    for (vector<int>::iterator it=s_stl.begin(); it!=s_stl.end(); ++it)
        cout << " " << *it;
    return 0;
}

 

posted @ 2012-10-15 21:28  涵曦  阅读(11559)  评论(16编辑  收藏  举报