如何在不将文件存储在内存中的情况下从文件中读取N个随机行?

时间:2009-04-23 00:16:46

标签: algorithm random

我熟悉the algorithm for reading a single random line from a file without reading the whole file into memory。我想知道这种技术是否可以扩展到N个随机线?

用例是一个密码生成器,它连接从字典文件中提取的N个随机字,每行一个字(如/usr/share/dict/words)。您可以提出angela.ham.lewis.pathos。现在它将整个字典文件读入一个数组,并从该数组中选择N个随机元素。我想删除该文件的数组或任何其他内存存储,并只读取该文件一次。

(不,这不是一个实际的优化练习。我对算法感兴趣。)

更新: 谢谢大家的答案。

答案分为三类:完整读取算法的修改,随机搜索或索引行并随机搜索它们。

随机搜索要快得多,并且在文件大小方面保持不变,但是根据文件大小而不是文字数量进行分配。它还允许重复(可以避免,但它使算法O(inf))。这是我使用该算法重新实现我的密码生成器。我意识到,通过从搜索点向前读取,而不是向后读取,如果搜索落在最后一行,它会有一个一个一个错误。纠正留作编辑的练习。

#!/usr/bin/perl -lw

my $Words       = "/usr/share/dict/words";
my $Max_Length  = 8;
my $Num_Words   = 4;

my $size = -s $Words;

my @words;
open my $fh, "<", $Words or die $!;

for(1..$Num_Words) {
    seek $fh, int rand $size, 0 or die $!;
    <$fh>;
    my $word = <$fh>;
    chomp $word;
    redo if length $word > $Max_Length;
    push @words, $word;
}
print join ".", @words;

然后是Guffa的答案,这就是我所寻找的;原始算法的扩展。更慢,它必须读取整个文件,但是按字分发,允许过滤而不改变算法的效率,并且(我认为)没有重复。

#!/usr/bin/perl -lw

my $Words       = "/usr/share/dict/words";
my $Max_Length  = 8;
my $Num_Words   = 4;

my @words;
open my $fh, "<", $Words or die $!;
my $count = 0;
while(my $line = <$fh>) {
    chomp $line;
    $count++;
    if( $count <= $Num_Words ) {
        $words[$count-1] = $line;
    }
    elsif( rand($count) <= $Num_Words ) {
        $words[rand($Num_Words)] = $line;
    }
}

print join ".", @words;

最后,索引和搜索算法具有按字而不是文件大小分布的优点。缺点是它读取整个文件和内存使用量与文件中的单词数量呈线性关系。不妨使用Guffa的算法。

8 个答案:

答案 0 :(得分:13)

在该示例中,该算法未以非常好的方式实现...一些更好地解释它的伪代码将是:

cnt = 0
while not end of file {
   read line
   cnt = cnt + 1
   if random(1 to cnt) = 1 {
      result = line
   }
}

如您所见,我们的想法是您阅读文件中的每一行并计算该行应该是所选行的概率。读完第一行后,概率为100%,读完第二行后概率为50%,依此类推。

这可以扩展为通过保持大小为N而不是单个变量的数组来挑选N个项目,并计算一行替换数组中当前一个的概率:

var result[1..N]
cnt = 0
while not end of file {
   read line
   cnt = cnt + 1
   if cnt <= N {
      result[cnt] = line
   } else if random(1 to cnt) <= N {
      result[random(1 to N)] = line
   }
}

编辑:
这是用C#实现的代码:

public static List<string> GetRandomLines(string path, int count) {
    List<string> result = new List<string>();
    Random rnd = new Random();
    int cnt = 0;
    string line;
    using (StreamReader reader = new StreamReader(path)) {
        while ((line = reader.ReadLine()) != null) {
            cnt++;
            int pos = rnd.Next(cnt);
            if (cnt <= count) {
                result.Insert(pos, line);
            } else {
                if (pos < count) {
                    result[pos] = line;
                }
            }
        }
    }
    return result;
}

我通过运行方法100000次,从20个中挑选5行进行测试,并计算出线的出现次数。这是结果:

25105
24966
24808
24966
25279
24824
25068
24901
25145
24895
25087
25272
24971
24775
25024
25180
25027
25000
24900
24807

如您所见,分发情况与您想要的一样好。 :)

(我在运行测试时将Random对象的创建移出了方法,以避免播种问题,因为种子是从系统时钟中取出的。)

