이 면접 질문을 받았습니다 :
40 억 개의 정수를 가진 입력 파일이 주어지면 파일에 포함되지 않은 정수를 생성하는 알고리즘을 제공하십시오. 1GB 메모리가 있다고 가정하십시오. 10MB의 메모리 만있는 경우 수행 할 작업을 추적하십시오.
내 분석 :
파일 크기는 4 × 10 9 × 4 bytes = 16GB입니다.
외부 정렬을 수행 할 수 있으므로 정수 범위를 알려줍니다.
내 질문은 정렬 된 큰 정수 세트에서 누락 된 정수를 감지하는 가장 좋은 방법은 무엇입니까?
내 이해 (모든 답변을 읽은 후) :
32 비트 정수에 대해 이야기하고 있다고 가정하면 2 32 = 4 * 10 9 개의 고유 정수가 있습니다.
사례 1 : 1GB = 1 * 10 9 * 8 비트 = 80 억 비트 메모리가 있습니다.
해결책:
하나의 고유 한 정수를 나타내는 하나의 비트를 사용하면 충분합니다. 우리는 정렬 할 필요가 없습니다.
이행:
int radix = 8;
byte[] bitfield = new byte[0xffffffff/radix];
void F() throws FileNotFoundException{
Scanner in = new Scanner(new FileReader("a.txt"));
while(in.hasNextInt()){
int n = in.nextInt();
bitfield[n/radix] |= (1 << (n%radix));
}
for(int i = 0; i< bitfield.lenght; i++){
for(int j =0; j<radix; j++){
if( (bitfield[i] & (1<<j)) == 0) System.out.print(i*radix+j);
}
}
}
사례 2 : 10MB 메모리 = 10 * 10 6 * 8 비트 = 8 천만 비트
해결책:
가능한 모든 16 비트 접두사에 대해 2 16 개의 정수 = 65536이 있으며 2 16 * 4 * 8 = 2 백만 비트가 필요합니다. 65536 버킷을 빌드해야합니다. 각 버킷마다 모든 가능성을 보유하는 4 바이트가 필요합니다. 최악의 경우 40 억 정수가 모두 동일한 버킷에 속하기 때문입니다.
- 파일을 통한 첫 번째 통과를 통해 각 버킷의 카운터를 만듭니다.
- 버킷을 스캔하고 적중률이 65536 미만인 첫 번째 버킷을 찾으십시오.
- 2 단계에서 파일의 두 번째 패스를 통해 높은 16 비트 접두사가있는 새 버킷을 빌드하십시오.
- 3 단계에서 빌드 한 버킷을 스캔하고 적중하지 않은 첫 번째 버킷을 찾으십시오.
코드는 위의 코드와 매우 유사합니다.
결론 : 파일 전달을 늘려 메모리를 줄입니다.
늦게 도착하는 사람들을위한 설명 : 질문에 따르면, 질문에 따르면 파일에 포함되지 않은 정수가 정확히 하나 있다고 말하지는 않습니다. 적어도 대부분의 사람들이 그것을 해석하는 방식이 아닙니다. 주석 스레드의 많은 의견이 있다 하지만, 작업의 변화에 대해. 불행하게도 주석 도입 이후 저자에 의해 삭제 된 코멘트 스레드에, 그래서 지금은 오해 다 그것에 고아 응답 것 같습니다. 매우 혼란 스럽습니다. 죄송합니다.
답변
“정수”가 32 비트를 의미한다고 가정하면, 10MB의 공간이 주어진 16 비트 접두사를 가진 입력 파일에 몇 개의 숫자가 있는지 계산하기에 충분합니다. 입력 파일. 버킷 중 하나 이상이 2 16 회 미만으로 적중 되었습니다. 해당 버킷에서 사용 가능한 숫자 중 어떤 것이 이미 사용 중인지 확인하려면 두 번째 패스를 수행하십시오.
32 비트를 초과하지만 여전히 경계 크기 인 경우 : 부호있는 또는 부호없는, 선택한 32 비트 범위를 벗어나는 모든 입력 숫자를 무시하고 위와 같이하십시오.
“정수”가 수학 정수를 의미하는 경우 : 입력을 한 번 읽고 읽은 가장 긴 숫자 의 최대 길이를 추적하십시오 . 완료되면 최대 자릿수에 숫자 하나가 더 많은 난수를 더한 값을 출력 하십시오 . (파일의 숫자 중 하나는 정확하게 나타내는 데 10MB 이상이 걸리는 큰 숫자 일 수 있지만 입력이 파일 인 경우 최소한 파일에 맞는 길이 를 나타낼 수 있습니다 ).
답변
통계 정보 알고리즘은 결정적 접근 방식보다 적은 패스를 사용하여이 문제를 해결합니다.
매우 큰 정수가 허용되면 O (1) 시간에 고유 한 숫자를 생성 할 수 있습니다. GUID 와 같은 의사 난수 128 비트 정수는 640 억 억 건 중 1 개 미만의 집합에서 기존의 40 억 정수 중 하나와 만 충돌합니다.
정수가 32 비트로 제한되면 10MB보다 훨씬 적은 단일 패스에서 고유 한 숫자를 생성 할 수 있습니다. 의사 랜덤 32 비트 정수가 40 억 개의 기존 정수 중 하나와 충돌 할 확률은 약 93 % (4e9 / 2 ^ 32)입니다. 1000 개의 의사 난수 정수가 모두 충돌 할 확률은 1 천 2 백억 억 (1 개의 충돌 률 ^ 1000) 중 1 개 미만입니다. 따라서 프로그램이 1000 개의 의사 랜덤 후보를 포함하는 데이터 구조를 유지하고 알려진 정수를 반복하여 후보에서 일치하는 항목을 제거하는 경우 파일에없는 하나 이상의 정수를 찾는 것이 확실합니다.
답변
이 문제에 대한 자세한 설명은 Jon Bentley “Column 1. Cracking the Oyster” Programming Pearls Addison-Wesley pp.3-10에서 논의되었습니다.
벤틀리 정렬 등 여러 외부 파일을 사용하여 외부 정렬, 병합 등 여러 가지 방법을 설명하지만 벤틀리가 제안하는 가장 좋은 방법은 사용하는 단일 패스 알고리즘 비트 필드 그는 익살 “정렬 원더”호출 🙂 문제에 40 억 오는 숫자는 다음과 같이 나타낼 수 있습니다.
4 billion bits = (4000000000 / 8) bytes = about 0.466 GB
비트 셋을 구현하는 코드는 간단하다 : ( solutions page 에서 가져온 )
#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];
void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int test(int i){ return a[i>>SHIFT] & (1<<(i & MASK)); }
Bentley의 알고리즘은 파일에서 단일 패스를 생성하여 set
배열의 해당 비트를 팅한 다음 test
위의 매크로를 사용하여이 배열을 검사 하여 누락 된 숫자를 찾습니다.
사용 가능한 메모리가 0.466GB 미만인 경우 Bentley는 k-pass 알고리즘을 제안합니다.이 알고리즘은 입력을 사용 가능한 메모리에 따라 범위로 나눕니다. 아주 간단한 예를 들어, 1 바이트 (즉, 8 개의 숫자를 처리하는 메모리) 만 사용할 수 있고 범위가 0 ~ 31 인 경우이를 0 ~ 7, 8-15, 16-22 등의 범위로 나눕니다. 각 32/8 = 4
패스 에서이 범위를 처리하십시오 .
HTH.
답변
문제는 파일에없는 가장 작은 가능한 숫자를 찾아야한다고 지정하지 않기 때문에 입력 파일 자체보다 긴 숫자 만 생성 할 수 있습니다. 🙂
답변
1GB RAM 변형의 경우 비트 벡터를 사용할 수 있습니다. 40 억 비트 == 500MB 바이트 배열을 할당해야합니다. 입력에서 읽은 각 숫자에 대해 해당 비트를 ‘1’로 설정하십시오. 완료되면 비트를 반복하여 여전히 ‘0’인 첫 번째 비트를 찾으십시오. 그 색인이 답입니다.
답변
32 비트 정수인 경우 (2 32에 가까운 ~ 40 억 숫자 선택에서와 같이) 40 억 숫자 의 목록은 가능한 정수의 최대 93 %를 차지합니다 (4 * 10 9 / (2 32 ) ). 따라서 각 비트가 0으로 초기화 된 2 32 비트의 비트 배열을 만들면 (2 29 바이트 ~ 500 MB의 RAM이 필요합니다. 바이트 = 2 3을 기억하십시오. 비트 = 8 비트를 하십시오) 정수 목록을 읽고 각각의 int에 대해 대응하는 비트 어레이 요소를 0 내지 1로 설정하고; 그런 다음 비트 배열을 읽고 여전히 0 인 첫 번째 비트를 반환하십시오.
RAM이 적 으면 (~ 10MB)이 솔루션을 약간 수정해야합니다. 10 MB ~ 83886080 비트는 여전히 0에서 83886079 사이의 모든 숫자에 대해 비트 배열을 수행하기에 충분합니다. 따라서 int 목록을 읽을 수 있습니다. 비트 배열에서 0에서 83886079 사이의 레코드 번호 만 기록하십시오. 숫자가 무작위로 분포 된 경우; (이 약 100 %의 차이 확률이 압도적으로 (10) -2592069 )를) 누락 INT를 찾을 수 있습니다. 실제로 1에서 2048까지의 숫자 (256 바이트의 RAM 만 있음) 만 선택하면 시간의 압도적 비율 (99.99999999999999999999999999999999999999999999999999999999999995 %)이 여전히 누락 된 숫자임을 알 수 있습니다.
그러나 약 40 억 개의 숫자 대신에 2 32-1 숫자와 10MB 미만의 RAM이 있습니다. 따라서 작은 범위의 정수는 숫자를 포함하지 않을 가능성이 적습니다.
당신이 목록의 각 INT 고유 것을 보장한다면, 당신은 숫자를 합계 한 # 전체 합계 (½)에 누락 된 합을 빼기 수 (2 32 ) (2 32 – 1)의 누락 INT를 찾을 수 = 9223372034707292160 . 그러나 int가 두 번 발생하면이 방법은 실패합니다.
그러나 항상 나누고 정복 할 수 있습니다. 순진한 방법은 배열을 읽고 전반 (0 ~ 2 31-1 )과 후반 (2 31 , 2 32 )에 있는 숫자의 수를 세는 것 입니다. 그런 다음 더 적은 수의 범위를 선택하고 해당 범위를 반으로 나누십시오. (말 (2 적은 수의 두이 있다면 (31) , 2 (32) ) 다음, 다음 검색은 범위의 숫자 (2 카운트 것 (31) , 3 * 2 30 -1), (3 * 2 (30) , 2 (32) ). 계속 숫자가 0 인 범위를 찾을 때까지 반복하고 답을 얻으십시오. 배열을 통해 O (lg N) ~ 32 읽기를 가져와야합니다.
그 방법은 비효율적이었습니다. 각 단계에서 2 개의 정수만 사용하거나 4 바이트 (32 비트) 정수를 갖는 약 8 바이트의 RAM을 사용합니다. 더 나은 방법은 sqrt (2 32 ) = 2 16 = 65536 bin 으로 나누는 것입니다 . 각 bin에는 65536 개의 숫자가 있습니다. 각 저장소에는 카운트를 저장하는 데 4 바이트가 필요하므로 2 18 바이트 = 256kB 가 필요합니다 . 빈 0이되어, (0 (2) = 65535 (16) -1), 빈 1 (2, 16 = 65536 (2) * 2 (16) , 빈 (2) -1 = 131,071) (2 * 2 16 = 131072 3 2 * 16 – 1 = 196607). 파이썬에서는 다음과 같은 것이 있습니다.
import numpy as np
nums_in_bin = np.zeros(65536, dtype=np.uint32)
for N in four_billion_int_array:
nums_in_bin[N // 65536] += 1
for bin_num, bin_count in enumerate(nums_in_bin):
if bin_count < 65536:
break # we have found an incomplete bin with missing ints (bin_num)
~ 40 억 개의 정수 목록을 읽습니다. 2 개의 16 개 빈 각각에 몇 개의 정수가 있는지 계산 하고 65536 개의 숫자가 모두없는 incomplete_bin을 찾으십시오. 그런 다음 40 억 개의 정수 목록을 다시 읽습니다. 그러나 이번에는 정수가 그 범위에있을 때만 주목할 것입니다. 당신이 그들을 찾을 때 조금 뒤집어.
del nums_in_bin # allow gc to free old 256kB array
from bitarray import bitarray
my_bit_array = bitarray(65536) # 32 kB
my_bit_array.setall(0)
for N in four_billion_int_array:
if N // 65536 == bin_num:
my_bit_array[N % 65536] = 1
for i, bit in enumerate(my_bit_array):
if not bit:
print bin_num*65536 + i
break
답변
왜 그렇게 복잡하게 만드나요? 파일에없는 정수를 요청 하시겠습니까?
지정된 규칙에 따라 저장해야 할 유일한 것은 파일에서 지금까지 본 가장 큰 정수입니다. 전체 파일을 읽은 후에는 1보다 큰 숫자를 리턴하십시오.
규칙에 따라 정수의 크기 또는 알고리즘이 반환하는 숫자에 대한 제한이 없으므로 maxint 또는 기타를 칠 위험이 없습니다.