C++ Wie kann man effizient 10 % aus einer List mit vielen Elementen ziehen?

Web-Schecki schrieb:
Klar, hier im konkreten Fall geht's auch sicherlich nicht um kryptographisch sicheren Zufall. Macht den Einwurf von Hardware-RNGs doch aber nur noch absurder ;)
Kommt auf die genaue Fragestellung an wie "gut" der Zufall sein soll. ;)
 
@JackForceOne
Also kryptografische Zufallsquellen hat jedes große Betriebssystem und auf µControllern gibt es mittlerweile oft auch Zufall aus Hardware. Als Seed für Pseudorand reicht es allemal. Vor allem da der TE die Güteanforderung an den Zufall ja auf Teufel komm raus nicht festlegen mag.
 
Och man will man mich denn nicht verstehen? Woher kommt deine Annahme, dass es um eine tatsächliche Implementierung geht? Geht aus dem Eingangspost nicht hervor. Es könnte auch eine theoretisch gehaltene Aufgabe sein. Und wenn man da einfach annimmt ja beliebig viele Münzwürfe stehen zur Verfügung, macht man es sich aus meiner Sicht etwas zu leicht.

Was du sagst ist natürlich richtig, aber mir scheint der TE ja interessiert an Informationen zu sein und ich wollte nur mal darauf hinweisen, dass Zufall auf Computern so eine Sache ist, die man bedenken sollte. Wenn er das schon wusste, toll, wenn nicht, dann weiß er es jetzt.
 
Piktogramm schrieb:
Vor allem da der TE die Güteanforderung an den Zufall ja auf Teufel komm raus nicht festlegen mag.
Vielleicht so etwas?

spin-lottery.gif


Nein, Spaß beiseite... Ein PRNG reicht aus, es muss kein kryptografisch starker Zufall sein... nur, wenn 20mal in Folge Rot käme, würde ich sagen, stimmt etwas nicht.
 
kali-hi schrieb:
Nein, Spaß beiseite... Ein PRNG reicht aus, es muss kein kryptografisch starker Zufall sein... nur, wenn 20mal in Folge Rot käme, würde ich sagen, stimmt etwas nicht.
Dann probier mal deinen originalen Code aus, indem du dein select_n_elements(in, out, 3); 20 Mal aufrufst (mit demselben Input-Container) ... wird vermutlich ziemlich rot-stichig werden ;)
 
JackForceOne schrieb:
Woher kommt deine Annahme, dass es um eine tatsächliche Implementierung geht?
Der Thread ist mit "C++" getagged und der TE spricht von recht konkreten Datenstrukturen wie Listen (wenn auch möglicherweise unabsichtlich). Ist schon sehr konkret an einer Implementierung.

JackForceOne schrieb:
Und wenn man da einfach annimmt ja beliebig viele Münzwürfe stehen zur Verfügung, macht man es sich aus meiner Sicht etwas zu leicht.
Nein, macht man nicht. Es ist eine absolute Standardannahme bspw. bei der Analyse von randomisierten Algorithmen. Zufallsbits zu bekommen ist schlicht kein Problem, um das man sich irgendwelche tieferen theoretischen Gedanken machen muss, wenn es nicht gerade kryptographisch sicher sein muss.
Es gibt sicherlich ein paar erwähnenswerte Best-Practices. Hardware-RNGs sind meilenweilt davon entfernt.
 
  • Gefällt mir
Reaktionen: kali-hi
Piktogramm schrieb:
Jain, es kommt drauf an, wie groß im Mittel die Strings aus L1 sind. In deinem Beispiel sind in L1 eigentlich nur Chars, im Indexvektor aber uInt32. Entsprechend ist der volle Index in deinem Beispiel immer größer als die Nutzdaten. Das Verhältnis verschiebt sich halt.
Die Größe eines std::string ist immer (!) dieselbe (also eine Konstante) und für eine gegebene Implementierung per sizeof(std::string) bereits zur Kompilierzeit ermittelbar. Damit ist auch das memory, dass ein std::vector<std::string> mindestens benötigt, für eine gegebene Anzahl an Elementen darin ebenfalls fix vorgegeben.

Typische std::string-Implementierungen bestehen z.B. aus einem pointer zum dazugehörigen Character-Array und einen unsigned int Wert für die Anzahl an characters in diesem Array, also schematisch ca. so (mit char als Charracter-Typ):
class string {
unsigned int size;
char * p;
}

Auf einem 32-bit System ist die Größe damit typischerweise 8 Bytes, und auf einem 64-bit System 16 Bytes.
Und das ist auf jeden Fall größer als ein uint32_t, weswegen der Index-Vector also weniger Speicherplatz benötigt als der String-Vector.

