숫자 목록에서 "구멍"찾기


14

주어진 정렬되지 않은 정수 목록에 존재하지 않는 첫 번째 (가장 작은) 정수를 찾는 가장 빠른 방법은 무엇입니까 ?

내 기본 접근 방식은 정렬하고 목록을 단계별로 실행하는 것입니다. 더 좋은 방법이 있습니까?


6
@Jodrell 나는 무한한 진행을 정렬하는 것이 어렵다고 생각한다 ;-)
maple_shaft

3
@maple_shaft가 동의했습니다. 시간이 걸릴 수 있습니다.
Jodrell

4
정렬되지 않은 목록에 대해 먼저 정의하는 방법은 무엇입니까?
Jodrell

1
방금 이것이 개념적 문제가 아니기 때문에 이것이 아마도 StackOverflow에 속한다는 것을 깨달았습니다.
JasonTrue

2
@JasonTrue FAQ If you have a question about… •algorithm and data structure concepts에서 주제 IMHO에 있습니다.
maple_shaft

답변:


29

"숫자"라고 말할 때 "정수"를 의미한다고 가정하면 크기가 2 ^ n 인 비트 벡터를 사용할 수 있습니다. 여기서 n은 요소의 수입니다 (예 : 범위는 1과 256 사이의 정수를 포함하므로 256- 비트 또는 32 바이트, 비트 벡터). 범위의 n 위치에서 정수를 만나면 n 번째 비트를 설정하십시오.

정수 컬렉션 열거가 끝나면 비트 벡터의 비트를 반복하여 비트 세트 0의 위치를 ​​찾습니다. 이제 누락 된 정수의 위치 n과 일치합니다.

이것은 O (2 * N)이므로 O (N)이며 전체 목록을 정렬하는 것보다 메모리 효율성이 높습니다.


6
직접 비교로서, 부호없는 32 비트 정수를 모두 1로 설정하면 약 기가 바이트의 메모리에서 누락 된 정수 문제를 해결할 수 있습니다. 대신 정렬하면 8GB 이상의 메모리를 사용해야합니다. 그리고 이와 같은 특별한 경우를 제외하고 (비트 벡터가 있으면 목록이 정렬되면) 정렬은 거의 항상 n log n 또는 그 이상이므로 상수가 비용의 복잡성을 능가하는 경우를 제외하고 선형 접근법이 승리합니다.
JasonTrue

1
선험 범위를 모른다면 어떻게해야합니까?
Blrfl

2
정수 데이터 유형 Blrfl이있는 경우 추가로 좁힐 정보가 충분하지 않더라도 범위의 최대 범위를 확실히 알고 있습니다. 작은 목록이지만 정확한 크기를 모르는 경우 정렬이 더 간단한 해결책 일 수 있습니다.
JasonTrue

1
또는 목록에서 다른 루프를 먼저 수행하여 가장 작고 큰 요소를 찾으십시오. 그런 다음 가장 작은 값을 기본 오프셋으로 사용하여 정확한 크기의 배열을 할당 할 수 있습니다. 여전히).
보안

1
@ JPatrick : 숙제, 사업이 아니며 CS 년 전에 졸업했습니다 :).
Fabian Zeindl

4

전체 목록을 먼저 정렬하면 최악의 런타임이 보장됩니다. 또한 정렬 알고리즘을 선택하는 것이 중요합니다.

이 문제에 접근하는 방법은 다음과 같습니다.

  1. 목록에서 가장 작은 요소에 중점을 둔 힙 정렬을 사용하십시오 .
  2. 각 스왑 후 간격이 있는지 확인하십시오.
  3. 당신이 차이를 발견하면 return: 당신은 당신의 대답을 찾았습니다.
  4. 간격이 없으면 계속 교체하십시오.

다음 은 힙 정렬시각화입니다 .


하나의 질문은리스트의 "가장 작은"요소를 어떻게 식별 하는가?
Jodrell

4

"구멍"이 하나 뿐인 배열의 특수한 경우에는 난해하고 "영리한"XOR 기반 솔루션을 사용해 볼 수 있습니다.

  • 배열의 범위를 결정하십시오. 이는 "max"및 "min"변수를 배열의 첫 번째 요소로 설정하여 수행되며 그 이후의 각 요소에 대해 해당 요소가 최소값보다 작거나 최대 값보다 큰 경우 최소값 또는 최대 값을 새로운 가치.
  • 범위가 세트의 카디널리티보다 1보다 작 으면 "구멍"이 하나만 있으므로 XOR을 사용할 수 있습니다.
  • 정수 변수 X를 0으로 초기화하십시오.
  • 최소부터 최대까지의 정수마다 X와 함께 해당 값을 XOR하고 결과를 X에 저장합니다.
  • 이제 배열의 각 정수를 X와 XOR하여 이전과 같이 각 연속 결과를 X에 저장합니다.
  • 완료되면 X가 "구멍"의 가치가됩니다.

