Enhance docs, add tests in MinHeap (#5985)

This commit is contained in:
Hardik Pawar
2024-10-26 22:23:32 +05:30
committed by GitHub
parent bc6ea1ec87
commit a78b15dc24
5 changed files with 345 additions and 53 deletions

View File

@ -871,6 +871,7 @@
* [LeftistHeapTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/heaps/LeftistHeapTest.java)
* [MedianFinderTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/heaps/MedianFinderTest.java)
* [MergeKSortedArraysTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/heaps/MergeKSortedArraysTest.java)
* [MinHeapTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/heaps/MinHeapTest.java)
* [MinPriorityQueueTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/heaps/MinPriorityQueueTest.java)
* lists
* [CircleLinkedListTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/lists/CircleLinkedListTest.java)

View File

@ -40,5 +40,5 @@ public interface Heap {
* @param elementIndex int containing the position in the heap of the
* element to be deleted.
*/
void deleteElement(int elementIndex);
void deleteElement(int elementIndex) throws EmptyHeapException;
}

View File

@ -167,4 +167,8 @@ public class HeapElement {
result += (additionalInfo != null) ? additionalInfo.hashCode() : 0;
return result;
}
public String getValue() {
return additionalInfo.toString();
}
}

View File

@ -4,8 +4,25 @@ import java.util.ArrayList;
import java.util.List;
/**
* Heap tree where a node's key is higher than or equal to its parent's and
* lower than or equal to its children's.
* A Min Heap implementation where each node's key is lower than or equal to its children's keys.
* This data structure provides O(log n) time complexity for insertion and deletion operations,
* and O(1) for retrieving the minimum element.
*
* Properties:
* 1. Complete Binary Tree
* 2. Parent node's key ≤ Children nodes' keys
* 3. Root contains the minimum element
*
* Example usage:
* ```java
* List<HeapElement> elements = Arrays.asList(
* new HeapElement(5, "Five"),
* new HeapElement(2, "Two")
* );
* MinHeap heap = new MinHeap(elements);
* heap.insertElement(new HeapElement(1, "One"));
* HeapElement min = heap.getElement(); // Returns and removes the minimum element
* ```
*
* @author Nicolas Renard
*/
@ -13,113 +30,242 @@ public class MinHeap implements Heap {
private final List<HeapElement> minHeap;
/**
* Constructs a new MinHeap from a list of elements.
* Null elements in the input list are ignored with a warning message.
*
* @param listElements List of HeapElement objects to initialize the heap
* @throws IllegalArgumentException if the input list is null
*/
public MinHeap(List<HeapElement> listElements) {
if (listElements == null) {
throw new IllegalArgumentException("Input list cannot be null");
}
minHeap = new ArrayList<>();
// Safe initialization: directly add elements first
for (HeapElement heapElement : listElements) {
if (heapElement != null) {
insertElement(heapElement);
minHeap.add(heapElement);
} else {
System.out.println("Null element. Not added to heap");
}
}
// Heapify the array bottom-up
for (int i = minHeap.size() / 2; i >= 0; i--) {
heapifyDown(i + 1);
}
if (minHeap.isEmpty()) {
System.out.println("No element has been added, empty heap.");
}
}
// Get the element at a given index. The key for the list is equal to index value - 1
/**
* Retrieves the element at the specified index without removing it.
* Note: The index is 1-based for consistency with heap operations.
*
* @param elementIndex 1-based index of the element to retrieve
* @return HeapElement at the specified index
* @throws IndexOutOfBoundsException if the index is invalid
*/
public HeapElement getElement(int elementIndex) {
if ((elementIndex <= 0) || (elementIndex > minHeap.size())) {
throw new IndexOutOfBoundsException("Index out of heap range");
throw new IndexOutOfBoundsException("Index " + elementIndex + " is out of heap range [1, " + minHeap.size() + "]");
}
return minHeap.get(elementIndex - 1);
}
// Get the key of the element at a given index
/**
* Retrieves the key value of an element at the specified index.
*
* @param elementIndex 1-based index of the element
* @return double value representing the key
* @throws IndexOutOfBoundsException if the index is invalid
*/
private double getElementKey(int elementIndex) {
if ((elementIndex <= 0) || (elementIndex > minHeap.size())) {
throw new IndexOutOfBoundsException("Index out of heap range");
throw new IndexOutOfBoundsException("Index " + elementIndex + " is out of heap range [1, " + minHeap.size() + "]");
}
return minHeap.get(elementIndex - 1).getKey();
}
// Swaps two elements in the heap
/**
* Swaps two elements in the heap.
*
* @param index1 1-based index of first element
* @param index2 1-based index of second element
*/
private void swap(int index1, int index2) {
HeapElement temporaryElement = minHeap.get(index1 - 1);
minHeap.set(index1 - 1, minHeap.get(index2 - 1));
minHeap.set(index2 - 1, temporaryElement);
}
// Toggle an element up to its right place as long as its key is lower than its parent's
private void toggleUp(int elementIndex) {
double key = minHeap.get(elementIndex - 1).getKey();
while (getElementKey((int) Math.floor(elementIndex / 2.0) + 1) > key) {
swap(elementIndex, (int) Math.floor(elementIndex / 2.0));
elementIndex = (int) Math.floor(elementIndex / 2.0);
/**
* Maintains heap properties by moving an element down the heap.
* Used specifically during initialization.
*
* @param elementIndex 1-based index of the element to heapify
*/
private void heapifyDown(int elementIndex) {
int smallest = elementIndex - 1; // Convert to 0-based index
int leftChild = 2 * elementIndex - 1;
int rightChild = 2 * elementIndex;
// Check if left child is smaller than root
if (leftChild < minHeap.size() && minHeap.get(leftChild).getKey() < minHeap.get(smallest).getKey()) {
smallest = leftChild;
}
// Check if right child is smaller than smallest so far
if (rightChild < minHeap.size() && minHeap.get(rightChild).getKey() < minHeap.get(smallest).getKey()) {
smallest = rightChild;
}
// If smallest is not root
if (smallest != elementIndex - 1) {
HeapElement swap = minHeap.get(elementIndex - 1);
minHeap.set(elementIndex - 1, minHeap.get(smallest));
minHeap.set(smallest, swap);
// Recursively heapify the affected sub-tree
heapifyDown(smallest + 1); // Convert back to 1-based index
}
}
// Toggle an element down to its right place as long as its key is higher
// than any of its children's
/**
* Moves an element up the heap until heap properties are satisfied.
* This operation is called after insertion to maintain heap properties.
*
* @param elementIndex 1-based index of the element to move up
*/
private void toggleUp(int elementIndex) {
if (elementIndex <= 1) {
return;
}
double key = minHeap.get(elementIndex - 1).getKey();
int parentIndex = (int) Math.floor(elementIndex / 2.0);
while (elementIndex > 1 && getElementKey(parentIndex) > key) {
swap(elementIndex, parentIndex);
elementIndex = parentIndex;
parentIndex = (int) Math.floor(elementIndex / 2.0);
}
}
/**
* Moves an element down the heap until heap properties are satisfied.
* This operation is called after deletion to maintain heap properties.
*
* @param elementIndex 1-based index of the element to move down
*/
private void toggleDown(int elementIndex) {
double key = minHeap.get(elementIndex - 1).getKey();
boolean wrongOrder = (key > getElementKey(elementIndex * 2)) || (key > getElementKey(Math.min(elementIndex * 2, minHeap.size())));
while ((2 * elementIndex <= minHeap.size()) && wrongOrder) {
// Check whether it shall swap the element with its left child or its right one if any.
if ((2 * elementIndex < minHeap.size()) && (getElementKey(elementIndex * 2 + 1) < getElementKey(elementIndex * 2))) {
swap(elementIndex, 2 * elementIndex + 1);
elementIndex = 2 * elementIndex + 1;
} else {
swap(elementIndex, 2 * elementIndex);
elementIndex = 2 * elementIndex;
int size = minHeap.size();
while (true) {
int smallest = elementIndex;
int leftChild = 2 * elementIndex;
int rightChild = 2 * elementIndex + 1;
if (leftChild <= size && getElementKey(leftChild) < key) {
smallest = leftChild;
}
wrongOrder = (key > getElementKey(elementIndex * 2)) || (key > getElementKey(Math.min(elementIndex * 2, minHeap.size())));
if (rightChild <= size && getElementKey(rightChild) < getElementKey(smallest)) {
smallest = rightChild;
}
if (smallest == elementIndex) {
break;
}
swap(elementIndex, smallest);
elementIndex = smallest;
}
}
private HeapElement extractMin() {
HeapElement result = minHeap.get(0);
deleteElement(0);
/**
* Extracts and returns the minimum element from the heap.
*
* @return HeapElement with the lowest key
* @throws EmptyHeapException if the heap is empty
*/
private HeapElement extractMin() throws EmptyHeapException {
if (minHeap.isEmpty()) {
throw new EmptyHeapException("Cannot extract from empty heap");
}
HeapElement result = minHeap.getFirst();
deleteElement(1);
return result;
}
/**
* {@inheritDoc}
*/
@Override
public final void insertElement(HeapElement element) {
public void insertElement(HeapElement element) {
if (element == null) {
throw new IllegalArgumentException("Cannot insert null element");
}
minHeap.add(element);
toggleUp(minHeap.size());
}
/**
* {@inheritDoc}
*/
@Override
public void deleteElement(int elementIndex) {
public void deleteElement(int elementIndex) throws EmptyHeapException {
if (minHeap.isEmpty()) {
try {
throw new EmptyHeapException("Attempt to delete an element from an empty heap");
} catch (EmptyHeapException e) {
e.printStackTrace();
}
throw new EmptyHeapException("Cannot delete from empty heap");
}
if ((elementIndex > minHeap.size()) || (elementIndex <= 0)) {
throw new IndexOutOfBoundsException("Index out of heap range");
throw new IndexOutOfBoundsException("Index " + elementIndex + " is out of heap range [1, " + minHeap.size() + "]");
}
// The last element in heap replaces the one to be deleted
minHeap.set(elementIndex - 1, getElement(minHeap.size()));
minHeap.remove(minHeap.size());
// Shall the new element be moved up...
if (getElementKey(elementIndex) < getElementKey((int) Math.floor(elementIndex / 2.0))) {
// Replace with last element and remove last position
minHeap.set(elementIndex - 1, minHeap.getLast());
minHeap.removeLast();
// No need to toggle if we just removed the last element
if (!minHeap.isEmpty() && elementIndex <= minHeap.size()) {
// Determine whether to toggle up or down
if (elementIndex > 1 && getElementKey(elementIndex) < getElementKey((int) Math.floor(elementIndex / 2.0))) {
toggleUp(elementIndex);
} // ... or down ?
else if (((2 * elementIndex <= minHeap.size()) && (getElementKey(elementIndex) > getElementKey(elementIndex * 2))) || ((2 * elementIndex < minHeap.size()) && (getElementKey(elementIndex) > getElementKey(elementIndex * 2)))) {
} else {
toggleDown(elementIndex);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public HeapElement getElement() throws EmptyHeapException {
try {
return extractMin();
} catch (Exception e) {
throw new EmptyHeapException("Heap is empty. Error retrieving element", e);
}
}
/**
* Returns the current size of the heap.
*
* @return number of elements in the heap
*/
public int size() {
return minHeap.size();
}
/**
* Checks if the heap is empty.
*
* @return true if the heap contains no elements
*/
public boolean isEmpty() {
return minHeap.isEmpty();
}
}

View File

@ -0,0 +1,141 @@
package com.thealgorithms.datastructures.heaps;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class MinHeapTest {
private MinHeap heap;
@BeforeEach
void setUp() {
// Create a fresh heap for each test
List<HeapElement> elements = Arrays.asList(new HeapElement(5.0, "Five"), new HeapElement(2.0, "Two"), new HeapElement(8.0, "Eight"), new HeapElement(1.0, "One"), new HeapElement(9.0, "Nine"));
heap = new MinHeap(elements);
}
@Test
void testConstructorWithNullList() {
assertThrows(IllegalArgumentException.class, () -> new MinHeap(null));
}
@Test
void testConstructorWithEmptyList() {
MinHeap emptyHeap = new MinHeap(new ArrayList<>());
assertTrue(emptyHeap.isEmpty());
}
@Test
void testConstructorWithNullElements() {
List<HeapElement> elements = Arrays.asList(new HeapElement(1.0, "One"), null, new HeapElement(2.0, "Two"));
MinHeap heap = new MinHeap(elements);
assertEquals(2, heap.size());
}
@Test
void testInsertElement() {
heap.insertElement(new HeapElement(0.5, "Half"));
assertEquals(0.5, heap.getElement(1).getKey());
assertEquals(6, heap.size());
}
@Test
void testInsertNullElement() {
assertThrows(IllegalArgumentException.class, () -> heap.insertElement(null));
}
@Test
void testGetElementAtIndex() {
HeapElement element = heap.getElement(1);
assertEquals(1.0, element.getKey());
assertEquals("One", element.getValue());
}
@Test
void testGetElementAtInvalidIndex() {
assertThrows(IndexOutOfBoundsException.class, () -> heap.getElement(0));
assertThrows(IndexOutOfBoundsException.class, () -> heap.getElement(10));
}
@Test
void testDeleteElement() throws EmptyHeapException {
heap.deleteElement(1);
assertEquals(2.0, heap.getElement(1).getKey());
assertEquals(4, heap.size());
}
@Test
void testDeleteElementAtInvalidIndex() {
assertThrows(IndexOutOfBoundsException.class, () -> heap.deleteElement(0));
assertThrows(IndexOutOfBoundsException.class, () -> heap.deleteElement(10));
}
@Test
void testDeleteFromEmptyHeap() {
MinHeap emptyHeap = new MinHeap(new ArrayList<>());
assertThrows(EmptyHeapException.class, () -> emptyHeap.deleteElement(1));
}
@Test
void testExtractMin() throws EmptyHeapException {
HeapElement min = heap.getElement();
assertEquals(1.0, min.getKey());
assertEquals("One", min.getValue());
assertEquals(4, heap.size());
min = heap.getElement();
assertEquals(2.0, min.getKey());
assertEquals(3, heap.size());
}
@Test
void testExtractMinFromEmptyHeap() {
MinHeap emptyHeap = new MinHeap(new ArrayList<>());
assertThrows(EmptyHeapException.class, () -> emptyHeap.getElement());
}
@Test
void testHeapOrder() {
// Test that parent is always smaller than or equal to children
for (int i = 1; i <= heap.size() / 2; i++) {
double parentKey = heap.getElement(i).getKey();
// Check left child
if (2 * i <= heap.size()) {
assertTrue(parentKey <= heap.getElement(2 * i).getKey());
}
// Check right child
if (2 * i + 1 <= heap.size()) {
assertTrue(parentKey <= heap.getElement(2 * i + 1).getKey());
}
}
}
@Test
void testSizeAndEmpty() {
assertEquals(5, heap.size());
assertFalse(heap.isEmpty());
// Remove all elements
while (!heap.isEmpty()) {
try {
heap.getElement();
} catch (EmptyHeapException e) {
Assertions.fail("Should not throw EmptyHeapException while heap is not empty");
}
}
assertEquals(0, heap.size());
assertTrue(heap.isEmpty());
}
}