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


180

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


5
您应该指定一些条件,以查看这是否确实是您想要的。-您打算多少次选择随机元素?-是否需要将数据存储在HashSet或LinkedHashSet中,两者都不可以随机访问。-杂凑集很大吗?钥匙小吗?
David Nehme,

Answers:


88
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++;
}

93
如果myHashSet大,那么这将是一个相当慢的解决方案,因为平均而言,需要(n / 2)次迭代才能找到随机对象。
丹尼尔

6
如果数据在哈希集中,则需要O(n)时间。如果仅选择一个元素并且数据存储在HashSet中,则无法解决。
David Nehme

8
@David Nehme:这是Java中HashSet规范的一个缺点。在C ++中,通常能够直接访问构成哈希集的存储桶,这使我们能够更有效地选择随机元素。如果在Java中需要随机元素,则可能值得定义一个自定义的哈希集,以使用户可以深入了解。有关更多信息,请参见[boost的文档] [1]。[1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
亚伦·麦克戴德

11
如果没有通过多次访问对集合进行突变,则可以将其复制到数组中,然后访问O(1)。只需使用myHashSet.toArray()
ykaganovich,2010年

2
@ykaganovich会使情况变得更糟,因为必须将集合复制到新数组中?docs.oracle.com/javase/7/docs/api/java/util/… “即使此集合由数组支持,此方法也必须分配一个新数组”
anton1980

73

您是否知道一些相关的信息:

有一些有用的方法可以java.util.Collections对整个集合进行改组:Collections.shuffle(List<?>)Collections.shuffle(List<?> list, Random rnd)


太棒了!在Java文档中的任何地方都没有交叉引用!就像Python的random.shuffle()
smci 2012年

25
但这仅适用于列表,即具有.get()函数的结构。
bourbaki4481472 2015年

4
@ bourbaki4481472是绝对正确的。这仅适用于扩展List接口的那些集合,不适Set用于OP讨论的接口。
托马斯

31

Java快速解决方案,使用ArrayListand和HashMap:[element-> index]。

动机:我需要一组具有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的具体类型引用。
约翰·提登(JohanTidén)

我真的很喜欢这种解决方案,但是它不是线程安全的,因此Map和List之间可能会出现错误,因此我将添加一些同步块
Kostas Chalkias

@KonstantinosChalkias内置集合也不是线程安全的。只有带有名称的名称Concurrent才是真正安全的,带有名称的名称Collections.synchronized()是半安全的。另外,OP没有对并发发表任何评论,因此这是一个有效且很好的答案。
TWiStErRob'8

这里返回的迭代器应该不能从中删除元素dtaIterators.unmodifiableIterator例如,可以通过番石榴来实现)。否则,例如AbstractSet中的removeAll和keepAll及其父级与该迭代器一起使用的默认实现会搞砸您的RandomSet
2013年

不错的解决方案。如果每个节点包含其所根的子树中的节点数,则实际上可以使用树。然后在0..1中计算一个随机实数,并根据节点数在每个节点上进行加权的3向决策(选择当前节点或进入左或右子树)。但是,imo您的解决方案要好得多。
基因

29

这比接受的答案中的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 < 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();
    }
}

15

如果要用Java做到这一点,则应考虑将元素复制到某种随机访问集合中(例如ArrayList)。因为,除非您的集合很小,否则访问所选元素将很昂贵(O(n)而不是O(1))。[ed:列表副本也为O(n)]

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


8
复制到列表将花费O(n)的时间,并且还会使用O(n)的内存,因此,为什么比直接从地图中获取更好的选择呢?
mdma

12
这取决于您要从集合中选择多少次。复制是一次操作,然后您可以根据需要从集合中选择多次。如果您只选择一个元素,那么是的,副本不会使事情变得更快。
Dan Dyer

如果您希望能够重复进行选择,那么这只是一次操作。如果要从集合中删除选定的项目,则应返回O(n)。
TurnipEntropy

12

在Java 8中:

static <E> E getRandomSetElement(Set<E> set) {
    return set.stream().skip(new Random().nextInt(set.size())).findFirst().orElse(null);
}

9

在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())]);
}

11
您的答案有效,但是由于set.toArray()部分的原因,效率不是很高。
线索较少

12
您应该将toArray移到循环之外。
David Nehme,

8
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);

