From 948ada943792e026a5df3fc8c0a3f501ec20f991 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Thu, 2 Apr 2026 21:36:38 -0700 Subject: [PATCH 1/4] Refactor PowerSet: return lists, add overflow guard, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace print-based void methods with generic List-returning methods. Use incremental backtracking list instead of boolean[] used array. Add overflow guard for n > 30 in the binary method. Update README complexity from O(2^n) to O(n · 2^n). Add 9 JUnit 5 tests. Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- .../com/williamfiset/algorithms/other/BUILD | 7 -- .../algorithms/other/PowerSet.java | 111 +++++++----------- .../com/williamfiset/algorithms/other/BUILD | 11 ++ .../algorithms/other/PowerSetTest.java | 88 ++++++++++++++ 5 files changed, 145 insertions(+), 74 deletions(-) create mode 100644 src/test/java/com/williamfiset/algorithms/other/PowerSetTest.java diff --git a/README.md b/README.md index 83f48ee64..62a543244 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [Bit manipulations](src/main/java/com/williamfiset/algorithms/other/BitManipulations.java) **- O(1)** - [List permutations](src/main/java/com/williamfiset/algorithms/other/Permutations.java) **- O(n!)** -- [:movie_camera:](https://www.youtube.com/watch?v=RnlHPR0lyOE) [Power set (set of all subsets)](src/main/java/com/williamfiset/algorithms/other/PowerSet.java) **- O(2n)** +- [:movie_camera:](https://www.youtube.com/watch?v=RnlHPR0lyOE) [Power set (set of all subsets)](src/main/java/com/williamfiset/algorithms/other/PowerSet.java) **- O(n · 2n)** - [Set combinations](src/main/java/com/williamfiset/algorithms/other/Combinations.java) **- O(n choose r)** - [Set combinations with repetition](src/main/java/com/williamfiset/algorithms/other/CombinationsWithRepetition.java) **- O((n+r-1) choose r)** - [Sliding Window Minimum/Maximum](src/main/java/com/williamfiset/algorithms/other/SlidingWindowMaximum.java) **- O(1)** diff --git a/src/main/java/com/williamfiset/algorithms/other/BUILD b/src/main/java/com/williamfiset/algorithms/other/BUILD index 77c05d3da..d2fa5d342 100644 --- a/src/main/java/com/williamfiset/algorithms/other/BUILD +++ b/src/main/java/com/williamfiset/algorithms/other/BUILD @@ -35,13 +35,6 @@ java_binary( runtime_deps = [":other"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/other:PowerSet -java_binary( - name = "PowerSet", - main_class = "com.williamfiset.algorithms.other.PowerSet", - runtime_deps = [":other"], -) - # bazel run //src/main/java/com/williamfiset/algorithms/other:SquareRootDecomposition java_binary( name = "SquareRootDecomposition", diff --git a/src/main/java/com/williamfiset/algorithms/other/PowerSet.java b/src/main/java/com/williamfiset/algorithms/other/PowerSet.java index 5ed95c8b5..0a98489e3 100644 --- a/src/main/java/com/williamfiset/algorithms/other/PowerSet.java +++ b/src/main/java/com/williamfiset/algorithms/other/PowerSet.java @@ -1,84 +1,63 @@ /** - * This code snippet shows how to generate the powerset of a set which is the set of all subsets of - * a set. There are two common ways of doing this which are to use the binary representation of - * numbers on a computer or to do it recursively. Both methods are shown here, pick your flavor! + * Generates the power set of a set, which is the set of all subsets. * - *

Time Complexity: O( 2^n ) + * Two approaches are provided: an iterative method using binary representation of numbers, and a + * recursive backtracking method. Both produce the same result. + * + * Time Complexity: O(n * 2^n) * * @author William Fiset, william.alexandre.fiset@gmail.com */ package com.williamfiset.algorithms.other; -public class PowerSet { - - // Use the fact that numbers represented in binary can be - // used to generate all the subsets of an array - static void powerSetUsingBinary(int[] set) { +import java.util.*; - final int N = set.length; - final int MAX_VAL = 1 << N; +public class PowerSet { - for (int subset = 0; subset < MAX_VAL; subset++) { - System.out.print("{ "); - for (int i = 0; i < N; i++) { - int mask = 1 << i; - if ((subset & mask) == mask) System.out.print(set[i] + " "); + /** + * Generates the power set using binary representation. Each integer from 0 to 2^n - 1 represents + * a subset, where bit i indicates whether element i is included. + */ + public static List> powerSetBinary(List set) { + int n = set.size(); + if (n > 30) + throw new IllegalArgumentException("Set too large: n=" + n + " (max 30)"); + int total = 1 << n; + List> result = new ArrayList<>(total); + + for (int mask = 0; mask < total; mask++) { + List subset = new ArrayList<>(); + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) + subset.add(set.get(i)); } - System.out.println("}"); + result.add(subset); } + return result; } - // Recursively generate the powerset (set of all subsets) of an array by maintaining - // a boolean array used to indicate which element have been selected - static void powerSetRecursive(int at, int[] set, boolean[] used) { - - if (at == set.length) { - - // Print found subset! - System.out.print("{ "); - for (int i = 0; i < set.length; i++) if (used[i]) System.out.print(set[i] + " "); - System.out.println("}"); - - } else { - - // Include this element - used[at] = true; - powerSetRecursive(at + 1, set, used); - - // Backtrack and don't include this element - used[at] = false; - powerSetRecursive(at + 1, set, used); - } + /** + * Generates the power set using recursive backtracking. At each element, branches into including + * or excluding it. + */ + public static List> powerSetRecursive(List set) { + List> result = new ArrayList<>(); + recurse(0, set, new ArrayList<>(), result); + return result; } - public static void main(String[] args) { - - // Example usage: - int[] set = {1, 2, 3}; - - powerSetUsingBinary(set); - // prints: - // { } - // { 1 } - // { 2 } - // { 1 2 } - // { 3 } - // { 1 3 } - // { 2 3 } - // { 1 2 3 } - - System.out.println(); - - powerSetRecursive(0, set, new boolean[set.length]); - // prints: - // { 1 2 3 } - // { 1 2 } - // { 1 3 } - // { 1 } - // { 2 3 } - // { 2 } - // { 3 } - // { } + private static void recurse(int at, List set, List current, List> result) { + if (at == set.size()) { + // Snapshot the current subset — must copy since 'current' is mutated during backtracking + result.add(new ArrayList<>(current)); + return; + } + // Include set[at] and explore all subsets of the remaining elements + current.add(set.get(at)); + recurse(at + 1, set, current, result); + // Backtrack: undo the inclusion of set[at], then explore without it + current.remove(current.size() - 1); + recurse(at + 1, set, current, result); } } diff --git a/src/test/java/com/williamfiset/algorithms/other/BUILD b/src/test/java/com/williamfiset/algorithms/other/BUILD index f11afce4d..134ed57b6 100644 --- a/src/test/java/com/williamfiset/algorithms/other/BUILD +++ b/src/test/java/com/williamfiset/algorithms/other/BUILD @@ -46,3 +46,14 @@ java_test( runtime_deps = JUNIT5_RUNTIME_DEPS, deps = TEST_DEPS, ) + +# bazel test //src/test/java/com/williamfiset/algorithms/other:PowerSetTest +java_test( + name = "PowerSetTest", + srcs = ["PowerSetTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.other.PowerSetTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) diff --git a/src/test/java/com/williamfiset/algorithms/other/PowerSetTest.java b/src/test/java/com/williamfiset/algorithms/other/PowerSetTest.java new file mode 100644 index 000000000..e5243a9b1 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/other/PowerSetTest.java @@ -0,0 +1,88 @@ +package com.williamfiset.algorithms.other; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.*; +import java.util.stream.Collectors; +import org.junit.jupiter.api.*; + +public class PowerSetTest { + + /** Converts a power set to a set of sorted string representations for easy comparison. */ + private static > Set normalize(List> powerSet) { + return powerSet.stream() + .map(subset -> { + List sorted = new ArrayList<>(subset); + Collections.sort(sorted); + return sorted.toString(); + }) + .collect(Collectors.toSet()); + } + + @Test + public void emptySet_binary() { + List> result = PowerSet.powerSetBinary(List.of()); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEmpty(); + } + + @Test + public void emptySet_recursive() { + List> result = PowerSet.powerSetRecursive(List.of()); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEmpty(); + } + + @Test + public void singleElement_binary() { + List> result = PowerSet.powerSetBinary(List.of(42)); + assertThat(normalize(result)).containsExactly("[]", "[42]"); + } + + @Test + public void singleElement_recursive() { + List> result = PowerSet.powerSetRecursive(List.of(42)); + assertThat(normalize(result)).containsExactly("[]", "[42]"); + } + + @Test + public void threeElements_binary() { + List> result = PowerSet.powerSetBinary(List.of(1, 2, 3)); + assertThat(result).hasSize(8); + assertThat(normalize(result)) + .containsExactly("[]", "[1]", "[2]", "[3]", "[1, 2]", "[1, 3]", "[2, 3]", "[1, 2, 3]"); + } + + @Test + public void threeElements_recursive() { + List> result = PowerSet.powerSetRecursive(List.of(1, 2, 3)); + assertThat(result).hasSize(8); + assertThat(normalize(result)) + .containsExactly("[]", "[1]", "[2]", "[3]", "[1, 2]", "[1, 3]", "[2, 3]", "[1, 2, 3]"); + } + + @Test + public void bothMethodsProduceSameSubsets() { + List set = List.of(5, 10, 15, 20); + Set binary = normalize(PowerSet.powerSetBinary(set)); + Set recursive = normalize(PowerSet.powerSetRecursive(set)); + assertThat(binary).isEqualTo(recursive); + } + + @Test + public void correctSize() { + for (int n = 0; n <= 5; n++) { + List set = new ArrayList<>(); + for (int i = 0; i < n; i++) + set.add(i); + assertThat(PowerSet.powerSetBinary(set)).hasSize(1 << n); + assertThat(PowerSet.powerSetRecursive(set)).hasSize(1 << n); + } + } + + @Test + public void worksWithStrings() { + List> result = PowerSet.powerSetBinary(List.of("a", "b")); + assertThat(normalize(result)).containsExactly("[]", "[a]", "[b]", "[a, b]"); + } +} From 0e55a9b638460f195c2bcadb9142d4fb973f4d0a Mon Sep 17 00:00:00 2001 From: William Fiset Date: Thu, 2 Apr 2026 21:41:08 -0700 Subject: [PATCH 2/4] Add main method with example usage to PowerSet Co-Authored-By: Claude Opus 4.6 --- .../java/com/williamfiset/algorithms/other/BUILD | 7 +++++++ .../com/williamfiset/algorithms/other/PowerSet.java | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/com/williamfiset/algorithms/other/BUILD b/src/main/java/com/williamfiset/algorithms/other/BUILD index d2fa5d342..77c05d3da 100644 --- a/src/main/java/com/williamfiset/algorithms/other/BUILD +++ b/src/main/java/com/williamfiset/algorithms/other/BUILD @@ -35,6 +35,13 @@ java_binary( runtime_deps = [":other"], ) +# bazel run //src/main/java/com/williamfiset/algorithms/other:PowerSet +java_binary( + name = "PowerSet", + main_class = "com.williamfiset.algorithms.other.PowerSet", + runtime_deps = [":other"], +) + # bazel run //src/main/java/com/williamfiset/algorithms/other:SquareRootDecomposition java_binary( name = "SquareRootDecomposition", diff --git a/src/main/java/com/williamfiset/algorithms/other/PowerSet.java b/src/main/java/com/williamfiset/algorithms/other/PowerSet.java index 0a98489e3..ff8e6b7f0 100644 --- a/src/main/java/com/williamfiset/algorithms/other/PowerSet.java +++ b/src/main/java/com/williamfiset/algorithms/other/PowerSet.java @@ -60,4 +60,16 @@ private static void recurse(int at, List set, List current, List set = List.of(1, 2, 3); + + System.out.println("Binary method:"); + for (List subset : powerSetBinary(set)) + System.out.println(subset); + + System.out.println("\nRecursive method:"); + for (List subset : powerSetRecursive(set)) + System.out.println(subset); + } } From d57da500be3fd26960a0d0d57605ff37d9e1d171 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Thu, 2 Apr 2026 21:42:33 -0700 Subject: [PATCH 3/4] Remove LazyRangeAdder and its test Co-Authored-By: Claude Opus 4.6 --- README.md | 1 - .../com/williamfiset/algorithms/other/BUILD | 7 -- .../algorithms/other/LazyRangeAdder.java | 71 ---------------- .../com/williamfiset/algorithms/other/BUILD | 10 --- .../algorithms/other/LazyRangeAdderTest.java | 80 ------------------- 5 files changed, 169 deletions(-) delete mode 100644 src/main/java/com/williamfiset/algorithms/other/LazyRangeAdder.java delete mode 100644 src/test/java/com/williamfiset/algorithms/other/LazyRangeAdderTest.java diff --git a/README.md b/README.md index 62a543244..25a5d3f00 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,6 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [Sliding Window Minimum/Maximum](src/main/java/com/williamfiset/algorithms/other/SlidingWindowMaximum.java) **- O(1)** - [Square Root Decomposition](src/main/java/com/williamfiset/algorithms/other/SquareRootDecomposition.java) **- O(1) point updates, O(√n) range queries** - [Unique set combinations](src/main/java/com/williamfiset/algorithms/other/UniqueCombinations.java) **- O(n choose r)** -- [Lazy Range Adder](src/main/java/com/williamfiset/algorithms/other/LazyRangeAdder.java) **- O(1) range updates, O(n) to finalize all updates** # Search algorithms diff --git a/src/main/java/com/williamfiset/algorithms/other/BUILD b/src/main/java/com/williamfiset/algorithms/other/BUILD index 77c05d3da..3a7586729 100644 --- a/src/main/java/com/williamfiset/algorithms/other/BUILD +++ b/src/main/java/com/williamfiset/algorithms/other/BUILD @@ -21,13 +21,6 @@ java_binary( runtime_deps = [":other"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/other:LazyRangeAdder -java_binary( - name = "LazyRangeAdder", - main_class = "com.williamfiset.algorithms.other.LazyRangeAdder", - runtime_deps = [":other"], -) - # bazel run //src/main/java/com/williamfiset/algorithms/other:Permutations java_binary( name = "Permutations", diff --git a/src/main/java/com/williamfiset/algorithms/other/LazyRangeAdder.java b/src/main/java/com/williamfiset/algorithms/other/LazyRangeAdder.java deleted file mode 100644 index 33eaacfab..000000000 --- a/src/main/java/com/williamfiset/algorithms/other/LazyRangeAdder.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * The LazyRangerAdder is a handy class for performing addition range updates of constant values on - * an array. This range adder is especially useful for offline algorithms which know all range - * updates ahead of time. - * - *

Time complexity to update O(1) but time complexity to finalize all additions is O(n) - * - * @author Atharva Thorve, aaathorve@gmail.com - */ -package com.williamfiset.algorithms.other; - -public class LazyRangeAdder { - - // The number of elements in the input array. - private int n; - - // The original input array - private int[] array; - - // The difference array with the deltas between values, size n+1 - private int[] differenceArray; - - // Initialize an instance of a LazyRangeAdder on some input values - public LazyRangeAdder(int[] array) { - this.array = array; - this.n = array.length; - - differenceArray = new int[n + 1]; - differenceArray[0] = array[0]; - for (int i = 1; i < n; i++) { - differenceArray[i] = array[i] - array[i - 1]; - } - } - - // Add `x` to the range [l, r] inclusive - public void add(int l, int r, int x) { - differenceArray[l] += x; - differenceArray[r + 1] -= x; - } - - // IMPORTANT: Make certain to call this method once all the additions - // have been made with add(l, r, x) - public void done() { - for (int i = 0; i < n; i++) { - if (i == 0) { - array[i] = differenceArray[i]; - } else { - array[i] = differenceArray[i] + array[i - 1]; - } - } - } - - public static void main(String[] args) { - // Array to be updated - int[] array = {10, 4, 6, 13, 8, 15, 17, 22}; - LazyRangeAdder lazyRangeAdder = new LazyRangeAdder(array); - - // After below add(l, r, x), the - // elements should become [10, 14, 16, 23, 18, 15, 17, 22] - lazyRangeAdder.add(1, 4, 10); - lazyRangeAdder.done(); - System.out.println(java.util.Arrays.toString(array)); - - // After below add(l, r, x), the - // elements should become [22, 26, 28, 30, 25, 22, 24, 34] - lazyRangeAdder.add(3, 6, -5); - lazyRangeAdder.add(0, 7, 12); - lazyRangeAdder.done(); - System.out.println(java.util.Arrays.toString(array)); - } -} diff --git a/src/test/java/com/williamfiset/algorithms/other/BUILD b/src/test/java/com/williamfiset/algorithms/other/BUILD index 134ed57b6..c41074f2a 100644 --- a/src/test/java/com/williamfiset/algorithms/other/BUILD +++ b/src/test/java/com/williamfiset/algorithms/other/BUILD @@ -17,16 +17,6 @@ TEST_DEPS = [ "@maven//:com_google_truth_truth", ] + JUNIT5_DEPS -java_test( - name = "LazyRangeAdderTest", - srcs = ["LazyRangeAdderTest.java"], - main_class = "org.junit.platform.console.ConsoleLauncher", - use_testrunner = False, - args = ["--select-class=com.williamfiset.algorithms.other.LazyRangeAdderTest"], - runtime_deps = JUNIT5_RUNTIME_DEPS, - deps = TEST_DEPS, -) - java_test( name = "SlidingWindowMaximumTest", srcs = ["SlidingWindowMaximumTest.java"], diff --git a/src/test/java/com/williamfiset/algorithms/other/LazyRangeAdderTest.java b/src/test/java/com/williamfiset/algorithms/other/LazyRangeAdderTest.java deleted file mode 100644 index 357a7bbfd..000000000 --- a/src/test/java/com/williamfiset/algorithms/other/LazyRangeAdderTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.williamfiset.algorithms.other; - -import static com.google.common.truth.Truth.assertThat; - -import org.junit.jupiter.api.Test; - -public class LazyRangeAdderTest { - - @Test - public void rangeUpdateTest1() { - int[] a = {10, 5, 20, 40}; - LazyRangeAdder lazyRangeAdder = new LazyRangeAdder(a); - lazyRangeAdder.add(0, 1, 10); - lazyRangeAdder.add(1, 3, 20); - lazyRangeAdder.add(2, 2, 30); - lazyRangeAdder.done(); - int[] expected = {20, 35, 70, 60}; - assertThat(a).isEqualTo(expected); - } - - @Test - public void rangeUpdateTest2() { - int[] a = {270, 311, 427, 535, 334, 193, 174}; - LazyRangeAdder lazyRangeAdder = new LazyRangeAdder(a); - lazyRangeAdder.add(2, 5, 32); - lazyRangeAdder.add(0, 4, 101); - lazyRangeAdder.add(5, 6, -73); - lazyRangeAdder.done(); - int[] expected = {371, 412, 560, 668, 467, 152, 101}; - assertThat(a).isEqualTo(expected); - } - - @Test - public void randomRangeAdditionTests() { - // Try several different array sizes - for (int n = 1; n < 1000; n++) { - - int[] arr1 = new int[n]; - randomFill(arr1); - int[] arr2 = arr1.clone(); - - LazyRangeAdder lazyRangeAdder = new LazyRangeAdder(arr1); - - // Do 50 random range adds - for (int i = 0; i < 50; i++) { - // Generate a random range - int l = randValue(0, n); - int r = randValue(0, n); - l = Math.min(l, r); - r = Math.max(l, r); - - int x = randValue(-100, 100); - lazyRangeAdder.add(l, r, x); - slowRangeAdd(arr2, l, r, x); - } - - lazyRangeAdder.done(); - - assertThat(arr1).isEqualTo(arr2); - } - } - - // Adds `x` to the range [l, r] in arr - private static void slowRangeAdd(int[] arr, int l, int r, int x) { - for (int i = l; i <= r; i++) { - arr[i] += x; - } - } - - private static void randomFill(int[] arr) { - for (int i = 0; i < arr.length; i++) { - arr[i] = randValue(0, 1000); - } - } - - // Generates a random number between [min, max) - private static int randValue(int min, int max) { - return min + (int) (Math.random() * ((max - min))); - } -} From 69b49f7436afeff2b7572c9e7e9c58b9ad001307 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Thu, 2 Apr 2026 21:52:11 -0700 Subject: [PATCH 4/4] Add Tim sort implementation with tests Hybrid merge sort + insertion sort: splits array into runs of 32, sorts each with insertion sort, then iteratively merges. Stable, O(n log n) worst case, O(n) best case. Implements InplaceSort and is integrated into the shared SortingTest. Co-Authored-By: Claude Opus 4.6 --- README.md | 1 + .../com/williamfiset/algorithms/sorting/BUILD | 7 ++ .../algorithms/sorting/TimSort.java | 90 +++++++++++++++ .../com/williamfiset/algorithms/sorting/BUILD | 10 ++ .../algorithms/sorting/SortingTest.java | 6 +- .../algorithms/sorting/TimSortTest.java | 103 ++++++++++++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/williamfiset/algorithms/sorting/TimSort.java create mode 100644 src/test/java/com/williamfiset/algorithms/sorting/TimSortTest.java diff --git a/README.md b/README.md index 25a5d3f00..72679b39a 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [Quicksort (in-place, Hoare partitioning)](src/main/java/com/williamfiset/algorithms/sorting/QuickSort.java) **- Θ(nlog(n))** - [Quicksort3 (Dutch National Flag algorithm)](src/main/java/com/williamfiset/algorithms/sorting/QuickSort3.java) **- Θ(nlog(n))** - [Selection sort](src/main/java/com/williamfiset/algorithms/sorting/SelectionSort.java) **- O(n2)** +- [Tim sort](src/main/java/com/williamfiset/algorithms/sorting/TimSort.java) **- O(nlog(n))** - [Radix sort](src/main/java/com/williamfiset/algorithms/sorting/RadixSort.java) **- O(n\*w)** # String algorithms diff --git a/src/main/java/com/williamfiset/algorithms/sorting/BUILD b/src/main/java/com/williamfiset/algorithms/sorting/BUILD index 1c3df657c..91238898b 100644 --- a/src/main/java/com/williamfiset/algorithms/sorting/BUILD +++ b/src/main/java/com/williamfiset/algorithms/sorting/BUILD @@ -77,6 +77,13 @@ java_binary( runtime_deps = [":sorting"], ) +# bazel run //src/main/java/com/williamfiset/algorithms/sorting:TimSort +java_binary( + name = "TimSort", + main_class = "com.williamfiset.algorithms.sorting.TimSort", + runtime_deps = [":sorting"], +) + # bazel run //src/main/java/com/williamfiset/algorithms/sorting:SelectionSort java_binary( name = "SelectionSort", diff --git a/src/main/java/com/williamfiset/algorithms/sorting/TimSort.java b/src/main/java/com/williamfiset/algorithms/sorting/TimSort.java new file mode 100644 index 000000000..b98fb5870 --- /dev/null +++ b/src/main/java/com/williamfiset/algorithms/sorting/TimSort.java @@ -0,0 +1,90 @@ +/** + * Tim sort implementation — a hybrid sorting algorithm combining merge sort and insertion sort. + * + * Tim sort divides the array into small chunks called "runs" and sorts each run using insertion + * sort (which is efficient for small or nearly-sorted data). It then merges the runs using a + * merge step similar to merge sort. This is the algorithm used by Java's Arrays.sort() for objects + * and Python's built-in sort. + * + * Time Complexity: O(n log n) worst case, O(n) best case (already sorted) + * Space Complexity: O(n) for the merge buffer + * + * @author Claude + */ +package com.williamfiset.algorithms.sorting; + +public class TimSort implements InplaceSort { + + private static final int MIN_RUN = 32; + + @Override + public void sort(int[] values) { + timSort(values); + } + + public static void timSort(int[] ar) { + if (ar == null || ar.length <= 1) + return; + + int n = ar.length; + + // Sort individual runs using insertion sort + for (int i = 0; i < n; i += MIN_RUN) + insertionSort(ar, i, Math.min(i + MIN_RUN - 1, n - 1)); + + // Merge runs, doubling the merge size each iteration + for (int size = MIN_RUN; size < n; size *= 2) { + for (int left = 0; left < n; left += 2 * size) { + int mid = Math.min(left + size - 1, n - 1); + int right = Math.min(left + 2 * size - 1, n - 1); + if (mid < right) + merge(ar, left, mid, right); + } + } + } + + /** Insertion sort on ar[lo..hi] inclusive. */ + private static void insertionSort(int[] ar, int lo, int hi) { + for (int i = lo + 1; i <= hi; i++) { + int key = ar[i]; + int j = i - 1; + while (j >= lo && ar[j] > key) { + ar[j + 1] = ar[j]; + j--; + } + ar[j + 1] = key; + } + } + + /** Merges two sorted sub-arrays ar[lo..mid] and ar[mid+1..hi]. */ + private static void merge(int[] ar, int lo, int mid, int hi) { + int len1 = mid - lo + 1; + int len2 = hi - mid; + + int[] left = new int[len1]; + int[] right = new int[len2]; + System.arraycopy(ar, lo, left, 0, len1); + System.arraycopy(ar, mid + 1, right, 0, len2); + + int i = 0, j = 0, k = lo; + while (i < len1 && j < len2) { + // Use <= to maintain stability: equal elements from the left run come first + if (left[i] <= right[j]) + ar[k++] = left[i++]; + else + ar[k++] = right[j++]; + } + while (i < len1) + ar[k++] = left[i++]; + while (j < len2) + ar[k++] = right[j++]; + } + + public static void main(String[] args) { + int[] array = {10, 4, 6, 4, 8, -13, 2, 3}; + timSort(array); + // Prints: + // [-13, 2, 3, 4, 4, 6, 8, 10] + System.out.println(java.util.Arrays.toString(array)); + } +} diff --git a/src/test/java/com/williamfiset/algorithms/sorting/BUILD b/src/test/java/com/williamfiset/algorithms/sorting/BUILD index 51be23a74..092e69fe2 100644 --- a/src/test/java/com/williamfiset/algorithms/sorting/BUILD +++ b/src/test/java/com/williamfiset/algorithms/sorting/BUILD @@ -127,6 +127,16 @@ java_test( deps = TEST_DEPS, ) +java_test( + name = "TimSortTest", + srcs = ["TimSortTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.sorting.TimSortTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + java_test( name = "SortingTest", srcs = ["SortingTest.java"], diff --git a/src/test/java/com/williamfiset/algorithms/sorting/SortingTest.java b/src/test/java/com/williamfiset/algorithms/sorting/SortingTest.java index 94a5765f6..3ff9b497f 100644 --- a/src/test/java/com/williamfiset/algorithms/sorting/SortingTest.java +++ b/src/test/java/com/williamfiset/algorithms/sorting/SortingTest.java @@ -25,7 +25,8 @@ enum SortingAlgorithm { QUICK_SORT(new QuickSort()), QUICK_SORT3(new QuickSort3()), RADIX_SORT(new RadixSort()), - SELECTION_SORT(new SelectionSort()); + SELECTION_SORT(new SelectionSort()), + TIM_SORT(new TimSort()); private InplaceSort algorithm; @@ -49,7 +50,8 @@ public InplaceSort getSortingAlgorithm() { SortingAlgorithm.QUICK_SORT, SortingAlgorithm.QUICK_SORT3, SortingAlgorithm.RADIX_SORT, - SortingAlgorithm.SELECTION_SORT); + SortingAlgorithm.SELECTION_SORT, + SortingAlgorithm.TIM_SORT); @Test public void verifySortingAlgorithms_smallPositiveIntegersOnly() { diff --git a/src/test/java/com/williamfiset/algorithms/sorting/TimSortTest.java b/src/test/java/com/williamfiset/algorithms/sorting/TimSortTest.java new file mode 100644 index 000000000..f31c56d51 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/sorting/TimSortTest.java @@ -0,0 +1,103 @@ +package com.williamfiset.algorithms.sorting; + +import static com.google.common.truth.Truth.assertThat; + +import com.williamfiset.algorithms.utils.TestUtils; +import java.util.Arrays; +import org.junit.jupiter.api.*; + +public class TimSortTest { + + private final TimSort sorter = new TimSort(); + + @Test + public void testEmptyArray() { + int[] array = {}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {}); + } + + @Test + public void testSingleElement() { + int[] array = {42}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {42}); + } + + @Test + public void testAlreadySorted() { + int[] array = {1, 2, 3, 4, 5}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {1, 2, 3, 4, 5}); + } + + @Test + public void testReverseSorted() { + int[] array = {5, 4, 3, 2, 1}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {1, 2, 3, 4, 5}); + } + + @Test + public void testWithDuplicates() { + int[] array = {10, 4, 6, 4, 8, -13, 2, 3}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {-13, 2, 3, 4, 4, 6, 8, 10}); + } + + @Test + public void testAllSameElements() { + int[] array = {3, 3, 3, 3, 3}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {3, 3, 3, 3, 3}); + } + + @Test + public void testNegativeNumbers() { + int[] array = {-3, -1, -4, -1, -5}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {-5, -4, -3, -1, -1}); + } + + @Test + public void testMixedPositiveAndNegative() { + int[] array = {3, -2, 0, 7, -5, 1}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {-5, -2, 0, 1, 3, 7}); + } + + @Test + public void testTwoElements() { + int[] array = {9, 1}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {1, 9}); + } + + @Test + public void testSmallerThanMinRun() { + int[] array = {5, 3, 8, 1, 9, 2, 7}; + sorter.sort(array); + assertThat(array).isEqualTo(new int[] {1, 2, 3, 5, 7, 8, 9}); + } + + @Test + public void testLargerThanMinRun() { + // 100 elements to exercise the merge phase + int[] array = TestUtils.randomIntegerArray(100, -50, 51); + int[] expected = array.clone(); + Arrays.sort(expected); + sorter.sort(array); + assertThat(array).isEqualTo(expected); + } + + @Test + public void testRandomized() { + for (int size = 0; size < 500; size++) { + int[] values = TestUtils.randomIntegerArray(size, -50, 51); + int[] expected = values.clone(); + Arrays.sort(expected); + sorter.sort(values); + assertThat(values).isEqualTo(expected); + } + } +}