Scegliere un elemento casuale da un set


180

Come scelgo un elemento casuale da un set? Sono particolarmente interessato a scegliere un elemento casuale da un HashSet o LinkedHashSet, in Java. Anche le soluzioni per altre lingue sono benvenute.


5
Dovresti specificare alcune condizioni per vedere se questo è davvero quello che vuoi. - In che modo sceglierai un elemento casuale? - I dati devono essere archiviati in un HashSet o LinkedHashSet, né sono accessibili in modo casuale? - L'hash è impostato su grande? Le chiavi sono piccole?
David Nehme,

Risposte:


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 è grande, questa sarà una soluzione piuttosto lenta poiché, in media, saranno necessarie iterazioni (n / 2) per trovare l'oggetto casuale.
daniel,

6
se i tuoi dati sono in un set di hash, hai bisogno di O (n) tempo. Non c'è modo di aggirarlo se stai solo selezionando un singolo elemento e i dati sono memorizzati in un HashSet.
David Nehme,

8
@ David Nehme: questo è uno svantaggio nelle specifiche di HashSet in Java. In C ++, è tipico poter accedere direttamente ai bucket che compongono l'hashset, il che ci consente di selezionare in modo più efficiente elementi casuali. Se in Java sono necessari elementi casuali, potrebbe essere utile definire un set di hash personalizzato che consenta all'utente di guardare sotto il cofano. Vedi [boost's docs] [1] per un po 'di più in questo. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Aaron McDaid

11
Se il set non è mutato su più accessi, è possibile copiarlo in un array e quindi accedere a O (1). Usa myHashSet.toArray ()
ykaganovich il

2
@ykaganovich non peggiorerebbe le cose, dal momento che il set dovrebbe essere copiato su un nuovo array? docs.oracle.com/javase/7/docs/api/java/util/… "questo metodo deve allocare una nuova matrice anche se questa raccolta è supportata da una matrice"
anton1980

73

Un po 'correlato Lo sapevi:

Ci sono metodi utili java.util.Collectionsper mescolare intere raccolte: Collections.shuffle(List<?>)e Collections.shuffle(List<?> list, Random rnd).


Eccezionale! Questo non è referenziato da nessuna parte nel documento Java! Like a Python random.shuffle ()
smci

25
Ma questo funziona solo con Elenchi, ovvero strutture che hanno una funzione .get ().
bourbaki4481472,

4
@ bourbaki4481472 è assolutamente corretto. Questo funziona solo per quelle raccolte che estendono l' Listinterfaccia, non l' Setinterfaccia discussa dall'OP.
Thomas,

31

Soluzione rapida per Java usando an ArrayListe a HashMap: [element -> index].

Motivazione: avevo bisogno di un insieme di oggetti con RandomAccessproprietà, in particolare per scegliere un oggetto casuale dall'insieme (vedi pollRandommetodo). La navigazione casuale in un albero binario non è precisa: gli alberi non sono perfettamente bilanciati, il che non porterebbe a una distribuzione 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();
    }
}

Bene, avrebbe funzionato ma la domanda riguardava l'interfaccia Set. Questa soluzione obbliga gli utenti ad avere riferimenti di tipo concreto di RandomSet.
Johan Tidén,

Mi piace molto questa soluzione, ma non è thread-safe, potrebbero verificarsi inesattezze tra la mappa e l'elenco, quindi aggiungerei alcuni blocchi sincronizzati
Kostas Chalkias,

@KonstantinosChalkias le raccolte integrate non sono neanche sicure per i thread. Solo quelli con il nome Concurrentsono davvero sicuri, quelli avvolti Collections.synchronized()sono semi-sicuri. Inoltre, l'OP non ha detto nulla sulla concorrenza, quindi questa è una risposta valida e buona.
TWiStErRob

L'iteratore restituito qui non dovrebbe essere in grado di rimuovere gli elementi da dta(questo può essere ottenuto tramite guava Iterators.unmodifiableIteratorper esempio). Altrimenti le implementazioni predefinite di es. RemoveAll e retainAll in AbstractSet e i suoi genitori che lavorano con quell'iteratore rovineranno il tuo RandomSet!
scena il

Bella soluzione. In realtà puoi usare un albero se ogni nodo contiene il numero di nodi nella sottostruttura che attacca. Quindi calcola un reale casuale in 0..1 e prendi una decisione ponderata a 3 vie (seleziona il nodo corrente o scendi nella sottostruttura sinistra o destra) su ciascun nodo in base al conteggio dei nodi. Ma la tua soluzione è molto più bella.
Gene,

30

Questo è più veloce del ciclo for-each nella risposta accettata:

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

