Temukan median yang berjalan dari aliran bilangan bulat


223

Kemungkinan Duplikat:
Algoritma median bergulir dalam C

Mengingat bahwa bilangan bulat dibaca dari aliran data. Temukan median elemen yang dibaca sejauh ini dengan cara yang efisien.

Solusi yang saya baca: Kita dapat menggunakan tumpukan maksimum di sisi kiri untuk mewakili elemen yang kurang dari median efektif, dan tumpukan minimum di sisi kanan untuk mewakili elemen yang lebih besar dari median efektif.

Setelah memproses elemen yang masuk, jumlah elemen dalam tumpukan berbeda paling banyak dengan 1 elemen. Ketika kedua tumpukan berisi jumlah elemen yang sama, kami menemukan rata-rata data root tumpukan sebagai median efektif. Ketika tumpukan tidak seimbang, kami memilih median efektif dari akar tumpukan yang mengandung lebih banyak elemen.

Tetapi bagaimana kita membangun tumpukan maksimum dan tumpukan minimum yaitu bagaimana kita tahu median efektif di sini? Saya berpikir bahwa kita akan memasukkan 1 elemen di max-heap dan kemudian 1 elemen berikutnya di min-heap, dan seterusnya untuk semua elemen. Koreksi saya Jika saya salah di sini.


10
Algoritma pintar, menggunakan tumpukan. Dari judulnya saya tidak bisa langsung memikirkan solusi.
Mooing Duck

1
solusi wazir terlihat bagus bagi saya, kecuali bahwa saya berasumsi (meskipun Anda tidak menyatakan) bahwa aliran ini bisa lama, jadi Anda tidak bisa menyimpan semuanya dalam memori. Apakah itu masalahnya?
Berlari Liar

2
@RunningWild Untuk aliran panjang yang sewenang-wenang, Anda bisa mendapatkan median elemen N terakhir dengan menggunakan tumpukan Fibonacci (sehingga Anda mendapatkan log (N) dihapus) dan menyimpan pointer ke elemen yang dimasukkan secara berurutan (dalam misalnya deque), lalu menghapus yang tertua elemen pada setiap langkah setelah tumpukan penuh (mungkin juga memindahkan sesuatu dari satu tumpukan ke yang lain). Anda bisa mendapatkan sedikit lebih baik daripada N dengan menyimpan jumlah elemen berulang (jika ada banyak pengulangan), tetapi secara umum, saya pikir Anda harus membuat semacam asumsi distribusi jika Anda ingin median seluruh aliran.
Dougal

2
Anda bisa mulai dengan kedua tumpukan kosong. Int pertama berjalan dalam satu tumpukan; kedua berjalan di yang lain, atau Anda memindahkan item pertama ke tumpukan lain dan kemudian masukkan. Ini menggeneralisasi ke "jangan biarkan satu tumpukan pergi lebih besar dari yang lain +1" dan tidak ada casing khusus yang diperlukan ("nilai root" dari tumpukan kosong dapat didefinisikan sebagai 0)
Jon Watte

Saya HANYA mendapat pertanyaan ini pada wawancara MSFT. Terima kasih telah mengirim
R Claven

Jawaban:


383

Ada sejumlah solusi berbeda untuk menemukan median berjalan dari data yang dialirkan, saya akan membicarakannya secara singkat di akhir jawaban.

Pertanyaannya adalah tentang perincian dari solusi spesifik (max heap / min heap solution), dan bagaimana solusi berbasis heap bekerja dijelaskan di bawah ini:

Untuk dua elemen pertama tambahkan yang lebih kecil ke maxHeap di sebelah kiri, dan yang lebih besar ke minHeap di sebelah kanan. Kemudian proses aliran data satu per satu,

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

Kemudian pada waktu tertentu Anda dapat menghitung median seperti ini:

   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

