Java 如何在O(n)时间内,根据其在映射中的整数值相对于其他值随机选择一个键?

Java 如何在O(n)时间内,根据其在映射中的整数值相对于其他值随机选择一个键?,java,random,map,uniform,Java,Random,Map,Uniform,如果我们有一个映射,假设整数值表示“有多少个”T。因此,我想根据整数值统一选择一个T。如果映射包含“a”=4和“b”=6的字符串,那么我希望它在40%的时间“a”被选中,在60%的时间“b”被选中 最重要的是,我希望在O(n)中这样,在我前面的示例中,n是两个(而不是十个)。我最初制作了一个ArrayList,其中包含键的数量(并简单地返回任意随机索引),但这个过程不仅非常缓慢,而且对于映射所表示的内容来说完全违反直觉。要做到这一点,您必须缓存每个值T的相对频率。这将为您提供O(n)O(n)插入

如果我们有一个
映射
,假设整数值表示“有多少个”T。因此,我想根据整数值统一选择一个T。如果映射包含“a”=4和“b”=6的字符串,那么我希望它在40%的时间“a”被选中,在60%的时间“b”被选中


最重要的是,我希望在O(n)中这样,在我前面的示例中,n是两个(而不是十个)。我最初制作了一个ArrayList,其中包含键的数量(并简单地返回任意随机索引),但这个过程不仅非常缓慢,而且对于
映射所表示的内容来说完全违反直觉。

要做到这一点,您必须缓存每个值T的相对频率。这将为您提供O(n)O(n)插入成本价格的概率分布(每次插入时,必须更新每个T的相对频率)。

如果可以存储总金额,这很容易做到:

只需将对(T,int)存储为类或普通数组中的任何内容,然后进行检查:

int val = Random.nextInt(total);
for (Pair p : pairs) {
    val -= p.val;
    if (val < 0) return p;
}
int val=Random.nextInt(总计);
用于(p对:对){
val-=p.val;
如果(val<0)返回p;
}

考虑到循环遍历ArrayList是遍历n个值的最有效的方法,并且显然不可能比O(n)做得更好,所以不能更快。唯一的开销是nextInt(),在每个解决方案中都需要它(或类似的东西)。 根据您组织ArrayList的方式(排序与否),其他操作会变得更便宜/更昂贵,但这对于特定操作并不重要

编辑:尽管考虑到这一点,“你显然需要O(n)”不是真的。如果您很少更改数组中的值,并且允许进行昂贵的准备,并且内存不是问题,那么您可以通过存储HashMap来做得更好。 例如,如果您有一个发行版: T0:2 T1:3 T2:1

您可以在hashmap中插入(0,T0),(1,T0),(2,T1),(4,T1),(5,T2)


Edit2:或者看看phooji的方法,该方法对于更大的数据集应该是可行的。

使用arraylist实际上比使用Map更快,因为您可以在O(1)中完成

class-RandVal{
列表=新的ArrayList();
Random rand=新的Random();
公共T值(){
int next=rand.nextInt(list.size());
返回列表。获取(下一步);
}
}

这是一件坏事的唯一方式是,如果顺序很重要(a a B B a B B B a B B B a B B a或其他),但显然不是这样,因为您使用的是一个没有顺序的映射…

很抱歉延迟,但我认为我有一个相对优雅的解决方案,
O(n lg n)
构造时间和
O(lg n)
fetch-a-random-element时间。来吧


加权概率图: 此类实现了随机元素生成器。它是基于一个
Iterable
构建的;请参见下面的
Test.java

import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;

class WeightedProbMap<EltType>  {
    private SortedMap<Integer, EltType> elts = new TreeMap<Integer, EltType>();
    private Random rand = new Random();
    private int sum = 0;

    // assume: each weight is > 0; there is at least one element;
    //         elements should not be repeated
    // ensure: this.elts maps cumulative weights to elements;
    //         this.sum is the total weight
    public WeightedProbMap(Iterable<Pair<Integer, EltType>> weights) {
        for (Pair<Integer, EltType> e : weights) {
            this.elts.put(this.sum, e.second);
            this.sum += e.first;
        }
    }

