【问题标题】:Random enumeration of a hash table in OCamlOCaml 中哈希表的随机枚举
【发布时间】:2011-05-02 09:41:06
【问题描述】:

抱歉,问题太长了。我决定先解释问题的背景,因为我的问题可能还有其他解决方案。如果您赶时间,请阅读下面的问题。

(已编辑——同时我添加了一些解决问题的尝试。第四个是我的最终结论,你可以直接跳到它。)

背景

我有一个包含大约 20k 对(键(i),值(i))的哈希表。我想生成像这样的随机列表

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

限制是,一旦我选择 key(213) 作为列表的第一个元素,并不是所有的键都可以跟随它(我有一些其他功能“决定”可以决定某个键是否可以成为下一个是否在列表中)。所以,我想选择一个随机的下一个元素并检查它是否合适——在上面的示例中选择了 key(127)。如果该元素被我的“决定”功能拒绝,我想随机选择另一个。但是我不想选择刚刚被拒绝的相同,因为我知道它会再次被拒绝,这不仅效率低下,而且我还冒着风险,如果只有几个键可以是下一个键,需要很长时间直到找到合适的钥匙。请注意,可以重复,例如

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

这没问题,只要“决定”函数接受 key(213) 作为列表中的下一个。所以,我需要的只是一种随机枚举哈希表中的(键,值)对的方法。每当我必须选择一个键时,我都会创建一个枚举,我通过使用“决定”函数检查每个新元素来使用它(因此,不会发生重复),当我找到一个时,我将它添加到列表中并继续增加列表.问题是我不希望哈希表的枚举每次都相同。我希望它是随机的。 (这与我在特定问题中的搜索空间结构有关,这里不相关。)

我当然可以通过生成随机整数并仅使用列表来实现这一点——这就是我目前正在做的事情。但是,由于这是我经常遇到的问题,我想知道某处是否有一些用于哈希表的随机枚举工具。

问题

在某处是否有一些用于哈希表的随机枚举函数?我知道函数BatHashtbl.enum(电池库),但我认为它总是会给我相同的哈希表相同的枚举(这是正确的吗?)。此外,该 BatHashtbl 模块中似乎不存在任何此类内容。我会对类似的东西感兴趣

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

当提供哈希表和一些整数作为随机生成器的种子时,它将给出哈希表的不同随机枚举。有什么想法吗?

感谢您的帮助!

最好, 苏瑞卡托。

第一次尝试

根据 Niki 在 cmets 中的建议,并通过电池库查看了更多详细信息,我想出了这个

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

它使用 Fisher-Yates 算法进行在 O(n) 中运行的洗牌。它返回一个列表而不是枚举,这很烦人,因为这意味着即使我对使用 rand_enum 获得的列表的第三个元素感到满意,该函数仍然会为整个 20k 元素计算随机枚举哈希表。

最好, 苏瑞克特

第二次尝试

我将模块 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

使用随机种子 1236 创建哈希表 ht 的随机枚举(在此代码中,我假设哈希表是之前定义的),然后我们可以编写

let (k,v) = RndHashtblEnum.next re

从随机枚举中获取下一个 (k,v) 对。

我们可能会问的一个问题是,这是否真的是公平随机性,因为我在下次需要随机枚举时使用列表的剩余部分来随机枚举哈希表。好吧,它不是。如果我的哈希表有 1000 个元素,并且在提取 5 个随机元素后我对结果感到满意,我知道在接下来的 995 个(第二组提取)中,这 5 个元素都不会被提取。所以,这不是公平的随机性。情况更糟。很可能在接下来的 1000 次提取中(此列表中的 995 个,下一个枚举列表中的 5 个)将不会涵盖某些元素。平均而言,该算法是公平的,但并不总是公平的。

最好, 苏瑞卡托。

第三次尝试

你好,

包括 Niki 关于使用 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

这个新模块摆脱了将数组传递给列表的(愚蠢的)成本,并且在开始时只使用一次 Fisher-Yates 算法——因此,从长远来看,我们可以考虑 Fisher-Yates 算法的贡献-Yates 位为 O(1)。

