Bir tamsayı akışından çalışan medyanı bulun


223

Olası Çoğaltma:
C'de yuvarlanan medyan algoritması

Tamsayıların bir veri akışından okunduğu göz önüne alındığında. Şimdiye kadar okunan öğelerin medyanını verimli bir şekilde bulun.

Okuduğum Çözüm: Etkili medyandan daha az olan öğeleri temsil etmek için sol tarafta bir maksimum yığın ve etkili medyandan daha büyük öğeleri temsil etmek için sağ tarafta bir min yığın kullanabilirsiniz.

Gelen bir eleman işlendikten sonra, yığınlardaki eleman sayısı en fazla 1 eleman değişir. Her iki yığın da aynı sayıda öğe içerdiğinde, yığının kök verilerinin ortalamasını etkili medyan olarak buluruz. Yığınlar dengelenmediğinde, daha fazla öğe içeren yığın kökünden etkili medyanı seçeriz.

Ancak, maksimum ve minimum yığınları nasıl oluşturabiliriz, yani burada etkili medyanı nasıl bilebiliriz? Bence max-yığına 1 element ve sonra min-yığına bir sonraki 1 element ekleyeceğimizi düşünüyorum. Beni düzelt Burada yanılıyorsam.


10
Yığınları kullanan akıllı algoritma. Başlıktan hemen bir çözüm düşünemedim.
Mooing Duck

1
vizier'in çözümü bana iyi geliyor, ancak bu akışın keyfi olarak uzun olabileceğini varsayıyordum (bu yüzden her şeyi hafızada tutamadınız). Durum bu mu?
Vahşi Koşu

2
@RunningWild İsteğe bağlı olarak uzun akışlar için, Fibonacci yığınlarını kullanarak (böylece log (N) siler) ve işaretçileri eklenen öğelere sırayla (örn. Bir deque) kaydederek son N öğelerinin medyanını alabilirsiniz. yığınlar dolduktan sonra her adımda eleman (belki de bir yığından diğerine hareket etmek). Tekrarlanan elemanların sayısını (çok sayıda tekrar varsa) depolayarak N'den biraz daha iyi olabilirsiniz, ancak genel olarak, tüm akışın medyanını istiyorsanız bir tür dağıtım varsayımları yapmanız gerektiğini düşünüyorum.
Dougal

2
Her iki kümeyi de boş olarak başlatabilirsiniz. İlk int bir yığın halinde gider; ikincisi ya diğerine gider ya da ilk öğeyi diğer yığına taşır ve sonra eklersiniz. Bu, "bir yığının diğer + 1'den daha büyük olmasına izin verme" anlamına gelir ve özel bir kasaya gerek yoktur (boş bir yığının "kök değeri" 0 olarak tanımlanabilir)
Jon Watte

Bu soruyu MSFT röportajında ​​SADECE aldım.
Gönderdiğiniz

Yanıtlar:


383

Akan verilerden çalışan medyan bulmak için bir dizi farklı çözüm var, cevabın sonunda kısaca bunlar hakkında konuşacağım.

Soru, belirli bir çözümün (maksimum yığın / dak yığın çözümü) ayrıntıları ve yığın tabanlı çözümün nasıl çalıştığı hakkında aşağıda açıklanmaktadır:

İlk iki öğe için soldaki maxHeap'e daha küçük, sağdaki minHeap'e daha büyük bir öğe ekleyin. Ardından akış verilerini tek tek işleyin,

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

Sonra herhangi bir zamanda medyanı şu şekilde hesaplayabilirsiniz:

   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

Şimdi genel olarak sorunun cevabının başında vaat edildiği gibi konuşacağım. Bir veri akışından çalışan medyan bulmak zor bir sorundur ve bellek kısıtlamaları ile etkili bir çözüm bulmak genel durum için muhtemelen imkansızdır. Öte yandan, verilerden yararlanabileceğimiz bazı özellikler varsa, etkili özel çözümler geliştirebiliriz. Örneğin, verilerin ayrılmaz bir tür olduğunu biliyorsanız, sayma sıralaması kullanabilirizsize sabit bir bellek sabit zaman algoritması verebilir. Yığın tabanlı çözüm daha genel bir çözümdür, çünkü diğer veri türleri (çiftler) için de kullanılabilir. Ve son olarak, tam medyan gerekli değilse ve bir tahmin yeterliyse, veriler için bir olasılık yoğunluk fonksiyonunu tahmin etmeye ve bunu kullanarak medyanı tahmin etmeye çalışabilirsiniz.


