Isi Tumpukan di Jawa

1. Pengenalan

Dalam tutorial ini, kita akan melihat bagaimana Heap Sort berfungsi, dan kita akan melaksanakannya di Java.

Heap Sort berdasarkan struktur data Heap. Untuk memahami Heap Sort dengan betul, pertama-tama kita akan menggali Heaps dan bagaimana ia dilaksanakan.

2. Struktur Data timbunan

A Heap adalah struktur data berasaskan pokok khusus . Oleh itu ia terdiri daripada nod. Kami menetapkan elemen ke nod: setiap nod mengandungi satu elemen.

Node juga boleh mempunyai anak. Sekiranya simpul tidak mempunyai anak, kami memanggilnya daun.

Apa yang menjadikan Heap istimewa adalah dua perkara:

  1. nilai setiap nod mesti kurang atau sama dengan semua nilai yang disimpan dalam anak-anaknya
  2. ia adalah pokok yang lengkap , yang bermaksud mempunyai ketinggian yang paling sedikit

Oleh kerana peraturan pertama, unsur paling sedikit selalu ada di akar pokok .

Cara kita menegakkan peraturan ini bergantung pada pelaksanaan.

Heap biasanya digunakan untuk melaksanakan antrian prioriti kerana Heap adalah pelaksanaan yang sangat efisien untuk mengekstrak elemen paling sedikit (atau paling besar).

2.1. Varian Tumpukan

Heap mempunyai banyak varian, semuanya berbeza dalam beberapa perincian pelaksanaan.

Sebagai contoh, apa yang kami jelaskan di atas adalah Min-Heap, kerana ibu bapa selalu kurang daripada semua anak-anaknya . Sebagai alternatif, kita boleh menentukan Max-Heap, dalam hal ini ibu bapa selalu lebih besar daripada anak-anaknya. Oleh itu, elemen terbesar akan berada di simpul akar.

Kita boleh memilih dari banyak pelaksanaan pokok. Yang paling mudah adalah Pokok Binari. Di Pokok Binari, setiap simpul boleh mempunyai maksimum dua anak. Kami memanggil mereka anak kiri dan anak kanan .

Cara termudah untuk menguatkuasakan peraturan ke-2 adalah dengan menggunakan Full Binary Tree. Pokok Perduaan Penuh mengikuti beberapa peraturan mudah:

  1. jika simpul hanya mempunyai satu anak, itu mestilah anak kirinya
  2. hanya simpul paling kanan pada tahap terdalam yang boleh mempunyai seorang anak
  3. daun hanya boleh berada di tahap paling dalam

Mari lihat peraturan ini dengan beberapa contoh:

 1 2 3 4 5 6 7 8 9 10 () () () () () () () () () () / \ / \ / \ / \ / \ / / / \ () () () () () () () () () () () () () () / \ / \ / \ / / \ () () () () () () () () () / ()

Pokok 1, 2, 4, 5 dan 7 mengikut peraturan.

Pokok 3 dan 6 melanggar peraturan 1, 8 dan 9 peraturan ke-2, dan 10 melanggar peraturan ke-3.

Dalam tutorial ini, kita akan fokus pada Min-Heap dengan pelaksanaan Binary Tree .

2.2. Memasukkan Elemen

Kita harus melaksanakan semua operasi dengan cara, yang menjaga invariant Heap. Dengan cara ini, kita dapat membina Heap dengan sisipan berulang , jadi kita akan fokus pada operasi memasukkan tunggal.

Kita boleh memasukkan elemen dengan langkah-langkah berikut:

  1. buat daun baru yang merupakan slot paling kanan yang terdapat pada tahap terdalam dan simpan item tersebut di simpul tersebut
  2. jika elemennya kurang daripada induknya, kami menukarnya
  3. teruskan dengan langkah 2, sehingga elemennya kurang daripada induknya atau menjadi akar baru

Perhatikan, langkah 2 tidak akan melanggar peraturan Heap, kerana jika kita mengganti nilai simpul dengan yang kurang, nilai tetap akan lebih kecil daripada anak-anak.

