194 votes

Choisir un élément aléatoire dans un ensemble

Comment choisir un élément aléatoire dans un ensemble ? Je suis particulièrement intéressé par le prélèvement d'un élément aléatoire dans un HashSet ou un LinkedHashSet, en Java. Les solutions pour d'autres langages sont également les bienvenues.

5 votes

Vous devriez spécifier quelques conditions pour voir si c'est vraiment ce que vous voulez. - Combien de fois allez-vous sélectionner un élément aléatoire ? - Les données doivent-elles être stockées dans un HashSet ou un LinkedHashSet, les deux n'étant pas accessibles de manière aléatoire. - L'ensemble de hachage est-il grand ? Les clés sont-elles petites ?

98voto

Khoth Points 7001
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++;
}

101 votes

Si myHashSet est grand, alors cette solution sera plutôt lente puisqu'en moyenne, (n / 2) itérations seront nécessaires pour trouver l'objet aléatoire.

6 votes

Si vos données sont dans un ensemble de hachage, vous avez besoin d'un temps O(n). Il n'y a aucun moyen de contourner ce problème si vous choisissez un seul élément et que les données sont stockées dans un HashSet.

8 votes

@David Nehme : C'est un inconvénient de la spécification de HashSet en Java. En C++, il est typique de pouvoir accéder directement aux buckets qui composent le hashset, ce qui nous permet de sélectionner plus efficacement un élément aléatoire. Si des éléments aléatoires sont nécessaires en Java, il peut être intéressant de définir un ensemble de hachage personnalisé qui permet à l'utilisateur de regarder sous le capot. Voir la [docs de boost][1] pour en savoir un peu plus à ce sujet. [1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html

75voto

Ash Kim Points 3369

Un peu plus loin, vous savez :

Il existe des méthodes utiles dans java.util.Collections pour mélanger des collections entières : Collections.shuffle(List<?>) y Collections.shuffle(List<?> list, Random rnd) .

0 votes

Génial ! Ce n'est pas référencé n'importe où dans la doc Java ! Comme random.shuffle() de Python

30 votes

Mais cela ne fonctionne qu'avec les listes, c'est-à-dire les structures qui ont une fonction .get().

5 votes

@bourbaki4481472 a tout à fait raison. Cela ne fonctionne que pour les collections qui étendent le List et non l'interface Set interface discutée par le PO.

36voto

fandrew Points 81

Solution rapide pour Java en utilisant un ArrayList et un HashMap : [élément -> indice].

Motivation : J'avais besoin d'un ensemble d'articles avec RandomAccess notamment pour choisir un élément aléatoire dans l'ensemble (cf. pollRandom méthode). La navigation aléatoire dans un arbre binaire n'est pas précise : les arbres ne sont pas parfaitement équilibrés, ce qui ne conduirait pas à une distribution 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();
    }
}

0 votes

Cela fonctionnerait, mais la question portait sur l'interface Set. Cette solution oblige les utilisateurs à avoir des références de type concret du RandomSet.

0 votes

J'aime beaucoup cette solution, mais elle n'est pas thread safe, des imprécisions entre la carte et la liste peuvent se produire, j'ajouterais donc des blocs synchronisés

0 votes

@KonstantinosChalkias les collections intégrées ne sont pas non plus thread safe. Seuls ceux qui portent le nom Concurrent sont vraiment sûrs, ceux qui sont emballés avec Collections.synchronized() sont semi-sûrs. De plus, l'OP n'a rien dit au sujet de la concurrence, donc c'est une réponse valide et bonne.

16voto

Dan Dyer Points 30082

Si vous voulez le faire en Java, vous devriez envisager de copier les éléments dans une sorte de collection à accès aléatoire (telle qu'une ArrayList). Car, à moins que votre ensemble soit petit, l'accès à l'élément sélectionné sera coûteux (O(n) au lieu de O(1)). [ed : la copie de liste est aussi O(n)].

Sinon, vous pouvez chercher une autre implémentation de Set qui correspond mieux à vos besoins. Le site ListOrderedSet de Commons Collections semble prometteur.

8 votes

Copier dans une liste coûtera O(n) en temps et utilisera également O(n) de mémoire, alors pourquoi serait-ce un meilleur choix que d'aller chercher directement dans la carte ?

13 votes

Cela dépend du nombre de fois que vous voulez choisir dans le jeu. La copie est une opération unique et vous pouvez ensuite choisir dans l'ensemble autant de fois que vous le souhaitez. Si vous ne sélectionnez qu'un seul élément, alors oui, la copie ne rend pas les choses plus rapides.

0 votes

Il s'agit d'une opération unique si vous voulez pouvoir choisir de manière répétée. Si vous voulez que l'élément choisi soit retiré de l'ensemble, alors vous en reviendrez à O(n).

9voto

smink Points 39640

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

12 votes

Votre réponse fonctionne, mais elle n'est pas très efficace à cause de la partie set.toArray( ).

12 votes

Vous devriez déplacer le toArray en dehors de la boucle.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X