Sekarang saya akan berbicara tentang masalah secara umum seperti yang dijanjikan di awal jawaban. Menemukan menjalankan median dari aliran data adalah masalah yang sulit, dan menemukan solusi yang tepat dengan kendala memori secara efisien mungkin tidak mungkin untuk kasus umum. Di sisi lain, jika data memiliki beberapa karakteristik yang dapat kita eksploitasi, kita dapat mengembangkan solusi khusus yang efisien. Misalnya, jika kita tahu bahwa data adalah tipe integral, maka kita dapat menggunakan penghitungan, Yang dapat memberi Anda algoritma waktu konstan memori konstan. Solusi berbasis heap adalah solusi yang lebih umum karena dapat digunakan untuk tipe data lain (ganda) juga. Dan akhirnya, jika median yang tepat tidak diperlukan dan perkiraan sudah cukup, Anda bisa mencoba memperkirakan fungsi kepadatan probabilitas untuk data dan memperkirakan median yang menggunakannya.


6
Tumpukan ini tumbuh tanpa batas (yaitu jendela elemen 100 yang meluncur lebih dari 10 juta elemen akan membutuhkan 10 juta elemen untuk semua disimpan dalam memori). Lihat di bawah untuk jawaban lain menggunakan daftar cepat yang dapat diindeks yang hanya membutuhkan 100 elemen yang terakhir dilihat disimpan dalam memori.
Raymond Hettinger

1
Anda dapat memiliki solusi memori terbatas menggunakan tumpukan juga, seperti yang dijelaskan dalam salah satu komentar untuk pertanyaan itu sendiri.
Hakan Serce

1
Anda dapat menemukan implementasi solusi berbasis heap di c di sini.
AShelly

1
Wow ini membantu saya tidak hanya menyelesaikan masalah khusus ini tetapi juga membantu saya belajar tumpukan di sini adalah implementasi dasar saya di python: github.com/PythonAlgo/DataStruct
swati saoji

2
@HakanSerce Bisakah Anda jelaskan mengapa kami melakukan apa yang kami lakukan? Maksud saya, saya bisa melihat ini berhasil, tetapi saya tidak dapat memahaminya secara intuitif.
Siwa

51

Jika Anda tidak dapat menyimpan semua item dalam memori sekaligus, masalah ini menjadi jauh lebih sulit. Solusi tumpukan meminta Anda untuk menahan semua elemen dalam memori sekaligus. Ini tidak mungkin di sebagian besar aplikasi dunia nyata dari masalah ini.

Alih-alih, saat Anda melihat angka, catat hitungan berapa kali Anda melihat setiap bilangan bulat. Dengan asumsi bilangan bulat 4 byte, itu 2 ^ 32 ember, atau paling banyak 2 ^ 33 bilangan bulat (kunci dan hitung untuk setiap int), yaitu 2 ^ 35 byte atau 32GB. Kemungkinan akan jauh lebih sedikit daripada ini karena Anda tidak perlu menyimpan kunci atau menghitung entri yang 0 (mis. Seperti defaultdict in python). Ini membutuhkan waktu yang konstan untuk memasukkan setiap integer baru.

Kemudian pada titik mana pun, untuk menemukan median, cukup gunakan hitungan untuk menentukan bilangan bulat mana yang merupakan elemen tengah. Ini membutuhkan waktu yang konstan (meskipun konstan besar, tetapi tetap konstan).


3
Jika hampir semua angka terlihat sekali, maka daftar yang jarang akan memakan lebih banyak memori. Dan agaknya jika Anda memiliki begitu banyak angka, maka angka tersebut tidak cocok sehingga sebagian besar angka akan muncul satu kali. Dispite itu, ini adalah solusi cerdas untuk besar jumlah angka.
Mooing Duck

1
Untuk daftar yang jarang, saya setuju, ini lebih buruk dalam hal memori. Meskipun jika bilangan bulat didistribusikan secara acak, Anda akan mulai mendapatkan duplikat jauh lebih cepat daripada intuisi. Lihat mathworld.wolfram.com/BirthdayProblem.html . Jadi saya cukup yakin ini akan menjadi efektif segera setelah Anda memiliki beberapa GB data.
Andrew C

4
@AndrewC dapatkah Anda menjelaskan bagaimana akan butuh waktu konstan untuk menemukan median. Jika saya telah melihat n jenis integer yang berbeda maka dalam kasus terburuk elemen terakhir mungkin adalah median. Ini membuat median menemukan O (n) aktivitas.
shshnk

