Penyortiran tumpukan lemah


Dari seluruh tumpukan kebun binatang, struktur ini mungkin yang paling tidak biasa. Pada saat yang sama, kesederhanaan yang elegan dari algoritma ini sangat cocok dengan eksentrisitasnya yang menakjubkan.

Saat menyortir menggunakan tumpukan lemah, selalu ada lebih sedikit perbandingan dan pertukaran daripada menggunakan tumpukan biasa. Jadi ya, tumpukan yang lemah lebih kuat dari tumpukan yang biasa.
Perangkat Lunak EDISON - pengembangan web
Artikel ini ditulis dengan dukungan EDISON.

Kami terlibat dalam pembuatan perangkat lunak tertanam serta pengembangan aplikasi dan situs web .

Kami menyukai teori algoritma! ;-)

Tumpukan lemah


Tumpukan biasa adalah pohon penyortiran di mana setiap orang tua lebih besar (atau sama) dari keturunannya. Dalam tumpukan lemah, persyaratan ini melemah - setiap orang tua lebih besar dari (atau sama dengan) keturunan hanya dari subtree kanannya. Di subtree kiri, keturunan bisa lebih kecil dan lebih besar dari induknya, di sana Anda sangat beruntung.


Pendekatan ini secara signifikan dapat mengurangi biaya pemeliharaan set data dalam kondisi heap. Lagi pula, perlu untuk memastikan prinsip "keturunan tidak lebih dari orangtua" bukan untuk seluruh struktur, tetapi hanya untuk setengahnya. Pada saat yang sama, tumpukan yang lemah, tidak menjadi pohon penyortiran 100%, tidak lebih buruk dari tumpukan biasa, dan dalam beberapa hal bahkan lebih baik. Apakah setengah pertempuran - berjalan dengan berani!

Karena akar tumpukan, bahkan yang lemah, membutuhkan persis elemen terbesar, akar subtree kiri tidak.

Meminimalkan jumlah perbandingan



Ronald D. Dutton, seorang spesialis dalam algoritma dan teori grafik, memberi kami sekelompok lemah pada tahun 1993. Tumpukan yang secara konsep lemah lebih sulit untuk dipahami (tetapi kesulitan ini lebih cenderung tidak dalam kompleksitas, tetapi dalam pemborosan, Anda harus mematahkan pola kesadaran Anda melalui lutut) daripada tumpukan biasa, sehingga belum menerima banyak distribusi praktis. Namun, ketika Dutton menemukan struktur ini, ia tidak hanya ingin mempraktikkan abstraksi abstrak, tetapi ia juga mengejar tujuan yang sepenuhnya pragmatis.

Ada batas bawah teoritis untuk memperkirakan jumlah minimum perbandingan (dalam penyortiran di mana perbandingan ini digunakan secara luas):

log n ! = n log n - n / ln 2 + O (log n), di mana 1 / ln 2 = 1,4426

Dalam menyortir dengan tumpukan lemah, jumlah perbandingan diminimalkan dan cukup dekat dengan batas bawah.

Ini bisa sangat penting secara praktis jika Anda perlu mengatur objek yang perbandingannya mahal, misalnya, jika perlu menyortir string panjang.

Keturunan juggling


"Kiri" dan "kanan" di tumpukan lemah adalah fenomena situasional. Subtree dapat berupa turunan kiri atau kanan untuk simpul induknya - apalagi, relasi “kiri / kanan” ini untuk subtree dan induk dapat berulang kali beralih dari satu nilai ke nilai yang berlawanan selama proses.

Untuk menunjukkan bagi orang tua yang memiliki putra kanannya dan siapa putri kirinya tidak sederhana, tetapi sangat sederhana. Untuk melakukan ini, Anda memerlukan bitmap tambahan (hanya terdiri dari nilai 0/1) untuk node yang memiliki turunan.

Ingat bagaimana indeks elemen induk ke- i , kami mendefinisikan indeks turunan kiri dan kanannya dalam tumpukan konvensional (indeks array diukur dari nol):

