diff --git a/src/main/java/com/thealgorithms/divideandconquer/QuickSelectMedianOfMedians.java b/src/main/java/com/thealgorithms/divideandconquer/QuickSelectMedianOfMedians.java new file mode 100644 index 000000000000..4ce2df7277df --- /dev/null +++ b/src/main/java/com/thealgorithms/divideandconquer/QuickSelectMedianOfMedians.java @@ -0,0 +1,114 @@ +package com.thealgorithms.divideandconquer; + +import java.util.Arrays; + +/** + * QuickSelect algorithm using the Median of Medians method. + * + *

This algorithm finds the kth smallest element in an unsorted array in + * O(n) worst-case time complexity. + * + *

Steps: + *

    + *
  1. Divide the array into groups of five elements each.
  2. + *
  3. Find the median of each group.
  4. + *
  5. Recursively find the median of these medians, which becomes the pivot.
  6. + *
  7. Partition the array around this pivot.
  8. + *
  9. Recurse into the part that contains the kth smallest element.
  10. + *
+ * + *

Reference: + * + * Median of Medians Algorithm + */ +public final class QuickSelectMedianOfMedians { + + private QuickSelectMedianOfMedians() { + // Utility class; prevent instantiation + } + + /** + * Returns the kth smallest element in the given array using the + * deterministic Median of Medians approach. + * + * @param arr the input array + * @param k index (0-based) of the kth smallest element to find + * @return the kth smallest element + * @throws IllegalArgumentException if input is invalid + */ + public static int quickSelect(int[] arr, int k) { + if (arr == null || arr.length == 0 || k < 0 || k >= arr.length) { + throw new IllegalArgumentException("Invalid input"); + } + return select(arr, 0, arr.length - 1, k); + } + + private static int select(int[] arr, int left, int right, int k) { + if (left == right) { + return arr[left]; + } + + int pivotIndex = getPivotIndex(arr, left, right); + int pivotValue = arr[pivotIndex]; + int partitionIndex = partition(arr, left, right, pivotValue); + + if (k == partitionIndex) { + return arr[k]; + } else if (k < partitionIndex) { + return select(arr, left, partitionIndex - 1, k); + } else { + return select(arr, partitionIndex + 1, right, k); + } + } + + private static int getPivotIndex(int[] arr, int left, int right) { + int n = right - left + 1; + if (n < 5) { + Arrays.sort(arr, left, right + 1); + return left + n / 2; + } + + int numMedians = (int) Math.ceil(n / 5.0); + int[] medians = new int[numMedians]; + + for (int i = 0; i < numMedians; i++) { + int subLeft = left + i * 5; + int subRight = Math.min(subLeft + 4, right); + Arrays.sort(arr, subLeft, subRight + 1); + medians[i] = arr[subLeft + (subRight - subLeft) / 2]; + } + + int medianOfMedians = quickSelect(medians, numMedians / 2); + for (int i = left; i <= right; i++) { + if (arr[i] == medianOfMedians) { + return i; + } + } + return left; // fallback + } + + private static int partition(int[] arr, int left, int right, int pivotValue) { + int i = left; + for (int j = left; j <= right; j++) { + if (arr[j] < pivotValue) { + swap(arr, i, j); + i++; + } + } + + int pivotIndex = i; + for (int j = i; j <= right; j++) { + if (arr[j] == pivotValue) { + swap(arr, j, pivotIndex); + break; + } + } + return pivotIndex; + } + + private static void swap(int[] arr, int i, int j) { + int tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +} diff --git a/src/test/java/com/thealgorithms/divideandconquer/DeterministicQuickSelectTest.java b/src/test/java/com/thealgorithms/divideandconquer/DeterministicQuickSelectTest.java new file mode 100644 index 000000000000..b8260e118fd8 --- /dev/null +++ b/src/test/java/com/thealgorithms/divideandconquer/DeterministicQuickSelectTest.java @@ -0,0 +1,28 @@ +package com.thealgorithms.divideandconquer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public final class DeterministicQuickSelectTest { + + @Test + public void testBasicCases() { + assertEquals(7, QuickSelectMedianOfMedians.quickSelect(new int[] {12, 3, 5, 7, 4, 19, 26}, 3)); + assertEquals(4, QuickSelectMedianOfMedians.quickSelect(new int[] {4, 2, 5, 2, 7, 4, 3}, 4)); + assertEquals(0, QuickSelectMedianOfMedians.quickSelect(new int[] {-5, -10, 0, 5, 10}, 2)); + assertEquals(3, QuickSelectMedianOfMedians.quickSelect(new int[] {8, 3, 1}, 1)); + assertEquals(10, QuickSelectMedianOfMedians.quickSelect(new int[] {10, 20, 30, 40, 50}, 0)); + assertEquals(50, QuickSelectMedianOfMedians.quickSelect(new int[] {10, 20, 30, 40, 50}, 4)); + assertEquals(42, QuickSelectMedianOfMedians.quickSelect(new int[] {42}, 0)); + } + + @Test + public void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, () -> QuickSelectMedianOfMedians.quickSelect(null, 0)); + assertThrows(IllegalArgumentException.class, () -> QuickSelectMedianOfMedians.quickSelect(new int[] {1, 2, 3}, -1)); + assertThrows(IllegalArgumentException.class, () -> QuickSelectMedianOfMedians.quickSelect(new int[] {1, 2, 3}, 3)); + assertThrows(IllegalArgumentException.class, () -> QuickSelectMedianOfMedians.quickSelect(new int[] {}, 0)); + } +}