@shshnk Bukankah n jumlah elemen yang >>> 2 ^ 35 dalam hal ini?
VishAmdi

@shshnk Anda benar bahwa itu masih linier dalam jumlah bilangan bulat berbeda yang pernah Anda lihat, seperti kata VishAmdi, asumsi yang saya buat untuk solusi ini adalah bahwa n adalah jumlah angka yang telah Anda lihat, yang jauh lebih banyak lebih besar dari 2 ^ 33. Jika Anda tidak melihat angka sebanyak itu, solusi maxheap pasti lebih baik.
Andrew C

49

Jika varian input terdistribusi secara statistik (mis. Normal, log-normal, dll.) Maka pengambilan sampel reservoir adalah cara yang masuk akal untuk memperkirakan persentil / median dari aliran angka yang sewenang-wenang.

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

"reservoir" kemudian merupakan sampel semua input yang berjalan, seragam (wajar) - berapapun ukurannya. Menemukan median (atau persentil apa pun) adalah hal yang mudah untuk menyortir reservoir dan mengumpulkan poin yang menarik.

Karena reservoir berukuran tetap, penyortiran dapat dianggap efektif O (1) - dan metode ini berjalan dengan konsumsi waktu dan memori yang konstan.


karena penasaran, mengapa Anda perlu varians?
LazyCat

Aliran mungkin mengembalikan kurang dari elemen UKURAN membiarkan reservoir setengah kosong. Ini harus dipertimbangkan ketika menghitung median.
Alex

Apakah ada cara untuk membuat ini lebih cepat dengan menghitung selisih daripada median? Apakah sampel yang dihapus dan ditambahkan dan median informasi sebelumnya cukup untuk itu?
inf3rno

30

Cara paling efisien untuk menghitung persentil aliran yang saya temukan adalah algoritma P²: Raj Jain, Imrich Chlamtac: Algoritma P² untuk Perhitungan Dinamis Kuantiil dan Histogram Tanpa Menyimpan Pengamatan. Komunal. ACM 28 (10): 1076-1085 (1985)

Algoritma ini lurus ke depan untuk mengimplementasikan dan bekerja dengan sangat baik. Namun, ini merupakan perkiraan, jadi ingatlah itu. Dari abstrak:

Algoritma heuristik diusulkan untuk perhitungan dinamis jika median dan kuantil lainnya. Estimasi tersebut dihasilkan secara dinamis saat pengamatan dihasilkan. Pengamatan tidak disimpan; oleh karena itu, algoritma ini memiliki persyaratan penyimpanan yang sangat kecil dan tetap terlepas dari jumlah pengamatan. Ini membuatnya ideal untuk diimplementasikan dalam chip kuantil yang dapat digunakan dalam pengontrol dan perekam industri. Algoritma selanjutnya diperluas ke plot histogram. Keakuratan algoritma dianalisis.


2
Sketsa Count-Min lebih baik daripada P ^ 2 karena Sketch -nya juga memberikan batas kesalahan sedangkan yang kedua tidak.
sinoTrinity

1
Juga pertimbangkan "Komputasi Online Penjumlahan Kuantil yang Efisien-Ruang" oleh Greenwald dan Khanna, yang juga memberikan batasan kesalahan dan memiliki persyaratan memori yang baik.
Paul Chernoch

1
Juga, untuk pendekatan probabilistik, lihat posting blog ini: research.neustar.biz/2013/09/16/… dan makalah yang dimaksud di sini: arxiv.org/pdf/1407.1121v1.pdf Ini disebut "Frugal Streaming "
Paul Chernoch

27

Jika kita ingin menemukan median dari n elemen yang paling baru dilihat, masalah ini memiliki solusi tepat yang hanya membutuhkan n elemen yang paling baru dilihat untuk disimpan dalam memori. Ini cepat dan bersisik dengan baik.

Daftar lompatan yang dapat diindeks mendukung O (ln n) penyisipan, penghapusan, dan pencarian yang diindeks dari elemen-elemen sembarang sambil mempertahankan urutan yang diurutkan. Ketika digabungkan dengan antrian FIFO yang melacak entri tertua ke-n, solusinya sederhana:

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]