Turunan kiri 2 × i + 1
Turunan kanan 2 × i + 2

Dalam tumpukan yang lemah, kami memiliki ceri pada kue - root yang hanya memiliki subtree yang tepat, jadi kami akan menyesuaikan formula ini untuk indeks turunan dengan menambahkan pergeseran mundur ke 1 posisi:

Turunan kiri: 2 × i
Turunan kanan: 2 × i + 1

Dan akhirnya , diperlukan bitmap tambahan (sebut saja bIT ), dimana untuk elemen ke- i yang dicatat adalah apakah pertukaran tempat antara subtrees kiri dan kanannya. Jika nilai elemen adalah 0, maka tidak ada pertukaran. Jika nilainya 1, maka anak-anak kiri dan kanan pergi dalam urutan yang berlawanan. Dan rumus dalam hal ini adalah sebagai berikut:

Keturunan kiri: 2 × i + BIT [ i ]
Keturunan kanan: 2 × i + 1 - BIT [ i ]

Ini adalah tampilannya. Elemen yang turunannya terletak "sebaliknya" disorot dengan warna biru. Nilai dalam array BIT untuk mereka adalah 1.


Anda dapat memeriksa dengan mengganti nilai-nilai induk i dan 0/1 yang sesuai dari array BIT dalam rumus turunan - indeks turunan akan berubah sesuai kebutuhan.

Seperti yang Anda lihat, untuk orangtua mana saja untuk menukar subtrees kiri dan kanan, dalam array itu sendiri, kelompok elemen tidak perlu dipindahkan ke mana pun. Hanya nilai 0/1 untuk induk dalam larik BIT yang diaktifkan dan hanya itu.

Berikutnya - sesi sihir dengan paparan berikutnya.

Bangunlah tumpukan yang lemah


Menukar keturunan kiri dan kanan adalah alat utama untuk mengkonversi menjadi tumpukan data yang lemah dari sebuah array.

Dalam proses pembentukan heap utama lemah, kita perlu memilah-milah elemen array dalam urutan terbalik (mulai dari yang terakhir) dan untuk masing-masing menemukan cabang dari induk pertama (kanan), yang akan menjadi subtree yang tepat.

Jika elemennya adalah keturunan kanan seseorang , maka Anda tidak perlu pergi jauh. Induk langsungnya adalah yang Anda butuhkan:


Jika elemen tersebut adalah keturunan kiri seseorang , maka Anda harus naik sejumlah tingkat sebelum Anda bertemu dengan kakek-nenek yang diinginkan, yang elemennya ada di subtree kanan:



Maka Anda perlu membandingkan keturunan dan nenek moyang yang ditemukan di suatu tempat di atas. Dan jika keturunannya lebih besar dari leluhur, maka yang berikut harus dilakukan:

  1. Jika keturunan memiliki keturunannya, maka tukar subpohon kiri dan kanannya (mis. Ubah 0/1 dalam array BIT untuk elemen ini).
  2. Tukarkan nilai-nilai dari node turunan dan progenitor.

Lihatlah contoh spesifik. Katakanlah situasi berikut muncul:


Untuk elemen array A [6] = 87, leluhur yang diperlukan A [1] = 76 ditemukan.
Progenitor A [1] lebih kecil dari elemen A [6] (76 <87).
Elemen A [6] memiliki subtrees kiri dan kanan (ditandai dengan nuansa hijau).
Anda perlu menukar subpohon ini
(yaitu, untuk elemen A [6] dalam array BIT , ubah nilainya dari 0 ke 1).
Juga perlu untuk menukar nilai elemen A [6] dan A [1].


Setelah tindakan yang diperlukan selesai:


Untuk elemen A [6], subtrees kiri dan kanan dipertukarkan
(yaitu, dalam array BIT untuk elemen A [6], nilai dari 0 diubah menjadi 1).
Ada juga pertukaran nilai antara A [6] dan A [1].


