diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java index 62013ee31183..95f86f63f720 100644 --- a/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java +++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java @@ -4,50 +4,74 @@ import java.util.PriorityQueue; /** - * @author shrutisheoran + * A generic abstract class to compute the median of a dynamically growing stream of numbers. + * + * @param the number type, must extend Number and be Comparable + * + * Usage: + * Extend this class and implement {@code calculateAverage(T a, T b)} to define how averaging is done. */ public abstract class MedianOfRunningArray> { - private PriorityQueue maxHeap; - private PriorityQueue minHeap; + private final PriorityQueue maxHeap; // Lower half (max-heap) + private final PriorityQueue minHeap; // Upper half (min-heap) - // Constructor public MedianOfRunningArray() { - this.maxHeap = new PriorityQueue<>(Collections.reverseOrder()); // Max Heap - this.minHeap = new PriorityQueue<>(); // Min Heap + this.maxHeap = new PriorityQueue<>(Collections.reverseOrder()); + this.minHeap = new PriorityQueue<>(); } - /* - Inserting lower half of array to max Heap - and upper half to min heap + /** + * Inserts a new number into the data structure. + * + * @param element the number to insert */ - public void insert(final T e) { - if (!minHeap.isEmpty() && e.compareTo(minHeap.peek()) < 0) { - maxHeap.offer(e); - if (maxHeap.size() > minHeap.size() + 1) { - minHeap.offer(maxHeap.poll()); - } + public final void insert(final T element) { + if (!minHeap.isEmpty() && element.compareTo(minHeap.peek()) < 0) { + maxHeap.offer(element); + balanceHeapsIfNeeded(); } else { - minHeap.offer(e); - if (minHeap.size() > maxHeap.size() + 1) { - maxHeap.offer(minHeap.poll()); - } + minHeap.offer(element); + balanceHeapsIfNeeded(); } } - /* - Returns median at any given point + /** + * Returns the median of the current elements. + * + * @return the median value + * @throws IllegalArgumentException if no elements have been inserted */ - public T median() { + public final T getMedian() { if (maxHeap.isEmpty() && minHeap.isEmpty()) { - throw new IllegalArgumentException("Enter at least 1 element, Median of empty list is not defined!"); - } else if (maxHeap.size() == minHeap.size()) { - T maxHeapTop = maxHeap.peek(); - T minHeapTop = minHeap.peek(); - return calculateAverage(maxHeapTop, minHeapTop); + throw new IllegalArgumentException("Median is undefined for an empty data set."); } - return maxHeap.size() > minHeap.size() ? maxHeap.peek() : minHeap.peek(); + + if (maxHeap.size() == minHeap.size()) { + return calculateAverage(maxHeap.peek(), minHeap.peek()); + } + + return (maxHeap.size() > minHeap.size()) ? maxHeap.peek() : minHeap.peek(); } - public abstract T calculateAverage(T a, T b); + /** + * Calculates the average between two values. + * Concrete subclasses must define how averaging works (e.g., for Integer, Double, etc.). + * + * @param a first number + * @param b second number + * @return the average of a and b + */ + protected abstract T calculateAverage(T a, T b); + + /** + * Balances the two heaps so that their sizes differ by at most 1. + */ + private void balanceHeapsIfNeeded() { + if (maxHeap.size() > minHeap.size() + 1) { + minHeap.offer(maxHeap.poll()); + } else if (minHeap.size() > maxHeap.size() + 1) { + maxHeap.offer(minHeap.poll()); + } + } } diff --git a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java index e64ae1b741b6..f41953035846 100644 --- a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java @@ -11,12 +11,12 @@ */ public class MedianOfRunningArrayTest { - private static final String EXCEPTION_MESSAGE = "Enter at least 1 element, Median of empty list is not defined!"; + private static final String EXCEPTION_MESSAGE = "Median is undefined for an empty data set."; @Test public void testWhenInvalidInoutProvidedShouldThrowException() { var stream = new MedianOfRunningArrayInteger(); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> stream.median()); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, stream::getMedian); assertEquals(exception.getMessage(), EXCEPTION_MESSAGE); } @@ -24,68 +24,68 @@ public void testWhenInvalidInoutProvidedShouldThrowException() { public void testWithNegativeValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); stream.insert(-2); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); stream.insert(-3); - assertEquals(-2, stream.median()); + assertEquals(-2, stream.getMedian()); } @Test public void testWithSingleValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); } @Test public void testWithRandomValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(10); - assertEquals(10, stream.median()); + assertEquals(10, stream.getMedian()); stream.insert(5); - assertEquals(7, stream.median()); + assertEquals(7, stream.getMedian()); stream.insert(20); - assertEquals(10, stream.median()); + assertEquals(10, stream.getMedian()); stream.insert(15); - assertEquals(12, stream.median()); + assertEquals(12, stream.getMedian()); stream.insert(25); - assertEquals(15, stream.median()); + assertEquals(15, stream.getMedian()); stream.insert(30); - assertEquals(17, stream.median()); + assertEquals(17, stream.getMedian()); stream.insert(35); - assertEquals(20, stream.median()); + assertEquals(20, stream.getMedian()); stream.insert(1); - assertEquals(17, stream.median()); + assertEquals(17, stream.getMedian()); } @Test public void testWithNegativeAndPositiveValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); stream.insert(2); - assertEquals(0, stream.median()); + assertEquals(0, stream.getMedian()); stream.insert(-3); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); } @Test public void testWithDuplicateValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); stream.insert(-1); - assertEquals(-1, stream.median()); + assertEquals(-1, stream.getMedian()); } @Test @@ -98,20 +98,20 @@ public void testWithDuplicateValuesB() { stream.insert(20); stream.insert(0); stream.insert(50); - assertEquals(10, stream.median()); + assertEquals(10, stream.getMedian()); } @Test public void testWithLargeValues() { var stream = new MedianOfRunningArrayInteger(); stream.insert(1000000); - assertEquals(1000000, stream.median()); + assertEquals(1000000, stream.getMedian()); stream.insert(12000); - assertEquals(506000, stream.median()); + assertEquals(506000, stream.getMedian()); stream.insert(15000000); - assertEquals(1000000, stream.median()); + assertEquals(1000000, stream.getMedian()); stream.insert(2300000); - assertEquals(1650000, stream.median()); + assertEquals(1650000, stream.getMedian()); } @Test @@ -120,7 +120,7 @@ public void testWithLargeCountOfValues() { for (int i = 1; i <= 1000; i++) { stream.insert(i); } - assertEquals(500, stream.median()); + assertEquals(500, stream.getMedian()); } @Test @@ -129,7 +129,7 @@ public void testWithThreeValuesInDescendingOrder() { stream.insert(30); stream.insert(20); stream.insert(10); - assertEquals(20, stream.median()); + assertEquals(20, stream.getMedian()); } @Test @@ -138,7 +138,7 @@ public void testWithThreeValuesInOrder() { stream.insert(10); stream.insert(20); stream.insert(30); - assertEquals(20, stream.median()); + assertEquals(20, stream.getMedian()); } @Test @@ -147,7 +147,7 @@ public void testWithThreeValuesNotInOrderA() { stream.insert(30); stream.insert(10); stream.insert(20); - assertEquals(20, stream.median()); + assertEquals(20, stream.getMedian()); } @Test @@ -156,46 +156,46 @@ public void testWithThreeValuesNotInOrderB() { stream.insert(20); stream.insert(10); stream.insert(30); - assertEquals(20, stream.median()); + assertEquals(20, stream.getMedian()); } @Test public void testWithFloatValues() { var stream = new MedianOfRunningArrayFloat(); stream.insert(20.0f); - assertEquals(20.0f, stream.median()); + assertEquals(20.0f, stream.getMedian()); stream.insert(10.5f); - assertEquals(15.25f, stream.median()); + assertEquals(15.25f, stream.getMedian()); stream.insert(30.0f); - assertEquals(20.0f, stream.median()); + assertEquals(20.0f, stream.getMedian()); } @Test public void testWithByteValues() { var stream = new MedianOfRunningArrayByte(); stream.insert((byte) 120); - assertEquals((byte) 120, stream.median()); + assertEquals((byte) 120, stream.getMedian()); stream.insert((byte) -120); - assertEquals((byte) 0, stream.median()); + assertEquals((byte) 0, stream.getMedian()); stream.insert((byte) 127); - assertEquals((byte) 120, stream.median()); + assertEquals((byte) 120, stream.getMedian()); } @Test public void testWithLongValues() { var stream = new MedianOfRunningArrayLong(); stream.insert(120000000L); - assertEquals(120000000L, stream.median()); + assertEquals(120000000L, stream.getMedian()); stream.insert(92233720368547757L); - assertEquals(46116860244273878L, stream.median()); + assertEquals(46116860244273878L, stream.getMedian()); } @Test public void testWithDoubleValues() { var stream = new MedianOfRunningArrayDouble(); stream.insert(12345.67891); - assertEquals(12345.67891, stream.median()); + assertEquals(12345.67891, stream.getMedian()); stream.insert(23456789.98); - assertEquals(11734567.83, stream.median(), .01); + assertEquals(11734567.83, stream.getMedian(), .01); } }