从集合中选择一个随机元素

Posted

技术标签:

【中文标题】从集合中选择一个随机元素【英文标题】:Picking a random element from a set 【发布时间】:2010-09-12 14:03:50 【问题描述】:

如何从集合中选择随机元素? 我对从 Java 中的 HashSet 或 LinkedHashSet。 也欢迎其他语言的解决方案。

【问题讨论】:

你应该指定一些条件来看看这是否真的是你想要的。 - 你会选择随机元素多少次? - 数据是否需要存储在 HashSet 或 LinkedHashSet 中,两者都不能随机访问。 - 散列集大吗?钥匙小吗? 【参考方案1】:

既然你说“也欢迎其他语言的解决方案”,这里是 Python 的版本:

>>> import random
>>> random.choice([1,2,3,4,5,6])
3
>>> random.choice([1,2,3,4,5,6])
4

【讨论】:

只是,[1,2,3,4,5,6] 不是一个集合,而是一个列表,因为它不支持快速查找之类的东西。 你仍然可以这样做:>>> random.choice(list(set(range(5)))) >>> 4 不理想,但如果你绝对需要,它会做。跨度> 【参考方案2】:

在 Java 中:

Set<Integer> set = new LinkedHashSet<Integer>(3);
set.add(1);
set.add(2);
set.add(3);

Random rand = new Random(System.currentTimeMillis());
int[] setArray = (int[]) set.toArray();
for (int i = 0; i < 10; ++i) 
    System.out.println(setArray[rand.nextInt(set.size())]);

【讨论】:

您的答案有效,但由于 set.toArray( ) 部分,它不是很有效。 你应该把 toArray 移到循环之外。【参考方案3】:
int size = myHashSet.size();
int item = new Random().nextInt(size); // In real life, the Random object should be rather more shared than this
int i = 0;
for(Object obj : myhashSet)

    if (i == item)
        return obj;
    i++;

【讨论】:

如果 myHashSet 很大,那么这将是一个相当慢的解决方案,因为平均需要 (n / 2) 次迭代才能找到随机对象。 如果你的数据在一个哈希集中,你需要 O(n) 时间。如果您只选择一个元素并且数据存储在 HashSet 中,则无法绕过它。 @David Nehme:这是 Java 中 HashSet 规范中的一个缺点。在 C++ 中,通常能够直接访问组成哈希集的桶,这使我们能够更有效地选择随机元素。如果在 Java 中需要随机元素,则可能值得定义一个自定义散列集,以允许用户深入了解。请参阅 [boost 的文档][1] 了解更多信息。 [1]boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html 如果该集合在多次访问中没有发生变异,您可以将其复制到一个数组中,然后访问 O(1)。只需使用 myHashSet.toArray() @ykaganovich 这不会让事情变得更糟,因为必须将集合复制到一个新数组中? docs.oracle.com/javase/7/docs/api/java/util/… "这个方法必须分配一个新的数组,即使这个集合是由一个数组支持的"【参考方案4】:

你不能只获取集合/数组的大小/长度,生成一个介于 0 和大小/长度之间的随机数,然后调用索引与该数字匹配的元素吗? HashSet 有一个 .size() 方法,我很确定。

在伪代码中 -

function randFromSet(target)
 var targetLength:uint = target.length()
 var randomIndex:uint = random(0,targetLength);
 return target[randomIndex];

【讨论】:

这仅在相关容器支持随机索引查找时才有效。许多容器实现都没有(例如,哈希表、二叉树、链表)。【参考方案5】:

php,假设“set”是一个数组:

$foo = array("alpha", "bravo", "charlie");
$index = array_rand($foo);
$val = $foo[$index];

Mersenne Twister 函数更好,但 PHP 中没有与 array_rand 等效的 MT。

【讨论】:

大多数集合实现没有 get(i) 或索引运算符,所以我假设这就是 OP 指定它的集合【参考方案6】:

PHP,使用 MT:

$items_array = array("alpha", "bravo", "charlie");
$last_pos = count($items_array) - 1;
$random_pos = mt_rand(0, $last_pos);
$random_item = $items_array[$random_pos];

【讨论】:

【参考方案7】:

javascript 解决方案 ;)

function choose (set) 
    return set[Math.floor(Math.random() * set.length)];


var set  = [1, 2, 3, 4], rand = choose (set);

或者:

Array.prototype.choose = function () 
    return this[Math.floor(Math.random() * this.length)];
;

[1, 2, 3, 4].choose();

【讨论】:

