Skip to content

refactor: improve median calculator class design and readability #6349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 53 additions & 29 deletions src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> 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<T extends Number & Comparable<T>> {

private PriorityQueue<T> maxHeap;
private PriorityQueue<T> minHeap;
private final PriorityQueue<T> maxHeap; // Lower half (max-heap)
private final PriorityQueue<T> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,81 +11,81 @@
*/

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);
}

@Test
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
}
}
Loading