Jika Anda pergi melalui array dari ujung ke awal dan sepanjang jalan melakukan prosedur ini untuk semua elemen, Anda akan berakhir dengan tumpukan lemah.

Mengapa mekanisme aneh ini bekerja adalah penjelasan yang lebih dekat ke akhir artikel.

Parsing tumpukan lemah


Tumpukan adalah tumpukan jika elemen maksimum di root. Menggunakan fakta ini, semua penyortiran tumpukan bekerja dengan cara yang sama. Root (tempat maksimum terletak) bertukar nilai dengan elemen terakhir dari bagian array yang tidak disortir - sebagai akibatnya, bagian array yang tidak disortir berkurang, dan bagian array yang disortir meningkat. Setelah pertukaran ini, heap tidak lagi menjadi heap, karena elemen maksimum saat ini tidak lagi di root. Tumpukan perlu dipulihkan, yaitu, membuat pohon yang dihasilkan tumpukan lagi - menemukan elemen maksimum lain dan memindahkannya ke root.

Bagaimana memulihkan tumpukan biner biasa, kita tahu - dengan bantuan ayakan . Tetapi bagaimana cara mengembalikan heap yang lemah? Untuk melakukan ini, lakukan hal berikut.

Dari akar kita turun turun keturunan kiri (sampai yang terendah):


Kemudian kita naik kembali keturunan kiri dan sepanjang jalan masing-masing keturunan kiri dibandingkan dengan elemen di root tumpukan. Dan jika keturunan kiri berikutnya lebih besar dari root, maka kita melakukan hal yang sama seperti pada tahap sebelumnya: pada keturunan kiri kita menukar subtree (jika ada) dan mengubah nilai-nilai keturunan kiri dan root.

Sebagai hasilnya, kami akan mengembalikan tumpukan lemah - elemen maksimum yang ada di pohon yang tersisa akan muncul di akarnya.

Dan lagi, kita memiliki korsel mistis ini dengan subtree yang menggantikan satu sama lain. Jadi apa rahasia kesuksesan? Mengapa, jika selama pertukaran node dengan nilai, keturunan kiri dan kanan dari node yang lebih rendah dipertukarkan, apakah kita berakhir dengan array yang diurutkan? Anda tidak akan pernah menebak, meskipun jawabannya sederhana dalam kejeniusannya.

Sortir heap lemah :: Sortir heap lemah


Jadi, algoritma terakhir:

  • I. :
    • I.1. -.
    • I.2. «» .
    • I.3. .
    • I.4. , :
      • I.4.. ( ⇔ ) , .
      • I.4.. «» .
  • II. , :
    • II.1. .
    • II.2. . .
    • II.3. , . :
      • II.3.. .
      • II.3.. , .
      • II.3.. , , :
        • II.3.c. 1. Kami bertukar (kiri ⇔ kanan) sub pohon dengan keturunan untuk simpul di mana keturunan kiri saat ini berada.
        • II.3.c.2. Kami mengubah root tumpukan dan simpul dengan anak kiri saat ini.
    • II.4. Pada akar tumpukan lemah sekali lagi adalah elemen maksimum untuk bagian array yang belum disortir yang tersisa. Kami kembali ke paragraf II.1 dan ulangi proses sampai semua elemen diurutkan.


Animasi (indeks larik di animasi saya mulai dengan satu):



Kode C ++


Di bagian bawah bagian "Tautan", mereka yang tertarik dapat membiasakan diri dengan implementasi penyortiran ini dalam C ++. Di sini saya hanya memberikan bagian yang menggambarkan algoritma itu sendiri.

#define GETFLAG(r, x) ((r[(x) >> 3] >> ((x) & 7)) & 1)
#define TOGGLEFLAG(r, x) (r[(x) >> 3] ^= 1 << ((x) & 7))

void WeakHeap::WeakHeapMerge(unsigned char *r, int i, int j) {
  if (wheap[i] < wheap[j]) {//""  ?
    //  ,   
    //( "",   "")
    TOGGLEFLAG(r, j);
    //  ""  
    swap(wheap[i], wheap[j]);
  }
}