我更喜欢第二种选择。 :-) 哦,我喜欢扩展添加新的数组方法!【参考方案8】:

一个有点相关的你知道吗:

java.util.Collections 中有一些有用的方法可以洗牌整个集合:Collections.shuffle(List&lt;?&gt;)Collections.shuffle(List&lt;?&gt; list, Random rnd)

【讨论】:

太棒了!这在 java doc 中的任何地方都没有交叉引用!喜欢Python's random.shuffle() 但这仅适用于列表,即具有 .get() 函数的结构。 @bourbaki4481472 是绝对正确的。这仅适用于那些扩展了List 接口的集合,而不是OP 讨论的Set 接口。 但是,OP 希望选择 a (我假设是一个)元素。洗牌整个列表(以及将集合中的所有元素转移到列表中)对于该项目来说非常昂贵......如果你有一个列表,你可以使用List.get(new Random().nextInt(size))【参考方案9】:

Perl 5

@hash_keys = (keys %hash);
$rand = int(rand(@hash_keys));
print $hash$hash_keys[$rand];

这是一种方法。

【讨论】:

【参考方案10】:

Icon 有一个集合类型和一个随机元素运算符,一元“?”,所以表达式

? set( [1, 2, 3, 4, 5] )

将产生一个介于 1 和 5 之间的随机数。

随机种子在程序运行时被初始化为 0,因此要在每次运行时产生不同的结果,请使用 randomize()

【讨论】:

【参考方案11】:

在 C# 中

        Random random = new Random((int)DateTime.Now.Ticks);

        OrderedDictionary od = new OrderedDictionary();

        od.Add("abc", 1);
        od.Add("def", 2);
        od.Add("ghi", 3);
        od.Add("jkl", 4);


        int randomIndex = random.Next(od.Count);

        Console.WriteLine(od[randomIndex]);

        // Can access via index or key value:
        Console.WriteLine(od[1]);
        Console.WriteLine(od["def"]);

【讨论】:

【参考方案12】:

在口齿不清

(defun pick-random (set)
       (nth (random (length set)) set))

【讨论】:

这仅适用于列表,对吧?使用ELT,它可以适用于任何序列。【参考方案13】:

如果您想在 Java 中执行此操作,您应该考虑将元素复制到某种随机访问集合(例如 ArrayList)中。因为,除非您的集合很小,否则访问所选元素的成本会很高(O(n) 而不是 O(1))。 [ed: 列表副本也是 O(n)]

或者,您可以寻找更符合您要求的另一个 Set 实现。来自 Commons Collections 的 ListOrderedSet 看起来很有希望。

【讨论】:

复制到一个列表会花费 O(n) 的时间并且也会使用 O(n) 的内存,那么为什么这会比直接从地图中获取更好的选择呢? 这取决于你想从集合中选择多少次。复制是一次性操作,然后您可以根据需要多次从集合中选择。如果您只选择一个元素,那么是的,副本不会让事情变得更快。 如果您希望能够重复选择,这只是一次操作。如果您希望从集合中删除所选项目,那么您将回到 O(n)。【参考方案14】:

Clojure 解决方案:

(defn pick-random [set] (let [sq (seq set)] (nth sq (rand-int (count sq)))))

【讨论】:

这个解决方案也是线性的,因为要获得nth 元素,您还必须遍历seq 它也是线性的,因为它非常适合一行:D【参考方案15】:

不幸的是,这在任何标准库集容器中都无法有效地完成(优于 O(n))。

这很奇怪,因为向哈希集和二进制集添加随机选择函数非常容易。在一个不稀疏的哈希集中,您可以尝试随机条目,直到获得成功。对于二叉树,您可以在左子树或右子树之间随机选择,最多 O(log2) 步。我已经实现了下面的演示:

import random

class Node:
    def __init__(self, object):
        self.object = object
        self.value = hash(object)
        self.size = 1
        self.a = self.b = None

class RandomSet:
    def __init__(self):
        self.top = None

    def add(self, object):
        """ Add any hashable object to the set.
            Notice: In this simple implementation you shouldn't add two
                    identical items. """
        new = Node(object)
        if not self.top: self.top = new
        else: self._recursiveAdd(self.top, new)
    def _recursiveAdd(self, top, new):
        top.size += 1
        if new.value < top.value:
            if not top.a: top.a = new
            else: self._recursiveAdd(top.a, new)
        else:
            if not top.b: top.b = new
            else: self._recursiveAdd(top.b, new)

    def pickRandom(self):
        """ Pick a random item in O(log2) time.
            Does a maximum of O(log2) calls to random as well. """
        return self._recursivePickRandom(self.top)
    def _recursivePickRandom(self, top):
        r = random.randrange(top.size)
        if r == 0: return top.object
        elif top.a and r <= top.a.size: return self._recursivePickRandom(top.a)
        return self._recursivePickRandom(top.b)