21
这是非常低效的。您的ArrayList构造函数在提供的集合上调用.toArray()。ToArray(在大多数(如果不是全部)标准集合实现中)在整个集合中进行迭代,并随即填充数组。然后,您对列表进行混洗,将每个元素与一个随机元素交换。您可以简单地将集合遍历为随机元素,这样会更好。
克里斯·波德

4

这等同于接受的答案(Khoth),但与不必要sizei除去变量。

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

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


1
第三行也可能是if (--random < 0) {,在random到达-1
萨尔瓦多

3

Clojure解决方案:

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

1
该解决方案也是线性的,因为要获取nth元素,您还必须遍历该元素seq
Bruno Kim

1
它也是线性的,因为它非常适合一行:D
Krzysztof Wolny

2

Perl 5

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

这是一种方法。


2

C ++。这应该相当快,因为​​它不需要遍历整个集合或对其进行排序。假设大多数现代编译器支持tr1,这应该是开箱即用的。如果没有,您可能需要使用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;
  }
}

2

上面的解决方案在等待时间方面讲,但是不能保证每个索引被选择的可能性相等。
如果需要考虑,请尝试进行储层采样。http://en.wikipedia.org/wiki/Reservoir_sampling
Collections.shuffle()(很少有人建议)使用一种这样的算法。


1

由于您说过“也欢迎使用其他语言的解决方案”,因此以下是适用于Python的版本:

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

3
仅[1,2,3,4,5,6]不是集合,而是列表,因为它不支持快速查找之类的功能。
Thomas Ahle

您仍然可以执行以下操作:>>> random.choice(list(set(range(5))))>>> 4不理想,但如果确实需要,它会这样做。
蓝宝石

1

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

在伪代码中-

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

仅当所涉及的容器支持随机索引查找时,此方法才有效。许多容器实现则没有(例如哈希表,二叉树,链表)。
David Haley 2010年

1

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

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

Mersenne Twister功能更好,但PHP中没有MT等效于array_rand。


大多数set实现没有get(i)或indexing运算符,因此id假定这就是OP指定其set的原因
DownloadPizza

1

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

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

会产生1到5之间的随机数。

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


1

在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"]);

似乎他们投票否决了,因为cannot的Java词典(或所谓的LinkedHashSet,无论是什么地狱)无法“随机访问”(我想可以通过键访问)。Java废话让我大笑
Federico Berasategui 2012年

1

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();

我更喜欢第二种选择。:-)
marcospereira

哦,我喜欢扩展添加新的数组方法!
matt lohkamp

1

口齿不清

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

这仅适用于列表,对吧?使用ELT它可以适用于任何顺序。
肯(Ken)2010年

1

在Mathematica中:

a = {1, 2, 3, 4, 5}

a[[  Length[a] Random[]  ]]

或者,在最新版本中,只需:

RandomChoice[a]

这得到了反对,也许是因为它缺乏解释,所以这里是:

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

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

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


1

为了好玩,我写了一个基于拒绝采样的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;
        }
    }
}

1
正是我在想的!最佳答案!
毫米

实际上,回到它,我想如果哈希图有很多冲突并且我们进行了很多查询,这并不是很统一。那是因为Java哈希图使用存储桶/链接,并且此代码将始终返回特定存储桶中的第一个元素。尽管我们仍然对哈希函数的随机性保持统一。
Thomas Ahle


1

使用番石榴,我们可以做得比霍斯的答案更好:

public static E random(Set<E> set) {
  int index = random.nextInt(set.size();
  if (set instanceof ImmutableSet) {
    // ImmutableSet.asList() is O(1), as is .get() on the returned list
    return set.asList().get(index);
  }
  return Iterables.get(set, index);
}

0

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];

0

你也可以将集合转移到使用数组的数组上,它可能会在小规模上起作用我仍然认为最投票的答案中的for循环是O(n)

Object[] arr = set.toArray();

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

0

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

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

1
不过,这不是一个随机选择。想象一下,对同一集合执行相同的操作多次。我认为顺序将是相同的。
Menezes Sousa 2015年

0

以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;
}

0

不幸的是,这在任何标准库集合容器中都无法高效地完成(优于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,但是对于我来说,今天的工作太多了:)


1
我认为未在二叉树中实现的原因是这种方法不会统一选择项目。由于它们是没有左/右子节点的节点,因此可能发生以下情况:左子节点比右子节点包含更多项目(反之亦然),这将使在右(或左)子节点中选择项目的可能性更大。
Willem Van Onsem

1
@CommuSoft:这就是为什么我存储每个子树的大小的原因,因此我可以基于这些子树来选择概率。
Thomas Ahle 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.