Escolhendo um elemento aleatório de um conjunto


180

Como escolho um elemento aleatório de um conjunto? Estou particularmente interessado em escolher um elemento aleatório de um HashSet ou LinkedHashSet, em Java. Soluções para outros idiomas também são bem-vindas.


5
Você deve especificar algumas condições para ver se é realmente isso que você deseja. - Em que horas você estará selecionando um elemento aleatório? - Os dados precisam ser armazenados em um HashSet ou LinkedHashSet, e não são acessíveis aleatoriamente. - O hash está grande? As chaves são pequenas?
David Nehme 25/09/08

Respostas:


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

94
Se myHashSet for grande, será uma solução bastante lenta, pois, em média, (n / 2) serão necessárias iterações para encontrar o objeto aleatório.
Daniel

6
se seus dados estiverem em um conjunto de hash, você precisará de O (n) tempo. Não há como contornar se você estiver apenas escolhendo um único elemento e os dados forem armazenados em um HashSet.
David Nehme 25/09/08

8
@ David Nehme: Esta é uma desvantagem na especificação do HashSet em Java. No C ++, é típico poder acessar diretamente os buckets que compõem o hashset, o que nos permite selecionar com mais eficiência elementos aleatórios. Se elementos aleatórios forem necessários em Java, pode valer a pena definir um conjunto de hash customizado que permita ao usuário olhar sob o capô. Veja [docs do boost] [1] para um pouco mais sobre isso. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Aaron McDaid

11
Se o conjunto não for alterado por vários acessos, você poderá copiá-lo em uma matriz e acessar O (1). Basta usar myHashSet.toArray ()
ykaganovich

2
@ykaganovich isso não tornaria as coisas piores, já que o conjunto teria que ser copiado para uma nova matriz? docs.oracle.com/javase/7/docs/api/java/util/... "este método deve alocar um novo array mesmo se esta coleção é apoiado por um conjunto"
anton1980

73

Um pouco relacionado Você sabia:

Existem métodos úteis java.util.Collectionspara embaralhar coleções inteiras: Collections.shuffle(List<?>)e Collections.shuffle(List<?> list, Random rnd).


Impressionante! Isso não é referência cruzada em nenhum lugar no java doc! Como random.shuffle () do Python
smci

25
Mas isso só funciona com Listas, ou seja, estruturas que possuem uma função .get ().
bourbaki4481472

4
@ bourbaki4481472 está absolutamente correto. Isso funciona apenas para as coleções que estendem a Listinterface, não a Setinterface discutida pelo OP.
Thomas

31

Solução rápida para Java usando an ArrayListe a HashMap: [elemento -> índice].

Motivação: eu precisava de um conjunto de itens com RandomAccesspropriedades, especialmente para escolher um item aleatório do conjunto (consulte o pollRandommétodo). A navegação aleatória em uma árvore binária não é precisa: as árvores não são perfeitamente equilibradas, o que não levaria a uma distribuição uniforme.

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

Bem, isso funcionaria, mas a questão era sobre a interface Set. Essa solução força os usuários a ter referências de tipo concreto do RandomSet.
Johan Tidén 5/05

Eu realmente gosto desta solução, mas não é thread-safe, imprecisões entre o mapa ea lista pode ocorrer, então eu gostaria de acrescentar alguns blocos sincronizados
Kostas Chalkias

@KonstantinosChalkias as coleções internas também não são seguras para threads. Somente aqueles com o nome Concurrentsão realmente seguros, os que estão envoltos Collections.synchronized()são semi-seguros. Além disso, o OP não disse nada sobre concorrência, portanto, essa é uma resposta válida e boa.
TWIStErRob 03/08/19

O iterador retornado aqui não deve ser capaz de remover elementos dta(isso pode ser alcançado por meio de goiabas, Iterators.unmodifiableIteratorpor exemplo). Caso contrário, as implementações padrão de, por exemplo, removeAll e retemAllAll no AbstractSet e seus pais que trabalham com esse iterador atrapalharão o seu RandomSet!
muued 12/08/16

Ótima solução. Na verdade, você pode usar uma árvore se cada nó contiver o número de nós na subárvore que ele enraíza. Em seguida, calcule um real aleatório em 0..1 e tome uma decisão de três vias ponderada (selecione o nó atual ou desça na subárvore esquerda ou direita) em cada nó com base nas contagens do nó. Mas a sua solução é muito melhor.
Gene

29

Isso é mais rápido que o loop for-each na resposta aceita:

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

A construção for-each chama Iterator.hasNext()cada loop, mas desde então index < set.size(), essa verificação é desnecessária. Eu vi um aumento de 10-20% na velocidade, mas YMMV. (Além disso, isso é compilado sem a necessidade de adicionar uma declaração de retorno extra.)