if __name__ == '__main__':
    s = RandomSet()
    for i in [5,3,7,1,4,6,9,2,8,0]:
        s.add(i)

    dists = [0]*10
    for i in xrange(10000):
        dists[s.pickRandom()] += 1
    print dists

我得到了 [995, 975, 971, 995, 1057, 1004, 966, 1052, 984, 1001] 作为输出,所以分布接缝很好。

我自己也遇到过同样的问题,但我还没有决定,这个更高效的选择所带来的性能提升是否值得使用基于 python 的集合的开销。我当然可以对其进行改进并将其翻译成 C,但今天这对我来说工作量太大了 :)

【讨论】:

我认为这没有在二叉树中实现的一个原因是这种方法不会统一选择项目。由于它们是没有左/右孩子的节点,因此可能会出现左孩子比右孩子包含更多项目的情况(反之亦然),这将使在右(或左)孩子中选择项目的可能性更大。 @CommuSoft:这就是我存储每个子树大小的原因,所以我可以根据这些选择我的概率。【参考方案16】:

C++。这应该相当快,因为​​它不需要遍历整个集合或对其进行排序。假设它们支持tr1,这应该适用于大多数现代编译器。如果没有,您可能需要使用 Boost。

Boost docs 在这里有助于解释这一点,即使您不使用 Boost。

诀窍是利用数据被划分为桶的事实,并快速识别随机选择的桶(以适当的概率)。

//#include <boost/unordered_set.hpp>  
//using namespace boost;
#include <tr1/unordered_set>
using namespace std::tr1;
#include <iostream>
#include <stdlib.h>
#include <assert.h>
using namespace std;

int main() 
  unordered_set<int> u;
  u.max_load_factor(40);
  for (int i=0; i<40; i++) 
    u.insert(i);
    cout << ' ' << i;
  
  cout << endl;
  cout << "Number of buckets: " << u.bucket_count() << endl;

  for(size_t b=0; b<u.bucket_count(); b++)
    cout << "Bucket " << b << " has " << u.bucket_size(b) << " elements. " << endl;

  for(size_t i=0; i<20; i++) 
    size_t x = rand() % u.size();
    cout << "we'll quickly get the " << x << "th item in the unordered set. ";
    size_t b;
    for(b=0; b<u.bucket_count(); b++) 
      if(x < u.bucket_size(b)) 
        break;
       else
        x -= u.bucket_size(b);
    
    cout << "it'll be in the " << b << "th bucket at offset " << x << ". ";
    unordered_set<int>::const_local_iterator l = u.begin(b);
    while(x>0) 
      l++;
      assert(l!=u.end(b));
      x--;
    
    cout << "random item is " << *l << ". ";
    cout << endl;
  

【讨论】:

【参考方案17】:

看完这个帖子,我能写的最好的是:

static Random random = new Random(System.currentTimeMillis());
public static <T> T randomChoice(T[] choices)

    int index = random.nextInt(choices.length);
    return choices[index];

【讨论】:

问题是关于集合,而不是数组。也不需要用当前时间播种Randomnew Random() 返回一个开箱即用的正确种子实例。【参考方案18】:

使用ArrayListHashMap 的Java 快速解决方案:[元素-> 索引]。

动机:我需要一组具有RandomAccess 属性的项目,尤其是从集合中选择一个随机项目(参见pollRandom 方法)。二叉树中的随机导航是不准确的:树不是完全平衡的,这不会导致均匀分布。

