Das Generieren aller Indizes einer Sequenz ist im Allgemeinen eine schlechte Idee, da dies viel Zeit in Anspruch nehmen kann, insbesondere wenn das Verhältnis der zu wählenden Zahlen MAX
gering ist (die Komplexität wird dominiert von O(MAX)
). Dies wird schlimmer, wenn sich das Verhältnis der zu wählenden Zahlen zu MAX
eins nähert, da dann das Entfernen der gewählten Indizes aus der Folge aller ebenfalls teuer wird (wir nähern uns O(MAX^2/2)
). Bei kleinen Zahlen funktioniert dies jedoch im Allgemeinen gut und ist nicht besonders fehleranfällig.
Das Filtern der generierten Indizes mithilfe einer Sammlung ist ebenfalls eine schlechte Idee, da einige Zeit für das Einfügen der Indizes in die Sequenz aufgewendet wird und der Fortschritt nicht garantiert ist, da dieselbe Zufallszahl mehrmals gezeichnet werden kann (aber groß genug MAX
ist dies unwahrscheinlich ). Dies kann nahezu komplex sein
O(k n log^2(n)/2)
, da die Duplikate ignoriert werden und angenommen wird, dass die Sammlung einen Baum für eine effiziente Suche verwendet (jedoch mit erheblichen konstanten Kosten k
für die Zuweisung der Baumknoten und möglicherweise für die Neuverteilung ).
Eine andere Möglichkeit besteht darin, die Zufallswerte von Anfang an eindeutig zu generieren, um sicherzustellen, dass Fortschritte erzielt werden. Das heißt, in der ersten Runde wird ein zufälliger Index in [0, MAX]
generiert:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
In der zweiten Runde wird nur [0, MAX - 1]
generiert (da bereits ein Element ausgewählt wurde):
items i0 i1 i3 i4 i5 i6 (total 6 items)
idx 1 ^^ (index 2 out of these 6, but 3 out of the original 7)
Die Werte der Indizes müssen dann angepasst werden: Wenn der zweite Index in die zweite Hälfte der Sequenz fällt (nach dem ersten Index), muss er erhöht werden, um die Lücke zu berücksichtigen. Wir können dies als Schleife implementieren und so eine beliebige Anzahl eindeutiger Elemente auswählen.
Für kurze Sequenzen ist dies ein ziemlich schneller O(n^2/2)
Algorithmus:
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear(); // !!
// b1: 3187.000 msec (the fastest)
// b2: 3734.000 msec
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
// get a random number
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
// see where it should be inserted
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
// insert it in the list, maintain a sorted sequence
}
// tier 1 - use comparison with offset instead of increment
}
Wo n_select_num
ist Ihr 5 und n_number_num
ist Ihr MAX
. Das n_Rand(x)
gibt zufällige ganze Zahlen in [0, x]
(einschließlich) zurück. Dies kann etwas beschleunigt werden, wenn viele Elemente (z. B. nicht 5, sondern 500) ausgewählt werden, indem die binäre Suche verwendet wird, um die Einfügemarke zu finden. Dazu müssen wir sicherstellen, dass wir die Anforderungen erfüllen.
Wir werden eine binäre Suche mit dem Vergleich durchführen, n + j < rand_num[j]
der der gleiche ist wie
n < rand_num[j] - j
. Wir müssen zeigen, dass dies rand_num[j] - j
immer noch eine sortierte Sequenz für eine sortierte Sequenz ist rand_num[j]
. Dies lässt sich glücklicherweise leicht zeigen, da der niedrigste Abstand zwischen zwei Elementen des Originals rand_num
eins ist (die generierten Zahlen sind eindeutig, sodass immer ein Unterschied von mindestens 1 besteht). Wenn wir gleichzeitig die Indizes j
von allen Elementen subtrahieren, sind die Indexunterschiede
rand_num[j]
genau 1. Im "schlimmsten" Fall erhalten wir also eine konstante Sequenz - die jedoch niemals abnimmt. Die binäre Suche kann daher verwendet werden und ergibt einen O(n log(n))
Algorithmus:
struct TNeedle { // in the comparison operator we need to make clear which argument is the needle and which is already in the list; we do that using the type system.
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset { // custom comparison "n < rand_num[j] - j"
protected:
std::vector<int>::iterator m_p_begin_it;
public:
CCompareWithOffset(std::vector<int>::iterator p_begin_it)
:m_p_begin_it(p_begin_it)
{}
bool operator ()(const int &r_value, TNeedle n) const
{
size_t n_index = &r_value - &*m_p_begin_it;
// calculate index in the array
return r_value < n.n + n_index; // or r_value - n_index < n.n
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
// calculate index in the array
return n.n + n_index < r_value; // or n.n < r_value - n_index
}
};
Und schlussendlich:
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear(); // !!
// b1: 3578.000 msec
// b2: 1703.000 msec (the fastest)
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
// get a random number
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
// see where it should be inserted
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
// insert it in the list, maintain a sorted sequence
}
// tier 4 - use binary search
}
Ich habe dies an drei Benchmarks getestet. Zuerst wurden 3 Zahlen aus 7 Elementen ausgewählt und ein Histogramm der ausgewählten Elemente wurde über 10.000 Läufe akkumuliert:
4265 4229 4351 4267 4267 4364 4257
Dies zeigt, dass jedes der 7 Elemente ungefähr gleich oft ausgewählt wurde und keine offensichtliche Verzerrung durch den Algorithmus verursacht wird. Alle Sequenzen wurden auch auf Richtigkeit (Eindeutigkeit des Inhalts) überprüft.
Der zweite Benchmark umfasste die Auswahl von 7 Zahlen aus 5000 Artikeln. Die Zeit mehrerer Versionen des Algorithmus wurde über 10.000.000 Läufe akkumuliert. Die Ergebnisse werden in Kommentaren im Code als bezeichnet b1
. Die einfache Version des Algorithmus ist etwas schneller.
Der dritte Benchmark umfasste die Auswahl von 700 Zahlen aus 5000 Artikeln. Die Zeit mehrerer Versionen des Algorithmus wurde erneut akkumuliert, diesmal über 10.000 Läufe. Die Ergebnisse werden in Kommentaren im Code als bezeichnet b2
. Die binäre Suchversion des Algorithmus ist jetzt mehr als zweimal schneller als die einfache.
Die zweite Methode ist schneller für die Auswahl von mehr als ca. 75 Elementen auf meinem Computer (beachten Sie, dass die Komplexität beider Algorithmen nicht von der Anzahl der Elemente abhängt MAX
).
Es ist erwähnenswert, dass die obigen Algorithmen die Zufallszahlen in aufsteigender Reihenfolge erzeugen. Es wäre jedoch einfach, ein weiteres Array hinzuzufügen, zu dem die Zahlen in der Reihenfolge gespeichert werden, in der sie generiert wurden, und diese stattdessen zurückzugeben (zu vernachlässigbaren zusätzlichen Kosten O(n)
). Es ist nicht notwendig, die Ausgabe zu mischen: das wäre viel langsamer.
Beachten Sie, dass sich die Quellen in C ++ befinden. Ich habe kein Java auf meinem Computer, aber das Konzept sollte klar sein.
EDIT :
Zur Unterhaltung habe ich auch den Ansatz implementiert, der eine Liste mit allen Indizes generiert
0 .. MAX
, sie zufällig auswählt und aus der Liste entfernt, um die Eindeutigkeit zu gewährleisten. Da ich ziemlich hoch gewählt habe MAX
(5000), ist die Leistung katastrophal:
// b1: 519515.000 msec
// b2: 20312.000 msec
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
// generate all the numbers
for(size_t i = 0; i < n_number_num; ++ i) {
assert(all_numbers.size() == n_item_num - i);
int n = n_Rand(n_item_num - i - 1);
// get a random number
rand_num.push_back(all_numbers[n]); // put it in the output list
all_numbers.erase(all_numbers.begin() + n); // erase it from the input
}
// generate random numbers
Ich habe den Ansatz auch mit einer set
(einer C ++ - Sammlung) implementiert , die beim Benchmark tatsächlich an zweiter Stelle b2
steht und nur etwa 50% langsamer ist als der Ansatz mit der binären Suche. Dies ist verständlich, da set
ein Binärbaum verwendet wird, bei dem die Einfügekosten der binären Suche ähnlich sind. Der einzige Unterschied besteht in der Möglichkeit, doppelte Elemente zu erhalten, was den Fortschritt verlangsamt.
// b1: 20250.000 msec
// b2: 2296.000 msec
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1)); // might have duplicates here
// generate unique random numbers
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
// copy the numbers from a set to a vector
Der vollständige Quellcode ist hier .