Mari lihat contoh! Kami ingin memasukkan 4 ke dalam Heap ini:

 2 / \ / \ 3 6 / \ 5 7

Langkah pertama adalah membuat daun baru yang menyimpan 4:

 2 / \ / \ 3 6 / \ / 5 7 4

Oleh kerana 4 lebih kecil daripada induknya, 6, kami menukarnya:

 2 / \ / \ 3 4 / \ / 5 7 6

Sekarang kita periksa sama ada 4 kurang daripada induknya atau tidak. Oleh kerana induknya berusia 2 tahun, kami berhenti. Tumpukan masih berlaku, dan kami memasukkan nombor 4.

Mari masukkan 1:

 2 / \ / \ 3 4 / \ / \ 5 7 6 1

Kita mesti menukar 1 dan 4:

 2 / \ / \ 3 1 / \ / \ 5 7 6 4

Sekarang kita harus menukar 1 dan 2:

 1 / \ / \ 3 2 / \ / \ 5 7 6 4

Oleh kerana 1 adalah akar baru, kami berhenti.

3. Pelaksanaan Tumpukan di Jawa

Oleh kerana kita menggunakan Full Binary Tree, kita dapat menerapkannya dengan array : elemen dalam array akan menjadi simpul di pohon. Kami menandakan setiap nod dengan indeks array dari kiri ke kanan, dari atas ke bawah dengan cara berikut:

 0 / \ / \ 1 2 / \ / 3 4 5

Satu-satunya perkara yang kita perlukan adalah mengawasi berapa banyak elemen yang kita simpan di dalam pokok. Dengan cara ini indeks elemen seterusnya yang ingin kita masukkan akan menjadi ukuran array.

Dengan menggunakan pengindeksan ini, kita dapat mengira indeks nod ibu bapa dan anak:

  • ibu bapa: (indeks - 1) / 2
  • left child: 2 * index + 1
  • right child: 2 * index + 2

Since we don't want to bother with array reallocating, we'll simplify the implementation even more and use an ArrayList.

A basic Binary Tree implementation looks like this:

class BinaryTree { List elements = new ArrayList(); void add(E e) { elements.add(e); } boolean isEmpty() { return elements.isEmpty(); } E elementAt(int index) { return elements.get(index); } int parentIndex(int index) { return (index - 1) / 2; } int leftChildIndex(int index) { return 2 * index + 1; } int rightChildIndex(int index) { return 2 * index + 2; } }

The code above only adds the new element to the end of the tree. Therefore, we need to traverse the new element up if necessary. We can do it with the following code:

class Heap
    
      { // ... void add(E e) { elements.add(e); int elementIndex = elements.size() - 1; while (!isRoot(elementIndex) && !isCorrectChild(elementIndex)) { int parentIndex = parentIndex(elementIndex); swap(elementIndex, parentIndex); elementIndex = parentIndex; } } boolean isRoot(int index) { return index == 0; } boolean isCorrectChild(int index) { return isCorrect(parentIndex(index), index); } boolean isCorrect(int parentIndex, int childIndex) { if (!isValidIndex(parentIndex) || !isValidIndex(childIndex)) { return true; } return elementAt(parentIndex).compareTo(elementAt(childIndex)) < 0; } boolean isValidIndex(int index) { return index < elements.size(); } void swap(int index1, int index2) { E element1 = elementAt(index1); E element2 = elementAt(index2); elements.set(index1, element2); elements.set(index2, element1); } // ... }
    

Note, that since we need to compare the elements, they need to implement java.util.Comparable.

4. Heap Sort

Since the root of the Heap always contains the smallest element, the idea behind Heap Sort is pretty simple: remove the root node until the Heap becomes empty.

The only thing we need is a remove operation, which keeps the Heap in a consistent state. We must ensure that we don't violate the structure of the Binary Tree or the Heap property.

To keep the structure, we can't delete any element, except the rightmost leaf. So the idea is to remove the element from the root node and store the rightmost leaf in the root node.

But this operation will most certainly violate the Heap property. So if the new root is greater than any of its child nodes, we swap it with its least child node. Since the least child node is less than all other child nodes, it doesn't violate the Heap property.

We keep swapping until the element becomes a leaf, or it's less than all of its children.

Let's delete the root from this tree:

 1 / \ / \ 3 2 / \ / \ 5 7 6 4

First, we place the last leaf in the root:

 4 / \ / \ 3 2 / \ / 5 7 6

Then, since it's greater than both of its children, we swap it with its least child, which is 2:

 2 / \ / \ 3 4 / \ / 5 7 6

4 is less than 6, so we stop.

5. Heap Sort Implementation in Java

With all we have, removing the root (popping) looks like this:

class Heap
    
      { // ... E pop() { if (isEmpty()) { throw new IllegalStateException("You cannot pop from an empty heap"); } E result = elementAt(0); int lasElementIndex = elements.size() - 1; swap(0, lasElementIndex); elements.remove(lasElementIndex); int elementIndex = 0; while (!isLeaf(elementIndex) && !isCorrectParent(elementIndex)) { int smallerChildIndex = smallerChildIndex(elementIndex); swap(elementIndex, smallerChildIndex); elementIndex = smallerChildIndex; } return result; } boolean isLeaf(int index) { return !isValidIndex(leftChildIndex(index)); } boolean isCorrectParent(int index) { return isCorrect(index, leftChildIndex(index)) && isCorrect(index, rightChildIndex(index)); } int smallerChildIndex(int index) { int leftChildIndex = leftChildIndex(index); int rightChildIndex = rightChildIndex(index); if (!isValidIndex(rightChildIndex)) { return leftChildIndex; } if (elementAt(leftChildIndex).compareTo(elementAt(rightChildIndex)) < 0) { return leftChildIndex; } return rightChildIndex; } // ... }
    

Like we said before, sorting is just creating a Heap, and removing the root repeatedly:

class Heap
    
      { // ... static 
     
       List sort(Iterable elements) { Heap heap = of(elements); List result = new ArrayList(); while (!heap.isEmpty()) { result.add(heap.pop()); } return result; } static 
      
        Heap of(Iterable elements) { Heap result = new Heap(); for (E element : elements) { result.add(element); } return result; } // ... }
      
     
    

We can verify it's working with the following test:

@Test void givenNotEmptyIterable_whenSortCalled_thenItShouldReturnElementsInSortedList() { // given List elements = Arrays.asList(3, 5, 1, 4, 2); // when List sortedElements = Heap.sort(elements); // then assertThat(sortedElements).isEqualTo(Arrays.asList(1, 2, 3, 4, 5)); }

Note, that we could provide an implementation, which sorts in-place, which means we provide the result in the same array we got the elements. Additionally, this way we don't need any intermediate memory allocation. However, that implementation would be a bit harder to understand.

6. Time Complexity

Heap sort consists of two key steps, inserting an element and removing the root node. Both steps have the complexity O(log n).

Since we repeat both steps n times, the overall sorting complexity is O(n log n).

Note, that we didn't mention the cost of array reallocation, but since it's O(n), it doesn't affect the overall complexity. Also, as we mentioned before, it's possible to implement an in-place sorting, which means no array reallocation is necessary.

Also worth mentioning, that 50% of the elements are leaves, and 75% of elements are at the two bottommost levels. Therefore, most insert operations won't take more, than two steps.

Perhatikan, bahawa pada data dunia nyata, Quicksort biasanya lebih berprestasi daripada Heap Sort. Lapisan perak adalah bahawa Heap Sort selalu mempunyai kerumitan masa O (n log n) terburuk .

7. Kesimpulannya

Dalam tutorial ini, kami melihat pelaksanaan Binary Heap dan Heap Sort.

Walaupun kerumitan masa adalah O (n log n) , dalam kebanyakan kes, itu bukan algoritma terbaik untuk data dunia nyata.

Seperti biasa, contohnya terdapat di GitHub.