O (n) 시간 및 O (1) 공간에서 중복 찾기


121

입력 : 0에서 n-1까지의 요소를 포함하는 n 요소의 배열이 주어지며,이 숫자 중 임의의 횟수가 나타납니다.

목표 : O (n)에서 이러한 반복되는 숫자를 찾고 일정한 메모리 공간 만 사용합니다.

예를 들어, n이 7이고 배열이 {1, 2, 3, 1, 3, 0, 6}이라고합시다. 대답은 1 & 3이되어야합니다. 여기에서 비슷한 질문을 확인했지만 대답은 HashSet기타 와 같은 일부 데이터 구조를 사용했습니다 .

동일한 효율적인 알고리즘이 있습니까?

답변:


164

이것은 추가 부호 비트가 필요하지 않은 내가 생각해 낸 것입니다.

for i := 0 to n - 1
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 0 to n - 1
    if A[i] != i then 
        print A[i]
    end if
end for

첫 번째 루프 x는 요소 가 한 번 이상 존재하면 해당 항목 중 하나가 위치에 있도록 배열을 순열합니다 A[x].

처음에는 O (n)처럼 보이지 않을 수도 있지만, 중첩 된 루프가 있어도 여전히 제 O(N)시간에 실행됩니다 . 스왑은 ithat 이있는 경우에만 발생 A[i] != i하며 각 스왑은 A[i] == i이전에 true가 아니었던 that 과 같은 요소를 하나 이상 설정합니다 . 즉, 총 스왑 수 (따라서 while루프 본문 의 총 실행 수 )가 최대 N-1.

두 번째 루프는 xfor가 A[x]같지 않은 값을 인쇄합니다 x. 첫 번째 루프 x는 배열에 적어도 한 번 존재 하는 경우 해당 인스턴스 중 하나가에 있음을 보장하기 때문에 해당 인스턴스에 존재하지 않는 A[x]값을 인쇄합니다 x. 배열.

(아이디 온 링크로 플레이 할 수 있습니다)


10
@arasmussen : 네. 그래도 먼저 깨진 버전을 생각해 냈습니다. 문제의 a[a[i]]제약 은 솔루션에 대한 약간의 단서를 제공합니다. 모든 유효한 배열 값은에서 유효한 배열 인덱스 힌트이기도 하고 O (1) 공간 제약 조건은 swap()작업에서 핵심 적인 힌트 라는 사실입니다.
caf

2
@caf : 실패한 {3,4,5,3,4} 배열로 코드를 실행하십시오.
NirmalGeo 2011

6
@NirmalGeo : 5범위 0..N-1( N이 경우 5) 가 아니기 때문에 유효한 입력 이 아닙니다 .
caf 2011

2
@caf {1,2,3,1,3,0,0,0,0,6}에 대한 출력은 3 1 0 0 0 또는 반복이 2보다 큰 경우입니다. 올바른 O / p입니까?
터미널 :

3
이것은 놀랍다! 이 질문에 대해 일반적으로 더 제한적인 여러 가지 변형을 보았으며 이것이 내가 본 것 중 가장 일반적인 해결 방법입니다. 간단히 print설명을 변경하여 print i이를 stackoverflow.com/questions/5249985/… 에 대한 솔루션으로 바꾸고 ( "bag"이 수정 가능한 배열이라고 가정) Qk of stackoverflow.com/questions/3492302/… 를 언급하겠습니다 .
j_random_hacker

35

caf의 훌륭한 대답 은 배열에 k 번 나타나는 각 숫자를 k-1 번 인쇄합니다. 이것은 유용한 동작이지만 질문은 각 복제물이 한 번만 인쇄되도록 요구하며 선형 시간 / 상수 공간 경계를 날리지 않고이를 수행 할 수있는 가능성을 암시합니다. 이것은 그의 두 번째 루프를 다음 의사 코드로 대체하여 수행 할 수 있습니다.

for (i = 0; i < N; ++i) {
    if (A[i] != i && A[A[i]] == A[i]) {
        print A[i];
        A[A[i]] = i;
    }
}

이것은 첫 번째 루프가 실행 된 후 값 m이 두 번 이상 나타나는 경우 해당 모양 중 하나가 올바른 위치에 있음을 보장 하는 속성을 이용합니다 A[m]. 주의를 기울이면 해당 "홈"위치를 사용하여 사본이 아직 인쇄되었는지 여부에 대한 정보를 저장할 수 있습니다.

