Случайное перечисление хеш-таблицы в OCaml - PullRequest
7 голосов
/ 29 октября 2010

Извините за длинный вопрос.Я решил сначала объяснить контекст проблемы, так как, возможно, есть другие решения моей проблемы.Если вы спешите, просто прочитайте ВОПРОС ниже.

(РЕДАКТИРОВАНИЕ - Тем временем я добавил несколько попыток решить проблему. Четвертый завершает мой окончательный вывод, вы можете сразу перейти кит.)

КОНТЕКСТ

У меня есть хеш-таблица, заполненная примерно 20 тысячами пар (ключ (i), значение (i)).Я хочу генерировать случайные списки, подобные этому

[(key(213),value(213));(key(127),value(127));(key(89),value(89));...]

Ограничение состоит в том, что как только я выбрал ключ (213) в качестве первого элемента списка, не все ключи могут следовать за ним (у меня есть некоторыедругая функция 'решить', которая может решить, может ли какая-то клавиша быть следующей в списке или нет).Итак, я бы хотел выбрать случайный следующий элемент и проверить, подходит ли он - в приведенном выше примере был выбран ключ (127).В случае, если этот элемент отклонен моей функцией 'решить', я бы хотел выбрать другой случайным образом.Но я бы не хотел выбирать то же самое, только что отклоненное, потому что я знаю, что оно будет отклонено снова, и это будет не только неэффективно, но и я рискую, если только несколько ключей могут быть следующими, что займет много временипока я не найду подходящий ключ.Обратите внимание, что может быть повторением, например

[(key(213),value(213));(key(213),value(213));(key(78),value(78));...]

Это нормально, пока функция 'решает' принимает клавишу (213) как следующую в списке.Итак, все, что мне нужно, это способ случайного перечисления пар (ключ, значение) в хеш-таблице.Всякий раз, когда мне нужно выбрать ключ, я создаю перечисление, которое я использую, проверяя каждый новый элемент с помощью функции «решить» (то есть повторений не происходит), и когда я нахожу его, я добавляю его в список и продолжаю увеличивать список.,Дело в том, что я не хочу, чтобы это перечисление хеш-таблицы было одинаковым каждый раз.Я хочу, чтобы это было случайно.(Это связано со структурой пространства поиска, которая у меня есть в моей конкретной проблеме, которая здесь не актуальна.)

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

ВОПРОС

Есть ли какое-то случайноеФункция перечисления для хеш-таблиц где-нибудь?Мне известна функция BatHashtbl.enum (Библиотека батарей), но я думаю, что она всегда будет давать мне одно и то же перечисление для одной и той же хеш-таблицы (это правильно?).Кроме того, похоже, в этом модуле BatHashtbl ничего подобного не существует.Мне было бы интересно что-то вроде

random_enum: ('a, 'b) t -> int -> ('a * 'b) Enum.t

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

Спасибо за любую помощь!

Best, Surikator.

ПЕРВАЯ ПОПЫТКА

После предложения Ники в комментарияхи, просматривая более подробно библиотеку аккумуляторов, я пришел к этому

let rand_enum ht n =
BatRandom.init n;
let hte = BatHashtbl.enum ht
in let s = BatRandom.shuffle hte (* This returns*)
in Array.to_list s

, который имеет тип

val rand_enum : ('a,'b) BatHashtbl.t -> int -> ('a*'b) list

. Он использует алгоритм Фишера-Йейтса для перемешиваниякоторый работает в O (N).Он возвращает список вместо перечисления, и это довольно раздражает, потому что это означает, что даже если я доволен третьим элементом списка, полученным с помощью rand_enum, функция все равно вычислит случайное перечисление для целых 20k элементов вхеш-таблица.

Best, Surikator

ВТОРАЯ ПОПЫТКА

Я определил модуль RndHashtblEnum как

(* Random Hashtable Enumeration Module *)
type ('a,'b) t = {
   ht:('a,'b) BatHashtbl.t;
   mutable ls:('a*'b) list;
   f: (('a,'b) BatHashtbl.t -> ('a*'b) list)}

let shuffle ht =
  let hte = BatHashtbl.enum ht
  in let s = BatRandom.shuffle hte
  in Array.to_list s

let create ht n = (BatRandom.init n; {ht=ht;ls=shuffle ht;f=shuffle})

let rec next re =
match re.ls with
    | [] -> re.ls<-(re.f re.ht);next re
    | h::t -> re.ls<-t; h

Он имеет новый тип t для случайного перечисления хеш-таблиц.Этот тип хранит хеш-таблицу, которую мы хотим перечислить, список, из которого мы будем перечислять, и функцию для вычисления нового перечисляемого списка (из хеш-таблицы), когда список у нас закончится.Как только список исчерпан, когда мы запрашиваем новый случайный элемент хеш-таблицы, тип t автоматически помещает новый случайный список, созданный из хеш-таблицы.

Итак, используя вышеуказанный модуль, если мыЧтобы случайным образом перечислить хеш-таблицу, мы просто делаем:

let re = RndHashtblEnum.create ht 1236

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

let (k,v) = RndHashtblEnum.next re