이것은 비트 벡터 솔루션과 비슷한 약 2N 시간에 실행되지만 N> sizeof (int)에 대해 더 적은 메모리 공간이 필요합니다. 그러나 배열에 "구멍"이 여러 개 있으면 X는 모든 구멍의 XOR "합"이되어 실제 구멍 값으로 분리하기 어렵거나 불가능합니다. 이 경우 다른 답변의 "피벗"또는 "비트 벡터"접근과 같은 다른 방법으로 넘어갑니다.

복잡성을 더 줄이기 위해 피벗 방법과 유사한 것을 사용하여 이것을 재귀 할 수도 있습니다. 피벗 점을 기준으로 배열을 다시 정렬합니다 (왼쪽 최대 값과 오른쪽 최소값이됩니다. 피벗하는 동안 전체 배열의 최대 값과 최소값을 찾는 것이 쉽지 않습니다). 피벗의 왼쪽에 하나 이상의 구멍이 있으면 해당 측면으로 만 반복하십시오. 그렇지 않으면 반대쪽으로 재귀합니다. 구멍이 하나만 있다고 판단 할 수있는 지점에서 XOR 방법을 사용하여 구멍을 찾으십시오 (공구가 알려진 두 개의 요소 모음으로 계속 피벗하는 것보다 전체적으로 저렴해야 함). 순수한 피벗 알고리즘).


어리석게도 영리하고 굉장합니다! 이제 다양한 구멍 으로이 작업을 수행 할 수 있습니까? :-D

2

당신이 보게 될 숫자의 범위는 무엇입니까? 해당 범위가 매우 크지 않은 경우 숫자가있는 요소가 많은 배열을 사용하여 두 번의 스캔 (선형 시간 O (n))으로이 문제를 해결할 수 있습니다. 한 번 더 스캔하여 범위를 동적으로 찾을 수 있습니다. 공간을 줄이기 위해 각 숫자에 1 비트를 할당하여 바이트 당 8 개의 숫자 스토리지를 제공 할 수 있습니다.

초기 시나리오에서 더 좋으며 메모리를 복사하는 대신 원치 않는 다른 옵션은 검색 단계에서 찾은 최소값이 마지막으로 찾은 최소값보다 1이 아닌 경우 선택 정렬을 수정하여 일찍 종료하는 것입니다.


1

아니 정말. 아직 스캔되지 않은 숫자는 항상 주어진 "구멍"을 채우는 숫자 일 수 있으므로 각 숫자를 한 번 이상 스캔 한 다음 가능한 이웃과 비교하는 것을 피할 수 없습니다. 바이너리 트리를 만들거나 구멍을 찾을 때까지 왼쪽에서 오른쪽으로 순회하여 속도를 높일 수는 있지만 정렬하는 것과 본질적으로 정렬과 시간이 복잡합니다. 그리고 당신은 아마도 Timsort 보다 더 빠른 것을 내놓지 않을 것 입니다.


1
목록을 순회하는 것이 정렬과 시간이 복잡하다는 것을 말하고 있습니까?
maple_shaft

@maple_shaft : 아니요, 임의의 데이터에서 이진 트리를 만든 다음 왼쪽에서 오른쪽으로 순회하는 것은 정렬에서 작은 순으로 순회하는 것과 같습니다.
pillmuncher

1

여기에있는 대부분의 아이디어는 단순한 분류에 지나지 않습니다. 비트 벡터 버전은 일반 Bucketsort입니다. 힙 정렬도 언급되었습니다. 기본적으로 시간 / 공간 요구 사항과 요소의 범위 및 수에 따라 올바른 정렬 알고리즘을 선택합니다.

필자의 관점에서 힙 구조를 사용하는 것이 가장 일반적인 솔루션 일 것입니다 (힙은 기본적으로 완전한 정렬없이 가장 작은 요소를 효율적으로 제공합니다).

가장 작은 숫자를 먼저 찾은 다음 그보다 큰 각 정수를 스캔하는 접근법을 분석 할 수도 있습니다. 또는 의지에 차이가 있기를 희망하는 5 개의 가장 작은 숫자를 찾으십시오.

이러한 모든 알고리즘은 입력 특성 및 프로그램 요구 사항에 따라 그 강도가 높습니다.


0

