Bir dizinin tüm indekslerini oluşturmak genellikle kötü bir fikirdir, çünkü özellikle seçilecek sayıların oranı MAX
düşükse (karmaşıklık baskın hale gelirse O(MAX)
) çok zaman alabilir. Bu, seçilecek sayıların oranı MAX
bire yaklaşırsa daha da kötüleşir , çünkü seçilen endeksleri tüm dizilerden çıkarmak da pahalı hale gelir (yaklaşıyoruz O(MAX^2/2)
). Ancak küçük sayılar için bu genellikle iyi çalışır ve özellikle hataya açık değildir.
Oluşturulan endeksleri bir koleksiyon kullanarak filtrelemek de kötü bir fikirdir, çünkü dizinleri diziye eklemek için biraz zaman harcanır ve aynı rasgele sayı birkaç kez çizilebildiği için ilerleme garanti edilmez (ancak yeterince büyükse MAX
bu olası değildir. ). Bu karmaşıklığa yakın olabilir
O(k n log^2(n)/2)
, kopyaları göz ardı ederek ve koleksiyonun verimli arama için bir ağaç kullandığını varsayarsak (ancak k
ağaç düğümlerini ayırmanın önemli bir sabit maliyeti ve muhtemelen yeniden dengeleme zorunluluğu vardır ).
Diğer bir seçenek, rastgele değerleri en başından itibaren benzersiz bir şekilde oluşturarak ilerlemeyi garanti etmektir. Bu, ilk turda rastgele bir indeks [0, MAX]
oluşturulduğu anlamına gelir :
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
İkinci turda, yalnızca [0, MAX - 1]
oluşturulur (zaten bir öğe seçildiği için):
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)
Endekslerin değerlerinin daha sonra ayarlanması gerekir: eğer ikinci indeks, dizinin ikinci yarısına denk gelirse (birinci indeksten sonra), boşluğu hesaba katmak için artırılması gerekir. Bunu, rastgele sayıda benzersiz öğe seçmemize izin veren bir döngü olarak uygulayabiliriz.
Kısa diziler için bu oldukça hızlı bir O(n^2/2)
algoritmadır:
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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
}
}
n_select_num
5'in nerede ve n_number_num
senin MAX
. n_Rand(x)
İçinde döner rasgele tamsayılar [0, x]
(dahil). Bu, ekleme noktasını bulmak için ikili arama kullanılarak çok sayıda öğe seçildiğinde (örn. 5 değil 500) biraz daha hızlı yapılabilir. Bunu yapmak için, gereksinimleri karşıladığımızdan emin olmalıyız.
İle n + j < rand_num[j]
aynı olan karşılaştırma ile ikili arama yapacağız
n < rand_num[j] - j
. Bunun rand_num[j] - j
hala sıralanmış bir dizi için sıralanmış bir dizi olduğunu göstermemiz gerekir rand_num[j]
. Orijinalin iki öğesi arasındaki en düşük mesafe rand_num
bir olduğundan (üretilen sayılar benzersizdir, bu nedenle her zaman en az 1 fark vardır), bu durum neyse ki kolayca gösterilebilir . Aynı zamanda, indisleri j
tüm elemanlardan çıkarırsak, indeksteki
rand_num[j]
farklar tam olarak 1'dir. Yani "en kötü" durumda, sabit bir sıra elde ederiz - ama asla azalmaz. İkili arama bu nedenle kullanılabilir ve O(n log(n))
algoritma elde edilir :
struct TNeedle {
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset {
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;
return r_value < n.n + n_index;
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return n.n + n_index < r_value;
}
};
Ve sonunda:
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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
}
}
Bunu üç kriterde test ettim. İlk olarak, 7 öğeden 3 sayı seçildi ve seçilen öğelerin histogramı 10.000 denemenin üzerinde toplandı:
4265 4229 4351 4267 4267 4364 4257
Bu, 7 maddenin her birinin yaklaşık olarak aynı sayıda seçildiğini ve algoritmanın neden olduğu belirgin bir önyargı olmadığını göstermektedir. Tüm diziler ayrıca doğruluk (içeriklerin benzersizliği) açısından da kontrol edildi.
İkinci kriter, 5000 maddeden 7 sayıyı seçmeyi içeriyordu. Algoritmanın çeşitli sürümlerinin süresi 10.000.000'den fazla çalıştırma biriktirildi. Sonuçlar koddaki yorumlarda olarak belirtilmiştir b1
. Algoritmanın basit versiyonu biraz daha hızlıdır.
Üçüncü kriter, 5000 maddeden 700 sayıyı seçmeyi içeriyordu. Algoritmanın çeşitli sürümlerinin zamanı tekrar toplandı, bu sefer 10.000'den fazla çalıştırma. Sonuçlar koddaki yorumlarda olarak belirtilmiştir b2
. Algoritmanın ikili arama versiyonu artık basit olandan iki kat daha hızlı.
İkinci yöntem, makinemde 75'ten fazla öğe seçmek için daha hızlı olmaya başlıyor (her iki algoritmanın karmaşıklığının öğelerin sayısına bağlı olmadığını unutmayın MAX
).
Yukarıdaki algoritmaların rastgele sayıları artan sırada ürettiğinden bahsetmeye değer. Ancak, sayıların oluşturulma sırasına göre kaydedileceği başka bir dizi eklemek ve bunun yerine (göz ardı edilebilir ek maliyetle O(n)
) geri dönmek basit olacaktır . Çıktıyı karıştırmak gerekli değildir: bu çok daha yavaş olacaktır.
Kaynakların C ++ olduğuna dikkat edin, makinemde Java yok, ancak konsept açık olmalı.
DÜZENLE :
Eğlence için, tüm endeksleri içeren bir liste oluşturan
0 .. MAX
, bunları rastgele seçen ve benzersizliği garanti etmek için listeden çıkaran yaklaşımı da uyguladım . Oldukça yüksek MAX
(5000) seçtiğimden beri , performans felaket:
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
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);
rand_num.push_back(all_numbers[n]);
all_numbers.erase(all_numbers.begin() + n);
}
Ayrıca yaklaşımı set
, aslında kıyaslamada ikinci sırada gelen b2
ve ikili arama yaklaşımından sadece yaklaşık% 50 daha yavaş olan a (bir C ++ koleksiyonu) ile uyguladım . Bu anlaşılabilir bir durumdur, çünkü set
ekleme maliyetinin ikili aramaya benzer olduğu bir ikili ağaç kullanılır. Tek fark, ilerlemeyi yavaşlatan yinelenen öğeler elde etme şansıdır.
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1));
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
Tam kaynak kodu burada .