Observe que esse código (e a maioria das outras respostas) pode ser aplicado a qualquer coleção, não apenas a conjunto. Na forma genérica do método:

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

Se você quiser fazer isso em Java, considere copiar os elementos em algum tipo de coleção de acesso aleatório (como um ArrayList). Porque, a menos que seu conjunto seja pequeno, o acesso ao elemento selecionado será caro (O (n) em vez de O (1)). [ed: list copy também é O (n)]

Como alternativa, você pode procurar outra implementação de Conjunto que melhor atenda aos seus requisitos. O ListOrderedSet da Commons Collections parece promissor.


8
Copiar para uma lista custará O (n) no tempo e também usará memória (n). Então, por que essa seria uma escolha melhor do que buscar diretamente no mapa?
Mdma

12
Depende de quantas vezes você deseja escolher no aparelho. A cópia é uma operação única e, em seguida, você pode escolher entre o conjunto quantas vezes for necessário. Se você está escolhendo apenas um elemento, sim, a cópia não torna as coisas mais rápidas.
Dan Dyer

É apenas uma operação única, se você quiser escolher com repetição. Se você deseja que o item escolhido seja removido do conjunto, retorne para O (n).
precisa saber é o seguinte

12

No Java 8:

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

9

Em 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
Sua resposta funciona, mas não é muito eficiente por causa da parte set.toArray ().
Pista Menos

12
você deve mover o toArray para fora do loop.
David Nehme 25/09/08

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

21
Isso é incrivelmente ineficiente. Seu construtor ArrayList chama .toArray () no conjunto fornecido. ToArray (na maioria das implementações de coleção padrão, se não todas), interage com toda a coleção, preenchendo uma matriz à medida que ela é executada. Em seguida, você embaralha a lista, que troca cada elemento com um elemento aleatório. Você seria muito melhor simplesmente iterando o conjunto para um elemento aleatório.
precisa

4

Isso é idêntico à resposta aceita (Khoth), mas com o desnecessário sizee as ivariáveis ​​removidas.

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

Apesar de acabar com as duas variáveis ​​acima mencionadas, a solução acima ainda permanece aleatória, porque confiamos no aleatório (começando em um índice selecionado aleatoriamente) para diminuir 0cada vez mais a iteração.


1
Terceira linha também pode ser if (--random < 0) {, onde randomchega -1.
Salvador

3

Solução Clojure:

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

1
Essa solução também é linear, porque para obter o nthelemento, você também deve atravessá-lo seq.
de Bruno Kim

1
É também linear, uma vez que se adapta muito bem em uma linha: D
Krzysztof Wolny

2

Perl 5

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

Aqui está uma maneira de fazer isso.


2

C ++. Isso deve ser razoavelmente rápido, pois não requer iteração em todo o conjunto ou classificação. Isso deve funcionar imediatamente com os compiladores mais modernos, assumindo que eles suportem tr1 . Caso contrário, pode ser necessário usar o Boost.

Os documentos do Boost são úteis aqui para explicar isso, mesmo que você não use o Boost.

O truque é fazer uso do fato de que os dados foram divididos em intervalos e identificar rapidamente um intervalo escolhido aleatoriamente (com a probabilidade apropriada).

//#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

A solução acima fala em termos de latência, mas não garante a mesma probabilidade de cada índice selecionado.
Se isso precisar ser considerado, tente a amostragem do reservatório. http://en.wikipedia.org/wiki/Reservoir_sampling .
Collections.shuffle () (como sugerido por poucos) usa um desses algoritmos.


1

Como você disse que "soluções para outros idiomas também são bem-vindas", aqui está a versão do Python:

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

3
Somente [1,2,3,4,5,6] não é um conjunto, mas uma lista, pois não suporta coisas como pesquisas rápidas.
22416 Thomas Ahle

Você ainda pode fazer: >>> random.choice (list (set (range (5)))) >>> 4 Não é o ideal, mas funcionará se você precisar.
SapphireSun

1

Você não pode simplesmente obter o tamanho / comprimento do conjunto / matriz, gerar um número aleatório entre 0 e o tamanho / comprimento e chamar o elemento cujo índice corresponde a esse número? HashSet tem um método .size (), tenho certeza.

Em psuedocode -

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

Isso funciona apenas se o contêiner em questão oferecer suporte à pesquisa aleatória de índice. Muitas implementações de contêineres não o fazem (por exemplo, tabelas de hash, árvores binárias, listas vinculadas).
David Haley

1

PHP, assumindo "set" é uma matriz:

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

As funções do Mersenne Twister são melhores, mas não há equivalente em MT ao array_rand no PHP.


A maioria das implementações conjunto não tem um get (i) ou operador de indexação, de modo id assumir que é porque OP especificado é um conjunto
DownloadPizza

1

O ícone possui um tipo de conjunto e um operador de elemento aleatório, unário "?", Portanto, a expressão

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

produzirá um número aleatório entre 1 e 5.

A semente aleatória é inicializada como 0 quando um programa é executado, para produzir resultados diferentes em cada execução. randomize()


1

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

parece que eles foram prejudicados porque o dicionário java de baixa qualidade (ou o chamado LinkedHashSet, qualquer que seja o inferno) não pode ser "acessado aleatoriamente" (que está sendo acessado por chave, eu acho). A porcaria java me faz rir muito
Federico Berasategui

1

Solução Javascript;)

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

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

