From cb6f438d08e15b1d99778911cabfff966f130211 Mon Sep 17 00:00:00 2001 From: datpp Date: Fri, 9 Oct 2026 16:13:12 +0700 Subject: [PATCH 01/34] test: add white-box unit tests for Luhn.luhnCheck (#7641) * test: add white-box unit tests for Luhn.luhnCheck Cover the loop (zero, one and many iterations), both outcomes of the `i % 2 == 0` and `temp > 9` decisions, boundary values (8 / 10 after doubling), valid and invalid checksums, input immutability and null input. * test: drop null input case flagged by Infer Infer reports NULLPTR_DEREFERENCE for passing null to luhnCheck on purpose. The method does not document null handling, so the case is removed. --- .../com/thealgorithms/others/LuhnTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/test/java/com/thealgorithms/others/LuhnTest.java diff --git a/src/test/java/com/thealgorithms/others/LuhnTest.java b/src/test/java/com/thealgorithms/others/LuhnTest.java new file mode 100644 index 000000000000..2e2c294a3352 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/LuhnTest.java @@ -0,0 +1,105 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * White-box tests for {@link Luhn#luhnCheck(int[])}. + * + *

The method has one loop and two decisions: + *

+ * The tests below cover the loop executed zero, one and many times, both + * outcomes of each decision, and both outcomes of the final {@code sum % 10 == 0}. + */ +class LuhnTest { + + @Test + void testEmptyArraySkipsLoopAndIsValid() { + // Loop body never executes, sum stays 0 + assertTrue(Luhn.luhnCheck(new int[] {})); + } + + @Test + void testSingleZeroDigitIsValid() { + // One iteration: D1 true, D2 false (0 * 2 = 0) + assertTrue(Luhn.luhnCheck(new int[] {0})); + } + + @Test + void testSingleDigitDoubledWithoutSubtractionIsInvalid() { + // One iteration: D1 true, D2 false (4 * 2 = 8, upper boundary of temp <= 9) + assertFalse(Luhn.luhnCheck(new int[] {4})); + } + + @Test + void testSingleDigitDoubledWithSubtractionIsInvalid() { + // One iteration: D1 true, D2 true (5 * 2 = 10, lower boundary of temp > 9 -> 1) + assertFalse(Luhn.luhnCheck(new int[] {5})); + } + + @Test + void testTwoDigitsWithoutSubtraction() { + // i = 1: D1 false -> 2; i = 0: D1 true, D2 false -> 8; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {4, 2})); + } + + @Test + void testTwoDigitsWithSubtraction() { + // i = 1: D1 false -> 9; i = 0: D1 true, D2 true -> 10 - 9 = 1; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {5, 9})); + } + + @Test + void testTwoDigitsInvalidChecksum() { + // i = 1: D1 false -> 3; i = 0: D1 true, D2 false -> 8; sum = 11 + assertFalse(Luhn.luhnCheck(new int[] {4, 3})); + } + + @Test + void testMaxDigitDoubled() { + // i = 1: D1 false -> 0; i = 0: D1 true, D2 true -> 18 - 9 = 9; sum = 9 + assertFalse(Luhn.luhnCheck(new int[] {9, 0})); + // i = 1: D1 false -> 1; i = 0: D1 true, D2 true -> 9; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {9, 1})); + } + + @Test + void testValidCardNumbers() { + // Many iterations, all branch combinations exercised + assertTrue(Luhn.luhnCheck(new int[] {4, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + assertTrue(Luhn.luhnCheck(new int[] {5, 2, 6, 5, 9, 2, 5, 1, 6, 1, 5, 1, 1, 4, 1, 2})); + assertTrue(Luhn.luhnCheck(new int[] {4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})); + } + + @Test + void testCardNumberWithTypoInLastDigitIsInvalid() { + assertFalse(Luhn.luhnCheck(new int[] {4, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 4})); + } + + @Test + void testCardNumberWithSingleDigitErrorIsInvalid() { + // Error in a doubled position (index 0) + assertFalse(Luhn.luhnCheck(new int[] {3, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + // Error in a non-doubled position (index 1) + assertFalse(Luhn.luhnCheck(new int[] {4, 6, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + } + + @Test + void testAllZerosIsValid() { + assertTrue(Luhn.luhnCheck(new int[16])); + } + + @Test + void testInputArrayIsNotModified() { + int[] digits = {5, 2, 6, 5, 9, 2, 5, 1, 6, 1, 5, 1, 1, 4, 1, 2}; + int[] copy = digits.clone(); + Luhn.luhnCheck(digits); + assertArrayEquals(copy, digits); + } +} From 5eb9d6634cfe84b1d53daf83187f8d11eca0bbb8 Mon Sep 17 00:00:00 2001 From: Oleksandr Klymenko Date: Fri, 9 Oct 2026 12:17:42 +0300 Subject: [PATCH 02/34] feat: add TDigest, every quantile of a stream from one sketch (#7642) P2QuantileEstimator tracks one quantile chosen in advance, in constant memory. A t-digest keeps a small list of centroids, each a mean and a weight, sorted by mean, and answers any quantile and the CDF afterwards from the same sketch. A centroid may absorb points only while it spans at most one unit of the scale function k(q) = d / (2 pi) * asin(2q - 1), and asin is steep at both ends, so centroids in the tails stay at one or a few points while those near the median are allowed to grow. The rank error is of the order of 1 / d around the median and shrinks towards q = 0 and q = 1, which is the trade latency percentiles want: p99.9 comes out nearly exact from a sketch of a few kilobytes. Insertions are buffered and folded into the centroid list in batches, so add is O(log d) amortised; quantile and cdf are O(d) and interpolate linearly between centroids, each treated as sitting at the centre of the weight it carries. The minimum and maximum are tracked separately and returned exactly for q = 0 and q = 1. Two digests merge the way partial sums do, so sketches built on separate shards combine into one. Non-finite samples, invalid weights, quantiles outside [0, 1] and a compression below 10 are refused; queries fold the pending buffer in first, so the class is documented as not thread-safe. Tests: on 100 000 uniform samples every tested quantile lands within 0.01 of the requested rank; on 200 000 Gaussian samples the rank error stays under a bound that narrows towards the tails, from 0.01 at the median to 0.0005 at q = 0.001 and q = 0.999; three shards merged answer within 0.01 of the true rank, with total weight, minimum and maximum exact; the centroid count stays at or below the compression after 200 000 samples for compressions of 20, 100 and 500; and weighted, sorted, constant and heavily duplicated input are covered. Signed-off-by: alxkm <19151554+alxkm@users.noreply.github.com> Co-authored-by: alxkm <19151554+alxkm@users.noreply.github.com> --- .../com/thealgorithms/streaming/TDigest.java | 540 ++++++++++++++++++ .../thealgorithms/streaming/TDigestTest.java | 391 +++++++++++++ 2 files changed, 931 insertions(+) create mode 100644 src/main/java/com/thealgorithms/streaming/TDigest.java create mode 100644 src/test/java/com/thealgorithms/streaming/TDigestTest.java diff --git a/src/main/java/com/thealgorithms/streaming/TDigest.java b/src/main/java/com/thealgorithms/streaming/TDigest.java new file mode 100644 index 000000000000..b1c826b1857c --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/TDigest.java @@ -0,0 +1,540 @@ +package com.thealgorithms.streaming; + +import java.util.Arrays; + +/** + * A t-digest: a small, mergeable sketch that answers any quantile of a stream, with + * much better accuracy at the tails than in the middle. + * + *

The sketch is a list of centroids, each holding a mean and a weight, kept sorted by mean. + * Together they approximate the distribution of everything ever added. What makes a t-digest more + * than a histogram is the rule that decides how big a centroid is allowed to be: instead of a fixed + * bucket width, the size limit is expressed through a scale function + * + *

+ * k(q) = compression / (2 * pi) * asin(2q - 1)
+ * 
+ * + *

and a centroid may absorb points only while it spans at most one unit of {@code k}. Because + * {@code asin} is steep near {@code q = 0} and {@code q = 1}, centroids near the tails are forced to + * stay tiny - often a single point - while centroids near the median are allowed to grow large. + * That is exactly the trade one wants in practice: the rank error is of the order of + * {@code 1 / compression} around the median and shrinks as {@code q} approaches 0 or 1, so + * {@code p99.9} comes out nearly exact. On smooth data the interpolation between centroids brings + * the error down by another order of magnitude, and the whole sketch still fits in a few kilobytes. + * + *

Insertions are buffered and folded into the centroid list in batches, which keeps the amortised + * cost per sample low. The minimum and the maximum are tracked separately and reported exactly. + * + * + * + * + * + * + * + * + *
Cost, with {@code d} the compression parameter
OperationComplexity
{@link #add(double)}O(log d) amortised
{@link #quantile(double)}, {@link #cdf(double)}O(d)
{@link #merge(TDigest)}O(d log d)
memoryO(d), independent of the stream length
+ * + *

Usage

+ * + *
{@code
+ * TDigest digest = new TDigest(100);
+ * for (double latency : latencies) {
+ *     digest.add(latency);
+ * }
+ * digest.quantile(0.5);   // median
+ * digest.quantile(0.999); // deep tail, the case t-digest is built for
+ * digest.cdf(250.0);      // share of samples below 250
+ *
+ * // Sketches computed on different shards combine the way partial sums do.
+ * shardA.merge(shardB);
+ * }
+ * + *

Query methods fold the pending insertion buffer in before answering, so they mutate internal + * state; they are not safe to call concurrently. This class is not thread-safe. + * + * @see P2QuantileEstimator for a constant-memory estimator of one fixed quantile + * @see T. Dunning, O. Ertl, Computing extremely accurate quantiles using t-digests + */ +public final class TDigest { + + /** Below roughly this value the scale function stops leaving room for a useful number of centroids. */ + private static final double MIN_COMPRESSION = 10.0; + + private static final double TWO_PI = 2.0 * Math.PI; + + /** Buffered points per centroid slot; larger buffers mean fewer, bigger merge passes. */ + private static final int BUFFER_FACTOR = 5; + + /** + * How many centroid slots to reserve per unit of compression. Repeated merges make centroids + * atomic, so they pack less tightly than the scale function alone would suggest: in practice the + * sketch settles around {@code 0.6 * compression} centroids rather than the ideal + * {@code compression / 2}. Twice the compression leaves ample headroom. + */ + private static final int CENTROID_SLOTS_PER_UNIT = 2; + + /** Below this length the parallel sort switches to insertion sort. */ + private static final int INSERTION_SORT_THRESHOLD = 16; + + private final double compression; + + private final double[] centroidMeans; + private final double[] centroidWeights; + private int centroidCount; + + private final double[] bufferMeans; + private final double[] bufferWeights; + private int bufferCount; + + private final double[] scratchMeans; + private final double[] scratchWeights; + + private double totalWeight; + private double min = Double.POSITIVE_INFINITY; + private double max = Double.NEGATIVE_INFINITY; + + /** + * Creates an empty digest with a compression of 100, a good default for most streams. + */ + public TDigest() { + this(100.0); + } + + /** + * Creates an empty digest. + * + * @param compression the size/accuracy trade-off; around {@code 0.6 * compression} centroids end + * up being kept, so larger values cost more memory and give smaller quantile errors + * @throws IllegalArgumentException if {@code compression} is smaller than 10 or not finite + */ + public TDigest(double compression) { + if (!(compression >= MIN_COMPRESSION) || !Double.isFinite(compression)) { + throw new IllegalArgumentException("The compression must be finite and at least " + MIN_COMPRESSION + ", but was " + compression); + } + this.compression = compression; + + int scale = (int) Math.ceil(compression) + 2; + int centroidCapacity = CENTROID_SLOTS_PER_UNIT * scale; + this.centroidMeans = new double[centroidCapacity]; + this.centroidWeights = new double[centroidCapacity]; + + int bufferCapacity = BUFFER_FACTOR * scale; + this.bufferMeans = new double[bufferCapacity]; + this.bufferWeights = new double[bufferCapacity]; + + this.scratchMeans = new double[centroidCapacity + bufferCapacity]; + this.scratchWeights = new double[centroidCapacity + bufferCapacity]; + } + + /** + * Adds one sample of unit weight. + * + * @param value the sample to add + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public void add(double value) { + add(value, 1.0); + } + + /** + * Adds a sample that stands for several observations of the same value. + * + * @param value the sample to add + * @param weight how many observations the sample represents, strictly positive + * @throws IllegalArgumentException if {@code value} is not finite or {@code weight} is not strictly positive + */ + public void add(double value, double weight) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + if (!(weight > 0.0) || !Double.isFinite(weight)) { + throw new IllegalArgumentException("The weight must be finite and positive, but was " + weight); + } + if (bufferCount == bufferMeans.length) { + flushBuffer(); + } + bufferMeans[bufferCount] = value; + bufferWeights[bufferCount] = weight; + bufferCount++; + min = Math.min(min, value); + max = Math.max(max, value); + } + + /** + * Adds every given sample with unit weight. + * + * @param values the samples to add + * @throws IllegalArgumentException if any value is NaN or infinite + * @throws NullPointerException if {@code values} is {@code null} + */ + public void addAll(double... values) { + for (double value : values) { + add(value); + } + } + + /** + * Folds another digest into this one. The result approximates the union of the two streams, which + * is what makes t-digests usable as a map-reduce style aggregate. + * + * @param other the digest to absorb; it is left unchanged apart from its pending buffer being folded in + * @throws NullPointerException if {@code other} is {@code null} + */ + public void merge(TDigest other) { + other.flushBuffer(); + for (int i = 0; i < other.centroidCount; i++) { + add(other.centroidMeans[i], other.centroidWeights[i]); + } + if (!other.isEmpty()) { + min = Math.min(min, other.min); + max = Math.max(max, other.max); + } + } + + /** + * Estimates the value below which the given fraction of the stream lies. + * + * @param q the requested quantile, between 0 and 1 inclusive + * @return the estimated quantile; exactly {@link #min()} for {@code q == 0} and {@link #max()} for {@code q == 1} + * @throws IllegalArgumentException if {@code q} is outside {@code [0, 1]} or is NaN + * @throws IllegalStateException if no sample has been added yet + */ + public double quantile(double q) { + if (!(q >= 0.0) || !(q <= 1.0)) { + throw new IllegalArgumentException("The quantile probability must lie in [0, 1], but was " + q); + } + flushBuffer(); + requireNonEmpty(); + if (centroidCount == 1) { + return centroidMeans[0]; + } + + double index = q * totalWeight; + if (index <= 0.0) { + return min; + } + if (index >= totalWeight) { + return max; + } + + // The centroids are treated as knots of a piecewise linear quantile function, each sitting at + // the centre of the weight it carries, with min and max closing the two ends. + double previousValue = min; + double previousIndex = 0.0; + double currentIndex = 0.0; + for (int i = 0; i < centroidCount; i++) { + currentIndex += i == 0 ? centroidWeights[0] / 2.0 : (centroidWeights[i - 1] + centroidWeights[i]) / 2.0; + if (index < currentIndex) { + return interpolate(previousIndex, previousValue, currentIndex, centroidMeans[i], index); + } + previousValue = centroidMeans[i]; + previousIndex = currentIndex; + } + return interpolate(previousIndex, previousValue, totalWeight, max, index); + } + + /** + * Estimates the fraction of the stream that is smaller than the given value, i.e. the empirical + * cumulative distribution function. + * + * @param value the value to look up + * @return a number in {@code [0, 1]} + * @throws IllegalArgumentException if {@code value} is NaN + * @throws IllegalStateException if no sample has been added yet + */ + public double cdf(double value) { + if (Double.isNaN(value)) { + throw new IllegalArgumentException("The value must not be NaN"); + } + flushBuffer(); + requireNonEmpty(); + if (value < min) { + return 0.0; + } + if (value > max) { + return 1.0; + } + if (max <= min) { + return 0.5; + } + + double previousValue = min; + double previousIndex = 0.0; + double currentIndex = 0.0; + for (int i = 0; i < centroidCount; i++) { + currentIndex += i == 0 ? centroidWeights[0] / 2.0 : (centroidWeights[i - 1] + centroidWeights[i]) / 2.0; + if (value < centroidMeans[i]) { + return interpolate(previousValue, previousIndex, centroidMeans[i], currentIndex, value) / totalWeight; + } + previousValue = centroidMeans[i]; + previousIndex = currentIndex; + } + return interpolate(previousValue, previousIndex, max, totalWeight, value) / totalWeight; + } + + /** + * Returns the smallest sample ever added, tracked exactly. + * + * @return the minimum + * @throws IllegalStateException if no sample has been added yet + */ + public double min() { + requireAnySample(); + return min; + } + + /** + * Returns the largest sample ever added, tracked exactly. + * + * @return the maximum + * @throws IllegalStateException if no sample has been added yet + */ + public double max() { + requireAnySample(); + return max; + } + + /** + * Returns the total weight of everything added, which for unweighted input is the number of + * samples. + * + * @return the accumulated weight + */ + public double totalWeight() { + double pending = 0.0; + for (int i = 0; i < bufferCount; i++) { + pending += bufferWeights[i]; + } + return totalWeight + pending; + } + + /** + * Returns how many centroids the sketch currently holds, after folding in pending insertions. + * + * @return the number of centroids, bounded by roughly {@code compression / 2} + */ + public int centroidCount() { + flushBuffer(); + return centroidCount; + } + + /** + * Returns the compression parameter. + * + * @return the value given at construction time + */ + public double compression() { + return compression; + } + + /** + * Tells whether the digest holds no samples. + * + * @return {@code true} if nothing has been added + */ + public boolean isEmpty() { + return centroidCount == 0 && bufferCount == 0; + } + + /** + * Forgets every sample. + */ + public void reset() { + centroidCount = 0; + bufferCount = 0; + totalWeight = 0.0; + min = Double.POSITIVE_INFINITY; + max = Double.NEGATIVE_INFINITY; + } + + @Override + public String toString() { + return "TDigest{compression=" + compression + ", weight=" + totalWeight() + ", centroids=" + (isEmpty() ? 0 : centroidCount()) + '}'; + } + + /** + * Folds every buffered point into the centroid list. + * + *

Buffer and centroids are concatenated, sorted by mean and then swept once from the smallest + * mean upwards. The sweep keeps filling the current output centroid while doing so would not push + * it past the weight limit given by the scale function at the quantile the centroid starts at; + * otherwise the centroid is closed and a new one is opened. + */ + private void flushBuffer() { + if (bufferCount == 0) { + return; + } + + int size = centroidCount + bufferCount; + System.arraycopy(centroidMeans, 0, scratchMeans, 0, centroidCount); + System.arraycopy(centroidWeights, 0, scratchWeights, 0, centroidCount); + System.arraycopy(bufferMeans, 0, scratchMeans, centroidCount, bufferCount); + System.arraycopy(bufferWeights, 0, scratchWeights, centroidCount, bufferCount); + bufferCount = 0; + sortByMean(scratchMeans, scratchWeights, 0, size - 1); + + double weight = 0.0; + for (int i = 0; i < size; i++) { + weight += scratchWeights[i]; + } + totalWeight = weight; + + int last = 0; + centroidMeans[0] = scratchMeans[0]; + centroidWeights[0] = scratchWeights[0]; + double weightOfClosedCentroids = 0.0; + double weightLimit = totalWeight * quantileLimit(0.0); + + for (int i = 1; i < size; i++) { + double projected = centroidWeights[last] + scratchWeights[i]; + boolean roomLeft = weightOfClosedCentroids + projected <= weightLimit; + if (roomLeft || last == centroidMeans.length - 1) { + centroidWeights[last] = projected; + centroidMeans[last] += (scratchMeans[i] - centroidMeans[last]) * scratchWeights[i] / projected; + } else { + weightOfClosedCentroids += centroidWeights[last]; + weightLimit = totalWeight * quantileLimit(weightOfClosedCentroids / totalWeight); + last++; + centroidMeans[last] = scratchMeans[i]; + centroidWeights[last] = scratchWeights[i]; + } + } + centroidCount = last + 1; + } + + /** + * Returns the quantile at which the centroid starting at quantile {@code q} must be closed, that + * is {@code kInverse(k(q) + 1)} for the scale function {@code k}. + */ + private double quantileLimit(double q) { + // Rounding in the running weight sum could push the ratio a hair outside [0, 1], which would + // turn asin into NaN and silently disable the size limit. + double bounded = Math.min(1.0, Math.max(0.0, q)); + double k = compression / TWO_PI * Math.asin(2.0 * bounded - 1.0) + 1.0; + double angle = k * TWO_PI / compression; + if (angle >= Math.PI / 2.0) { + return 1.0; + } + if (angle <= -Math.PI / 2.0) { + return 0.0; + } + return (Math.sin(angle) + 1.0) / 2.0; + } + + private void requireNonEmpty() { + if (centroidCount == 0) { + throw new IllegalStateException("The digest has not seen any sample yet"); + } + } + + private void requireAnySample() { + if (isEmpty()) { + throw new IllegalStateException("The digest has not seen any sample yet"); + } + } + + /** + * Linear interpolation between two knots, degenerating to the right knot when they coincide. + */ + private static double interpolate(double x0, double y0, double x1, double y1, double x) { + if (x1 <= x0) { + return y1; + } + return y0 + (x - x0) / (x1 - x0) * (y1 - y0); + } + + /** + * Sorts two parallel arrays by the values of the first one, using quicksort with a + * median-of-three pivot and an insertion sort for short ranges. + */ + private static void sortByMean(double[] means, double[] weights, int from, int to) { + int low = from; + int high = to; + while (low < high) { + if (high - low < INSERTION_SORT_THRESHOLD) { + insertionSort(means, weights, low, high); + return; + } + int pivotIndex = partition(means, weights, low, high); + // Recurse into the smaller half and loop on the larger one to keep the stack shallow. + if (pivotIndex - low < high - pivotIndex) { + sortByMean(means, weights, low, pivotIndex - 1); + low = pivotIndex + 1; + } else { + sortByMean(means, weights, pivotIndex + 1, high); + high = pivotIndex - 1; + } + } + } + + private static int partition(double[] means, double[] weights, int low, int high) { + int middle = low + (high - low) / 2; + if (means[middle] < means[low]) { + swap(means, weights, low, middle); + } + if (means[high] < means[low]) { + swap(means, weights, low, high); + } + if (means[high] < means[middle]) { + swap(means, weights, middle, high); + } + swap(means, weights, middle, high - 1); + double pivot = means[high - 1]; + + int store = low; + for (int i = low; i < high - 1; i++) { + if (means[i] < pivot) { + swap(means, weights, store, i); + store++; + } + } + swap(means, weights, store, high - 1); + return store; + } + + private static void insertionSort(double[] means, double[] weights, int low, int high) { + for (int i = low + 1; i <= high; i++) { + double mean = means[i]; + double weight = weights[i]; + int j = i - 1; + while (j >= low && means[j] > mean) { + means[j + 1] = means[j]; + weights[j + 1] = weights[j]; + j--; + } + means[j + 1] = mean; + weights[j + 1] = weight; + } + } + + private static void swap(double[] means, double[] weights, int i, int j) { + double mean = means[i]; + means[i] = means[j]; + means[j] = mean; + double weight = weights[i]; + weights[i] = weights[j]; + weights[j] = weight; + } + + /** + * Returns the centroid means, smallest first. Exposed for tests and for inspecting the shape of + * the sketch; the returned array is a copy. + * + * @return a copy of the centroid means + */ + double[] centroidMeansSnapshot() { + flushBuffer(); + return Arrays.copyOf(centroidMeans, centroidCount); + } + + /** + * Returns the centroid weights, aligned with {@link #centroidMeansSnapshot()}. + * + * @return a copy of the centroid weights + */ + double[] centroidWeightsSnapshot() { + flushBuffer(); + return Arrays.copyOf(centroidWeights, centroidCount); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/TDigestTest.java b/src/test/java/com/thealgorithms/streaming/TDigestTest.java new file mode 100644 index 000000000000..6968513824c9 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/TDigestTest.java @@ -0,0 +1,391 @@ +package com.thealgorithms.streaming; + +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.Arrays; +import java.util.Random; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TDigestTest { + + /** + * Fraction of the sample that is strictly smaller than the given value, i.e. the rank the digest + * actually hit. Comparing ranks rather than values is the meaningful way to score a quantile + * sketch: it is insensitive to how steep the distribution happens to be. + */ + private static double trueRank(double[] sortedValues, double value) { + int index = Arrays.binarySearch(sortedValues, value); + if (index < 0) { + index = -(index + 1); + } + return (double) index / sortedValues.length; + } + + private static double[] gaussianSample(int count, long seed) { + Random random = new Random(seed); + double[] values = new double[count]; + for (int i = 0; i < count; i++) { + values[i] = random.nextGaussian(); + } + return values; + } + + private static double[] uniformSample(int count, long seed) { + Random random = new Random(seed); + double[] values = new double[count]; + for (int i = 0; i < count; i++) { + values[i] = random.nextDouble(); + } + return values; + } + + @ParameterizedTest + @ValueSource(doubles = {9.0, 0.0, -5.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidCompression(double compression) { + assertThrows(IllegalArgumentException.class, () -> new TDigest(compression)); + } + + @Test + void queriesBeforeTheFirstSampleFail() { + TDigest digest = new TDigest(); + assertTrue(digest.isEmpty()); + assertEquals(0.0, digest.totalWeight()); + assertEquals(100.0, digest.compression()); + assertThrows(IllegalStateException.class, () -> digest.quantile(0.5)); + assertThrows(IllegalStateException.class, () -> digest.cdf(0.5)); + assertThrows(IllegalStateException.class, digest::min); + assertThrows(IllegalStateException.class, digest::max); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + TDigest digest = new TDigest(); + assertThrows(IllegalArgumentException.class, () -> digest.add(value)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidWeights(double weight) { + TDigest digest = new TDigest(); + assertThrows(IllegalArgumentException.class, () -> digest.add(1.0, weight)); + } + + @ParameterizedTest + @ValueSource(doubles = {-0.1, 1.1, Double.NaN}) + void rejectsInvalidQuantiles(double q) { + TDigest digest = new TDigest(); + digest.add(1.0); + assertThrows(IllegalArgumentException.class, () -> digest.quantile(q)); + } + + @Test + void rejectsNaNInCdf() { + TDigest digest = new TDigest(); + digest.add(1.0); + assertThrows(IllegalArgumentException.class, () -> digest.cdf(Double.NaN)); + } + + @Test + void aSingleSampleAnswersEveryQuantile() { + TDigest digest = new TDigest(); + digest.add(4.0); + assertEquals(4.0, digest.quantile(0.0)); + assertEquals(4.0, digest.quantile(0.5)); + assertEquals(4.0, digest.quantile(1.0)); + assertEquals(4.0, digest.min()); + assertEquals(4.0, digest.max()); + assertEquals(1.0, digest.totalWeight()); + assertEquals(1, digest.centroidCount()); + } + + @Test + void aConstantStreamCollapsesIntoOneValue() { + TDigest digest = new TDigest(); + for (int i = 0; i < 10_000; i++) { + digest.add(3.5); + } + assertEquals(3.5, digest.quantile(0.1), 1e-12); + assertEquals(3.5, digest.quantile(0.9), 1e-12); + assertEquals(0.5, digest.cdf(3.5), 1e-12); + } + + @Test + void extremesAreExact() { + double[] values = gaussianSample(20_000, 11L); + TDigest digest = new TDigest(); + digest.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + assertEquals(sorted[0], digest.min(), 0.0); + assertEquals(sorted[sorted.length - 1], digest.max(), 0.0); + assertEquals(sorted[0], digest.quantile(0.0), 0.0); + assertEquals(sorted[sorted.length - 1], digest.quantile(1.0), 0.0); + assertEquals(0.0, digest.cdf(sorted[0] - 1.0)); + assertEquals(1.0, digest.cdf(sorted[sorted.length - 1] + 1.0)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.001, 0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99, 0.999}) + @DisplayName("hits the requested rank of a uniform stream") + void hitsTheRequestedRankOnUniformData(double q) { + double[] values = uniformSample(100_000, 20240517L); + TDigest digest = new TDigest(100.0); + digest.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + assertEquals(q, trueRank(sorted, digest.quantile(q)), 0.01, "requested q=" + q); + } + + @ParameterizedTest + @ValueSource(doubles = {0.001, 0.01, 0.1, 0.5, 0.9, 0.99, 0.999}) + @DisplayName("is far more accurate in the tails than in the middle, by design") + void isMostAccurateInTheTails(double q) { + double[] values = gaussianSample(200_000, 4242L); + TDigest digest = new TDigest(200.0); + digest.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + double rankError = Math.abs(trueRank(sorted, digest.quantile(q)) - q); + + // The tolerance follows the shape of the scale function: it shrinks as q approaches 0 or 1. + double allowedRankError = 0.01 * Math.max(0.05, 4.0 * q * (1.0 - q)); + assertTrue(rankError <= allowedRankError, "q=" + q + " rank error " + rankError + " exceeded " + allowedRankError); + } + + @Test + void quantilesAreMonotonic() { + TDigest digest = new TDigest(); + digest.addAll(gaussianSample(50_000, 777L)); + + double previous = digest.quantile(0.0); + for (int i = 1; i <= 1_000; i++) { + double current = digest.quantile(i / 1_000.0); + assertTrue(current >= previous, "quantile decreased at q=" + i / 1_000.0); + previous = current; + } + } + + @Test + @DisplayName("the cdf inverts the quantile function") + void cdfAgreesWithQuantile() { + TDigest digest = new TDigest(200.0); + digest.addAll(uniformSample(100_000, 314159L)); + + for (int step = 1; step <= 19; step++) { + double q = step * 0.05; + assertEquals(q, digest.cdf(digest.quantile(q)), 0.02, "at q=" + q); + } + } + + @Test + void cdfMatchesTheEmpiricalDistribution() { + double[] values = uniformSample(50_000, 2718L); + TDigest digest = new TDigest(); + digest.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + for (int step = 1; step <= 19; step++) { + double x = step * 0.05; + assertEquals(trueRank(sorted, x), digest.cdf(x), 0.02, "at x=" + x); + } + } + + @Test + @DisplayName("a weighted sample stands for that many observations") + void supportsWeightedSamples() { + TDigest weighted = new TDigest(); + TDigest repeated = new TDigest(); + for (int i = 0; i < 100; i++) { + weighted.add(i, 10.0); + for (int repeat = 0; repeat < 10; repeat++) { + repeated.add(i); + } + } + + assertEquals(1_000.0, weighted.totalWeight(), 1e-9); + assertEquals(repeated.totalWeight(), weighted.totalWeight(), 1e-9); + assertEquals(repeated.quantile(0.5), weighted.quantile(0.5), 2.0); + } + + @Test + @DisplayName("merging shard digests approximates a digest of the whole stream") + void mergesLikeAPartialAggregate() { + double[] values = gaussianSample(120_000, 5150L); + TDigest whole = new TDigest(200.0); + TDigest shardA = new TDigest(200.0); + TDigest shardB = new TDigest(200.0); + TDigest shardC = new TDigest(200.0); + for (int i = 0; i < values.length; i++) { + whole.add(values[i]); + if (i % 3 == 0) { + shardA.add(values[i]); + } else if (i % 3 == 1) { + shardB.add(values[i]); + } else { + shardC.add(values[i]); + } + } + + shardA.merge(shardB); + shardA.merge(shardC); + + assertEquals(whole.totalWeight(), shardA.totalWeight(), 1e-9); + assertEquals(whole.min(), shardA.min(), 0.0); + assertEquals(whole.max(), shardA.max(), 0.0); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + for (double q : new double[] {0.01, 0.1, 0.5, 0.9, 0.99}) { + assertEquals(q, trueRank(sorted, shardA.quantile(q)), 0.01, "merged digest at q=" + q); + } + } + + @Test + void mergingAnEmptyDigestChangesNothing() { + TDigest digest = new TDigest(); + digest.addAll(1.0, 2.0, 3.0, 4.0, 5.0); + double median = digest.quantile(0.5); + + digest.merge(new TDigest()); + assertEquals(median, digest.quantile(0.5), 0.0); + assertEquals(5.0, digest.totalWeight()); + } + + @ParameterizedTest + @ValueSource(doubles = {20.0, 100.0, 500.0}) + @DisplayName("the sketch stays small no matter how long the stream is") + void keepsTheCentroidCountBounded(double compression) { + TDigest digest = new TDigest(compression); + digest.addAll(gaussianSample(200_000, 8L)); + + assertTrue(digest.centroidCount() <= compression, "kept " + digest.centroidCount() + " centroids for compression " + compression); + assertEquals(digest.centroidCount(), digest.centroidMeansSnapshot().length); + assertEquals(digest.centroidCount(), digest.centroidWeightsSnapshot().length); + + double[] means = digest.centroidMeansSnapshot(); + for (int i = 1; i < means.length; i++) { + assertTrue(means[i - 1] <= means[i], "centroids are not sorted at index " + i); + } + assertEquals(digest.totalWeight(), Arrays.stream(digest.centroidWeightsSnapshot()).sum(), 1e-6); + } + + @Test + @DisplayName("a higher compression buys accuracy") + void higherCompressionIsMoreAccurate() { + double[] values = gaussianSample(200_000, 6060L); + double[] sorted = values.clone(); + Arrays.sort(sorted); + + TDigest coarse = new TDigest(20.0); + TDigest fine = new TDigest(500.0); + coarse.addAll(values); + fine.addAll(values); + + double coarseError = Math.abs(trueRank(sorted, coarse.quantile(0.5)) - 0.5); + double fineError = Math.abs(trueRank(sorted, fine.quantile(0.5)) - 0.5); + assertTrue(fineError <= coarseError, "coarse=" + coarseError + " fine=" + fineError); + } + + @Test + void handlesSortedInput() { + TDigest digest = new TDigest(); + for (int i = 1; i <= 100_000; i++) { + digest.add(i); + } + assertEquals(50_000.0, digest.quantile(0.5), 1_000.0); + assertEquals(1.0, digest.min()); + assertEquals(100_000.0, digest.max()); + } + + @Test + void resetForgetsEverything() { + TDigest digest = new TDigest(); + digest.addAll(gaussianSample(1_000, 3L)); + digest.reset(); + + assertTrue(digest.isEmpty()); + assertEquals(0.0, digest.totalWeight()); + assertThrows(IllegalStateException.class, () -> digest.quantile(0.5)); + + digest.addAll(1.0, 2.0, 3.0); + assertEquals(3.0, digest.totalWeight()); + assertEquals(2.0, digest.quantile(0.5), 0.5); + } + + @Test + void toStringMentionsTheState() { + TDigest digest = new TDigest(); + assertTrue(digest.toString().contains("centroids=0"), digest.toString()); + digest.addAll(1.0, 2.0, 3.0); + assertFalse(digest.isEmpty()); + assertTrue(digest.toString().contains("weight=3.0"), digest.toString()); + } + + @Test + @DisplayName("the very edges of the quantile function interpolate towards the tracked extremes") + void interpolatesTowardsTheExtremes() { + double[] values = uniformSample(100_000, 161803L); + TDigest digest = new TDigest(); + digest.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + double deepTail = digest.quantile(0.999999); + assertTrue(deepTail <= digest.max(), "the estimate must not exceed the maximum"); + assertTrue(deepTail >= sorted[sorted.length - 10], "the deep tail should sit among the largest samples"); + + assertEquals(1.0, digest.cdf(digest.max()), 1e-9); + assertEquals(0.0, digest.cdf(digest.min()), 0.01); + } + + @Test + @DisplayName("a stream made of two repeated values keeps its quantiles sane") + void handlesHeavilyDuplicatedValues() { + TDigest digest = new TDigest(); + for (int i = 0; i < 20_000; i++) { + digest.add(i % 2 == 0 ? 1.0 : 2.0); + } + + assertEquals(1.0, digest.min()); + assertEquals(2.0, digest.max()); + assertTrue(digest.quantile(0.25) <= 1.5, "the lower quarter is made of ones"); + assertTrue(digest.quantile(0.75) >= 1.5, "the upper quarter is made of twos"); + assertTrue(digest.cdf(1.5) >= 0.0 && digest.cdf(1.5) <= 1.0); + } + + @Test + @DisplayName("weights spanning eighteen orders of magnitude still give sane, monotone quantiles") + void survivesWildlyDisparateWeights() { + TDigest digest = new TDigest(50.0); + digest.add(0.0, 1e12); + for (int i = 1; i <= 10_000; i++) { + digest.add(i, 1e-6); + } + + // A point mass this dominant is the regime where the packing rule degenerates into one centroid + // per sample, so it is also the regime that decides whether the centroid array is large enough. + assertEquals(1e12 + 10_000 * 1e-6, digest.totalWeight(), 1.0); + assertEquals(0.0, digest.min()); + assertEquals(10_000.0, digest.max()); + assertEquals(0.0, digest.quantile(0.5), 1e-6, "essentially all the weight sits at zero"); + assertTrue(digest.quantile(0.999999) > 0.0, "the tail must not collapse onto the bulk"); + + double previous = digest.quantile(0.0); + for (int i = 1; i <= 1_000; i++) { + double current = digest.quantile(i / 1_000.0); + assertTrue(current >= previous, "quantiles stopped being monotonic at q=" + i / 1_000.0); + previous = current; + } + } +} From e43ec3897e48f23843972d02cd05a5bb1b6864a2 Mon Sep 17 00:00:00 2001 From: Vedant Hemraj Kapgate Date: Sun, 11 Oct 2026 01:46:44 +0530 Subject: [PATCH 03/34] Add Duval's Lyndon factorization algorithm (#7639) * Add Duval Lyndon factorization * Test Lyndon factorization * Handle null input explicitly in Lyndon factorization * Align null-input test with explicit validation --- .../strings/LyndonFactorization.java | 61 +++++++++++++++++++ .../strings/LyndonFactorizationTest.java | 35 +++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/main/java/com/thealgorithms/strings/LyndonFactorization.java create mode 100644 src/test/java/com/thealgorithms/strings/LyndonFactorizationTest.java diff --git a/src/main/java/com/thealgorithms/strings/LyndonFactorization.java b/src/main/java/com/thealgorithms/strings/LyndonFactorization.java new file mode 100644 index 000000000000..3eb13b00d0fb --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LyndonFactorization.java @@ -0,0 +1,61 @@ +package com.thealgorithms.strings; + +import java.util.ArrayList; +import java.util.List; + +/** + * Factorizes a string into its Lyndon words using Duval's algorithm. + * + *

Duval's algorithm runs in O(n) time and uses O(1) auxiliary working + * space, excluding the returned list and its strings. + * + *

Reference: + * https://cp-algorithms.com/string/lyndon_factorization.html + */ +public final class LyndonFactorization { + private LyndonFactorization() { + } + + /** + * Returns the standard Lyndon factorization of {@code text}. + * + *

The returned factors are in non-increasing lexicographic order + * according to Java's UTF-16 code-unit ordering. + * + * @param text input string to factorize + * @return Lyndon factors in non-increasing lexicographic order + * @throws IllegalArgumentException if {@code text} is null + */ + public static List factorize(String text) { + if (text == null) { + throw new IllegalArgumentException("text must not be null"); + } + + List factors = new ArrayList<>(); + int n = text.length(); + int start = 0; + + while (start < n) { + int candidate = start + 1; + int match = start; + + while (candidate < n && text.charAt(match) <= text.charAt(candidate)) { + if (text.charAt(match) < text.charAt(candidate)) { + match = start; + } else { + match++; + } + candidate++; + } + + int length = candidate - match; + + while (start <= match) { + factors.add(text.substring(start, start + length)); + start += length; + } + } + + return factors; + } +} diff --git a/src/test/java/com/thealgorithms/strings/LyndonFactorizationTest.java b/src/test/java/com/thealgorithms/strings/LyndonFactorizationTest.java new file mode 100644 index 000000000000..4ecb86e2a99c --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LyndonFactorizationTest.java @@ -0,0 +1,35 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class LyndonFactorizationTest { + + @Test + void factorizesBanana() { + assertEquals(List.of("b", "an", "an", "a"), LyndonFactorization.factorize("banana")); + } + + @Test + void factorizesRepeatedPattern() { + assertEquals(List.of("ab", "ab", "ab"), LyndonFactorization.factorize("ababab")); + } + + @Test + void factorizesSingleCharacter() { + assertEquals(List.of("a"), LyndonFactorization.factorize("a")); + } + + @Test + void factorizesEmptyString() { + assertEquals(List.of(), LyndonFactorization.factorize("")); + } + + @Test + void rejectsNullInput() { + assertThrows(IllegalArgumentException.class, () -> LyndonFactorization.factorize(null)); + } +} From 8dbdc51edea193d103fe212c897f1a84ae479ce6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:24:58 +0300 Subject: [PATCH 04/34] chore(deps): bump github/codeql-action from 4.38.2 to 4.38.3 in /.github/workflows (#7645) chore(deps): bump github/codeql-action in /.github/workflows Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.38.2 to 4.38.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.38.2...v4.38.3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.38.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 82963c044afb..763cc4980982 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.2 + uses: github/codeql-action/init@v4.38.3 with: languages: 'java-kotlin' @@ -38,7 +38,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.38.2 + uses: github/codeql-action/analyze@v4.38.3 with: category: "/language:java-kotlin" @@ -55,12 +55,12 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.2 + uses: github/codeql-action/init@v4.38.3 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.38.2 + uses: github/codeql-action/analyze@v4.38.3 with: category: "/language:actions" ... From 0ab112410e7f3bb57cca28987671f08503244771 Mon Sep 17 00:00:00 2001 From: Kaan Hascan <133219984+kaanhascan@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:00:52 +0300 Subject: [PATCH 05/34] chore: remove accidental file breaking Windows checkout (#7646) --- "ts\"" | 179 --------------------------------------------------------- 1 file changed, 179 deletions(-) delete mode 100644 "ts\"" diff --git "a/ts\"" "b/ts\"" deleted file mode 100644 index daddada971db..000000000000 --- "a/ts\"" +++ /dev/null @@ -1,179 +0,0 @@ -diff --git a/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java b/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java -index 5378a01..be8a92e 100644 ---- a/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java -+++ b/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java -@@ -1,29 +1,33 @@ - package com.thealgorithms.datastructures.trees; -  -+/** Fenwick Tree for point updates and prefix-sum queries in O(log n). */ - public class FenwickTree { -  -- private int n; -- private int[] fenTree; -+ private final int n; -+ private final int[] fenTree; -  -- /* Constructor which takes the size of the array as a parameter */ -+ /** Creates a Fenwick tree with n elements, all initialized to zero. */ - public FenwickTree(int n) { -+ if (n < 0) { -+ throw new IllegalArgumentException("Size must be non-negative"); -+ } - this.n = n; - this.fenTree = new int[n + 1]; - } -  -- /* A function which will add the element val at index i*/ -+ /** Adds val to the element at index i. */ - public void update(int i, int val) { -- // As index starts from 0, increment the index by 1 -- i += 1; -+ checkIndex(i); -+ i += 1; // Convert to the internal 1-based index - while (i <= n) { - fenTree[i] += val; - i += i & (-i); - } - } -  -- /* A function which will return the cumulative sum from index 1 to index i*/ -+ /** Returns the sum of elements from index 0 to i. */ - public int query(int i) { -- // As index starts from 0, increment the index by 1 -+ checkIndex(i); - i += 1; - int cumSum = 0; - while (i > 0) { -@@ -32,4 +36,11 @@ public class FenwickTree { - } - return cumSum; - } -+ -+ // Check that the index is within the valid range. -+ private void checkIndex(int i) { -+ if (i < 0 || i >= n) { -+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds for size " + n); -+ } -+ } - } -diff --git a/src/test/java/com/thealgorithms/datastructures/trees/FenwickTree.java b/src/test/java/com/thealgorithms/datastructures/trees/FenwickTree.java -new file mode 100644 -index 0000000..47ef568 ---- /dev/null -+++ b/src/test/java/com/thealgorithms/datastructures/trees/FenwickTree.java -@@ -0,0 +1,115 @@ -+package com.thealgorithms.datastructures.trees; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertThrows; -+ -+import java.util.Random; -+import org.junit.jupiter.api.Test; -+ -+class FenwickTreeTest { -+ -+ @Test -+ void queryOnFreshTreeReturnsZero() { -+ FenwickTree tree = new FenwickTree(5); -+ for (int i = 0; i < 5; i++) { -+ assertEquals(0, tree.query(i)); -+ } -+ } -+ -+ @Test -+ void singleElementTree() { -+ FenwickTree tree = new FenwickTree(1); -+ tree.update(0, 7); -+ assertEquals(7, tree.query(0)); -+ } -+ -+ @Test -+ void prefixSumsAfterUpdates() { -+ FenwickTree tree = new FenwickTree(5); -+ int[] values = {3, 2, -1, 6, 5}; -+ for (int i = 0; i < values.length; i++) { -+ tree.update(i, values[i]); -+ } -+ assertEquals(3, tree.query(0)); -+ assertEquals(5, tree.query(1)); -+ assertEquals(4, tree.query(2)); -+ assertEquals(10, tree.query(3)); -+ assertEquals(15, tree.query(4)); -+ } -+ -+ @Test -+ void repeatedUpdatesOnSameIndexAccumulate() { -+ FenwickTree tree = new FenwickTree(4); -+ tree.update(2, 5); -+ tree.update(2, 3); -+ tree.update(2, -2); -+ assertEquals(0, tree.query(1)); -+ assertEquals(6, tree.query(2)); -+ assertEquals(6, tree.query(3)); -+ } -+ -+ @Test -+ void negativeValues() { -+ FenwickTree tree = new FenwickTree(3); -+ tree.update(0, -4); -+ tree.update(1, -6); -+ tree.update(2, 10); -+ assertEquals(-4, tree.query(0)); -+ assertEquals(-10, tree.query(1)); -+ assertEquals(0, tree.query(2)); -+ } -+ -+ @Test -+ void nonPowerOfTwoSize() { -+ int size = 13; -+ FenwickTree tree = new FenwickTree(size); -+ for (int i = 0; i < size; i++) { -+ tree.update(i, 1); -+ } -+ for (int i = 0; i < size; i++) { -+ assertEquals(i + 1, tree.query(i)); -+ } -+ } -+ -+ @Test -+ void matchesNaivePrefixSumOnRandomData() { -+ Random random = new Random(42); -+ int size = 100; -+ FenwickTree tree = new FenwickTree(size); -+ int[] naive = new int[size]; -+ -+ for (int step = 0; step < 500; step++) { -+ int index = random.nextInt(size); -+ int delta = random.nextInt(21) - 10; -+ tree.update(index, delta); -+ naive[index] += delta; -+ } -+ -+ int running = 0; -+ for (int i = 0; i < size; i++) { -+ running += naive[i]; -+ assertEquals(running, tree.query(i), "Mismatch at index " + i); -+ } -+ } -+ -+ @Test -+ void zeroSizeTreeRejectsAnyIndex() { -+ FenwickTree tree = new FenwickTree(0); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.update(0, 1)); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.query(0)); -+ } -+ -+ @Test -+ void negativeSizeThrows() { -+ assertThrows(IllegalArgumentException.class, () -> new FenwickTree(-1)); -+ } -+ -+ @Test -+ void outOfBoundsIndicesThrow() { -+ FenwickTree tree = new FenwickTree(5); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.update(-1, 1)); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.update(5, 1)); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.query(-1)); -+ assertThrows(IndexOutOfBoundsException.class, () -> tree.query(5)); -+ } -+} From d9b86d1992b8bd4cadbd832822c792628117e69d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:06:17 +0000 Subject: [PATCH 06/34] chore(deps-dev): bump com.github.spotbugs:spotbugs-maven-plugin from 4.10.4.1 to 4.10.5.0 (#7647) chore(deps-dev): bump com.github.spotbugs:spotbugs-maven-plugin Bumps [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) from 4.10.4.1 to 4.10.5.0. - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.10.4.1...spotbugs-maven-plugin-4.10.5.0) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.10.5.0 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a166dbf4fe5..21dd79ad17b1 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.10.4.1 + 4.10.5.0 spotbugs-exclude.xml true From f8207cc34ca0bd5694cce3120a9741ee50b2d2e8 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Wed, 21 Oct 2026 21:11:35 +0530 Subject: [PATCH 07/34] fix(searches): support duplicates and null safety in LowerBound and UpperBound (#7648) * fix(searches): find first occurrence in LowerBound with duplicate values When multiple elements equal to key exist, LowerBound should return the index of the first such value per its specification. Continue searching the left partition when comp <= 0 instead of prematurely returning the median element. * fix(searches): add null and empty array safety to LowerBound and UpperBound Guard against NullPointerException and empty array bounds access in LowerBound and UpperBound find methods when array is null/empty or key is null. * test(searches): add unit tests for LowerBound duplicate values and null safety Verify LowerBound returns the first occurrence for duplicate elements, and handles empty arrays, null arrays, and null keys gracefully in LowerBound and UpperBound. * docs(searches): document null return behavior, update best-case complexity, and test UpperBound duplicates --- .../thealgorithms/searches/LowerBound.java | 13 ++++----- .../thealgorithms/searches/UpperBound.java | 7 +++-- .../searches/LowerBoundTest.java | 27 +++++++++++++++++++ .../searches/UpperBoundTest.java | 15 +++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/LowerBound.java b/src/main/java/com/thealgorithms/searches/LowerBound.java index 5a1401edd3c2..7325082c223b 100644 --- a/src/main/java/com/thealgorithms/searches/LowerBound.java +++ b/src/main/java/com/thealgorithms/searches/LowerBound.java @@ -13,7 +13,7 @@ * This is an extension of BinarySearch. * *

- * Worst-case performance O(log n) Best-case performance O(1) Average + * Worst-case performance O(log n) Best-case performance O(log n) Average * performance O(log n) Worst-case space complexity O(1) * * @author Pratik Padalia (https://github.com/15pratik) @@ -26,10 +26,13 @@ class LowerBound implements SearchAlgorithm { * @param array is an array where the LowerBound value is to be found * @param key is an element for which the LowerBound is to be found * @param is any comparable type - * @return index of the LowerBound element + * @return index of the first element not less than key, or 0 if array is null/empty or key is null */ @Override public > int find(T[] array, T key) { + if (array == null || array.length == 0 || key == null) { + return 0; + } return search(array, key, 0, array.length - 1); } @@ -51,10 +54,8 @@ private > int search(T[] array, T key, int left, int rig int median = (left + right) >>> 1; int comp = key.compareTo(array[median]); - if (comp == 0) { - return median; - } else if (comp < 0) { - // median position can be a possible solution + if (comp <= 0) { + // key is less than or equal to array[median], median position can be a possible solution return search(array, key, left, median); } else { // key we are looking is greater, so we must look on the right of median position diff --git a/src/main/java/com/thealgorithms/searches/UpperBound.java b/src/main/java/com/thealgorithms/searches/UpperBound.java index ec52c7a0ae5c..5344f22994b1 100644 --- a/src/main/java/com/thealgorithms/searches/UpperBound.java +++ b/src/main/java/com/thealgorithms/searches/UpperBound.java @@ -13,7 +13,7 @@ * This is an extension of BinarySearch. * *

- * Worst-case performance O(log n) Best-case performance O(1) Average + * Worst-case performance O(log n) Best-case performance O(log n) Average * performance O(log n) Worst-case space complexity O(1) * * @author Pratik Padalia (https://github.com/15pratik) @@ -26,10 +26,13 @@ class UpperBound implements SearchAlgorithm { * @param array is an array where the UpperBound value is to be found * @param key is an element for which the UpperBound is to be found * @param is any comparable type - * @return index of the UpperBound element + * @return index of the first element greater than key, last index if no such element exists, or 0 if array is null/empty or key is null */ @Override public > int find(T[] array, T key) { + if (array == null || array.length == 0 || key == null) { + return 0; + } return search(array, key, 0, array.length - 1); } diff --git a/src/test/java/com/thealgorithms/searches/LowerBoundTest.java b/src/test/java/com/thealgorithms/searches/LowerBoundTest.java index 30f4a5cb257c..058ea08cf614 100644 --- a/src/test/java/com/thealgorithms/searches/LowerBoundTest.java +++ b/src/test/java/com/thealgorithms/searches/LowerBoundTest.java @@ -56,4 +56,31 @@ void testLowerBoundNonExistentValue() { assertEquals(4, lowerBound.find(array, 7), "Lower bound for 7 should be at index 4"); assertEquals(0, lowerBound.find(array, 0), "Lower bound for 0 should be at index 0"); } + + /** + * Test finding the lower bound for an element with duplicate occurrences. + * Lower bound should point to the FIRST element not less than key. + */ + @Test + void testLowerBoundDuplicates() { + LowerBound lowerBound = new LowerBound(); + Integer[] arrayWithDuplicates = {1, 2, 2, 2, 3, 4, 5}; + + assertEquals(1, lowerBound.find(arrayWithDuplicates, 2), "Lower bound for 2 should be at index 1 (first occurrence)"); + + Integer[] allSame = {2, 2, 2, 2, 2}; + assertEquals(0, lowerBound.find(allSame, 2), "Lower bound for 2 in all-same array should be at index 0"); + } + + /** + * Test edge cases: empty array, null array, and null key. + */ + @Test + void testLowerBoundNullAndEmptyEdgeCases() { + LowerBound lowerBound = new LowerBound(); + Integer[] emptyArray = {}; + assertEquals(0, lowerBound.find(emptyArray, 5), "Lower bound for an empty array should be 0"); + assertEquals(0, lowerBound.find(null, 5), "Lower bound for a null array should be 0"); + assertEquals(0, lowerBound.find(new Integer[] {1, 2, 3}, null), "Lower bound for a null key should be 0"); + } } diff --git a/src/test/java/com/thealgorithms/searches/UpperBoundTest.java b/src/test/java/com/thealgorithms/searches/UpperBoundTest.java index dc0cbdd97e19..4f5eddbe924f 100644 --- a/src/test/java/com/thealgorithms/searches/UpperBoundTest.java +++ b/src/test/java/com/thealgorithms/searches/UpperBoundTest.java @@ -57,6 +57,15 @@ void testUpperBoundMultipleValues() { assertTrue(arrayWithDuplicates[index] > key, "Element at the upper bound index should be greater than the key."); } + @Test + void testUpperBoundDuplicates() { + Integer[] arrayWithDuplicates = {1, 2, 2, 2, 3}; + assertEquals(4, upperBound.find(arrayWithDuplicates, 2), "Upper bound for 2 in {1, 2, 2, 2, 3} should be at index 4"); + + Integer[] allSame = {2, 2, 2, 2, 2}; + assertEquals(4, upperBound.find(allSame, 2), "Upper bound for 2 in all-same array should be at last index 4"); + } + @Test void testUpperBoundLowerThanMin() { int key = 0; // Test with a key lower than the minimum element @@ -96,4 +105,10 @@ void testUpperBoundSingleElementArray() { index = upperBound.find(singleElementArray, 15); assertEquals(0, index, "Upper bound for 15 in a single element array should be 0."); } + + @Test + void testUpperBoundNullArrayAndKey() { + assertEquals(0, upperBound.find(null, 5), "Upper bound for a null array should be 0."); + assertEquals(0, upperBound.find(new Integer[] {1, 2, 3}, null), "Upper bound for a null key should be 0."); + } } From ff360047c5de460a20bd3dbd3715f211bb7a00f4 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Wed, 21 Oct 2026 22:20:13 +0530 Subject: [PATCH 08/34] fix(searches): refine recursion bounds and add null safety to TernarySearch (#7649) --- .../thealgorithms/searches/TernarySearch.java | 56 ++++++++++------- .../searches/TernarySearchTest.java | 62 +++++++++++++++++++ 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/TernarySearch.java b/src/main/java/com/thealgorithms/searches/TernarySearch.java index 4d9f55ea9917..1071d172ef21 100644 --- a/src/main/java/com/thealgorithms/searches/TernarySearch.java +++ b/src/main/java/com/thealgorithms/searches/TernarySearch.java @@ -3,38 +3,44 @@ import com.thealgorithms.devutils.searches.SearchAlgorithm; /** - * A ternary search algorithm is a technique in computer science for finding the - * minimum or maximum of a unimodal function The algorithm determines either - * that the minimum or maximum cannot be in the first third of the domain or - * that it cannot be in the last third of the domain, then repeats on the - * remaining third. + * A ternary search algorithm is a divide-and-conquer algorithm that searches for + * a key in a sorted array by dividing the search domain into three parts. * *

- * Worst-case performance Θ(log3(N)) Best-case performance O(1) Average - * performance Θ(log3(N)) Worst-case space complexity O(1) + * Worst-case performance: Θ(log3(N))
+ * Best-case performance: O(1)
+ * Average performance: Θ(log3(N))
+ * Worst-case space complexity: O(log3(N)) * * @author Podshivalov Nikita (https://github.com/nikitap492) * @see SearchAlgorithm * @see IterativeBinarySearch */ -public class TernarySearch implements SearchAlgorithm { +public final class TernarySearch implements SearchAlgorithm { /** - * @param arr The **Sorted** array in which we will search the element. + * Searches for a value in a sorted array using ternary search. + * + * @param arr The sorted array in which we will search for the element. * @param value The value that we want to search for. - * @return The index of the element if found. Else returns -1. + * @return The index of the element if found. Returns -1 if the array is null or empty, if value is null, or if not found. */ @Override public > int find(T[] arr, T value) { + if (arr == null || arr.length == 0 || value == null) { + return -1; + } return ternarySearch(arr, value, 0, arr.length - 1); } /** - * @param arr The **Sorted** array in which we will search the element. + * Recursively searches for the specified key within the range [start, end]. + * + * @param arr The sorted array in which we will search the element. * @param key The value that we want to search for. - * @param start The starting index from which we will start Searching. - * @param end The ending index till which we will Search. - * @return Returns the index of the Element if found. Else returns -1. + * @param start The starting index from which we will start searching. + * @param end The ending index till which we will search. + * @return Returns the index of the element if found. Else returns -1. */ private > int ternarySearch(T[] arr, T key, int start, int end) { if (start > end) { @@ -45,16 +51,22 @@ private > int ternarySearch(T[] arr, T key, int start, i /* Second boundary: add 2/3 of length to start */ int mid2 = start + 2 * (end - start) / 3; - if (key.compareTo(arr[mid1]) == 0) { + int cmp1 = key.compareTo(arr[mid1]); + if (cmp1 == 0) { return mid1; - } else if (key.compareTo(arr[mid2]) == 0) { + } + + int cmp2 = key.compareTo(arr[mid2]); + if (cmp2 == 0) { return mid2; - } /* Search the first (1/3) rd part of the array.*/ else if (key.compareTo(arr[mid1]) < 0) { - return ternarySearch(arr, key, start, --mid1); - } /* Search 3rd (1/3)rd part of the array */ else if (key.compareTo(arr[mid2]) > 0) { - return ternarySearch(arr, key, ++mid2, end); - } /* Search middle (1/3)rd part of the array */ else { - return ternarySearch(arr, key, mid1, mid2); + } + + if (cmp1 < 0) { + return ternarySearch(arr, key, start, mid1 - 1); + } else if (cmp2 > 0) { + return ternarySearch(arr, key, mid2 + 1, end); + } else { + return ternarySearch(arr, key, mid1 + 1, mid2 - 1); } } } diff --git a/src/test/java/com/thealgorithms/searches/TernarySearchTest.java b/src/test/java/com/thealgorithms/searches/TernarySearchTest.java index 367b595e55d9..fa62b3f8f847 100644 --- a/src/test/java/com/thealgorithms/searches/TernarySearchTest.java +++ b/src/test/java/com/thealgorithms/searches/TernarySearchTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.searches; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -78,4 +79,65 @@ void testEdgeCaseEmptyArray() { assertEquals(-1, index, "Should return -1 for an empty array"); } + + @Test + void testNullArrayReturnsMinusOne() { + TernarySearch search = new TernarySearch(); + assertEquals(-1, search.find(null, 5)); + } + + @Test + void testNullKeyReturnsMinusOne() { + Integer[] arr = {1, 2, 3, 4, 5}; + TernarySearch search = new TernarySearch(); + assertEquals(-1, search.find(arr, null)); + } + + @Test + void testSingleElementPresent() { + Integer[] arr = {42}; + TernarySearch search = new TernarySearch(); + assertEquals(0, search.find(arr, 42)); + } + + @Test + void testSingleElementAbsent() { + Integer[] arr = {42}; + TernarySearch search = new TernarySearch(); + assertEquals(-1, search.find(arr, 24)); + } + + @Test + void testTwoElements() { + Integer[] arr = {10, 20}; + TernarySearch search = new TernarySearch(); + assertEquals(0, search.find(arr, 10)); + assertEquals(1, search.find(arr, 20)); + assertEquals(-1, search.find(arr, 15)); + } + + @Test + void testMiddlePartitionSearch() { + Integer[] arr = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19}; + TernarySearch search = new TernarySearch(); + assertEquals(3, search.find(arr, 7)); + assertEquals(4, search.find(arr, 9)); + assertEquals(5, search.find(arr, 11)); + } + + @Test + void testDuplicateElements() { + Integer[] arr = {1, 2, 2, 2, 3}; + TernarySearch search = new TernarySearch(); + int index = search.find(arr, 2); + assertTrue(index >= 1 && index <= 3, "Index should be one of the occurrences of 2"); + } + + @Test + void testGenericStrings() { + String[] words = {"apple", "banana", "cherry", "date", "elderberry"}; + TernarySearch search = new TernarySearch(); + assertEquals(2, search.find(words, "cherry")); + assertEquals(-1, search.find(words, "fig")); + } } From 18c99d90c3cd9e0f1b5c8d54d927a866b43604a2 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Wed, 21 Oct 2026 22:42:46 +0530 Subject: [PATCH 09/34] fix(searches): handle null array safely and eliminate raw type warnings in FibonacciSearch (#7650) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- .../com/thealgorithms/searches/FibonacciSearch.java | 10 ++++++---- .../thealgorithms/searches/FibonacciSearchTest.java | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/FibonacciSearch.java b/src/main/java/com/thealgorithms/searches/FibonacciSearch.java index fa91cd14a1af..4db6874f0c08 100644 --- a/src/main/java/com/thealgorithms/searches/FibonacciSearch.java +++ b/src/main/java/com/thealgorithms/searches/FibonacciSearch.java @@ -15,8 +15,7 @@ * Note: This algorithm requires that the input array be sorted. *

*/ -@SuppressWarnings({"rawtypes", "unchecked"}) -public class FibonacciSearch implements SearchAlgorithm { +public final class FibonacciSearch implements SearchAlgorithm { /** * Finds the index of the specified key in a sorted array using Fibonacci search. @@ -24,11 +23,14 @@ public class FibonacciSearch implements SearchAlgorithm { * @param array The sorted array to search. * @param key The element to search for. * @param The type of the elements in the array, which must be comparable. - * @throws IllegalArgumentException if the input array is not sorted or empty, or if the key is null. + * @throws IllegalArgumentException if the input array is null, not sorted or empty, or if the key is null. * @return The index of the key if found, otherwise -1. */ @Override public > int find(T[] array, T key) { + if (array == null) { + throw new IllegalArgumentException("Input array must not be null."); + } if (array.length == 0) { throw new IllegalArgumentException("Input array must not be empty."); } @@ -76,7 +78,7 @@ public > int find(T[] array, T key) { return -1; } - private boolean isSorted(Comparable[] array) { + private > boolean isSorted(T[] array) { for (int i = 1; i < array.length; i++) { if (array[i - 1].compareTo(array[i]) > 0) { return false; diff --git a/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java b/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java index 04a270864223..f3056b216a7b 100644 --- a/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java @@ -94,6 +94,15 @@ void testFibonacciSearchEmptyArray() { assertThrows(IllegalArgumentException.class, () -> fibonacciSearch.find(array, key), "An empty array should throw an IllegalArgumentException."); } + /** + * Test for Fibonacci search with a null array. + */ + @Test + void testFibonacciSearchNullArray() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + assertThrows(IllegalArgumentException.class, () -> fibonacciSearch.find(null, 5), "A null array should throw an IllegalArgumentException."); + } + @Test void testFibonacciSearchUnsortedArray() { FibonacciSearch fibonacciSearch = new FibonacciSearch(); From 3c3f04e7e34fac0666adcc37f47efbd049d40d2f Mon Sep 17 00:00:00 2001 From: Hardik K Date: Wed, 21 Oct 2026 23:02:37 +0530 Subject: [PATCH 10/34] fix(searches): add null safety and make ExponentialSearch public (#7651) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- .../searches/ExponentialSearch.java | 8 +-- .../searches/ExponentialSearchTest.java | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/ExponentialSearch.java b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java index e666b9148aaa..9405f5995687 100644 --- a/src/main/java/com/thealgorithms/searches/ExponentialSearch.java +++ b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java @@ -19,7 +19,7 @@ * Note: This algorithm requires that the input array be sorted. *

*/ -class ExponentialSearch implements SearchAlgorithm { +public final class ExponentialSearch implements SearchAlgorithm { /** * Finds the index of the specified key in a sorted array using exponential search. @@ -31,13 +31,13 @@ class ExponentialSearch implements SearchAlgorithm { */ @Override public > int find(T[] array, T key) { - if (array.length == 0) { + if (array == null || array.length == 0 || key == null) { return -1; } - if (array[0].equals(key)) { + if (key.compareTo(array[0]) == 0) { return 0; } - if (array[array.length - 1].equals(key)) { + if (key.compareTo(array[array.length - 1]) == 0) { return array.length - 1; } diff --git a/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java b/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java index c6b07ca2b4d5..ed6f0f801bf4 100644 --- a/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.searches; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -58,6 +59,17 @@ void testExponentialSearchSingleElementFound() { assertEquals(expectedIndex, exponentialSearch.find(array, key), "The index of the single element should be 0."); } + /** + * Test for exponential search with a single element not present. + */ + @Test + void testExponentialSearchSingleElementNotFound() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {1}; + int key = 2; // Key not present + assertEquals(-1, exponentialSearch.find(array, key), "The element should not be found in the array."); + } + /** * Test for exponential search with an empty array. */ @@ -70,6 +82,25 @@ void testExponentialSearchEmptyArray() { assertEquals(expectedIndex, exponentialSearch.find(array, key), "The element should not be found in an empty array."); } + /** + * Test for exponential search with a null array. + */ + @Test + void testExponentialSearchNullArray() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + assertEquals(-1, exponentialSearch.find(null, 5), "Should return -1 for null array."); + } + + /** + * Test for exponential search with a null key. + */ + @Test + void testExponentialSearchNullKey() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {1, 2, 3, 4, 5}; + assertEquals(-1, exponentialSearch.find(array, null), "Should return -1 for null key."); + } + /** * Test for exponential search on large array. */ @@ -123,4 +154,26 @@ void testExponentialSearchNotFoundReturnsMinusOne() { assertEquals(-1, exponentialSearch.find(array, 0), "A key below the minimum should give -1."); assertEquals(-1, exponentialSearch.find(array, 12), "A key above the maximum should give -1."); } + + /** + * Test for duplicate elements in the array. + */ + @Test + void testExponentialSearchDuplicates() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {1, 2, 2, 2, 3}; + int index = exponentialSearch.find(array, 2); + assertTrue(index >= 1 && index <= 3, "Index should be one of the occurrences of 2."); + } + + /** + * Test for generic Comparable types such as Strings. + */ + @Test + void testExponentialSearchStrings() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + String[] words = {"apple", "banana", "cherry", "date", "elderberry"}; + assertEquals(2, exponentialSearch.find(words, "cherry"), "Should find 'cherry' at index 2."); + assertEquals(-1, exponentialSearch.find(words, "fig"), "Should return -1 for absent word."); + } } From b526730bcec649472d0009caf4a42344033fbc22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:53:35 +0300 Subject: [PATCH 11/34] chore(deps): bump actions/setup-java from 6.0.1 to 6.1.0 in /.github/workflows (#7652) chore(deps): bump actions/setup-java in /.github/workflows Bumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.1 to 6.1.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v6.0.1...v6.1.0) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/infer.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c582fbe0a3d..38d1471dcf13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v6.0.1 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 763cc4980982..7d7a1e5755c2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v6.0.1 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' diff --git a/.github/workflows/infer.yml b/.github/workflows/infer.yml index 7b0973996a41..962c090ee699 100644 --- a/.github/workflows/infer.yml +++ b/.github/workflows/infer.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v6.0.1 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' From 2994ec327173331e340d0ee57b731a9f22578e87 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 22 Oct 2026 14:54:22 +0530 Subject: [PATCH 12/34] fix(searches): fix null element skipping in SentinelLinearSearch (#7654) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- .../searches/SentinelLinearSearch.java | 6 +++--- .../searches/SentinelLinearSearchTest.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java b/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java index 473fc2c3f094..eb32669d1741 100644 --- a/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java +++ b/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java @@ -33,7 +33,7 @@ * @see LinearSearch * @see SearchAlgorithm */ -public class SentinelLinearSearch implements SearchAlgorithm { +public final class SentinelLinearSearch implements SearchAlgorithm { /** * Performs sentinel linear search on the given array. * @@ -65,8 +65,8 @@ public > int find(T[] array, T key) { int i = 0; // Search without bound checking since sentinel guarantees we'll find the key - // Null check for array element to prevent NPE when array contains null elements - while (array[i] != null && array[i].compareTo(key) != 0) { + // Null check for array element to skip nulls and prevent NPE when array contains null elements + while (array[i] == null || array[i].compareTo(key) != 0) { i++; } diff --git a/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java b/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java index 0da8e1c83997..087f7c9a5e2a 100644 --- a/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java @@ -222,4 +222,25 @@ void testSentinelLinearSearchKeyEqualsLastElement() { Integer key = 3; // Key equals last element assertEquals(2, sentinelLinearSearch.find(array, key), "Should find the first occurrence at index 2, not the last."); } + + @Test + void testSentinelLinearSearchWithNullElementsPresent() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, null, 3, 4}; + assertEquals(2, sentinelLinearSearch.find(array, 3), "Should correctly find element located after a null element."); + } + + @Test + void testSentinelLinearSearchWithNullElementsAbsent() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, null, 3, 4}; + assertEquals(-1, sentinelLinearSearch.find(array, 5), "Should return -1 when key is absent even if null elements exist."); + } + + @Test + void testSentinelLinearSearchMultipleNullElements() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + String[] array = {null, null, "target", null}; + assertEquals(2, sentinelLinearSearch.find(array, "target"), "Should correctly find target after multiple null elements."); + } } From 3d71df93280ba52187b607d65ee5d4540f74007d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:48:22 +0200 Subject: [PATCH 13/34] chore(deps): bump github/codeql-action from 4.38.3 to 4.39.0 in /.github/workflows (#7656) chore(deps): bump github/codeql-action in /.github/workflows Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.38.3 to 4.39.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.38.3...v4.39.0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7d7a1e5755c2..ae38aa619695 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.3 + uses: github/codeql-action/init@v4.39.0 with: languages: 'java-kotlin' @@ -38,7 +38,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.38.3 + uses: github/codeql-action/analyze@v4.39.0 with: category: "/language:java-kotlin" @@ -55,12 +55,12 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.3 + uses: github/codeql-action/init@v4.39.0 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.38.3 + uses: github/codeql-action/analyze@v4.39.0 with: category: "/language:actions" ... From 773d745acb32e92dfb1bc890e88c19d8756651c4 Mon Sep 17 00:00:00 2001 From: Anwesha Date: Sat, 24 Oct 2026 21:47:40 +0530 Subject: [PATCH 14/34] Improve Javadoc for LinearSearch find method (#7657) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- src/main/java/com/thealgorithms/searches/LinearSearch.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/searches/LinearSearch.java b/src/main/java/com/thealgorithms/searches/LinearSearch.java index c5f6e6ba9776..3ec1768916e5 100644 --- a/src/main/java/com/thealgorithms/searches/LinearSearch.java +++ b/src/main/java/com/thealgorithms/searches/LinearSearch.java @@ -54,7 +54,8 @@ public class LinearSearch implements SearchAlgorithm { * * @param array List to be searched (can be unsorted) * @param value Key being searched for - * @return Location of the key, -1 if array is null or empty, or key not found + * @return the index of the matching element, or -1 if the array is null or empty, + * the value is null, or no matching element is found; null array elements are skipped */ @Override public > int find(T[] array, T value) { From 862de93168d075b950c475c5f7a96239ef161163 Mon Sep 17 00:00:00 2001 From: vanneladas rashmi Date: Sun, 25 Oct 2026 13:33:46 +0530 Subject: [PATCH 15/34] Update README to clarify directory content (#7658) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d60d5104c385..91766e523bca 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ These implementations are intended for learning purposes. As such, they may be l Please read our [Contribution Guidelines](CONTRIBUTING.md) before you contribute to this project. ## Algorithms -Our [directory](DIRECTORY.md) has the full list of applications. +Our [directory](DIRECTORY.md) contains the full list of algorithms implemented in Java. From 2dd1a0a189b0946736debe8cf3999324a0c78cdc Mon Sep 17 00:00:00 2001 From: Vedant Hemraj Kapgate Date: Sun, 25 Oct 2026 23:53:06 +0530 Subject: [PATCH 16/34] Add Booth's algorithm for minimal string rotation (#7644) * Add Booth's algorithm for minimal string rotation * Add tests for Booth's minimal rotation algorithm * Use constant-space comparisons in Booth's algorithm --- .../strings/BoothsAlgorithm.java | 69 +++++++++++++++++++ .../strings/BoothsAlgorithmTest.java | 45 ++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/main/java/com/thealgorithms/strings/BoothsAlgorithm.java create mode 100644 src/test/java/com/thealgorithms/strings/BoothsAlgorithmTest.java diff --git a/src/main/java/com/thealgorithms/strings/BoothsAlgorithm.java b/src/main/java/com/thealgorithms/strings/BoothsAlgorithm.java new file mode 100644 index 000000000000..01c2f67a8569 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/BoothsAlgorithm.java @@ -0,0 +1,69 @@ +package com.thealgorithms.strings; + +/** + * Finds the lexicographically smallest cyclic rotation of a string using + * Booth's algorithm. + * + *

The algorithm compares candidate rotations without materializing them, + * running in O(n) time and O(1) auxiliary space.

+ * + * @see + * Lexicographically minimal string rotation + */ +public final class BoothsAlgorithm { + private BoothsAlgorithm() { + } + + /** + * Returns the starting index of the lexicographically smallest cyclic + * rotation of {@code input}. If several rotations are equal and minimal, + * the algorithm returns the smallest starting index among them. + * + *

For example, the smallest rotation of {@code "baca"} is + * {@code "abac"}, which starts at index {@code 3}.

+ * + * @param input the string whose minimal cyclic rotation is sought + * @return the starting index of the minimal rotation, or {@code 0} for an + * empty string + * @throws IllegalArgumentException if {@code input} is {@code null} + */ + public static int minimalRotationIndex(String input) { + if (input == null) { + throw new IllegalArgumentException("Input string must not be null"); + } + + int length = input.length(); + if (length == 0) { + return 0; + } + + int first = 0; + int second = 1; + int offset = 0; + + while (first < length && second < length && offset < length) { + char firstChar = input.charAt((first + offset) % length); + char secondChar = input.charAt((second + offset) % length); + + if (firstChar == secondChar) { + offset++; + continue; + } + + if (firstChar > secondChar) { + first += offset + 1; + if (first <= second) { + first = second + 1; + } + } else { + second += offset + 1; + if (second <= first) { + second = first + 1; + } + } + offset = 0; + } + + return Math.min(first, second); + } +} diff --git a/src/test/java/com/thealgorithms/strings/BoothsAlgorithmTest.java b/src/test/java/com/thealgorithms/strings/BoothsAlgorithmTest.java new file mode 100644 index 000000000000..19ff58eb0988 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/BoothsAlgorithmTest.java @@ -0,0 +1,45 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class BoothsAlgorithmTest { + + @Test + public void testMinimalRotationIndexForExample() { + assertEquals(3, BoothsAlgorithm.minimalRotationIndex("baca")); + } + + @Test + public void testEmptyString() { + assertEquals(0, BoothsAlgorithm.minimalRotationIndex("")); + } + + @Test + public void testSingleCharacter() { + assertEquals(0, BoothsAlgorithm.minimalRotationIndex("x")); + } + + @Test + public void testRepeatedCharacters() { + assertEquals(0, BoothsAlgorithm.minimalRotationIndex("aaaaa")); + } + + @Test + public void testPeriodicString() { + assertEquals(1, BoothsAlgorithm.minimalRotationIndex("baba")); + assertEquals(1, BoothsAlgorithm.minimalRotationIndex("cabcab")); + } + + @Test + public void testStringAlreadyHasMinimalRotationAtStart() { + assertEquals(0, BoothsAlgorithm.minimalRotationIndex("abc")); + } + + @Test + public void testNullInput() { + assertThrows(IllegalArgumentException.class, () -> BoothsAlgorithm.minimalRotationIndex(null)); + } +} From 976cebc723837cd689af8f6358f7052bc477de6a Mon Sep 17 00:00:00 2001 From: Hardik K Date: Mon, 26 Oct 2026 00:05:41 +0530 Subject: [PATCH 17/34] fix(searches): add null safety and Comparable consistency to JumpSearch (#7653) --- .../thealgorithms/searches/JumpSearch.java | 8 +++-- .../searches/JumpSearchTest.java | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/JumpSearch.java b/src/main/java/com/thealgorithms/searches/JumpSearch.java index 4253f4182db1..5e4c6da15f86 100644 --- a/src/main/java/com/thealgorithms/searches/JumpSearch.java +++ b/src/main/java/com/thealgorithms/searches/JumpSearch.java @@ -56,7 +56,7 @@ * @see BinarySearch * @see LinearSearch */ -public class JumpSearch implements SearchAlgorithm { +public final class JumpSearch implements SearchAlgorithm { /** * Jump Search algorithm implementation. @@ -67,6 +67,10 @@ public class JumpSearch implements SearchAlgorithm { */ @Override public > int find(T[] array, T key) { + if (array == null || array.length == 0 || key == null) { + return -1; + } + int length = array.length; int blockSize = (int) Math.sqrt(length); @@ -78,7 +82,7 @@ public > int find(T[] array, T key) { // Perform linear search within the identified block for (int i = limit - blockSize; i <= limit && i < length; i++) { - if (array[i].equals(key)) { + if (array[i] != null && key.compareTo(array[i]) == 0) { return i; } } diff --git a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java index a5ce93b8a3af..3ecbaeb14dbf 100644 --- a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java @@ -139,4 +139,40 @@ void testJumpSearchFindsEveryElement() { } } } + + @Test + void testJumpSearchNullArray() { + JumpSearch jumpSearch = new JumpSearch(); + assertEquals(-1, jumpSearch.find(null, 5), "Searching in a null array should return -1."); + } + + @Test + void testJumpSearchNullKey() { + JumpSearch jumpSearch = new JumpSearch(); + Integer[] array = {1, 2, 3}; + assertEquals(-1, jumpSearch.find(array, null), "Searching for a null key should return -1."); + } + + @Test + void testJumpSearchSingleElementAbsent() { + JumpSearch jumpSearch = new JumpSearch(); + Integer[] array = {10}; + assertEquals(-1, jumpSearch.find(array, 5), "Searching for an absent key in single element array should return -1."); + } + + @Test + void testJumpSearchStringArray() { + JumpSearch jumpSearch = new JumpSearch(); + String[] array = {"apple", "banana", "cherry", "date", "elderberry"}; + assertEquals(2, jumpSearch.find(array, "cherry"), "The index of 'cherry' should be 2."); + assertEquals(-1, jumpSearch.find(array, "fig"), "Key 'fig' should not be found."); + } + + @Test + void testJumpSearchDuplicates() { + JumpSearch jumpSearch = new JumpSearch(); + Integer[] array = {1, 2, 2, 2, 3, 4, 5}; + int index = jumpSearch.find(array, 2); + assertEquals(2, array[index], "The element at the returned index should equal the target key."); + } } From 96ed2d7970606b51014afd25212c6c86d2af6255 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Mon, 26 Oct 2026 00:42:58 +0530 Subject: [PATCH 18/34] fix(searches): support rectangular matrices and null safety in SearchInARowAndColWiseSortedMatrix (#7655) * fix(searches): support rectangular matrices and null safety in SearchInARowAndColWiseSortedMatrix * style: format SearchInARowAndColWiseSortedMatrixTest with clang-format --- .../SearchInARowAndColWiseSortedMatrix.java | 74 +++++++++++-------- ...earchInARowAndColWiseSortedMatrixTest.java | 32 ++++++++ 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java b/src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java index b53c7e5256ca..20a07adafd26 100644 --- a/src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java +++ b/src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java @@ -1,32 +1,42 @@ -package com.thealgorithms.searches; - -public class SearchInARowAndColWiseSortedMatrix { - /** - * Search a key in row and column wise sorted matrix - * - * @param matrix matrix to be searched - * @param value Key being searched for - * @author Sadiul Hakim : https://github.com/sadiul-hakim - */ - public int[] search(int[][] matrix, int value) { - int n = matrix.length; - // This variable iterates over rows - int i = 0; - // This variable iterates over columns - int j = n - 1; - int[] result = {-1, -1}; - while (i < n && j >= 0) { - if (matrix[i][j] == value) { - result[0] = i; - result[1] = j; - return result; - } - if (value > matrix[i][j]) { - i++; - } else { - j--; - } - } - return result; - } -} +package com.thealgorithms.searches; + +public final class SearchInARowAndColWiseSortedMatrix { + /** + * Search a key in row and column wise sorted matrix + * + * @param matrix matrix to be searched + * @param value Key being searched for + * @return row and column indices if found, otherwise {-1, -1} + * @author Sadiul Hakim : https://github.com/sadiul-hakim + */ + public int[] search(int[][] matrix, int value) { + if (matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0) { + return new int[] {-1, -1}; + } + + int rows = matrix.length; + int cols = matrix[0].length; + // This variable iterates over rows + int i = 0; + // This variable iterates over columns, starting from top-right corner + int j = cols - 1; + int[] result = {-1, -1}; + while (i < rows && j >= 0) { + if (matrix[i] == null || j >= matrix[i].length) { + i++; + continue; + } + if (matrix[i][j] == value) { + result[0] = i; + result[1] = j; + return result; + } + if (value > matrix[i][j]) { + i++; + } else { + j--; + } + } + return result; + } +} diff --git a/src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java b/src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java index 88227804b775..e37eec82cb8e 100644 --- a/src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java +++ b/src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java @@ -63,4 +63,36 @@ void testSearchInColWiseSortedMatrix() { int[] expected = {1, 1}; // Found at (1, 1) assertArrayEquals(expected, searcher.search(matrix, value), "Value should be found in the column-wise sorted matrix"); } + + @Test + void testSearchInNullMatrix() { + assertArrayEquals(new int[] {-1, -1}, searcher.search(null, 5), "Should return {-1, -1} for null matrix"); + } + + @Test + void testSearchInMatrixWithEmptyRows() { + int[][] matrix = {{}}; + assertArrayEquals(new int[] {-1, -1}, searcher.search(matrix, 5), "Should return {-1, -1} for matrix with empty first row"); + } + + @Test + void testSearchInRectangularMatrixMoreColsThanRowsFound() { + int[][] matrix = {{1, 3, 5, 7, 9}, {2, 4, 6, 8, 10}}; + int[] expected = {0, 4}; // Value 9 is at row 0, col 4 + assertArrayEquals(expected, searcher.search(matrix, 9), "Value 9 should be found at row 0, col 4"); + } + + @Test + void testSearchInRectangularMatrixMoreRowsThanColsFound() { + int[][] matrix = {{1, 5}, {2, 6}, {3, 7}, {4, 8}}; + int[] expected = {2, 0}; // Value 3 is at row 2, col 0 + assertArrayEquals(expected, searcher.search(matrix, 3), "Value 3 should be found at row 2, col 0"); + } + + @Test + void testSearchInRectangularMatrixNotFound() { + int[][] matrix = {{1, 3, 5}, {2, 4, 6}, {7, 8, 9}, {10, 11, 12}}; + int[] expected = {-1, -1}; + assertArrayEquals(expected, searcher.search(matrix, 15), "Value 15 should not be found"); + } } From 12f69b38dc094a24116335c75f2baf5323999a05 Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:00:08 +0545 Subject: [PATCH 19/34] fix(sorts): keep duplicate values in TreeSort (#7661) --- .../com/thealgorithms/sorts/TreeSort.java | 49 ++++++++++--------- .../com/thealgorithms/sorts/TreeSortTest.java | 13 ++++- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/thealgorithms/sorts/TreeSort.java b/src/main/java/com/thealgorithms/sorts/TreeSort.java index 6f4e5509489d..1c0b407c7f84 100644 --- a/src/main/java/com/thealgorithms/sorts/TreeSort.java +++ b/src/main/java/com/thealgorithms/sorts/TreeSort.java @@ -3,6 +3,8 @@ import static com.thealgorithms.sorts.SortUtils.print; import com.thealgorithms.datastructures.trees.BSTRecursiveGeneric; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -30,38 +32,39 @@ public > List sort(List unsortedList) { } private > T[] doTreeSortArray(T[] unsortedArray) { - // create a generic BST tree - BSTRecursiveGeneric tree = new BSTRecursiveGeneric(); - - // add all elements to the tree - for (T element : unsortedArray) { - tree.add(element); - } - - // get the sorted list by inorder traversal of the tree - List sortedList = tree.inorderSort(); - - // add the elements back to the initial array - int i = 0; - for (T element : sortedList) { - unsortedArray[i++] = element; - } - - // return the array - return unsortedArray; + // sort through the list path and copy the result back into the initial array + List sortedList = doTreeSortList(Arrays.asList(unsortedArray)); + return sortedList.toArray(unsortedArray); } private > List doTreeSortList(Iterable unsortedList) { - // create a generic BST tree - BSTRecursiveGeneric tree = new BSTRecursiveGeneric(); + // the BST ignores duplicate keys, so pair every element with its position to keep equal values apart + BSTRecursiveGeneric> tree = new BSTRecursiveGeneric<>(); // add all elements to the tree + int position = 0; for (T element : unsortedList) { - tree.add(element); + tree.add(new Keyed<>(element, position++)); } // get the sorted list by inorder traversal of the tree and return it - return tree.inorderSort(); + List sortedList = new ArrayList<>(); + for (Keyed keyed : tree.inorderSort()) { + sortedList.add(keyed.value()); + } + return sortedList; + } + + /** + * An element plus its original position. Equal elements are ordered by position, + * which also makes the sort stable. + */ + private record Keyed>(T value, int position) implements Comparable> { + @Override + public int compareTo(Keyed other) { + int byValue = value.compareTo(other.value); + return byValue != 0 ? byValue : Integer.compare(position, other.position); + } } public static void main(String[] args) { diff --git a/src/test/java/com/thealgorithms/sorts/TreeSortTest.java b/src/test/java/com/thealgorithms/sorts/TreeSortTest.java index 5fb4c18e953e..6cede929ea51 100644 --- a/src/test/java/com/thealgorithms/sorts/TreeSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/TreeSortTest.java @@ -9,9 +9,20 @@ * @see TreeSort */ -public class TreeSortTest { +public class TreeSortTest extends SortingAlgorithmTest { private TreeSort treeSort = new TreeSort(); + @Override + SortAlgorithm getSortAlgorithm() { + return new TreeSort(); + } + + @Test + public void treeSortKeepsDuplicates() { + Integer[] array = {-1, -1, 0}; + assertArrayEquals(new Integer[] {-1, -1, 0}, treeSort.sort(array)); + } + @Test public void treeSortEmptyArray() { Integer[] inputArray = {}; From 6f19dd4eed9ed426c87dcdcb80f0c0dacbfa9f7c Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:16:34 +0545 Subject: [PATCH 20/34] fix(sorts): stop SwapSort looping forever on duplicate values (#7660) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- src/main/java/com/thealgorithms/sorts/SwapSort.java | 7 ++++++- .../java/com/thealgorithms/sorts/SwapSortTest.java | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/sorts/SwapSort.java b/src/main/java/com/thealgorithms/sorts/SwapSort.java index b5c1c3892546..169fba93fc57 100644 --- a/src/main/java/com/thealgorithms/sorts/SwapSort.java +++ b/src/main/java/com/thealgorithms/sorts/SwapSort.java @@ -17,7 +17,12 @@ public > T[] sort(T[] array) { final int amountSmallerElements = this.getSmallerElementCount(array, index); if (amountSmallerElements > 0) { - SortUtils.swap(array, index, index + amountSmallerElements); + int target = index + amountSmallerElements; + // Skip slots that already hold an equal element, otherwise two equal values swap forever + while (array[target].compareTo(array[index]) == 0) { + target++; + } + SortUtils.swap(array, index, target); } else { index++; } diff --git a/src/test/java/com/thealgorithms/sorts/SwapSortTest.java b/src/test/java/com/thealgorithms/sorts/SwapSortTest.java index c1638a385940..e29bbc5f1b64 100644 --- a/src/test/java/com/thealgorithms/sorts/SwapSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/SwapSortTest.java @@ -1,8 +1,21 @@ package com.thealgorithms.sorts; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + public class SwapSortTest extends SortingAlgorithmTest { @Override SortAlgorithm getSortAlgorithm() { return new SwapSort(); } + + @Test + @Timeout(value = 1, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void shouldSortWhenDuplicateWouldBeSwappedOntoItsTwin() { + Integer[] array = {2, 2, 1, 1, 0, 0, 0}; + assertArrayEquals(new Integer[] {0, 0, 0, 1, 1, 2, 2}, new SwapSort().sort(array)); + } } From bd64e8d4028a8f50792528c1557f2d5118d39ef6 Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:40:39 +0545 Subject: [PATCH 21/34] fix(sorts): handle small arrays in FlashSort and empty arrays in DutchNationalFlagSort (#7662) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- .../com/thealgorithms/sorts/DutchNationalFlagSort.java | 3 +++ src/main/java/com/thealgorithms/sorts/FlashSort.java | 3 ++- .../com/thealgorithms/sorts/DutchNationalFlagSortTest.java | 6 ++++++ src/test/java/com/thealgorithms/sorts/FlashSortTest.java | 7 +++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java b/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java index f7e12da06568..2e3a2fcea704 100644 --- a/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java +++ b/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java @@ -13,6 +13,9 @@ public class DutchNationalFlagSort implements SortAlgorithm { @Override public > T[] sort(T[] array) { + if (array.length == 0) { + return array; + } return dutchNationalFlagSort(array, array[(int) Math.ceil((array.length) / 2.0) - 1]); } diff --git a/src/main/java/com/thealgorithms/sorts/FlashSort.java b/src/main/java/com/thealgorithms/sorts/FlashSort.java index e8dbf8c42742..79c2d8b82700 100644 --- a/src/main/java/com/thealgorithms/sorts/FlashSort.java +++ b/src/main/java/com/thealgorithms/sorts/FlashSort.java @@ -76,7 +76,8 @@ private > void flashSort(T[] arr) { return; // All elements are the same } - final int m = (int) (classificationRatio * arr.length); + // At least one class, otherwise small arrays index into an empty classification table + final int m = Math.max(1, (int) (classificationRatio * arr.length)); final int[] classificationArray = new int[m]; diff --git a/src/test/java/com/thealgorithms/sorts/DutchNationalFlagSortTest.java b/src/test/java/com/thealgorithms/sorts/DutchNationalFlagSortTest.java index c7581e7c8f7b..d9c014e8b21d 100644 --- a/src/test/java/com/thealgorithms/sorts/DutchNationalFlagSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/DutchNationalFlagSortTest.java @@ -109,4 +109,10 @@ void testOddStringsMidGivenDnfs() { dutchNationalFlagSort.sort(strings, "e"); assertArrayEquals(strings, stringsResult); } + + @Test + void testEmptyArray() { + Integer[] integers = {}; + assertArrayEquals(new Integer[] {}, new DutchNationalFlagSort().sort(integers)); + } } diff --git a/src/test/java/com/thealgorithms/sorts/FlashSortTest.java b/src/test/java/com/thealgorithms/sorts/FlashSortTest.java index 5b27975d3bea..6b4b1230925f 100644 --- a/src/test/java/com/thealgorithms/sorts/FlashSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/FlashSortTest.java @@ -1,5 +1,6 @@ package com.thealgorithms.sorts; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -59,6 +60,12 @@ public List dynamicTestsForSorting() { return dynamicTests; } + @Test + public void testArraysTooSmallForMoreThanOneClass() { + assertArrayEquals(new Integer[] {1, 2}, new FlashSort().sort(new Integer[] {2, 1})); + assertArrayEquals(new Integer[] {1, 2, 3, 4, 5}, new FlashSort(0.1).sort(new Integer[] {5, 3, 1, 4, 2})); + } + private List createDynamicTestsForRatio(double ratio) { List dynamicTests = new ArrayList<>(); for (TestMethod testMethod : getTestMethodsFromSuperClass()) { From a7a244157cf22e62a8f7c95fd6f6be5628edfa7a Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:06:47 +0545 Subject: [PATCH 22/34] fix(strings): use case-insensitive shift table in HorspoolSearch.findFirstInsensitive (#7663) Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> --- .../thealgorithms/strings/HorspoolSearch.java | 18 +++++++++++------- .../strings/HorspoolSearchTest.java | 7 +++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/thealgorithms/strings/HorspoolSearch.java b/src/main/java/com/thealgorithms/strings/HorspoolSearch.java index d9187cbf66c4..51dcdd804b25 100644 --- a/src/main/java/com/thealgorithms/strings/HorspoolSearch.java +++ b/src/main/java/com/thealgorithms/strings/HorspoolSearch.java @@ -95,7 +95,7 @@ public static Integer getLastComparisons() { * @return index of first occurrence of the pattern in the text */ private static int firstOccurrence(String pattern, String text, boolean caseSensitive) { - shiftValues = calcShiftValues(pattern); // build the bad symbol table + shiftValues = calcShiftValues(pattern, caseSensitive); // build the bad symbol table comparisons = 0; // reset comparisons if (pattern.length() == 0) { // return failure, if pattern empty @@ -113,7 +113,7 @@ private static int firstOccurrence(String pattern, String text, boolean caseSens char patternChar = pattern.charAt(i); char textChar = text.charAt((textIndex + i) - (pattern.length() - 1)); if (!charEquals(patternChar, textChar, caseSensitive)) { // bad character, shift pattern - textIndex += getShiftValue(text.charAt(textIndex)); + textIndex += getShiftValue(text.charAt(textIndex), caseSensitive); break; } i--; @@ -153,14 +153,15 @@ private static boolean charEquals(char c1, char c2, boolean caseSensitive) { * in the table, then it is not a rightmost occurrence, so it continues. * * @param pattern basis for the bad symbol table + * @param caseSensitive whether to build the table with case-sensitive keys * @return the bad symbol table */ - private static HashMap calcShiftValues(String pattern) { + private static HashMap calcShiftValues(String pattern, boolean caseSensitive) { patternLength = pattern.length(); HashMap table = new HashMap<>(); for (int i = pattern.length() - 2; i >= 0; i--) { // length - 2 is the index of the second to last character - char c = pattern.charAt(i); + char c = caseSensitive ? pattern.charAt(i) : Character.toLowerCase(pattern.charAt(i)); int finalI = i; table.computeIfAbsent(c, k -> pattern.length() - 1 - finalI); } @@ -173,11 +174,14 @@ private static HashMap calcShiftValues(String pattern) { * appropriate shift value for a given character * * @param c character + * @param caseSensitive whether the table was built with case-sensitive keys * @return shift value that corresponds to the character argument */ - private static Integer getShiftValue(char c) { - if (shiftValues.get(c) != null) { - return shiftValues.get(c); + private static Integer getShiftValue(char c, boolean caseSensitive) { + // the table holds lower-case keys for case-insensitive searches + char key = caseSensitive ? c : Character.toLowerCase(c); + if (shiftValues.get(key) != null) { + return shiftValues.get(key); } else { return patternLength; } diff --git a/src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java b/src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java index 9649a89a3325..d364e4923e70 100644 --- a/src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java +++ b/src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java @@ -61,6 +61,13 @@ void testFindFirstInsensitiveNotMatch() { assertEquals(-1, index); } + @Test + void testFindFirstInsensitiveWhenCaseDiffersFromPattern() { + assertEquals(1, HorspoolSearch.findFirstInsensitive("AB", "xab")); + assertEquals(1, HorspoolSearch.findFirstInsensitive("ab", "xAB")); + assertEquals(2, HorspoolSearch.findFirstInsensitive("ba", "bbBA")); + } + @Test void testGetLastComparisons() { HorspoolSearch.findFirst("World", "Hello World"); From 231979449940934d77c34ba8fa2cb7143de71916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:39:29 +0200 Subject: [PATCH 23/34] chore(deps): bump github/codeql-action from 4.39.0 to 4.39.1 in /.github/workflows (#7676) chore(deps): bump github/codeql-action in /.github/workflows Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.39.0 to 4.39.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.39.0...v4.39.1) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.39.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ae38aa619695..d9d9ed1e54e9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v4.39.0 + uses: github/codeql-action/init@v4.39.1 with: languages: 'java-kotlin' @@ -38,7 +38,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.39.0 + uses: github/codeql-action/analyze@v4.39.1 with: category: "/language:java-kotlin" @@ -55,12 +55,12 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.39.0 + uses: github/codeql-action/init@v4.39.1 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.39.0 + uses: github/codeql-action/analyze@v4.39.1 with: category: "/language:actions" ... From 410f08753505a0af9e46423396e8768bc97cbc1c Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:01:39 +0545 Subject: [PATCH 24/34] fix(lists): leave SkipList size unchanged when removing a missing element (#7666) --- .../datastructures/lists/SkipList.java | 3 +++ .../datastructures/lists/SkipListTest.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java b/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java index 0b4fcd91483c..7e03ca3856b5 100644 --- a/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java +++ b/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java @@ -121,6 +121,9 @@ public void remove(E e) { current = next; } } + if (layer < 0) { + return; // not found, nothing to unlink + } for (int i = 0; i <= layer; i++) { current.previous(i).setNext(i, current.next(i)); if (current.next(i) != null) { diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java index 16d1a015a4d9..53707ffab1b6 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java @@ -106,6 +106,20 @@ void testClearViaRemovals() { assertEquals(0, skipList.size()); } + @Test + @DisplayName("Removing a missing element leaves the list unchanged") + void testRemoveMissing() { + skipList.remove("missing"); + assertEquals(0, skipList.size()); + + skipList.add("a"); + skipList.add("c"); + skipList.remove("b"); + assertEquals(2, skipList.size()); + assertEquals("a", skipList.get(0)); + assertEquals("c", skipList.get(1)); + } + private SkipList createSkipList() { SkipList s = new SkipList<>(); String[] values = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}; From 7643e245b57a4f1da04460a0ff5c30c116f94e16 Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:18:05 +0545 Subject: [PATCH 25/34] fix(searches): support characters outside extended ASCII in BoyerMoore (#7664) --- .../com/thealgorithms/searches/BoyerMoore.java | 18 ++++++++---------- .../thealgorithms/searches/BoyerMooreTest.java | 7 +++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/BoyerMoore.java b/src/main/java/com/thealgorithms/searches/BoyerMoore.java index 6998021503cc..5c491210302b 100644 --- a/src/main/java/com/thealgorithms/searches/BoyerMoore.java +++ b/src/main/java/com/thealgorithms/searches/BoyerMoore.java @@ -1,5 +1,8 @@ package com.thealgorithms.searches; +import java.util.HashMap; +import java.util.Map; + /** * Boyer-Moore string search algorithm. * Efficient algorithm for substring search. @@ -7,21 +10,16 @@ */ public class BoyerMoore { - private final int radix; // Radix (number of possible characters) - private final int[] right; // Bad character rule table + private final Map right; // Bad character rule table: last index of each pattern character private final String pattern; public BoyerMoore(String pat) { this.pattern = pat; - this.radix = 256; - this.right = new int[radix]; - - for (int c = 0; c < radix; c++) { - right[c] = -1; - } + // A map instead of a 256-entry array, so characters outside extended ASCII work too + this.right = new HashMap<>(); for (int j = 0; j < pat.length(); j++) { - right[pat.charAt(j)] = j; + right.put(pat.charAt(j), j); } } @@ -40,7 +38,7 @@ public int search(String text) { char txtChar = text.charAt(i + j); char patChar = pattern.charAt(j); if (patChar != txtChar) { - skip = Math.max(1, j - right[txtChar]); + skip = Math.max(1, j - right.getOrDefault(txtChar, -1)); break; } } diff --git a/src/test/java/com/thealgorithms/searches/BoyerMooreTest.java b/src/test/java/com/thealgorithms/searches/BoyerMooreTest.java index 9021eacbb8ee..b89fa23b1195 100644 --- a/src/test/java/com/thealgorithms/searches/BoyerMooreTest.java +++ b/src/test/java/com/thealgorithms/searches/BoyerMooreTest.java @@ -52,4 +52,11 @@ public void testStaticSearchMethod() { int index = BoyerMoore.staticSearch(text, "CDE"); assertEquals(2, index); // Primera ocurrencia de "CDE" } + + @Test + public void testCharactersOutsideExtendedAscii() { + assertEquals(1, BoyerMoore.staticSearch("x中文", "中")); + assertEquals(-1, BoyerMoore.staticSearch("中文", "x")); + assertEquals(2, BoyerMoore.staticSearch("€ €uro", "€uro")); + } } From 8b49bc5046c6df87441f85aabb1f110fc8c03be6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:55:53 +0000 Subject: [PATCH 26/34] chore(deps): bump actions/setup-python from 7.0.0 to 7.1.0 in /.github/workflows (#7677) chore(deps): bump actions/setup-python in /.github/workflows Bumps [actions/setup-python](https://github.com/actions/setup-python) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v7.0.0...v7.1.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/project_structure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project_structure.yml b/.github/workflows/project_structure.yml index a70c3240ce08..764832b7b6a6 100644 --- a/.github/workflows/project_structure.yml +++ b/.github/workflows/project_structure.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v7.0.0 + - uses: actions/setup-python@v7.1.0 with: python-version: '3.13' From 6edd2d878947c7fb08a6f3286b316eca09fe9200 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:50:13 +0200 Subject: [PATCH 27/34] chore(deps-dev): bump org.jacoco:jacoco-maven-plugin from 0.8.15 to 0.8.16 (#7727) chore(deps-dev): bump org.jacoco:jacoco-maven-plugin Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.15 to 0.8.16. - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.15...v0.8.16) --- updated-dependencies: - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.16 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 21dd79ad17b1..c31c3e7915eb 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ org.jacoco jacoco-maven-plugin - 0.8.15 + 0.8.16 From 36e6d6669120bee21b6cb73c7a9b5eb01469c137 Mon Sep 17 00:00:00 2001 From: NanditaMazz Date: Tue, 3 Nov 2026 17:52:16 +0530 Subject: [PATCH 28/34] test: add Fibonacci matrix exponentiation tests (#7719) * test: add Fibonacci matrix exponentiation tests * style: fix Fibonacci test formatting --- .../matrixexponentiation/FibonacciTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/java/com/thealgorithms/matrix/matrixexponentiation/FibonacciTest.java diff --git a/src/test/java/com/thealgorithms/matrix/matrixexponentiation/FibonacciTest.java b/src/test/java/com/thealgorithms/matrix/matrixexponentiation/FibonacciTest.java new file mode 100644 index 000000000000..9f70940bde19 --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/matrixexponentiation/FibonacciTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.matrix.matrixexponentiation; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; +class FibonacciTest { + private static BigDecimal[][] matrix(String a, String b, String c, String d) { + return new BigDecimal[][] {{new BigDecimal(a), new BigDecimal(b)}, {new BigDecimal(c), new BigDecimal(d)}}; + } + @Test + void testFibZeroReturnsIdentityMatrix() { + assertArrayEquals(matrix("1", "0", "0", "1"), Fibonacci.fib(0)); + } + @Test + void testFibOne() { + assertArrayEquals(matrix("1", "1", "1", "0"), Fibonacci.fib(1)); + } + @Test + void testFibEvenInput() { + assertArrayEquals(matrix("2", "1", "1", "1"), Fibonacci.fib(2)); + } + @Test + void testFibOddInput() { + assertArrayEquals(matrix("3", "2", "2", "1"), Fibonacci.fib(3)); + } + @Test + void testFibTen() { + assertArrayEquals(matrix("89", "55", "55", "34"), Fibonacci.fib(10)); + } + @Test + void testFibLargeInput() { + assertArrayEquals(matrix("20365011074", "12586269025", "12586269025", "7778742049"), Fibonacci.fib(50)); + } +} From e6b2ecb50f8278c55ade38b947c5c82afdc68438 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:01:25 +0000 Subject: [PATCH 29/34] chore(deps-dev): bump org.apache.maven.plugins:maven-surefire-plugin from 3.6.0 to 3.6.1 (#7700) chore(deps-dev): bump org.apache.maven.plugins:maven-surefire-plugin Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.6.0 to 3.6.1. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.6.0...surefire-3.6.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.6.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c31c3e7915eb..d7c7a0849fb8 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ maven-surefire-plugin - 3.6.0 + 3.6.1 From 4f535f583503c5225d3f2d8cb681b0cb0492bc5d Mon Sep 17 00:00:00 2001 From: Rishavbhattarai <67967801+Rishavbhattarai@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:52:09 +0545 Subject: [PATCH 30/34] fix(searches): avoid int overflow and short-text crash in RabinKarpAlgorithm (#7665) --- .../thealgorithms/searches/RabinKarpAlgorithm.java | 11 ++++++++--- .../searches/RabinKarpAlgorithmTest.java | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java b/src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java index 81e16dbbbf07..35d696f2a2f2 100644 --- a/src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java +++ b/src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java @@ -13,9 +13,14 @@ public static int search(String pattern, String text, int primeNumber) { int index = -1; // -1 here represents not found int patternLength = pattern.length(); int textLength = text.length(); - int hashForPattern = 0; - int hashForText = 0; - int h = 1; + if (patternLength > textLength) { + return index; + } + + // long, so hash * ALPHABET_SIZE cannot overflow for large primes such as 1_000_000_007 + long hashForPattern = 0; + long hashForText = 0; + long h = 1; // The value of h would be "pow(d, patternLength-1)%primeNumber" for (int i = 0; i < patternLength - 1; i++) { diff --git a/src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java b/src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java index 40e1acce9c3a..3e2314ad89cd 100644 --- a/src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java +++ b/src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java @@ -8,7 +8,7 @@ class RabinKarpAlgorithmTest { @ParameterizedTest - @CsvSource({"This is an example for rabin karp algorithmn, algorithmn, 101", "AAABBDDG, AAA, 137", "AAABBCCBB, BBCC, 101", "AAABBCCBB, BBCC, 131", "AAAABBBBCCC, CCC, 41", "ABCBCBCAAB, AADB, 293", "Algorithm The Algorithm, Algorithm, 101"}) + @CsvSource({"This is an example for rabin karp algorithmn, algorithmn, 101", "AAABBDDG, AAA, 137", "AAABBCCBB, BBCC, 101", "AAABBCCBB, BBCC, 131", "AAAABBBBCCC, CCC, 41", "ABCBCBCAAB, AADB, 293", "Algorithm The Algorithm, Algorithm, 101", "nzdjsjggwx, jggw, 1000000007", "AB, ABC, 101"}) void rabinKarpAlgorithmTestExample(String txt, String pat, int q) { int indexFromOurAlgorithm = RabinKarpAlgorithm.search(pat, txt, q); int indexFromLinearSearch = txt.indexOf(pat); From 51a0156d8431f1e93021fa9a3dd6ef0533d04880 Mon Sep 17 00:00:00 2001 From: Bhumit Peswani Date: Thu, 5 Nov 2026 18:45:16 +0530 Subject: [PATCH 31/34] fix(maths): prevent infinite recursion on negative inputs in BinomialCoefficient (#7730) --- .../com/thealgorithms/maths/BinomialCoefficient.java | 5 +++++ .../thealgorithms/maths/BinomialCoefficientTest.java | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java b/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java index 1de39adbc18a..a7caf7efa44f 100644 --- a/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java +++ b/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java @@ -21,9 +21,14 @@ private BinomialCoefficient() { * @param numberOfObjects Number of objects to be chosen from total_objects * @return number of ways in which no_of_objects objects can be chosen from total_objects * objects + * @throws IllegalArgumentException if totalObjects or numberOfObjects is negative */ public static int binomialCoefficient(int totalObjects, int numberOfObjects) { + if (totalObjects < 0 || numberOfObjects < 0) { + throw new IllegalArgumentException("Arguments must be non-negative."); + } + // Base Case if (numberOfObjects > totalObjects) { return 0; diff --git a/src/test/java/com/thealgorithms/maths/BinomialCoefficientTest.java b/src/test/java/com/thealgorithms/maths/BinomialCoefficientTest.java index 58cfcbad57fb..29081f01b26a 100644 --- a/src/test/java/com/thealgorithms/maths/BinomialCoefficientTest.java +++ b/src/test/java/com/thealgorithms/maths/BinomialCoefficientTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.maths; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -12,5 +13,14 @@ void testBinomialCoefficient() { assertEquals(792, BinomialCoefficient.binomialCoefficient(12, 5)); assertEquals(84, BinomialCoefficient.binomialCoefficient(9, 3)); assertEquals(1, BinomialCoefficient.binomialCoefficient(17, 17)); + assertEquals(0, BinomialCoefficient.binomialCoefficient(5, 6)); + assertEquals(1, BinomialCoefficient.binomialCoefficient(0, 0)); + } + + @Test + void testNegativeInputs() { + assertThrows(IllegalArgumentException.class, () -> BinomialCoefficient.binomialCoefficient(5, -1)); + assertThrows(IllegalArgumentException.class, () -> BinomialCoefficient.binomialCoefficient(-1, 5)); + assertThrows(IllegalArgumentException.class, () -> BinomialCoefficient.binomialCoefficient(-5, -2)); } } From 0a76bf8db406f8c6b6569cd83bfc31b3e118460f Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:57:49 +0530 Subject: [PATCH 32/34] fix(searches): fix out-of-bounds in single-row and single-column in BinarySearch2dArray (#7724) --- .../searches/BinarySearch2dArray.java | 9 +++- .../searches/BinarySearch2dArrayTest.java | 53 ++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/thealgorithms/searches/BinarySearch2dArray.java b/src/main/java/com/thealgorithms/searches/BinarySearch2dArray.java index aa938447b864..317db0dead05 100644 --- a/src/main/java/com/thealgorithms/searches/BinarySearch2dArray.java +++ b/src/main/java/com/thealgorithms/searches/BinarySearch2dArray.java @@ -28,13 +28,13 @@ private BinarySearch2dArray() { * @return An array containing the row and column indices of the target, or [-1, * -1] if the target is not found. */ - static int[] binarySearch(int[][] arr, int target) { + public static int[] binarySearch(int[][] arr, int target) { int rowCount = arr.length; int colCount = arr[0].length; // Edge case: If there's only one row, search that row directly. if (rowCount == 1) { - return binarySearch(arr, target, 0, 0, colCount); + return binarySearch(arr, target, 0, 0, colCount - 1); } // Set initial boundaries for binary search on rows. @@ -70,6 +70,11 @@ else if (arr[midRow][midCol] < target) { return new int[] {endRow, midCol}; } + // Edge case: If there is only one column, target cannot be outside midCol. + if (colCount == 1) { + return new int[] {-1, -1}; + } + // If target is smaller than the element in the left of startRow, perform a // binary search on the left of startRow. if (target <= arr[startRow][midCol - 1]) { diff --git a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java index dec2c86de9c7..236e1be5cc23 100644 --- a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java +++ b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java @@ -1,5 +1,6 @@ package com.thealgorithms.searches; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -84,7 +85,7 @@ public void binarySearch2dArrayTestLowerSide() { // valid test case public void binarySearch2dArrayTestNotFound() { int[][] arr = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; - int target = 101; + int target = 100; int[] ans = BinarySearch2dArray.binarySearch(arr, target); System.out.println(Arrays.toString(ans)); @@ -93,7 +94,7 @@ public void binarySearch2dArrayTestNotFound() { } /** - * Test if the method works with input arrays consisting only of one row. + * Test if the method works with an array that only has one row. */ @Test public void binarySearch2dArrayTestOneRow() { @@ -154,4 +155,52 @@ public void binarySearch2dArrayTestEmptyArray() { // Assert that an empty array is not valid input for the method. assertThrows(ArrayIndexOutOfBoundsException.class, () -> BinarySearch2dArray.binarySearch(arr, target)); } + + @Test + public void binarySearch2dArrayTestSingleRowTargetNotFoundGreater() { + int[][] arr = {{1, 2, 3, 4}}; + int target = 10; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {-1, -1}, ans); + } + + @Test + public void binarySearch2dArrayTestSingleRowTargetNotFoundSmaller() { + int[][] arr = {{1, 2, 3, 4}}; + int target = 0; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {-1, -1}, ans); + } + + @Test + public void binarySearch2dArrayTestSingleColumnPresent() { + int[][] arr = {{1}, {3}, {5}}; + int target = 3; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {1, 0}, ans); + } + + @Test + public void binarySearch2dArrayTestSingleColumnNotFound() { + int[][] arr = {{1}, {3}, {5}}; + int target = 4; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {-1, -1}, ans); + } + + @Test + public void binarySearch2dArrayTestSingleElementPresent() { + int[][] arr = {{42}}; + int target = 42; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {0, 0}, ans); + } + + @Test + public void binarySearch2dArrayTestSingleElementNotFound() { + int[][] arr = {{42}}; + int target = 10; + int[] ans = BinarySearch2dArray.binarySearch(arr, target); + assertArrayEquals(new int[] {-1, -1}, ans); + } } From 8b19b878dbeb1caf06fc41d86c8c07c162bc69a2 Mon Sep 17 00:00:00 2001 From: Bhumit Peswani Date: Fri, 6 Nov 2026 02:25:37 +0530 Subject: [PATCH 33/34] fix(dynamicprogramming): handle n=0 and negative inputs safely in CatalanNumber (#7731) --- .../thealgorithms/dynamicprogramming/CatalanNumber.java | 8 ++++++++ .../dynamicprogramming/CatalanNumberTest.java | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/CatalanNumber.java b/src/main/java/com/thealgorithms/dynamicprogramming/CatalanNumber.java index d01066f611bd..b21beafba746 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/CatalanNumber.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/CatalanNumber.java @@ -21,8 +21,16 @@ private CatalanNumber() { * n > 50, BigInteger class should be used instead long * * @return catalanArray[n] the nth Catalan number + * @throws IllegalArgumentException if n is negative */ static long findNthCatalan(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be non-negative"); + } + if (n <= 1) { + return 1; + } + // Array to store the results of subproblems i.e Catalan numbers from [1...n-1] long[] catalanArray = new long[n + 1]; diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/CatalanNumberTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/CatalanNumberTest.java index a065bb8c2a10..08dcde5decb6 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/CatalanNumberTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/CatalanNumberTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.dynamicprogramming; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -8,7 +9,14 @@ public class CatalanNumberTest { @Test public void testCatalanNumber() { + assertEquals(1, CatalanNumber.findNthCatalan(0)); + assertEquals(1, CatalanNumber.findNthCatalan(1)); assertEquals(42, CatalanNumber.findNthCatalan(5)); assertEquals(16796, CatalanNumber.findNthCatalan(10)); } + + @Test + public void testNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> CatalanNumber.findNthCatalan(-1)); + } } From 1e59594d8dee696a85cad5561cc6a533b6824930 Mon Sep 17 00:00:00 2001 From: Nithin U <106614289+NithinU2802@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:36:30 +0530 Subject: [PATCH 34/34] To Fix maze recursion boundary validation with code improvements (#7672) * HeavyLightDecomposition Update * HeavyLightDecompositionTest Update * Update DIRECTORY.md * Improving Code Coverage Update * Format Update * Format Update * Format code with clang-format * Fix Multiple Variable Declarations * Multiple Variable Declarations * Build Failure Update * Revert Changes * New Line Update * Array to List Conversion * fix: handle maze boundaries with validation and improvements * fix: to update lint issue * Patch: to improve test coverage * test: to avoid Infer null dereference warning * patch: to improve test coverage --- .../backtracking/MazeRecursion.java | 162 +++++++++--------- .../backtracking/MazeRecursionTest.java | 99 +++++++++++ 2 files changed, 183 insertions(+), 78 deletions(-) diff --git a/src/main/java/com/thealgorithms/backtracking/MazeRecursion.java b/src/main/java/com/thealgorithms/backtracking/MazeRecursion.java index 8247172e7ee0..00c3fedca71f 100644 --- a/src/main/java/com/thealgorithms/backtracking/MazeRecursion.java +++ b/src/main/java/com/thealgorithms/backtracking/MazeRecursion.java @@ -4,122 +4,128 @@ * This class contains methods to solve a maze using recursive backtracking. * The maze is represented as a 2D array where walls, paths, and visited/dead * ends are marked with different integers. - * - * The goal is to find a path from a starting position to the target position - * (map[6][5]) while navigating through the maze. */ public final class MazeRecursion { + private static final int[][] FIRST_STRATEGY_DIRECTIONS = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + private static final int[][] SECOND_STRATEGY_DIRECTIONS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; private MazeRecursion() { } /** - * This method solves the maze using the "down -> right -> up -> left" - * movement strategy. + * Solves the maze using the "down -> right -> up -> left" movement strategy, + * starting at (1, 1) and targeting (6, 5). * - * @param map The 2D array representing the maze (walls, paths, etc.) - * @return The solved maze with paths marked, or null if no solution exists. + * @param map the 2D array representing the maze (walls, paths, etc.) + * @return the solved maze with paths marked, or null if no solution exists or a fixed endpoint is outside the maze + * @throws IllegalArgumentException if the maze is null, empty, or not rectangular */ public static int[][] solveMazeUsingFirstStrategy(int[][] map) { - if (setWay(map, 1, 1)) { + validateMaze(map); + if (!isInBounds(map, 1, 1) || !isInBounds(map, 6, 5)) { + return null; + } + if (map[6][5] == 2) { return map; } - return null; + return solve(map, 1, 1, 6, 5, FIRST_STRATEGY_DIRECTIONS) ? map : null; } /** - * This method solves the maze using the "up -> right -> down -> left" - * movement strategy. + * Solves the maze using the "up -> right -> down -> left" movement strategy, + * starting at (1, 1) and targeting (6, 5). * - * @param map The 2D array representing the maze (walls, paths, etc.) - * @return The solved maze with paths marked, or null if no solution exists. + * @param map the 2D array representing the maze (walls, paths, etc.) + * @return the solved maze with paths marked, or null if no solution exists or a fixed endpoint is outside the maze + * @throws IllegalArgumentException if the maze is null, empty, or not rectangular */ public static int[][] solveMazeUsingSecondStrategy(int[][] map) { - if (setWay2(map, 1, 1)) { + validateMaze(map); + if (!isInBounds(map, 1, 1) || !isInBounds(map, 6, 5)) { + return null; + } + if (map[6][5] == 2) { return map; } - return null; + return solve(map, 1, 1, 6, 5, SECOND_STRATEGY_DIRECTIONS) ? map : null; } /** - * Attempts to find a path through the maze using a "down -> right -> up -> left" - * movement strategy. The path is marked with '2' for valid paths and '3' for dead ends. + * Solves the maze using the "down -> right -> up -> left" movement strategy. * - * @param map The 2D array representing the maze (walls, paths, etc.) - * @param i The current x-coordinate of the ball (row index) - * @param j The current y-coordinate of the ball (column index) - * @return True if a path is found to (6,5), otherwise false + * @param map the 2D array representing the maze (0 = open, non-zero = blocked) + * @param startRow the starting row + * @param startColumn the starting column + * @param goalRow the goal row + * @param goalColumn the goal column + * @return the solved maze with paths marked, or null if no solution exists + * @throws IllegalArgumentException if the maze is null, empty, not rectangular, + * or a coordinate is outside the maze */ - private static boolean setWay(int[][] map, int i, int j) { - if (map[6][5] == 2) { - return true; + public static int[][] solveMazeUsingFirstStrategy(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) { + validateCoordinates(map, startRow, startColumn, goalRow, goalColumn); + return solve(map, startRow, startColumn, goalRow, goalColumn, FIRST_STRATEGY_DIRECTIONS) ? map : null; + } + + /** + * Solves the maze using the "up -> right -> down -> left" movement strategy. + * + * @param map the 2D array representing the maze (0 = open, non-zero = blocked) + * @param startRow the starting row + * @param startColumn the starting column + * @param goalRow the goal row + * @param goalColumn the goal column + * @return the solved maze with paths marked, or null if no solution exists + * @throws IllegalArgumentException if the maze is null, empty, not rectangular, + * or a coordinate is outside the maze + */ + public static int[][] solveMazeUsingSecondStrategy(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) { + validateCoordinates(map, startRow, startColumn, goalRow, goalColumn); + return solve(map, startRow, startColumn, goalRow, goalColumn, SECOND_STRATEGY_DIRECTIONS) ? map : null; + } + + private static boolean solve(int[][] map, int row, int column, int goalRow, int goalColumn, int[][] directions) { + if (!isInBounds(map, row, column) || map[row][column] != 0) { + return false; } - // If the current position is unvisited (0), explore it - if (map[i][j] == 0) { - // Mark the current position as '2' - map[i][j] = 2; + map[row][column] = 2; + if (row == goalRow && column == goalColumn) { + return true; + } - // Move down - if (setWay(map, i + 1, j)) { - return true; - } - // Move right - else if (setWay(map, i, j + 1)) { - return true; - } - // Move up - else if (setWay(map, i - 1, j)) { - return true; - } - // Move left - else if (setWay(map, i, j - 1)) { + for (int[] direction : directions) { + if (solve(map, row + direction[0], column + direction[1], goalRow, goalColumn, directions)) { return true; } - - map[i][j] = 3; // Mark as dead end (3) if no direction worked - return false; } + + map[row][column] = 3; return false; } - /** - * Attempts to find a path through the maze using an alternative movement - * strategy "up -> right -> down -> left". - * - * @param map The 2D array representing the maze (walls, paths, etc.) - * @param i The current x-coordinate of the ball (row index) - * @param j The current y-coordinate of the ball (column index) - * @return True if a path is found to (6,5), otherwise false - */ - private static boolean setWay2(int[][] map, int i, int j) { - if (map[6][5] == 2) { - return true; + private static int validateMaze(int[][] map) { + if (map == null || map.length == 0 || map[0] == null || map[0].length == 0) { + throw new IllegalArgumentException("Maze must not be null or empty."); } - if (map[i][j] == 0) { - map[i][j] = 2; - - // Move up - if (setWay2(map, i - 1, j)) { - return true; - } - // Move right - else if (setWay2(map, i, j + 1)) { - return true; - } - // Move down - else if (setWay2(map, i + 1, j)) { - return true; - } - // Move left - else if (setWay2(map, i, j - 1)) { - return true; + int columnCount = map[0].length; + for (int[] row : map) { + if (row == null || row.length != columnCount) { + throw new IllegalArgumentException("Maze must be a non-empty rectangular matrix."); } + } + return columnCount; + } - map[i][j] = 3; // Mark as dead end (3) if no direction worked - return false; + private static void validateCoordinates(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) { + int columnCount = validateMaze(map); + if (startRow < 0 || startRow >= map.length || startColumn < 0 || startColumn >= columnCount || goalRow < 0 || goalRow >= map.length || goalColumn < 0 || goalColumn >= columnCount) { + throw new IllegalArgumentException("Start and goal coordinates must be inside the maze."); } - return false; + } + + private static boolean isInBounds(int[][] map, int row, int column) { + return row >= 0 && row < map.length && column >= 0 && column < map[row].length; } } diff --git a/src/test/java/com/thealgorithms/backtracking/MazeRecursionTest.java b/src/test/java/com/thealgorithms/backtracking/MazeRecursionTest.java index b8e77fb38bad..7d8a0c66c262 100644 --- a/src/test/java/com/thealgorithms/backtracking/MazeRecursionTest.java +++ b/src/test/java/com/thealgorithms/backtracking/MazeRecursionTest.java @@ -1,6 +1,9 @@ package com.thealgorithms.backtracking; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -10,6 +13,102 @@ */ public class MazeRecursionTest { + @Test + public void testSmallMazeWithExplicitCoordinates() { + int[][] firstStrategyMaze = {{0, 0}, {0, 0}}; + int[][] secondStrategyMaze = {{0, 0}, {0, 0}}; + + assertArrayEquals(new int[][] {{2, 0}, {2, 2}}, MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze, 0, 0, 1, 1)); + assertArrayEquals(new int[][] {{2, 2}, {0, 2}}, MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze, 0, 0, 1, 1)); + } + + @Test + public void testLegacyMethodsReturnNullWhenFixedGoalIsOutsideMaze() { + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[1][1])); + assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[1][1])); + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[2][2])); + assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[2][2])); + } + + @Test + public void testLegacyMethodsReturnNullWhenFixedGoalRowOrColumnIsOutsideMaze() { + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[7][5])); + assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[6][6])); + } + + @Test + public void testLegacyMethodsReturnNullWhenNoPathExists() { + int[][] firstStrategyMaze = new int[7][6]; + int[][] secondStrategyMaze = new int[7][6]; + firstStrategyMaze[1][1] = 1; + secondStrategyMaze[1][1] = 1; + + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze)); + assertNull(MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze)); + } + + @Test + public void testLegacyMethodsAcceptAlreadyMarkedGoal() { + int[][] firstStrategyMaze = new int[7][6]; + int[][] secondStrategyMaze = new int[7][6]; + firstStrategyMaze[6][5] = 2; + secondStrategyMaze[6][5] = 2; + + assertSame(firstStrategyMaze, MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze)); + assertSame(secondStrategyMaze, MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze)); + } + + @Test + public void testNoPathExistsWithExplicitCoordinates() { + int[][] maze = {{0, 1}, {1, 0}}; + + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, 1)); + } + + @Test + public void testExplicitCoordinatesMustBeInsideMaze() { + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[2][2], 0, 0, 2, 1)); + } + + @Test + public void testEachStartAndGoalCoordinateMustBeInsideMaze() { + int[][] maze = new int[2][2]; + + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, -1, 0, 1, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 2, 0, 1, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, -1, 1, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 2, 1, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, -1, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 2, 1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, -1)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, 2)); + } + + @Test + public void testMazeMustNotBeNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(null)); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[0][0])); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {null})); + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {new int[0]})); + } + + @Test + public void testMazeMustBeRectangular() { + assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{0, 0}, {0}}, 0, 0, 1, 0)); + } + + @Test + public void testBlockedStartAndGoalHaveNoSolution() { + assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{1, 0}, {0, 0}}, 0, 0, 1, 1)); + assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[][] {{0, 0}, {0, 1}}, 0, 0, 1, 1)); + } + + @Test + public void testOpenSingleCellMazeIsAlreadySolved() { + assertArrayEquals(new int[][] {{2}}, MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{0}}, 0, 0, 0, 0)); + assertArrayEquals(new int[][] {{2}}, MazeRecursion.solveMazeUsingSecondStrategy(new int[][] {{0}}, 0, 0, 0, 0)); + } + @Test public void testSolveMazeUsingFirstAndSecondStrategy() { int[][] map = new int[8][7];