(Das von p referenzierte Character-Array nimmt zur Laufzeit natürlich weiteren Speicherplatz ein, ist aber nicht Teil der String-Klasse selbst, und beeinflusst daher folglich weder sizeof(std::string) noch den Memory-Bedarf des String-Vectors).

Exkurs:

Obige Implementierung entspricht eher der Anfangszeit von std::string Implementierungen. Später wurde dann die geniale Idee der small-string Optimization entwickelt: Wenn ein String-Wert nur aus wenigen Zeichen besteht (was in der Praxis häufig vorkommt), kann man diese Character-Werte DIREKT in den von size und p belegten Bytes speichern und sich damit das dynamische Character-Array komplett ersparen. Das spart dann nicht nur bei der string-Erstellung oder beim Kopieren dieses Strings die jeweilige memory-allocation, sondern bei jedem Zugriff auf den String auch noch den Indirektions-Weg zu diesem Character-Array. Enorme Ersparnis!

Eine Umsetzung dieser small-string Optimization könnte z.B. so aussehen:
Ein bit in size speichert ob die Character-Werte direkt lokal im std::string Objekt oder in einem externen Character-Array liegen. Falls ersteres der Fall ist, liegen diese Character-Werte dann direkt in den restlichen von size und p belegten Bytes. Auf einer typischen 64-bit Plattform kann man also einen String-Wert bestehend aus maximal 15 Character-Werten direkt im string-Objekt selbst speichern (das erste Byte von size enthält dabei sowohl das obige Bit-Flag wie auch dann in den restlichen Bits dieses Bytes gespeichert die Anzahl der Character-Werte).

Ob diese Variation genutzt wird, und falls ja, wie genau diese umgesetzt ist, liegt natürlich vollkommen im Ermessen der Implementierung.

Dass der OP nicht hard-codiert uint32_t als Typ für die Indices nehmen sollte, sondern besser typename std::vector<T>::size_type (was typischerweise std::size_t ist, und somit auf 64-bit Plattformen typischerweise auf einen 64-bit Integer Typ hinausläuft) ist wiederum eine ganz andere Sache (dann benötigt der Index-Vector auch entsprechend mehr memory), und wird dem OP hoffentlich auffallen falls sein vector je mal mehr als 2^32 Elemente enthalten sollte und sich seine Funktion zum Ziehen daraus dann etwas unerwartet verhält ...
 
  • Gefällt mir
Reaktionen: Piktogramm und kali-hi
Wäre das besser?

C++:
#include <cstdint>
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <numeric>
#include <random>

using namespace std;

template < typename E >
    void select_n_elements(const vector < E > & in, vector < E > & out, uint32_t n) {
        static auto rng = default_random_engine {};
        vector < uint32_t > indexes(in.size());
        iota(begin(indexes), end(indexes), 0);
        ranges::shuffle(indexes, rng);
        set < uint32_t > indexes_set(indexes.begin(), indexes.begin() + n);
        for (auto it = in.begin(); it != in.end(); ++it) {
            int index = std::distance(in.begin(), it);
            if (indexes_set.contains(index)) {
                out.push_back(in [index]);
            }
        }
    }

int main() {
    vector < string > in = {
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "10"
    };
    for (int i = 0; i <= in.size(); i++) {
        vector < string > out = {};
        select_n_elements(in, out, i);
        for (auto & e: out) cout << e << ' ';
        cout << endl;
    }
}

Die Ausgabe lautet dann zum Beispiel:

Code:
4
8 9
2 7 9
1 2 4 5
2 3 6 7 10
2 4 5 6 7 8
2 3 4 6 7 9 10
1 2 3 4 5 6 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10

Meiner Ansicht nach ist das zufällig (und effizient).

Es fehlt aber noch ein Check, ob n immer kleiner gleich size() wäre...

@firespot Postest du Antworten von ChatGpt? 🤔
Ergänzung ()

Nein sorry, war noch falsch...

Der rng muss noch initialisiert werden (nur einmal), sonst wird zum Beispiel im 9. Schritt die 10 immer nicht gezogen werden...

C++:
#include <time.h>
#include <cstdint>
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <numeric>
#include <random>

using namespace std;

default_random_engine get_rng() {
    static auto rng = default_random_engine {};
    static bool is_initialized = false;
    if (!is_initialized) {
        cout << "Init RNG..." << endl;
        rng.seed(time(NULL));
        is_initialized = true;
    }
    return rng;
}