, чтобы получить следующую (k, v) пару из случайного перечисления.

Один вопрос, который мы можем задать, заключается в том, является ли это на самом деле справедливой случайностью, потому что я использую оставшиесясписок для случайного перечисления хеш-таблицы при следующем случайном перечислении.Ну, это не так.Если моя хеш-таблица содержит, скажем, 1000 элементов, и после извлечения 5 случайных элементов я удовлетворен результатом, я знаю, что в следующие 995 (из второго набора извлечений) ни один из этих 5 элементов не будет извлечен.Так что это не честная случайность.Это еще хуже.Вполне может быть, что в следующих 1000 извлечениях (995 из этого списка, 5 из следующего списка перечисления) некоторые элементы не будут охвачены.В среднем алгоритм честен, но не всегда честен.

Best, Surikator.

ТРЕТЬЯ ПОПЫТКА

Привет еще раз,

Включая предложение Ники об использовании BatArray.enum и фундаментальное изменение стохастической части алгоритма, я предложил новую улучшенную версию модуля RndHashtblEnum.Это предложение:

(* Improved Random Hashtable Enumeration Module *)
type ('a,'b) t = {ht:('a,'b) BatHashtbl.t; mutable enum:('a*'b) BatEnum.t; enum0: ('a*'b) BatEnum.t}

let shuffle ht =
let hte = BatHashtbl.enum ht
in let s = BatRandom.shuffle hte
in BatArray.enum s

let create ht n =
let e = shuffle ht
in (BatRandom.init n; {ht=ht;enum=BatEnum.clone e;enum0=e})

let rec next re =
match BatEnum.get re.enum with
    | None -> re.enum<-re.enum0; next re
    | Some e -> e

Этот новый модуль избавляет от (глупых) затрат на передачу массива в список и использует алгоритм Фишера-Йейтса только один раз в начале - так что в долгосрочной перспективезапустим, мы можем считать вклад бита Фишера-Йейтса равным O (1).

Новая версия теперь справедлива с точки зрения случайности.Это не так легко увидеть, и мне понадобилось немного времени, чтобы понять это.Предположим, что хэш-таблица имеет 1000 записей.В новой версии мы всегда используем одно и то же перечисление (enum0 - исправлено, когда мы создаем случайное перечисление с помощью функции «create»).Это означает, что при попытке найти следующий элемент в нашем окончательном списке, поскольку какой-то ключ в хэш-таблице должен удовлетворять функции «решить» (в противном случае мы не смогли бы продолжить работу с алгоритмом и просто остановились бы),это будет сделано где-то между 0-й и 999-й записью.Предположим, он находится на входе 300. Теперь, когда мы выбрали этот ключ, для определения следующего ключа в окончательном списке наше перечисление будет продолжено с оставшимися 700 элементами, а затем перейдет к следующим 300 в копии того же самогоперечисление.Таким образом, 700 + 300 составляют ровно 1000 в хеш-таблице.Это означает, что мы всегда будем рассматривать каждую запись в хэш-таблице один раз и только один раз.Другое дело, что каждый раз, когда мы пытаемся найти ключ для включения в список, метка может быть найдена в записи 300, но также и в записи 734 или что-то еще, потому что функция выбора фактически зависит от того, какие предыдущие ключи были выбраны доэта точка в окончательном списке.Таким образом, каждый раз, когда мы начинаем заново искать элемент для окончательного списка в хеш-таблице, мы начинаем со случайного элемента хеш-таблицы.

Извините, если это не очень понятно.Это трудно объяснить.=)

Спасибо за все комментарии.

Best, Surikator.

ЧЕТВЕРТАЯ И ЗАКЛЮЧИТЕЛЬНАЯ ПОПЫТКА - ЭТО МОЕ ПРЕДЛАГАЕМОЕ РЕШЕНИЕ

Привет еще раз,

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

Я создал тип

type 'a node_t =
   | ENil
   | ECons of 'a * 'a list * 'a t
and 'a t = ('a node_t) Lazy.t

для ленивых случайных перечислений списков.Каждое перечисление либо пустое (ENil), либо нет (ECons), и в этом случае оно состоит из трех частей: (1) элемент, находящийся в данный момент в фокусе, (2) остальные доступные элементы для перечисления, (3) другое перечисление для продолженияэто перечисление.

Затем можно получить случайное перечисление списка, используя функцию create

let rec create ls =
lazy(   match ls with
    | [] -> ENil
    | h::t -> let n = Random.int (List.length ls)
              in let newx,rest=remove ls n
          in ECons(newx,rest,create t))

, где была определена вспомогательная функция remove для извлечения n-го элемента.списка и вернуть пару (x,ls), где x - извлеченный элемент, а ls - новый список без извлеченного элемента.Просто для полноты я добавлю здесь код функции remove.

let rec remove ls n =
let rec remove_ ls acc k n =
    match ls with
        | []        -> raise (Failure "remove")
        | h::t  -> if k=n
            then    h, List.rev_append acc t
            else remove_ t (h::acc) (k+1) n
in remove_ ls [] 0 n

