我想创建这个函数,它从Set:
中选择一个随机元素randElem :: (RandomGen g) => Set a -> g -> (a, g)
可以编写简单的listy实现。例如(代码更新,验证工作):
import Data.Set as Set
import System.Random (getStdGen, randomR, RandomGen)
randElem :: (RandomGen g) => Set a -> g -> (a, g)
randElem s g = (Set.toList s !! n, g')
where (n, g') = randomR (0, Set.size s - 1) g
-- simple test drive
main = do g <- getStdGen
print . fst $ randElem s g
where s = Set.fromList [1,3,5,7,9]
但是使用!!
会导致大型(随机选择)n
的线性查找成本。有没有更快的方法来选择集合中的随机元素?理想情况下,重复随机选择应该在所有选项上产生均匀分布,这意味着它不喜欢某些元素而不是其他元素。
编辑:答案中出现了一些很棒的想法,所以我只想对我正在寻找的内容进行更多澄清。我用套装作为this situation的解决方案问了这个问题。我更喜欢这两个答案
我也非常喜欢使用代码,如果您的答案包含有效的解决方案,那么我希望(至少)能获得+1。
答案 0 :(得分:6)
Data.Map具有索引功能(elemAt),因此请使用:
import qualified Data.Map as M
import Data.Map(member, size, empty)
import System.Random
type Set a = M.Map a ()
insert :: (Ord a) => a -> Set a -> Set a
insert a = M.insert a ()
fromList :: Ord a => [a] -> Set a
fromList = M.fromList . flip zip (repeat ())
elemAt i = fst . M.elemAt i
randElem :: (RandomGen g) => Set a -> g -> (a, g)
randElem s g = (elemAt n s, g')
where (n, g') = randomR (0, size s - 1) g
你有一些与Data.Set(关于接口和性能)完全兼容的东西,它还有一个log(n)索引函数和你请求的randElem函数。
请注意,randElem是log(n)(它可能是这种复杂性可以获得的最快实现),并且所有其他函数都具有与Data.Set相同的复杂性。如果您需要Set API中的任何其他特定功能,请告诉我,我将添加它们。
答案 1 :(得分:5)
据我所知,正确的解决方案是使用索引集 - 即IntMap
。您只需存储随地图添加的元素总数。每次添加元素时,都会使用比以前更高的键添加元素。删除元素很好 - 只是不要改变总元素计数器。如果在查找键控元素时该元素不再存在,则生成一个新的随机数并重试。这一直有效,直到删除的总数占据集合中活动元素的数量。如果这是一个问题,您可以在插入新元素时保留一组单独的已删除键。
答案 2 :(得分:4)
这是一个想法:你可以做间隔二等分。
size s
是恒定时间。使用randomR
获取您选择的集合的距离。split
和findMin
之间使用各种值进行findMax
,直到您将元素放在所需位置。如果你真的担心这个集合会说实话并且非常紧密地聚集在一起,你可以每次重新计算findMin
和findMax
,以保证每次都能敲掉一些元素。性能将是O(n log n),基本上不比你当前的解决方案差,但只有相当弱的条件才能使得集合不能完全聚集在某个积累点附近,平均性能应该是〜( (logn)^ 2),这是相当不变的。如果它是一组整数,则得到O(log n * log m),其中m是该集合的初始范围;它只是在区间二分区(或其顺序类型有累积点的其他数据类型)中可能导致真正令人讨厌的性能的实数。
PS。这样可以产生完美均匀的分布,只要观察一下即可,以确保可以将元素放在顶部和底部。
一些不优雅的,未经检查的(伪?)代码。在我当前的机器上没有编译器进行冒烟测试,可能需要使用较少的if
来完成。一件事:查看如何生成mid
;它需要进行一些调整,具体取决于你是否正在寻找适用于整数或实数的东西(区间二分法本质上是拓扑的,对于具有不同拓扑的集合,它不应该完全相同)。
import Data.Set as Set
import System.Random (getStdGen, randomR, RandomGen)
getNth (s, n) = if n = 0 then (Set.findMin s) else if n + 1 = Set.size s then Set.findMax s
else if n < Set.size bott then getNth (bott, n) else if pres and Set.size bott = n then n
else if pres then getNth (top, n - Set.size bott - 1) else getNth (top, n - Set.size)
where mid = ((Set.findMax s) - (Set.findMin s)) /2 + (Set.findMin s)
(bott, pres, top) = (splitMember mid s)
randElem s g = (getNth(s, n), g')
where (n, g') = randomR (0, Set.size s - 1) g
答案 3 :(得分:3)
如果您有权访问the internals of Data.Set,它只是一棵二叉树,您可以在树上递归,在每个节点根据各自的大小选择其中一个分支。这非常简单,在内存管理和分配方面为您提供了非常好的性能,因为您没有额外的簿记功能。 OTOH,你必须调用RNG O(log n)次。
一个变体是使用Jonas的建议首先获取大小并根据该大小选择随机元素的索引,然后使用(尚未添加的elemAt)函数到Data.Set。
答案 4 :(得分:3)
从containers-0.5.2.0开始,Data.Set
模块具有elemAt
函数,该函数按排序的元素序列中的从零开始的索引检索值。所以现在写这个函数是微不足道的
import Control.Monad.Random
import Data.Set (Set)
import qualified Data.Set as Set
randElem :: (MonadRandom m, Ord a) -> Set a -> m (a, Set a)
randElem xs = do
n <- getRandomR (0, Set.size xs - 1)
return (Set.elemAt n xs, Set.deleteAt n xs)
由于Set.elemAt
和Set.deleteAt
都是O( log n ),其中 n 是集合中元素的数量,整个操作是O( log n )
答案 5 :(得分:2)
如果您不需要修改您的设置或需要不经常修改它,您可以使用数组作为具有O(1)访问时间的查找表。
import qualified Data.Vector
import qualified Data.Set
newtype RandSet a = RandSet (V.Vector a)
randElem :: RandSet a -> RandomGen -> (a, RandomGen)
randElem (RandSet v) g
| V.empty v = error "Cannot select from empty set"
| otherwise =
let (i,g') = randomR (0, V.length v - 1) g
in (v ! i, g')
-- Of course you have to rebuild array on insertion/deletion which is O(n)
insert :: a -> RandSet a -> RandSet a
insert x = V.fromList . Set.toList . Set.insert x . Set.fromList . V.toList`
答案 6 :(得分:2)
如果您不介意完全使用RandomGen
,可以稍微解决这个问题。使用可分离的发电机,这是一个A-OK的事情。基本思想是为集合创建一个查找表:
randomElems :: Set a -> RandomGen -> [a]
randomElems set = map (table !) . randomRs bounds where
bounds = (1, size set)
table = listArray bounds (toList set)
这将具有非常好的性能:它将花费您O(n + m)时间,其中n是集合的大小,m是您评估的结果列表的元素数量。 (当然,加上在边界中随机选择m个数字所需的时间。)
答案 7 :(得分:2)
实现此目的的另一种方法可能是使用Data.Sequence而不是Data.Set。这将允许您在O(1)时间内添加元素到O(log n)时间的索引元素。如果您还需要能够进行成员资格测试或删除,则必须使用更通用的fingertree包并使用FingerTree (Sum 1, Max a) a
之类的内容。要插入元素,请使用Max a
注释找到要插入的正确位置;这基本上需要O(log n)时间(对于某些使用模式,它可能会少一些)。要进行成员资格测试,基本上做同样的事情,所以它是O(log n)时间(同样,对于某些使用模式,这可能会少一些)。要选择随机元素,请使用Sum 1
注释进行索引,取O(log n)时间(这将是均匀随机索引的平均情况)。