컬렉션에서 임의의 하위 집합을 선택하는 가장 좋은 방법은 무엇입니까?
-
02-07-2019 - |
문제
벡터에 임의의 하위 집합을 선택하려는 개체 집합이 있습니다(예:100개의 항목이 다시 돌아옵니다.무작위로 5개를 선택하세요).첫 번째 (매우 성급한) 패스에서 나는 매우 간단하고 아마도 지나치게 영리한 솔루션을 수행했습니다.
Vector itemsVector = getItems();
Collections.shuffle(itemsVector);
itemsVector.setSize(5);
이는 훌륭하고 단순하다는 장점이 있지만 확장성이 좋지 않을 것으로 생각됩니다.Collections.shuffle()은 최소한 O(n)이어야 합니다.나의 덜 영리한 대안은
Vector itemsVector = getItems();
Random rand = new Random(System.currentTimeMillis()); // would make this static to the class
List subsetList = new ArrayList(5);
for (int i = 0; i < 5; i++) {
// be sure to use Vector.remove() or you may get the same item twice
subsetList.add(itemsVector.remove(rand.nextInt(itemsVector.size())));
}
컬렉션에서 임의의 하위 집합을 추출하는 더 나은 방법에 대한 제안이 있습니까?
해결책
Jon Bentley는 이것을 '프로그래밍 진주'또는 '더 프로그래밍 진주'에서 논의합니다. N of M 선택 프로세스에주의를 기울여야하지만 표시된 코드가 올바르게 작동한다고 생각합니다. 모든 항목을 무작위로 셔플하는 대신, 임의의 셔플은 첫 번째 N 위치 만 셔플하는 것만 할 수 있습니다. 이는 n << M에 유용한 절약입니다.
Knuth는 또한 이러한 알고리즘에 대해 논의합니다. Vol 3 "정렬 및 검색"이라고 생각하지만, 내 세트는 집의 이동이 보류 중이므로 공식적으로 확인할 수 없습니다.
다른 팁
@홍옥,
나는 이것이 당신이 말하는 솔루션이라고 생각합니다.
void genknuth(int m, int n)
{ for (int i = 0; i < n; i++)
/* select m of remaining n-i */
if ((bigrand() % (n-i)) < m) {
cout << i << "\n";
m--;
}
}
Jon Bentley의 프로그래밍 진주 127 페이지에 있으며 Knuth의 구현을 기반으로합니다.
편집 : 방금 129 페이지의 추가 수정을 보았습니다.
void genshuf(int m, int n)
{ int i,j;
int *x = new int[n];
for (i = 0; i < n; i++)
x[i] = i;
for (i = 0; i < m; i++) {
j = randint(i, n-1);
int t = x[i]; x[i] = x[j]; x[j] = t;
}
sort(x, x+m);
for (i = 0; i< m; i++)
cout << x[i] << "\n";
}
이것은 "... 우리는 첫 번째 셔플 만 셔플이 필요하다는 아이디어를 기반으로합니다. 중 배열의 요소 ... "
n 목록에서 k 개의 개별 요소를 선택하려는 경우 위에서 제공한 방법은 O(n) 또는 O(kn)이 됩니다. 왜냐하면 Vector에서 요소를 제거하면 arraycopy가 모든 요소를 아래로 이동하게 되기 때문입니다. .
최선의 방법을 요구하고 있으므로 입력 목록으로 무엇을 할 수 있는지에 따라 다릅니다.
예제에서와 같이 입력 목록을 수정하는 것이 허용되는 경우 k 임의의 요소를 목록의 시작 부분으로 바꾸고 다음과 같이 O(k) 시간에 반환할 수 있습니다.
public static <T> List<T> getRandomSubList(List<T> input, int subsetSize)
{
Random r = new Random();
int inputSize = input.size();
for (int i = 0; i < subsetSize; i++)
{
int indexToSwap = i + r.nextInt(inputSize - i);
T temp = input.get(i);
input.set(i, input.get(indexToSwap));
input.set(indexToSwap, temp);
}
return input.subList(0, subsetSize);
}
목록이 시작된 것과 동일한 상태로 끝나야 하는 경우, 교체한 위치를 추적한 다음 선택한 하위 목록을 복사한 후 목록을 원래 상태로 되돌릴 수 있습니다.이것은 여전히 O(k) 솔루션입니다.
그러나 입력 목록을 전혀 수정할 수 없고 k가 n보다 훨씬 작은 경우(예: 100에서 5) 매번 선택한 요소를 제거하지 않고 간단히 각 요소를 선택하는 것이 훨씬 더 좋습니다. 중복된 것을 버리고 다시 선택하세요.이것은 n이 k를 지배할 때 여전히 O(k)에 가까운 O(kn / (n-k))를 제공합니다.(예를 들어 k가 n/2보다 작으면 O(k)로 줄어듭니다.)
k가 n의 지배를 받지 않고 목록을 수정할 수 없는 경우 O(n)이 O(k)만큼 좋기 때문에 원래 목록을 복사하고 첫 번째 솔루션을 사용할 수도 있습니다.
다른 사람들이 언급했듯이 모든 하위 목록이 가능하고 편견이 없는 강력한 무작위성에 의존하는 경우 다음보다 더 강력한 것이 필요합니다. java.util.Random
.보다 java.security.SecureRandom
.
나는 썼다 이것의 효율적인 구현 몇 주 전에. C#이지만 Java로 변환은 사소합니다 (본질적으로 동일한 코드). 플러스 측면은 완전히 편견이 없다는 것입니다 (기존 답변 중 일부는 그렇지 않은 것) - 여기에 테스트하는 방법.
그것은 Fisher-Yates Shuffle의 Durstenfeld 구현을 기반으로합니다.
그러나 임의의 요소를 선택하는 두 번째 솔루션은 소리가 들립니다.
데이터가 얼마나 민감한 지에 따라 임의의 해싱 방법을 사용하여 임의의 숫자 시드를 스크램블링하는 것이 좋습니다. 좋은 사례 연구는 참조하십시오 우리가 온라인 포커에서 속임수를 배운 방법 (그러나이 링크는 2015-12-18 년 현재 404입니다). 대체 URL (이중 인용문으로 기사 제목에서 Google 검색을 통해 찾을 수 있음)은 다음을 포함합니다.
벡터가 동기화됩니다. 가능하면 ArrayList를 사용하여 성능을 향상시킵니다.
제거 비용은 얼마입니까? 배열을 새로운 메모리 덩어리로 다시 작성 해야하는 경우, 이전에 원하는 O (n)가 아닌 두 번째 버전에서 O (5n) 작업을 수행했습니다.
거짓으로 설정된 부울 배열을 만들 수 있습니다.
for (int i = 0; i < 5; i++){
int r = rand.nextInt(itemsVector.size());
while (boolArray[r]){
r = rand.nextInt(itemsVector.size());
}
subsetList.add(itemsVector[r]);
boolArray[r] = true;
}
이 접근법은 서브 세트가 총 크기보다 큰 마진으로 작아지면 작동합니다. 이러한 크기가 서로 가까워지면 (예 : 크기 또는 무언가의 1/4) 임의의 숫자 생성기에 대해 더 많은 충돌이 발생할 수 있습니다. 이 경우 정수 목록을 더 큰 배열의 크기로 만들고 해당 정수 목록을 셔플 한 다음 첫 번째 요소를 끌어내어 (콜링하지 않은) inces를 얻습니다. 이렇게하면 정수 배열을 구축하는 데 O (n)과 셔플에 다른 o (n) 비용이 들지만 내부에서 내부에서 충돌은없고 체커에서 충돌하지 않고 비용을 제거하는 잠재적 O (5N)보다 적습니다.
나는 당신의 초기 구현을 개인적으로 선택합니다 : 매우 간결합니다. 성능 테스트는 그것이 얼마나 잘 확장되는지를 보여줍니다. 나는 괜찮은 학대 방법으로 매우 유사한 코드 블록을 구현했으며 충분히 확장되었습니다. 특정 코드는> 10,000 개의 항목이 포함 된 배열에 의존했습니다.
Set<Integer> s = new HashSet<Integer>()
// add random indexes to s
while(s.size() < 5)
{
s.add(rand.nextInt(itemsVector.size()))
}
// iterate over s and put the items in the list
for(Integer i : s)
{
out.add(itemsVector.get(i));
}
이것 StackoverFlow에서 매우 유사한 질문입니다.
해당 페이지에서 내가 가장 좋아하는 답변을 요약하려면 (사용자 Kyle의 Furst One) :
- O (n) 솔루션: 목록을 반복하고 확률 (#needed / #Remaining)과 함께 요소 (또는 참조)를 복사하십시오. 예 : K = 5 및 n = 100 인 경우, 첫 번째 요소를 프로브 5/100으로 취합니다. 당신이 그것을 복사하면, 당신은 Prob 4/99로 다음을 선택합니다. 그러나 첫 번째를 취하지 않았다면 프로브는 5/99입니다.
- o (k log k) 또는 o (k2): 숫자 <n을 무작위로 선택한 다음 숫자 <n-1 등을 무작위로 선택하여 k 인덱스 ({0, 1, ..., n-1}의 숫자)의 정렬 된 목록을 작성하십시오. 충돌을 피하고 확률을 유지하기 위해 선택을 리콜해야합니다. 예를 들어, k = 5 및 n = 100이고 첫 번째 선택은 43이면 다음 선택은 [0, 98] 범위에 있으며> = 43 인 경우 1을 추가합니다. 따라서 두 번째 선택이 50이면 1을 추가하면 {43, 51}이 있습니다. 다음 선택이 51이면 추가합니다 2 {43, 51, 53}을 얻기 위해.
여기 몇 가지 의사가 있습니다.
# Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
for i in range(k):
r = UniformRandom(0, n-i) # May be 0, must be < n-i
q = s.FirstIndexSuchThat( s[q] - q > r ) # This is the search.
s.InsertInOrder(q ? r + q : r + len(s)) # Inserts right before q.
return s
시간 복잡성은 O (k2) 또는 o (k log k)는 s를 위해 컨테이너에 얼마나 빨리 검색하고 삽입 할 수 있는지에 따라 다릅니다. S가 일반 목록 인 경우 해당 작업 중 하나는 선형이며 K^2를 얻습니다. 그러나 균형 잡힌 이진 트리로 S를 기꺼이 구축하려는 경우 O (k log k) 시간을 꺼낼 수 있습니다.
내가 여기에 나타나지 않는 두 가지 솔루션 - 서명은 상당히 길고 일부 링크를 포함하지만 모든 게시물이 N 요소 세트에서 k elemetns를 선택하는 문제와 관련이 없다고 생각합니다. . [ "세트"에 의해, 나는 수학적 용어를 언급합니다. 즉, 모든 요소가 한 번 나타나고 순서가 중요하지 않습니다].
졸 1 :
//Assume the set is given as an array:
Object[] set ....;
for(int i=0;i<K; i++){
randomNumber = random() % N;
print set[randomNumber];
//swap the chosen element with the last place
temp = set[randomName];
set[randomName] = set[N-1];
set[N-1] = temp;
//decrease N
N--;
}
이것은 다니엘이 준 대답과 비슷해 보이지만 실제로는 매우 다릅니다. O (k) 실행 시간입니다.
또 다른 해결책은 일부 수학을 사용하는 것입니다. 배열 인덱스를 Z_N으로 고려하고 무작위 2 숫자, X는 N, 즉 chhose gcd (x, n) = 1, a, a는 "시작점" - 시리즈 : A % N, A+X % N, A+2*X % N, ... A+(K -1)*X % N은 고유 한 숫자의 시퀀스입니다. k <= n).