template < typename E >
    void select_n_elements(const vector < E > & in, vector < E > & out, const uint32_t n) {
        vector < uint32_t > indexes(in.size());
        iota(begin(indexes), end(indexes), 0);
        ranges::shuffle(indexes, get_rng());
        set < uint32_t > indexes_set(indexes.begin(), indexes.begin() + n);
        for (auto it = in.begin(); it != in.end(); it++) {
            int index = distance(in.begin(), it);
            if (indexes_set.contains(index)) {
                out.push_back(in [index]);
            }
        }
    }

int main() {
    vector < string > in = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
    for (int i = 0; i <= in.size(); i++) {
        vector < string > out = {};
        select_n_elements(in, out, i);
        for (auto & e: out) cout << e << ' ';
        cout << endl;
    }
}
 
Zuletzt bearbeitet:
Nein, noch nicht gut, alles...

Es wäre sinnvoller, nur über alle Indexes zu iterieren, anstatt über das komplette in. Dafür müssten die Indexes aber zunächst aufsteigend sortiert sein. Dadurch würde sich die Laufzeit verringern von 2*m auf m+n (wenn m die Länge von in wäre und n die Anzahl der zu ziehenden Elemente).

@firespot Das mit ChatGpt war nicht so gemeint gewesen, entschuldige.
 
..kommen die Daten eigentlichen Daten (vor der Liste) aus einer Datenbank?
 
@dms Ist doch nur ein Gedankenexperiment :) Aber so etwas wäre denkbar.
 
kali-hi schrieb:
Wäre das besser?
[...]
Auf einem Desktoprechner kannst du random_device zur Initialisierung von default_random_engine nehmen.
Ansonsten sind Zeilen 13-16 deines ersten Code-Snippets in der Antwort nahezu exakt das, was ich gestern für mich getippt habe.

Aber die mehrfachen Hinweise, dass du in dem Fall eines std::vector (statt einer verketteten Liste) dann gerade kein Set brauchst (und kein set.contains), hast du da noch nicht berücksichtigt. Die nachfolgende for-Schleife braucht entsprechend (mit demnach anderem [simplerem] Aufbau) auch nicht über mehr als n Elemente laufen.

Überlege dir doch mal, was der Output von iota ist, und warum demnach kein Set nötig ist.
 
kali-hi schrieb:
eher ein Gedankenexperiment
Wenn man verschiedene Ansprüche reduziert und es wirklich eine linked list ist kann man auch über die Liste iterieren und per Zufall bei jedem Element entscheiden lassen ob man dieses Element zieht.
Aus einer linked-list kann man effizient Elemente entfernen, und auch während der dieser einen Iteration diese Liste kopieren, da werden ja nur Pointer befrickelt. Die Laufzeit dürfte dann im wesentlichen nur von der random funktion abhängen.
 
Zuletzt bearbeitet:
simpsonsfan schrieb:
Überlege dir doch mal, was der Output von iota ist, und warum demnach kein Set nötig ist.

Alles klar, dann so:

C++:
#include <cmath>
#include <cstdint>
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <random>

using namespace std;

template < typename E >
    void select_n_elements(const vector < E > & in, vector < E > & out,
        const uint32_t n) {
        static random_device rd;
        static default_random_engine rng(rd());
        vector < uint32_t > indexes(in.size());
        iota(begin(indexes), end(indexes), 0);
        ranges::shuffle(indexes, rng);
        sort(indexes.begin(), indexes.begin() + n);
        for (auto it = indexes.begin(); it != indexes.begin() + n; it++) {
            out.push_back(in [ * it]);
        }
    }

// tests until the first match
void test1() {
    vector < int > in(10);
    iota(begin(in), end(in), 0);
    for (int i = 0; i < in.size();) {
        vector < int > out = {};
        select_n_elements(in, out, 1);
        for (auto & e: out) cout << e << ' ';
        cout << endl;

        if (out[0] == i) {
            i++;
            cout << endl;
        }
    }
}

// tests until its missing the first element
void test9() {
    vector < int > in(10);
    iota(begin(in), end(in), 0);
    for (int i = 0; i < in.size();) {
        vector < int > out = {};
        select_n_elements(in, out, 9);
        for (auto & e: out) cout << e << ' ';
        cout << endl;

        if (find(out.begin(), out.end(), i) == out.end()) {
            i++;
            cout << endl;
        }
    }
}

int main() {
    test1();
    test9();
}

Kannst du mir kurz sagen, ob man Zeile 14 und 15 (Seeding des Randoms...) noch vereinfachen kann?


foofoobar schrieb:
... per Zufall bei jedem Element entscheiden lassen ob man dieses Element zieht.
Ich glaube, das wäre dann ungerecht.
 
kali-hi schrieb:
Ich glaube, das wäre dann ungerecht.
Deswegen sprach ich ja von "Anspruch reduzieren".

