시퀀스의 모든 인덱스를 생성하는 것은 일반적으로 좋지 않은 생각입니다. 특히 선택할 숫자의 비율 MAX이 낮은 경우 (복잡도가 지배적 인 경우) 많은 시간이 걸릴 수 있기 때문 O(MAX)입니다. 선택한 숫자의 비율이 MAX1 에 가까워지면 모든 시퀀스에서 선택한 인덱스를 제거하는 것도 비용이 많이 듭니다 (우리는O(MAX^2/2) ). 그러나 작은 숫자의 경우 일반적으로 잘 작동하며 특히 오류가 발생하지 않습니다.
컬렉션을 사용하여 생성 된 인덱스를 필터링하는 것도 좋지 않은 생각입니다. 인덱스를 시퀀스에 삽입하는 데 약간의 시간이 소요되고 동일한 난수를 여러 번 그릴 수 있기 때문에 진행률이 보장되지 않기 MAX때문입니다 (하지만 충분히 크면 그럴 가능성이 낮습니다). ). 이것은
O(k n log^2(n)/2)중복을 무시하고 컬렉션이 효율적인 조회를 위해 트리를 사용한다고 가정하는 복잡성에 가까울 수 있습니다 (그러나 k트리 노드를 할당하고 재조정 해야하는 상당한 비용 이 계속 발생 함 ).
또 다른 옵션은 처음부터 고유하게 임의 값을 생성하여 진행을 보장하는 것입니다. 즉, 첫 번째 라운드에서 임의 인덱스 [0, MAX]가 생성됩니다.
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
두 번째 라운드에서는 다음 [0, MAX - 1]항목 만 생성됩니다 (하나의 항목이 이미 선택되었으므로).
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)
그런 다음 인덱스 값을 조정해야합니다. 두 번째 인덱스가 시퀀스의 두 번째 절반 (첫 번째 인덱스 이후)에 속하면 간격을 고려하여 증가해야합니다. 이를 루프로 구현하여 임의의 고유 항목을 선택할 수 있습니다.
짧은 시퀀스의 경우 매우 빠른 O(n^2/2)알고리즘입니다.
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_num5는 어디에 n_number_num있고 MAX. n_Rand(x)수익률은 임의의 정수 [0, x](포함). 이진 검색을 사용하여 삽입 지점을 찾아 많은 항목 (예 : 5 개가 아닌 500 개)을 선택하는 경우이 작업을 조금 더 빠르게 수행 할 수 있습니다. 이를 위해 우리는 요구 사항을 충족하는지 확인해야합니다.
과 n + j < rand_num[j]동일한 비교로 이진 검색을 수행합니다
n < rand_num[j] - j. rand_num[j] - j정렬 된 시퀀스에 대해 여전히 정렬 된 시퀀스 임을 보여줄 필요 가 있습니다 rand_num[j]. 다행스럽게도 원본의 두 요소 사이의 가장 낮은 거리 rand_num가 1이기 때문에 쉽게 표시됩니다 (생성 된 숫자는 고유하므로 항상 최소 1의 차이가 있음). 동시에 j모든 요소에서 인덱스 를 빼면 인덱스
rand_num[j]의 차이는 정확히 1이됩니다. 따라서 "최악"의 경우 상수 시퀀스를 얻지 만 절대 감소하지 않습니다. 따라서 이진 검색을 사용하여 O(n log(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;
}
};
그리고 마지막으로:
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());
}
}
나는 이것을 세 가지 벤치 마크에서 테스트했습니다. 먼저 7 개 항목 중 3 개 숫자가 선택되었고 선택한 항목의 히스토그램이 10,000 회 이상 누적되었습니다.
4265 4229 4351 4267 4267 4364 4257
이는 7 개 항목 각각이 거의 동일한 횟수로 선택되었으며 알고리즘으로 인한 명백한 편향이 없음을 보여줍니다. 모든 시퀀스의 정확성 (내용의 고유성)도 확인했습니다.
두 번째 벤치 마크는 5000 개 항목 중 7 개 숫자를 선택하는 것입니다. 여러 버전의 알고리즘 시간이 10,000,000 회 이상 누적되었습니다. 결과는 코드의 주석에 b1. 알고리즘의 간단한 버전은 약간 더 빠릅니다.
세 번째 벤치 마크는 5000 개 항목 중 700 개를 선택하는 것입니다. 알고리즘의 여러 버전의 시간이 다시 누적되었으며 이번에는 10,000 회 이상 실행되었습니다. 결과는 코드의 주석에 b2. 알고리즘의 이진 검색 버전은 이제 단순한 알고리즘보다 두 배 이상 빠릅니다.
두 번째 방법은 내 컴퓨터에서 cca 75 개 이상의 항목을 선택하는 데 더 빠르기 시작합니다 (두 알고리즘의 복잡성은 항목 수에 따라 달라지지 않음 MAX).
위의 알고리즘은 오름차순으로 난수를 생성한다는 점을 언급 할 가치가 있습니다. 그러나 번호가 생성 된 순서대로 저장 될 다른 배열을 추가하고 대신 반환하는 것은 간단합니다 (무시할 수있는 추가 비용 O(n)). 출력을 섞을 필요는 없습니다. 그것은 훨씬 느릴 것입니다.
소스는 C ++로되어 있고, 내 컴퓨터에는 Java가 없지만 개념은 명확해야합니다.
수정 :
재미를 위해 모든 인덱스가 포함 된 목록을 생성
0 .. MAX하고 무작위로 선택한 다음 목록에서 제거하여 고유성을 보장 하는 접근 방식도 구현했습니다 . 상당히 높음 MAX(5000)을 선택했기 때문에 성능은 치명적입니다.
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);
}
또한 set실제로 벤치 마크에서 2 위를 b2차지하는 (C ++ 컬렉션)을 사용 하여 접근 방식을 구현 했습니다 . 이진 검색 방식보다 약 50 % 더 느립니다. set삽입 비용이 이진 검색과 유사한 이진 트리를 사용하므로 이해할 수 있습니다. 유일한 차이점은 중복 항목을 얻을 가능성이있어 진행 속도가 느려집니다.
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());
전체 소스 코드는 여기에 있습니다 .