void WeakHeap::WeakHeapSort() {
  int n = Size();
  if(n > 1) {
		
    int i, j, x, y, Gparent;
    int s = (n + 7) / 8;
    unsigned char * r = new unsigned char [s];
		
    //  ,    
    // "",   ""
    for(i = 0; i < n / 8; ++i) r[i] = 0;
		
    //   
    for(i = n - 1; i > 0; --i) {
      j = i;
      //    , 
      //   ""  
      while ((j & 1) == GETFLAG(r, j >> 1)) j >>= 1;
      //       ""  
      Gparent = j >> 1;
      //  ,   
      //   ""
      WeakHeapMerge(r, Gparent, i);
    }
		
    //      -->
    //  -->    
    for(i = n - 1; i >= 2; --i) {
      //      
      //       
      swap(wheap[0], wheap[i]);
      x = 1;
      //    "" 
      while((y = 2 * x + GETFLAG(r, x)) < i) x = y;
      //  ""     
      //        
      while(x > 0) {
        WeakHeapMerge(r, 0, x);
        x >>= 1;
      }
    }
    //  -   
    //    
    swap(wheap[0], wheap[1]);
    delete[] r;
  }
}

Saya terutama suka bagaimana pohon biner dilintasi dengan mudah dan alami menggunakan operasi bitwise.

Kompleksitas memori ekstra


Sepertinya O ( n ) - array tambahan diperlukan, di mana untuk node dengan turunan (ada sekitar setengah dari yang ada dalam array), urutan subtree kiri / kanan ditetapkan.

Namun, ada pendapat bahwa kompleksitas penyortiran di sini sebenarnya O (1)! Untuk sebuah elemen, kita hanya perlu satu bit tambahan (nol / satu) untuk menunjukkan urutan keturunan. Jika kita mengurutkan, misalnya, string, maka cukup layak untuk menambahkan bit tambahan ini ke elemen itu sendiri.

Cara lain untuk mengubah O ( n ) menjadi O (1) adalah dengan menyimpan bendera dalam angka bulat. Ekspansi biner angka - seperangkat nol dan yang bertanggung jawab atas urutan subpohon dari semua elemen array. The i- elemen th berkorespondensi array ke i- bit th dari nomor tersebut.

Kompleksitas waktu


Menurut waktu, O ( n log n ) sama dengan heap biasa. Saat menyortir baris (terutama yang panjang), tumpukan yang lemah bisa lebih cepat dari tumpukan biasa. Tapi ini kalau kita urutkan garis panjang. Jika kita mengurutkan angka, maka, menurut rumor, tumpukan biasa lebih cepat dikelola.

Penyaringan penuh


Pada tahap pembentukan tumpukan lemah awal, dengan analogi dengan tumpukan biasa, ide yang cukup jelas menyarankan peningkatan elemen besar setinggi mungkin. Artinya, jika kita bertukar nilai-nilai simpul bawah dan leluhurnya, maka akan logis untuk segera mengulangi langkah-langkah untuk leluhur - untuk menemukan leluhur kanan terdekatnya untuk dia dan membandingkan (dan jika itu juga diperlukan untuk bertukar nilai + pertukaran sub-pohon). Dan, jika mungkin, naikkan elemen besar ke root. Ini adalah tampilannya pada tahap pertama (tindakan pada tahap kedua algoritma tidak berubah):


Skor kompleksitas waktu tetap sama.

Tumpukan binomial


Semua yang sampai pada titik ini hanyalah tipuan, ilusi. Tentu saja, kami secara formal melakukan beberapa manipulasi dengan pohon biner di sana, mengubah node dengan nilai, mengatur ulang subtree dan semua itu. Namun, algoritme memiliki dasar ganda, yang sekarang akan kita lihat.

Untuk memahami jenis ini, Anda harus memahami apa sebenarnya tumpukan lemah itu.

