Как прочитать N случайных строк из файла без сохранения файла в памяти?

Я знаком с алгоритм чтения одной случайной строки из файла без чтения всего файла в память. Интересно, можно ли распространить эту технику на N случайных линий?

вариант использования для генератора паролей, который объединяет N случайных слов, вытащенных из файла словаря, по одному слову на строку (например,/usr/share/dict/words). Вы могли бы придумать angela.ham.lewis.pathos. Сейчас он читает весь файл словаря в массив и выборка N случайных элементов из этот массив. Я хотел бы удалить массив или любое другое хранилище в памяти файла и прочитать файл только один раз.

(нет, это не практическое упражнение оптимизации. Меня интересует алгоритм.)

обновление: Спасибо всем за ответы.

ответы делились на три категории: модификации алгоритма полного чтения, случайный поиск или индексирование строк и поиск к ним случайным образом.

случайный поиск много быстрее, и константа относительно размера файла, но распределяет на основе размера файла не по количеству слов. Он также позволяет дублировать (этого можно избежать, но это делает алгоритм O (inf)). Вот мое переосмысление моего генератора паролей с использованием этого алгоритма. Я понимаю, что, читая вперед от точки поиска, а не назад, он имеет ошибку off-by-one, если поиск падает в последней строке. Исправление оставляется в качестве упражнения для редактора.

#!/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;

и затем вот ответ Гуффы, который я искал: расширение исходного алгоритма. Медленнее, он должен читать весь файл, но распределяет по словам, позволяет фильтровать без изменения эффективности алгоритма и (я думаю) не имеет дубликатов.

#!/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;

наконец, алгоритм индекса и поиска имеет преимущество распространения по слову, а не по размеру файла. Недостатком является то, что он считывает весь файл и масштаб использования памяти линейно с количеством слов в папка. С таким же успехом можно использовать алгоритм Гуффы.

8 ответов


алгоритм не реализован очень хорошо и ясно в этом примере... Некоторый псевдо-код, который лучше объясняет это, будет:

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
   }
}

Edit:
Вот код, реализованный в 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 раз, выбирая 5 строк из 20, и подсчитал появления строк. Вот результат:

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

как вы видите, распределение так хорошо, как вы когда-либо хотели. :)

(Я переехала создание Random объект из метода при запуске теста, чтобы избежать проблем с посевом, поскольку семя берется из системных часов.)

Примечание:
Возможно, вы захотите скремблировать порядок в результирующем массиве, если хотите, чтобы они были случайным образом упорядочены. Поскольку первые N строк расположены в порядке в массиве, они не размещаются случайным образом, если они остаются в конце. Для exmaple, если N три или больше и выбрана третья строка, она всегда будет на третьей позиции в массиве.

Edit 2:
Я изменил код, чтобы использовать List<string> вместо string[]. Это позволяет легко вставлять первые N элементов в случайном порядке. Я обновил тестовые данные с нового тестового запуска, чтобы вы могли видеть, что распределение по-прежнему хорошо.


теперь мой Perl не то, что раньше, но доверяя неявному утверждению о вашей ссылке (что распределение номеров строк, выбранных таким образом, однородно), похоже, это должно работать:

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

Как и исходный алгоритм, это однопроходная и постоянная память.

редактировать Я просто понял, что вам нужно N, а не 2. Выражение OR-ed можно повторить N раз, если заранее знать.


довольно первый раз, когда я вижу какой-то код Perl ... это невероятно нечитабельно ... ;) Но это не важно. Почему бы вам просто не повторить загадочную строку N раз?

Если бы мне пришлось написать это, я бы просто искал случайную позицию в файле, читал до конца строки (следующая новая строка), а затем читал одну строку до следующей новой строки. Добавьте некоторую обработку ошибок, если вы только что видели в последней строке, повторите все это N раз, и все готово. Я думаю

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

- Это способ Perl сделать такой один шаг. Вы также можете прочитать назад от начальной позиции до новой строки priviouse или начала файла, а затем снова прочитать строку вперед. Но это не имеет значения.

обновление

Я должен признать, что поиск где-то в файле не будет генерировать идеальное равномерное распределение из-за разных длин строк. Если эта флуктуация имеет значение, зависит от использования сценарий, конечно.

Если вам нужно идеальное равномерное распределение, вам нужно прочитать весь файл хотя бы один раз, чтобы получить количество строк. В этом случае алгоритм, заданный Guffa, вероятно, является самым умным решением, потому что он требует чтения файла ровно один раз.


Если вам не нужно делать это в рамках языка Perl Шуфа - Это очень хорошая утилита командной строки для этого. Чтобы сделать то, что вы хотите сделать:

$ shuf -n N file > newfile


быстрый и грязный Баш

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

randomLine test.sh
echo $RETURN

выберите случайную точку в файле, посмотрите назад для предыдущего 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 = '';
startp++;
return startp;

много ошибок и дерьмо там, плохое управление памятью и другие ужасы. Если это действительно компилируется, вы получаете никель. (Пожалуйста, отправьте конверт с печатью и $ 5, чтобы получить бесплатный никель.)

но вы должны уловить идею.

более длинные строки статистически имеют более высокий шанс быть выделено чем более короткие строки. Но время выполнения этого фактически постоянно, независимо от размера файла. Если у вас есть много слов в основном одинаковой длины, статистики не будут счастливы (они никогда не будут), но на практике это будет достаточно близко.


Я говорю:

  • прочитайте файл и найдите количество \n. Это количество строк-назовем это L
  • хранить свои позиции в небольшом массиве в
  • получить две случайные строки ниже, чем L, принесите их смещения, и все готово.

вы бы использовали только небольшой массив и прочитали весь файл один раз + 2 строки после этого.


вы можете сделать алгоритм 2 прохода. Сначала получите позиции каждой новой строки, толкая эти позиции в вектор. Затем выберите случайные элементы в этом векторе, назовите это i.

чтение из файла в позиции v[i] в v[i+1], чтобы получить вашу строку.

во время первого прохода Вы читаете файл с небольшим буфером, чтобы не читать его сразу в ОЗУ.