注意:
如果您希望随机排序,可能需要对结果数组中的顺序进行加扰。由于前N行在数组中按顺序放置,如果它们保留在末尾,则不会随机放置它们。例如,如果N为3或更大并且第三行被选中,它将始终位于数组中的第三个位置。

编辑2:
我将代码更改为使用List<string>而不是string[]。这使得以随机顺序插入前N个项目变得容易。我从新的测试运行中更新了测试数据,这样您就可以看到分布仍然很好。

答案 1 :(得分:1)

我第一次看到一些Perl代码......令人难以置信的难以理解......;)但这无关紧要。你为什么不重复这段神秘的N次?

如果我必须写这个,我只是在文件中寻找一个随机位置,读到行尾(下一个换行符),然后读一行直到下一个换行符。如果您刚刚进入最后一行,请添加一些错误处理,重复所有这些N次并完成。我想

srand;
rand($.) < 1 && ($line = $_) while <>;

是Perl做这么一步的方法。你也可以从初始位置向后读到私有换行符或文件的开头,然后再读一行。但这并不重要。

<强>更新

我不得不承认,由于行长不同,寻找文件中的某个地方不会产生完美的均匀分布。如果这种波动很重要,当然取决于使用场景。

如果您需要完美的均匀分布,则需要至少读取整个文件一次以获得行数。在这种情况下,Guffa给出的算法可能是最聪明的解决方案,因为它只需要读取文件一次。

答案 2 :(得分:1)

现在我的Perl不是以前的版本,但是相信你的引用上隐含的声明(这样选择的行号的分布是统一的),它似乎应该有效:

srand;
(rand($.) < 1 && ($line1 = $_)) || (rand($.) <1 && ($line2 = $_)) while <>;

就像原始算法一样,这是一次通过和恒定的记忆。

修改 我刚刚意识到你需要N而不是2.如果你事先知道N,你可以重复OR-ed表达式N次。

答案 3 :(得分:1)

如果您不需要在Perl范围内执行此操作,shuf是一个非常好的命令行实用程序。要做你想做的事:

$ shuf -n N file > newfile

答案 4 :(得分:0)

快速而肮脏的bash

function randomLine {
  numlines=`wc -l $1| awk {'print $1'}`
  t=`date +%s`
  t=`expr $t + $RANDOM`
  a=`expr $t % $numlines + 1`
  RETURN=`head -n $a $1|tail -n 1`
  return 0
}

randomLine test.sh
echo $RETURN

答案 5 :(得分:0)

在文件中选择一个随机点,向后查找以前的EOL,向前搜索当前EOL,然后返回该行。

FILE * file = fopen("words.txt");
int fs = filesize("words.txt");
int ptr = rand(fs); // 0 to fs-1
int start = min(ptr - MAX_LINE_LENGTH, 0);
int end = min(ptr + MAX_LINE_LENGTH, fs - 1);
int bufsize = end - start;

fseek(file, start);
char *buf = malloc(bufsize);
read(file, buf, bufsize);

char *startp = buf + ptr - start;
char *finp = buf + ptr - start + 1;

while (startp > buf  && *startp != '\n') {
    startp--;
}

while (finp < buf + bufsize && *finp != '\n') {
    finp++;
}

*finp = '\0';
startp++;
return startp;

很多一次性错误和废话,糟糕的内存管理和其他恐怖。如果这实际上是编译,你得到镍。 (请发送自填邮件信封和5美元处理以获得免费镍币。)

但你应该明白这一点。

从统计上看,较长的线条比较短的线条具有更高的选择机会。但无论文件大小如何,其运行时间实际上都是恒定的。如果你有很多大致相似长度的单词,那么统计学家就不会感到高兴(他们从来都不会这样),但在实践中它会足够接近。

答案 6 :(得分:-1)

我会说:

  • 阅读文件并搜索\n的金额。那就是行数 - 让我们称之为L
  • 将他们的位置存储在内存中的小数组
  • 获取低于L的两条随机行,获取它们的偏移量,然后就完成了。

你只使用一个小数组,然后读取整个文件+ 2行。

答案 7 :(得分:-2)

你可以做2遍算法。首先获取每个换行符的位置,将这些位置推入向量。然后在该向量中选择随机项,称之为i。

从位置v [i]到v [i + 1]的文件中读取以获取您的行。

在第一次传递期间,您使用一个小缓冲区读取文件,而不是立即将其全部读入RAM。