정수 스트림에서 연속 중앙값 찾기


223

가능한 중복 :
C에서 롤링 중간 알고리즘

정수는 데이터 스트림에서 읽습니다. 지금까지 효율적으로 읽은 요소의 중앙값을 찾으십시오.

내가 읽은 해결책 : 왼쪽에서 최대 힙을 사용하여 유효 중간보다 작은 요소를 나타내고 오른쪽에서 최소 힙을 사용하여 유효 중간보다 큰 요소를 나타낼 수 있습니다.

들어오는 요소를 처리 한 후 힙의 요소 수는 최대 1 개씩 다릅니다. 두 힙에 동일한 수의 요소가 포함 된 경우 힙의 루트 데이터 평균이 유효 중앙값으로 확인됩니다. 힙의 균형이 맞지 않으면 더 많은 요소를 포함하는 힙의 루트에서 유효 중앙값을 선택합니다.

그러나 우리는 어떻게 최대 힙과 최소 힙을 구성 할 것입니까, 즉 여기서 효과적인 중앙값을 어떻게 알 수 있습니까? 모든 요소에 대해 max-heap에 1 요소를 삽입 한 다음 min-heap에 다음 1 요소 등을 삽입한다고 생각합니다. 내가 틀렸다면 정정 해주세요.


10
힙을 사용하는 영리한 알고리즘. 제목에서 나는 해결책을 즉시 생각할 수 없었다.
Mooing Duck

1
vizier의 솔루션은이 스트림이 임의로 길어질 수 있다고 가정한다는 것을 제외하고는 나에게 잘 어울립니다. 따라서 모든 것을 메모리에 보관할 수는 없습니다. 그 경우입니까?
야생 실행

2
@RunningWild 임의로 긴 스트림의 경우 피보나치 힙을 사용하고 (log (N) 삭제를 얻음) 순서대로 삽입 된 요소에 대한 포인터를 저장 한 다음 가장 오래된 요소를 제거하여 마지막 N 요소의 중앙값을 얻을 수 있습니다. 힙이 가득 차면 각 단계에서 요소 (한 힙에서 다른 힙으로 물건을 이동할 수도 있음). 반복되는 요소 수를 많이 저장하면 N보다 다소 나아질 수 있지만 (반복 횟수가 많은 경우) 일반적으로 전체 스트림의 중앙값을 원하면 일종의 분포 가정을 만들어야한다고 생각합니다.
Dougal

2
두 힙을 모두 비워서 시작할 수 있습니다. 첫 번째 int는 하나의 힙에 들어갑니다. 두 번째는 다른 항목으로 이동하거나 첫 번째 항목을 다른 힙으로 이동 한 다음 삽입합니다. 이것은 "하나의 힙이 다른 하나의 +1보다 커지지 않도록"일반화하고 특별한 케이싱이 필요하지 않습니다 (빈 힙의 "루트 값"은 0으로 정의 할 수 있음)
Jon Watte

나는 단지 MSFT 인터뷰에서이 질문을 받았다. 게시 해 주셔서 감사합니다
R Claven

답변:


383

스트리밍 된 데이터에서 실행되는 중앙값을 찾는 데에는 여러 가지 솔루션이 있습니다. 답변의 마지막 부분에 대해 간단히 이야기하겠습니다.

문제는 특정 솔루션 (최대 힙 / 최소 힙 솔루션)의 세부 사항에 관한 것이며 힙 기반 솔루션의 작동 방식은 다음과 같습니다.

처음 두 요소의 경우 왼쪽의 maxHeap에 작은 것을 추가하고 오른쪽의 minHeap에 더 큰 것을 추가하십시오. 그런 다음 스트림 데이터를 하나씩 처리하고

Step 1: Add next item to one of the heaps

   if next item is smaller than maxHeap root add it to maxHeap,
   else add it to minHeap

Step 2: Balance the heaps (after this step heaps will be either balanced or
   one of them will contain 1 more item)

   if number of elements in one of the heaps is greater than the other by
   more than 1, remove the root element from the one containing more elements and
   add to the other one

그런 다음 언제든지 다음과 같은 중앙값을 계산할 수 있습니다.

   If the heaps contain equal amount of elements;
     median = (root of maxHeap + root of minHeap)/2
   Else
     median = root of the heap with more elements

