Elegir un elemento aleatorio de un conjunto

180

¿Cómo elijo un elemento aleatorio de un conjunto? Estoy particularmente interesado en elegir un elemento aleatorio de un HashSet o LinkedHashSet, en Java. Las soluciones para otros idiomas también son bienvenidas.

Pista Menos
fuente
55
Debe especificar algunas condiciones para ver si esto es realmente lo que desea. - ¿Cuántas veces vas a seleccionar un elemento aleatorio? - ¿Es necesario almacenar los datos en un HashSet o LinkedHashSet? No se puede acceder a ninguno de ellos de manera aleatoria. - ¿Es grande el hash? ¿Son pequeñas las llaves?
David Nehme

Respuestas:

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++;
}
Khoth
fuente
94
Si myHashSet es grande, entonces esta será una solución bastante lenta ya que, en promedio, se necesitarán (n / 2) iteraciones para encontrar el objeto aleatorio.
daniel
66
Si sus datos están en un conjunto hash, necesita tiempo O (n). No hay forma de evitarlo si solo está eligiendo un solo elemento y los datos se almacenan en un HashSet.
David Nehme
8
@David Nehme: este es un inconveniente en la especificación de HashSet en Java. En C ++, es típico poder acceder directamente a los cubos que componen el hashset, lo que nos permite seleccionar de manera más eficiente elementos aleatorios. Si los elementos aleatorios son necesarios en Java, podría valer la pena definir un conjunto de hash personalizado que permita al usuario mirar debajo del capó. Ver [documentos de impulso] [1] para un poco más en esto. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Aaron McDaid el
11
Si el conjunto no está mutado en múltiples accesos, puede copiarlo en una matriz y luego acceder a O (1). Solo use myHashSet.toArray ()
ykaganovich el
2
@ykaganovich, ¿esto no empeoraría las cosas, ya que el conjunto tendría que copiarse en una nueva matriz? docs.oracle.com/javase/7/docs/api/java/util/… "este método debe asignar una nueva matriz incluso si esta colección está respaldada por una matriz"
anton1980
73

Un poco relacionado ¿Sabía usted:

Existen métodos útiles java.util.Collectionspara barajar colecciones enteras: Collections.shuffle(List<?>)y Collections.shuffle(List<?> list, Random rnd).

galletas de pollo
fuente
¡Increíble! ¡Esto no tiene referencias cruzadas en ningún lugar del documento de Java! Al igual que random.shuffle de Python ()
smci
25
Pero esto solo funciona con Listas, es decir, estructuras que tienen una función .get ().
bourbaki4481472
44
@ bourbaki4481472 es absolutamente correcto. Esto solo funciona para aquellas colecciones que extienden la Listinterfaz, no la Setinterfaz discutida por el OP.
Thomas
31

Solución rápida para Java utilizando una ArrayListy una HashMap: [elemento -> índice].

Motivación: necesitaba un conjunto de elementos con RandomAccesspropiedades, especialmente para elegir un elemento aleatorio del conjunto (ver pollRandommétodo). La navegación aleatoria en un árbol binario no es precisa: los árboles no están perfectamente equilibrados, lo que no conduciría a una distribución 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();
    }
}
Fandrew
fuente
Bueno, eso funcionaría, pero la pregunta era sobre la interfaz Set. Esta solución obliga a los usuarios a tener referencias de tipos concretos de RandomSet.
Johan Tidén
Realmente me gusta esta solución, pero no es segura para subprocesos, pueden ocurrir imprecisiones entre el Mapa y la Lista, por lo que agregaría algunos bloques sincronizados
Kostas Chalkias
@KonstantinosChalkias las colecciones integradas tampoco son seguras para subprocesos. Solo los que tienen el nombre Concurrentson realmente seguros, los que están envueltos Collections.synchronized()son semi-seguros. Además, el OP no dijo nada sobre la concurrencia, por lo que esta es una respuesta válida y buena.
TWiStErRob
El iterador devuelto aquí no debería poder eliminar elementos de dta(esto se puede lograr a través de guayaba, Iterators.unmodifiableIteratorpor ejemplo). De lo contrario, las implementaciones predeterminadas de, por ejemplo, removeAll y retenerAll en AbstractSet y sus padres que trabajan con ese iterador lo arruinarán RandomSet.
silenciado el
Buena solución En realidad, puede usar un árbol si cada nodo contiene el número de nodos en el subárbol que arraiga. Luego calcule un real aleatorio en 0..1 y tome una decisión de 3 vías ponderada (seleccione el nodo actual o descienda al subárbol izquierdo o derecho) en cada nodo en función de los recuentos de nodos. Pero mi solución es mucho mejor.
Gene
29