Aber man muss ja nicht zwingend am Anfang der Liste anfangen sondern irgendwo zufällig starten, was die Laufzeit erhöht damit wäre der "Bereich" aus dem möglicherweise doppelt oder gar nicht gezogen wird "zufällig".

Eine Reduzierung des Anspruchs wäre auch nicht genau die vorgebende Anzahl zu ziehen.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: kali-hi und simpsonsfan
kali-hi schrieb:
Ich glaube, das wäre dann ungerecht.
Was heißt "ungerecht"?
Du solltest nicht mit schwammigen Begriffen um dich werfen.

Mit der von @foofoobar beschriebenen Methode erhältst du (nur) im Erwartungswert die gewünschte Anzahl an Elementen. Die tatsächliche Anzahl ist binomialverteilt. In einem Array kann man das sogar mit einem kleinen Trick in O(k) bei k gezogenen Elementen implementieren. In einer Liste brauchst du natürlich O(n) bei n Elementen insgesamt.
 
Web-Schecki schrieb:
Was heißt "ungerecht"?
int rand() { return(1) }

:-)
Web-Schecki schrieb:
Mit der von @foofoobar beschriebenen Methode erhältst du (nur) im Erwartungswert die gewünschte Anzahl an Elementen. Die tatsächliche Anzahl ist binomialverteilt. In einem Array kann man das sogar mit einem kleinen Trick in O(k) bei k gezogenen Elementen implementieren. In einer Liste brauchst du natürlich O(n) bei n Elementen insgesamt.
Und der TO sollte die Datenstrukturen genau beschreiben, seinen Anspruch an "Zufälligkeit" und ob viel oder weniger Speicher verbraucht werden darf/soll.
Letztendlich stehen auch die Kosten verschiedener Operationen nicht auf allen Maschine im gleichen Verhältnis zueinander.

EDIT:

- Man kann auch bei jedem Element zufällig bestimmen wie viele Elemente man weiterspringen will, dann spart man rand() Aufrufe.

- Sind die Anzahl der Elemente aus denen gezogen wird überhaupt initial bekannt oder müssen die erst bestimmt werden?

Da ist wieder viel "it depends" dabei.

@kali-hi: Schärfe deine Vorgaben noch mal nach. Und auch ob die Datenstrukturen für die Eingabedaten wahlfrei sein dürfen.
 
Zuletzt bearbeitet:
foofoobar schrieb:
Aber man muss ja nicht zwingend am Anfang der Liste anfangen sondern irgendwo zufällig starten, was die Laufzeit erhöht damit wäre der "Bereich" aus dem möglicherweise doppelt oder gar nicht gezogen wird "zufällig".
Und/oder vielleicht zieht man mit etwas unterhalb der durchschnittlichen Wahrscheinlichkeit und dann eben so lange, bis die gewünschte Anzahl gezogen ist. Man muss sich halt dem eingebrachten Bias / der reduzierten Anforderung bewusst sein.
kali-hi schrieb:
Kannst du mir kurz sagen, ob man Zeile 14 und 15 (Seeding des Randoms...) noch vereinfachen kann?
Ich würd's so machen. Aber sagen, kann ich dir das nicht, mein (modernes/aktuelles) C++ ist eher auf B1-Niveau.

Edit: Aber weshalb du auf einem unnötiogen Sorting beharrst, wo das Ganze doch möglichst effizient sein soll, erschließt sich mir nicht.
 
  • Gefällt mir
Reaktionen: kali-hi
simpsonsfan schrieb:
Edit: Aber weshalb du auf einem unnötiogen Sorting beharrst, wo das Ganze doch möglichst effizient sein soll, erschließt sich mir nicht.
In der Liste können Meier, Müller, Schulze stehen...

Wenn Meier und Schulze gezogen werden, dann soll dort nicht auf einmal Schulze und Meier herauskommen. Am Beispiel verständlich? ;)
Ergänzung ()

Siehe Weiteres hier: https://en.wikipedia.org/wiki/Principle_of_least_astonishment
 
Zuletzt bearbeitet:
kali-hi schrieb:
In der Liste können Meier, Müller, Schulze stehen...

Wenn Meier und Schulze gezogen werden, dann soll dort nicht auf einmal Schulze und Meier herauskommen. Am Beispiel verständlich? ;)
Je nach Implementierung ist es doch zufällig was als "erstes" gezogen wird.

BTW: Und Vector ist nicht gerade die effizienteste Datenstruktur. Brauchst du wirklich alle Eigenschaften von C++ Vektoren? Und du solltest nicht von einer Liste sprechen wenn du Vektoren meinst.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: BeBur und kali-hi
Zurück
Oben