Saya pikir ada beberapa pertanyaan yang terkubur dalam topik ini:
- Bagaimana Anda menerapkannya
buildHeap
sehingga berjalan dalam waktu O (n) ?
- Bagaimana Anda menunjukkan bahwa
buildHeap
berjalan dalam O (n) waktu ketika diimplementasikan dengan benar?
- Mengapa logika yang sama itu tidak berfungsi untuk membuat heap sort berjalan pada waktu O (n) daripada O (n log n) ?
Bagaimana Anda menerapkannya buildHeap
sehingga berjalan dalam waktu O (n) ?
Seringkali, jawaban atas pertanyaan-pertanyaan ini fokus pada perbedaan antara siftUp
dan siftDown
. Membuat pilihan yang tepat antara siftUp
dan siftDown
sangat penting untuk mendapatkan kinerja O (n)buildHeap
, tetapi tidak melakukan apa pun untuk membantu orang memahami perbedaan antara buildHeap
dan heapSort
secara umum. Memang, implementasi yang tepat dari keduanya buildHeap
dan hanyaheapSort
akan digunakan . The Operasi hanya dibutuhkan untuk melakukan sisipan ke dalam tumpukan yang ada, sehingga akan digunakan untuk mengimplementasikan antrian prioritas menggunakan tumpukan biner, misalnya.siftDown
siftUp
Saya telah menulis ini untuk menjelaskan cara kerja tumpukan maks. Ini adalah tipe tumpukan yang biasanya digunakan untuk mengurutkan tumpukan atau untuk antrian prioritas di mana nilai yang lebih tinggi menunjukkan prioritas yang lebih tinggi. Tumpukan min juga berguna; misalnya, saat mengambil item dengan kunci integer dalam urutan menaik atau string dalam urutan abjad. Prinsip-prinsipnya persis sama; cukup alihkan urutan.
The properti tumpukan menetapkan bahwa setiap node dalam tumpukan biner harus setidaknya sama besar dengan kedua anak-anaknya. Secara khusus, ini menyiratkan bahwa item terbesar di heap adalah di root. Memilah ke bawah dan memilah pada dasarnya adalah operasi yang sama di arah yang berlawanan: pindahkan simpul yang menyinggung sampai memenuhi properti tumpukan:
siftDown
menukar simpul yang terlalu kecil dengan anak terbesarnya (dengan demikian memindahkannya ke bawah) hingga setidaknya sama besar dengan kedua simpul di bawahnya.
siftUp
menukar simpul yang terlalu besar dengan induknya (dengan demikian memindahkannya ke atas) sampai tidak lebih besar dari simpul di atasnya.
Jumlah operasi yang diperlukan untuk siftDown
dan siftUp
sebanding dengan jarak node mungkin harus bergerak. Sebab siftDown
, itu jarak ke bagian bawah pohon, jadi siftDown
mahal untuk node di bagian atas pohon. Dengan siftUp
, pekerjaan sebanding dengan jarak ke puncak pohon, jadi siftUp
mahal untuk simpul di bagian bawah pohon. Meskipun kedua operasi adalah O (log n) dalam kasus terburuk, dalam heap, hanya satu node di bagian atas sedangkan setengah dari node terletak di lapisan bawah. Jadi seharusnya tidak terlalu mengejutkan bahwa jika kita harus menerapkan operasi ke setiap node, kita akan memilih siftDown
lebih siftUp
.
The buildHeap
Fungsi mengambil array dari item disortir dan bergerak mereka sampai mereka semua memenuhi properti heap, sehingga menghasilkan tumpukan valid. Ada dua pendekatan yang mungkin diambil untuk buildHeap
menggunakan siftUp
dan siftDown
operasi yang telah kami jelaskan.
Mulai di bagian atas tumpukan (awal array) dan panggil siftUp
setiap item. Pada setiap langkah, item yang diayak sebelumnya (item sebelum item saat ini dalam array) membentuk heap yang valid, dan menyaring item berikutnya menempatkannya ke posisi yang valid di heap. Setelah menyaring setiap node, semua item memenuhi properti heap.
Atau, pergi ke arah yang berlawanan: mulai dari ujung array dan bergerak mundur ke arah depan. Pada setiap iterasi, Anda menyaring suatu barang sampai berada di lokasi yang benar.
Untuk implementasi mana buildHeap
yang lebih efisien?
Kedua solusi ini akan menghasilkan tumpukan yang valid. Tidak mengherankan, yang lebih efisien adalah operasi kedua yang digunakan siftDown
.
Biarkan h = log n mewakili ketinggian tumpukan. Pekerjaan yang dibutuhkan untuk siftDown
pendekatan diberikan oleh penjumlahan
(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).
Setiap istilah dalam penjumlahan memiliki jarak maksimum, sebuah simpul pada ketinggian tertentu harus bergerak (nol untuk lapisan bawah, h untuk root) dikalikan dengan jumlah simpul pada ketinggian itu. Sebaliknya, jumlah untuk memanggil siftUp
setiap node adalah
(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).
Harus jelas bahwa jumlah kedua lebih besar. Istilah pertama saja adalah hn / 2 = 1/2 n log n , jadi pendekatan ini memiliki kompleksitas paling baik O (n log n) .
Bagaimana kita membuktikan bahwa jumlah untuk siftDown
pendekatan itu memang O (n) ?
Salah satu metode (ada analisis lain yang juga berfungsi) adalah mengubah jumlah terbatas menjadi seri tak terbatas dan kemudian menggunakan seri Taylor. Kami dapat mengabaikan istilah pertama, yaitu nol:
Jika Anda tidak yakin mengapa masing-masing langkah itu berhasil, berikut ini adalah pembenaran untuk proses ini dengan kata-kata:
- Persyaratannya semua positif, sehingga jumlah yang terbatas harus lebih kecil dari jumlah yang tak terbatas.
- Seri ini sama dengan seri daya yang dievaluasi pada x = 1/2 .
- Rangkaian daya itu sama dengan (waktu konstan) turunan dari deret Taylor untuk f (x) = 1 / (1-x) .
- x = 1/2 berada dalam interval konvergensi dari seri Taylor itu.
- Oleh karena itu, kita dapat mengganti seri Taylor dengan 1 / (1-x) , membedakan, dan mengevaluasi untuk menemukan nilai seri infinite.
Karena jumlah tak terhingga tepat n , kami menyimpulkan bahwa jumlah terbatas tidak lebih besar, dan karenanya, O (n) .
Mengapa tumpukan membutuhkan waktu O (n log n) ?
Jika dimungkinkan untuk berjalan buildHeap
dalam waktu linier, mengapa sortir heap membutuhkan waktu O (n log n) ? Nah, heap sort terdiri dari dua tahap. Pertama, kita memanggil buildHeap
array, yang membutuhkan waktu O (n) jika diterapkan secara optimal. Tahap selanjutnya adalah berulang kali menghapus item terbesar di heap dan meletakkannya di akhir array. Karena kami menghapus item dari heap, selalu ada tempat terbuka tepat setelah akhir heap di mana kami dapat menyimpan item. Jadi heap sort mencapai urutan diurutkan dengan secara berturut-turut menghapus item terbesar berikutnya dan memasukkannya ke dalam array mulai dari posisi terakhir dan bergerak ke arah depan. Kompleksitas dari bagian terakhir inilah yang mendominasi dalam heap sort. Loop terlihat seperti ini:
for (i = n - 1; i > 0; i--) {
arr[i] = deleteMax();
}
Jelas, loop menjalankan O (n) kali ( n - 1 tepatnya, item terakhir sudah ada di tempatnya). Kompleksitas deleteMax
untuk heap adalah O (log n) . Ini biasanya diterapkan dengan menghapus root (item terbesar yang tersisa di heap) dan menggantinya dengan item terakhir di heap, yang merupakan daun, dan karena itu salah satu item terkecil. Root baru ini hampir pasti akan melanggar properti heap, jadi Anda harus menelepon siftDown
sampai Anda memindahkannya kembali ke posisi yang dapat diterima. Ini juga memiliki efek memindahkan item terbesar berikutnya ke root. Perhatikan bahwa, berbeda dengan buildHeap
tempat sebagian besar simpul yang kita panggil siftDown
dari bawah pohon, kita sekarang memanggil siftDown
dari atas pohon pada setiap iterasi!Meskipun pohon menyusut, ia tidak menyusut cukup cepat : Ketinggian pohon tetap konstan sampai Anda telah menghapus setengah bagian pertama dari simpul (ketika Anda membersihkan lapisan bawah sepenuhnya). Kemudian untuk kuartal berikutnya, ketinggiannya adalah h - 1 . Jadi total pekerjaan untuk tahap kedua ini adalah
h*n/2 + (h-1)*n/4 + ... + 0 * 1.
Perhatikan sakelar: sekarang kasus kerja nol berhubungan dengan satu node dan kasus kerja h sesuai dengan setengah node. Jumlah ini adalah O (n log n) sama seperti versi tidak efisien buildHeap
yang diimplementasikan menggunakan siftUp. Tetapi dalam kasus ini, kami tidak punya pilihan karena kami mencoba menyortir dan kami membutuhkan item terbesar berikutnya dihapus berikutnya.
Singkatnya, pekerjaan untuk heap sort adalah jumlah dari dua tahap: O (n) waktu untuk buildHeap dan O (n log n) untuk menghapus setiap node secara berurutan , sehingga kompleksitasnya adalah O (n log n) . Anda dapat membuktikan (menggunakan beberapa ide dari teori informasi) bahwa untuk jenis berbasis perbandingan, O (n log n) adalah yang terbaik yang bisa Anda harapkan, jadi tidak ada alasan untuk kecewa dengan ini atau mengharapkan tumpukan tumpukan untuk mencapai O (n) batas waktu yang buildHeap
berlaku.