caf의 버전에서 배열을 살펴 A[i] != i보았을 때 그것이 A[i]중복 임을 암시했습니다 . 내 버전에서는 약간 다른 불변에 의존합니다. A[i] != i && A[A[i]] == A[i]이는 이전에 본 적이없는A[i] 중복 임을 의미합니다 . ( "우리가 이전에 본 적이없는"부분을 삭제하면 나머지는 caf의 불변의 진실과 모든 복제본이 홈 위치에 일부 사본이 있다는 보장에 의해 암시되는 것으로 볼 수 있습니다.)이 속성은 다음과 같습니다. 처음 (카페의 첫 번째 루프가 끝난 후)와 각 단계 후에 유지된다는 것을 아래에 보여줍니다.

배열을 살펴보면 A[i] != i테스트 부분의 성공은 이전에 본 적이없는 중복 일 A[i] 있음을 의미합니다 . 이전에 본 적이 없다면 A[i]의 집 위치가 자신을 가리킬 것으로 예상 if합니다. 이것이 조건의 후반부에서 테스트 한 것입니다. 이 경우 인쇄하고 홈 위치를 변경하여 처음 발견 된이 복제본을 다시 가리 키도록하여 2 단계 "주기"를 만듭니다.

이 작업은 우리의 불변을 변경할 생각하지 않는 것을 확인하려면 m = A[i]특정 위치에 대한 i만족 A[i] != i && A[A[i]] == A[i]. 우리가 만든 변경 사항 ( A[A[i]] = i)은 조건의 m후반부 if를 실패하게하여 다른 집이 아닌 항목이 중복으로 출력되는 것을 방지하기 위해 작동 하지만 i, 집 위치에 도착하면 작동 m할까요? 예, 이제이 새로운 조건 i에서 if조건의 전반부 A[i] != i가 참인 것을 발견 하더라도 후반부는 가리키는 위치가 집 위치인지 아닌지 여부를 테스트하기 때문입니다. 이 상황에서 우리는 더 이상 여부를 알 수 없습니다 m또는 A[m]중복 값이었다, 그러나 우리는 어느 쪽이든 것을 알고,이 2주기가 caf의 첫 번째 루프의 결과에 나타나지 않도록 보장되기 때문에 이미보고되었습니다 . ( m != A[m]그런 다음 정확히 하나의 mand A[m]가 두 번 이상 발생하고 다른 하나는 전혀 발생하지 않습니다.)


1
예, 그것은 제가 생각해 낸 것과 매우 유사합니다. 동일한 첫 번째 루프가 다른 인쇄 루프에서 여러 다른 문제에 어떻게 유용한 지 흥미 롭습니다.
caf

22

다음은 의사 코드입니다.

for i <- 0 to n-1:
   if (A[abs(A[i])]) >= 0 :
       (A[abs(A[i])]) = -(A[abs(A[i])])
   else
      print i
end for

C ++의 샘플 코드


3
매우 영리합니다-인덱싱 된 항목의 부호 비트에 답변을 인코딩합니다!
holtavolt 2011

3
@sashang : 그럴 수 없습니다. 문제 사양을 확인하십시오. " 0부터 n-1까지의 요소 를 포함하는 n 요소의 배열이 주어 졌습니다. "
Prasoon Saurav 2011

5
이것은 중복 0을 감지하지 않으며 중복 된 것과 동일한 숫자를 여러 번 발견합니다.
Null Set

1
@Null 설정 : 당신은 그냥 대체 할 수 -~제로 문제에 대한.
user541686 2011-04-21

26
이것은 문제가 발생하는 답일 수 있지만 기술적으로는 O(n)숨겨진 공간 ( n부호 비트)을 사용합니다. 각 요소가 0및 사이의 값만 보유 할 수 있도록 배열이 정의되면 n-1분명히 작동하지 않습니다.
caf

2

상대적으로 작은 N의 경우 div / mod 작업을 사용할 수 있습니다.

n.times do |i|
  e = a[i]%n
  a[e] += n
end

n.times do |i| 
  count = a[i]/n
  puts i if count > 1
end

C / C ++는 아니지만 어쨌든

http://ideone.com/GRZPI


+1 좋은 솔루션입니다. 두 번 후 항목에 n 추가를 중지하면 더 큰 n 을 수용 할 수 있습니다.
Apshir

1

정말 예쁘지는 않지만 적어도 O (N) 및 O (1) 속성을 쉽게 볼 수 있습니다. 기본적으로 배열을 스캔하고 각 숫자에 대해 해당 위치가 이미 본 한 번 (N) 또는 이미 본 여러 번 (N + 1) 플래그가 지정되었는지 확인합니다. 이미 본 한 번 플래그가 지정된 경우이를 인쇄하고 이미 본 여러 번 플래그를 지정합니다. 플래그가 지정되지 않은 경우 이미 본 한 번 플래그를 지정하고 해당 인덱스의 원래 값을 현재 위치로 이동합니다 (플래 깅은 파괴적인 작업입니다).