Esto es más rápido que el ciclo for-each en la respuesta aceptada:

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

La construcción for-each llama Iterator.hasNext()a cada ciclo, pero desde entonces index < set.size(), esa verificación es una sobrecarga innecesaria. Vi un aumento del 10-20% en la velocidad, pero YMMV. (Además, esto se compila sin tener que agregar una declaración de devolución adicional).

Tenga en cuenta que este código (y la mayoría de las otras respuestas) se pueden aplicar a cualquier Colección, no solo a Establecer. En forma de método genérico:

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();
    }
}
Sean Van Gorder
fuente
15

Si desea hacerlo en Java, debería considerar copiar los elementos en algún tipo de colección de acceso aleatorio (como una ArrayList). Porque, a menos que su conjunto sea pequeño, acceder al elemento seleccionado será costoso (O (n) en lugar de O (1)). [ed: la copia de la lista también es O (n)]

Alternativamente, puede buscar otra implementación de Set que coincida más estrechamente con sus requisitos. El ListOrderedSet de Commons Collections parece prometedor.

Dan Dyer
fuente
8
Copiar a una lista le costará O (n) a tiempo y también usará memoria O (n), entonces, ¿por qué sería una mejor opción que buscar directamente del mapa?
mdma
12
Depende de cuántas veces desee elegir del conjunto. La copia es una operación única y luego puede elegir del conjunto tantas veces como sea necesario. Si solo está eligiendo un elemento, entonces sí, la copia no hace las cosas más rápido.
Dan Dyer
Es solo una operación de una sola vez si desea poder elegir con repetición. Si desea que el elemento elegido se elimine del conjunto, volvería a O (n).
TurnipEntropy
12

En Java 8:

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

En 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())]);
}
Jorge Ferreira
fuente
11
Su respuesta funciona, pero no es muy eficiente debido a la parte set.toArray ().
Clue Less
12
debe mover toArray hacia afuera del bucle.
David Nehme
8
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);
Ben Noland
fuente
21
Esto es abismalmente ineficiente. Su constructor ArrayList llama a .toArray () en el conjunto suministrado. ToArray (en la mayoría, si no en todas las implementaciones de colección estándar) itera sobre toda la colección, llenando una matriz a medida que avanza. Luego baraja la lista, que intercambia cada elemento con un elemento aleatorio. Sería mucho mejor simplemente iterar sobre el conjunto a un elemento aleatorio.
Chris Bode
4

Esto es idéntico a la respuesta aceptada (Khoth), pero con lo innecesario sizey las ivariables eliminadas.

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

Aunque eliminando las dos variables mencionadas anteriormente, la solución anterior sigue siendo aleatoria porque confiamos en aleatoria (comenzando en un índice seleccionado aleatoriamente) para disminuir hacia 0cada iteración.

Jason Hartley
fuente
1
La tercera línea también podría ser if (--random < 0) {, donde randomllega -1.
Salvador
3

Solución Clojure:

(defn pick-random [set] (let [sq (seq set)] (nth sq (rand-int (count sq)))))
pjb3
fuente
1
Esta solución también es lineal, porque para obtener el nthelemento también debe atravesarlo seq.
Bruno Kim
1
También es lineal, ya que encaja perfectamente en una línea: D
Krzysztof Wolny
2

Perl 5

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

Aquí hay una forma de hacerlo.

JJ
fuente
2

C ++. Esto debería ser razonablemente rápido, ya que no requiere iterar en todo el conjunto u ordenarlo. Esto debería funcionar de inmediato con la mayoría de los compiladores modernos, suponiendo que sean compatibles con tr1 . Si no, es posible que deba usar Boost.

Los documentos de Boost son útiles aquí para explicar esto, incluso si no usa Boost.

El truco consiste en hacer uso del hecho de que los datos se han dividido en cubos e identificar rápidamente un cubo elegido al azar (con la probabilidad adecuada).

//#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;
  }
}
Aaron McDaid
fuente
2