Jika kita mengambil sebuah array di mana jumlah elemen adalah kekuatan dua, maka heap yang lemah dan heap binomial yang dibangun atas dasar isomorfis.


Jangan melihat fakta bahwa tumpukan lemah adalah biner, dan binomial tidak. Dalam tumpukan lemah, subtree kiri dan kanan pada dasarnya berbeda. Subtree kanan adalah keturunan dalam arti klasik, tetapi subtree kiri agak "saudara". Meski tidak. Subtree kiri bahkan bukan "saudara", tetapi vektor "saudara" dengan simpul yang lebih sedikit.

Namun, tumpukan lemah dan tumpukan binomial tidak 100% sama, meskipun mereka adalah kerabat terdekat. Perbedaannya jelas, jika Anda mengambil array yang jumlah elemennya tidak sama dengan 2 n . Dekomposisi binomial dari array semacam itu akan memberikan daftar terhubung dari beberapa tumpukan ideal (jumlah node di masing-masing adalah kekuatan tertentu dari dua):


Tumpukan yang lemah dalam hal ini akan menjadi satu pohon biner yang tidak sempurna:



Tumpukan binomial dan tumpukan lemah adalah saudara kembar. DNA-nya sama, meskipun secara penampilan Anda tidak bisa mengatakannya.

Algoritma rahasia


Mengingat bahwa tumpukan lemah adalah tumpukan cryptobinomial, menyeret sub pohon tiba-tiba menemukan penjelasan sederhana.


Dengan heap yang lemah, sapu perada pseudobinary dan lihat hubungan nyata antara node dalam gaya heap binomial. Segalanya menjadi jelas.

Faktanya:

  1. Tidak ada "pelemahan", itu adalah penyortiran penuh (non-binary) pohon di mana prinsip "setiap orang tua lebih besar daripada keturunan" dicapai dan dipelihara.
  2. Pada semua tahap, kita membandingkan keturunan bukan dengan kakek-nenek mereka, tetapi dengan orang tua langsung mereka.
  3. Apa yang tampak seperti pertukaran nilai antara keturunan dan leluhur + pertukaran tempat subtree dalam keturunan - ternyata merupakan pertukaran rasio itu sendiri (keturunan / orang tua). Jika node induk lebih kecil dari turunan berdasarkan nilai, maka induk itu sendiri menjadi turunan, dan turunan menjadi induk.

Berikut ini visualisasi yang jujur:



Di seri selanjutnya


Tumpukan berikutnya yang ingin saya bicarakan adalah favorit saya - pohon Cartesian. Ini bukan hanya banyak, tetapi juga pohon pencarian biner . Tapi kemudian pertama, di artikel berikutnya, sesuatu yang menarik perlu diklarifikasi tentang pohon BST. Dan hanya kemudian, melalui artikel, dan tentang pembicaraan Cartesian.

Referensi


Weak Heap , Binomial Heap / Binomial

Heap, C ++ Implementasi Weap Heap,

Ronald D. Dutton: Halaman Pribadi , Profil Situs Web UCF , Weap Heap

and Friends: Perkembangan Terbaru

Struktur Data Weak-Heap: Varian dan Aplikasi

pada Kinerja WEAK-HEAPSORT

Adaptif heapsort: Kode sumber

Sergey Kopeliovich - Lecture Hall - Weak pile (dari 48:32 hingga 1:16:06)

Artikel Seri:




Penyortiran hari ini ditambahkan ke aplikasi AlgoLab oleh sekelompok orang yang lemah, yang menggunakannya - perbarui file excel dengan makro.

Di komentar ke sel dengan nama pengurutan, Anda dapat menentukan beberapa pengaturan. Jika Anda mengatur siftup = 1, maka penyortiran akan menggunakan penyaringan penuh di tahap pertama (secara default siftup = 0).

Jika Anda meresepkan binomial = 1, maka pohon itu akan menjadi la "binomial heap" (secara default binomial = 0, artinya, hanya heap yang lemah).

All Articles