for (i=0; i<a.length; i++) {
  value = a[i];
  if (value >= N)
    continue;
  if (a[value] == N)  {
    a[value] = N+1; 
    print value;
  } else if (a[value] < N) {
    if (value > i)
      a[i--] = a[value];
    a[value] = N;
  }
}

또는 더 나은 방법 (이중 루프에도 불구하고 더 빠름) :

for (i=0; i<a.length; i++) {
  value = a[i];
  while (value < N) {
    if (a[value] == N)  {
      a[value] = N+1; 
      print value;
      value = N;
    } else if (a[value] < N) {
      newvalue = value > i ? a[value] : N;
      a[value] = N;
      value = newvalue;
    }
  }
}

+1, 잘 작동하지만 정확히 왜 if (value > i) a[i--] = a[value];작동 하는지 알아 내기 위해 약간의 생각이 필요했습니다 . 만약 value <= i그렇다면 우리가 이미 값을 처리하고 a[value]안전하게 덮어 쓸 수 있다면. 또한 나는 O (N) 본성이 명백하다고 말하지 않을 것입니다! 철자법 : 메인 루프는 N여러 번 a[i--] = a[value];실행 되지만 행이 여러 번 실행됩니다. 해당 라인은 실행될 a[value] < N때마다, 아직 N설정 되지 않은 배열 값이로 설정된 직후 에만 실행될 수 N있으므로 N최대 2N루프 반복 횟수 까지 최대 횟수까지 실행할 수 있습니다 .
j_random_hacker jul.

1

C의 한 가지 해결책은 다음과 같습니다.

#include <stdio.h>

int finddup(int *arr,int len)
{
    int i;
    printf("Duplicate Elements ::");
    for(i = 0; i < len; i++)
    {
        if(arr[abs(arr[i])] > 0)
          arr[abs(arr[i])] = -arr[abs(arr[i])];
        else if(arr[abs(arr[i])] == 0)
        {
             arr[abs(arr[i])] = - len ;
        }
        else
          printf("%d ", abs(arr[i]));
    }

}
int main()
{   
    int arr1[]={0,1,1,2,2,0,2,0,0,5};
    finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
    return 0;
}

O (n) 시간과 O (1) 공간 복잡성입니다.


1
이것의 공간 복잡도는 N 개의 추가 부호 비트를 사용하기 때문에 O (N)입니다. 알고리즘은 배열 요소 유형이 0에서 N-1까지의 숫자 보유 할 수 있다는 가정하에 작동해야합니다 .
caf

예 그 사실이지만 그들이 0에서 n-1까지의 알고리즘을 원했기 때문에 완벽하다는 요청을 받았으며 또한 O (n)
이상이되는

1

이 배열을 단방향 그래프 데이터 구조로 제시한다고 가정 해 보겠습니다. 각 숫자는 꼭지점이고 배열의 인덱스는 그래프의 가장자리를 형성하는 다른 꼭지점을 가리 킵니다.

더 간단하게하기 위해 0에서 n-1까지의 인덱스와 0..n-1의 숫자 범위가 있습니다. 예 :

   0  1  2  3  4 
 a[3, 2, 4, 3, 1]

0 (3)-> 3 (3)은주기입니다.

답변 : 인덱스를 사용하여 배열을 순회하십시오. a [x] = a [y]이면 순환이므로 중복됩니다. 다음 인덱스로 건너 뛰고 배열이 끝날 때까지 계속합니다. 복잡성 : O (n) 시간 및 O (1) 공간.


0

위의 caf 방법을 보여주는 작은 파이썬 코드 :

a = [3, 1, 1, 0, 4, 4, 6] 
n = len(a)
for i in range(0,n):
    if a[ a[i] ] != a[i]: a[a[i]], a[i] = a[i], a[a[i]]
for i in range(0,n):
    if a[i] != i: print( a[i] )

스왑은 단일 i값에 대해 두 번 이상 발생해야 할 수 있습니다 while. 내 대답에 유의하십시오 .
caf

0

알고리즘은 다음 C 함수에서 쉽게 볼 수 있습니다. 필요하지는 않지만 원래 배열을 검색하는 것은 모듈로 n의 각 항목을 사용하여 가능합니다 .

void print_repeats(unsigned a[], unsigned n)
{
    unsigned i, _2n = 2*n;
    for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
    for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
    putchar('\n');
}

테스트를위한 Ideone Link.