이제 답변의 시작 부분에서 약속 한 일반적인 문제에 대해 이야기하겠습니다. 데이터 스트림에서 중간 값을 찾는 것은 어려운 문제이며 , 일반적으로 메모리 제약이 있는 정확한 솔루션 을 찾는 것은 불가능할 것입니다. 반면에 데이터에 활용 가능한 특성이있는 경우 효율적인 특수 솔루션을 개발할 수 있습니다. 예를 들어, 데이터가 정수 유형이라는 것을 알고 있다면 계산 정렬을 사용할 수 있습니다일정한 메모리 상수 시간 알고리즘을 제공 할 수 있습니다. 힙 기반 솔루션은 다른 데이터 유형 (더블)에도 사용할 수 있으므로보다 일반적인 솔루션입니다. 마지막으로 정확한 중앙값이 필요하지 않고 근사값이 충분하면 데이터의 확률 밀도 함수를 추정하고이를 사용하여 중앙값을 추정하면됩니다.


6
이러한 힙은 제한없이 커집니다. 즉, 1 천만 개 이상의 요소를 슬라이딩하는 100 개의 요소 창은 천만 개의 요소를 모두 메모리에 저장해야합니다. 가장 최근에 본 100 개의 요소 만 메모리에 유지해야하는 색인 ​​가능한 건너 뛰기 목록을 사용하는 다른 답변은 아래를 참조하십시오.
Raymond Hettinger

1
질문 자체에 대한 설명 중 하나에서 설명하는 것처럼 힙을 사용하여 제한된 메모리 솔루션을 가질 수 있습니다.
Hakan Serce

1
c에서 힙 기반 솔루션의 구현을 찾을 수 있습니다 .
AShelly

1
와우 이것은이 특정 문제를 해결할뿐만 아니라 여기에서 힙을 배우는 데 도움이되었습니다. 파이썬에서 기본 구현입니다 : github.com/PythonAlgo/DataStruct
swati

2
@HakanSerce 왜 우리가 한 짓을했는지 설명해 주시겠습니까? 나는이 작품을 볼 수 있지만 직관적으로 이해할 수는 없다는 것을 의미합니다.
시바

51

모든 항목을 한 번에 메모리에 담을 수 없으면이 문제가 훨씬 더 어려워집니다. 힙 솔루션을 사용하려면 모든 요소를 ​​메모리에 한 번에 보유해야합니다. 이 문제의 대부분의 실제 응용에서는 불가능합니다.

대신 숫자 를 볼 때 각 정수 를 본 횟수 를 추적 하십시오. 4 바이트 정수, 즉 2 ^ 32 버킷 또는 최대 2 ^ 33 정수 (각 int의 키 및 개수)를 가정하면 2 ^ 35 바이트 또는 32GB입니다. 키를 저장하거나 0 (예 : python의 defaultdict와 같은) 항목의 수를 저장할 필요가 없으므로 이보다 훨씬 적을 것입니다. 각각의 새로운 정수를 삽입하려면 일정한 시간이 걸립니다.

그런 다음 어느 시점에서든 중앙값을 찾으려면 카운트를 사용하여 어떤 정수가 중간 요소인지 판별하십시오. 이것은 일정한 시간이 걸립니다 (큰 상수이지만 그럼에도 불구하고 일정합니다).


3
거의 모든 숫자가 한 번 표시되면 스파 스 목록 보다 더 많은 메모리가 필요합니다. 그리고 숫자가 너무 많으면 숫자에 맞지 않아 대부분의 숫자가 한 번 나타날 것입니다. 그럼에도 불구하고 이것은 엄청난 수의 영리한 솔루션입니다 .
Mooing Duck

1
희소 목록의 경우 메모리 측면에서 더 나쁘다는 데 동의합니다. 정수가 무작위로 분포되어 있지만 직감이 암시하는 것보다 훨씬 빨리 복제가 시작됩니다. mathworld.wolfram.com/BirthdayProblem.html을 참조하십시오 . 따라서 몇 GB의 데이터가있는 즉시 이것이 효과가있을 것이라고 확신합니다.
앤드류 C

4
@AndrewC는 중간 값을 찾는 데 일정한 시간이 걸리는 방법을 설명 할 수 있습니까? 내가 n 개의 다른 종류의 정수를 본다면 최악의 경우 마지막 요소가 중앙값 일 수 있습니다. 이것은 O (n) 활동을 찾는 중간 값을 만듭니다.
shshnk

@shshnk이 경우에 >>> 2 ^ 35 인 총 요소 개수가 아닌가?
VishAmdi

@ shshnk VishAmdi가 말했듯이, 당신이 본 다른 정수의 수는 여전히 선형이라는 것이 맞습니다.이 솔루션을 위해 내가 만들고있는 가정은 n이 당신이 본 숫자의 수라는 것입니다. 2 ^ 33보다 큽니다. 숫자가 많지 않으면 maxheap 솔루션이 더 좋습니다.
Andrew C

