我有以下C代码,但我觉得它很疯狂,必须有一种更好的方法来做到这一点,但只是想不到一个而还没有学习算法。

/*generates 1000 random numbers, lists frequency*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int count, generated_num ;
    char nums[10] = {0};
    srand(time(NULL));
    for (count = 0; count < 1000; count++) {
        generated_num = rand() % 10 + 1;
        nums[generated_num-1] += 1;
    }

    for (count = 0; count < 10; count++) {
        printf("%d occured %d times\n", count+1, nums[count]);
    }

    return 0;
}


我如何学习编写速度高效但又可读/可修改/不混乱的高效代码?

评论

您的代码的目的是什么?您想确定什么?您是要衡量rand()的行为,还是要“公平地”滚动并保持计数以查看它与均匀分布之间的差异?

您应该意识到,这实际上并没有创建统一的分布,但是有一种解决方法。对于您的应用程序是否值得这样做取决于您,但是您应该意识到这一问题。

请记住对您认为有帮助的答案进行投票。您可能还会接受最有帮助的一个答案。

如果您关心(甚至一点点)生成的随机数的质量,则应该使用比rand()+ srand更好的RNG库。提到了@Edward的模偏差,但是与兰德吮吸的效果相比很小。

实际上,在现代架构上使用char或short可能会更慢,您可以做一个基准测试来看看。您应该改用int,并且只能在不适合缓存的超大型数组中使用较小的类型

#1 楼

速度和效率

不用担心。您需要生成1000个随机数,并做一些会计工作。任何合理的解决方案,例如您的解决方案,都将具有相似的效果。

类型

为什么使用char数组来跟踪出现的次数?您要将1000个随机数放入10个容器中。如果其中一个垃圾箱中的物品超过127个怎么办?您会溢出。

总而言之,这些垃圾箱应为int类型。如果您希望代码不是一成不变的,甚至不要考虑跳过几字节的存储空间。

命名


nums实际上存储计数。在第二个循环中使用过的count实际上是指生成的数字。我建议进行以下重命名:



numsbins


counti



评论


\ $ \ begingroup \ $
您应该详细说明偏见问题及其解决方法。
\ $ \ endgroup \ $
–爱德华
2014年8月5日在18:16

\ $ \ begingroup \ $
+1表示应将计数重命名为i-非规范循环变量是我阅读代码时首先想到的东西。
\ $ \ endgroup \ $
– godlygeek
2014年8月5日19:11

\ $ \ begingroup \ $
@Edward我匆忙发布了答案。进一步想想,在大多数情况下,20亿分之一的偏差太小,不足以引起大惊小怪,因此,我撤回了部分答案。
\ $ \ endgroup \ $
– 200_success
2014年8月5日在20:04

\ $ \ begingroup \ $
@ 200_success:是否有问题取决于用例。
\ $ \ endgroup \ $
–爱德华
2014年8月5日在21:12

\ $ \ begingroup \ $
@Edward:如果在这种情况下存在偏见是一个问题,则不应首先使用rand()。
\ $ \ endgroup \ $
–加布
2014年8月6日在4:11

#2 楼

总的来说,这段代码看起来还不错,但是有一些地方可以改进。



此行:



不会初始化nums的所有元素(我认为某些编译器会在某些构建模式下默认初始化局部变量,但您不应依赖于此)。使用for循环或标准库函数memset()可以做到这一点:

char nums[10] = {0};


作为200_success注释,使用char作为数组的基本类型会使您容易溢出如果您碰巧随机生成了127个以上的给定数字(假设您的char有8位,则可能会这样做)。对于计数,通常最好使用unsigned数量,如果您知道计数永远不会为负,则应将nums声明为:

#include <string.h>
...
memset(nums, 0, sizeof(nums));



在此代码中,您要在generated_num上加1,然后在下一行将其用作索引时减去1:

unsigned int nums[10];


不进行以下操作:

generated_num = rand() % 10 + 1;
nums[generated_num-1] += 1;


可以想象,将行折叠成一条,在main的开头保存generated_num的声明:

generated_num = rand() % 10;
nums[generated_num] += 1;


还可以使用前或后递增运算符来更新生成的数字的计数:

nums[rand() % 10] += 1;



使用魔术在代码中的多个位置编号10nums数组的大小)。您应该声明以下常量之一:

nums[rand() % 10]++;


这样,如果您决定要使用11或12或37个随机数,则只需更改它即可在一个地方。



评论


\ $ \ begingroup \ $
关于您的第一点:C99 6.7.8§10明确表示它将被初始化为全零。
\ $ \ endgroup \ $
–分裂
2014年8月5日在18:39

\ $ \ begingroup \ $
char nums [10] = {0};比char nums [10]更具可读性,并且(几乎可以肯定)执行速度更快; for(int i = 0; i \ $ \ endgroup \ $
– godlygeek
2014年8月5日19:06

\ $ \ begingroup \ $
@Snowbody:他们当然可以...但是,如果他们愿意,他们还可以重新定义0或sizeof(char)。问题是您要走多远,以适应那些已经遵循了十年半的著名标准的编译器。 (我,我会说“一点也不远”。)
\ $ \ endgroup \ $
– cHao
2014年8月5日19:52



\ $ \ begingroup \ $
@Snowbody:显然,C标准甚至在C99之前都指定了相同的东西。 C89(第一个正式的C标准)说:“如果列表中的初始化程序少于聚合成员的数量,则聚合的其余部分应隐式地初始化为具有静态存储持续时间的对象。”静态对象隐式初始化的规则是“隐式初始化,就好像每个具有算术类型的成员都被分配了0,而每个具有指针类型的成员都被分配了一个空指针常量。”
\ $ \ endgroup \ $
– godlygeek
2014年8月5日在20:38

\ $ \ begingroup \ $
在C语言中,{0}构造被称为通用(零)初始化程序,因为它可以应用于每种类型(尽管gcc在复合类型上使用它时会发出不当的警告,因此似乎不喜欢它,但是这是gcc愚蠢的问题)。请使用它,这就是它的用途。
\ $ \ endgroup \ $
–托马斯
2014年8月6日在2:22



#3 楼

其他人已经指出了有关编码样式的一些内容。遵循他们的建议,您将与更专业的C代码保持一致。不幸的是,这仍然不能为您提供良好的随机数。

现在,根据您要执行的操作,可能还不错,但是我仍然想指出一点。 C函数rand是一个非常非常糟糕的随机数生成器。仅给出很少的(完整)输出,一个人已经可以推断出一些有关下一个数字的信息。

srand(time(NULL));也是一个相对不好的种子。在两台不同的计算机上同时运行该程序(几秒钟!)将得到相同的数字。短期连续运行该程序将得到相似的数字。此外,rand() % 10不会创建均匀分布的数字。 RAND_MAX通常不能被10整除,因此较低的数字将比8或9更有可能。当您要从较大的范围绘制数字时,这一点更加明显。 RAND_MAX通常仅为32k数量级,因此1-2000或类似范围内的图形编号已经明显偏向较低的数字。

tl; dr如果随机数不重要(例如电脑游戏),一切都应该没问题。如果您想进行数字或加密,请不要使用rand()


编辑:在C中获得良好的随机数有点困难...

如果要进行加密,请在linux系统上阅读/dev/random(请注意,在OSX下,/dev/random不具有加密安全性!)或在Windows系统上调用CryptGenRandom。两者都将提供“真实”随机数。对于数字,您可以(例如)使用Mersenne Twister(维基百科上的创意-公共代码)并使用真正的随机数或当前时间(最好是哈希值)以纳秒为单位播种。无论哪种情况,您都必须做一些工作才能在任何给定范围内获得均匀的分布。一种简单但无效的方法只是在需要时绘制一个新的随机数

/// @arg (*random) - a function providing (pseudo) random unsigned numbers
/// @arg randMax   - the largest number that can be returned by (*random)
/// @arg rangeMax  - the largest desired number
/// @returns a uniformly random number in [0, rangeMax]
unsigned uniform(unsigned (*random)(void), unsigned randMax, unsigned rangeMax) {
    if (rangeMax <= randMax) {
        // the following assumes, that both maxima are smaller than 2^32-1
        randMax += 1;
        rangeMax += 1;
        unsigned cutoff = (randMax / rangeMax) * rangeMax;
        unsigned result = random();
        while (result >= cutoff) {
            result = random();
        }
        return result % rangeMax;
    } else {
        // more complicated code or error
    }
}


如果您不想丢弃采样的任何随机数,则必须管理一些随机数。内部状态。这很快变得复杂,超出了此答案的范围。

评论


\ $ \ begingroup \ $
如果您提到rand的替代方法,那就太好了。
\ $ \ endgroup \ $
–RubberDuck
2014年8月6日下午0:17

\ $ \ begingroup \ $
@ ckuhn203:这可能只是一个普遍的想法。除非有人试图从头开始实现某项功能,否则C中没有太多选择。
\ $ \ endgroup \ $
– Jamal♦
14年8月6日,0:55

\ $ \ begingroup \ $
@ ckuhn203我不知道有哪个库可以为C提供简单的解决方案,但我添加了一些有关如何手动获取适当随机数的指针。
\ $ \ endgroup \ $
–示例
2014年8月6日上午10:29

#4 楼

对于一个简单的程序来说,这实际上非常简洁,但是我还有另外两件事:


在循环之前声明count,因为它是循环计数器变量。您不应该只是在顶部“列出”每个变量,因为如果不再使用它们,可能会使跟踪它们变得更加困难。
对于循环计数器,通常适用于C99之前的版本。如果您使用的是较新的版本,从而可以在loop语句中初始化计数器变量,请改为执行此操作。它分开了,尽管这并不是什么大不了的事情。但是,总的来说,重要的是仅在更多模块化程序中才在srand()中调用它(以避免重置种子)。



评论


\ $ \ begingroup \ $
我始终在开始时就声明了所有变量,因为这是我看到ii并一直这样做的方式。您对srand()表示什么?您是说要使其成为全局功能,只要它仅在一个功能中使用?
\ $ \ endgroup \ $
–user50585
14年8月5日在18:28

\ $ \ begingroup \ $
@ user50585:抱歉!我的意思是main()的顶部。我会解决的。
\ $ \ endgroup \ $
– Jamal♦
2014年8月5日在18:29

\ $ \ begingroup \ $
但是,如果有多个功能使用它,我应该放在哪里?在主要的顶部还是全球?跨多个文件呢,只有一个文件可以。
\ $ \ endgroup \ $
–user50585
2014年8月5日在18:31



\ $ \ begingroup \ $
@ user50585:这可能是从过去的习惯开始的,那时您实际上已经习惯了这样做(不确定是否适用于C ++,但适用于C)。如今,您可以这样做(int i = 0; i <1000; i ++)。它提高了可读性,并确保您不在其他地方使用迭代变量,因为您忘记了通过将其范围限制为循环本身而在循环中进行设置。
\ $ \ endgroup \ $
–user622505
2014年8月5日19:12



\ $ \ begingroup \ $
@ user50585:是的,这很糟糕。通过将变量名称(和类型)与实际使用位置分开来降低可读性,这使读者在使用变量时更难知道变量的类型。这也使删除变量的工作更加繁复-您必须从声明它的列表和使用它的地方都删除它,因此“在块开头声明的所有变量”方法趋向于离开周围有许多未使用的变量,没人愿意删除。
\ $ \ endgroup \ $
– godlygeek
2014年8月5日在19:20