Il costrutto for-each chiama Iterator.hasNext()ogni loop, ma dal momento index < set.size()che quel controllo è un sovraccarico non necessario. Ho visto un aumento del 10-20% della velocità, ma YMMV. (Inoltre, questo viene compilato senza dover aggiungere un'istruzione di restituzione aggiuntiva.)

Nota che questo codice (e la maggior parte delle altre risposte) può essere applicato a qualsiasi Collezione, non solo a Set. In forma di metodo generico:

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 vuoi farlo in Java, dovresti considerare di copiare gli elementi in una specie di raccolta ad accesso casuale (come una ArrayList). Perché, a meno che il tuo set non sia piccolo, l'accesso all'elemento selezionato sarà costoso (O (n) invece di O (1)). [ed: la copia dell'elenco è anche O (n)]

In alternativa, è possibile cercare un'altra implementazione di Set che corrisponda maggiormente alle proprie esigenze. Il ListOrderedSet di Commons Collections sembra promettente.


8
La copia in un elenco costerà O (n) in tempo e utilizzerà anche la memoria O (n), quindi perché sarebbe una scelta migliore rispetto al recupero diretto dalla mappa?
mdma,

12
Dipende da quante volte vuoi scegliere dal set. La copia è un'operazione di una volta e quindi è possibile scegliere dal set tutte le volte che è necessario. Se scegli solo un elemento, sì, la copia non rende le cose più veloci.
Dan Dyer,

È solo una volta un'operazione se vuoi essere in grado di scegliere con ripetizione. Se desideri che l'elemento scelto venga rimosso dal set, tornerai a O (n).
TurnipEntropy,

13

In Java 8:

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

9

In 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
La tua risposta funziona, ma non è molto efficiente a causa della parte set.toArray ().
Indizio meno

12
dovresti spostare toArray all'esterno del loop.
David Nehme,

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

21
Questo è incredibilmente inefficiente. Il costruttore di ArrayList chiama .toArray () sul set fornito. ToArray (nella maggior parte se non tutte le implementazioni di raccolte standard) scorre sull'intera raccolta, riempiendo una matrice mentre procede. Quindi mescoli l'elenco, che scambia ogni elemento con un elemento casuale. Staresti molto meglio semplicemente iterando il set su un elemento casuale.
Chris Bode,

4

Questo è identico alla risposta accettata (Khoth), ma con il superfluo sizee le ivariabili rimosse.

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

Pur eliminando le due variabili sopra menzionate, la soluzione di cui sopra rimane ancora casuale perché ci affidiamo al casuale (a partire da un indice selezionato casualmente) per diminuire se stesso verso 0ogni iterazione.


1
La terza linea potrebbe anche essere if (--random < 0) {, dove randomraggiunge -1.
Salvador,

3

Soluzione di Clojure:

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

1
Questa soluzione è anche lineare, perché per ottenere l' nthelemento devi attraversare seqanche.
Bruno Kim,

1
È lineare anche perché si adatta perfettamente a una riga: D
Krzysztof Wolny

2

Perl 5

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

Ecco un modo per farlo.


2

C ++. Questo dovrebbe essere ragionevolmente veloce, in quanto non richiede l'iterazione sull'intero set o l'ordinamento. Questo dovrebbe funzionare immediatamente con i compilatori più moderni, supponendo che supportino tr1 . In caso contrario, potrebbe essere necessario utilizzare Boost.

I documenti Boost sono utili qui per spiegare questo, anche se non usi Boost.

Il trucco è sfruttare il fatto che i dati sono stati divisi in bucket e identificare rapidamente un bucket scelto casualmente (con la probabilità appropriata).

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

La soluzione sopra parla in termini di latenza ma non garantisce la stessa probabilità di ciascun indice selezionato.
Se questo deve essere considerato, prova il campionamento del serbatoio. http://it.wikipedia.org/wiki/Reservoir_sampling .
Collections.shuffle () (come suggerito da pochi) utilizza uno di questi algoritmi.


1

Dato che hai detto "Anche le soluzioni per altre lingue sono benvenute", ecco la versione per Python:

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

3
Solo [1,2,3,4,5,6] non è un set, ma un elenco, poiché non supporta elementi come le ricerche rapide.
Thomas Ahle,

Puoi ancora fare: >>> random.choice (list (set (range (5)))) >>> 4 Non è l'ideale ma lo farà se è assolutamente necessario.
SapphireSun

1

Non puoi semplicemente ottenere la dimensione / lunghezza dell'insieme / matrice, generare un numero casuale compreso tra 0 e la dimensione / lunghezza, quindi chiamare l'elemento il cui indice corrisponde a quel numero? HashSet ha un metodo .size (), ne sono abbastanza sicuro.

In psuedocode -

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

Funziona solo se il contenitore in questione supporta la ricerca di indici casuali. Molte implementazioni di container no (ad es. Tabelle hash, alberi binari, elenchi collegati).
David Haley,

1

PHP, supponendo che "set" sia un array:

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

Le funzioni di Mersenne Twister sono migliori ma non esiste un equivalente MT di array_rand in PHP.


La maggior parte delle implementazioni di set non ha un operatore get (i) o di indicizzazione, quindi id suppongo che sia per questo motivo che OP ha specificato che è un set
DownloadPizza

1

L'icona ha un tipo impostato e un operatore ad elementi casuali, unario "?", Quindi l'espressione

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

produrrà un numero casuale compreso tra 1 e 5.

Il seed casuale viene inizializzato su 0 quando viene eseguito un programma, in modo da produrre risultati diversi su ogni utilizzo della corsa randomize()


1

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

sembra che abbiano effettuato il downgrade perché il maledetto dizionario java (o il cosiddetto LinkedHashSet, qualunque diavolo sia) non può essere "casualmente accessibile" (a cui si accede tramite chiave, immagino). La merda di java mi fa ridere così tanto
Federico Berasategui il

1

Soluzione Javascript;)

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

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

O in alternativa:

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

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

Preferisco la seconda alternativa. :-)
marcospereira,

ooh, mi piace estendere l'aggiunta del nuovo metodo array!
matt lohkamp,

1

In lisp

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

Funziona solo con gli elenchi, giusto? Con ELTesso potrebbe funzionare per qualsiasi sequenza.
Ken,

1

In Mathematica:

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

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

O, nelle versioni recenti, semplicemente:

RandomChoice[a]

Questo ha ricevuto un voto negativo, forse perché manca di spiegazione, quindi eccone uno:

Random[]genera un float pseudocasuale tra 0 e 1. Questo viene moltiplicato per la lunghezza dell'elenco e quindi la funzione soffitto viene utilizzata per arrotondare al numero intero successivo. Questo indice viene quindi estratto da a.

Poiché la funzionalità della tabella hash viene spesso eseguita con le regole in Mathematica e le regole sono memorizzate in elenchi, è possibile utilizzare:

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

1

Che ne dici di solo

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

1

Per divertimento ho scritto un RandomHashSet basato sul campionamento del rifiuto. È un po 'confuso, poiché HashMap non ci consente di accedere direttamente alla sua tabella, ma dovrebbe funzionare bene.

Non utilizza memoria aggiuntiva e il tempo di ricerca è ammortizzato O (1). (Perché 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
Esattamente quello che stavo pensando! Migliore risposta!
mmm

In realtà, tornando ad esso, suppongo che questo non sia del tutto uniforme, se l'hashmap ha molte collisioni e facciamo molte domande. Questo perché l'hashmap java utilizza bucket / concatenamento e questo codice restituirà sempre il primo elemento nel bucket specifico. Tuttavia, siamo ancora uniformi sulla casualità della funzione hash.
Thomas Ahle,

1

Il più semplice con Java 8 è:

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

dove nè un numero intero casuale. Ovviamente è meno performante di quello con ilfor(elem: Col)


1

Con Guava possiamo fare un po 'meglio della risposta di 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

puoi anche trasferire il set in array usa array probabilmente funzionerà su piccola scala vedo il ciclo for nella risposta più votata è comunque O (n)

Object[] arr = set.toArray();

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

0

Se vuoi davvero scegliere "qualsiasi" oggetto dal Set, senza alcuna garanzia sulla casualità, il più semplice è prendere il primo restituito dall'iteratore.

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

1
Questa non sarà una scelta casuale però. Immagina di eseguire più volte la stessa operazione sullo stesso set. Penso che l'ordine sarà lo stesso.
Menezes Sousa,

0

Una soluzione generica che utilizza la risposta di Khoth come punto di partenza.

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

Sfortunatamente, ciò non può essere fatto in modo efficiente (meglio di O (n)) in nessuno dei contenitori di set di librerie standard.

Questo è strano, dal momento che è molto facile aggiungere una funzione di scelta casuale ai set di hash e ai set binari. In un set di hash non sparse, puoi provare voci casuali, fino a quando non ottieni un successo. Per un albero binario, puoi scegliere casualmente tra la sottostruttura sinistra o destra, con un massimo di O (log2) passi. Ho implementato una demo di seguito:

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

Ho ottenuto [995, 975, 971, 995, 1057, 1004, 966, 1052, 984, 1001] come output, quindi la distribuzione è buona.

Ho lottato con lo stesso problema per me stesso e non ho ancora deciso il tempo che il guadagno in termini di prestazioni di questa scelta più efficiente vale il sovraccarico dell'uso di una collezione basata su Python. Potrei ovviamente perfezionarlo e tradurlo in C, ma oggi è troppo lavoro per me :)


1
Un motivo per cui penso che questo non sia implementato in un albero binario è che un tale metodo non selezionerebbe gli oggetti in modo uniforme. Poiché si tratta di nodi senza figli sinistro / destro, potrebbe verificarsi una situazione in cui il figlio sinistro contiene più elementi rispetto al figlio destro (o viceversa), ciò renderebbe più probabile la raccolta di un oggetto sul figlio destro (o sinistro).
Willem Van Onsem,

1
@CommuSoft: ecco perché memorizzo le dimensioni di ogni sottostruttura, in modo da poter scegliere le mie probabilità in base a quelle.
Thomas Ahle,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.