49

입력의 분산이 통계적으로 분포 된 경우 (예 : 정규, 로그 정규 등), 저수지 샘플링은 임의로 긴 수의 스트림에서 백분위 수 / 중앙값을 추정하는 합리적인 방법입니다.

int n = 0;  // Running count of elements observed so far  
#define SIZE 10000
int reservoir[SIZE];  

while(streamHasData())
{
  int x = readNumberFromStream();

  if (n < SIZE)
  {
       reservoir[n++] = x;
  }         
  else 
  {
      int p = random(++n); // Choose a random number 0 >= p < n
      if (p < SIZE)
      {
           reservoir[p] = x;
      }
  }
}

"저장소"는 크기에 관계없이 모든 입력의 실행중인 균일 한 (공정한) 샘플입니다. 중앙값 (또는 백분위 수)을 찾는 것은 저수지를 분류하고 흥미로운 지점을 폴링하는 간단한 문제입니다.

저장소는 고정 크기이므로 정렬은 효과적으로 O (1)로 간주 될 수 있습니다.이 방법은 일정한 시간과 메모리 소비로 실행됩니다.


호기심에서 왜 차이가 필요합니까?
LazyCat

저수지 절반이 비워 지도록 스트림이 SIZE 요소 미만을 반환 할 수 있습니다. 이것은 중앙값을 계산할 때 고려해야합니다.
Alex

중앙값 대신 차이를 계산하여 더 빠르게 만들 수있는 방법이 있습니까? 제거 및 추가 된 샘플과 이전 중앙값이 충분한 정보입니까?
inf3rno

30

내가 찾은 스트림의 백분위 수를 계산하는 가장 효율적인 방법은 P² 알고리즘입니다. Raj Jain, Imrich Chlamtac : 저장 관찰없이 Quantitles 및 히스토그램의 동적 계산을위한 P² 알고리즘입니다. 코뮌. ACM 28 (10) : 1076-1085 (1985)

이 알고리즘은 구현하기가 매우 쉽고 매우 잘 작동합니다. 그러나 추정치이므로 명심하십시오. 초록에서 :

휴리스틱 알고리즘은 중앙값과 다른 Quantile의 동적 계산을 위해 제안됩니다. 관측치가 생성 될 때 추정치는 동적으로 생성됩니다. 관측치는 저장되지 않습니다. 따라서 알고리즘은 관측 횟수에 관계없이 매우 작고 고정 된 저장 요구 사항을 갖습니다. 따라서 산업용 컨트롤러 및 레코더에 사용할 수있는 Quantile 칩으로 구현하는 데 이상적입니다. 알고리즘은 히스토그램 플로팅으로 더욱 확장됩니다. 알고리즘의 정확성이 분석됩니다.


2
Count-Min Sketch 는 P ^ 2보다 낫습니다. 후자는 그렇지 않은 경우 오류가 발생합니다.
sinoTrinity

1
또한 Greenwald와 Khanna의 "공간 효율적인 온라인 Quantile 요약 계산"을 고려하십시오.이 오류는 오류 범위를 제공하고 메모리 요구 사항이 좋습니다.
Paul Chernoch

1
또한, 확률 적 접근 방식이 블로그 게시물을 참조하십시오 research.neustar.biz/2013/09/16/... 과가 참조하는 종이는 여기 : arxiv.org/pdf/1407.1121v1.pdf 이 검소한 "라고 스트리밍 "
Paul Chernoch

27

가장 최근에 본 n 개의 요소 의 중앙값을 찾으려면 이 문제에는 가장 최근에 본 n 개의 요소 만 메모리에 보관 해야하는 정확한 솔루션이 있습니다. 빠르며 잘 확장됩니다.

인덱싱 skiplist의 지지체 O (LN N)의 삽입, 제거 및 임의의 요소의 인덱스 검색 정렬 된 순서를 유지하면서. n 번째로 오래된 항목을 추적 하는 FIFO 대기열 과 결합 하면 솔루션은 간단합니다.

class RunningMedian:
    'Fast running median with O(lg n) updates where n is the window size'

    def __init__(self, n, iterable):
        self.it = iter(iterable)
        self.queue = deque(islice(self.it, n))
        self.skiplist = IndexableSkiplist(n)
        for elem in self.queue:
            self.skiplist.insert(elem)

    def __iter__(self):
        queue = self.queue
        skiplist = self.skiplist
        midpoint = len(queue) // 2
        yield skiplist[midpoint]
        for newelem in self.it:
            oldelem = queue.popleft()
            skiplist.remove(oldelem)
            queue.append(newelem)
            skiplist.insert(newelem)
            yield skiplist[midpoint]

