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

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

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

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

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

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



답변

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

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

처음 두 요소의 경우 왼쪽의 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

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


답변

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

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

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


답변

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

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)로 간주 될 수 있습니다.이 방법은 일정한 시간과 메모리 소비로 실행됩니다.


답변

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

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

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


답변

가장 최근에 본 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]

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


답변

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

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

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


답변

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

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

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