public class RandomSet<E> extends AbstractSet<E> 

    List<E> dta = new ArrayList<E>();
    Map<E, Integer> idx = new HashMap<E, Integer>();

    public RandomSet() 
    

    public RandomSet(Collection<E> items) 
        for (E item : items) 
            idx.put(item, dta.size());
            dta.add(item);
        
    

    @Override
    public boolean add(E item) 
        if (idx.containsKey(item)) 
            return false;
        
        idx.put(item, dta.size());
        dta.add(item);
        return true;
    

    /**
     * Override element at position <code>id</code> with last element.
     * @param id
     */
    public E removeAt(int id) 
        if (id >= dta.size()) 
            return null;
        
        E res = dta.get(id);
        idx.remove(res);
        E last = dta.remove(dta.size() - 1);
        // skip filling the hole if last is removed
        if (id < dta.size()) 
            idx.put(last, id);
            dta.set(id, last);
        
        return res;
    

    @Override
    public boolean remove(Object item) 
        @SuppressWarnings(value = "element-type-mismatch")
        Integer id = idx.get(item);
        if (id == null) 
            return false;
        
        removeAt(id);
        return true;
    

    public E get(int i) 
        return dta.get(i);
    

    public E pollRandom(Random rnd) 
        if (dta.isEmpty()) 
            return null;
        
        int id = rnd.nextInt(dta.size());
        return removeAt(id);
    

    @Override
    public int size() 
        return dta.size();
    

    @Override
    public Iterator<E> iterator() 
        return dta.iterator();
    

【讨论】:

这会起作用,但问题是关于 Set 界面。此解决方案强制用户拥有 RandomSet 的具体类型引用。 我很喜欢这个解决方案,但是它不是线程安全的,可能会出现 Map 和 List 之间的不准确,所以我会添加一些同步块 @KonstantinosChalkias 内置集合也不是线程安全的。只有名称为Concurrent 的才是真正安全的,带有Collections.synchronized() 的才是半安全的。此外,OP 没有说任何关于并发的内容,所以这是一个有效且很好的答案。 这里返回的迭代器应该不能从dta 中删除元素(这可以通过guava 的Iterators.unmodifiableIterator 来实现)。否则,例如的默认实现AbstractSet 中的 removeAll 和 retainAll 及其使用该迭代器的父级会弄乱你的 RandomSet! 不错的解决方案。如果每个节点包含其根的子树中的节点数,您实际上可以使用树。然后在 0..1 中计算一个随机实数,并根据节点数在每个节点上做出加权 3 向决策(选择当前节点或下降到左子树或右子树)。但 imo 你的解决方案要好得多。【参考方案19】:

在数学中:

a = 1, 2, 3, 4, 5

a[[ ⌈ Length[a] Random[] ⌉ ]]

或者,在最近的版本中,简单地说:

RandomChoice[a]

这遭到了反对,可能是因为它缺乏解释,所以这里是:

Random[] 生成一个介于 0 和 1 之间的伪随机浮点数。它乘以列表的长度,然后使用上限函数向上舍入到下一个整数。然后从a 中提取此索引。

由于哈希表功能经常使用 Mathematica 中的规则完成,并且规则存储在列表中,因此可以使用:

a = "Badger" -> 5, "Bird" -> 1, "Fox" -> 3, "Frog" -> 2, "Wolf" -> 4;

【讨论】:

【参考方案20】:
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);

【讨论】:

这是非常低效的。您的 ArrayList 构造函数在提供的集合上调用 .toArray() 。 ToArray(在大多数(如果不是全部的话)标准集合实现中)迭代整个集合,在它进行时填充一个数组。然后你打乱列表,用随机元素交换每个元素。你最好简单地将集合迭代到一个随机元素。【参考方案21】:

怎么样

public static <A> A getRandomElement(Collection<A> c, Random r) 
  return new ArrayList<A>(c).get(r.nextInt(c.size()));

【讨论】:

【参考方案22】:

为了好玩,我写了一个基于拒绝采样的 RandomHashSet。这有点 hacky,因为 HashMap 不允许我们直接访问它的表,但它应该可以正常工作。

它不使用任何额外的内存,并且查找时间是 O(1) 摊销的。 (因为 java HashTable 比较密集)。

class RandomHashSet<V> extends AbstractSet<V> 
    private Map<Object,V> map = new HashMap<>();
    public boolean add(V v) 
        return map.put(new WrapKey<V>(v),v) == null;
    
    @Override
    public Iterator<V> iterator() 
        return new Iterator<V>() 
            RandKey key = new RandKey();
            @Override public boolean hasNext() 
                return true;
            
            @Override public V next() 
                while (true) 
                    key.next();
                    V v = map.get(key);
                    if (v != null)
                        return v;
                
            
            @Override public void remove() 
                throw new NotImplementedException();
            
        ;
    
    @Override
    public int size() 
        return map.size();
    
    static class WrapKey<V> 
        private V v;
        WrapKey(V v) 
            this.v = v;
        
        @Override public int hashCode() 
            return v.hashCode();
        
        @Override public boolean equals(Object o) 
            if (o instanceof RandKey)
                return true;
            return v.equals(o);
        
    
    static class RandKey 
        private Random rand = new Random();
        int key = rand.nextInt();
        public void next() 
            key = rand.nextInt();
        
        @Override public int hashCode() 
            return key;
        
        @Override public boolean equals(Object o) 
            return true;
        
    