新版本现在在随机性方面是公平的。这不是那么容易看到的,我花了一点时间才意识到这一点。假设哈希表有 1000 个条目。在新版本中,我们始终使用相同的枚举(enum0——在我们使用“create”函数创建随机枚举时已修复)。这意味着,当试图在我们的最终列表中找到下一个元素时,因为哈希表中的某些键必须满足“决定”功能(否则我们将无法继续使用算法,我们将停止),它将在第 0 和第 999 个条目之间的某个位置执行此操作。假设它在条目 300 上。现在,鉴于我们已经选择了这个键,为了确定最终列表中的下一个键,我们的枚举将继续使用剩余的 700 个元素,然后将继续到相同副本中的下一个 300枚举。所以,700+300 正好是哈希表中的 1000。这意味着我们将始终只考虑哈希表中的每个条目一次。另一件事是,每次我们尝试在列表中找到一个键时,该标签可以在条目 300 上找到,也可以在条目 734 或其他东西上找到,因为决定功能实际上取决于之前选择了哪些键最终列表中的那个点。因此,每次我们重新开始寻找哈希表中最终列表的元素时,我们都会从哈希表的随机元素开始。

对不起,如果这不是很清楚。很难解释。 =)

感谢所有cmets。

最好, 苏瑞卡托。

第四次也是最后一次尝试——这是我提出的解决方案

你好,

分享 gasche 对可变字段和枚举的担忧,以及所有可能来自那里的奇怪副作用,我决定忘记使用可用哈希表库的现成解决方案,并使用普通列表编写我的东西。我还带来了惰性来处理避免生成只使用部分的随机列表(所以有一些有用的惰性东西可以按照你的建议使用,Niki)。

我创建了类型

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 只是一种在列表中获取哈希表的所有 (key,value) 对的方法(感谢 Pascal 在此 question 中的回答)。

