webentwicklung-frage-antwort-db.com.de

beste Möglichkeit, eine zufällige Teilmenge aus einer Sammlung auszuwählen?

Ich habe eine Reihe von Objekten in einem Vektor, aus denen ich eine zufällige Untermenge auswählen möchte (z. B. 100 zurückkommende Elemente; 5 zufällig auswählen). In meinem ersten (sehr voreiligen) Pass habe ich eine extrem einfache und vielleicht zu clevere Lösung gefunden:

Vector itemsVector = getItems();

Collections.shuffle(itemsVector);
itemsVector.setSize(5);

Während dies den Vorteil hat, nett und einfach zu sein, vermute ich, dass es nicht gut skalierbar ist, d. H. Collections.shuffle () muss mindestens O(n) sein. Meine weniger clevere Alternative ist

Vector itemsVector = getItems();

Random Rand = new Random(System.currentTimeMillis()); // would make this static to the class    

List subsetList = new ArrayList(5);
for (int i = 0; i < 5; i++) {
     // be sure to use Vector.remove() or you may get the same item twice
     subsetList.add(itemsVector.remove(Rand.nextInt(itemsVector.size())));
}

Irgendwelche Vorschläge, wie Sie eine zufällige Teilmenge aus einer Sammlung besser ziehen können?

64
Tom

Jon Bentley erläutert dies entweder in 'Programmierperlen' oder 'Mehr Programmierperlen'. Sie müssen vorsichtig sein mit Ihrem N of M-Auswahlprozess, aber ich denke, dass der gezeigte Code richtig funktioniert. Anstatt alle Elemente zufällig zu mischen, können Sie die zufällige Zufallswiedergabe nur durch Mischen der ersten N-Positionen durchführen. Dies ist eine nützliche Ersparnis, wenn N << M.

Knuth bespricht auch diese Algorithmen - ich glaube, dass dies Band 3 "Sortieren und Suchen" wäre, aber mein Set ist bis zum Umzug vollgepackt, sodass ich das nicht formal überprüfen kann.

10

@ Jonathan,

Ich glaube, das ist die Lösung, über die Sie sprechen:

void genknuth(int m, int n)
{    for (int i = 0; i < n; i++)
         /* select m of remaining n-i */
         if ((bigrand() % (n-i)) < m) {
             cout << i << "\n";
             m--;
         }
}

Sie befindet sich auf Seite 127 von Programming Pearls von Jon Bentley und basiert auf Knuths Implementierung.

EDIT: Ich habe gerade eine weitere Änderung auf Seite 129 gesehen:

void genshuf(int m, int n)
{    int i,j;
     int *x = new int[n];
     for (i = 0; i < n; i++)
         x[i] = i;
     for (i = 0; i < m; i++) {
         j = randint(i, n-1);
         int t = x[i]; x[i] = x[j]; x[j] = t;
     }
     sort(x, x+m);
     for (i = 0; i< m; i++)
         cout << x[i] << "\n";
}

Dies basiert auf der Idee, dass "... wir nur die ersten m Elemente des Arrays mischen müssen ..."

8
daniel

Ich schrieb eine effiziente Umsetzung davon vor ein paar Wochen. Es ist in C #, aber die Übersetzung nach Java ist trivial (im Wesentlichen derselbe Code). Das Plus ist, dass es auch völlig unvoreingenommen ist (was einige der vorhandenen Antworten nicht sind) - eine Möglichkeit, dies zu testen .

Es basiert auf einer Durstenfeld-Implementierung des Fisher-Yates-Shuffles.

4
Greg Beech

Wenn Sie versuchen, verschiedene Elemente aus einer Liste von n auszuwählen, sind die oben angegebenen Methoden O(n) oder O (kn), da durch das Entfernen eines Elements aus einem Vector eine Arraycopy verschoben wird alle Elemente nach unten.

Da Sie nach dem besten Weg fragen, hängt es davon ab, was Sie mit Ihrer Eingabeliste tun dürfen. 