6
Bu yığınlar sınırsız büyür (yani 10 milyon elementin üzerinde kayan 100 elementli bir pencere, 10 milyon elementin hafızada saklanmasını gerektirir). Yalnızca en son görülen 100 öğenin bellekte tutulmasını gerektiren dizinlenebilir atlama listelerini kullanan başka bir yanıt için aşağıya bakın.
Raymond Hettinger

1
Sorunun kendisinin yorumlarından birinde açıklandığı gibi yığınları da kullanarak sınırlı bir bellek çözümüne sahip olabilirsiniz.
Hakan Serce


1
Vay bu ben değilim yalnızca bu sorunun çözümü olduğunu yardımcı değil, aynı zamanda beni yığınları burada piton benim temel uygulamasıdır öğrenmek yardımcı: github.com/PythonAlgo/DataStruct
swati saoji

2
@HakanSerce Yaptıklarımızı neden yaptığımızı açıklayabilir misiniz? Yani bunun işe yaradığını görebiliyorum, ama sezgisel olarak anlayamıyorum.
shiva

51

Bellekteki tüm öğeleri bir kerede tutamazsanız, bu sorun çok daha zorlaşır. Yığın çözümü, bellekteki tüm öğeleri bir kerede tutmanızı gerektirir. Bu sorunun gerçek dünyadaki uygulamalarının çoğunda bu mümkün değildir.

Rakamları gördüğünüz gibi yerine, takip sayımı her tamsayı bakınız kaç defa. 4 bayt tamsayı varsayarsak, bu 2 ^ 32 kova veya en fazla 2 ^ 33 tamsayı (her int için anahtar ve sayım), 2 ^ 35 bayt veya 32 GB'dir. Büyük olasılıkla bundan daha az olacaktır, çünkü 0 olan girişler için anahtarı depolamanız veya saymanız gerekmez (örneğin, python'daki bir varsayılan ifade gibi). Bu, her yeni tamsayıyı eklemek için sabit bir zaman alır.

Daha sonra, herhangi bir noktada, medyanı bulmak için, hangi tamsayının orta eleman olduğunu belirlemek için sayıları kullanın. Bu sabit zaman alır (büyük bir sabit olsa da, ancak yine de sabit).


3
Neredeyse tüm sayılar bir kez görülürse, seyrek bir listeden daha fazla bellek gerekir. Ve eğer çok sayıda numaranız varsa, sayıya uymuyorlarsa, sayıların çoğunun bir kez görüneceği muhtemel görünüyor. Buna rağmen, bu büyük sayılar için akıllı bir çözümdür .
Mooing Ördek

1
Seyrek bir liste için, katılıyorum, bu bellek açısından daha kötü. Tamsayılar rastgele dağıtılırsa, sezginin ima ettiğinden çok daha erken kopyalar almaya başlayacaksınız. Bkz. Mathworld.wolfram.com/BirthdayProblem.html . Bu yüzden, birkaç GB'lık veriye sahip olduğunuzda bunun etkili olacağından eminim.
Andrew C

4
@ AndrewC pls medyan bulmak için nasıl sabit zaman alacağını pls açıklayabilir. Eğer n farklı türde tamsayılar gördüysem en kötü durumda son eleman medyan olabilir. Bu medyan bulma O (n) aktivitesini yapar.
shshnk

@shshnk Bu durumda >>> 2 ^ 35 olan toplam öğe sayısı değil mi?
VishAmdi

@shshnk VishAmdi'nin dediği gibi, gördüğünüz farklı tamsayıların sayısının hala doğrusal olduğunu doğru söylüyorsunuz, bu çözüm için yaptığım varsayım, n'nin gördüğünüz sayı sayısı, yani 2 ^ 33'den daha büyük. Bu kadar çok sayı görmüyorsanız, maxheap çözümü kesinlikle daha iyidir.
Andrew C