这结束了整个哈希表枚举的事情。为了完整起见,我还添加了我试图解决的整体问题的解决方案,在上面的“上下文”中进行了解释。如果您还记得,问题是从 (1) 哈希表和 (2) decide 函数中随机生成 (key,value) 对的列表,该函数可以判断是否可以附加 (key,value)到一些特定的对列表。由于整个生成过程可能永远不会终止,为了确保终止,我认为有第三个参数是有意义的,它是一个告诉我们是否应该停止进程的函数(并且我们应该确保它会在某个时候返回 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 []

非常感谢所有为有用的 cmets 做出贡献的人。这真的很有帮助。

一切顺利, 苏瑞卡托。

【问题讨论】:

  • 如果你可能不需要整个列表,那么不要随机化整个列表;你应该重写 Fisher-Yates 所以它是懒惰的。
  • @nlucaroni 谢谢,这是个好建议。实际上,我的处理方式略有不同。我重复使用随机列表的其余部分。
  • @nlucaroni - 我刚刚写了一个懒惰版本的 Fisher-yates 来探索这种可能性!但是我认为使用 Enum.t 无法有效地做到这一点,您需要先将其转换为数组。由于 shuffle 为您执行此操作,因此我认为惰性方法没有多大意义。
  • 你考虑过另一种数据结构吗?您可以扩展 map 或 set 或 hashtbl(作为我在另一条评论中的链接),以包含一个拉取随机元素并将其删除并返回一对的函数。很简单,不需要转换成数组。
  • @nlucaroni 是的,这就是我最终所做的。我创建了一个惰性“随机枚举”类型和适当的函数来有效地获取随机元素。感谢您的建议。

标签: random hashtable ocaml enumeration


【解决方案1】:

我有两个建议。第一个是更改您的 rand_enum 函数,使其返回 Enum.t:

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

这并没有太大的不同(它仍在计算所有 20k 的随机枚举),但更接近您最初想要的。

或者,您可以随时获取 HashTbl 的源代码并使用 rand_enum 函数重新编译它。然而,这也可能不会那么不同,因为 HashTbl 是作为数组实现的,如果你想避免错误的重复,你可能最终会使用 shuffle。

【讨论】:

  • 是的,Array.enum 更有意义。谢谢!
  • 可以扩展模块;这是我用其他一些属性扩展的地图(实际上是从地图中获取随机元素)。您可以像使用 Map 模块一样使用它。 nicholas.lucaroni.com/repo_pub/ocamlmaze/xMap.ml
  • ...是的,在 ocaml 3.12+ 中,您也可以使用 include 作为签名(这就是为什么我没有该文件的签名)。而且,您让我修复了该代码,不久前我尝试编译该项目时出现错误。呵呵,谢谢!
【解决方案2】:

潜在的下一个元素的密度是多少?您的decide 功能的成本是多少?

您当前的所有解决方案都有 O(n) 成本。 Fisher-Yates 是 O(n)(尝试将其用于 Enums 并没有多大意义,因为无论如何它都需要强制枚举),而 Array.to_list 也是 O(n)。

如果您的decide 函数足够快且密度足够低,我认为只构建所有符合条件的元素的列表/数组(在表的每个元素上调用decide)可能会更简单,然后随机选择其中之一。

如果密度足够高并且decide 成本很高,我认为您的第一个想法是随机选择密钥并保留已经遇到的密钥列表。您将能够选择遇到的第一个符合条件的元素(decide 调用的最佳数量)。当所有元素都已经被挑选出来时,这种枚举序列的方式“最终”会变得昂贵,但如果你的密度很高,你就不会遇到这种情况。

如果您不知道,从“高密度”假设开始可能会很有趣,当您看到表格的给定部分后改变主意,但仍然一无所获。

最后:如果您不需要在生成序列期间添加/删除元素,那么将您的哈希表一次性转换为数组会很有趣(在某处保留另一个键 -> 数组索引表),如当索引是连续的时,所有这些问题都会变得更简单。

【讨论】:

  • 感谢非常有用的 cmets。我不知道。我正在研究一个未知的搜索空间。决定功能的成本不高,我怀疑下一个潜在元素的密度会很低。我现在再次编辑了问题以包含不同的随机哈希表枚举模块。它处理了将数组传递给列表的成本,并且在开始时只使用一次 Fisher-Yates 算法,因此从长远来看,我们可以考虑其复杂度 O(1)。如果您有任何 cmets,请阅读并告诉我。
【解决方案3】:

您的实现)(第二和第三)太复杂了。我不喜欢mutable,也不喜欢Enum。将它们结合起来是最好的办法,让自己在脚上开枪,但副作用不受控制。

我还认为您的特定问题过于具体,无法通过看起来通用的“随机播放”功能来解决。试图找到这样一个独立于域的函数,它也可以解决您的特定领域问题,这可能是您的后续实现在每次尝试时变得更丑陋和更复杂的原因。

从 Hashtable 生成随机流很简单:BatHashtbl.enum |- BatRandom.shuffle |- BatArray.enum。您的其余代码应该关注decide 函数的使用。

【讨论】:

  • 我也不喜欢mutableEnum。我现在更改了实现以不使用它们。我不同意这个问题太具体。我上面提出的解决方案是针对通用哈希表和通用决策函数。有了这个解决方案,现在可以插入一个特定的哈希表和一个特定的函数,并从随机获得的哈希表中获取 (key,value) 的列表。感谢有用的 cmets。
【解决方案4】:

我怀疑Hashtbl暴露的接口是否存在这样的功能。像将所有值放入数组并通过Array.get a (Random.int (Array.length a)) 进行查找这样的明显方法对我来说看起来不错。

【讨论】:

  • 感谢您的回复。该解决方案可能会重复使用 Array.get 提取的元素。如果我提取了一个元素但它不起作用,我不想再次提取它(如果 Random.int 碰巧重复,这可能会发生)。但是,是的,我同意这可以在没有特定 Hashtbl 函数的情况下使用。
  • @Surikator - 您可以打乱数组(使用 Fisher-Yates 算法),然后按顺序遍历元素,而不是随机选择一个元素。
  • @Niki 这是个好建议。我已经编辑了问题以包含该想法的代码。不过,在效率方面仍有一些工作要做。
猜你喜欢
  • 1970-01-01
  • 2013-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多