2 * n까지의 숫자로 작업하려면 원래 숫자를 저장하는 데 필요한 것보다 어레이 항목 당 추가 1 비트의 저장 공간이 필요하기 때문에 이것이 기술적으로 "속임수"입니다. 실제로 최대 3 * n-1까지 숫자를 저장하기 때문에 항목 당 추가 비트가 log2 (3) = 1.58에 가까워 야합니다.
j_random_hacker jul.

0
static void findrepeat()
{
    int[] arr = new int[7] {0,2,1,0,0,4,4};

    for (int i = 0; i < arr.Length; i++)
    {
        if (i != arr[i])
        {
            if (arr[i] == arr[arr[i]])
            {
                Console.WriteLine(arr[i] + "!!!");
            }

            int t = arr[i];
            arr[i] = arr[arr[i]];
            arr[t] = t;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();

    for (int j = 0; j < arr.Length; j++)
    {
        if (j == arr[j])
        {
            arr[j] = 1;
        }
        else
        {
            arr[arr[j]]++;
            arr[j] = 0;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();
}

0

0 (n) 시간 복잡성과 일정한 추가 공간에서 중복을 찾기 위해 신속하게 하나의 샘플 놀이터 앱을 만들었습니다. 중복을 찾는 URL을 확인하십시오

IMP 위의 솔루션은 배열이 0에서 n-1까지의 요소를 포함 할 때 작동하며 이러한 숫자 중 하나가 횟수에 관계없이 나타납니다.


0
private static void printRepeating(int arr[], int size) {
        int i = 0;
        int j = 1;
        while (i < (size - 1)) {
            if (arr[i] == arr[j]) {
                System.out.println(arr[i] + " repeated at index " + j);
                j = size;
            }
            j++;
            if (j >= (size - 1)) {
                i++;
                j = i + 1;
            }
        }

    }

위의 솔루션은 O (n)의 시간 복잡도와 일정한 공간에서 동일한 결과를 얻을 수 있습니다.
user12704811

3
제한된 단기 도움을 제공 할 수있는이 코드 스 니펫에 감사드립니다. 적절한 설명 이것이 문제에 대한 좋은 해결책 인 이유 를 보여줌으로써 장기적인 가치를 크게 향상시키고 다른 유사한 질문을 가진 미래의 독자에게 더 유용하게 만들 것입니다. 제발 편집 당신이 만든 가정 등 일부 설명을 추가 할 답변을.
Toby Speight

3
BTW, 시간 복잡도는 여기에서 O (n²) 인 것 같습니다. 내부 루프를 숨기는 것은 변경되지 않습니다.
Toby Speight

-2

배열이 너무 크지 않은 경우이 솔루션은 더 간단합니다. 동일한 크기의 다른 배열을 생성합니다.

1 입력 배열과 동일한 크기의 비트 맵 / 배열을 만듭니다.

 int check_list[SIZE_OF_INPUT];
 for(n elements in checklist)
     check_list[i]=0;    //initialize to zero

2 입력 배열을 스캔하고 위 배열에서 개수를 늘립니다.

for(i=0;i<n;i++) // every element in input array
{
  check_list[a[i]]++; //increment its count  
}  

3 이제 check_list 배열을 스캔하고 복제 된 항목을 한 번 또는 여러 번 인쇄합니다.

for(i=0;i<n;i++)
{

    if(check_list[i]>1) // appeared as duplicate
    {
        printf(" ",i);  
    }
}

물론 위의 솔루션에 의해 소비되는 공간의 두 배를 차지하지만 시간 효율성은 기본적으로 O (n) 인 O (2n)입니다.


이것은 O(1)공간 이 아닙니다 .
Daniel Kamil Kozar

이런 ...! 알아 차리지 못 했어 ... 내 잘못이야.
Deepthought

@nikhil 어떻게 O (1) ?. 내 배열 check_list는 입력 크기가 커짐에 따라 선형 적으로 증가하므로 O (1)이라고 부르기 위해 사용하는 휴리스틱은 무엇입니까?
Deepthought

주어진 입력에 대해 일정한 공간이 필요합니다. O (1) 아닌가요? 내가 잘 잘못 :)이 될 수
nikhil

내 솔루션은 입력이 증가함에 따라 더 많은 공간이 필요합니다. 특정 입력에 대해 측정되지 않은 알고리즘의 효율성 (공간 / 시간) (이 경우 모든 검색 알고리즘의 시간 효율성은 일정합니다. 즉, 검색 한 첫 번째 인덱스에서 발견 된 요소) 모든 입력에 대해 측정됩니다. 우리가 최고의 케이스, 최악의 케이스 및 평균 케이스가있는 이유.
Deepthought
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.