49

Girdinin varyansı istatistiksel olarak dağıtılmışsa (örneğin normal, log-normal, vb.), O zaman rezervuar örneklemesi, keyfi olarak uzun bir sayı akışından yüzdelikleri / medyanları tahmin etmenin makul bir yoludur.

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;
      }
  }
}

"rezervuar" o zaman boyutuna bakılmaksızın tüm girdilerin çalışan, tekdüze (adil) bir örneğidir. Ortanca (veya herhangi bir yüzdelik dilim) bulmak, rezervuarı sıralamak ve ilginç noktayı sorgulamak için basit bir konudur.

Rezervuar sabit büyüklükte olduğundan, sıralama etkili bir şekilde O (1) olarak kabul edilebilir - ve bu yöntem hem sabit zaman hem de bellek tüketimi ile çalışır.


meraktan, neden varyansa ihtiyacınız var?
LazyCat

Akış, rezervuarın yarısını boş bırakarak SIZE öğeden daha az dönebilir. Medyan hesaplanırken bu dikkate alınmalıdır.
Alex

Medyan yerine farkı hesaplayarak bunu daha hızlı yapmanın bir yolu var mı? Çıkarılan ve eklenen örnek ve önceki medyan bunun için yeterli bilgi mi?
inf3rno

30

Bulduğum bir akışın yüzdelik oranını hesaplamanın en etkili yolu algoritmasıdır: Raj Jain, Imrich Chlamtac: Gözlemleri Saklamaksızın Miktarların ve Histogramların Dinamik Hesaplanması için P² Algoritması. Commun. ACM 28 (10): 1076-1085 (1985)

Algoritmanın uygulanması kolaydır ve son derece iyi çalışır. Ancak bu bir tahmindir, bu yüzden bunu aklınızda bulundurun. Özetden:

Medyan ve diğer niceliklerin dinamik hesaplanması için sezgisel algoritma önerilir. Gözlemler üretilirken tahminler dinamik olarak üretilir. Gözlemler saklanmaz; bu nedenle algoritmanın, gözlem sayısına bakılmaksızın çok küçük ve sabit bir depolama gereksinimi vardır. Bu, endüstriyel kontrolörlerde ve kayıt cihazlarında kullanılabilen bir kantil çipte uygulama için idealdir. Algoritma ayrıca histogram çizilmesine genişletilir. Algoritmanın doğruluğu analiz edilir.


2
Count-Min Sketch , P ^ 2'den daha iyidir, çünkü ikincisi aynı zamanda hataya bağlı değildir.
sinoTrinity

1
Ayrıca, hata sınırları veren ve iyi bellek gereksinimlerine sahip olan Greenwald ve Khanna tarafından "Uzay-Verimli Çevrimiçi Kantil Özetleri Hesaplaması" nı düşünün.
Paul Chernoch

1
Ayrıca, olasılıklı bir yaklaşım için şu blog yayınına bakın: araştırma.neustar.biz/ 2013/09/16/ … ve atıfta bulunulan makale burada: arxiv.org/pdf/1407.1121v1.pdf Buna "Tutumlu" denir Akış "
Paul Chernoch

27

Biz ortancasını bulmak istiyorsanız n en son görülen elemanlar, bu sorun yalnızca ihtiyacı olduğunu kesin bir çözümü vardır en son görülen n elementin hafızada tutulması . Hızlıdır ve iyi ölçeklendirilir.

Bir çift taraflı skiplist destekler O (ln) ekleme, çıkarma ve rasgele elemanların endeksli ara sıralanmış sırası tutulur. En eski n'inci girdiyi izleyen bir FIFO kuyruğuyla birleştiğinde çözüm basittir:

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]

İşte tam çalışma koduna bağlantılar (anlaşılması kolay bir sınıf sürümü ve dizine eklenebilir atlama listesi kodu ile optimize edilmiş bir jeneratör sürümü):