La solución anterior habla en términos de latencia, pero no garantiza la misma probabilidad de que se seleccione cada índice.
Si eso necesita ser considerado, pruebe el muestreo de yacimientos. http://en.wikipedia.org/wiki/Reservoir_sampling .
Collections.shuffle () (como sugieren algunos) usa uno de esos algoritmos.

el paso
fuente
1

Como dijiste "Las soluciones para otros idiomas también son bienvenidas", aquí está la versión para Python:

>>> import random
>>> random.choice([1,2,3,4,5,6])
3
>>> random.choice([1,2,3,4,5,6])
4
Swaroop CH
fuente
3
Solo que [1,2,3,4,5,6] no es un conjunto, sino una lista, ya que no admite cosas como búsquedas rápidas.
Thomas Ahle
Todavía puede hacer: >>> random.choice (list (set (range (5)))) >>> 4 No es ideal, pero lo hará si es absolutamente necesario.
SapphireSun
1

¿No puede obtener el tamaño / longitud del conjunto / matriz, generar un número aleatorio entre 0 y el tamaño / longitud, y luego llamar al elemento cuyo índice coincide con ese número? HashSet tiene un método .size (), estoy bastante seguro.

En psuedocode -

function randFromSet(target){
 var targetLength:uint = target.length()
 var randomIndex:uint = random(0,targetLength);
 return target[randomIndex];
}
Matt Lohkamp
fuente
Esto solo funciona si el contenedor en cuestión admite la búsqueda de índice aleatorio. Muchas implementaciones de contenedores no lo hacen (por ejemplo, tablas hash, árboles binarios, listas vinculadas).
David Haley
1

PHP, suponiendo que "set" es una matriz:

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

Las funciones de Mersenne Twister son mejores, pero no hay un equivalente MT de array_rand en PHP.

lado de tierra
fuente
La mayoría de las implementaciones establecidas no tienen un operador get (i) o de indexación, así que supongo que es por eso que OP especificó su conjunto
DownloadPizza
1

El icono tiene un tipo de conjunto y un operador de elemento aleatorio, unario "?", Por lo que la expresión

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

producirá un número aleatorio entre 1 y 5.

La inicialización aleatoria se inicializa a 0 cuando se ejecuta un programa, por lo que para producir resultados diferentes en cada ejecución, use randomize()

Hugh Allen
fuente
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"]);
Trigo Mitch
fuente
parece que se votó en contra porque el diccionario de Java de mierda (o el llamado LinkedHashSet, lo que sea que sea) no se puede "acceder aleatoriamente" (al que se accede por clave, supongo). La mierda de Java me hace reír mucho
Federico Berasategui
1

Solución Javascript;)

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

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

O alternativamente:

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

[1, 2, 3, 4].choose();
Mathew Byrne
fuente
Prefiero la segunda alternativa. :-)
marcospereira
ooh, me gusta extender agregando el nuevo método de matriz!
matt lohkamp
1

En lisp

(defun pick-random (set)
       (nth (random (length set)) set))
inglesp
fuente
Esto solo funciona para listas, ¿verdad? Con ELTeso podría funcionar para cualquier secuencia.
Ken
1

En Mathematica:

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

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

O, en versiones recientes, simplemente:

RandomChoice[a]

Esto recibió un voto negativo, tal vez porque carece de explicación, así que aquí hay uno:

Random[]genera un flotante pseudoaleatorio entre 0 y 1. Esto se multiplica por la longitud de la lista y luego la función de techo se utiliza para redondear al siguiente entero. Este índice se extrae de a.