Ou alternativamente:

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

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

Eu prefiro a segunda alternativa. :-)
marcospereira

ooh, eu gosto de estender a adição do novo método de array!
Matt lohkamp

1

Em lisp

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

Isso funciona apenas para listas, certo? Com ELTele poderia funcionar para qualquer sequência.
Ken

1

No Mathematica:

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

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

Ou, nas versões recentes, simplesmente:

RandomChoice[a]

Isso recebeu um voto negativo, talvez por falta de explicação, então aqui está:

Random[]gera uma flutuação pseudo-aleatória entre 0 e 1. Isso é multiplicado pelo comprimento da lista e, em seguida, a função de teto é usada para arredondar para o próximo número inteiro. Este índice é então extraído a.

Como a funcionalidade da tabela de hash é frequentemente feita com regras no Mathematica e as regras são armazenadas em listas, pode-se usar:

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

1

Que tal apenas

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

1

Por diversão, escrevi um RandomHashSet baseado em amostras de rejeição. É um pouco hacky, já que o HashMap não nos permite acessar sua tabela diretamente, mas deve funcionar muito bem.

Ele não usa memória extra e o tempo de pesquisa é O (1) amortizado. (Como o java HashTable é denso).

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
Exatamente o que eu estava pensando! Melhor resposta!
mmm

Na verdade, voltando a ele, acho que isso não é bastante uniforme, se o hashmap tiver muitas colisões e fizermos muitas consultas. Isso ocorre porque o hashmap java usa buckets / encadeamento e esse código sempre retornará o primeiro elemento no bucket específico. Ainda somos uniformes quanto à aleatoriedade da função hash.
Thomas Ahle

1

O mais fácil com o Java 8 é:

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

onde né um número inteiro aleatório. É claro que tem menos desempenho do que com ofor(elem: Col)


1

Com a Goiaba , podemos fazer um pouco melhor do que a resposta de Khoth:

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, usando 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

você também pode transferir o conjunto para o array usar o array, provavelmente ele funcionará em pequena escala.

Object[] arr = set.toArray();

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

0

Se você realmente deseja selecionar "qualquer" objeto do Set, sem nenhuma garantia de aleatoriedade, o mais fácil é obter o primeiro retornado pelo iterador.

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

1
Porém, isso não será uma escolha aleatória. Imagine realizar a mesma operação no mesmo conjunto várias vezes. Eu acho que a ordem será a mesma.
Menezes Sousa

0

Uma solução genérica usando a resposta de Khoth como ponto de partida.

/**
 * @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

Infelizmente, isso não pode ser feito com eficiência (melhor que O (n)) em qualquer um dos contêineres da Biblioteca Padrão.

Isso é estranho, pois é muito fácil adicionar uma função de seleção aleatória a conjuntos de hash e também a conjuntos binários. Em um conjunto de hash não esparso, você pode tentar entradas aleatórias até receber um hit. Para uma árvore binária, você pode escolher aleatoriamente entre a subárvore esquerda ou direita, com no máximo O (log2) etapas. Implementei uma demonstração dos itens abaixo abaixo:

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

Eu tenho [995, 975, 971, 995, 1057, 1004, 966, 1052, 984, 1001] como saída, então a distribuição parece boa.

Eu lutei com o mesmo problema para mim e ainda não decidi enfrentar o ganho de desempenho dessa escolha mais eficiente que vale a pena a sobrecarga do uso de uma coleção baseada em python. É claro que eu poderia refiná-lo e traduzi-lo para C, mas isso é muito trabalho para mim hoje :)


1
Uma razão pela qual acho que isso não foi implementado em uma árvore binária é que esse método não selecionaria itens de maneira uniforme. Como são nós sem filhos esquerdo / direito, pode ocorrer uma situação em que o filho esquerdo contenha mais itens que o filho direito (ou vice-versa), isso tornaria mais provável a escolha de um item no filho direito (ou esquerdo).
Willem Van Onsem

1
@CommuSoft: É por isso que armazeno o tamanho de cada subárvore, para poder escolher minhas probabilidades com base nessas.
Thomas Ahle
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.