Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
package com.thealgorithms.datastructures.lists;

/**
* Singly-linked list supporting middle-element lookup via Floyd's tortoise-and-hare pointers.
*
* <p>Supported boundaries:
* <ul>
* <li>Empty list (0 nodes, {@code head == null}): {@link #getMiddle()} returns {@code null} and
* {@link #toString()} returns {@code "[]"}.</li>
* <li>Smallest non-empty list (1 {@link Node}): {@link #getMiddle()} skips the fast/slow loop
* and returns the single node's value.</li>
* <li>Even-length lists ({@code 2k} nodes, {@code k >= 1}): {@link #getMiddle()} returns the
* second middle element (0-based index {@code k}).</li>
* <li>Element values may be {@code null}; {@link #append(Object)} always terminates new nodes
* with {@code next = null} and {@link Node}'s constructor is {@code private}, so lists built
* through the public API remain acyclic.</li>
* </ul>
*
* @param <E> element type stored in list nodes
*/
public class TortoiseHareAlgo<E> {
/**
* Singly-linked node storing an element {@code value} (nullable) and {@code next} link.
* The constructor is {@code private} so callers append values only through
* {@link TortoiseHareAlgo#append(Object)}, keeping {@code next} acyclic.
*/
static final class Node<E> {
Node<E> next;
E value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package com.thealgorithms.datastructures.lists;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.List;
import org.junit.jupiter.api.Test;

/**
* Boundary tests for {@link TortoiseHareAlgo.Node} and {@link TortoiseHareAlgo}.
*
* <p>Source contract ({@code TortoiseHareAlgo.java}):
* <ul>
* <li>lines 27-35: package-private {@code static final class Node<E>} with fields {@code next}
* (line 28) and {@code value} (line 29), and {@code private Node(E value, Node<E> next)}
* (lines 31-34) storing {@code value} (including {@code null}) and {@code next}</li>
* <li>line 37 &amp; lines 39-41: {@code private Node<E> head = null} starts empty</li>
* <li>lines 43-54: {@code append(E value)} allocates {@code new Node<>(value, null)} (line 44),
* sets {@code head = newNode} when empty (lines 45-48), or appends at tail (lines 49-53)</li>
* <li>lines 56-70: {@code getMiddle()} returns {@code null} sentinel on empty list (lines 57-59);
* advances {@code slow} by 1 and {@code fast} by 2 while {@code fast != null && fast.next != null}
* (lines 64-67), returning {@code slow.value} (line 69, second middle for even lengths)</li>
* <li>lines 72-85: {@code toString()} formats {@code "[e0, e1, ...]"} or {@code "[]"}</li>
* </ul>
*
* <p>Boundary partition:
* <ul>
* <li>Valid boundaries: 0 nodes (empty, {@code head == null}), 1 node (smallest non-empty,
* {@code fast.next == null} skips loop), 1 node with {@code null} value, 2 nodes (smallest even,
* second middle), 3 nodes (smallest odd entering loop).</li>
* <li>Valid interior: ordinary multi-node lists (4 nodes, 5 nodes) and step-by-step growth.</li>
* <li>Encapsulated / unsupported externally: {@code Node(E, Node<E>)} is {@code private} (line 31)
* and {@code head} is {@code private} (line 37), preventing direct cyclic link construction.</li>
* </ul>
*/
public class TortoiseHareAlgoBoundaryTest {

private record BoundaryCase(
String id,
String partition,
List<Integer> values,
Integer expectedMiddle,
String expectedString
) {
}

private static void logBoundary(
String id,
String partition,
String input,
Object expected,
Object actual
) {
System.out.println("[R45-T012] " + id + " [" + partition + "] | input=" + input
+ " | expected=" + expected + " | actual=" + actual);
}

/**
* Smallest valid input for {@code Node} (single node created by {@code append(7)} at line 44)
* alongside the empty-list sentinel boundary ({@code head == null} at line 57).
*/
@Test
public void smallestValidNodeAndEmptyBoundaryMatchExpectedContract() {
TortoiseHareAlgo<Integer> empty = new TortoiseHareAlgo<>();
Integer emptyMiddle = empty.getMiddle();
String emptyStr = empty.toString();
logBoundary("boundary-empty-middle", "valid-boundary", "[]", null, emptyMiddle);
logBoundary("boundary-empty-str", "valid-boundary", "[]", "[]", emptyStr);
assertNull(emptyMiddle);
assertEquals("[]", emptyStr);

TortoiseHareAlgo<Integer> single = new TortoiseHareAlgo<>();
single.append(7);
Integer singleMiddle = single.getMiddle();
String singleStr = single.toString();
logBoundary("smallest-single-middle", "valid-boundary", "[7]", 7, singleMiddle);
logBoundary("smallest-single-str", "valid-boundary", "[7]", "[7]", singleStr);
assertEquals(7, singleMiddle);
assertEquals("[7]", singleStr);
}

/**
* Ordinary interior input (5 nodes: {@code [10, 20, 30, 40, 50]}), plus step-by-step growth
* across boundaries 0 -> 1 -> 2 -> 3 -> 4 -> 5.
*/
@Test
public void ordinaryInteriorAndGrowthAcrossBoundariesMatchExpectedContract() {
TortoiseHareAlgo<Integer> list = new TortoiseHareAlgo<>();
int[] inputs = {10, 20, 30, 40, 50};
Integer[] expectedMiddlesAfterAppend = {10, 20, 20, 30, 30};
String[] expectedStringsAfterAppend = {
"[10]",
"[10, 20]",
"[10, 20, 30]",
"[10, 20, 30, 40]",
"[10, 20, 30, 40, 50]"
};

for (int i = 0; i < inputs.length; i++) {
list.append(inputs[i]);
Integer actualMiddle = list.getMiddle();
String actualStr = list.toString();
logBoundary("growth-step-" + (i + 1), "valid-interior",
expectedStringsAfterAppend[i],
expectedMiddlesAfterAppend[i] + " / " + expectedStringsAfterAppend[i],
actualMiddle + " / " + actualStr);
assertEquals(expectedMiddlesAfterAppend[i], actualMiddle);
assertEquals(expectedStringsAfterAppend[i], actualStr);
}
}

/**
* Explicit boundary table covering each side of the documented boundaries:
* empty (0 nodes), single non-null (1 node), single null (1 node), smallest even (2 nodes),
* smallest odd entering loop (3 nodes), and interior even/odd (4 and 5 nodes).
*/
@Test
public void boundaryTableAssertionsCoverEachDocumentedSide() {
List<BoundaryCase> table = List.of(
new BoundaryCase("b-00-empty", "valid-boundary", List.of(), null, "[]"),
new BoundaryCase("b-01-single", "valid-boundary", List.of(42), 42, "[42]"),
new BoundaryCase("b-02-two-even", "valid-boundary", List.of(1, 2), 2, "[1, 2]"),
new BoundaryCase("b-03-three-odd", "valid-boundary", List.of(1, 2, 3), 2, "[1, 2, 3]"),
new BoundaryCase("b-04-four-even", "valid-interior", List.of(1, 2, 3, 4), 3, "[1, 2, 3, 4]"),
new BoundaryCase("b-05-five-odd", "valid-interior", List.of(1, 2, 3, 4, 5), 3, "[1, 2, 3, 4, 5]")
);

for (BoundaryCase bc : table) {
TortoiseHareAlgo<Integer> list = new TortoiseHareAlgo<>();
for (Integer v : bc.values()) {
list.append(v);
}
Integer actualMiddle = list.getMiddle();
String actualStr = list.toString();
logBoundary(bc.id(), bc.partition(), bc.values().toString(),
bc.expectedMiddle() + " / " + bc.expectedString(),
actualMiddle + " / " + actualStr);
assertEquals(bc.expectedMiddle(), actualMiddle, "Middle mismatch for " + bc.id());
assertEquals(bc.expectedString(), actualStr, "String mismatch for " + bc.id());
}
}

/**
* Null-value boundary: {@code Node} permits {@code null} element values (line 32),
* distinguishing a single {@code null}-valued node ({@code "[null]"}) from an empty list
* ({@code "[]"}).
*/
@Test
public void nullElementValueIsPermittedByNodeContract() {
TortoiseHareAlgo<String> singleNull = new TortoiseHareAlgo<>();
singleNull.append(null);
String middle = singleNull.getMiddle();
String str = singleNull.toString();
logBoundary("boundary-single-null", "valid-boundary", "[null]", "null / [null]", middle + " / " + str);
assertNull(middle);
assertEquals("[null]", str);

TortoiseHareAlgo<String> mixedNull = new TortoiseHareAlgo<>();
mixedNull.append("a");
mixedNull.append(null);
mixedNull.append("c");
String mixedMiddle = mixedNull.getMiddle();
String mixedStr = mixedNull.toString();
logBoundary("boundary-mixed-null", "valid-interior", "[a, null, c]",
"null / [a, null, c]", mixedMiddle + " / " + mixedStr);
assertNull(mixedMiddle);
assertEquals("[a, null, c]", mixedStr);
}

/**
* Encapsulation boundary: {@code Node} is package-private {@code static final} (line 27) and its
* constructor {@code Node(E, Node<E>)} is {@code private} (line 31), so external code cannot
* instantiate {@code Node} with an arbitrary {@code next} reference to form cycles.
*/
@Test
public void nodeConstructorEncapsulationBoundaryIsPreserved() throws NoSuchMethodException {
Constructor<?> ctor =
TortoiseHareAlgo.Node.class.getDeclaredConstructor(Object.class, TortoiseHareAlgo.Node.class);
boolean isPrivate = Modifier.isPrivate(ctor.getModifiers());
boolean isClassFinal = Modifier.isFinal(TortoiseHareAlgo.Node.class.getModifiers());
logBoundary("boundary-node-encapsulation", "encapsulated-boundary",
"Node(Object, Node)", "private=true,final=true",
"private=" + isPrivate + ",final=" + isClassFinal);
assertTrue(isPrivate);
assertTrue(isClassFinal);
}
}
70 changes: 70 additions & 0 deletions work/R45-T012/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T012: boundary checks for TortoiseHareAlgo.Node.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T012/,
and exits non-zero on any failure.
"""
import glob
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parent.parent
TEST_CLASS = "TortoiseHareAlgoBoundaryTest"
CMD = ["mvn", "-B", "test", f"-Dtest={TEST_CLASS}", "-Dsurefire.failIfNoSpecifiedTests=false"]


def tool_version(cmd):
try:
out = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
return "\n".join(l for l in (out.stdout + out.stderr).splitlines()
if not l.startswith("Picked up")).strip()
except FileNotFoundError:
return f"{cmd[0]}: not found"


def main():
(HERE / "tool_versions.txt").write_text(
"java:\n" + tool_version(["java", "-version"]) + "\n\nmvn:\n"
+ tool_version(["mvn", "-v"]) + "\n\npython:\n" + sys.version + "\n")

print("Running:", " ".join(CMD))
proc = subprocess.run(CMD, capture_output=True, text=True, cwd=REPO)
combined_output = proc.stdout + proc.stderr
(HERE / "mvn_output.txt").write_text(combined_output)

comparisons = [line.strip() for line in combined_output.splitlines()
if line.strip().startswith("[R45-T012]")]
(HERE / "comparisons.txt").write_text(
("\n".join(comparisons) + "\n") if comparisons else "No boundary records captured.\n")

reports = glob.glob(str(REPO / "target/surefire-reports" / f"TEST-*{TEST_CLASS}.xml"))
if not reports:
(HERE / "result.txt").write_text(
f"command: {' '.join(CMD)}\nFAIL: no surefire report found\n")
print("FAIL: no surefire report found")
return 1

suite = ET.parse(reports[0]).getroot()
tests = int(suite.get("tests", 0))
failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0))
skipped = int(suite.get("skipped", 0))
passed = proc.returncode == 0 and failures == 0 and tests > 0 and len(comparisons) > 0
status = "PASS" if passed else "FAIL"
summary = (
f"command: {' '.join(CMD)}\n"
f"tests={tests} failures/errors={failures} skipped={skipped} "
f"boundaries={len(comparisons)} mvn_exit={proc.returncode}\n"
f"status={status}\n"
)
(HERE / "result.txt").write_text(summary)
print(summary)

return 0 if passed else 1


if __name__ == "__main__":
sys.exit(main())
18 changes: 18 additions & 0 deletions work/R45-T012/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[R45-T012] boundary-empty-middle [valid-boundary] | input=[] | expected=null | actual=null
[R45-T012] boundary-empty-str [valid-boundary] | input=[] | expected=[] | actual=[]
[R45-T012] smallest-single-middle [valid-boundary] | input=[7] | expected=7 | actual=7
[R45-T012] smallest-single-str [valid-boundary] | input=[7] | expected=[7] | actual=[7]
[R45-T012] b-00-empty [valid-boundary] | input=[] | expected=null / [] | actual=null / []
[R45-T012] b-01-single [valid-boundary] | input=[42] | expected=42 / [42] | actual=42 / [42]
[R45-T012] b-02-two-even [valid-boundary] | input=[1, 2] | expected=2 / [1, 2] | actual=2 / [1, 2]
[R45-T012] b-03-three-odd [valid-boundary] | input=[1, 2, 3] | expected=2 / [1, 2, 3] | actual=2 / [1, 2, 3]
[R45-T012] b-04-four-even [valid-interior] | input=[1, 2, 3, 4] | expected=3 / [1, 2, 3, 4] | actual=3 / [1, 2, 3, 4]
[R45-T012] b-05-five-odd [valid-interior] | input=[1, 2, 3, 4, 5] | expected=3 / [1, 2, 3, 4, 5] | actual=3 / [1, 2, 3, 4, 5]
[R45-T012] boundary-node-encapsulation [encapsulated-boundary] | input=Node(Object, Node) | expected=private=true,final=true | actual=private=true,final=true
[R45-T012] boundary-single-null [valid-boundary] | input=[null] | expected=null / [null] | actual=null / [null]
[R45-T012] boundary-mixed-null [valid-interior] | input=[a, null, c] | expected=null / [a, null, c] | actual=null / [a, null, c]
[R45-T012] growth-step-1 [valid-interior] | input=[10] | expected=10 / [10] | actual=10 / [10]
[R45-T012] growth-step-2 [valid-interior] | input=[10, 20] | expected=20 / [10, 20] | actual=20 / [10, 20]
[R45-T012] growth-step-3 [valid-interior] | input=[10, 20, 30] | expected=20 / [10, 20, 30] | actual=20 / [10, 20, 30]
[R45-T012] growth-step-4 [valid-interior] | input=[10, 20, 30, 40] | expected=30 / [10, 20, 30, 40] | actual=30 / [10, 20, 30, 40]
[R45-T012] growth-step-5 [valid-interior] | input=[10, 20, 30, 40, 50] | expected=30 / [10, 20, 30, 40, 50] | actual=30 / [10, 20, 30, 40, 50]
67 changes: 67 additions & 0 deletions work/R45-T012/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[INFO] Scanning for projects...
[INFO] Loaded 24447 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO]
[INFO] -----------------------< com.thealgorithms:Java >-----------------------
[INFO] Building Java 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jacoco:0.8.16:prepare-agent (default) @ Java ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] argLine set to -javaagent:/Users/shyamlibakale/.m2/repository/org/jacoco/org.jacoco.agent/0.8.16/org.jacoco.agent-0.8.16-runtime.jar=destfile=/Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ Java ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 848 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ Java ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 797 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ Java ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thealgorithms.datastructures.lists.TortoiseHareAlgoBoundaryTest
[R45-T012] boundary-empty-middle [valid-boundary] | input=[] | expected=null | actual=null
[R45-T012] boundary-empty-str [valid-boundary] | input=[] | expected=[] | actual=[]
[R45-T012] smallest-single-middle [valid-boundary] | input=[7] | expected=7 | actual=7
[R45-T012] smallest-single-str [valid-boundary] | input=[7] | expected=[7] | actual=[7]
[R45-T012] b-00-empty [valid-boundary] | input=[] | expected=null / [] | actual=null / []
[R45-T012] b-01-single [valid-boundary] | input=[42] | expected=42 / [42] | actual=42 / [42]
[R45-T012] b-02-two-even [valid-boundary] | input=[1, 2] | expected=2 / [1, 2] | actual=2 / [1, 2]
[R45-T012] b-03-three-odd [valid-boundary] | input=[1, 2, 3] | expected=2 / [1, 2, 3] | actual=2 / [1, 2, 3]
[R45-T012] b-04-four-even [valid-interior] | input=[1, 2, 3, 4] | expected=3 / [1, 2, 3, 4] | actual=3 / [1, 2, 3, 4]
[R45-T012] b-05-five-odd [valid-interior] | input=[1, 2, 3, 4, 5] | expected=3 / [1, 2, 3, 4, 5] | actual=3 / [1, 2, 3, 4, 5]
[R45-T012] boundary-node-encapsulation [encapsulated-boundary] | input=Node(Object, Node) | expected=private=true,final=true | actual=private=true,final=true
[R45-T012] boundary-single-null [valid-boundary] | input=[null] | expected=null / [null] | actual=null / [null]
[R45-T012] boundary-mixed-null [valid-interior] | input=[a, null, c] | expected=null / [a, null, c] | actual=null / [a, null, c]
[R45-T012] growth-step-1 [valid-interior] | input=[10] | expected=10 / [10] | actual=10 / [10]
[R45-T012] growth-step-2 [valid-interior] | input=[10, 20] | expected=20 / [10, 20] | actual=20 / [10, 20]
[R45-T012] growth-step-3 [valid-interior] | input=[10, 20, 30] | expected=20 / [10, 20, 30] | actual=20 / [10, 20, 30]
[R45-T012] growth-step-4 [valid-interior] | input=[10, 20, 30, 40] | expected=30 / [10, 20, 30, 40] | actual=30 / [10, 20, 30, 40]
[R45-T012] growth-step-5 [valid-interior] | input=[10, 20, 30, 40, 50] | expected=30 / [10, 20, 30, 40, 50] | actual=30 / [10, 20, 30, 40, 50]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.044 s -- in com.thealgorithms.datastructures.lists.TortoiseHareAlgoBoundaryTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.16:report (generate-code-coverage-report) @ Java ---
[INFO] Loading execution data file /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO] Analyzed bundle 'Java' with 1073 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.696 s
[INFO] Finished at: 2026-11-11T17:24:45+05:30
[INFO] ------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions work/R45-T012/result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command: mvn -B test -Dtest=TortoiseHareAlgoBoundaryTest -Dsurefire.failIfNoSpecifiedTests=false
tests=5 failures/errors=0 skipped=0 boundaries=18 mvn_exit=0
status=PASS
Loading