Wenn Sie die Eingabeliste wie in Ihren Beispielen ändern können, können Sie einfach k zufällige Elemente an den Anfang der Liste tauschen und sie in der Zeit O(k) wie folgt zurückgeben:

public static <T> List<T> getRandomSubList(List<T> input, int subsetSize)
{
    Random r = new Random();
    int inputSize = input.size();
    for (int i = 0; i < subsetSize; i++)
    {
        int indexToSwap = i + r.nextInt(inputSize - i);
        T temp = input.get(i);
        input.set(i, input.get(indexToSwap));
        input.set(indexToSwap, temp);
    }
    return input.subList(0, subsetSize);
}

Wenn die Liste in dem gleichen Zustand sein muss, in dem sie begonnen hat, können Sie die getauschten Positionen nachverfolgen und die Liste nach dem Kopieren der ausgewählten Unterliste in ihren ursprünglichen Zustand zurückversetzen. Dies ist immer noch eine O(k) - Lösung.

Wenn Sie die Eingabeliste jedoch nicht ändern können und k viel weniger als n ist (wie 5 von 100), ist es viel besser, die ausgewählten Elemente nicht jedes Mal zu entfernen, sondern einfach jedes Element auszuwählen ein Duplikat, werfen Sie es heraus und wählen Sie es erneut aus. Dies gibt Ihnen O (kn/(n-k)), das immer noch nahe an O(k) liegt, wenn n k dominiert. (Wenn beispielsweise k kleiner als n/2 ist, wird es auf O (k) reduziert).

Wenn k nicht von n dominiert wird und Sie die Liste nicht ändern können, können Sie auch Ihre ursprüngliche Liste kopieren und Ihre erste Lösung verwenden, da O(n) genauso gut ist wie O (k).

Wie andere bereits erwähnt haben, wenn Sie auf starke Zufälligkeiten angewiesen sind, bei denen jede Unterliste möglich ist (und unvoreingenommen), benötigen Sie auf jeden Fall etwas Stärkeres als Java.util.Random. Siehe Java.security.SecureRandom.

4
Dave L.

Ihre zweite Lösung der Verwendung von Random zum Auswählen von Elementen scheint jedoch solide zu sein:

2
qualidafial

Dies ist eine sehr ähnliche Frage zu stackoverflow.