【讨论】:

正是我的想法!最佳答案! 实际上,回到它,我想这不是很统一,如果 hashmap 有很多冲突并且我们做了很多查询。这是因为 java hashmap 使用桶/链接,并且此代码将始终返回特定桶中的第一个元素。尽管如此,我们仍然对哈希函数的随机性保持一致。【参考方案23】:

这比接受答案中的 for-each 循环要快:

int index = rand.nextInt(set.size());
Iterator<Object> iter = set.iterator();
for (int i = 0; i < index; i++) 
    iter.next();

return iter.next();

for-each 构造在每个循环上调用Iterator.hasNext(),但由于index &lt; set.size(),该检查是不必要的开销。我看到速度提高了 10-20%,但是 YMMV。 (此外,这无需添加额外的 return 语句即可编译。)

请注意,此代码(以及大多数其他答案)可以应用于任何 Collection,而不仅仅是 Set。以泛型方法形式:

public static <E> E choice(Collection<? extends E> coll, Random rand) 
    if (coll.size() == 0) 
        return null; // or throw IAE, if you prefer
    

    int index = rand.nextInt(coll.size());
    if (coll instanceof List)  // optimization
        return ((List<? extends E>) coll).get(index);
     else 
        Iterator<? extends E> iter = coll.iterator();
        for (int i = 0; i < index; i++) 
            iter.next();
        
        return iter.next();
    

【讨论】:

【参考方案24】:

您也可以将集合转移到数组使用数组 它可能会在小规模上工作我看到for循环中投票最多的答案是O(n)反正

Object[] arr = set.toArray();

int v = (int) arr[rnd.nextInt(arr.length)];

【讨论】:

【参考方案25】:

这与接受的答案 (Khoth) 相同,但删除了不必要的 sizei 变量。

    int random = new Random().nextInt(myhashSet.size());
    for(Object obj : myhashSet) 
        if (random-- == 0) 
            return obj;
        
    

虽然取消了上述两个变量,但上述解决方案仍然是随机的,因为我们依赖随机(从随机选择的索引开始)在每次迭代中将自身递减到 0

【讨论】:

第三行也可以是if (--random &lt; 0) ,其中random 到达-1【参考方案26】:

上述解决方案涉及延迟,但不保证选择每个索引的概率相等。 如果需要考虑这一点,请尝试水库采样。 http://en.wikipedia.org/wiki/Reservoir_sampling. Collections.shuffle()(少数人建议)使用这样一种算法。

【讨论】:

【参考方案27】:

如果你真的只是想从Set 中选择“任何”对象,而不保证随机性,最简单的方法是获取迭代器返回的第一个。

    Set<Integer> s = ...
    Iterator<Integer> it = s.iterator();
    if(it.hasNext())
        Integer i = it.next();
        // i is a "random" object from set
    

【讨论】:

这不是随机选择。想象一下对同一个集合多次执行相同的操作。我认为顺序是一样的。【参考方案28】:

Java 8 最简单的是:

outbound.stream().skip(n % outbound.size()).findFirst().get()

其中n 是一个随机整数。当然它的性能不如for(elem: Col)

【讨论】:

【参考方案29】:

以 Khoth 的回答为起点的通用解决方案。

/**
 * @param set a Set in which to look for a random element
 * @param <T> generic type of the Set elements
 * @return a random element in the Set or null if the set is empty
 */
public <T> T randomElement(Set<T> set) 
    int size = set.size();
    int item = random.nextInt(size);
    int i = 0;
    for (T obj : set) 
        if (i == item) 
            return obj;
        
        i++;
    
    return null;

【讨论】:

【参考方案30】:

如果设置大小不大,则可以使用数组来完成。

int random;
HashSet someSet;
<Type>[] randData;
random = new Random(System.currentTimeMillis).nextInt(someSet.size());
randData = someSet.toArray();
<Type> sResult = randData[random];

【讨论】:

以上是关于从集合中选择一个随机元素的主要内容,如果未能解决你的问题,请参考以下文章

python random 从集合中随机选择元素

从集合中获取随机元素

Python--从集合中随机取出一个元素

Python--从集合中随机取出一个元素

从集合中随机选择? Python

java 从几十万条数据的list集合中随机取出1000个元素重新生成集合?