Теперь мы можем определить очень простые функции для генерации следующего состояния случайного перечисления и для получения фактического элемента в каждом состоянии.перечисления.Это

exception End_of_enum
let next e =
match Lazy.force e with
    | ENil -> raise End_of_enum
    | ECons(x,ls,t) -> t
let rec get e =
match Lazy.force e with
    | ENil -> raise End_of_enum
    | ECons(x,ls,t) -> x

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

let rand_enum ht =
let ls = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ht []
in create ls

, чтобы получить случайное перечисление пар в хеш-таблице, и мы можем использовать next и get для получения пар (ключ, значение),fold, но это просто способ получить все пары (ключ, значение) хеш-таблицы в списке (спасибо Паскалю за ответ на этот вопрос ).

ThisЗавершает все перечисление хеш-таблицы.Ради полноты я добавляю также решение общей проблемы, которую я пытался решить, объясненной в «Контексте» выше.Проблема, если вы помните, состоит в том, чтобы случайным образом генерировать список пар (ключ, значение) из (1) хэш-таблицы и (2) функции decide, которая может сказать, можно ли добавить (ключ, значение)к определенному списку пар.Поскольку весь процесс генерации может никогда не завершиться, для обеспечения завершения я подумал, что имеет смысл иметь третий аргумент, который является функцией, которая сообщает, следует ли нам остановить процесс или нет (и что мы должны быть уверены, что в какой-то момент он вернет trueчтобы завершить весь процесс).

Функция generate может выглядеть примерно так:

let generate ht d stop =
let rec gen1 d fst e =
    if d (List.rev fst) (get e)
                then (get e)::fst
                else gen1 d fst (next e)
in let rec generate_ ht d stop acc =
            let e = rand_enum ht
            in  if stop acc
                        then acc
                        else    try generate_ ht d stop (gen1 d acc e)
                          with End_of_enum -> generate_ ht d stop (List.tl acc)
in generate_ ht d stop []

Большое спасибо всем, кто предоставил полезные комментарии.Это было действительно полезно.

Всего наилучшего, Surikator.

Ответы [ 4 ]

3 голосов
/ 29 октября 2010

У меня есть два предложения.Во-первых, нужно изменить функцию rand_enum, чтобы она возвращала Enum.t:

let rand_enum ht n =
BatRandom.init n;
let hte = BatHashtbl.enum ht
in Array.enum (BatRandom.shuffle hte)

, который не сильно отличается (он все еще вычисляет случайное перечисление для всех 20 КБ), но ближе к тому, что выпервоначально хотел.

В качестве альтернативы, вы всегда можете взять исходный код HashTbl и перекомпилировать его с помощью функции rand_enum.Однако это также, вероятно, не будет таким иным, поскольку HashTbl реализован в виде массива, и если вы хотите избежать плохих дубликатов, вы, вероятно, в конечном итоге будете использовать случайное перемешивание.

2 голосов
/ 29 октября 2010

Ваши реализации) (вторая и третья) слишком сложны. Мне не нравится mutable и мне не нравится Enum. Объединение их обоих - лучший способ выстрелить себе в ногу с неконтролируемыми побочными эффектами.

Я также думаю, что ваша конкретная проблема слишком специфична, чтобы ее можно было решить с помощью универсальной функции "перемешать что-то и все". Попытка найти такую ​​независимую от домена функцию, которая также решает вашу проблемную область, может быть, поэтому ваша последовательная реализация становится все уродливее и сложнее при каждой попытке.

Создать случайный поток из Hashtable просто: BatHashtbl.enum |- BatRandom.shuffle |- BatArray.enum. Остальная часть вашего кода должна касаться использования функции decide.

2 голосов
/ 29 октября 2010

Какова плотность потенциального следующего элемента? Какова стоимость вашей decide функции?

Все ваши текущие решения имеют стоимость O (n). Fisher-Yates - это O (n) (и не имеет смысла пытаться адаптировать его для Enums, так как в любом случае это потребует принудительного перечисления), а Array.to_list alos - O (n).

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

Если плотность достаточно высокая и decide дорогостоящая, я думаю, что ваша первая идея - выбирать ключи в случайном порядке и вести список уже встреченных ключей. Вы сможете выбрать первый подходящий элемент (оптимальное количество вызовов decide). Этот способ перечисления последовательности становится дорогостоящим «в конце», когда все элементы уже выбраны, но если ваша плотность высока, вы не столкнетесь с этим случаем.

Если вы не знаете, может быть интересно начать с гипотезы «высокой плотности» и передумать, как только вы увидите заданную часть таблицы и все еще ничего не нашли.

Наконец: если вам не нужно добавлять / удалять элементы во время генерации вашей последовательности, было бы интересно преобразовать вашу хеш-таблицу в массив один раз и полностью (сохраняя где-нибудь другой ключ -> таблицу индексов массива), как все такие проблемы проще, если индексирование непрерывно.

0 голосов
/ 29 октября 2010

Я сомневаюсь, что есть такая функция, учитывая интерфейс, выставленный Hashtbl.Очевидный подход, такой как получение всех значений в массиве и поиск по Array.get a (Random.int (Array.length a)), мне подходит.

...