Tạo tất cả các chỉ số của một chuỗi nói chung là một ý tưởng tồi, vì nó có thể mất rất nhiều thời gian, đặc biệt nếu tỷ lệ các số được chọn MAX
thấp (độ phức tạp bị chi phối O(MAX)
). Điều này trở nên tồi tệ hơn nếu tỷ lệ các số được chọn MAX
tiếp cận một, vì khi đó việc loại bỏ các chỉ số đã chọn khỏi chuỗi của tất cả cũng trở nên đắt đỏ (chúng tôi tiếp cận O(MAX^2/2)
). Nhưng đối với số lượng nhỏ, điều này thường hoạt động tốt và đặc biệt không dễ xảy ra lỗi.
Lọc các chỉ số đã tạo bằng cách sử dụng một bộ sưu tập cũng là một ý tưởng tồi, vì dành một số thời gian để chèn các chỉ số vào chuỗi và tiến trình không được đảm bảo vì cùng một số ngẫu nhiên có thể được rút ra nhiều lần (nhưng với số lượng đủ lớn MAX
thì không chắc ). Điều này có thể gần đến mức phức tạp
O(k n log^2(n)/2)
, bỏ qua các bản sao và giả sử tập hợp sử dụng một cây để tra cứu hiệu quả (nhưng với chi phí k
phân bổ các nút cây không đổi đáng kể và có thể phải cân bằng lại ).
Một tùy chọn khác là tạo các giá trị ngẫu nhiên duy nhất ngay từ đầu, đảm bảo tiến độ đang được thực hiện. Điều đó có nghĩa là trong vòng đầu tiên, một chỉ mục ngẫu nhiên trong [0, MAX]
được tạo:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
Trong vòng thứ hai, chỉ [0, MAX - 1]
được tạo (vì một mục đã được chọn):
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)
Giá trị của các chỉ số sau đó cần được điều chỉnh: nếu chỉ số thứ hai rơi vào nửa sau của chuỗi (sau chỉ số đầu tiên), thì nó cần được tăng lên để bù đắp khoảng cách. Chúng tôi có thể thực hiện điều này như một vòng lặp, cho phép chúng tôi chọn số lượng mục duy nhất tùy ý.
Đối với các chuỗi ngắn, đây là O(n^2/2)
thuật toán khá nhanh :
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);
}
}
Đâu n_select_num
là số 5 của bạn và n_number_num
là của bạn MAX
. Trả n_Rand(x)
về các số nguyên ngẫu nhiên trong [0, x]
(bao gồm). Điều này có thể được thực hiện nhanh hơn một chút nếu chọn nhiều mục (ví dụ: không phải 5 mà là 500) bằng cách sử dụng tìm kiếm nhị phân để tìm điểm chèn. Để làm được điều đó, chúng tôi cần đảm bảo rằng chúng tôi đáp ứng các yêu cầu.
Chúng tôi sẽ thực hiện tìm kiếm nhị phân với phép so sánh n + j < rand_num[j]
giống như
n < rand_num[j] - j
. Chúng ta cần chỉ ra rằng đó rand_num[j] - j
vẫn là một chuỗi được sắp xếp cho một chuỗi đã được sắp xếp rand_num[j]
. May mắn thay, điều này dễ dàng được chỉ ra, vì khoảng cách thấp nhất giữa hai phần tử của bản gốc rand_num
là một (các số được tạo là duy nhất, vì vậy luôn có sự khác biệt ít nhất là 1). Đồng thời, nếu chúng ta trừ các chỉ số j
khỏi tất cả các phần tử
rand_num[j]
, thì sự khác biệt về chỉ số chính xác là 1. Vì vậy, trong trường hợp "xấu nhất", chúng ta nhận được một dãy không đổi - nhưng không bao giờ giảm. Do đó, tìm kiếm nhị phân có thể được sử dụng, tạo ra O(n log(n))
thuật toán:
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;
}
};
Và cuối cùng:
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());
}
}
Tôi đã thử nghiệm điều này trên ba điểm chuẩn. Đầu tiên, 3 số được chọn trong số 7 mục và biểu đồ của các mục được chọn đã được tích lũy qua 10.000 lần chạy:
4265 4229 4351 4267 4267 4364 4257
Điều này cho thấy rằng mỗi mục trong số 7 mục được chọn với số lần gần như nhau và không có sự sai lệch rõ ràng do thuật toán gây ra. Tất cả các trình tự cũng được kiểm tra tính đúng đắn (tính duy nhất của nội dung).
Điểm chuẩn thứ hai liên quan đến việc chọn 7 số trong số 5000 mục. Thời gian của một số phiên bản của thuật toán đã được tích lũy hơn 10.000.000 lần chạy. Các kết quả được biểu thị trong các nhận xét trong mã như b1
. Phiên bản đơn giản của thuật toán nhanh hơn một chút.
Điểm chuẩn thứ ba liên quan đến việc chọn 700 số trong số 5000 mục. Thời gian của một số phiên bản của thuật toán lại được cộng dồn, lần này là hơn 10.000 lần chạy. Các kết quả được biểu thị trong các nhận xét trong mã như b2
. Phiên bản tìm kiếm nhị phân của thuật toán hiện nhanh hơn hai lần so với phiên bản đơn giản.
Phương pháp thứ hai bắt đầu nhanh hơn để chọn nhiều hơn cca 75 mục trên máy của tôi (lưu ý rằng độ phức tạp của một trong hai thuật toán không phụ thuộc vào số lượng mục, MAX
).
Điều đáng nói là các thuật toán trên tạo ra các số ngẫu nhiên theo thứ tự tăng dần. Nhưng sẽ thật đơn giản nếu thêm một mảng khác mà các số sẽ được lưu vào thứ tự mà chúng được tạo và trả về thay vào đó (với chi phí bổ sung không đáng kể O(n)
). Không cần thiết phải xáo trộn đầu ra: điều đó sẽ chậm hơn nhiều.
Lưu ý rằng các nguồn là C ++, tôi không có Java trên máy của mình, nhưng khái niệm phải rõ ràng.
CHỈNH SỬA :
Để giải trí, tôi cũng đã triển khai phương pháp tạo danh sách với tất cả các chỉ số
0 .. MAX
, chọn chúng ngẫu nhiên và xóa chúng khỏi danh sách để đảm bảo tính duy nhất. Vì tôi đã chọn khá cao MAX
(5000), hiệu suất rất thảm:
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);
}
Tôi cũng đã triển khai cách tiếp cận với set
(một bộ sưu tập C ++), thực sự đứng thứ hai về điểm chuẩn b2
, chỉ chậm hơn khoảng 50% so với cách tiếp cận với tìm kiếm nhị phân. Điều đó có thể hiểu được, vì nó set
sử dụng cây nhị phân, trong đó chi phí chèn tương tự như tìm kiếm nhị phân. Sự khác biệt duy nhất là cơ hội nhận được các vật phẩm trùng lặp, điều này làm chậm tiến độ.
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());
Mã nguồn đầy đủ ở đây .