Berikut ini tautan untuk menyelesaikan kode kerja (versi kelas yang mudah dipahami dan versi generator yang dioptimalkan dengan kode daftar lewati yang dapat diindekskan):


7
Jika saya memahaminya dengan benar, ini hanya memberi Anda median elemen N terakhir yang terlihat, tidak semua elemen hingga saat itu. Ini sepertinya solusi yang sangat apik untuk operasi itu.
Andrew C

16
Baik. Jawabannya terdengar seolah-olah itu mungkin untuk menemukan median dari semua elemen dengan hanya menyimpan n elemen terakhir dalam memori - itu tidak mungkin secara umum. Algoritma hanya menemukan median dari n elemen terakhir.
Hans-Peter Storr

8
Istilah "running median" biasanya digunakan untuk merujuk ke median subset data. OP digunakan istilah umum dengan cara yang tidak standar.
Rachel Hettinger

18

Cara intuitif untuk memikirkan hal ini adalah bahwa jika Anda memiliki pohon pencarian biner seimbang penuh, maka root akan menjadi elemen median, karena akan ada jumlah elemen yang lebih kecil dan lebih besar. Sekarang, jika pohon tidak penuh ini tidak akan menjadi masalah karena akan ada elemen yang hilang dari tingkat terakhir.

Jadi yang bisa kita lakukan adalah memiliki median, dan dua pohon biner seimbang, satu untuk elemen kurang dari median, dan satu untuk elemen lebih besar dari median. Kedua pohon harus disimpan pada ukuran yang sama.

Ketika kami mendapatkan integer baru dari aliran data, kami membandingkannya dengan median. Jika lebih besar dari median, kami menambahkannya ke pohon yang benar. Jika dua ukuran pohon berbeda lebih dari 1, kami menghapus elemen min dari pohon kanan, menjadikannya median baru, dan menempatkan median lama di pohon kiri. Begitu pula untuk yang lebih kecil.


Bagaimana kamu akan melakukan itu? "Kami menghapus elemen min dari pohon yang benar"
Hengameh

2
Maksud saya pohon pencarian biner, jadi elemen min adalah jauh dari akar.
Irene Papakonstantinou

7

Efisien adalah kata yang tergantung pada konteks. Solusi untuk masalah ini tergantung pada jumlah kueri yang dilakukan relatif terhadap jumlah penyisipan. Misalkan Anda memasukkan angka N dan waktu K menjelang akhir Anda tertarik pada median. Kompleksitas algoritma heap based adalah O (N log N + K).

Pertimbangkan alternatif berikut. Memotong angka-angka dalam array, dan untuk setiap permintaan, jalankan algoritma seleksi linear (menggunakan pivot quicksort, katakanlah). Sekarang Anda memiliki algoritma dengan running time O (KN).

Sekarang jika K cukup kecil (kueri yang jarang), algoritma yang terakhir sebenarnya lebih efisien dan sebaliknya.


1
Dalam contoh heap, pencarian adalah waktu yang konstan, jadi saya pikir itu harus O (N log N + K), tetapi poin Anda masih berlaku.
Andrew C

Ya, bagus, akan mengedit ini. Anda benar N log N masih merupakan istilah terkemuka.
Peteris

-2

Tidak bisakah kau melakukan ini hanya dengan satu tumpukan? Pembaruan: no. Lihat komentarnya.

Invarian: Setelah membaca 2*ninput, tumpukan min memegang yang nterbesar.

Loop: Baca 2 input. Tambahkan keduanya ke heap, dan hapus min heap. Ini membangun kembali invarian.

Jadi, ketika 2ninput telah dibaca, min heap adalah yang terbesar ke-n. Perlu ada sedikit komplikasi tambahan untuk rata-rata dua elemen di sekitar posisi median dan untuk menangani permintaan setelah jumlah input ganjil.


1
Tidak berfungsi: Anda dapat membuang benda yang nantinya ternyata berada di dekat bagian atas. Misalnya, coba algoritme Anda dengan angka 1 hingga 100, tetapi dalam urutan terbalik: 100, 99, ..., 1.
zellyn

Terima kasih, zellyn. Bodoh bagi saya untuk meyakinkan diri saya bahwa invarian itu dibangun kembali.
Darius Bacon
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.