    // assume: this was initialized properly (cf. constructor req)
    // ensure: return an EltType with relative probability proportional
    //         to its associated weight
    public EltType nextElt() {
        int index = this.rand.nextInt(this.sum) + 1;
        SortedMap<Integer, EltType> view = this.elts.headMap(index);
        return view.get(view.lastKey());
    }
}

对此进行测试:

  • 取消对
    Test.java
    中的一行或两行
    elt.add(…)
    行的注释
  • 编译时使用:

    $javac Pair.java WeightedProbMap.java Test.java

  • 使用运行(例如,在Unix中):

    $java测试| grep“Hello”| wc-l

  • 这将为您提供该特定执行的计数


    说明:

    建造商:
    WeightedProbMap
    (WPM)类使用一个函数将累积权重映射到元素。图形说明:

    The constructor takes weights...     ...and creates a mapping from the
          3 +---+                            number line:
            |   | 
      2 +---+   +---+ 2                   0      2         5      7
        |   |   |   |                     +------+---------+------+
        |   |   |   |                     |   X  |    Y    |   Z  |
      --+---+---+---+--                   +------+---------+------+
          X   Y   Z
    
    nextElt()
    SortedMap
    按键顺序存储数据,这使得它能够廉价地提供地图子集的“视图”。特别是这条线

    SortedMap<Integer, EltType> view = this.elts.headMap(index)
    
    SortedMap视图=此.elts.headMap(索引)
    
    返回原始映射的视图(
    this.elts
    ),其中仅包含严格小于
    索引的键。此操作()是固定时间:
    view
    需要
    O(1)
    时间来构造,如果以后要更改
    This.elts
    ,则更改也会反映在
    view


    一旦我们创建了小于随机数的所有内容的
    视图
    ,我们现在只需要在该子集中找到最大的键。我们使用
    SortedMap.lastKey()
    来实现这一点,对于
    TreeMap
    ,这应该需要
    \Theta(lg n)
    时间

    构建一个反向映射,
    映射
    ,使每个关键点都是迄今为止处理的所有权重的总和

    例如,如果您有此地图:

    T1 -> 10
    T2 -> 8
    T3 -> 3
    
    此反向映射为:

    10 -> T1
    18 -> T2
    21 -> T3
    
    (为了获得更好的性能,您可以先按降序排列权重。)

    然后在0和所有权重之和之间生成一个均匀分布的随机数,并在反向映射的键集中对该数字执行二进制搜索。

    OP here

    我想出了一个优雅的解决方案!对于任何误解:我最初的想法是按一个ArrayList中的多少值来存储所有键,而完全不考虑使用映射来存储“使用整数的键实例”;任何类似的解决方案都会适得其反!假设地图是无序的,下面是我的解决方案:

    public T randomPick(Random r) {
    
            int randomValue = r.nextInt(size());
            int currentSum = 0;
            T lastElement = null;
    
            for (T t : map.keySet()){
                if (randomValue < currentSum + map.get(t)){
                    return t;
                }
                currentSum+= map.get(t);
                lastElement = t;
            }
            return lastElement;
        }
    
    publictrandompick(随机r){
    int randomValue=r.nextInt(size());
    int currentSum=0;
    T lastElement=null;
    for(T:map.keySet()){
    if(随机值
    它将
    随机值
    当前和+当前元素的值
    进行比较。如果小于该值,则返回当前键。否则,继续并将该值添加到总和中。如果是这样的情况,随机值永远不小于任何值,我们返回t
    T1 -> 10
    T2 -> 8
    T3 -> 3
    
    10 -> T1
    18 -> T2
    21 -> T3
    
    public T randomPick(Random r) {
    
            int randomValue = r.nextInt(size());
            int currentSum = 0;
            T lastElement = null;
    
            for (T t : map.keySet()){
                if (randomValue < currentSum + map.get(t)){
                    return t;
                }
                currentSum+= map.get(t);
                lastElement = t;
            }
            return lastElement;
        }