[c] 알고리즘 : 배열에서 중복 정수를 제거하는 효율적인 방법
Microsoft와의 인터뷰에서이 문제가 발생했습니다.
임의의 정수 배열이 주어지면 중복 된 숫자를 제거하고 원래 배열의 고유 한 숫자를 반환하는 알고리즘을 C로 작성합니다.
예 : 입력 : {4, 8, 4, 1, 1, 2, 9}
출력 :{4, 8, 1, 2, 9, ?, ?}
한 가지주의 할 점은 예상 알고리즘이 배열을 먼저 정렬 할 필요가 없다는 것입니다. 요소가 제거되면 다음 요소도 앞으로 이동해야합니다. 어쨌든, 요소가 앞으로 이동 한 배열의 꼬리에있는 요소의 값은 무시할 수 있습니다.
업데이트 : 결과는 원래 배열로 반환되어야하며 도우미 데이터 구조 (예 : 해시 테이블)를 사용해서는 안됩니다. 그러나 주문 보존은 필요하지 않은 것 같습니다.
업데이트 2 : 왜 이러한 비실용적 인 제약이 있는지 궁금해하는 사람들을 위해 이것은 인터뷰 질문이었고 이러한 모든 제약은 제가 다른 아이디어를 어떻게 생각 해낼 수 있는지보기 위해 사고 과정에서 논의됩니다.
답변
어때 :
void rmdup(int *array, int length)
{
int *current , *end = array + length - 1;
for ( current = array + 1; array < end; array++, current = array + 1 )
{
while ( current <= end )
{
if ( *current == *array )
{
*current = *end--;
}
else
{
current++;
}
}
}
}
O (n ^ 2) 이하 여야합니다.
답변
내 여자 친구가 제안한 해결책은 병합 정렬의 변형입니다. 유일한 수정은 병합 단계에서 중복 된 값을 무시하는 것입니다. 이 솔루션은 O (n log n)입니다. 이 접근 방식에서는 정렬 / 중복 제거가 함께 결합됩니다. 그러나 그것이 어떤 차이를 만드는지 확실하지 않습니다.
답변
예전에 한번 올렸는데 꽤 멋있기 때문에 여기서 재현하겠습니다. 해싱을 사용하여 제자리에 설정된 해시와 같은 것을 만듭니다. 겨드랑이 공간에서 O (1)이 보장되며 (재귀는 꼬리 호출 임) 일반적으로 O (N) 시간 복잡도입니다. 알고리즘은 다음과 같습니다.
- 배열의 첫 번째 요소를 가져 오면 이것이 센티널이됩니다.
- 각 요소가 해시에 해당하는 위치에 있도록 가능한 한 나머지 배열의 순서를 변경하십시오. 이 단계가 완료되면 중복 항목이 발견됩니다. 센티넬과 동일하게 설정하십시오.
- 인덱스가 해시와 동일한 모든 요소를 배열의 시작 부분으로 이동합니다.
- 배열의 첫 번째 요소를 제외하고 sentinel과 동일한 모든 요소를 배열의 끝으로 이동합니다.
- 올바르게 해시 된 요소와 중복 요소 사이에 남는 것은 충돌로 인해 해시에 해당하는 인덱스에 배치 할 수없는 요소입니다. 이러한 요소를 처리하려면 재귀하십시오.
이것은 해싱에 병리학 적 시나리오가없는 경우 O (N)으로 표시 될 수 있습니다. 중복 항목이 없더라도 각 재귀에서 요소의 약 2/3가 제거됩니다. 각 재귀 수준은 O (n)이며 small n은 남은 요소의 양입니다. 유일한 문제는 실제로 중복이 거의 없을 때 즉, 많은 충돌이있을 때 빠른 정렬보다 느리다는 것입니다. 그러나 엄청난 양의 중복이있을 때는 놀랍도록 빠릅니다.
편집 : D의 현재 구현에서 hash_t는 32 비트입니다. 이 알고리즘에 대한 모든 것은 전체 32 비트 공간에서 해시 충돌이 거의 없다고 가정합니다. 그러나 충돌은 모듈러스 공간에서 자주 발생할 수 있습니다. 그러나이 가정은 합리적인 크기의 데이터 세트에 대해 모두 사실입니다. 키가 32 비트보다 작거나 같으면 자체 해시가 될 수 있으므로 전체 32 비트 공간에서 충돌이 불가능합니다. 더 크면 문제가 될 수있는 32 비트 메모리 주소 공간에 충분히 들어갈 수 없습니다. 저는 D의 64 비트 구현에서 hash_t가 64 비트로 증가 할 것이라고 가정합니다. 여기서 데이터 세트는 더 클 수 있습니다. 또한 이것이 문제가된다면 각 재귀 수준에서 해시 함수를 변경할 수 있습니다.
다음은 D 프로그래밍 언어로 구현 된 것입니다.
void uniqueInPlace(T)(ref T[] dataIn) {
uniqueInPlaceImpl(dataIn, 0);
}
void uniqueInPlaceImpl(T)(ref T[] dataIn, size_t start) {
if(dataIn.length - start < 2)
return;
invariant T sentinel = dataIn[start];
T[] data = dataIn[start + 1..$];
static hash_t getHash(T elem) {
static if(is(T == uint) || is(T == int)) {
return cast(hash_t) elem;
} else static if(__traits(compiles, elem.toHash)) {
return elem.toHash;
} else {
static auto ti = typeid(typeof(elem));
return ti.getHash(&elem);
}
}
for(size_t index = 0; index < data.length;) {
if(data[index] == sentinel) {
index++;
continue;
}
auto hash = getHash(data[index]) % data.length;
if(index == hash) {
index++;
continue;
}
if(data[index] == data[hash]) {
data[index] = sentinel;
index++;
continue;
}
if(data[hash] == sentinel) {
swap(data[hash], data[index]);
index++;
continue;
}
auto hashHash = getHash(data[hash]) % data.length;
if(hashHash != hash) {
swap(data[index], data[hash]);
if(hash < index)
index++;
} else {
index++;
}
}
size_t swapPos = 0;
foreach(i; 0..data.length) {
if(data[i] != sentinel && i == getHash(data[i]) % data.length) {
swap(data[i], data[swapPos++]);
}
}
size_t sentinelPos = data.length;
for(size_t i = swapPos; i < sentinelPos;) {
if(data[i] == sentinel) {
swap(data[i], data[--sentinelPos]);
} else {
i++;
}
}
dataIn = dataIn[0..sentinelPos + start + 1];
uniqueInPlaceImpl(dataIn, start + swapPos + 1);
}
답변
한 가지 더 효율적인 구현
int i, j;
/* new length of modified array */
int NewLength = 1;
for(i=1; i< Length; i++){
for(j=0; j< NewLength ; j++)
{
if(array[i] == array[j])
break;
}
/* if none of the values in index[0..j] of array is not same as array[i],
then copy the current value to corresponding new position in array */
if (j==NewLength )
array[NewLength++] = array[i];
}
이 구현에서는 배열을 정렬 할 필요가 없습니다. 또한 중복 요소가 발견되면 그 이후의 모든 요소를 한 위치만큼 이동할 필요가 없습니다.
이 코드의 출력은 NewLength 크기의 array []입니다.
여기서 우리는 배열의 두 번째 요소부터 시작하여이 배열까지 배열의 모든 요소와 비교합니다. 입력 배열을 수정하기 위해 추가 인덱스 변수 ‘NewLength’를 보유하고 있습니다. NewLength 변수는 0으로 초기화됩니다.
array [1]의 요소는 array [0]과 비교됩니다. 서로 다르면 array [NewLength]의 값이 array [1]로 수정되고 NewLength가 증가합니다. 동일하면 NewLength가 수정되지 않습니다.
따라서 배열 [1 2 1 3 1]이 있으면
‘j’루프의 첫 번째 패스에서 array [1] (2)는 array0과 비교되고 2는 array [NewLength] = array [1]에 기록되므로 NewLength = 2이므로 배열은 [1 2]가됩니다.
‘j’루프의 두 번째 패스에서 array [2] (1)는 array0 및 array1과 비교됩니다. 여기서 array [2] (1)과 array0은 동일한 루프이므로 여기서 중단됩니다. 따라서 배열은 NewLength = 2이므로 [1 2]가됩니다.
등등
답변
우수한 O 표기법을 찾고 있다면 O (n log n) 정렬로 배열을 정렬 한 다음 O (n) 순회를 수행하는 것이 가장 좋은 방법 일 수 있습니다. 정렬하지 않고 O (n ^ 2)를보고 있습니다.
편집 : 정수만 수행하는 경우 기수 정렬을 수행하여 O (n)을 얻을 수도 있습니다.
답변
1. O (n log n) 시간에 O (1) 추가 공간 사용
예를 들면 다음과 같습니다.
- 먼저 내부 O (n log n) 정렬을 수행합니다.
- 그런 다음 목록을 한 번 살펴보고 목록의 시작 부분에 모든 백의 첫 번째 인스턴스를 작성하십시오.
ejel의 파트너가이 작업을 수행하는 가장 좋은 방법은 간단한 병합 단계를 사용하는 내부 병합 정렬이며, 예를 들어 질문의 의도 일 수 있다는 것이 옳다고 생각합니다. 입력을 개선 할 능력없이 가능한 한 효율적으로이를 수행하기 위해 새 라이브러리 함수를 작성하며, 입력의 종류에 따라 해시 테이블없이이를 수행하는 것이 유용한 경우가 있습니다. 그러나 나는 이것을 실제로 확인하지 않았습니다.
2. O (n) 시간에 O (lots) 추가 공간 사용
- 모든 정수를 담을 수있을만큼 충분히 큰 0 배열을 선언
- 어레이를 한 번 살펴보십시오.
- 각 정수에 대해 해당 배열 요소를 1로 설정하십시오.
- 이미 1이면 해당 정수를 건너 뜁니다.
이것은 몇 가지 의심스러운 가정이있는 경우에만 작동합니다.
- 저렴하게 메모리를 제로화하는 것이 가능하거나 int의 크기가 개수에 비해 작습니다.
- OS에 256 ^ sizepof (int) 메모리를 요청하시면됩니다.
- 거대하다면 정말 효율적으로 캐시합니다.
잘못된 대답이지만 입력 요소가 많지만 모두 8 비트 정수 (또는 16 비트 정수일 수도 있음) 인 경우 가장 좋은 방법이 될 수 있습니다.
3. O (작은)-같은 여분의 공간, O (n)-쉬운 시간
# 2로 해시 테이블을 사용합니다.
4. 명확한 방법
요소 수가 적 으면 다른 코드가 더 빨리 작성되고 더 빨리 읽을 수 있으면 적절한 알고리즘을 작성하는 것이 유용하지 않습니다.
예 : 모든 동일한 요소를 제거하는 각 고유 요소 (즉, 첫 번째 요소, 두 번째 요소 (제거 된 첫 번째 요소의 중복) 등)에 대한 배열을 살펴 봅니다. O (1) 추가 공간, O (n ^ 2) 시간.
예 : 이를 수행하는 라이브러리 함수를 사용하십시오. 효율성은 쉽게 사용할 수있는 항목에 따라 다릅니다.
답변
음, 기본 구현은 매우 간단합니다. 모든 요소를 살펴보고 나머지 요소에 중복 항목이 있는지 확인하고 나머지 요소를 이동합니다.
끔찍한 비효율적이며 출력 또는 정렬 / 이진 트리에 대한 도우미 배열로 속도를 높일 수 있지만 허용되지 않는 것 같습니다.