【问题标题】:What's the best way to return a random line in a text file using C?使用 C 在文本文件中返回随机行的最佳方法是什么?
【发布时间】:2010-09-18 22:26:57
【问题描述】:

使用 C 在文本文件中返回随机行的最佳方法是什么?它必须使用标准 I/O 库 (<stdio.h>),因为它适用于 Nintendo DS 自制软件。

说明:

  • 使用文件中的标题来存储行数对于我想要做的事情不起作用。
  • 我希望它尽可能随机(最好是每条线被选中的概率与其他每条线相同。)
  • 在程序运行时文件永远不会改变。 (它是 DS,所以没有多任务处理。)

【问题讨论】:

  • 您对文件有任何控制权吗?能否在自己的机器上对文件进行预处理以统计行数,并在文件顶部添加一个标头,表明有多少完整的行?
  • 我希望文件可以在文本编辑器中轻松编辑,因此每次编辑文件时更新标题会花费太长时间。

标签: c file random stdio


【解决方案1】:

读取每一行,并使用一个随机数来选择是保留该行还是忽略它。对于第一行,您希望保持 1:1 的赔率;第二个,你想要 1:2 的赔率,等等。

count = 0;
while (fgets(line, length, stream) != NULL)
{
    count++;
    if ((rand() * count) / RAND_MAX == 0)
        strcpy(keptline, line);
}

我尚未验证这是否具有适当的随机特性,但乍一看似乎是正确的。


有人指出,整数溢出很快就会成为比较编码方式的问题,我自己也独立得出了相同的结论。可能有很多方法可以解决它,但这是首先想到的:
if ((rand() / (float)RAND_MAX) <= (1.0 / count)) 

【讨论】:

  • 第一行:50% 的机会。第 2 行:(100-50%)*(1/3)=16.7%,第 3 行:(100-50-16.7%)*(1/4)=8.3%,...这是一个有效的分布,但是它并不都是统一的(并且OP没有说它必须是统一的)。
  • 其实用这个方法,一半的时间就能拿到文件的第一行!
  • 大家都在说什么鬼?此解决方案确实以相等的概率选择每一行,无论有多少行。第一行有 1/1 的机会被选中,但以后有 (n-1)/n 的机会被替换。
  • tvanoffsen - 也许你的误解是当它“选择”一行时它不会停止 - 后续行可以替换它(概率递减)
  • 现在我了解了它的工作原理,那么使用 rand() % count == 0 作为测试呢?
【解决方案2】:

马克的回答几乎是正确的,除了两个问题:

  1. 如果一行的长度超过length - 1 个字符(包括换行符),那么while 循环将为同一行增加至少两次count:一次用于第一个length - 1 字符,另一次用于下一个length - 1 字符等
  2. rand() * count 的计算会导致整数溢出。

要解决第一个问题,您可以将fgets 调用到垃圾缓冲区,直到它返回NULL(表示I/O 错误或没有读取数据的EOF)或垃圾缓冲区包含换行符:

count = 0;
while (fgets(line, length, stream) != NULL)
{
    char *p = strchr(line, '\n');
    if (p != NULL) {
        assert(*p == '\n');
        *p = '\0'; // trim the newline
    }
    else { // haven't reached EOL yet. Read & discard the rest of the line.
#define TRASH_LENGTH 1024
        char trash[TRASH_LENGTH];
        while((p = fgets(trash, TRASH_LENGTH, stream)) != NULL) {
            if ((p = strchr(trash, '\n')) != NULL) // reached EOL
                break;
        }
    }
    assert(strchr(line, '\n') == NULL); // `line` does not contain a newline
    count++;
    // ...

如果浮点运算不可用,可以通过@tvanfosson 的建议解决第二个问题:

int one_chance_in(size_t n)
{
    if (rand() % n == 0) // `rand` returns an integer in [0, `RAND_MAX`]
        return 1;
    else
        return 0;
}

但请注意rand() % n 不是uniform, discrete random variable,即使rand() 被假定为1,因为rand() % n == 0 的概率可能比期望的概率1/@ 高1/RAND_MAX 987654336@。在我的机器上,RAND_MAX 是 2147483647,所以差是 4.66 × 10-10,但 C 标准只要求 RAND_MAX 至少为 32767(3.05 × 10-5 区别)。

另外,对于任何想知道为什么这个方案有效的人(就像我一样),如果有 m lines and generalize:在循环的第一次迭代中,第一行被复制到keptline的概率为1/1。在循环的第二次迭代中,第二行不覆盖第一行的概率为 1/2。在第三次迭代中,第三行不覆盖第一行的概率是 2/3。继续,最后一行不覆盖第一行的概率是 (m - 1)/m。因此,在遍历所有行之后,第一行保留在keptline 中的概率为:

1/1 × 1/2 × 2/3 × 3/4 × ... × (m - 2)/(m - 1) × ( m - 1)/m = 1/m

第二行留在keptline的概率是:

1/2 × 2/3 × 3/4 × ... × (m - 2)/(m - 1) × (m - 1)/m = 1/m

第三行留在keptline中的概率为:

1/3 × 3/4 × ... × (m - 2)/(m - 1) × (m - 1)/m = 1/m

等等。它们都是 1/m。

【讨论】:

  • 一个老式的单线 C:return !(rand() % n);
【解决方案3】:

这个方法很好,因为:

i) 你可以不费吹灰之力地继续生成随机线

ii) 您只需每次随机读取文件 1 次 + 1 行。多余的读取数据只等于文件的大小。

iii) 它给每一行一个公平的机会,不管它在文件中的位置。

iv) 它给每一行一个公平的机会,不管它在文件中的长度是多少。

建议:

我建议使用 2 遍算法。嗯,真的是1 pass + N 行。其中 N 是您想要的随机行数。

您将用于计算行数和每行起始位置的第一遍。

然后,您从 0 到行数减 1 取一个随机数。使用该随机数,即您的行索引,获取该行索引的起始位置。寻找那个位置。

然后您只需要再读取 1 次,并且您知道确切的大小。 (直到下一行的开始索引)

如何存储行数和每行的索引:

要存储行数,显然可以只使用 int。

如果您可以使用向量,那么您可以将每个行索引添加到向量中。如果不是,您可以创建一个整数数组,其中包含您认为会有的最大行数。然后索引到该数组。

其他答案:

另一个答案提到您可以从 1 到文件大小选择一个随机数,然后使用最接近的换行符。但这行不通。例如,您可能有 1 行非常长,而其他行则不那么长。在这种情况下,您的分布会不均匀。

【讨论】:

  • 如果您必须不断生成随机行,他的解决方案并不好。很多你不需要的额外阅读。
  • 试了几次,但你终于想出了一个不错的答案,所以我推翻了我的反对意见。在使用你的方法和马克的方法之前,我已经解决了同样的问题,它们都是很好的解决方案,最好视情况而定。
  • 如果你想要一个均匀的分布(甚至选择任何一行的几率),你需要做这样的事情。 Mark Random 严重偏向文件中的第一行(这对应用程序可能合适,也可能不合适)。
  • 随机标记?我认为以前没有人这么叫我,但在这种情况下,它适合......
  • @Mr Fooz,Mark 的解决方案(如果我解释正确的话)是解决这个问题的著名算法,出现在 TAoCP 中。起初它可能看起来有偏见,但当你计算出数学时你会发现情况并非如此。
【解决方案4】:
  1. 获取文件的长度。
  2. 在文件中随机选择一个位置。
  3. 寻找那个位置。
  4. 向前迭代直到找到换行符。
  5. 如果您没有找到换行符,请回到开头。
  6. 使用gets() 读取该行。

【讨论】:

  • 这里有一个权衡,因为您的方法将偏爱较长的行而不是较短的行。 Mark Ransom 的解决方案没有这个问题,但它需要读取整个文件(但不是一次全部)。
  • 不错的答案,但这不是一个好的解决方案,因为您不知道每行的大小。
  • 这是在 DS(或任何此类设备)的有限资源内操作的不错选择。
  • 不错。为什么不只是向后寻找呢?
  • @Draemon - 向后搜索无济于事,考虑一个文件,其中第 1 行是 98 个字节,第 2 行和第 3 行都是 1 个字节。您有 98% 的机会进入第 1 行,无论您从那里做什么,每次运行该流程时,您都有 98% 的机会进入同一行。
【解决方案5】:

我有一个替代解决方案。由于平台是 DS,您可能不想尝试将文件保存在内存中。这会读取文件两次。一次计算行数,第二次找到它想要的行。这将比目前建议的其他解决方案运行得慢,但它几乎不使用任何内存。我什至用 C 为你写了它(我省略了错误处理):

main(int argc, char **argv)
{
    FILE *f;
    int nLines = 0;
    char line[1024];
    int randLine;
    int i;

    srand(time(0));
    f = fopen(argv[1], "r");

/* 1st pass - count the lines. */
    while(!feof(f))
    {
        fgets(line, 1024, f);
        nLines++;
    }

    randLine = rand() % nLines;
    printf("Chose %d of %d lines\n", randLine, nLines);

/* 2nd pass - find the line we want. */
    fseek(f, 0, SEEK_SET);
    for(i = 0; !feof(f) && i <= randLine; i++)
        fgets(line, 1024, f);

    printf("%s", line);
}

更新:糟糕,我应该在发布此内容之前阅读 Brian R. Bondy 的回答,但我有点痴迷于编写代码并且没有注意到。这几乎相同,只是它不将行位置存储在数组中。根据文件的大小以及速度是否比节省内存更重要,您可以采用任何一种方式。

【讨论】:

    【解决方案6】:

    您需要做的就是每行生成一个未缩放的随机数,同时保持您生成的所有随机数的最大值。每当您更新最大值时,都会用当前行覆盖所选行。

    最后,您会得到与 rand() 吐出的最大数字相关联的行,这在您的所有行中应该是同样可能的。

    【讨论】:

      【解决方案7】:

      简要说明Mark Ransom 避免整数溢出的方法:DS 没有 FPU,因此浮点除法将在软件中模拟,并且非常慢。如果速度是一个问题,您将不惜一切代价避免类型转换/提升为浮动或加倍。

      这是避免任何浮点数学运算的整数溢出的另一种方法:

      if(rand() <= RAND_MAX / count)
      

      由于整数除法,概率可能略有偏差,但这在 DS 上运行肯定会更快。

      【讨论】:

        【解决方案8】:

        结合使用 Adam 的随机偏移到文件方法和 Mark 的概率方法。亚当的方法可以让你随机进入文件的一部分。然后您使用 Mark 的方法来避免偏爱较大的字符串。 Mark 的算法会优先选择从哪里开始的前几个字符串,

        【讨论】:

        • Adam 的算法有致命的缺陷,无论您尝试做什么来适应这一点,您都无法克服这样一个事实,即所选的初始位置将根据行长度而倾斜,这会破坏任何可能的均匀分布。
        • 是的,我误解了 Mark 的算法有偏差,这应该是一种获得均匀分布的廉价方法
        猜你喜欢
        • 2010-11-06
        • 2010-10-25
        • 2012-03-17
        • 1970-01-01
        • 2022-11-17
        • 1970-01-01
        • 2012-03-24
        • 1970-01-01
        • 2019-09-18
        相关资源
        最近更新 更多