Menghasilkan semua indeks dari suatu urutan umumnya merupakan ide yang buruk, karena mungkin membutuhkan banyak waktu, terutama jika rasio angka yang akan dipilih MAX
rendah (kompleksitas menjadi didominasi oleh O(MAX)
). Ini menjadi lebih buruk jika rasio angka yang akan dipilih MAX
mendekati satu, karena kemudian menghapus indeks yang dipilih dari urutan semua juga menjadi mahal (kami mendekatiO(MAX^2/2)
). Tetapi untuk jumlah kecil, ini umumnya berfungsi dengan baik dan tidak terlalu rawan kesalahan.
Memfilter indeks yang dihasilkan dengan menggunakan koleksi juga merupakan ide yang buruk, karena beberapa waktu dihabiskan untuk memasukkan indeks ke dalam urutan, dan kemajuan tidak dijamin karena nomor acak yang sama dapat ditarik beberapa kali (tetapi untuk yang cukup besar MAX
kemungkinannya kecil. ). Ini bisa mendekati kompleksitas
O(k n log^2(n)/2)
, mengabaikan duplikat dan mengasumsikan koleksi menggunakan pohon untuk pencarian yang efisien (tetapi dengan biaya konstan yang signifikan k
untuk mengalokasikan simpul pohon dan mungkin harus menyeimbangkan ulang ).
Pilihan lainnya adalah menghasilkan nilai acak secara unik sejak awal, menjamin kemajuan sedang dibuat. Itu berarti di babak pertama, indeks acak dalam [0, MAX]
dihasilkan:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
Di babak kedua, hanya [0, MAX - 1]
dibuat (karena satu item telah dipilih):
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)
Nilai-nilai indeks kemudian perlu disesuaikan: jika indeks kedua jatuh di paruh kedua urutan (setelah indeks pertama), itu perlu ditambahkan untuk memperhitungkan kesenjangan. Kita bisa mengimplementasikan ini sebagai loop, memungkinkan kita memilih sembarang jumlah item unik.
Untuk urutan pendek, ini adalah O(n^2/2)
algoritme yang cukup cepat :
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);
}
}
Dimana n_select_num
5 Anda dan milik n_number_num
Anda MAX
. The n_Rand(x)
mengembalikan bilangan bulat acak di[0, x]
(inklusif). Ini dapat dibuat sedikit lebih cepat jika memilih banyak item (misalnya bukan 5 tetapi 500) dengan menggunakan pencarian biner untuk menemukan titik penyisipan. Untuk melakukan itu, kami perlu memastikan bahwa kami memenuhi persyaratan.
Kami akan melakukan pencarian biner dengan perbandingan n + j < rand_num[j]
yang sama
n < rand_num[j] - j
. Kita perlu menunjukkan bahwa rand_num[j] - j
masih merupakan urutan terurut untuk urutan terurut rand_num[j]
. Untungnya, ini mudah ditampilkan, karena jarak terendah antara dua elemen aslinya rand_num
adalah satu (angka yang dihasilkan unik, jadi selalu ada selisih minimal 1). Pada saat yang sama, jika kita mengurangi indeks j
dari semua elemen
rand_num[j]
, perbedaan indeksnya persis 1. Jadi dalam kasus "terburuk", kita mendapatkan urutan konstan - tetapi tidak pernah menurun. Oleh karena itu, pencarian biner dapat digunakan, menghasilkan O(n log(n))
algoritma:
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;
}
};
Dan akhirnya:
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());
}
}
Saya telah menguji ini pada tiga tolok ukur. Pertama, 3 nomor dipilih dari 7 item, dan histogram item yang dipilih dikumpulkan lebih dari 10.000 proses:
4265 4229 4351 4267 4267 4364 4257
Ini menunjukkan bahwa masing-masing dari 7 item dipilih kira-kira dalam jumlah yang sama, dan tidak ada bias yang disebabkan oleh algoritma. Semua urutan juga diperiksa kebenarannya (keunikan isinya).
Tolok ukur kedua melibatkan pemilihan 7 angka dari 5000 item. Waktu dari beberapa versi algoritme diakumulasikan lebih dari 10.000.000 berjalan. Hasilnya dilambangkan dalam komentar di kode sebagaib1
. Versi algoritme yang sederhana sedikit lebih cepat.
Tolok ukur ketiga melibatkan pemilihan 700 nomor dari 5000 item. Waktu dari beberapa versi algoritme dikumpulkan kembali, kali ini lebih dari 10.000 berjalan. Hasilnya dilambangkan dalam komentar di kode sebagaib2
. Versi penelusuran biner dari algoritme sekarang lebih dari dua kali lebih cepat daripada versi sederhana.
Metode kedua mulai lebih cepat untuk memilih lebih dari 75 item cca di mesin saya (perhatikan bahwa kompleksitas algoritme mana pun tidak bergantung pada jumlah item, MAX
).
Perlu disebutkan bahwa algoritme di atas menghasilkan angka acak dalam urutan menaik. Tetapi akan mudah untuk menambahkan larik lain yang nomornya akan disimpan sesuai urutan pembuatannya, dan mengembalikannya sebagai gantinya (dengan biaya tambahan yang dapat diabaikan O(n)
). Tidak perlu mengacak keluaran: itu akan jauh lebih lambat.
Perhatikan bahwa sumbernya ada dalam C ++, saya tidak memiliki Java di komputer saya, tetapi konsepnya harus jelas.
EDIT :
Untuk hiburan, saya juga telah menerapkan pendekatan yang menghasilkan daftar dengan semua indeks
0 .. MAX
, memilihnya secara acak dan menghapusnya dari daftar untuk menjamin keunikan. Karena saya telah memilih cukup tinggi MAX
(5000), kinerjanya sangat dahsyat:
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);
}
Saya juga telah menerapkan pendekatan dengan set
(koleksi C ++), yang sebenarnya berada di urutan kedua pada benchmark b2
, hanya sekitar 50% lebih lambat daripada pendekatan dengan pencarian biner. Hal ini dapat dimaklumi, karena set
menggunakan pohon biner, di mana biaya penyisipannya mirip dengan pencarian biner. Satu-satunya perbedaan adalah peluang mendapatkan item duplikat, yang memperlambat kemajuan.
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());
Kode sumber lengkap ada di sini .