Ensemble Python avec la possibilité de faire apparaître un élément aléatoire

J'ai besoin d'un objet Python (2.7) qui fonctionne comme un ensemble (insertion rapide, suppression et vérification d'appartenance) mais qui a la capacité de renvoyer une valeur aléatoire. Les questions précédentes posées sur stackoverflow ont des réponses comme:

import random
random.sample(mySet, 1)

Mais c'est assez lent pour les grands ensembles (il s'exécute en temps O (N)).

Les autres solutions ne sont pas assez aléatoires (elles dépendent de la représentation interne des ensembles python, ce qui produit des résultats très non aléatoire):

for e in mySet:
    break
# e is now an element from mySet

J'ai codé ma propre classe rudimentaire qui a une recherche en temps constant, une suppression et des valeurs aléatoires.

class randomSet:
    def __init__(self):
        self.dict = {}
        self.list = []

    def add(self, item):
        if item not in self.dict:
            self.dict[item] = len(self.list)
            self.list.append(item)

    def addIterable(self, item):
        for a in item:
            self.add(a)

    def delete(self, item):
        if item in self.dict:
            index = self.dict[item]
            if index == len(self.list)-1:
                del self.dict[self.list[index]]
                del self.list[index]
            else:
                self.list[index] = self.list.pop()
                self.dict[self.list[index]] = index
                del self.dict[item]

    def getRandom(self):
        if self.list:
            return self.list[random.randomint(0,len(self.list)-1)]

    def popRandom(self):
        if self.list:
            index = random.randint(0,len(self.list)-1)
            if index == len(self.list)-1:
                del self.dict[self.list[index]]
                return self.list.pop()
            returnValue = self.list[index]
            self.list[index] = self.list.pop()
            self.dict[self.list[index]] = index
            del self.dict[returnValue]
            return returnValue

Y a-t-il de meilleures implémentations pour cela, ou de grandes améliorations à apporter à ce code?

23
demandé sur GrantS 2012-09-25 20:37:51

6 réponses

Je pense que la meilleure façon de le faire serait d'utiliser l'MutableSet classe de base abstraite dans collections. Hériter de MutableSet, puis définir add, discard, __len__, __iter__, et __contains__; également réécrire __init__ pour éventuellement accepter une séquence, tout comme les set constructeur n'. MutableSet fournit des définitions intégrées de toutes les autres méthodes set basées sur ces méthodes. De cette façon, vous obtenez l'interface set complète à moindre coût. (Et si vous faites cela, addIterable est défini pour vous, sous le nom de extend.)

discard dans la norme set interface semble être ce que vous avez appelé delete ici. Alors renommez delete en discard. De plus, au lieu d'avoir une méthode popRandom séparée, Vous pouvez simplement définir popRandom comme ceci:

def popRandom(self):
    item = self.getRandom()
    self.discard(item)
    return item

De cette façon, vous n'avez pas à maintenir deux méthodes de suppression d'éléments distinctes.

Enfin, dans votre élément méthode de suppression (delete maintenant, discard selon la norme de l'interface), vous n'avez pas besoin d'une instruction if. Au lieu de tester si index == len(self.list) - 1, échangez simplement l'élément final de la liste avec l'élément à l'index de la liste à sauter, et apporter la modification nécessaire au dictionnaire d'indexation inverse. Puis pop le dernier élément de la liste et le supprimer du dictionnaire. Cela fonctionne si index == len(self.list) - 1 ou non:

def discard(self, item):
    if item in self.dict:
        index = self.dict[item]
        self.list[index], self.list[-1] = self.list[-1], self.list[index]
        self.dict[self.list[index]] = index
        del self.list[-1]                    # or in one line:
        del self.dict[item]                  # del self.dict[self.list.pop()]
19
répondu senderle 2016-11-01 20:08:06

Une approche que vous pourriez prendre est de dériver une nouvelle classe de {[0] } qui se salit avec des objets aléatoires d'un type dérivé de int.

Vous pouvez ensuite utiliser pop pour sélectionner un élément aléatoire, et s'il n'est pas du type salt, réinsérez-le et renvoyez-le, mais s'il est du type salt, insérez un nouvel objet salt généré aléatoirement (et pop pour sélectionner un nouvel objet).

Cela aura tendance à modifier l'ordre dans lequel les objets sont sélectionnés. En moyenne, le nombre de tentatives dépendra de la proportion d'éléments de salage, c'est-à-dire amortis O(K) performance.

2
répondu Marcin 2012-09-25 17:20:48

Ne Pouvons-nous pas implémenter une nouvelle classe héritant de set avec quelques modifications (hackish) qui nous permettent de récupérer un élément aléatoire de la liste avec un temps de recherche O(1)? Btw, sur Python 2.x vous devez hériter de object, c'est à dire utiliser class randomSet(object). Aussi PEP8 est quelque chose à considérer pour vous :-)

Modifier: Pour avoir quelques idées sur ce que les solutions hackish pourraient être capables de faire, ce fil vaut la peine d'être lu: http://python.6.n6.nabble.com/Get-item-from-set-td1530758.html

1
répondu Jan-Philip Gehrcke 2012-09-25 17:39:08

Oui, j'implémenterais un" ensemble ordonné " de la même manière que vous l'avez fait - et j'utiliserais une liste comme structure de données interne.

Cependant, j'hériterais directement de " set " et garderais juste une trace des éléments ajoutés dans un liste interne ( comme vous l'avez fait) - et laissez les méthodes que je n'utilise pas seul.

Peut-être ajouter une méthode "sync" pour mettre à jour la liste interne chaque fois que l'ensemble est mis à jour par des opérations spécifiques à l'ensemble, comme les méthodes *_update.

Que si vous utilisez un "dict ordonné" ne couvre pas votre les cas d'utilisation. (Je viens de trouver qu'essayer de convertir des clés ordered_dict en un ensemble régulier n'est pas optmisé, donc si vous avez besoin d'opérations de définition sur vos données, ce n'est pas une option)

0
répondu jsbueno 2012-09-25 17:30:57

Si cela ne vous dérange pas de ne prendre en charge que des éléments comparables, vous pouvez utiliser blist.sortedset.

0
répondu Neil G 2012-09-28 05:09:12

Voici une solution à partir de zéro, qui ajoute et apparaît en temps constant. J'ai également inclus quelques fonctions de jeu supplémentaires à des fins démonstratives.

from random import randint


class RandomSet(object):
  """
  Implements a set in which elements can be
  added and drawn uniformly and randomly in
  constant time.
  """

  def __init__(self, seq=None):
    self.dict = {}
    self.list = []
    if seq is not None:
      for x in seq:
        self.add(x)

  def add(self, x):
    if x not in self.dict:
      self.dict[x] = len(self.list)
      self.list.append(x)

  def pop(self, x=None):
    if x is None:
      i = randint(0,len(self.list)-1)
      x = self.list[i]
    else:
      i = self.dict[x]
    self.list[i] = self.list[-1]
    self.dict[self.list[-1]] = i
    self.list.pop()
    self.dict.pop(x)
    return x

  def __contains__(self, x):
    return x in self.dict

  def __iter__(self):
    return iter(self.list)

  def __repr__(self):
    return "{" + ", ".join(str(x) for x in self.list) + "}"

  def __len__(self):
    return len(self.list)
0
répondu markrogersjr 2017-08-08 18:15:34