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 82963c044afb..d9d9ed1e54e9 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -24,13 +24,13 @@ 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'
- name: Initialize CodeQL
- uses: github/codeql-action/init@v4.38.2
+ 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.38.2
+ 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.38.2
+ uses: github/codeql-action/init@v4.39.1
with:
languages: 'actions'
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v4.38.2
+ uses: github/codeql-action/analyze@v4.39.1
with:
category: "/language:actions"
...
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'
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'
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.
diff --git a/pom.xml b/pom.xml
index 5a166dbf4fe5..d7c7a0849fb8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,7 +61,7 @@
- * 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
- * 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))
- * 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 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
+ *
+ * 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.
+ *
+ * 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/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. For example, the smallest rotation of {@code "baca"} is
+ * {@code "abac"}, which starts at index {@code 3}. 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 The method has one loop and two decisions:
+ *
+ * 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
+ * k(q) = compression / (2 * pi) * asin(2q - 1)
+ *
+ *
+ *
+ *
+ *
+ *
+ * Operation Complexity
+ * {@link #add(double)} O(log d) amortised
+ * {@link #quantile(double)}, {@link #cdf(double)} O(d)
+ * {@link #merge(TDigest)} O(d log d)
+ * memory O(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);
+ * }
+ *
+ *
+ *
+ * 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);
+ }
+}
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);
+ }
}
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"));
+ }
}
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.");
+ }
}
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();
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.");
+ }
}
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/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);
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");
+ }
}
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.");
+ }
}
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"));
+ }
}
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.");
+ }
}
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