Um meine Lieblingsantworten von dieser Seite zusammenzufassen (vor allem von Benutzer Kyle):

  • O(n) solution: Durchlaufen Sie Ihre Liste und kopieren Sie ein Element (oder einen Verweis darauf) mit Wahrscheinlichkeit (#needed/#remaining). Beispiel: Wenn k = 5 und n = 100 ist, nehmen Sie das erste Element mit prob 5/100. Wenn Sie diesen kopieren, wählen Sie den nächsten mit Prob 4/99; Aber wenn Sie die erste nicht genommen haben, ist das Problem 5/99.
  • O (k log k) oder O (k2): Erstellen Sie eine sortierte Liste von k Indizes (Zahlen in {0, 1, ..., n-1}), indem Sie zufällig eine Zahl <n und dann zufällig eine Zahl <n-1 usw. auswählen In diesem Schritt müssen Sie Ihre Wahl erneut kalibrieren, um Kollisionen zu vermeiden und die Wahrscheinlichkeiten gleich zu halten. Wenn beispielsweise k = 5 und n = 100 ist und Ihre erste Wahl 43 ist, liegt Ihre nächste Auswahl im Bereich [0, 98]. Wenn es> 43 ist, fügen Sie 1 hinzu. Wenn Ihre zweite Wahl 50 ist, fügen Sie 1 hinzu und Sie haben {43, 51}. Wenn Ihre nächste Wahl 51 ist, fügen Sie 2 hinzu, um {43, 51, 53} zu erhalten.

Hier ist ein Pseudopython -

# Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
    s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
  return s 

Ich sage, dass die Zeitkomplexität O ist (k2) oder O (k log k), da dies davon abhängt, wie schnell Sie suchen und in Ihren Container einfügen können. Wenn s eine normale Liste ist, ist eine dieser Operationen linear und Sie erhalten k ^ 2. Wenn Sie jedoch s als symmetrischen Binärbaum erstellen möchten, können Sie die O (k log k) -Zeit herausholen.

0
Tyler
Set<Integer> s = new HashSet<Integer>()
// add random indexes to s
while(s.size() < 5)
{
    s.add(Rand.nextInt(itemsVector.size()))
}
// iterate over s and put the items in the list
for(Integer i : s)
{
    out.add(itemsVector.get(i));
}
0
Wesley Tarle

Wie viel kostet das Entfernen? Wenn dann das Array in einen neuen Speicherbereich umgeschrieben werden muss, haben Sie in der zweiten Version O(5n) Vorgänge ausgeführt, und nicht die O(n) Sie wollte vorher.

Sie könnten ein Array von Booleans erstellen, das auf false gesetzt ist, und dann:

for (int i = 0; i < 5; i++){
   int r = Rand.nextInt(itemsVector.size());
   while (boolArray[r]){
       r = Rand.nextInt(itemsVector.size());
   }
   subsetList.add(itemsVector[r]);
   boolArray[r] = true;
}

Dieser Ansatz funktioniert, wenn Ihre Teilmenge um einen erheblichen Unterschied kleiner als Ihre Gesamtgröße ist. Wenn sich diese Größen einander annähern (dh 1/4 der Größe oder etwas ähnliches), würden Sie bei diesem Zufallszahlengenerator mehr Kollisionen erhalten. In diesem Fall würde ich eine Liste von Ganzzahlen von der Größe Ihres größeren Arrays erstellen und diese Liste von Ganzzahlen neu mischen und die ersten Elemente daraus ziehen, um Ihre (nicht kollidierenden) Unbestimmtheiten zu erhalten. Auf diese Weise haben Sie die Kosten für O(n) beim Erstellen des Ganzzahl-Arrays und ein weiteres O(n) beim Shuffle, jedoch keine Kollisionen von einem internen Prüfer und weniger als das Potenzial O(5n), das das Entfernen möglicherweise kostet.

0
mmr

Ich würde mich persönlich für Ihre erste Implementierung entscheiden: sehr knapp. Leistungstests zeigen, wie gut es skaliert. Ich habe einen sehr ähnlichen Codeblock in einer anständig missbrauchten Methode implementiert und ausreichend skaliert. Der spezielle Code stützte sich auf Arrays, die ebenfalls> 10.000 Elemente enthielten.

0
daniel

zwei Lösungen, von denen ich glaube, dass sie nicht hier erscheinen, sind ziemlich lang und enthalten einige Links. Ich denke jedoch nicht, dass sich alle Posts auf das Problem beziehen, einen Substantiv aus K elemetns aus einer Menge von N-Elementen auszuwählen . [Mit "set" beziehe ich mich auf den mathematischen Begriff, d. H. Alle Elemente erscheinen einmal, die Reihenfolge ist nicht wichtig].

Sol 1:

//Assume the set is given as an array:
Object[] set ....;
for(int i=0;i<K; i++){
randomNumber = random() % N;
    print set[randomNumber];
    //swap the chosen element with the last place
    temp = set[randomName];
    set[randomName] = set[N-1];
    set[N-1] = temp;
    //decrease N
    N--;
}

Das sieht ähnlich aus wie die Antwort, die Daniel gegeben hat, ist aber tatsächlich sehr unterschiedlich. Es ist von O(k) Laufzeit.

Eine andere Lösung ist die Verwendung von math: Betrachten Sie die Array-Indizes als Z_n. So können wir zufällig 2 Zahlen auswählen, x, die zu n co-prime sind, dh chhose gcd (x, n) = 1 und eine andere, a , was "Startpunkt" ist - dann die Reihe: a% n, a + x% n, a + 2 * x% n, ... a + (k-1) * x% n ist eine Folge unterschiedlicher Zahlen ( solange k <= n).

0
user967710