[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) 시간 복잡도입니다. 알고리즘은 다음과 같습니다.

  1. 배열의 첫 번째 요소를 가져 오면 이것이 센티널이됩니다.
  2. 각 요소가 해시에 해당하는 위치에 있도록 가능한 한 나머지 배열의 순서를 변경하십시오. 이 단계가 완료되면 중복 항목이 발견됩니다. 센티넬과 동일하게 설정하십시오.
  3. 인덱스가 해시와 동일한 모든 요소를 ​​배열의 시작 부분으로 이동합니다.
  4. 배열의 첫 번째 요소를 제외하고 sentinel과 동일한 모든 요소를 ​​배열의 끝으로 이동합니다.
  5. 올바르게 해시 된 요소와 중복 요소 사이에 남는 것은 충돌로 인해 해시에 해당하는 인덱스에 배치 할 수없는 요소입니다. 이러한 요소를 처리하려면 재귀하십시오.

이것은 해싱에 병리학 적 시나리오가없는 경우 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) 시간.

예 : 이를 수행하는 라이브러리 함수를 사용하십시오. 효율성은 쉽게 사용할 수있는 항목에 따라 다릅니다.


답변

음, 기본 구현은 매우 간단합니다. 모든 요소를 ​​살펴보고 나머지 요소에 중복 항목이 있는지 확인하고 나머지 요소를 이동합니다.

끔찍한 비효율적이며 출력 또는 정렬 / 이진 트리에 대한 도우미 배열로 속도를 높일 수 있지만 허용되지 않는 것 같습니다.