7
Eğer doğru bir şekilde anlıyorsam, bu size sadece son görülen N elementin medyanını verir, o ana kadar olan tüm elementleri değil. Bu, bu operasyon için gerçekten kaygan bir çözüm gibi görünüyor.
Andrew C

16
Sağ. Cevap, sadece son n öğeyi hafızada tutarak tüm öğelerin medyanını bulmak mümkün gibi geliyor - bu genel olarak imkansız. Algoritma sadece son n öğenin medyanını bulur.
Hans-Peter Störr

8
"Çalışan medyan" terimi tipik olarak bir veri alt kümesinin medyanını ifade etmek için kullanılır . OP standart olmayan bir şekilde ortak bir terim kullanılır.
Rachel Hettinger

18

Bunu düşünmenin sezgisel bir yolu, tam dengeli bir ikili arama ağacınız olsaydı, o zaman kök medyan element olurdu, çünkü aynı sayıda daha küçük ve daha büyük element olurdu. Şimdi, ağaç dolu değilse, durum son derece eksik olan unsurlar olacağından durum böyle olmayacaktır.

Bunun yerine yapabileceğimiz, medyan ve biri dengeli medyandan daha küçük elemanlar için, diğeri medyandan daha büyük elementler için olmak üzere iki dengeli ikili ağaçtır. İki ağaç aynı büyüklükte tutulmalıdır.

Veri akışından yeni bir tamsayı elde ettiğimizde, bunu medyanla karşılaştırırız. Medyandan büyükse, doğru ağaca ekleriz. İki ağaç boyutu 1'den farklıysa, sağ ağacın min öğesini kaldırır, yeni medyan yaparız ve eski medyanı sol ağaca koyarız. Benzer şekilde daha küçükler için.


Bunu nasıl yapacaksın? "sağ ağacın min elementini kaldırıyoruz"
Hengameh

2
İkili arama ağaçları demek istedim, bu yüzden min öğesi kökten geriye doğru kaldı.
Irene Papakonstantinou

7

Verimli, bağlama bağlı bir kelimedir. Bu sorunun çözümü, ekleme miktarına göre gerçekleştirilen sorguların miktarına bağlıdır. Ortanca ilgilendiğiniz noktaya N sayı ve K çarpı eklediğinizi varsayalım. Yığın tabanlı algoritmanın karmaşıklığı O (N log N + K) olacaktır.

Aşağıdaki alternatifi düşünün. Bir dizideki sayıları daraltın ve her sorgu için doğrusal seçim algoritmasını çalıştırın (quicksort pivot'unu kullanarak). Şimdi çalışma süresi O (KN) olan bir algoritmaya sahipsiniz.

Şimdi K yeterince küçükse (nadiren sorgular), ikinci algoritma aslında daha verimlidir ve tersi de geçerlidir.


1
Öbek örneğinde, arama sabit bir süredir, bu yüzden O (N log N + K) olması gerektiğini düşünüyorum, ancak noktanız hala duruyor.
Andrew C

Evet, iyi bir nokta, bunu düzenleyecek. Haklısın N log N hala önde gelen terim.
Peteris

-2

Bunu tek bir yığınla yapamaz mısın? Güncelleme: hayır. Yoruma bakın.

Değişmez: 2*nGirdileri okuduktan sonra , min-yığın nbunların en büyüğünü tutar .

Döngü: 2 girişi okuyun. Her ikisini de öbeğe ekleyin ve yığının min. Bu, değişmezi yeniden kurar.

Bu yüzden 2ngirdiler okunduğunda, yığının min'inci en büyük değerdir. Ortanca konumun etrafındaki iki öğeyi ortalamak ve tek bir girdiden sonra sorguları işlemek için biraz fazladan karmaşıklığa ihtiyaç olacaktır.


1
Çalışmıyor: Daha sonra zirveye yakın olduğu ortaya çıkan şeyleri bırakabilirsiniz. Örneğin, algoritmanızı 1'den 100'e kadar sayılarla, ancak ters sırayla deneyin: 100, 99, ..., 1.
zellyn

Teşekkürler zellyn. Aptal kendimi kendimi ikna etmem için yeniden kuruldu.
Darius Bacon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.