对C++的整数数组进行洗牌

本文探讨了一种改进的数组打乱算法,并与传统算法进行了对比,旨在提高效率。通过引入随机数生成技术和洗牌方法,文章详细解释了如何避免重复随机数的产生,进而提升了算法性能。此外,文章还介绍了如何使用更高精度的时间测量方法来评估算法效率。

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

转自:http://blog.sina.com.cn/s/blog_6ae5ed170100q3jg.html

本文要解决的问题是:


  1. 给定一个整数数组,如何打乱该数组的顺序?

  2. 如何确定算法的效率?

  1. 算法的实现

  《Beginning Microsoft Visual C# 2008》一书中有一种算法,我把它改写为C++的形式如下:

  const int ARRAY_SIZE = 54;

  void CheckedShuffle(int* theArray)

  {

  int newArray[ARRAY_SIZE];

  bool assigned[ARRAY_SIZE];

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

  {

  assigned[i] = false;

  }

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

  {

  int destIndex = 0;

  bool foundIndex = false;

  while (foundIndex == false)

  {

  destIndex = rand() % ARRAY_SIZE;

  if (assigned[destIndex] == false)

  foundIndex = true;

  }

  assigned[destIndex] = true;

  newArray[destIndex] = theArray[i];

  }

  memcpy(theArray, newArray, sizeof(newArray));

  }

  这种算法的思路是,对于数组中每个元素,先产生一个随机数,以这个随机数作为目标数组的索引值,将该元素复制至目标数组中。例如,第一个数组元素值为0,所产生的随机数为27,则将目标数组的第27个元素值设为0. 这种算法还使用了一个assigned数组记录已经产生过的随机数。对于每个新产生的随机数,则将assigned数组相应的位置设为true,这样,对于以后产生随机数,只要在assigned数组的对应位置的值为false,就可以复制数组元素了。

  这种算法实现起来并不困难,但算法并不高效,尤其是数组越大,越到最后,产生的废弃随机数就越多。例如,对于元素个数为54的数组,假设所有的随机数已经产生,就差39了。代码先产生一个随机数,如20,由于新数组中该位置已经有元素,则废弃20,再产生一个随机数,再比较,再废弃,直到最终产生了39为止。这一步的正确概率为1/54. 数组越大,正确概率就越低,花费的时间就越长。

  因为这种算法总是要检查以前产生的随机数,因此我将实现这种算法的函数称为CheckedShuffle.

  那么如何避免产生重复的随机数?

  玩扑克牌时,一种洗牌的方法是将牌平均分为两摊,左右手各一摊,然后两摊相对,左右手轮流插牌,这样,左边的牌就能与右边的牌相互聚到一起,达到洗牌效果。这种方式使用了交换牌位的方法,简单好用。但由于相邻的牌实际上还是不太分散,因此,效果不是很好。

  我这里采用的是一种比较怪异的洗牌方法。54张牌持在手上,由一个忠实的观众先喊出一个1-54的数,如35,则将第35张牌抽出,放在桌面上。再由观众喊出另一个数字。他这时还能喊54吗?不能了,因为手上的牌只剩53张牌,因此,他只能从喊出1-53的数字。这样,桌面上的牌越来越多,而观众能喊的范围的也越来越小。每喊一次,只要该数小于或等于手中持牌数,总是有效的。这样,当手中最后只剩一张牌时,观众就可以领奖退场了。他喊了多少次?最后一张不算,他只喊了53次。

  当然,这种方法在现实中很难做到,很费时间,但人机有别,在计算机看来,这是最受欢迎的方法!下面给出这种方法的算法。

  int GetRandNumInRange(int min, int max)

  {

  int result = rand() % (max - min + 1) + min;

  return result;

  }

  void IndexShuffle(int* theArray)

  {

  for (int i = 0; i < ARRAY_SIZE - 1; i++)

  {

  int randomIndex = GetRandNumInRange(i + 1, ARRAY_SIZE - 1);

  swap(theArray[i], theArray[randomIndex]);

  }

  }

  这种算法的思路是,对于每一张牌,与该牌其后的任意一张牌交换。例如,第1张牌与第38(随机)张牌交换后,第1张牌就固定下来了,等同于将该牌放至桌面上。然后,第2张与第43张牌交换后放至桌面,如此等等。这样,随机数的范围就从[2, 53]开始,意为从第2张牌开始,在剩下的53张中取一随机数。之后,范围缩小为[3, 53] ...,最后为[53,53].

  C++中产生随机函数只有一个rand(),所产生的数值范围为[0, 32767]。当然,很多时候,我们只需要在一个较小的特定范围内产生随机数,此时,可以通过取模的方式实现。

  rand() % 100 -> [0, 99]

  rand() % 100 + 1 -> [1, 100]

  rand() % 30 + 10 -> [0, 29] + 10 -> [0 + 10, 29 + 10] -> [10, 39]

  现在,假设我们要求得[10, 39]的随机数,如何反推出rand() % 30 + 10的公式来?

  设min = 10, max = 39,

  则[10, 39] -> [min, max] -> [0 + min, (max - min) + min] -> [0, (max - min)] + min -> rand() % (max - min + 1) + min.

  因此,rand() % (max - min + 1) + min总能生成[min, max]范围内的随机数。因为此公式表面看来难以理解且令人头晕,因此,我将其重构为一个名为GetRandNumInRange(int, int)的函数。

  swap函数在标准库algorithm中,因此不需我们再定义该函数了。

  2. 算法的效率

  算法出来了,现在我们要比较CheckedShuffle及IndexShufle这两种算法的效率。

  我先试用time_t来比较,但很可惜,time_t的精确度只支持到秒数。而这两种算法所花费的时间都是0秒。在计算机世界中,0秒并不等于不费时间,我们需要一个更高精度的时间。

  C++的标准库无法支持毫秒级的时间精度。实际上,我们这里的算法需要用微秒来衡量。所幸,Windows API中有这样的珍宝。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值