다음은 완전한 작업 코드에 대한 링크입니다 (알기 쉬운 클래스 버전 및 인덱싱 가능한 건너 뛰기 코드가 인라인 된 최적화 된 생성기 버전).


7
그래도 올바르게 이해한다면, 이것은 당신에게 마지막까지 본 N 원소의 중앙값만을 제공합니다. 이것은 그 작업을위한 정말 매끄러운 솔루션처럼 보입니다.
Andrew C

16
권리. 대답은 메모리에 마지막 n 개의 요소를 유지하여 모든 요소의 중앙값을 찾을 수있는 것처럼 들립니다. 일반적으로 불가능합니다. 알고리즘은 마지막 n 요소의 중앙값을 찾습니다.
Hans-Peter Störr

8
"중간 실행 중"이라는 용어는 일반적으로 데이터 하위 집합 의 중간을 나타내는 데 사용됩니다 . OP는 비표준 방식으로 공통 용어를 사용합니다.
Rachel Hettinger

18

이것을 생각하는 직관적 인 방법은 균형 잡힌 이진 검색 트리가 있다면 루트는 중간 요소가 될 것입니다. 왜냐하면 같은 수의 더 작고 큰 요소가 있기 때문입니다. 이제 나무가 가득 차 있지 않으면 마지막 레벨에서 누락 된 요소가 있기 때문에 이것은 사실이 아닙니다.

따라서 우리가 할 수있는 것은 중앙값과 두 개의 균형 잡힌 이진 트리가 있습니다. 하나는 중앙값보다 작은 요소에 대한 것이고 다른 하나는 중앙값보다 큰 요소에 대한 것입니다. 두 나무는 같은 크기로 유지되어야합니다.

데이터 스트림에서 새로운 정수를 얻으면 정수와 중앙값을 비교합니다. 중앙값보다 크면 오른쪽 트리에 추가합니다. 두 개의 나무 크기가 1보다 큰 경우 오른쪽 나무의 최소 요소를 제거하고 새로운 중앙값으로 만들고 오래된 트리를 왼쪽 트리에 넣습니다. 더 작게 비슷합니다.


어떻게 하시겠습니까? "우리는 오른쪽 나무의 최소 요소를 제거합니다"
Hengameh

2
바이너리 검색 트리를 의미했기 때문에 min 요소는 루트에서 완전히 벗어났습니다.
Irene Papakonstantinou

7

효율적인 것은 문맥에 의존하는 단어입니다. 이 문제에 대한 솔루션은 삽입 량과 관련하여 수행되는 쿼리 량에 따라 다릅니다. 중앙값에 관심이있는 끝에 N 개의 숫자와 K 번을 삽입한다고 가정합니다. 힙 기반 알고리즘의 복잡성은 O (N log N + K)입니다.

다음 대안을 고려하십시오. 배열의 숫자를 펑크하고 각 쿼리에 대해 선형 선택 알고리즘을 실행하십시오 (즉, 빠른 정렬 피벗 사용). 이제 실행 시간이 O (KN) 인 알고리즘이 있습니다.

이제 K가 충분히 작 으면 (빈번한 쿼리) 후자의 알고리즘이 실제로 더 효율적이며 그 반대도 마찬가지입니다.


1
힙 예제에서 조회는 일정한 시간이므로 O (N log N + K)이어야하지만 포인트는 여전히 유지됩니다.
앤드류 C

그렇습니다. 좋은 지적입니다. 당신은 맞습니다 N log N은 여전히 ​​최고의 용어입니다.
Peteris

-2

하나의 힙 만으로이 작업을 수행 할 수 없습니까? 업데이트 : 아니요. 의견을 참조하십시오.

불변 : 2*n입력을 읽은 후 최소 힙이 n가장 큰 값을 유지합니다.

루프 : 2 개의 입력을 읽습니다. 둘 다 힙에 추가하고 힙의 최소값을 제거하십시오. 이것은 불변을 재설정합니다.

따라서 2n입력을 읽었을 때 힙의 최소값은 n 번째입니다. 중간 위치 주변의 두 요소를 평균화하고 홀수의 입력 후 쿼리를 처리하려면 약간의 추가 복잡성이 필요합니다.


1
작동하지 않습니다 : 나중에 맨 위에있는 것으로 떨어 뜨릴 수 있습니다. 예를 들어, 1에서 100까지의 숫자를 사용하지만 100, 99, ..., 1의 역순으로 알고리즘을 시도하십시오.
zellyn

고마워, 젤린 불변 인이 재건되었다고 스스로를 설득하는 바보.
다리우스 베이컨
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.