关于洗牌算法的一点思考

2012/05/04 17:04
阅读数 1.6K

最近在做梭哈这个游戏,于是自然而然会用到洗牌算法。洗牌算法网上讲的也不少了,归结起来有如下两种形式。

第一种是每次找一个随机的位置,将54个数依次放到找到的位置中,其思路大概这样的:

1、用一个Bool型数组记录各个位置是否已经放置了数,如果放置则置true,没有则为false。在算法开始时数组初始化为false。
2、每次产生一个0~53的随机数,看这个位置是否已经放置了数,如果已经放置了,则继续用同样的方法找一个随机位置判断;如果这个位置还未放置,则设置此位置,并标记其已经放置。
3、反复执行(2)直到所有的位置都放置了数为止。只要设置成功54次数就说明所有位置已经设置了数。

它的一个例子:

void shuffle(int dest[],int n)
{
    int pos,card;
    memset(dest,0,sizeof(int)*n);
    for(card=1;card<=n;card++)
    {
            do
           {
                  pos=rand()%(n+1);
                 
           }while(dest[pos]!=0);
           dest[pos]=card;
    }    
}

这个算法其实是很变态的,它套了两层循环,而且随着空位置越来越少,寻找就会越来越难。因此这算法的时间复杂度和空间复杂度都非常大。

我曾经做猜数字这个游戏时,就是用类似的算法来产生4个不相同的随机数,结果用的时间非常久……

第二种算法从思路和实现上都很简单,而且计算量也不是很大,就是先对数组进行初始化然后随机交换两个位置,共交换n次,n越大,越接近随机。

void shuffle ( int a[], int n )
{
    int tmp = 0, p1, p2;
    int cnt = rand() % 1023;
    while (cnt--)
    {
           p1 = rand() % n;
           p2 = rand() % n;          
           tmp = a[p1];
           a[p1] = a[p2];
           a[p2] = tmp;
    }
}

这个算法比第一个要好很多,他不需要用额外的空间来记录位置是否被占用,而且只循环n次,当然执行的次数还是由n来决定。它的一个隐含的缺陷就是当n很小的时候就不会表现得很随机。

以上两个算法都是用C实现的,而我游戏中用的是C++,而且我的程序中也不是这样洗牌的。事实上我根本没有洗牌。所有牌是按顺序储存在vector 中的,并由一个类进行托管,发牌时从牌组中随机选出一个,然后将它从牌组删除,这样就节省了许多运算。但由此让我想到了另一个洗牌的算法:

这种算法基于链表结构,并且利用C++中的STL中已经实现并封装的功能和算法,而无需考虑那些基本功能的实现。它的基本思想是初始化一个 list,顺序加入所有牌,即0~53个数。然后从这组数中随机抽取一个加到另一个list或vector中,这个过程一共执行54次。

下面给出vector和list两种储存结构的算法实现,在此之前先定义一些类型以方便操作:

typedef vector<int> IntVector;
typedef vector<int> IntList;
typedef unsigned int VIndex;

基于vector的洗牌算法

void vectorShuffle(IntVector &unshuffled,IntVector &shuffled)
{
 VIndex _p,_size=unshuffled.size();
 while(_size)
 {
  _p=rand()%_size--;
  shuffled.push_back(unshuffled[_p]);
  unshuffled.erase(unshuffled.begin()+_p);
 }
}

基于list的洗牌算法
void listShuffle(IntList &unshuffled,IntList &shuffled)
{
 VIndex _p,_size=unshuffled.size();
 IntList::iterator _c;
 while(_size)
 {
  _p=rand()%_size--;
  _c=unshuffled.begin()+_p;
  shuffled.push_back(*_c);
  unshuffled.erase(_c);
 }
}

其中变量unshuffled是初始化好的数列,shuffled刚进来时是空的。

这两种算法的过程十分相似,操作上仅有些许许不同。下面一段程序用来考察这两种算法洗54张牌所用的时间:

int main()
{
 ostream_iterator<int> os(cout," ");
 clock_t start,end;
 srand(unsigned int(time(NULL)));
 IntVector c,sc;
 IntList cl,scl;
 for(VIndex i=0;i<54;i++)
 {
  c.push_back(i);
  cl.push_back(i);
 }
 cout<<"Before Shuffle"<<endl;
 copy(c.begin(),c.end(),os); cout<<endl;
 start=clock();
 vectorShuffle(c,sc);
 end=clock();
 cout<<"\nAfter Shuffled"<<endl;
 copy(sc.begin(),sc.end(),os); cout<<endl<<endl;
 cout<<"Used Time: "<<((double)(end-start))<<"ms"<<endl<<endl;

 cout<<"Before Shuffle"<<endl;
 copy(cl.begin(),cl.end(),os); cout<<endl;
 start=clock();
 listShuffle(cl,scl);
 end=clock();
 cout<<"\nAfter Shuffled"<<endl;
 copy(scl.begin(),scl.end(),os); cout<<endl<<endl;
 cout<<"Used Time: "<<((double)(end-start))<<"ms"<<endl<<endl;
 return 0;
}

从程序运行结果来看,执行算法后得到的数列是十分随机的,所用时间都在1ms以下。

但如果初始数列是54000个,那么执行vectorShuffle要用1845ms,而listShuffle要用1898ms。用的时间还是比较多的。

之前那种随机交换两个位置的元素来洗牌的算法,我把它改成了基于STL的形式

基于STL的Swap洗牌算法

void SwapShuffle(IntVector &datas, int time)
{
 VIndex _size=datas.size(),_p1,_p2;
 while(time--)
 {
  _p1=rand()%_size;
  _p2=rand()%_size;
  swap(datas[_p1],datas[_p2]);
 }
}

其中time是要执行交换的次数,如果是54张牌的话,交换次数大于27的话就已经表现出很随机的排列了。如果初始为54000个元素的话,用的时间还是比较少的,但关键的问题是这时的tiem取多少才合适。

从这里可以看出交换的方法还是比较好的。

最后要说的是STL里面的 random_shuffle 算法,这是一个封装好的现成的洗牌算法。它有两种形式

template<class RandomAccessIterator>
   void random_shuffle(
      RandomAccessIterator _First,
      RandomAccessIterator _Last
      );

template<class RandomAccessIterator, class RandomNumberGenerator>
   void random_shuffle(
      RandomAccessIterator _First,
      RandomAccessIterator _Last,
      RandomNumberGenerator& _Rand
   );

它的三个参数

_First  要进行该算法的序列的起始迭代器(泛型指针)
_Last  要进行该算法的序列的终止迭代器(泛型指针)
_Rand  特定(自定)的随机数产生器,好像是一个类

前一个形式的使用比较简单

如果定义有 vector<int> datas,那么直接调用

random_shuffle(datas.begin(),datas.end());

还可以定义起始迭代器和末尾迭代器来对序列中的某一部分进行洗牌,是十分方便的。如果datas中有54000个元素,那么执行 random_shuffle 所用的时间大概为 229ms,还是相当快的了。

至于第三个参数,我到现在还没搞清楚它是怎么用的,而且也很少有人提到……

另外在用 random_shuffle 洗牌前要先初始化随机数种子,不然程序会得到这样的结果:

执行 random_shuffle ,得到一个结果,例如 5 2 4 3 1

再 random_shuffle ,又得到一个结果,这次可能是 5 4 3 1 2

然后又调用 random_shuffle ,得到的结果和上两次不一样 2 1 3 5 4

好了当你关闭程序,然后重新运行它,你仍然会得到和上面一样的结果。

还有一点,就算初始化了随机数种子,得到的结果好像也是可以预料的,并不是真的很随机。因此我想这个算法的强度可能依赖于_Rand 这个参数。


---------------------------------------

2.  Fisher_Yates算法

void ShuffleArray_Fisher_Yates(char* arr, int len)
{
    int i = len, j;
    char temp;
 
    if ( i == 0 ) return;
    while ( --i ) {
        j = rand() % (i+1);
        temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

展开阅读全文
打赏
0
10 收藏
分享
加载中
汗,记得大一的时候帮同学做个期末"小东西"就是这个,都不好意思交上来了....
2012/05/25 16:09
回复
举报
交换洗牌,是不是在单位时间内多次交换一对位置?
2012/05/09 18:28
回复
举报
更多评论
打赏
2 评论
10 收藏
0
分享
返回顶部
顶部