Dado que la funcionalidad de la tabla hash se realiza con frecuencia con reglas en Mathematica, y las reglas se almacenan en listas, se podría usar:

a = {"Badger" -> 5, "Bird" -> 1, "Fox" -> 3, "Frog" -> 2, "Wolf" -> 4};
Señor mago
fuente
1

¿Qué tal solo

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

Por diversión, escribí un RandomHashSet basado en muestras de rechazo. Es un poco hacky, ya que HashMap no nos permite acceder a su tabla directamente, pero debería funcionar bien.

No utiliza memoria adicional, y el tiempo de búsqueda es O (1) amortizado. (Porque Java HashTable es 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;
        }
    }
}
Thomas Ahle
fuente
1
¡Exactamente lo que estaba pensando! ¡La mejor respuesta!
mmm
En realidad, volviendo a eso, supongo que esto no es bastante uniforme, si el hashmap tiene muchas colisiones y hacemos muchas consultas. Esto se debe a que el hashmap de Java usa cubos / encadenamiento y este código siempre devolverá el primer elemento en el cubo particular. Sin embargo, todavía somos uniformes sobre la aleatoriedad de la función hash.
Thomas Ahle
1

Lo más fácil con Java 8 es:

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

donde nes un entero aleatorio. Por supuesto, es de menor rendimiento que eso con elfor(elem: Col)

Nicu Marasoiu
fuente
1

Con Guava podemos hacer un poco mejor que la respuesta 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);
}
dimo414
fuente
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];
da5id
fuente
0

también puede transferir el conjunto a la matriz usar matriz, probablemente funcionará a pequeña escala, veo que el bucle for en la respuesta más votada es O (n) de todos modos

Object[] arr = set.toArray();

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

Si realmente solo desea elegir "cualquier" objeto del Set, sin ninguna garantía sobre la aleatoriedad, lo más fácil es tomar el primero devuelto por el iterador.

    Set<Integer> s = ...
    Iterator<Integer> it = s.iterator();
    if(it.hasNext()){
        Integer i = it.next();
        // i is a "random" object from set
    }
Philipp
fuente
1
Sin embargo, esta no será una elección aleatoria. Imagine realizar la misma operación en el mismo conjunto varias veces. Creo que el orden será el mismo.
Menezes Sousa
0

Una solución genérica que utiliza la respuesta de Khoth como punto 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;
}
stivlo
fuente
0

Desafortunadamente, esto no se puede hacer de manera eficiente (mejor que O (n)) en ninguno de los contenedores de conjuntos de la Biblioteca estándar.

Esto es extraño, ya que es muy fácil agregar una función de selección aleatoria a conjuntos de hash y conjuntos binarios. En un conjunto de hash no escaso, puede intentar entradas aleatorias, hasta que obtenga un acierto. Para un árbol binario, puede elegir aleatoriamente entre el subárbol izquierdo o derecho, con un máximo de O (log2) pasos. He implementado una demostración de lo siguiente a continuación:

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

Obtuve [995, 975, 971, 995, 1057, 1004, 966, 1052, 984, 1001] como salida, por lo que la distribución parece buena.

He luchado con el mismo problema para mí, y aún no he decidido si el aumento de rendimiento de esta elección más eficiente vale la pena de usar una colección basada en Python. Por supuesto, podría refinarlo y traducirlo a C, pero eso es demasiado trabajo para mí hoy :)

Thomas Ahle
fuente
1
Una razón por la que creo que esto no se implementa en un árbol binario es que dicho método no seleccionaría elementos de manera uniforme. Dado que son nodos sin hijos izquierdo / derecho, puede ocurrir una situación en la que el hijo izquierdo contiene más elementos que el hijo derecho (o viceversa), esto haría que elegir un elemento en el hijo derecho (o izquierdo) sea más probable.
Willem Van Onsem
1
@CommuSoft: por eso almaceno el tamaño de cada subárbol, para poder elegir mis probabilidades en función de ellas.
Thomas Ahle