추가 스토리지를 사용하지 않거나 정수의 너비 (32 비트)를 가정하는 솔루션입니다.

  1. 하나의 선형 패스에서 가장 작은 숫자를 찾으십시오. 이것을 "min"이라고하자. O (n) 시간 복잡성.

  2. 임의의 피벗 요소를 선택하고 퀵 정렬 스타일 파티션을 수행하십시오.

  3. 피벗이 = ( "피벗"- "최소") 위치에 있으면 파티션의 오른쪽에서 되풀이되고, 그렇지 않으면 파티션의 왼쪽에서 되풀이됩니다. 여기서 아이디어는 처음부터 구멍이 없으면 피벗이 ( "피벗"- "최소") 위치에있게되므로 첫 번째 구멍은 파티션의 오른쪽에 있어야하고 그 반대도 마찬가지입니다.

  4. 기본 사례는 1 요소의 배열이며이 요소와 다음 요소 사이에 구멍이 있습니다.

예상되는 총 실행 시간 복잡도는 O (n) (상수의 경우 8 * n)이고 최악의 경우는 O (n ^ 2)입니다. 비슷한 문제에 대한 시간 복잡도 분석은 여기 에서 찾을 수 있습니다 .


0

나는 중복이 없다고 보장되면 일반적으로 효율적으로 작동 해야하는 무언가를 생각해 냈습니다 * (그러나 구멍의 수와 범위의 정수로 확장 할 수 있어야 함).

이 방법의 배후에있는 아이디어는 퀵 정렬과 비슷합니다. 피벗과 그 주변을 분할 한 다음 구멍이있는 측면에서 재귀합니다. 어느쪽에 구멍이 있는지 확인하기 위해 가장 낮은 숫자와 가장 높은 숫자를 찾아서 그 쪽의 피벗 및 값 수와 비교합니다. 피벗이 17이고 최소 수가 11이라고 가정합니다. 구멍이 없으면 6 개의 숫자 (11, 12, 13, 14, 15, 16, 17)가 있어야합니다. 5가 있다면, 우리는 그쪽에 구멍이 있다는 것을 알고 우리는 그 쪽에서 재귀하여 그것을 찾을 수 있습니다. 그보다 더 명확하게 설명하는 데 어려움이 있으므로 예를 들어 보겠습니다.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

피벗:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15는 파이프 ( ||)로 표시된 피벗 입니다. 피벗의 왼쪽에는 5 개의 숫자가 있으며, 오른쪽에는 (15-10), 오른쪽에는 9가 있어야합니다 (10 (25-15)). 그래서 우리는 오른쪽에 재귀합니다. 구멍이 인접 해있을 경우 이전 경계는 15라는 점에 유의하십시오 (16).

[15] 18 16 17 20 |21| 22 23 24 25

왼쪽에는 4 개의 숫자가 있지만 5 (21-16)가 있어야합니다. 그래서 우리는 그곳에서 되풀이하고 다시 괄호 안의 이전 경계를 주목할 것입니다.

[15] 16 17 |18| 20 [21]

왼쪽에는 올바른 2 개의 숫자 (18-16)가 있지만 오른쪽에는 2 대신에 1 (20-18)이 있습니다. 우리의 결말 조건에 따라 1 수를 양변 (18, 20)과 비교하고 19가 한 번 더 누락되거나 재귀하는 것을 볼 수 있습니다.

[18] |20| [21]

왼쪽의 크기는 0이며 피벗 (20)과 이전 경계 (18) 사이에 간격이 있으므로 19는 구멍입니다.

* : 중복이있는 경우 전체 메소드 O (N)을 유지하면서 해시 세트를 사용하여 O (N) 시간에 제거 할 있지만 다른 메소드를 사용하는 것보다 시간이 더 걸릴 있습니다.


1
나는 OP가 단 하나의 구멍이 있다는 것에 대해 아무 말도하지 않았다고 생각합니다. 입력은 정렬되지 않은 숫자의 목록입니다. 얼마나 많은 숫자가 "반드시 있어야하는지"를 어떻게 결정했는지는 명확하지 않습니다.
Caleb

@caleb 얼마나 많은 구멍이 있는지는 중요하지 않으며 중복은 없습니다 (실제로는 다른 방법보다 오버 헤드가있을 수 있지만 해시 세트로 O (N)에서 제거 할 수 있음). 설명을 개선하려고 시도했지만 더 나은지 확인하십시오.
Kevin

이것은 선형이 아닙니다. IMO. (logN) ^ 2와 비슷합니다. 각 단계에서 관심있는 컬렉션의 하위 집합 (첫 번째 "구멍"이있는 것으로 식별 된 이전 하위 배열의 절반)을 피벗 한 다음 "구멍"이 있으면 왼쪽으로 되풀이합니다. 왼쪽이 그렇지 않으면 오른쪽입니다. (logN) ^ 2는 여전히 선형보다 낫습니다. N이 10 배 증가하면 2 (log (N) -1) + 1 단계 정도만 수행하면됩니다.
KeithS

@Keith-불행히도 각 레벨의 모든 숫자를 피벗해야하므로 약 n + n / 2 + n / 4 + ... = 2n (기술적으로 2 (nm)) 비교가 필요합니다 .
Kevin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.