diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e5622f7b51d..1b91763b2d53 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,14 +5,20 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 - package-ecosystem: "github-actions" directory: "/.github/workflows/" schedule: interval: "daily" + cooldown: + default-days: 7 - package-ecosystem: "maven" directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 ... diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d9cc4c3c35c5..30b46bbf45b5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,4 +13,5 @@ In order to reduce the number of notifications sent to the maintainers, please: - [ ] All filenames are in PascalCase. - [ ] All functions and variable names follow Java naming conventions. - [ ] All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations. -- [ ] All new code is formatted with `clang-format -i --style=file path/to/your/file.java` \ No newline at end of file +- [ ] All new algorithms include a corresponding test class that validates their functionality. +- [ ] All new code is formatted with `clang-format -i --style=file path/to/your/file.java` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39bde758d68e..38d1471dcf13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,9 +8,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' @@ -20,7 +20,7 @@ jobs: if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: fail_ci_if_error: true - name: Upload coverage to codecov (with token) @@ -28,7 +28,7 @@ jobs: github.repository == 'TheAlgorithms/Java' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/clang-format-lint.yml b/.github/workflows/clang-format-lint.yml index ca014e115282..622679f73842 100644 --- a/.github/workflows/clang-format-lint.yml +++ b/.github/workflows/clang-format-lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: DoozyX/clang-format-lint-action@v0.20 with: source: './src' diff --git a/.github/workflows/close-failed-prs.yml b/.github/workflows/close-failed-prs.yml index 6deea88f0daf..4013e87b6569 100644 --- a/.github/workflows/close-failed-prs.yml +++ b/.github/workflows/close-failed-prs.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Close stale PRs - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3a4cfd829b6b..d9d9ed1e54e9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,16 +21,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + 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 + uses: github/codeql-action/analyze@v4.39.1 with: category: "/language:java-kotlin" @@ -52,15 +52,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.39.1 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + 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 3df7c4b1fc9e..962c090ee699 100644 --- a/.github/workflows/infer.yml +++ b/.github/workflows/infer.yml @@ -15,42 +15,16 @@ jobs: run_infer: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v6.1.0 with: java-version: 21 distribution: 'temurin' - - name: Set up OCaml - uses: ocaml/setup-ocaml@v3 - with: - ocaml-compiler: 5 - - - name: Get current year/weak - run: echo "year_week=$(date +'%Y_%U')" >> $GITHUB_ENV - - - name: Cache infer build - id: cache-infer - uses: actions/cache@v4 - with: - path: infer - key: ${{ runner.os }}-infer-${{ env.year_week }} - - - name: Build infer - if: steps.cache-infer.outputs.cache-hit != 'true' - run: | - cd .. - git clone https://github.com/facebook/infer.git - cd infer - git checkout 01aaa268f9d38723ba69c139e10f9e2a04b40b1c - ./build-infer.sh java - cp -r infer ../Java - - - name: Add infer to PATH - run: | - echo "infer/bin" >> $GITHUB_PATH + - name: Set up inferAdd commentMore actions + uses: srz-zumix/setup-infer@v1 - name: Display infer version run: | @@ -60,5 +34,5 @@ jobs: - name: Run infer run: | mvn clean - infer --fail-on-issue --print-logs --no-progress-bar -- mvn test + infer --java-version 21 --fail-on-issue --print-logs --no-progress-bar -- mvn test ... diff --git a/.github/workflows/project_structure.yml b/.github/workflows/project_structure.yml index f9fb82a2781c..764832b7b6a6 100644 --- a/.github/workflows/project_structure.yml +++ b/.github/workflows/project_structure.yml @@ -15,8 +15,8 @@ jobs: check_structure: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7.1.0 with: python-version: '3.13' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bb613daf8f1d..b961d15c240e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/stale@v10 + - uses: actions/stale@v11.0.0 with: stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution!' close-issue-message: 'Please reopen this issue once you have made the required changes. If you need help, feel free to ask in our [Discord](https://the-algorithms.com/discord) server or ping one of the maintainers here. Thank you for your contribution!' diff --git a/.github/workflows/update-directorymd.yml b/.github/workflows/update-directorymd.yml index 6692a884a867..3977bfda86bf 100644 --- a/.github/workflows/update-directorymd.yml +++ b/.github/workflows/update-directorymd.yml @@ -14,7 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Run Directory Tree Generator uses: DenizAltunkapan/directory-tree-generator@v2 @@ -31,7 +33,7 @@ jobs: git diff --cached --quiet || git commit -m "Update DIRECTORY.md" - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.REPO_SCOPED_TOKEN }} branch: update-directory diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index 4195f928d1bc..a8951da7de26 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -1,4 +1,4 @@ -FROM gitpod/workspace-java-21:2025-10-06-13-14-25 +FROM gitpod/workspace-java-21:2025-11-14-10-05-32 ENV LLVM_SCRIPT="tmp_llvm.sh" diff --git a/.inferconfig b/.inferconfig index 6af4f9e2e818..cf26212feac5 100644 --- a/.inferconfig +++ b/.inferconfig @@ -1,6 +1,8 @@ { "report-block-list-path-regex": [ "src/main/java/com/thealgorithms/ciphers/a5/CompositeLFSR.java", + "src/main/java/com/thealgorithms/compression/ArithmeticCoding.java", + "src/main/java/com/thealgorithms/datastructures/caches/FIFOCache.java", "src/main/java/com/thealgorithms/datastructures/crdt/GCounter.java", "src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java", "src/main/java/com/thealgorithms/datastructures/graphs/KahnsAlgorithm.java", @@ -8,15 +10,19 @@ "src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java", "src/main/java/com/thealgorithms/datastructures/trees/CreateBinaryTreeFromInorderPreorder.java", "src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java", + "src/main/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistance.java", "src/main/java/com/thealgorithms/dynamicprogramming/Fibonacci.java", "src/main/java/com/thealgorithms/maths/SimpsonIntegration.java", "src/main/java/com/thealgorithms/others/Dijkstra.java", "src/main/java/com/thealgorithms/sorts/TopologicalSort.java", "src/main/java/com/thealgorithms/strings/AhoCorasick.java", + "src/test/java/com/thealgorithms/compression/ShannonFanoTest.java", "src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java", "src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java", "src/test/java/com/thealgorithms/datastructures/trees/KDTreeTest.java", "src/test/java/com/thealgorithms/datastructures/trees/LazySegmentTreeTest.java", + "src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java", + "src/test/java/com/thealgorithms/others/HuffmanTest.java", "src/test/java/com/thealgorithms/searches/QuickSelectTest.java", "src/test/java/com/thealgorithms/stacks/PostfixToInfixTest.java", "src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java" diff --git a/DIRECTORY.md b/DIRECTORY.md index eca2453fad0f..37d9b3c295e2 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -13,6 +13,7 @@ - πŸ“„ [AllPathsFromSourceToTarget](src/main/java/com/thealgorithms/backtracking/AllPathsFromSourceToTarget.java) - πŸ“„ [ArrayCombination](src/main/java/com/thealgorithms/backtracking/ArrayCombination.java) - πŸ“„ [Combination](src/main/java/com/thealgorithms/backtracking/Combination.java) + - πŸ“„ [CombinationSum](src/main/java/com/thealgorithms/backtracking/CombinationSum.java) - πŸ“„ [CrosswordSolver](src/main/java/com/thealgorithms/backtracking/CrosswordSolver.java) - πŸ“„ [FloodFill](src/main/java/com/thealgorithms/backtracking/FloodFill.java) - πŸ“„ [KnightsTour](src/main/java/com/thealgorithms/backtracking/KnightsTour.java) @@ -23,11 +24,14 @@ - πŸ“„ [Permutation](src/main/java/com/thealgorithms/backtracking/Permutation.java) - πŸ“„ [PowerSum](src/main/java/com/thealgorithms/backtracking/PowerSum.java) - πŸ“„ [SubsequenceFinder](src/main/java/com/thealgorithms/backtracking/SubsequenceFinder.java) + - πŸ“„ [SudokuSolver](src/main/java/com/thealgorithms/backtracking/SudokuSolver.java) + - πŸ“„ [UniquePermutation](src/main/java/com/thealgorithms/backtracking/UniquePermutation.java) - πŸ“„ [WordPatternMatcher](src/main/java/com/thealgorithms/backtracking/WordPatternMatcher.java) - πŸ“„ [WordSearch](src/main/java/com/thealgorithms/backtracking/WordSearch.java) - πŸ“ **bitmanipulation** - πŸ“„ [BcdConversion](src/main/java/com/thealgorithms/bitmanipulation/BcdConversion.java) - πŸ“„ [BinaryPalindromeCheck](src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java) + - πŸ“„ [BitRotate](src/main/java/com/thealgorithms/bitmanipulation/BitRotate.java) - πŸ“„ [BitSwap](src/main/java/com/thealgorithms/bitmanipulation/BitSwap.java) - πŸ“„ [BitwiseGCD](src/main/java/com/thealgorithms/bitmanipulation/BitwiseGCD.java) - πŸ“„ [BooleanAlgebraGates](src/main/java/com/thealgorithms/bitmanipulation/BooleanAlgebraGates.java) @@ -76,6 +80,7 @@ - πŸ“„ [ECC](src/main/java/com/thealgorithms/ciphers/ECC.java) - πŸ“„ [HillCipher](src/main/java/com/thealgorithms/ciphers/HillCipher.java) - πŸ“„ [MonoAlphabetic](src/main/java/com/thealgorithms/ciphers/MonoAlphabetic.java) + - πŸ“„ [OneTimePadCipher](src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java) - πŸ“„ [PermutationCipher](src/main/java/com/thealgorithms/ciphers/PermutationCipher.java) - πŸ“„ [PlayfairCipher](src/main/java/com/thealgorithms/ciphers/PlayfairCipher.java) - πŸ“„ [Polybius](src/main/java/com/thealgorithms/ciphers/Polybius.java) @@ -93,6 +98,12 @@ - πŸ“„ [LFSR](src/main/java/com/thealgorithms/ciphers/a5/LFSR.java) - πŸ“„ [Utils](src/main/java/com/thealgorithms/ciphers/a5/Utils.java) - πŸ“ **compression** + - πŸ“„ [ArithmeticCoding](src/main/java/com/thealgorithms/compression/ArithmeticCoding.java) + - πŸ“„ [BurrowsWheelerTransform](src/main/java/com/thealgorithms/compression/BurrowsWheelerTransform.java) + - πŸ“„ [LZ77](src/main/java/com/thealgorithms/compression/LZ77.java) + - πŸ“„ [LZ78](src/main/java/com/thealgorithms/compression/LZ78.java) + - πŸ“„ [LZW](src/main/java/com/thealgorithms/compression/LZW.java) + - πŸ“„ [MoveToFront](src/main/java/com/thealgorithms/compression/MoveToFront.java) - πŸ“„ [RunLengthEncoding](src/main/java/com/thealgorithms/compression/RunLengthEncoding.java) - πŸ“„ [ShannonFano](src/main/java/com/thealgorithms/compression/ShannonFano.java) - πŸ“ **conversions** @@ -125,6 +136,7 @@ - πŸ“„ [PhoneticAlphabetConverter](src/main/java/com/thealgorithms/conversions/PhoneticAlphabetConverter.java) - πŸ“„ [RgbHsvConversion](src/main/java/com/thealgorithms/conversions/RgbHsvConversion.java) - πŸ“„ [RomanToInteger](src/main/java/com/thealgorithms/conversions/RomanToInteger.java) + - πŸ“„ [TemperatureConverter](src/main/java/com/thealgorithms/conversions/TemperatureConverter.java) - πŸ“„ [TimeConverter](src/main/java/com/thealgorithms/conversions/TimeConverter.java) - πŸ“„ [TurkishToLatinConversion](src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java) - πŸ“„ [UnitConversions](src/main/java/com/thealgorithms/conversions/UnitConversions.java) @@ -173,7 +185,6 @@ - πŸ“„ [FordFulkerson](src/main/java/com/thealgorithms/datastructures/graphs/FordFulkerson.java) - πŸ“„ [Graphs](src/main/java/com/thealgorithms/datastructures/graphs/Graphs.java) - πŸ“„ [HamiltonianCycle](src/main/java/com/thealgorithms/datastructures/graphs/HamiltonianCycle.java) - - πŸ“„ [HierholzerAlgorithm](src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java) - πŸ“„ [JohnsonsAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/JohnsonsAlgorithm.java) - πŸ“„ [KahnsAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/KahnsAlgorithm.java) - πŸ“„ [Kosaraju](src/main/java/com/thealgorithms/datastructures/graphs/Kosaraju.java) @@ -190,6 +201,7 @@ - πŸ“„ [GenericHashMapUsingArrayList](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayList.java) - πŸ“„ [HashMap](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMap.java) - πŸ“„ [HashMapCuckooHashing](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapCuckooHashing.java) + - πŸ“„ [ImmutableHashMap](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMap.java) - πŸ“„ [Intersection](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Intersection.java) - πŸ“„ [LinearProbingHashMap](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/LinearProbingHashMap.java) - πŸ“„ [MainCuckooHashing](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/MainCuckooHashing.java) @@ -201,6 +213,7 @@ - πŸ“„ [GenericHeap](src/main/java/com/thealgorithms/datastructures/heaps/GenericHeap.java) - πŸ“„ [Heap](src/main/java/com/thealgorithms/datastructures/heaps/Heap.java) - πŸ“„ [HeapElement](src/main/java/com/thealgorithms/datastructures/heaps/HeapElement.java) + - πŸ“„ [IndexedPriorityQueue](src/main/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueue.java) - πŸ“„ [KthElementFinder](src/main/java/com/thealgorithms/datastructures/heaps/KthElementFinder.java) - πŸ“„ [LeftistHeap](src/main/java/com/thealgorithms/datastructures/heaps/LeftistHeap.java) - πŸ“„ [MaxHeap](src/main/java/com/thealgorithms/datastructures/heaps/MaxHeap.java) @@ -255,8 +268,10 @@ - πŸ“„ [BSTRecursiveGeneric](src/main/java/com/thealgorithms/datastructures/trees/BSTRecursiveGeneric.java) - πŸ“„ [BTree](src/main/java/com/thealgorithms/datastructures/trees/BTree.java) - πŸ“„ [BinaryTree](src/main/java/com/thealgorithms/datastructures/trees/BinaryTree.java) + - πŸ“„ [BinaryTreeToString](src/main/java/com/thealgorithms/datastructures/trees/BinaryTreeToString.java) - πŸ“„ [BoundaryTraversal](src/main/java/com/thealgorithms/datastructures/trees/BoundaryTraversal.java) - πŸ“„ [CeilInBinarySearchTree](src/main/java/com/thealgorithms/datastructures/trees/CeilInBinarySearchTree.java) + - πŸ“„ [CentroidDecomposition](src/main/java/com/thealgorithms/datastructures/trees/CentroidDecomposition.java) - πŸ“„ [CheckBinaryTreeIsValidBST](src/main/java/com/thealgorithms/datastructures/trees/CheckBinaryTreeIsValidBST.java) - πŸ“„ [CheckIfBinaryTreeBalanced](src/main/java/com/thealgorithms/datastructures/trees/CheckIfBinaryTreeBalanced.java) - πŸ“„ [CheckTreeIsSymmetric](src/main/java/com/thealgorithms/datastructures/trees/CheckTreeIsSymmetric.java) @@ -276,6 +291,7 @@ - πŸ“„ [SameTreesCheck](src/main/java/com/thealgorithms/datastructures/trees/SameTreesCheck.java) - πŸ“„ [SegmentTree](src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java) - πŸ“„ [SplayTree](src/main/java/com/thealgorithms/datastructures/trees/SplayTree.java) + - πŸ“„ [ThreadedBinaryTree](src/main/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTree.java) - πŸ“„ [Treap](src/main/java/com/thealgorithms/datastructures/trees/Treap.java) - πŸ“„ [TreeRandomNode](src/main/java/com/thealgorithms/datastructures/trees/TreeRandomNode.java) - πŸ“„ [Trie](src/main/java/com/thealgorithms/datastructures/trees/Trie.java) @@ -358,8 +374,10 @@ - πŸ“„ [WildcardMatching](src/main/java/com/thealgorithms/dynamicprogramming/WildcardMatching.java) - πŸ“„ [WineProblem](src/main/java/com/thealgorithms/dynamicprogramming/WineProblem.java) - πŸ“ **geometry** + - πŸ“„ [BentleyOttmann](src/main/java/com/thealgorithms/geometry/BentleyOttmann.java) - πŸ“„ [BresenhamLine](src/main/java/com/thealgorithms/geometry/BresenhamLine.java) - πŸ“„ [ConvexHull](src/main/java/com/thealgorithms/geometry/ConvexHull.java) + - πŸ“„ [DDALine](src/main/java/com/thealgorithms/geometry/DDALine.java) - πŸ“„ [GrahamScan](src/main/java/com/thealgorithms/geometry/GrahamScan.java) - πŸ“„ [Haversine](src/main/java/com/thealgorithms/geometry/Haversine.java) - πŸ“„ [MidpointCircle](src/main/java/com/thealgorithms/geometry/MidpointCircle.java) @@ -372,10 +390,14 @@ - πŸ“„ [Dinic](src/main/java/com/thealgorithms/graph/Dinic.java) - πŸ“„ [Edmonds](src/main/java/com/thealgorithms/graph/Edmonds.java) - πŸ“„ [EdmondsKarp](src/main/java/com/thealgorithms/graph/EdmondsKarp.java) + - πŸ“„ [GomoryHuTree](src/main/java/com/thealgorithms/graph/GomoryHuTree.java) + - πŸ“„ [HierholzerAlgorithm](src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java) + - πŸ“„ [HierholzerEulerianPath](src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java) - πŸ“„ [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java) - πŸ“„ [HungarianAlgorithm](src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java) - πŸ“„ [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java) - πŸ“„ [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java) + - πŸ“„ [StoerWagner](src/main/java/com/thealgorithms/graph/StoerWagner.java) - πŸ“„ [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java) - πŸ“„ [TravelingSalesman](src/main/java/com/thealgorithms/graph/TravelingSalesman.java) - πŸ“„ [YensKShortestPaths](src/main/java/com/thealgorithms/graph/YensKShortestPaths.java) @@ -409,6 +431,7 @@ - πŸ“„ [AbsoluteMax](src/main/java/com/thealgorithms/maths/AbsoluteMax.java) - πŸ“„ [AbsoluteMin](src/main/java/com/thealgorithms/maths/AbsoluteMin.java) - πŸ“„ [AbsoluteValue](src/main/java/com/thealgorithms/maths/AbsoluteValue.java) + - πŸ“„ [AbundantNumber](src/main/java/com/thealgorithms/maths/AbundantNumber.java) - πŸ“„ [AliquotSum](src/main/java/com/thealgorithms/maths/AliquotSum.java) - πŸ“„ [AmicableNumber](src/main/java/com/thealgorithms/maths/AmicableNumber.java) - πŸ“„ [Area](src/main/java/com/thealgorithms/maths/Area.java) @@ -420,6 +443,7 @@ - πŸ“„ [BinomialCoefficient](src/main/java/com/thealgorithms/maths/BinomialCoefficient.java) - πŸ“„ [CatalanNumbers](src/main/java/com/thealgorithms/maths/CatalanNumbers.java) - πŸ“„ [Ceil](src/main/java/com/thealgorithms/maths/Ceil.java) + - πŸ“„ [ChebyshevIteration](src/main/java/com/thealgorithms/maths/ChebyshevIteration.java) - πŸ“„ [ChineseRemainderTheorem](src/main/java/com/thealgorithms/maths/ChineseRemainderTheorem.java) - πŸ“„ [CircularConvolutionFFT](src/main/java/com/thealgorithms/maths/CircularConvolutionFFT.java) - πŸ“„ [CollatzConjecture](src/main/java/com/thealgorithms/maths/CollatzConjecture.java) @@ -434,10 +458,11 @@ - πŸ“„ [EulerMethod](src/main/java/com/thealgorithms/maths/EulerMethod.java) - πŸ“„ [EulerPseudoprime](src/main/java/com/thealgorithms/maths/EulerPseudoprime.java) - πŸ“„ [EulersFunction](src/main/java/com/thealgorithms/maths/EulersFunction.java) + - πŸ“„ [EvilNumber](src/main/java/com/thealgorithms/maths/EvilNumber.java) + - πŸ“„ [ExtendedEuclideanAlgorithm](src/main/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithm.java) - πŸ“„ [FFT](src/main/java/com/thealgorithms/maths/FFT.java) - πŸ“„ [FFTBluestein](src/main/java/com/thealgorithms/maths/FFTBluestein.java) - πŸ“„ [Factorial](src/main/java/com/thealgorithms/maths/Factorial.java) - - πŸ“„ [FactorialRecursion](src/main/java/com/thealgorithms/maths/FactorialRecursion.java) - πŸ“„ [FastExponentiation](src/main/java/com/thealgorithms/maths/FastExponentiation.java) - πŸ“„ [FastInverseSqrt](src/main/java/com/thealgorithms/maths/FastInverseSqrt.java) - πŸ“„ [FibonacciJavaStreams](src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java) @@ -471,6 +496,7 @@ - πŸ“„ [LinearDiophantineEquationsSolver](src/main/java/com/thealgorithms/maths/LinearDiophantineEquationsSolver.java) - πŸ“„ [LongDivision](src/main/java/com/thealgorithms/maths/LongDivision.java) - πŸ“„ [LucasSeries](src/main/java/com/thealgorithms/maths/LucasSeries.java) + - πŸ“„ [LuckyNumber](src/main/java/com/thealgorithms/maths/LuckyNumber.java) - πŸ“„ [MagicSquare](src/main/java/com/thealgorithms/maths/MagicSquare.java) - πŸ“„ [MathBuilder](src/main/java/com/thealgorithms/maths/MathBuilder.java) - πŸ“„ [MaxValue](src/main/java/com/thealgorithms/maths/MaxValue.java) @@ -478,6 +504,7 @@ - πŸ“„ [Median](src/main/java/com/thealgorithms/maths/Median.java) - πŸ“„ [MinValue](src/main/java/com/thealgorithms/maths/MinValue.java) - πŸ“„ [Mode](src/main/java/com/thealgorithms/maths/Mode.java) + - πŸ“„ [Neville](src/main/java/com/thealgorithms/maths/Neville.java) - πŸ“„ [NonRepeatingElement](src/main/java/com/thealgorithms/maths/NonRepeatingElement.java) - πŸ“„ [NthUglyNumber](src/main/java/com/thealgorithms/maths/NthUglyNumber.java) - πŸ“„ [NumberOfDigits](src/main/java/com/thealgorithms/maths/NumberOfDigits.java) @@ -493,6 +520,7 @@ - πŸ“„ [PiNilakantha](src/main/java/com/thealgorithms/maths/PiNilakantha.java) - πŸ“„ [PollardRho](src/main/java/com/thealgorithms/maths/PollardRho.java) - πŸ“„ [Pow](src/main/java/com/thealgorithms/maths/Pow.java) + - πŸ“„ [PowerOfFour](src/main/java/com/thealgorithms/maths/PowerOfFour.java) - πŸ“„ [PowerOfTwoOrNot](src/main/java/com/thealgorithms/maths/PowerOfTwoOrNot.java) - πŸ“„ [PowerUsingRecursion](src/main/java/com/thealgorithms/maths/PowerUsingRecursion.java) - πŸ“ **Prime** @@ -511,6 +539,7 @@ - πŸ“„ [SieveOfAtkin](src/main/java/com/thealgorithms/maths/SieveOfAtkin.java) - πŸ“„ [SieveOfEratosthenes](src/main/java/com/thealgorithms/maths/SieveOfEratosthenes.java) - πŸ“„ [SimpsonIntegration](src/main/java/com/thealgorithms/maths/SimpsonIntegration.java) + - πŸ“„ [SmithNumber](src/main/java/com/thealgorithms/maths/SmithNumber.java) - πŸ“„ [SolovayStrassenPrimalityTest](src/main/java/com/thealgorithms/maths/SolovayStrassenPrimalityTest.java) - πŸ“„ [SquareRootWithBabylonianMethod](src/main/java/com/thealgorithms/maths/SquareRootWithBabylonianMethod.java) - πŸ“„ [SquareRootWithNewtonRaphsonMethod](src/main/java/com/thealgorithms/maths/SquareRootWithNewtonRaphsonMethod.java) @@ -531,6 +560,7 @@ - πŸ“„ [ZellersCongruence](src/main/java/com/thealgorithms/maths/ZellersCongruence.java) - πŸ“ **matrix** - πŸ“„ [InverseOfMatrix](src/main/java/com/thealgorithms/matrix/InverseOfMatrix.java) + - πŸ“„ [LUDecomposition](src/main/java/com/thealgorithms/matrix/LUDecomposition.java) - πŸ“„ [MatrixMultiplication](src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java) - πŸ“„ [MatrixRank](src/main/java/com/thealgorithms/matrix/MatrixRank.java) - πŸ“„ [MatrixTranspose](src/main/java/com/thealgorithms/matrix/MatrixTranspose.java) @@ -539,6 +569,7 @@ - πŸ“„ [PrintAMatrixInSpiralOrder](src/main/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrder.java) - πŸ“„ [RotateMatrixBy90Degrees](src/main/java/com/thealgorithms/matrix/RotateMatrixBy90Degrees.java) - πŸ“„ [SolveSystem](src/main/java/com/thealgorithms/matrix/SolveSystem.java) + - πŸ“„ [StochasticMatrix](src/main/java/com/thealgorithms/matrix/StochasticMatrix.java) - πŸ“ **matrixexponentiation** - πŸ“„ [Fibonacci](src/main/java/com/thealgorithms/matrix/matrixexponentiation/Fibonacci.java) - πŸ“ **utils** @@ -591,17 +622,22 @@ - πŸ“„ [PageRank](src/main/java/com/thealgorithms/others/PageRank.java) - πŸ“„ [PasswordGen](src/main/java/com/thealgorithms/others/PasswordGen.java) - πŸ“„ [PerlinNoise](src/main/java/com/thealgorithms/others/PerlinNoise.java) - - πŸ“„ [PrintAMatrixInSpiralOrder](src/main/java/com/thealgorithms/others/PrintAMatrixInSpiralOrder.java) - πŸ“„ [QueueUsingTwoStacks](src/main/java/com/thealgorithms/others/QueueUsingTwoStacks.java) - πŸ“„ [SkylineProblem](src/main/java/com/thealgorithms/others/SkylineProblem.java) - πŸ“„ [TwoPointers](src/main/java/com/thealgorithms/others/TwoPointers.java) - πŸ“„ [Verhoeff](src/main/java/com/thealgorithms/others/Verhoeff.java) - - πŸ“ **cn** - - πŸ“„ [HammingDistance](src/main/java/com/thealgorithms/others/cn/HammingDistance.java) - πŸ“ **physics** + - πŸ“„ [CoulombsLaw](src/main/java/com/thealgorithms/physics/CoulombsLaw.java) + - πŸ“„ [DampedOscillator](src/main/java/com/thealgorithms/physics/DampedOscillator.java) + - πŸ“„ [ElasticCollision2D](src/main/java/com/thealgorithms/physics/ElasticCollision2D.java) + - πŸ“„ [Gravitation](src/main/java/com/thealgorithms/physics/Gravitation.java) - πŸ“„ [GroundToGroundProjectileMotion](src/main/java/com/thealgorithms/physics/GroundToGroundProjectileMotion.java) + - πŸ“„ [Kinematics](src/main/java/com/thealgorithms/physics/Kinematics.java) + - πŸ“„ [ProjectileMotion](src/main/java/com/thealgorithms/physics/ProjectileMotion.java) + - πŸ“„ [SimplePendulumRK4](src/main/java/com/thealgorithms/physics/SimplePendulumRK4.java) + - πŸ“„ [SnellLaw](src/main/java/com/thealgorithms/physics/SnellLaw.java) + - πŸ“„ [ThinLens](src/main/java/com/thealgorithms/physics/ThinLens.java) - πŸ“ **puzzlesandgames** - - πŸ“„ [Sudoku](src/main/java/com/thealgorithms/puzzlesandgames/Sudoku.java) - πŸ“„ [TowerOfHanoi](src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java) - πŸ“„ [WordBoggle](src/main/java/com/thealgorithms/puzzlesandgames/WordBoggle.java) - πŸ“ **randomized** @@ -613,6 +649,7 @@ - πŸ“„ [ReservoirSampling](src/main/java/com/thealgorithms/randomized/ReservoirSampling.java) - πŸ“ **recursion** - πŸ“„ [DiceThrower](src/main/java/com/thealgorithms/recursion/DiceThrower.java) + - πŸ“„ [FactorialRecursion](src/main/java/com/thealgorithms/recursion/FactorialRecursion.java) - πŸ“„ [FibonacciSeries](src/main/java/com/thealgorithms/recursion/FibonacciSeries.java) - πŸ“„ [GenerateSubsets](src/main/java/com/thealgorithms/recursion/GenerateSubsets.java) - πŸ“„ [SylvesterSequence](src/main/java/com/thealgorithms/recursion/SylvesterSequence.java) @@ -649,7 +686,7 @@ - πŸ“„ [BoyerMoore](src/main/java/com/thealgorithms/searches/BoyerMoore.java) - πŸ“„ [BreadthFirstSearch](src/main/java/com/thealgorithms/searches/BreadthFirstSearch.java) - πŸ“„ [DepthFirstSearch](src/main/java/com/thealgorithms/searches/DepthFirstSearch.java) - - πŸ“„ [ExponentalSearch](src/main/java/com/thealgorithms/searches/ExponentalSearch.java) + - πŸ“„ [ExponentialSearch](src/main/java/com/thealgorithms/searches/ExponentialSearch.java) - πŸ“„ [FibonacciSearch](src/main/java/com/thealgorithms/searches/FibonacciSearch.java) - πŸ“„ [HowManyTimesRotated](src/main/java/com/thealgorithms/searches/HowManyTimesRotated.java) - πŸ“„ [InterpolationSearch](src/main/java/com/thealgorithms/searches/InterpolationSearch.java) @@ -662,7 +699,6 @@ - πŸ“„ [LowerBound](src/main/java/com/thealgorithms/searches/LowerBound.java) - πŸ“„ [MonteCarloTreeSearch](src/main/java/com/thealgorithms/searches/MonteCarloTreeSearch.java) - πŸ“„ [OrderAgnosticBinarySearch](src/main/java/com/thealgorithms/searches/OrderAgnosticBinarySearch.java) - - πŸ“„ [PerfectBinarySearch](src/main/java/com/thealgorithms/searches/PerfectBinarySearch.java) - πŸ“„ [QuickSelect](src/main/java/com/thealgorithms/searches/QuickSelect.java) - πŸ“„ [RabinKarpAlgorithm](src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java) - πŸ“„ [RandomSearch](src/main/java/com/thealgorithms/searches/RandomSearch.java) @@ -671,7 +707,6 @@ - πŸ“„ [SaddlebackSearch](src/main/java/com/thealgorithms/searches/SaddlebackSearch.java) - πŸ“„ [SearchInARowAndColWiseSortedMatrix](src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java) - πŸ“„ [SentinelLinearSearch](src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java) - - πŸ“„ [SortOrderAgnosticBinarySearch](src/main/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearch.java) - πŸ“„ [SquareRootBinarySearch](src/main/java/com/thealgorithms/searches/SquareRootBinarySearch.java) - πŸ“„ [TernarySearch](src/main/java/com/thealgorithms/searches/TernarySearch.java) - πŸ“„ [UnionFind](src/main/java/com/thealgorithms/searches/UnionFind.java) @@ -758,6 +793,8 @@ - πŸ“„ [SortStack](src/main/java/com/thealgorithms/stacks/SortStack.java) - πŸ“„ [StackPostfixNotation](src/main/java/com/thealgorithms/stacks/StackPostfixNotation.java) - πŸ“„ [StackUsingTwoQueues](src/main/java/com/thealgorithms/stacks/StackUsingTwoQueues.java) + - πŸ“„ [TrappingRainwater](src/main/java/com/thealgorithms/stacks/TrappingRainwater.java) + - πŸ“„ [ValidParentheses](src/main/java/com/thealgorithms/stacks/ValidParentheses.java) - πŸ“ **strings** - πŸ“„ [AhoCorasick](src/main/java/com/thealgorithms/strings/AhoCorasick.java) - πŸ“„ [Alphabetical](src/main/java/com/thealgorithms/strings/Alphabetical.java) @@ -772,13 +809,14 @@ - πŸ“„ [Isogram](src/main/java/com/thealgorithms/strings/Isogram.java) - πŸ“„ [Isomorphic](src/main/java/com/thealgorithms/strings/Isomorphic.java) - πŸ“„ [KMP](src/main/java/com/thealgorithms/strings/KMP.java) + - πŸ“„ [LengthOfLastWord](src/main/java/com/thealgorithms/strings/LengthOfLastWord.java) - πŸ“„ [LetterCombinationsOfPhoneNumber](src/main/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumber.java) - πŸ“„ [LongestCommonPrefix](src/main/java/com/thealgorithms/strings/LongestCommonPrefix.java) - πŸ“„ [LongestNonRepetitiveSubstring](src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java) - - πŸ“„ [LongestPalindromicSubstring](src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java) - πŸ“„ [Lower](src/main/java/com/thealgorithms/strings/Lower.java) - πŸ“„ [Manacher](src/main/java/com/thealgorithms/strings/Manacher.java) - πŸ“„ [MyAtoi](src/main/java/com/thealgorithms/strings/MyAtoi.java) + - πŸ“„ [MoveHashToEnd](src/main/java/com/thealgorithms/strings/MoveHashToEnd.java) - πŸ“„ [Palindrome](src/main/java/com/thealgorithms/strings/Palindrome.java) - πŸ“„ [Pangram](src/main/java/com/thealgorithms/strings/Pangram.java) - πŸ“„ [PermuteString](src/main/java/com/thealgorithms/strings/PermuteString.java) @@ -792,8 +830,8 @@ - πŸ“„ [StringMatchFiniteAutomata](src/main/java/com/thealgorithms/strings/StringMatchFiniteAutomata.java) - πŸ“„ [SuffixArray](src/main/java/com/thealgorithms/strings/SuffixArray.java) - πŸ“„ [Upper](src/main/java/com/thealgorithms/strings/Upper.java) - - πŸ“„ [ValidParentheses](src/main/java/com/thealgorithms/strings/ValidParentheses.java) - πŸ“„ [WordLadder](src/main/java/com/thealgorithms/strings/WordLadder.java) + - πŸ“„ [ZAlgorithm](src/main/java/com/thealgorithms/strings/ZAlgorithm.java) - πŸ“ **zigZagPattern** - πŸ“„ [ZigZagPattern](src/main/java/com/thealgorithms/strings/zigZagPattern/ZigZagPattern.java) - πŸ“ **tree** @@ -808,6 +846,7 @@ - πŸ“ **backtracking** - πŸ“„ [AllPathsFromSourceToTargetTest](src/test/java/com/thealgorithms/backtracking/AllPathsFromSourceToTargetTest.java) - πŸ“„ [ArrayCombinationTest](src/test/java/com/thealgorithms/backtracking/ArrayCombinationTest.java) + - πŸ“„ [CombinationSumTest](src/test/java/com/thealgorithms/backtracking/CombinationSumTest.java) - πŸ“„ [CombinationTest](src/test/java/com/thealgorithms/backtracking/CombinationTest.java) - πŸ“„ [CrosswordSolverTest](src/test/java/com/thealgorithms/backtracking/CrosswordSolverTest.java) - πŸ“„ [FloodFillTest](src/test/java/com/thealgorithms/backtracking/FloodFillTest.java) @@ -819,11 +858,14 @@ - πŸ“„ [PermutationTest](src/test/java/com/thealgorithms/backtracking/PermutationTest.java) - πŸ“„ [PowerSumTest](src/test/java/com/thealgorithms/backtracking/PowerSumTest.java) - πŸ“„ [SubsequenceFinderTest](src/test/java/com/thealgorithms/backtracking/SubsequenceFinderTest.java) + - πŸ“„ [SudokuSolverTest](src/test/java/com/thealgorithms/backtracking/SudokuSolverTest.java) + - πŸ“„ [UniquePermutationTest](src/test/java/com/thealgorithms/backtracking/UniquePermutationTest.java) - πŸ“„ [WordPatternMatcherTest](src/test/java/com/thealgorithms/backtracking/WordPatternMatcherTest.java) - πŸ“„ [WordSearchTest](src/test/java/com/thealgorithms/backtracking/WordSearchTest.java) - πŸ“ **bitmanipulation** - πŸ“„ [BcdConversionTest](src/test/java/com/thealgorithms/bitmanipulation/BcdConversionTest.java) - πŸ“„ [BinaryPalindromeCheckTest](src/test/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheckTest.java) + - πŸ“„ [BitRotateTest](src/test/java/com/thealgorithms/bitmanipulation/BitRotateTest.java) - πŸ“„ [BitSwapTest](src/test/java/com/thealgorithms/bitmanipulation/BitSwapTest.java) - πŸ“„ [BitwiseGCDTest](src/test/java/com/thealgorithms/bitmanipulation/BitwiseGCDTest.java) - πŸ“„ [BooleanAlgebraGatesTest](src/test/java/com/thealgorithms/bitmanipulation/BooleanAlgebraGatesTest.java) @@ -871,6 +913,7 @@ - πŸ“„ [ECCTest](src/test/java/com/thealgorithms/ciphers/ECCTest.java) - πŸ“„ [HillCipherTest](src/test/java/com/thealgorithms/ciphers/HillCipherTest.java) - πŸ“„ [MonoAlphabeticTest](src/test/java/com/thealgorithms/ciphers/MonoAlphabeticTest.java) + - πŸ“„ [OneTimePadCipherTest](src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java) - πŸ“„ [PermutationCipherTest](src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java) - πŸ“„ [PlayfairTest](src/test/java/com/thealgorithms/ciphers/PlayfairTest.java) - πŸ“„ [PolybiusTest](src/test/java/com/thealgorithms/ciphers/PolybiusTest.java) @@ -884,6 +927,12 @@ - πŸ“„ [A5KeyStreamGeneratorTest](src/test/java/com/thealgorithms/ciphers/a5/A5KeyStreamGeneratorTest.java) - πŸ“„ [LFSRTest](src/test/java/com/thealgorithms/ciphers/a5/LFSRTest.java) - πŸ“ **compression** + - πŸ“„ [ArithmeticCodingTest](src/test/java/com/thealgorithms/compression/ArithmeticCodingTest.java) + - πŸ“„ [BurrowsWheelerTransformTest](src/test/java/com/thealgorithms/compression/BurrowsWheelerTransformTest.java) + - πŸ“„ [LZ77Test](src/test/java/com/thealgorithms/compression/LZ77Test.java) + - πŸ“„ [LZ78Test](src/test/java/com/thealgorithms/compression/LZ78Test.java) + - πŸ“„ [LZWTest](src/test/java/com/thealgorithms/compression/LZWTest.java) + - πŸ“„ [MoveToFrontTest](src/test/java/com/thealgorithms/compression/MoveToFrontTest.java) - πŸ“„ [RunLengthEncodingTest](src/test/java/com/thealgorithms/compression/RunLengthEncodingTest.java) - πŸ“„ [ShannonFanoTest](src/test/java/com/thealgorithms/compression/ShannonFanoTest.java) - πŸ“ **conversions** @@ -914,6 +963,7 @@ - πŸ“„ [OctalToHexadecimalTest](src/test/java/com/thealgorithms/conversions/OctalToHexadecimalTest.java) - πŸ“„ [PhoneticAlphabetConverterTest](src/test/java/com/thealgorithms/conversions/PhoneticAlphabetConverterTest.java) - πŸ“„ [RomanToIntegerTest](src/test/java/com/thealgorithms/conversions/RomanToIntegerTest.java) + - πŸ“„ [TemperatureConverterTest](src/test/java/com/thealgorithms/conversions/TemperatureConverterTest.java) - πŸ“„ [TimeConverterTest](src/test/java/com/thealgorithms/conversions/TimeConverterTest.java) - πŸ“„ [TurkishToLatinConversionTest](src/test/java/com/thealgorithms/conversions/TurkishToLatinConversionTest.java) - πŸ“„ [UnitConversionsTest](src/test/java/com/thealgorithms/conversions/UnitConversionsTest.java) @@ -947,8 +997,10 @@ - πŸ“„ [DynamicArrayTest](src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java) - πŸ“ **graphs** - πŸ“„ [AStarTest](src/test/java/com/thealgorithms/datastructures/graphs/AStarTest.java) + - πŸ“„ [BellmanFordTest](src/test/java/com/thealgorithms/datastructures/graphs/BellmanFordTest.java) - πŸ“„ [BipartiteGraphDFSTest](src/test/java/com/thealgorithms/datastructures/graphs/BipartiteGraphDFSTest.java) - πŸ“„ [BoruvkaAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/BoruvkaAlgorithmTest.java) + - πŸ“„ [ConnectedComponentTest](src/test/java/com/thealgorithms/datastructures/graphs/ConnectedComponentTest.java) - πŸ“„ [DialsAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java) - πŸ“„ [DijkstraAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithmTest.java) - πŸ“„ [DijkstraOptimizedAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java) @@ -971,6 +1023,7 @@ - πŸ“„ [GenericHashMapUsingArrayTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java) - πŸ“„ [HashMapCuckooHashingTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapCuckooHashingTest.java) - πŸ“„ [HashMapTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapTest.java) + - πŸ“„ [ImmutableHashMapTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMapTest.java) - πŸ“„ [IntersectionTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/IntersectionTest.java) - πŸ“„ [LinearProbingHashMapTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/LinearProbingHashMapTest.java) - πŸ“„ [MajorityElementTest](src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MajorityElementTest.java) @@ -979,6 +1032,7 @@ - πŸ“„ [FibonacciHeapTest](src/test/java/com/thealgorithms/datastructures/heaps/FibonacciHeapTest.java) - πŸ“„ [GenericHeapTest](src/test/java/com/thealgorithms/datastructures/heaps/GenericHeapTest.java) - πŸ“„ [HeapElementTest](src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java) + - πŸ“„ [IndexedPriorityQueueTest](src/test/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueueTest.java) - πŸ“„ [KthElementFinderTest](src/test/java/com/thealgorithms/datastructures/heaps/KthElementFinderTest.java) - πŸ“„ [LeftistHeapTest](src/test/java/com/thealgorithms/datastructures/heaps/LeftistHeapTest.java) - πŸ“„ [MaxHeapTest](src/test/java/com/thealgorithms/datastructures/heaps/MaxHeapTest.java) @@ -1028,8 +1082,10 @@ - πŸ“„ [BSTRecursiveTest](src/test/java/com/thealgorithms/datastructures/trees/BSTRecursiveTest.java) - πŸ“„ [BTreeTest](src/test/java/com/thealgorithms/datastructures/trees/BTreeTest.java) - πŸ“„ [BinaryTreeTest](src/test/java/com/thealgorithms/datastructures/trees/BinaryTreeTest.java) + - πŸ“„ [BinaryTreeToStringTest](src/test/java/com/thealgorithms/datastructures/trees/BinaryTreeToStringTest.java) - πŸ“„ [BoundaryTraversalTest](src/test/java/com/thealgorithms/datastructures/trees/BoundaryTraversalTest.java) - πŸ“„ [CeilInBinarySearchTreeTest](src/test/java/com/thealgorithms/datastructures/trees/CeilInBinarySearchTreeTest.java) + - πŸ“„ [CentroidDecompositionTest](src/test/java/com/thealgorithms/datastructures/trees/CentroidDecompositionTest.java) - πŸ“„ [CheckBinaryTreeIsValidBSTTest](src/test/java/com/thealgorithms/datastructures/trees/CheckBinaryTreeIsValidBSTTest.java) - πŸ“„ [CheckIfBinaryTreeBalancedTest](src/test/java/com/thealgorithms/datastructures/trees/CheckIfBinaryTreeBalancedTest.java) - πŸ“„ [CheckTreeIsSymmetricTest](src/test/java/com/thealgorithms/datastructures/trees/CheckTreeIsSymmetricTest.java) @@ -1043,6 +1099,7 @@ - πŸ“„ [QuadTreeTest](src/test/java/com/thealgorithms/datastructures/trees/QuadTreeTest.java) - πŸ“„ [SameTreesCheckTest](src/test/java/com/thealgorithms/datastructures/trees/SameTreesCheckTest.java) - πŸ“„ [SplayTreeTest](src/test/java/com/thealgorithms/datastructures/trees/SplayTreeTest.java) + - πŸ“„ [ThreadedBinaryTreeTest](src/test/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTreeTest.java) - πŸ“„ [TreapTest](src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java) - πŸ“„ [TreeTestUtils](src/test/java/com/thealgorithms/datastructures/trees/TreeTestUtils.java) - πŸ“„ [TrieTest](src/test/java/com/thealgorithms/datastructures/trees/TrieTest.java) @@ -1114,8 +1171,10 @@ - πŸ“„ [WildcardMatchingTest](src/test/java/com/thealgorithms/dynamicprogramming/WildcardMatchingTest.java) - πŸ“„ [WineProblemTest](src/test/java/com/thealgorithms/dynamicprogramming/WineProblemTest.java) - πŸ“ **geometry** + - πŸ“„ [BentleyOttmannTest](src/test/java/com/thealgorithms/geometry/BentleyOttmannTest.java) - πŸ“„ [BresenhamLineTest](src/test/java/com/thealgorithms/geometry/BresenhamLineTest.java) - πŸ“„ [ConvexHullTest](src/test/java/com/thealgorithms/geometry/ConvexHullTest.java) + - πŸ“„ [DDALineTest](src/test/java/com/thealgorithms/geometry/DDALineTest.java) - πŸ“„ [GrahamScanTest](src/test/java/com/thealgorithms/geometry/GrahamScanTest.java) - πŸ“„ [HaversineTest](src/test/java/com/thealgorithms/geometry/HaversineTest.java) - πŸ“„ [MidpointCircleTest](src/test/java/com/thealgorithms/geometry/MidpointCircleTest.java) @@ -1128,9 +1187,14 @@ - πŸ“„ [DinicTest](src/test/java/com/thealgorithms/graph/DinicTest.java) - πŸ“„ [EdmondsKarpTest](src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java) - πŸ“„ [EdmondsTest](src/test/java/com/thealgorithms/graph/EdmondsTest.java) + - πŸ“„ [GomoryHuTreeTest](src/test/java/com/thealgorithms/graph/GomoryHuTreeTest.java) + - πŸ“„ [HierholzerAlgorithmTest](src/test/java/com/thealgorithms/graph/HierholzerAlgorithmTest.java) + - πŸ“„ [HierholzerEulerianPathTest](src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java) - πŸ“„ [HopcroftKarpTest](src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java) + - πŸ“„ [HungarianAlgorithmTest](src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java) - πŸ“„ [PredecessorConstrainedDfsTest](src/test/java/com/thealgorithms/graph/PredecessorConstrainedDfsTest.java) - πŸ“„ [PushRelabelTest](src/test/java/com/thealgorithms/graph/PushRelabelTest.java) + - πŸ“„ [StoerWagnerTest](src/test/java/com/thealgorithms/graph/StoerWagnerTest.java) - πŸ“„ [StronglyConnectedComponentOptimizedTest](src/test/java/com/thealgorithms/graph/StronglyConnectedComponentOptimizedTest.java) - πŸ“„ [TravelingSalesmanTest](src/test/java/com/thealgorithms/graph/TravelingSalesmanTest.java) - πŸ“„ [YensKShortestPathsTest](src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java) @@ -1161,6 +1225,7 @@ - πŸ“„ [AbsoluteMaxTest](src/test/java/com/thealgorithms/maths/AbsoluteMaxTest.java) - πŸ“„ [AbsoluteMinTest](src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java) - πŸ“„ [AbsoluteValueTest](src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java) + - πŸ“„ [AbundantNumberTest](src/test/java/com/thealgorithms/maths/AbundantNumberTest.java) - πŸ“„ [AliquotSumTest](src/test/java/com/thealgorithms/maths/AliquotSumTest.java) - πŸ“„ [AmicableNumberTest](src/test/java/com/thealgorithms/maths/AmicableNumberTest.java) - πŸ“„ [AreaTest](src/test/java/com/thealgorithms/maths/AreaTest.java) @@ -1172,6 +1237,7 @@ - πŸ“„ [BinomialCoefficientTest](src/test/java/com/thealgorithms/maths/BinomialCoefficientTest.java) - πŸ“„ [CatalanNumbersTest](src/test/java/com/thealgorithms/maths/CatalanNumbersTest.java) - πŸ“„ [CeilTest](src/test/java/com/thealgorithms/maths/CeilTest.java) + - πŸ“„ [ChebyshevIterationTest](src/test/java/com/thealgorithms/maths/ChebyshevIterationTest.java) - πŸ“„ [ChineseRemainderTheoremTest](src/test/java/com/thealgorithms/maths/ChineseRemainderTheoremTest.java) - πŸ“„ [CollatzConjectureTest](src/test/java/com/thealgorithms/maths/CollatzConjectureTest.java) - πŸ“„ [CombinationsTest](src/test/java/com/thealgorithms/maths/CombinationsTest.java) @@ -1185,8 +1251,9 @@ - πŸ“„ [EulerMethodTest](src/test/java/com/thealgorithms/maths/EulerMethodTest.java) - πŸ“„ [EulerPseudoprimeTest](src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java) - πŸ“„ [EulersFunctionTest](src/test/java/com/thealgorithms/maths/EulersFunctionTest.java) + - πŸ“„ [EvilNumberTest](src/test/java/com/thealgorithms/maths/EvilNumberTest.java) + - πŸ“„ [ExtendedEuclideanAlgorithmTest](src/test/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithmTest.java) - πŸ“„ [FFTTest](src/test/java/com/thealgorithms/maths/FFTTest.java) - - πŸ“„ [FactorialRecursionTest](src/test/java/com/thealgorithms/maths/FactorialRecursionTest.java) - πŸ“„ [FactorialTest](src/test/java/com/thealgorithms/maths/FactorialTest.java) - πŸ“„ [FastExponentiationTest](src/test/java/com/thealgorithms/maths/FastExponentiationTest.java) - πŸ“„ [FastInverseSqrtTests](src/test/java/com/thealgorithms/maths/FastInverseSqrtTests.java) @@ -1211,6 +1278,7 @@ - πŸ“„ [HarshadNumberTest](src/test/java/com/thealgorithms/maths/HarshadNumberTest.java) - πŸ“„ [HeronsFormulaTest](src/test/java/com/thealgorithms/maths/HeronsFormulaTest.java) - πŸ“„ [JosephusProblemTest](src/test/java/com/thealgorithms/maths/JosephusProblemTest.java) + - πŸ“„ [JugglerSequenceTest](src/test/java/com/thealgorithms/maths/JugglerSequenceTest.java) - πŸ“„ [KaprekarNumbersTest](src/test/java/com/thealgorithms/maths/KaprekarNumbersTest.java) - πŸ“„ [KaratsubaMultiplicationTest](src/test/java/com/thealgorithms/maths/KaratsubaMultiplicationTest.java) - πŸ“„ [KeithNumberTest](src/test/java/com/thealgorithms/maths/KeithNumberTest.java) @@ -1220,12 +1288,14 @@ - πŸ“„ [LinearDiophantineEquationsSolverTest](src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java) - πŸ“„ [LongDivisionTest](src/test/java/com/thealgorithms/maths/LongDivisionTest.java) - πŸ“„ [LucasSeriesTest](src/test/java/com/thealgorithms/maths/LucasSeriesTest.java) + - πŸ“„ [LuckyNumberTest](src/test/java/com/thealgorithms/maths/LuckyNumberTest.java) - πŸ“„ [MathBuilderTest](src/test/java/com/thealgorithms/maths/MathBuilderTest.java) - πŸ“„ [MaxValueTest](src/test/java/com/thealgorithms/maths/MaxValueTest.java) - πŸ“„ [MeansTest](src/test/java/com/thealgorithms/maths/MeansTest.java) - πŸ“„ [MedianTest](src/test/java/com/thealgorithms/maths/MedianTest.java) - πŸ“„ [MinValueTest](src/test/java/com/thealgorithms/maths/MinValueTest.java) - πŸ“„ [ModeTest](src/test/java/com/thealgorithms/maths/ModeTest.java) + - πŸ“„ [NevilleTest](src/test/java/com/thealgorithms/maths/NevilleTest.java) - πŸ“„ [NonRepeatingElementTest](src/test/java/com/thealgorithms/maths/NonRepeatingElementTest.java) - πŸ“„ [NthUglyNumberTest](src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java) - πŸ“„ [NumberOfDigitsTest](src/test/java/com/thealgorithms/maths/NumberOfDigitsTest.java) @@ -1240,6 +1310,7 @@ - πŸ“„ [PiApproximationTest](src/test/java/com/thealgorithms/maths/PiApproximationTest.java) - πŸ“„ [PollardRhoTest](src/test/java/com/thealgorithms/maths/PollardRhoTest.java) - πŸ“„ [PowTest](src/test/java/com/thealgorithms/maths/PowTest.java) + - πŸ“„ [PowerOfFourTest](src/test/java/com/thealgorithms/maths/PowerOfFourTest.java) - πŸ“„ [PowerOfTwoOrNotTest](src/test/java/com/thealgorithms/maths/PowerOfTwoOrNotTest.java) - πŸ“„ [PowerUsingRecursionTest](src/test/java/com/thealgorithms/maths/PowerUsingRecursionTest.java) - πŸ“„ [PronicNumberTest](src/test/java/com/thealgorithms/maths/PronicNumberTest.java) @@ -1249,6 +1320,7 @@ - πŸ“„ [SecondMinMaxTest](src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java) - πŸ“„ [SieveOfAtkinTest](src/test/java/com/thealgorithms/maths/SieveOfAtkinTest.java) - πŸ“„ [SieveOfEratosthenesTest](src/test/java/com/thealgorithms/maths/SieveOfEratosthenesTest.java) + - πŸ“„ [SmithNumberTest](src/test/java/com/thealgorithms/maths/SmithNumberTest.java) - πŸ“„ [SolovayStrassenPrimalityTestTest](src/test/java/com/thealgorithms/maths/SolovayStrassenPrimalityTestTest.java) - πŸ“„ [SquareFreeIntegerTest](src/test/java/com/thealgorithms/maths/SquareFreeIntegerTest.java) - πŸ“„ [SquareRootWithNewtonRaphsonTestMethod](src/test/java/com/thealgorithms/maths/SquareRootWithNewtonRaphsonTestMethod.java) @@ -1275,14 +1347,16 @@ - πŸ“„ [PrimeFactorizationTest](src/test/java/com/thealgorithms/maths/prime/PrimeFactorizationTest.java) - πŸ“ **matrix** - πŸ“„ [InverseOfMatrixTest](src/test/java/com/thealgorithms/matrix/InverseOfMatrixTest.java) + - πŸ“„ [LUDecompositionTest](src/test/java/com/thealgorithms/matrix/LUDecompositionTest.java) - πŸ“„ [MatrixMultiplicationTest](src/test/java/com/thealgorithms/matrix/MatrixMultiplicationTest.java) - πŸ“„ [MatrixRankTest](src/test/java/com/thealgorithms/matrix/MatrixRankTest.java) - πŸ“„ [MatrixTransposeTest](src/test/java/com/thealgorithms/matrix/MatrixTransposeTest.java) - πŸ“„ [MatrixUtilTest](src/test/java/com/thealgorithms/matrix/MatrixUtilTest.java) - πŸ“„ [MedianOfMatrixTest](src/test/java/com/thealgorithms/matrix/MedianOfMatrixTest.java) - πŸ“„ [MirrorOfMatrixTest](src/test/java/com/thealgorithms/matrix/MirrorOfMatrixTest.java) + - πŸ“„ [PrintAMatrixInSpiralOrderTest](src/test/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrderTest.java) - πŸ“„ [SolveSystemTest](src/test/java/com/thealgorithms/matrix/SolveSystemTest.java) - - πŸ“„ [TestPrintMatrixInSpiralOrder](src/test/java/com/thealgorithms/matrix/TestPrintMatrixInSpiralOrder.java) + - πŸ“„ [StochasticMatrixTest](src/test/java/com/thealgorithms/matrix/StochasticMatrixTest.java) - πŸ“ **misc** - πŸ“„ [ColorContrastRatioTest](src/test/java/com/thealgorithms/misc/ColorContrastRatioTest.java) - πŸ“„ [MapReduceTest](src/test/java/com/thealgorithms/misc/MapReduceTest.java) @@ -1316,22 +1390,26 @@ - πŸ“„ [MaximumSumOfDistinctSubarraysWithLengthKTest](src/test/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthKTest.java) - πŸ“„ [MiniMaxAlgorithmTest](src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java) - πŸ“„ [MosAlgorithmTest](src/test/java/com/thealgorithms/others/MosAlgorithmTest.java) - - πŸ“„ [NewManShanksPrimeTest](src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java) - πŸ“„ [NextFitTest](src/test/java/com/thealgorithms/others/NextFitTest.java) - πŸ“„ [PageRankTest](src/test/java/com/thealgorithms/others/PageRankTest.java) - πŸ“„ [PasswordGenTest](src/test/java/com/thealgorithms/others/PasswordGenTest.java) - πŸ“„ [PerlinNoiseTest](src/test/java/com/thealgorithms/others/PerlinNoiseTest.java) - πŸ“„ [QueueUsingTwoStacksTest](src/test/java/com/thealgorithms/others/QueueUsingTwoStacksTest.java) - πŸ“„ [SkylineProblemTest](src/test/java/com/thealgorithms/others/SkylineProblemTest.java) - - πŸ“„ [TestPrintMatrixInSpiralOrder](src/test/java/com/thealgorithms/others/TestPrintMatrixInSpiralOrder.java) - πŸ“„ [TwoPointersTest](src/test/java/com/thealgorithms/others/TwoPointersTest.java) - πŸ“„ [WorstFitCPUTest](src/test/java/com/thealgorithms/others/WorstFitCPUTest.java) - - πŸ“ **cn** - - πŸ“„ [HammingDistanceTest](src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java) - πŸ“ **physics** + - πŸ“„ [CoulombsLawTest](src/test/java/com/thealgorithms/physics/CoulombsLawTest.java) + - πŸ“„ [DampedOscillatorTest](src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java) + - πŸ“„ [ElasticCollision2DTest](src/test/java/com/thealgorithms/physics/ElasticCollision2DTest.java) + - πŸ“„ [GravitationTest](src/test/java/com/thealgorithms/physics/GravitationTest.java) - πŸ“„ [GroundToGroundProjectileMotionTest](src/test/java/com/thealgorithms/physics/GroundToGroundProjectileMotionTest.java) + - πŸ“„ [KinematicsTest](src/test/java/com/thealgorithms/physics/KinematicsTest.java) + - πŸ“„ [ProjectileMotionTest](src/test/java/com/thealgorithms/physics/ProjectileMotionTest.java) + - πŸ“„ [SimplePendulumRK4Test](src/test/java/com/thealgorithms/physics/SimplePendulumRK4Test.java) + - πŸ“„ [SnellLawTest](src/test/java/com/thealgorithms/physics/SnellLawTest.java) + - πŸ“„ [ThinLensTest](src/test/java/com/thealgorithms/physics/ThinLensTest.java) - πŸ“ **puzzlesandgames** - - πŸ“„ [SudokuTest](src/test/java/com/thealgorithms/puzzlesandgames/SudokuTest.java) - πŸ“„ [TowerOfHanoiTest](src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java) - πŸ“„ [WordBoggleTest](src/test/java/com/thealgorithms/puzzlesandgames/WordBoggleTest.java) - πŸ“ **randomized** @@ -1343,6 +1421,7 @@ - πŸ“„ [ReservoirSamplingTest](src/test/java/com/thealgorithms/randomized/ReservoirSamplingTest.java) - πŸ“ **recursion** - πŸ“„ [DiceThrowerTest](src/test/java/com/thealgorithms/recursion/DiceThrowerTest.java) + - πŸ“„ [FactorialRecursionTest](src/test/java/com/thealgorithms/recursion/FactorialRecursionTest.java) - πŸ“„ [FibonacciSeriesTest](src/test/java/com/thealgorithms/recursion/FibonacciSeriesTest.java) - πŸ“„ [GenerateSubsetsTest](src/test/java/com/thealgorithms/recursion/GenerateSubsetsTest.java) - πŸ“„ [SylvesterSequenceTest](src/test/java/com/thealgorithms/recursion/SylvesterSequenceTest.java) @@ -1392,7 +1471,6 @@ - πŸ“„ [LowerBoundTest](src/test/java/com/thealgorithms/searches/LowerBoundTest.java) - πŸ“„ [MonteCarloTreeSearchTest](src/test/java/com/thealgorithms/searches/MonteCarloTreeSearchTest.java) - πŸ“„ [OrderAgnosticBinarySearchTest](src/test/java/com/thealgorithms/searches/OrderAgnosticBinarySearchTest.java) - - πŸ“„ [PerfectBinarySearchTest](src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java) - πŸ“„ [QuickSelectTest](src/test/java/com/thealgorithms/searches/QuickSelectTest.java) - πŸ“„ [RabinKarpAlgorithmTest](src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java) - πŸ“„ [RandomSearchTest](src/test/java/com/thealgorithms/searches/RandomSearchTest.java) @@ -1401,7 +1479,6 @@ - πŸ“„ [SaddlebackSearchTest](src/test/java/com/thealgorithms/searches/SaddlebackSearchTest.java) - πŸ“„ [SearchInARowAndColWiseSortedMatrixTest](src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java) - πŸ“„ [SentinelLinearSearchTest](src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java) - - πŸ“„ [SortOrderAgnosticBinarySearchTest](src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java) - πŸ“„ [SquareRootBinarySearchTest](src/test/java/com/thealgorithms/searches/SquareRootBinarySearchTest.java) - πŸ“„ [TernarySearchTest](src/test/java/com/thealgorithms/searches/TernarySearchTest.java) - πŸ“„ [TestSearchInARowAndColWiseSortedMatrix](src/test/java/com/thealgorithms/searches/TestSearchInARowAndColWiseSortedMatrix.java) @@ -1487,6 +1564,8 @@ - πŸ“„ [SortStackTest](src/test/java/com/thealgorithms/stacks/SortStackTest.java) - πŸ“„ [StackPostfixNotationTest](src/test/java/com/thealgorithms/stacks/StackPostfixNotationTest.java) - πŸ“„ [StackUsingTwoQueuesTest](src/test/java/com/thealgorithms/stacks/StackUsingTwoQueuesTest.java) + - πŸ“„ [TrappingRainwaterTest](src/test/java/com/thealgorithms/stacks/TrappingRainwaterTest.java) + - πŸ“„ [ValidParenthesesTest](src/test/java/com/thealgorithms/stacks/ValidParenthesesTest.java) - πŸ“ **strings** - πŸ“„ [AhoCorasickTest](src/test/java/com/thealgorithms/strings/AhoCorasickTest.java) - πŸ“„ [AlphabeticalTest](src/test/java/com/thealgorithms/strings/AlphabeticalTest.java) @@ -1500,10 +1579,10 @@ - πŸ“„ [HorspoolSearchTest](src/test/java/com/thealgorithms/strings/HorspoolSearchTest.java) - πŸ“„ [IsogramTest](src/test/java/com/thealgorithms/strings/IsogramTest.java) - πŸ“„ [IsomorphicTest](src/test/java/com/thealgorithms/strings/IsomorphicTest.java) + - πŸ“„ [LengthOfLastWordTest](src/test/java/com/thealgorithms/strings/LengthOfLastWordTest.java) - πŸ“„ [LetterCombinationsOfPhoneNumberTest](src/test/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumberTest.java) - πŸ“„ [LongestCommonPrefixTest](src/test/java/com/thealgorithms/strings/LongestCommonPrefixTest.java) - πŸ“„ [LongestNonRepetitiveSubstringTest](src/test/java/com/thealgorithms/strings/LongestNonRepetitiveSubstringTest.java) - - πŸ“„ [LongestPalindromicSubstringTest](src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java) - πŸ“„ [LowerTest](src/test/java/com/thealgorithms/strings/LowerTest.java) - πŸ“„ [ManacherTest](src/test/java/com/thealgorithms/strings/ManacherTest.java) - πŸ“„ [MyAtoiTest](src/test/java/com/thealgorithms/strings/MyAtoiTest.java) @@ -1519,8 +1598,8 @@ - πŸ“„ [StringMatchFiniteAutomataTest](src/test/java/com/thealgorithms/strings/StringMatchFiniteAutomataTest.java) - πŸ“„ [SuffixArrayTest](src/test/java/com/thealgorithms/strings/SuffixArrayTest.java) - πŸ“„ [UpperTest](src/test/java/com/thealgorithms/strings/UpperTest.java) - - πŸ“„ [ValidParenthesesTest](src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java) - πŸ“„ [WordLadderTest](src/test/java/com/thealgorithms/strings/WordLadderTest.java) + - πŸ“„ [ZAlgorithmTest](src/test/java/com/thealgorithms/strings/ZAlgorithmTest.java) - πŸ“ **zigZagPattern** - πŸ“„ [ZigZagPatternTest](src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java) - πŸ“ **tree** diff --git a/README-ko.md b/README-ko.md deleted file mode 100644 index 4f8cab92fc42..000000000000 --- a/README-ko.md +++ /dev/null @@ -1,191 +0,0 @@ -# μ•Œκ³ λ¦¬μ¦˜ - μžλ°” - -## 이 [개발브런치](https://github.com/TheAlgorithms/Java/tree/Development)λŠ” κΈ°μ‘΄ ν”„λ‘œμ νŠΈλ₯Ό Java ν”„λ‘œμ νŠΈ ꡬ쑰둜 μž¬κ°œλ°œν•˜κΈ° μœ„ν•΄ μž‘μ„±λ˜μ—ˆλ‹€. 기여도λ₯Ό μœ„ν•΄ 개발 μ§€μ‚¬λ‘œ μ „ν™˜ν•  수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ 이 문제λ₯Ό μ°Έμ‘°ν•˜μ‹­μ‹œμ˜€. μ»¨νŠΈλ¦¬λ·°μ…˜μ„ μœ„ν•΄ [개발브런치](https://github.com/TheAlgorithms/Java/tree/Development)둜 μ „ν™˜ν•  수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ [이 이슈](https://github.com/TheAlgorithms/Java/issues/474)λ₯Ό μ°Έκ³ ν•˜μ‹­μ‹œμ˜€. - -### μžλ°”λ‘œ κ΅¬ν˜„λœ λͺ¨λ“  μ•Œκ³ λ¦¬μ¦˜λ“€ (ꡐ윑용) - -이것듀은 단지 μ‹œλ²”μ„ μœ„ν•œ 것이닀. ν‘œμ€€ μžλ°” λΌμ΄λΈŒλŸ¬λ¦¬μ—λŠ” μ„±λŠ₯μƒμ˜ 이유둜 더 λ‚˜μ€ 것듀이 κ΅¬ν˜„λ˜μ–΄μžˆλ‹€ - -## μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜ - -### Bubble(버블 μ •λ ¬) - -![alt text][bubble-image] - -From [Wikipedia][bubble-wiki]: 버블 μ†ŒνŠΈ(sinking sor라고도 λΆˆλ¦¬μ›€)λŠ” 리슀트λ₯Ό 반볡적인 λ‹¨κ³„λ‘œ μ ‘κ·Όν•˜μ—¬ μ •λ ¬ν•œλ‹€. 각각의 짝을 λΉ„κ΅ν•˜λ©°, μˆœμ„œκ°€ 잘λͺ»λœ 경우 κ·Έμ ‘ν•œ μ•„μ΄ν…œλ“€μ„ μŠ€μ™‘ν•˜λŠ” μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. 더 이상 μŠ€μ™‘ν•  것이 없을 λ•ŒκΉŒμ§€ λ°˜λ³΅ν•˜λ©°, 반볡이 λλ‚¨μŒ λ¦¬μŠ€νŠΈκ°€ μ •λ ¬λ˜μ—ˆμŒμ„ μ˜λ―Έν•œλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n) -- 평균 μ„±λŠ₯ O(n^2) - -###### View the algorithm in [action][bubble-toptal] - -### Insertion(μ‚½μž… μ •λ ¬) - -![alt text][insertion-image] - -From [Wikipedia][insertion-wiki]: μ‚½μž… 정렬은 μ΅œμ’… μ •λ ¬λœ λ°°μ—΄(λ˜λŠ” 리슀트)을 ν•œλ²ˆμ— ν•˜λ‚˜μ”© κ΅¬μΆ•ν•˜λŠ” μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. 이것은 큰 λ¦¬μŠ€νŠΈμ—μ„œ 더 λ‚˜μ€ μ•Œκ³ λ¦¬μ¦˜μΈ 퀡 μ†ŒνŠΈ, νž™ μ†ŒνŠΈ, λ˜λŠ” λ¨Έμ§€ μ†ŒνŠΈλ³΄λ‹€ 훨씬 μ•ˆμ’‹μ€ νš¨μœ¨μ„ κ°€μ§„λ‹€. κ·Έλ¦Όμ—μ„œ 각 λ§‰λŒ€λŠ” μ •λ ¬ν•΄μ•Ό ν•˜λŠ” λ°°μ—΄μ˜ μš”μ†Œλ₯Ό λ‚˜νƒ€λ‚Έλ‹€. 상단과 두 번째 상단 λ§‰λŒ€μ˜ 첫 번째 κ΅μ°¨μ μ—μ„œ λ°œμƒν•˜λŠ” 것은 두 번째 μš”μ†Œκ°€ 첫 번째 μš”μ†Œλ³΄λ‹€ 더 높은 μš°μ„  μˆœμœ„λ₯Ό κ°€μ§€κΈ° λ•Œλ¬Έμ— λ§‰λŒ€λ‘œ ν‘œμ‹œλ˜λŠ” μ΄λŸ¬ν•œ μš”μ†Œλ₯Ό κ΅ν™˜ν•œ 것이닀. 이 방법을 λ°˜λ³΅ν•˜λ©΄ μ‚½μž… 정렬이 μ™„λ£Œλœλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n) -- 평균 O(n^2) - -###### View the algorithm in [action][insertion-toptal] - -### Merge(합병 μ •λ ¬) - -![alt text][merge-image] - -From [Wikipedia][merge-wiki]: 컴퓨터 κ³Όν•™μ—μ„œ, 합병 정렬은 효율적인, λ²”μš©μ μΈ, 비ꡐ 기반 μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. λŒ€λΆ€λΆ„μ˜ κ΅¬ν˜„μ€ μ•ˆμ •μ μΈ λΆ„λ₯˜λ₯Ό μ΄λ£¨λŠ”λ°, 이것은 κ΅¬ν˜„μ΄ μ •λ ¬λœ 좜λ ₯에 λ™μΌν•œ μš”μ†Œμ˜ μž…λ ₯ μˆœμ„œλ₯Ό μœ μ§€ν•œλ‹€λŠ” 것을 μ˜λ―Έν•œλ‹€. 합병 정렬은 1945년에 John von Neumann이 발λͺ…ν•œ λΆ„ν•  정볡 μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n log n) (일반적) -- 졜고의 μ„±λŠ₯ O(n log n) -- 평균 O(n log n) - -###### View the algorithm in [action][merge-toptal] - -### Quick(퀡 μ •λ ¬) - -![alt text][quick-image] - -From [Wikipedia][quick-wiki]: 퀡 μ •λ ¬sometimes called partition-exchange sort)은 효율적인 μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μœΌλ‘œ, λ°°μ—΄μ˜ μš”μ†Œλ₯Ό μˆœμ„œλŒ€λ‘œ μ •λ ¬ν•˜λŠ” 체계적인 방법 μ—­ν™œμ„ ν•œλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n log n) or O(n) with three-way partition -- 평균 O(n log n) - -###### View the algorithm in [action][quick-toptal] - -### Selection(선택 μ •λ ¬) - -![alt text][selection-image] - -From [Wikipedia][selection-wiki]: μ•Œκ³ λ¦¬μ¦˜ μž…λ ₯ 리슀트λ₯Ό 두 λΆ€λΆ„μœΌλ‘œ λ‚˜λˆˆλ‹€ : 첫 뢀뢄은 μ•„μ΄ν…œλ“€μ΄ 이미 μ™Όμͺ½μ—μ„œ 였λ₯Έμͺ½μœΌλ‘œ μ •λ ¬λ˜μ—ˆλ‹€. 그리고 남은 λΆ€λΆ„μ˜ μ•„μ΄ν…œλ“€μ€ λ‚˜λ¨Έμ§€ ν•­λͺ©μ„ μ°¨μ§€ν•˜λŠ” λ¦¬μŠ€νŠΈμ΄λ‹€. μ²˜μŒμ—λŠ” μ •λ ¬λœ λ¦¬μŠ€νŠΈλŠ” 곡백이고 λ‚˜λ¨Έμ§€κ°€ 전뢀이닀. 였λ₯΄μ°¨μˆœ(λ˜λŠ” λ‚΄λ¦Όμ°¨μˆœ) μ•Œκ³ λ¦¬μ¦˜μ€ κ°€μž₯ μž‘μ€ μš”μ†Œλ₯Ό μ •λ ¬λ˜μ§€ μ•Šμ€ λ¦¬μŠ€νŠΈμ—μ„œ μ°Ύκ³  정렬이 μ•ˆλœ κ°€μž₯ μ™Όμͺ½(μ •λ ¬λœ 리슀트) λ¦¬μŠ€νŠΈμ™€ λ°”κΎΌλ‹€. μ΄λ ‡κ²Œ 였λ₯Έμͺ½μœΌλ‘œ λ‚˜μ•„κ°„λ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n^2) -- 평균 O(n^2) - -###### View the algorithm in [action][selection-toptal] - -### Shell(μ‰˜ μ •λ ¬) - -![alt text][shell-image] - -From [Wikipedia][shell-wiki]: μ‰˜ 정렬은 멀리 λ–¨μ–΄μ Έ μžˆλŠ” ν•­λͺ©μ˜ κ΅ν™˜μ„ ν—ˆμš©ν•˜λŠ” μ‚½μž… μ’…λ₯˜μ˜ μΌλ°˜ν™”μ΄λ‹€. κ·Έ μ•„μ΄λ””μ–΄λŠ” λͺ¨λ“  n번째 μš”μ†Œκ°€ μ •λ ¬λœ λͺ©λ‘μ„ μ œκ³΅ν•œλ‹€λŠ” 것을 κ³ λ €ν•˜μ—¬ μ–΄λŠ κ³³μ—μ„œλ“ μ§€ μ‹œμž‘ν•˜λ„λ‘ μš”μ†Œμ˜ λͺ©λ‘μ„ λ°°μ—΄ν•˜λŠ” 것이닀. μ΄λŸ¬ν•œ λͺ©λ‘μ€ h-sorted둜 μ•Œλ €μ Έ μžˆλ‹€. λ§ˆμ°¬κ°€μ§€λ‘œ, 각각 κ°œλ³„μ μœΌλ‘œ μ •λ ¬λœ h μΈν„°λ¦¬λΈŒ λͺ©λ‘μœΌλ‘œ κ°„μ£Όν•  수 μžˆλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(nlog2 2n) -- 졜고의 μ„±λŠ₯ O(n log n) -- Average case performance depends on gap sequence - -###### View the algorithm in [action][shell-toptal] - -### μ‹œκ°„ λ³΅μž‘μ„± κ·Έλž˜ν”„ - -μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ˜ λ³΅μž‘μ„± 비ꡐ (버블 μ •λ ¬, μ‚½μž… μ •λ ¬, 선택 μ •λ ¬) - -[λ³΅μž‘μ„± κ·Έλž˜ν”„](https://github.com/prateekiiest/Python/blob/master/sorts/sortinggraphs.png) - ---- - -## 검색 μ•Œκ³ λ¦¬μ¦˜ - -### Linear (μ„ ν˜• 탐색) - -![alt text][linear-image] - -From [Wikipedia][linear-wiki]: μ„ ν˜• 탐색 λ˜λŠ” 순차 탐색은 λͺ©λ‘ λ‚΄μ—μ„œ λͺ©ν‘œκ°’을 μ°ΎλŠ” 방법이닀. 일치 ν•­λͺ©μ΄ λ°œκ²¬λ˜κ±°λ‚˜ λͺ¨λ“  μš”μ†Œκ°€ 탐색될 λ•ŒκΉŒμ§€ λͺ©λ‘μ˜ 각 μš”μ†Œμ— λŒ€ν•΄ λͺ©ν‘œκ°’을 순차적으둜 κ²€μ‚¬ν•œλ‹€. -μ„ ν˜• 검색은 μ΅œμ•…μ˜ μ„ ν˜• μ‹œκ°„μœΌλ‘œ μ‹€ν–‰λ˜λ©° μ΅œλŒ€ n개의 λΉ„κ΅μ—μ„œ 이루어진닀. μ—¬κΈ°μ„œ n은 λͺ©λ‘μ˜ 길이닀. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n) -- 졜고의 μ„±λŠ₯ O(1) -- 평균 O(n) -- μ΅œμ•…μ˜ 경우 곡간 λ³΅μž‘μ„± O(1) iterative - -### Binary (이진 탐색) - -![alt text][binary-image] - -From [Wikipedia][binary-wiki]: 이진 탐색, (also known as half-interval search or logarithmic search), 은 μ •λ ¬λœ λ°°μ—΄ λ‚΄μ—μ„œ λͺ©ν‘œκ°’μ˜ μœ„μΉ˜λ₯Ό μ°ΎλŠ” 검색 μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. λͺ©ν‘œκ°’을 λ°°μ—΄μ˜ 쀑간 μš”μ†Œμ™€ λΉ„κ΅ν•œλ‹€; λ§Œμ•½ λͺ©ν‘œκ°’이 λ™μΌν•˜μ§€ μ•ŠμœΌλ©΄, λͺ©ν‘œλ¬Όμ˜ 절반이 제거되고 검색이 성곡할 λ•ŒκΉŒμ§€ λ‚˜λ¨Έμ§€ μ ˆλ°˜μ—μ„œ μ†λœλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(log n) -- 졜고의 μ„±λŠ₯ O(1) -- 평균 O(log n) -- μ΅œμ•…μ˜ 경우 곡간 λ³΅μž‘μ„± O(1) - -[bubble-toptal]: https://www.toptal.com/developers/sorting-algorithms/bubble-sort -[bubble-wiki]: https://en.wikipedia.org/wiki/Bubble_sort -[bubble-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Bubblesort-edited-color.svg/220px-Bubblesort-edited-color.svg.png "Bubble Sort" -[insertion-toptal]: https://www.toptal.com/developers/sorting-algorithms/insertion-sort -[insertion-wiki]: https://en.wikipedia.org/wiki/Insertion_sort -[insertion-image]: https://upload.wikimedia.org/wikipedia/commons/7/7e/Insertionsort-edited.png "Insertion Sort" -[quick-toptal]: https://www.toptal.com/developers/sorting-algorithms/quick-sort -[quick-wiki]: https://en.wikipedia.org/wiki/Quicksort -[quick-image]: https://upload.wikimedia.org/wikipedia/commons/6/6a/Sorting_quicksort_anim.gif "Quick Sort" -[merge-toptal]: https://www.toptal.com/developers/sorting-algorithms/merge-sort -[merge-wiki]: https://en.wikipedia.org/wiki/Merge_sort -[merge-image]: https://upload.wikimedia.org/wikipedia/commons/c/cc/Merge-sort-example-300px.gif "Merge Sort" -[selection-toptal]: https://www.toptal.com/developers/sorting-algorithms/selection-sort -[selection-wiki]: https://en.wikipedia.org/wiki/Selection_sort -[selection-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Selection_sort_animation.gif/250px-Selection_sort_animation.gif "Selection Sort Sort" -[shell-toptal]: https://www.toptal.com/developers/sorting-algorithms/shell-sort -[shell-wiki]: https://en.wikipedia.org/wiki/Shellsort -[shell-image]: https://upload.wikimedia.org/wikipedia/commons/d/d8/Sorting_shellsort_anim.gif "Shell Sort" -[linear-wiki]: https://en.wikipedia.org/wiki/Linear_search -[linear-image]: http://www.tutorialspoint.com/data_structures_algorithms/images/linear_search.gif -[binary-wiki]: https://en.wikipedia.org/wiki/Binary_search_algorithm -[binary-image]: https://upload.wikimedia.org/wikipedia/commons/f/f7/Binary_search_into_array.png - ---- - -## λ‚˜λ¨Έμ§€ μ•Œκ³ λ¦¬μ¦˜μ— λŒ€ν•œ 링크 - -| μ „ν™˜ | λ‹€μ΄λ‚˜λ―Ήν”„λ‘œκ·Έλž˜λ°(DP) | μ•”ν˜Έ | κ·Έ μ™Έ 것듀 | -| --------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | -| [Any Base to Any Base](Conversions/AnyBaseToAnyBase.java) | [Coin Change](DynamicProgramming/CoinChange.java) | [Caesar](Ciphers/Caesar.java) | [Heap Sort](Sorts/HeapSort.java) | -| [Any Base to Decimal](Conversions/AnyBaseToDecimal.java) | [Egg Dropping](DynamicProgramming/EggDropping.java) | [Columnar Transposition Cipher](Ciphers/ColumnarTranspositionCipher.java) | [Palindromic Prime Checker](Misc/PalindromePrime.java) | -| [Binary to Decimal](Conversions/BinaryToDecimal.java) | [Fibonacci](DynamicProgramming/Fibonacci.java) | [RSA](Ciphers/RSA.java) | More soon... | -| [Binary to HexaDecimal](Conversions/BinaryToHexadecimal.java) | [Kadane Algorithm](DynamicProgramming/KadaneAlgorithm.java) | more coming soon... | -| [Binary to Octal](Conversions/BinaryToOctal.java) | [Knapsack](DynamicProgramming/Knapsack.java) | -| [Decimal To Any Base](Conversions/DecimalToAnyBase.java) | [Longest Common Subsequence](DynamicProgramming/LongestCommonSubsequence.java) | -| [Decimal To Binary](Conversions/DecimalToBinary.java) | [Longest Increasing Subsequence](DynamicProgramming/LongestIncreasingSubsequence.java) | -| [Decimal To Hexadecimal](Conversions/DecimalToHexaDecimal.java) | [Rod Cutting](DynamicProgramming/RodCutting.java) | -| and much more... | and more... | - -### 자료 ꡬ쑰 - -| κ·Έλž˜ν”„ | νž™ | 리슀트 | 큐 | -| ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------- | -| | [빈 νž™ μ˜ˆμ™Έμ²˜λ¦¬](DataStructures/Heaps/EmptyHeapException.java) | [μ›ν˜• μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/CircleLinkedList.java) | [μ œλ„ˆλ¦­ μ–΄λ ˆμ΄ 리슀트 큐](DataStructures/Queues/GenericArrayListQueue.java) | -| | [νž™](DataStructures/Heaps/Heap.java) | [이쀑 μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/DoublyLinkedList.java) | [큐](DataStructures/Queues/Queues.java) | -| [κ·Έλž˜ν”„](DataStructures/Graphs/Graphs.java) | [νž™ μš”μ†Œ](DataStructures/Heaps/HeapElement.java) | [λ‹¨μˆœ μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/SinglyLinkedList.java) | -| [크루슀칼 μ•Œκ³ λ¦¬μ¦˜](DataStructures/Graphs/Kruskal.java) | [μ΅œλŒ€νž™](DataStructures/Heaps/MaxHeap.java) | -| [ν–‰λ ¬ κ·Έλž˜ν”„](DataStructures/Graphs/MatrixGraphs.java) | [μ΅œμ†Œνž™](DataStructures/Heaps/MinHeap.java) | -| [ν”„λ¦Ό μ΅œμ†Œμ‹ μž₯트리](DataStructures/Graphs/PrimMST.java) | - -| μŠ€νƒ | 트리 | -| --------------------------------------------------------------- | ------------------------------------------------- | -| [λ…Έλ“œ μŠ€νƒ](DataStructures/Stacks/NodeStack.java) | [AVL 트리](DataStructures/Trees/AVLTree.java) | -| [μ—°κ²°λ¦¬μŠ€νŠΈ μŠ€νƒ](DataStructures/Stacks/StackOfLinkedList.java) | [이진 트리](DataStructures/Trees/BinaryTree.java) | -| [μŠ€νƒ](DataStructures/Stacks) | And much more... | - -- [Bags](DataStructures/Bags/Bag.java) -- [Buffer](DataStructures/Buffers/CircularBuffer.java) -- [HashMap](DataStructures/HashMap/Hashing/HashMap.java) -- 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/pmd-exclude.properties b/pmd-exclude.properties index a3c95b12fa4b..26653d9dc17b 100644 --- a/pmd-exclude.properties +++ b/pmd-exclude.properties @@ -53,14 +53,11 @@ com.thealgorithms.maths.Gaussian=UselessParentheses com.thealgorithms.maths.GcdSolutionWrapper=UselessParentheses com.thealgorithms.maths.HeronsFormula=UselessParentheses com.thealgorithms.maths.JugglerSequence=UselessMainMethod -com.thealgorithms.maths.KaprekarNumbers=UselessParentheses com.thealgorithms.maths.KeithNumber=UselessMainMethod,UselessParentheses -com.thealgorithms.maths.LeonardoNumber=UselessParentheses com.thealgorithms.maths.LinearDiophantineEquationsSolver=UselessMainMethod,UselessParentheses com.thealgorithms.maths.MagicSquare=UselessMainMethod com.thealgorithms.maths.PiNilakantha=UselessMainMethod com.thealgorithms.maths.Prime.PrimeCheck=UselessMainMethod -com.thealgorithms.maths.PythagoreanTriple=UselessMainMethod com.thealgorithms.maths.RomanNumeralUtil=UselessParentheses com.thealgorithms.maths.SecondMinMax=UselessParentheses com.thealgorithms.maths.SecondMinMaxTest=UnnecessaryFullyQualifiedName @@ -71,7 +68,6 @@ com.thealgorithms.maths.TrinomialTriangle=UselessMainMethod,UselessParentheses com.thealgorithms.maths.VectorCrossProduct=UselessMainMethod com.thealgorithms.maths.Volume=UselessParentheses com.thealgorithms.matrix.RotateMatrixBy90Degrees=UselessMainMethod -com.thealgorithms.misc.Sparsity=UselessParentheses com.thealgorithms.others.BankersAlgorithm=UselessMainMethod com.thealgorithms.others.BrianKernighanAlgorithm=UselessMainMethod com.thealgorithms.others.CRC16=UselessMainMethod,UselessParentheses @@ -79,11 +75,9 @@ com.thealgorithms.others.CRC32=UselessMainMethod com.thealgorithms.others.Damm=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.others.Dijkstra=UselessMainMethod com.thealgorithms.others.GaussLegendre=UselessMainMethod -com.thealgorithms.others.HappyNumbersSeq=UselessMainMethod com.thealgorithms.others.Huffman=UselessMainMethod com.thealgorithms.others.InsertDeleteInArray=UselessMainMethod com.thealgorithms.others.KochSnowflake=UselessMainMethod -com.thealgorithms.others.Krishnamurthy=UselessMainMethod com.thealgorithms.others.LinearCongruentialGenerator=UselessMainMethod com.thealgorithms.others.Luhn=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.others.Mandelbrot=UselessMainMethod,UselessParentheses @@ -92,9 +86,8 @@ com.thealgorithms.others.MosAlgorithm=UselessMainMethod com.thealgorithms.others.PageRank=UselessMainMethod,UselessParentheses com.thealgorithms.others.PerlinNoise=UselessMainMethod,UselessParentheses com.thealgorithms.others.QueueUsingTwoStacks=UselessParentheses -com.thealgorithms.others.Trieac=UselessMainMethod,UselessParentheses +com.thealgorithms.datastructures.trees.TrieAutocomplete=UselessMainMethod,UselessParentheses com.thealgorithms.others.Verhoeff=UnnecessaryFullyQualifiedName,UselessMainMethod -com.thealgorithms.puzzlesandgames.Sudoku=UselessMainMethod com.thealgorithms.recursion.DiceThrower=UselessMainMethod com.thealgorithms.searches.HowManyTimesRotated=UselessMainMethod com.thealgorithms.searches.InterpolationSearch=UselessParentheses @@ -108,15 +101,11 @@ com.thealgorithms.sorts.MergeSortNoExtraSpace=UselessParentheses com.thealgorithms.sorts.RadixSort=UselessParentheses com.thealgorithms.sorts.TreeSort=UselessMainMethod com.thealgorithms.sorts.WiggleSort=UselessParentheses -com.thealgorithms.stacks.LargestRectangle=UselessMainMethod com.thealgorithms.stacks.MaximumMinimumWindow=UselessMainMethod com.thealgorithms.stacks.PostfixToInfix=UselessParentheses -com.thealgorithms.strings.Alphabetical=UselessMainMethod com.thealgorithms.strings.HorspoolSearch=UnnecessaryFullyQualifiedName,UselessParentheses -com.thealgorithms.strings.KMP=UselessMainMethod com.thealgorithms.strings.Lower=UselessMainMethod com.thealgorithms.strings.Palindrome=UselessParentheses com.thealgorithms.strings.Pangram=UselessMainMethod -com.thealgorithms.strings.RabinKarp=UselessMainMethod com.thealgorithms.strings.Rotation=UselessMainMethod com.thealgorithms.strings.Upper=UselessMainMethod diff --git a/pom.xml b/pom.xml index 7e936967208b..21dd79ad17b1 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ UTF-8 21 21 - 3.27.6 + 3.27.7 @@ -20,7 +20,7 @@ org.junit junit-bom - 6.0.1 + 6.1.3 pom import @@ -42,18 +42,18 @@ org.mockito mockito-core - 5.20.0 + 5.24.0 test org.apache.commons commons-lang3 - 3.19.0 + 3.21.0 org.apache.commons commons-collections4 - 4.5.0 + 4.6.0 @@ -61,7 +61,7 @@ maven-surefire-plugin - 3.5.4 + 3.6.0 @@ -69,7 +69,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.1 + 3.16.0 21 @@ -82,7 +82,7 @@ org.jacoco jacoco-maven-plugin - 0.8.14 + 0.8.15 @@ -112,14 +112,14 @@ com.puppycrawl.tools checkstyle - 12.1.1 + 14.3.0 com.github.spotbugs spotbugs-maven-plugin - 4.9.8.1 + 4.10.5.0 spotbugs-exclude.xml true @@ -127,7 +127,7 @@ com.mebigfatguy.fb-contrib fb-contrib - 7.6.15 + 7.7.4 com.h3xstream.findsecbugs diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index d2e094556d61..1c34e47fb651 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -8,18 +8,9 @@ - - - - - - - - - @@ -32,15 +23,9 @@ - - - - - - @@ -50,15 +35,9 @@ - - - - - - @@ -71,9 +50,6 @@ - - - @@ -84,7 +60,10 @@ - + + + + @@ -114,12 +93,6 @@ - - - - - - @@ -129,9 +102,6 @@ - - - @@ -150,9 +120,6 @@ - - - @@ -190,16 +157,19 @@ - + - + - + - + + + + diff --git a/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java b/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java index 0dd23e937953..4a9e954bd202 100644 --- a/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java +++ b/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java @@ -3,16 +3,19 @@ /** * Exponential Moving Average (EMA) Filter for smoothing audio signals. * - *

This filter applies an exponential moving average to a sequence of audio + *

+ * This filter applies an exponential moving average to a sequence of audio * signal values, making it useful for smoothing out rapid fluctuations. * The smoothing factor (alpha) controls the degree of smoothing. * - *

Based on the definition from + *

+ * Based on the definition from * Wikipedia link. */ public class EMAFilter { private final double alpha; private double emaValue; + /** * Constructs an EMA filter with a given smoothing factor. * @@ -26,14 +29,17 @@ public EMAFilter(double alpha) { this.alpha = alpha; this.emaValue = 0.0; } + /** * Applies the EMA filter to an audio signal array. + * EMA formula: + * EMA = alpha * currentSample + (1 - alpha) * previousEMA * * @param audioSignal Array of audio samples to process * @return Array of processed (smoothed) samples */ public double[] apply(double[] audioSignal) { - if (audioSignal.length == 0) { + if (audioSignal == null || audioSignal.length == 0) { return new double[0]; } double[] emaSignal = new double[audioSignal.length]; diff --git a/src/main/java/com/thealgorithms/backtracking/AllPathsFromSourceToTarget.java b/src/main/java/com/thealgorithms/backtracking/AllPathsFromSourceToTarget.java index c35a36d97a57..269880b8ddae 100644 --- a/src/main/java/com/thealgorithms/backtracking/AllPathsFromSourceToTarget.java +++ b/src/main/java/com/thealgorithms/backtracking/AllPathsFromSourceToTarget.java @@ -4,11 +4,36 @@ import java.util.List; /** - * Program description - To find all possible paths from source to destination - * Wikipedia + * Finds all possible simple paths from a given source vertex to a destination vertex + * in a directed graph using backtracking. * - * @author Siddhant Swarup Mallick + *

This algorithm performs a Depth First Search (DFS) traversal while keeping track + * of visited vertices to avoid cycles. Whenever the destination vertex is reached, + * the current path is stored as one valid path.

+ * + *

Key Characteristics:

+ *
    + *
  • Works on directed graphs
  • + *
  • Does not allow revisiting vertices in the same path
  • + *
  • Stores all possible paths from source to destination
  • + *
+ * + *

Time Complexity:

+ *
    + *
  • Worst Case: O(V!) β€” when the graph is fully connected
  • + *
+ * + *

Space Complexity:

+ *
    + *
  • O(V) for recursion stack and visited array
  • + *
  • Additional space for storing all valid paths
  • + *
+ * + *

This implementation is intended for educational purposes.

+ * + * @see Depth First Search */ + @SuppressWarnings({"rawtypes", "unchecked"}) public class AllPathsFromSourceToTarget { diff --git a/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java b/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java index f8cd0c40c20e..d05e33a4242f 100644 --- a/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java +++ b/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java @@ -20,8 +20,8 @@ private ArrayCombination() { * @throws IllegalArgumentException if n or k are negative, or if k is greater than n. */ public static List> combination(int n, int k) { - if (n < 0 || k < 0 || k > n) { - throw new IllegalArgumentException("Invalid input: n must be non-negative, k must be non-negative and less than or equal to n."); + if (k < 0 || k > n) { + throw new IllegalArgumentException("Invalid input: 0 ≀ k ≀ n is required."); } List> combinations = new ArrayList<>(); @@ -48,7 +48,7 @@ private static void combine(List> combinations, List curr for (int i = start; i < n; i++) { current.add(i); combine(combinations, current, i + 1, n, k); - current.remove(current.size() - 1); // Backtrack + current.removeLast(); // Backtrack } } } diff --git a/src/main/java/com/thealgorithms/backtracking/Combination.java b/src/main/java/com/thealgorithms/backtracking/Combination.java index ecaf7428f986..377d2c862d54 100644 --- a/src/main/java/com/thealgorithms/backtracking/Combination.java +++ b/src/main/java/com/thealgorithms/backtracking/Combination.java @@ -7,8 +7,7 @@ import java.util.TreeSet; /** - * Finds all permutations of given array - * @author Alan Piao (git-Alan Piao) + * Finds all combinations of a given array using backtracking algorithm * @author Alan Piao (git-Alan Piao) */ public final class Combination { private Combination() { diff --git a/src/main/java/com/thealgorithms/backtracking/NQueens.java b/src/main/java/com/thealgorithms/backtracking/NQueens.java index 1a8e453e34cb..404f677738a0 100644 --- a/src/main/java/com/thealgorithms/backtracking/NQueens.java +++ b/src/main/java/com/thealgorithms/backtracking/NQueens.java @@ -1,7 +1,9 @@ package com.thealgorithms.backtracking; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Problem statement: Given a N x N chess board. Return all arrangements in @@ -32,7 +34,22 @@ * queen is not placed safely. If there is no such way then return an empty list * as solution */ + +/* + * Time Complexity: O(N!) + * space Complexity: O(N) + */ public final class NQueens { + + // Store occupied rows for constant time safety check + private static final Set OCROWS = new HashSet<>(); + + // Store occupied main diagonals (row - column) + private static final Set OCDIAG = new HashSet<>(); + + // Store occupied anti-diagonals (row + columns) + private static final Set OCANTIDIAG = new HashSet<>(); + private NQueens() { } @@ -43,10 +60,10 @@ public static List> getNQueensArrangements(int queens) { } public static void placeQueens(final int queens) { - List> arrangements = new ArrayList>(); + List> arrangements = new ArrayList<>(); getSolution(queens, arrangements, new int[queens], 0); if (arrangements.isEmpty()) { - System.out.println("There is no way to place " + queens + " queens on board of size " + queens + "x" + queens); + System.out.println(" no way to place " + queens + " queens on board of size " + queens + "x" + queens); } else { System.out.println("Arrangement for placing " + queens + " queens"); } @@ -59,15 +76,15 @@ public static void placeQueens(final int queens) { /** * This is backtracking function which tries to place queen recursively * - * @param boardSize: size of chess board - * @param solutions: this holds all possible arrangements - * @param columns: columns[i] = rowId where queen is placed in ith column. + * @param boardSize: size of chess board + * @param solutions: this holds all possible arrangements + * @param columns: columns[i] = rowId where queen is placed in ith column. * @param columnIndex: This is the column in which queen is being placed */ private static void getSolution(int boardSize, List> solutions, int[] columns, int columnIndex) { if (columnIndex == boardSize) { // this means that all queens have been placed - List sol = new ArrayList(); + List sol = new ArrayList<>(); for (int i = 0; i < boardSize; i++) { StringBuilder sb = new StringBuilder(); for (int j = 0; j < boardSize; j++) { @@ -82,30 +99,29 @@ private static void getSolution(int boardSize, List> solutions, int // This loop tries to place queen in a row one by one for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) { columns[columnIndex] = rowIndex; - if (isPlacedCorrectly(columns, rowIndex, columnIndex)) { - // If queen is placed successfully at rowIndex in column=columnIndex then try - // placing queen in next column - getSolution(boardSize, solutions, columns, columnIndex + 1); - } - } - } - /** - * This function checks if queen can be placed at row = rowIndex in column = - * columnIndex safely - * - * @param columns: columns[i] = rowId where queen is placed in ith column. - * @param rowIndex: row in which queen has to be placed - * @param columnIndex: column in which queen is being placed - * @return true: if queen can be placed safely false: otherwise - */ - private static boolean isPlacedCorrectly(int[] columns, int rowIndex, int columnIndex) { - for (int i = 0; i < columnIndex; i++) { - int diff = Math.abs(columns[i] - rowIndex); - if (diff == 0 || columnIndex - i == diff) { - return false; + // Skip current position if row or diagonal is already occupied + boolean isROp = OCROWS.contains(rowIndex); + + boolean isDOp = OCDIAG.contains(rowIndex - columnIndex) || OCANTIDIAG.contains(rowIndex + columnIndex); + + if (isROp || isDOp) { + continue; } + + // Mark current row and diagonal as occupied + OCROWS.add(rowIndex); + OCDIAG.add(rowIndex - columnIndex); + OCANTIDIAG.add(rowIndex + columnIndex); + + // Move to the next column after placing current queen + getSolution(boardSize, solutions, columns, columnIndex + 1); + + // Backtrack by removing current queen + + OCROWS.remove(rowIndex); + OCDIAG.remove(rowIndex - columnIndex); + OCANTIDIAG.remove(rowIndex + columnIndex); } - return true; } } diff --git a/src/main/java/com/thealgorithms/backtracking/RatInAMaze.java b/src/main/java/com/thealgorithms/backtracking/RatInAMaze.java new file mode 100644 index 000000000000..183b4bbd97f8 --- /dev/null +++ b/src/main/java/com/thealgorithms/backtracking/RatInAMaze.java @@ -0,0 +1,119 @@ +package com.thealgorithms.backtracking; + +import java.util.ArrayList; +import java.util.List; + +/** + * Rat in a Maze Problem using Backtracking. + * + *

Given an {@code n x n} binary maze where {@code 1} represents an open cell + * and {@code 0} represents a blocked cell, find all paths for a rat starting at + * the top-left cell {@code (0, 0)} to reach the bottom-right cell {@code (n-1, n-1)}. + * + *

The rat can move in four directions: Up (U), Down (D), Left (L), Right (R). + * Each cell may be visited at most once per path. + * + *

Time Complexity: O(4^(nΒ²)) in the worst case (four choices per cell). + * Space Complexity: O(nΒ²) for the visited matrix and recursion stack. + * + *

Example: + *

+ *   maze = { {1, 0, 0, 0},
+ *            {1, 1, 0, 1},
+ *            {0, 1, 0, 0},
+ *            {0, 1, 1, 1} }
+ *   Output: ["DDRDRR", "DRDDRR"]  (two valid paths)
+ * 
+ * + * @see Maze solving algorithm + * @author the-Sunny-Sharma (GitHub) + */ +public final class RatInAMaze { + + private RatInAMaze() { + } + + /** + * Finds all paths from the top-left to the bottom-right of the given maze. + * + * @param maze an {@code n x n} binary matrix where {@code 1} = open, {@code 0} = blocked + * @return a sorted list of all valid path strings using directions D, L, R, U; + * an empty list if no path exists + * @throws IllegalArgumentException if the maze is null, empty, or not square + */ + public static List findPaths(final int[][] maze) { + if (maze == null || maze.length == 0) { + throw new IllegalArgumentException("Maze must not be null or empty."); + } + int n = maze.length; + for (int[] row : maze) { + if (row.length != n) { + throw new IllegalArgumentException("Maze must be a square (n x n) matrix."); + } + } + List results = new ArrayList<>(); + if (maze[0][0] == 0 || maze[n - 1][n - 1] == 0) { + return results; + } + boolean[][] visited = new boolean[n][n]; + solve(maze, 0, 0, n, "", visited, results); + return results; + } + + /** + * Recursive backtracking helper that explores all four directions. + * + * @param maze the binary maze + * @param row current row position + * @param col current column position + * @param n maze dimension + * @param path path string built so far + * @param visited tracks visited cells for the current path + * @param results accumulates complete paths + */ + private static void solve(final int[][] maze, final int row, final int col, final int n, final String path, final boolean[][] visited, final List results) { + // Base case: reached destination + if (row == n - 1 && col == n - 1) { + results.add(path); + return; + } + + // Mark current cell as visited + visited[row][col] = true; + + // Explore in alphabetical order: Down, Left, Right, Up + // Down + if (isSafe(maze, row + 1, col, n, visited)) { + solve(maze, row + 1, col, n, path + 'D', visited, results); + } + // Left + if (isSafe(maze, row, col - 1, n, visited)) { + solve(maze, row, col - 1, n, path + 'L', visited, results); + } + // Right + if (isSafe(maze, row, col + 1, n, visited)) { + solve(maze, row, col + 1, n, path + 'R', visited, results); + } + // Up + if (isSafe(maze, row - 1, col, n, visited)) { + solve(maze, row - 1, col, n, path + 'U', visited, results); + } + + // Backtrack: unmark current cell + visited[row][col] = false; + } + + /** + * Checks whether moving to {@code (row, col)} is valid. + * + * @param maze the binary maze + * @param row target row + * @param col target column + * @param n maze dimension + * @param visited tracks visited cells for the current path + * @return {@code true} if the cell is within bounds, open, and not yet visited + */ + private static boolean isSafe(final int[][] maze, final int row, final int col, final int n, final boolean[][] visited) { + return row >= 0 && row < n && col >= 0 && col < n && maze[row][col] == 1 && !visited[row][col]; + } +} diff --git a/src/main/java/com/thealgorithms/backtracking/SudokuSolver.java b/src/main/java/com/thealgorithms/backtracking/SudokuSolver.java new file mode 100644 index 000000000000..543fe2d02b50 --- /dev/null +++ b/src/main/java/com/thealgorithms/backtracking/SudokuSolver.java @@ -0,0 +1,157 @@ +package com.thealgorithms.backtracking; + +/** + * Sudoku Solver using Backtracking Algorithm + * Solves a 9x9 Sudoku puzzle by filling empty cells with valid digits (1-9) + * + * @author Navadeep0007 + */ +public final class SudokuSolver { + + private static final int GRID_SIZE = 9; + private static final int SUBGRID_SIZE = 3; + private static final int EMPTY_CELL = 0; + + private SudokuSolver() { + // Utility class, prevent instantiation + } + + /** + * Solves the Sudoku puzzle using backtracking + * + * @param board 9x9 Sudoku board with 0 representing empty cells + * @return true if puzzle is solved, false otherwise + */ + public static boolean solveSudoku(int[][] board) { + if (board == null || board.length != GRID_SIZE) { + return false; + } + + for (int row = 0; row < GRID_SIZE; row++) { + if (board[row].length != GRID_SIZE) { + return false; + } + } + + return solve(board); + } + + /** + * Recursive helper method to solve the Sudoku puzzle + * + * @param board the Sudoku board + * @return true if solution is found, false otherwise + */ + private static boolean solve(int[][] board) { + for (int row = 0; row < GRID_SIZE; row++) { + for (int col = 0; col < GRID_SIZE; col++) { + if (board[row][col] == EMPTY_CELL) { + for (int number = 1; number <= GRID_SIZE; number++) { + if (isValidPlacement(board, row, col, number)) { + board[row][col] = number; + + if (solve(board)) { + return true; + } + + // Backtrack + board[row][col] = EMPTY_CELL; + } + } + return false; + } + } + } + return true; + } + + /** + * Checks if placing a number at given position is valid + * + * @param board the Sudoku board + * @param row row index + * @param col column index + * @param number number to place (1-9) + * @return true if placement is valid, false otherwise + */ + private static boolean isValidPlacement(int[][] board, int row, int col, int number) { + return !isNumberInRow(board, row, number) && !isNumberInColumn(board, col, number) && !isNumberInSubgrid(board, row, col, number); + } + + /** + * Checks if number exists in the given row + * + * @param board the Sudoku board + * @param row row index + * @param number number to check + * @return true if number exists in row, false otherwise + */ + private static boolean isNumberInRow(int[][] board, int row, int number) { + for (int col = 0; col < GRID_SIZE; col++) { + if (board[row][col] == number) { + return true; + } + } + return false; + } + + /** + * Checks if number exists in the given column + * + * @param board the Sudoku board + * @param col column index + * @param number number to check + * @return true if number exists in column, false otherwise + */ + private static boolean isNumberInColumn(int[][] board, int col, int number) { + for (int row = 0; row < GRID_SIZE; row++) { + if (board[row][col] == number) { + return true; + } + } + return false; + } + + /** + * Checks if number exists in the 3x3 subgrid + * + * @param board the Sudoku board + * @param row row index + * @param col column index + * @param number number to check + * @return true if number exists in subgrid, false otherwise + */ + private static boolean isNumberInSubgrid(int[][] board, int row, int col, int number) { + int subgridRowStart = row - row % SUBGRID_SIZE; + int subgridColStart = col - col % SUBGRID_SIZE; + + for (int i = subgridRowStart; i < subgridRowStart + SUBGRID_SIZE; i++) { + for (int j = subgridColStart; j < subgridColStart + SUBGRID_SIZE; j++) { + if (board[i][j] == number) { + return true; + } + } + } + return false; + } + + /** + * Prints the Sudoku board + * + * @param board the Sudoku board + */ + public static void printBoard(int[][] board) { + for (int row = 0; row < GRID_SIZE; row++) { + if (row % SUBGRID_SIZE == 0 && row != 0) { + System.out.println("-----------"); + } + for (int col = 0; col < GRID_SIZE; col++) { + if (col % SUBGRID_SIZE == 0 && col != 0) { + System.out.print("|"); + } + System.out.print(board[row][col]); + } + System.out.println(); + } + } +} diff --git a/src/main/java/com/thealgorithms/backtracking/UniquePermutation.java b/src/main/java/com/thealgorithms/backtracking/UniquePermutation.java new file mode 100644 index 000000000000..4804e247ab03 --- /dev/null +++ b/src/main/java/com/thealgorithms/backtracking/UniquePermutation.java @@ -0,0 +1,62 @@ +package com.thealgorithms.backtracking; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Generates all UNIQUE permutations of a string, even when duplicate characters exist. + * + * Example: + * Input: "AAB" + * Output: ["AAB", "ABA", "BAA"] + * + * Time Complexity: O(n! * n) + */ +public final class UniquePermutation { + + private UniquePermutation() { + // Prevent instantiation + throw new UnsupportedOperationException("Utility class"); + } + + public static List generateUniquePermutations(String input) { + List result = new ArrayList<>(); + if (input == null) { + return result; + } + + char[] chars = input.toCharArray(); + Arrays.sort(chars); // important: sort to detect duplicates + + backtrack(chars, new boolean[chars.length], new StringBuilder(), result); + return result; + } + + private static void backtrack(char[] chars, boolean[] used, StringBuilder current, List result) { + + if (current.length() == chars.length) { + result.add(current.toString()); + return; + } + + for (int i = 0; i < chars.length; i++) { + + // skip duplicates + if (i > 0 && chars[i] == chars[i - 1] && !used[i - 1]) { + continue; + } + + if (!used[i]) { + used[i] = true; + current.append(chars[i]); + + backtrack(chars, used, current, result); + + // undo changes + used[i] = false; + current.deleteCharAt(current.length() - 1); + } + } + } +} diff --git a/src/main/java/com/thealgorithms/backtracking/WordSearch.java b/src/main/java/com/thealgorithms/backtracking/WordSearch.java index 174ca90ccaab..452f17b6ace6 100644 --- a/src/main/java/com/thealgorithms/backtracking/WordSearch.java +++ b/src/main/java/com/thealgorithms/backtracking/WordSearch.java @@ -35,22 +35,6 @@ * - Stack space for the recursive DFS function, where L is the maximum depth of recursion (length of the word). */ public class WordSearch { - private final int[] dx = {0, 0, 1, -1}; - private final int[] dy = {1, -1, 0, 0}; - private boolean[][] visited; - private char[][] board; - private String word; - - /** - * Checks if the given (x, y) coordinates are valid positions in the board. - * - * @param x The row index. - * @param y The column index. - * @return True if the coordinates are within the bounds of the board; false otherwise. - */ - private boolean isValid(int x, int y) { - return x >= 0 && x < board.length && y >= 0 && y < board[0].length; - } /** * Performs Depth First Search (DFS) from the cell (x, y) @@ -58,28 +42,27 @@ private boolean isValid(int x, int y) { * * @param x The current row index. * @param y The current column index. - * @param nextIdx The index of the next character in the word to be matched. + * @param idx The index of the next character in the word to be matched. * @return True if a valid path is found to match the remaining characters of the word; false otherwise. */ - private boolean doDFS(int x, int y, int nextIdx) { - visited[x][y] = true; - if (nextIdx == word.length()) { + + private boolean dfs(char[][] board, int x, int y, String word, int idx) { + if (idx == word.length()) { return true; } - for (int i = 0; i < 4; ++i) { - int xi = x + dx[i]; - int yi = y + dy[i]; - if (isValid(xi, yi) && board[xi][yi] == word.charAt(nextIdx) && !visited[xi][yi]) { - boolean exists = doDFS(xi, yi, nextIdx + 1); - if (exists) { - return true; - } - } + if (x < 0 || y < 0 || x >= board.length || y >= board[0].length || board[x][y] != word.charAt(idx)) { + return false; } - visited[x][y] = false; // Backtrack - return false; + char temp = board[x][y]; + board[x][y] = '#'; + + boolean found = dfs(board, x + 1, y, word, idx + 1) || dfs(board, x - 1, y, word, idx + 1) || dfs(board, x, y + 1, word, idx + 1) || dfs(board, x, y - 1, word, idx + 1); + + board[x][y] = temp; + + return found; } /** @@ -90,20 +73,21 @@ private boolean doDFS(int x, int y, int nextIdx) { * @param word The target word to search for in the board. * @return True if the word exists in the board; false otherwise. */ + public boolean exist(char[][] board, String word) { - this.board = board; - this.word = word; - for (int i = 0; i < board.length; ++i) { - for (int j = 0; j < board[0].length; ++j) { - if (board[i][j] == word.charAt(0)) { - visited = new boolean[board.length][board[0].length]; - boolean exists = doDFS(i, j, 1); - if (exists) { - return true; - } + + int m = board.length; + int n = board[0].length; + + // DFS search + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == word.charAt(0) && dfs(board, i, j, word, 0)) { + return true; } } } + return false; } } diff --git a/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java b/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java index 0d6fd140c720..5038d44079ec 100644 --- a/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java +++ b/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java @@ -9,6 +9,12 @@ *

* * @author Hardvan + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ public final class BinaryPalindromeCheck { private BinaryPalindromeCheck() { diff --git a/src/main/java/com/thealgorithms/bitmanipulation/CountSetBits.java b/src/main/java/com/thealgorithms/bitmanipulation/CountSetBits.java index 242f35fc35f2..7df522ca8f69 100644 --- a/src/main/java/com/thealgorithms/bitmanipulation/CountSetBits.java +++ b/src/main/java/com/thealgorithms/bitmanipulation/CountSetBits.java @@ -1,79 +1,79 @@ package com.thealgorithms.bitmanipulation; -public class CountSetBits { +/** + * Utility class to count total set bits from 1 to N + * A set bit is a bit in binary representation that is 1 + * + * @author navadeep + */ +public final class CountSetBits { + + private CountSetBits() { + // Utility class, prevent instantiation + } /** - * The below algorithm is called as Brian Kernighan's algorithm - * We can use Brian Kernighan’s algorithm to improve the above naive algorithm’s performance. - The idea is to only consider the set bits of an integer by turning off its rightmost set bit - (after counting it), so the next iteration of the loop considers the next rightmost bit. - - The expression n & (n-1) can be used to turn off the rightmost set bit of a number n. This - works as the expression n-1 flips all the bits after the rightmost set bit of n, including the - rightmost set bit itself. Therefore, n & (n-1) results in the last bit flipped of n. - - For example, consider number 52, which is 00110100 in binary, and has a total 3 bits set. - - 1st iteration of the loop: n = 52 - - 00110100 & (n) - 00110011 (n-1) - ~~~~~~~~ - 00110000 + * Counts total number of set bits in all numbers from 1 to n + * Time Complexity: O(log n) + * + * @param n the upper limit (inclusive) + * @return total count of set bits from 1 to n + * @throws IllegalArgumentException if n is negative + */ + public static int countSetBits(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative"); + } + if (n == 0) { + return 0; + } - 2nd iteration of the loop: n = 48 + // Find the largest power of 2 <= n + int x = largestPowerOf2InNumber(n); - 00110000 & (n) - 00101111 (n-1) - ~~~~~~~~ - 00100000 + // Total bits at position x: x * 2^(x-1) + int bitsAtPositionX = x * (1 << (x - 1)); + // Remaining numbers after 2^x + int remainingNumbers = n - (1 << x) + 1; - 3rd iteration of the loop: n = 32 + // Recursively count for the rest + int rest = countSetBits(n - (1 << x)); - 00100000 & (n) - 00011111 (n-1) - ~~~~~~~~ - 00000000 (n = 0) + return bitsAtPositionX + remainingNumbers + rest; + } - * @param num takes Long number whose number of set bit is to be found - * @return the count of set bits in the binary equivalent - */ - public long countSetBits(long num) { - long cnt = 0; - while (num > 0) { - cnt++; - num &= (num - 1); + /** + * Finds the position of the most significant bit in n + * + * @param n the number + * @return position of MSB (0-indexed from right) + */ + private static int largestPowerOf2InNumber(int n) { + int position = 0; + while ((1 << position) <= n) { + position++; } - return cnt; + return position - 1; } /** - * This approach takes O(1) running time to count the set bits, but requires a pre-processing. + * Alternative naive approach - counts set bits by iterating through all numbers + * Time Complexity: O(n log n) * - * So, we divide our 32-bit input into 8-bit chunks, with four chunks. We have 8 bits in each chunk. - * - * Then the range is from 0-255 (0 to 2^7). - * So, we may need to count set bits from 0 to 255 in individual chunks. - * - * @param num takes a long number - * @return the count of set bits in the binary equivalent + * @param n the upper limit (inclusive) + * @return total count of set bits from 1 to n */ - public int lookupApproach(int num) { - int[] table = new int[256]; - table[0] = 0; - - for (int i = 1; i < 256; i++) { - table[i] = (i & 1) + table[i >> 1]; // i >> 1 equals to i/2 + public static int countSetBitsNaive(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative"); } - int res = 0; - for (int i = 0; i < 4; i++) { - res += table[num & 0xff]; - num >>= 8; + int count = 0; + for (int i = 1; i <= n; i++) { + count += Integer.bitCount(i); } - - return res; + return count; } } diff --git a/src/main/java/com/thealgorithms/ciphers/AESEncryption.java b/src/main/java/com/thealgorithms/ciphers/AESEncryption.java index 14582205442f..6f155e73f47d 100644 --- a/src/main/java/com/thealgorithms/ciphers/AESEncryption.java +++ b/src/main/java/com/thealgorithms/ciphers/AESEncryption.java @@ -38,7 +38,7 @@ public static void main(String[] args) throws Exception { System.out.println("Original Text:" + plainText); System.out.println("AES Key (Hex Form):" + bytesToHex(secKey.getEncoded())); System.out.println("Encrypted Text (Hex Form):" + bytesToHex(cipherText)); - System.out.println("Descrypted Text:" + decryptedText); + System.out.println("Decryption successful. Decrypted text matches original: " + decryptedText.equals(plainText)); } /** diff --git a/src/main/java/com/thealgorithms/ciphers/Caesar.java b/src/main/java/com/thealgorithms/ciphers/Caesar.java index 23535bc2b5d2..7a5e70e6eb78 100644 --- a/src/main/java/com/thealgorithms/ciphers/Caesar.java +++ b/src/main/java/com/thealgorithms/ciphers/Caesar.java @@ -9,8 +9,8 @@ * @author khalil2535 */ public class Caesar { - private static char normalizeShift(final int shift) { - return (char) (shift % 26); + private static int normalizeShift(final int shift) { + return ((shift % 26) + 26) % 26; } /** @@ -22,21 +22,18 @@ private static char normalizeShift(final int shift) { public String encode(String message, int shift) { StringBuilder encoded = new StringBuilder(); - final char shiftChar = normalizeShift(shift); + final int shiftChar = normalizeShift(shift); final int length = message.length(); for (int i = 0; i < length; i++) { - // int current = message.charAt(i); //using char to shift characters because - // ascii - // is in-order latin alphabet - char current = message.charAt(i); // Java law : char + int = char + final char current = message.charAt(i); if (isCapitalLatinLetter(current)) { - current += shiftChar; - encoded.append((char) (current > 'Z' ? current - 26 : current)); // 26 = number of latin letters + final int shifted = current + shiftChar; + encoded.append((char) (shifted > 'Z' ? shifted - 26 : shifted)); // 26 = number of latin letters } else if (isSmallLatinLetter(current)) { - current += shiftChar; - encoded.append((char) (current > 'z' ? current - 26 : current)); // 26 = number of latin letters + final int shifted = current + shiftChar; + encoded.append((char) (shifted > 'z' ? shifted - 26 : shifted)); // 26 = number of latin letters } else { encoded.append(current); } @@ -53,17 +50,17 @@ public String encode(String message, int shift) { public String decode(String encryptedMessage, int shift) { StringBuilder decoded = new StringBuilder(); - final char shiftChar = normalizeShift(shift); + final int shiftChar = normalizeShift(shift); final int length = encryptedMessage.length(); for (int i = 0; i < length; i++) { - char current = encryptedMessage.charAt(i); + final char current = encryptedMessage.charAt(i); if (isCapitalLatinLetter(current)) { - current -= shiftChar; - decoded.append((char) (current < 'A' ? current + 26 : current)); // 26 = number of latin letters + final int shifted = current - shiftChar; + decoded.append((char) (shifted < 'A' ? shifted + 26 : shifted)); // 26 = number of latin letters } else if (isSmallLatinLetter(current)) { - current -= shiftChar; - decoded.append((char) (current < 'a' ? current + 26 : current)); // 26 = number of latin letters + final int shifted = current - shiftChar; + decoded.append((char) (shifted < 'a' ? shifted + 26 : shifted)); // 26 = number of latin letters } else { decoded.append(current); } diff --git a/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java new file mode 100644 index 000000000000..6383caa59b1f --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java @@ -0,0 +1,174 @@ +package com.thealgorithms.ciphers; + +import java.math.BigInteger; +import java.security.SecureRandom; + +/** + * ElGamal Encryption Algorithm Implementation. + * + *

+ * ElGamal is an asymmetric key encryption algorithm for public-key cryptography + * based on the Diffie–Hellman key exchange. It relies on the difficulty + * of computing discrete logarithms in a cyclic group. + *

+ * + *

+ * Key Features: + *

    + *
  • Uses Safe Primes (p = 2q + 1) to ensure group security.
  • + *
  • Verifies the generator is a primitive root modulo p.
  • + *
  • Stateless design using Java Records.
  • + *
  • SecureRandom for all cryptographic operations.
  • + *
+ *

+ * + * @author Chahat Sandhu, singhc7 + * @see ElGamal Encryption (Wikipedia) + * @see Safe Primes + */ +public final class ElGamalCipher { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int PRIME_CERTAINTY = 40; + private static final int MIN_BIT_LENGTH = 256; + + private ElGamalCipher() { + } + + /** + * A container for the Public and Private keys. + * + * @param p The prime modulus. + * @param g The generator (primitive root). + * @param y The public key component (g^x mod p). + * @param x The private key. + */ + public record KeyPair(BigInteger p, BigInteger g, BigInteger y, BigInteger x) { + } + + /** + * Container for the encryption result. + * + * @param a The first component (g^k mod p). + * @param b The second component (y^k * m mod p). + */ + public record CipherText(BigInteger a, BigInteger b) { + } + + /** + * Generates a valid ElGamal KeyPair using a Safe Prime. + * + * @param bitLength The bit length of the prime modulus p. Must be at least 256. + * @return A valid KeyPair (p, g, y, x). + * @throws IllegalArgumentException if bitLength is too small. + */ + public static KeyPair generateKeys(int bitLength) { + if (bitLength < MIN_BIT_LENGTH) { + throw new IllegalArgumentException("Bit length must be at least " + MIN_BIT_LENGTH + " for security."); + } + + BigInteger p; + BigInteger q; + BigInteger g; + BigInteger x; + BigInteger y; + + // Generate Safe Prime p = 2q + 1 + do { + q = new BigInteger(bitLength - 1, PRIME_CERTAINTY, RANDOM); + p = q.multiply(BigInteger.TWO).add(BigInteger.ONE); + } while (!p.isProbablePrime(PRIME_CERTAINTY)); + + // Find a Generator g (Primitive Root modulo p) + do { + g = new BigInteger(bitLength, RANDOM).mod(p.subtract(BigInteger.TWO)).add(BigInteger.TWO); + } while (!isValidGenerator(g, p, q)); + + // Generate Private Key x in range [2, p-2] + do { + x = new BigInteger(bitLength, RANDOM); + } while (x.compareTo(BigInteger.TWO) < 0 || x.compareTo(p.subtract(BigInteger.TWO)) > 0); + + // Compute Public Key y = g^x mod p + y = g.modPow(x, p); + + return new KeyPair(p, g, y, x); + } + + /** + * Encrypts a message using the public key. + * + * @param message The message converted to BigInteger. + * @param p The prime modulus. + * @param g The generator. + * @param y The public key component. + * @return The CipherText pair (a, b). + * @throws IllegalArgumentException if inputs are null, negative, or message >= p. + */ + public static CipherText encrypt(BigInteger message, BigInteger p, BigInteger g, BigInteger y) { + if (message == null || p == null || g == null || y == null) { + throw new IllegalArgumentException("Inputs cannot be null."); + } + if (message.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Message must be non-negative."); + } + if (message.compareTo(p) >= 0) { + throw new IllegalArgumentException("Message must be smaller than the prime modulus p."); + } + + BigInteger k; + BigInteger pMinus1 = p.subtract(BigInteger.ONE); + + // Select ephemeral key k such that 1 < k < p-1 and gcd(k, p-1) = 1 + do { + k = new BigInteger(p.bitLength(), RANDOM); + } while (k.compareTo(BigInteger.ONE) <= 0 || k.compareTo(pMinus1) >= 0 || !k.gcd(pMinus1).equals(BigInteger.ONE)); + + BigInteger a = g.modPow(k, p); + BigInteger b = y.modPow(k, p).multiply(message).mod(p); + + return new CipherText(a, b); + } + + /** + * Decrypts a ciphertext using the private key. + * + * @param cipher The CipherText (a, b). + * @param x The private key. + * @param p The prime modulus. + * @return The decrypted message as BigInteger. + * @throws IllegalArgumentException if inputs are null. + */ + public static BigInteger decrypt(CipherText cipher, BigInteger x, BigInteger p) { + if (cipher == null || x == null || p == null) { + throw new IllegalArgumentException("Inputs cannot be null."); + } + + BigInteger a = cipher.a(); + BigInteger b = cipher.b(); + + BigInteger s = a.modPow(x, p); + BigInteger sInverse = s.modInverse(p); + + return b.multiply(sInverse).mod(p); + } + + /** + * Verifies if g is a valid generator for safe prime p = 2q + 1. + * + * @param g The candidate generator. + * @param p The safe prime. + * @param q The Sophie Germain prime (p-1)/2. + * @return True if g is a primitive root, False otherwise. + */ + private static boolean isValidGenerator(BigInteger g, BigInteger p, BigInteger q) { + // Fix: Must use braces {} for all if statements + if (g.equals(BigInteger.ONE)) { + return false; + } + if (g.modPow(BigInteger.TWO, p).equals(BigInteger.ONE)) { + return false; + } + return !g.modPow(q, p).equals(BigInteger.ONE); + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/HorsSignature.java b/src/main/java/com/thealgorithms/ciphers/HorsSignature.java new file mode 100644 index 000000000000..64731cde76d2 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/HorsSignature.java @@ -0,0 +1,211 @@ +package com.thealgorithms.ciphers; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * HORS (Hash to Obtain Random Subset) is a hash-based few-time signature scheme by Reyzin and + * Reyzin (2002). + * + *

The private key consists of {@code t = 2^Ο„} random 32-byte secrets and the public key contains + * their SHA-256 hashes. To sign, the SHA-256 digest of the message is split into {@code k} chunks of + * {@code Ο„} bits (most significant bit first); each chunk is an index into the key, and the secrets + * at those {@code k} indices form the signature. The verifier derives the same indices and checks + * that each revealed secret hashes to the matching public-key value. The same index may appear more + * than once, as in the original scheme. Signing is deterministic. + * + *

HORS is a few-time scheme: each signature reveals {@code k} secrets, so after {@code r} + * signatures an attacker knows at most {@code rΒ·k} of them. If all indices of another message fall + * into that set, its signature can be forged, so security decreases with every signature. + * + *

HORST compresses the large public key with a Merkle tree. FORS, used in SPHINCS+ and SLH-DSA + * (FIPS 205), uses a forest of trees instead, which also removes the repeated-index weakness. This + * implementation is educational and must not be used in production. + * + *

References: Reyzin and Reyzin, Better than BiBa, + * Wikipedia: Hash-based cryptography + * + * @author dilaraacetin + * @see LamportSignature + * @see WinternitzSignature + * @see MerkleSignatureScheme + */ +public final class HorsSignature { + + // SHA-256 output length in bytes + private static final int HASH_BYTES = 32; + private static final int DIGEST_BITS = 256; + private static final int MIN_T = 16; + private static final int MAX_T = 65536; + private static final int DEFAULT_T = 1024; + private static final int DEFAULT_K = 16; + + private final int k; + private final int tau; + private final byte[][] privateKey; + private final byte[][] publicKey; + + /** + * Generates a new key pair with {@code t = 1024} and {@code k = 16}. + */ + public HorsSignature() { + this(DEFAULT_T, DEFAULT_K); + } + + /** + * Generates a new key pair. + * + * @param t the number of secrets; a power of two between 16 and 65536 + * @param k the number of secrets revealed per signature; {@code kΒ·log2(t)} must not exceed 256 + * @throws IllegalArgumentException if {@code t} or {@code k} is not supported + */ + public HorsSignature(int t, int k) { + this.tau = tauOf(t); + validateK(k, tau); + this.k = k; + SecureRandom secureRandom = new SecureRandom(); + privateKey = new byte[t][HASH_BYTES]; + publicKey = new byte[t][]; + for (int i = 0; i < t; i++) { + secureRandom.nextBytes(privateKey[i]); + publicKey[i] = hash(privateKey[i]); + } + } + + /** + * Returns a copy of the public key. + * + * @return {@code t} hash values, each 32 bytes long + */ + public byte[][] getPublicKey() { + return deepCopy(publicKey); + } + + /** + * Returns the number of secrets revealed per signature. Like {@code t}, it is part of the public + * key and is needed for verification. + * + * @return the parameter {@code k} + */ + public int getK() { + return k; + } + + /** + * Signs a message. Every signature reveals {@code k} secrets, so a key pair should only sign a + * few messages. + * + * @param message the message to sign + * @return the signature: {@code k} secrets of 32 bytes each + * @throws IllegalArgumentException if the message is null + */ + public byte[][] sign(byte[] message) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + int[] indices = messageIndices(hash(message), k, tau); + byte[][] signature = new byte[k][]; + for (int j = 0; j < k; j++) { + signature[j] = privateKey[indices[j]].clone(); + } + return signature; + } + + /** + * Verifies a signature against a public key. {@code t} is taken from the public key length. + * {@code k} is part of the public key and must be supplied by the verifier: taking it from the + * signature would let an attacker submit a shorter signature that reveals fewer secrets. + * + * @param message the signed message + * @param signature the signature to check + * @param publicKey the public key of the signer + * @param k the number of secrets per signature used by the signer (see {@link #getK()}) + * @return true if the signature is valid for the message and public key, false otherwise + * @throws IllegalArgumentException if an argument is null, the public key length is not a + * supported {@code t}, {@code k} is not supported for this {@code t}, a value is not 32 + * bytes long, or the signature does not contain exactly {@code k} values + */ + public static boolean verify(byte[] message, byte[][] signature, byte[][] publicKey, int k) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + validateValues(publicKey, "publicKey"); + validateValues(signature, "signature"); + int tau = tauOf(publicKey.length); + validateK(k, tau); + if (signature.length != k) { + throw new IllegalArgumentException("signature must contain exactly " + k + " values, got " + signature.length); + } + + int[] indices = messageIndices(hash(message), k, tau); + for (int j = 0; j < k; j++) { + if (!MessageDigest.isEqual(hash(signature[j]), publicKey[indices[j]])) { + return false; + } + } + return true; + } + + /** + * Splits the first {@code kΒ·Ο„} bits of a digest into {@code k} unsigned {@code Ο„}-bit indices, + * most significant bit first. Shared by sign and verify. + */ + static int[] messageIndices(byte[] digest, int k, int tau) { + if ((long) k * tau > digest.length * 8L) { + throw new IllegalArgumentException("digest is too short for " + k + " indices of " + tau + " bits"); + } + int[] indices = new int[k]; + for (int j = 0; j < k; j++) { + int index = 0; + for (int i = 0; i < tau; i++) { + int bit = j * tau + i; + // bit 0 is the most significant bit of digest[0]; & 0xFF reads the byte as unsigned + index = (index << 1) | (((digest[bit / 8] & 0xFF) >> (7 - bit % 8)) & 1); + } + indices[j] = index; + } + return indices; + } + + private static int tauOf(int t) { + if (t < MIN_T || t > MAX_T || (t & (t - 1)) != 0) { + throw new IllegalArgumentException("t must be a power of two between " + MIN_T + " and " + MAX_T + ", got " + t); + } + return Integer.numberOfTrailingZeros(t); + } + + private static void validateK(int k, int tau) { + // k > DIGEST_BITS / tau is the same as k * tau > DIGEST_BITS, but cannot overflow + if (k < 1 || k > DIGEST_BITS / tau) { + throw new IllegalArgumentException("k must be at least 1 and k * log2(t) at most " + DIGEST_BITS + ", got k = " + k); + } + } + + private static void validateValues(byte[][] values, String name) { + if (values == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + for (byte[] value : values) { + if (value == null || value.length != HASH_BYTES) { + throw new IllegalArgumentException(name + " values must be exactly " + HASH_BYTES + " bytes long"); + } + } + } + + private static byte[] hash(byte[] data) { + try { + return MessageDigest.getInstance("SHA-256").digest(data); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 is required by the Java SE specification", e); + } + } + + private static byte[][] deepCopy(byte[][] values) { + byte[][] copy = new byte[values.length][]; + for (int i = 0; i < values.length; i++) { + copy[i] = values[i].clone(); + } + return copy; + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/LWEEncryption.java b/src/main/java/com/thealgorithms/ciphers/LWEEncryption.java new file mode 100644 index 000000000000..bf552c605af0 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/LWEEncryption.java @@ -0,0 +1,216 @@ +package com.thealgorithms.ciphers; + +import java.security.SecureRandom; + +/** + * Public-key encryption based on the Learning With Errors (LWE) problem, as proposed by Regev (2005). + * + *

Toy parameters for educational purposes only β€” NOT secure for real-world use. + * + *

The LWE problem: given many noisy samples {@code b = AΒ·s + e (mod q)} with a random matrix + * {@code A} and small random errors {@code e}, it is hard to recover the secret vector {@code s}. + * Without the errors, {@code s} could be found with Gaussian elimination. + * + *

The public key is {@code (A, b)} and the private key is {@code s}. To encrypt a bit, a random + * subset {@code r} of the samples is summed: {@code u = rα΅€Β·A} and {@code v = rΒ·b + bit·⌊q/2βŒ‹}. + * Decryption computes {@code v - uΒ·s = rΒ·e + bit·⌊q/2βŒ‹}: a value close to 0 means bit 0, a value + * close to {@code q/2} means bit 1. + * + *

Decryption is always correct because the remaining noise is bounded: {@code r} is a 0/1 vector + * and {@code |e_i| ≀ B}, so {@code |rΒ·e| ≀ mΒ·B = 384 < q/4 β‰ˆ 832}. + * + *

The modulus {@code q = 3329} is the one used by Kyber / ML-KEM (FIPS 203), which uses the + * module-lattice version of this construction with discrete Gaussian-like noise and much larger + * parameters. For simplicity, one instance holds both the public and the private key; in real use + * they are kept separately. + * + *

Reference: Wikipedia: Learning with errors + * + * @author dilaraacetin + */ +public final class LWEEncryption { + + // dimension of the secret vector + private static final int N = 32; + // modulus, same as Kyber / ML-KEM + private static final int Q = 3329; + // number of samples in the public key + private static final int M = 128; + // errors are uniform in [-B, B] + private static final int B = 3; + private static final int BITS_PER_BYTE = 8; + + private final SecureRandom random = new SecureRandom(); + private final int[][] a; + private final int[] b; + private final int[] s; + + /** + * Generates a new key pair. + * + * @throws IllegalStateException if the parameters do not guarantee correct decryption + */ + public LWEEncryption() { + checkDecryptionBound(M, B, Q); + s = new int[N]; + for (int j = 0; j < N; j++) { + s[j] = random.nextInt(Q); + } + a = new int[M][N]; + b = new int[M]; + for (int i = 0; i < M; i++) { + long sum = 0; + for (int j = 0; j < N; j++) { + a[i][j] = random.nextInt(Q); + sum += (long) a[i][j] * s[j]; + } + int error = random.nextInt(2 * B + 1) - B; + // b_i = A_iΒ·s + e_i (mod q) + b[i] = (int) Math.floorMod(sum + error, (long) Q); + } + } + + /** + * Encrypts a single bit. Encryption is randomized, so the same bit gives different ciphertexts. + * + * @param bit the bit to encrypt, 0 or 1 + * @return the ciphertext {@code (u, v)} + * @throws IllegalArgumentException if {@code bit} is not 0 or 1 + */ + public Ciphertext encryptBit(int bit) { + if (bit != 0 && bit != 1) { + throw new IllegalArgumentException("bit must be 0 or 1, got " + bit); + } + long[] u = new long[N]; + long v = (long) bit * (Q / 2); + for (int i = 0; i < M; i++) { + // r_i is 0 or 1: add sample i or skip it + if (random.nextBoolean()) { + for (int j = 0; j < N; j++) { + u[j] += a[i][j]; + } + v += b[i]; + } + } + int[] reduced = new int[N]; + for (int j = 0; j < N; j++) { + reduced[j] = (int) Math.floorMod(u[j], (long) Q); + } + return new Ciphertext(reduced, (int) Math.floorMod(v, (long) Q)); + } + + /** + * Decrypts a single bit. + * + * @param ciphertext the ciphertext to decrypt + * @return the decrypted bit, 0 or 1 + * @throws IllegalArgumentException if the ciphertext is null + */ + public int decryptBit(Ciphertext ciphertext) { + if (ciphertext == null) { + throw new IllegalArgumentException("ciphertext must not be null"); + } + long dot = 0; + for (int j = 0; j < N; j++) { + dot += (long) ciphertext.u[j] * s[j]; + } + // d = rΒ·e + bit·⌊q/2βŒ‹ (mod q); floorMod keeps it non-negative + int d = (int) Math.floorMod(ciphertext.v - dot, (long) Q); + return d >= Q / 4 && d < 3 * Q / 4 ? 1 : 0; + } + + /** + * Encrypts a message bit by bit, most significant bit of each byte first. + * + * @param message the message to encrypt + * @return eight ciphertexts per message byte + * @throws IllegalArgumentException if the message is null or empty + */ + public Ciphertext[] encrypt(byte[] message) { + if (message == null || message.length == 0) { + throw new IllegalArgumentException("message must not be null or empty"); + } + Ciphertext[] ciphertexts = new Ciphertext[message.length * BITS_PER_BYTE]; + for (int i = 0; i < message.length; i++) { + for (int k = 0; k < BITS_PER_BYTE; k++) { + ciphertexts[i * BITS_PER_BYTE + k] = encryptBit((message[i] >> (7 - k)) & 1); + } + } + return ciphertexts; + } + + /** + * Decrypts a message encrypted with {@link #encrypt(byte[])}. + * + * @param ciphertexts eight ciphertexts per message byte + * @return the decrypted message + * @throws IllegalArgumentException if the array is null, empty, contains null, or its length is + * not a multiple of 8 + */ + public byte[] decrypt(Ciphertext[] ciphertexts) { + if (ciphertexts == null || ciphertexts.length == 0) { + throw new IllegalArgumentException("ciphertexts must not be null or empty"); + } + if (ciphertexts.length % BITS_PER_BYTE != 0) { + throw new IllegalArgumentException("ciphertexts length must be a multiple of 8, got " + ciphertexts.length); + } + byte[] message = new byte[ciphertexts.length / BITS_PER_BYTE]; + for (int i = 0; i < message.length; i++) { + int value = 0; + for (int k = 0; k < BITS_PER_BYTE; k++) { + value = (value << 1) | decryptBit(ciphertexts[i * BITS_PER_BYTE + k]); + } + message[i] = (byte) value; + } + return message; + } + + /** + * Checks that the maximum decryption noise {@code mΒ·B} stays below {@code q/4}. + * + * @throws IllegalStateException if decryption could fail with these parameters + */ + static void checkDecryptionBound(int samples, int errorBound, int modulus) { + if (samples * errorBound >= modulus / 4) { + throw new IllegalStateException("m * B must be less than q / 4 for correct decryption"); + } + } + + /** + * An LWE ciphertext {@code (u, v)} for one bit. Immutable: the array is copied on construction + * and on access. + */ + public static final class Ciphertext { + private final int[] u; + private final int v; + + /** + * Creates a ciphertext. + * + * @param u the vector part, of length 32 + * @param v the scalar part + * @throws IllegalArgumentException if {@code u} is null or does not have length 32 + */ + public Ciphertext(int[] u, int v) { + if (u == null || u.length != N) { + throw new IllegalArgumentException("u must contain exactly " + N + " values"); + } + this.u = u.clone(); + this.v = v; + } + + /** + * @return a copy of the vector part + */ + public int[] getU() { + return u.clone(); + } + + /** + * @return the scalar part + */ + public int getV() { + return v; + } + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java new file mode 100644 index 000000000000..c4a6459fd4c3 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java @@ -0,0 +1,145 @@ +package com.thealgorithms.ciphers; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * Lamport one-time signatures (OTS) are a hash-based post-quantum signature scheme. + * + *

The scheme is based on a private key consisting of 256 pairs of random 32-byte secrets, + * one pair per bit position of a SHA-256 digest. The public key contains the SHA-256 hashes of + * those secrets. + * + *

To sign a message, the algorithm hashes the message and reveals the secret corresponding to + * each bit value in the digest. Verification recomputes the digest and checks that each revealed + * secret hashes to the expected public-key value. + * + *

This implementation is educational and intentionally keeps dependencies minimal. Each key pair + * is strictly one-time: signing a second message with the same key is rejected. + * + *

Reference: Wikipedia: Lamport signature + */ +public final class LamportSignature { + + private static final int DIGEST_BITS = 256; + private static final int SECRET_BYTES = 32; + private static final int NUM_VALUES = 2; + + private LamportSignature() { + } + + /** + * A Lamport key pair containing both the private and public material. + */ + public static final class KeyPair { + private final byte[][][] privateKey; + private final byte[][][] publicKey; + private boolean used; + + private KeyPair(byte[][][] privateKey, byte[][][] publicKey) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + /** + * Signs a message using this key pair. + * + * @param message the message to sign + * @return the signature bytes; exactly 256 secret values ordered by digest bit positions + * @throws IllegalArgumentException if the message is null + * @throws IllegalStateException if this key pair has already been used to sign a message + */ + public byte[] sign(byte[] message) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (used) { + throw new IllegalStateException("This Lamport key pair can only sign one message"); + } + used = true; + + byte[] digest = sha256(message); + byte[] signature = new byte[DIGEST_BITS * SECRET_BYTES]; + for (int bit = 0; bit < DIGEST_BITS; bit++) { + int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01; + byte[] secret = privateKey[bit][value]; + System.arraycopy(secret, 0, signature, bit * SECRET_BYTES, SECRET_BYTES); + } + return signature; + } + + /** + * Verifies a message signature against this public key. + * + * @param message the message to verify + * @param signature the signature to verify + * @return true if the signature is valid for the provided message and public key + * @throws IllegalArgumentException if message or signature is null or malformed + */ + public boolean verify(byte[] message, byte[] signature) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (signature == null) { + throw new IllegalArgumentException("signature must not be null"); + } + if (signature.length != DIGEST_BITS * SECRET_BYTES) { + throw new IllegalArgumentException("signature length must be exactly 8192 bytes"); + } + + byte[] digest = sha256(message); + for (int bit = 0; bit < DIGEST_BITS; bit++) { + int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01; + byte[] revealedSecret = Arrays.copyOfRange(signature, bit * SECRET_BYTES, (bit + 1) * SECRET_BYTES); + byte[] expectedHash = publicKey[bit][value]; + byte[] actualHash = sha256(revealedSecret); + if (!MessageDigest.isEqual(actualHash, expectedHash)) { + return false; + } + } + return true; + } + + public byte[][][] getPrivateKey() { + return privateKey; + } + + public byte[][][] getPublicKey() { + return publicKey; + } + } + + /** + * Generates a new Lamport key pair. + * + * @return a fresh Lamport key pair + */ + public static KeyPair generateKeyPair() { + SecureRandom secureRandom = new SecureRandom(); + byte[][][] privateKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES]; + byte[][][] publicKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES]; + + for (int bit = 0; bit < DIGEST_BITS; bit++) { + for (int value = 0; value < NUM_VALUES; value++) { + secureRandom.nextBytes(privateKey[bit][value]); + publicKey[bit][value] = sha256(privateKey[bit][value]); + } + } + return new KeyPair(privateKey, publicKey); + } + + static byte[] sha256(byte[] input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(input); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 is required by the Java SE specification", e); + } + } + + static byte[] sha256(byte[] message, int offset, int length) { + return sha256(Arrays.copyOfRange(message, offset, offset + length)); + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/MerkleSignatureScheme.java b/src/main/java/com/thealgorithms/ciphers/MerkleSignatureScheme.java new file mode 100644 index 000000000000..783dc2863d7e --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/MerkleSignatureScheme.java @@ -0,0 +1,261 @@ +package com.thealgorithms.ciphers; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * The Merkle signature scheme (MSS) turns a one-time signature scheme into a many-time scheme. + * + *

Key generation creates {@code 2^h} Winternitz one-time key pairs and hashes each WOTS public + * key into a leaf of a binary hash tree of height {@code h}. The tree root is the single MSS public + * key. Every signature uses the next unused leaf, so one key pair can sign {@code 2^h} messages. + * + *

A signature contains the leaf index, the WOTS signature, the WOTS public key of that leaf and + * the authentication path: the {@code h} sibling hashes needed to recompute the root from the leaf. + * The verifier checks the WOTS signature, hashes the WOTS public key into the leaf and walks up the + * tree with the authentication path. Including the WOTS public key in the signature is an + * educational simplification; XMSS instead recomputes it from the WOTS signature. + * + *

MSS is the core of XMSS (RFC 8391). This implementation is educational and must not be used in + * production. + * + *

Reference: Wikipedia: Merkle signature scheme + * + * @author dilaraacetin + * @see WinternitzSignature + * @see LamportSignature + */ +public final class MerkleSignatureScheme { + + // SHA-256 output length in bytes + private static final int HASH_BYTES = 32; + private static final int MIN_HEIGHT = 2; + private static final int MAX_HEIGHT = 10; + private static final int DEFAULT_HEIGHT = 4; + private static final int DEFAULT_W = 16; + + private final int height; + private final WinternitzSignature[] oneTimeKeys; + // tree[level][node]: level 0 holds the leaves, level h holds the root + private final byte[][][] tree; + private int nextLeafIndex; + + /** + * Generates a new key pair with height {@code h = 4} (16 signatures) and {@code w = 16}. + */ + public MerkleSignatureScheme() { + this(DEFAULT_HEIGHT, DEFAULT_W); + } + + /** + * Generates a new key pair that can sign {@code 2^h} messages. + * + * @param h the tree height; must be between 2 and 10 + * @param w the Winternitz parameter of the one-time keys; must be 4, 16 or 256 + * @throws IllegalArgumentException if {@code h} or {@code w} is not supported + */ + public MerkleSignatureScheme(int h, int w) { + if (h < MIN_HEIGHT || h > MAX_HEIGHT) { + throw new IllegalArgumentException("h must be between " + MIN_HEIGHT + " and " + MAX_HEIGHT + ", got " + h); + } + this.height = h; + int leafCount = 1 << h; + oneTimeKeys = new WinternitzSignature[leafCount]; + tree = new byte[h + 1][][]; + tree[0] = new byte[leafCount][]; + for (int i = 0; i < leafCount; i++) { + oneTimeKeys[i] = new WinternitzSignature(w); + tree[0][i] = hashPublicKey(oneTimeKeys[i].getPublicKey()); + } + for (int level = 1; level <= h; level++) { + tree[level] = new byte[tree[level - 1].length / 2][]; + for (int i = 0; i < tree[level].length; i++) { + tree[level][i] = hash(tree[level - 1][2 * i], tree[level - 1][2 * i + 1]); + } + } + } + + /** + * Returns a copy of the public key, which is the root of the Merkle tree. + * + * @return the 32-byte tree root + */ + public byte[] getPublicKey() { + return tree[height][0].clone(); + } + + /** + * Returns how many messages this key pair can still sign. + * + * @return the number of unused one-time keys + */ + public int remainingSignatures() { + return oneTimeKeys.length - nextLeafIndex; + } + + /** + * Signs a message with the next unused one-time key. + * + * @param message the message to sign + * @return the Merkle signature + * @throws IllegalArgumentException if the message is null + * @throws IllegalStateException if all {@code 2^h} one-time keys have been used + */ + public MerkleSignature sign(byte[] message) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (nextLeafIndex >= oneTimeKeys.length) { + throw new IllegalStateException("All " + oneTimeKeys.length + " signatures of this key pair have been used"); + } + int leafIndex = nextLeafIndex++; + WinternitzSignature oneTimeKey = oneTimeKeys[leafIndex]; + byte[][] wotsSignature = oneTimeKey.sign(message); + + // collect the sibling of the current node on every level, from the leaf up to the root + byte[][] authPath = new byte[height][]; + int index = leafIndex; + for (int level = 0; level < height; level++) { + authPath[level] = tree[level][index ^ 1]; + index >>= 1; + } + return new MerkleSignature(leafIndex, wotsSignature, oneTimeKey.getPublicKey(), authPath); + } + + /** + * Verifies a Merkle signature against a public key (tree root). The tree height is taken from + * the length of the authentication path. + * + * @param message the signed message + * @param signature the signature to check + * @param publicKey the tree root of the signer + * @param w the Winternitz parameter used to create the key pair; must be 4, 16 or 256 + * @return true if the signature is valid for the message and public key, false otherwise + * @throws IllegalArgumentException if an argument is null, {@code w} is unsupported, or the + * public key or WOTS part of the signature is malformed + */ + public static boolean verify(byte[] message, MerkleSignature signature, byte[] publicKey, int w) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (signature == null) { + throw new IllegalArgumentException("signature must not be null"); + } + if (publicKey == null || publicKey.length != HASH_BYTES) { + throw new IllegalArgumentException("publicKey must be exactly " + HASH_BYTES + " bytes long"); + } + if (!WinternitzSignature.verify(message, signature.wotsSignature, signature.wotsPublicKey, w)) { + return false; + } + + byte[] node = hashPublicKey(signature.wotsPublicKey); + int index = signature.leafIndex; + for (byte[] sibling : signature.authPath) { + // an even index is a left child, an odd index a right child + node = (index & 1) == 0 ? hash(node, sibling) : hash(sibling, node); + index >>= 1; + } + return MessageDigest.isEqual(node, publicKey); + } + + /** + * A Merkle signature: leaf index, WOTS signature, WOTS public key and authentication path. + * All arrays are copied on construction and on access. + */ + public static final class MerkleSignature { + private final int leafIndex; + private final byte[][] wotsSignature; + private final byte[][] wotsPublicKey; + private final byte[][] authPath; + + /** + * Creates a signature from its parts. + * + * @param leafIndex the index of the leaf used for signing + * @param wotsSignature the Winternitz signature of the message + * @param wotsPublicKey the Winternitz public key of the leaf + * @param authPath the sibling hashes from the leaf up to the root + * @throws IllegalArgumentException if an argument is null, the authentication path length is + * not between 2 and 10, an authentication path value is not 32 bytes long, or the leaf + * index is outside the tree + */ + public MerkleSignature(int leafIndex, byte[][] wotsSignature, byte[][] wotsPublicKey, byte[][] authPath) { + if (authPath == null || authPath.length < MIN_HEIGHT || authPath.length > MAX_HEIGHT) { + throw new IllegalArgumentException("authPath must contain between " + MIN_HEIGHT + " and " + MAX_HEIGHT + " values"); + } + for (byte[] sibling : authPath) { + if (sibling == null || sibling.length != HASH_BYTES) { + throw new IllegalArgumentException("authPath values must be exactly " + HASH_BYTES + " bytes long"); + } + } + if (leafIndex < 0 || leafIndex >= 1 << authPath.length) { + throw new IllegalArgumentException("leafIndex must be between 0 and " + ((1 << authPath.length) - 1) + ", got " + leafIndex); + } + this.leafIndex = leafIndex; + this.wotsSignature = deepCopy(wotsSignature, "wotsSignature"); + this.wotsPublicKey = deepCopy(wotsPublicKey, "wotsPublicKey"); + this.authPath = deepCopy(authPath, "authPath"); + } + + /** + * @return the index of the leaf used for signing + */ + public int getLeafIndex() { + return leafIndex; + } + + /** + * @return a copy of the Winternitz signature + */ + public byte[][] getWotsSignature() { + return deepCopy(wotsSignature, "wotsSignature"); + } + + /** + * @return a copy of the Winternitz public key of the leaf + */ + public byte[][] getWotsPublicKey() { + return deepCopy(wotsPublicKey, "wotsPublicKey"); + } + + /** + * @return a copy of the authentication path, ordered from the leaf level up to the root + */ + public byte[][] getAuthPath() { + return deepCopy(authPath, "authPath"); + } + } + + /** + * Hashes a WOTS public key into a Merkle leaf. Shared by key generation and verification. + */ + private static byte[] hashPublicKey(byte[][] wotsPublicKey) { + return hash(wotsPublicKey); + } + + private static byte[] hash(byte[]... parts) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (byte[] part : parts) { + digest.update(part); + } + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 is required by the Java SE specification", e); + } + } + + private static byte[][] deepCopy(byte[][] values, String name) { + if (values == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + byte[][] copy = new byte[values.length][]; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException(name + " values must not be null"); + } + copy[i] = values[i].clone(); + } + return copy; + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java b/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java new file mode 100644 index 000000000000..7733f5cb46f2 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java @@ -0,0 +1,89 @@ +package com.thealgorithms.ciphers; + +import java.security.SecureRandom; +import java.util.Objects; + +/** + * One-Time Pad (OTP) cipher implementation. + * + *

The One-Time Pad is information-theoretically secure if: + *

    + *
  • The key is truly random.
  • + *
  • The key length is at least as long as the plaintext.
  • + *
  • The key is used only once and kept secret.
  • + *
+ * + *

This implementation is for educational purposes only and should not be + * used in production systems. + */ +public final class OneTimePadCipher { + + private static final SecureRandom RANDOM = new SecureRandom(); + + private OneTimePadCipher() { + // utility class + } + + /** + * Generates a random key of the given length in bytes. + * + * @param length the length of the key in bytes, must be non-negative + * @return a new random key + * @throws IllegalArgumentException if length is negative + */ + public static byte[] generateKey(int length) { + if (length < 0) { + throw new IllegalArgumentException("length must be non-negative"); + } + byte[] key = new byte[length]; + RANDOM.nextBytes(key); + return key; + } + + /** + * Encrypts the given plaintext bytes using the provided key. + *

The key length must be exactly the same as the plaintext length. + * + * @param plaintext the plaintext bytes, must not be {@code null} + * @param key the one-time pad key bytes, must not be {@code null} + * @return the ciphertext bytes + * @throws IllegalArgumentException if the key length does not match plaintext length + * @throws NullPointerException if plaintext or key is {@code null} + */ + public static byte[] encrypt(byte[] plaintext, byte[] key) { + validateInputs(plaintext, key); + return xor(plaintext, key); + } + + /** + * Decrypts the given ciphertext bytes using the provided key. + *

For a One-Time Pad, decryption is identical to encryption: + * {@code plaintext = ciphertext XOR key}. + * + * @param ciphertext the ciphertext bytes, must not be {@code null} + * @param key the one-time pad key bytes, must not be {@code null} + * @return the decrypted plaintext bytes + * @throws IllegalArgumentException if the key length does not match ciphertext length + * @throws NullPointerException if ciphertext or key is {@code null} + */ + public static byte[] decrypt(byte[] ciphertext, byte[] key) { + validateInputs(ciphertext, key); + return xor(ciphertext, key); + } + + private static void validateInputs(byte[] input, byte[] key) { + Objects.requireNonNull(input, "input must not be null"); + Objects.requireNonNull(key, "key must not be null"); + if (input.length != key.length) { + throw new IllegalArgumentException("Key length must match input length"); + } + } + + private static byte[] xor(byte[] data, byte[] key) { + byte[] result = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + result[i] = (byte) (data[i] ^ key[i]); + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java b/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java index f81252980468..324dc88e4f19 100644 --- a/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java +++ b/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java @@ -1,7 +1,5 @@ package com.thealgorithms.ciphers; -import java.util.Arrays; - /** * The rail fence cipher (also called a zigzag cipher) is a classical type of transposition cipher. * It derives its name from the manner in which encryption is performed, in analogy to a fence built with horizontal rails. @@ -14,28 +12,27 @@ public class RailFenceCipher { // Encrypts the input string using the rail fence cipher method with the given number of rails. public String encrypt(String str, int rails) { + checkInput(str, rails); + // Base case of single rail or rails are more than the number of characters in the string if (rails == 1 || rails >= str.length()) { return str; } - // Boolean flag to determine if the movement is downward or upward in the rail matrix. + // Boolean flag to determine if the movement is downward or upward in the rail pattern. boolean down = true; - // Create a 2D array to represent the rails (rows) and the length of the string (columns). - char[][] strRail = new char[rails][str.length()]; - - // Initialize all positions in the rail matrix with a placeholder character ('\n'). + // Collect the characters of every rail separately. Using one buffer per rail (instead of a + // rails x length matrix with a placeholder character) keeps every character of the input, + // including characters that would otherwise be indistinguishable from the placeholder. + StringBuilder[] railBuffers = new StringBuilder[rails]; for (int i = 0; i < rails; i++) { - Arrays.fill(strRail[i], '\n'); + railBuffers[i] = new StringBuilder(); } - int row = 0; // Start at the first row - int col = 0; // Start at the first column + int row = 0; // Start at the first rail - int i = 0; - - // Fill the rail matrix with characters from the string based on the rail pattern. - while (col < str.length()) { + // Distribute the characters of the string over the rails following the zigzag pattern. + for (int i = 0; i < str.length(); i++) { // Change direction to down when at the first row. if (row == 0) { down = true; @@ -45,33 +42,28 @@ else if (row == rails - 1) { down = false; } - // Place the character in the current position of the rail matrix. - strRail[row][col] = str.charAt(i); - col++; // Move to the next column. + // Append the character to the rail it belongs to. + railBuffers[row].append(str.charAt(i)); // Move to the next row based on the direction. if (down) { row++; } else { row--; } - - i++; } - // Construct the encrypted string by reading characters row by row. - StringBuilder encryptedString = new StringBuilder(); - for (char[] chRow : strRail) { - for (char ch : chRow) { - if (ch != '\n') { - encryptedString.append(ch); - } - } + // Construct the encrypted string by reading the rails top to bottom. + StringBuilder encryptedString = new StringBuilder(str.length()); + for (StringBuilder railBuffer : railBuffers) { + encryptedString.append(railBuffer); } return encryptedString.toString(); } // Decrypts the input string using the rail fence cipher method with the given number of rails. public String decrypt(String str, int rails) { + checkInput(str, rails); + // Base case of single rail or rails are more than the number of characters in the string if (rails == 1 || rails >= str.length()) { return str; @@ -144,4 +136,14 @@ else if (row == rails - 1) { return decryptedString.toString(); } + + // Rejects inputs the zigzag pattern is not defined for. + private static void checkInput(String str, int rails) { + if (str == null) { + throw new IllegalArgumentException("Input string must not be null"); + } + if (rails <= 0) { + throw new IllegalArgumentException("Number of rails must be positive, but was " + rails); + } + } } diff --git a/src/main/java/com/thealgorithms/ciphers/WinternitzSignature.java b/src/main/java/com/thealgorithms/ciphers/WinternitzSignature.java new file mode 100644 index 000000000000..e4239b170adb --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/WinternitzSignature.java @@ -0,0 +1,227 @@ +package com.thealgorithms.ciphers; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * Winternitz one-time signatures (WOTS) are a hash-based post-quantum signature scheme and a + * size-optimized version of the Lamport signature. + * + *

The message digest is split into base-{@code w} digits and each digit is signed with a hash + * chain: secret {@code sk[i]} is hashed {@code m[i]} times, while the public key is the chain end + * after {@code w - 1} hashes. A larger {@code w} gives smaller signatures but more hashing + * (133, 67 and 34 values for {@code w} = 4, 16 and 256). + * + *

Security relies only on the one-wayness of the hash function, which is why the scheme is + * considered quantum-resistant. A checksum over the digits is signed too, so an attacker cannot + * hash revealed values further to forge a signature. Each key pair can sign only one message, + * since a second signature reveals enough chain values to forge new ones. + * + *

WOTS is the building block of XMSS and SPHINCS+, which use the WOTS+ variant with per-step + * bitmasks. This implementation is educational and must not be used in production. + * + *

Reference: Wikipedia: Hash-based cryptography + * + * @author dilaraacetin + * @see LamportSignature + */ +public final class WinternitzSignature { + + // SHA-256 output length in bytes + private static final int HASH_BYTES = 32; + private static final int DEFAULT_W = 16; + + private final Parameters parameters; + private final byte[][] privateKey; + private final byte[][] publicKey; + private boolean used; + + /** + * Generates a new key pair with the default Winternitz parameter {@code w = 16}. + */ + public WinternitzSignature() { + this(DEFAULT_W); + } + + /** + * Generates a new key pair with the given Winternitz parameter. + * + * @param w the Winternitz parameter; must be 4, 16 or 256 + * @throws IllegalArgumentException if {@code w} is not a supported value + */ + public WinternitzSignature(int w) { + this.parameters = Parameters.forW(w); + SecureRandom secureRandom = new SecureRandom(); + privateKey = new byte[parameters.len()][HASH_BYTES]; + publicKey = new byte[parameters.len()][]; + for (int i = 0; i < parameters.len(); i++) { + secureRandom.nextBytes(privateKey[i]); + publicKey[i] = chain(privateKey[i], w - 1); + } + } + + /** + * Returns a copy of the public key. + * + * @return {@code len} hash chain end values, each 32 bytes long + */ + public byte[][] getPublicKey() { + return deepCopy(publicKey); + } + + /** + * Signs a message. A key pair can sign only one message. + * + * @param message the message to sign + * @return the signature: {@code len} values of 32 bytes each (67 values for {@code w = 16}) + * @throws IllegalArgumentException if the message is null + * @throws IllegalStateException if this key pair has already been used to sign a message + */ + public byte[][] sign(byte[] message) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (used) { + throw new IllegalStateException("This Winternitz key pair can only sign one message"); + } + used = true; + + int[] digits = messageDigits(hash(message), parameters); + byte[][] signature = new byte[parameters.len()][]; + for (int i = 0; i < parameters.len(); i++) { + signature[i] = chain(privateKey[i], digits[i]); + } + return signature; + } + + /** + * Verifies a signature against a public key. Using a different {@code w} than the signer + * causes a size mismatch and an {@link IllegalArgumentException}. + * + * @param message the signed message + * @param signature the signature to check + * @param publicKey the public key of the signer + * @param w the Winternitz parameter used to create the key pair; must be 4, 16 or 256 + * @return true if the signature is valid for the message and public key, false otherwise + * @throws IllegalArgumentException if an argument is null, {@code w} is unsupported, or the + * signature or public key is malformed + */ + public static boolean verify(byte[] message, byte[][] signature, byte[][] publicKey, int w) { + Parameters params = Parameters.forW(w); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + validateShape(signature, params.len(), "signature"); + validateShape(publicKey, params.len(), "publicKey"); + + int[] digits = messageDigits(hash(message), params); + for (int i = 0; i < params.len(); i++) { + // complete the chain: m[i] + (w - 1 - m[i]) = w - 1 steps + byte[] chainEnd = chain(signature[i], w - 1 - digits[i]); + if (!MessageDigest.isEqual(chainEnd, publicKey[i])) { + return false; + } + } + return true; + } + + /** + * Parameters derived from {@code w}: {@code len1} message chains and {@code len2} checksum chains. + */ + record Parameters(int w, int log2w, int len1, int len2) { + + static Parameters forW(int w) { + if (w != 4 && w != 16 && w != 256) { + throw new IllegalArgumentException("w must be 4, 16 or 256, got " + w); + } + int log2w = Integer.numberOfTrailingZeros(w); + // len1 = ceil(8n / log2(w)) + int len1 = (8 * HASH_BYTES + log2w - 1) / log2w; + // len2 = floor(log2(len1 * (w - 1)) / log2(w)) + 1 + int maxChecksumBits = 32 - Integer.numberOfLeadingZeros(len1 * (w - 1)); + int len2 = (maxChecksumBits - 1) / log2w + 1; + return new Parameters(w, log2w, len1, len2); + } + + int len() { + return len1 + len2; + } + } + + /** + * Converts a digest into {@code len1} base-{@code w} message digits followed by {@code len2} + * checksum digits. Shared by sign and verify. + */ + private static int[] messageDigits(byte[] digest, Parameters params) { + int[] digits = new int[params.len()]; + int mask = params.w() - 1; + + // split the digest into log2(w)-bit digits + int bitsLeft = 0; + int currentByte = 0; + int byteIndex = 0; + for (int i = 0; i < params.len1(); i++) { + if (bitsLeft == 0) { + currentByte = digest[byteIndex++] & 0xFF; // unsigned byte + bitsLeft = 8; + } + bitsLeft -= params.log2w(); + digits[i] = (currentByte >> bitsLeft) & mask; + } + + // checksum C = sum(w - 1 - m[i]) + int checksum = 0; + for (int i = 0; i < params.len1(); i++) { + checksum += mask - digits[i]; + } + + // write C as len2 base-w digits, most significant first + for (int i = params.len() - 1; i >= params.len1(); i--) { + digits[i] = checksum & mask; + checksum >>>= params.log2w(); + } + return digits; + } + + /** + * Hashes {@code x} {@code iterations} times; returns a copy of {@code x} for zero iterations. + */ + private static byte[] chain(byte[] x, int iterations) { + byte[] result = x.clone(); + for (int i = 0; i < iterations; i++) { + result = hash(result); + } + return result; + } + + private static byte[] hash(byte[] data) { + try { + return MessageDigest.getInstance("SHA-256").digest(data); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 is required by the Java SE specification", e); + } + } + + private static void validateShape(byte[][] values, int expectedLength, String name) { + if (values == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + if (values.length != expectedLength) { + throw new IllegalArgumentException(name + " must contain exactly " + expectedLength + " values, got " + values.length); + } + for (byte[] value : values) { + if (value == null || value.length != HASH_BYTES) { + throw new IllegalArgumentException(name + " values must be exactly " + HASH_BYTES + " bytes long"); + } + } + } + + private static byte[][] deepCopy(byte[][] values) { + byte[][] copy = new byte[values.length][]; + for (int i = 0; i < values.length; i++) { + copy[i] = values[i].clone(); + } + return copy; + } +} diff --git a/src/main/java/com/thealgorithms/compression/HuffmanCoding.java b/src/main/java/com/thealgorithms/compression/HuffmanCoding.java new file mode 100644 index 000000000000..d7f9d58d2429 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/HuffmanCoding.java @@ -0,0 +1,253 @@ +package com.thealgorithms.compression; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.PriorityQueue; + +/** + * Huffman Coding Compression Algorithm Implementation. + *

+ * Huffman Coding is a popular greedy algorithm used for lossless data compression. + * It reduces the overall size of data by assigning variable-length, prefix-free + * binary codes to input characters, ensuring that more frequent characters receive + * the shortest possible codes. + *

+ *

+ * Key Features: + *

    + *
  • Uses a PriorityQueue (min-heap) to efficiently construct the optimal prefix tree.
  • + *
  • Fail-fast design throws exceptions for unsupported characters and malformed binary payloads.
  • + *
  • Immutable internal dictionary state prevents external tampering with generated codes.
  • + *
  • Robust handling of edge cases, including single-character strings and incomplete sequences.
  • + *
+ *

+ * @author Chahat Sandhu, singhc7 + * @see Huffman Coding (Wikipedia) + */ +public class HuffmanCoding { + + private Node root; + private final Map huffmanCodes; + + /** + * Represents a node within the Huffman Tree. + * Implements {@link Comparable} to allow sorting by frequency in a PriorityQueue. + */ + private static class Node implements Comparable { + final char ch; + final int freq; + final Node left; + final Node right; + + /** + * Constructs a leaf node containing a specific character and its frequency. + * + * @param ch The character stored in this leaf. + * @param freq The frequency of occurrence of the character. + */ + Node(char ch, int freq) { + this.ch = ch; + this.freq = freq; + this.left = null; + this.right = null; + } + + /** + * Constructs an internal node that merges two child nodes. + * The character is defaulted to the null character ('\0'). + * + * @param freq The combined frequency of the left and right child nodes. + * @param left The left child node. + * @param right The right child node. + */ + Node(int freq, Node left, Node right) { + this.ch = '\0'; + this.freq = freq; + this.left = left; + this.right = right; + } + + /** + * Determines if the current node is a leaf (contains no children). + * + * @return {@code true} if both left and right children are null, {@code false} otherwise. + */ + boolean isLeaf() { + return left == null && right == null; + } + + /** + * Compares this node with another node based on their frequencies. + * Used by the PriorityQueue to maintain the min-heap property. + * + * @param other The other Node to compare against. + * @return A negative integer, zero, or a positive integer as this node's frequency + * is less than, equal to, or greater than the specified node's frequency. + */ + @Override + public int compareTo(Node other) { + return Integer.compare(this.freq, other.freq); + } + } + + /** + * Initializes the Huffman Tree and generates immutable prefix-free codes + * based on the character frequencies in the provided text. + * + * @param text The input string used to calculate frequencies and build the optimal tree. + * If null or empty, an empty tree and dictionary are created. + */ + public HuffmanCoding(String text) { + if (text == null || text.isEmpty()) { + this.huffmanCodes = Collections.emptyMap(); + return; + } + + Map tempCodes = new HashMap<>(); + buildTree(text); + generateCodes(root, "", tempCodes); + + if (tempCodes.size() == 1) { + tempCodes.put(root.ch, "0"); + } + + this.huffmanCodes = Collections.unmodifiableMap(tempCodes); + } + + /** + * Computes character frequencies and constructs the Huffman Tree using a min-heap. + * The optimal tree is built by repeatedly extracting the two lowest-frequency nodes + * and merging them until a single root node remains. + * + * @param text The input text to analyze. + */ + private void buildTree(String text) { + Map freqMap = new HashMap<>(); + for (char c : text.toCharArray()) { + freqMap.put(c, freqMap.getOrDefault(c, 0) + 1); + } + + PriorityQueue pq = new PriorityQueue<>(); + for (Map.Entry entry : freqMap.entrySet()) { + pq.add(new Node(entry.getKey(), entry.getValue())); + } + + while (pq.size() > 1) { + Node left = pq.poll(); + Node right = pq.poll(); + pq.add(new Node(left.freq + right.freq, left, right)); + } + + root = pq.poll(); + } + + /** + * Recursively traverses the Huffman Tree to generate prefix-free binary codes. + * Left traversals append a '0' to the code, while right traversals append a '1'. + * + * @param node The current node in the traversal. + * @param code The accumulated binary string for the current path. + * @param map The temporary dictionary to populate with the final character-to-code mappings. + */ + private void generateCodes(Node node, String code, Map map) { + if (node == null) { + return; + } + if (node.isLeaf()) { + map.put(node.ch, code); + return; + } + generateCodes(node.left, code + "0", map); + generateCodes(node.right, code + "1", map); + } + + /** + * Encodes the given plaintext string into a binary string using the generated Huffman dictionary. + * + * @param text The plaintext string to compress. + * @return A string of '0's and '1's representing the compressed data. + * Returns an empty string if the input is null or empty. + * @throws IllegalStateException If attempting to encode when the Huffman tree is empty. + * @throws IllegalArgumentException If the input text contains a character not present + * in the original text used to build the tree. + */ + public String encode(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + if (root == null) { + throw new IllegalStateException("Huffman tree is empty."); + } + + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) { + if (!huffmanCodes.containsKey(c)) { + throw new IllegalArgumentException(String.format("Character '%c' (U+%04X) not found in Huffman dictionary.", c, (int) c)); + } + sb.append(huffmanCodes.get(c)); + } + return sb.toString(); + } + + /** + * Decodes the given binary string back into the original plaintext using the Huffman Tree. + * Validates the integrity of the binary payload during traversal. + * + * @param encodedText The binary string of '0's and '1's to decompress. + * @return The reconstructed plaintext string. Returns an empty string if the input is null or empty. + * @throws IllegalStateException If attempting to decode when the Huffman tree is empty. + * @throws IllegalArgumentException If the binary string contains characters other than '0' or '1', + * or if the sequence ends abruptly without reaching a leaf node. + */ + public String decode(String encodedText) { + if (encodedText == null || encodedText.isEmpty()) { + return ""; + } + if (root == null) { + throw new IllegalStateException("Huffman tree is empty."); + } + + StringBuilder sb = new StringBuilder(); + + if (root.isLeaf()) { + for (char bit : encodedText.toCharArray()) { + if (bit != '0') { + throw new IllegalArgumentException("Invalid binary sequence for single-character tree."); + } + sb.append(root.ch); + } + return sb.toString(); + } + + Node current = root; + for (char bit : encodedText.toCharArray()) { + if (bit != '0' && bit != '1') { + throw new IllegalArgumentException("Encoded text contains invalid characters: " + bit); + } + + current = (bit == '0') ? current.left : current.right; + + if (current.isLeaf()) { + sb.append(current.ch); + current = root; + } + } + + if (current != root) { + throw new IllegalArgumentException("Malformed encoded string: incomplete sequence ending."); + } + + return sb.toString(); + } + + /** + * Retrieves the generated Huffman dictionary mapping characters to their binary codes. + * + * @return An unmodifiable map containing the character-to-binary-code mappings to prevent + * external mutation of the algorithm's state. + */ + public Map getHuffmanCodes() { + return huffmanCodes; + } +} diff --git a/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java b/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java index 7a9448fd8fe7..314e7fba38a3 100644 --- a/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java +++ b/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java @@ -1,3 +1,4 @@ + package com.thealgorithms.conversions; import java.util.Arrays; diff --git a/src/main/java/com/thealgorithms/conversions/Base64.java b/src/main/java/com/thealgorithms/conversions/Base64.java index 5219c4ba7f4e..fb4411b399a3 100644 --- a/src/main/java/com/thealgorithms/conversions/Base64.java +++ b/src/main/java/com/thealgorithms/conversions/Base64.java @@ -119,8 +119,15 @@ public static byte[] decode(String input) { // Validate padding: '=' can only appear at the end (last 1 or 2 chars) int firstPadding = input.indexOf('='); - if (firstPadding != -1 && firstPadding < input.length() - 2) { - throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)"); + if (firstPadding != -1) { + if (firstPadding < input.length() - 2) { + throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)"); + } + for (int i = firstPadding; i < input.length(); i++) { + if (input.charAt(i) != '=') { + throw new IllegalArgumentException("A padding '=' must not be followed by a non-padding character"); + } + } } List result = new ArrayList<>(); diff --git a/src/main/java/com/thealgorithms/conversions/TemperatureConverter.java b/src/main/java/com/thealgorithms/conversions/TemperatureConverter.java new file mode 100644 index 000000000000..901db17c665d --- /dev/null +++ b/src/main/java/com/thealgorithms/conversions/TemperatureConverter.java @@ -0,0 +1,46 @@ +package com.thealgorithms.conversions; + +/** + * A utility class to convert between different temperature units. + * + *

This class supports conversions between the following units: + *

    + *
  • Celsius
  • + *
  • Fahrenheit
  • + *
  • Kelvin
  • + *
+ * + *

This class is final and cannot be instantiated. + * + * @author krishna-medapati (https://github.com/krishna-medapati) + * @see Wikipedia: Temperature Conversion + */ +public final class TemperatureConverter { + + private TemperatureConverter() { + } + + public static double celsiusToFahrenheit(double celsius) { + return celsius * 9.0 / 5.0 + 32.0; + } + + public static double celsiusToKelvin(double celsius) { + return celsius + 273.15; + } + + public static double fahrenheitToCelsius(double fahrenheit) { + return (fahrenheit - 32.0) * 5.0 / 9.0; + } + + public static double fahrenheitToKelvin(double fahrenheit) { + return (fahrenheit - 32.0) * 5.0 / 9.0 + 273.15; + } + + public static double kelvinToCelsius(double kelvin) { + return kelvin - 273.15; + } + + public static double kelvinToFahrenheit(double kelvin) { + return (kelvin - 273.15) * 9.0 / 5.0 + 32.0; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregator.java b/src/main/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregator.java new file mode 100644 index 000000000000..ae20c88cd334 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregator.java @@ -0,0 +1,447 @@ +package com.thealgorithms.datastructures.buffers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.Function; + +/** + * A fixed-capacity ring (circular) buffer that additionally keeps an aggregate of every element it + * currently holds, i.e. a sliding window with aggregation. + * + *

The window slides automatically: once the buffer is full, adding a new element evicts the + * oldest one, and the aggregate is kept in sync without rescanning the window. + * + *

How the aggregate is maintained

+ * + *

The naive way to keep a windowed sum is "add the new value, subtract the evicted one". That + * trick only works for invertible operators: there is no way to subtract a value from a + * minimum. This class instead uses the classic two-stack sliding window aggregation + * algorithm, which needs nothing but associativity: + * + *

    + *
  • The window is split into a front section {@code [head, flip)} and a back + * section {@code [flip, tail)}.
  • + *
  • For every slot of the front section the buffer stores the aggregate of that element and all + * younger elements of the front section (a suffix aggregate), so the aggregate of the + * whole front section is readable from the single slot pointed at by {@code head}.
  • + *
  • The back section is summarised by one running aggregate, updated on every insertion.
  • + *
  • {@link #aggregate()} combines the two, in that order, so the operator does not need to be + * commutative.
  • + *
  • When the front section runs out, the back section is turned into a new front section by one + * backwards pass. Every element takes part in at most one such pass, which makes the cost + * O(1) amortised per element.
  • + *
+ * + *

A pleasant side effect is numerical stability: because aggregates are recomputed from scratch + * on every flip, floating-point error cannot accumulate indefinitely the way it does with the + * add-then-subtract approach. + * + *

Complexity

+ * + * + * + * + * + * + * + * + * + *
Time and space complexity
OperationComplexity
{@link #add(Object)}O(1) amortised, O(n) worst case
{@link #removeOldest()}O(1) amortised, O(n) worst case
{@link #aggregate()}O(1)
{@link #get(int)}, {@link #peekOldest()}, {@link #peekNewest()}O(1)
memoryO(capacity), no allocation after construction
+ * + *

Usage

+ * + *
{@code
+ * // Maximum of the last three readings.
+ * SlidingWindowAggregator window = SlidingWindowAggregator.maximum(3, Comparator.naturalOrder());
+ * for (int value : new int[] {1, 3, 2, 5, 0}) {
+ *     window.add(value);
+ *     System.out.println(window.aggregate());
+ * }
+ * // prints 1, 3, 3, 5, 5
+ *
+ * // Moving average of the last 100 samples: the window knows both the sum and its own size.
+ * SlidingWindowAggregator sum = SlidingWindowAggregator.sumOfDoubles(100);
+ * sum.add(sample);
+ * double movingAverage = sum.aggregate() / sum.size();
+ * }
+ * + *

Contract

+ * + *
    + *
  • {@code null} elements are rejected, and neither the mapper nor the combiner may return + * {@code null}.
  • + *
  • The combiner must be associative; it does not have to be commutative and no + * identity element is required. Elements are always combined oldest-first.
  • + *
  • The mapper is applied exactly once per element, at insertion time.
  • + *
  • This class is not thread-safe.
  • + *
+ * + * @param type of the elements stored in the window + * @param type of the aggregate; use {@code A == E} for operators such as min, max or sum + * @see CircularBuffer + * @see Circular buffer + * @see K. Tangwongsan, M. Hirzel, S. Schneider, K.-L. Wu, General incremental sliding-window aggregation (VLDB 2015) + */ +public final class SlidingWindowAggregator implements Iterable { + + private final Object[] elements; + + /** + * Parallel ring holding one aggregate per occupied slot. The meaning depends on the section the + * slot belongs to: inside the front section it is the aggregate of the element and every younger + * element of that section, inside the back section it is simply the mapped element itself. + */ + private final Object[] aggregates; + + private final Function mapper; + private final BinaryOperator combiner; + private final int capacity; + + /** Absolute index of the oldest element; {@code head <= flip <= tail} always holds. */ + private long head; + + /** Absolute index of the boundary between the front and the back section. */ + private long flip; + + /** Absolute index one past the newest element. */ + private long tail; + + /** Aggregate of the back section, or {@code null} when that section is empty. */ + private A backAggregate; + + private int modCount; + + /** + * Creates an empty window. + * + * @param capacity maximum number of elements held at once; adding beyond it evicts the oldest + * @param mapper turns an element into the value the aggregate is computed over + * @param combiner associative operator used to merge two aggregates + * @throws IllegalArgumentException if {@code capacity} is not positive + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + */ + public SlidingWindowAggregator(int capacity, Function mapper, BinaryOperator combiner) { + if (capacity <= 0) { + throw new IllegalArgumentException("Window capacity must be positive, but was " + capacity); + } + this.capacity = capacity; + this.elements = new Object[capacity]; + this.aggregates = new Object[capacity]; + this.mapper = Objects.requireNonNull(mapper, "mapper must not be null"); + this.combiner = Objects.requireNonNull(combiner, "combiner must not be null"); + } + + /** + * Creates a window whose aggregate has the same type as its elements. + * + * @param capacity maximum number of elements held at once + * @param combiner associative operator used to merge two elements + * @param type of the elements and of the aggregate + * @return a new window + */ + public static SlidingWindowAggregator of(int capacity, BinaryOperator combiner) { + return new SlidingWindowAggregator<>(capacity, Function.identity(), combiner); + } + + /** + * Creates a window that keeps the sum of the last {@code capacity} values. + * + * @param capacity maximum number of elements held at once + * @return a new window aggregating with {@link Long#sum(long, long)} + */ + public static SlidingWindowAggregator sumOfLongs(int capacity) { + return of(capacity, Long::sum); + } + + /** + * Creates a window that keeps the sum of the last {@code capacity} values. Combined with + * {@link #size()} this is the cheapest way to obtain a moving average. + * + * @param capacity maximum number of elements held at once + * @return a new window aggregating with {@link Double#sum(double, double)} + */ + public static SlidingWindowAggregator sumOfDoubles(int capacity) { + return of(capacity, Double::sum); + } + + /** + * Creates a window that keeps the smallest of the last {@code capacity} values. + * + * @param capacity maximum number of elements held at once + * @param comparator ordering used to pick the minimum + * @param type of the elements + * @return a new window aggregating with {@link BinaryOperator#minBy(Comparator)} + */ + public static SlidingWindowAggregator minimum(int capacity, Comparator comparator) { + return of(capacity, BinaryOperator.minBy(comparator)); + } + + /** + * Creates a window that keeps the largest of the last {@code capacity} values. + * + * @param capacity maximum number of elements held at once + * @param comparator ordering used to pick the maximum + * @param type of the elements + * @return a new window aggregating with {@link BinaryOperator#maxBy(Comparator)} + */ + public static SlidingWindowAggregator maximum(int capacity, Comparator comparator) { + return of(capacity, BinaryOperator.maxBy(comparator)); + } + + /** + * Appends an element, evicting the oldest one if the window is already full. + * + * @param element the element to append + * @return the evicted element, or {@code null} if the window was not full + * @throws NullPointerException if {@code element} is {@code null} or the mapper returns {@code null} + */ + public E add(E element) { + Objects.requireNonNull(element, "This window does not accept null elements"); + E evicted = isFull() ? removeOldest() : null; + + int slot = slotOf(tail); + elements[slot] = element; + A mapped = requireNonNullResult(mapper.apply(element), "mapper"); + aggregates[slot] = mapped; + backAggregate = tail == flip ? mapped : requireNonNullResult(combiner.apply(backAggregate, mapped), "combiner"); + tail++; + modCount++; + return evicted; + } + + /** + * Appends every given element in order. The insertion is not atomic: if an element turns out to be + * {@code null}, the ones before it are already in the window. + * + * @param items the elements to append + * @throws NullPointerException if {@code items} or any of its elements is {@code null} + */ + public void addAll(Iterable items) { + Objects.requireNonNull(items, "items must not be null"); + for (E item : items) { + add(item); + } + } + + /** + * Removes the oldest element, shrinking the window. + * + * @return the removed element + * @throws NoSuchElementException if the window is empty + */ + public E removeOldest() { + if (isEmpty()) { + throw new NoSuchElementException("The window is empty"); + } + if (head == flip) { + rotateBackSectionToFront(); + } + + int slot = slotOf(head); + E oldest = elementAt(slot); + elements[slot] = null; + aggregates[slot] = null; + head++; + modCount++; + return oldest; + } + + /** + * Returns the aggregate of every element currently in the window, combined from the oldest to + * the newest. + * + * @return the aggregate of the window + * @throws NoSuchElementException if the window is empty + */ + public A aggregate() { + if (isEmpty()) { + throw new NoSuchElementException("The aggregate of an empty window is undefined"); + } + if (head == flip) { + return backAggregate; + } + A frontAggregate = aggregateAt(slotOf(head)); + return tail == flip ? frontAggregate : requireNonNullResult(combiner.apply(frontAggregate, backAggregate), "combiner"); + } + + /** + * Returns the element at the given position, counting from the oldest one. + * + * @param index zero-based position, {@code 0} being the oldest element in the window + * @return the element at that position + * @throws IndexOutOfBoundsException if {@code index} is negative or not smaller than {@link #size()} + */ + public E get(int index) { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException("Index " + index + " is out of bounds for a window of size " + size()); + } + return elementAt(slotOf(head + index)); + } + + /** + * Returns the oldest element without removing it. + * + * @return the element that would be evicted next + * @throws NoSuchElementException if the window is empty + */ + public E peekOldest() { + if (isEmpty()) { + throw new NoSuchElementException("The window is empty"); + } + return elementAt(slotOf(head)); + } + + /** + * Returns the most recently added element without removing it. + * + * @return the newest element + * @throws NoSuchElementException if the window is empty + */ + public E peekNewest() { + if (isEmpty()) { + throw new NoSuchElementException("The window is empty"); + } + return elementAt(slotOf(tail - 1)); + } + + /** + * Returns the number of elements currently in the window. + * + * @return the current size, never greater than {@link #capacity()} + */ + public int size() { + return (int) (tail - head); + } + + /** + * Returns the maximum number of elements the window can hold. + * + * @return the capacity given at construction time + */ + public int capacity() { + return capacity; + } + + /** + * Tells whether the window holds no elements. + * + * @return {@code true} if the window is empty + */ + public boolean isEmpty() { + return head == tail; + } + + /** + * Tells whether the next insertion will evict the oldest element. + * + * @return {@code true} if the window is saturated + */ + public boolean isFull() { + return size() == capacity; + } + + /** + * Discards every element, leaving the window as if freshly constructed. + */ + public void clear() { + Arrays.fill(elements, null); + Arrays.fill(aggregates, null); + head = 0; + flip = 0; + tail = 0; + backAggregate = null; + modCount++; + } + + /** + * Returns a snapshot of the window, ordered from the oldest element to the newest one. The list + * is detached from the window: later insertions do not affect it. + * + * @return a new list holding the current contents of the window + */ + public List toList() { + List snapshot = new ArrayList<>(size()); + for (long i = head; i < tail; i++) { + snapshot.add(elementAt(slotOf(i))); + } + return snapshot; + } + + /** + * Returns a fail-fast iterator walking the window from the oldest element to the newest one. + * + * @return an iterator over the current contents of the window + */ + @Override + public Iterator iterator() { + return new WindowIterator(); + } + + @Override + public String toString() { + return toList().toString(); + } + + /** + * Turns the back section into the front section by walking it backwards and storing, for every + * slot, the aggregate of that element and all younger ones. Called only when the front section is + * exhausted, so each element takes part in at most one such pass. + */ + private void rotateBackSectionToFront() { + A suffix = null; + for (long i = tail - 1; i >= head; i--) { + int slot = slotOf(i); + A own = aggregateAt(slot); + suffix = suffix == null ? own : requireNonNullResult(combiner.apply(own, suffix), "combiner"); + aggregates[slot] = suffix; + } + flip = tail; + backAggregate = null; + } + + private int slotOf(long absoluteIndex) { + return (int) (absoluteIndex % capacity); + } + + @SuppressWarnings("unchecked") + private E elementAt(int slot) { + return (E) elements[slot]; + } + + @SuppressWarnings("unchecked") + private A aggregateAt(int slot) { + return (A) aggregates[slot]; + } + + private static T requireNonNullResult(T value, String producer) { + return Objects.requireNonNull(value, "The " + producer + " of a SlidingWindowAggregator must not return null"); + } + + private final class WindowIterator implements Iterator { + private long cursor = head; + private final int expectedModCount = modCount; + + @Override + public boolean hasNext() { + return cursor < tail; + } + + @Override + public E next() { + if (expectedModCount != modCount) { + throw new ConcurrentModificationException(); + } + if (!hasNext()) { + throw new NoSuchElementException("The iterator has been exhausted"); + } + return elementAt(slotOf(cursor++)); + } + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java b/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java index cd5dc580b694..dbdb2d806209 100644 --- a/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java +++ b/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java @@ -63,7 +63,8 @@ public void add(final E element) { * * @param index the index at which the element is to be placed * @param element the element to be inserted at the specified index - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the number of elements + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the number of elements */ public void put(final int index, E element) { if (index < 0) { @@ -82,7 +83,8 @@ public void put(final int index, E element) { * * @param index the index of the element to retrieve * @return the element at the specified index - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the current size + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the current size */ @SuppressWarnings("unchecked") public E get(final int index) { @@ -97,7 +99,8 @@ public E get(final int index) { * * @param index the index of the element to be removed * @return the element that was removed from the array - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the current size + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the current size */ public E remove(final int index) { if (index < 0 || index >= size) { @@ -127,6 +130,21 @@ public boolean isEmpty() { return size == 0; } + /** + * Checks whether the array contains the specified element. + * + * @param element the element to check for + * @return true if the array contains the specified element, false otherwise + */ + public boolean contains(final E element) { + for (int i = 0; i < size; i++) { + if (Objects.equals(elements[i], element)) { + return true; + } + } + return false; + } + /** * Returns a sequential stream with this collection as its source. * @@ -137,7 +155,8 @@ public Stream stream() { } /** - * Ensures that the array has enough capacity to hold the specified number of elements. + * Ensures that the array has enough capacity to hold the specified number of + * elements. * * @param minCapacity the minimum capacity required */ @@ -150,7 +169,8 @@ private void ensureCapacity(int minCapacity) { /** * Removes the element at the specified index without resizing the array. - * This method shifts any subsequent elements to the left and clears the last element. + * This method shifts any subsequent elements to the left and clears the last + * element. * * @param index the index of the element to remove */ @@ -163,7 +183,8 @@ private void fastRemove(int index) { } /** - * Returns a string representation of the array, including only the elements that are currently stored. + * Returns a string representation of the array, including only the elements + * that are currently stored. * * @return a string containing the elements in the array */ @@ -227,7 +248,9 @@ public E next() { /** * Removes the last element returned by this iterator. * - * @throws IllegalStateException if the next method has not yet been called, or the remove method has already been called after the last call to the next method + * @throws IllegalStateException if the next method has not yet been called, or + * the remove method has already been called after + * the last call to the next method */ @Override public void remove() { @@ -242,7 +265,8 @@ public void remove() { /** * Checks for concurrent modifications to the array during iteration. * - * @throws ConcurrentModificationException if the array has been modified structurally + * @throws ConcurrentModificationException if the array has been modified + * structurally */ private void checkForComodification() { if (modCount != expectedModCount) { @@ -251,7 +275,8 @@ private void checkForComodification() { } /** - * Performs the given action for each remaining element in the iterator until all elements have been processed. + * Performs the given action for each remaining element in the iterator until + * all elements have been processed. * * @param action the action to be performed for each element * @throws NullPointerException if the specified action is null diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java index 70699a9461f7..1b5f765843be 100644 --- a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java +++ b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java @@ -1,6 +1,7 @@ package com.thealgorithms.datastructures.graphs; import java.util.Arrays; +import java.util.PriorityQueue; /** * Dijkstra's algorithm for finding the shortest path from a single source vertex to all other vertices in a graph. @@ -18,6 +19,21 @@ public DijkstraAlgorithm(int vertexCount) { this.vertexCount = vertexCount; } + private static class Node implements Comparable { + int id; + int distance; + + Node(int id, int distance) { + this.id = id; + this.distance = distance; + } + + @Override + public int compareTo(Node other) { + return Integer.compare(this.distance, other.distance); + } + } + /** * Executes Dijkstra's algorithm on the provided graph to find the shortest paths from the source vertex to all other vertices. * @@ -36,18 +52,25 @@ public int[] run(int[][] graph, int source) { int[] distances = new int[vertexCount]; boolean[] processed = new boolean[vertexCount]; + PriorityQueue unprocessed = new PriorityQueue<>(); Arrays.fill(distances, Integer.MAX_VALUE); - Arrays.fill(processed, false); distances[source] = 0; + unprocessed.add(new Node(source, 0)); + + while (!unprocessed.isEmpty()) { + Node current = unprocessed.poll(); + int u = current.id; - for (int count = 0; count < vertexCount - 1; count++) { - int u = getMinDistanceVertex(distances, processed); + if (processed[u]) { + continue; + } processed[u] = true; for (int v = 0; v < vertexCount; v++) { if (!processed[v] && graph[u][v] != 0 && distances[u] != Integer.MAX_VALUE && distances[u] + graph[u][v] < distances[v]) { distances[v] = distances[u] + graph[u][v]; + unprocessed.add(new Node(v, distances[v])); } } } @@ -56,27 +79,6 @@ public int[] run(int[][] graph, int source) { return distances; } - /** - * Finds the vertex with the minimum distance value from the set of vertices that have not yet been processed. - * - * @param distances The array of current shortest distances from the source vertex. - * @param processed The array indicating whether each vertex has been processed. - * @return The index of the vertex with the minimum distance value. - */ - private int getMinDistanceVertex(int[] distances, boolean[] processed) { - int min = Integer.MAX_VALUE; - int minIndex = -1; - - for (int v = 0; v < vertexCount; v++) { - if (!processed[v] && distances[v] <= min) { - min = distances[v]; - minIndex = v; - } - } - - return minIndex; - } - /** * Prints the shortest distances from the source vertex to all other vertices. * diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java deleted file mode 100644 index a686b808a970..000000000000 --- a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.thealgorithms.datastructures.graphs; - -import java.util.Arrays; -import java.util.Set; -import java.util.TreeSet; -import org.apache.commons.lang3.tuple.Pair; - -/** - * Dijkstra's algorithm for finding the shortest path from a single source vertex to all other vertices in a graph. - */ -public class DijkstraOptimizedAlgorithm { - - private final int vertexCount; - - /** - * Constructs a Dijkstra object with the given number of vertices. - * - * @param vertexCount The number of vertices in the graph. - */ - public DijkstraOptimizedAlgorithm(int vertexCount) { - this.vertexCount = vertexCount; - } - - /** - * Executes Dijkstra's algorithm on the provided graph to find the shortest paths from the source vertex to all other vertices. - * - * The graph is represented as an adjacency matrix where {@code graph[i][j]} represents the weight of the edge from vertex {@code i} - * to vertex {@code j}. A value of 0 indicates no edge exists between the vertices. - * - * @param graph The graph represented as an adjacency matrix. - * @param source The source vertex. - * @return An array where the value at each index {@code i} represents the shortest distance from the source vertex to vertex {@code i}. - * @throws IllegalArgumentException if the source vertex is out of range. - */ - public int[] run(int[][] graph, int source) { - if (source < 0 || source >= vertexCount) { - throw new IllegalArgumentException("Incorrect source"); - } - - int[] distances = new int[vertexCount]; - boolean[] processed = new boolean[vertexCount]; - Set> unprocessed = new TreeSet<>(); - - Arrays.fill(distances, Integer.MAX_VALUE); - Arrays.fill(processed, false); - distances[source] = 0; - unprocessed.add(Pair.of(0, source)); - - while (!unprocessed.isEmpty()) { - Pair distanceAndU = unprocessed.iterator().next(); - unprocessed.remove(distanceAndU); - int u = distanceAndU.getRight(); - processed[u] = true; - - for (int v = 0; v < vertexCount; v++) { - if (!processed[v] && graph[u][v] != 0 && distances[u] != Integer.MAX_VALUE && distances[u] + graph[u][v] < distances[v]) { - unprocessed.remove(Pair.of(distances[v], v)); - distances[v] = distances[u] + graph[u][v]; - unprocessed.add(Pair.of(distances[v], v)); - } - } - } - - return distances; - } -} diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java b/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java new file mode 100644 index 000000000000..3ce8696f55ff --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java @@ -0,0 +1,114 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Multi-source Breadth-First Search (BFS) implementation for the Rotting Oranges problem. + * + *

Algorithm explanation: + * https://en.wikipedia.org/wiki/Breadth-first_search + * + *

Problem reference: + * https://leetcode.com/problems/rotting-oranges/ + * + *

Given a grid where: + *

    + *
  • 0 represents an empty cell
  • + *
  • 1 represents a fresh orange
  • + *
  • 2 represents a rotten orange
  • + *
+ * + *

Returns the minimum number of minutes required for all fresh oranges + * to become rotten. Returns {@code -1} if it is impossible. + * + *

Time Complexity: O(m Γ— n) + *
Space Complexity: O(m Γ— n) + */ +public class RottingOranges { + + private static final int[] DEL_ROW = {-1, 0, 1, 0}; + private static final int[] DEL_COL = {0, 1, 0, -1}; + + private static final class Cell { + private final int row; + private final int col; + private final int minute; + + Cell(int row, int col, int minute) { + this.row = row; + this.col = col; + this.minute = minute; + } + } + + /** + * Executes the Rotting Oranges algorithm. + * + * @param grid the input grid + * @return minimum minutes required to rot all fresh oranges, + * or -1 if impossible + */ + public int run(int[][] grid) { + + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int rows = grid.length; + int cols = grid[0].length; + + // Create a copy so original input is not modified + int[][] copy = new int[rows][cols]; + + for (int i = 0; i < rows; i++) { + copy[i] = grid[i].clone(); + } + + Queue queue = new LinkedList<>(); + int freshOranges = 0; + + // Find all rotten oranges and count fresh oranges + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + + if (copy[row][col] == 2) { + queue.offer(new Cell(row, col, 0)); + } else if (copy[row][col] == 1) { + freshOranges++; + } + } + } + + if (freshOranges == 0) { + return 0; + } + + int rottedFresh = 0; + int minutes = 0; + + // Multi-source BFS + while (!queue.isEmpty()) { + + Cell current = queue.poll(); + + minutes = Math.max(minutes, current.minute); + + for (int i = 0; i < 4; i++) { + + int newRow = current.row + DEL_ROW[i]; + int newCol = current.col + DEL_COL[i]; + + if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && copy[newRow][newCol] == 1) { + + copy[newRow][newCol] = 2; + rottedFresh++; + + queue.offer(new Cell(newRow, newCol, current.minute + 1)); + } + } + } + + return rottedFresh == freshOranges ? minutes : -1; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/Readme.md b/src/main/java/com/thealgorithms/datastructures/hashmap/Readme.md index 252b06ea59b0..4400a97d8128 100644 --- a/src/main/java/com/thealgorithms/datastructures/hashmap/Readme.md +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/Readme.md @@ -2,6 +2,8 @@ A hash map organizes data so you can quickly look up values for a given key. +> Note: The term β€œhash map” refers to the data structure concept, while `HashMap` refers specifically to Java’s implementation. + ## Strengths: - **Fast lookups**: Lookups take O(1) time on average. - **Flexible keys**: Most data types can be used for keys, as long as they're hashable. diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMap.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMap.java new file mode 100644 index 000000000000..f6e09ec623b6 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMap.java @@ -0,0 +1,115 @@ +package com.thealgorithms.datastructures.hashmap.hashing; + +/** + * Immutable HashMap implementation using separate chaining. + * + *

This HashMap does not allow modification of existing instances. + * Any update operation returns a new ImmutableHashMap. + * + * @param key type + * @param value type + */ +public final class ImmutableHashMap { + + private static final int DEFAULT_CAPACITY = 16; + + private final Node[] table; + private final int size; + + /** + * Private constructor to enforce immutability. + */ + private ImmutableHashMap(Node[] table, int size) { + this.table = table; + this.size = size; + } + + /** + * Creates an empty ImmutableHashMap. + * + * @param key type + * @param value type + * @return empty ImmutableHashMap + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static ImmutableHashMap empty() { + Node[] table = (Node[]) new Node[DEFAULT_CAPACITY]; + return new ImmutableHashMap<>(table, 0); + } + + /** + * Returns a new ImmutableHashMap with the given key-value pair added. + * + * @param key key to add + * @param value value to associate + * @return new ImmutableHashMap instance + */ + public ImmutableHashMap put(K key, V value) { + Node[] newTable = table.clone(); + int index = hash(key); + + newTable[index] = new Node<>(key, value, newTable[index]); + return new ImmutableHashMap<>(newTable, size + 1); + } + + /** + * Retrieves the value associated with the given key. + * + * @param key key to search + * @return value if found, otherwise null + */ + public V get(K key) { + int index = hash(key); + Node current = table[index]; + + while (current != null) { + if ((key == null && current.key == null) || (key != null && key.equals(current.key))) { + return current.value; + } + current = current.next; + } + return null; + } + + /** + * Checks whether the given key exists in the map. + * + * @param key key to check + * @return true if key exists, false otherwise + */ + public boolean containsKey(K key) { + return get(key) != null; + } + + /** + * Returns the number of key-value pairs. + * + * @return size of the map + */ + public int size() { + return size; + } + + /** + * Computes hash index for a given key. + */ + private int hash(K key) { + return key == null ? 0 : (key.hashCode() & Integer.MAX_VALUE) % table.length; + } + + /** + * Node class for separate chaining. + */ + private static final class Node { + + private final K key; + private final V value; + private final Node next; + + private Node(K key, V value, Node next) { + this.key = key; + this.value = value; + this.next = next; + } + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueue.java b/src/main/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueue.java new file mode 100644 index 000000000000..ad7229760fd0 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueue.java @@ -0,0 +1,327 @@ +package com.thealgorithms.datastructures.heaps; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * An addressable (indexed) min-priority queue with O(log n) updates. + * + *

Key features: + *

    + *
  • Each element E is tracked by a handle (its current heap index) via a map, + * enabling O(log n) {@code remove(e)} and O(log n) key updates + * ({@code changeKey/decreaseKey/increaseKey}).
  • + *
  • The queue order is determined by the provided {@link Comparator}. If the + * comparator is {@code null}, elements must implement {@link Comparable} + * (same contract as {@link java.util.PriorityQueue}).
  • + *
  • By default this implementation uses {@link IdentityHashMap} for the index + * mapping to avoid issues with duplicate-equals elements or mutable equals/hashCode. + * If you need value-based equality, switch to {@code HashMap} and read the caveats + * in the class-level Javadoc carefully.
  • + *
+ * + *

IMPORTANT contracts

+ *
    + *
  • Do not mutate comparator-relevant fields of an element directly while it is + * inside the queue. Always use {@code changeKey}/{@code decreaseKey}/{@code increaseKey} + * so the heap can be restored accordingly.
  • + *
  • If you replace {@link IdentityHashMap} with {@link HashMap}, you must ensure: + * (a) no two distinct elements are {@code equals()}-equal at the same time in the queue, and + * (b) {@code equals/hashCode} of elements remain stable while enqueued.
  • + *
  • {@code peek()} returns {@code null} when empty (matching {@link java.util.PriorityQueue}).
  • + *
  • Not thread-safe.
  • + *
+ * + *

Complexities: + * {@code offer, poll, remove(e), changeKey, decreaseKey, increaseKey} are O(log n); + * {@code peek, isEmpty, size, contains} are O(1). + */ +public class IndexedPriorityQueue { + + /** Binary heap storage (min-heap). */ + private Object[] heap; + + /** Number of elements in the heap. */ + private int size; + + /** Comparator used for ordering; if null, elements must be Comparable. */ + private final Comparator cmp; + + /** + * Index map: element -> current heap index. + *

We use IdentityHashMap by default to: + *

    + *
  • allow duplicate-equals elements;
  • + *
  • avoid corruption when equals/hashCode are mutable or not ID-based.
  • + *
+ * If you prefer value-based semantics, replace with HashMap and + * respect the warnings in the class Javadoc. + */ + private final IdentityHashMap index; + + private static final int DEFAULT_INITIAL_CAPACITY = 11; + + public IndexedPriorityQueue() { + this(DEFAULT_INITIAL_CAPACITY, null); + } + + public IndexedPriorityQueue(Comparator cmp) { + this(DEFAULT_INITIAL_CAPACITY, cmp); + } + + public IndexedPriorityQueue(int initialCapacity, Comparator cmp) { + if (initialCapacity < 1) { + throw new IllegalArgumentException("initialCapacity < 1"); + } + this.heap = new Object[initialCapacity]; + this.cmp = cmp; + this.index = new IdentityHashMap<>(); + } + + /** Returns current number of elements. */ + public int size() { + return size; + } + + /** Returns {@code true} if empty. */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns the minimum element without removing it, or {@code null} if empty. + * Matches {@link java.util.PriorityQueue#peek()} behavior. + */ + @SuppressWarnings("unchecked") + public E peek() { + return size == 0 ? null : (E) heap[0]; + } + + /** + * Inserts the specified element (O(log n)). + * @throws NullPointerException if {@code e} is null + * @throws ClassCastException if {@code cmp == null} and {@code e} is not Comparable, + * or if incompatible with other elements + */ + public boolean offer(E e) { + Objects.requireNonNull(e, "element is null"); + if (size >= heap.length) { + grow(size + 1); + } + // Insert at the end and bubble up. siftUp will maintain 'index' for all touched nodes. + siftUp(size, e); + size++; + return true; + } + + /** + * Removes and returns the minimum element (O(log n)), or {@code null} if empty. + */ + @SuppressWarnings("unchecked") + public E poll() { + if (size == 0) { + return null; + } + E min = (E) heap[0]; + removeAt(0); // updates map and heap structure + return min; + } + + /** + * Removes one occurrence of the specified element e (O(log n)) if present. + * Uses the index map for O(1) lookup. + */ + public boolean remove(Object o) { + Integer i = index.get(o); + if (i == null) { + return false; + } + removeAt(i); + return true; + } + + /** O(1): returns whether the queue currently contains the given element reference. */ + public boolean contains(Object o) { + return index.containsKey(o); + } + + /** Clears the heap and the index map. */ + public void clear() { + Arrays.fill(heap, 0, size, null); + index.clear(); + size = 0; + } + + // ------------------------------------------------------------------------------------ + // Key update API + // ------------------------------------------------------------------------------------ + + /** + * Changes comparator-relevant fields of {@code e} via the provided {@code mutator}, + * then restores the heap in O(log n) by bubbling in the correct direction. + * + *

IMPORTANT: The mutator must not change {@code equals/hashCode} of {@code e} + * if you migrate this implementation to value-based indexing (HashMap). + * + * @throws IllegalArgumentException if {@code e} is not in the queue + */ + public void changeKey(E e, Consumer mutator) { + Integer i = index.get(e); + if (i == null) { + throw new IllegalArgumentException("Element not in queue"); + } + // Mutate fields used by comparator (do NOT mutate equality/hash if using value-based map) + mutator.accept(e); + // Try bubbling up; if no movement occurred, bubble down. + if (!siftUp(i)) { + siftDown(i); + } + } + + /** + * Faster variant if the new key is strictly smaller (higher priority). + * Performs a single sift-up (O(log n)). + */ + public void decreaseKey(E e, Consumer mutator) { + Integer i = index.get(e); + if (i == null) { + throw new IllegalArgumentException("Element not in queue"); + } + mutator.accept(e); + siftUp(i); + } + + /** + * Faster variant if the new key is strictly larger (lower priority). + * Performs a single sift-down (O(log n)). + */ + public void increaseKey(E e, Consumer mutator) { + Integer i = index.get(e); + if (i == null) { + throw new IllegalArgumentException("Element not in queue"); + } + mutator.accept(e); + siftDown(i); + } + + // ------------------------------------------------------------------------------------ + // Internal utilities + // ------------------------------------------------------------------------------------ + + /** Grows the internal array to accommodate at least {@code minCapacity}. */ + private void grow(int minCapacity) { + int old = heap.length; + int pref = (old < 64) ? old + 2 : old + (old >> 1); // +2 if small, else +50% + int newCap = Math.max(minCapacity, pref); + heap = Arrays.copyOf(heap, newCap); + } + + @SuppressWarnings("unchecked") + private int compare(E a, E b) { + if (cmp != null) { + return cmp.compare(a, b); + } + return ((Comparable) a).compareTo(b); + } + + /** + * Inserts item {@code x} at position {@code k}, bubbling up while maintaining the heap. + * Also maintains the index map for all moved elements. + */ + @SuppressWarnings("unchecked") + private void siftUp(int k, E x) { + while (k > 0) { + int p = (k - 1) >>> 1; + E e = (E) heap[p]; + if (compare(x, e) >= 0) { + break; + } + heap[k] = e; + index.put(e, k); + k = p; + } + heap[k] = x; + index.put(x, k); + } + + /** + * Attempts to bubble up the element currently at {@code k}. + * @return true if it moved; false otherwise. + */ + @SuppressWarnings("unchecked") + private boolean siftUp(int k) { + int orig = k; + E x = (E) heap[k]; + while (k > 0) { + int p = (k - 1) >>> 1; + E e = (E) heap[p]; + if (compare(x, e) >= 0) { + break; + } + heap[k] = e; + index.put(e, k); + k = p; + } + if (k != orig) { + heap[k] = x; + index.put(x, k); + return true; + } + return false; + } + + /** Bubbles down the element currently at {@code k}. */ + @SuppressWarnings("unchecked") + private void siftDown(int k) { + int n = size; + E x = (E) heap[k]; + int half = n >>> 1; // loop while k has at least one child + while (k < half) { + int child = (k << 1) + 1; // assume left is smaller + E c = (E) heap[child]; + int r = child + 1; + if (r < n && compare(c, (E) heap[r]) > 0) { + child = r; + c = (E) heap[child]; + } + if (compare(x, c) <= 0) { + break; + } + heap[k] = c; + index.put(c, k); + k = child; + } + heap[k] = x; + index.put(x, k); + } + + /** + * Removes the element at heap index {@code i}, restoring the heap afterwards. + *

Returns nothing; the standard {@code PriorityQueue} returns a displaced + * element in a rare case to help its iterator. We don't need that here, so + * we keep the API simple. + */ + @SuppressWarnings("unchecked") + private void removeAt(int i) { + int n = --size; // last index after removal + E moved = (E) heap[n]; + E removed = (E) heap[i]; + heap[n] = null; // help GC + index.remove(removed); // drop mapping for removed element + + if (i == n) { + return; // removed last element; done + } + + heap[i] = moved; + index.put(moved, i); + + // Try sift-up first (cheap if key decreased); if no movement, sift-down. + if (!siftUp(i)) { + siftDown(i); + } + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java index 58898ddc0fcf..462521e464cc 100644 --- a/src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java +++ b/src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java @@ -7,7 +7,7 @@ *

* A linked list is similar to an array, it holds values. However, links in a * linked list do not have indexes. With a linked list you do not need to - * predetermine it's size as it grows and shrinks as it is edited. This is an + * predetermine its size as it grows and shrinks as it is edited. This is an * example of a double ended, doubly linked list. Each link references the next * link and the previous one. * diff --git a/src/main/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedList.java new file mode 100644 index 000000000000..0ee788db2ff9 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedList.java @@ -0,0 +1,46 @@ +package com.thealgorithms.datastructures.lists; + +/** + * Returns the middle node of a singly linked list using the two-pointer technique. + * + *

The {@code slow} pointer advances by one node per iteration while {@code fast} advances by two. + * When {@code fast == null} or {@code fast.next == null}, {@code slow} points to the middle node. + * For even-length lists, this returns the second middle node.

+ * + *

This method does not modify the input list.

+ * + *

Reference: https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare

+ * + *

Complexity:

+ *
    + *
  • Time: {@code O(n)}
  • + *
  • Space: {@code O(1)}
  • + *
+ */ +public final class MiddleOfLinkedList { + + private MiddleOfLinkedList() { + } + + /** + * Returns the middle node of the list. + * + * @param head the head of the singly linked list; may be {@code null} + * @return the middle node (second middle for even-sized lists), or {@code null} if {@code head} is {@code null} + */ + public static SinglyLinkedListNode middleNode(final SinglyLinkedListNode head) { + if (head == null) { + return null; + } + + SinglyLinkedListNode slow = head; + SinglyLinkedListNode fast = head; + + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + return slow; + } +} diff --git a/src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java similarity index 84% rename from src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java rename to src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java index c81476eaec32..7bb16921b9ef 100644 --- a/src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java +++ b/src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java @@ -1,4 +1,4 @@ -package com.thealgorithms.misc; +package com.thealgorithms.datastructures.lists; import java.util.Stack; @@ -9,6 +9,13 @@ * * See more: * https://www.geeksforgeeks.org/function-to-check-if-a-singly-linked-list-is-palindrome/ + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ @SuppressWarnings("rawtypes") public final class PalindromeSinglyLinkedList { diff --git a/src/main/java/com/thealgorithms/datastructures/lists/QuickSortLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/QuickSortLinkedList.java index f018781ada70..a7c2da913cd5 100644 --- a/src/main/java/com/thealgorithms/datastructures/lists/QuickSortLinkedList.java +++ b/src/main/java/com/thealgorithms/datastructures/lists/QuickSortLinkedList.java @@ -20,7 +20,7 @@ * List lessThanPivot : 3 -> 1 -> 2 -> 4 * List greaterThanPivot : 5 -> 8 -> 10 -> 7 -> 9 -> 6 * - * -> reccur for lessThanPivot and greaterThanPivot + * -> recur for lessThanPivot and greaterThanPivot * * lessThanPivot : * current pivot : 3 @@ -32,7 +32,7 @@ * lessThanPivot : null * greaterThanPivot : 8 -> 10 -> 7 -> 9 -> 6 * - * By following the above pattern, reccuring tree will form like below : + * By following the above pattern, the recursion tree will form like below : * * List-> 5 -> 3 -> 8 -> 1 -> 10 -> 2 -> 7 -> 4 -> 9 -> 6 * @@ -66,7 +66,7 @@ * (N) (N) (N) (N) * * - * -> After this the tree will reccur back (or backtrack) + * -> After this the tree will recur back (or backtrack) * and the returning list from left and right subtree will attach * themselves around pivot. * i.e. , diff --git a/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java new file mode 100644 index 000000000000..200c636ce1ab --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java @@ -0,0 +1,105 @@ +package com.thealgorithms.datastructures.lists; + +import java.util.Objects; + +/** + * A Self-Organizing Linked List implementation using the Move-To-Front (MTF) strategy. + * When an element is searched, it is automatically moved to the head of the list + * to optimize subsequent lookups. + * + * @param the type of elements held in this list + */ +public class SelfOrganizingLinkedList { + + /** + * Node structure for the self-organizing linked list. + * + * @param the type of element held in this node + */ + private static class Node { + E value; + Node next; + + Node(E value) { + this.value = value; + this.next = null; + } + } + + private Node head; + private int size; + + public SelfOrganizingLinkedList() { + this.size = 0; + this.head = null; + } + + /** + * Inserts a new value at the end of the list. + * + * @param value the element to add + */ + public void insert(E value) { + Node newNode = new Node<>(value); + if (head == null) { + head = newNode; + } else { + Node temp = head; + while (temp.next != null) { + temp = temp.next; + } + temp.next = newNode; + } + size++; + } + + /** + * Searches for a value in the list. + * If found, moves the node to the front (head) of the list. + * + * @param key the value to search for + * @return true if the element is present, false otherwise + */ + public boolean search(E key) { + if (head == null) { + return false; + } + // If the key is already at the head, no pointers need to be rewired + if (Objects.equals(head.value, key)) { + return true; + } + + Node prev = head; + Node curr = head.next; + + while (curr != null && !Objects.equals(curr.value, key)) { + prev = curr; + curr = curr.next; + } + + if (curr == null) { + return false; + } + + // Unlink curr from its current position and move it to head + prev.next = curr.next; + curr.next = head; + head = curr; + return true; + } + + /** Gets the current head value of the list. */ + public E getHeadValue() { + return head != null ? head.value : null; + } + + /** Returns the size of the list. */ + public int getSize() { + return size; + } + + /** Returns true if the list contains no elements. */ + public boolean isEmpty() { + return size == 0; + } +} 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/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java b/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java index b877a5843b98..5d4157deef70 100644 --- a/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java +++ b/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java @@ -85,7 +85,7 @@ private void swim(int pos) { private void sink(int pos) { // Check if node's position is that of parent node while (2 * pos <= nItems) { - int current = 2 * pos; // Jump to the positon of child node + int current = 2 * pos; // Jump to the position of child node // Compare both the children for the greater one if (current < nItems && queueArray[current] < queueArray[current + 1]) { current++; diff --git a/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java b/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java new file mode 100644 index 000000000000..79275dcefe20 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java @@ -0,0 +1,28 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.Queue; + +/** + * Reverse a queue using recursion. + */ +public final class ReverseQueueRecursion { + private ReverseQueueRecursion() { + // private constructor to prevent instantiation + } + + /** + * Reverses the given queue recursively. + * + * @param queue the queue to reverse + * @param the type of elements in the queue + */ + public static void reverseQueue(final Queue queue) { + if (queue == null || queue.isEmpty()) { + return; + } + + final T front = queue.poll(); + reverseQueue(queue); + queue.add(front); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java b/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java new file mode 100644 index 000000000000..a943b0028974 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java @@ -0,0 +1,186 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @brief Thread-safe bounded queue implementation using ReentrantLock and Condition variables + * @details A blocking queue that supports multiple producers and consumers. + * Uses a circular buffer internally with lock-based synchronization to ensure + * thread safety. Producers block when the queue is full, and consumers block + * when the queue is empty. + * @see
Producer-Consumer Problem + */ +public class ThreadSafeQueue { + + private final Object[] buffer; + private final int capacity; + private int head; + private int tail; + private int count; + private final ReentrantLock lock; + private final Condition notFull; + private final Condition notEmpty; + + /** + * @brief Constructs a ThreadSafeQueue with the specified capacity + * @param capacity the maximum number of elements the queue can hold + * @throws IllegalArgumentException if capacity is less than or equal to zero + */ + public ThreadSafeQueue(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero."); + } + this.capacity = capacity; + this.buffer = new Object[capacity]; + this.head = 0; + this.tail = 0; + this.count = 0; + this.lock = new ReentrantLock(); + this.notFull = lock.newCondition(); + this.notEmpty = lock.newCondition(); + } + + /** + * @brief Adds an element to the tail of the queue, blocking if full + * @param item the element to add + * @throws InterruptedException if the thread is interrupted while waiting + * @throws IllegalArgumentException if the item is null + */ + public void enqueue(T item) throws InterruptedException { + if (item == null) { + throw new IllegalArgumentException("Cannot enqueue null item."); + } + + lock.lock(); + try { + while (count == capacity) { + notFull.await(); + } + buffer[tail] = item; + tail = (tail + 1) % capacity; + count++; + notEmpty.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * @brief Removes and returns the element at the head of the queue, blocking if empty + * @return the element at the head of the queue + * @throws InterruptedException if the thread is interrupted while waiting + */ + @SuppressWarnings("unchecked") + public T dequeue() throws InterruptedException { + lock.lock(); + try { + while (count == 0) { + notEmpty.await(); + } + T item = (T) buffer[head]; + buffer[head] = null; + head = (head + 1) % capacity; + count--; + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + /** + * @brief Adds an element to the tail of the queue without blocking + * @param item the element to add + * @return true if the element was added, false if the queue was full + * @throws IllegalArgumentException if the item is null + */ + public boolean offer(T item) { + if (item == null) { + throw new IllegalArgumentException("Cannot enqueue null item."); + } + + lock.lock(); + try { + if (count == capacity) { + return false; + } + buffer[tail] = item; + tail = (tail + 1) % capacity; + count++; + notEmpty.signalAll(); + return true; + } finally { + lock.unlock(); + } + } + + /** + * @brief Removes and returns the element at the head without blocking + * @return the element at the head, or null if the queue is empty + */ + @SuppressWarnings("unchecked") + public T poll() { + lock.lock(); + try { + if (count == 0) { + return null; + } + T item = (T) buffer[head]; + buffer[head] = null; + head = (head + 1) % capacity; + count--; + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + /** + * @brief Returns the number of elements in the queue + * @return the current size of the queue + */ + public int size() { + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } + + /** + * @brief Checks if the queue is empty + * @return true if the queue contains no elements + */ + public boolean isEmpty() { + lock.lock(); + try { + return count == 0; + } finally { + lock.unlock(); + } + } + + /** + * @brief Checks if the queue is full + * @return true if the queue has reached its capacity + */ + public boolean isFull() { + lock.lock(); + try { + return count == capacity; + } finally { + lock.unlock(); + } + } + + /** + * @brief Returns the maximum capacity of the queue + * @return the capacity + */ + public int capacity() { + return capacity; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/trees/BinaryTreeToString.java b/src/main/java/com/thealgorithms/datastructures/trees/BinaryTreeToString.java new file mode 100644 index 000000000000..2f9b3b489d56 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/BinaryTreeToString.java @@ -0,0 +1,100 @@ +package com.thealgorithms.datastructures.trees; + +/** + * Leetcode 606: Construct String from Binary Tree: + * https://leetcode.com/problems/construct-string-from-binary-tree/ + * + * Utility class to convert a {@link BinaryTree} into its string representation. + *

+ * The conversion follows a preorder traversal pattern (root β†’ left β†’ right) + * and uses parentheses to denote the tree structure. + * Empty parentheses "()" are used to explicitly represent missing left children + * when a right child exists, ensuring the structure is unambiguous. + *

+ * + *

Rules:

+ *
    + *
  • Each node is represented as {@code (value)}.
  • + *
  • If a node has only a right child, include {@code ()} before the right + * child + * to indicate the missing left child.
  • + *
  • If a node has no children, it appears as just {@code (value)}.
  • + *
  • The outermost parentheses are removed from the final string.
  • + *
+ * + *

Example:

+ * + *
+ *     Input tree:
+ *           1
+ *          / \
+ *         2   3
+ *          \
+ *           4
+ *
+ *     Output string:
+ *     "1(2()(4))(3)"
+ * 
+ * + *

+ * This implementation matches the logic from LeetCode problem 606: + * Construct String from Binary Tree. + *

+ * + * @author Muhammad Junaid + * @see BinaryTree + */ +public class BinaryTreeToString { + + /** String builder used to accumulate the string representation. */ + private StringBuilder sb; + + /** + * Converts a binary tree (given its root node) to its string representation. + * + * @param root the root node of the binary tree + * @return the string representation of the binary tree, or an empty string if + * the tree is null + */ + public String tree2str(BinaryTree.Node root) { + if (root == null) { + return ""; + } + + sb = new StringBuilder(); + dfs(root); + + // Remove the leading and trailing parentheses added by the root call + return sb.substring(1, sb.length() - 1); + } + + /** + * Performs a recursive depth-first traversal to build the string. + * Each recursive call appends the node value and its children (if any) + * enclosed in parentheses. + * + * @param node the current node being processed + */ + private void dfs(BinaryTree.Node node) { + if (node == null) { + return; + } + + sb.append("(").append(node.data); + + // Recursively build left and right subtrees + if (node.left != null) { + dfs(node.left); + } + + // Handle the special case: right child exists but left child is null + if (node.right != null && node.left == null) { + sb.append("()"); + dfs(node.right); + } else if (node.right != null) { + dfs(node.right); + } + + sb.append(")"); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/trees/CentroidDecomposition.java b/src/main/java/com/thealgorithms/datastructures/trees/CentroidDecomposition.java new file mode 100644 index 000000000000..0b29dd6f5f5e --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/CentroidDecomposition.java @@ -0,0 +1,217 @@ +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Centroid Decomposition is a divide-and-conquer technique for trees. + * It recursively partitions a tree by finding centroids - nodes whose removal + * creates balanced subtrees (each with at most N/2 nodes). + * + *

+ * Time Complexity: O(N log N) for construction + * Space Complexity: O(N) + * + *

+ * Applications: + * - Distance queries on trees + * - Path counting problems + * - Nearest neighbor searches + * + * @see Centroid Decomposition + * @see Centroid Decomposition Tutorial + * @author lens161 + */ +public final class CentroidDecomposition { + + private CentroidDecomposition() { + } + + /** + * Represents the centroid tree structure. + */ + public static final class CentroidTree { + private final int n; + private final List> adj; + private final int[] parent; + private final int[] subtreeSize; + private final boolean[] removed; + private int root; + + /** + * Constructs a centroid tree from an adjacency list. + * + * @param adj adjacency list representation of the tree (0-indexed) + * @throws IllegalArgumentException if tree is empty or null + */ + public CentroidTree(List> adj) { + if (adj == null || adj.isEmpty()) { + throw new IllegalArgumentException("Tree cannot be empty or null"); + } + + this.n = adj.size(); + this.adj = adj; + this.parent = new int[n]; + this.subtreeSize = new int[n]; + this.removed = new boolean[n]; + Arrays.fill(parent, -1); + + // Build centroid tree starting from node 0 + this.root = decompose(0, -1); + } + + /** + * Recursively builds the centroid tree. + * + * @param u current node + * @param p parent in centroid tree + * @return centroid of current component + */ + private int decompose(int u, int p) { + int size = getSubtreeSize(u, -1); + int centroid = findCentroid(u, -1, size); + + removed[centroid] = true; + parent[centroid] = p; + + // Recursively decompose each subtree + for (int v : adj.get(centroid)) { + if (!removed[v]) { + decompose(v, centroid); + } + } + + return centroid; + } + + /** + * Calculates subtree size from node u. + * + * @param u current node + * @param p parent node (-1 for root) + * @return size of subtree rooted at u + */ + private int getSubtreeSize(int u, int p) { + subtreeSize[u] = 1; + for (int v : adj.get(u)) { + if (v != p && !removed[v]) { + subtreeSize[u] += getSubtreeSize(v, u); + } + } + return subtreeSize[u]; + } + + /** + * Finds the centroid of a subtree. + * A centroid is a node whose removal creates components with size <= totalSize/2. + * + * @param u current node + * @param p parent node + * @param totalSize total size of current component + * @return centroid node + */ + private int findCentroid(int u, int p, int totalSize) { + for (int v : adj.get(u)) { + if (v != p && !removed[v] && subtreeSize[v] > totalSize / 2) { + return findCentroid(v, u, totalSize); + } + } + return u; + } + + /** + * Gets the parent of a node in the centroid tree. + * + * @param node the node + * @return parent node in centroid tree, or -1 if root + */ + public int getParent(int node) { + if (node < 0 || node >= n) { + throw new IllegalArgumentException("Invalid node: " + node); + } + return parent[node]; + } + + /** + * Gets the root of the centroid tree. + * + * @return root node + */ + public int getRoot() { + return root; + } + + /** + * Gets the number of nodes in the tree. + * + * @return number of nodes + */ + public int size() { + return n; + } + + /** + * Returns the centroid tree structure as a string. + * Format: node -> parent (or ROOT for root node) + * + * @return string representation + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Centroid Tree:\n"); + for (int i = 0; i < n; i++) { + sb.append("Node ").append(i).append(" -> "); + if (parent[i] == -1) { + sb.append("ROOT"); + } else { + sb.append("Parent ").append(parent[i]); + } + sb.append("\n"); + } + return sb.toString(); + } + } + + /** + * Creates a centroid tree from an edge list. + * + * @param n number of nodes (0-indexed: 0 to n-1) + * @param edges list of edges where each edge is [u, v] + * @return CentroidTree object + * @throws IllegalArgumentException if n <= 0 or edges is invalid + */ + public static CentroidTree buildFromEdges(int n, int[][] edges) { + if (n <= 0) { + throw new IllegalArgumentException("Number of nodes must be positive"); + } + if (edges == null) { + throw new IllegalArgumentException("Edges cannot be null"); + } + if (edges.length != n - 1) { + throw new IllegalArgumentException("Tree must have exactly n-1 edges"); + } + + List> adj = new ArrayList<>(); + for (int i = 0; i < n; i++) { + adj.add(new ArrayList<>()); + } + + for (int[] edge : edges) { + if (edge.length != 2) { + throw new IllegalArgumentException("Each edge must have exactly 2 nodes"); + } + int u = edge[0]; + int v = edge[1]; + + if (u < 0 || u >= n || v < 0 || v >= n) { + throw new IllegalArgumentException("Invalid node in edge: [" + u + ", " + v + "]"); + } + + adj.get(u).add(v); + adj.get(v).add(u); + } + + return new CentroidTree(adj); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java b/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java index 5378a01f6642..e9c355baa4eb 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/FenwickTree.java @@ -1,29 +1,37 @@ package com.thealgorithms.datastructures.trees; +/** + * Fenwick Tree for point updates and prefix-sum queries in O(log n). + * + * @see Fenwick Tree + */ 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 +40,11 @@ public int query(int i) { } 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/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java b/src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java similarity index 99% rename from src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java rename to src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java index 236a23205180..ed67f9ae3394 100644 --- a/src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java @@ -1,4 +1,4 @@ -package com.thealgorithms.tree; +package com.thealgorithms.datastructures.trees; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/thealgorithms/datastructures/trees/KDTree.java b/src/main/java/com/thealgorithms/datastructures/trees/KDTree.java index 5190e82f74ef..f2bab248ff28 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/KDTree.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/KDTree.java @@ -406,12 +406,14 @@ private Node delete(Node root, Node node) { } else if (root.left != null) { Node min = findMin(root.left, root.getAxis()); root.point = min.point; - root.left = delete(root.left, min); + root.right = delete(root.left, min); + root.left = null; } else { return null; } + return root; } - if (root.getAxisCoordinate() < node.point.getCoordinate(root.getAxis())) { + if (node.point.getCoordinate(root.getAxis()) < root.getAxisCoordinate()) { root.left = delete(root.left, node); } else { root.right = delete(root.right, node); diff --git a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java index 57b3edc163ca..4fbb578dcf50 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java @@ -8,13 +8,18 @@ public class SegmentTree { /* Constructor which takes the size of the array and the array as a parameter*/ public SegmentTree(int n, int[] arr) { + if (arr == null) { + throw new IllegalArgumentException("Input array must not be null"); + } + if (n <= 0 || n > arr.length) { + throw new IllegalArgumentException("Size must be in the range [1, " + arr.length + "], but was " + n); + } this.n = n; int x = (int) (Math.ceil(Math.log(n) / Math.log(2))); int segSize = 2 * (int) Math.pow(2, x) - 1; this.segTree = new int[segSize]; this.arr = arr; - this.n = n; constructTree(arr, 0, n - 1, 0); } @@ -30,7 +35,7 @@ public final int constructTree(int[] arr, int start, int end, int index) { return this.segTree[index]; } - /* A function which will update the value at a index i. This will be called by the + /* A function which will update the value at an index i. This will be called by the update function internally*/ private void updateTree(int start, int end, int index, int diff, int segIndex) { if (index < start || index > end) { @@ -47,7 +52,8 @@ private void updateTree(int start, int end, int index, int diff, int segIndex) { /* A function to update the value at a particular index*/ public void update(int index, int value) { - if (index < 0 || index > n) { + // Valid positions are 0..n-1; index == n is out of bounds and must not reach arr[index]. + if (index < 0 || index >= n) { return; } @@ -73,7 +79,8 @@ private int getSumTree(int start, int end, int qStart, int qEnd, int segIndex) { /* A function to query the sum of the subarray [start...end]*/ public int getSum(int start, int end) { - if (start < 0 || end > n || start > end) { + // The last queryable position is n-1, so end == n is an out of range query. + if (start < 0 || end >= n || start > end) { return 0; } return getSumTree(0, n - 1, start, end, 0); diff --git a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java new file mode 100644 index 000000000000..40b9e8a73533 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java @@ -0,0 +1,201 @@ +package com.thealgorithms.datastructures.trees; + +/** + * 2D Segment Tree (Tree of Trees) implementation. + * This data structure supports point updates and submatrix sum queries + * in a 2D grid. It achieves this by nesting 1D Segment Trees within a 1D Segment Tree. + * + * Time Complexity: + * - Build/Initialization: O(N * M) + * - Point Update: O(log N * log M) + * - Submatrix Query: O(log N * log M) + * + * @see 2D Segment Tree + */ +public class SegmentTree2D { + + /** + * Represents a 1D Segment Tree. + * This is equivalent to your 'Sagara' struct. It manages the columns (X-axis). + */ + public static class SegmentTree1D { + private int n; + private final int[] tree; + + /** + * Initializes the 1D Segment Tree with the nearest power of 2. + * + * @param size The expected number of elements (columns). + */ + public SegmentTree1D(int size) { + n = 1; + while (n < size) { + n *= 2; + } + tree = new int[n * 2]; + } + + /** + * Recursively updates a point in the 1D tree. + */ + private void update(int index, int val, int node, int lx, int rx) { + if (rx - lx == 1) { + tree[node] = val; + return; + } + + int mid = lx + (rx - lx) / 2; + int leftChild = node * 2 + 1; + int rightChild = node * 2 + 2; + + if (index < mid) { + update(index, val, leftChild, lx, mid); + } else { + update(index, val, rightChild, mid, rx); + } + + tree[node] = tree[leftChild] + tree[rightChild]; + } + + /** + * Public wrapper to update a specific index. + * + * @param index The column index to update. + * @param val The new value. + */ + public void update(int index, int val) { + update(index, val, 0, 0, n); + } + + /** + * Retrieves the exact value at a specific leaf node. + * + * @param index The column index. + * @return The value at the given index. + */ + public int get(int index) { + return query(index, index + 1, 0, 0, n); + } + + /** + * Recursively queries the sum in a 1D range. + */ + private int query(int l, int r, int node, int lx, int rx) { + if (lx >= r || rx <= l) { + return 0; // Out of bounds + } + if (lx >= l && rx <= r) { + return tree[node]; // Fully inside + } + + int mid = lx + (rx - lx) / 2; + int leftSum = query(l, r, node * 2 + 1, lx, mid); + int rightSum = query(l, r, node * 2 + 2, mid, rx); + + return leftSum + rightSum; + } + + /** + * Public wrapper to query the sum in the range [l, r). + * + * @param l Left boundary (inclusive). + * @param r Right boundary (exclusive). + * @return The sum of the range. + */ + public int query(int l, int r) { + return query(l, r, 0, 0, n); + } + } + + // --- Start of 2D Segment Tree (equivalent to 'Sagara2D') --- + + private int n; + private final SegmentTree1D[] tree; + + /** + * Initializes the 2D Segment Tree. + * + * @param rows The number of rows in the matrix. + * @param cols The number of columns in the matrix. + */ + public SegmentTree2D(int rows, int cols) { + n = 1; + while (n < rows) { + n *= 2; + } + tree = new SegmentTree1D[n * 2]; + for (int i = 0; i < n * 2; i++) { + // Every node in the outer tree is a full 1D tree! + tree[i] = new SegmentTree1D(cols); + } + } + + /** + * Recursively updates a point in the 2D grid. + */ + private void update(int row, int col, int val, int node, int lx, int rx) { + if (rx - lx == 1) { + tree[node].update(col, val); + return; + } + + int mid = lx + (rx - lx) / 2; + int leftChild = node * 2 + 1; + int rightChild = node * 2 + 2; + + if (row < mid) { + update(row, col, val, leftChild, lx, mid); + } else { + update(row, col, val, rightChild, mid, rx); + } + + // The value of the current node's column is the sum of its children's column values + int leftVal = tree[leftChild].get(col); + int rightVal = tree[rightChild].get(col); + tree[node].update(col, leftVal + rightVal); + } + + /** + * Public wrapper to update a specific point (row, col). + * + * @param row The row index. + * @param col The column index. + * @param val The new value. + */ + public void update(int row, int col, int val) { + update(row, col, val, 0, 0, n); + } + + /** + * Recursively queries the sum in a submatrix. + */ + private int query(int top, int bottom, int left, int right, int node, int lx, int rx) { + if (lx >= bottom || rx <= top) { + return 0; // Out of bounds + } + if (lx >= top && rx <= bottom) { + // Fully inside the row range, so delegate the column query to the 1D tree + return tree[node].query(left, right); + } + + int mid = lx + (rx - lx) / 2; + int leftSum = query(top, bottom, left, right, node * 2 + 1, lx, mid); + int rightSum = query(top, bottom, left, right, node * 2 + 2, mid, rx); + + return leftSum + rightSum; + } + + /** + * Public wrapper to query the sum of a submatrix. + * Note: boundaries are [top, bottom) and [left, right). + * + * @param top Top row index (inclusive). + * @param bottom Bottom row index (exclusive). + * @param left Left column index (inclusive). + * @param right Right column index (exclusive). + * @return The sum of the submatrix. + */ + public int query(int top, int bottom, int left, int right) { + return query(top, bottom, left, right, 0, 0, n); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTree.java b/src/main/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTree.java new file mode 100644 index 000000000000..fd8876cecb70 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTree.java @@ -0,0 +1,145 @@ +/* + * TheAlgorithms (https://github.com/TheAlgorithms/Java) + * Author: Shewale41 + * This file is licensed under the MIT License. + */ + +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; +import java.util.List; + +/** + * Threaded binary tree implementation that supports insertion and + * in-order traversal without recursion or stack by using threads. + * + *

In this implementation, a node's null left/right pointers are used + * to point to the in-order predecessor/successor respectively. Two flags + * indicate whether left/right pointers are real children or threads. + * + * @see Wikipedia: + * Threaded binary tree + */ +public final class ThreadedBinaryTree { + + private Node root; + + private static final class Node { + int value; + Node left; + Node right; + boolean leftIsThread; + boolean rightIsThread; + + Node(int value) { + this.value = value; + this.left = null; + this.right = null; + this.leftIsThread = false; + this.rightIsThread = false; + } + } + + public ThreadedBinaryTree() { + this.root = null; + } + + /** + * Inserts a value into the threaded binary tree. Duplicate values are inserted + * to the right subtree (consistent deterministic rule). + * + * @param value the integer value to insert + */ + public void insert(int value) { + Node newNode = new Node(value); + if (root == null) { + root = newNode; + return; + } + + Node current = root; + Node parent = null; + + while (true) { + parent = current; + if (value < current.value) { + if (!current.leftIsThread && current.left != null) { + current = current.left; + } else { + break; + } + } else { // value >= current.value + if (!current.rightIsThread && current.right != null) { + current = current.right; + } else { + break; + } + } + } + + if (value < parent.value) { + // attach newNode as left child + newNode.left = parent.left; + newNode.leftIsThread = parent.leftIsThread; + newNode.right = parent; + newNode.rightIsThread = true; + + parent.left = newNode; + parent.leftIsThread = false; + } else { + // attach newNode as right child + newNode.right = parent.right; + newNode.rightIsThread = parent.rightIsThread; + newNode.left = parent; + newNode.leftIsThread = true; + + parent.right = newNode; + parent.rightIsThread = false; + } + } + + /** + * Returns the in-order traversal of the tree as a list of integers. + * Traversal is done without recursion or an explicit stack by following threads. + * + * @return list containing the in-order sequence of node values + */ + public List inorderTraversal() { + List result = new ArrayList<>(); + Node current = root; + if (current == null) { + return result; + } + + // Move to the leftmost node + while (current.left != null && !current.leftIsThread) { + current = current.left; + } + + while (current != null) { + result.add(current.value); + + // If right pointer is a thread, follow it + if (current.rightIsThread) { + current = current.right; + } else { + // Move to leftmost node in right subtree + current = current.right; + while (current != null && !current.leftIsThread && current.left != null) { + current = current.left; + } + } + } + + return result; + } + + /** + * Helper: checks whether the tree is empty. + * + * @return true if tree has no nodes + */ + public boolean isEmpty() { + return root == null; + } +} diff --git a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java b/src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java similarity index 98% rename from src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java rename to src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java index 7a1a7aadd805..624e3d65bfc1 100644 --- a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java @@ -1,8 +1,8 @@ -package com.thealgorithms.others; +package com.thealgorithms.datastructures.trees; // Java Program to implement Auto-Complete // Feature using Trie -class Trieac { +class TrieAutocomplete { // Alphabet size (# of symbols) public static final int ALPHABET_SIZE = 26; diff --git a/src/main/java/com/thealgorithms/datastructures/trees/VerticalOrderTraversal.java b/src/main/java/com/thealgorithms/datastructures/trees/VerticalOrderTraversal.java index c1d15390d4b9..5484b64641a9 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/VerticalOrderTraversal.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/VerticalOrderTraversal.java @@ -73,7 +73,7 @@ public static ArrayList verticalTraversal(BinaryTree.Node root) { ArrayList a = new ArrayList<>(); map.put(index.peek(), a); } - /*For a index, corresponding Node data is added + /*For an index, corresponding Node data is added to the respective ArrayList present at that index. */ map.get(index.peek()).add(queue.peek().data); diff --git a/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java b/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java new file mode 100644 index 000000000000..6feaa6f35048 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java @@ -0,0 +1,235 @@ +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Wavelet Tree is a highly efficient data structure used to store sequences + * and answer queries like rank, select, and quantile in O(log(max_val - min_val)) time. + * This structure is particularly useful in competitive programming and text compression. + */ +public class WaveletTree { + + private class Node { + int low; + int high; + Node left; + Node right; + List leftCount; // Prefix sums of elements going to the left child + + /** + * Recursively constructs the tree nodes by partitioning the array. + * + * @param arr the subarray for the current node + * @param low the minimum possible value in the current node + * @param high the maximum possible value in the current node + */ + Node(int[] arr, int low, int high) { + this.low = low; + this.high = high; + + if (low == high) { + return; + } + + int mid = low + (high - low) / 2; + leftCount = new ArrayList<>(arr.length + 1); + leftCount.add(0); + + List leftArr = new ArrayList<>(); + List rightArr = new ArrayList<>(); + + for (int x : arr) { + if (x <= mid) { + leftArr.add(x); + leftCount.add(leftCount.get(leftCount.size() - 1) + 1); + } else { + rightArr.add(x); + leftCount.add(leftCount.get(leftCount.size() - 1)); + } + } + + if (!leftArr.isEmpty()) { + this.left = new Node(leftArr.stream().mapToInt(i -> i).toArray(), low, mid); + } + if (!rightArr.isEmpty()) { + this.right = new Node(rightArr.stream().mapToInt(i -> i).toArray(), mid + 1, high); + } + } + } + + private Node root; + private final int n; + + /** + * Constructs a Wavelet Tree from the given array. + * The min and max values are determined dynamically from the array. + * + * @param arr the input array + */ + public WaveletTree(int[] arr) { + if (arr == null || arr.length == 0) { + this.n = 0; + return; + } + this.n = arr.length; + int min = arr[0]; + int max = arr[0]; + for (int x : arr) { + if (x < min) { + min = x; + } + if (x > max) { + max = x; + } + } + root = new Node(arr, min, max); + } + + /** + * Constructs a Wavelet Tree from the given array with specific min and max values. + * + * @param arr the input array + * @param minValue the minimum possible value + * @param maxValue the maximum possible value + */ + public WaveletTree(int[] arr, int minValue, int maxValue) { + if (arr == null || arr.length == 0) { + this.n = 0; + return; + } + this.n = arr.length; + root = new Node(arr, minValue, maxValue); + } + + /** + * How many times does the number x appear in the array from index 0 to i (inclusive)? + * + * @param x the number to search for + * @param i the end index (0-based, inclusive) + * @return the number of occurrences of x in arr[0...i] + */ + public int rank(int x, int i) { + if (root == null || x < root.low || x > root.high || i < 0) { + return 0; + } + // If i is out of bounds, cap it at n - 1 + int endIdx = Math.min(i, n - 1); + return rank(root, x, endIdx + 1); + } + + private int rank(Node node, int x, int count) { + if (node == null || count == 0) { + return 0; + } + if (node.low == node.high) { + return count; + } + int mid = node.low + (node.high - node.low) / 2; + int leftC = node.leftCount.get(count); + if (x <= mid) { + return rank(node.left, x, leftC); + } else { + return rank(node.right, x, count - leftC); + } + } + + /** + * What is the 0-based index of the k-th occurrence of the number x in the array? + * + * @param x the number to search for + * @param k the occurrence count (1-based) + * @return the 0-based index in the original array, or -1 if x occurs less than k times + */ + public int select(int x, int k) { + if (root == null || x < root.low || x > root.high || k <= 0) { + return -1; + } + if (rank(x, n - 1) < k) { + return -1; + } + return select(root, x, k); + } + + private int select(Node node, int x, int k) { + if (node.low == node.high) { + return k - 1; // 0-based index within the imaginary array at the leaf + } + int mid = node.low + (node.high - node.low) / 2; + if (x <= mid) { + int posInLeft = select(node.left, x, k); + return binarySearchLeft(node.leftCount, posInLeft + 1); + } else { + int posInRight = select(node.right, x, k); + return binarySearchRight(node.leftCount, posInRight + 1); + } + } + + private int binarySearchLeft(List prefixSums, int k) { + int l = 1; + int r = prefixSums.size() - 1; + int ans = -1; + while (l <= r) { + int mid = l + (r - l) / 2; + if (prefixSums.get(mid) >= k) { + ans = mid; + r = mid - 1; + } else { + l = mid + 1; + } + } + return ans == -1 ? -1 : ans - 1; // Convert to 0-based index + } + + private int binarySearchRight(List prefixSums, int k) { + int l = 1; + int r = prefixSums.size() - 1; + int ans = -1; + while (l <= r) { + int mid = l + (r - l) / 2; + if (mid - prefixSums.get(mid) >= k) { + ans = mid; + r = mid - 1; + } else { + l = mid + 1; + } + } + return ans == -1 ? -1 : ans - 1; // Convert to 0-based index + } + + /** + * If you sort the subarray from index left to right, what would be the k-th smallest element? + * This query is also commonly known as the quantile query. + * + * @param left the start index of the subarray (0-based, inclusive) + * @param right the end index of the subarray (0-based, inclusive) + * @param k the rank of the smallest element (1-based, e.g., k=1 is the minimum) + * @return the k-th smallest element in the subarray, or -1 if invalid parameters + */ + public int kthSmallest(int left, int right, int k) { + if (root == null || left > right || left < 0 || k < 1 || k > right - left + 1) { + return -1; + } + return kthSmallest(root, left, right, k); + } + + private int kthSmallest(Node node, int left, int right, int k) { + if (node.low == node.high) { + return node.low; + } + + int countLeftInLMinus1 = (left == 0) ? 0 : node.leftCount.get(left); + int countLeftInR = node.leftCount.get(right + 1); + int elementsToLeft = countLeftInR - countLeftInLMinus1; + + if (k <= elementsToLeft) { + int newL = countLeftInLMinus1; + int newR = countLeftInR - 1; + return kthSmallest(node.left, newL, newR, k); + } else { + int newL = left - countLeftInLMinus1; + int newR = right - countLeftInR; + return kthSmallest(node.right, newL, newR, k - elementsToLeft); + } + } +} diff --git a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java index 4c9c40c83174..323098a99887 100644 --- a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java +++ b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java @@ -66,10 +66,6 @@ public static class Location { } } - public Location[] createLocation(int numberValues) { - return new Location[numberValues]; - } - public Location buildLocation(double x, double y) { return new Location(x, y); } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java new file mode 100644 index 000000000000..7dae7603fedc --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -0,0 +1,111 @@ +package com.thealgorithms.dynamicprogramming; +import java.util.Arrays; + +/** + * A generalized template for the Digit Dynamic Programming (Digit DP) + * technique. + * Digit DP is used to count numbers within a range [L, R] that satisfy specific + * digit properties. + * This specific implementation demonstrates counting the numbers whose digit + * sum equals a target value. + * + *

+ * Example: + * countRangeWithDigitSum(1, 100, 5) returns 6 (numbers: 5, 14, 23, 32, 41, 50) + */ +public final class DigitDP { + + // Maximum theoretical digit sum for a 64-bit signed long integer (9 * 19 digits + // = 171) + private static final int MAX_DIGIT_SUM = 171; + + private DigitDP() { + // Prevent instantiation for utility/algorithm template class + } + + /** + * Counts how many numbers in the range [L, R] have a digit sum equal to the + * target. + * + * @param l The lower bound of the range (inclusive). + * @param r The upper bound of the range (inclusive). + * @param target The exact sum of digits required. + * @return The count of valid integers. + */ + public static long countRangeWithDigitSum(long l, long r, int target) { + if (l > r || target < 0 || target > MAX_DIGIT_SUM) { + return 0; + } + long countR = countWithDigitSum(r, target); + long countLMinus1 = countWithDigitSum(l - 1, target); + return countR - countLMinus1; + } + + private static long countWithDigitSum(long number, int target) { + if (number < 0) { + return 0; + } + String numStr = Long.toString(number); + int length = numStr.length(); + + // dp[index][current_sum][tight] + long[][][] dp = new long[length][MAX_DIGIT_SUM + 1][2]; + for (long[][] row : dp) { + for (long[] col : row) { + Arrays.fill(col, -1); + } + } + + return solve(0, 0, 1, numStr, target, dp); + } + + /** + * Recursive memoized function to explore digit placements. + * + * Time Complexity: O(number_of_digits * target_sum * 10) + * Space Complexity: O(number_of_digits * target_sum * 2) + * + * @param index Current digit position from left to right (most significant + * first). + * @param currentSum Cumulative sum of digits chosen so far. + * @param tight Flag indicating if current prefix matches the original + * number boundary. + * @param numStr String representation of the upper ceiling limit. + * @param target The exact required sum of digits. + * @param dp Memoization matrix cache table. + * @return Total valid combinations from the current state configuration. + */ + private static long solve(int index, int currentSum, int tight, String numStr, int target, long[][][] dp) { + // Base case: If we have processed all digits + if (index == numStr.length()) { + return currentSum == target ? 1 : 0; + } + + // Return memoized state if already evaluated + if (dp[index][currentSum][tight] != -1) { + return dp[index][currentSum][tight]; + } + + long ans = 0; + // Determine the maximum limit for the current position digit + int limit = (tight == 1) ? (numStr.charAt(index) - '0') : 9; + + // Iterate through all possible valid digits for this position + for (int digit = 0; digit <= limit; digit++) { + int nextSum = currentSum + digit; + + // Optimization: If the digit sum exceeds the target, prune branch + if (nextSum > target) { + continue; + } + + // Next state remains tight only if current state is tight and we place the + // exact limit digit + int nextTight = (tight == 1 && digit == limit) ? 1 : 0; + ans += solve(index + 1, nextSum, nextTight, numStr, target, dp); + } + + dp[index][currentSum][tight] = ans; + return ans; + } +} diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/Fibonacci.java b/src/main/java/com/thealgorithms/dynamicprogramming/Fibonacci.java index 0d6aff2bbef3..df158112d233 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/Fibonacci.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/Fibonacci.java @@ -1,10 +1,22 @@ package com.thealgorithms.dynamicprogramming; - import java.util.HashMap; import java.util.Map; - /** - * @author Varun Upadhyay (https://github.com/varunu28) + * Collection of Dynamic Programming techniques to solve for the n-th Fibonacci number. + *

+ * This file showcases Top-Down Memoization ({@code fibMemo}), Bottom-Up Tabulation ({@code fibBotUp}), + * and Space-Optimized Iteration ({@code fibOptimized}). + *

+ * For alternative structural paradigms, mathematical formulas, or verification steps, see: + *

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
+ * * @author Varun Upadhyay (https://github.com/varunu28) */ public final class Fibonacci { private Fibonacci() { diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/Knapsack.java b/src/main/java/com/thealgorithms/dynamicprogramming/Knapsack.java index 134561766830..0d4c8d501f9f 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/Knapsack.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/Knapsack.java @@ -3,53 +3,76 @@ import java.util.Arrays; /** - * A Dynamic Programming based solution for the 0-1 Knapsack problem. - * This class provides a method, `knapSack`, that calculates the maximum value that can be - * obtained from a given set of items with weights and values, while not exceeding a - * given weight capacity. + * 0/1 Knapsack Problem - Dynamic Programming solution. * - * @see 0-1 Knapsack Problem + * This algorithm solves the classic optimization problem where we have n items, + * each with a weight and a value. The goal is to maximize the total value + * without exceeding the knapsack's weight capacity. + * + * Time Complexity: O(n * W) + * Space Complexity: O(W) + * + * Example: + * values = {60, 100, 120} + * weights = {10, 20, 30} + * W = 50 + * Output: 220 + * + * @author Arpita + * @see Knapsack Problem */ public final class Knapsack { private Knapsack() { } + /** + * Validates the input to ensure correct constraints. + */ private static void throwIfInvalidInput(final int weightCapacity, final int[] weights, final int[] values) { if (weightCapacity < 0) { throw new IllegalArgumentException("Weight capacity should not be negative."); } if (weights == null || values == null || weights.length != values.length) { - throw new IllegalArgumentException("Input arrays must not be null and must have the same length."); + throw new IllegalArgumentException("Weights and values must be non-null and of the same length."); } if (Arrays.stream(weights).anyMatch(w -> w <= 0)) { - throw new IllegalArgumentException("Input array should not contain non-positive weight(s)."); + throw new IllegalArgumentException("Weights must be positive."); } } /** - * Solves the 0-1 Knapsack problem using Dynamic Programming. + * Solves the 0/1 Knapsack problem using Dynamic Programming (bottom-up approach). * * @param weightCapacity The maximum weight capacity of the knapsack. - * @param weights An array of item weights. - * @param values An array of item values. - * @return The maximum value that can be obtained without exceeding the weight capacity. - * @throws IllegalArgumentException If the input arrays are null or have different lengths. + * @param weights The array of item weights. + * @param values The array of item values. + * @return The maximum total value achievable without exceeding capacity. */ - public static int knapSack(final int weightCapacity, final int[] weights, final int[] values) throws IllegalArgumentException { + public static int knapSack(final int weightCapacity, final int[] weights, final int[] values) { throwIfInvalidInput(weightCapacity, weights, values); - // DP table to store the state of the maximum possible return for a given weight capacity. int[] dp = new int[weightCapacity + 1]; + // Fill dp[] array iteratively for (int i = 0; i < values.length; i++) { - for (int w = weightCapacity; w > 0; w--) { - if (weights[i] <= w) { - dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]); - } + for (int w = weightCapacity; w >= weights[i]; w--) { + dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]); } } return dp[weightCapacity]; } + + /* + // Example main method for local testing only. + public static void main(String[] args) { + int[] values = {60, 100, 120}; + int[] weights = {10, 20, 30}; + int weightCapacity = 50; + + int maxValue = knapSack(weightCapacity, weights, values); + System.out.println("Maximum value = " + maxValue); // Output: 220 + } + */ } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java b/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java index 0b40d4559341..5c8a6a953f83 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java @@ -1,58 +1,42 @@ package com.thealgorithms.dynamicprogramming; /** - * Algorithm explanation - * https://www.educative.io/edpresso/longest-palindromic-subsequence-algorithm + * Longest Palindromic Subsequence algorithm. + * A palindromic subsequence is a subsequence that reads the same forwards and backwards. + * This implementation finds the longest such subsequence by computing the LCS of the + * original string and its reverse. + * + * @see Wikipedia */ public final class LongestPalindromicSubsequence { private LongestPalindromicSubsequence() { } - public static void main(String[] args) { - String a = "BBABCBCAB"; - String b = "BABCBAB"; - - String aLPS = lps(a); - String bLPS = lps(b); - - System.out.println(a + " => " + aLPS); - System.out.println(b + " => " + bLPS); - } - - public static String lps(String original) throws IllegalArgumentException { - StringBuilder reverse = new StringBuilder(original); - reverse = reverse.reverse(); - return recursiveLPS(original, reverse.toString()); + /** + * Returns the longest palindromic subsequence of the given string. + * + * @param original the input string + * @return the longest palindromic subsequence + * @throws IllegalArgumentException if the input string is null + */ + public static String lps(String original) { + if (original == null) { + throw new IllegalArgumentException("Input string must not be null"); + } + String reverse = new StringBuilder(original).reverse().toString(); + return recursiveLPS(original, reverse); } private static String recursiveLPS(String original, String reverse) { - String bestResult = ""; - - // no more chars, then return empty - if (original.length() == 0 || reverse.length() == 0) { - bestResult = ""; - } else { - // if the last chars match, then remove it from both strings and recur - if (original.charAt(original.length() - 1) == reverse.charAt(reverse.length() - 1)) { - String bestSubResult = recursiveLPS(original.substring(0, original.length() - 1), reverse.substring(0, reverse.length() - 1)); - - bestResult = reverse.charAt(reverse.length() - 1) + bestSubResult; - } else { - // otherwise (1) ignore the last character of reverse, and recur on original and - // updated reverse again (2) ignore the last character of original and recur on the - // updated original and reverse again then select the best result from these two - // subproblems. - - String bestSubResult1 = recursiveLPS(original, reverse.substring(0, reverse.length() - 1)); - String bestSubResult2 = recursiveLPS(original.substring(0, original.length() - 1), reverse); - if (bestSubResult1.length() > bestSubResult2.length()) { - bestResult = bestSubResult1; - } else { - bestResult = bestSubResult2; - } - } + if (original.isEmpty() || reverse.isEmpty()) { + return ""; } - - return bestResult; + if (original.charAt(original.length() - 1) == reverse.charAt(reverse.length() - 1)) { + String bestSubResult = recursiveLPS(original.substring(0, original.length() - 1), reverse.substring(0, reverse.length() - 1)); + return reverse.charAt(reverse.length() - 1) + bestSubResult; + } + String sub1 = recursiveLPS(original, reverse.substring(0, reverse.length() - 1)); + String sub2 = recursiveLPS(original.substring(0, original.length() - 1), reverse); + return sub1.length() >= sub2.length() ? sub1 : sub2; } } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java b/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java new file mode 100644 index 000000000000..428176ea6c40 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java @@ -0,0 +1,130 @@ +package com.thealgorithms.dynamicprogramming; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * Computes the minimum search cost of an optimal binary search tree. + * + *

The algorithm sorts the keys, preserves the corresponding search frequencies, and uses + * dynamic programming with Knuth's optimization to compute the minimum weighted search cost. + * + *

Example: if keys = [10, 12] and frequencies = [34, 50], the best tree puts 12 at the root + * and 10 as its left child. The total cost is 50 * 1 + 34 * 2 = 118. + * + *

Reference: + * https://en.wikipedia.org/wiki/Optimal_binary_search_tree + */ +public final class OptimalBinarySearchTree { + private OptimalBinarySearchTree() { + } + + /** + * Computes the minimum weighted search cost for the given keys and search frequencies. + * + * @param keys the BST keys + * @param frequencies the search frequencies associated with the keys + * @return the minimum search cost + * @throws IllegalArgumentException if the input is invalid + */ + public static long findOptimalCost(int[] keys, int[] frequencies) { + validateInput(keys, frequencies); + if (keys.length == 0) { + return 0L; + } + + int[][] sortedNodes = sortNodes(keys, frequencies); + int nodeCount = sortedNodes.length; + long[] prefixSums = buildPrefixSums(sortedNodes); + long[][] optimalCost = new long[nodeCount][nodeCount]; + int[][] root = new int[nodeCount][nodeCount]; + + // Small example: + // keys = [10, 12] + // frequencies = [34, 50] + // Choosing 12 as the root gives cost 50 * 1 + 34 * 2 = 118, + // which is better than choosing 10 as the root. + + // Base case: a subtree containing one key has cost equal to its frequency, + // because that key becomes the root of the subtree and is searched at depth 1. + for (int index = 0; index < nodeCount; index++) { + optimalCost[index][index] = sortedNodes[index][1]; + root[index][index] = index; + } + + // Build solutions for longer and longer key ranges. + // optimalCost[start][end] stores the minimum search cost for keys in that range. + for (int length = 2; length <= nodeCount; length++) { + for (int start = 0; start <= nodeCount - length; start++) { + int end = start + length - 1; + + // Every key in this range moves one level deeper when we choose a root, + // so the sum of frequencies is added once to the subtree cost. + long frequencySum = prefixSums[end + 1] - prefixSums[start]; + optimalCost[start][end] = Long.MAX_VALUE; + + // Knuth's optimization: + // the best root for [start, end] lies between the best roots of + // [start, end - 1] and [start + 1, end], so we search only this interval. + int leftBoundary = root[start][end - 1]; + int rightBoundary = root[start + 1][end]; + for (int currentRoot = leftBoundary; currentRoot <= rightBoundary; currentRoot++) { + long leftCost = currentRoot > start ? optimalCost[start][currentRoot - 1] : 0L; + long rightCost = currentRoot < end ? optimalCost[currentRoot + 1][end] : 0L; + long currentCost = frequencySum + leftCost + rightCost; + + if (currentCost < optimalCost[start][end]) { + optimalCost[start][end] = currentCost; + root[start][end] = currentRoot; + } + } + } + } + + return optimalCost[0][nodeCount - 1]; + } + + private static void validateInput(int[] keys, int[] frequencies) { + if (keys == null || frequencies == null) { + throw new IllegalArgumentException("Keys and frequencies cannot be null"); + } + if (keys.length != frequencies.length) { + throw new IllegalArgumentException("Keys and frequencies must have the same length"); + } + + for (int frequency : frequencies) { + if (frequency < 0) { + throw new IllegalArgumentException("Frequencies cannot be negative"); + } + } + } + + private static int[][] sortNodes(int[] keys, int[] frequencies) { + int[][] sortedNodes = new int[keys.length][2]; + for (int index = 0; index < keys.length; index++) { + sortedNodes[index][0] = keys[index]; + sortedNodes[index][1] = frequencies[index]; + } + + // Sort by key so the nodes can be treated as an in-order BST sequence. + Arrays.sort(sortedNodes, Comparator.comparingInt(node -> node[0])); + + for (int index = 1; index < sortedNodes.length; index++) { + if (sortedNodes[index - 1][0] == sortedNodes[index][0]) { + throw new IllegalArgumentException("Keys must be distinct"); + } + } + + return sortedNodes; + } + + private static long[] buildPrefixSums(int[][] sortedNodes) { + long[] prefixSums = new long[sortedNodes.length + 1]; + for (int index = 0; index < sortedNodes.length; index++) { + // prefixSums[i] holds the total frequency of the first i sorted keys. + // This lets us get the frequency sum of any range in O(1) time. + prefixSums[index + 1] = prefixSums[index] + sortedNodes[index][1]; + } + return prefixSums; + } +} diff --git a/src/main/java/com/thealgorithms/geometry/LineIntersection.java b/src/main/java/com/thealgorithms/geometry/LineIntersection.java new file mode 100644 index 000000000000..8d65833816b3 --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/LineIntersection.java @@ -0,0 +1,105 @@ +package com.thealgorithms.geometry; + +import java.awt.geom.Point2D; +import java.util.Optional; + +/** + * Utility methods for checking and computing 2D line segment intersections. + */ +public final class LineIntersection { + private LineIntersection() { + } + + /** + * Checks whether two line segments intersect. + * + * @param p1 first endpoint of segment 1 + * @param p2 second endpoint of segment 1 + * @param q1 first endpoint of segment 2 + * @param q2 second endpoint of segment 2 + * @return true when the segments intersect (including touching endpoints) + */ + public static boolean intersects(Point p1, Point p2, Point q1, Point q2) { + int o1 = orientation(p1, p2, q1); + int o2 = orientation(p1, p2, q2); + int o3 = orientation(q1, q2, p1); + int o4 = orientation(q1, q2, p2); + + if (o1 != o2 && o3 != o4) { + return true; + } + + if (o1 == 0 && onSegment(p1, q1, p2)) { + return true; + } + if (o2 == 0 && onSegment(p1, q2, p2)) { + return true; + } + if (o3 == 0 && onSegment(q1, p1, q2)) { + return true; + } + if (o4 == 0 && onSegment(q1, p2, q2)) { + return true; + } + + return false; + } + + /** + * Computes the single geometric intersection point between two non-parallel + * segments when it exists. + * + *

For parallel/collinear overlap, this method returns {@code Optional.empty()}. + * + * @param p1 first endpoint of segment 1 + * @param p2 second endpoint of segment 1 + * @param q1 first endpoint of segment 2 + * @param q2 second endpoint of segment 2 + * @return the intersection point when uniquely defined and on both segments + */ + public static Optional intersectionPoint(Point p1, Point p2, Point q1, Point q2) { + if (!intersects(p1, p2, q1, q2)) { + return Optional.empty(); + } + + long x1 = p1.x(); + long y1 = p1.y(); + long x2 = p2.x(); + long y2 = p2.y(); + long x3 = q1.x(); + long y3 = q1.y(); + long x4 = q2.x(); + long y4 = q2.y(); + + long denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (denominator == 0L) { + return sharedEndpoint(p1, p2, q1, q2); + } + + long determinant1 = x1 * y2 - y1 * x2; + long determinant2 = x3 * y4 - y3 * x4; + long numeratorX = determinant1 * (x3 - x4) - (x1 - x2) * determinant2; + long numeratorY = determinant1 * (y3 - y4) - (y1 - y2) * determinant2; + + return Optional.of(new Point2D.Double(numeratorX / (double) denominator, numeratorY / (double) denominator)); + } + + private static int orientation(Point a, Point b, Point c) { + long cross = ((long) b.x() - a.x()) * ((long) c.y() - a.y()) - ((long) b.y() - a.y()) * ((long) c.x() - a.x()); + return Long.compare(cross, 0L); + } + + private static Optional sharedEndpoint(Point p1, Point p2, Point q1, Point q2) { + if (p1.equals(q1) || p1.equals(q2)) { + return Optional.of(new Point2D.Double(p1.x(), p1.y())); + } + if (p2.equals(q1) || p2.equals(q2)) { + return Optional.of(new Point2D.Double(p2.x(), p2.y())); + } + return Optional.empty(); + } + + private static boolean onSegment(Point a, Point b, Point c) { + return b.x() >= Math.min(a.x(), c.x()) && b.x() <= Math.max(a.x(), c.x()) && b.y() >= Math.min(a.y(), c.y()) && b.y() <= Math.max(a.y(), c.y()); + } +} diff --git a/src/main/java/com/thealgorithms/graph/AccountMerge.java b/src/main/java/com/thealgorithms/graph/AccountMerge.java new file mode 100644 index 000000000000..86f0a24b6c1c --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/AccountMerge.java @@ -0,0 +1,127 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Merges account records using Disjoint Set Union (Union-Find) on shared emails. + * + *

Each account is expected to be a list where the first element is the user name and the + * remaining elements are email addresses. Accounts that share at least one email are merged into a + * single record. + */ +public final class AccountMerge { + private AccountMerge() { + // Utility class; do not instantiate. + } + + /** + * Merges accounts that share one or more email addresses. + * + *

The returned list is sorted by account owner name, then by the first email address when + * multiple merged groups have the same owner name. Within each merged account, emails are + * returned in lexicographic order. + * + * @param accounts a list of accounts where each entry contains a user name followed by emails + * @return merged accounts, or an empty list when {@code accounts} is null or empty + */ + public static List> mergeAccounts(List> accounts) { + if (accounts == null || accounts.isEmpty()) { + return List.of(); + } + + UnionFind dsu = new UnionFind(accounts.size()); + Map emailToAccount = new HashMap<>(); + + for (int i = 0; i < accounts.size(); i++) { + List account = accounts.get(i); + for (int j = 1; j < account.size(); j++) { + String email = account.get(j); + Integer previous = emailToAccount.putIfAbsent(email, i); + if (previous != null) { + dsu.union(i, previous); + } + } + } + + Map> rootToEmails = new LinkedHashMap<>(); + for (Map.Entry entry : emailToAccount.entrySet()) { + int root = dsu.find(entry.getValue()); + rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()).add(entry.getKey()); + } + for (int i = 0; i < accounts.size(); i++) { + if (accounts.get(i).size() <= 1) { + int root = dsu.find(i); + rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()); + } + } + + List> merged = new ArrayList<>(); + for (Map.Entry> entry : rootToEmails.entrySet()) { + int root = entry.getKey(); + List emails = entry.getValue(); + Collections.sort(emails); + + List mergedAccount = new ArrayList<>(); + mergedAccount.add(accounts.get(root).getFirst()); + mergedAccount.addAll(emails); + merged.add(mergedAccount); + } + + merged.sort((a, b) -> { + int cmp = a.getFirst().compareTo(b.getFirst()); + if (cmp != 0) { + return cmp; + } + if (a.size() == 1 || b.size() == 1) { + return Integer.compare(a.size(), b.size()); + } + return a.get(1).compareTo(b.get(1)); + }); + return merged; + } + + /** + * Lightweight union-find structure with path compression and union by rank. + */ + private static final class UnionFind { + private final int[] parent; + private final int[] rank; + + private UnionFind(int size) { + this.parent = new int[size]; + this.rank = new int[size]; + for (int i = 0; i < size; i++) { + parent[i] = i; + } + } + + private int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + private void union(int x, int y) { + int rootX = find(x); + int rootY = find(y); + if (rootX == rootY) { + return; + } + + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else { + parent[rootY] = rootX; + rank[rootX]++; + } + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/GomoryHuTree.java b/src/main/java/com/thealgorithms/graph/GomoryHuTree.java new file mode 100644 index 000000000000..f8c110f25571 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/GomoryHuTree.java @@ -0,0 +1,144 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Gomory–Hu tree construction for undirected graphs via nβˆ’1 max-flow computations. + * + *

API: {@code buildTree(int[][])} returns {@code {parent, weight}} arrays for the tree. + * + * @see Wikipedia: Gomory–Hu tree + */ + +public final class GomoryHuTree { + private GomoryHuTree() { + } + + public static int[][] buildTree(int[][] cap) { + validateCapacityMatrix(cap); + final int n = cap.length; + if (n == 1) { + return new int[][] {new int[] {-1}, new int[] {0}}; + } + + int[] parent = new int[n]; + int[] weight = new int[n]; + Arrays.fill(parent, 0); + parent[0] = -1; + weight[0] = 0; + + for (int s = 1; s < n; s++) { + int t = parent[s]; + MaxFlowResult res = edmondsKarpWithMinCut(cap, s, t); + int f = res.flow; + weight[s] = f; + + for (int v = 0; v < n; v++) { + if (v != s && parent[v] == t && res.reachable[v]) { + parent[v] = s; + } + } + + if (t != 0 && res.reachable[parent[t]]) { + parent[s] = parent[t]; + parent[t] = s; + weight[s] = weight[t]; + weight[t] = f; + } + } + return new int[][] {parent, weight}; + } + + private static void validateCapacityMatrix(int[][] cap) { + if (cap == null || cap.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + final int n = cap.length; + for (int i = 0; i < n; i++) { + if (cap[i] == null || cap[i].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int j = 0; j < n; j++) { + if (cap[i][j] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + } + + private static final class MaxFlowResult { + final int flow; + final boolean[] reachable; + MaxFlowResult(int flow, boolean[] reachable) { + this.flow = flow; + this.reachable = reachable; + } + } + + private static MaxFlowResult edmondsKarpWithMinCut(int[][] capacity, int source, int sink) { + final int n = capacity.length; + int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + int[] parent = new int[n]; + int maxFlow = 0; + + while (bfs(residual, source, sink, parent)) { + int pathFlow = Integer.MAX_VALUE; + for (int v = sink; v != source; v = parent[v]) { + int u = parent[v]; + pathFlow = Math.min(pathFlow, residual[u][v]); + } + for (int v = sink; v != source; v = parent[v]) { + int u = parent[v]; + residual[u][v] -= pathFlow; + residual[v][u] += pathFlow; + } + maxFlow += pathFlow; + } + + boolean[] reachable = new boolean[n]; + markReachable(residual, source, reachable); + return new MaxFlowResult(maxFlow, reachable); + } + + private static boolean bfs(int[][] residual, int source, int sink, int[] parent) { + Arrays.fill(parent, -1); + parent[source] = source; + Queue q = new ArrayDeque<>(); + q.add(source); + while (!q.isEmpty()) { + int u = q.poll(); + for (int v = 0; v < residual.length; v++) { + if (residual[u][v] > 0 && parent[v] == -1) { + parent[v] = u; + if (v == sink) { + return true; + } + q.add(v); + } + } + } + return false; + } + + private static void markReachable(int[][] residual, int source, boolean[] vis) { + Arrays.fill(vis, false); + Queue q = new ArrayDeque<>(); + vis[source] = true; + q.add(source); + while (!q.isEmpty()) { + int u = q.poll(); + for (int v = 0; v < residual.length; v++) { + if (!vis[v] && residual[u][v] > 0) { + vis[v] = true; + q.add(v); + } + } + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/TarjanBridges.java b/src/main/java/com/thealgorithms/graph/TarjanBridges.java new file mode 100644 index 000000000000..dbe2e710429a --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/TarjanBridges.java @@ -0,0 +1,122 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of Tarjan's Bridge-Finding Algorithm for undirected graphs. + * + *

A bridge (also called a cut-edge) is an edge in an undirected graph whose removal + * increases the number of connected components. Bridges represent critical links + * in a network β€” if any bridge is removed, part of the network becomes unreachable.

+ * + *

The algorithm performs a single Depth-First Search (DFS) traversal, tracking two + * values for each vertex:

+ *
    + *
  • discoveryTime β€” the time step at which the vertex was first visited.
  • + *
  • lowLink β€” the smallest discovery time reachable from the subtree rooted + * at that vertex (via back edges).
  • + *
+ * + *

An edge (u, v) is a bridge if and only if {@code lowLink[v] > discoveryTime[u]}, + * meaning there is no back edge from the subtree of v that can reach u or any ancestor of u.

+ * + *

Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges.

+ *

Space Complexity: O(V + E) for the adjacency list, discovery/low arrays, and recursion stack.

+ * + * @see Wikipedia: Bridge (graph theory) + */ +public final class TarjanBridges { + + private TarjanBridges() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Finds all bridge edges in an undirected graph. + * + *

The graph is represented as an adjacency list where each vertex is identified by + * an integer in the range {@code [0, vertexCount)}. For each undirected edge (u, v), + * v must appear in {@code adjacencyList.get(u)} and u must appear in + * {@code adjacencyList.get(v)}.

+ * + * @param vertexCount the total number of vertices in the graph (must be non-negative) + * @param adjacencyList the adjacency list representation of the graph; must contain + * exactly {@code vertexCount} entries (one per vertex) + * @return a list of bridge edges, where each bridge is represented as an {@code int[]} + * of length 2 with {@code edge[0] < edge[1]}; returns an empty list if no bridges exist + * @throws IllegalArgumentException if {@code vertexCount} is negative, or if + * {@code adjacencyList} is null or its size does not match + * {@code vertexCount} + */ + public static List findBridges(int vertexCount, List> adjacencyList) { + if (vertexCount < 0) { + throw new IllegalArgumentException("vertexCount must be non-negative"); + } + if (adjacencyList == null || adjacencyList.size() != vertexCount) { + throw new IllegalArgumentException("adjacencyList size must equal vertexCount"); + } + + List bridges = new ArrayList<>(); + + if (vertexCount == 0) { + return bridges; + } + + BridgeFinder finder = new BridgeFinder(vertexCount, adjacencyList, bridges); + + // Run DFS from every unvisited vertex to handle disconnected graphs + for (int i = 0; i < vertexCount; i++) { + if (!finder.visited[i]) { + finder.dfs(i, -1); + } + } + + return bridges; + } + + private static class BridgeFinder { + private final List> adjacencyList; + private final List bridges; + private final int[] discoveryTime; + private final int[] lowLink; + boolean[] visited; + private int timer; + + BridgeFinder(int vertexCount, List> adjacencyList, List bridges) { + this.adjacencyList = adjacencyList; + this.bridges = bridges; + this.discoveryTime = new int[vertexCount]; + this.lowLink = new int[vertexCount]; + this.visited = new boolean[vertexCount]; + this.timer = 0; + } + + /** + * Performs DFS from the given vertex, computing discovery times and low-link values, + * and collects any bridge edges found. + * + * @param u the current vertex being explored + * @param parent the parent of u in the DFS tree (-1 if u is a root) + */ + void dfs(int u, int parent) { + visited[u] = true; + discoveryTime[u] = timer; + lowLink[u] = timer; + timer++; + + for (int v : adjacencyList.get(u)) { + if (!visited[v]) { + dfs(v, u); + lowLink[u] = Math.min(lowLink[u], lowLink[v]); + + if (lowLink[v] > discoveryTime[u]) { + bridges.add(new int[] {Math.min(u, v), Math.max(u, v)}); + } + } else if (v != parent) { + lowLink[u] = Math.min(lowLink[u], discoveryTime[v]); + } + } + } + } +} diff --git a/src/main/java/com/thealgorithms/greedyalgorithms/CoinChange.java b/src/main/java/com/thealgorithms/greedyalgorithms/CoinChange.java index 8054581d21d7..5f9f6080d0e1 100644 --- a/src/main/java/com/thealgorithms/greedyalgorithms/CoinChange.java +++ b/src/main/java/com/thealgorithms/greedyalgorithms/CoinChange.java @@ -6,10 +6,30 @@ // Problem Link : https://en.wikipedia.org/wiki/Change-making_problem +/** + * The Coin Change problem finds the minimum number of coins needed + * to make a given amount using a greedy approach. + * + *

Note: This greedy approach works optimally for standard coin systems + * (like Indian currency), but may not work for all arbitrary coin sets. + * For arbitrary denominations, dynamic programming is preferred. + * + * @see Change-making problem + */ public final class CoinChange { private CoinChange() { } - // Function to solve the coin change problem + + /** + * Returns the list of coins used to make the given amount + * using a greedy algorithm with standard denominations. + * + *

Time Complexity: O(n log n) where n is the number of coin denominations + *

Space Complexity: O(n) + * + * @param amount the total amount to make change for + * @return list of coins used to make the amount + */ public static ArrayList coinChangeProblem(int amount) { // Define an array of coin denominations in descending order Integer[] coins = {1, 2, 5, 10, 20, 50, 100, 500, 2000}; diff --git a/src/main/java/com/thealgorithms/io/BufferedReader.java b/src/main/java/com/thealgorithms/io/BufferedReader.java index 66673fe281ae..c0b7e8a449bd 100644 --- a/src/main/java/com/thealgorithms/io/BufferedReader.java +++ b/src/main/java/com/thealgorithms/io/BufferedReader.java @@ -108,7 +108,7 @@ public int peek(int n) throws IOException { /** * Removes the already read bytes from the buffer - * in-order to make space for new bytes to be filled up. + * in order to make space for new bytes to be filled up. *

* This may also do the job to read first time data (the whole buffer is empty) */ diff --git a/src/main/java/com/thealgorithms/machinelearning/KNearestNeighbors.java b/src/main/java/com/thealgorithms/machinelearning/KNearestNeighbors.java new file mode 100644 index 000000000000..02c3943365bf --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/KNearestNeighbors.java @@ -0,0 +1,203 @@ +package com.thealgorithms.machinelearning; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * K-Nearest Neighbors (KNN) classifier. + * + *

+ * K-Nearest Neighbors is a supervised machine learning algorithm that + * classifies a sample based on the majority class among its {@code k} + * nearest training samples using the Euclidean distance metric. + * + *

+ * The classifier stores the training dataset during the fitting phase and + * predicts class labels for new samples without building an explicit model. + * + * @see + * K-Nearest Neighbors + */ +public final class KNearestNeighbors { + private final int k; + private double[][] trainingFeatures; + private int[] trainingLabels; + private int numFeatures; + + /** + * Constructs a K-Nearest Neighbors classifier with the specified number + * of neighbors. + * + * @param k the number of nearest neighbors to consider during prediction + */ + public KNearestNeighbors(int k) { + + if (k <= 0) { + throw new IllegalArgumentException("k must be greater than 0."); + } + + this.k = k; + } + + /** + * Represents a neighboring training sample and its distance from the test + * sample. + */ + private record Neighbor(double distance, int label) { + } + + /** + * Fits the classifier using the provided training dataset. + * + *

+ * The training feature vectors and their corresponding class labels are + * stored for use during prediction. + * + * @param features the training feature vectors + * @param labels the corresponding class labels + */ + public void fit(double[][] features, int[] labels) { + + if (features == null || labels == null) { + throw new IllegalArgumentException("Features and labels cannot be null."); + } + + if (features.length == 0 || labels.length == 0) { + throw new IllegalArgumentException("Features and labels cannot be empty."); + } + + if (features.length != labels.length) { + throw new IllegalArgumentException("Features and labels must have the same length."); + } + + if (k > features.length) { + throw new IllegalArgumentException("k cannot be greater than the number of training samples."); + } + + if (features[0] == null) { + throw new IllegalArgumentException("Feature vectors cannot be null."); + } + + numFeatures = features[0].length; + + if (numFeatures == 0) { + throw new IllegalArgumentException("Feature vectors cannot be empty."); + } + + for (double[] sample : features) { + if (sample == null) { + throw new IllegalArgumentException("Feature vectors cannot be null."); + } + + if (sample.length != numFeatures) { + throw new IllegalArgumentException("All feature vectors must have the same dimension."); + } + } + + this.trainingFeatures = new double[features.length][]; + + for (int i = 0; i < features.length; i++) { + this.trainingFeatures[i] = features[i].clone(); + } + this.trainingLabels = labels.clone(); + } + + /** + * Computes the Euclidean distance between two feature vectors. + * + * @param first the first feature vector + * @param second the second feature vector + * @return the Euclidean distance between the two vectors + */ + private static double euclideanDistance(double[] first, double[] second) { + double sum = 0.0; + + for (int i = 0; i < first.length; i++) { + double difference = first[i] - second[i]; + sum += difference * difference; + } + + return Math.sqrt(sum); + } + + /** + * Predicts the class label for a single sample. + * + *

+ * The prediction is made by finding the {@code k} nearest neighbors + * among the training samples and selecting the class with the highest + * number of votes. In the event of a tie, the smaller class label is + * returned. + * + * @param testPoint the sample to classify + * @return the predicted class label + */ + public int predict(double[] testPoint) { + if (trainingFeatures == null || trainingLabels == null) { + throw new IllegalStateException("Classifier has not been fitted."); + } + + if (testPoint == null) { + throw new IllegalArgumentException("Sample cannot be null."); + } + + if (testPoint.length != numFeatures) { + throw new IllegalArgumentException("Sample length must match training feature count."); + } + + List neighbors = new ArrayList<>(trainingFeatures.length); + + for (int i = 0; i < trainingFeatures.length; i++) { + double distance = euclideanDistance(trainingFeatures[i], testPoint); + neighbors.add(new Neighbor(distance, trainingLabels[i])); + } + + neighbors.sort(Comparator.comparingDouble(Neighbor::distance)); + + Map votes = new HashMap<>(); + + for (int i = 0; i < k; i++) { + int label = neighbors.get(i).label(); + votes.merge(label, 1, Integer::sum); + } + + int predictedLabel = -1; + int maxVotes = -1; + + for (Map.Entry entry : votes.entrySet()) { + int label = entry.getKey(); + int count = entry.getValue(); + + if (count > maxVotes || (count == maxVotes && label < predictedLabel)) { + maxVotes = count; + predictedLabel = label; + } + } + + return predictedLabel; + } + + /** + * Predicts class labels for multiple samples. + * + * @param samples the samples to classify + * @return an array containing the predicted class label for each sample + */ + public int[] predict(double[][] samples) { + + if (samples == null) { + throw new IllegalArgumentException("Samples cannot be null."); + } + + int[] predictions = new int[samples.length]; + + for (int i = 0; i < samples.length; i++) { + predictions[i] = predict(samples[i]); + } + + return predictions; + } +} diff --git a/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java b/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java new file mode 100644 index 000000000000..134d4eee8c4c --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java @@ -0,0 +1,102 @@ +package com.thealgorithms.machinelearning; + +/** + * A simple Linear Regression model implemented from scratch using Gradient Descent. + * + * @see Linear Regression (Wikipedia) + * @author Vraj Prajapati (Rosander0) + */ +public class LinearRegression { + private double m; // Slope (weight) + private double b; // Y-intercept (bias) + private final double learningRate; + private final int epochs; + + /** + * Constructs a Linear Regression model with the given hyperparameters. + * + * @param learningRate controls the step size during gradient descent + * @param epochs the number of iterations to train the model + */ + public LinearRegression(double learningRate, int epochs) { + this.learningRate = learningRate; + this.epochs = epochs; + this.m = 0.0; + this.b = 0.0; + } + + /** + * Trains the model on the provided dataset using batch gradient descent. + * + * @param x the input feature values + * @param y the corresponding target values + * @throws IllegalArgumentException if the arrays are null, empty, or of differing lengths + */ + public void fit(double[] x, double[] y) { + if (x == null || y == null || x.length != y.length || x.length == 0) { + throw new IllegalArgumentException("X and Y must be non-null, non-empty, and of the same length."); + } + + int n = x.length; + + for (int epoch = 0; epoch < epochs; epoch++) { + double mGradient = 0; + double bGradient = 0; + + // Calculate gradients across the entire dataset + for (int i = 0; i < n; i++) { + double prediction = (m * x[i]) + b; + double error = prediction - y[i]; + + // Partial derivatives of the Mean Squared Error cost function + mGradient += error * x[i]; + bGradient += error; + } + + // Average the gradients and update the parameters + m -= 2.0 / n * mGradient * learningRate; + b -= 2.0 / n * bGradient * learningRate; + } + } + + /** + * Predicts the output for a given input x. + * + * @param x the input value + * @return the predicted output + */ + public double predict(double x) { + return (m * x) + b; + } + + /** + * Calculates the Mean Squared Error of the model against a dataset. + * + * @param x the input feature values + * @param y the corresponding target values + * @return the mean squared error + */ + public double calculateMSE(double[] x, double[] y) { + double totalSquaredError = 0; + int n = x.length; + for (int i = 0; i < n; i++) { + double error = predict(x[i]) - y[i]; + totalSquaredError += error * error; + } + return totalSquaredError / n; + } + + /** + * @return the learned slope of the regression line + */ + public double getSlope() { + return m; + } + + /** + * @return the learned y-intercept of the regression line + */ + public double getIntercept() { + return b; + } +} diff --git a/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java new file mode 100644 index 000000000000..43e7e555814c --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java @@ -0,0 +1,143 @@ +package com.thealgorithms.machinelearning; + +import java.util.HashMap; +import java.util.Map; + +/** + * Multinomial Naive Bayes classifier. + * + *

Suited to discrete, count-based features (e.g. word frequencies in text + * classification). Class priors and feature likelihoods are estimated from + * training data with Laplace (add-alpha) smoothing to avoid zero + * probabilities for unseen feature/class combinations. Predictions are made + * by comparing summed log-probabilities across classes, which avoids the + * numerical underflow that repeated multiplication of small probabilities + * would cause. + * + *

Reference: + * Naive Bayes classifier + * + * @author Vraj Prajapati(Rosander0) + */ +public final class MultinomialNaiveBayesClassifier { + + private final double alpha; + private final Map logPriors; + private final Map logLikelihoods; + private int numFeatures; + + /** + * Constructs a classifier with the given Laplace smoothing parameter. + * + * @param alpha smoothing constant; must be greater than 0. A value of 1.0 + * corresponds to standard Laplace smoothing. + */ + public MultinomialNaiveBayesClassifier(double alpha) { + if (alpha <= 0) { + throw new IllegalArgumentException("alpha must be greater than 0"); + } + this.alpha = alpha; + this.logPriors = new HashMap<>(); + this.logLikelihoods = new HashMap<>(); + } + + /** Constructs a classifier using the standard Laplace smoothing constant of 1.0. */ + public MultinomialNaiveBayesClassifier() { + this(1.0); + } + + /** + * Fits the classifier on the given feature matrix and labels. + * + * @param features training samples, each row a vector of non-negative + * feature counts + * @param labels class label for each row of {@code features} + */ + public void fit(double[][] features, int[] labels) { + if (features.length == 0 || features.length != labels.length) { + throw new IllegalArgumentException("features and labels must be non-empty and of equal length"); + } + logPriors.clear(); + logLikelihoods.clear(); + numFeatures = features[0].length; + + Map classCounts = new HashMap<>(); + Map featureSums = new HashMap<>(); + Map totalFeatureCount = new HashMap<>(); + + for (int i = 0; i < features.length; i++) { + int label = labels[i]; + classCounts.merge(label, 1, Integer::sum); + double[] sums = featureSums.computeIfAbsent(label, k -> new double[numFeatures]); + double total = totalFeatureCount.getOrDefault(label, 0.0); + for (int j = 0; j < numFeatures; j++) { + sums[j] += features[i][j]; + total += features[i][j]; + } + totalFeatureCount.put(label, total); + } + + int totalSamples = features.length; + for (Map.Entry entry : featureSums.entrySet()) { + int label = entry.getKey(); + double[] sums = entry.getValue(); + int count = classCounts.getOrDefault(label, 0); + double total = totalFeatureCount.getOrDefault(label, 0.0); + + logPriors.put(label, Math.log((double) count / totalSamples)); + + double denom = total + alpha * numFeatures; + double[] logLikelihood = new double[numFeatures]; + for (int j = 0; j < numFeatures; j++) { + logLikelihood[j] = Math.log((sums[j] + alpha) / denom); + } + logLikelihoods.put(label, logLikelihood); + } + } + + /** + * Predicts the most likely class for a single sample. + * + * @param sample feature vector of non-negative counts + * @return the predicted class label + */ + public int predict(double[] sample) { + if (logPriors.isEmpty()) { + throw new IllegalStateException("classifier has not been fitted"); + } + if (sample.length != numFeatures) { + throw new IllegalArgumentException("sample length must match training feature count"); + } + + int bestLabel = -1; + double bestScore = Double.NEGATIVE_INFINITY; + + for (Map.Entry entry : logLikelihoods.entrySet()) { + int label = entry.getKey(); + double[] logLikelihood = entry.getValue(); + double score = logPriors.getOrDefault(label, Double.NEGATIVE_INFINITY); + for (int j = 0; j < numFeatures; j++) { + score += sample[j] * logLikelihood[j]; + } + if (score > bestScore) { + bestScore = score; + bestLabel = label; + } + } + return bestLabel; + } + + /** + * Predicts class labels for a batch of samples. + * + * @param samples feature vectors of non-negative counts + * @return predicted class label for each row of {@code samples} + */ + public int[] predict(double[][] samples) { + int[] predictions = new int[samples.length]; + for (int i = 0; i < samples.length; i++) { + predictions[i] = predict(samples[i]); + } + return predictions; + } +} diff --git a/src/main/java/com/thealgorithms/machinelearning/Perceptron.java b/src/main/java/com/thealgorithms/machinelearning/Perceptron.java new file mode 100644 index 000000000000..7b5e5c4b5429 --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/Perceptron.java @@ -0,0 +1,255 @@ +package com.thealgorithms.machinelearning; + +/** + * A binary Perceptron classifier. + * + *

The Perceptron is a single-layer neural network that learns a linear + * decision boundary. It updates its weights whenever a training sample is + * misclassified. Convergence is guaranteed for linearly separable data, but + * training stops after the configured epoch limit for non-separable data. + * Labels must be either {@code 0} or {@code 1}. + * + *

The prediction rule is {@code 1} when the weighted sum plus bias is + * greater than or equal to zero, and {@code 0} otherwise. For a + * misclassified sample, the update is {@code weight += learningRate * error * + * feature} and {@code bias += learningRate * error}, where {@code error} is + * the true label minus the prediction. Samples are visited one at a time, so + * each prediction uses the parameters produced by the preceding updates of + * the same epoch. + * + * @see Perceptron + */ +public final class Perceptron { + private final double learningRate; + private final int maxEpochs; + private double[] weights; + private double bias; + private int epochsRun; + private boolean converged; + + /** + * Constructs a Perceptron with the given training hyperparameters. + * + * @param learningRate positive step size used for each update + * @param maxEpochs positive maximum number of passes over the training data + * @throws IllegalArgumentException if a hyperparameter is invalid + */ + public Perceptron(double learningRate, int maxEpochs) { + if (!Double.isFinite(learningRate) || learningRate <= 0.0) { + throw new IllegalArgumentException("learningRate must be finite and greater than 0"); + } + if (maxEpochs <= 0) { + throw new IllegalArgumentException("maxEpochs must be greater than 0"); + } + this.learningRate = learningRate; + this.maxEpochs = maxEpochs; + } + + /** + * Fits the classifier using binary training labels. + * + *

Fitting resets the weights and bias to zero before training. The + * method records whether an entire epoch completed without an update. A + * large {@code learningRate} combined with large feature values can push + * the parameters past the range of {@code double}; the classifier then + * returns to its unfitted state instead of reporting predictions derived + * from non-finite parameters. + * + * @param features training feature vectors + * @param labels corresponding binary labels, each either {@code 0} or + * {@code 1} + * @throws IllegalArgumentException if the training data is invalid + * @throws ArithmeticException if training diverges and the learned + * parameters stop being finite + */ + public void fit(double[][] features, int[] labels) { + validateTrainingData(features, labels); + + weights = new double[features[0].length]; + bias = 0.0; + epochsRun = 0; + converged = false; + + for (int epoch = 0; epoch < maxEpochs; epoch++) { + boolean updated = false; + + for (int sampleIndex = 0; sampleIndex < features.length; sampleIndex++) { + int prediction = rawPredict(features[sampleIndex]); + int error = labels[sampleIndex] - prediction; + + if (error != 0) { + update(features[sampleIndex], error); + updated = true; + } + } + + epochsRun = epoch + 1; + if (!updated) { + converged = true; + break; + } + } + + ensureParametersAreFinite(); + } + + /** + * Predicts the binary label for one sample. + * + * @param sample feature vector to classify + * @return {@code 0} or {@code 1} + * @throws IllegalStateException if the classifier has not been fitted + * @throws IllegalArgumentException if the sample is invalid + */ + public int predict(double[] sample) { + ensureFitted(); + validateSample(sample); + return rawPredict(sample); + } + + /** + * Predicts binary labels for a batch of samples. + * + * @param samples feature vectors to classify + * @return one prediction for each sample + * @throws IllegalStateException if the classifier has not been fitted + * @throws IllegalArgumentException if the batch or one of its samples is + * invalid + */ + public int[] predict(double[][] samples) { + ensureFitted(); + if (samples == null) { + throw new IllegalArgumentException("samples cannot be null"); + } + + int[] predictions = new int[samples.length]; + for (int sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + predictions[sampleIndex] = predict(samples[sampleIndex]); + } + return predictions; + } + + /** + * Returns a defensive copy of the learned feature weights. + * + * @return learned weights in feature order + * @throws IllegalStateException if the classifier has not been fitted + */ + public double[] getWeights() { + ensureFitted(); + return weights.clone(); + } + + /** + * Returns the learned bias term. + * + * @return learned bias + * @throws IllegalStateException if the classifier has not been fitted + */ + public double getBias() { + ensureFitted(); + return bias; + } + + /** + * Reports whether training completed with an update-free epoch. + * + * @return {@code true} if an epoch completed without an update + * @throws IllegalStateException if the classifier has not been fitted + */ + public boolean hasConverged() { + ensureFitted(); + return converged; + } + + /** + * Returns the number of epochs performed by the last fit. + * + * @return number of completed epochs + * @throws IllegalStateException if the classifier has not been fitted + */ + public int getEpochsRun() { + ensureFitted(); + return epochsRun; + } + + private int rawPredict(double[] sample) { + double weightedSum = bias; + for (int featureIndex = 0; featureIndex < weights.length; featureIndex++) { + weightedSum += weights[featureIndex] * sample[featureIndex]; + } + return weightedSum >= 0.0 ? 1 : 0; + } + + private void update(double[] sample, int error) { + for (int featureIndex = 0; featureIndex < weights.length; featureIndex++) { + weights[featureIndex] += learningRate * error * sample[featureIndex]; + } + bias += learningRate * error; + } + + private void ensureParametersAreFinite() { + if (!Double.isFinite(bias) || !isFinite(weights)) { + weights = null; + throw new ArithmeticException("training diverged; try a smaller learningRate or scaled features"); + } + } + + private void ensureFitted() { + if (weights == null) { + throw new IllegalStateException("classifier has not been fitted"); + } + } + + private void validateTrainingData(double[][] features, int[] labels) { + if (features == null || labels == null) { + throw new IllegalArgumentException("features and labels cannot be null"); + } + if (features.length == 0 || labels.length == 0) { + throw new IllegalArgumentException("features and labels cannot be empty"); + } + if (features.length != labels.length) { + throw new IllegalArgumentException("features and labels must have the same length"); + } + if (features[0] == null || features[0].length == 0) { + throw new IllegalArgumentException("feature vectors cannot be null or empty"); + } + + int featureCount = features[0].length; + for (int sampleIndex = 0; sampleIndex < features.length; sampleIndex++) { + double[] sample = features[sampleIndex]; + if (sample == null) { + throw new IllegalArgumentException("feature vectors cannot be null or empty"); + } + if (sample.length != featureCount) { + throw new IllegalArgumentException("all feature vectors must have the same dimension"); + } + validateFiniteValues(sample); + if (labels[sampleIndex] != 0 && labels[sampleIndex] != 1) { + throw new IllegalArgumentException("labels must be either 0 or 1"); + } + } + } + + private void validateSample(double[] sample) { + if (sample == null || sample.length != weights.length) { + throw new IllegalArgumentException("sample must match the training feature dimension"); + } + validateFiniteValues(sample); + } + + private static void validateFiniteValues(double[] values) { + if (!isFinite(values)) { + throw new IllegalArgumentException("feature values must be finite"); + } + } + + private static boolean isFinite(double[] values) { + for (double value : values) { + if (!Double.isFinite(value)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/thealgorithms/maths/AbsoluteMin.java b/src/main/java/com/thealgorithms/maths/AbsoluteMin.java index 1b9575a330dd..aab6fe0f426d 100644 --- a/src/main/java/com/thealgorithms/maths/AbsoluteMin.java +++ b/src/main/java/com/thealgorithms/maths/AbsoluteMin.java @@ -1,7 +1,5 @@ package com.thealgorithms.maths; -import java.util.Arrays; - public final class AbsoluteMin { private AbsoluteMin() { } @@ -13,14 +11,17 @@ private AbsoluteMin() { * @return The absolute min value */ public static int getMinValue(int... numbers) { - if (numbers.length == 0) { - throw new IllegalArgumentException("Numbers array cannot be empty"); + if (numbers == null || numbers.length == 0) { + throw new IllegalArgumentException("Numbers array cannot be empty or null"); } - var absMinWrapper = new Object() { int value = numbers[0]; }; - - Arrays.stream(numbers).skip(1).filter(number -> Math.abs(number) <= Math.abs(absMinWrapper.value)).forEach(number -> absMinWrapper.value = Math.min(absMinWrapper.value, number)); - - return absMinWrapper.value; + long absMin = numbers[0]; + for (int i = 1; i < numbers.length; i++) { + long current = numbers[i]; + if (Math.abs(current) < Math.abs(absMin) || (Math.abs(current) == Math.abs(absMin) && current < absMin)) { + absMin = current; + } + } + return (int) absMin; } } diff --git a/src/main/java/com/thealgorithms/maths/AbsoluteValue.java b/src/main/java/com/thealgorithms/maths/AbsoluteValue.java index b9279d5a244a..acc7302eb451 100644 --- a/src/main/java/com/thealgorithms/maths/AbsoluteValue.java +++ b/src/main/java/com/thealgorithms/maths/AbsoluteValue.java @@ -10,7 +10,7 @@ private AbsoluteValue() { * @param number The number to be transformed * @return The absolute value of the {@code number} */ - public static int getAbsValue(int number) { - return number < 0 ? -number : number; + public static long getAbsValue(long number) { + return Math.abs(number); } } diff --git a/src/main/java/com/thealgorithms/maths/AbundantNumber.java b/src/main/java/com/thealgorithms/maths/AbundantNumber.java new file mode 100644 index 000000000000..804ac4d71477 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/AbundantNumber.java @@ -0,0 +1,58 @@ +package com.thealgorithms.maths; + +/** + * In number theory, an abundant number or excessive number is a positive integer for which + * the sum of its proper divisors is greater than the number. + * Equivalently, it is a number for which the sum of proper divisors (or aliquot sum) is greater than n. + * + * The integer 12 is the first abundant number. Its proper divisors are 1, 2, 3, 4 and 6 for a total of 16. + * + * Wiki: https://en.wikipedia.org/wiki/Abundant_number + */ +public final class AbundantNumber { + + private AbundantNumber() { + } + + // Function to calculate sum of all divisors including n + private static int sumOfDivisors(int n) { + int sum = 1 + n; // 1 and n are always divisors + for (int i = 2; i <= n / 2; i++) { + if (n % i == 0) { + sum += i; // adding divisor to sum + } + } + return sum; + } + + // Common validation method + private static void validatePositiveNumber(int number) { + if (number <= 0) { + throw new IllegalArgumentException("Number must be positive."); + } + } + + /** + * Check if {@code number} is an Abundant number or not by checking sum of divisors > 2n + * + * @param number the number + * @return {@code true} if {@code number} is an Abundant number, otherwise false + */ + public static boolean isAbundant(int number) { + validatePositiveNumber(number); + + return sumOfDivisors(number) > 2 * number; + } + + /** + * Check if {@code number} is an Abundant number or not by checking Aliquot Sum > n + * + * @param number the number + * @return {@code true} if {@code number} is a Abundant number, otherwise false + */ + public static boolean isAbundantNumber(int number) { + validatePositiveNumber(number); + + return AliquotSum.getAliquotSum(number) > number; + } +} diff --git a/src/main/java/com/thealgorithms/maths/Area.java b/src/main/java/com/thealgorithms/maths/Area.java index a34ad6b01ab5..84fc67159379 100644 --- a/src/main/java/com/thealgorithms/maths/Area.java +++ b/src/main/java/com/thealgorithms/maths/Area.java @@ -10,17 +10,17 @@ private Area() { /** * String of IllegalArgumentException for radius */ - private static final String POSITIVE_RADIUS = "Must be a positive radius"; + private static final String POSITIVE_RADIUS = "Radius must be greater than 0"; /** * String of IllegalArgumentException for height */ - private static final String POSITIVE_HEIGHT = "Must be a positive height"; + private static final String POSITIVE_HEIGHT = "Height must be greater than 0"; /** * String of IllegalArgumentException for base */ - private static final String POSITIVE_BASE = "Must be a positive base"; + private static final String POSITIVE_BASE = "Base must be greater than 0"; /** * Calculate the surface area of a cube. @@ -30,11 +30,32 @@ private Area() { */ public static double surfaceAreaCube(final double sideLength) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException("Side length must be greater than 0"); } return 6 * sideLength * sideLength; } + /** + * Calculate the surface area of a cuboid. + * + * @param length length of the cuboid + * @param width width of the cuboid + * @param height height of the cuboid + * @return surface area of given cuboid + */ + public static double surfaceAreaCuboid(final double length, double width, double height) { + if (length <= 0) { + throw new IllegalArgumentException("Length must be greater than 0"); + } + if (width <= 0) { + throw new IllegalArgumentException("Width must be greater than 0"); + } + if (height <= 0) { + throw new IllegalArgumentException("Height must be greater than 0"); + } + return 2 * (length * width + length * height + width * height); + } + /** * Calculate the surface area of a sphere. * @@ -57,10 +78,10 @@ public static double surfaceAreaSphere(final double radius) { */ public static double surfaceAreaPyramid(final double sideLength, final double slantHeight) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException(""); } if (slantHeight <= 0) { - throw new IllegalArgumentException("Must be a positive slantHeight"); + throw new IllegalArgumentException("slant height must be greater than 0"); } double baseArea = sideLength * sideLength; double lateralSurfaceArea = 2 * sideLength * slantHeight; @@ -76,10 +97,10 @@ public static double surfaceAreaPyramid(final double sideLength, final double sl */ public static double surfaceAreaRectangle(final double length, final double width) { if (length <= 0) { - throw new IllegalArgumentException("Must be a positive length"); + throw new IllegalArgumentException("Length must be greater than 0"); } if (width <= 0) { - throw new IllegalArgumentException("Must be a positive width"); + throw new IllegalArgumentException("Width must be greater than 0"); } return length * width; } @@ -96,7 +117,7 @@ public static double surfaceAreaCylinder(final double radius, final double heigh throw new IllegalArgumentException(POSITIVE_RADIUS); } if (height <= 0) { - throw new IllegalArgumentException(POSITIVE_RADIUS); + throw new IllegalArgumentException(POSITIVE_HEIGHT); } return 2 * (Math.PI * radius * radius + Math.PI * radius * height); } @@ -109,7 +130,7 @@ public static double surfaceAreaCylinder(final double radius, final double heigh */ public static double surfaceAreaSquare(final double sideLength) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException("Side Length must be greater than 0"); } return sideLength * sideLength; } @@ -121,14 +142,14 @@ public static double surfaceAreaSquare(final double sideLength) { * @param height height of triangle * @return area of given triangle */ - public static double surfaceAreaTriangle(final double base, final double height) { - if (base <= 0) { + public static double surfaceAreaTriangle(final double baseLength, final double height) { + if (baseLength <= 0) { throw new IllegalArgumentException(POSITIVE_BASE); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return base * height / 2; + return baseLength * height / 2; } /** @@ -138,14 +159,14 @@ public static double surfaceAreaTriangle(final double base, final double height) * @param height height of a parallelogram * @return area of given parallelogram */ - public static double surfaceAreaParallelogram(final double base, final double height) { - if (base <= 0) { + public static double surfaceAreaParallelogram(final double baseLength, final double height) { + if (baseLength <= 0) { throw new IllegalArgumentException(POSITIVE_BASE); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return base * height; + return baseLength * height; } /** @@ -156,17 +177,17 @@ public static double surfaceAreaParallelogram(final double base, final double he * @param height height of trapezium * @return area of given trapezium */ - public static double surfaceAreaTrapezium(final double base1, final double base2, final double height) { - if (base1 <= 0) { + public static double surfaceAreaTrapezium(final double baseLength1, final double baseLength2, final double height) { + if (baseLength1 <= 0) { throw new IllegalArgumentException(POSITIVE_BASE + 1); } - if (base2 <= 0) { + if (baseLength2 <= 0) { throw new IllegalArgumentException(POSITIVE_BASE + 2); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return (base1 + base2) * height / 2; + return (baseLength1 + baseLength2) * height / 2; } /** diff --git a/src/main/java/com/thealgorithms/maths/Average.java b/src/main/java/com/thealgorithms/maths/Average.java index a550a7f6504d..cf55af509ccc 100644 --- a/src/main/java/com/thealgorithms/maths/Average.java +++ b/src/main/java/com/thealgorithms/maths/Average.java @@ -1,9 +1,16 @@ package com.thealgorithms.maths; +import java.util.Arrays; +import java.util.OptionalDouble; + /** * A utility class for computing the average of numeric arrays. - * This class provides static methods to calculate the average of arrays - * of both {@code double} and {@code int} values. + * + *

This class provides static methods to calculate the arithmetic mean + * of arrays of both {@code double} and {@code int} values. It also offers + * a Stream-based alternative for modern, declarative usage. + * + *

All methods guard against {@code null} or empty inputs. */ public final class Average { @@ -13,11 +20,14 @@ private Average() { } /** - * Computes the average of a {@code double} array. + * Computes the arithmetic mean of a {@code double} array. + * + *

The average is calculated as the sum of all elements divided + * by the number of elements: {@code avg = Ξ£(numbers[i]) / n}. * - * @param numbers an array of {@code double} values - * @return the average of the given numbers - * @throws IllegalArgumentException if the input array is {@code null} or empty + * @param numbers a non-null, non-empty array of {@code double} values + * @return the arithmetic mean of the given numbers + * @throws IllegalArgumentException if {@code numbers} is {@code null} or empty */ public static double average(double[] numbers) { if (numbers == null || numbers.length == 0) { @@ -31,11 +41,14 @@ public static double average(double[] numbers) { } /** - * Computes the average of an {@code int} array. + * Computes the arithmetic mean of an {@code int} array. + * + *

The sum is accumulated in a {@code long} to prevent integer overflow + * when processing large arrays or large values. * - * @param numbers an array of {@code int} values - * @return the average of the given numbers - * @throws IllegalArgumentException if the input array is {@code null} or empty + * @param numbers a non-null, non-empty array of {@code int} values + * @return the arithmetic mean as a {@code long} (truncated toward zero) + * @throws IllegalArgumentException if {@code numbers} is {@code null} or empty */ public static long average(int[] numbers) { if (numbers == null || numbers.length == 0) { @@ -47,4 +60,21 @@ public static long average(int[] numbers) { } return sum / numbers.length; } + + /** + * Computes the arithmetic mean of a {@code double} array using Java Streams. + * + *

This method is a declarative alternative to {@link #average(double[])}. + * Instead of throwing on empty input, it returns an empty {@link OptionalDouble}, + * following the convention of the Stream API. + * + * @param numbers an array of {@code double} values, may be {@code null} or empty + * @return an {@link OptionalDouble} with the mean, or empty if input is null/empty + */ + public static OptionalDouble averageStream(double[] numbers) { + if (numbers == null || numbers.length == 0) { + return OptionalDouble.empty(); + } + return Arrays.stream(numbers).average(); + } } diff --git a/src/main/java/com/thealgorithms/maths/BellNumbers.java b/src/main/java/com/thealgorithms/maths/BellNumbers.java new file mode 100644 index 000000000000..d4dc1014f48b --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/BellNumbers.java @@ -0,0 +1,59 @@ +package com.thealgorithms.maths; + +/** + * The Bell numbers count the number of partitions of a set. + * The n-th Bell number is the number of ways a set of n elements can be partitioned + * into nonempty subsets. + * + *

+ * This implementation uses the Bell Triangle (Aitken's array) method. + * Time Complexity: O(n^2) + * Space Complexity: O(n^2) + *

+ * + * @author Chahat Sandhu, singhc7 + * @see Bell Number (Wikipedia) + */ +public final class BellNumbers { + + private BellNumbers() { + } + + /** + * Calculates the n-th Bell number using the Bell Triangle. + * + * @param n the index of the Bell number (must be non-negative) + * @return the n-th Bell number + * @throws IllegalArgumentException if n is negative or n > 25 + */ + public static long compute(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be non-negative"); + } + if (n == 0) { + return 1; + } + if (n > 25) { + throw new IllegalArgumentException("n must be <= 25. For larger n, use BigInteger implementation."); + } + + // We use a 2D array to visualize the Bell Triangle + long[][] bellTriangle = new long[n + 1][n + 1]; + + // Base case: The triangle starts with 1 + bellTriangle[0][0] = 1; + + for (int i = 1; i <= n; i++) { + // Rule 1: The first number in a new row is the LAST number of the previous row + bellTriangle[i][0] = bellTriangle[i - 1][i - 1]; + + // Rule 2: Fill the rest of the row by adding the previous neighbor and the upper-left neighbor + for (int j = 1; j <= i; j++) { + bellTriangle[i][j] = bellTriangle[i][j - 1] + bellTriangle[i - 1][j - 1]; + } + } + + // The Bell number B_n is the first number in the n-th row + return bellTriangle[n][0]; + } +} diff --git a/src/main/java/com/thealgorithms/maths/ComplexNumberMultiply.java b/src/main/java/com/thealgorithms/maths/ComplexNumberMultiply.java new file mode 100644 index 000000000000..4b68b7824574 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/ComplexNumberMultiply.java @@ -0,0 +1,32 @@ +package com.thealgorithms.maths; + +/** + * Multiplies two complex numbers represented as strings in the form "a+bi". + * Supports negative values and validates input format. + */ +public final class ComplexNumberMultiply { + + private ComplexNumberMultiply() { + } + + private static int[] parse(String num) { + if (num == null || !num.matches("-?\\d+\\+-?\\d+i")) { + throw new IllegalArgumentException("Invalid complex number format: " + num); + } + + String[] parts = num.split("\\+"); + int real = Integer.parseInt(parts[0]); + int imaginary = Integer.parseInt(parts[1].replace("i", "")); + return new int[] {real, imaginary}; + } + + public static String multiply(String num1, String num2) { + int[] a = parse(num1); + int[] b = parse(num2); + + int real = a[0] * b[0] - a[1] * b[1]; + int imaginary = a[0] * b[1] + a[1] * b[0]; + + return real + "+" + imaginary + "i"; + } +} diff --git a/src/main/java/com/thealgorithms/maths/Correlation.java b/src/main/java/com/thealgorithms/maths/Correlation.java new file mode 100644 index 000000000000..a46445fb23b7 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/Correlation.java @@ -0,0 +1,51 @@ +package com.thealgorithms.maths; + +/** + * Class for correlation of two discrete variables + */ + +public final class Correlation { + private Correlation() { + } + + public static final double DELTA = 1e-9; + + /** + * Discrete correlation function. + * Correlation between two discrete variables is calculated + * according to the formula: Cor(x, y)=Cov(x, y)/sqrt(Var(x)*Var(y)). + * Correlation with a constant variable is taken to be zero. + * + * @param x The first discrete variable + * @param y The second discrete variable + * @param n The number of values for each variable + * @return The result of the correlation of variables x,y. + */ + public static double correlation(double[] x, double[] y, int n) { + double exy = 0; // E(XY) + double ex = 0; // E(X) + double exx = 0; // E(X^2) + double ey = 0; // E(Y) + double eyy = 0; // E(Y^2) + for (int i = 0; i < n; i++) { + exy += x[i] * y[i]; + ex += x[i]; + exx += x[i] * x[i]; + ey += y[i]; + eyy += y[i] * y[i]; + } + exy /= n; + ex /= n; + exx /= n; + ey /= n; + eyy /= n; + double cov = exy - ex * ey; // Cov(X, Y) = E(XY)-E(X)E(Y) + double varx = Math.sqrt(exx - ex * ex); // Var(X) = sqrt(E(X^2)-E(X)^2) + double vary = Math.sqrt(eyy - ey * ey); // Var(Y) = sqrt(E(Y^2)-E(Y)^2) + if (varx * vary < DELTA) { // Var(X) = 0 means X = const, the same about Y + return 0; + } else { + return cov / Math.sqrt(varx * vary); + } + } +} diff --git a/src/main/java/com/thealgorithms/maths/DigitalRoot.java b/src/main/java/com/thealgorithms/maths/DigitalRoot.java index e8f5305c7569..c7367e5965df 100644 --- a/src/main/java/com/thealgorithms/maths/DigitalRoot.java +++ b/src/main/java/com/thealgorithms/maths/DigitalRoot.java @@ -55,7 +55,7 @@ public static int digitalRoot(int n) { // This function is used for finding the sum of the digits of number public static int single(int n) { - if (n <= 9) { // if n becomes less than 10 than return n + if (n <= 9) { // if n becomes less than 10 then return n return n; } else { return (n % 10) + single(n / 10); // n % 10 for extracting digits one by one diff --git a/src/main/java/com/thealgorithms/maths/DisariumNumber.java b/src/main/java/com/thealgorithms/maths/DisariumNumber.java new file mode 100644 index 000000000000..0196d0797bcc --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/DisariumNumber.java @@ -0,0 +1,37 @@ +package com.thealgorithms.maths; + +/** + * Disarium number is a number where the sum of its digits powered + * with their respective positions is equal to the number itself. + * Example: 135 = 1^1 + 3^2 + 5^3 = 1 + 9 + 125 = 135 + * + * @see Disarium Number + */ +public final class DisariumNumber { + + private DisariumNumber() { + } + + /** + * Checks if a number is a Disarium number. + * + * @param number the number to check (must be positive) + * @return true if number is Disarium, false otherwise + * @throws IllegalArgumentException if number is not positive + */ + public static boolean isDisarium(int number) { + if (number <= 0) { + throw new IllegalArgumentException("Input must be a positive integer."); + } + int digits = String.valueOf(number).length(); + int temp = number; + int sum = 0; + while (temp > 0) { + int lastDigit = temp % 10; + sum += (int) Math.pow(lastDigit, digits); + digits--; + temp /= 10; + } + return sum == number; + } +} diff --git a/src/main/java/com/thealgorithms/maths/DistanceBetweenTwoPoints.java b/src/main/java/com/thealgorithms/maths/DistanceBetweenTwoPoints.java new file mode 100644 index 000000000000..cd1c9205b328 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/DistanceBetweenTwoPoints.java @@ -0,0 +1,33 @@ +package com.thealgorithms.maths; + +/** + * Distance Between Two Points in 2D Space. + * + *

This class provides a method to calculate the Euclidean distance between two points in a + * two-dimensional plane. + * + *

Formula: d = sqrt((x2 - x1)^2 + (y2 - y1)^2) + * + *

Reference: https://en.wikipedia.org/wiki/Euclidean_distance + */ +public final class DistanceBetweenTwoPoints { + + private DistanceBetweenTwoPoints() { + // Utility class; prevent instantiation + } + + /** + * Calculate the Euclidean distance between two points. + * + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @return Euclidean distance between the two points + */ + public static double calculate(final double x1, final double y1, final double x2, final double y2) { + final double deltaX = x2 - x1; + final double deltaY = y2 - y1; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } +} diff --git a/src/main/java/com/thealgorithms/maths/EvilNumber.java b/src/main/java/com/thealgorithms/maths/EvilNumber.java new file mode 100644 index 000000000000..a10794fb5a85 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/EvilNumber.java @@ -0,0 +1,39 @@ +package com.thealgorithms.maths; + +/** + * In number theory, an evil number is a non-negative integer that has an even number of 1s in its binary expansion. + * Non-negative integers that are not evil are called odious numbers. + * + * Evil Number Wiki: https://en.wikipedia.org/wiki/Evil_number + * Odious Number Wiki: https://en.wikipedia.org/wiki/Odious_number + */ +public final class EvilNumber { + + private EvilNumber() { + } + + // Function to count number of one bits in a number using bitwise operators + private static int countOneBits(int number) { + int oneBitCounter = 0; + while (number > 0) { + oneBitCounter += number & 1; // increment count if last bit is 1 + number >>= 1; // right shift to next bit + } + return oneBitCounter; + } + + /** + * Check either {@code number} is an Evil number or Odious number + * + * @param number the number + * @return {@code true} if {@code number} is an Evil number, otherwise false (in case of Odious number) + */ + public static boolean isEvilNumber(int number) { + if (number < 0) { + throw new IllegalArgumentException("Negative numbers are not allowed."); + } + + int noOfOneBits = countOneBits(number); + return noOfOneBits % 2 == 0; + } +} diff --git a/src/main/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithm.java b/src/main/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithm.java new file mode 100644 index 000000000000..4934d4493bf2 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithm.java @@ -0,0 +1,48 @@ +package com.thealgorithms.maths; + +/** + * In mathematics, the extended Euclidean algorithm is an extension to the + * Euclidean algorithm, and computes, in addition to the greatest common divisor + * (gcd) of integers a and b, also the coefficients of BΓ©zout's identity, which + * are integers x and y such that ax + by = gcd(a, b). + * + *

+ * For more details, see + * https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm + */ +public final class ExtendedEuclideanAlgorithm { + + private ExtendedEuclideanAlgorithm() { + } + + /** + * This method implements the extended Euclidean algorithm. + * + * @param a The first number. + * @param b The second number. + * @return An array of three integers: + *

    + *
  • Index 0: The greatest common divisor (gcd) of a and b.
  • + *
  • Index 1: The value of x in the equation ax + by = gcd(a, b).
  • + *
  • Index 2: The value of y in the equation ax + by = gcd(a, b).
  • + *
+ */ + public static long[] extendedGCD(long a, long b) { + if (b == 0) { + // Base case: gcd(a, 0) = a. The equation is a*1 + 0*0 = a. + return new long[] {a, 1, 0}; + } + + // Recursive call + long[] result = extendedGCD(b, a % b); + long gcd = result[0]; + long x1 = result[1]; + long y1 = result[2]; + + // Update coefficients using the results from the recursive call + long x = y1; + long y = x1 - a / b * y1; + + return new long[] {gcd, x, y}; + } +} diff --git a/src/main/java/com/thealgorithms/maths/Factorial.java b/src/main/java/com/thealgorithms/maths/Factorial.java index 511cc1f84f05..8ad219a3066c 100644 --- a/src/main/java/com/thealgorithms/maths/Factorial.java +++ b/src/main/java/com/thealgorithms/maths/Factorial.java @@ -1,23 +1,19 @@ package com.thealgorithms.maths; +import java.math.BigInteger; + public final class Factorial { private Factorial() { } - /** - * Calculate factorial N using iteration - * - * @param n the number - * @return the factorial of {@code n} - */ - public static long factorial(int n) { + public static BigInteger factorial(int n) { if (n < 0) { throw new IllegalArgumentException("Input number cannot be negative"); } - long factorial = 1; - for (int i = 1; i <= n; ++i) { - factorial *= i; + BigInteger result = BigInteger.ONE; + for (int i = 1; i <= n; i++) { + result = result.multiply(BigInteger.valueOf(i)); } - return factorial; + return result; } } diff --git a/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java b/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java index 84390860ccc4..8a93580a4b72 100644 --- a/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java +++ b/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java @@ -6,9 +6,23 @@ import java.util.stream.Stream; /** - * @author: caos321 - * @date: 14 October 2021 (Thursday) + * Calculates Fibonacci numbers using a functional programming paradigm with Java Streams. + *

+ * This specific implementation uses {@link java.util.stream.Stream#iterate} and reductions to generate terms. + *

+ * For alternative approaches to compute or verify Fibonacci numbers, see: + *

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming approaches (Memoization, Bottom-Up, Optimized)
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
+ * * @author caos321 + * @date 14 October 2021 (Thursday) */ + public final class FibonacciJavaStreams { private FibonacciJavaStreams() { } diff --git a/src/main/java/com/thealgorithms/maths/FibonacciLoop.java b/src/main/java/com/thealgorithms/maths/FibonacciLoop.java index de23a4305c3f..f19e3a6969c5 100644 --- a/src/main/java/com/thealgorithms/maths/FibonacciLoop.java +++ b/src/main/java/com/thealgorithms/maths/FibonacciLoop.java @@ -1,9 +1,20 @@ package com.thealgorithms.maths; - import java.math.BigInteger; - /** * This class provides methods for calculating Fibonacci numbers using BigInteger for large values of 'n'. + *

+ * This specific implementation uses an Iterative approach (Loop) with {@code O(n)} time complexity + * and {@code O(1)} space complexity. + *

+ * For alternative approaches to compute or verify Fibonacci numbers, see: + *

    + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming approaches (Memoization, Bottom-Up, Optimized)
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
*/ public final class FibonacciLoop { diff --git a/src/main/java/com/thealgorithms/maths/FibonacciNumberCheck.java b/src/main/java/com/thealgorithms/maths/FibonacciNumberCheck.java index 781275d3130d..6bdc2d6ed9fb 100644 --- a/src/main/java/com/thealgorithms/maths/FibonacciNumberCheck.java +++ b/src/main/java/com/thealgorithms/maths/FibonacciNumberCheck.java @@ -4,6 +4,18 @@ * Fibonacci: 0 1 1 2 3 5 8 13 21 ... * This code checks Fibonacci Numbers up to 45th number. * Other checks fail because of 'long'-type overflow. + *

+ * This class serves as a verification utility rather than a generation algorithm. + *

+ * For approaches that actively compute the n-th Fibonacci number, see: + *

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming approaches (Memoization, Bottom-Up, Optimized)
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
*/ public final class FibonacciNumberCheck { private FibonacciNumberCheck() { diff --git a/src/main/java/com/thealgorithms/maths/FibonacciNumberGoldenRation.java b/src/main/java/com/thealgorithms/maths/FibonacciNumberGoldenRation.java index 4df37a40f541..eca6379b93bc 100644 --- a/src/main/java/com/thealgorithms/maths/FibonacciNumberGoldenRation.java +++ b/src/main/java/com/thealgorithms/maths/FibonacciNumberGoldenRation.java @@ -3,6 +3,18 @@ /** * This class provides methods for calculating Fibonacci numbers using Binet's formula. * Binet's formula is based on the golden ratio and allows computing Fibonacci numbers efficiently. + *

+ * This specific implementation provides a closed-form solution with an expected {@code O(1)} time complexity. + *

+ * For alternative approaches to compute or verify Fibonacci numbers, see: + *

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming approaches (Memoization, Bottom-Up, Optimized)
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
* * @see Binet's formula on Wikipedia */ diff --git a/src/main/java/com/thealgorithms/maths/FriendlyNumber.java b/src/main/java/com/thealgorithms/maths/FriendlyNumber.java new file mode 100644 index 000000000000..900ce89295a4 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/FriendlyNumber.java @@ -0,0 +1,49 @@ +package com.thealgorithms.maths; + +/** + * Two numbers are Friendly if they share the same abundancy index, + * which is the ratio of the sum of divisors to the number itself. + * Example: 6 and 28 are friendly because sigma(6)/6 = 2 and sigma(28)/28 = 2 + * + * @see + * Wikipedia: Friendly Number + * + * @author Vraj Prajapati @Rosander0 + */ +public final class FriendlyNumber { + + private FriendlyNumber() { + // Utility class + } + + private static int sumOfDivisors(final int number) { + int sum = 0; + final int root = (int) Math.sqrt(number); + for (int i = 1; i <= root; i++) { + if (number % i == 0) { + sum += i; + final int other = number / i; + if (other != i) { + sum += other; + } + } + } + return sum; + } + + /** + * Checks whether two numbers are Friendly Numbers. + * + * @param a First number (must be positive) + * @param b Second number (must be positive) + * @return true if a and b are friendly numbers, false otherwise + */ + public static boolean areFriendly(final int a, final int b) { + if (a <= 0 || b <= 0) { + return false; + } + final long sigmaA = sumOfDivisors(a); + final long sigmaB = sumOfDivisors(b); + return sigmaA * b == sigmaB * a; + } +} diff --git a/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java b/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java new file mode 100644 index 000000000000..f4f2e23c3932 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java @@ -0,0 +1,44 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +/** + * The Jacobsthal Sequence is a sequence of integers defined by the recurrence relation: + * J(n) = J(n-1) + 2*J(n-2) with initial values J(0) = 0, J(1) = 1. + * Example: 0, 1, 1, 3, 5, 11, 21, 43, 85, 171, 341... + * + * @see + * Wikipedia: Jacobsthal Number + */ +public final class JacobsthalNumber { + + private JacobsthalNumber() { + // Utility class + } + + /** + * Calculates the nth term of the Jacobsthal Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Jacobsthal Sequence + */ + public static long jacobsthal(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative!"); + } + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + long a = 0; + long b = 1; + long result = 0; + for (int i = 2; i <= n; i++) { + result = b + 2 * a; + a = b; + b = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java b/src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java similarity index 94% rename from src/main/java/com/thealgorithms/others/LowestBasePalindrome.java rename to src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java index a3ca8d6f6db8..4a79b4298fc4 100644 --- a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java +++ b/src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java @@ -1,4 +1,4 @@ -package com.thealgorithms.others; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.List; @@ -23,6 +23,12 @@ * * @see OEIS A016026 - Smallest base in which * n is palindromic + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber * @author TheAlgorithms Contributors */ public final class LowestBasePalindrome { diff --git a/src/main/java/com/thealgorithms/maths/LuckyNumber.java b/src/main/java/com/thealgorithms/maths/LuckyNumber.java new file mode 100644 index 000000000000..70308e1e0edd --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/LuckyNumber.java @@ -0,0 +1,78 @@ +package com.thealgorithms.maths; + +/** + * In number theory, a lucky number is a natural number in a set which is generated by a certain "sieve". + * This sieve is similar to the sieve of Eratosthenes that generates the primes, + * but it eliminates numbers based on their position in the remaining set, + * instead of their value (or position in the initial set of natural numbers). + * + * Wiki: https://en.wikipedia.org/wiki/Lucky_number + */ +public final class LuckyNumber { + + private LuckyNumber() { + } + + // Common validation method + private static void validatePositiveNumber(int number) { + if (number <= 0) { + throw new IllegalArgumentException("Number must be positive."); + } + } + + // Function to check recursively for Lucky Number + private static boolean isLuckyRecursiveApproach(int n, int counter) { + // Base case: If counter exceeds n, number is lucky + if (counter > n) { + return true; + } + + // If number is eliminated in this step, it's not lucky + if (n % counter == 0) { + return false; + } + + // Calculate new position after removing every counter-th number + int newNumber = n - (n / counter); + + // Recursive call for next round + return isLuckyRecursiveApproach(newNumber, counter + 1); + } + + /** + * Check if {@code number} is a Lucky number or not using recursive approach + * + * @param number the number + * @return {@code true} if {@code number} is a Lucky number, otherwise false + */ + public static boolean isLuckyNumber(int number) { + validatePositiveNumber(number); + int counterStarting = 2; + return isLuckyRecursiveApproach(number, counterStarting); + } + + /** + * Check if {@code number} is a Lucky number or not using iterative approach + * + * @param number the number + * @return {@code true} if {@code number} is a Lucky number, otherwise false + */ + public static boolean isLucky(int number) { + validatePositiveNumber(number); + + int counter = 2; // Position starts from 2 (since first elimination happens at 2) + int position = number; // The position of the number in the sequence + + while (counter <= position) { + if (position % counter == 0) { + return false; + } // Number is eliminated + + // Update the position of n after removing every counter-th number + position = position - (position / counter); + counter++; + } + + return true; // Survives all eliminations β†’ Lucky Number + } +} diff --git a/src/main/java/com/thealgorithms/maths/MathBuilder.java b/src/main/java/com/thealgorithms/maths/MathBuilder.java index 1cf3d8b7fc9a..08d2ae2c9456 100644 --- a/src/main/java/com/thealgorithms/maths/MathBuilder.java +++ b/src/main/java/com/thealgorithms/maths/MathBuilder.java @@ -481,8 +481,8 @@ public Builder closeParenthesisAndDivide() { } public Builder format(String format) { - DecimalFormat formater = new DecimalFormat(format); - String num = formater.format(number); + DecimalFormat formatter = new DecimalFormat(format); + String num = formatter.format(number); number = Double.parseDouble(num); return this; } @@ -490,8 +490,8 @@ public Builder format(String format) { public Builder format(int decimalPlace) { String pattern = "." + "#".repeat(decimalPlace); - DecimalFormat formater = new DecimalFormat(pattern); - String num = formater.format(number); + DecimalFormat formatter = new DecimalFormat(pattern); + String num = formatter.format(number); number = Double.parseDouble(num); return this; } diff --git a/src/main/java/com/thealgorithms/maths/Means.java b/src/main/java/com/thealgorithms/maths/Means.java index 5445a3caebc7..d77eb1d3f661 100644 --- a/src/main/java/com/thealgorithms/maths/Means.java +++ b/src/main/java/com/thealgorithms/maths/Means.java @@ -107,6 +107,28 @@ public static Double harmonic(final Iterable numbers) { return size / sumOfReciprocals; } + /** + * Computes the quadratic mean (root mean square) of the given numbers. + *

+ * The quadratic mean is calculated as: √[(x₁^2 Γ— xβ‚‚^2 Γ— ... Γ— xβ‚™^2)/n] + *

+ *

+ * Example: For numbers [1, 7], the quadratic mean is √[(1^2+7^2)/2] = √25 = 5.0 + *

+ * + * @param numbers the input numbers (must not be empty) + * @return the quadratic mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Quadratic + * Mean + */ + public static Double quadratic(final Iterable numbers) { + checkIfNotEmpty(numbers); + double sumOfSquares = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + y * y); + int size = IterableUtils.size(numbers); + return Math.pow(sumOfSquares / size, 0.5); + } + /** * Validates that the input iterable is not empty. * diff --git a/src/main/java/com/thealgorithms/maths/PadovanSequence.java b/src/main/java/com/thealgorithms/maths/PadovanSequence.java new file mode 100644 index 000000000000..51e7d2441b15 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PadovanSequence.java @@ -0,0 +1,43 @@ +package com.thealgorithms.maths; + +/** + * The Padovan Sequence is a sequence of integers defined by the recurrence relation: + * P(n) = P(n-2) + P(n-3) with initial values P(0) = P(1) = P(2) = 1. + * Example: 1, 1, 1, 2, 2, 3, 4, 5, 7, 9, 12, 16, 21, 28, 37... + * + * @see + * Wikipedia: Padovan Sequence + * @author Vraj Prajapati (@Rosander0) + */ +public final class PadovanSequence { + + private PadovanSequence() { + // Utility class + } + + /** + * Calculates the nth term of the Padovan Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Padovan Sequence + */ + public static long padovan(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative. Received: " + n); + } + if (n <= 2) { + return 1; + } + long a = 1; + long b = 1; + long c = 1; + long result = 0; + for (int i = 3; i <= n; i++) { + result = a + b; + a = b; + b = c; + c = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/maths/PalindromeNumber.java b/src/main/java/com/thealgorithms/maths/PalindromeNumber.java index a22d63897b37..9543f83332a7 100644 --- a/src/main/java/com/thealgorithms/maths/PalindromeNumber.java +++ b/src/main/java/com/thealgorithms/maths/PalindromeNumber.java @@ -1,5 +1,16 @@ package com.thealgorithms.maths; +/** + * A class to check if a given number is a palindrome. + * A palindromic number is a number that remains the same when its digits are reversed. + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + */ public final class PalindromeNumber { private PalindromeNumber() { } diff --git a/src/main/java/com/thealgorithms/misc/PalindromePrime.java b/src/main/java/com/thealgorithms/maths/PalindromePrime.java similarity index 73% rename from src/main/java/com/thealgorithms/misc/PalindromePrime.java rename to src/main/java/com/thealgorithms/maths/PalindromePrime.java index 164e957a9d12..21b76acefee8 100644 --- a/src/main/java/com/thealgorithms/misc/PalindromePrime.java +++ b/src/main/java/com/thealgorithms/maths/PalindromePrime.java @@ -1,8 +1,19 @@ -package com.thealgorithms.misc; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.List; +/** + * A class to check and generate palindromic prime numbers. + * A palindromic prime is a prime number that is also a palindromic number. + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromeNumber + */ public final class PalindromePrime { private PalindromePrime() { } diff --git a/src/main/java/com/thealgorithms/maths/PerfectSquare.java b/src/main/java/com/thealgorithms/maths/PerfectSquare.java index e9318bd7d805..aec43062121a 100644 --- a/src/main/java/com/thealgorithms/maths/PerfectSquare.java +++ b/src/main/java/com/thealgorithms/maths/PerfectSquare.java @@ -15,6 +15,9 @@ private PerfectSquare() { * false */ public static boolean isPerfectSquare(final int number) { + if (number < 0) { + return false; + } final int sqrt = (int) Math.sqrt(number); return sqrt * sqrt == number; } @@ -27,6 +30,9 @@ public static boolean isPerfectSquare(final int number) { * {@code false} */ public static boolean isPerfectSquareUsingPow(long number) { + if (number < 0) { + return false; + } long a = (long) Math.pow(number, 1.0 / 2); return a * a == number; } diff --git a/src/main/java/com/thealgorithms/maths/Perimeter.java b/src/main/java/com/thealgorithms/maths/Perimeter.java index f8aa1876d388..670851eb346b 100644 --- a/src/main/java/com/thealgorithms/maths/Perimeter.java +++ b/src/main/java/com/thealgorithms/maths/Perimeter.java @@ -27,7 +27,7 @@ public static float perimeterRegularPolygon(int n, float side) { * @param side2 for length of side 2 * @param side3 for length of side 3 * @param sides for length of remaining sides - * @return Perimeter of given trapezoid. + * @return Perimeter of given irregular polygon. */ public static float perimeterIrregularPolygon(float side1, float side2, float side3, float... sides) { float perimeter = side1 + side2 + side3; diff --git a/src/main/java/com/thealgorithms/maths/PerrinNumber.java b/src/main/java/com/thealgorithms/maths/PerrinNumber.java new file mode 100644 index 000000000000..cee45a1c5538 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PerrinNumber.java @@ -0,0 +1,53 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +/** + * The Perrin Sequence is a sequence of integers defined by the recurrence relation: + * P(n) = P(n-2) + P(n-3) with initial values P(0) = 3, P(1) = 0, P(2) = 2. + * Example: 3, 0, 2, 3, 2, 5, 5, 7, 10, 12, 17, 22, 29, 39, 51... + * + * Note: The Perrin Sequence uses the same recurrence relation as the Padovan Sequence + * but has different initial values. + * + * @see + * Wikipedia: Perrin Number + * @see PadovanSequence + */ +public final class PerrinNumber { + + private PerrinNumber() { + // Utility class + } + + /** + * Calculates the nth term of the Perrin Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Perrin Sequence + */ + public static long perrin(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative!"); + } + if (n == 0) { + return 3; + } + if (n == 1) { + return 0; + } + if (n == 2) { + return 2; + } + long a = 3; + long b = 0; + long c = 2; + long result = 0; + for (int i = 3; i <= n; i++) { + result = a + b; + a = b; + b = c; + c = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/maths/PollardRho.java b/src/main/java/com/thealgorithms/maths/PollardRho.java index 7fa913b21b7e..c63f93d839bf 100644 --- a/src/main/java/com/thealgorithms/maths/PollardRho.java +++ b/src/main/java/com/thealgorithms/maths/PollardRho.java @@ -54,7 +54,7 @@ static int g(int base, int modulus) { /** * This method returns a non-trivial factor of given integer number * - * @param number Integer is a integer value whose non-trivial factor is to be found + * @param number Integer is an integer value whose non-trivial factor is to be found * @return Integer non-trivial factor of number * @throws RuntimeException object if GCD of given number cannot be found */ diff --git a/src/main/java/com/thealgorithms/maths/PowerOfFour.java b/src/main/java/com/thealgorithms/maths/PowerOfFour.java new file mode 100644 index 000000000000..e5fe6255821b --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PowerOfFour.java @@ -0,0 +1,36 @@ +package com.thealgorithms.maths; + +/** + * Utility class for checking if a number is a power of four. + * A power of four is a number that can be expressed as 4^n where n is a non-negative integer. + * This class provides a method to determine if a given integer is a power of four using bit manipulation. + * + * @author krishna-medapati (https://github.com/krishna-medapati) + */ +public final class PowerOfFour { + private PowerOfFour() { + } + + /** + * Checks if the given integer is a power of four. + * + * A number is considered a power of four if: + * 1. It is greater than zero + * 2. It has exactly one '1' bit in its binary representation (power of two) + * 3. The '1' bit is at an even position (0, 2, 4, 6, ...) + * + * The method uses the mask 0x55555555 (binary: 01010101010101010101010101010101) + * to check if the set bit is at an even position. + * + * @param number the integer to check + * @return true if the number is a power of four, false otherwise + */ + public static boolean isPowerOfFour(int number) { + if (number <= 0) { + return false; + } + boolean isPowerOfTwo = (number & (number - 1)) == 0; + boolean hasEvenBitPosition = (number & 0x55555555) != 0; + return isPowerOfTwo && hasEvenBitPosition; + } +} diff --git a/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java b/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java index 3d4e4eff0f03..ec1785a916c7 100644 --- a/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java +++ b/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java @@ -31,27 +31,28 @@ public static int mobius(int number) { throw new IllegalArgumentException("Number must be greater than zero."); } - if (number == 1) { - // return 1 if number passed is less or is 1 - return 1; - } - int primeFactorCount = 0; + int remaining = number; - for (int i = 1; i <= number; i++) { - // find prime factors of number - if (number % i == 0 && PrimeCheck.isPrime(i)) { - // check if number is divisible by square of prime factor - if (number % (i * i) == 0) { - // if number is divisible by square of prime factor + /* Divide out every prime factor in turn. Trial division only has to run up to the square + root of the remaining value, and the multiplication is widened to long so that the bound + does not overflow for numbers close to Integer.MAX_VALUE. */ + for (int factor = 2; (long) factor * factor <= remaining; factor++) { + if (remaining % factor == 0) { + remaining /= factor; + if (remaining % factor == 0) { + // number is divisible by the square of this prime factor return 0; } - /*increment primeFactorCount by 1 - if number is not divisible by square of found prime factor*/ primeFactorCount++; } } + /* Whatever is left is either 1 or a single prime factor larger than the square root. */ + if (remaining > 1) { + primeFactorCount++; + } + return (primeFactorCount % 2 == 0) ? 1 : -1; } } diff --git a/src/main/java/com/thealgorithms/maths/SieveOfEratosthenes.java b/src/main/java/com/thealgorithms/maths/SieveOfEratosthenes.java index f22d22e8c6af..5a15c4201a15 100644 --- a/src/main/java/com/thealgorithms/maths/SieveOfEratosthenes.java +++ b/src/main/java/com/thealgorithms/maths/SieveOfEratosthenes.java @@ -1,66 +1,82 @@ package com.thealgorithms.maths; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; /** - * @brief utility class implementing Sieve of Eratosthenes + * Sieve of Eratosthenes Algorithm + * An efficient algorithm to find all prime numbers up to a given limit. + * + * Algorithm: + * 1. Create a boolean array of size n+1, initially all true + * 2. Mark 0 and 1 as not prime + * 3. For each number i from 2 to sqrt(n): + * - If i is still marked as prime + * - Mark all multiples of i (starting from iΒ²) as not prime + * 4. Collect all numbers still marked as prime + * + * Time Complexity: O(n log log n) + * Space Complexity: O(n) + * + * @author Navadeep0007 + * @see Sieve of Eratosthenes */ public final class SieveOfEratosthenes { + private SieveOfEratosthenes() { + // Utility class, prevent instantiation } - private static void checkInput(int n) { - if (n <= 0) { - throw new IllegalArgumentException("n must be positive."); + /** + * Finds all prime numbers up to n using the Sieve of Eratosthenes algorithm + * + * @param n the upper limit (inclusive) + * @return a list of all prime numbers from 2 to n + * @throws IllegalArgumentException if n is negative + */ + public static List findPrimes(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative"); } - } - private static Type[] sievePrimesTill(int n) { - checkInput(n); - Type[] isPrimeArray = new Type[n + 1]; - Arrays.fill(isPrimeArray, Type.PRIME); - isPrimeArray[0] = Type.NOT_PRIME; - isPrimeArray[1] = Type.NOT_PRIME; + if (n < 2) { + return new ArrayList<>(); + } + + // Create boolean array, initially all true + boolean[] isPrime = new boolean[n + 1]; + for (int i = 2; i <= n; i++) { + isPrime[i] = true; + } - double cap = Math.sqrt(n); - for (int i = 2; i <= cap; i++) { - if (isPrimeArray[i] == Type.PRIME) { - for (int j = 2; i * j <= n; j++) { - isPrimeArray[i * j] = Type.NOT_PRIME; + // Sieve process + for (int i = 2; i * i <= n; i++) { + if (isPrime[i]) { + // Mark all multiples of i as not prime + for (int j = i * i; j <= n; j += i) { + isPrime[j] = false; } } } - return isPrimeArray; - } - - private static int countPrimes(Type[] isPrimeArray) { - return (int) Arrays.stream(isPrimeArray).filter(element -> element == Type.PRIME).count(); - } - private static int[] extractPrimes(Type[] isPrimeArray) { - int numberOfPrimes = countPrimes(isPrimeArray); - int[] primes = new int[numberOfPrimes]; - int primeIndex = 0; - for (int curNumber = 0; curNumber < isPrimeArray.length; ++curNumber) { - if (isPrimeArray[curNumber] == Type.PRIME) { - primes[primeIndex++] = curNumber; + // Collect all prime numbers + List primes = new ArrayList<>(); + for (int i = 2; i <= n; i++) { + if (isPrime[i]) { + primes.add(i); } } + return primes; } /** - * @brief finds all of the prime numbers up to the given upper (inclusive) limit - * @param n upper (inclusive) limit - * @exception IllegalArgumentException n is non-positive - * @return the array of all primes up to the given number (inclusive) + * Counts the number of prime numbers up to n + * + * @param n the upper limit (inclusive) + * @return count of prime numbers from 2 to n */ - public static int[] findPrimesTill(int n) { - return extractPrimes(sievePrimesTill(n)); - } - - private enum Type { - PRIME, - NOT_PRIME, + public static int countPrimes(int n) { + return findPrimes(n).size(); } } diff --git a/src/main/java/com/thealgorithms/maths/SigmoidActivation.java b/src/main/java/com/thealgorithms/maths/SigmoidActivation.java new file mode 100644 index 000000000000..aa12886e171b --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SigmoidActivation.java @@ -0,0 +1,74 @@ +package com.thealgorithms.maths; + +/** + * Implementation of the Sigmoid Activation function. + * Sigmoid function is used as an activation function in machine learning and neural networks + * for modeling binary classification problems, smoothing outputs, and introducing non-linearity + * into models. + * + * @author Alikhan Turugeldiyev + */ + +public final class SigmoidActivation { + + private SigmoidActivation() { + } + + /** + * @summary Maps any real-valued number into a value between 0 and 1 + * @param x In machine learning, x could be a weighted sum of inputs in a neural network neuron or a raw score in logistic regression. + * @return The output (range) of the sigmoid function is always strictly between 0 and 1. + */ + public static double activate(double x) { + // If the number x is NaN then, returning NaN to saving from unexpected output. + if (Double.isNaN(x)) { + return Double.NaN; + } + // Saving from unnecessary and heavy calculations. + // lim x->-inf sigmoid(x) will return number very close to 0 + if (x < -745) { + return 0.0; + } + // lim x->inf sigmoid(x) will return number very close to 1 + if (x > 745) { + return 1.0; + } + // sigmoid function's formula + return 1.0 / (1 + Math.exp((-1) * x)); + } + + public static double[][] activate(double[][] x) { + // apply calculation to every value in batch. + double[][] activatedNumbers = new double[x.length][]; + for (int i = 0; i < x.length; i++) { + activatedNumbers[i] = new double[x[i].length]; + for (int j = 0; j < x[i].length; j++) { + activatedNumbers[i][j] = activate(x[i][j]); + } + } + return activatedNumbers; + } + + /** + * @summary Calculates gradients for mapped values. By the chain rule, you can calculate error. + * @param y Activated by sigmoid function value. + * @return The output is a gradient of the activated value. + */ + public static double grad(double y) { + // sigmoid function derivative is reducing to this value. + // sigmoid'(x) = sigmoid(x) * (1-sigmoid(x)) + return y * (1 - y); + } + + public static double[][] grad(double[][] y) { + // apply calculation to every value in batch. + double[][] grads = new double[y.length][]; + for (int i = 0; i < y.length; i++) { + grads[i] = new double[y[i].length]; + for (int j = 0; j < y[i].length; j++) { + grads[i][j] = grad(y[i][j]); + } + } + return grads; + } +} diff --git a/src/main/java/com/thealgorithms/maths/SmithNumber.java b/src/main/java/com/thealgorithms/maths/SmithNumber.java new file mode 100644 index 000000000000..c06e0023d9bb --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SmithNumber.java @@ -0,0 +1,52 @@ +package com.thealgorithms.maths; + +import com.thealgorithms.maths.Prime.PrimeCheck; + +/** + * In number theory, a smith number is a composite number for which, in a given number base, + * the sum of its digits is equal to the sum of the digits in its prime factorization in the same base. + * + * For example, in base 10, 378 = 21 X 33 X 71 is a Smith number since 3 + 7 + 8 = 2 X 1 + 3 X 3 + 7 X 1, + * and 22 = 21 X 111 is a Smith number, because 2 + 2 = 2 X 1 + (1 + 1) X 1. + * + * Wiki: https://en.wikipedia.org/wiki/Smith_number + */ +public final class SmithNumber { + + private SmithNumber() { + } + + private static int primeFactorDigitSum(int n) { + int sum = 0; + int num = n; + + // Factorize the number using trial division + for (int i = 2; i * i <= num; i++) { + while (n % i == 0) { // while i divides n + sum += SumOfDigits.sumOfDigits(i); // add sum of digits of factor + n /= i; // divide n by the factor + } + } + + // If n is still > 1, it itself is a prime factor + if (n > 1) { + sum += SumOfDigits.sumOfDigits(n); + } + + return sum; + } + + /** + * Check if {@code number} is Smith number or not + * + * @param number the number + * @return {@code true} if {@code number} is a Smith number, otherwise false + */ + public static boolean isSmithNumber(int number) { + if (PrimeCheck.isPrime(number)) { + return false; // Smith numbers must be composite + } + + return SumOfDigits.sumOfDigits(number) == primeFactorDigitSum(number); + } +} diff --git a/src/main/java/com/thealgorithms/maths/SociableNumber.java b/src/main/java/com/thealgorithms/maths/SociableNumber.java new file mode 100644 index 000000000000..9ce644e61dfc --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SociableNumber.java @@ -0,0 +1,67 @@ +package com.thealgorithms.maths; + +/** + * Sociable numbers are natural numbers that form a cyclic sequence where the + * sum of proper divisors of each number equals the next number in the sequence, + * with the sequence eventually returning to the starting number. + * Amicable numbers are a special case of sociable numbers with a cycle length of 2. + * Example: (12496, 14288, 15472, 14536, 14264) is a sociable cycle of length 5. + * + * @author Vraj Prajapati (@Rosander0) + * @see Wikipedia: Sociable Number + * @see AmicableNumber + */ +public final class SociableNumber { + + private SociableNumber() { + // Utility class + } + + /** + * Calculates the sum of proper divisors of a number + * (all divisors excluding the number itself). + * + * @param number the number to calculate proper divisors sum for + * @return sum of proper divisors, or 0 if number is less than or equal to 1 + */ + static int sumOfProperDivisors(final int number) { + if (number <= 1) { + return 0; + } + int sum = 1; // 1 is a proper divisor of every number > 1 + final int root = (int) Math.sqrt(number); + for (int i = 2; i <= root; i++) { + if (number % i == 0) { + final int other = number / i; + sum += i; + if (other != i) { + sum += other; + } + } + } + return sum; + } + + /** + * Checks whether a number is part of a sociable cycle of a given length. + * Starting from the given number, it follows the chain of proper divisor + * sums and checks if it returns to the starting number in exactly cycleLength steps. + * + * @param number the starting number (must be positive) + * @param cycleLength the expected cycle length (must be greater than 1) + * @return true if the number is part of a sociable cycle of given length, false otherwise + */ + public static boolean isSociable(final int number, final int cycleLength) { + if (number <= 0 || cycleLength <= 1) { + return false; + } + int current = number; + for (int i = 0; i < cycleLength; i++) { + current = sumOfProperDivisors(current); + if (current == number) { + return i == cycleLength - 1; + } + } + return false; + } +} diff --git a/src/main/java/com/thealgorithms/maths/SumOfSquares.java b/src/main/java/com/thealgorithms/maths/SumOfSquares.java index c050d5a75f7b..77acbcc2a609 100644 --- a/src/main/java/com/thealgorithms/maths/SumOfSquares.java +++ b/src/main/java/com/thealgorithms/maths/SumOfSquares.java @@ -5,7 +5,6 @@ * Find minimum number of perfect squares that sum to given number * * @see Lagrange's Four Square Theorem - * @author BEASTSHRIRAM */ public final class SumOfSquares { @@ -16,10 +15,15 @@ private SumOfSquares() { /** * Find minimum number of perfect squares that sum to n * - * @param n the target number + * @param n the target number (must be non-negative) * @return minimum number of squares needed + * @throws IllegalArgumentException if n is negative */ public static int minSquares(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative"); + } + if (isPerfectSquare(n)) { return 1; } diff --git a/src/main/java/com/thealgorithms/maths/VectorCrossProduct.java b/src/main/java/com/thealgorithms/maths/VectorCrossProduct.java index e2769744bcda..41bbbb077c68 100644 --- a/src/main/java/com/thealgorithms/maths/VectorCrossProduct.java +++ b/src/main/java/com/thealgorithms/maths/VectorCrossProduct.java @@ -15,7 +15,7 @@ * matrix consisting of the first row with unit vectors of magnitude 1, the * second row with the direction ratios of the first vector and the third row * with the direction ratios of the second vector. The magnitude of a vector is - * it's value expressed as a number. Let the direction ratios of the first + * its value expressed as a number. Let the direction ratios of the first * vector, P be: a, b, c Let the direction ratios of the second vector, Q be: x, * y, z Therefore the calculation for the cross product can be arranged as: * diff --git a/src/main/java/com/thealgorithms/maths/Volume.java b/src/main/java/com/thealgorithms/maths/Volume.java index 0f282b2abae2..488b921cae83 100644 --- a/src/main/java/com/thealgorithms/maths/Volume.java +++ b/src/main/java/com/thealgorithms/maths/Volume.java @@ -102,4 +102,39 @@ public static double volumePyramid(double baseArea, double height) { public static double volumeFrustumOfCone(double r1, double r2, double height) { return (Math.PI * height / 3) * (r1 * r1 + r2 * r2 + r1 * r2); } + + /** + * Calculate the volume of a frustum of a pyramid. + * + * @param upperBaseArea area of the upper base + * @param lowerBaseArea area of the lower base + * @param height height of the frustum + * @return volume of the frustum + */ + public static double volumeFrustumOfPyramid(double upperBaseArea, double lowerBaseArea, double height) { + return (upperBaseArea + lowerBaseArea + Math.sqrt(upperBaseArea * lowerBaseArea)) * height / 3; + } + + /** + * Calculate the volume of a torus. + * + * @param majorRadius major radius of a torus + * @param minorRadius minor radius of a torus + * @return volume of the torus + */ + public static double volumeTorus(double majorRadius, double minorRadius) { + return 2 * Math.PI * Math.PI * majorRadius * minorRadius * minorRadius; + } + + /** + * Calculate the volume of an ellipsoid. + * + * @param a first semi-axis of an ellipsoid + * @param b second semi-axis of an ellipsoid + * @param c third semi-axis of an ellipsoid + * @return volume of the ellipsoid + */ + public static double volumeEllipsoid(double a, double b, double c) { + return (4 * Math.PI * a * b * c) / 3; + } } diff --git a/src/main/java/com/thealgorithms/matrix/QRDecomposition.java b/src/main/java/com/thealgorithms/matrix/QRDecomposition.java new file mode 100644 index 000000000000..45f56bc14729 --- /dev/null +++ b/src/main/java/com/thealgorithms/matrix/QRDecomposition.java @@ -0,0 +1,149 @@ +package com.thealgorithms.matrix; + +/** + * @brief Implementation of QR Decomposition using the Gram-Schmidt process + * @details Decomposes a matrix A into an orthogonal matrix Q and an upper + * triangular matrix R such that A = Q * R. The Gram-Schmidt process + * orthogonalizes the columns of A to produce Q, and R is computed as Q^T * A. + * This decomposition is useful for solving linear least squares problems, + * eigenvalue computations, and numerical stability in linear algebra. + * @see QR Decomposition + */ +public final class QRDecomposition { + + private QRDecomposition() { + } + + /** + * A helper class to store both Q and R matrices + */ + public static class QR { + private final double[][] q; + private final double[][] r; + + QR(double[][] q, double[][] r) { + this.q = q; + this.r = r; + } + + public double[][] getQ() { + return q; + } + + public double[][] getR() { + return r; + } + } + + /** + * @brief Performs QR decomposition on a matrix using the Gram-Schmidt process + * @param matrix the input matrix (m x n) + * @return QR object containing orthogonal matrix Q (m x n) and upper triangular matrix R (n x n) + * @throws IllegalArgumentException if the matrix is null, empty, or has invalid rows + */ + public static QR decompose(double[][] matrix) { + validateInputMatrix(matrix); + + int m = matrix.length; + int n = matrix[0].length; + + double[][] q = new double[m][n]; + double[][] r = new double[n][n]; + + for (int j = 0; j < n; j++) { + double[] v = getColumn(matrix, j); + + for (int i = 0; i < j; i++) { + double[] qi = getColumn(q, i); + r[i][j] = dotProduct(qi, v); + v = subtractVectors(v, scalarMultiply(qi, r[i][j])); + } + + r[j][j] = norm(v); + if (r[j][j] == 0) { + throw new ArithmeticException("Matrix is rank deficient. Cannot perform QR decomposition."); + } + double[] qj = scalarMultiply(v, 1.0 / r[j][j]); + setColumn(q, j, qj); + } + + return new QR(q, r); + } + + private static double[] getColumn(double[][] matrix, int col) { + int m = matrix.length; + double[] column = new double[m]; + for (int i = 0; i < m; i++) { + column[i] = matrix[i][col]; + } + return column; + } + + private static void setColumn(double[][] matrix, int col, double[] column) { + for (int i = 0; i < matrix.length; i++) { + matrix[i][col] = column[i]; + } + } + + private static double dotProduct(double[] a, double[] b) { + double sum = 0; + for (int i = 0; i < a.length; i++) { + sum += a[i] * b[i]; + } + return sum; + } + + private static double[] subtractVectors(double[] a, double[] b) { + double[] result = new double[a.length]; + for (int i = 0; i < a.length; i++) { + result[i] = a[i] - b[i]; + } + return result; + } + + private static double[] scalarMultiply(double[] v, double scalar) { + double[] result = new double[v.length]; + for (int i = 0; i < v.length; i++) { + result[i] = v[i] * scalar; + } + return result; + } + + private static double norm(double[] v) { + return Math.sqrt(dotProduct(v, v)); + } + + private static void validateInputMatrix(double[][] matrix) { + if (matrix == null) { + throw new IllegalArgumentException("The input matrix cannot be null"); + } + if (matrix.length == 0) { + throw new IllegalArgumentException("The input matrix cannot be empty"); + } + if (!hasValidRows(matrix)) { + throw new IllegalArgumentException("The input matrix cannot have null or empty rows"); + } + if (isJaggedMatrix(matrix)) { + throw new IllegalArgumentException("The input matrix cannot be jagged"); + } + } + + private static boolean hasValidRows(double[][] matrix) { + for (double[] row : matrix) { + if (row == null || row.length == 0) { + return false; + } + } + return true; + } + + private static boolean isJaggedMatrix(double[][] matrix) { + int numColumns = matrix[0].length; + for (double[] row : matrix) { + if (row.length != numColumns) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/thealgorithms/matrix/StochasticMatrix.java b/src/main/java/com/thealgorithms/matrix/StochasticMatrix.java new file mode 100644 index 000000000000..8b071113f9cc --- /dev/null +++ b/src/main/java/com/thealgorithms/matrix/StochasticMatrix.java @@ -0,0 +1,74 @@ +package com.thealgorithms.matrix; + +/** + * Utility class to check whether a matrix is stochastic. + * A matrix is stochastic if all its elements are non-negative + * and the sum of each row or column is equal to 1. + *Reference: https://en.wikipedia.org/wiki/Stochastic_matrix + */ +public final class StochasticMatrix { + + private static final double TOLERANCE = 1e-9; + + private StochasticMatrix() { + // Utility class + } + /** + * Checks if a matrix is row-stochastic. + * + * @param matrix the matrix to check + * @return true if the matrix is row-stochastic + * @throws IllegalArgumentException if matrix is null or empty + */ + public static boolean isRowStochastic(double[][] matrix) { + validateMatrix(matrix); + + for (double[] row : matrix) { + double sum = 0.0; + for (double value : row) { + if (value < 0) { + return false; + } + sum += value; + } + if (Math.abs(sum - 1.0) > TOLERANCE) { + return false; + } + } + return true; + } + + /** + * Checks if a matrix is column-stochastic. + * + * @param matrix the matrix to check + * @return true if the matrix is column-stochastic + * @throws IllegalArgumentException if matrix is null or empty + */ + public static boolean isColumnStochastic(double[][] matrix) { + validateMatrix(matrix); + + int rows = matrix.length; + int cols = matrix[0].length; + + for (int j = 0; j < cols; j++) { + double sum = 0.0; + for (int i = 0; i < rows; i++) { + if (matrix[i][j] < 0) { + return false; + } + sum += matrix[i][j]; + } + if (Math.abs(sum - 1.0) > TOLERANCE) { + return false; + } + } + return true; + } + + private static void validateMatrix(double[][] matrix) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { + throw new IllegalArgumentException("Matrix must not be null or empty"); + } + } +} diff --git a/src/main/java/com/thealgorithms/others/ArrayRightRotation.java b/src/main/java/com/thealgorithms/others/ArrayRightRotation.java index 125edadb6e73..ba80f29af710 100644 --- a/src/main/java/com/thealgorithms/others/ArrayRightRotation.java +++ b/src/main/java/com/thealgorithms/others/ArrayRightRotation.java @@ -34,7 +34,7 @@ public static int[] rotateRight(int[] arr, int k) { } /** - * Performs reversing of a array + * Performs reversing of an array * @param arr the array to be reversed * @param start starting position * @param end ending position diff --git a/src/main/java/com/thealgorithms/others/Damm.java b/src/main/java/com/thealgorithms/others/Damm.java index 55a4c5b81a89..c6365ada42f7 100644 --- a/src/main/java/com/thealgorithms/others/Damm.java +++ b/src/main/java/com/thealgorithms/others/Damm.java @@ -60,7 +60,7 @@ public static boolean dammCheck(String digits) { } /** - * Calculate check digit for initial digits and add it tho the last + * Calculate check digit for initial digits and add it to the last * position. * * @param initialDigits initial value diff --git a/src/main/java/com/thealgorithms/others/Dijkstra.java b/src/main/java/com/thealgorithms/others/Dijkstra.java deleted file mode 100644 index a379100a2f3b..000000000000 --- a/src/main/java/com/thealgorithms/others/Dijkstra.java +++ /dev/null @@ -1,248 +0,0 @@ -package com.thealgorithms.others; - -import java.util.HashMap; -import java.util.Map; -import java.util.NavigableSet; -import java.util.TreeSet; -/** - * Dijkstra's algorithm,is a graph search algorithm that solves the - * single-source shortest path problem for a graph with nonnegative edge path - * costs, producing a shortest path tree. - * - *

- * NOTE: The inputs to Dijkstra's algorithm are a directed and weighted graph - * consisting of 2 or more nodes, generally represented by an adjacency matrix - * or list, and a start node. - * - *

- * Original source of code: - * https://rosettacode.org/wiki/Dijkstra%27s_algorithm#Java Also most of the - * comments are from RosettaCode. - */ -public final class Dijkstra { - private Dijkstra() { - } - - private static final Graph.Edge[] GRAPH = { - // Distance from node "a" to node "b" is 7. - // In the current Graph there is no way to move the other way (e,g, from "b" to "a"), - // a new edge would be needed for that - new Graph.Edge("a", "b", 7), - new Graph.Edge("a", "c", 9), - new Graph.Edge("a", "f", 14), - new Graph.Edge("b", "c", 10), - new Graph.Edge("b", "d", 15), - new Graph.Edge("c", "d", 11), - new Graph.Edge("c", "f", 2), - new Graph.Edge("d", "e", 6), - new Graph.Edge("e", "f", 9), - }; - private static final String START = "a"; - private static final String END = "e"; - - /** - * main function Will run the code with "GRAPH" that was defined above. - */ - public static void main(String[] args) { - Graph g = new Graph(GRAPH); - g.dijkstra(START); - g.printPath(END); - // g.printAllPaths(); - } -} - -class Graph { - - // mapping of vertex names to Vertex objects, built from a set of Edges - - private final Map graph; - - /** - * One edge of the graph (only used by Graph constructor) - */ - public static class Edge { - - public final String v1; - public final String v2; - public final int dist; - - Edge(String v1, String v2, int dist) { - this.v1 = v1; - this.v2 = v2; - this.dist = dist; - } - } - - /** - * One vertex of the graph, complete with mappings to neighbouring vertices - */ - public static class Vertex implements Comparable { - - public final String name; - // MAX_VALUE assumed to be infinity - public int dist = Integer.MAX_VALUE; - public Vertex previous = null; - public final Map neighbours = new HashMap<>(); - - Vertex(String name) { - this.name = name; - } - - private void printPath() { - if (this == this.previous) { - System.out.printf("%s", this.name); - } else if (this.previous == null) { - System.out.printf("%s(unreached)", this.name); - } else { - this.previous.printPath(); - System.out.printf(" -> %s(%d)", this.name, this.dist); - } - } - - public int compareTo(Vertex other) { - if (dist == other.dist) { - return name.compareTo(other.name); - } - - return Integer.compare(dist, other.dist); - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null || getClass() != object.getClass()) { - return false; - } - if (!super.equals(object)) { - return false; - } - - Vertex vertex = (Vertex) object; - - if (dist != vertex.dist) { - return false; - } - if (name != null ? !name.equals(vertex.name) : vertex.name != null) { - return false; - } - if (previous != null ? !previous.equals(vertex.previous) : vertex.previous != null) { - return false; - } - return neighbours != null ? neighbours.equals(vertex.neighbours) : vertex.neighbours == null; - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (name != null ? name.hashCode() : 0); - result = 31 * result + dist; - result = 31 * result + (previous != null ? previous.hashCode() : 0); - result = 31 * result + (neighbours != null ? neighbours.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "(" + name + ", " + dist + ")"; - } - } - - /** - * Builds a graph from a set of edges - */ - Graph(Edge[] edges) { - graph = new HashMap<>(edges.length); - - // one pass to find all vertices - for (Edge e : edges) { - if (!graph.containsKey(e.v1)) { - graph.put(e.v1, new Vertex(e.v1)); - } - if (!graph.containsKey(e.v2)) { - graph.put(e.v2, new Vertex(e.v2)); - } - } - - // another pass to set neighbouring vertices - for (Edge e : edges) { - graph.get(e.v1).neighbours.put(graph.get(e.v2), e.dist); - // graph.get(e.v2).neighbours.put(graph.get(e.v1), e.dist); // also do this for an - // undirected graph - } - } - - /** - * Runs dijkstra using a specified source vertex - */ - public void dijkstra(String startName) { - if (!graph.containsKey(startName)) { - System.err.printf("Graph doesn't contain start vertex \"%s\"%n", startName); - return; - } - final Vertex source = graph.get(startName); - NavigableSet q = new TreeSet<>(); - - // set-up vertices - for (Vertex v : graph.values()) { - v.previous = v == source ? source : null; - v.dist = v == source ? 0 : Integer.MAX_VALUE; - q.add(v); - } - - dijkstra(q); - } - - /** - * Implementation of dijkstra's algorithm using a binary heap. - */ - private void dijkstra(final NavigableSet q) { - Vertex u; - Vertex v; - while (!q.isEmpty()) { - // vertex with shortest distance (first iteration will return source) - u = q.pollFirst(); - if (u.dist == Integer.MAX_VALUE) { - break; // we can ignore u (and any other remaining vertices) since they are - // unreachable - } - // look at distances to each neighbour - for (Map.Entry a : u.neighbours.entrySet()) { - v = a.getKey(); // the neighbour in this iteration - - final int alternateDist = u.dist + a.getValue(); - if (alternateDist < v.dist) { // shorter path to neighbour found - q.remove(v); - v.dist = alternateDist; - v.previous = u; - q.add(v); - } - } - } - } - - /** - * Prints a path from the source to the specified vertex - */ - public void printPath(String endName) { - if (!graph.containsKey(endName)) { - System.err.printf("Graph doesn't contain end vertex \"%s\"%n", endName); - return; - } - - graph.get(endName).printPath(); - System.out.println(); - } - - /** - * Prints the path from the source to every vertex (output order is not - * guaranteed) - */ - public void printAllPaths() { - for (Vertex v : graph.values()) { - v.printPath(); - System.out.println(); - } - } -} diff --git a/src/main/java/com/thealgorithms/others/PasswordGen.java b/src/main/java/com/thealgorithms/others/PasswordGen.java index da9f21bc918f..4f139963eae5 100644 --- a/src/main/java/com/thealgorithms/others/PasswordGen.java +++ b/src/main/java/com/thealgorithms/others/PasswordGen.java @@ -41,7 +41,7 @@ public static String generatePassword(int minLength, int maxLength) { letters.add(c); } - // Inbuilt method to randomly shuffle a elements of a list + // Inbuilt method to randomly shuffle the elements of a list Collections.shuffle(letters); StringBuilder password = new StringBuilder(); diff --git a/src/main/java/com/thealgorithms/others/Verhoeff.java b/src/main/java/com/thealgorithms/others/Verhoeff.java index 9088612aaa43..bd25113f494f 100644 --- a/src/main/java/com/thealgorithms/others/Verhoeff.java +++ b/src/main/java/com/thealgorithms/others/Verhoeff.java @@ -108,7 +108,7 @@ public static boolean verhoeffCheck(String digits) { } /** - * Calculate check digit for initial digits and add it tho the last + * Calculate check digit for initial digits and add it to the last * position. * * @param initialDigits initial value diff --git a/src/main/java/com/thealgorithms/others/cn/HammingDistance.java b/src/main/java/com/thealgorithms/others/cn/HammingDistance.java deleted file mode 100644 index c8239d53d606..000000000000 --- a/src/main/java/com/thealgorithms/others/cn/HammingDistance.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.thealgorithms.others.cn; - -public final class HammingDistance { - private HammingDistance() { - } - - private static void checkChar(char inChar) { - if (inChar != '0' && inChar != '1') { - throw new IllegalArgumentException("Input must be a binary string."); - } - } - - public static int compute(char charA, char charB) { - checkChar(charA); - checkChar(charB); - return charA == charB ? 0 : 1; - } - - public static int compute(String bitsStrA, String bitsStrB) { - if (bitsStrA.length() != bitsStrB.length()) { - throw new IllegalArgumentException("Input strings must have the same length."); - } - - int totalErrorBitCount = 0; - - for (int i = 0; i < bitsStrA.length(); i++) { - totalErrorBitCount += compute(bitsStrA.charAt(i), bitsStrB.charAt(i)); - } - - return totalErrorBitCount; - } -} diff --git a/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java index 399c3f1e041f..d096e0a8d7cd 100644 --- a/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java +++ b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java @@ -41,7 +41,7 @@ public static void resolveCollision(Body a, Body b) { double dy = b.y - a.y; double dist = Math.hypot(dx, dy); - if (dist == 0) { + if (dist < a.radius + b.radius) { return; // overlapping } diff --git a/src/main/java/com/thealgorithms/physics/Relativity.java b/src/main/java/com/thealgorithms/physics/Relativity.java new file mode 100644 index 000000000000..ed823c2cc879 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/Relativity.java @@ -0,0 +1,81 @@ +package com.thealgorithms.physics; + +/** + * Implements relativity theory formulae. + * Provides simple static methods to calculate length contraction and time dilation + * in the laboratory frame with respect to the object's own frame, and velocity + * with respect to the moving frame. + * + * @see Wikipedia + */ +public final class Relativity { + + /* Speed of light in m s^-1 */ + public static final double SPEED_OF_LIGHT = 299792458.0; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Relativity() { + } + + /** + * Calculates the gamma parameter that is of paramount importance in relativity + * theory. It is a dimensionless parameter that is equal to 1 for zero velocity + * but tends to infinity when velocity approaches the speed of light. + * + * @param v The velocity (m/s). + * @return The value of gamma parameter. + */ + public static double gamma(double v) { + if (Math.abs(v) >= SPEED_OF_LIGHT) { + throw new IllegalArgumentException("Speed must be lower than the speed of light"); + } + return 1.0 / Math.sqrt(1 - v * v / (SPEED_OF_LIGHT * SPEED_OF_LIGHT)); + } + + /** + * Calculates the length of an object in the moving frame. + * + * @param length The length of an object in its own frame (m). + * @param v The velocity of the object (m/s). + * @return The length of an object in the laboratory frame (m). + */ + public static double lengthContraction(double length, double v) { + if (length < 0) { + throw new IllegalArgumentException("Length must be non-negative"); + } + return length / gamma(v); + } + + /** + * Calculates the time that has passed in the moving frame. + * + * @param length The time that has passed in the object's own frame (s). + * @param v The velocity of the object (m/s). + * @return The time that has passed in the laboratory frame (s). + */ + public static double timeDilation(double time, double v) { + if (time < 0) { + throw new IllegalArgumentException("Time must be non-negative"); + } + return time * gamma(v); + } + + /** + * Calculates the velocity with respect to the moving frame. + * + * @param v1 The velocity of the object with respect to laboratory frame (m/s). + * @param v The velocity of the moving frame (m/s). + * @return The velocity with respect to the moving frame (m/s). + */ + public static double velocityAddition(double v1, double v) { + if (Math.abs(v1) > SPEED_OF_LIGHT) { + throw new IllegalArgumentException("Speed must not exceed the speed of light"); + } + if (Math.abs(v) >= SPEED_OF_LIGHT) { + throw new IllegalArgumentException("Frame speed must be lower than the speed of light"); + } + return (v1 - v) / (1 - v1 * v / (SPEED_OF_LIGHT * SPEED_OF_LIGHT)); + } +} diff --git a/src/main/java/com/thealgorithms/physics/SnellLaw.java b/src/main/java/com/thealgorithms/physics/SnellLaw.java new file mode 100644 index 000000000000..2736984814fd --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/SnellLaw.java @@ -0,0 +1,33 @@ +package com.thealgorithms.physics; + +/** + * Calculates refraction angle using Snell's Law: + * n1 * sin(theta1) = n2 * sin(theta2) + * @see Snell's Law + */ +public final class SnellLaw { + + private SnellLaw() { + throw new AssertionError("No instances."); + } + + /** + * Computes the refracted angle (theta2) in radians. + * + * @param n1 index of refraction of medium 1 + * @param n2 index of refraction of medium 2 + * @param theta1 incident angle in radians + * @return refracted angle (theta2) in radians + * @throws IllegalArgumentException if total internal reflection occurs + */ + public static double refractedAngle(double n1, double n2, double theta1) { + double ratio = n1 / n2; + double sinTheta2 = ratio * Math.sin(theta1); + + if (Math.abs(sinTheta2) > 1.0) { + throw new IllegalArgumentException("Total internal reflection: no refraction possible."); + } + + return Math.asin(sinTheta2); + } +} diff --git a/src/main/java/com/thealgorithms/physics/ThinLens.java b/src/main/java/com/thealgorithms/physics/ThinLens.java new file mode 100644 index 000000000000..5fb29d8c41e4 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/ThinLens.java @@ -0,0 +1,74 @@ +package com.thealgorithms.physics; + +/** + * Implements the Thin Lens Formula used in ray optics: + * + *

+ *     1/f = 1/v + 1/u
+ * 
+ * + * where: + *
    + *
  • f = focal length
  • + *
  • u = object distance
  • + *
  • v = image distance
  • + *
+ * + * Uses the Cartesian sign convention. + * + * @see Thin Lens + */ +public final class ThinLens { + + private ThinLens() { + throw new AssertionError("No instances."); + } + + /** + * Computes the image distance using the thin lens formula. + * + * @param focalLength focal length of the lens (f) + * @param objectDistance object distance (u) + * @return image distance (v) + * @throws IllegalArgumentException if focal length or object distance is zero + */ + public static double imageDistance(double focalLength, double objectDistance) { + + if (focalLength == 0 || objectDistance == 0) { + throw new IllegalArgumentException("Focal length and object distance must be non-zero."); + } + + return 1.0 / ((1.0 / focalLength) - (1.0 / objectDistance)); + } + + /** + * Computes magnification of the image. + * + *
+     *     m = v / u
+     * 
+ * + * @param imageDistance image distance (v) + * @param objectDistance object distance (u) + * @return magnification + * @throws IllegalArgumentException if object distance is zero + */ + public static double magnification(double imageDistance, double objectDistance) { + + if (objectDistance == 0) { + throw new IllegalArgumentException("Object distance must be non-zero."); + } + + return imageDistance / objectDistance; + } + + /** + * Determines whether the image formed is real or virtual. + * + * @param imageDistance image distance (v) + * @return {@code true} if image is real, {@code false} if virtual + */ + public static boolean isRealImage(double imageDistance) { + return imageDistance > 0; + } +} diff --git a/src/main/java/com/thealgorithms/prefixsum/DifferenceArray.java b/src/main/java/com/thealgorithms/prefixsum/DifferenceArray.java new file mode 100644 index 000000000000..1be55039cff0 --- /dev/null +++ b/src/main/java/com/thealgorithms/prefixsum/DifferenceArray.java @@ -0,0 +1,87 @@ +package com.thealgorithms.prefixsum; + +/** + * Implements the Difference Array algorithm. + * + *

+ * The Difference Array is an auxiliary data structure that enables efficient range update operations. + * It is based on the mathematical concept of Finite Differences. + *

+ * + *

+ * Key Operations: + *

    + *
  • Range Update (Add value to [L, R]): O(1)
  • + *
  • Reconstruction (Prefix Sum): O(N)
  • + *
+ *

+ * + * @see Finite Difference (Wikipedia) + * @see Prefix Sum (Wikipedia) + * @author Chahat Sandhu, singhc7 + */ +public class DifferenceArray { + + private final long[] differenceArray; + private final int n; + + /** + * Initializes the Difference Array from a given integer array. + * + * @param inputArray The initial array. Cannot be null or empty. + * @throws IllegalArgumentException if the input array is null or empty. + */ + public DifferenceArray(int[] inputArray) { + if (inputArray == null || inputArray.length == 0) { + throw new IllegalArgumentException("Input array cannot be null or empty."); + } + this.n = inputArray.length; + // Size n + 1 allows for branchless updates at the right boundary (r + 1). + this.differenceArray = new long[n + 1]; + initializeDifferenceArray(inputArray); + } + + private void initializeDifferenceArray(int[] inputArray) { + differenceArray[0] = inputArray[0]; + for (int i = 1; i < n; i++) { + differenceArray[i] = inputArray[i] - inputArray[i - 1]; + } + } + + /** + * Adds a value to all elements in the range [l, r]. + * + *

+ * This method uses a branchless approach by allocating an extra element at the end + * of the array, avoiding the conditional check for the right boundary. + *

+ * + * @param l The starting index (inclusive). + * @param r The ending index (inclusive). + * @param val The value to add. + * @throws IllegalArgumentException if the range is invalid. + */ + public void update(int l, int r, int val) { + if (l < 0 || r >= n || l > r) { + throw new IllegalArgumentException(String.format("Invalid range: [%d, %d] for array of size %d", l, r, n)); + } + + differenceArray[l] += val; + differenceArray[r + 1] -= val; + } + + /** + * Reconstructs the final array using prefix sums. + * + * @return The resulting array after all updates. Returns long[] to handle potential overflows. + */ + public long[] getResultArray() { + long[] result = new long[n]; + result[0] = differenceArray[0]; + + for (int i = 1; i < n; i++) { + result[i] = differenceArray[i] + result[i - 1]; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/prefixsum/PrefixSum.java b/src/main/java/com/thealgorithms/prefixsum/PrefixSum.java new file mode 100644 index 000000000000..47f6366e2924 --- /dev/null +++ b/src/main/java/com/thealgorithms/prefixsum/PrefixSum.java @@ -0,0 +1,54 @@ +package com.thealgorithms.prefixsum; + +/** + * A class that implements the Prefix Sum algorithm. + * + *

Prefix Sum is a technique used to preprocess an array such that + * range sum queries can be answered in O(1) time. + * The preprocessing step takes O(N) time. + * + *

This implementation uses a long array for the prefix sums to prevent + * integer overflow when the sum of elements exceeds Integer.MAX_VALUE. + * + * @see Prefix Sum (Wikipedia) + * @author Chahat Sandhu, singhc7 + */ +public class PrefixSum { + + private final long[] prefixSums; + + /** + * Constructor to preprocess the input array. + * + * @param array The input integer array. + * @throws IllegalArgumentException if the array is null. + */ + public PrefixSum(int[] array) { + if (array == null) { + throw new IllegalArgumentException("Input array cannot be null"); + } + this.prefixSums = new long[array.length + 1]; + this.prefixSums[0] = 0; + + for (int i = 0; i < array.length; i++) { + // Automatically promotes int to long during addition + this.prefixSums[i + 1] = this.prefixSums[i] + array[i]; + } + } + + /** + * Calculates the sum of elements in the range [left, right]. + * Indices are 0-based. + * + * @param left The starting index (inclusive). + * @param right The ending index (inclusive). + * @return The sum of elements from index left to right as a long. + * @throws IndexOutOfBoundsException if indices are out of valid range. + */ + public long sumRange(int left, int right) { + if (left < 0 || right >= prefixSums.length - 1 || left > right) { + throw new IndexOutOfBoundsException("Invalid range indices"); + } + return prefixSums[right + 1] - prefixSums[left]; + } +} diff --git a/src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java b/src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java new file mode 100644 index 000000000000..9c168bc6bcc4 --- /dev/null +++ b/src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java @@ -0,0 +1,64 @@ +package com.thealgorithms.prefixsum; + +/** + * A class that implements the 2D Prefix Sum algorithm. + * + *

2D Prefix Sum is a technique used to preprocess a 2D matrix such that + * sub-matrix sum queries can be answered in O(1) time. + * The preprocessing step takes O(N*M) time. + * + *

This implementation uses a long array for the prefix sums to prevent + * integer overflow. + * + * @see Summed-area table (Wikipedia) + * @author Chahat Sandhu, singhc7 + */ +public class PrefixSum2D { + + private final long[][] prefixSums; + + /** + * Constructor to preprocess the input matrix. + * + * @param matrix The input integer matrix. + * @throws IllegalArgumentException if the matrix is null or empty. + */ + public PrefixSum2D(int[][] matrix) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { + throw new IllegalArgumentException("Input matrix cannot be null or empty"); + } + + int rows = matrix.length; + int cols = matrix[0].length; + this.prefixSums = new long[rows + 1][cols + 1]; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // P[i+1][j+1] = current + above + left - diagonal_overlap + this.prefixSums[i + 1][j + 1] = matrix[i][j] + this.prefixSums[i][j + 1] + this.prefixSums[i + 1][j] - this.prefixSums[i][j]; + } + } + } + + /** + * Calculates the sum of the sub-matrix defined by (row1, col1) to (row2, col2). + * Indices are 0-based. + * + * @param row1 Top row index. + * @param col1 Left column index. + * @param row2 Bottom row index. + * @param col2 Right column index. + * @return The sum of the sub-matrix. + * @throws IndexOutOfBoundsException if indices are invalid. + */ + public long sumRegion(int row1, int col1, int row2, int col2) { + if (row1 < 0 || row2 >= prefixSums.length - 1 || row2 < row1) { + throw new IndexOutOfBoundsException("Invalid row indices"); + } + if (col1 < 0 || col2 >= prefixSums[0].length - 1 || col2 < col1) { + throw new IndexOutOfBoundsException("Invalid column indices"); + } + + return prefixSums[row2 + 1][col2 + 1] - prefixSums[row1][col2 + 1] - prefixSums[row2 + 1][col1] + prefixSums[row1][col1]; + } +} diff --git a/src/main/java/com/thealgorithms/prefixsum/RangeSumQuery.java b/src/main/java/com/thealgorithms/prefixsum/RangeSumQuery.java new file mode 100644 index 000000000000..14a02a2de4d0 --- /dev/null +++ b/src/main/java/com/thealgorithms/prefixsum/RangeSumQuery.java @@ -0,0 +1,73 @@ +package com.thealgorithms.prefixsum; + +/** + * Implements an algorithm to efficiently compute the sum of elements + * between any two indices in an integer array using the Prefix Sum technique. + * + *

+ * Given an array nums, this algorithm precomputes the prefix sum array + * to allow O(1) sum queries for any range [left, right]. + *

+ * + *

+ * Let prefixSum[i] be the sum of elements from index 0 to i-1. + * The sum of elements from left to right is: + * + *

+ * prefixSum[right + 1] - prefixSum[left]
+ * 
+ *

+ * + *

+ * Time Complexity: O(N) for preprocessing, O(1) per query
+ * Space Complexity: O(N) + *

+ * + * @author Ruturaj Jadhav, ruturajjadhav07 + */ +public final class RangeSumQuery { + + private RangeSumQuery() { + // Utility class; prevent instantiation + } + + /** + * Computes the prefix sum array for efficient range queries. + * + * @param nums The input integer array. + * @return Prefix sum array where prefixSum[i+1] = sum of nums[0..i]. + * @throws IllegalArgumentException if nums is null. + */ + public static int[] buildPrefixSum(int[] nums) { + if (nums == null) { + throw new IllegalArgumentException("Input array cannot be null"); + } + + int n = nums.length; + int[] prefixSum = new int[n + 1]; + for (int i = 0; i < n; i++) { + prefixSum[i + 1] = prefixSum[i] + nums[i]; + } + return prefixSum; + } + + /** + * Returns the sum of elements from index left to right (inclusive) + * using the provided prefix sum array. + * + * @param prefixSum The prefix sum array computed using buildPrefixSum. + * @param left The start index (inclusive). + * @param right The end index (inclusive). + * @return The sum of elements in the range [left, right]. + * @throws IllegalArgumentException if indices are invalid. + */ + public static int sumRange(int[] prefixSum, int left, int right) { + if (prefixSum == null) { + throw new IllegalArgumentException("Prefix sum array cannot be null"); + } + if (left < 0 || right >= prefixSum.length - 1 || left > right) { + throw new IllegalArgumentException("Invalid range indices"); + } + return prefixSum[right + 1] - prefixSum[left]; + } +} diff --git a/src/main/java/com/thealgorithms/prefixsum/SubarraySumEqualsK.java b/src/main/java/com/thealgorithms/prefixsum/SubarraySumEqualsK.java new file mode 100644 index 000000000000..d6a6bbc01663 --- /dev/null +++ b/src/main/java/com/thealgorithms/prefixsum/SubarraySumEqualsK.java @@ -0,0 +1,72 @@ +package com.thealgorithms.prefixsum; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implements an algorithm to count the number of continuous subarrays + * whose sum equals a given value k. + * + *

+ * This algorithm uses the Prefix Sum technique combined with a HashMap + * to achieve O(N) time complexity. + *

+ * + *

+ * Let prefixSum[i] be the sum of elements from index 0 to i. + * A subarray (j + 1) to i has sum k if: + * + *

+ * prefixSum[i] - prefixSum[j] = k
+ * 
+ *

+ * + *

+ * The HashMap stores the frequency of each prefix sum encountered so far. + *

+ * + *

+ * Time Complexity: O(N)
+ * Space Complexity: O(N) + *

+ * + * @see Prefix Sum (Wikipedia) + * @author Ruturaj Jadhav, ruturajjadhav07 + */ +public final class SubarraySumEqualsK { + + private SubarraySumEqualsK() { + // Utility class; prevent instantiation + } + + /** + * Counts the number of subarrays whose sum equals k. + * + * @param nums The input integer array. + * @param k The target sum. + * @return The number of continuous subarrays summing to k. + * @throws IllegalArgumentException if nums is null. + */ + public static int countSubarrays(int[] nums, int k) { + if (nums == null) { + throw new IllegalArgumentException("Input array cannot be null"); + } + + Map prefixSumFrequency = new HashMap<>(); + prefixSumFrequency.put(0L, 1); + + long prefixSum = 0; + int count = 0; + + for (int num : nums) { + prefixSum += num; + + long requiredSum = prefixSum - k; + count += prefixSumFrequency.getOrDefault(requiredSum, 0); + + prefixSumFrequency.put(prefixSum, prefixSumFrequency.getOrDefault(prefixSum, 0) + 1); + } + + return count; + } +} diff --git a/src/main/java/com/thealgorithms/puzzlesandgames/Sudoku.java b/src/main/java/com/thealgorithms/puzzlesandgames/Sudoku.java deleted file mode 100644 index fce665c4de00..000000000000 --- a/src/main/java/com/thealgorithms/puzzlesandgames/Sudoku.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.thealgorithms.puzzlesandgames; - -/** - * A class that provides methods to solve Sudoku puzzles of any n x n size - * using a backtracking approach, where n must be a perfect square. - * The algorithm checks for safe number placements in rows, columns, - * and subgrids (which are sqrt(n) x sqrt(n) in size) and recursively solves the puzzle. - * Though commonly used for 9x9 grids, it is adaptable to other valid Sudoku dimensions. - */ -final class Sudoku { - - private Sudoku() { - } - - /** - * Checks if placing a number in a specific position on the Sudoku board is safe. - * The number is considered safe if it does not violate any of the Sudoku rules: - * - It should not be present in the same row. - * - It should not be present in the same column. - * - It should not be present in the corresponding 3x3 subgrid. - * - It should not be present in the corresponding subgrid, which is sqrt(n) x sqrt(n) in size (e.g., for a 9x9 grid, the subgrid will be 3x3). - * - * @param board The current state of the Sudoku board. - * @param row The row index where the number is to be placed. - * @param col The column index where the number is to be placed. - * @param num The number to be placed on the board. - * @return True if the placement is safe, otherwise false. - */ - public static boolean isSafe(int[][] board, int row, int col, int num) { - // Check the row for duplicates - for (int d = 0; d < board.length; d++) { - if (board[row][d] == num) { - return false; - } - } - - // Check the column for duplicates - for (int r = 0; r < board.length; r++) { - if (board[r][col] == num) { - return false; - } - } - - // Check the corresponding 3x3 subgrid for duplicates - int sqrt = (int) Math.sqrt(board.length); - int boxRowStart = row - row % sqrt; - int boxColStart = col - col % sqrt; - - for (int r = boxRowStart; r < boxRowStart + sqrt; r++) { - for (int d = boxColStart; d < boxColStart + sqrt; d++) { - if (board[r][d] == num) { - return false; - } - } - } - - return true; - } - - /** - * Solves the Sudoku puzzle using backtracking. - * The algorithm finds an empty cell and tries placing numbers - * from 1 to n, where n is the size of the board - * (for example, from 1 to 9 in a standard 9x9 Sudoku). - * The algorithm finds an empty cell and tries placing numbers from 1 to 9. - * The standard version of Sudoku uses numbers from 1 to 9, so the algorithm can be - * easily modified for other variations of the game. - * If a number placement is valid (checked via `isSafe`), the number is - * placed and the function recursively attempts to solve the rest of the puzzle. - * If no solution is possible, the number is removed (backtracked), - * and the process is repeated. - * - * @param board The current state of the Sudoku board. - * @param n The size of the Sudoku board (typically 9 for a standard puzzle). - * @return True if the Sudoku puzzle is solvable, false otherwise. - */ - public static boolean solveSudoku(int[][] board, int n) { - int row = -1; - int col = -1; - boolean isEmpty = true; - - // Find the next empty cell - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - if (board[i][j] == 0) { - row = i; - col = j; - isEmpty = false; - break; - } - } - if (!isEmpty) { - break; - } - } - - // No empty space left - if (isEmpty) { - return true; - } - - // Try placing numbers 1 to n in the empty cell (n should be a perfect square) - // Eg: n=9 for a standard 9x9 Sudoku puzzle, n=16 for a 16x16 puzzle, etc. - for (int num = 1; num <= n; num++) { - if (isSafe(board, row, col, num)) { - board[row][col] = num; - if (solveSudoku(board, n)) { - return true; - } else { - // replace it - board[row][col] = 0; - } - } - } - return false; - } - - /** - * Prints the current state of the Sudoku board in a readable format. - * Each row is printed on a new line, with numbers separated by spaces. - * - * @param board The current state of the Sudoku board. - * @param n The size of the Sudoku board (typically 9 for a standard puzzle). - */ - public static void print(int[][] board, int n) { - // Print the board in a nxn grid format - // if n=9, print the board in a 9x9 grid format - // if n=16, print the board in a 16x16 grid format - for (int r = 0; r < n; r++) { - for (int d = 0; d < n; d++) { - System.out.print(board[r][d]); - System.out.print(" "); - } - System.out.print("\n"); - - if ((r + 1) % (int) Math.sqrt(n) == 0) { - System.out.print(""); - } - } - } - - /** - * The driver method to demonstrate solving a Sudoku puzzle. - * A sample 9x9 Sudoku puzzle is provided, and the program attempts to solve it - * using the `solveSudoku` method. If a solution is found, it is printed to the console. - * - * @param args Command-line arguments (not used in this program). - */ - public static void main(String[] args) { - int[][] board = new int[][] { - {3, 0, 6, 5, 0, 8, 4, 0, 0}, - {5, 2, 0, 0, 0, 0, 0, 0, 0}, - {0, 8, 7, 0, 0, 0, 0, 3, 1}, - {0, 0, 3, 0, 1, 0, 0, 8, 0}, - {9, 0, 0, 8, 6, 3, 0, 0, 5}, - {0, 5, 0, 0, 9, 0, 6, 0, 0}, - {1, 3, 0, 0, 0, 0, 2, 5, 0}, - {0, 0, 0, 0, 0, 0, 0, 7, 4}, - {0, 0, 5, 2, 0, 6, 3, 0, 0}, - }; - int n = board.length; - - if (solveSudoku(board, n)) { - print(board, n); - } else { - System.out.println("No solution"); - } - } -} diff --git a/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java b/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java index 72e9a14ac070..d94bef69cd3a 100644 --- a/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java +++ b/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java @@ -3,27 +3,32 @@ import java.util.List; /** - * The {@code TowerOfHanoi} class provides a recursive solution to the Tower of Hanoi puzzle. - * This puzzle involves moving a set of discs from one pole to another, following specific rules: + * Recursive solution to the Tower of Hanoi puzzle. + * + *

+ * The puzzle rules are: * 1. Only one disc can be moved at a time. * 2. A disc can only be placed on top of a larger disc. * 3. All discs must start on one pole and end on another. + *

* - * This implementation recursively calculates the steps required to solve the puzzle and stores them - * in a provided list. + *

+ * The recursion follows three steps: + * 1. Move {@code n-1} discs from start to intermediate. + * 2. Move the largest disc from start to end. + * 3. Move {@code n-1} discs from intermediate to end. + *

* *

- * For more information about the Tower of Hanoi, see - * Tower of Hanoi on Wikipedia. + * Time Complexity: O(2^n) - exponential due to recursive expansion. + * Space Complexity: O(n) - recursion stack depth. *

* - * The {@code shift} method takes the number of discs and the names of the poles, - * and appends the steps required to solve the puzzle to the provided list. - * Time Complexity: O(2^n) - Exponential time complexity due to the recursive nature of the problem. - * Space Complexity: O(n) - Linear space complexity due to the recursion stack. - * Wikipedia: https://en.wikipedia.org/wiki/Tower_of_Hanoi + *

+ * See Tower of Hanoi on Wikipedia. + *

*/ -final class TowerOfHanoi { +public final class TowerOfHanoi { private TowerOfHanoi() { } @@ -36,6 +41,7 @@ private TowerOfHanoi() { * @param intermediatePole The name of the intermediate pole used as a temporary holding area. * @param endPole The name of the end pole to which discs are moved. * @param result A list to store the steps required to solve the puzzle. + * @throws IllegalArgumentException if {@code n} is negative. * *

* This method is called recursively to move n-1 discs @@ -51,15 +57,20 @@ private TowerOfHanoi() { *

*/ public static void shift(int n, String startPole, String intermediatePole, String endPole, List result) { - if (n != 0) { - // Move n-1 discs from startPole to intermediatePole - shift(n - 1, startPole, endPole, intermediatePole, result); + if (n < 0) { + throw new IllegalArgumentException("Number of discs must be non-negative"); + } + if (n == 0) { + return; + } - // Add the move of the nth disc from startPole to endPole - result.add(String.format("Move %d from %s to %s", n, startPole, endPole)); + // Move n-1 discs from startPole to intermediatePole + shift(n - 1, startPole, endPole, intermediatePole, result); - // Move the n-1 discs from intermediatePole to endPole - shift(n - 1, intermediatePole, startPole, endPole, result); - } + // Add the move of the nth disc from startPole to endPole + result.add(String.format("Move %d from %s to %s", n, startPole, endPole)); + + // Move the n-1 discs from intermediatePole to endPole + shift(n - 1, intermediatePole, startPole, endPole, result); } } diff --git a/src/main/java/com/thealgorithms/randomized/MonteCarloIntegration.java b/src/main/java/com/thealgorithms/randomized/MonteCarloIntegration.java index 05d7abbbcd6c..06101295e880 100644 --- a/src/main/java/com/thealgorithms/randomized/MonteCarloIntegration.java +++ b/src/main/java/com/thealgorithms/randomized/MonteCarloIntegration.java @@ -64,13 +64,21 @@ private static double doApproximate(Function fx, double a, doubl if (!validate(fx, a, b, n)) { throw new IllegalArgumentException("Invalid input parameters"); } - double totalArea = 0.0; + double total = 0.0; double interval = b - a; - for (int i = 0; i < n; i++) { + int pairs = n / 2; + for (int i = 0; i < pairs; i++) { + double u = generator.nextDouble(); + double x1 = a + u * interval; + double x2 = a + (1.0 - u) * interval; + total += fx.apply(x1); + total += fx.apply(x2); + } + if ((n & 1) == 1) { double x = a + generator.nextDouble() * interval; - totalArea += fx.apply(x); + total += fx.apply(x); } - return interval * totalArea / n; + return interval * total / n; } private static boolean validate(Function fx, double a, double b, int n) { diff --git a/src/main/java/com/thealgorithms/maths/FactorialRecursion.java b/src/main/java/com/thealgorithms/recursion/FactorialRecursion.java similarity index 92% rename from src/main/java/com/thealgorithms/maths/FactorialRecursion.java rename to src/main/java/com/thealgorithms/recursion/FactorialRecursion.java index d9bafd1e39e9..673f216bdc9a 100644 --- a/src/main/java/com/thealgorithms/maths/FactorialRecursion.java +++ b/src/main/java/com/thealgorithms/recursion/FactorialRecursion.java @@ -1,4 +1,4 @@ -package com.thealgorithms.maths; +package com.thealgorithms.recursion; public final class FactorialRecursion { private FactorialRecursion() { diff --git a/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java b/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java index e5f474085367..404adfdf0cf1 100644 --- a/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java +++ b/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java @@ -1,21 +1,43 @@ package com.thealgorithms.recursion; -/* - The Fibonacci series is a sequence of numbers where each number is the sum of the two preceding ones, - starting with 0 and 1. - NUMBER 0 1 2 3 4 5 6 7 8 9 10 ... - FIBONACCI 0 1 1 2 3 5 8 13 21 34 55 ... -*/ - +/** + * The Fibonacci series is a sequence of numbers where each number is the sum of the two preceding ones, + * starting with 0 and 1. + *

+ * Example: + * 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ... + *

+ *

+ * This specific implementation demonstrates a Naive Recursive approach with {@code O(2^n)} time complexity. + * For more performant variations or different programming paradigms, see: + *

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming variants (Memoization / Bottom-Up)
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
+ */ public final class FibonacciSeries { private FibonacciSeries() { throw new UnsupportedOperationException("Utility class"); } + + /** + * Calculates the nth term in the Fibonacci sequence using recursion. + * + * @param n the position in the Fibonacci sequence (must be non-negative) + * @return the nth Fibonacci number + * @throws IllegalArgumentException if n is negative + */ public static int fibonacci(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be a non-negative integer"); + } if (n <= 1) { return n; - } else { - return fibonacci(n - 1) + fibonacci(n - 2); } + return fibonacci(n - 1) + fibonacci(n - 2); } } diff --git a/src/main/java/com/thealgorithms/searches/BinarySearch.java b/src/main/java/com/thealgorithms/searches/BinarySearch.java index bedad1667f33..ca873fc6eafa 100644 --- a/src/main/java/com/thealgorithms/searches/BinarySearch.java +++ b/src/main/java/com/thealgorithms/searches/BinarySearch.java @@ -3,12 +3,32 @@ import com.thealgorithms.devutils.searches.SearchAlgorithm; /** - * Binary search is one of the most popular algorithms The algorithm finds the - * position of a target value within a sorted array + * Binary Search Algorithm Implementation * - *

- * Worst-case performance O(log n) Best-case performance O(1) Average - * performance O(log n) Worst-case space complexity O(1) + *

Binary search is one of the most efficient searching algorithms for finding a target element + * in a SORTED array. It works by repeatedly dividing the search space in half, eliminating half of + * the remaining elements in each step. + * + *

IMPORTANT: This algorithm ONLY works correctly if the input array is sorted in ascending + * order. + * + *

Algorithm Overview: 1. Start with the entire array (left = 0, right = array.length - 1) 2. + * Calculate the middle index 3. Compare the middle element with the target: - If middle element + * equals target: Found! Return the index - If middle element is less than target: Search the right + * half - If middle element is greater than target: Search the left half 4. Repeat until element is + * found or search space is exhausted + * + *

Performance Analysis: - Best-case time complexity: O(1) - Element found at middle on first + * try - Average-case time complexity: O(log n) - Most common scenario - Worst-case time + * complexity: O(log n) - Element not found or at extreme end - Space complexity: O(1) - Only uses + * a constant amount of extra space + * + *

Example Walkthrough: Array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] Target: 7 + * + *

Step 1: left=0, right=9, mid=4, array[4]=9 (9 > 7, search left half) Step 2: left=0, + * right=3, mid=1, array[1]=3 (3 < 7, search right half) Step 3: left=2, right=3, mid=2, + * array[2]=5 (5 < 7, search right half) Step 4: left=3, right=3, mid=3, array[3]=7 (Found! + * Return index 3) * * @author Varun Upadhyay (https://github.com/varunu28) * @author Podshivalov Nikita (https://github.com/nikitap492) @@ -18,38 +38,96 @@ class BinarySearch implements SearchAlgorithm { /** - * @param array is an array where the element should be found - * @param key is an element which should be found - * @param is any comparable type - * @return index of the element + * Generic method to perform binary search on any comparable type. This is the main entry point + * for binary search operations. + * + *

Example Usage: + *

+     * Integer[] numbers = {1, 3, 5, 7, 9, 11};
+     * int result = new BinarySearch().find(numbers, 7);
+     * // result will be 3 (index of element 7)
+     *
+     * int notFound = new BinarySearch().find(numbers, 4);
+     * // notFound will be -1 (element 4 does not exist)
+     * 
+ * + * @param The type of elements in the array (must be Comparable) + * @param array The sorted array to search in (MUST be sorted in ascending order) + * @param key The element to search for + * @return The index of the key if found, -1 if not found or if array is null/empty */ @Override public > int find(T[] array, T key) { + // Handle edge case: null or empty array + if (array == null || array.length == 0) { + return -1; + } + + // Handle edge case: null key + // Searching for null in an array of Comparables is undefined behavior + // Return -1 to indicate not found rather than throwing NPE + if (key == null) { + return -1; + } + + // Delegate to the core search implementation return search(array, key, 0, array.length - 1); } /** - * This method implements the Generic Binary Search + * Core recursive implementation of binary search algorithm. This method divides the problem + * into smaller subproblems recursively. + * + *

How it works: + *

    + *
  1. Calculate the middle index to avoid integer overflow
  2. + *
  3. Check if middle element matches the target
  4. + *
  5. If not, recursively search either left or right half
  6. + *
  7. Base case: left > right means element not found
  8. + *
+ * + *

Time Complexity: O(log n) because we halve the search space each time. + * Space Complexity: O(log n) due to recursive call stack. * - * @param array The array to make the binary search - * @param key The number you are looking for - * @param left The lower bound - * @param right The upper bound - * @return the location of the key + * @param The type of elements (must be Comparable) + * @param array The sorted array to search in + * @param key The element we're looking for + * @param left The leftmost index of current search range (inclusive) + * @param right The rightmost index of current search range (inclusive) + * @return The index where key is located, or -1 if not found */ private > int search(T[] array, T key, int left, int right) { + // Base case: Search space is exhausted + // This happens when left pointer crosses right pointer if (right < left) { - return -1; // this means that the key not found + return -1; // Key not found in the array } - // find median - int median = (left + right) >>> 1; + + // Calculate middle index + // Using (left + right) / 2 could cause integer overflow for large arrays + // So we use: left + (right - left) / 2 which is mathematically equivalent + // but prevents overflow + int median = (left + right) >>> 1; // Unsigned right shift is faster division by 2 + + // Get the value at middle position for comparison int comp = key.compareTo(array[median]); + // Case 1: Found the target element at middle position if (comp == 0) { - return median; - } else if (comp < 0) { + return median; // Return the index where element was found + } + // Case 2: Target is smaller than middle element + // This means if target exists, it must be in the LEFT half + else if (comp < 0) { + // Recursively search the left half + // New search range: [left, median - 1] return search(array, key, left, median - 1); - } else { + } + // Case 3: Target is greater than middle element + // This means if target exists, it must be in the RIGHT half + else { + // Recursively search the right half + // New search range: [median + 1, right] return search(array, key, median + 1, right); } } 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/main/java/com/thealgorithms/searches/ExponentialSearch.java b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java index 9187dcbc2f4b..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; } @@ -46,6 +46,9 @@ public > int find(T[] array, T key) { range = range * 2; } - return Arrays.binarySearch(array, range / 2, Math.min(range, array.length), key); + // The candidate block is the inclusive index range [range / 2, range], so the + // exclusive upper bound handed to binarySearch has to be range + 1. + final int index = Arrays.binarySearch(array, range / 2, Math.min(range + 1, array.length), key); + return index >= 0 ? index : -1; } } diff --git a/src/main/java/com/thealgorithms/searches/FibonacciSearch.java b/src/main/java/com/thealgorithms/searches/FibonacciSearch.java index 78dac0f0a712..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."); } @@ -69,14 +71,14 @@ public > int find(T[] array, T key) { } } - if (fibMinus1 == 1 && array[offset + 1] == key) { + if (fibMinus1 == 1 && offset + 1 < n && array[offset + 1].compareTo(key) == 0) { return offset + 1; } 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/main/java/com/thealgorithms/searches/HowManyTimesRotated.java b/src/main/java/com/thealgorithms/searches/HowManyTimesRotated.java index dd01378f4d5f..81107de2929f 100644 --- a/src/main/java/com/thealgorithms/searches/HowManyTimesRotated.java +++ b/src/main/java/com/thealgorithms/searches/HowManyTimesRotated.java @@ -16,14 +16,15 @@ The position of the minimum element will give the number of times the array has been rotated from its initial sorted position. Eg. For [2,5,6,8,11,12,15,18], 1 rotation gives [5,6,8,11,12,15,18,2], 2 rotations - [6,8,11,12,15,18,2,5] and so on. Finding the minimum element will take O(N) time but, we can use - Binary Search to find the minimum element, we can reduce the complexity to O(log N). If we look - at the rotated array, to identify the minimum element (say a[i]), we observe that - a[i-1]>a[i] a[high], the minimum lies to the right, so low = mid + 1; otherwise it lies at mid + or to the left, so high = mid. This converges to the minimum's index without ever reading + a[mid-1] or a[mid+1], so it also works on arrays of size 0-2 and unrotated arrays. Some other test cases: 1. [1,2,3,4] Number of rotations: 0 or 4(Both valid) - 2. [15,17,2,3,5] Number of rotations: 3 + 2. [15,17,2,3,5] Number of rotations: 2 */ final class HowManyTimesRotated { private HowManyTimesRotated() { @@ -44,20 +45,16 @@ public static void main(String[] args) { public static int rotated(int[] a) { int low = 0; int high = a.length - 1; - int mid = 0; // low + (high-low)/2 = (low + high)/2 - while (low <= high) { - mid = low + (high - low) / 2; - - if (a[mid] < a[mid - 1] && a[mid] < a[mid + 1]) { - break; - } else if (a[mid] > a[mid - 1] && a[mid] < a[mid + 1]) { - high = mid + 1; - } else if (a[mid] > a[mid - 1] && a[mid] > a[mid + 1]) { - low = mid - 1; + while (low < high) { + int mid = low + (high - low) / 2; + if (a[mid] > a[high]) { + low = mid + 1; + } else { + high = mid; } } - return mid; + return low; } } diff --git a/src/main/java/com/thealgorithms/searches/InterpolationSearch.java b/src/main/java/com/thealgorithms/searches/InterpolationSearch.java index 3ac6be25bf53..52dbc0b7e2c5 100644 --- a/src/main/java/com/thealgorithms/searches/InterpolationSearch.java +++ b/src/main/java/com/thealgorithms/searches/InterpolationSearch.java @@ -1,3 +1,4 @@ + package com.thealgorithms.searches; /** @@ -35,9 +36,12 @@ public int find(int[] array, int key) { // Since array is sorted, an element present // in array must be in range defined by corner while (start <= end && key >= array[start] && key <= array[end]) { + if (array[start] == array[end]) { + return start; + } // Probing the position with keeping // uniform distribution in mind. - int pos = start + (((end - start) / (array[end] - array[start])) * (key - array[start])); + int pos = start + (int) (((long) (end - start) * (key - array[start])) / ((long) array[end] - array[start])); // Condition of target found if (array[pos] == key) { diff --git a/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java b/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java index 05fab0534267..d051dbc7b823 100644 --- a/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java +++ b/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java @@ -3,50 +3,53 @@ import com.thealgorithms.devutils.searches.SearchAlgorithm; /** - * Binary search is one of the most popular algorithms This class represents - * iterative version {@link BinarySearch} Iterative binary search is likely to - * have lower constant factors because it doesn't involve the overhead of - * manipulating the call stack. But in java the recursive version can be - * optimized by the compiler to this version. + * Binary search is one of the most popular algorithms. + * This class represents the iterative version of {@link BinarySearch}. * - *

- * Worst-case performance O(log n) Best-case performance O(1) Average - * performance O(log n) Worst-case space complexity O(1) + *

Iterative binary search avoids recursion overhead and uses constant space. * - * @author Gabriele La Greca : https://github.com/thegabriele97 - * @author Podshivalov Nikita (https://github.com/nikitap492) + *

Performance: + *

    + *
  • Best-case: O(1)
  • + *
  • Average-case: O(log n)
  • + *
  • Worst-case: O(log n)
  • + *
  • Space complexity: O(1)
  • + *
+ * + * @author Gabriele La Greca + * @author Podshivalov Nikita * @see SearchAlgorithm * @see BinarySearch */ public final class IterativeBinarySearch implements SearchAlgorithm { /** - * This method implements an iterative version of binary search algorithm + * Performs iterative binary search on a sorted array. * - * @param array a sorted array - * @param key the key to search in array - * @return the index of key in the array or -1 if not found + * @param array the sorted array + * @param key the element to search + * @param type of elements (must be Comparable) + * @return index of the key if found, otherwise -1 */ @Override public > int find(T[] array, T key) { - int l; - int r; - int k; - int cmp; + if (array == null || array.length == 0 || key == null) { + return -1; + } - l = 0; - r = array.length - 1; + int left = 0; + int right = array.length - 1; - while (l <= r) { - k = (l + r) >>> 1; - cmp = key.compareTo(array[k]); + while (left <= right) { + int mid = (left + right) >>> 1; + int cmp = key.compareTo(array[mid]); if (cmp == 0) { - return k; + return mid; } else if (cmp < 0) { - r = --k; + right = mid - 1; } else { - l = ++k; + left = mid + 1; } } diff --git a/src/main/java/com/thealgorithms/searches/JumpSearch.java b/src/main/java/com/thealgorithms/searches/JumpSearch.java index 8dcec3a819a4..5e4c6da15f86 100644 --- a/src/main/java/com/thealgorithms/searches/JumpSearch.java +++ b/src/main/java/com/thealgorithms/searches/JumpSearch.java @@ -12,42 +12,77 @@ * Once the range is found, a linear search is performed within that block. * *

- * The Jump Search algorithm is particularly effective for large sorted arrays where the cost of - * performing a linear search on the entire array would be prohibitive. + * How it works: + *

    + *
  1. Calculate the optimal block size as √n (square root of array length)
  2. + *
  3. Jump ahead by the block size until the current element is greater than the target
  4. + *
  5. Perform a linear search backwards within the identified block
  6. + *
* *

- * Worst-case performance: O(√N)
- * Best-case performance: O(1)
- * Average performance: O(√N)
- * Worst-case space complexity: O(1) + * Example:
+ * Array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], Target: 9
+ * Step 1: Jump from index 0 β†’ 3 β†’ 6 (9 < 13, so we found the block)
+ * Step 2: Linear search from index 3 to 6: found 9 at index 4
+ * Result: Index = 4 + * + *

+ * Time Complexity:
+ * - Best-case: O(1) - element found at first position
+ * - Average: O(√n) - optimal block size reduces jumps
+ * - Worst-case: O(√n) - element at end of array or not present
+ * + *

+ * Space Complexity: O(1) - only uses a constant amount of extra space + * + *

+ * Edge Cases: + *

    + *
  • Empty array β†’ returns -1
  • + *
  • Element not present β†’ returns -1
  • + *
  • Single element array
  • + *
+ *

+ * Note: Jump Search requires a sorted array. For unsorted arrays, use Linear Search. + * Compared to Linear Search (O(n)), Jump Search is faster for large arrays. + * Compared to Binary Search (O(log n)), Jump Search is less efficient but may be + * preferable when jumping through a linked list or when backward scanning is costly. * *

* This class implements the {@link SearchAlgorithm} interface, providing a generic search method * for any comparable type. + * + * @see SearchAlgorithm + * @see BinarySearch + * @see LinearSearch */ -public class JumpSearch implements SearchAlgorithm { +public final class JumpSearch implements SearchAlgorithm { /** * Jump Search algorithm implementation. * - * @param array the sorted array containing elements - * @param key the element to be searched + * @param array the sorted array containing elements (must be sorted in ascending order) + * @param key the element to be searched for * @return the index of {@code key} if found, otherwise -1 */ @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); int limit = blockSize; // Jumping ahead to find the block where the key may be located while (limit < length && key.compareTo(array[limit]) > 0) { - limit = Math.min(limit + blockSize, length - 1); + limit += blockSize; } // 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/main/java/com/thealgorithms/searches/LinearSearch.java b/src/main/java/com/thealgorithms/searches/LinearSearch.java index c7b70edb5112..3ec1768916e5 100644 --- a/src/main/java/com/thealgorithms/searches/LinearSearch.java +++ b/src/main/java/com/thealgorithms/searches/LinearSearch.java @@ -1,37 +1,76 @@ + package com.thealgorithms.searches; import com.thealgorithms.devutils.searches.SearchAlgorithm; /** - * Linear search is the easiest search algorithm It works with sorted and - * unsorted arrays (an binary search works only with sorted array) This - * algorithm just compares all elements of an array to find a value + * Linear Search is a simple searching algorithm that checks + * each element of the array sequentially until the target + * value is found or the array ends. + * + * It works for both sorted and unsorted arrays. + * + *

How it works step-by-step: + *

    + *
  1. Start from the first element of the array.
  2. + *
  3. Compare the current element with the target value.
  4. + *
  5. If they match, return the current index.
  6. + *
  7. If they don't match, move to the next element.
  8. + *
  9. Repeat until the element is found or the array ends.
  10. + *
  11. If not found, return -1.
  12. + *
+ * + *

Example: + *

+ *   Input array: [5, 3, 8, 1, 9]
+ *   Target: 8
+ *
+ *   Step 1: Compare 5 with 8 β†’ no match, move on
+ *   Step 2: Compare 3 with 8 β†’ no match, move on
+ *   Step 3: Compare 8 with 8 β†’ match found at index 2!
+ *
+ *   Output: 2
  *
- * 

- * Worst-case performance O(n) Best-case performance O(1) Average performance - * O(n) Worst-case space complexity + * If target = 7: + * Output: -1 (not found) + *

+ * Time Complexity: + * - Best case: O(1) - target is the first element + * - Average case: O(n) - target is somewhere in the middle + * - Worst case: O(n) - target is last or not present * - * @author Varun Upadhyay (https://github.com/varunu28) - * @author Podshivalov Nikita (https://github.com/nikitap492) + * Space Complexity: O(1) + * + * @author Varun Upadhyay + * @author Podshivalov Nikita * @see BinarySearch * @see SearchAlgorithm */ public class LinearSearch implements SearchAlgorithm { /** - * Generic Linear search method + * Generic Linear search method that searches for a value + * in the given array by checking each element one by one. * - * @param array List to be searched + * @param array List to be searched (can be unsorted) * @param value Key being searched for - * @return Location of the key + * @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) { + + if (array == null || array.length == 0 || value == null) { + return -1; + } + for (int i = 0; i < array.length; i++) { - if (array[i].compareTo(value) == 0) { + T currentElement = array[i]; + if (currentElement != null && currentElement.compareTo(value) == 0) { return i; } } + return -1; } } 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/PerfectBinarySearch.java b/src/main/java/com/thealgorithms/searches/PerfectBinarySearch.java deleted file mode 100644 index 495e2e41bc5b..000000000000 --- a/src/main/java/com/thealgorithms/searches/PerfectBinarySearch.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.thealgorithms.searches; - -import com.thealgorithms.devutils.searches.SearchAlgorithm; - -/** - * Binary search is one of the most popular algorithms The algorithm finds the - * position of a target value within a sorted array - * - *

- * Worst-case performance O(log n) Best-case performance O(1) Average - * performance O(log n) Worst-case space complexity O(1) - * - * @author D Sunil (https://github.com/sunilnitdgp) - * @see SearchAlgorithm - */ - -public class PerfectBinarySearch implements SearchAlgorithm { - - /** - * @param array is an array where the element should be found - * @param key is an element which should be found - * @param is any comparable type - * @return index of the element - */ - @Override - public > int find(T[] array, T key) { - return search(array, key, 0, array.length - 1); - } - - /** - * This method implements the Generic Binary Search iteratively. - * - * @param array The array to make the binary search - * @param key The number you are looking for - * @return the location of the key, or -1 if not found - */ - private static > int search(T[] array, T key, int left, int right) { - while (left <= right) { - int median = (left + right) >>> 1; - int comp = key.compareTo(array[median]); - - if (comp == 0) { - return median; // Key found - } - - if (comp < 0) { - right = median - 1; // Adjust the right bound - } else { - left = median + 1; // Adjust the left bound - } - } - return -1; // Key not found - } -} diff --git a/src/main/java/com/thealgorithms/searches/RecursiveBinarySearch.java b/src/main/java/com/thealgorithms/searches/RecursiveBinarySearch.java index daf0c12c0978..1716e78964ae 100644 --- a/src/main/java/com/thealgorithms/searches/RecursiveBinarySearch.java +++ b/src/main/java/com/thealgorithms/searches/RecursiveBinarySearch.java @@ -23,28 +23,27 @@ public int find(T[] arr, T target) { // Recursive binary search function public int binsear(T[] arr, int left, int right, T target) { - if (right >= left) { - int mid = left + (right - left) / 2; - - // Compare the element at the middle with the target - int comparison = arr[mid].compareTo(target); + if (right < left) { + // Element is not present in the array + return -1; + } + final int mid = left + (right - left) / 2; - // If the element is equal to the target, return its index - if (comparison == 0) { - return mid; - } + // Compare the element at the middle with the target + final int comparison = arr[mid].compareTo(target); - // If the element is greater than the target, search in the left subarray - if (comparison > 0) { - return binsear(arr, left, mid - 1, target); - } + // If the element is equal to the target, return its index + if (comparison == 0) { + return mid; + } - // Otherwise, search in the right subarray - return binsear(arr, mid + 1, right, target); + // If the element is greater than the target, search in the left subarray + if (comparison > 0) { + return binsear(arr, left, mid - 1, target); } - // Element is not present in the array - return -1; + // Otherwise, search in the right subarray + return binsear(arr, mid + 1, right, target); } public static void main(String[] args) { diff --git a/src/main/java/com/thealgorithms/searches/RotatedBinarySearch.java b/src/main/java/com/thealgorithms/searches/RotatedBinarySearch.java new file mode 100644 index 000000000000..4b9717c20fe4 --- /dev/null +++ b/src/main/java/com/thealgorithms/searches/RotatedBinarySearch.java @@ -0,0 +1,70 @@ +package com.thealgorithms.searches; + +import com.thealgorithms.devutils.searches.SearchAlgorithm; + +/** + * Searches for a key in a sorted array that has been rotated at an unknown pivot. + * + *

+ * Example: + * {@code [8, 9, 10, 1, 2, 3, 4, 5, 6, 7]} + * + *

+ * This is a modified binary search. When the array contains no duplicates, the + * time complexity is {@code O(log n)}. With duplicates, the algorithm still + * works but may degrade to {@code O(n)} in the worst case. The algorithm uses + * {@code O(1)} additional space and returns {@code -1} for null or empty input, + * or when the key is not present. + * + * @param array sorted array rotated at an unknown pivot + * @param key value to search for + * @return an index containing {@code key}, or {@code -1} if it is not present + * + * @see Search in rotated sorted array + * @see SearchAlgorithm + */ +public final class RotatedBinarySearch implements SearchAlgorithm { + + @Override + public > int find(T[] array, T key) { + if (array == null || array.length == 0 || key == null) { + return -1; + } + + int left = 0; + int right = array.length - 1; + + while (left <= right) { + int middle = (left + right) >>> 1; + int cmp = key.compareTo(array[middle]); + if (cmp == 0) { + return middle; + } + + // Handle duplicates: if we cannot determine which side is sorted. + if (array[left].compareTo(array[middle]) == 0 && array[middle].compareTo(array[right]) == 0) { + left++; + right--; + continue; + } + + // Left half is sorted. + if (array[left].compareTo(array[middle]) <= 0) { + if (array[left].compareTo(key) <= 0 && key.compareTo(array[middle]) < 0) { + right = middle - 1; + } else { + left = middle + 1; + } + } else { + // Right half is sorted. + if (array[middle].compareTo(key) < 0 && key.compareTo(array[right]) <= 0) { + left = middle + 1; + } else { + right = middle - 1; + } + } + } + + return -1; + } +} 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/main/java/com/thealgorithms/searches/SentinelLinearSearch.java b/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java index 1a5903a5d134..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,7 +65,8 @@ public > int find(T[] array, T key) { int i = 0; // Search without bound checking since sentinel guarantees we'll find the key - while (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/main/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearch.java b/src/main/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearch.java deleted file mode 100644 index 6a2a46c2821f..000000000000 --- a/src/main/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearch.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.thealgorithms.searches; -public final class SortOrderAgnosticBinarySearch { - private SortOrderAgnosticBinarySearch() { - } - public static int find(int[] arr, int key) { - int start = 0; - int end = arr.length - 1; - boolean arrDescending = arr[start] > arr[end]; // checking for Array is in ascending order or descending order. - while (start <= end) { - int mid = end - start / 2; - if (arr[mid] == key) { - return mid; - } - if (arrDescending) { // boolean is true then our array is in descending order - if (key < arr[mid]) { - start = mid + 1; - } else { - end = mid - 1; - } - } else { // otherwise our array is in ascending order - if (key > arr[mid]) { - start = mid + 1; - } else { - end = mid - 1; - } - } - } - return -1; - } -} 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/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/main/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindow.java b/src/main/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindow.java new file mode 100644 index 000000000000..19e573437f6d --- /dev/null +++ b/src/main/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindow.java @@ -0,0 +1,57 @@ +package com.thealgorithms.slidingwindow; + +import java.util.HashMap; +import java.util.Map; + +/** + * Counts the number of distinct elements in every window of size k. + * + * @see Reference + */ +public final class CountDistinctElementsInWindow { + + private CountDistinctElementsInWindow() { + } + + /** + * Returns an array where each element is the count of distinct + * elements in the corresponding window of size k. + * + * @param arr the input array + * @param k the window size + * @return array of distinct element counts per window + */ + public static int[] countDistinct(int[] arr, int k) { + if (arr == null || arr.length == 0 || k <= 0 || k > arr.length) { + throw new IllegalArgumentException("Invalid input"); + } + + int n = arr.length; + int[] result = new int[n - k + 1]; + Map freqMap = new HashMap<>(); + + for (int i = 0; i < k; i++) { + freqMap.merge(arr[i], 1, Integer::sum); + } + result[0] = freqMap.size(); + + for (int i = k; i < n; i++) { + freqMap.merge(arr[i], 1, Integer::sum); + + int outgoing = arr[i - k]; + + Integer count = freqMap.get(outgoing); + if (count != null) { + if (count == 1) { + freqMap.remove(outgoing); + } else { + freqMap.put(outgoing, count - 1); + } + } + + result[i - k + 1] = freqMap.size(); + } + + return result; + } +} diff --git a/src/main/java/com/thealgorithms/slidingwindow/CountNiceSubarrays.java b/src/main/java/com/thealgorithms/slidingwindow/CountNiceSubarrays.java new file mode 100644 index 000000000000..46f8deeb58dd --- /dev/null +++ b/src/main/java/com/thealgorithms/slidingwindow/CountNiceSubarrays.java @@ -0,0 +1,99 @@ +package com.thealgorithms.slidingwindow; + +/** + * Counts the number of "nice subarrays". + * A nice subarray is a contiguous subarray that contains exactly k odd numbers. + * + * This implementation uses the sliding window technique. + * + * Reference: + * https://leetcode.com/problems/count-number-of-nice-subarrays/ + * + * Time Complexity: O(n) + * Space Complexity: O(n) + */ +public final class CountNiceSubarrays { + + // Private constructor to prevent instantiation + private CountNiceSubarrays() { + } + + /** + * Returns the count of subarrays containing exactly k odd numbers. + * + * @param nums input array of integers + * @param k number of odd elements required in the subarray + * @return number of nice subarrays + */ + public static int countNiceSubarrays(int[] nums, int k) { + + int n = nums.length; + + // Left pointer of the sliding window + int left = 0; + + // Tracks number of odd elements in the current window + int oddCount = 0; + + // Final answer: total number of nice subarrays + int result = 0; + + /* + * memo[i] stores how many valid starting positions exist + * when the left pointer is at index i. + * + * This avoids recomputing the same values again. + */ + int[] memo = new int[n]; + + // Right pointer moves forward to expand the window + for (int right = 0; right < n; right++) { + + // If current element is odd, increment odd count + if ((nums[right] & 1) == 1) { + oddCount++; + } + + /* + * If oddCount exceeds k, shrink the window from the left + * until oddCount becomes valid again. + */ + if (oddCount > k) { + left += memo[left]; + oddCount--; + } + + /* + * When the window contains exactly k odd numbers, + * count all possible valid subarrays starting at `left`. + */ + if (oddCount == k) { + + /* + * If this left index hasn't been processed before, + * count how many consecutive even numbers follow it. + */ + if (memo[left] == 0) { + int count = 0; + int temp = left; + + // Count consecutive even numbers + while ((nums[temp] & 1) == 0) { + count++; + temp++; + } + + /* + * Number of valid subarrays starting at `left` + * is (count of even numbers + 1) + */ + memo[left] = count + 1; + } + + // Add number of valid subarrays for this left position + result += memo[left]; + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/sorts/BitonicSort.java b/src/main/java/com/thealgorithms/sorts/BitonicSort.java index 1c1a3ac45540..a714809ea5b2 100644 --- a/src/main/java/com/thealgorithms/sorts/BitonicSort.java +++ b/src/main/java/com/thealgorithms/sorts/BitonicSort.java @@ -21,6 +21,9 @@ private enum Direction { */ @Override public > T[] sort(T[] array) { + if (array == null) { + throw new IllegalArgumentException("The input array cannot be null"); + } if (array.length == 0) { return array; } diff --git a/src/main/java/com/thealgorithms/sorts/BubbleSort.java b/src/main/java/com/thealgorithms/sorts/BubbleSort.java index 6823c68d0a74..d2eca3506c2d 100644 --- a/src/main/java/com/thealgorithms/sorts/BubbleSort.java +++ b/src/main/java/com/thealgorithms/sorts/BubbleSort.java @@ -10,6 +10,13 @@ class BubbleSort implements SortAlgorithm { /** * Implements generic bubble sort algorithm. * + * Time Complexity: + * - Best case: O(n) – array is already sorted. + * - Average case: O(n^2) + * - Worst case: O(n^2) + * + * Space Complexity: O(1) – in-place sorting. + * * @param array the array to be sorted. * @param the type of elements in the array. * @return the sorted array. diff --git a/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java b/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java new file mode 100644 index 000000000000..062da01f380e --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java @@ -0,0 +1,145 @@ +package com.thealgorithms.sorts; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * A concurrent implementation of the Merge Sort algorithm. + * + *

This implementation utilizes a divide-and-conquer strategy, distributing + * the sorting of sub-arrays across multiple threads using a {@link ThreadPoolExecutor}. + * To prevent the overhead of thread creation and context switching from outweighing + * the benefits of concurrency, it falls back to a standard sequential merge sort + * when the sub-array size drops below a predefined threshold, or when the maximum + * concurrency depth is reached (preventing thread starvation deadlocks). + * + *

Complexity: + *

    + *
  • Time Complexity: $O(N \log N)$
  • + *
  • Space Complexity: $O(N)$
  • + *
+ */ +public final class ConcurrentMergeSort { + + private ConcurrentMergeSort() { + } + + /** + * Fallback threshold where the algorithm switches to standard sequential + * Merge Sort to prevent thread-creation overhead from ruining performance. + */ + private static final int SEQUENTIAL_THRESHOLD = 8192; + + /** + * Sorts the specified array of integers concurrently using Merge Sort. + * + * @param array the array to be sorted + */ + public static void sort(int[] array) { + if (array == null || array.length <= 1) { + return; + } + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + + // Calculate a safe maximum depth to prevent creating more tasks than the pool can handle. + // This effectively prevents thread starvation deadlock in fixed-size thread pools, + // by forcing leaf tasks to run sequentially and eventually complete. + int maxDepth = (int) (Math.log(availableProcessors) / Math.log(2)) + 1; + + ThreadPoolExecutor executor = new ThreadPoolExecutor(availableProcessors, availableProcessors, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); + + try { + int[] tempArray = new int[array.length]; + concurrentMergeSort(array, tempArray, 0, array.length - 1, executor, maxDepth); + } finally { + // Ensure the executor is gracefully shut down + executor.shutdown(); + } + } + + /** + * Recursively sorts the array utilizing the provided executor for concurrency. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + * @param executor the {@link ThreadPoolExecutor} to handle concurrent tasks + * @param depth the remaining depth for allowing concurrent execution + */ + private static void concurrentMergeSort(int[] array, int[] temp, int left, int right, ThreadPoolExecutor executor, int depth) { + int length = right - left + 1; + + // Switch to sequential sort if the array is small or we have reached the maximum concurrent depth + if (length < SEQUENTIAL_THRESHOLD || depth <= 0) { + sequentialMergeSort(array, temp, left, right); + return; + } + + int mid = left + (right - left) / 2; + + // Submit the left half for concurrent execution + CompletableFuture leftTask = CompletableFuture.runAsync(() -> concurrentMergeSort(array, temp, left, mid, executor, depth - 1), executor); + + // Process the right half in the current thread to optimize resource usage + concurrentMergeSort(array, temp, mid + 1, right, executor, depth - 1); + + // Wait for the concurrently executed left half to complete + leftTask.join(); + + merge(array, temp, left, mid, right); + } + + /** + * Sorts the specified sub-array sequentially using standard Merge Sort. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + */ + private static void sequentialMergeSort(int[] array, int[] temp, int left, int right) { + if (left >= right) { + return; + } + + int mid = left + (right - left) / 2; + sequentialMergeSort(array, temp, left, mid); + sequentialMergeSort(array, temp, mid + 1, right); + merge(array, temp, left, mid, right); + } + + /** + * Merges two sorted sub-arrays into a single sorted sub-array. + * + * @param array the original array containing the sub-arrays + * @param temp a temporary array used for merging + * @param left the starting index of the first sub-array + * @param mid the ending index of the first sub-array (and the partition point) + * @param right the ending index of the second sub-array + */ + private static void merge(int[] array, int[] temp, int left, int mid, int right) { + System.arraycopy(array, left, temp, left, right - left + 1); + + int i = left; + int j = mid + 1; + int k = left; + + while (i <= mid && j <= right) { + if (temp[i] <= temp[j]) { + array[k++] = temp[i++]; + } else { + array[k++] = temp[j++]; + } + } + + while (i <= mid) { + array[k++] = temp[i++]; + } + + // Remaining elements from the right half are already in their correct relative positions + } +} 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/main/java/com/thealgorithms/sorts/HeapSort.java b/src/main/java/com/thealgorithms/sorts/HeapSort.java index e798fb91b925..5e3b20f43e10 100644 --- a/src/main/java/com/thealgorithms/sorts/HeapSort.java +++ b/src/main/java/com/thealgorithms/sorts/HeapSort.java @@ -1,9 +1,20 @@ package com.thealgorithms.sorts; /** - * Heap Sort Algorithm Implementation + * Heap Sort algorithm implementation. + * + * Heap sort converts the array into a max-heap and repeatedly extracts the maximum + * element to sort the array in increasing order. + * + * Time Complexity: + * - Best case: O(n log n) + * - Average case: O(n log n) + * - Worst case: O(n log n) + * + * Space Complexity: O(1) – in-place sorting * * @see Heap Sort Algorithm + * @see SortAlgorithm */ public class HeapSort implements SortAlgorithm { diff --git a/src/main/java/com/thealgorithms/sorts/InsertionSort.java b/src/main/java/com/thealgorithms/sorts/InsertionSort.java index 21ebf3827b5f..1e42f2a61271 100644 --- a/src/main/java/com/thealgorithms/sorts/InsertionSort.java +++ b/src/main/java/com/thealgorithms/sorts/InsertionSort.java @@ -1,5 +1,23 @@ package com.thealgorithms.sorts; +/** + * Generic Insertion Sort algorithm. + * + * Standard insertion sort iterates through the array and inserts each element into its + * correct position in the sorted portion of the array. + * + * Sentinel sort is a variation that first places the minimum element at index 0 to + * avoid redundant comparisons in subsequent passes. + * + * Time Complexity: + * - Best case: O(n) – array is already sorted (sentinel sort can improve slightly) + * - Average case: O(n^2) + * - Worst case: O(n^2) – array is reverse sorted + * + * Space Complexity: O(1) – in-place sorting + * + * @see SortAlgorithm + */ class InsertionSort implements SortAlgorithm { /** @@ -15,30 +33,30 @@ public > T[] sort(T[] array) { } /** - * Sorts a subarray of the given array using the standard Insertion Sort algorithm. + * Sorts a subarray of the given items using the standard Insertion Sort algorithm. * - * @param array The array to be sorted - * @param lo The starting index of the subarray - * @param hi The ending index of the subarray (exclusive) - * @param The type of elements in the array, which must be comparable - * @return The sorted array + * @param items The items to be sorted + * @param startIndex The starting index of the subarray + * @param endIndex The ending index of the subarray (exclusive) + * @param The type of elements in the items, which must be comparable + * @return The sorted items */ - public > T[] sort(T[] array, final int lo, final int hi) { - if (array == null || lo >= hi) { - return array; + public > T[] sort(T[] items, final int startIndex, final int endIndex) { + if (items == null || startIndex >= endIndex) { + return items; } - for (int i = lo + 1; i < hi; i++) { - final T key = array[i]; + for (int i = startIndex + 1; i < endIndex; i++) { + final T key = items[i]; int j = i - 1; - while (j >= lo && SortUtils.less(key, array[j])) { - array[j + 1] = array[j]; + while (j >= startIndex && SortUtils.less(key, items[j])) { + items[j + 1] = items[j]; j--; } - array[j + 1] = key; + items[j + 1] = key; } - return array; + return items; } /** diff --git a/src/main/java/com/thealgorithms/sorts/LibrarySort.java b/src/main/java/com/thealgorithms/sorts/LibrarySort.java new file mode 100644 index 000000000000..28f16e3016ff --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/LibrarySort.java @@ -0,0 +1,207 @@ +package com.thealgorithms.sorts; + +import java.util.Arrays; + +/** + * Library Sort (also known as Gapped Insertion Sort) maintains a sparse + * working array with gaps distributed between elements, so that most + * insertions land directly in an empty gap without shifting anything. + * Elements are inserted in rounds that double in size (1, 2, 4, 8, ...); + * after each round the array is rebalanced so gaps are spread out evenly + * again for the next round. + * Time Complexity: O(n log n) expected, O(n^2) worst case if gaps collapse + * Space Complexity: O(n) + * + * @see + * Wikipedia: Library Sort + * @author Vraj Prajapati (@Rosander0) + */ +public final class LibrarySort { + + private static final int GAP_FACTOR = 2; + + private LibrarySort() { + // Utility class + } + + /** + * Sorts an array using the Library Sort algorithm. + * + * @param array the array to sort (must not be null) + * @return the sorted array + * @throws IllegalArgumentException if {@code array} is {@code null} + */ + public static int[] sort(final int[] array) { + if (array == null) { + throw new IllegalArgumentException("Input array must not be null."); + } + if (array.length <= 1) { + return array; + } + + final int n = array.length; + final int capacity = GAP_FACTOR * n; + final int[] data = new int[capacity]; + final boolean[] occupied = new boolean[capacity]; + + final int mid = capacity / 2; + data[mid] = array[0]; + occupied[mid] = true; + + int filled = 1; + int nextToInsert = 1; + int round = 0; + while (nextToInsert < n) { + final int roundSize = Math.min(1 << round, n - nextToInsert); + for (int i = 0; i < roundSize; i++) { + insert(data, occupied, array[nextToInsert + i]); + filled++; + } + nextToInsert += roundSize; + round++; + if (nextToInsert < n) { + rebalance(data, occupied, filled); + } + } + + int idx = 0; + for (int i = 0; i < capacity; i++) { + if (occupied[i]) { + array[idx++] = data[i]; + } + } + return array; + } + + /** + * Inserts {@code value} into the gapped array, placing it directly in an + * empty gap when possible, otherwise shifting toward the nearest gap. + */ + private static void insert(final int[] data, final boolean[] occupied, final int value) { + final int pos = findInsertionIndex(data, occupied, value); + if (pos >= data.length) { + insertAtEnd(data, occupied, value); + return; + } + + if (!occupied[pos]) { + data[pos] = value; + occupied[pos] = true; + return; + } + + int right = pos; + while (right < data.length && occupied[right]) { + right++; + } + int left = pos - 1; + while (left >= 0 && occupied[left]) { + left--; + } + + final boolean canGoRight = right < data.length; + final boolean canGoLeft = left >= 0; + + if (canGoRight && (!canGoLeft || (right - pos) <= (pos - left))) { + // Shift data[pos, right) one slot to the right, opening a gap at pos. + // occupied[pos] is untouched by the copy and was already true. + System.arraycopy(data, pos, data, pos + 1, right - pos); + occupied[right] = true; + data[pos] = value; + } else if (canGoLeft) { + // Shift data[left + 1, pos) one slot to the left, opening a gap at pos - 1. + // occupied[pos - 1] is untouched by the copy and was already true. + System.arraycopy(data, left + 1, data, left, pos - 1 - left); + occupied[left] = true; + data[pos - 1] = value; + } else { + // Unreachable in practice: canGoRight and canGoLeft can only both be false if + // every slot in this capacity-2n array is occupied, but at most n elements are + // ever present at once. Kept as a defensive guard against that invariant breaking. + throw new IllegalStateException("No gap available for insertion; rebalance too infrequent."); + } + } + + /** + * Handles insertion of a new global maximum, which must land after every + * currently occupied slot. Since there is no room to its right, this + * shifts occupied slots left into the nearest gap instead. + */ + private static void insertAtEnd(final int[] data, final boolean[] occupied, final int value) { + final int last = data.length - 1; + // occupied[last] is unreachable as false here: insertAtEnd() is only called when + // findInsertionIndex() returns data.length, which requires data[last] to already be + // occupied. Kept as a defensive guard in case that invariant is ever broken. + if (!occupied[last]) { + data[last] = value; + occupied[last] = true; + return; + } + int left = last - 1; + while (left >= 0 && occupied[left]) { + left--; + } + // left < 0 is unreachable in practice: at most n elements ever occupy this + // capacity-2n array, so fewer than half the slots left of `last` can be filled, + // guaranteeing a gap exists before the scan reaches index -1. + if (left < 0) { + throw new IllegalStateException("No gap available for insertion; rebalance too infrequent."); + } + // Shift data[left + 1, last] one slot to the left, opening a gap at last. + // occupied[last] is untouched by the copy and was already true. + System.arraycopy(data, left + 1, data, left, last - left); + occupied[left] = true; + data[last] = value; + } + + /** + * Finds the leftmost index at which {@code value} can be inserted so + * that occupied slots remain sorted. Empty slots are compared using the + * value of the nearest occupied slot at or after them, which is a + * monotonic function of index and therefore safe to binary search over. + */ + private static int findInsertionIndex(final int[] data, final boolean[] occupied, final int value) { + int lo = 0; + int hi = data.length; + while (lo < hi) { + final int mid = lo + (hi - lo) / 2; + final int probe = nearestOccupiedValueAtOrAfter(data, occupied, mid); + if (probe != Integer.MAX_VALUE && probe <= value) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; + } + + private static int nearestOccupiedValueAtOrAfter(final int[] data, final boolean[] occupied, final int index) { + for (int i = index; i < data.length; i++) { + if (occupied[i]) { + return data[i]; + } + } + return Integer.MAX_VALUE; + } + + /** + * Redistributes the {@code filled} occupied elements evenly across the + * full capacity of {@code data}, restoring uniform gaps between them. + */ + private static void rebalance(final int[] data, final boolean[] occupied, final int filled) { + final int capacity = data.length; + final int[] temp = new int[filled]; + int idx = 0; + for (int i = 0; i < capacity; i++) { + if (occupied[i]) { + temp[idx++] = data[i]; + } + } + Arrays.fill(occupied, false); + for (int k = 0; k < filled; k++) { + final int pos = (int) ((long) k * capacity / filled); + data[pos] = temp[k]; + occupied[pos] = true; + } + } +} diff --git a/src/main/java/com/thealgorithms/sorts/LinkListSort.java b/src/main/java/com/thealgorithms/sorts/LinkListSort.java index d8fd76a86236..4af789f1e460 100644 --- a/src/main/java/com/thealgorithms/sorts/LinkListSort.java +++ b/src/main/java/com/thealgorithms/sorts/LinkListSort.java @@ -13,7 +13,7 @@ public static boolean isSorted(int[] p, int option) { int[] b = p; // array similar to a int ch = option; - // Choice is choosed as any number from 1 to 3 (So the linked list will be + // Choice is chosen as any number from 1 to 3 (So the linked list will be // sorted by Merge sort technique/Insertion sort technique/Heap sort technique) switch (ch) { case 1: @@ -176,7 +176,7 @@ int count(Node head) { } return c; // This Method is used to count number of elements/nodes present in the linklist - // It will return a integer type value denoting the number of nodes present + // It will return an integer type value denoting the number of nodes present } void task(int[] n, int i, int j) { @@ -252,7 +252,7 @@ static int count(Node head) { } return c; // This Method is used to count number of elements/nodes present in the linklist - // It will return a integer type value denoting the number of nodes present + // It will return an integer type value denoting the number of nodes present } // The method task and task1 is used to sort the linklist using insertion sort } @@ -288,7 +288,7 @@ int count(Node head) { } return c; // This Method is used to count number of elements/nodes present in the linklist - // It will return a integer type value denoting the number of nodes present + // It will return an integer type value denoting the number of nodes present } void task(int[] n) { diff --git a/src/main/java/com/thealgorithms/sorts/MergeSort.java b/src/main/java/com/thealgorithms/sorts/MergeSort.java index 86a184f67b26..5db9c48b4f61 100644 --- a/src/main/java/com/thealgorithms/sorts/MergeSort.java +++ b/src/main/java/com/thealgorithms/sorts/MergeSort.java @@ -10,18 +10,23 @@ @SuppressWarnings("rawtypes") class MergeSort implements SortAlgorithm { - private Comparable[] aux; + private Comparable[] tempArray; /** - * Generic merge sort algorithm implements. + * Generic merge sort algorithm. * - * @param unsorted the array which should be sorted. - * @param Comparable class. - * @return sorted array. + * Time Complexity: + * - Best case: O(n log n) + * - Average case: O(n log n) + * - Worst case: O(n log n) + * + * Space Complexity: O(n) – requires auxiliary array for merging. + * + * @see SortAlgorithm */ @Override public > T[] sort(T[] unsorted) { - aux = new Comparable[unsorted.length]; + tempArray = new Comparable[unsorted.length]; doSort(unsorted, 0, unsorted.length - 1); return unsorted; } @@ -53,17 +58,17 @@ private > void doSort(T[] arr, int left, int right) { private > void merge(T[] arr, int left, int mid, int right) { int i = left; int j = mid + 1; - System.arraycopy(arr, left, aux, left, right + 1 - left); + System.arraycopy(arr, left, tempArray, left, right + 1 - left); for (int k = left; k <= right; k++) { if (j > right) { - arr[k] = (T) aux[i++]; + arr[k] = (T) tempArray[i++]; } else if (i > mid) { - arr[k] = (T) aux[j++]; - } else if (less(aux[j], aux[i])) { - arr[k] = (T) aux[j++]; + arr[k] = (T) tempArray[j++]; + } else if (less(tempArray[j], tempArray[i])) { + arr[k] = (T) tempArray[j++]; } else { - arr[k] = (T) aux[i++]; + arr[k] = (T) tempArray[i++]; } } } diff --git a/src/main/java/com/thealgorithms/sorts/PancakeSort.java b/src/main/java/com/thealgorithms/sorts/PancakeSort.java index 6079672a1d77..6522aefd7ae3 100644 --- a/src/main/java/com/thealgorithms/sorts/PancakeSort.java +++ b/src/main/java/com/thealgorithms/sorts/PancakeSort.java @@ -15,7 +15,7 @@ public > T[] sort(T[] array) { } for (int currentSize = 0; currentSize < array.length; currentSize++) { - int maxIndex = findMaxIndex(array, currentSize); + int maxIndex = findIndexOfMax(array, currentSize); SortUtils.flip(array, maxIndex, array.length - 1 - currentSize); } @@ -30,7 +30,7 @@ public > T[] sort(T[] array) { * @param the type of elements in the array * @return the index of the maximum element */ - private > int findMaxIndex(T[] array, int currentSize) { + private > int findIndexOfMax(T[] array, int currentSize) { T max = array[0]; int maxIndex = 0; for (int i = 0; i < array.length - currentSize; i++) { diff --git a/src/main/java/com/thealgorithms/sorts/QuickSort.java b/src/main/java/com/thealgorithms/sorts/QuickSort.java index 3abb1aae2306..b0ca8b9f159d 100644 --- a/src/main/java/com/thealgorithms/sorts/QuickSort.java +++ b/src/main/java/com/thealgorithms/sorts/QuickSort.java @@ -1,17 +1,36 @@ package com.thealgorithms.sorts; /** - * @author Varun Upadhyay (https://github.com/varunu28) - * @author Podshivalov Nikita (https://github.com/nikitap492) + * QuickSort is a divide-and-conquer sorting algorithm. + * + *

The algorithm selects a pivot element and partitions the array into two + * subarrays such that: + *

    + *
  • Elements smaller than the pivot are placed on the left
  • + *
  • Elements greater than the pivot are placed on the right
  • + *
+ * + *

The subarrays are then recursively sorted until the entire array is ordered. + * + *

This implementation uses randomization to reduce the probability of + * encountering worst-case performance on already sorted inputs. + * + *

Time Complexity: + *

    + *
  • Best Case: O(n log n)
  • + *
  • Average Case: O(n log n)
  • + *
  • Worst Case: O(n^2)
  • + *
+ * + *

Space Complexity: O(log n) due to recursion stack (in-place sorting). + * + * @author Varun Upadhyay + * @author Podshivalov Nikita * @see SortAlgorithm */ + class QuickSort implements SortAlgorithm { - /** - * This method implements the Generic Quick Sort - * - * @param array The array to be sorted Sorts the array in increasing order - */ @Override public > T[] sort(T[] array) { doSort(array, 0, array.length - 1); @@ -21,27 +40,33 @@ public > T[] sort(T[] array) { /** * The sorting process * - * @param left The first index of an array - * @param right The last index of an array * @param array The array to be sorted + * @param left The first index of an array + * @param right The last index of an array */ private static > void doSort(T[] array, final int left, final int right) { + // Continue sorting only if the subarray has more than one element if (left < right) { + // Randomly choose a pivot and partition the array final int pivot = randomPartition(array, left, right); + // Recursively sort elements before the pivot doSort(array, left, pivot - 1); + // Recursively sort elements after the pivot doSort(array, pivot, right); } } /** - * Randomize the array to avoid the basically ordered sequences + * Randomizes the array to avoid already ordered or nearly ordered sequences * * @param array The array to be sorted - * @param left The first index of an array + * @param left The first index of an array * @param right The last index of an array * @return the partition index of the array */ private static > int randomPartition(T[] array, final int left, final int right) { + // Randomizing the pivot helps avoid worst-case performance + // for already sorted or nearly sorted arrays final int randomIndex = left + (int) (Math.random() * (right - left + 1)); SortUtils.swap(array, randomIndex, right); return partition(array, left, right); @@ -51,21 +76,26 @@ private static > int randomPartition(T[] array, final in * This method finds the partition index for an array * * @param array The array to be sorted - * @param left The first index of an array - * @param right The last index of an array Finds the partition index of an - * array + * @param left The first index of an array + * @param right The last index of an array */ private static > int partition(T[] array, int left, int right) { final int mid = (left + right) >>> 1; + // Choose the middle element as the pivot final T pivot = array[mid]; - + // Move the left and right pointers towards each other while (left <= right) { + // Move left pointer until an element >= pivot is found while (SortUtils.less(array[left], pivot)) { ++left; } + + // Move right pointer until an element <= pivot is found while (SortUtils.less(pivot, array[right])) { --right; } + + // Swap elements that are on the wrong side of the pivot if (left <= right) { SortUtils.swap(array, left, right); ++left; diff --git a/src/main/java/com/thealgorithms/sorts/SelectionSort.java b/src/main/java/com/thealgorithms/sorts/SelectionSort.java index db7732d7e218..e6d1a16a7af7 100644 --- a/src/main/java/com/thealgorithms/sorts/SelectionSort.java +++ b/src/main/java/com/thealgorithms/sorts/SelectionSort.java @@ -1,16 +1,27 @@ package com.thealgorithms.sorts; +/** + * Implementation of the Selection Sort algorithm. + * + * @see SortAlgorithm + */ public class SelectionSort implements SortAlgorithm { /** - * Sorts an array of comparable elements in increasing order using the selection sort algorithm. + * Generic Selection Sort algorithm. + * + * Time Complexity: + * - Best case: O(n^2) + * - Average case: O(n^2) + * - Worst case: O(n^2) + * + * Space Complexity: O(1) – in-place sorting. * * @param array the array to be sorted - * @param the class of array elements + * @param the type of elements in the array * @return the sorted array */ @Override public > T[] sort(T[] array) { - for (int i = 0; i < array.length - 1; i++) { final int minIndex = findIndexOfMin(array, i); SortUtils.swap(array, i, minIndex); @@ -18,6 +29,14 @@ public > T[] sort(T[] array) { return array; } + /** + * Finds the index of the minimum element in the array starting from a given index. + * + * @param array the array to search + * @param startIndex the index to start searching from + * @param the type of elements in the array + * @return the index of the minimum element + */ private static > int findIndexOfMin(T[] array, final int startIndex) { int minIndex = startIndex; for (int i = startIndex + 1; i < array.length; i++) { diff --git a/src/main/java/com/thealgorithms/sorts/SmoothSort.java b/src/main/java/com/thealgorithms/sorts/SmoothSort.java new file mode 100644 index 000000000000..c45d6f1f02b2 --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/SmoothSort.java @@ -0,0 +1,168 @@ +package com.thealgorithms.sorts; + +/** + * Smooth Sort is an in-place, comparison-based sorting algorithm proposed by Edsger W. Dijkstra (1981). + * + *

It can be viewed as a variant of heapsort that maintains a forest of heap-ordered Leonardo trees + * (trees whose sizes are Leonardo numbers). The algorithm is adaptive: when the input is already + * sorted or nearly sorted, the heap invariants are often satisfied and the expensive rebalancing + * operations do little work, yielding near-linear behavior. + * + *

Time Complexity: + *

    + *
  • Best case: O(n) for already sorted input
  • + *
  • Average case: O(n log n)
  • + *
  • Worst case: O(n log n)
  • + *
+ * + *

Space Complexity: O(1) auxiliary space (in-place). + * + * @see Smoothsort + * @see Leonardo numbers + * @see SortAlgorithm + */ +public class SmoothSort implements SortAlgorithm { + + /** + * Leonardo numbers (L(0) = L(1) = 1, L(k+2) = L(k+1) + L(k) + 1) up to the largest value that + * fits into a signed 32-bit integer. + */ + private static final int[] LEONARDO = {1, 1, 3, 5, 9, 15, 25, 41, 67, 109, 177, 287, 465, 753, 1219, 1973, 3193, 5167, 8361, 13529, 21891, 35421, 57313, 92735, 150049, 242785, 392835, 635621, 1028457, 1664079, 2692537, 4356617, 7049155, 11405773, 18454929, 29860703, 48315633, 78176337, + 126491971, 204668309, 331160281, 535828591, 866988873, 1402817465}; + + /** + * Sorts the given array in ascending order using Smooth Sort. + * + * @param array the array to sort + * @param the element type + * @return the sorted array + */ + @Override + public > T[] sort(final T[] array) { + if (array.length < 2) { + return array; + } + + final int last = array.length - 1; + + // The forest shape is encoded as (p, pshift): p is a bit-vector of present tree orders, + // shifted right by pshift. pshift is the order of the rightmost (current) Leonardo tree. + long p = 1L; + int pshift = 1; + + int head = 0; + while (head < last) { + if ((p & 3L) == 3L) { + sift(array, pshift, head); + p >>>= 2; + pshift += 2; + } else { + // Add a new singleton tree; if it will not be merged anymore, we must fully trinkle. + if (LEONARDO[pshift - 1] >= last - head) { + trinkle(array, p, pshift, head, false); + } else { + // This tree will be merged later, so it is enough to restore its internal heap property. + sift(array, pshift, head); + } + + if (pshift == 1) { + // If L(1) is used, the new singleton is L(0). + p <<= 1; + pshift = 0; + } else { + // Otherwise, shift to order 1 and append a singleton of order 1. + p <<= (pshift - 1); + pshift = 1; + } + } + + p |= 1L; + head++; + } + + trinkle(array, p, pshift, head, false); + + // Repeatedly remove the maximum (always at head) by shrinking the heap region. + while (pshift != 1 || p != 1L) { + if (pshift <= 1) { + // Rightmost tree is a singleton (order 0 or 1). Move to the previous tree root. + final long mask = p & ~1L; + final int shift = Long.numberOfTrailingZeros(mask); + p >>>= shift; + pshift += shift; + } else { + // Split a tree of order (pshift) into two children trees of orders (pshift-1) and (pshift-2). + p <<= 2; + p ^= 7L; + pshift -= 2; + + trinkle(array, p >>> 1, pshift + 1, head - LEONARDO[pshift] - 1, true); + trinkle(array, p, pshift, head - 1, true); + } + + head--; + } + + return array; + } + + private static > void sift(final T[] array, int order, int root) { + final T value = array[root]; + + while (order > 1) { + final int right = root - 1; + final int left = root - 1 - LEONARDO[order - 2]; + + if (!SortUtils.less(value, array[left]) && !SortUtils.less(value, array[right])) { + break; + } + + if (!SortUtils.less(array[left], array[right])) { + array[root] = array[left]; + root = left; + order -= 1; + } else { + array[root] = array[right]; + root = right; + order -= 2; + } + } + + array[root] = value; + } + + private static > void trinkle(final T[] array, long p, int order, int root, boolean trusty) { + final T value = array[root]; + + while (p != 1L) { + final int stepson = root - LEONARDO[order]; + + if (!SortUtils.less(value, array[stepson])) { + break; + } + + if (!trusty && order > 1) { + final int right = root - 1; + final int left = root - 1 - LEONARDO[order - 2]; + + if (!SortUtils.less(array[right], array[stepson]) || !SortUtils.less(array[left], array[stepson])) { + break; + } + } + + array[root] = array[stepson]; + root = stepson; + + final long mask = p & ~1L; + final int shift = Long.numberOfTrailingZeros(mask); + p >>>= shift; + order += shift; + trusty = false; + } + + if (!trusty) { + array[root] = value; + sift(array, order, root); + } + } +} 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/main/java/com/thealgorithms/sorts/TopologicalSort.java b/src/main/java/com/thealgorithms/sorts/TopologicalSort.java index e4ed240a9947..382ddde9a6f2 100644 --- a/src/main/java/com/thealgorithms/sorts/TopologicalSort.java +++ b/src/main/java/com/thealgorithms/sorts/TopologicalSort.java @@ -11,9 +11,16 @@ * a linked list. A Directed Graph is proven to be acyclic when a DFS or Depth First Search is * performed, yielding no back-edges. * - * https://en.wikipedia.org/wiki/Topological_sorting + * Time Complexity: O(V + E) + * - V: number of vertices + * - E: number of edges * - * @author Jonathan Taylor (https://github.com/Jtmonument) + * Space Complexity: O(V + E) + * - adjacency list and recursion stack in DFS + * + * Reference: https://en.wikipedia.org/wiki/Topological_sorting + * + * Author: Jonathan Taylor (https://github.com/Jtmonument) * Based on Introduction to Algorithms 3rd Edition */ public final class TopologicalSort { diff --git a/src/main/java/com/thealgorithms/sorts/TournamentSort.java b/src/main/java/com/thealgorithms/sorts/TournamentSort.java new file mode 100644 index 000000000000..ec51a1e2c0a9 --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/TournamentSort.java @@ -0,0 +1,84 @@ +package com.thealgorithms.sorts; + +import java.util.Arrays; + +/** + * Tournament Sort algorithm implementation. + * + * Tournament sort builds a winner tree (a complete binary tree storing the index + * of the smallest element in each subtree). It then repeatedly extracts the + * winner (minimum) and updates the path from the removed leaf to the root. + * + * Time Complexity: + * - Best case: O(n log n) + * - Average case: O(n log n) + * - Worst case: O(n log n) + * + * Space Complexity: O(n) – additional winner-tree storage + * + * @see Tournament Sort Algorithm + * @see SortAlgorithm + */ +public class TournamentSort implements SortAlgorithm { + + @Override + public > T[] sort(T[] array) { + if (array == null || array.length < 2) { + return array; + } + + final int n = array.length; + final int leafCount = nextPowerOfTwo(n); + + // Winner tree represented as an array: + // - Leaves live at [leafCount .. 2*leafCount) + // - Internal nodes live at [1 .. leafCount) + // Each node stores an index into the original array or -1 for "empty". + final int[] tree = new int[2 * leafCount]; + Arrays.fill(tree, -1); + + for (int i = 0; i < n; i++) { + tree[leafCount + i] = i; + } + + for (int node = leafCount - 1; node >= 1; node--) { + tree[node] = winnerIndex(array, tree[node * 2], tree[node * 2 + 1]); + } + + final T[] result = array.clone(); + for (int out = 0; out < n; out++) { + final int winner = tree[1]; + result[out] = array[winner]; + + int node = leafCount + winner; + tree[node] = -1; + + for (node /= 2; node >= 1; node /= 2) { + tree[node] = winnerIndex(array, tree[node * 2], tree[node * 2 + 1]); + } + } + + System.arraycopy(result, 0, array, 0, n); + return array; + } + + private static int nextPowerOfTwo(int n) { + int power = 1; + while (power < n) { + power <<= 1; + } + return power; + } + + private static > int winnerIndex(T[] array, int leftIndex, int rightIndex) { + if (leftIndex == -1) { + return rightIndex; + } + if (rightIndex == -1) { + return leftIndex; + } + + // If equal, prefer the left element to keep ordering deterministic. + return SortUtils.less(array[rightIndex], array[leftIndex]) ? rightIndex : leftIndex; + } +} 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/main/java/com/thealgorithms/sorts/WiggleSort.java b/src/main/java/com/thealgorithms/sorts/WiggleSort.java index c272b820d07a..0349971d1c95 100644 --- a/src/main/java/com/thealgorithms/sorts/WiggleSort.java +++ b/src/main/java/com/thealgorithms/sorts/WiggleSort.java @@ -11,9 +11,9 @@ * https://cs.stackexchange.com/questions/125372/how-to-wiggle-sort-an-array-in-linear-time-complexity * Also have a look at: * https://cs.stackexchange.com/questions/125372/how-to-wiggle-sort-an-array-in-linear-time-complexity?noredirect=1&lq=1 - * Not all arrays are wiggle-sortable. This algorithm will find some obviously not wiggle-sortable - * arrays and throw an error, but there are some exceptions that won't be caught, for example [1, 2, - * 2]. + * Not all arrays are wiggle-sortable. This algorithm detects non-wiggle-sortable inputs β€” for + * example [1, 2, 2], or arrays where more than half the values are equal β€” and throws an + * IllegalArgumentException instead of returning a wrongly ordered result. */ public class WiggleSort implements SortAlgorithm { diff --git a/src/main/java/com/thealgorithms/stacks/PalindromeWithStack.java b/src/main/java/com/thealgorithms/stacks/PalindromeWithStack.java index 98c439341a21..7afe2c99aae8 100644 --- a/src/main/java/com/thealgorithms/stacks/PalindromeWithStack.java +++ b/src/main/java/com/thealgorithms/stacks/PalindromeWithStack.java @@ -8,6 +8,13 @@ * which we will pop one-by-one to create the string in reverse. * * Reference: https://www.geeksforgeeks.org/check-whether-the-given-string-is-palindrome-using-stack/ + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ public class PalindromeWithStack { private LinkedList stack; diff --git a/src/main/java/com/thealgorithms/stacks/StockSpanProblem.java b/src/main/java/com/thealgorithms/stacks/StockSpanProblem.java new file mode 100644 index 000000000000..2e9f6863c90a --- /dev/null +++ b/src/main/java/com/thealgorithms/stacks/StockSpanProblem.java @@ -0,0 +1,67 @@ +package com.thealgorithms.stacks; + +import java.util.Stack; + +/** + * Calculates the stock span for each day in a series of stock prices. + * + *

The span of a price on a given day is the number of consecutive days ending on that day + * for which the price was less than or equal to the current day's price. + * + *

Idea: keep a stack of indices whose prices are strictly greater than the current price. + * While processing each day, pop smaller or equal prices because they are part of the current + * span. After popping, the nearest greater price left on the stack tells us where the span stops. + * + *

Time complexity is O(n) because each index is pushed onto the stack once and popped at most + * once, so the total number of stack operations grows linearly with the number of prices. This + * makes the stack approach efficient because it avoids rechecking earlier days repeatedly, unlike + * a naive nested-loop solution that can take O(n^2) time. + * + *

Example: for prices [100, 80, 60, 70, 60, 75, 85], the spans are + * [1, 1, 1, 2, 1, 4, 6]. + */ +public final class StockSpanProblem { + private StockSpanProblem() { + } + + /** + * Calculates the stock span for each price in the input array. + * + * @param prices the stock prices + * @return the span for each day + * @throws IllegalArgumentException if the input array is null + */ + public static int[] calculateSpan(int[] prices) { + if (prices == null) { + throw new IllegalArgumentException("Input prices cannot be null"); + } + + int[] spans = new int[prices.length]; + Stack stack = new Stack<>(); + + // Small example: + // prices = [100, 80, 60, 70] + // spans = [ 1, 1, 1, 2] + // When we process 70, we pop 60 because 60 <= 70, so the span becomes 2. + // + // The stack stores indices of days with prices greater than the current day's price. + for (int index = 0; index < prices.length; index++) { + // Remove all previous days whose prices are less than or equal to the current price. + while (!stack.isEmpty() && prices[stack.peek()] <= prices[index]) { + stack.pop(); + } + + // If the stack is empty, there is no earlier day with a greater price, + // so the count will be from day 0 to this day (index + 1). + // + // Otherwise, the span is the number of days between + // the nearest earlier day with a greater price and the current day. + spans[index] = stack.isEmpty() ? index + 1 : index - stack.peek(); + + // Store the current index as a candidate for future span calculations. + stack.push(index); + } + + return spans; + } +} diff --git a/src/main/java/com/thealgorithms/stacks/ValidParentheses.java b/src/main/java/com/thealgorithms/stacks/ValidParentheses.java new file mode 100644 index 000000000000..2cc616a38826 --- /dev/null +++ b/src/main/java/com/thealgorithms/stacks/ValidParentheses.java @@ -0,0 +1,74 @@ +package com.thealgorithms.stacks; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +/** + * Valid Parentheses Problem + * + * Given a string containing just the characters '(', ')', '{', '}', '[' and ']', + * determine if the input string is valid. + * + * An input string is valid if: + * 1. Open brackets must be closed by the same type of brackets. + * 2. Open brackets must be closed in the correct order. + * 3. Every close bracket has a corresponding open bracket of the same type. + * + * Examples: + * Input: "()" + * Output: true + * + * Input: "()[]{}" + * Output: true + * + * Input: "(]" + * Output: false + * + * Input: "([)]" + * Output: false + * + * @author Gokul45-45 + */ +public final class ValidParentheses { + private ValidParentheses() { + } + + /** + * Checks if the given string has valid parentheses + * + * @param s the input string containing parentheses + * @return true if valid, false otherwise + */ + public static boolean isValid(String s) { + if (s == null || s.length() % 2 != 0) { + return false; + } + + Map parenthesesMap = new HashMap<>(); + parenthesesMap.put('(', ')'); + parenthesesMap.put('{', '}'); + parenthesesMap.put('[', ']'); + + Stack stack = new Stack<>(); + + for (char c : s.toCharArray()) { + if (parenthesesMap.containsKey(c)) { + // Opening bracket - push to stack + stack.push(c); + } else { + // Closing bracket - check if it matches + if (stack.isEmpty()) { + return false; + } + char openBracket = stack.pop(); + if (parenthesesMap.get(openBracket) != c) { + return false; + } + } + } + + // Stack should be empty if all brackets are matched + return stack.isEmpty(); + } +} diff --git a/src/main/java/com/thealgorithms/streaming/Adwin.java b/src/main/java/com/thealgorithms/streaming/Adwin.java new file mode 100644 index 000000000000..dc95d98461b6 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/Adwin.java @@ -0,0 +1,354 @@ +package com.thealgorithms.streaming; + +/** + * ADWIN, adaptive windowing, after Bifet and Gavalda: an average over a window whose length is + * not a parameter but a result. + * + *

Every windowed estimator forces the same bad choice. A long window is accurate while nothing + * changes and hopelessly slow once something does; a short one reacts immediately and is noisy the + * rest of the time. ADWIN refuses the choice: it keeps a window of recent values and, after every + * sample, looks for a way to split it into an old part and a recent part whose means are too far + * apart to be explained by chance. When it finds one, the old part is dropped. The window therefore + * grows on its own while the stream is stationary and collapses as soon as the stream moves, and the + * length it settles at is an estimate of how long the current regime has been running. + * + *

"Too far apart" is a variance sensitive Hoeffding bound. For a cut into sub-windows of + * {@code n0} and {@code n1} elements, with {@code v} the variance of the whole window: + * + *

+ * m       = 1 / (n0 - minLength + 1) + 1 / (n1 - minLength + 1)
+ * d       = ln( 2 * ln(width) / delta )
+ * epsilon = sqrt(2 * m * v * d) + 2/3 * d * m
+ * cut when |mean0 - mean1| > epsilon
+ * 
+ * + *

The {@code delta} parameter is a confidence level: the probability of cutting a window that + * never changed is bounded by it, which is the guarantee that makes the window length trustworthy. + * Smaller values make the detector more conservative and slower. + * + *

Keeping every sample would cost O(n) memory, so the window is stored as an exponential + * histogram: buckets of 1, 2, 4, 8 ... elements, at most {@code MAX_BUCKETS} of each size, each + * holding the sum and the variance of the elements it covers. That is O(log n) buckets for a window + * of n elements, and cuts are only tried at bucket boundaries, which is what keeps a sample O(log n) + * instead of O(n) while costing only a bounded loss of resolution. + * + *

Usage

+ * + *
{@code
+ * Adwin window = new Adwin(0.002);
+ * for (double sample : stream) {
+ *     if (window.accept(sample)) {
+ *         alert(window.estimate(), window.width());
+ *     }
+ * }
+ * }
+ * + *

This class is not thread-safe. + * + * @see CusumDetector + * @see Concept drift, the problem ADWIN was written for; the algorithm is due to A. Bifet and R. Gavalda, Learning from Time-Changing Data with Adaptive Windowing, SDM 2007 + */ +public final class Adwin { + + /** Confidence level used when none is given. */ + public static final double DEFAULT_DELTA = 0.002; + + /** How many buckets of the same size the histogram holds before merging the two oldest. */ + public static final int MAX_BUCKETS = 5; + + private static final int MIN_SUBWINDOW = 5; + private static final long MIN_WIDTH_FOR_DETECTION = 2L * MIN_SUBWINDOW; + + private final double delta; + + private Row newest; + private Row oldest; + + private long width; + private double total; + private double variance; + private int bucketCount; + private long count; + private long changeCount; + + /** + * Creates a window with the customary confidence level of {@code 0.002}. + */ + public Adwin() { + this(DEFAULT_DELTA); + } + + /** + * Creates a window. + * + * @param delta the confidence level, in {@code (0, 1)}; smaller values cut less eagerly + * @throws IllegalArgumentException if {@code delta} is outside {@code (0, 1)} + */ + public Adwin(double delta) { + if (!(delta > 0.0) || !(delta < 1.0)) { + throw new IllegalArgumentException("The delta must lie in (0, 1), but was " + delta); + } + this.delta = delta; + start(); + } + + /** + * Feeds one sample into the window. + * + * @param value the incoming sample + * @return {@code true} if the window was cut, that is if the stream changed + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public boolean accept(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + count++; + insert(value); + return detectChange(); + } + + /** + * Runs the window over a whole signal. + * + * @param signal the samples to inspect + * @return a new array of the same length saying for every sample whether it cut the window + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public boolean[] scan(double[] signal) { + boolean[] cuts = new boolean[signal.length]; + for (int i = 0; i < signal.length; i++) { + cuts[i] = accept(signal[i]); + } + return cuts; + } + + /** + * Returns the current estimate of the level of the stream. + * + * @return the mean of the window, {@code 0} while the window is empty + */ + public double estimate() { + return width == 0 ? 0.0 : total / width; + } + + /** + * Returns the length of the window, which is how many recent samples the estimate rests on. + * + * @return the window width + */ + public long width() { + return width; + } + + /** + * Returns the variance of the samples inside the window. + * + * @return the window variance, {@code 0} while the window holds fewer than two samples + */ + public double variance() { + return width < 2 ? 0.0 : variance / width; + } + + /** + * Returns how many buckets the histogram holds, which grows like the logarithm of the width. + * + * @return the bucket count + */ + public int bucketCount() { + return bucketCount; + } + + /** + * Returns how many samples have been inspected since the last reset. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Returns how many times the window has been cut since the last reset. + * + * @return the number of detected changes + */ + public long changeCount() { + return changeCount; + } + + /** + * Returns the configured confidence level. + * + * @return the delta given at construction time + */ + public double delta() { + return delta; + } + + /** + * Empties the window. + */ + public void reset() { + start(); + count = 0; + changeCount = 0; + } + + @Override + public String toString() { + return "Adwin{width=" + width + ", estimate=" + estimate() + ", buckets=" + bucketCount + ", changes=" + changeCount + "}"; + } + + private void start() { + newest = new Row(0); + oldest = newest; + width = 0; + total = 0.0; + variance = 0.0; + bucketCount = 0; + } + + private void insert(double value) { + width++; + newest.add(value, 0.0); + bucketCount++; + if (width > 1) { + double deviation = value - total / (width - 1); + variance += (width - 1) * deviation * deviation / width; + } + total += value; + compress(); + } + + /** + * Merges the two oldest buckets of every row that has run out of room into one bucket of the next + * row, which is what keeps the number of buckets logarithmic in the width. + */ + private void compress() { + Row row = newest; + while (row != null && row.size > MAX_BUCKETS) { + if (row.older == null) { + row.older = new Row(row.level + 1); + row.older.newer = row; + oldest = row.older; + } + long size = 1L << row.level; + double firstMean = row.totals[0] / size; + double secondMean = row.totals[1] / size; + double merged = size * size * (firstMean - secondMean) * (firstMean - secondMean) / (size + size); + row.older.add(row.totals[0] + row.totals[1], row.variances[0] + row.variances[1] + merged); + row.removeOldest(2); + bucketCount--; + row = row.older; + } + } + + /** + * Tries every cut the histogram allows, from the oldest boundary inwards, and drops the oldest + * bucket whenever a cut is significant. Repeats until no cut is left. + * + * @return whether anything was dropped + */ + private boolean detectChange() { + boolean changed = false; + boolean searching = true; + + while (searching && width >= MIN_WIDTH_FOR_DETECTION) { + searching = false; + long oldWidth = 0; + double oldTotal = 0.0; + + outer: + for (Row row = oldest; row != null; row = row.newer) { + for (int bucket = 0; bucket < row.size; bucket++) { + if (row.newer == null && bucket == row.size - 1) { + break outer; + } + oldWidth += 1L << row.level; + oldTotal += row.totals[bucket]; + long recentWidth = width - oldWidth; + double recentTotal = total - oldTotal; + if (recentWidth < MIN_SUBWINDOW) { + break outer; + } + if (oldWidth >= MIN_SUBWINDOW && isSignificant(oldWidth, recentWidth, oldTotal / oldWidth - recentTotal / recentWidth)) { + changed = true; + searching = true; + changeCount++; + dropOldestBucket(); + break outer; + } + } + } + } + return changed; + } + + private boolean isSignificant(long oldWidth, long recentWidth, double difference) { + double harmonic = 1.0 / (oldWidth - MIN_SUBWINDOW + 1) + 1.0 / (recentWidth - MIN_SUBWINDOW + 1); + double confidence = Math.log(2.0 * Math.log(width) / delta); + double windowVariance = variance / width; + double epsilon = Math.sqrt(2.0 * harmonic * windowVariance * confidence) + 2.0 / 3.0 * confidence * harmonic; + return Math.abs(difference) > epsilon; + } + + private void dropOldestBucket() { + Row row = oldest; + long size = 1L << row.level; + double bucketTotal = row.totals[0]; + + width -= size; + total -= bucketTotal; + if (width > 0) { + double bucketMean = bucketTotal / size; + double difference = bucketMean - total / width; + variance -= row.variances[0] + size * width * difference * difference / (size + width); + } else { + variance = 0.0; + } + if (variance < 0.0) { + variance = 0.0; + } + + row.removeOldest(1); + bucketCount--; + if (row.size == 0 && row.newer != null) { + oldest = row.newer; + oldest.older = null; + } + } + + /** + * One row of the exponential histogram: up to {@code MAX_BUCKETS + 1} buckets that each cover + * {@code 2^level} samples, the oldest at index zero. + */ + private static final class Row { + + private final int level; + private final double[] totals = new double[MAX_BUCKETS + 1]; + private final double[] variances = new double[MAX_BUCKETS + 1]; + private int size; + private Row older; + private Row newer; + + Row(int level) { + this.level = level; + } + + void add(double total, double variance) { + totals[size] = total; + variances[size] = variance; + size++; + } + + void removeOldest(int buckets) { + for (int i = buckets; i < size; i++) { + totals[i - buckets] = totals[i]; + variances[i - buckets] = variances[i]; + } + size -= buckets; + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/ComplementaryFilter.java b/src/main/java/com/thealgorithms/streaming/ComplementaryFilter.java new file mode 100644 index 000000000000..c4dac5799d79 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/ComplementaryFilter.java @@ -0,0 +1,239 @@ +package com.thealgorithms.streaming; + +/** + * The complementary filter: one estimate out of two sensors that are each wrong in a + * different way. + * + *

The classic pair is an accelerometer and a gyroscope measuring the same tilt. The accelerometer + * knows where down is and never drifts, but every vibration of the frame shows up in it. The + * gyroscope is smooth and immune to vibration, but it measures a rate, so using it means integrating, + * and the smallest bias in that rate integrates into an angle that walks away without limit. Neither + * is usable alone; their errors live in different parts of the spectrum, which is exactly the + * situation this filter is for. + * + *

+ * value <- a * (value + rate * dt) + (1 - a) * reference
+ * 
+ * + *

Read as a pair of filters that add up to one, it is a high pass on the integrated rate and a low + * pass on the absolute reading: the drift of the first is cut off below the corner frequency and the + * noise of the second above it. The two transfer functions sum to unity at every frequency, so the + * true signal passes through untouched whatever {@code a} is. That is where the name comes from, and + * it is also why the filter cannot introduce a lag of its own the way a plain low pass on the + * accelerometer would. + * + *

The single parameter is best thought of as a time constant rather than as a number near one: + * + *

+ * tau = a * dt / (1 - a)
+ * 
+ * + *

Below {@code tau} the answer comes from the gyroscope, above it from the accelerometer. That + * also fixes the price of the trade exactly: a gyroscope with a constant bias {@code b} leaves a + * steady state error of {@code tau * b} and no more, where plain integration would have grown without + * limit. Use {@link #ofTimeConstant(double, double)} to set it that way round. + * + *

Against {@link KalmanFilter}: the Kalman filter is the right answer when the noise of both + * sensors is known and worth modelling, and it will beat this one when it is. The complementary + * filter needs no covariance, no model of the process, two multiplications per sample and one number + * of state, and it degrades gracefully when the noise is not what anybody assumed. That is why it is + * what actually runs on small flight controllers. + * + *

Usage

+ * + *
{@code
+ * ComplementaryFilter tilt = ComplementaryFilter.ofTimeConstant(0.5, 0.01);
+ * for (Reading reading : imu) {
+ *     double angle = tilt.accept(reading.gyroscopeRate(), reading.accelerometerAngle(), reading.dt());
+ * }
+ * }
+ * + *

Each sample costs O(1) time and the filter keeps one number of state. This class is not + * thread-safe. + * + * @see KalmanFilter + * @see Complementary filter + */ +public final class ComplementaryFilter { + + /** Weight given to the integrated rate when none is chosen, the usual setting for an IMU. */ + public static final double DEFAULT_COEFFICIENT = 0.98; + + private final double coefficient; + + private double value; + private long count; + + /** + * Creates a filter that leans on the rate with the customary weight of {@code 0.98}. + */ + public ComplementaryFilter() { + this(DEFAULT_COEFFICIENT); + } + + /** + * Creates a filter. + * + * @param coefficient how much of the estimate comes from the integrated rate, in {@code (0, 1)}; + * closer to one trusts the rate for longer, closer to zero follows the reference more quickly + * @throws IllegalArgumentException if {@code coefficient} is outside {@code (0, 1)} + */ + public ComplementaryFilter(double coefficient) { + if (!(coefficient > 0.0) || !(coefficient < 1.0)) { + throw new IllegalArgumentException("The coefficient must lie in (0, 1), but was " + coefficient); + } + this.coefficient = coefficient; + } + + /** + * Creates a filter from the time constant that separates the two sensors, which is usually the + * quantity that is actually known: {@code a = tau / (tau + dt)}. + * + * @param timeConstant how long the rate is trusted before the reference takes over, strictly positive + * @param samplingInterval the interval between samples, strictly positive and in the same unit + * @return a new filter + * @throws IllegalArgumentException if either argument is not finite and strictly positive + */ + public static ComplementaryFilter ofTimeConstant(double timeConstant, double samplingInterval) { + requirePositive(timeConstant, "time constant"); + requirePositive(samplingInterval, "sampling interval"); + return new ComplementaryFilter(timeConstant / (timeConstant + samplingInterval)); + } + + /** + * Feeds one pair of readings taken one unit of time after the previous one. + * + * @param rate the reading of the drifting sensor, a derivative of the estimated quantity + * @param reference the reading of the noisy but drift free sensor, in the unit of the estimate + * @return the updated estimate + * @throws IllegalArgumentException if a reading is NaN or infinite + */ + public double accept(double rate, double reference) { + return accept(rate, reference, 1.0); + } + + /** + * Feeds one pair of readings. + * + * @param rate the reading of the drifting sensor, a derivative of the estimated quantity + * @param reference the reading of the noisy but drift free sensor, in the unit of the estimate + * @param elapsed time since the previous pair, strictly positive + * @return the updated estimate; the very first pair is answered with the reference alone, because + * there is nothing yet to integrate from + * @throws IllegalArgumentException if a reading is NaN or infinite, or if {@code elapsed} is not + * finite and strictly positive + */ + public double accept(double rate, double reference, double elapsed) { + requireFinite(rate, "rate"); + requireFinite(reference, "reference"); + requirePositive(elapsed, "elapsed time"); + + if (count == 0) { + value = reference; + } else { + value = coefficient * (value + rate * elapsed) + (1.0 - coefficient) * reference; + } + count++; + return value; + } + + /** + * Runs the filter over a whole pair of recordings sampled at unit intervals. + * + * @param rates the readings of the drifting sensor + * @param references the readings of the drift free sensor, as many as there are rates + * @return a new array of the same length holding the estimate after every sample + * @throws IllegalArgumentException if the two recordings differ in length or hold a reading that + * is NaN or infinite + * @throws NullPointerException if a recording is {@code null} + */ + public double[] scan(double[] rates, double[] references) { + if (rates.length != references.length) { + throw new IllegalArgumentException("Every rate needs a reference, but there were " + rates.length + " and " + references.length); + } + double[] estimates = new double[rates.length]; + for (int i = 0; i < rates.length; i++) { + estimates[i] = accept(rates[i], references[i]); + } + return estimates; + } + + /** + * Returns the current estimate. + * + * @return the estimate after the last pair of readings, {@code 0} before the first one + */ + public double value() { + return value; + } + + /** + * Returns the weight given to the integrated rate. + * + * @return the coefficient given at construction time + */ + public double coefficient() { + return coefficient; + } + + /** + * Returns the time constant the filter works out to at a given sampling interval, that is + * {@code a * dt / (1 - a)}: the horizon below which the rate decides the answer and above which + * the reference does. + * + * @param samplingInterval the interval between samples, strictly positive + * @return the time constant, in the unit of the interval + * @throws IllegalArgumentException if {@code samplingInterval} is not finite and strictly positive + */ + public double timeConstant(double samplingInterval) { + requirePositive(samplingInterval, "sampling interval"); + return coefficient * samplingInterval / (1.0 - coefficient); + } + + /** + * Returns how many pairs of readings have been filtered since the last reset. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Forgets everything seen so far, so that the next reference seeds the estimate again. + */ + public void reset() { + value = 0.0; + count = 0; + } + + /** + * Restarts the filter from a known estimate, which is what to do after the process has been moved + * by something the sensors could not see. + * + * @param estimate the value to carry on from + * @throws IllegalArgumentException if {@code estimate} is NaN or infinite + */ + public void reset(double estimate) { + requireFinite(estimate, "estimate"); + value = estimate; + count = 1; + } + + @Override + public String toString() { + return "ComplementaryFilter{coefficient=" + coefficient + ", value=" + value + ", samples=" + count + "}"; + } + + private static void requireFinite(double value, String name) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but was " + value); + } + } + + private static void requirePositive(double value, String name) { + if (!(value > 0.0) || !Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite and strictly positive, but was " + value); + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/CusumDetector.java b/src/main/java/com/thealgorithms/streaming/CusumDetector.java new file mode 100644 index 000000000000..618edb25a9af --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/CusumDetector.java @@ -0,0 +1,254 @@ +package com.thealgorithms.streaming; + +/** + * CUSUM (cumulative sum) change detection: it notices that the level of a stream has shifted, + * long before the shift is visible in a moving average. + * + *

A threshold on the raw samples can only catch a change that is large compared with the noise. + * CUSUM instead accumulates evidence. Every sample is normalised into + * {@code z = (x - target) / sigma} and pushed into two one-sided sums: + * + *

+ * upper <- max(0, upper + z - allowance)
+ * lower <- max(0, lower - z - allowance)
+ * 
+ * + *

The allowance {@code k} is a toll paid on every step. While the stream sits at its + * target the toll exceeds the average evidence, both sums are pinned at zero and the detector stays + * quiet no matter how long it runs. As soon as the mean shifts by more than {@code k} standard + * deviations, the corresponding sum starts drifting upwards, and it keeps drifting: a small but + * persistent bias accumulates until it crosses the decision threshold {@code h}. That is the whole + * point of the method - a shift of half a standard deviation is invisible in any single sample, yet + * unmistakable after twenty of them. + * + *

The classic tuning is {@code k = delta / 2} for the shift size {@code delta} one wants to catch + * quickly, together with {@code h} between 4 and 5, which keeps false alarms rare while detecting a + * one sigma shift within roughly ten samples. Both sums are cleared whenever an alarm fires, so the + * detector immediately starts looking for the next change instead of latching. + * + *

Usage

+ * + *
{@code
+ * // Watch for a shift of one standard deviation around a target of 20.0.
+ * CusumDetector detector = new CusumDetector(20.0, 0.5, 0.5, 5.0);
+ * for (double sample : stream) {
+ *     ShiftSignal signal = detector.accept(sample);
+ *     if (signal.isAlarm()) {
+ *         alert(signal, detector.count());
+ *     }
+ * }
+ * }
+ * + *

Each sample costs O(1) time and the detector keeps O(1) state. This class is not thread-safe. + * + * @see CUSUM + */ +public final class CusumDetector { + + /** Allowance used when none is given; it targets shifts of one standard deviation. */ + public static final double DEFAULT_ALLOWANCE = 0.5; + + /** Decision threshold used when none is given. */ + public static final double DEFAULT_THRESHOLD = 5.0; + + private final double target; + private final double standardDeviation; + private final double allowance; + private final double threshold; + + private double upperSum; + private double lowerSum; + private long count; + private long alarmCount; + private ShiftSignal lastSignal = ShiftSignal.NONE; + + /** + * Creates a detector tuned for shifts of about one standard deviation. + * + * @param target the level the stream is expected to sit at + * @param standardDeviation the noise level of the stream, strictly positive + * @throws IllegalArgumentException if {@code target} is not finite or {@code standardDeviation} is not strictly positive + */ + public CusumDetector(double target, double standardDeviation) { + this(target, standardDeviation, DEFAULT_ALLOWANCE, DEFAULT_THRESHOLD); + } + + /** + * Creates a detector. + * + * @param target the level the stream is expected to sit at + * @param standardDeviation the noise level of the stream, strictly positive + * @param allowance the toll subtracted on every step, in standard deviations; half of the shift + * size one wants to detect quickly + * @param threshold how much accumulated evidence raises an alarm, in standard deviations + * @throws IllegalArgumentException if any argument is not finite, if {@code standardDeviation} or + * {@code threshold} is not strictly positive, or if {@code allowance} is negative + */ + public CusumDetector(double target, double standardDeviation, double allowance, double threshold) { + requireFinite(target, "target"); + if (!(standardDeviation > 0.0) || !Double.isFinite(standardDeviation)) { + throw new IllegalArgumentException("The standard deviation must be finite and strictly positive, but was " + standardDeviation); + } + if (!(allowance >= 0.0) || !Double.isFinite(allowance)) { + throw new IllegalArgumentException("The allowance must be finite and non-negative, but was " + allowance); + } + if (!(threshold > 0.0) || !Double.isFinite(threshold)) { + throw new IllegalArgumentException("The threshold must be finite and strictly positive, but was " + threshold); + } + this.target = target; + this.standardDeviation = standardDeviation; + this.allowance = allowance; + this.threshold = threshold; + } + + /** + * Feeds one sample into the detector. + * + * @param value the incoming sample + * @return {@link ShiftSignal#NONE} while the stream stays in control, otherwise the direction of + * the detected shift; the accumulated sums are cleared on an alarm + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public ShiftSignal accept(double value) { + requireFinite(value, "sample"); + count++; + + double normalized = (value - target) / standardDeviation; + upperSum = Math.max(0.0, upperSum + normalized - allowance); + lowerSum = Math.max(0.0, lowerSum - normalized - allowance); + + if (upperSum > threshold) { + lastSignal = ShiftSignal.UPWARD; + } else if (lowerSum > threshold) { + lastSignal = ShiftSignal.DOWNWARD; + } else { + lastSignal = ShiftSignal.NONE; + } + + if (lastSignal.isAlarm()) { + alarmCount++; + upperSum = 0.0; + lowerSum = 0.0; + } + return lastSignal; + } + + /** + * Runs the detector over a whole signal. + * + * @param signal the samples to inspect + * @return a new array of the same length holding the verdict for every sample + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public ShiftSignal[] scan(double[] signal) { + ShiftSignal[] signals = new ShiftSignal[signal.length]; + for (int i = 0; i < signal.length; i++) { + signals[i] = accept(signal[i]); + } + return signals; + } + + /** + * Returns the evidence accumulated in favour of an upward shift. + * + * @return the one-sided upper sum, never negative + */ + public double upperSum() { + return upperSum; + } + + /** + * Returns the evidence accumulated in favour of a downward shift. + * + * @return the one-sided lower sum, never negative + */ + public double lowerSum() { + return lowerSum; + } + + /** + * Returns the verdict on the most recent sample. + * + * @return the last signal, {@link ShiftSignal#NONE} before the first sample + */ + public ShiftSignal lastSignal() { + return lastSignal; + } + + /** + * Returns how many samples have been inspected since the last reset. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Returns how many alarms have been raised since the last reset. + * + * @return the alarm count + */ + public long alarmCount() { + return alarmCount; + } + + /** + * Returns the expected level of the stream. + * + * @return the target given at construction time + */ + public double target() { + return target; + } + + /** + * Returns the assumed noise level. + * + * @return the standard deviation given at construction time + */ + public double standardDeviation() { + return standardDeviation; + } + + /** + * Returns the per-step allowance. + * + * @return the allowance given at construction time + */ + public double allowance() { + return allowance; + } + + /** + * Returns the decision threshold. + * + * @return the threshold given at construction time + */ + public double threshold() { + return threshold; + } + + /** + * Clears the accumulated evidence and the counters. + */ + public void reset() { + upperSum = 0.0; + lowerSum = 0.0; + count = 0; + alarmCount = 0; + lastSignal = ShiftSignal.NONE; + } + + @Override + public String toString() { + return "CusumDetector{target=" + target + ", upperSum=" + upperSum + ", lowerSum=" + lowerSum + ", alarms=" + alarmCount + '}'; + } + + private static void requireFinite(double value, String name) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but was " + value); + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/DynamicTimeWarping.java b/src/main/java/com/thealgorithms/streaming/DynamicTimeWarping.java new file mode 100644 index 000000000000..52e188cee2c5 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/DynamicTimeWarping.java @@ -0,0 +1,190 @@ +package com.thealgorithms.streaming; + +import java.util.Arrays; + +/** + * Dynamic time warping: how far apart two series are once one of them is allowed to be + * stretched and squeezed in time. + * + *

The Euclidean distance compares sample {@code i} with sample {@code i} and nothing else, so two + * recordings of the same gesture, one performed slightly faster, come out as far apart as two + * unrelated ones. Dynamic time warping instead looks for the cheapest way to line the two series up: + * every point of the first has to be matched to at least one point of the second and the other way + * round, the matching may never go backwards, and the cost is the sum over the matched pairs. That + * alignment is a shortest path through a grid, and the dynamic program is the obvious one: + * + *

+ * D[i][j] = |a[i] - b[j]| + min( D[i-1][j], D[i][j-1], D[i-1][j-1] )
+ * 
+ * + *

The three predecessors are exactly the three legal moves: consume a point of the first series, + * of the second, or of both. The answer is the bottom right corner. + * + *

Left alone, the alignment may match one point of a series against an arbitrarily long stretch of + * the other, which is rarely meaningful and costs {@code O(n * m)} regardless. The Sakoe-Chiba band + * forbids matches further apart in time than a given width, which both rules out those degenerate + * alignments and narrows the grid that has to be filled. The band has to be at least the difference + * in length, or no alignment exists at all. + * + *

Note that the result is not a metric: it does not satisfy the triangle inequality, so it can be + * used to rank candidates but not to index them without further care. + * + *

Usage

+ * + *
{@code
+ * double distance = DynamicTimeWarping.distance(query, candidate);
+ * double banded = DynamicTimeWarping.distance(query, candidate, 10);
+ * int[][] alignment = DynamicTimeWarping.path(query, candidate);
+ * }
+ * + *

The distance costs O(n * m) time and O(min(n, m)) memory; the path costs O(n * m) of both, + * because it has to remember the grid. + * + * @see Dynamic time warping + */ +public final class DynamicTimeWarping { + + private DynamicTimeWarping() { + } + + /** + * Returns the warping distance between two series. + * + * @param first the first series, left untouched + * @param second the second series, left untouched + * @return the cost of the cheapest alignment + * @throws IllegalArgumentException if a series is empty or holds a non-finite value + * @throws NullPointerException if a series is {@code null} + */ + public static double distance(double[] first, double[] second) { + return distance(first, second, Math.max(first.length, second.length)); + } + + /** + * Returns the warping distance between two series, with the alignment confined to a Sakoe-Chiba + * band. + * + * @param first the first series, left untouched + * @param second the second series, left untouched + * @param band how far apart in time two matched points may be, at least the difference in length + * @return the cost of the cheapest alignment inside the band + * @throws IllegalArgumentException if a series is empty or holds a non-finite value, or if the + * band is too narrow for any alignment to exist + * @throws NullPointerException if a series is {@code null} + */ + public static double distance(double[] first, double[] second, int band) { + requireSeries(first, "first"); + requireSeries(second, "second"); + requireBand(band, first.length, second.length); + + double[] previous = new double[second.length + 1]; + double[] current = new double[second.length + 1]; + Arrays.fill(previous, Double.POSITIVE_INFINITY); + previous[0] = 0.0; + + for (int i = 1; i <= first.length; i++) { + Arrays.fill(current, Double.POSITIVE_INFINITY); + int from = Math.max(1, i - band); + int to = Math.min(second.length, i + band); + for (int j = from; j <= to; j++) { + double cost = Math.abs(first[i - 1] - second[j - 1]); + double best = Math.min(previous[j], Math.min(current[j - 1], previous[j - 1])); + current[j] = cost + best; + } + double[] swap = previous; + previous = current; + current = swap; + } + return previous[second.length]; + } + + /** + * Returns the cheapest alignment itself. + * + * @param first the first series, left untouched + * @param second the second series, left untouched + * @return the matched pairs of indices, from {@code (0, 0)} to the two last indices + * @throws IllegalArgumentException if a series is empty or holds a non-finite value + * @throws NullPointerException if a series is {@code null} + */ + public static int[][] path(double[] first, double[] second) { + return path(first, second, Math.max(first.length, second.length)); + } + + /** + * Returns the cheapest alignment inside a Sakoe-Chiba band. + * + * @param first the first series, left untouched + * @param second the second series, left untouched + * @param band how far apart in time two matched points may be, at least the difference in length + * @return the matched pairs of indices, from {@code (0, 0)} to the two last indices + * @throws IllegalArgumentException if a series is empty or holds a non-finite value, or if the + * band is too narrow for any alignment to exist + * @throws NullPointerException if a series is {@code null} + */ + public static int[][] path(double[] first, double[] second, int band) { + requireSeries(first, "first"); + requireSeries(second, "second"); + requireBand(band, first.length, second.length); + + double[][] grid = new double[first.length + 1][second.length + 1]; + for (double[] row : grid) { + Arrays.fill(row, Double.POSITIVE_INFINITY); + } + grid[0][0] = 0.0; + + for (int i = 1; i <= first.length; i++) { + int from = Math.max(1, i - band); + int to = Math.min(second.length, i + band); + for (int j = from; j <= to; j++) { + double cost = Math.abs(first[i - 1] - second[j - 1]); + grid[i][j] = cost + Math.min(grid[i - 1][j], Math.min(grid[i][j - 1], grid[i - 1][j - 1])); + } + } + + int steps = 0; + int row = first.length; + int column = second.length; + int[][] reversed = new int[first.length + second.length][2]; + while (row > 0 && column > 0) { + reversed[steps][0] = row - 1; + reversed[steps][1] = column - 1; + steps++; + double diagonal = grid[row - 1][column - 1]; + double above = grid[row - 1][column]; + double left = grid[row][column - 1]; + if (diagonal <= above && diagonal <= left) { + row--; + column--; + } else if (above <= left) { + row--; + } else { + column--; + } + } + + int[][] alignment = new int[steps][2]; + for (int i = 0; i < steps; i++) { + alignment[i] = reversed[steps - 1 - i]; + } + return alignment; + } + + private static void requireSeries(double[] series, String name) { + if (series.length == 0) { + throw new IllegalArgumentException("The " + name + " series must not be empty"); + } + for (double value : series) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but the " + name + " series held " + value); + } + } + } + + private static void requireBand(int band, int firstLength, int secondLength) { + int minimum = Math.abs(firstLength - secondLength); + if (band < minimum) { + throw new IllegalArgumentException("The band must be at least the difference in length, " + minimum + ", but was " + band); + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/EwmaChangeDetector.java b/src/main/java/com/thealgorithms/streaming/EwmaChangeDetector.java new file mode 100644 index 000000000000..88d60da02988 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/EwmaChangeDetector.java @@ -0,0 +1,251 @@ +package com.thealgorithms.streaming; + +/** + * An EWMA control chart: change detection built on the exponentially weighted moving average. + * + *

The detector smooths the stream with + * {@link ExponentialMovingAverage} seeded at the target level and compares the smoothed value with a + * band around that target: + * + *

+ * z     <- z + alpha * (x - target)
+ * limit  = width * sigma * sqrt( alpha / (2 - alpha) * (1 - (1 - alpha)^(2n)) )
+ * 
+ * + *

The band is the exact standard deviation of {@code z} under the null hypothesis, multiplied by + * the desired width in sigmas. It starts narrow and widens towards its asymptote + * {@code width * sigma * sqrt(alpha / (2 - alpha))}, which keeps the false alarm rate steady during + * the warm-up instead of letting the first few samples trip the alarm. An alarm fires as soon as the + * smoothed value leaves the band; the average is then reset to the target so that the detector + * starts fresh on the next change rather than latching. + * + *

Where {@link CusumDetector} accumulates evidence without limit and so excels at small, + * persistent shifts, an EWMA chart looks at a decaying window of the recent past: {@code alpha} + * around {@code 0.1 - 0.3} is a good compromise, larger values reacting faster to big jumps and + * smaller ones being more sensitive to slow drifts. A width of 3 sigmas is the customary setting. + * + *

Usage

+ * + *
{@code
+ * EwmaChangeDetector detector = new EwmaChangeDetector(20.0, 0.5, 0.2, 3.0);
+ * for (double sample : stream) {
+ *     if (detector.accept(sample).isAlarm()) {
+ *         alert(detector.statistic(), detector.controlLimit());
+ *     }
+ * }
+ * }
+ * + *

Each sample costs O(1) time and the detector keeps O(1) state. This class is not thread-safe. + * + * @see CusumDetector + * @see ExponentialMovingAverage + * @see EWMA chart + */ +public final class EwmaChangeDetector { + + /** Smoothing factor used when none is given. */ + public static final double DEFAULT_ALPHA = 0.2; + + /** Half-width of the control band, in sigmas, used when none is given. */ + public static final double DEFAULT_WIDTH = 3.0; + + private final double target; + private final double standardDeviation; + private final double alpha; + private final double width; + + private final ExponentialMovingAverage average; + private long count; + private long stepsSinceAlarm; + private long alarmCount; + private ShiftSignal lastSignal = ShiftSignal.NONE; + + /** + * Creates a detector with the customary smoothing factor of {@code 0.2} and a band of three + * sigmas. + * + * @param target the level the stream is expected to sit at + * @param standardDeviation the noise level of the stream, strictly positive + * @throws IllegalArgumentException if {@code target} is not finite or {@code standardDeviation} is not strictly positive + */ + public EwmaChangeDetector(double target, double standardDeviation) { + this(target, standardDeviation, DEFAULT_ALPHA, DEFAULT_WIDTH); + } + + /** + * Creates a detector. + * + * @param target the level the stream is expected to sit at + * @param standardDeviation the noise level of the stream, strictly positive + * @param alpha smoothing factor in {@code (0, 1]}; larger values react faster but tolerate less noise + * @param width half-width of the control band in sigmas, strictly positive + * @throws IllegalArgumentException if any argument is not finite, if {@code standardDeviation} or + * {@code width} is not strictly positive, or if {@code alpha} is outside {@code (0, 1]} + */ + public EwmaChangeDetector(double target, double standardDeviation, double alpha, double width) { + if (!Double.isFinite(target)) { + throw new IllegalArgumentException("The target must be finite, but was " + target); + } + if (!(standardDeviation > 0.0) || !Double.isFinite(standardDeviation)) { + throw new IllegalArgumentException("The standard deviation must be finite and strictly positive, but was " + standardDeviation); + } + if (!(width > 0.0) || !Double.isFinite(width)) { + throw new IllegalArgumentException("The width must be finite and strictly positive, but was " + width); + } + this.target = target; + this.standardDeviation = standardDeviation; + this.width = width; + this.average = ExponentialMovingAverage.ofAlpha(alpha, target); + this.alpha = this.average.alpha(); + } + + /** + * Feeds one sample into the detector. + * + * @param value the incoming sample + * @return {@link ShiftSignal#NONE} while the smoothed value stays inside the control band, + * otherwise the direction in which it left the band; the average is reset to the target on an alarm + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public ShiftSignal accept(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + count++; + stepsSinceAlarm++; + double smoothed = average.add(value); + double deviation = smoothed - target; + double limit = controlLimit(); + + if (deviation > limit) { + lastSignal = ShiftSignal.UPWARD; + } else if (deviation < -limit) { + lastSignal = ShiftSignal.DOWNWARD; + } else { + lastSignal = ShiftSignal.NONE; + } + + if (lastSignal.isAlarm()) { + alarmCount++; + stepsSinceAlarm = 0; + average.reset(); + } + return lastSignal; + } + + /** + * Runs the detector over a whole signal. + * + * @param signal the samples to inspect + * @return a new array of the same length holding the verdict for every sample + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public ShiftSignal[] scan(double[] signal) { + ShiftSignal[] signals = new ShiftSignal[signal.length]; + for (int i = 0; i < signal.length; i++) { + signals[i] = accept(signal[i]); + } + return signals; + } + + /** + * Returns the smoothed value the alarm decision is based on. + * + * @return the exponentially weighted average of the stream + */ + public double statistic() { + return average.value(); + } + + /** + * Returns the current half-width of the control band, which widens during the warm-up and then + * settles. + * + * @return the distance from the target at which an alarm fires + */ + public double controlLimit() { + double asymptotic = alpha / (2.0 - alpha); + double warmUp = -Math.expm1(2.0 * stepsSinceAlarm * Math.log1p(-alpha)); + return width * standardDeviation * Math.sqrt(asymptotic * warmUp); + } + + /** + * Returns the verdict on the most recent sample. + * + * @return the last signal, {@link ShiftSignal#NONE} before the first sample + */ + public ShiftSignal lastSignal() { + return lastSignal; + } + + /** + * Returns how many samples have been inspected since the last reset. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Returns how many alarms have been raised since the last reset. + * + * @return the alarm count + */ + public long alarmCount() { + return alarmCount; + } + + /** + * Returns the expected level of the stream. + * + * @return the target given at construction time + */ + public double target() { + return target; + } + + /** + * Returns the assumed noise level. + * + * @return the standard deviation given at construction time + */ + public double standardDeviation() { + return standardDeviation; + } + + /** + * Returns the smoothing factor in use. + * + * @return alpha + */ + public double alpha() { + return alpha; + } + + /** + * Returns the configured band half-width. + * + * @return the width in sigmas given at construction time + */ + public double width() { + return width; + } + + /** + * Returns the average to the target and clears the counters. + */ + public void reset() { + average.reset(); + count = 0; + stepsSinceAlarm = 0; + alarmCount = 0; + lastSignal = ShiftSignal.NONE; + } + + @Override + public String toString() { + return "EwmaChangeDetector{target=" + target + ", statistic=" + statistic() + ", limit=" + controlLimit() + ", alarms=" + alarmCount + '}'; + } +} diff --git a/src/main/java/com/thealgorithms/streaming/ExponentialMovingAverage.java b/src/main/java/com/thealgorithms/streaming/ExponentialMovingAverage.java new file mode 100644 index 000000000000..ce97c6339c8c --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/ExponentialMovingAverage.java @@ -0,0 +1,233 @@ +package com.thealgorithms.streaming; + +/** + * Exponentially weighted moving average (EWMA) and, alongside it, the exponentially weighted + * variance of the same stream. + * + *

A plain moving average has to remember the whole window. An exponentially weighted one does + * not: every sample simply decays, so a single number carries the entire history. + * + *

+ * mean     <- mean + alpha * (x - mean)
+ * variance <- (1 - alpha) * (variance + alpha * (x - mean_before)^2)
+ * 
+ * + *

The smoothing factor {@code alpha} in {@code (0, 1]} sets how fast the past is forgotten: + * {@code alpha == 1} keeps only the latest sample, while a small {@code alpha} produces a smooth but + * sluggish estimate. It is usually easier to specify the responsiveness in terms of a window, which + * the factory methods do for you: + * + * + * + * + * + * + * + *
Ways to choose alpha
FactoryalphaMeaning
{@link #ofAlpha(double)}as givendirect control
{@link #ofSpan(double)}{@code 2 / (span + 1)}comparable to a simple moving average of {@code span} samples
{@link #ofHalfLife(double)}{@code 1 - exp(-ln2 / halfLife)}a sample loses half of its weight after {@code halfLife} steps
+ * + *

The average is seeded with the first sample, which avoids the warm-up bias that a zero seed + * would introduce. Pass an explicit seed to {@link #ofAlpha(double, double)} when the resting level + * of the signal is known in advance, as a control chart does. + * + *

Both the update and every query run in O(1) time and O(1) memory. This class is not + * thread-safe. + * + * @see EwmaChangeDetector + * @see Exponential moving average + */ +public final class ExponentialMovingAverage { + + private final double alpha; + private final double seed; + private final boolean seeded; + + private double mean; + private double variance; + private long count; + private boolean initialized; + + private ExponentialMovingAverage(double alpha, double seed, boolean seeded) { + if (!(alpha > 0.0) || alpha > 1.0) { + throw new IllegalArgumentException("The smoothing factor alpha must lie in (0, 1], but was " + alpha); + } + if (seeded && !Double.isFinite(seed)) { + throw new IllegalArgumentException("The seed must be finite, but was " + seed); + } + this.alpha = alpha; + this.seed = seed; + this.seeded = seeded; + reset(); + } + + /** + * Creates an average that is seeded with its first sample. + * + * @param alpha smoothing factor in {@code (0, 1]} + * @return a new average + * @throws IllegalArgumentException if {@code alpha} is outside {@code (0, 1]} + */ + public static ExponentialMovingAverage ofAlpha(double alpha) { + return new ExponentialMovingAverage(alpha, 0.0, false); + } + + /** + * Creates an average that starts from a known level instead of waiting for the first sample. + * + * @param alpha smoothing factor in {@code (0, 1]} + * @param seed the initial value of the average + * @return a new average + * @throws IllegalArgumentException if {@code alpha} is outside {@code (0, 1]} or {@code seed} is not finite + */ + public static ExponentialMovingAverage ofAlpha(double alpha, double seed) { + return new ExponentialMovingAverage(alpha, seed, true); + } + + /** + * Creates an average whose responsiveness matches a simple moving average of {@code span} + * samples, that is {@code alpha = 2 / (span + 1)}. + * + * @param span the equivalent window length, greater than or equal to one + * @return a new average + * @throws IllegalArgumentException if {@code span} is smaller than one or not finite + */ + public static ExponentialMovingAverage ofSpan(double span) { + if (!(span >= 1.0) || !Double.isFinite(span)) { + throw new IllegalArgumentException("The span must be finite and at least 1, but was " + span); + } + return ofAlpha(2.0 / (span + 1.0)); + } + + /** + * Creates an average in which a sample loses half of its weight after {@code halfLife} updates, + * that is {@code alpha = 1 - exp(-ln2 / halfLife)}. + * + * @param halfLife number of updates after which a weight is halved, strictly positive + * @return a new average + * @throws IllegalArgumentException if {@code halfLife} is not strictly positive or not finite + */ + public static ExponentialMovingAverage ofHalfLife(double halfLife) { + if (!(halfLife > 0.0) || !Double.isFinite(halfLife)) { + throw new IllegalArgumentException("The half-life must be finite and positive, but was " + halfLife); + } + return ofAlpha(-Math.expm1(-Math.log(2.0) / halfLife)); + } + + /** + * Incorporates one sample. + * + * @param value the sample to add + * @return the updated average + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public double add(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + count++; + if (!initialized) { + initialized = true; + mean = value; + variance = 0.0; + return mean; + } + double deviation = value - mean; + mean += alpha * deviation; + variance = (1.0 - alpha) * (variance + alpha * deviation * deviation); + return mean; + } + + /** + * Incorporates every given sample, in order. + * + * @param values the samples to add + * @return the average after the last sample + * @throws IllegalArgumentException if any value is NaN or infinite + * @throws IllegalStateException if {@code values} is empty and the average is not initialized yet + * @throws NullPointerException if {@code values} is {@code null} + */ + public double addAll(double... values) { + for (double value : values) { + add(value); + } + return value(); + } + + /** + * Returns the current average. + * + * @return the exponentially weighted mean + * @throws IllegalStateException if the average has neither a seed nor a sample yet + */ + public double value() { + if (!initialized) { + throw new IllegalStateException("The average has not seen any sample yet"); + } + return mean; + } + + /** + * Returns the exponentially weighted variance of the stream, the natural companion of + * {@link #value()} when the spread matters as much as the level. + * + * @return the weighted variance, {@code 0} until the second sample arrives + * @throws IllegalStateException if the average has neither a seed nor a sample yet + */ + public double variance() { + if (!initialized) { + throw new IllegalStateException("The average has not seen any sample yet"); + } + return variance; + } + + /** + * Returns the square root of {@link #variance()}. + * + * @return the weighted standard deviation + * @throws IllegalStateException if the average has neither a seed nor a sample yet + */ + public double standardDeviation() { + return Math.sqrt(variance()); + } + + /** + * Returns the smoothing factor in use. + * + * @return alpha + */ + public double alpha() { + return alpha; + } + + /** + * Returns the number of samples added since the last reset. + * + * @return the sample count, which excludes the seed + */ + public long count() { + return count; + } + + /** + * Tells whether {@link #value()} may be queried. + * + * @return {@code true} once a seed or at least one sample is available + */ + public boolean isInitialized() { + return initialized; + } + + /** + * Restores the state the average had right after construction. + */ + public void reset() { + count = 0; + variance = 0.0; + mean = seeded ? seed : 0.0; + initialized = seeded; + } + + @Override + public String toString() { + return "ExponentialMovingAverage{alpha=" + alpha + ", value=" + (initialized ? mean : Double.NaN) + ", count=" + count + '}'; + } +} diff --git a/src/main/java/com/thealgorithms/streaming/ExtendedKalmanFilter.java b/src/main/java/com/thealgorithms/streaming/ExtendedKalmanFilter.java new file mode 100644 index 000000000000..21ef27212a50 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/ExtendedKalmanFilter.java @@ -0,0 +1,422 @@ +package com.thealgorithms.streaming; + +import com.thealgorithms.matrix.InverseOfMatrix; +import java.util.Arrays; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * The extended Kalman filter: the Kalman filter for a process that does not move in straight + * lines. + * + *

The ordinary Kalman filter is optimal, but only for a linear model with Gaussian noise. Most + * things worth tracking are not linear: a bearing is an arctangent of the state, a range is a square + * root of it, an orbit is not a straight line. The extended filter keeps the whole machinery and + * replaces the one step that needs linearity. The state is still pushed forward through the real, + * nonlinear functions, but the covariance is pushed through their Jacobians, evaluated afresh + * at the current estimate: + * + *

+ * predict:  x <- f(x)                      P <- F P F' + Q,   F = df/dx at x
+ * update:   y  = z - h(x)                  S  = H P H' + R,   H = dh/dx at x
+ *           K  = P H' S^-1                 x <- x + K y
+ *                                          P <- (I - K H) P (I - K H)' + K R K'
+ * 
+ * + *

The covariance update is written in Joseph form rather than as the shorter + * {@code (I - K H) P}. The two are equal in exact arithmetic, but the short one subtracts two nearly + * equal matrices and, after a few thousand steps of rounding, can leave a covariance that is no + * longer symmetric or has a negative variance on its diagonal, at which point the filter quietly + * stops being a filter. The Joseph form is a sum of two products of the form {@code A P A'}, so it + * stays symmetric by construction and costs one extra multiplication. + * + *

What the linearisation costs is worth stating plainly: the filter is no longer optimal and no + * longer guaranteed to converge. A Jacobian is a good description of the model only near the point + * it was taken at, so a large error, a sharp nonlinearity or too long a step between updates can all + * make the estimate diverge, and the covariance will not warn about it, because the covariance was + * computed from the same linearisation. When that happens the answer is smaller steps, a better + * initial estimate, or the unscented variant, which samples the nonlinear function instead of + * differentiating it. + * + *

For a one dimensional linear process {@link KalmanFilter} is smaller, faster and exactly + * equivalent; this class earns its place once the state is a vector or the model bends. + * + *

Usage

+ * + *
{@code
+ * // a target moving at constant velocity, watched by a sensor that only measures range
+ * ExtendedKalmanFilter filter = new ExtendedKalmanFilter(new double[] {0.0, 1.0}, identity);
+ * filter.predict(new double[][] {{1.0, dt}, {0.0, 1.0}}, processNoise);
+ * filter.update(new double[] {range},
+ *               state -> new double[] {Math.hypot(state[0], offset)},
+ *               state -> new double[][] {{state[0] / Math.hypot(state[0], offset), 0.0}},
+ *               measurementNoise);
+ * }
+ * + *

A step costs O(n^3) in the size of the state, dominated by the matrix products, plus the + * inversion of the m by m innovation covariance, which is done by + * {@link InverseOfMatrix#invert(double[][])} rather than by another copy of Gaussian elimination. + * This class is not thread-safe. + * + * @see KalmanFilter + * @see Extended Kalman filter + */ +public final class ExtendedKalmanFilter { + + private final int stateSize; + + private double[] state; + private double[][] covariance; + private double[] innovation; + private double[][] gain; + private long predictions; + private long corrections; + + /** + * Creates a filter. + * + * @param initialState the first guess at the state, copied + * @param initialCovariance how uncertain that guess is, a square matrix of the same size, copied + * @throws IllegalArgumentException if the state is empty, if the covariance is not square and of + * the same size, or if either holds a value that is not finite + * @throws NullPointerException if an argument is {@code null} + */ + public ExtendedKalmanFilter(double[] initialState, double[][] initialCovariance) { + if (initialState.length == 0) { + throw new IllegalArgumentException("The state must not be empty"); + } + this.stateSize = initialState.length; + requireVector(initialState, stateSize, "initial state"); + requireMatrix(initialCovariance, stateSize, stateSize, "initial covariance"); + this.state = initialState.clone(); + this.covariance = copy(initialCovariance); + this.innovation = new double[0]; + this.gain = new double[0][0]; + } + + /** + * Pushes the estimate forward through a nonlinear model. + * + * @param transition the model itself, which maps a state to the state one step later + * @param jacobian the derivative of that model, evaluated by the filter at the current estimate + * @param processNoise how much the model is trusted, a square matrix of the size of the state + * @return a copy of the predicted state + * @throws IllegalArgumentException if the model returns the wrong shape or a value that is not + * finite, or if the process noise has the wrong shape + * @throws NullPointerException if an argument is {@code null} + */ + public double[] predict(UnaryOperator transition, Function jacobian, double[][] processNoise) { + requireMatrix(processNoise, stateSize, stateSize, "process noise"); + + double[][] derivative = jacobian.apply(state.clone()); + requireMatrix(derivative, stateSize, stateSize, "transition jacobian"); + double[] predicted = transition.apply(state.clone()); + requireVector(predicted, stateSize, "predicted state"); + + state = predicted; + covariance = add(multiply(multiply(derivative, covariance), transpose(derivative)), processNoise); + predictions++; + return state.clone(); + } + + /** + * Pushes the estimate forward through a linear model, which is the plain Kalman prediction. + * + * @param transition the transition matrix, a square matrix of the size of the state + * @param processNoise how much the model is trusted, a square matrix of the size of the state + * @return a copy of the predicted state + * @throws IllegalArgumentException if either matrix has the wrong shape or holds a value that is + * not finite + * @throws NullPointerException if an argument is {@code null} + */ + public double[] predict(double[][] transition, double[][] processNoise) { + requireMatrix(transition, stateSize, stateSize, "transition"); + return predict(current -> multiply(transition, current), current -> transition, processNoise); + } + + /** + * Folds one nonlinear measurement into the estimate. + * + * @param measurement what the sensor reported + * @param model the measurement model, which maps a state to what the sensor would report + * @param jacobian the derivative of that model, evaluated by the filter at the current estimate + * @param measurementNoise how much the sensor is trusted, a square matrix of the size of the + * measurement + * @return a copy of the corrected state + * @throws IllegalArgumentException if the measurement is empty, if the model returns the wrong + * shape or a value that is not finite, or if the noise has the wrong shape + * @throws ArithmeticException if the innovation covariance cannot be inverted, which means the + * measurement carries no information the filter can use + * @throws NullPointerException if an argument is {@code null} + */ + public double[] update(double[] measurement, Function model, Function jacobian, double[][] measurementNoise) { + if (measurement.length == 0) { + throw new IllegalArgumentException("The measurement must not be empty"); + } + int measurementSize = measurement.length; + requireVector(measurement, measurementSize, "measurement"); + requireMatrix(measurementNoise, measurementSize, measurementSize, "measurement noise"); + + double[] expected = model.apply(state.clone()); + requireVector(expected, measurementSize, "expected measurement"); + double[][] derivative = jacobian.apply(state.clone()); + requireMatrix(derivative, measurementSize, stateSize, "measurement jacobian"); + + double[] residual = new double[measurementSize]; + for (int i = 0; i < measurementSize; i++) { + residual[i] = measurement[i] - expected[i]; + } + + double[][] transposed = transpose(derivative); + double[][] crossCovariance = multiply(covariance, transposed); + double[][] innovationCovariance = add(multiply(derivative, crossCovariance), measurementNoise); + double[][] inverted = InverseOfMatrix.invert(copy(innovationCovariance)); + requireFiniteMatrix(inverted); + + gain = multiply(crossCovariance, inverted); + for (int i = 0; i < stateSize; i++) { + double correction = 0.0; + for (int j = 0; j < measurementSize; j++) { + correction += gain[i][j] * residual[j]; + } + state[i] += correction; + } + + // Joseph form: symmetric by construction, unlike the shorter (I - K H) P + double[][] spread = subtract(identity(stateSize), multiply(gain, derivative)); + covariance = add(multiply(multiply(spread, covariance), transpose(spread)), multiply(multiply(gain, measurementNoise), transpose(gain))); + + innovation = residual; + corrections++; + return state.clone(); + } + + /** + * Folds one linear measurement into the estimate, which is the plain Kalman correction. + * + * @param measurement what the sensor reported + * @param model the measurement matrix, with one row per measurement and one column per state + * @param measurementNoise how much the sensor is trusted, a square matrix of the size of the + * measurement + * @return a copy of the corrected state + * @throws IllegalArgumentException if a shape is wrong or a value is not finite + * @throws ArithmeticException if the innovation covariance cannot be inverted + * @throws NullPointerException if an argument is {@code null} + */ + public double[] update(double[] measurement, double[][] model, double[][] measurementNoise) { + requireMatrix(model, measurement.length, stateSize, "measurement model"); + return update(measurement, current -> multiply(model, current), current -> model, measurementNoise); + } + + /** + * Returns the current estimate. + * + * @return a copy of the state + */ + public double[] state() { + return state.clone(); + } + + /** + * Returns how uncertain the current estimate is. + * + * @return a copy of the covariance + */ + public double[][] covariance() { + return copy(covariance); + } + + /** + * Returns the variance of one component of the state, which is what a caller usually wants out of + * the covariance. + * + * @param component which component, from zero to the size of the state + * @return the variance of that component + * @throws IllegalArgumentException if {@code component} is outside the state + */ + public double variance(int component) { + if (component < 0 || component >= stateSize) { + throw new IllegalArgumentException("The component must lie in [0, " + (stateSize - 1) + "], but was " + component); + } + return covariance[component][component]; + } + + /** + * Returns the last innovation, that is how far the sensor was from what the filter expected. A + * run of innovations that are not centred on zero means the model no longer describes the + * process. + * + * @return a copy of the last residual, empty before the first update + */ + public double[] lastInnovation() { + return innovation.clone(); + } + + /** + * Returns the gain the last update applied. + * + * @return a copy of the last Kalman gain, empty before the first update + */ + public double[][] lastGain() { + return copy(gain); + } + + /** + * Returns the size of the state. + * + * @return how many components the state holds + */ + public int stateSize() { + return stateSize; + } + + /** + * Returns how many predictions have been made since the last reset. + * + * @return the prediction count + */ + public long predictionCount() { + return predictions; + } + + /** + * Returns how many measurements have been folded in since the last reset. + * + * @return the update count + */ + public long updateCount() { + return corrections; + } + + /** + * Restarts the filter from a known estimate. + * + * @param newState the state to carry on from, copied + * @param newCovariance how uncertain it is, copied + * @throws IllegalArgumentException if a shape is wrong or a value is not finite + * @throws NullPointerException if an argument is {@code null} + */ + public void reset(double[] newState, double[][] newCovariance) { + requireVector(newState, stateSize, "state"); + requireMatrix(newCovariance, stateSize, stateSize, "covariance"); + state = newState.clone(); + covariance = copy(newCovariance); + innovation = new double[0]; + gain = new double[0][0]; + predictions = 0; + corrections = 0; + } + + @Override + public String toString() { + return "ExtendedKalmanFilter{state=" + Arrays.toString(state) + ", predictions=" + predictions + ", updates=" + corrections + "}"; + } + + private static void requireVector(double[] vector, int size, String name) { + if (vector.length != size) { + throw new IllegalArgumentException("The " + name + " must hold " + size + " values, but held " + vector.length); + } + for (double value : vector) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but held " + value); + } + } + } + + private static void requireMatrix(double[][] matrix, int rows, int columns, String name) { + if (matrix.length != rows) { + throw new IllegalArgumentException("The " + name + " must have " + rows + " rows, but had " + matrix.length); + } + for (double[] row : matrix) { + if (row.length != columns) { + throw new IllegalArgumentException("The " + name + " must have " + columns + " columns, but a row had " + row.length); + } + for (double value : row) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but held " + value); + } + } + } + } + + private static void requireFiniteMatrix(double[][] matrix) { + for (double[] row : matrix) { + for (double value : row) { + if (!Double.isFinite(value)) { + throw new ArithmeticException("The innovation covariance is singular, so the gain is undefined"); + } + } + } + } + + private static double[][] copy(double[][] matrix) { + double[][] copied = new double[matrix.length][]; + for (int i = 0; i < matrix.length; i++) { + copied[i] = matrix[i].clone(); + } + return copied; + } + + private static double[][] identity(int size) { + double[][] result = new double[size][size]; + for (int i = 0; i < size; i++) { + result[i][i] = 1.0; + } + return result; + } + + private static double[][] transpose(double[][] matrix) { + double[][] result = new double[matrix[0].length][matrix.length]; + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + result[j][i] = matrix[i][j]; + } + } + return result; + } + + private static double[][] multiply(double[][] left, double[][] right) { + double[][] result = new double[left.length][right[0].length]; + for (int i = 0; i < left.length; i++) { + for (int k = 0; k < right.length; k++) { + double factor = left[i][k]; + for (int j = 0; j < right[0].length; j++) { + result[i][j] += factor * right[k][j]; + } + } + } + return result; + } + + private static double[] multiply(double[][] matrix, double[] vector) { + double[] result = new double[matrix.length]; + for (int i = 0; i < matrix.length; i++) { + double sum = 0.0; + for (int j = 0; j < vector.length; j++) { + sum += matrix[i][j] * vector[j]; + } + result[i] = sum; + } + return result; + } + + private static double[][] add(double[][] left, double[][] right) { + double[][] result = new double[left.length][left[0].length]; + for (int i = 0; i < left.length; i++) { + for (int j = 0; j < left[i].length; j++) { + result[i][j] = left[i][j] + right[i][j]; + } + } + return result; + } + + private static double[][] subtract(double[][] left, double[][] right) { + double[][] result = new double[left.length][left[0].length]; + for (int i = 0; i < left.length; i++) { + for (int j = 0; j < left[i].length; j++) { + result[i][j] = left[i][j] - right[i][j]; + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/streaming/GeneralizedEsd.java b/src/main/java/com/thealgorithms/streaming/GeneralizedEsd.java new file mode 100644 index 000000000000..f4fee3a90a34 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/GeneralizedEsd.java @@ -0,0 +1,334 @@ +package com.thealgorithms.streaming; + +import java.util.Arrays; + +/** + * The generalized extreme Studentized deviate test of Rosner: finds up to {@code r} outliers + * in a sample, without being told how many are there. + * + *

Grubbs' test asks whether the single most extreme point is an outlier, and it breaks as soon as + * there are two of them: each one inflates the standard deviation the other is measured against, so + * a pair of outliers can hide one another completely. This is the masking problem, and the + * generalized test is the standard answer to it. It removes the most extreme point, recomputes the + * statistics on what is left, and repeats {@code r} times: + * + *

+ * R_i      = max |x - mean| / sd            over the points that are still in
+ * lambda_i = (n - i) * t / sqrt( (n - i - 1 + t^2)(n - i + 1) )
+ * where t  = the 1 - alpha / (2(n - i + 1)) quantile of a t distribution with n - i - 1 degrees of freedom
+ * 
+ * + *

The number of outliers is the largest {@code i} whose statistic exceeds its critical + * value, not the first one. That is what defeats masking: in a sample with three outliers the first + * two tests can easily fall short while the third one succeeds, and the test then reports all three. + * + *

This is a batch test and it needs the whole sample, which is exactly the trade it makes against + * the streaming detectors in this package: it has a stated significance level and it decides how many + * points are outliers, where {@link HampelFilter} answers one sample at a time against a threshold the + * caller has to choose. Rosner's own recommendation is to use it on at least 25 points, and {@code r} + * is an upper bound that may be set generously, because overstating it costs accuracy only in extreme + * cases. + * + *

Usage

+ * + *
{@code
+ * GeneralizedEsd test = new GeneralizedEsd(0.05);
+ * int[] outliers = test.findOutliers(sample, 10);
+ * }
+ * + *

The test costs O(r * n) time and allocates O(n). The t quantile behind the critical value is + * computed from the regularized incomplete beta function, so no table is needed. + * + * @see HampelFilter + * @see Grubbs's test and its generalization + */ +public final class GeneralizedEsd { + + /** Significance level used when none is given. */ + public static final double DEFAULT_SIGNIFICANCE_LEVEL = 0.05; + + private static final int MINIMUM_SAMPLE_SIZE = 3; + private static final double[] LANCZOS = { + 0.99999999999980993, + 676.5203681218851, + -1259.1392167224028, + 771.32342877765313, + -176.61502916214059, + 12.507343278686905, + -0.13857109526572012, + 9.9843695780195716e-6, + 1.5056327351493116e-7, + }; + + private final double significanceLevel; + + /** + * Creates a test at the customary significance level of {@code 0.05}. + */ + public GeneralizedEsd() { + this(DEFAULT_SIGNIFICANCE_LEVEL); + } + + /** + * Creates a test. + * + * @param significanceLevel the probability of declaring an outlier in clean data, in {@code (0, 1)} + * @throws IllegalArgumentException if {@code significanceLevel} is not inside {@code (0, 1)} + */ + public GeneralizedEsd(double significanceLevel) { + if (!(significanceLevel > 0.0) || !(significanceLevel < 1.0)) { + throw new IllegalArgumentException("The significance level must lie in (0, 1), but was " + significanceLevel); + } + this.significanceLevel = significanceLevel; + } + + /** + * Finds the outliers of a sample. + * + * @param sample the data to inspect, left untouched + * @param maxOutliers upper bound on the number of outliers, at least one and at most + * {@code sample.length - 2} + * @return the indices of the outliers in ascending order, empty if the sample looks clean + * @throws IllegalArgumentException if the sample holds fewer than three points or a non-finite + * value, or if {@code maxOutliers} is out of range + * @throws NullPointerException if {@code sample} is {@code null} + */ + public int[] findOutliers(double[] sample, int maxOutliers) { + if (sample.length < MINIMUM_SAMPLE_SIZE) { + throw new IllegalArgumentException("The sample must hold at least " + MINIMUM_SAMPLE_SIZE + " points, but held " + sample.length); + } + if (maxOutliers < 1 || maxOutliers > sample.length - 2) { + throw new IllegalArgumentException("The number of outliers to look for must lie in [1, " + (sample.length - 2) + "], but was " + maxOutliers); + } + for (double value : sample) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + } + + int size = sample.length; + double[] values = Arrays.copyOf(sample, size); + int[] indices = new int[size]; + for (int i = 0; i < size; i++) { + indices[i] = i; + } + + int[] removed = new int[maxOutliers]; + int outliers = 0; + int remaining = size; + + for (int step = 1; step <= maxOutliers; step++) { + double mean = mean(values, remaining); + double deviation = standardDeviation(values, remaining, mean); + if (deviation == 0.0) { + break; + } + + int worst = indexOfLargestDeviation(values, remaining, mean); + double statistic = Math.abs(values[worst] - mean) / deviation; + removed[step - 1] = indices[worst]; + if (statistic > criticalValue(size, step)) { + outliers = step; + } + + remaining--; + values[worst] = values[remaining]; + indices[worst] = indices[remaining]; + } + + int[] result = Arrays.copyOf(removed, outliers); + Arrays.sort(result); + return result; + } + + /** + * Returns the critical value a statistic has to exceed at one step of the test. + * + * @param sampleSize the size of the whole sample + * @param step which removal this is, counting from one + * @return the critical value, that is {@code lambda_i} + * @throws IllegalArgumentException if {@code sampleSize} is smaller than three, or if {@code step} + * is outside {@code [1, sampleSize - 2]} + */ + public double criticalValue(int sampleSize, int step) { + if (sampleSize < MINIMUM_SAMPLE_SIZE) { + throw new IllegalArgumentException("The sample size must be at least " + MINIMUM_SAMPLE_SIZE + ", but was " + sampleSize); + } + if (step < 1 || step > sampleSize - 2) { + throw new IllegalArgumentException("The step must lie in [1, " + (sampleSize - 2) + "], but was " + step); + } + int size = sampleSize - step + 1; + int degreesOfFreedom = size - 2; + double probability = 1.0 - significanceLevel / (2.0 * size); + double t = studentTQuantile(probability, degreesOfFreedom); + return (size - 1) * t / Math.sqrt((degreesOfFreedom + t * t) * size); + } + + /** + * Returns the significance level of the test. + * + * @return the level given at construction time + */ + public double significanceLevel() { + return significanceLevel; + } + + @Override + public String toString() { + return "GeneralizedEsd{significanceLevel=" + significanceLevel + "}"; + } + + private static double mean(double[] values, int size) { + double sum = 0.0; + for (int i = 0; i < size; i++) { + sum += values[i]; + } + return sum / size; + } + + private static double standardDeviation(double[] values, int size, double mean) { + double sum = 0.0; + for (int i = 0; i < size; i++) { + double deviation = values[i] - mean; + sum += deviation * deviation; + } + return Math.sqrt(sum / (size - 1)); + } + + private static int indexOfLargestDeviation(double[] values, int size, double mean) { + int worst = 0; + double largest = -1.0; + for (int i = 0; i < size; i++) { + double deviation = Math.abs(values[i] - mean); + if (deviation > largest) { + largest = deviation; + worst = i; + } + } + return worst; + } + + /** + * Returns the {@code probability} quantile of a t distribution, found by bisecting its cumulative + * distribution function. + * + * @param probability the probability to invert, in {@code (0, 1)} + * @param degreesOfFreedom the degrees of freedom, at least one + * @return the quantile + */ + private static double studentTQuantile(double probability, int degreesOfFreedom) { + if (probability < 0.5) { + return -studentTQuantile(1.0 - probability, degreesOfFreedom); + } + double low = 0.0; + double high = 1.0; + while (studentTCumulative(high, degreesOfFreedom) < probability && high < 1e12) { + high *= 2.0; + } + for (int i = 0; i < 200; i++) { + double middle = 0.5 * (low + high); + if (studentTCumulative(middle, degreesOfFreedom) < probability) { + low = middle; + } else { + high = middle; + } + } + return 0.5 * (low + high); + } + + /** + * Returns the probability that a t distributed variable stays below {@code t}. + * + * @param t the point to evaluate at + * @param degreesOfFreedom the degrees of freedom, at least one + * @return the cumulative probability + */ + private static double studentTCumulative(double t, int degreesOfFreedom) { + double x = degreesOfFreedom / (degreesOfFreedom + t * t); + double tail = 0.5 * regularizedIncompleteBeta(0.5 * degreesOfFreedom, 0.5, x); + return t >= 0.0 ? 1.0 - tail : tail; + } + + /** + * Returns the regularized incomplete beta function, evaluated with the continued fraction of + * Lentz. + * + * @param a first shape parameter, strictly positive + * @param b second shape parameter, strictly positive + * @param x the point to evaluate at, in {@code [0, 1]} + * @return {@code I_x(a, b)} + */ + private static double regularizedIncompleteBeta(double a, double b, double x) { + if (x <= 0.0) { + return 0.0; + } + if (x >= 1.0) { + return 1.0; + } + double logBeta = logGamma(a) + logGamma(b) - logGamma(a + b); + if (x < (a + 1.0) / (a + b + 2.0)) { + return Math.exp(a * Math.log(x) + b * Math.log1p(-x) - logBeta) * betaContinuedFraction(a, b, x) / a; + } + return 1.0 - Math.exp(b * Math.log1p(-x) + a * Math.log(x) - logBeta) * betaContinuedFraction(b, a, 1.0 - x) / b; + } + + private static double betaContinuedFraction(double a, double b, double x) { + double tiny = 1e-300; + double c = 1.0; + double d = 1.0 - (a + b) * x / (a + 1.0); + if (Math.abs(d) < tiny) { + d = tiny; + } + d = 1.0 / d; + double fraction = d; + + for (int m = 1; m <= 300; m++) { + int even = 2 * m; + double numerator = m * (b - m) * x / ((a + even - 1.0) * (a + even)); + d = 1.0 + numerator * d; + if (Math.abs(d) < tiny) { + d = tiny; + } + c = 1.0 + numerator / c; + if (Math.abs(c) < tiny) { + c = tiny; + } + d = 1.0 / d; + fraction *= d * c; + + numerator = -(a + m) * (a + b + m) * x / ((a + even) * (a + even + 1.0)); + d = 1.0 + numerator * d; + if (Math.abs(d) < tiny) { + d = tiny; + } + c = 1.0 + numerator / c; + if (Math.abs(c) < tiny) { + c = tiny; + } + d = 1.0 / d; + double step = d * c; + fraction *= step; + + if (Math.abs(step - 1.0) < 1e-15) { + break; + } + } + return fraction; + } + + /** + * Returns the logarithm of the gamma function, using the Lanczos approximation. + * + * @param x the point to evaluate at, strictly positive + * @return {@code log(gamma(x))} + */ + private static double logGamma(double x) { + double shifted = x - 1.0; + double series = LANCZOS[0]; + for (int i = 1; i < LANCZOS.length; i++) { + series += LANCZOS[i] / (shifted + i); + } + double t = shifted + 7.5; + return 0.5 * Math.log(2.0 * Math.PI) + (shifted + 0.5) * Math.log(t) - t + Math.log(series); + } +} diff --git a/src/main/java/com/thealgorithms/streaming/HampelFilter.java b/src/main/java/com/thealgorithms/streaming/HampelFilter.java new file mode 100644 index 000000000000..f6969d4cbca6 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/HampelFilter.java @@ -0,0 +1,263 @@ +package com.thealgorithms.streaming; + +/** + * The Hampel filter, also known as the Hampel identifier: a decision rule that replaces the + * samples of a signal that look like outliers and leaves every other sample untouched. + * + *

A {@link MedianFilter} rewrites every sample, which throws away detail even where the signal + * was perfectly clean. The Hampel filter is the conservative version. For each incoming sample it + * looks at the window that ends at that sample and computes two robust statistics: + * + *

    + *
  • the median of the window, a robust estimate of where the signal is;
  • + *
  • the median absolute deviation {@code MAD = median(|x_i - median|)}, a robust estimate + * of how much the signal normally moves.
  • + *
+ * + *

The MAD is rescaled by {@code 1.4826}, the factor that makes it match the standard deviation + * for normally distributed data, and the sample is declared an outlier when + * + *

+ * |x - median| > threshold * 1.4826 * MAD
+ * 
+ * + *

Outliers are reported and replaced by the window median; everything else passes through + * unchanged. Both statistics are robust, so the spike being tested cannot inflate the very + * yardstick it is measured against - which is exactly what happens if one uses a mean and a standard + * deviation instead. + * + *

A threshold of 3 is the usual starting point, but how eager the filter turns out to be depends + * just as much on the window, because the MAD of a handful of samples is itself a noisy estimate of + * the spread. Measured on clean Gaussian noise with a threshold of 3, the filter flags about 7% of + * the samples with a window of 5, 3% with a window of 11 and 0.5% with a window of 101, slowly + * approaching the 0.3% one would get from a perfect estimate of sigma. Widen the window, or raise + * the threshold, when too many good samples are being rewritten. + * + *

Usage

+ * + *
{@code
+ * HampelFilter filter = new HampelFilter(7, 3.0);
+ * for (double sample : sensorReadings) {
+ *     double clean = filter.accept(sample);
+ *     if (filter.lastWasOutlier()) {
+ *         log.warn("spike of {} replaced by {}", sample, clean);
+ *     }
+ * }
+ * }
+ * + *

The window is causal and includes the sample being judged, so the filter introduces no delay, + * and the raw sample - not its replacement - is what enters the window, which keeps the statistics + * honest. Before the window has filled up the statistics are computed over the samples seen so far. + * If more than half of the window holds one and the same value the MAD is zero, and any sample that + * differs from the median at all is then flagged; that is the textbook behaviour of the identifier. + * + *

Each sample costs O(w) time for a window of {@code w} samples and nothing is allocated after + * construction. This class is not thread-safe. + * + * @see MedianFilter + * @see Median absolute deviation + */ +public final class HampelFilter { + + /** Makes the MAD of normally distributed data an unbiased estimator of its standard deviation. */ + public static final double GAUSSIAN_MAD_SCALE = 1.4826; + + /** Threshold, in robust standard deviations, used when none is given. */ + public static final double DEFAULT_THRESHOLD = 3.0; + + private final MedianFilter window; + private final double threshold; + private final double[] sortedWindow; + private final double[] deviations; + + private boolean lastWasOutlier; + private long outlierCount; + + /** + * Creates a filter with the customary threshold of three robust standard deviations. + * + * @param windowSize how many recent samples the statistics are computed over; odd sizes are the usual choice + * @throws IllegalArgumentException if {@code windowSize} is not positive + */ + public HampelFilter(int windowSize) { + this(windowSize, DEFAULT_THRESHOLD); + } + + /** + * Creates a filter. + * + * @param windowSize how many recent samples the statistics are computed over + * @param threshold how many robust standard deviations a sample may deviate before it counts as an + * outlier; must be non-negative + * @throws IllegalArgumentException if {@code windowSize} is not positive or {@code threshold} is negative or not finite + */ + public HampelFilter(int windowSize, double threshold) { + if (!(threshold >= 0.0) || !Double.isFinite(threshold)) { + throw new IllegalArgumentException("The threshold must be finite and non-negative, but was " + threshold); + } + this.window = new MedianFilter(windowSize); + this.threshold = threshold; + this.sortedWindow = new double[windowSize]; + this.deviations = new double[windowSize]; + } + + /** + * Feeds one sample into the filter. + * + * @param value the incoming sample + * @return the sample itself, or the window median if the sample was judged to be an outlier + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public double accept(double value) { + double median = window.accept(value); + double scaledDeviation = threshold * GAUSSIAN_MAD_SCALE * medianAbsoluteDeviation(); + lastWasOutlier = Math.abs(value - median) > scaledDeviation; + if (lastWasOutlier) { + outlierCount++; + return median; + } + return value; + } + + /** + * Filters a whole signal, one sample at a time, starting from the current state. + * + * @param signal the samples to filter + * @return a new array of the same length in which flagged samples are replaced by the local median + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public double[] filter(double[] signal) { + double[] filtered = new double[signal.length]; + for (int i = 0; i < signal.length; i++) { + filtered[i] = accept(signal[i]); + } + return filtered; + } + + /** + * Runs the filter over a signal and reports which samples were flagged, without altering them. + * + * @param signal the samples to inspect + * @return a new array of the same length, {@code true} where the sample was judged an outlier + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public boolean[] detectOutliers(double[] signal) { + boolean[] flags = new boolean[signal.length]; + for (int i = 0; i < signal.length; i++) { + accept(signal[i]); + flags[i] = lastWasOutlier; + } + return flags; + } + + /** + * Returns the median absolute deviation of the current window, the robust counterpart of the + * standard deviation. + * + * @return the MAD, {@code 0} when the window holds a single sample + * @throws IllegalStateException if no sample has been accepted yet + */ + public double medianAbsoluteDeviation() { + int size = window.copySortedWindow(sortedWindow); + if (size == 0) { + throw new IllegalStateException("The window is empty"); + } + double median = window.median(); + + // The window is sorted, so the absolute deviations form two already sorted runs that meet at + // the median: descending to its left, ascending to its right. Merging them is linear. + int split = size; + for (int i = 0; i < size; i++) { + if (sortedWindow[i] > median) { + split = i; + break; + } + } + int left = split - 1; + int right = split; + for (int i = 0; i < size; i++) { + boolean takeLeft = right >= size || (left >= 0 && median - sortedWindow[left] <= sortedWindow[right] - median); + if (takeLeft) { + deviations[i] = median - sortedWindow[left]; + left--; + } else { + deviations[i] = sortedWindow[right] - median; + right++; + } + } + + int middle = size / 2; + return size % 2 != 0 ? deviations[middle] : 0.5 * (deviations[middle - 1] + deviations[middle]); + } + + /** + * Returns the median of the current window. + * + * @return the window median + * @throws IllegalStateException if no sample has been accepted yet + */ + public double median() { + return window.median(); + } + + /** + * Tells whether the most recently accepted sample was flagged. + * + * @return {@code true} if the last sample was replaced by the median + */ + public boolean lastWasOutlier() { + return lastWasOutlier; + } + + /** + * Returns how many samples have been flagged since the last reset. + * + * @return the number of detected outliers + */ + public long outlierCount() { + return outlierCount; + } + + /** + * Returns the configured window length. + * + * @return the window size given at construction time + */ + public int windowSize() { + return window.windowSize(); + } + + /** + * Returns the configured threshold. + * + * @return the threshold in robust standard deviations + */ + public double threshold() { + return threshold; + } + + /** + * Returns how many samples the window currently holds. + * + * @return the current fill level, at most {@link #windowSize()} + */ + public int size() { + return window.size(); + } + + /** + * Empties the window and forgets the outlier count. + */ + public void reset() { + window.reset(); + lastWasOutlier = false; + outlierCount = 0; + } + + @Override + public String toString() { + return "HampelFilter{windowSize=" + windowSize() + ", threshold=" + threshold + ", outliers=" + outlierCount + '}'; + } +} diff --git a/src/main/java/com/thealgorithms/streaming/KalmanFilter.java b/src/main/java/com/thealgorithms/streaming/KalmanFilter.java new file mode 100644 index 000000000000..5e6d5143b12d --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/KalmanFilter.java @@ -0,0 +1,287 @@ +package com.thealgorithms.streaming; + +/** + * A scalar (one dimensional) Kalman filter: the optimal way to track a quantity that drifts + * slowly while every measurement of it is noisy. + * + *

The filter carries two numbers: the current estimate {@code x} and how much it distrusts that + * estimate, the error variance {@code p}. Each step has two halves. + * + *

+ * predict:  x <- x + u            p <- p + q
+ * update:   k <- p / (p + r)      x <- x + k * (z - x)      p <- (1 - k) * p
+ * 
+ * + *

where {@code q} is the process noise (how much the tracked quantity is expected to wander + * between two steps), {@code r} the measurement noise, {@code z} the measurement and {@code k} the + * Kalman gain. The gain is the whole story: it is the share of the measurement that gets believed. + * When the filter is unsure ({@code p} large) or the sensor is good ({@code r} small), {@code k} + * approaches 1 and the filter follows the sensor; in the opposite case it clings to its own + * prediction. Nothing tunes this by hand, the variances do it. + * + *

Sensor fusion

+ * + *

Fusing several sensors is not a separate algorithm: it is what happens when the same estimate + * is corrected once per sensor, each with its own noise level. A cheap sensor with a large {@code r} + * nudges the estimate a little, a precise one pulls it a lot, and the result is exactly the + * inverse-variance weighted combination that {@link #fuse(double[], double[])} computes in closed + * form: + * + *

{@code
+ * KalmanFilter filter = new KalmanFilter(startingHeight, 1.0, 0.01, 1.0);
+ * for (int t = 0; t < steps; t++) {
+ *     filter.predict();
+ *     filter.update(barometer[t], barometerVariance);
+ *     filter.update(gps[t], gpsVariance);          // second sensor, same estimate
+ *     double height = filter.estimate();
+ * }
+ * }
+ * + *

Both steps run in O(1) time and memory. This class is not thread-safe. + * + * @see Kalman filter + */ +public final class KalmanFilter { + + private final double processNoise; + private final double measurementNoise; + + private double estimate; + private double errorCovariance; + private double lastGain; + + private final double initialEstimate; + private final double initialErrorCovariance; + + /** + * Creates a filter. + * + * @param initialEstimate the starting guess for the tracked quantity + * @param initialErrorCovariance how uncertain that guess is; a large value makes the filter trust + * the first measurements almost completely + * @param processNoise variance added on every {@link #predict()}, i.e. how fast the quantity is + * expected to change on its own + * @param measurementNoise default variance of a measurement, used by {@link #update(double)} + * @throws IllegalArgumentException if any argument is not finite, or if a variance is negative, + * or if {@code measurementNoise} is zero + */ + public KalmanFilter(double initialEstimate, double initialErrorCovariance, double processNoise, double measurementNoise) { + requireFinite(initialEstimate, "initialEstimate"); + requireNonNegativeVariance(initialErrorCovariance, "initialErrorCovariance"); + requireNonNegativeVariance(processNoise, "processNoise"); + requirePositiveVariance(measurementNoise, "measurementNoise"); + + this.initialEstimate = initialEstimate; + this.initialErrorCovariance = initialErrorCovariance; + this.processNoise = processNoise; + this.measurementNoise = measurementNoise; + reset(); + } + + /** + * Advances the model by one step without any control input, growing the uncertainty by the + * process noise. + * + * @return the predicted estimate, unchanged in value for this constant model + */ + public double predict() { + return predict(0.0); + } + + /** + * Advances the model by one step, shifting the estimate by a known control input. + * + * @param controlInput the change the estimate is expected to undergo, e.g. velocity times the + * time step when tracking a position + * @return the predicted estimate + * @throws IllegalArgumentException if {@code controlInput} is not finite + */ + public double predict(double controlInput) { + requireFinite(controlInput, "controlInput"); + estimate += controlInput; + errorCovariance += processNoise; + return estimate; + } + + /** + * Corrects the estimate with a measurement taken by the default sensor. + * + * @param measurement the observed value + * @return the corrected estimate + * @throws IllegalArgumentException if {@code measurement} is not finite + */ + public double update(double measurement) { + return update(measurement, measurementNoise); + } + + /** + * Corrects the estimate with a measurement whose noise differs from the default one. Calling this + * several times per step, once per sensor, is the whole of sensor fusion. + * + * @param measurement the observed value + * @param noise variance of this particular measurement, strictly positive + * @return the corrected estimate + * @throws IllegalArgumentException if {@code measurement} is not finite or {@code noise} is not strictly positive + */ + public double update(double measurement, double noise) { + requireFinite(measurement, "measurement"); + requirePositiveVariance(noise, "noise"); + + double innovationVariance = errorCovariance + noise; + lastGain = errorCovariance / innovationVariance; + estimate += lastGain * (measurement - estimate); + // Algebraically this is (1 - gain) * p, but computing 1 - gain cancels away most of the + // significant digits whenever the gain is close to one, as it is on the first measurements. + errorCovariance = errorCovariance * noise / innovationVariance; + return estimate; + } + + /** + * Runs one full cycle: predict, then correct with the given measurement. + * + * @param measurement the observed value + * @return the filtered estimate + * @throws IllegalArgumentException if {@code measurement} is not finite + */ + public double filter(double measurement) { + predict(); + return update(measurement); + } + + /** + * Filters a whole signal offline, one cycle per sample. + * + * @param measurements the noisy signal + * @return a new array holding the filtered signal, of the same length + * @throws IllegalArgumentException if any measurement is not finite + * @throws NullPointerException if {@code measurements} is {@code null} + */ + public double[] filter(double[] measurements) { + double[] filtered = new double[measurements.length]; + for (int i = 0; i < measurements.length; i++) { + filtered[i] = filter(measurements[i]); + } + return filtered; + } + + /** + * Combines simultaneous readings of the same quantity taken by independent sensors, weighting + * each by the inverse of its variance. This is the closed form of what repeated + * {@link #update(double, double)} calls achieve within one step. + * + * @param measurements one reading per sensor + * @param variances the noise variance of each sensor, strictly positive, same length as {@code measurements} + * @return the fused reading together with its variance, which is never larger than the variance of + * the best single sensor + * @throws IllegalArgumentException if the arrays are empty, differ in length, hold a non-finite + * measurement or a non-positive variance + * @throws NullPointerException if either array is {@code null} + */ + public static Estimate fuse(double[] measurements, double[] variances) { + if (measurements.length != variances.length) { + throw new IllegalArgumentException("There must be exactly one variance per measurement, but got " + measurements.length + " and " + variances.length); + } + if (measurements.length == 0) { + throw new IllegalArgumentException("At least one measurement is required"); + } + + double weightSum = 0.0; + double weightedSum = 0.0; + for (int i = 0; i < measurements.length; i++) { + requireFinite(measurements[i], "measurement"); + requirePositiveVariance(variances[i], "variance"); + double weight = 1.0 / variances[i]; + weightSum += weight; + weightedSum += weight * measurements[i]; + } + return new Estimate(weightedSum / weightSum, 1.0 / weightSum); + } + + /** + * Returns the current estimate of the tracked quantity. + * + * @return the state estimate + */ + public double estimate() { + return estimate; + } + + /** + * Returns the variance of the current estimate; it shrinks with every update and grows with every + * prediction. + * + * @return the error covariance + */ + public double errorCovariance() { + return errorCovariance; + } + + /** + * Returns the Kalman gain used by the most recent update, a number in {@code [0, 1)} telling how + * much of that measurement was believed. + * + * @return the last gain, {@code 0} if no update has happened yet + */ + public double lastGain() { + return lastGain; + } + + /** + * Returns the process noise variance. + * + * @return the value given at construction time + */ + public double processNoise() { + return processNoise; + } + + /** + * Returns the default measurement noise variance. + * + * @return the value given at construction time + */ + public double measurementNoise() { + return measurementNoise; + } + + /** + * Restores the state the filter had right after construction. + */ + public void reset() { + estimate = initialEstimate; + errorCovariance = initialErrorCovariance; + lastGain = 0.0; + } + + @Override + public String toString() { + return "KalmanFilter{estimate=" + estimate + ", errorCovariance=" + errorCovariance + ", lastGain=" + lastGain + '}'; + } + + private static void requireFinite(double value, String name) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but was " + value); + } + } + + private static void requireNonNegativeVariance(double value, String name) { + if (!(value >= 0.0) || !Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite and non-negative, but was " + value); + } + } + + private static void requirePositiveVariance(double value, String name) { + if (!(value > 0.0) || !Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite and strictly positive, but was " + value); + } + } + + /** + * A value paired with the variance that describes how much it can be trusted. + * + * @param value the estimated quantity + * @param variance the variance of that estimate + */ + public record Estimate(double value, double variance) { + } +} diff --git a/src/main/java/com/thealgorithms/streaming/MedianFilter.java b/src/main/java/com/thealgorithms/streaming/MedianFilter.java new file mode 100644 index 000000000000..fa876a4feece --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/MedianFilter.java @@ -0,0 +1,229 @@ +package com.thealgorithms.streaming; + +import java.util.Arrays; + +/** + * A streaming median filter: it reports the median of the last {@code windowSize} samples of + * a signal. + * + *

The median filter is the standard first line of defence against impulsive noise. A single + * corrupted sample - a dropped bit, a spike on an analogue line, a sensor glitch - drags a moving + * average with it, but it cannot drag a median: as long as fewer than half of the samples in the + * window are corrupted, the output stays on the true signal. Unlike a linear low-pass filter, a + * median filter also preserves sharp edges instead of smearing them. + * + *

Two arrays of {@code windowSize} doubles are kept: the samples in arrival order, so the oldest + * one can be evicted, and the same samples in ascending order, so the median is the middle element. + * Insertion and removal in the sorted copy are binary search plus a memory move, which makes an + * update O(w) in the worst case but with a very small constant, and nothing is allocated after + * construction. + * + *

Usage

+ * + *
{@code
+ * MedianFilter filter = new MedianFilter(5);
+ * for (double sample : signal) {
+ *     double clean = filter.accept(sample);
+ * }
+ * }
+ * + *

The filter is causal: the value it reports for a sample is the median of that sample and the + * {@code windowSize - 1} preceding ones, so the output lags the input by roughly half a window. + * Before the window is full the median is taken over however many samples have arrived. With an even + * window size the median is the average of the two middle samples. + * + *

This class is not thread-safe. + * + * @see HampelFilter for a variant that only replaces samples identified as outliers + * @see Median filter + */ +public final class MedianFilter { + + private final double[] arrivalOrder; + private final double[] ascending; + private final int windowSize; + + private int size; + private int oldest; + + /** + * Creates an empty filter. + * + * @param windowSize how many recent samples the median is taken over; odd sizes are the usual choice + * @throws IllegalArgumentException if {@code windowSize} is not positive + */ + public MedianFilter(int windowSize) { + if (windowSize <= 0) { + throw new IllegalArgumentException("The window size must be positive, but was " + windowSize); + } + this.windowSize = windowSize; + this.arrivalOrder = new double[windowSize]; + this.ascending = new double[windowSize]; + } + + /** + * Feeds one sample into the filter. + * + * @param value the incoming sample + * @return the median of the window that now ends at this sample + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public double accept(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + if (size == windowSize) { + removeFromSorted(arrivalOrder[oldest]); + size--; + } + insertIntoSorted(value); + arrivalOrder[oldest] = value; + oldest = (oldest + 1) % windowSize; + size++; + return median(); + } + + /** + * Filters a whole signal, one sample at a time, starting from the current state. + * + * @param signal the samples to filter + * @return a new array of the same length holding the running medians + * @throws IllegalArgumentException if any sample is NaN or infinite + * @throws NullPointerException if {@code signal} is {@code null} + */ + public double[] filter(double[] signal) { + double[] filtered = new double[signal.length]; + for (int i = 0; i < signal.length; i++) { + filtered[i] = accept(signal[i]); + } + return filtered; + } + + /** + * Returns the median of the samples currently in the window. + * + * @return the median, averaging the two middle samples when the window holds an even number of them + * @throws IllegalStateException if no sample has been accepted yet + */ + public double median() { + if (size == 0) { + throw new IllegalStateException("The window is empty"); + } + int middle = size / 2; + if (size % 2 != 0) { + return ascending[middle]; + } + return 0.5 * (ascending[middle - 1] + ascending[middle]); + } + + /** + * Returns the smallest sample currently in the window. + * + * @return the minimum of the window + * @throws IllegalStateException if no sample has been accepted yet + */ + public double min() { + requireNonEmpty(); + return ascending[0]; + } + + /** + * Returns the largest sample currently in the window. + * + * @return the maximum of the window + * @throws IllegalStateException if no sample has been accepted yet + */ + public double max() { + requireNonEmpty(); + return ascending[size - 1]; + } + + /** + * Returns the configured window length. + * + * @return the window size given at construction time + */ + public int windowSize() { + return windowSize; + } + + /** + * Returns how many samples the window currently holds. + * + * @return the current fill level, at most {@link #windowSize()} + */ + public int size() { + return size; + } + + /** + * Tells whether the window has warmed up. + * + * @return {@code true} once the window holds {@link #windowSize()} samples + */ + public boolean isFull() { + return size == windowSize; + } + + /** + * Tells whether the window holds no samples. + * + * @return {@code true} if nothing has been accepted since the last reset + */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns the window contents in ascending order. + * + * @return a new array holding the sorted window + */ + public double[] sortedWindow() { + return Arrays.copyOf(ascending, size); + } + + /** + * Empties the window. + */ + public void reset() { + size = 0; + oldest = 0; + } + + @Override + public String toString() { + return "MedianFilter{windowSize=" + windowSize + ", size=" + size + ", median=" + (size == 0 ? Double.NaN : median()) + '}'; + } + + /** + * Copies the sorted window into a caller supplied buffer, which lets tight loops avoid allocating. + * + * @param destination buffer of at least {@link #windowSize()} elements + * @return the number of values written + */ + int copySortedWindow(double[] destination) { + System.arraycopy(ascending, 0, destination, 0, size); + return size; + } + + private void insertIntoSorted(double value) { + int position = Arrays.binarySearch(ascending, 0, size, value); + if (position < 0) { + position = -(position + 1); + } + System.arraycopy(ascending, position, ascending, position + 1, size - position); + ascending[position] = value; + } + + private void removeFromSorted(double value) { + int position = Arrays.binarySearch(ascending, 0, size, value); + System.arraycopy(ascending, position + 1, ascending, position, size - position - 1); + } + + private void requireNonEmpty() { + if (size == 0) { + throw new IllegalStateException("The window is empty"); + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/P2QuantileEstimator.java b/src/main/java/com/thealgorithms/streaming/P2QuantileEstimator.java new file mode 100644 index 000000000000..9fa4bcf701b9 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/P2QuantileEstimator.java @@ -0,0 +1,310 @@ +package com.thealgorithms.streaming; + +import java.util.Arrays; + +/** + * The P-square (PΒ²) algorithm of Jain and Chlamtac: it estimates one quantile of a stream + * using five numbers and no memory of the samples themselves. + * + *

Computing an exact quantile requires storing everything seen so far, which is impossible for an + * unbounded stream. PΒ² instead keeps five markers that track the minimum, the quantile + * {@code p/2}, the quantile {@code p} itself, the quantile {@code (1 + p)/2} and the maximum. Each + * marker has a height (its current value) and a position (how many samples are known to be below + * it). Every new sample shifts the positions of the markers it falls below, and each marker is then + * nudged back towards the position it is supposed to occupy. The nudge follows a piecewise + * parabolic prediction fitted through the three neighbouring markers, falling back to a linear one + * whenever the parabola would break the ordering of the heights. + * + *

Accuracy is typically a fraction of a percent in rank for stationary streams and improves as + * more samples arrive. The markers adapt continuously, so a drifting distribution is tracked rather + * than averaged away, but no accuracy bound is guaranteed for adversarial inputs. + * + * + * + * + * + * + * + *
Cost
OperationComplexity
{@link #add(double)}O(1)
{@link #quantile()}O(1)
memoryO(1) - five markers, independent of the stream length
+ * + *

Usage

+ * + *
{@code
+ * P2QuantileEstimator p99 = new P2QuantileEstimator(0.99);
+ * for (double latency : latencies) {
+ *     p99.add(latency);
+ * }
+ * double tail = p99.quantile();
+ * }
+ * + *

The first five samples are stored verbatim, so the estimate is exact until the sixth one + * arrives. This class is not thread-safe. + * + * @see R. Jain, I. Chlamtac, The P-square algorithm (1985) + */ +public final class P2QuantileEstimator { + + private static final int MARKERS = 5; + + private final double probability; + + /** Marker heights, kept in non-decreasing order; also the raw buffer for the first five samples. */ + private final double[] heights = new double[MARKERS]; + + /** One-based rank of each marker, i.e. how many samples are known to sit at or below it. */ + private final int[] positions = new int[MARKERS]; + + /** Position each marker should ideally occupy. */ + private final double[] desiredPositions = new double[MARKERS]; + + /** How much {@link #desiredPositions} grows per sample. */ + private final double[] positionIncrements = new double[MARKERS]; + + private long count; + + /** + * Creates an estimator for a single quantile. + * + * @param probability the quantile to track, strictly between 0 and 1, e.g. {@code 0.5} for the median + * @throws IllegalArgumentException if {@code probability} is outside {@code (0, 1)} + */ + public P2QuantileEstimator(double probability) { + if (!(probability > 0.0) || !(probability < 1.0)) { + throw new IllegalArgumentException("The quantile probability must lie strictly between 0 and 1, but was " + probability); + } + this.probability = probability; + } + + /** + * Incorporates one sample. + * + * @param value the sample to add + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public void add(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + if (count < MARKERS) { + collectInitialSample(value); + return; + } + + int cell = locate(value); + for (int i = cell + 1; i < MARKERS; i++) { + positions[i]++; + } + for (int i = 0; i < MARKERS; i++) { + desiredPositions[i] += positionIncrements[i]; + } + for (int i = 1; i < MARKERS - 1; i++) { + adjustMarker(i); + } + count++; + } + + /** + * Incorporates every given sample, in order. + * + * @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); + } + } + + /** + * Returns the current estimate of the tracked quantile. + * + * @return the estimated quantile, exact while fewer than five samples have been seen + * @throws IllegalStateException if no sample has been added yet + */ + public double quantile() { + requireNonEmpty(); + if (count < MARKERS) { + return exactQuantileOfInitialSamples(); + } + return heights[2]; + } + + /** + * Returns the smallest sample seen so far, which PΒ² tracks exactly. + * + * @return the running minimum + * @throws IllegalStateException if no sample has been added yet + */ + public double min() { + requireNonEmpty(); + return count < MARKERS ? Arrays.stream(heights, 0, (int) count).min().orElse(Double.NaN) : heights[0]; + } + + /** + * Returns the largest sample seen so far, which PΒ² tracks exactly. + * + * @return the running maximum + * @throws IllegalStateException if no sample has been added yet + */ + public double max() { + requireNonEmpty(); + return count < MARKERS ? Arrays.stream(heights, 0, (int) count).max().orElse(Double.NaN) : heights[MARKERS - 1]; + } + + /** + * Returns the quantile this estimator was configured for. + * + * @return the probability given at construction time + */ + public double probability() { + return probability; + } + + /** + * Returns the number of samples seen so far. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Tells whether any sample has been added. + * + * @return {@code true} if the estimator holds no samples + */ + public boolean isEmpty() { + return count == 0; + } + + /** + * Forgets every sample. + */ + public void reset() { + count = 0; + Arrays.fill(heights, 0.0); + Arrays.fill(positions, 0); + Arrays.fill(desiredPositions, 0.0); + Arrays.fill(positionIncrements, 0.0); + } + + @Override + public String toString() { + return "P2QuantileEstimator{p=" + probability + ", count=" + count + ", quantile=" + (count == 0 ? Double.NaN : quantile()) + '}'; + } + + /** + * Stores one of the first five samples and, on the fifth, lays out the markers. + */ + private void collectInitialSample(double value) { + heights[(int) count] = value; + count++; + if (count < MARKERS) { + return; + } + Arrays.sort(heights); + // Marker positions are one-based, exactly as in the paper. Only differences of positions are + // ever used, so the origin is arithmetically irrelevant - but the desired positions are grown + // by repeated addition, and starting them one lower would round differently and flip the + // strict comparisons below on the samples where a desired position lands on a whole number. + for (int i = 0; i < MARKERS; i++) { + positions[i] = i + 1; + } + desiredPositions[0] = 1.0; + desiredPositions[1] = 1.0 + 2.0 * probability; + desiredPositions[2] = 1.0 + 4.0 * probability; + desiredPositions[3] = 3.0 + 2.0 * probability; + desiredPositions[4] = 5.0; + positionIncrements[0] = 0.0; + positionIncrements[1] = probability / 2.0; + positionIncrements[2] = probability; + positionIncrements[3] = (1.0 + probability) / 2.0; + positionIncrements[4] = 1.0; + } + + /** + * Finds the cell the sample falls into, stretching the outer markers when it falls outside the + * range seen so far. + * + * @param value the incoming sample + * @return the index of the marker immediately below the sample + */ + private int locate(double value) { + if (value < heights[0]) { + heights[0] = value; + return 0; + } + for (int i = 1; i < MARKERS - 1; i++) { + if (value < heights[i]) { + return i - 1; + } + } + if (value > heights[MARKERS - 1]) { + heights[MARKERS - 1] = value; + } + return MARKERS - 2; + } + + /** + * Moves one inner marker by a single position if it has drifted too far from where it should be. + */ + private void adjustMarker(int i) { + double drift = desiredPositions[i] - positions[i]; + boolean shiftRight = drift >= 1.0 && positions[i + 1] - positions[i] > 1; + boolean shiftLeft = drift <= -1.0 && positions[i - 1] - positions[i] < -1; + if (!shiftRight && !shiftLeft) { + return; + } + + int direction = shiftRight ? 1 : -1; + double candidate = parabolicPrediction(i, direction); + heights[i] = heights[i - 1] < candidate && candidate < heights[i + 1] ? candidate : linearPrediction(i, direction); + positions[i] += direction; + } + + /** + * Piecewise parabolic prediction: fits a parabola through markers {@code i - 1}, {@code i} and + * {@code i + 1} and evaluates it one position away from the current one. + */ + private double parabolicPrediction(int i, int direction) { + double left = positions[i] - positions[i - 1]; + double right = positions[i + 1] - positions[i]; + // Grouped exactly as in the paper. The result then decides, by a strict comparison against the + // neighbouring heights, whether the parabola is used at all, so regrouping the arithmetic would + // flip that decision on knife-edge inputs and make this estimator drift away from the reference. + return heights[i] + direction / (left + right) * ((left + direction) * (heights[i + 1] - heights[i]) / right + (right - direction) * (heights[i] - heights[i - 1]) / left); + } + + /** + * Linear fallback used whenever the parabolic prediction would violate the ordering of the + * marker heights. + */ + private double linearPrediction(int i, int direction) { + int neighbour = i + direction; + return heights[i] + direction * (heights[neighbour] - heights[i]) / (positions[neighbour] - positions[i]); + } + + /** + * Exact quantile of the fewer than five samples buffered so far, by linear interpolation between + * the two order statistics surrounding the requested rank. + */ + private double exactQuantileOfInitialSamples() { + double[] sorted = Arrays.copyOf(heights, (int) count); + Arrays.sort(sorted); + if (sorted.length == 1) { + return sorted[0]; + } + double rank = probability * (sorted.length - 1); + int lower = (int) Math.floor(rank); + int upper = Math.min(lower + 1, sorted.length - 1); + return sorted[lower] + (rank - lower) * (sorted[upper] - sorted[lower]); + } + + private void requireNonEmpty() { + if (count == 0) { + throw new IllegalStateException("The estimator has not seen any sample yet"); + } + } +} diff --git a/src/main/java/com/thealgorithms/streaming/ShiftSignal.java b/src/main/java/com/thealgorithms/streaming/ShiftSignal.java new file mode 100644 index 000000000000..467dcf060e0e --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/ShiftSignal.java @@ -0,0 +1,29 @@ +package com.thealgorithms.streaming; + +/** + * What a change detector reports after looking at one sample: either the stream still behaves as + * expected, or its level has shifted, in one direction or the other. + * + * @see CusumDetector + * @see EwmaChangeDetector + */ +public enum ShiftSignal { + + /** No evidence of a change; the stream is in control. */ + NONE, + + /** The level of the stream has moved above the target. */ + UPWARD, + + /** The level of the stream has moved below the target. */ + DOWNWARD; + + /** + * Tells whether this signal reports a change. + * + * @return {@code true} for {@link #UPWARD} and {@link #DOWNWARD} + */ + public boolean isAlarm() { + return this != NONE; + } +} 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/main/java/com/thealgorithms/streaming/UnscentedKalmanFilter.java b/src/main/java/com/thealgorithms/streaming/UnscentedKalmanFilter.java new file mode 100644 index 000000000000..69abdf0eb6c2 --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/UnscentedKalmanFilter.java @@ -0,0 +1,561 @@ +package com.thealgorithms.streaming; + +import com.thealgorithms.matrix.InverseOfMatrix; +import java.util.Arrays; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * The unscented Kalman filter: a Kalman filter for a nonlinear model that samples the model + * instead of differentiating it. + * + *

The extended filter pushes the covariance through a Jacobian, which is a straight line drawn at + * one point. That needs the model to be differentiable, needs somebody to write the derivative, and + * throws away everything the model does beyond first order. The unscented filter takes the opposite + * route. It picks a small, deterministic set of sigma points whose mean and covariance are + * exactly those of the current estimate, sends every one of them through the real model, and reads + * the new mean and covariance off where they land: + * + *

+ * lambda = alpha^2 (n + kappa) - n
+ * X_0 = x,   X_i = x + [sqrt((n + lambda) P)]_i,   X_{i+n} = x - [sqrt((n + lambda) P)]_i
+ * Wm_0 = lambda / (n + lambda),   Wc_0 = Wm_0 + 1 - alpha^2 + beta,   W_i = 1 / (2 (n + lambda))
+ * 
+ * + *

The square root is the Cholesky factor, so a step costs {@code 2n + 1} evaluations of the model + * and one factorisation, about what the extended filter costs, with no Jacobian anywhere. The payoff + * is accuracy: the transformed mean and covariance are right to second order for any model, where + * the extended filter is right to first. For {@code x^2} with {@code x} normally distributed, the + * extended filter predicts a mean of {@code mu^2} and misses the {@code sigma^2} that the spread + * itself contributes; the unscented one gets the mean exactly, and with the defaults it gets the + * variance {@code 4 mu^2 sigma^2 + 2 sigma^4} exactly as well. For a linear model it reduces to the + * ordinary Kalman filter, step for step. + * + *

The three parameters are left at {@code alpha = 1}, {@code beta = 2}, {@code kappa = 0}. The + * value {@code alpha = 1e-3} often quoted from the literature puts the sigma points almost on top of + * the mean and compensates with a central weight of about minus a million, which is exact on paper + * and a loss of six digits in floating point. {@code beta = 2} is the optimal choice for a Gaussian + * prior and is what makes the variance of the quadratic above come out exact. + * + *

After an update the covariance {@code P - K S K'} is symmetrised explicitly. The subtraction is + * symmetric in exact arithmetic but not after rounding, and a covariance that has drifted out of + * symmetry eventually fails the Cholesky factorisation of the next step. When that factorisation does + * fail, because the covariance has stopped being positive definite, the step is refused with an + * {@link ArithmeticException} rather than carried on with a square root that does not exist. + * + *

Usage

+ * + *
{@code
+ * UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0, 1.0}, identity);
+ * filter.predict(state -> new double[] {state[0] + dt * state[1], state[1]}, processNoise);
+ * filter.update(new double[] {range}, state -> new double[] {Math.hypot(state[0], offset)}, measurementNoise);
+ * }
+ * + *

A step costs O(n^3) for the factorisation plus {@code 2n + 1} calls of the model. The inversion + * of the innovation covariance reuses {@link InverseOfMatrix#invert(double[][])}. This class is not + * thread-safe. + * + * @see ExtendedKalmanFilter + * @see KalmanFilter + * @see Unscented Kalman filter + */ +public final class UnscentedKalmanFilter { + + /** Spread of the sigma points used when none is given. */ + public static final double DEFAULT_ALPHA = 1.0; + + /** Prior knowledge of the distribution used when none is given, optimal for a Gaussian. */ + public static final double DEFAULT_BETA = 2.0; + + /** Secondary scaling used when none is given. */ + public static final double DEFAULT_KAPPA = 0.0; + + private final int stateSize; + private final double alpha; + private final double beta; + private final double kappa; + private final double lambda; + private final double[] meanWeights; + private final double[] covarianceWeights; + + private double[] state; + private double[][] covariance; + private double[] innovation; + private long predictions; + private long corrections; + + /** + * Creates a filter with the default sigma point parameters. + * + * @param initialState the first guess at the state, copied + * @param initialCovariance how uncertain that guess is, copied + * @throws IllegalArgumentException if the state is empty, if the covariance is not square and of + * the same size, or if either holds a value that is not finite + * @throws NullPointerException if an argument is {@code null} + */ + public UnscentedKalmanFilter(double[] initialState, double[][] initialCovariance) { + this(initialState, initialCovariance, DEFAULT_ALPHA, DEFAULT_BETA, DEFAULT_KAPPA); + } + + /** + * Creates a filter. + * + * @param initialState the first guess at the state, copied + * @param initialCovariance how uncertain that guess is, copied + * @param alpha how far the sigma points spread from the mean, strictly positive + * @param beta what is known about the shape of the distribution, not negative; two is optimal for + * a Gaussian + * @param kappa a secondary scaling, chosen so that {@code n + kappa} is strictly positive + * @throws IllegalArgumentException if a shape is wrong, a value is not finite, or the parameters do + * not leave {@code n + lambda} strictly positive + * @throws NullPointerException if an argument is {@code null} + */ + public UnscentedKalmanFilter(double[] initialState, double[][] initialCovariance, double alpha, double beta, double kappa) { + if (initialState.length == 0) { + throw new IllegalArgumentException("The state must not be empty"); + } + if (!(alpha > 0.0) || !Double.isFinite(alpha)) { + throw new IllegalArgumentException("The alpha must be finite and strictly positive, but was " + alpha); + } + if (!(beta >= 0.0) || !Double.isFinite(beta)) { + throw new IllegalArgumentException("The beta must be finite and not negative, but was " + beta); + } + if (!Double.isFinite(kappa)) { + throw new IllegalArgumentException("The kappa must be finite, but was " + kappa); + } + + this.stateSize = initialState.length; + requireVector(initialState, stateSize, "initial state"); + requireMatrix(initialCovariance, stateSize, stateSize, "initial covariance"); + + this.alpha = alpha; + this.beta = beta; + this.kappa = kappa; + this.lambda = alpha * alpha * (stateSize + kappa) - stateSize; + double scale = stateSize + lambda; + if (!(scale > 0.0)) { + throw new IllegalArgumentException("The parameters leave n + lambda at " + scale + ", but it must be strictly positive"); + } + + int points = 2 * stateSize + 1; + this.meanWeights = new double[points]; + this.covarianceWeights = new double[points]; + meanWeights[0] = lambda / scale; + covarianceWeights[0] = meanWeights[0] + 1.0 - alpha * alpha + beta; + for (int i = 1; i < points; i++) { + meanWeights[i] = 1.0 / (2.0 * scale); + covarianceWeights[i] = meanWeights[i]; + } + + this.state = initialState.clone(); + this.covariance = copy(initialCovariance); + this.innovation = new double[0]; + } + + /** + * Pushes the estimate forward through the model. + * + * @param transition the model, which maps a state to the state one step later + * @param processNoise how much the model is trusted, a square matrix of the size of the state + * @return a copy of the predicted state + * @throws IllegalArgumentException if the model returns the wrong shape or a value that is not + * finite, or if the process noise has the wrong shape + * @throws ArithmeticException if the covariance is no longer positive definite + * @throws NullPointerException if an argument is {@code null} + */ + public double[] predict(UnaryOperator transition, double[][] processNoise) { + requireMatrix(processNoise, stateSize, stateSize, "process noise"); + + double[][] points = sigmaPoints(); + double[][] propagated = new double[points.length][]; + for (int i = 0; i < points.length; i++) { + propagated[i] = transition.apply(points[i]); + requireVector(propagated[i], stateSize, "propagated sigma point"); + } + + double[] mean = weightedMean(propagated); + double[][] spread = add(weightedCovariance(propagated, mean, propagated, mean), processNoise); + + state = mean; + covariance = symmetrised(spread); + predictions++; + return state.clone(); + } + + /** + * Folds one measurement into the estimate. + * + * @param measurement what the sensor reported + * @param model the measurement model, which maps a state to what the sensor would report + * @param measurementNoise how much the sensor is trusted, a square matrix of the size of the + * measurement + * @return a copy of the corrected state + * @throws IllegalArgumentException if the measurement is empty, if the model returns the wrong + * shape or a value that is not finite, or if the noise has the wrong shape + * @throws ArithmeticException if the covariance is no longer positive definite, or if the + * innovation covariance cannot be inverted + * @throws NullPointerException if an argument is {@code null} + */ + public double[] update(double[] measurement, Function model, double[][] measurementNoise) { + if (measurement.length == 0) { + throw new IllegalArgumentException("The measurement must not be empty"); + } + int measurementSize = measurement.length; + requireVector(measurement, measurementSize, "measurement"); + requireMatrix(measurementNoise, measurementSize, measurementSize, "measurement noise"); + + double[][] points = sigmaPoints(); + double[][] observed = new double[points.length][]; + for (int i = 0; i < points.length; i++) { + observed[i] = model.apply(points[i].clone()); + requireVector(observed[i], measurementSize, "expected measurement"); + } + + double[] expected = weightedMean(observed); + double[][] innovationCovariance = add(weightedCovariance(observed, expected, observed, expected), measurementNoise); + double[][] crossCovariance = weightedCovariance(points, state, observed, expected); + + double[][] inverted = InverseOfMatrix.invert(copy(innovationCovariance)); + requireFiniteMatrix(inverted, "The innovation covariance is singular, so the gain is undefined"); + double[][] gain = multiply(crossCovariance, inverted); + + double[] residual = new double[measurementSize]; + for (int i = 0; i < measurementSize; i++) { + residual[i] = measurement[i] - expected[i]; + } + for (int i = 0; i < stateSize; i++) { + double correction = 0.0; + for (int j = 0; j < measurementSize; j++) { + correction += gain[i][j] * residual[j]; + } + state[i] += correction; + } + + double[][] reduction = multiply(multiply(gain, innovationCovariance), transpose(gain)); + covariance = symmetrised(subtract(covariance, reduction)); + innovation = residual; + corrections++; + return state.clone(); + } + + /** + * Returns the sigma points of the current estimate, whose weighted mean and covariance are the + * estimate and its covariance. + * + * @return {@code 2n + 1} points, the mean first, each a fresh array + * @throws ArithmeticException if the covariance is no longer positive definite + */ + public double[][] sigmaPoints() { + double[][] root = cholesky(scaled(covariance, stateSize + lambda)); + double[][] points = new double[2 * stateSize + 1][]; + points[0] = state.clone(); + for (int column = 0; column < stateSize; column++) { + double[] plus = state.clone(); + double[] minus = state.clone(); + for (int row = 0; row < stateSize; row++) { + plus[row] += root[row][column]; + minus[row] -= root[row][column]; + } + points[column + 1] = plus; + points[column + 1 + stateSize] = minus; + } + return points; + } + + /** + * Returns the weights the sigma points carry when the mean is formed. + * + * @return a copy of the mean weights, which sum to one + */ + public double[] meanWeights() { + return meanWeights.clone(); + } + + /** + * Returns the weights the sigma points carry when the covariance is formed. + * + * @return a copy of the covariance weights + */ + public double[] covarianceWeights() { + return covarianceWeights.clone(); + } + + /** + * Returns the current estimate. + * + * @return a copy of the state + */ + public double[] state() { + return state.clone(); + } + + /** + * Returns how uncertain the current estimate is. + * + * @return a copy of the covariance + */ + public double[][] covariance() { + return copy(covariance); + } + + /** + * Returns the variance of one component of the state. + * + * @param component which component, from zero to the size of the state + * @return the variance of that component + * @throws IllegalArgumentException if {@code component} is outside the state + */ + public double variance(int component) { + if (component < 0 || component >= stateSize) { + throw new IllegalArgumentException("The component must lie in [0, " + (stateSize - 1) + "], but was " + component); + } + return covariance[component][component]; + } + + /** + * Returns the last innovation, how far the sensor was from what the filter expected. + * + * @return a copy of the last residual, empty before the first update + */ + public double[] lastInnovation() { + return innovation.clone(); + } + + /** + * Returns the size of the state. + * + * @return how many components the state holds + */ + public int stateSize() { + return stateSize; + } + + /** + * Returns the spread parameter. + * + * @return alpha + */ + public double alpha() { + return alpha; + } + + /** + * Returns the distribution parameter. + * + * @return beta + */ + public double beta() { + return beta; + } + + /** + * Returns the secondary scaling parameter. + * + * @return kappa + */ + public double kappa() { + return kappa; + } + + /** + * Returns how many predictions have been made since the last reset. + * + * @return the prediction count + */ + public long predictionCount() { + return predictions; + } + + /** + * Returns how many measurements have been folded in since the last reset. + * + * @return the update count + */ + public long updateCount() { + return corrections; + } + + /** + * Restarts the filter from a known estimate. + * + * @param newState the state to carry on from, copied + * @param newCovariance how uncertain it is, copied + * @throws IllegalArgumentException if a shape is wrong or a value is not finite + * @throws NullPointerException if an argument is {@code null} + */ + public void reset(double[] newState, double[][] newCovariance) { + requireVector(newState, stateSize, "state"); + requireMatrix(newCovariance, stateSize, stateSize, "covariance"); + state = newState.clone(); + covariance = copy(newCovariance); + innovation = new double[0]; + predictions = 0; + corrections = 0; + } + + @Override + public String toString() { + return "UnscentedKalmanFilter{state=" + Arrays.toString(state) + ", alpha=" + alpha + ", beta=" + beta + ", kappa=" + kappa + ", predictions=" + predictions + ", updates=" + corrections + "}"; + } + + private double[] weightedMean(double[][] points) { + double[] mean = new double[points[0].length]; + for (int i = 0; i < points.length; i++) { + for (int j = 0; j < mean.length; j++) { + mean[j] += meanWeights[i] * points[i][j]; + } + } + return mean; + } + + private double[][] weightedCovariance(double[][] first, double[] firstMean, double[][] second, double[] secondMean) { + double[][] result = new double[firstMean.length][secondMean.length]; + for (int i = 0; i < first.length; i++) { + for (int row = 0; row < firstMean.length; row++) { + double left = covarianceWeights[i] * (first[i][row] - firstMean[row]); + for (int column = 0; column < secondMean.length; column++) { + result[row][column] += left * (second[i][column] - secondMean[column]); + } + } + } + return result; + } + + /** + * Returns the lower triangular Cholesky factor {@code L} of a symmetric positive definite matrix, + * with {@code L L' = matrix}. + * + * @param matrix the matrix to factorise + * @return its Cholesky factor + * @throws ArithmeticException if the matrix is not positive definite + */ + private static double[][] cholesky(double[][] matrix) { + int size = matrix.length; + double[][] factor = new double[size][size]; + for (int row = 0; row < size; row++) { + for (int column = 0; column <= row; column++) { + double sum = matrix[row][column]; + for (int k = 0; k < column; k++) { + sum -= factor[row][k] * factor[column][k]; + } + if (row == column) { + if (!(sum > 0.0)) { + throw new ArithmeticException("The covariance is not positive definite, so it has no square root to spread the sigma points with"); + } + factor[row][row] = Math.sqrt(sum); + } else { + factor[row][column] = sum / factor[column][column]; + } + } + } + return factor; + } + + private static void requireVector(double[] vector, int size, String name) { + if (vector.length != size) { + throw new IllegalArgumentException("The " + name + " must hold " + size + " values, but held " + vector.length); + } + for (double value : vector) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but held " + value); + } + } + } + + private static void requireMatrix(double[][] matrix, int rows, int columns, String name) { + if (matrix.length != rows) { + throw new IllegalArgumentException("The " + name + " must have " + rows + " rows, but had " + matrix.length); + } + for (double[] row : matrix) { + if (row.length != columns) { + throw new IllegalArgumentException("The " + name + " must have " + columns + " columns, but a row had " + row.length); + } + for (double value : row) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("The " + name + " must be finite, but held " + value); + } + } + } + } + + private static void requireFiniteMatrix(double[][] matrix, String message) { + for (double[] row : matrix) { + for (double value : row) { + if (!Double.isFinite(value)) { + throw new ArithmeticException(message); + } + } + } + } + + private static double[][] copy(double[][] matrix) { + double[][] copied = new double[matrix.length][]; + for (int i = 0; i < matrix.length; i++) { + copied[i] = matrix[i].clone(); + } + return copied; + } + + private static double[][] scaled(double[][] matrix, double factor) { + double[][] result = new double[matrix.length][matrix[0].length]; + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + result[i][j] = factor * matrix[i][j]; + } + } + return result; + } + + private static double[][] symmetrised(double[][] matrix) { + double[][] result = new double[matrix.length][matrix.length]; + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix.length; j++) { + result[i][j] = 0.5 * (matrix[i][j] + matrix[j][i]); + } + } + return result; + } + + private static double[][] transpose(double[][] matrix) { + double[][] result = new double[matrix[0].length][matrix.length]; + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + result[j][i] = matrix[i][j]; + } + } + return result; + } + + private static double[][] multiply(double[][] left, double[][] right) { + double[][] result = new double[left.length][right[0].length]; + for (int i = 0; i < left.length; i++) { + for (int k = 0; k < right.length; k++) { + double factor = left[i][k]; + for (int j = 0; j < right[0].length; j++) { + result[i][j] += factor * right[k][j]; + } + } + } + return result; + } + + private static double[][] add(double[][] left, double[][] right) { + double[][] result = new double[left.length][left[0].length]; + for (int i = 0; i < left.length; i++) { + for (int j = 0; j < left[i].length; j++) { + result[i][j] = left[i][j] + right[i][j]; + } + } + return result; + } + + private static double[][] subtract(double[][] left, double[][] right) { + double[][] result = new double[left.length][left[0].length]; + for (int i = 0; i < left.length; i++) { + for (int j = 0; j < left[i].length; j++) { + result[i][j] = left[i][j] - right[i][j]; + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/streaming/WelfordAlgorithm.java b/src/main/java/com/thealgorithms/streaming/WelfordAlgorithm.java new file mode 100644 index 000000000000..62752df3d7da --- /dev/null +++ b/src/main/java/com/thealgorithms/streaming/WelfordAlgorithm.java @@ -0,0 +1,245 @@ +package com.thealgorithms.streaming; + +/** + * Online (single pass) mean and variance using Welford's algorithm. + * + *

The textbook formula {@code Var = (sum(x^2) - n * mean^2) / (n - 1)} is fast but numerically + * treacherous: {@code sum(x^2)} and {@code n * mean^2} may be huge and nearly equal, so their + * difference loses most of its significant digits and can even come out negative. Welford's + * recurrence never forms those large intermediate values. It keeps only the running mean and the sum + * of squared deviations from that running mean, {@code M2}: + * + *

+ * n     <- n + 1
+ * delta <- x - mean
+ * mean  <- mean + delta / n
+ * M2    <- M2 + delta * (x - mean)   // note: the second factor uses the *updated* mean
+ * 
+ * + *

Both {@link #add(double)} and {@link #remove(double)} run in O(1) time and the accumulator + * occupies O(1) memory no matter how many samples pass through it. + * + *

Sliding windows and map-reduce

+ * + *
    + *
  • {@link #remove(double)} runs the recurrence backwards, which turns the accumulator into the + * statistics of a sliding window: feed the incoming sample to {@code add} and the sample that + * just left the window to {@code remove}. Removal is the one operation that can degrade + * accuracy over a very long run, since the value being removed no longer matches the mean it + * was added to; recreate the accumulator periodically if that matters.
  • + *
  • {@link #merge(WelfordAlgorithm, WelfordAlgorithm)} implements Chan's parallel update, so + * partial results computed on different shards can be combined exactly.
  • + *
+ * + *

Usage

+ * + *
{@code
+ * WelfordAlgorithm stats = new WelfordAlgorithm();
+ * stats.add(2.0);
+ * stats.add(4.0);
+ * stats.add(4.0);
+ * stats.mean();                        // 3.3333...
+ * stats.populationStandardDeviation();  // 0.9428...
+ * }
+ * + *

This class is not thread-safe. + * + * @see ExponentialMovingAverage + * @see Algorithms for calculating variance + */ +public final class WelfordAlgorithm { + + private long count; + private double mean; + private double sumOfSquaredDeviations; + + /** + * Creates an empty accumulator. + */ + public WelfordAlgorithm() { + clear(); + } + + /** + * Incorporates one sample. + * + * @param value the sample to add + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public void add(double value) { + requireFinite(value); + count++; + double delta = value - mean; + mean += delta / count; + sumOfSquaredDeviations += delta * (value - mean); + } + + /** + * Incorporates every given sample, in order. + * + * @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); + } + } + + /** + * Removes a previously added sample, reversing {@link #add(double)}. This is what makes the + * accumulator usable for a sliding window. + * + * @param value the sample to remove; it must genuinely have been added before + * @throws IllegalStateException if the accumulator is empty + * @throws IllegalArgumentException if {@code value} is NaN or infinite + */ + public void remove(double value) { + requireFinite(value); + if (count == 0) { + throw new IllegalStateException("Cannot remove a sample from an empty accumulator"); + } + if (count == 1) { + clear(); + return; + } + double previousMean = mean; + mean = (count * mean - value) / (count - 1); + sumOfSquaredDeviations -= (value - previousMean) * (value - mean); + count--; + if (sumOfSquaredDeviations < 0.0) { + sumOfSquaredDeviations = 0.0; + } + } + + /** + * Combines two independently accumulated summaries using Chan's parallel variance update. + * + * @param left summary of the first batch of samples + * @param right summary of the second batch of samples + * @return a new summary describing the concatenation of both batches + * @throws NullPointerException if either argument is {@code null} + */ + public static WelfordAlgorithm merge(WelfordAlgorithm left, WelfordAlgorithm right) { + WelfordAlgorithm merged = new WelfordAlgorithm(); + merged.count = left.count + right.count; + if (merged.count == 0) { + return merged; + } + double delta = right.mean - left.mean; + merged.mean = left.mean + delta * right.count / merged.count; + merged.sumOfSquaredDeviations = left.sumOfSquaredDeviations + right.sumOfSquaredDeviations + delta * delta * left.count * right.count / merged.count; + return merged; + } + + /** + * Returns the number of samples seen so far. + * + * @return the sample count + */ + public long count() { + return count; + } + + /** + * Tells whether any sample has been added. + * + * @return {@code true} if no sample is currently accounted for + */ + public boolean isEmpty() { + return count == 0; + } + + /** + * Returns the arithmetic mean of the samples. + * + * @return the mean, or {@link Double#NaN} if no sample has been added + */ + public double mean() { + return count == 0 ? Double.NaN : mean; + } + + /** + * Returns the sum of the samples, reconstructed from the mean. + * + * @return {@code count * mean}, or {@code 0} if no sample has been added + */ + public double sum() { + return count == 0 ? 0.0 : mean * count; + } + + /** + * Returns the sum of squared deviations from the mean, {@code M2}. + * + * @return the sum of squared deviations, {@code 0} for an empty accumulator + */ + public double sumOfSquaredDeviations() { + return sumOfSquaredDeviations; + } + + /** + * Returns the unbiased sample variance, normalised by {@code count - 1}. + * + * @return the sample variance, or {@link Double#NaN} if fewer than two samples were added + */ + public double sampleVariance() { + return count < 2 ? Double.NaN : sumOfSquaredDeviations / (count - 1); + } + + /** + * Returns the population variance, normalised by {@code count}. + * + * @return the population variance, or {@link Double#NaN} if no sample has been added + */ + public double populationVariance() { + return count == 0 ? Double.NaN : sumOfSquaredDeviations / count; + } + + /** + * Returns the square root of {@link #sampleVariance()}. + * + * @return the sample standard deviation, or {@link Double#NaN} if fewer than two samples were added + */ + public double sampleStandardDeviation() { + return Math.sqrt(sampleVariance()); + } + + /** + * Returns the square root of {@link #populationVariance()}. + * + * @return the population standard deviation, or {@link Double#NaN} if no sample has been added + */ + public double populationStandardDeviation() { + return Math.sqrt(populationVariance()); + } + + /** + * Returns the standard error of the mean, {@code sampleStandardDeviation / sqrt(count)}. + * + * @return the standard error, or {@link Double#NaN} if fewer than two samples were added + */ + public double standardError() { + return sampleStandardDeviation() / Math.sqrt(count); + } + + /** + * Forgets every sample. + */ + public void clear() { + count = 0; + mean = 0.0; + sumOfSquaredDeviations = 0.0; + } + + @Override + public String toString() { + return "WelfordAlgorithm{count=" + count + ", mean=" + mean() + ", sampleStandardDeviation=" + sampleStandardDeviation() + '}'; + } + + private static void requireFinite(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Samples must be finite, but was " + value); + } + } +} diff --git a/src/main/java/com/thealgorithms/strings/Alphabetical.java b/src/main/java/com/thealgorithms/strings/Alphabetical.java index ef2974eb427d..37b1fb068b44 100644 --- a/src/main/java/com/thealgorithms/strings/Alphabetical.java +++ b/src/main/java/com/thealgorithms/strings/Alphabetical.java @@ -1,32 +1,58 @@ package com.thealgorithms.strings; +import java.util.Locale; + /** - * Utility class for checking if a string's characters are in alphabetical order. + * Utility class for checking whether a string's characters are in non-decreasing + * lexicographical order based on Unicode code points (case-insensitive). + *

+ * This does NOT implement language-aware alphabetical ordering (collation rules). + * It simply compares lowercase Unicode character values. *

- * Alphabetical order is a system whereby character strings are placed in order - * based on the position of the characters in the conventional ordering of an - * alphabet. + * Non-letter characters are not allowed and will cause the check to fail. *

- * Reference: Wikipedia: Alphabetical Order + * Reference: + * Wikipedia: Alphabetical order */ public final class Alphabetical { + private Alphabetical() { } /** - * Checks whether the characters in the given string are in alphabetical order. - * Non-letter characters will cause the check to fail. + * Checks whether the characters in the given string are in non-decreasing + * lexicographical order (case-insensitive). + *

+ * Rules: + *

    + *
  • String must not be null or blank
  • + *
  • All characters must be letters
  • + *
  • Comparison is based on lowercase Unicode values
  • + *
  • Order must be non-decreasing (equal or increasing allowed)
  • + *
* - * @param s the input string - * @return {@code true} if all characters are in alphabetical order (case-insensitive), otherwise {@code false} + * @param s input string + * @return {@code true} if characters are in non-decreasing order, otherwise {@code false} */ public static boolean isAlphabetical(String s) { - s = s.toLowerCase(); - for (int i = 0; i < s.length() - 1; ++i) { - if (!Character.isLetter(s.charAt(i)) || s.charAt(i) > s.charAt(i + 1)) { + if (s == null || s.isBlank()) { + return false; + } + + String normalized = s.toLowerCase(Locale.ROOT); + + if (!Character.isLetter(normalized.charAt(0))) { + return false; + } + + for (int i = 1; i < normalized.length(); i++) { + char prev = normalized.charAt(i - 1); + char curr = normalized.charAt(i); + + if (!Character.isLetter(curr) || prev > curr) { return false; } } - return !s.isEmpty() && Character.isLetter(s.charAt(s.length() - 1)); + return true; } } diff --git a/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java b/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java index cf736dbd8cab..016ee2821a17 100644 --- a/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java +++ b/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java @@ -21,12 +21,19 @@ private AlternativeStringArrange() { /** * Arranges two strings by alternating their characters. + * If one string is longer than the other, the remaining characters of the longer string + * are appended at the end of the result. * - * @param firstString the first input string - * @param secondString the second input string + * @param firstString the first input string, must not be {@code null} + * @param secondString the second input string, must not be {@code null} * @return a new string with characters from both strings arranged alternately + * @throws IllegalArgumentException if {@code firstString} or {@code secondString} is {@code null} */ public static String arrange(String firstString, String secondString) { + if (firstString == null || secondString == null) { + throw new IllegalArgumentException("Input strings must not be null"); + } + StringBuilder result = new StringBuilder(); int length1 = firstString.length(); int length2 = secondString.length(); diff --git a/src/main/java/com/thealgorithms/strings/Anagrams.java b/src/main/java/com/thealgorithms/strings/Anagrams.java index 5b97af0758f2..7bd84d47508f 100644 --- a/src/main/java/com/thealgorithms/strings/Anagrams.java +++ b/src/main/java/com/thealgorithms/strings/Anagrams.java @@ -5,7 +5,7 @@ /** * An anagram is a word or phrase formed by rearranging the letters of a different word or phrase, - * typically using all the original letters exactly once.[1] + * typically using all the original letters exactly once. * For example, the word anagram itself can be rearranged into nag a ram, * also the word binary into brainy and the word adobe into abode. * Reference from https://en.wikipedia.org/wiki/Anagram 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/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/main/java/com/thealgorithms/strings/KMP.java b/src/main/java/com/thealgorithms/strings/KMP.java index 07d3b0415006..0317abe6f39a 100644 --- a/src/main/java/com/thealgorithms/strings/KMP.java +++ b/src/main/java/com/thealgorithms/strings/KMP.java @@ -1,5 +1,8 @@ package com.thealgorithms.strings; +import java.util.ArrayList; +import java.util.List; + /** * Implementation of Knuth–Morris–Pratt algorithm Usage: see the main function * for an example @@ -8,16 +11,19 @@ public final class KMP { private KMP() { } - // a working example - - public static void main(String[] args) { - final String haystack = "AAAAABAAABA"; // This is the full string - final String needle = "AAAA"; // This is the substring that we want to find - kmpMatcher(haystack, needle); - } + /** + * find the starting index in string haystack[] that matches the search word P[] + * + * @param haystack The text to be searched + * @param needle The pattern to be searched for + * @return A list of starting indices where the pattern is found + */ + public static List kmpMatcher(final String haystack, final String needle) { + List occurrences = new ArrayList<>(); + if (haystack == null || needle == null || needle.isEmpty()) { + return occurrences; + } - // find the starting index in string haystack[] that matches the search word P[] - public static void kmpMatcher(final String haystack, final String needle) { final int m = haystack.length(); final int n = needle.length(); final int[] pi = computePrefixFunction(needle); @@ -32,10 +38,11 @@ public static void kmpMatcher(final String haystack, final String needle) { } if (q == n) { - System.out.println("Pattern starts: " + (i + 1 - n)); + occurrences.add(i + 1 - n); q = pi[q - 1]; } } + return occurrences; } // return the prefix function diff --git a/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java b/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java new file mode 100644 index 000000000000..b8b10dcf4538 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/KasaiAlgorithm.java @@ -0,0 +1,79 @@ +package com.thealgorithms.strings; + +/** + * Kasai's Algorithm for constructing the Longest Common Prefix (LCP) array. + * + *

+ * The LCP array stores the lengths of the longest common prefixes between + * lexicographically adjacent suffixes of a string. Kasai's algorithm computes + * this array in O(N) time given the string and its suffix array. + *

+ * + * @see LCP array - Wikipedia + */ +public final class KasaiAlgorithm { + + private KasaiAlgorithm() { + } + + /** + * Computes the LCP array using Kasai's algorithm. + * + * @param text the original string + * @param suffixArr the suffix array of the string + * @return the LCP array of length N, where LCP[i] is the length of the longest + * common prefix of the suffixes indexed by suffixArr[i] and suffixArr[i+1]. + * The last element LCP[N-1] is always 0. + * @throws IllegalArgumentException if text or suffixArr is null, or their lengths differ + */ + public static int[] kasai(String text, int[] suffixArr) { + if (text == null || suffixArr == null) { + throw new IllegalArgumentException("Text and suffix array must not be null."); + } + int n = text.length(); + if (suffixArr.length != n) { + throw new IllegalArgumentException("Suffix array length must match text length."); + } + if (n == 0) { + return new int[0]; + } + + // Compute the inverse suffix array + // invSuff[i] stores the index of the suffix text.substring(i) in the suffix array + int[] invSuff = new int[n]; + for (int i = 0; i < n; i++) { + if (suffixArr[i] < 0 || suffixArr[i] >= n) { + throw new IllegalArgumentException("Suffix array contains out-of-bounds index."); + } + invSuff[suffixArr[i]] = i; + } + + int[] lcp = new int[n]; + int k = 0; // Length of the longest common prefix + + for (int i = 0; i < n; i++) { + // Suffix at index i has not a next suffix in suffix array + int rank = invSuff[i]; + if (rank == n - 1) { + k = 0; + continue; + } + + int nextSuffixIndex = suffixArr[rank + 1]; + + // Directly match characters to find LCP + while (i + k < n && nextSuffixIndex + k < n && text.charAt(i + k) == text.charAt(nextSuffixIndex + k)) { + k++; + } + + lcp[rank] = k; + + // Delete the starting character from the string + if (k > 0) { + k--; + } + } + + return lcp; + } +} diff --git a/src/main/java/com/thealgorithms/strings/LengthOfLastWord.java b/src/main/java/com/thealgorithms/strings/LengthOfLastWord.java new file mode 100644 index 000000000000..7eed59a5ef99 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LengthOfLastWord.java @@ -0,0 +1,51 @@ +package com.thealgorithms.strings; + +/** + * The {@code LengthOfLastWord} class provides a utility method to determine + * the length of the last word in a given string. + * + *

A "word" is defined as a maximal substring consisting of non-space + * characters only. Trailing spaces at the end of the string are ignored. + * + *

Example: + *

{@code
+ * LengthOfLastWord obj = new LengthOfLastWord();
+ * System.out.println(obj.lengthOfLastWord("Hello World"));  // Output: 5
+ * System.out.println(obj.lengthOfLastWord("  fly me   to   the moon  "));  // Output: 4
+ * System.out.println(obj.lengthOfLastWord("luffy is still joyboy"));  // Output: 6
+ * }
+ * + *

This implementation runs in O(n) time complexity, where n is the length + * of the input string, and uses O(1) additional space. + */ +public class LengthOfLastWord { + + /** + * Returns the length of the last word in the specified string. + * + *

The method iterates from the end of the string, skipping trailing + * spaces first, and then counts the number of consecutive non-space characters + * characters until another space (or the beginning of the string) is reached. + * + * @param s the input string to analyze + * @return the length of the last word in {@code s}; returns 0 if there is no word + * @throws NullPointerException if {@code s} is {@code null} + */ + public int lengthOfLastWord(String s) { + int sizeOfString = s.length() - 1; + int lastWordLength = 0; + + // Skip trailing spaces from the end of the string + while (sizeOfString >= 0 && s.charAt(sizeOfString) == ' ') { + sizeOfString--; + } + + // Count the characters of the last word + while (sizeOfString >= 0 && s.charAt(sizeOfString) != ' ') { + lastWordLength++; + sizeOfString--; + } + + return lastWordLength; + } +} diff --git a/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java b/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java new file mode 100644 index 000000000000..27aef64ee883 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java @@ -0,0 +1,55 @@ +package com.thealgorithms.strings; + +/** + * Longest Common Substring finds the longest string that is a + * contiguous substring of two input strings. + * Example: "abcdef" and "zcdemf" -> "cde" + * + * @see + * Wikipedia: Longest Common Substring + * + * author: Vraj Prajapati @Rosander0 + */ +public final class LongestCommonSubstring { + + private LongestCommonSubstring() { + // Utility class + } + + /** + * Finds the longest common substring of two strings. + * + * @param a First input string + * @param b Second input string + * @return The longest common substring, or empty string if none exists. + * If multiple substrings share the maximum length, the first one found is returned. + */ + public static String longestCommonSubstring(final String a, final String b) { + if (a == null || b == null || a.isEmpty() || b.isEmpty()) { + return ""; + } + + int[] dp = new int[b.length() + 1]; + int maxLength = 0; + int endIndex = 0; + + for (int i = 1; i <= a.length(); i++) { + for (int j = b.length(); j >= 1; j--) { + if (a.charAt(i - 1) == b.charAt(j - 1)) { + dp[j] = dp[j - 1] + 1; + if (dp[j] > maxLength) { + maxLength = dp[j]; + endIndex = i; + } + } else { + dp[j] = 0; + } + } + } + + if (maxLength == 0) { + return ""; + } + return a.substring(endIndex - maxLength, endIndex); + } +} diff --git a/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java b/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java index 6808cd50602f..51e8dc6b02c3 100644 --- a/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java +++ b/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java @@ -13,6 +13,12 @@ private LongestNonRepetitiveSubstring() { /** * Finds the length of the longest substring without repeating characters. * + * Uses the sliding window technique with a HashMap to track + * the last seen index of each character. + * + * Time Complexity: O(n), where n is the length of the input string. + * Space Complexity: O(min(n, m)), where m is the size of the character set. + * * @param s the input string * @return the length of the longest non-repetitive substring */ diff --git a/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java b/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java deleted file mode 100644 index ca500357ba77..000000000000 --- a/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.thealgorithms.strings; - -final class LongestPalindromicSubstring { - private LongestPalindromicSubstring() { - } - - /** - * Finds the longest palindromic substring in the given string. - * - * @param s the input string - * @return the longest palindromic substring - */ - public static String longestPalindrome(String s) { - if (s == null || s.isEmpty()) { - return ""; - } - String maxStr = ""; - for (int i = 0; i < s.length(); ++i) { - for (int j = i; j < s.length(); ++j) { - if (isValid(s, i, j) && (j - i + 1 > maxStr.length())) { - maxStr = s.substring(i, j + 1); - } - } - } - return maxStr; - } - - private static boolean isValid(String s, int lo, int hi) { - int n = hi - lo + 1; - for (int i = 0; i < n / 2; ++i) { - if (s.charAt(lo + i) != s.charAt(hi - i)) { - return false; - } - } - return true; - } -} diff --git a/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java b/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java new file mode 100644 index 000000000000..87c9278fd4bf --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java @@ -0,0 +1,83 @@ +package com.thealgorithms.strings; + +/** + * Finds the longest substring that occurs at least twice in a given string. + * + *

Uses the suffix array (via {@link SuffixArray}) and Kasai's algorithm + * to build the LCP (Longest Common Prefix) array, then returns the substring + * corresponding to the maximum LCP value.

+ * + *

Time complexity: O(n logΒ² n) for suffix array construction + O(n) for LCP.

+ * + * @see Longest repeated substring problem + * @see SuffixArray + */ +public final class LongestRepeatedSubstring { + + private LongestRepeatedSubstring() { + } + + /** + * Returns the longest substring that appears at least twice in the given text. + * + * @param text the input string + * @return the longest repeated substring, or an empty string if none exists + */ + public static String longestRepeatedSubstring(String text) { + if (text == null || text.length() <= 1) { + return ""; + } + + final int[] suffixArray = SuffixArray.buildSuffixArray(text); + final int[] lcp = buildLcpArray(text, suffixArray); + + int maxLen = 0; + int maxIdx = 0; + for (int i = 0; i < lcp.length; i++) { + if (lcp[i] > maxLen) { + maxLen = lcp[i]; + maxIdx = suffixArray[i + 1]; + } + } + + return text.substring(maxIdx, maxIdx + maxLen); + } + + /** + * Builds the LCP (Longest Common Prefix) array using Kasai's algorithm. + * + *

LCP[i] is the length of the longest common prefix between the suffixes + * at positions suffixArray[i] and suffixArray[i+1] in sorted order.

+ * + * @param text the original string + * @param suffixArray the suffix array of the string + * @return the LCP array of length n-1 + */ + static int[] buildLcpArray(String text, int[] suffixArray) { + final int n = text.length(); + final int[] rank = new int[n]; + final int[] lcp = new int[n - 1]; + + for (int i = 0; i < n; i++) { + rank[suffixArray[i]] = i; + } + + int k = 0; + for (int i = 0; i < n; i++) { + if (rank[i] == n - 1) { + k = 0; + continue; + } + final int j = suffixArray[rank[i] + 1]; + while (i + k < n && j + k < n && text.charAt(i + k) == text.charAt(j + k)) { + k++; + } + lcp[rank[i]] = k; + if (k > 0) { + k--; + } + } + + return lcp; + } +} 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/main/java/com/thealgorithms/strings/MoveHashToEnd.java b/src/main/java/com/thealgorithms/strings/MoveHashToEnd.java new file mode 100644 index 000000000000..bd686a0c5ba0 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/MoveHashToEnd.java @@ -0,0 +1,56 @@ +package com.thealgorithms.strings; + +/** + * Moves all '#' characters to the end of the given string while preserving + * the order of the other characters. + * + * Example: + * Input : "h#e#l#llo" + * Output : "helllo###" + * + * The algorithm works by iterating through the string and collecting + * all non-# characters first, then filling the remaining positions + * with '#'. + * + * Time Complexity: O(n) + * Space Complexity: O(n) + * + * @see Move all special characters to end - GeeksForGeeks + */ +public final class MoveHashToEnd { + + /** + * Private constructor to prevent instantiation of utility class. + */ + private MoveHashToEnd() { + } + + /** + * Moves all '#' characters in the input string to the end. + * + * @param str the input string containing characters and '#' + * @return a new string with all '#' characters moved to the end + */ + public static String moveHashToEnd(String str) { + if (str == null || str.isEmpty()) { + return str; + } + + char[] arr = str.toCharArray(); + int insertPos = 0; + + // Place all non-# characters at the beginning + for (char ch : arr) { + if (ch != '#') { + arr[insertPos++] = ch; + } + } + + // Fill remaining positions with '#' + while (insertPos < arr.length) { + arr[insertPos++] = '#'; + } + + return new String(arr); + } +} diff --git a/src/main/java/com/thealgorithms/strings/MyAtoi.java b/src/main/java/com/thealgorithms/strings/MyAtoi.java index 5a7c2ce53b1c..92de4039a582 100644 --- a/src/main/java/com/thealgorithms/strings/MyAtoi.java +++ b/src/main/java/com/thealgorithms/strings/MyAtoi.java @@ -45,7 +45,9 @@ public static int myAtoi(String s) { int number = 0; while (index < length) { char ch = s.charAt(index); - if (!Character.isDigit(ch)) { + + // Accept only ASCII digits + if (ch < '0' || ch > '9') { break; } diff --git a/src/main/java/com/thealgorithms/strings/Palindrome.java b/src/main/java/com/thealgorithms/strings/Palindrome.java index 3567a371d70e..64de657df359 100644 --- a/src/main/java/com/thealgorithms/strings/Palindrome.java +++ b/src/main/java/com/thealgorithms/strings/Palindrome.java @@ -2,6 +2,13 @@ /** * Wikipedia: https://en.wikipedia.org/wiki/Palindrome + * + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ final class Palindrome { private Palindrome() { diff --git a/src/main/java/com/thealgorithms/strings/RabinKarp.java b/src/main/java/com/thealgorithms/strings/RabinKarp.java index bb8df3358453..be17f87c3656 100644 --- a/src/main/java/com/thealgorithms/strings/RabinKarp.java +++ b/src/main/java/com/thealgorithms/strings/RabinKarp.java @@ -1,32 +1,30 @@ package com.thealgorithms.strings; -import java.util.Scanner; +import java.util.ArrayList; +import java.util.List; /** * @author Prateek Kumar Oraon (https://github.com/prateekKrOraon) * - An implementation of Rabin-Karp string matching algorithm - Program will simply end if there is no match + * An implementation of Rabin-Karp string matching algorithm + * Program will simply end if there is no match */ public final class RabinKarp { private RabinKarp() { } - public static Scanner scanner = null; - public static final int ALPHABET_SIZE = 256; + private static final int ALPHABET_SIZE = 256; - public static void main(String[] args) { - scanner = new Scanner(System.in); - System.out.println("Enter String"); - String text = scanner.nextLine(); - System.out.println("Enter pattern"); - String pattern = scanner.nextLine(); - - int q = 101; - searchPat(text, pattern, q); + public static List search(String text, String pattern) { + return search(text, pattern, 101); } - private static void searchPat(String text, String pattern, int q) { + public static List search(String text, String pattern, int q) { + List occurrences = new ArrayList<>(); + if (text == null || pattern == null || pattern.isEmpty()) { + return occurrences; + } + int m = pattern.length(); int n = text.length(); int t = 0; @@ -35,48 +33,42 @@ private static void searchPat(String text, String pattern, int q) { int j = 0; int i = 0; - h = (int) Math.pow(ALPHABET_SIZE, m - 1) % q; + if (m > n) { + return new ArrayList<>(); + } + + // h = pow(ALPHABET_SIZE, m-1) % q + for (i = 0; i < m - 1; i++) { + h = h * ALPHABET_SIZE % q; + } for (i = 0; i < m; i++) { - // hash value is calculated for each character and then added with the hash value of the - // next character for pattern as well as the text for length equal to the length of - // pattern p = (ALPHABET_SIZE * p + pattern.charAt(i)) % q; t = (ALPHABET_SIZE * t + text.charAt(i)) % q; } for (i = 0; i <= n - m; i++) { - // if the calculated hash value of the pattern and text matches then - // all the characters of the pattern is matched with the text of length equal to length - // of the pattern if all matches then pattern exist in string if not then the hash value - // of the first character of the text is subtracted and hash value of the next character - // after the end of the evaluated characters is added if (p == t) { - // if hash value matches then the individual characters are matched for (j = 0; j < m; j++) { - // if not matched then break out of the loop if (text.charAt(i + j) != pattern.charAt(j)) { break; } } - // if all characters are matched then pattern exist in the string if (j == m) { - System.out.println("Pattern found at index " + i); + occurrences.add(i); } } - // if i 0) { + result.deleteCharAt(result.length() - 1); + } + } else { + result.append(c); + } + } + return result.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/strings/ReverseString.java b/src/main/java/com/thealgorithms/strings/ReverseString.java index 7b918ebe1a59..e373dd0b7174 100644 --- a/src/main/java/com/thealgorithms/strings/ReverseString.java +++ b/src/main/java/com/thealgorithms/strings/ReverseString.java @@ -62,7 +62,7 @@ public static String reverse3(String string) { /** * Reverses the given string using a stack. * This method uses a stack to reverse the characters of the string. - * * @param str The input string to be reversed. + * @param str The input string to be reversed. * @return The reversed string. */ public static String reverseStringUsingStack(String str) { diff --git a/src/main/java/com/thealgorithms/strings/TitleCase.java b/src/main/java/com/thealgorithms/strings/TitleCase.java new file mode 100644 index 000000000000..eb6c73623681 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/TitleCase.java @@ -0,0 +1,54 @@ +package com.thealgorithms.strings; + +/** + * Title Case converts a string so that the first letter of each word + * is capitalized and the rest are lowercase. + * Example: "the quick brown fox" -> "The Quick Brown Fox" + * + * @see + * Wikipedia: Title Case + */ +public final class TitleCase { + + private TitleCase() { + // Utility class + } + + /** + * Converts a string to title case. + * + * @param input The string to convert + * @return The title-cased string, or empty string if input is null/empty. + * If input contains only whitespace, it is returned as is. + */ + public static String toTitleCase(final String input) { + if (input == null || input.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(input.length()); + boolean capitalizeNext = true; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (Character.isWhitespace(c)) { + capitalizeNext = true; + result.append(c); + continue; + } + + if (capitalizeNext) { + if (Character.isLetter(c)) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + // Keep capitalizeNext=true so the first *letter* after + // punctuation/digits is capitalized. + result.append(c); + } + } else { + result.append(Character.toLowerCase(c)); + } + } + return result.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/strings/TopKFrequentWords.java b/src/main/java/com/thealgorithms/strings/TopKFrequentWords.java new file mode 100644 index 000000000000..106de304cf40 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/TopKFrequentWords.java @@ -0,0 +1,56 @@ +package com.thealgorithms.strings; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility class to find the top-k most frequent words. + * + *

Words are ranked by frequency in descending order. For equal frequencies, + * words are ranked in lexicographical ascending order. + * + *

Reference: + * https://en.wikipedia.org/wiki/Top-k_problem + * + */ +public final class TopKFrequentWords { + private TopKFrequentWords() { + } + + /** + * Finds the k most frequent words. + * + * @param words input array of words + * @param k number of words to return + * @return list of top-k words ordered by frequency then lexicographical order + * @throws IllegalArgumentException if words is null, k is negative, or words contains null + */ + public static List findTopKFrequentWords(String[] words, int k) { + if (words == null) { + throw new IllegalArgumentException("Input words array cannot be null."); + } + if (k < 0) { + throw new IllegalArgumentException("k cannot be negative."); + } + if (k == 0 || words.length == 0) { + return List.of(); + } + + Map frequency = new HashMap<>(); + for (String word : words) { + if (word == null) { + throw new IllegalArgumentException("Input words cannot contain null values."); + } + frequency.put(word, frequency.getOrDefault(word, 0) + 1); + } + + List candidates = new ArrayList<>(frequency.keySet()); + candidates.sort(Comparator.comparingInt(frequency::get).reversed().thenComparing(Comparator.naturalOrder())); + + int limit = Math.min(k, candidates.size()); + return new ArrayList<>(candidates.subList(0, limit)); + } +} diff --git a/src/main/java/com/thealgorithms/strings/Upper.java b/src/main/java/com/thealgorithms/strings/Upper.java index 5e248cb6ee39..85db7d41e1aa 100644 --- a/src/main/java/com/thealgorithms/strings/Upper.java +++ b/src/main/java/com/thealgorithms/strings/Upper.java @@ -15,23 +15,27 @@ public static void main(String[] args) { } /** - * Converts all the characters in this {@code String} to upper case + * Converts all the characters in this {@code String} to upper case. * * @param s the string to convert * @return the {@code String}, converted to uppercase. */ public static String toUpperCase(String s) { if (s == null) { - throw new IllegalArgumentException("Input string connot be null"); + throw new IllegalArgumentException("Input string cannot be null"); } if (s.isEmpty()) { return s; } - StringBuilder result = new StringBuilder(s); - for (int i = 0; i < result.length(); ++i) { - char currentChar = result.charAt(i); - if (Character.isLetter(currentChar) && Character.isLowerCase(currentChar)) { - result.setCharAt(i, Character.toUpperCase(currentChar)); + + StringBuilder result = new StringBuilder(s.length()); + + for (int i = 0; i < s.length(); ++i) { + char currentChar = s.charAt(i); + if (Character.isLowerCase(currentChar)) { + result.append(Character.toUpperCase(currentChar)); + } else { + result.append(currentChar); } } return result.toString(); diff --git a/src/main/java/com/thealgorithms/strings/ValidParentheses.java b/src/main/java/com/thealgorithms/strings/ValidParentheses.java deleted file mode 100644 index 25a72f379dec..000000000000 --- a/src/main/java/com/thealgorithms/strings/ValidParentheses.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.thealgorithms.strings; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Map; - -/** - * Validates if a given string has valid matching parentheses. - *

- * A string is considered valid if: - *

    - *
  • Open brackets are closed by the same type of brackets.
  • - *
  • Brackets are closed in the correct order.
  • - *
  • Every closing bracket has a corresponding open bracket of the same type.
  • - *
- * - * Allowed characters: '(', ')', '{', '}', '[', ']' - */ -public final class ValidParentheses { - private ValidParentheses() { - } - - private static final Map BRACKET_PAIRS = Map.of(')', '(', '}', '{', ']', '['); - - /** - * Checks if the input string has valid parentheses. - * - * @param s the string containing only bracket characters - * @return true if valid, false otherwise - * @throws IllegalArgumentException if the string contains invalid characters or is null - */ - public static boolean isValid(String s) { - if (s == null) { - throw new IllegalArgumentException("Input string cannot be null"); - } - - Deque stack = new ArrayDeque<>(); - - for (char c : s.toCharArray()) { - if (BRACKET_PAIRS.containsValue(c)) { - stack.push(c); // opening bracket - } else if (BRACKET_PAIRS.containsKey(c)) { - if (stack.isEmpty() || stack.pop() != BRACKET_PAIRS.get(c)) { - return false; - } - } else { - throw new IllegalArgumentException("Unexpected character: " + c); - } - } - - return stack.isEmpty(); - } -} diff --git a/src/main/java/com/thealgorithms/strings/ZAlgorithm.java b/src/main/java/com/thealgorithms/strings/ZAlgorithm.java new file mode 100644 index 000000000000..dc029b751f45 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/ZAlgorithm.java @@ -0,0 +1,48 @@ +/* + * https://en.wikipedia.org/wiki/Z-algorithm + */ +package com.thealgorithms.strings; + +public final class ZAlgorithm { + + private ZAlgorithm() { + throw new UnsupportedOperationException("Utility class"); + } + + public static int[] zFunction(String s) { + int n = s.length(); + int[] z = new int[n]; + int l = 0; + int r = 0; + + for (int i = 1; i < n; i++) { + if (i <= r) { + z[i] = Math.min(r - i + 1, z[i - l]); + } + + while (i + z[i] < n && s.charAt(z[i]) == s.charAt(i + z[i])) { + z[i]++; + } + + if (i + z[i] - 1 > r) { + l = i; + r = i + z[i] - 1; + } + } + + return z; + } + + public static int search(String text, String pattern) { + String s = pattern + "$" + text; + int[] z = zFunction(s); + int p = pattern.length(); + + for (int i = 0; i < z.length; i++) { + if (z[i] == p) { + return i - p - 1; + } + } + return -1; + } +} diff --git a/src/main/java/com/thealgorithms/strings/zigZagPattern/README.md b/src/main/java/com/thealgorithms/strings/zigZagPattern/README.md index 17e5ba6e26d8..a8689351f359 100644 --- a/src/main/java/com/thealgorithms/strings/zigZagPattern/README.md +++ b/src/main/java/com/thealgorithms/strings/zigZagPattern/README.md @@ -22,14 +22,14 @@ for example : # working if string size is smaller than numRows or numRows is smaller than 2 - than we can return string because it will make no changes to string. + then we can return string because it will make no changes to string. If not than we initiate three variable depth which is equalvalent to numRows , height which starts with 1 and start with starting index of string. - than we generate spaces to skip using formula 2 + (( n - 1 ) * 2 ) + then we generate spaces to skip using formula 2 + (( n - 1 ) * 2 ) for both height and depth - with each iteration we decrement depth and increate height and start - by one also we keep contantating character on to new string with first + with each iteration we decrement depth and increase height and start + by one also we keep concatenating character on to new string with first depth spaces and later height spaces that we generated using formula if not zero diff --git a/src/test/java/com/thealgorithms/backtracking/CombinationTest.java b/src/test/java/com/thealgorithms/backtracking/CombinationTest.java index a9d1163f3ecd..5d2f99ccadf8 100644 --- a/src/test/java/com/thealgorithms/backtracking/CombinationTest.java +++ b/src/test/java/com/thealgorithms/backtracking/CombinationTest.java @@ -28,16 +28,16 @@ void testNoElement() { @Test void testLengthOne() { List> result = Combination.combination(new Integer[] {1, 2}, 1); - assertTrue(result.get(0).iterator().next() == 1); - assertTrue(result.get(1).iterator().next() == 2); + assertEquals(1, result.get(0).iterator().next()); + assertEquals(2, result.get(1).iterator().next()); } @Test void testLengthTwo() { List> result = Combination.combination(new Integer[] {1, 2}, 2); Integer[] arr = result.get(0).toArray(new Integer[2]); - assertTrue(arr[0] == 1); - assertTrue(arr[1] == 2); + assertEquals(1, arr[0]); + assertEquals(2, arr[1]); } @Test diff --git a/src/test/java/com/thealgorithms/backtracking/PermutationTest.java b/src/test/java/com/thealgorithms/backtracking/PermutationTest.java index 76a714829109..54747e5e73a1 100644 --- a/src/test/java/com/thealgorithms/backtracking/PermutationTest.java +++ b/src/test/java/com/thealgorithms/backtracking/PermutationTest.java @@ -12,13 +12,13 @@ public class PermutationTest { @Test void testNoElement() { List result = Permutation.permutation(new Integer[] {}); - assertEquals(result.get(0).length, 0); + assertEquals(0, result.get(0).length); } @Test void testSingleElement() { List result = Permutation.permutation(new Integer[] {1}); - assertEquals(result.get(0)[0], 1); + assertEquals(1, result.get(0)[0]); } @Test diff --git a/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java b/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java new file mode 100644 index 000000000000..ecd1f3c4dfae --- /dev/null +++ b/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java @@ -0,0 +1,99 @@ +package com.thealgorithms.backtracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class RatInAMazeTest { + + @Test + void testMultiplePathsExist() { + int[][] maze = {{1, 0, 0, 0}, {1, 1, 0, 1}, {0, 1, 0, 0}, {0, 1, 1, 1}}; + + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() >= 1); + for (String path : paths) { + assertTrue(path.chars().allMatch(c -> "DLRU".indexOf(c) >= 0)); + } + } + + @Test + void testSinglePath() { + int[][] maze = {{1, 0, 0}, {1, 1, 0}, {0, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertEquals(1, paths.size()); + assertEquals("DRDR", paths.get(0)); + } + + @Test + void testNoPathExists() { + int[][] maze = {{1, 0, 0}, {0, 0, 0}, {0, 0, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testSourceBlocked() { + int[][] maze = {{0, 1}, {1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testDestinationBlocked() { + int[][] maze = {{1, 1}, {1, 0}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testSingleCellMazeOpen() { + int[][] maze = {{1}}; + List paths = RatInAMaze.findPaths(maze); + assertEquals(1, paths.size()); + assertEquals("", paths.get(0)); + } + + @Test + void testSingleCellMazeBlocked() { + int[][] maze = {{0}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testNullMazeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(null)); + } + + @Test + void testEmptyMazeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(new int[][] {})); + } + + @Test + void testNonSquareMazeThrowsException() { + int[][] maze = {{1, 0, 1}, {1, 1, 1}}; + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(maze)); + } + + @Test + void testAllCellsOpen() { + int[][] maze = {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() > 1); + } + + @Test + void testLargerMazeWithPath() { + int[][] maze = {{1, 1, 1, 1}, {0, 1, 0, 1}, {0, 1, 0, 1}, {0, 1, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() >= 1); + for (String path : paths) { + assertTrue(path.chars().allMatch(c -> "DLRU".indexOf(c) >= 0), "Path contains invalid characters: " + path); + } + } +} diff --git a/src/test/java/com/thealgorithms/backtracking/SudokuSolverTest.java b/src/test/java/com/thealgorithms/backtracking/SudokuSolverTest.java new file mode 100644 index 000000000000..75d3eae08629 --- /dev/null +++ b/src/test/java/com/thealgorithms/backtracking/SudokuSolverTest.java @@ -0,0 +1,53 @@ +package com.thealgorithms.backtracking; + +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; + +class SudokuSolverTest { + + @Test + void testSolveSudokuEasyPuzzle() { + int[][] board = {{5, 3, 0, 0, 7, 0, 0, 0, 0}, {6, 0, 0, 1, 9, 5, 0, 0, 0}, {0, 9, 8, 0, 0, 0, 0, 6, 0}, {8, 0, 0, 0, 6, 0, 0, 0, 3}, {4, 0, 0, 8, 0, 3, 0, 0, 1}, {7, 0, 0, 0, 2, 0, 0, 0, 6}, {0, 6, 0, 0, 0, 0, 2, 8, 0}, {0, 0, 0, 4, 1, 9, 0, 0, 5}, {0, 0, 0, 0, 8, 0, 0, 7, 9}}; + + assertTrue(SudokuSolver.solveSudoku(board)); + + int[][] expected = {{5, 3, 4, 6, 7, 8, 9, 1, 2}, {6, 7, 2, 1, 9, 5, 3, 4, 8}, {1, 9, 8, 3, 4, 2, 5, 6, 7}, {8, 5, 9, 7, 6, 1, 4, 2, 3}, {4, 2, 6, 8, 5, 3, 7, 9, 1}, {7, 1, 3, 9, 2, 4, 8, 5, 6}, {9, 6, 1, 5, 3, 7, 2, 8, 4}, {2, 8, 7, 4, 1, 9, 6, 3, 5}, {3, 4, 5, 2, 8, 6, 1, 7, 9}}; + + assertArrayEquals(expected, board); + } + + @Test + void testSolveSudokuHardPuzzle() { + int[][] board = {{0, 0, 0, 0, 0, 0, 6, 8, 0}, {0, 0, 0, 0, 7, 3, 0, 0, 9}, {3, 0, 9, 0, 0, 0, 0, 4, 5}, {4, 9, 0, 0, 0, 0, 0, 0, 0}, {8, 0, 3, 0, 5, 0, 9, 0, 2}, {0, 0, 0, 0, 0, 0, 0, 3, 6}, {9, 6, 0, 0, 0, 0, 3, 0, 8}, {7, 0, 0, 6, 8, 0, 0, 0, 0}, {0, 2, 8, 0, 0, 0, 0, 0, 0}}; + + assertTrue(SudokuSolver.solveSudoku(board)); + } + + @Test + void testSolveSudokuAlreadySolved() { + int[][] board = {{5, 3, 4, 6, 7, 8, 9, 1, 2}, {6, 7, 2, 1, 9, 5, 3, 4, 8}, {1, 9, 8, 3, 4, 2, 5, 6, 7}, {8, 5, 9, 7, 6, 1, 4, 2, 3}, {4, 2, 6, 8, 5, 3, 7, 9, 1}, {7, 1, 3, 9, 2, 4, 8, 5, 6}, {9, 6, 1, 5, 3, 7, 2, 8, 4}, {2, 8, 7, 4, 1, 9, 6, 3, 5}, {3, 4, 5, 2, 8, 6, 1, 7, 9}}; + + assertTrue(SudokuSolver.solveSudoku(board)); + } + + @Test + void testSolveSudokuInvalidSize() { + int[][] board = {{1, 2, 3}, {4, 5, 6}}; + assertFalse(SudokuSolver.solveSudoku(board)); + } + + @Test + void testSolveSudokuNullBoard() { + assertFalse(SudokuSolver.solveSudoku(null)); + } + + @Test + void testSolveSudokuEmptyBoard() { + int[][] board = {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}}; + + assertTrue(SudokuSolver.solveSudoku(board)); + } +} diff --git a/src/test/java/com/thealgorithms/backtracking/UniquePermutationTest.java b/src/test/java/com/thealgorithms/backtracking/UniquePermutationTest.java new file mode 100644 index 000000000000..c8e7cd0af0dd --- /dev/null +++ b/src/test/java/com/thealgorithms/backtracking/UniquePermutationTest.java @@ -0,0 +1,31 @@ +package com.thealgorithms.backtracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class UniquePermutationTest { + + @Test + void testUniquePermutationsAab() { + List expected = Arrays.asList("AAB", "ABA", "BAA"); + List result = UniquePermutation.generateUniquePermutations("AAB"); + assertEquals(expected, result); + } + + @Test + void testUniquePermutationsAbc() { + List expected = Arrays.asList("ABC", "ACB", "BAC", "BCA", "CAB", "CBA"); + List result = UniquePermutation.generateUniquePermutations("ABC"); + assertEquals(expected, result); + } + + @Test + void testEmptyString() { + List expected = Arrays.asList(""); + List result = UniquePermutation.generateUniquePermutations(""); + assertEquals(expected, result); + } +} diff --git a/src/test/java/com/thealgorithms/bitmanipulation/CountSetBitsTest.java b/src/test/java/com/thealgorithms/bitmanipulation/CountSetBitsTest.java index 61e0757f9c12..757c6edc0151 100644 --- a/src/test/java/com/thealgorithms/bitmanipulation/CountSetBitsTest.java +++ b/src/test/java/com/thealgorithms/bitmanipulation/CountSetBitsTest.java @@ -1,26 +1,56 @@ package com.thealgorithms.bitmanipulation; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; -public class CountSetBitsTest { +class CountSetBitsTest { @Test - void testSetBits() { - CountSetBits csb = new CountSetBits(); - assertEquals(1L, csb.countSetBits(16)); - assertEquals(4, csb.countSetBits(15)); - assertEquals(5, csb.countSetBits(10000)); - assertEquals(5, csb.countSetBits(31)); + void testCountSetBitsZero() { + assertEquals(0, CountSetBits.countSetBits(0)); } @Test - void testSetBitsLookupApproach() { - CountSetBits csb = new CountSetBits(); - assertEquals(1L, csb.lookupApproach(16)); - assertEquals(4, csb.lookupApproach(15)); - assertEquals(5, csb.lookupApproach(10000)); - assertEquals(5, csb.lookupApproach(31)); + void testCountSetBitsOne() { + assertEquals(1, CountSetBits.countSetBits(1)); + } + + @Test + void testCountSetBitsSmallNumber() { + assertEquals(4, CountSetBits.countSetBits(3)); // 1(1) + 10(1) + 11(2) = 4 + } + + @Test + void testCountSetBitsFive() { + assertEquals(7, CountSetBits.countSetBits(5)); // 1 + 1 + 2 + 1 + 2 = 7 + } + + @Test + void testCountSetBitsTen() { + assertEquals(17, CountSetBits.countSetBits(10)); + } + + @Test + void testCountSetBitsLargeNumber() { + assertEquals(42, CountSetBits.countSetBits(20)); // Changed from 93 to 42 + } + + @Test + void testCountSetBitsPowerOfTwo() { + assertEquals(13, CountSetBits.countSetBits(8)); // Changed from 9 to 13 + } + + @Test + void testCountSetBitsNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> CountSetBits.countSetBits(-1)); + } + + @Test + void testNaiveApproachMatchesOptimized() { + for (int i = 0; i <= 100; i++) { + assertEquals(CountSetBits.countSetBitsNaive(i), CountSetBits.countSetBits(i), "Mismatch at n = " + i); + } } } diff --git a/src/test/java/com/thealgorithms/ciphers/CaesarTest.java b/src/test/java/com/thealgorithms/ciphers/CaesarTest.java index 7aa41c4cf423..c8b20ad8d8f9 100644 --- a/src/test/java/com/thealgorithms/ciphers/CaesarTest.java +++ b/src/test/java/com/thealgorithms/ciphers/CaesarTest.java @@ -32,6 +32,29 @@ void caesarDecryptTest() { assertEquals("Encrypt this text", cipherText); } + @Test + void caesarEncryptWithNegativeShiftTest() { + // a shift of -1 must wrap 'A' backwards to 'Z', like a shift of +25 would + assertEquals("Z", caesar.encode("A", -1)); + assertEquals("z", caesar.encode("a", -1)); + assertEquals("EBIIL", caesar.encode("HELLO", -3)); + } + + @Test + void caesarDecryptWithNegativeShiftTest() { + assertEquals("A", caesar.decode("Z", -1)); + assertEquals("HELLO", caesar.decode("EBIIL", -3)); + } + + @Test + void caesarNegativeShiftRoundTripTest() { + // encode followed by decode with the same shift must return the original text + for (int shift : new int[] {-1, -5, -25, -26, -27, -52}) { + String message = "The quick brown Fox"; + assertEquals(message, caesar.decode(caesar.encode(message, shift), shift)); + } + } + @Test void caesarBruteForce() { // given diff --git a/src/test/java/com/thealgorithms/ciphers/ECCTest.java b/src/test/java/com/thealgorithms/ciphers/ECCTest.java index 701f801af1c8..b78ba51f7c3e 100644 --- a/src/test/java/com/thealgorithms/ciphers/ECCTest.java +++ b/src/test/java/com/thealgorithms/ciphers/ECCTest.java @@ -37,7 +37,7 @@ void testEncrypt() { System.out.println("Base Point G: " + curve.getBasePoint()); // Verify that the ciphertext is not empty - assertEquals(cipherText.length, 2); // Check if the ciphertext contains two points (R and S) + assertEquals(2, cipherText.length); // Check if the ciphertext contains two points (R and S) // Output the encrypted coordinate points System.out.println("Encrypted Points:"); diff --git a/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java new file mode 100644 index 000000000000..63dec4846bbc --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java @@ -0,0 +1,145 @@ +package com.thealgorithms.ciphers; + +import java.math.BigInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Unit tests for ElGamalCipher. + * Includes property-based testing (homomorphism), probabilistic checks, + * and boundary validation. + */ +class ElGamalCipherTest { + + private static ElGamalCipher.KeyPair sharedKeys; + + @BeforeAll + static void setup() { + // Generate 256-bit keys for efficient unit testing + sharedKeys = ElGamalCipher.generateKeys(256); + } + + @Test + @DisplayName("Test Key Generation Validity") + void testKeyGeneration() { + Assertions.assertNotNull(sharedKeys.p()); + Assertions.assertNotNull(sharedKeys.g()); + Assertions.assertNotNull(sharedKeys.x()); + Assertions.assertNotNull(sharedKeys.y()); + + // Verify generator bounds: 1 < g < p + Assertions.assertTrue(sharedKeys.g().compareTo(BigInteger.ONE) > 0); + Assertions.assertTrue(sharedKeys.g().compareTo(sharedKeys.p()) < 0); + + // Verify private key bounds: 1 < x < p-1 + Assertions.assertTrue(sharedKeys.x().compareTo(BigInteger.ONE) > 0); + Assertions.assertTrue(sharedKeys.x().compareTo(sharedKeys.p().subtract(BigInteger.ONE)) < 0); + } + + @Test + @DisplayName("Security Check: Probabilistic Encryption") + void testSemanticSecurity() { + // Encrypting the same message twice MUST yield different ciphertexts + // due to the random ephemeral key 'k'. + BigInteger message = new BigInteger("123456789"); + + ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Check that the ephemeral keys (and thus 'a' components) were different + Assertions.assertNotEquals(c1.a(), c2.a(), "Ciphertexts must be randomized (Semantic Security violation)"); + Assertions.assertNotEquals(c1.b(), c2.b()); + + // But both must decrypt to the original message + Assertions.assertEquals(ElGamalCipher.decrypt(c1, sharedKeys.x(), sharedKeys.p()), message); + Assertions.assertEquals(ElGamalCipher.decrypt(c2, sharedKeys.x(), sharedKeys.p()), message); + } + + @ParameterizedTest + @MethodSource("provideMessages") + @DisplayName("Parameterized Test: Encrypt and Decrypt various messages") + void testEncryptDecrypt(String messageStr) { + BigInteger message = new BigInteger(messageStr.getBytes()); + + // Skip if message exceeds the test key size (256 bits) + if (message.compareTo(sharedKeys.p()) >= 0) { + return; + } + + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(message, decrypted, "Decrypted BigInteger must match original"); + Assertions.assertEquals(messageStr, new String(decrypted.toByteArray()), "Decrypted string must match original"); + } + + static Stream provideMessages() { + return Stream.of("Hello World", "TheAlgorithms", "A", "1234567890", "!@#$%^&*()"); + } + + @Test + @DisplayName("Edge Case: Message equals 0") + void testMessageZero() { + BigInteger zero = BigInteger.ZERO; + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(zero, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(zero, decrypted, "Should successfully encrypt/decrypt zero"); + } + + @Test + @DisplayName("Edge Case: Message equals p-1") + void testMessageMaxBound() { + BigInteger pMinus1 = sharedKeys.p().subtract(BigInteger.ONE); + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(pMinus1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(pMinus1, decrypted, "Should successfully encrypt/decrypt p-1"); + } + + @Test + @DisplayName("Negative Test: Message >= p should fail") + void testMessageTooLarge() { + BigInteger tooLarge = sharedKeys.p(); + Assertions.assertThrows(IllegalArgumentException.class, () -> ElGamalCipher.encrypt(tooLarge, sharedKeys.p(), sharedKeys.g(), sharedKeys.y())); + } + + @Test + @DisplayName("Negative Test: Decrypt with wrong private key") + void testWrongKeyDecryption() { + BigInteger message = new BigInteger("99999"); + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Generate a fake private key + BigInteger wrongX = sharedKeys.x().add(BigInteger.ONE); + + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, wrongX, sharedKeys.p()); + + Assertions.assertNotEquals(message, decrypted, "Decryption with wrong key must yield incorrect result"); + } + + @Test + @DisplayName("Property Test: Multiplicative Homomorphism") + void testHomomorphism() { + BigInteger m1 = new BigInteger("50"); + BigInteger m2 = BigInteger.TEN; // Fix: Replaced new BigInteger("10") with BigInteger.TEN + + ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(m1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(m2, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Multiply ciphertexts component-wise: (a1*a2, b1*b2) + BigInteger aNew = c1.a().multiply(c2.a()).mod(sharedKeys.p()); + BigInteger bNew = c1.b().multiply(c2.b()).mod(sharedKeys.p()); + ElGamalCipher.CipherText cCombined = new ElGamalCipher.CipherText(aNew, bNew); + + BigInteger decrypted = ElGamalCipher.decrypt(cCombined, sharedKeys.x(), sharedKeys.p()); + BigInteger expected = m1.multiply(m2).mod(sharedKeys.p()); + + Assertions.assertEquals(expected, decrypted, "Cipher must satisfy multiplicative homomorphism"); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/HorsSignatureTest.java b/src/test/java/com/thealgorithms/ciphers/HorsSignatureTest.java new file mode 100644 index 000000000000..bb22a24a2a62 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/HorsSignatureTest.java @@ -0,0 +1,201 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class HorsSignatureTest { + + private static final byte[] MESSAGE = "hello hors".getBytes(StandardCharsets.UTF_8); + + @Test + void testValidSignatureVerifies() { + HorsSignature keyPair = new HorsSignature(); + + byte[][] signature = keyPair.sign(MESSAGE); + + assertTrue(HorsSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), keyPair.getK())); + } + + @Test + void testSameKeySignsSeveralMessages() { + HorsSignature keyPair = new HorsSignature(); + byte[][] publicKey = keyPair.getPublicKey(); + + for (int i = 0; i < 5; i++) { + byte[] message = ("message " + i).getBytes(StandardCharsets.UTF_8); + byte[][] signature = keyPair.sign(message); + + assertTrue(HorsSignature.verify(message, signature, publicKey, keyPair.getK())); + } + } + + @Test + void testSigningIsDeterministic() { + HorsSignature keyPair = new HorsSignature(); + + assertArrayEquals(keyPair.sign(MESSAGE), keyPair.sign(MESSAGE)); + } + + @Test + void testTamperedMessageFailsVerification() { + HorsSignature keyPair = new HorsSignature(); + byte[][] signature = keyPair.sign(MESSAGE); + byte[] tampered = MESSAGE.clone(); + + tampered[0] ^= 0x01; + + assertFalse(HorsSignature.verify(tampered, signature, keyPair.getPublicKey(), keyPair.getK())); + } + + @Test + void testTamperedSignatureFailsVerification() { + HorsSignature keyPair = new HorsSignature(); + byte[][] signature = keyPair.sign(MESSAGE); + + signature[3][0] ^= 0x01; + + assertFalse(HorsSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), keyPair.getK())); + } + + @Test + void testDifferentPublicKeyDoesNotVerify() { + HorsSignature keyPair1 = new HorsSignature(); + HorsSignature keyPair2 = new HorsSignature(); + + byte[][] signature = keyPair1.sign(MESSAGE); + + assertFalse(HorsSignature.verify(MESSAGE, signature, keyPair2.getPublicKey(), keyPair1.getK())); + } + + @Test + void testMessageIndicesWithByteAlignedTau() { + byte[] digest = new byte[32]; + digest[0] = (byte) 0xFF; + digest[1] = 0x00; + digest[2] = (byte) 0x80; + digest[3] = 0x7F; + + assertArrayEquals(new int[] {255, 0, 128, 127}, HorsSignature.messageIndices(digest, 4, 8)); + } + + @Test + void testMessageIndicesAcrossByteBoundaries() { + byte[] first = new byte[32]; + first[0] = (byte) 0xFF; + first[1] = (byte) 0xC0; + byte[] second = new byte[32]; + second[1] = 0x7F; + second[2] = (byte) 0xF0; + + // 11111111 11|000000 0000 -> [1023, 0] + assertArrayEquals(new int[] {1023, 0}, HorsSignature.messageIndices(first, 2, 10)); + // 00000000 01|111111 1111 -> [1, 1023] + assertArrayEquals(new int[] {1, 1023}, HorsSignature.messageIndices(second, 2, 10)); + } + + @Test + void testSignatureRevealsSecretsAtDerivedIndices() throws NoSuchAlgorithmException { + HorsSignature keyPair = new HorsSignature(); + byte[][] publicKey = keyPair.getPublicKey(); + + byte[][] signature = keyPair.sign(MESSAGE); + + int[] indices = HorsSignature.messageIndices(sha256(MESSAGE), 16, 10); + byte[][] expectedHashes = new byte[16][]; + byte[][] actualHashes = new byte[signature.length][]; + int[] expectedLengths = new int[16]; + int[] actualLengths = new int[signature.length]; + Arrays.fill(expectedLengths, 32); + for (int j = 0; j < signature.length; j++) { + actualHashes[j] = sha256(signature[j]); + actualLengths[j] = signature[j].length; + } + for (int j = 0; j < 16; j++) { + expectedHashes[j] = publicKey[indices[j]]; + } + + assertArrayEquals(expectedLengths, actualLengths); + assertArrayEquals(expectedHashes, actualHashes); + } + + @ParameterizedTest + @CsvSource({"16, 4", "256, 32", "1024, 16", "4096, 21"}) + void testSignAndVerifyForDifferentParameters(int t, int k) { + HorsSignature keyPair = new HorsSignature(t, k); + + byte[][] signature = keyPair.sign(MESSAGE); + + assertArrayEquals(new int[] {k, k, t}, new int[] {signature.length, keyPair.getK(), keyPair.getPublicKey().length}); + assertTrue(HorsSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), k)); + } + + @ParameterizedTest + @CsvSource({"0, 16", "8, 2", "100, 4", "131072, 4", "1024, 0", "1024, 26", "1024, -1", "1024, 2147483647"}) + void testUnsupportedParametersThrowException(int t, int k) { + assertThrows(IllegalArgumentException.class, () -> new HorsSignature(t, k)); + } + + @Test + void testNullAndMalformedInput() { + HorsSignature keyPair = new HorsSignature(16, 4); + byte[][] publicKey = keyPair.getPublicKey(); + byte[][] signature = keyPair.sign(MESSAGE); + byte[][] nullValue = signature.clone(); + nullValue[0] = null; + byte[][] shortValue = signature.clone(); + shortValue[0] = new byte[31]; + byte[][] tooManyValues = new byte[65][32]; + + assertThrows(IllegalArgumentException.class, () -> keyPair.sign(null)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(null, signature, publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, null, publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, nullValue, publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, shortValue, publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, new byte[0][], publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, tooManyValues, publicKey, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, signature, null, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, signature, new byte[15][32], 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, signature, nullValue, 4)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, signature, publicKey, 3)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, signature, publicKey, 0)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, tooManyValues, publicKey, 65)); + assertThrows(IllegalArgumentException.class, () -> HorsSignature.messageIndices(new byte[1], 2, 8)); + } + + @Test + void testTruncatedSignatureIsRejected() { + HorsSignature keyPair = new HorsSignature(); + byte[][] truncated = Arrays.copyOf(keyPair.sign(MESSAGE), 1); + + assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, truncated, keyPair.getPublicKey(), keyPair.getK())); + } + + @Test + void testModifyingReturnedArraysDoesNotChangeInternalState() { + HorsSignature keyPair = new HorsSignature(); + byte[][] expectedPublicKey = keyPair.getPublicKey(); + byte[][] expectedSignature = keyPair.sign(MESSAGE); + + keyPair.getPublicKey()[0][0] ^= 0x01; + keyPair.getPublicKey()[1] = new byte[32]; + keyPair.sign(MESSAGE)[0][0] ^= 0x01; + + assertArrayEquals(expectedPublicKey, keyPair.getPublicKey()); + assertArrayEquals(expectedSignature, keyPair.sign(MESSAGE)); + assertTrue(HorsSignature.verify(MESSAGE, expectedSignature, keyPair.getPublicKey(), keyPair.getK())); + } + + private static byte[] sha256(byte[] data) throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-256").digest(data); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/LWEEncryptionTest.java b/src/test/java/com/thealgorithms/ciphers/LWEEncryptionTest.java new file mode 100644 index 000000000000..56b3188361f3 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/LWEEncryptionTest.java @@ -0,0 +1,122 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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 java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LWEEncryptionTest { + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void testBitRoundTrip(int bit) { + LWEEncryption lwe = new LWEEncryption(); + + for (int i = 0; i < 100; i++) { + assertEquals(bit, lwe.decryptBit(lwe.encryptBit(bit))); + } + } + + @Test + void testTextMessageRoundTrip() { + LWEEncryption lwe = new LWEEncryption(); + byte[] message = "Hello, PQC!".getBytes(StandardCharsets.UTF_8); + + assertArrayEquals(message, lwe.decrypt(lwe.encrypt(message))); + } + + @Test + void testRandomMessageRoundTrip() { + LWEEncryption lwe = new LWEEncryption(); + byte[] message = new byte[1024]; + new SecureRandom().nextBytes(message); + + assertArrayEquals(message, lwe.decrypt(lwe.encrypt(message))); + } + + @Test + void testEncryptionIsRandomized() { + LWEEncryption lwe = new LWEEncryption(); + + LWEEncryption.Ciphertext first = lwe.encryptBit(1); + LWEEncryption.Ciphertext second = lwe.encryptBit(1); + + assertFalse(Arrays.equals(toArray(first), toArray(second))); + } + + @Test + void testWrongKeyDoesNotDecrypt() { + LWEEncryption sender = new LWEEncryption(); + LWEEncryption other = new LWEEncryption(); + byte[] message = "sixteen byte msg".getBytes(StandardCharsets.UTF_8); + + byte[] decrypted = other.decrypt(sender.encrypt(message)); + + assertFalse(Arrays.equals(message, decrypted)); + } + + @ParameterizedTest + @ValueSource(bytes = {(byte) 0b10110001, (byte) 0xFF, (byte) 0x80, 0x00, 0x01, 0x7F}) + void testSingleByteBitOrder(byte value) { + LWEEncryption lwe = new LWEEncryption(); + + assertArrayEquals(new byte[] {value}, lwe.decrypt(lwe.encrypt(new byte[] {value}))); + } + + @Test + void testInvalidInput() { + LWEEncryption lwe = new LWEEncryption(); + LWEEncryption.Ciphertext[] sevenCiphertexts = Arrays.copyOf(lwe.encrypt(new byte[] {1}), 7); + LWEEncryption.Ciphertext[] withNull = lwe.encrypt(new byte[] {1}); + withNull[3] = null; + + assertThrows(IllegalArgumentException.class, () -> lwe.encryptBit(2)); + assertThrows(IllegalArgumentException.class, () -> lwe.encryptBit(-1)); + assertThrows(IllegalArgumentException.class, () -> lwe.encrypt(null)); + assertThrows(IllegalArgumentException.class, () -> lwe.encrypt(new byte[0])); + assertThrows(IllegalArgumentException.class, () -> lwe.decryptBit(null)); + assertThrows(IllegalArgumentException.class, () -> lwe.decrypt(null)); + assertThrows(IllegalArgumentException.class, () -> lwe.decrypt(new LWEEncryption.Ciphertext[0])); + assertThrows(IllegalArgumentException.class, () -> lwe.decrypt(sevenCiphertexts)); + assertThrows(IllegalArgumentException.class, () -> lwe.decrypt(withNull)); + assertThrows(IllegalArgumentException.class, () -> new LWEEncryption.Ciphertext(null, 0)); + assertThrows(IllegalArgumentException.class, () -> new LWEEncryption.Ciphertext(new int[31], 0)); + assertThrows(IllegalArgumentException.class, () -> new LWEEncryption.Ciphertext(new int[33], 0)); + } + + @Test + void testCiphertextIsImmutable() { + int[] u = new int[32]; + Arrays.fill(u, 7); + LWEEncryption.Ciphertext ciphertext = new LWEEncryption.Ciphertext(u, 5); + int[] expected = u.clone(); + + u[0] = 100; + ciphertext.getU()[1] = 200; + + assertArrayEquals(expected, ciphertext.getU()); + assertEquals(5, ciphertext.getV()); + } + + @Test + void testDecryptionBoundCheck() { + LWEEncryption.checkDecryptionBound(128, 3, 3329); + + assertThrows(IllegalStateException.class, () -> LWEEncryption.checkDecryptionBound(128, 7, 3329)); + } + + // u followed by v, so that two ciphertexts can be compared with a single Arrays.equals + private static int[] toArray(LWEEncryption.Ciphertext ciphertext) { + int[] u = ciphertext.getU(); + int[] values = Arrays.copyOf(u, u.length + 1); + values[u.length] = ciphertext.getV(); + return values; + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java new file mode 100644 index 000000000000..99802f753a64 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java @@ -0,0 +1,101 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class LamportSignatureTest { + + @Test + void testValidSignatureVerifies() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + + byte[] signature = keyPair.sign(message); + + assertTrue(keyPair.verify(message, signature)); + } + + @Test + void testTamperedMessageFailsVerification() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + byte[] signature = keyPair.sign(message); + + assertFalse(keyPair.verify("hello lamport!".getBytes(StandardCharsets.UTF_8), signature)); + } + + @Test + void testTamperedSignatureFailsVerification() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + byte[] signature = keyPair.sign(message); + + signature[0] = (byte) (signature[0] ^ 0xFF); + + assertFalse(keyPair.verify(message, signature)); + } + + @Test + void testDifferentKeyPairDoesNotVerify() { + LamportSignature.KeyPair keyPair1 = LamportSignature.generateKeyPair(); + LamportSignature.KeyPair keyPair2 = LamportSignature.generateKeyPair(); + byte[] message = "post-quantum".getBytes(StandardCharsets.UTF_8); + + byte[] signature = keyPair1.sign(message); + + assertFalse(keyPair2.verify(message, signature)); + } + + @Test + void testSecondSignatureThrowsException() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] first = "first message".getBytes(StandardCharsets.UTF_8); + byte[] second = "second message".getBytes(StandardCharsets.UTF_8); + + keyPair.sign(first); + + assertThrows(IllegalStateException.class, () -> keyPair.sign(second)); + } + + @Test + void testNullAndMalformedInput() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] validMessage = "message".getBytes(StandardCharsets.UTF_8); + byte[] validSignature = keyPair.sign(validMessage); + + assertThrows(IllegalArgumentException.class, () -> keyPair.sign(null)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(null, validSignature)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, null)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, new byte[1])); + } + + @Test + void testPublicKeyMatchesPrivateKeyHash() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[][][] publicKey = keyPair.getPublicKey(); + byte[][][] privateKey = keyPair.getPrivateKey(); + + for (int bit = 0; bit < 256; bit++) { + for (int value = 0; value < 2; value++) { + byte[] hash = LamportSignature.sha256(privateKey[bit][value]); + assertArrayEquals(hash, publicKey[bit][value]); + } + } + } + + @Test + void testSha256SliceHelperMatchesRangeHash() { + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + + byte[] expected = LamportSignature.sha256(Arrays.copyOfRange(message, 2, 12)); + byte[] actual = LamportSignature.sha256(message, 2, 10); + + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/MerkleSignatureSchemeTest.java b/src/test/java/com/thealgorithms/ciphers/MerkleSignatureSchemeTest.java new file mode 100644 index 000000000000..dfa59e18e9c8 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/MerkleSignatureSchemeTest.java @@ -0,0 +1,195 @@ +package com.thealgorithms.ciphers; + +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.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MerkleSignatureSchemeTest { + + private static final byte[] MESSAGE = "hello merkle".getBytes(StandardCharsets.UTF_8); + + @Test + void testValidSignatureVerifies() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(); + + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + + assertTrue(MerkleSignatureScheme.verify(MESSAGE, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testEveryLeafSignsAndVerifies() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(4, 16); + byte[] publicKey = keyPair.getPublicKey(); + + for (int i = 0; i < 16; i++) { + byte[] message = ("message " + i).getBytes(StandardCharsets.UTF_8); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(message); + + assertEquals(i, signature.getLeafIndex()); + assertEquals(4, signature.getAuthPath().length); + assertTrue(MerkleSignatureScheme.verify(message, signature, publicKey, 16)); + } + } + + @Test + void testSigningBeyondCapacityThrowsException() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(2, 16); + for (int i = 0; i < 4; i++) { + keyPair.sign(MESSAGE); + } + + assertThrows(IllegalStateException.class, () -> keyPair.sign(MESSAGE)); + } + + @Test + void testRemainingSignaturesDecreases() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(2, 16); + + for (int expected = 4; expected > 0; expected--) { + assertEquals(expected, keyPair.remainingSignatures()); + keyPair.sign(MESSAGE); + } + assertEquals(0, keyPair.remainingSignatures()); + } + + @Test + void testTamperedMessageFailsVerification() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + byte[] tampered = MESSAGE.clone(); + + tampered[0] ^= 0x01; + + assertFalse(MerkleSignatureScheme.verify(tampered, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testTamperedWotsSignatureFailsVerification() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + byte[][] wotsSignature = signature.getWotsSignature(); + + wotsSignature[5][0] ^= 0x01; + MerkleSignatureScheme.MerkleSignature tampered = new MerkleSignatureScheme.MerkleSignature(signature.getLeafIndex(), wotsSignature, signature.getWotsPublicKey(), signature.getAuthPath()); + + assertFalse(MerkleSignatureScheme.verify(MESSAGE, tampered, keyPair.getPublicKey(), 16)); + } + + @Test + void testTamperedAuthPathFailsVerification() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + byte[][] authPath = signature.getAuthPath(); + + authPath[2][0] ^= 0x01; + MerkleSignatureScheme.MerkleSignature tampered = new MerkleSignatureScheme.MerkleSignature(signature.getLeafIndex(), signature.getWotsSignature(), signature.getWotsPublicKey(), authPath); + + assertFalse(MerkleSignatureScheme.verify(MESSAGE, tampered, keyPair.getPublicKey(), 16)); + } + + @Test + void testChangedLeafIndexFailsVerification() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + + MerkleSignatureScheme.MerkleSignature tampered = new MerkleSignatureScheme.MerkleSignature(signature.getLeafIndex() + 1, signature.getWotsSignature(), signature.getWotsPublicKey(), signature.getAuthPath()); + + assertFalse(MerkleSignatureScheme.verify(MESSAGE, tampered, keyPair.getPublicKey(), 16)); + } + + @Test + void testDifferentPublicKeyDoesNotVerify() { + MerkleSignatureScheme keyPair1 = new MerkleSignatureScheme(); + MerkleSignatureScheme keyPair2 = new MerkleSignatureScheme(); + + MerkleSignatureScheme.MerkleSignature signature = keyPair1.sign(MESSAGE); + + assertFalse(MerkleSignatureScheme.verify(MESSAGE, signature, keyPair2.getPublicKey(), 16)); + } + + @ParameterizedTest + @CsvSource({"2, 4", "2, 16", "4, 4", "4, 16", "6, 4", "6, 16"}) + void testSignAndVerifyForDifferentParameters(int h, int w) { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(h, w); + + MerkleSignatureScheme.MerkleSignature first = keyPair.sign(MESSAGE); + MerkleSignatureScheme.MerkleSignature second = keyPair.sign(MESSAGE); + + assertEquals((1 << h) - 2, keyPair.remainingSignatures()); + assertEquals(h, first.getAuthPath().length); + assertTrue(MerkleSignatureScheme.verify(MESSAGE, first, keyPair.getPublicKey(), w)); + assertTrue(MerkleSignatureScheme.verify(MESSAGE, second, keyPair.getPublicKey(), w)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 11, -4}) + void testUnsupportedHeightThrowsException(int h) { + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme(h, 16)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 3, 15}) + void testUnsupportedWThrowsException(int w) { + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme(2, w)); + } + + @Test + void testNullAndMalformedInput() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(2, 16); + byte[] publicKey = keyPair.getPublicKey(); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + byte[][] wotsSignature = signature.getWotsSignature(); + byte[][] wotsPublicKey = signature.getWotsPublicKey(); + byte[][] authPath = signature.getAuthPath(); + + assertThrows(IllegalArgumentException.class, () -> keyPair.sign(null)); + assertThrows(IllegalArgumentException.class, () -> MerkleSignatureScheme.verify(null, signature, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> MerkleSignatureScheme.verify(MESSAGE, null, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> MerkleSignatureScheme.verify(MESSAGE, signature, null, 16)); + assertThrows(IllegalArgumentException.class, () -> MerkleSignatureScheme.verify(MESSAGE, signature, new byte[31], 16)); + assertThrows(IllegalArgumentException.class, () -> MerkleSignatureScheme.verify(MESSAGE, signature, publicKey, 4)); + + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, null, wotsPublicKey, authPath)); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, null, authPath)); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, wotsPublicKey, null)); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, wotsPublicKey, new byte[1][32])); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, wotsPublicKey, new byte[11][32])); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, wotsPublicKey, new byte[2][31])); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, wotsSignature, wotsPublicKey, new byte[2][])); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(0, new byte[67][], wotsPublicKey, authPath)); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(-1, wotsSignature, wotsPublicKey, authPath)); + assertThrows(IllegalArgumentException.class, () -> new MerkleSignatureScheme.MerkleSignature(4, wotsSignature, wotsPublicKey, authPath)); + } + + @Test + void testModifyingReturnedArraysDoesNotChangeInternalState() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(2, 16); + MerkleSignatureScheme.MerkleSignature signature = keyPair.sign(MESSAGE); + + keyPair.getPublicKey()[0] ^= 0x01; + signature.getWotsSignature()[0][0] ^= 0x01; + signature.getWotsPublicKey()[0][0] ^= 0x01; + signature.getAuthPath()[0][0] ^= 0x01; + + assertTrue(MerkleSignatureScheme.verify(MESSAGE, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testSignaturesUseDifferentLeaves() { + MerkleSignatureScheme keyPair = new MerkleSignatureScheme(2, 16); + + MerkleSignatureScheme.MerkleSignature first = keyPair.sign(MESSAGE); + MerkleSignatureScheme.MerkleSignature second = keyPair.sign(MESSAGE); + + assertEquals(0, first.getLeafIndex()); + assertEquals(1, second.getLeafIndex()); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java b/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java new file mode 100644 index 000000000000..837c56c603d4 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java @@ -0,0 +1,49 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class OneTimePadCipherTest { + + @Test + void encryptAndDecryptWithRandomKeyRestoresPlaintext() { + String plaintext = "The quick brown fox jumps over the lazy dog."; + byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8); + + byte[] key = OneTimePadCipher.generateKey(plaintextBytes.length); + + byte[] ciphertext = OneTimePadCipher.encrypt(plaintextBytes, key); + byte[] decrypted = OneTimePadCipher.decrypt(ciphertext, key); + + assertArrayEquals(plaintextBytes, decrypted); + assertEquals(plaintext, new String(decrypted, StandardCharsets.UTF_8)); + } + + @Test + void generateKeyWithNegativeLengthThrowsException() { + assertThrows(IllegalArgumentException.class, () -> OneTimePadCipher.generateKey(-1)); + } + + @Test + void encryptWithMismatchedKeyLengthThrowsException() { + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + byte[] shortKey = OneTimePadCipher.generateKey(2); + + assertThrows(IllegalArgumentException.class, () -> OneTimePadCipher.encrypt(data, shortKey)); + } + + @Test + void decryptWithMismatchedKeyLengthThrowsException() { + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + byte[] key = OneTimePadCipher.generateKey(data.length); + byte[] ciphertext = OneTimePadCipher.encrypt(data, key); + + byte[] wrongSizedKey = OneTimePadCipher.generateKey(data.length + 1); + + assertThrows(IllegalArgumentException.class, () -> OneTimePadCipher.decrypt(ciphertext, wrongSizedKey)); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java b/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java index 4ba6787cc97e..ecb7455c1ba2 100644 --- a/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java +++ b/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.ciphers; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -121,8 +122,8 @@ void testNullString() { String decrypted = cipher.decrypt(encrypted, key); // then - assertEquals(null, encrypted); - assertEquals(null, decrypted); + assertNull(encrypted); + assertNull(decrypted); } @Test diff --git a/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java b/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java new file mode 100644 index 000000000000..041f8c0dd4c1 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class RailFenceCipherTest { + + private final RailFenceCipher railFenceCipher = new RailFenceCipher(); + + @Test + void testEncrypt() { + assertEquals("WECRLTEERDSOEEFEAOCAIVDEN", railFenceCipher.encrypt("WEAREDISCOVEREDFLEEATONCE", 3)); + } + + @Test + void testDecrypt() { + assertEquals("WEAREDISCOVEREDFLEEATONCE", railFenceCipher.decrypt("WECRLTEERDSOEEFEAOCAIVDEN", 3)); + } + + @ParameterizedTest + @CsvSource({"HELLOWORLD, 2", "HELLOWORLD, 3", "HELLOWORLD, 4", "ATTACKATDAWN, 5", "abcdefghij, 6"}) + void testRoundTrip(String message, int rails) { + assertEquals(message, railFenceCipher.decrypt(railFenceCipher.encrypt(message, rails), rails)); + } + + /** + * Every character of the input must survive encryption, including the ones that used to collide + * with the placeholder that marked unused cells of the rail matrix. + */ + @ParameterizedTest + @ValueSource(strings = {"ab\ncdef", "line1\nline2\nline3", "\n\n\n\n\n", "a\nb", "tabs\tand\nnewlines\r\n"}) + void testControlCharactersArePreserved(String message) { + for (int rails = 2; rails <= 5; rails++) { + String encrypted = railFenceCipher.encrypt(message, rails); + assertEquals(message.length(), encrypted.length(), "characters were dropped with " + rails + " rails"); + assertEquals(message, railFenceCipher.decrypt(encrypted, rails), "round trip failed with " + rails + " rails"); + } + } + + @Test + void testEncryptWithNewlineMatchesReferencePattern() { + // Rails of "ab\ncdef" with 3 rails: {a, d} / {b, c, e} / {\n, f} + assertEquals("adbce\nf", railFenceCipher.encrypt("ab\ncdef", 3)); + } + + @ParameterizedTest + @CsvSource({"HELLO, 1", "HELLO, 5", "HELLO, 9", "'', 1", "'', 4"}) + void testDegenerateRailCountsReturnInput(String message, int rails) { + assertEquals(message, railFenceCipher.encrypt(message, rails)); + assertEquals(message, railFenceCipher.decrypt(message, rails)); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, -7}) + void testNonPositiveRailCountThrows(int rails) { + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt("HELLO", rails)); + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt("HELLO", rails)); + } + + @Test + void testNullInputThrows() { + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt(null, 3)); + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt(null, 3)); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/WinternitzSignatureTest.java b/src/test/java/com/thealgorithms/ciphers/WinternitzSignatureTest.java new file mode 100644 index 000000000000..908bf9b53855 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/WinternitzSignatureTest.java @@ -0,0 +1,178 @@ +package com.thealgorithms.ciphers; + +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.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class WinternitzSignatureTest { + + private static final byte[] MESSAGE = "hello winternitz".getBytes(StandardCharsets.UTF_8); + + @Test + void testValidSignatureVerifies() { + WinternitzSignature keyPair = new WinternitzSignature(); + + byte[][] signature = keyPair.sign(MESSAGE); + + assertTrue(WinternitzSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), 16)); + } + + @ParameterizedTest + @ValueSource(ints = {4, 16, 256}) + void testSignAndVerifyForEverySupportedW(int w) { + WinternitzSignature keyPair = new WinternitzSignature(w); + + byte[][] signature = keyPair.sign(MESSAGE); + + assertTrue(WinternitzSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), w)); + } + + @ParameterizedTest + @CsvSource({"4, 128, 5", "16, 64, 3", "256, 32, 2"}) + void testParametersAreDerivedFromW(int w, int expectedLen1, int expectedLen2) { + WinternitzSignature.Parameters parameters = WinternitzSignature.Parameters.forW(w); + + assertEquals(expectedLen1, parameters.len1()); + assertEquals(expectedLen2, parameters.len2()); + assertEquals(expectedLen1 + expectedLen2, parameters.len()); + } + + @Test + void testDefaultSignatureHas67Values() { + WinternitzSignature keyPair = new WinternitzSignature(); + + byte[][] signature = keyPair.sign(MESSAGE); + + assertEquals(67, signature.length); + assertEquals(67, keyPair.getPublicKey().length); + for (byte[] value : signature) { + assertEquals(32, value.length); + } + } + + @Test + void testTamperedMessageFailsVerification() { + WinternitzSignature keyPair = new WinternitzSignature(); + byte[] message = MESSAGE.clone(); + byte[][] signature = keyPair.sign(message); + + message[0] ^= 0x01; + + assertFalse(WinternitzSignature.verify(message, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testTamperedSignatureFailsVerification() { + WinternitzSignature keyPair = new WinternitzSignature(); + byte[][] signature = keyPair.sign(MESSAGE); + + signature[10][0] ^= 0x01; + + assertFalse(WinternitzSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testDifferentPublicKeyDoesNotVerify() { + WinternitzSignature keyPair1 = new WinternitzSignature(); + WinternitzSignature keyPair2 = new WinternitzSignature(); + + byte[][] signature = keyPair1.sign(MESSAGE); + + assertFalse(WinternitzSignature.verify(MESSAGE, signature, keyPair2.getPublicKey(), 16)); + } + + @Test + void testVerifyingWithDifferentWThrowsException() { + WinternitzSignature keyPair = new WinternitzSignature(16); + byte[][] signature = keyPair.sign(MESSAGE); + byte[][] publicKey = keyPair.getPublicKey(); + + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, signature, publicKey, 4)); + } + + @Test + void testSecondSignatureThrowsException() { + WinternitzSignature keyPair = new WinternitzSignature(); + byte[] second = "second message".getBytes(StandardCharsets.UTF_8); + + keyPair.sign(MESSAGE); + + assertThrows(IllegalStateException.class, () -> keyPair.sign(second)); + } + + @Test + void testNullAndMalformedInput() { + WinternitzSignature keyPair = new WinternitzSignature(); + byte[][] publicKey = keyPair.getPublicKey(); + byte[][] signature = keyPair.sign(MESSAGE); + byte[][] tooShort = Arrays.copyOf(signature, 66); + byte[][] wrongValueLength = signature.clone(); + wrongValueLength[0] = new byte[31]; + byte[][] nullValue = signature.clone(); + nullValue[0] = null; + + assertThrows(IllegalArgumentException.class, () -> new WinternitzSignature().sign(null)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(null, signature, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, null, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, tooShort, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, wrongValueLength, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, nullValue, publicKey, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, signature, null, 16)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, signature, tooShort, 16)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 3, 15, -16, 2, 8, 512}) + void testUnsupportedWThrowsException(int w) { + assertThrows(IllegalArgumentException.class, () -> new WinternitzSignature(w)); + assertThrows(IllegalArgumentException.class, () -> WinternitzSignature.verify(MESSAGE, new byte[67][32], new byte[67][32], w)); + } + + @Test + void testModifyingReturnedPublicKeyDoesNotChangeInternalState() { + WinternitzSignature keyPair = new WinternitzSignature(); + byte[][] signature = keyPair.sign(MESSAGE); + + byte[][] exposed = keyPair.getPublicKey(); + exposed[0][0] ^= 0x01; + exposed[1] = new byte[32]; + + assertTrue(WinternitzSignature.verify(MESSAGE, signature, keyPair.getPublicKey(), 16)); + } + + @Test + void testEmptyAndLongMessages() { + byte[] empty = new byte[0]; + byte[] longMessage = new byte[8192]; + for (int i = 0; i < longMessage.length; i++) { + longMessage[i] = (byte) i; + } + WinternitzSignature keyPair1 = new WinternitzSignature(); + WinternitzSignature keyPair2 = new WinternitzSignature(); + + byte[][] emptySignature = keyPair1.sign(empty); + byte[][] longSignature = keyPair2.sign(longMessage); + + assertTrue(WinternitzSignature.verify(empty, emptySignature, keyPair1.getPublicKey(), 16)); + assertTrue(WinternitzSignature.verify(longMessage, longSignature, keyPair2.getPublicKey(), 16)); + } + + @Test + void testDifferentMessagesProduceDifferentSignatures() { + WinternitzSignature keyPair1 = new WinternitzSignature(); + WinternitzSignature keyPair2 = new WinternitzSignature(); + + byte[][] signature1 = keyPair1.sign("first message".getBytes(StandardCharsets.UTF_8)); + byte[][] signature2 = keyPair2.sign("second message".getBytes(StandardCharsets.UTF_8)); + + assertFalse(Arrays.deepEquals(signature1, signature2)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java b/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java new file mode 100644 index 000000000000..f919417899db --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java @@ -0,0 +1,110 @@ +package com.thealgorithms.compression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class HuffmanCodingTest { + + @Test + void testStandardLifecycle() { + String input = "efficiency is key"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertNotNull(encoded); + assertTrue(encoded.matches("[01]+")); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testNullAndEmptyHandling() { + HuffmanCoding huffman = new HuffmanCoding(""); + assertEquals("", huffman.encode("")); + assertEquals("", huffman.decode("")); + + HuffmanCoding huffmanNull = new HuffmanCoding(null); + assertEquals("", huffmanNull.encode(null)); + assertEquals("", huffmanNull.decode(null)); + } + + @Test + void testSingleCharacterEdgeCase() { + String input = "aaaaa"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertEquals("00000", encoded); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testUnicodeAndSpecialCharacters() { + // Tests spacing, symbols, non-latin alphabets, and surrogate pairs (emojis) + String input = "Hello, World! πŸš€\nLine 2: こんにけは"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testFailFastOnUnseenCharacter() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> huffman.encode("abcd") // 'd' was not in the original tree + ); + assertTrue(exception.getMessage().contains("not found in Huffman dictionary")); + } + + @Test + void testFailFastOnInvalidBinaryCharacter() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + String encoded = huffman.encode("abc"); + + // Inject a '2' into the binary stream + String corruptedEncoded = encoded + "2"; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> huffman.decode(corruptedEncoded)); + assertTrue(exception.getMessage().contains("contains invalid characters")); + } + + @Test + void testFailFastOnIncompleteSequence() { + HuffmanCoding huffman = new HuffmanCoding("abcd"); + String encoded = huffman.encode("abc"); + + // Truncate the last bit to simulate an incomplete byte/sequence transfer + String truncatedEncoded = encoded.substring(0, encoded.length() - 1); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> huffman.decode(truncatedEncoded)); + assertTrue(exception.getMessage().contains("incomplete sequence")); + } + + @Test + void testImmutabilityOfDictionary() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + var codes = huffman.getHuffmanCodes(); + + assertThrows(UnsupportedOperationException.class, () -> codes.put('z', "0101")); + } + + @Test + void testStressVolume() { + StringBuilder sb = new StringBuilder(); + // Generate a 100,000 character string + for (int i = 0; i < 100000; i++) { + sb.append((char) ('a' + (i % 26))); + } + String largeInput = sb.toString(); + + HuffmanCoding huffman = new HuffmanCoding(largeInput); + String encoded = huffman.encode(largeInput); + + assertEquals(largeInput, huffman.decode(encoded)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/LZ78Test.java b/src/test/java/com/thealgorithms/compression/LZ78Test.java index 7889b50b76f3..da1fd8d23318 100644 --- a/src/test/java/com/thealgorithms/compression/LZ78Test.java +++ b/src/test/java/com/thealgorithms/compression/LZ78Test.java @@ -1,7 +1,6 @@ package com.thealgorithms.compression; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -286,7 +285,6 @@ void testTokenStructure() { // All tokens should have valid indices (>= 0) for (LZ78.Token token : compressed) { assertTrue(token.index() >= 0); - assertNotNull(token.nextChar()); } String decompressed = LZ78.decompress(compressed); diff --git a/src/test/java/com/thealgorithms/conversions/Base64Test.java b/src/test/java/com/thealgorithms/conversions/Base64Test.java index fbc220c0ca95..cd0d6c8b38a8 100644 --- a/src/test/java/com/thealgorithms/conversions/Base64Test.java +++ b/src/test/java/com/thealgorithms/conversions/Base64Test.java @@ -127,6 +127,9 @@ void testInvalidPaddingPosition() { assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=QQ")); assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=Q=")); assertThrows(IllegalArgumentException.class, () -> Base64.decode("=QQQ")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQ=Q")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("AB=C")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("AB=A")); } @Test diff --git a/src/test/java/com/thealgorithms/conversions/TemperatureConverterTest.java b/src/test/java/com/thealgorithms/conversions/TemperatureConverterTest.java new file mode 100644 index 000000000000..24d55b706f36 --- /dev/null +++ b/src/test/java/com/thealgorithms/conversions/TemperatureConverterTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.conversions; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class TemperatureConverterTest { + + private static final double DELTA = 0.01; + + @Test + void testCelsiusToFahrenheit() { + assertEquals(32.0, TemperatureConverter.celsiusToFahrenheit(0.0), DELTA); + assertEquals(212.0, TemperatureConverter.celsiusToFahrenheit(100.0), DELTA); + assertEquals(-40.0, TemperatureConverter.celsiusToFahrenheit(-40.0), DELTA); + assertEquals(98.6, TemperatureConverter.celsiusToFahrenheit(37.0), DELTA); + } + + @Test + void testCelsiusToKelvin() { + assertEquals(273.15, TemperatureConverter.celsiusToKelvin(0.0), DELTA); + assertEquals(373.15, TemperatureConverter.celsiusToKelvin(100.0), DELTA); + assertEquals(233.15, TemperatureConverter.celsiusToKelvin(-40.0), DELTA); + } + + @Test + void testFahrenheitToCelsius() { + assertEquals(0.0, TemperatureConverter.fahrenheitToCelsius(32.0), DELTA); + assertEquals(100.0, TemperatureConverter.fahrenheitToCelsius(212.0), DELTA); + assertEquals(-40.0, TemperatureConverter.fahrenheitToCelsius(-40.0), DELTA); + assertEquals(37.0, TemperatureConverter.fahrenheitToCelsius(98.6), DELTA); + } + + @Test + void testFahrenheitToKelvin() { + assertEquals(273.15, TemperatureConverter.fahrenheitToKelvin(32.0), DELTA); + assertEquals(373.15, TemperatureConverter.fahrenheitToKelvin(212.0), DELTA); + assertEquals(233.15, TemperatureConverter.fahrenheitToKelvin(-40.0), DELTA); + } + + @Test + void testKelvinToCelsius() { + assertEquals(0.0, TemperatureConverter.kelvinToCelsius(273.15), DELTA); + assertEquals(100.0, TemperatureConverter.kelvinToCelsius(373.15), DELTA); + assertEquals(-273.15, TemperatureConverter.kelvinToCelsius(0.0), DELTA); + } + + @Test + void testKelvinToFahrenheit() { + assertEquals(32.0, TemperatureConverter.kelvinToFahrenheit(273.15), DELTA); + assertEquals(212.0, TemperatureConverter.kelvinToFahrenheit(373.15), DELTA); + assertEquals(-40.0, TemperatureConverter.kelvinToFahrenheit(233.15), DELTA); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java index 8212793dfb79..f85a4628a1ac 100644 --- a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java +++ b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java @@ -98,11 +98,9 @@ void testIterator() { @Test void testIteratorEmptyBag() { Bag bag = new Bag<>(); - int count = 0; - for (String ignored : bag) { - org.junit.jupiter.api.Assertions.fail("Iterator should not return any items for an empty bag"); + for (String item : bag) { + org.junit.jupiter.api.Assertions.fail("Iterator returned item for an empty bag:" + item); } - assertEquals(0, count, "Iterator should not traverse any items in an empty bag"); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregatorTest.java b/src/test/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregatorTest.java new file mode 100644 index 000000000000..2595678961ed --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/buffers/SlidingWindowAggregatorTest.java @@ -0,0 +1,449 @@ +package com.thealgorithms.datastructures.buffers; + +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.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SlidingWindowAggregatorTest { + + private static final Comparator NATURAL = Comparator.naturalOrder(); + + private static SlidingWindowAggregator sumWindow(int capacity) { + return SlidingWindowAggregator.of(capacity, Integer::sum); + } + + @Nested + @DisplayName("construction") + class Construction { + + @ParameterizedTest + @ValueSource(ints = {0, -1, Integer.MIN_VALUE}) + void rejectsNonPositiveCapacity(int capacity) { + assertThrows(IllegalArgumentException.class, () -> sumWindow(capacity)); + } + + @Test + void rejectsNullMapper() { + assertThrows(NullPointerException.class, () -> new SlidingWindowAggregator(4, null, Integer::sum)); + } + + @Test + void rejectsNullCombiner() { + assertThrows(NullPointerException.class, () -> new SlidingWindowAggregator(4, Function.identity(), null)); + } + + @Test + void startsEmpty() { + SlidingWindowAggregator window = sumWindow(3); + assertTrue(window.isEmpty()); + assertFalse(window.isFull()); + assertEquals(0, window.size()); + assertEquals(3, window.capacity()); + assertTrue(window.toList().isEmpty()); + assertEquals("[]", window.toString()); + } + } + + @Nested + @DisplayName("empty window") + class EmptyWindow { + + private final SlidingWindowAggregator window = sumWindow(3); + + @Test + void aggregateThrows() { + assertThrows(NoSuchElementException.class, window::aggregate); + } + + @Test + void peeksThrow() { + assertThrows(NoSuchElementException.class, window::peekOldest); + assertThrows(NoSuchElementException.class, window::peekNewest); + } + + @Test + void removeThrows() { + assertThrows(NoSuchElementException.class, window::removeOldest); + } + + @Test + void getThrows() { + assertThrows(IndexOutOfBoundsException.class, () -> window.get(0)); + } + + @Test + void iteratorIsExhausted() { + Iterator iterator = window.iterator(); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + } + + @Nested + @DisplayName("aggregation") + class Aggregation { + + @Test + void aggregatesWhileFilling() { + SlidingWindowAggregator window = sumWindow(4); + window.add(1); + assertEquals(1, window.aggregate()); + window.add(2); + assertEquals(3, window.aggregate()); + window.add(3); + assertEquals(6, window.aggregate()); + window.add(4); + assertEquals(10, window.aggregate()); + assertTrue(window.isFull()); + } + + @Test + void slidesOnceFull() { + SlidingWindowAggregator window = sumWindow(3); + window.addAll(List.of(1, 2, 3)); + assertEquals(6, window.aggregate()); + + assertEquals(1, window.add(4)); + assertEquals(9, window.aggregate()); + assertEquals(List.of(2, 3, 4), window.toList()); + + assertEquals(2, window.add(5)); + assertEquals(12, window.aggregate()); + } + + @Test + void addReturnsNullWhileTheWindowIsNotFull() { + SlidingWindowAggregator window = sumWindow(2); + Assertions.assertNull(window.add(1)); + Assertions.assertNull(window.add(2)); + assertEquals(1, window.add(3)); + } + + @Test + @DisplayName("keeps the maximum of the last three readings") + void tracksMaximum() { + SlidingWindowAggregator window = SlidingWindowAggregator.maximum(3, NATURAL); + List observed = new ArrayList<>(); + for (int value : new int[] {1, 3, 2, 5, 0, 0, 0}) { + window.add(value); + observed.add(window.aggregate()); + } + assertEquals(List.of(1, 3, 3, 5, 5, 5, 0), observed); + } + + @Test + void tracksMinimum() { + SlidingWindowAggregator window = SlidingWindowAggregator.minimum(3, NATURAL); + window.addAll(List.of(5, 4, 6, 7, 8)); + assertEquals(6, window.aggregate()); + } + + @Test + @DisplayName("combines oldest first, so non-commutative operators work") + void preservesOrderForNonCommutativeOperators() { + SlidingWindowAggregator window = SlidingWindowAggregator.of(3, (a, b) -> a + b); + window.addAll(List.of("a", "b", "c", "d", "e")); + assertEquals("cde", window.aggregate()); + window.removeOldest(); + assertEquals("de", window.aggregate()); + } + + @Test + void supportsAggregatesOfADifferentType() { + SlidingWindowAggregator lengths = new SlidingWindowAggregator<>(3, String::length, Integer::sum); + lengths.addAll(List.of("a", "bb", "ccc", "dddd")); + assertEquals(2 + 3 + 4, lengths.aggregate()); + } + + @Test + void movingAverageIsSumOverSize() { + SlidingWindowAggregator window = SlidingWindowAggregator.sumOfDoubles(3); + window.addAll(List.of(1.0, 2.0, 3.0, 6.0)); + assertEquals(11.0 / 3.0, window.aggregate() / window.size(), 1e-12); + } + + @Test + void sumOfLongsFactoryWorks() { + SlidingWindowAggregator window = SlidingWindowAggregator.sumOfLongs(2); + window.addAll(List.of(1L, 2L, 3L)); + assertEquals(5L, window.aggregate()); + } + + @Test + @DisplayName("the mapper runs exactly once per element") + void mapperIsCalledOncePerElement() { + AtomicInteger calls = new AtomicInteger(); + SlidingWindowAggregator window = new SlidingWindowAggregator<>(3, value -> { + calls.incrementAndGet(); + return value; + }, Integer::sum); + for (int i = 0; i < 100; i++) { + window.add(i); + window.aggregate(); + } + assertEquals(100, calls.get()); + } + } + + @Nested + @DisplayName("null handling") + class NullHandling { + + @Test + void rejectsNullElements() { + SlidingWindowAggregator window = sumWindow(2); + assertThrows(NullPointerException.class, () -> window.add(null)); + } + + @Test + @DisplayName("a null hidden inside a collection is rejected, and the elements before it stay") + void rejectsNullElementsInsideACollection() { + SlidingWindowAggregator window = sumWindow(2); + List items = new ArrayList<>(); + items.add(1); + items.add(null); + + assertThrows(NullPointerException.class, () -> window.addAll(items)); + assertEquals(1, window.size()); + assertEquals(1, window.aggregate()); + } + + @Test + void rejectsMappersReturningNull() { + SlidingWindowAggregator window = new SlidingWindowAggregator<>(2, value -> null, Integer::sum); + assertThrows(NullPointerException.class, () -> window.add(1)); + } + + @Test + void rejectsCombinersReturningNull() { + SlidingWindowAggregator window = SlidingWindowAggregator.of(2, (a, b) -> null); + window.add(1); + assertThrows(NullPointerException.class, () -> window.add(2)); + } + } + + @Nested + @DisplayName("access and iteration") + class Access { + + @Test + void indexedAccessStartsAtTheOldestElement() { + SlidingWindowAggregator window = sumWindow(3); + window.addAll(List.of(10, 20, 30, 40)); + assertEquals(20, window.get(0)); + assertEquals(30, window.get(1)); + assertEquals(40, window.get(2)); + assertEquals(20, window.peekOldest()); + assertEquals(40, window.peekNewest()); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 3, 100}) + void rejectsOutOfBoundsIndices(int index) { + SlidingWindowAggregator window = sumWindow(5); + window.addAll(List.of(1, 2, 3)); + assertThrows(IndexOutOfBoundsException.class, () -> window.get(index)); + } + + @Test + void iteratesFromOldestToNewest() { + SlidingWindowAggregator window = sumWindow(3); + window.addAll(List.of(1, 2, 3, 4, 5)); + List seen = new ArrayList<>(); + for (int value : window) { + seen.add(value); + } + assertEquals(List.of(3, 4, 5), seen); + assertEquals("[3, 4, 5]", window.toString()); + } + + @Test + void iteratorIsFailFast() { + SlidingWindowAggregator window = sumWindow(4); + window.addAll(List.of(1, 2, 3)); + Iterator iterator = window.iterator(); + assertEquals(1, iterator.next()); + window.add(4); + assertThrows(ConcurrentModificationException.class, iterator::next); + } + + @Test + void toListIsASnapshot() { + SlidingWindowAggregator window = sumWindow(3); + window.addAll(List.of(1, 2, 3)); + List snapshot = window.toList(); + window.add(4); + assertEquals(List.of(1, 2, 3), snapshot); + } + } + + @Nested + @DisplayName("removal and reuse") + class Removal { + + @Test + void removeOldestShrinksTheWindow() { + SlidingWindowAggregator window = sumWindow(4); + window.addAll(List.of(1, 2, 3, 4)); + assertEquals(1, window.removeOldest()); + assertEquals(9, window.aggregate()); + assertEquals(3, window.size()); + assertFalse(window.isFull()); + + assertEquals(2, window.removeOldest()); + assertEquals(3, window.removeOldest()); + assertEquals(4, window.aggregate()); + assertEquals(4, window.removeOldest()); + assertTrue(window.isEmpty()); + assertThrows(NoSuchElementException.class, window::aggregate); + } + + @Test + void clearRestoresTheInitialState() { + SlidingWindowAggregator window = sumWindow(3); + window.addAll(List.of(1, 2, 3, 4, 5)); + window.clear(); + assertTrue(window.isEmpty()); + assertEquals(0, window.size()); + assertThrows(NoSuchElementException.class, window::aggregate); + + window.addAll(List.of(7, 8)); + assertEquals(15, window.aggregate()); + assertEquals(List.of(7, 8), window.toList()); + } + + @Test + @DisplayName("a capacity of one keeps only the latest element") + void capacityOfOne() { + SlidingWindowAggregator window = sumWindow(1); + window.add(1); + assertEquals(1, window.aggregate()); + assertEquals(1, window.add(2)); + assertEquals(2, window.aggregate()); + assertEquals(2, window.add(3)); + assertEquals(3, window.aggregate()); + assertEquals(1, window.size()); + } + + @Test + @DisplayName("survives far more insertions than its capacity") + void survivesManyRotations() { + SlidingWindowAggregator window = sumWindow(3); + for (int i = 1; i <= 100_000; i++) { + window.add(i); + } + assertEquals(99_998 + 99_999 + 100_000, window.aggregate()); + assertEquals(3, window.size()); + } + } + + @Nested + @DisplayName("agreement with a brute force reference") + class BruteForceAgreement { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 5, 8, 13}) + void matchesBruteForceSum(int capacity) { + assertMatchesBruteForce(capacity, Integer::sum); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 5, 8, 13}) + void matchesBruteForceMaximum(int capacity) { + assertMatchesBruteForce(capacity, BinaryOperator.maxBy(NATURAL)); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 5, 8, 13}) + void matchesBruteForceMinimum(int capacity) { + assertMatchesBruteForce(capacity, BinaryOperator.minBy(NATURAL)); + } + + private void assertMatchesBruteForce(int capacity, BinaryOperator combiner) { + SlidingWindowAggregator window = SlidingWindowAggregator.of(capacity, combiner); + Deque reference = new ArrayDeque<>(); + Random random = new Random(20240517L + capacity); + + for (int step = 0; step < 2_000; step++) { + int value = random.nextInt(2_000) - 1_000; + window.add(value); + reference.addLast(value); + if (reference.size() > capacity) { + reference.removeFirst(); + } + assertEquals(fold(reference, combiner), window.aggregate(), "after step " + step); + } + } + + @Test + @DisplayName("interleaved insertions and removals stay in sync with the reference") + void matchesBruteForceUnderInterleavedOperations() { + int capacity = 7; + SlidingWindowAggregator window = SlidingWindowAggregator.of(capacity, Integer::sum); + Deque reference = new ArrayDeque<>(); + Random random = new Random(987654321L); + + for (int step = 0; step < 20_000; step++) { + if (reference.isEmpty() || random.nextInt(3) > 0) { + int value = random.nextInt(100); + window.add(value); + reference.addLast(value); + if (reference.size() > capacity) { + reference.removeFirst(); + } + } else { + assertEquals(reference.removeFirst(), window.removeOldest()); + } + + assertEquals(reference.size(), window.size()); + assertEquals(new ArrayList<>(reference), window.toList()); + if (reference.isEmpty()) { + assertThrows(NoSuchElementException.class, window::aggregate); + } else { + assertEquals(fold(reference, Integer::sum), window.aggregate(), "after step " + step); + } + } + } + + private Integer fold(Iterable values, BinaryOperator combiner) { + Integer accumulator = null; + for (Integer value : values) { + accumulator = accumulator == null ? value : combiner.apply(accumulator, value); + } + return accumulator; + } + } + + @Test + @DisplayName("the documented example from the class javadoc") + void documentedExample() { + SlidingWindowAggregator window = SlidingWindowAggregator.maximum(3, Comparator.naturalOrder()); + List printed = new ArrayList<>(); + for (int value : new int[] {1, 3, 2, 5, 0}) { + window.add(value); + printed.add(window.aggregate()); + } + assertEquals(List.of(1, 3, 3, 5, 5), printed); + Assertions.assertDoesNotThrow(window::clear); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java b/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java index 8fdc93e1ca22..39e3fa0abe77 100644 --- a/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java @@ -255,4 +255,23 @@ public void testCapacityDoubling() { assertEquals(3, array.getSize()); assertEquals("Charlie", array.get(2)); } + + @Test + public void testContains() { + DynamicArray array = new DynamicArray<>(); + array.add(1); + array.add(2); + array.add(3); + + assertTrue(array.contains(2)); + assertFalse(array.contains(5)); + } + + @Test + public void testContainsWithNull() { + DynamicArray array = new DynamicArray<>(); + array.add(null); + + assertTrue(array.contains(null)); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/BellmanFordTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/BellmanFordTest.java new file mode 100644 index 000000000000..c824241c680d --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/BellmanFordTest.java @@ -0,0 +1,158 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the BellmanFord algorithm implementation. + * Tests cover various graph scenarios including: + * - Simple weighted graphs + * - Graphs with negative weights + * - Single vertex graphs + * - Disconnected graphs + * - Linear path graphs + */ +class BellmanFordTest { + + @Test + void testSimpleGraph() { + // Create a simple graph with 5 vertices and 8 edges + // Graph visualization: + // 1 + // /|\ + // 6 | 7 + // / | \ + // 0 5 2 + // \ | / + // 8 | -2 + // \|/ + // 4---3 + // 9 + BellmanFord bellmanFord = new BellmanFord(5, 8); + bellmanFord.addEdge(0, 1, 6); + bellmanFord.addEdge(0, 4, 8); + bellmanFord.addEdge(1, 2, 7); + bellmanFord.addEdge(1, 4, 5); + bellmanFord.addEdge(2, 3, -2); + bellmanFord.addEdge(2, 4, -3); + bellmanFord.addEdge(3, 4, 9); + bellmanFord.addEdge(4, 3, 7); + + // Verify edge array creation + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(8, bellmanFord.getEdgeArray().length); + } + + @Test + void testGraphWithNegativeWeights() { + // Graph with negative edge weights (but no negative cycle) + BellmanFord bellmanFord = new BellmanFord(4, 5); + bellmanFord.addEdge(0, 1, 4); + bellmanFord.addEdge(0, 2, 5); + bellmanFord.addEdge(1, 2, -3); + bellmanFord.addEdge(2, 3, 4); + bellmanFord.addEdge(1, 3, 6); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(5, bellmanFord.getEdgeArray().length); + } + + @Test + void testSingleVertexGraph() { + // Graph with single vertex and no edges + BellmanFord bellmanFord = new BellmanFord(1, 0); + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(0, bellmanFord.getEdgeArray().length); + } + + @Test + void testLinearGraph() { + // Linear graph: 0 -> 1 -> 2 -> 3 + BellmanFord bellmanFord = new BellmanFord(4, 3); + bellmanFord.addEdge(0, 1, 2); + bellmanFord.addEdge(1, 2, 3); + bellmanFord.addEdge(2, 3, 4); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(3, bellmanFord.getEdgeArray().length); + } + + @Test + void testEdgeAddition() { + BellmanFord bellmanFord = new BellmanFord(3, 3); + + bellmanFord.addEdge(0, 1, 5); + bellmanFord.addEdge(1, 2, 3); + bellmanFord.addEdge(0, 2, 10); + + // Verify all edges were added + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(3, bellmanFord.getEdgeArray().length); + } + + @Test + void testGraphWithZeroWeightEdges() { + // Graph with zero weight edges + BellmanFord bellmanFord = new BellmanFord(3, 3); + bellmanFord.addEdge(0, 1, 0); + bellmanFord.addEdge(1, 2, 0); + bellmanFord.addEdge(0, 2, 1); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(3, bellmanFord.getEdgeArray().length); + } + + @Test + void testLargerGraph() { + // Larger graph with 6 vertices + BellmanFord bellmanFord = new BellmanFord(6, 9); + bellmanFord.addEdge(0, 1, 5); + bellmanFord.addEdge(0, 2, 3); + bellmanFord.addEdge(1, 3, 6); + bellmanFord.addEdge(1, 2, 2); + bellmanFord.addEdge(2, 4, 4); + bellmanFord.addEdge(2, 5, 2); + bellmanFord.addEdge(2, 3, 7); + bellmanFord.addEdge(3, 4, -1); + bellmanFord.addEdge(4, 5, -2); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(9, bellmanFord.getEdgeArray().length); + } + + @Test + void testVertexAndEdgeCount() { + BellmanFord bellmanFord = new BellmanFord(10, 15); + assertEquals(10, bellmanFord.vertex); + assertEquals(15, bellmanFord.edge); + } + + @Test + void testMultipleEdgesBetweenSameVertices() { + // Graph allowing multiple edges between same vertices + BellmanFord bellmanFord = new BellmanFord(2, 3); + bellmanFord.addEdge(0, 1, 5); + bellmanFord.addEdge(0, 1, 3); + bellmanFord.addEdge(1, 0, 2); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(3, bellmanFord.getEdgeArray().length); + } + + @Test + void testCompleteGraph() { + // Complete graph with 4 vertices (6 edges for undirected equivalent) + BellmanFord bellmanFord = new BellmanFord(4, 6); + bellmanFord.addEdge(0, 1, 1); + bellmanFord.addEdge(0, 2, 2); + bellmanFord.addEdge(0, 3, 3); + bellmanFord.addEdge(1, 2, 4); + bellmanFord.addEdge(1, 3, 5); + bellmanFord.addEdge(2, 3, 6); + + assertNotNull(bellmanFord.getEdgeArray()); + assertEquals(6, bellmanFord.getEdgeArray().length); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/ConnectedComponentTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/ConnectedComponentTest.java new file mode 100644 index 000000000000..b5cfdd9de04f --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/ConnectedComponentTest.java @@ -0,0 +1,204 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Graph class in ConnectedComponent.java. + * Tests the depth-first search implementation and connected component counting. + * Covers various graph topologies including: + * - Single connected components + * - Multiple disconnected components + * - Self-loops + * - Linear chains + * - Cyclic graphs + */ +class ConnectedComponentTest { + + @Test + void testSingleConnectedComponent() { + Graph graph = new Graph<>(); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + graph.addEdge(4, 1); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testTwoDisconnectedComponents() { + Graph graph = new Graph<>(); + // Component 1: 1-2-3 + graph.addEdge(1, 2); + graph.addEdge(2, 3); + // Component 2: 4-5 + graph.addEdge(4, 5); + + assertEquals(2, graph.countGraphs()); + } + + @Test + void testThreeDisconnectedComponents() { + Graph graph = new Graph<>(); + // Component 1: a-b-c-d-e + graph.addEdge('a', 'b'); + graph.addEdge('a', 'e'); + graph.addEdge('b', 'e'); + graph.addEdge('b', 'c'); + graph.addEdge('c', 'd'); + graph.addEdge('d', 'a'); + // Component 2: x-y-z + graph.addEdge('x', 'y'); + graph.addEdge('x', 'z'); + // Component 3: w (self-loop) + graph.addEdge('w', 'w'); + + assertEquals(3, graph.countGraphs()); + } + + @Test + void testSingleNodeSelfLoop() { + Graph graph = new Graph<>(); + graph.addEdge(1, 1); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testLinearChain() { + Graph graph = new Graph<>(); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + graph.addEdge(4, 5); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testStarTopology() { + // Star graph with center node 0 connected to nodes 1, 2, 3, 4 + Graph graph = new Graph<>(); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(0, 3); + graph.addEdge(0, 4); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testCompleteGraph() { + // Complete graph K4: every node connected to every other node + Graph graph = new Graph<>(); + graph.addEdge(1, 2); + graph.addEdge(1, 3); + graph.addEdge(1, 4); + graph.addEdge(2, 3); + graph.addEdge(2, 4); + graph.addEdge(3, 4); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testStringVertices() { + Graph graph = new Graph<>(); + // Component 1 + graph.addEdge("New York", "Los Angeles"); + graph.addEdge("Los Angeles", "Chicago"); + // Component 2 + graph.addEdge("London", "Paris"); + // Component 3 + graph.addEdge("Tokyo", "Tokyo"); + + assertEquals(3, graph.countGraphs()); + } + + @Test + void testEmptyGraph() { + Graph graph = new Graph<>(); + assertEquals(0, graph.countGraphs()); + } + + @Test + void testDepthFirstSearchBasic() { + Graph graph = new Graph<>(); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + + // Get the first node and perform DFS + assertNotNull(graph.nodeList); + assertEquals(3, graph.nodeList.size()); + } + + @Test + void testManyIsolatedComponents() { + Graph graph = new Graph<>(); + // Create 5 isolated components (each is a self-loop) + graph.addEdge(1, 1); + graph.addEdge(2, 2); + graph.addEdge(3, 3); + graph.addEdge(4, 4); + graph.addEdge(5, 5); + + assertEquals(5, graph.countGraphs()); + } + + @Test + void testBidirectionalEdges() { + Graph graph = new Graph<>(); + // Note: This is a directed graph representation + // Adding edge 1->2 does not automatically add 2->1 + graph.addEdge(1, 2); + graph.addEdge(2, 1); + graph.addEdge(2, 3); + graph.addEdge(3, 2); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testCyclicGraph() { + Graph graph = new Graph<>(); + // Create a cycle: 1 -> 2 -> 3 -> 4 -> 1 + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + graph.addEdge(4, 1); + + assertEquals(1, graph.countGraphs()); + } + + @Test + void testMultipleCycles() { + Graph graph = new Graph<>(); + // Cycle 1: 1 -> 2 -> 3 -> 1 + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 1); + // Cycle 2: 4 -> 5 -> 4 + graph.addEdge(4, 5); + graph.addEdge(5, 4); + + assertEquals(2, graph.countGraphs()); + } + + @Test + void testIntegerGraphFromMainExample() { + // Recreate the example from main method + Graph graph = new Graph<>(); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(2, 4); + graph.addEdge(3, 5); + graph.addEdge(7, 8); + graph.addEdge(8, 10); + graph.addEdge(10, 8); + + assertEquals(2, graph.countGraphs()); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithmTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithmTest.java index c5df9acdf33b..a189091c17d3 100644 --- a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithmTest.java +++ b/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithmTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.datastructures.graphs; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; @@ -61,4 +62,120 @@ void testInvalidSourceVertex() { assertThrows(IllegalArgumentException.class, () -> dijkstraAlgorithm.run(graph, -1)); assertThrows(IllegalArgumentException.class, () -> dijkstraAlgorithm.run(graph, graph.length)); } + + @Test + void testLinearGraph() { + // Linear graph: 0 - 1 - 2 - 3 + // with weights: 2 3 4 + int[][] linearGraph = {{0, 2, 0, 0}, {2, 0, 3, 0}, {0, 3, 0, 4}, {0, 0, 4, 0}}; + + DijkstraAlgorithm dijkstraLinear = new DijkstraAlgorithm(4); + int[] distances = dijkstraLinear.run(linearGraph, 0); + + assertArrayEquals(new int[] {0, 2, 5, 9}, distances); + } + + @Test + void testStarTopology() { + // Star graph: center node 0 connected to all others + // 1(2) + // | + // 3(4)-0-2(3) + // | + // 4(5) + int[][] starGraph = {{0, 2, 3, 4, 5}, {2, 0, 0, 0, 0}, {3, 0, 0, 0, 0}, {4, 0, 0, 0, 0}, {5, 0, 0, 0, 0}}; + + DijkstraAlgorithm dijkstraStar = new DijkstraAlgorithm(5); + int[] distances = dijkstraStar.run(starGraph, 0); + + assertArrayEquals(new int[] {0, 2, 3, 4, 5}, distances); + } + + @Test + void testCompleteGraphK4() { + // Complete graph K4 with varying weights + int[][] completeGraph = {{0, 1, 2, 3}, {1, 0, 4, 5}, {2, 4, 0, 6}, {3, 5, 6, 0}}; + + DijkstraAlgorithm dijkstraComplete = new DijkstraAlgorithm(4); + int[] distances = dijkstraComplete.run(completeGraph, 0); + + // Direct paths from 0 are shortest + assertArrayEquals(new int[] {0, 1, 2, 3}, distances); + } + + @Test + void testDifferentSourceVertex() { + // Test running from different source vertices + int[][] simpleGraph = {{0, 5, 0, 0}, {5, 0, 3, 0}, {0, 3, 0, 2}, {0, 0, 2, 0}}; + + DijkstraAlgorithm dijkstra = new DijkstraAlgorithm(4); + + // From vertex 0 + int[] distFrom0 = dijkstra.run(simpleGraph, 0); + assertArrayEquals(new int[] {0, 5, 8, 10}, distFrom0); + + // From vertex 2 + int[] distFrom2 = dijkstra.run(simpleGraph, 2); + assertArrayEquals(new int[] {8, 3, 0, 2}, distFrom2); + + // From vertex 3 + int[] distFrom3 = dijkstra.run(simpleGraph, 3); + assertArrayEquals(new int[] {10, 5, 2, 0}, distFrom3); + } + + @Test + void testUnitWeightGraph() { + // Graph with all unit weights (like BFS distance) + int[][] unitGraph = {{0, 1, 1, 0}, {1, 0, 1, 1}, {1, 1, 0, 1}, {0, 1, 1, 0}}; + + DijkstraAlgorithm dijkstraUnit = new DijkstraAlgorithm(4); + int[] distances = dijkstraUnit.run(unitGraph, 0); + + assertArrayEquals(new int[] {0, 1, 1, 2}, distances); + } + + @Test + void testTwoVertexGraph() { + int[][] twoVertexGraph = {{0, 7}, {7, 0}}; + + DijkstraAlgorithm dijkstraTwo = new DijkstraAlgorithm(2); + int[] distances = dijkstraTwo.run(twoVertexGraph, 0); + + assertArrayEquals(new int[] {0, 7}, distances); + } + + @Test + void testShortcutPath() { + // Graph where direct path is longer than indirect path + // 0 --(10)--> 2 + // 0 --(1)--> 1 --(2)--> 2 + int[][] shortcutGraph = {{0, 1, 10}, {1, 0, 2}, {10, 2, 0}}; + + DijkstraAlgorithm dijkstraShortcut = new DijkstraAlgorithm(3); + int[] distances = dijkstraShortcut.run(shortcutGraph, 0); + + // The shortest path to vertex 2 should be 3 (via vertex 1), not 10 (direct) + assertArrayEquals(new int[] {0, 1, 3}, distances); + } + + @Test + void testSourceToSourceDistanceIsZero() { + // Verify distance from source to itself is always 0 + int[] distances = dijkstraAlgorithm.run(graph, 0); + assertEquals(0, distances[0]); + + distances = dijkstraAlgorithm.run(graph, 5); + assertEquals(0, distances[5]); + } + + @Test + void testLargeWeights() { + // Graph with large weights + int[][] largeWeightGraph = {{0, 1000, 0}, {1000, 0, 2000}, {0, 2000, 0}}; + + DijkstraAlgorithm dijkstraLarge = new DijkstraAlgorithm(3); + int[] distances = dijkstraLarge.run(largeWeightGraph, 0); + + assertArrayEquals(new int[] {0, 1000, 3000}, distances); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java deleted file mode 100644 index bf4e2828e069..000000000000 --- a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.thealgorithms.datastructures.graphs; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class DijkstraOptimizedAlgorithmTest { - - private DijkstraOptimizedAlgorithm dijkstraOptimizedAlgorithm; - private int[][] graph; - - @BeforeEach - void setUp() { - graph = new int[][] { - {0, 4, 0, 0, 0, 0, 0, 8, 0}, - {4, 0, 8, 0, 0, 0, 0, 11, 0}, - {0, 8, 0, 7, 0, 4, 0, 0, 2}, - {0, 0, 7, 0, 9, 14, 0, 0, 0}, - {0, 0, 0, 9, 0, 10, 0, 0, 0}, - {0, 0, 4, 14, 10, 0, 2, 0, 0}, - {0, 0, 0, 0, 0, 2, 0, 1, 6}, - {8, 11, 0, 0, 0, 0, 1, 0, 7}, - {0, 0, 2, 0, 0, 0, 6, 7, 0}, - }; - - dijkstraOptimizedAlgorithm = new DijkstraOptimizedAlgorithm(graph.length); - } - - @Test - void testRunAlgorithm() { - int[] expectedDistances = {0, 4, 12, 19, 21, 11, 9, 8, 14}; - assertArrayEquals(expectedDistances, dijkstraOptimizedAlgorithm.run(graph, 0)); - } - - @Test - void testGraphWithDisconnectedNodes() { - int[][] disconnectedGraph = { - {0, 3, 0, 0}, {3, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0} // Node 3 is disconnected - }; - - DijkstraOptimizedAlgorithm dijkstraDisconnected = new DijkstraOptimizedAlgorithm(disconnectedGraph.length); - - // Testing from vertex 0 - int[] expectedDistances = {0, 3, 4, Integer.MAX_VALUE}; // Node 3 is unreachable - assertArrayEquals(expectedDistances, dijkstraDisconnected.run(disconnectedGraph, 0)); - } - - @Test - void testSingleVertexGraph() { - int[][] singleVertexGraph = {{0}}; - DijkstraOptimizedAlgorithm dijkstraSingleVertex = new DijkstraOptimizedAlgorithm(1); - - int[] expectedDistances = {0}; // The only vertex's distance to itself is 0 - assertArrayEquals(expectedDistances, dijkstraSingleVertex.run(singleVertexGraph, 0)); - } - - @Test - void testInvalidSourceVertex() { - assertThrows(IllegalArgumentException.class, () -> dijkstraOptimizedAlgorithm.run(graph, -1)); - assertThrows(IllegalArgumentException.class, () -> dijkstraOptimizedAlgorithm.run(graph, graph.length)); - } -} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/MatrixGraphsTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/MatrixGraphsTest.java index cc8a2df872ce..eaff0222bd36 100644 --- a/src/test/java/com/thealgorithms/datastructures/graphs/MatrixGraphsTest.java +++ b/src/test/java/com/thealgorithms/datastructures/graphs/MatrixGraphsTest.java @@ -137,4 +137,215 @@ void testDisconnectedGraph() { assertTrue(dfs.containsAll(Arrays.asList(0, 1))); assertTrue(bfs.containsAll(Arrays.asList(0, 1))); } + + @Test + void testSingleVertexGraphDfs() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(1); + + List dfs = graph.depthFirstOrder(0); + assertEquals(1, dfs.size()); + assertEquals(0, dfs.getFirst()); + } + + @Test + void testSingleVertexGraphBfs() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(1); + + List bfs = graph.breadthFirstOrder(0); + assertEquals(1, bfs.size()); + assertEquals(0, bfs.getFirst()); + } + + @Test + void testBfsLevelOrder() { + // Create a graph where BFS should visit level by level + // 0 + // /|\ + // 1 2 3 + // | + // 4 + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(0, 3); + graph.addEdge(1, 4); + + List bfs = graph.breadthFirstOrder(0); + assertEquals(5, bfs.size()); + assertEquals(0, bfs.get(0)); + // Level 1 vertices (1, 2, 3) should appear before level 2 vertex (4) + int indexOf4 = bfs.indexOf(4); + assertTrue(bfs.indexOf(1) < indexOf4); + assertTrue(bfs.indexOf(2) < indexOf4); + assertTrue(bfs.indexOf(3) < indexOf4); + } + + @Test + void testDfsStartFromDifferentVertices() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(4); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + + // DFS from vertex 0 + List dfs0 = graph.depthFirstOrder(0); + assertEquals(4, dfs0.size()); + assertEquals(0, dfs0.get(0)); + + // DFS from vertex 2 + List dfs2 = graph.depthFirstOrder(2); + assertEquals(4, dfs2.size()); + assertEquals(2, dfs2.get(0)); + + // DFS from vertex 3 + List dfs3 = graph.depthFirstOrder(3); + assertEquals(4, dfs3.size()); + assertEquals(3, dfs3.get(0)); + } + + @Test + void testBfsStartFromDifferentVertices() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(4); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + + // BFS from vertex 0 + List bfs0 = graph.breadthFirstOrder(0); + assertEquals(4, bfs0.size()); + assertEquals(0, bfs0.get(0)); + + // BFS from vertex 2 + List bfs2 = graph.breadthFirstOrder(2); + assertEquals(4, bfs2.size()); + assertEquals(2, bfs2.get(0)); + } + + @Test + void testStarTopologyBfs() { + // Star graph: 0 is center connected to 1, 2, 3, 4 + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(0, 3); + graph.addEdge(0, 4); + + List bfs = graph.breadthFirstOrder(0); + assertEquals(5, bfs.size()); + assertEquals(0, bfs.get(0)); + // All neighbors should be at distance 1 + assertTrue(bfs.containsAll(Arrays.asList(1, 2, 3, 4))); + } + + @Test + void testStarTopologyDfs() { + // Star graph: 0 is center connected to 1, 2, 3, 4 + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(0, 3); + graph.addEdge(0, 4); + + List dfs = graph.depthFirstOrder(0); + assertEquals(5, dfs.size()); + assertEquals(0, dfs.get(0)); + assertTrue(dfs.containsAll(Arrays.asList(1, 2, 3, 4))); + } + + @Test + void testNegativeStartVertexDfs() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + + List dfs = graph.depthFirstOrder(-1); + assertTrue(dfs.isEmpty()); + } + + @Test + void testNegativeStartVertexBfs() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + + List bfs = graph.breadthFirstOrder(-1); + assertTrue(bfs.isEmpty()); + } + + @Test + void testCompleteGraphKFour() { + // Complete graph K4: every vertex connected to every other vertex + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(4); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(0, 3); + graph.addEdge(1, 2); + graph.addEdge(1, 3); + graph.addEdge(2, 3); + + assertEquals(6, graph.numberOfEdges()); + + List dfs = graph.depthFirstOrder(0); + List bfs = graph.breadthFirstOrder(0); + + assertEquals(4, dfs.size()); + assertEquals(4, bfs.size()); + assertTrue(dfs.containsAll(Arrays.asList(0, 1, 2, 3))); + assertTrue(bfs.containsAll(Arrays.asList(0, 1, 2, 3))); + } + + @Test + void testLargerGraphTraversal() { + // Create a larger graph with 10 vertices + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(10); + graph.addEdge(0, 1); + graph.addEdge(0, 2); + graph.addEdge(1, 3); + graph.addEdge(1, 4); + graph.addEdge(2, 5); + graph.addEdge(2, 6); + graph.addEdge(3, 7); + graph.addEdge(4, 8); + graph.addEdge(5, 9); + + List dfs = graph.depthFirstOrder(0); + List bfs = graph.breadthFirstOrder(0); + + assertEquals(10, dfs.size()); + assertEquals(10, bfs.size()); + assertEquals(0, dfs.get(0)); + assertEquals(0, bfs.get(0)); + } + + @Test + void testSelfLoop() { + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(3); + graph.addEdge(0, 0); // Self loop + graph.addEdge(0, 1); + graph.addEdge(1, 2); + + List dfs = graph.depthFirstOrder(0); + List bfs = graph.breadthFirstOrder(0); + + assertEquals(3, dfs.size()); + assertEquals(3, bfs.size()); + } + + @Test + void testLinearGraphTraversal() { + // Linear graph: 0 - 1 - 2 - 3 - 4 + AdjacencyMatrixGraph graph = new AdjacencyMatrixGraph(5); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + + List dfs = graph.depthFirstOrder(0); + List bfs = graph.breadthFirstOrder(0); + + assertEquals(5, dfs.size()); + assertEquals(5, bfs.size()); + + // In a linear graph, BFS and DFS starting from 0 should be the same + assertEquals(Arrays.asList(0, 1, 2, 3, 4), dfs); + assertEquals(Arrays.asList(0, 1, 2, 3, 4), bfs); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java new file mode 100644 index 000000000000..003f6fcee812 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java @@ -0,0 +1,134 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class RottingOrangesTest { + + @Test + void testAllOrangesRotInSingleMinute() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } + + @Test + void testImpossibleToRotAllOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testNoFreshOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 2}, {2, 2}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testNoRottenOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{1, 1}, {1, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testEmptyGrid() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testSingleRottenOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testSingleFreshOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testSingleFreshOrangeNextToRottenOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1}}; + + assertEquals(1, rottingOranges.run(grid)); + } + + @Test + void testMultipleRottenSources() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 0, 2}, {1, 1, 1, 1}, {0, 1, 1, 1}}; + + assertEquals(3, rottingOranges.run(grid)); + } + + @Test + void testFreshOrangeBlockedByEmptyCells() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 0, 1}, {0, 0, 0}, {1, 0, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testLinearSpread() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } + + @Test + void testVerticalSpread() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2}, {1}, {1}, {1}}; + + assertEquals(3, rottingOranges.run(grid)); + } + + @Test + void testGridWithOnlyEmptyCells() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{0, 0}, {0, 0}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testComplexGrid() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {1, 1, 1}, {1, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java index 5d1733a3e97c..6b6e670a258b 100644 --- a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java @@ -1,11 +1,9 @@ package com.thealgorithms.datastructures.hashmap.hashing; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class GenericHashMapUsingArrayTest { @@ -16,10 +14,10 @@ void testGenericHashmapWhichUsesArrayAndBothKeyAndValueAreStrings() { map.put("Nepal", "Kathmandu"); map.put("India", "New Delhi"); map.put("Australia", "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Kathmandu", map.get("Nepal")); - assertEquals("Sydney", map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Kathmandu", map.get("Nepal")); + Assertions.assertEquals("Sydney", map.get("Australia")); } @Test @@ -29,12 +27,12 @@ void testGenericHashmapWhichUsesArrayAndKeyIsStringValueIsInteger() { map.put("Nepal", 25); map.put("India", 101); map.put("Australia", 99); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals(25, map.get("Nepal")); - assertEquals(99, map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals(25, map.get("Nepal")); + Assertions.assertEquals(99, map.get("Australia")); map.remove("Nepal"); - assertFalse(map.containsKey("Nepal")); + Assertions.assertFalse(map.containsKey("Nepal")); } @Test @@ -44,11 +42,11 @@ void testGenericHashmapWhichUsesArrayAndKeyIsIntegerValueIsString() { map.put(34, "Kathmandu"); map.put(46, "New Delhi"); map.put(89, "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Sydney", map.get(89)); - assertEquals("Washington DC", map.get(101)); - assertTrue(map.containsKey(46)); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Sydney", map.get(89)); + Assertions.assertEquals("Washington DC", map.get(101)); + Assertions.assertTrue(map.containsKey(46)); } @Test @@ -56,7 +54,7 @@ void testRemoveNonExistentKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.remove("Nepal"); // Attempting to remove a non-existent key - assertEquals(1, map.size()); // Size should remain the same + Assertions.assertEquals(1, map.size()); // Size should remain the same } @Test @@ -65,8 +63,8 @@ void testRehashing() { for (int i = 0; i < 20; i++) { map.put("Key" + i, "Value" + i); } - assertEquals(20, map.size()); // Ensure all items were added - assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash + Assertions.assertEquals(20, map.size()); // Ensure all items were added + Assertions.assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash } @Test @@ -74,7 +72,7 @@ void testUpdateValueForExistingKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.put("USA", "New Washington DC"); // Updating value for existing key - assertEquals("New Washington DC", map.get("USA")); + Assertions.assertEquals("New Washington DC", map.get("USA")); } @Test @@ -83,14 +81,154 @@ void testToStringMethod() { map.put("USA", "Washington DC"); map.put("Nepal", "Kathmandu"); String expected = "{USA : Washington DC, Nepal : Kathmandu}"; - assertEquals(expected, map.toString()); + Assertions.assertEquals(expected, map.toString()); } @Test void testContainsKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); - assertTrue(map.containsKey("USA")); - assertFalse(map.containsKey("Nepal")); + Assertions.assertTrue(map.containsKey("USA")); + Assertions.assertFalse(map.containsKey("Nepal")); + } + + // ======= Added tests from the new version ======= + + @Test + void shouldThrowNullPointerExceptionForNullKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + String nullKey = null; // Use variable to avoid static analysis false positive + Assertions.assertThrows(NullPointerException.class, () -> map.put(nullKey, "value")); + } + + @Test + void shouldStoreNullValueForKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyWithNullValue", null); + Assertions.assertEquals(1, map.size()); + Assertions.assertNull(map.get("keyWithNullValue")); + // Note: containsKey returns false for null values due to implementation + Assertions.assertFalse(map.containsKey("keyWithNullValue")); + } + + @Test + void shouldHandleCollisionWhenKeysHashToSameBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(200, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleEmptyStringAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("", "valueForEmptyKey"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("valueForEmptyKey", map.get("")); + Assertions.assertTrue(map.containsKey("")); + } + + @Test + void shouldHandleEmptyStringAsValue() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyForEmptyValue", ""); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("", map.get("keyForEmptyValue")); + Assertions.assertTrue(map.containsKey("keyForEmptyValue")); + } + + @Test + void shouldHandleNegativeIntegerKeys() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(-1, 100); + map.put(-100, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(-1)); + Assertions.assertEquals(200, map.get(-100)); + Assertions.assertTrue(map.containsKey(-1)); + Assertions.assertTrue(map.containsKey(-100)); + } + + @Test + void shouldHandleZeroAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(0, 100); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(100, map.get(0)); + Assertions.assertTrue(map.containsKey(0)); + } + + @Test + void shouldHandleStringWithSpecialCharacters() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("key!@#$%^&*()", "value<>?/\\|"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value<>?/\\|", map.get("key!@#$%^&*()")); + Assertions.assertTrue(map.containsKey("key!@#$%^&*()")); + } + + @Test + void shouldHandleLongStrings() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + StringBuilder longKey = new StringBuilder(); + StringBuilder longValue = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + longKey.append("a"); + longValue.append("b"); + } + String key = longKey.toString(); + String value = longValue.toString(); + map.put(key, value); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(value, map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "ab", "abc", "test", "longerString"}) + void shouldHandleKeysOfDifferentLengths(String key) { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(key, "value"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value", map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @Test + void shouldHandleUpdateOnExistingKeyInCollisionBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + map.put(key2, 999); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(999, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleExactlyLoadFactorBoundary() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + // Fill exactly to load factor (12 items with capacity 16 and 0.75 load factor) + for (int i = 0; i < 12; i++) { + map.put(i, i * 10); + } + Assertions.assertEquals(12, map.size()); + // Act - This should trigger rehash on 13th item + map.put(12, 120); + // Assert - Rehash should have happened + Assertions.assertEquals(13, map.size()); + Assertions.assertEquals(120, map.get(12)); + Assertions.assertTrue(map.containsKey(12)); } } diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMapTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMapTest.java new file mode 100644 index 000000000000..3d3fe63d775a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ImmutableHashMapTest.java @@ -0,0 +1,55 @@ +package com.thealgorithms.datastructures.hashmap.hashing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ImmutableHashMapTest { + + @Test + void testEmptyMap() { + ImmutableHashMap map = ImmutableHashMap.empty(); + + assertEquals(0, map.size()); + assertNull(map.get("A")); + } + + @Test + void testPutDoesNotModifyOriginalMap() { + ImmutableHashMap map1 = ImmutableHashMap.empty(); + + ImmutableHashMap map2 = map1.put("A", 1); + + assertEquals(0, map1.size()); + assertEquals(1, map2.size()); + assertNull(map1.get("A")); + assertEquals(1, map2.get("A")); + } + + @Test + void testMultiplePuts() { + ImmutableHashMap map = ImmutableHashMap.empty().put("A", 1).put("B", 2); + + assertEquals(2, map.size()); + assertEquals(1, map.get("A")); + assertEquals(2, map.get("B")); + } + + @Test + void testContainsKey() { + ImmutableHashMap map = ImmutableHashMap.empty().put("X", 100); + + assertTrue(map.containsKey("X")); + assertFalse(map.containsKey("Y")); + } + + @Test + void testNullKey() { + ImmutableHashMap map = ImmutableHashMap.empty().put(null, 50); + + assertEquals(50, map.get(null)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java index 44551a8adac6..ef7739a2e8a9 100644 --- a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java @@ -81,19 +81,19 @@ void containsTest() { @Test void sizeTest() { Map map = getMap(); - assertEquals(map.size(), 0); + assertEquals(0, map.size()); for (int i = -100; i < 100; i++) { map.put(i, String.valueOf(i)); } - assertEquals(map.size(), 200); + assertEquals(200, map.size()); for (int i = -50; i < 50; i++) { map.delete(i); } - assertEquals(map.size(), 100); + assertEquals(100, map.size()); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java b/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java index d04a9de8a94b..792969200c82 100644 --- a/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java +++ b/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -39,7 +40,7 @@ void testEquals() { assertEquals(element1, element2); // Same key and info assertNotEquals(element1, element3); // Different key - assertNotEquals(null, element1); // Check for null + assertNotNull(element1); assertNotEquals("String", element1); // Check for different type } diff --git a/src/test/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueueTest.java b/src/test/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueueTest.java new file mode 100644 index 000000000000..8d8c4e1db6bd --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/heaps/IndexedPriorityQueueTest.java @@ -0,0 +1,350 @@ +package com.thealgorithms.datastructures.heaps; + +import java.util.Comparator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link IndexedPriorityQueue}. + * + * Notes: + * - We mainly use a Node class with a mutable "prio" field to test changeKey/decreaseKey/increaseKey. + * - The queue is a min-heap, so smaller "prio" means higher priority. + * - By default the implementation uses IdentityHashMap so duplicate-equals objects are allowed. + */ +public class IndexedPriorityQueueTest { + + // ------------------------ + // Helpers + // ------------------------ + + /** Simple payload with mutable priority. */ + static class Node { + final String id; + int prio; // lower is better (min-heap) + + Node(String id, int prio) { + this.id = id; + this.prio = prio; + } + + @Override + public String toString() { + return id + "(" + prio + ")"; + } + } + + /** Same as Node but overrides equals/hashCode to simulate "duplicate-equals" scenario. */ + static class NodeWithEquals { + final String id; + int prio; + + NodeWithEquals(String id, int prio) { + this.id = id; + this.prio = prio; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NodeWithEquals)) { + return false; + } + NodeWithEquals other = (NodeWithEquals) o; + // Intentionally naive equality: equal if priority is equal + return this.prio == other.prio; + } + + @Override + public int hashCode() { + return Integer.hashCode(prio); + } + + @Override + public String toString() { + return id + "(" + prio + ")"; + } + } + + private static IndexedPriorityQueue newNodePQ() { + return new IndexedPriorityQueue<>(Comparator.comparingInt(n -> n.prio)); + } + + // ------------------------ + // Basic operations + // ------------------------ + + @Test + void testOfferPollWithIntegersComparableMode() { + // cmp == null -> elements must be Comparable + IndexedPriorityQueue pq = new IndexedPriorityQueue<>(); + Assertions.assertTrue(pq.isEmpty()); + + pq.offer(5); + pq.offer(1); + pq.offer(3); + + Assertions.assertEquals(3, pq.size()); + Assertions.assertEquals(1, pq.peek()); + Assertions.assertEquals(1, pq.poll()); + Assertions.assertEquals(3, pq.poll()); + Assertions.assertEquals(5, pq.poll()); + Assertions.assertNull(pq.poll()); // empty -> null + Assertions.assertTrue(pq.isEmpty()); + } + + @Test + void testPeekAndIsEmpty() { + IndexedPriorityQueue pq = newNodePQ(); + Assertions.assertTrue(pq.isEmpty()); + Assertions.assertNull(pq.peek()); + + pq.offer(new Node("A", 10)); + pq.offer(new Node("B", 5)); + pq.offer(new Node("C", 7)); + + Assertions.assertFalse(pq.isEmpty()); + Assertions.assertEquals("B(5)", pq.peek().toString()); + } + + @Test + void testRemoveSpecificElement() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + Node b = new Node("B", 5); + Node c = new Node("C", 7); + + pq.offer(a); + pq.offer(b); + pq.offer(c); + + // remove by reference (O(log n)) + Assertions.assertTrue(pq.remove(b)); + Assertions.assertEquals(2, pq.size()); + // now min should be C(7) + Assertions.assertEquals("C(7)", pq.peek().toString()); + // removing an element not present -> false + Assertions.assertFalse(pq.remove(b)); + } + + @Test + void testContainsAndClear() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 2); + Node b = new Node("B", 3); + + pq.offer(a); + pq.offer(b); + + Assertions.assertTrue(pq.contains(a)); + Assertions.assertTrue(pq.contains(b)); + + pq.clear(); + Assertions.assertTrue(pq.isEmpty()); + Assertions.assertFalse(pq.contains(a)); + Assertions.assertNull(pq.peek()); + } + + // ------------------------ + // Key updates + // ------------------------ + + @Test + void testDecreaseKeyMovesUp() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + Node b = new Node("B", 5); + Node c = new Node("C", 7); + + pq.offer(a); + pq.offer(b); + pq.offer(c); + + // current min is B(5) + Assertions.assertEquals("B(5)", pq.peek().toString()); + + // Make A more important: 10 -> 1 (smaller is better) + pq.decreaseKey(a, n -> n.prio = 1); + + // Now A should be at the top + Assertions.assertEquals("A(1)", pq.peek().toString()); + } + + @Test + void testIncreaseKeyMovesDown() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 1); + Node b = new Node("B", 2); + Node c = new Node("C", 3); + + pq.offer(a); + pq.offer(b); + pq.offer(c); + + // min is A(1) + Assertions.assertEquals("A(1)", pq.peek().toString()); + + // Make A worse: 1 -> 100 + pq.increaseKey(a, n -> n.prio = 100); + + // Now min should be B(2) + Assertions.assertEquals("B(2)", pq.peek().toString()); + } + + @Test + void testChangeKeyChoosesDirectionAutomatically() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + Node b = new Node("B", 20); + Node c = new Node("C", 30); + + pq.offer(a); + pq.offer(b); + pq.offer(c); + + // Decrease B to 0 -> should move up + pq.changeKey(b, n -> n.prio = 0); + Assertions.assertEquals("B(0)", pq.peek().toString()); + + // Increase B to 100 -> should move down + pq.changeKey(b, n -> n.prio = 100); + Assertions.assertEquals("A(10)", pq.peek().toString()); + } + + @Test + void testDirectMutationWithoutChangeKeyDoesNotReheapByDesign() { + // Demonstrates the contract: do NOT mutate comparator fields directly. + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 5); + Node b = new Node("B", 10); + + pq.offer(a); + pq.offer(b); + + // Illegally mutate priority directly + a.prio = 100; // worse than b now, but heap wasn't notified + + // The heap structure is unchanged; peek still returns A(100) (was A(5) before) + // This test documents the behavior/contract rather than relying on it. + Assertions.assertEquals("A(100)", pq.peek().toString()); + + // Now fix properly via changeKey (no change in value, but triggers reheap) + pq.changeKey(a, n -> n.prio = n.prio); + Assertions.assertEquals("B(10)", pq.peek().toString()); + } + + // ------------------------ + // Identity semantics & duplicates + // ------------------------ + + @Test + void testDuplicateEqualsElementsAreSupportedIdentityMap() { + IndexedPriorityQueue pq = new IndexedPriorityQueue<>(Comparator.comparingInt(n -> n.prio)); + + NodeWithEquals x1 = new NodeWithEquals("X1", 7); + NodeWithEquals x2 = new NodeWithEquals("X2", 7); // equals to X1 by prio, but different instance + + // With IdentityHashMap internally, both can coexist + pq.offer(x1); + pq.offer(x2); + + Assertions.assertEquals(2, pq.size()); + // Poll twice; both 7s should be returned (order between x1/x2 is unspecified) + Assertions.assertEquals(7, pq.poll().prio); + Assertions.assertEquals(7, pq.poll().prio); + Assertions.assertTrue(pq.isEmpty()); + } + + // ------------------------ + // Capacity growth + // ------------------------ + + @Test + void testGrowByManyInserts() { + IndexedPriorityQueue pq = new IndexedPriorityQueue<>(); + int n = 100; // beyond default capacity (11) + + for (int i = n; i >= 1; i--) { + pq.offer(i); + } + + Assertions.assertEquals(n, pq.size()); + // Ensure min-to-max order when polling + for (int expected = 1; expected <= n; expected++) { + Integer v = pq.poll(); + Assertions.assertEquals(expected, v); + } + Assertions.assertTrue(pq.isEmpty()); + Assertions.assertNull(pq.poll()); + } + + // ------------------------ + // remove/contains edge cases + // ------------------------ + + @Test + void testRemoveHeadAndMiddleAndTail() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 1); + Node b = new Node("B", 2); + Node c = new Node("C", 3); + Node d = new Node("D", 4); + + pq.offer(a); + pq.offer(b); + pq.offer(c); + pq.offer(d); + + // remove head + Assertions.assertTrue(pq.remove(a)); + Assertions.assertFalse(pq.contains(a)); + Assertions.assertEquals("B(2)", pq.peek().toString()); + + // remove middle + Assertions.assertTrue(pq.remove(c)); + Assertions.assertFalse(pq.contains(c)); + Assertions.assertEquals("B(2)", pq.peek().toString()); + + // remove tail (last) + Assertions.assertTrue(pq.remove(d)); + Assertions.assertFalse(pq.contains(d)); + Assertions.assertEquals("B(2)", pq.peek().toString()); + + // remove last remaining + Assertions.assertTrue(pq.remove(b)); + Assertions.assertTrue(pq.isEmpty()); + Assertions.assertNull(pq.peek()); + } + + // ------------------------ + // Error / edge cases for coverage + // ------------------------ + + @Test + void testInvalidInitialCapacityThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new IndexedPriorityQueue(0, Comparator.naturalOrder())); + } + + @Test + void testChangeKeyOnMissingElementThrows() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + + Assertions.assertThrows(IllegalArgumentException.class, () -> pq.changeKey(a, n -> n.prio = 5)); + } + + @Test + void testDecreaseKeyOnMissingElementThrows() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + + Assertions.assertThrows(IllegalArgumentException.class, () -> pq.decreaseKey(a, n -> n.prio = 5)); + } + + @Test + void testIncreaseKeyOnMissingElementThrows() { + IndexedPriorityQueue pq = newNodePQ(); + Node a = new Node("A", 10); + + Assertions.assertThrows(IllegalArgumentException.class, () -> pq.increaseKey(a, n -> n.prio = 15)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java index 5483bbcd0394..4390c0f5f2eb 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java @@ -2,98 +2,51 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class MergeSortedArrayListTest { - @Test - void testMergeTwoSortedLists() { - List listA = Arrays.asList(1, 3, 5, 7, 9); - List listB = Arrays.asList(2, 4, 6, 8, 10); + @ParameterizedTest(name = "{3}") + @MethodSource("provideMergeTestData") + void testMergeParameterizedScenarios(List listA, List listB, List expected, String scenarioName) { List result = new ArrayList<>(); - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - assertEquals(expected, result, "Merged list should be sorted and contain all elements from both input lists."); + assertEquals(expected, result, () -> "Failed scenario: " + scenarioName); } - @Test - void testMergeWithEmptyList() { - List listA = Arrays.asList(1, 2, 3); - List listB = new ArrayList<>(); // Empty list - List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 3); - assertEquals(expected, result, "Merged list should match listA when listB is empty."); - } - - @Test - void testMergeWithBothEmptyLists() { - List listA = new ArrayList<>(); // Empty list - List listB = new ArrayList<>(); // Empty list - List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - assertTrue(result.isEmpty(), "Merged list should be empty when both input lists are empty."); + private static Stream provideMergeTestData() { + return Stream.of(Arguments.of(Arrays.asList(1, 3, 5, 7, 9), Arrays.asList(2, 4, 6, 8, 10), Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), "Standard alternating sorted lists"), Arguments.of(Arrays.asList(1, 2, 3), new ArrayList<>(), Arrays.asList(1, 2, 3), "Merge with empty second list"), + Arguments.of(new ArrayList<>(), Arrays.asList(4, 5, 6), Arrays.asList(4, 5, 6), "Merge with empty first list"), Arguments.of(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), "Merge with both lists empty"), + Arguments.of(Arrays.asList(1, 2, 2, 3), Arrays.asList(2, 3, 4), Arrays.asList(1, 2, 2, 2, 3, 3, 4), "Handling duplicate elements gracefully"), + Arguments.of(Arrays.asList(-3, -1, 2), Arrays.asList(-2, 0, 3), Arrays.asList(-3, -2, -1, 0, 2, 3), "Handling negative numbers mixed with positive numbers")); } @Test - void testMergeWithDuplicateElements() { - List listA = Arrays.asList(1, 2, 2, 3); - List listB = Arrays.asList(2, 3, 4); + void testMergeThrowsExceptionWhenListAIsNull() { + List listB = Arrays.asList(1, 2, 3); List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 2, 2, 3, 3, 4); - assertEquals(expected, result, "Merged list should correctly handle and include duplicate elements."); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(null, listB, result)); } @Test - void testMergeWithNegativeAndPositiveNumbers() { - List listA = Arrays.asList(-3, -1, 2); - List listB = Arrays.asList(-2, 0, 3); + void testMergeThrowsExceptionWhenListBIsNull() { + List listA = Arrays.asList(1, 2, 3); List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(-3, -2, -1, 0, 2, 3); - assertEquals(expected, result, "Merged list should correctly handle negative and positive numbers."); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(listA, null, result)); } @Test - void testMergeThrowsExceptionOnNullInput() { - List listA = null; - List listB = Arrays.asList(1, 2, 3); - List result = new ArrayList<>(); - - List finalListB = listB; - List finalListA = listA; - List finalResult = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA, finalListB, finalResult), "Should throw NullPointerException if any input list is null."); - - listA = Arrays.asList(1, 2, 3); - listB = null; - List finalListA1 = listA; - List finalListB1 = listB; - List finalResult1 = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA1, finalListB1, finalResult1), "Should throw NullPointerException if any input list is null."); - - listA = Arrays.asList(1, 2, 3); - listB = Arrays.asList(4, 5, 6); - result = null; - List finalListA2 = listA; - List finalListB2 = listB; - List finalResult2 = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA2, finalListB2, finalResult2), "Should throw NullPointerException if the result collection is null."); + void testMergeThrowsExceptionWhenResultCollectionIsNull() { + List listA = Arrays.asList(1, 2, 3); + List listB = Arrays.asList(4, 5, 6); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(listA, listB, null)); } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java new file mode 100644 index 000000000000..ba5614a07916 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java @@ -0,0 +1,74 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Objects; +import org.junit.jupiter.api.Test; + +public class MiddleOfLinkedListTest { + + private static SinglyLinkedListNode listOf(int firstValue, int... remainingValues) { + SinglyLinkedListNode head = new SinglyLinkedListNode(firstValue); + SinglyLinkedListNode current = head; + + for (int i = 0; i < remainingValues.length; i++) { + current.next = new SinglyLinkedListNode(remainingValues[i]); + current = current.next; + } + return head; + } + + @Test + void middleNodeOddLength() { + SinglyLinkedListNode head = listOf(1, 2, 3, 4, 5); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(3, middle.value); + } + + @Test + void middleNodeEvenLengthReturnsSecondMiddle() { + SinglyLinkedListNode head = listOf(1, 2, 3, 4, 5, 6); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(4, middle.value); + } + + @Test + void middleNodeSingleElement() { + SinglyLinkedListNode head = listOf(42); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(42, middle.value); + } + + @Test + void middleNodeTwoElementsReturnsSecond() { + SinglyLinkedListNode head = listOf(10, 20); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(20, middle.value); + } + + @Test + void middleNodeNullHead() { + assertNull(MiddleOfLinkedList.middleNode(null)); + } + + @Test + void middleNodeDoesNotModifyListStructure() { + SinglyLinkedListNode first = new SinglyLinkedListNode(1); + SinglyLinkedListNode second = new SinglyLinkedListNode(2); + SinglyLinkedListNode third = new SinglyLinkedListNode(3); + SinglyLinkedListNode fourth = new SinglyLinkedListNode(4); + + first.next = second; + second.next = third; + third.next = fourth; + + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(first)); + assertEquals(3, middle.value); + + assertEquals(second, first.next); + assertEquals(third, second.next); + assertEquals(fourth, third.next); + assertNull(fourth.next); + } +} diff --git a/src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java similarity index 98% rename from src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java rename to src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java index 0f0577d39094..10f6b8536b19 100644 --- a/src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java @@ -1,9 +1,8 @@ -package com.thealgorithms.misc; +package com.thealgorithms.datastructures.lists; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.thealgorithms.datastructures.lists.SinglyLinkedList; import org.junit.jupiter.api.Test; public class PalindromeSinglyLinkedListTest { diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java new file mode 100644 index 000000000000..1d397cf09301 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java @@ -0,0 +1,119 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SelfOrganizingLinkedListTest { + + private SelfOrganizingLinkedList list; + + @BeforeEach + void setUp() { + list = new SelfOrganizingLinkedList<>(); + } + + @Test + void testEmptyListAndGetters() { + assertTrue(list.isEmpty()); + assertEquals(0, list.getSize()); + assertNull(list.getHeadValue()); + assertFalse(list.search(10)); + } + + @Test + void testInsertAndSizeState() { + assertTrue(list.isEmpty()); + list.insert(10); + assertFalse(list.isEmpty()); + assertEquals(1, list.getSize()); + + list.insert(20); + assertEquals(2, list.getSize()); + } + + @Test + void testMoveMiddleElementToFrontPreservesFullListStructure() { + list.insert(10); + list.insert(20); + list.insert(30); + list.insert(40); + + // Initial order: [10, 20, 30, 40] + assertTrue(list.search(30)); + + // Expected order: [30, 10, 20, 40] + assertEquals(4, list.getSize()); + assertEquals(30, list.getHeadValue()); + + // Sequential head tracking to verify middle and tail pointers didn't break + assertTrue(list.search(10)); // [10, 30, 20, 40] + assertEquals(10, list.getHeadValue()); + + assertTrue(list.search(20)); // [20, 10, 30, 40] + assertEquals(20, list.getHeadValue()); + + assertTrue(list.search(40)); // [40, 20, 10, 30] + assertEquals(40, list.getHeadValue()); + assertEquals(4, list.getSize()); + } + + @Test + void testMoveLastElementToFrontPreservesFullListStructure() { + list.insert(10); + list.insert(20); + list.insert(30); + + // Search tail element '30' + assertTrue(list.search(30)); // Order becomes [30, 10, 20] + + assertEquals(30, list.getHeadValue()); + assertEquals(3, list.getSize()); + + // Verify remaining chain order [10, 20] + assertTrue(list.search(20)); // [20, 30, 10] + assertEquals(20, list.getHeadValue()); + + assertTrue(list.search(10)); // [10, 20, 30] + assertEquals(10, list.getHeadValue()); + assertEquals(3, list.getSize()); + } + + @Test + void testSearchNonExistentElementPreservesStructureAndSize() { + list.insert(10); + list.insert(20); + list.insert(30); + + assertFalse(list.search(99)); + assertEquals(3, list.getSize()); + assertEquals(10, list.getHeadValue()); + } + + @Test + void testDuplicateValuesMovesFirstMatchedToFront() { + list.insert(10); + list.insert(20); + list.insert(10); // Duplicate '10' at tail + list.insert(30); + + // Initial list state: [10, 20, 10, 30] + // Searching '10' hits the head immediately -> no re-linking + assertTrue(list.search(10)); + assertEquals(10, list.getHeadValue()); + assertEquals(4, list.getSize()); + + // Searching '20' moves middle element to head: [20, 10, 10, 30] + assertTrue(list.search(20)); + assertEquals(20, list.getHeadValue()); + + // Searching '10' moves the FIRST instance of '10' (index 1) to head: [10, 20, 10, 30] + assertTrue(list.search(10)); + assertEquals(10, list.getHeadValue()); + assertEquals(4, list.getSize()); + } +} 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"}; diff --git a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java index e97fe091c556..3bb8bbabb761 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java @@ -9,14 +9,14 @@ class PriorityQueuesTest { void testPQInsertion() { PriorityQueue myQueue = new PriorityQueue(4); myQueue.insert(2); - Assertions.assertEquals(myQueue.peek(), 2); + Assertions.assertEquals(2, myQueue.peek()); myQueue.insert(5); myQueue.insert(3); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(5, myQueue.peek()); myQueue.insert(10); - Assertions.assertEquals(myQueue.peek(), 10); + Assertions.assertEquals(10, myQueue.peek()); } @Test @@ -28,32 +28,32 @@ void testPQDeletion() { myQueue.insert(10); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(5, myQueue.peek()); myQueue.remove(); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 2); + Assertions.assertEquals(2, myQueue.peek()); } @Test void testPQExtra() { PriorityQueue myQueue = new PriorityQueue(4); - Assertions.assertEquals(myQueue.isEmpty(), true); - Assertions.assertEquals(myQueue.isFull(), false); + Assertions.assertTrue(myQueue.isEmpty()); + Assertions.assertFalse(myQueue.isFull()); myQueue.insert(2); myQueue.insert(5); - Assertions.assertEquals(myQueue.isFull(), false); + Assertions.assertFalse(myQueue.isFull()); myQueue.insert(3); myQueue.insert(10); - Assertions.assertEquals(myQueue.isEmpty(), false); - Assertions.assertEquals(myQueue.isFull(), true); + Assertions.assertFalse(myQueue.isEmpty()); + Assertions.assertTrue(myQueue.isFull()); myQueue.remove(); - Assertions.assertEquals(myQueue.getSize(), 3); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(3, myQueue.getSize()); + Assertions.assertEquals(5, myQueue.peek()); myQueue.remove(); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 2); - Assertions.assertEquals(myQueue.getSize(), 1); + Assertions.assertEquals(2, myQueue.peek()); + Assertions.assertEquals(1, myQueue.getSize()); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java b/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java new file mode 100644 index 000000000000..e3abe15b6a46 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.datastructures.queues; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedList; +import java.util.Queue; +import org.junit.jupiter.api.Test; + +class ReverseQueueRecursionTest { + @Test + void testReverseMultipleElements() { + Queue queue = new LinkedList<>(); + queue.add(1); + queue.add(2); + queue.add(3); + queue.add(4); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals(4, queue.poll()); + assertEquals(3, queue.poll()); + assertEquals(2, queue.poll()); + assertEquals(1, queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseSingleElement() { + Queue queue = new LinkedList<>(); + queue.add(42); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals(42, queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseEmptyQueue() { + Queue queue = new LinkedList<>(); + ReverseQueueRecursion.reverseQueue(queue); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseStringQueue() { + Queue queue = new LinkedList<>(); + queue.add("A"); + queue.add("B"); + queue.add("C"); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals("C", queue.poll()); + assertEquals("B", queue.poll()); + assertEquals("A", queue.poll()); + assertTrue(queue.isEmpty()); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java b/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java new file mode 100644 index 000000000000..4c038c05b167 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java @@ -0,0 +1,295 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ThreadSafeQueueTest { + + @Test + public void testEnqueueDequeue() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + + Assertions.assertEquals(3, queue.size()); + Assertions.assertEquals(1, queue.dequeue()); + Assertions.assertEquals(2, queue.dequeue()); + Assertions.assertEquals(3, queue.dequeue()); + Assertions.assertTrue(queue.isEmpty()); + } + + @Test + public void testOfferPoll() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.offer("a")); + Assertions.assertTrue(queue.offer("b")); + Assertions.assertFalse(queue.offer("c")); + + Assertions.assertEquals("a", queue.poll()); + Assertions.assertEquals("b", queue.poll()); + Assertions.assertNull(queue.poll()); + } + + @Test + public void testOfferRejectsWhenFull() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.offer(1)); + Assertions.assertTrue(queue.offer(2)); + Assertions.assertFalse(queue.offer(3)); + Assertions.assertEquals(2, queue.size()); + } + + @Test + public void testPollReturnsNullWhenEmpty() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertNull(queue.poll()); + } + + @Test + public void testEnqueueNullThrows() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertThrows(IllegalArgumentException.class, () -> queue.enqueue(null)); + } + + @Test + public void testOfferNullThrows() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertThrows(IllegalArgumentException.class, () -> queue.offer(null)); + } + + @Test + public void testInvalidCapacityThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ThreadSafeQueue<>(0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ThreadSafeQueue<>(-1)); + } + + @Test + public void testIsEmptyAndIsFull() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.enqueue(1); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.enqueue(2); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertTrue(queue.isFull()); + + queue.dequeue(); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.dequeue(); + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + } + + @Test + public void testCapacity() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(10); + Assertions.assertEquals(10, queue.capacity()); + } + + @Test + public void testCircularBufferWrapAround() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(3); + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + + Assertions.assertEquals(1, queue.dequeue()); + Assertions.assertEquals(2, queue.dequeue()); + + queue.enqueue(4); + queue.enqueue(5); + + Assertions.assertEquals(3, queue.dequeue()); + Assertions.assertEquals(4, queue.dequeue()); + Assertions.assertEquals(5, queue.dequeue()); + } + + @Test + public void testMultipleProducersSingleConsumer() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(100); + int numProducers = 4; + int itemsPerProducer = 250; + int totalItems = numProducers * itemsPerProducer; + CountDownLatch doneLatch = new CountDownLatch(numProducers); + List results = new ArrayList<>(); + + ExecutorService executor = Executors.newFixedThreadPool(numProducers + 1); + + for (int p = 0; p < numProducers; p++) { + final int producerId = p; + executor.submit(() -> { + try { + for (int i = 0; i < itemsPerProducer; i++) { + queue.enqueue(producerId * itemsPerProducer + i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + Thread consumerThread = new Thread(() -> { + try { + while (results.size() < totalItems) { + Integer item = queue.poll(); + if (item != null) { + synchronized (results) { + results.add(item); + } + } + } + } catch (Exception e) { + Thread.currentThread().interrupt(); + } + }); + consumerThread.start(); + + Assertions.assertTrue(doneLatch.await(10, TimeUnit.SECONDS)); + consumerThread.join(5000); + + Assertions.assertEquals(totalItems, results.size()); + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + public void testSingleProducerMultipleConsumers() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(50); + int numConsumers = 4; + int totalItems = 1000; + CountDownLatch doneLatch = new CountDownLatch(numConsumers); + AtomicInteger consumedCount = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(numConsumers + 1); + + executor.submit(() -> { + try { + for (int i = 0; i < totalItems; i++) { + queue.enqueue(i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + for (int c = 0; c < numConsumers; c++) { + executor.submit(() -> { + try { + while (consumedCount.get() < totalItems) { + Integer item = queue.poll(); + if (item != null) { + consumedCount.incrementAndGet(); + } + } + } finally { + doneLatch.countDown(); + } + }); + } + + Assertions.assertTrue(doneLatch.await(10, TimeUnit.SECONDS)); + Assertions.assertEquals(totalItems, consumedCount.get()); + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + public void testBlockingEnqueueWhenFull() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(1); + queue.enqueue(1); + + AtomicInteger blockedCount = new AtomicInteger(0); + Thread producer = new Thread(() -> { + try { + queue.enqueue(2); + blockedCount.incrementAndGet(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + producer.start(); + + Thread.sleep(100); + Assertions.assertEquals(1, queue.dequeue()); + + producer.join(2000); + Assertions.assertEquals(1, blockedCount.get()); + Assertions.assertEquals(2, queue.dequeue()); + } + + @Test + public void testBlockingDequeueWhenEmpty() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + + AtomicInteger result = new AtomicInteger(-1); + Thread consumer = new Thread(() -> { + try { + result.set(queue.dequeue()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + consumer.start(); + + Thread.sleep(100); + queue.enqueue(42); + + consumer.join(2000); + Assertions.assertEquals(42, result.get()); + } + + @Test + public void testStressConcurrentAccess() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(10); + int numThreads = 8; + int opsPerThread = 500; + CountDownLatch latch = new CountDownLatch(numThreads); + AtomicInteger enqueueCount = new AtomicInteger(0); + AtomicInteger dequeueCount = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + + for (int t = 0; t < numThreads; t++) { + final boolean isProducer = t % 2 == 0; + executor.submit(() -> { + try { + for (int i = 0; i < opsPerThread; i++) { + if (isProducer) { + if (queue.offer(i)) { + enqueueCount.incrementAndGet(); + } + } else { + if (queue.poll() != null) { + dequeueCount.incrementAndGet(); + } + } + } + } finally { + latch.countDown(); + } + }); + } + + Assertions.assertTrue(latch.await(10, TimeUnit.SECONDS)); + Assertions.assertTrue(enqueueCount.get() >= dequeueCount.get()); + Assertions.assertEquals(enqueueCount.get() - dequeueCount.get(), queue.size()); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/BinaryTreeToStringTest.java b/src/test/java/com/thealgorithms/datastructures/trees/BinaryTreeToStringTest.java new file mode 100644 index 000000000000..2461fd74143d --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/BinaryTreeToStringTest.java @@ -0,0 +1,57 @@ +package com.thealgorithms.datastructures.trees; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for the BinaryTreeToString class. + */ +public class BinaryTreeToStringTest { + + @Test + public void testTreeToStringBasic() { + BinaryTree tree = new BinaryTree(); + tree.put(1); + tree.put(2); + tree.put(3); + tree.put(4); + + BinaryTreeToString converter = new BinaryTreeToString(); + String result = converter.tree2str(tree.getRoot()); + + // Output will depend on insertion logic of BinaryTree.put() + // which is BST-style, so result = "1()(2()(3()(4)))" + Assertions.assertEquals("1()(2()(3()(4)))", result); + } + + @Test + public void testSingleNodeTree() { + BinaryTree tree = new BinaryTree(); + tree.put(10); + + BinaryTreeToString converter = new BinaryTreeToString(); + String result = converter.tree2str(tree.getRoot()); + + Assertions.assertEquals("10", result); + } + + @Test + public void testComplexTreeStructure() { + BinaryTree.Node root = new BinaryTree.Node(10); + root.left = new BinaryTree.Node(5); + root.right = new BinaryTree.Node(20); + root.right.left = new BinaryTree.Node(15); + root.right.right = new BinaryTree.Node(25); + + BinaryTreeToString converter = new BinaryTreeToString(); + String result = converter.tree2str(root); + + Assertions.assertEquals("10(5)(20(15)(25))", result); + } + + @Test + public void testNullTree() { + BinaryTreeToString converter = new BinaryTreeToString(); + Assertions.assertEquals("", converter.tree2str(null)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/CentroidDecompositionTest.java b/src/test/java/com/thealgorithms/datastructures/trees/CentroidDecompositionTest.java new file mode 100644 index 000000000000..43d732e54f34 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/CentroidDecompositionTest.java @@ -0,0 +1,236 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Test cases for CentroidDecomposition + * + * @author lens161 + */ +class CentroidDecompositionTest { + + @Test + void testSingleNode() { + // Tree with just one node + int[][] edges = {}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(1, edges); + + assertEquals(1, tree.size()); + assertEquals(0, tree.getRoot()); + assertEquals(-1, tree.getParent(0)); + } + + @Test + void testTwoNodes() { + // Simple tree: 0 - 1 + int[][] edges = {{0, 1}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(2, edges); + + assertEquals(2, tree.size()); + int root = tree.getRoot(); + assertTrue(root == 0 || root == 1, "Root should be either node 0 or 1"); + + // One node should be root, other should have the root as parent + int nonRoot = (root == 0) ? 1 : 0; + assertEquals(-1, tree.getParent(root)); + assertEquals(root, tree.getParent(nonRoot)); + } + + @Test + void testLinearTree() { + // Linear tree: 0 - 1 - 2 - 3 - 4 + int[][] edges = {{0, 1}, {1, 2}, {2, 3}, {3, 4}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges); + + assertEquals(5, tree.size()); + // For a linear tree of 5 nodes, the centroid should be the middle node (node 2) + assertEquals(2, tree.getRoot()); + assertEquals(-1, tree.getParent(2)); + } + + @Test + void testBalancedBinaryTree() { + // Balanced binary tree: + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges); + + assertEquals(5, tree.size()); + // Root should be 0 or 1 (both are valid centroids) + int root = tree.getRoot(); + assertTrue(root == 0 || root == 1); + assertEquals(-1, tree.getParent(root)); + + // All nodes should have a parent in centroid tree except root + for (int i = 0; i < 5; i++) { + if (i != root) { + assertTrue(tree.getParent(i) >= 0 && tree.getParent(i) < 5); + } + } + } + + @Test + void testStarTree() { + // Star tree: center node 0 connected to 1, 2, 3, 4 + int[][] edges = {{0, 1}, {0, 2}, {0, 3}, {0, 4}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges); + + assertEquals(5, tree.size()); + // Center node (0) should be the root + assertEquals(0, tree.getRoot()); + + // All other nodes should have 0 as parent + for (int i = 1; i < 5; i++) { + assertEquals(0, tree.getParent(i)); + } + } + + @Test + void testCompleteTree() { + // Complete binary tree of 7 nodes: + // 0 + // / \ + // 1 2 + // / \ / \ + // 3 4 5 6 + int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {2, 6}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(7, edges); + + assertEquals(7, tree.size()); + assertEquals(0, tree.getRoot()); // Root should be the center + + // Verify all nodes are reachable in centroid tree + boolean[] visited = new boolean[7]; + visited[0] = true; + for (int i = 1; i < 7; i++) { + int parent = tree.getParent(i); + assertTrue(parent >= 0 && parent < 7); + assertTrue(visited[parent], "Parent should be processed before child"); + visited[i] = true; + } + } + + @Test + void testLargerTree() { + // Tree with 10 nodes + int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {2, 6}, {3, 7}, {4, 8}, {5, 9}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(10, edges); + + assertEquals(10, tree.size()); + int root = tree.getRoot(); + assertTrue(root >= 0 && root < 10); + assertEquals(-1, tree.getParent(root)); + + // Verify centroid tree structure is valid + for (int i = 0; i < 10; i++) { + if (i != root) { + assertTrue(tree.getParent(i) >= -1 && tree.getParent(i) < 10); + } + } + } + + @Test + void testPathGraph() { + // Path graph with 8 nodes: 0-1-2-3-4-5-6-7 + int[][] edges = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(8, edges); + + assertEquals(8, tree.size()); + // For path of 8 nodes, centroid should be around middle + int root = tree.getRoot(); + assertTrue(root >= 2 && root <= 5, "Root should be near the middle of path"); + } + + @Test + void testInvalidEmptyTree() { + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(0, new int[][] {}); }); + } + + @Test + void testInvalidNegativeNodes() { + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(-1, new int[][] {}); }); + } + + @Test + void testInvalidNullEdges() { + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(5, null); }); + } + + @Test + void testInvalidEdgeCount() { + // Tree with n nodes must have n-1 edges + int[][] edges = {{0, 1}, {1, 2}}; // 2 edges for 5 nodes (should be 4) + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(5, edges); }); + } + + @Test + void testInvalidEdgeFormat() { + int[][] edges = {{0, 1, 2}}; // Edge with 3 elements instead of 2 + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(3, edges); }); + } + + @Test + void testInvalidNodeInEdge() { + int[][] edges = {{0, 5}}; // Node 5 doesn't exist in tree of size 3 + assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(3, edges); }); + } + + @Test + void testInvalidNodeQuery() { + int[][] edges = {{0, 1}, {1, 2}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(3, edges); + + assertThrows(IllegalArgumentException.class, () -> { tree.getParent(-1); }); + + assertThrows(IllegalArgumentException.class, () -> { tree.getParent(5); }); + } + + @Test + void testToString() { + int[][] edges = {{0, 1}, {1, 2}}; + CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(3, edges); + + String result = tree.toString(); + assertNotNull(result); + assertTrue(result.contains("Centroid Tree")); + assertTrue(result.contains("Node")); + assertTrue(result.contains("ROOT")); + } + + @Test + void testAdjacencyListConstructor() { + List> adj = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + adj.add(new ArrayList<>()); + } + adj.get(0).add(1); + adj.get(1).add(0); + adj.get(1).add(2); + adj.get(2).add(1); + + CentroidDecomposition.CentroidTree tree = new CentroidDecomposition.CentroidTree(adj); + assertEquals(3, tree.size()); + assertEquals(1, tree.getRoot()); + } + + @Test + void testNullAdjacencyList() { + assertThrows(IllegalArgumentException.class, () -> { new CentroidDecomposition.CentroidTree(null); }); + } + + @Test + void testEmptyAdjacencyList() { + assertThrows(IllegalArgumentException.class, () -> { new CentroidDecomposition.CentroidTree(new ArrayList<>()); }); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/FenwickTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/FenwickTreeTest.java new file mode 100644 index 000000000000..47ef568e5d1f --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/FenwickTreeTest.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)); + } +} diff --git a/src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java b/src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java similarity index 97% rename from src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java rename to src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java index 29189290e1d4..f0cb1724f67c 100644 --- a/src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java +++ b/src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.tree; +package com.thealgorithms.datastructures.trees; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/com/thealgorithms/datastructures/trees/KDTreeDeletionTest.java b/src/test/java/com/thealgorithms/datastructures/trees/KDTreeDeletionTest.java new file mode 100644 index 000000000000..ee5d22f63b22 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/KDTreeDeletionTest.java @@ -0,0 +1,165 @@ +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class KDTreeDeletionTest { + @Test + void deleteLeavesOnBothSides() { + for (int coordinate : new int[] {10, 30}) { + List points = pointsOf(20, 10, 30); + KDTree tree = insertedTree(points); + KDTree.Point deleted = pointOf(coordinate); + tree.delete(deleted); + points.remove(deleted); + assertContents(tree, points); + Assertions.assertFalse(tree.search(deleted).isPresent()); + } + } + + @Test + void deleteNonRootWithDescendants() { + for (int coordinate : new int[] {10, 30}) { + List points = pointsOf(20, 10, 30, 5, 15, 25, 35, 13, 27); + KDTree tree = insertedTree(points); + KDTree.Point deleted = pointOf(coordinate); + tree.delete(deleted); + points.remove(deleted); + assertContents(tree, points); + Assertions.assertFalse(tree.search(deleted).isPresent()); + } + } + + @Test + void deleteRootWithOnlyLeftSubtree() { + List points = pointsOf(20, 10, 15, 5); + KDTree tree = insertedTree(points); + KDTree.Point deleted = points.removeFirst(); + tree.delete(deleted); + assertContents(tree, points); + Assertions.assertFalse(tree.search(deleted).isPresent()); + KDTree.Point inserted = pointOf(12); + tree.insert(inserted); + points.add(inserted); + assertContents(tree, points); + tree.delete(inserted); + points.remove(inserted); + assertContents(tree, points); + } + + @Test + void deleteRootWithNestedRightSuccessor() { + List points = pointsOf(20, 30, 25, 27, 40); + KDTree tree = insertedTree(points); + KDTree.Point deleted = points.removeFirst(); + tree.delete(deleted); + assertContents(tree, points); + Assertions.assertFalse(tree.search(deleted).isPresent()); + KDTree.Point successor = pointOf(25); + Assertions.assertEquals(successor, tree.getRoot().getPoint()); + tree.delete(successor); + points.remove(successor); + assertContents(tree, points); + Assertions.assertFalse(tree.search(successor).isPresent()); + } + + @Test + void deletingAbsentPointPreservesContents() { + List points = pointsOf(20, 10, 30); + KDTree tree = insertedTree(points); + Assertions.assertThrows(IllegalArgumentException.class, () -> tree.delete(pointOf(15))); + assertContents(tree, points); + for (KDTree.Point point : List.copyOf(points)) { + tree.delete(point); + points.remove(point); + assertContents(tree, points); + } + Assertions.assertNull(tree.getRoot()); + Assertions.assertThrows(IllegalArgumentException.class, () -> tree.delete(pointOf(20))); + Assertions.assertNull(tree.getRoot()); + } + + @Test + void deletionOrdersPreserveBuiltAndInsertedTrees() { + for (int dimension : new int[] {1, 2, 3}) { + List initial = new ArrayList<>(); + for (int index = 0; index < 15; index++) { + int[] coordinates = new int[dimension]; + for (int axis = 0; axis < dimension; axis++) { + coordinates[axis] = index * (axis + 1) % 17 - 8; + } + initial.add(new KDTree.Point(coordinates)); + } + for (boolean bulkBuild : new boolean[] {false, true}) { + for (int shift : new int[] {0, 4, 9}) { + List remaining = new ArrayList<>(initial); + List order = new ArrayList<>(initial); + Collections.rotate(order, shift); + KDTree tree = bulkBuild ? new KDTree(initial.toArray(KDTree.Point[] ::new)) : insertedTree(initial); + assertContents(tree, remaining); + for (KDTree.Point point : order) { + tree.delete(point); + remaining.remove(point); + assertContents(tree, remaining); + Assertions.assertFalse(tree.search(point).isPresent()); + } + Assertions.assertNull(tree.getRoot()); + } + } + } + } + + private static KDTree.Point pointOf(int coordinate) { + return new KDTree.Point(new int[] {coordinate, coordinate}); + } + + private static List pointsOf(int... coordinates) { + List points = new ArrayList<>(); + for (int coordinate : coordinates) { + points.add(pointOf(coordinate)); + } + return points; + } + + private static KDTree insertedTree(List points) { + KDTree tree = new KDTree(points.getFirst().getDimension()); + for (KDTree.Point point : points) { + tree.insert(point); + } + return tree; + } + + private static void assertContents(KDTree tree, List expected) { + List actual = new ArrayList<>(); + collectPoints(tree.getRoot(), actual); + Assertions.assertEquals(expected.size(), actual.size()); + Assertions.assertEquals(Set.copyOf(expected), Set.copyOf(actual)); + for (KDTree.Point point : expected) { + Assertions.assertTrue(tree.search(point).isPresent(), () -> "Surviving point is unreachable: " + point); + } + if (!expected.isEmpty()) { + for (int axis = 0; axis < expected.getFirst().getDimension(); axis++) { + int minimum = Integer.MAX_VALUE; + int maximum = Integer.MIN_VALUE; + for (KDTree.Point point : expected) { + minimum = Math.min(minimum, point.getCoordinate(axis)); + maximum = Math.max(maximum, point.getCoordinate(axis)); + } + Assertions.assertEquals(minimum, tree.findMin(axis).getCoordinate(axis)); + Assertions.assertEquals(maximum, tree.findMax(axis).getCoordinate(axis)); + } + } + } + + private static void collectPoints(KDTree.Node node, List points) { + if (node != null) { + points.add(node.getPoint()); + collectPoints(node.getLeft(), points); + collectPoints(node.getRight(), points); + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java new file mode 100644 index 000000000000..db081da2550a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java @@ -0,0 +1,71 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class SegmentTree2DTest { + + @Test + void testInitialEmptyQueries() { + SegmentTree2D segmentTree = new SegmentTree2D(4, 4); + + // Initial tree should return 0 for any query + assertEquals(0, segmentTree.query(0, 4, 0, 4)); + assertEquals(0, segmentTree.query(1, 3, 1, 3)); + } + + @Test + void testUpdateAndPointQuery() { + SegmentTree2D segmentTree = new SegmentTree2D(5, 5); + + segmentTree.update(2, 3, 10); + segmentTree.update(0, 0, 5); + + // Querying single points [row, row+1) x [col, col+1) + assertEquals(10, segmentTree.query(2, 3, 3, 4)); + assertEquals(5, segmentTree.query(0, 1, 0, 1)); + + // Empty point should be 0 + assertEquals(0, segmentTree.query(1, 2, 1, 2)); + } + + @Test + void testSubmatrixQuery() { + SegmentTree2D segmentTree = new SegmentTree2D(4, 4); + + // Matrix simulation: + // [1, 2, 0, 0] + // [3, 4, 0, 0] + // [0, 0, 0, 0] + // [0, 0, 0, 0] + segmentTree.update(0, 0, 1); + segmentTree.update(0, 1, 2); + segmentTree.update(1, 0, 3); + segmentTree.update(1, 1, 4); + + // Top-left 2x2 sum: 1+2+3+4 = 10 + assertEquals(10, segmentTree.query(0, 2, 0, 2)); + + // First row sum: 1+2 = 3 + assertEquals(3, segmentTree.query(0, 1, 0, 4)); + + // Second column sum: 2+4 = 6 + assertEquals(6, segmentTree.query(0, 4, 1, 2)); + } + + @Test + void testUpdateOverwriting() { + SegmentTree2D segmentTree = new SegmentTree2D(3, 3); + + segmentTree.update(1, 1, 5); + assertEquals(5, segmentTree.query(1, 2, 1, 2)); + + // Overwrite the same point + segmentTree.update(1, 1, 20); + assertEquals(20, segmentTree.query(1, 2, 1, 2)); + + // Full matrix sum should just be this point + assertEquals(20, segmentTree.query(0, 3, 0, 3)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java new file mode 100644 index 000000000000..196c32575709 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java @@ -0,0 +1,96 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SegmentTreeTest { + + private static SegmentTree treeOf(int... values) { + return new SegmentTree(values.length, values); + } + + @ParameterizedTest + @CsvSource({"0, 4, 15", "0, 0, 1", "4, 4, 5", "1, 3, 9", "2, 4, 12"}) + void testRangeSums(int start, int end, int expected) { + assertEquals(expected, treeOf(1, 2, 3, 4, 5).getSum(start, end)); + } + + @Test + void testSingleElementTree() { + SegmentTree tree = treeOf(42); + assertEquals(42, tree.getSum(0, 0)); + tree.update(0, 7); + assertEquals(7, tree.getSum(0, 0)); + } + + @Test + void testUpdateIsReflectedInSubsequentQueries() { + SegmentTree tree = treeOf(1, 2, 3, 4, 5); + tree.update(2, 10); + assertEquals(22, tree.getSum(0, 4)); + assertEquals(16, tree.getSum(1, 3)); + tree.update(0, -1); + assertEquals(20, tree.getSum(0, 4)); + } + + @Test + void testNegativeValues() { + SegmentTree tree = treeOf(-5, 3, -2, 8); + assertEquals(4, tree.getSum(0, 3)); + assertEquals(-4, tree.getSum(0, 2)); + } + + /** + * index == n is past the last element, so it must be rejected by the guard instead of reaching + * the backing array and throwing {@link ArrayIndexOutOfBoundsException}. + */ + @ParameterizedTest + @ValueSource(ints = {5, 6, 100, -1}) + void testUpdateOutOfRangeIndexIsIgnored(int index) { + SegmentTree tree = treeOf(1, 2, 3, 4, 5); + assertDoesNotThrow(() -> tree.update(index, 99)); + assertEquals(15, tree.getSum(0, 4), "out of range update must not modify the tree"); + } + + @ParameterizedTest + @CsvSource({"0, 5", "0, 6", "3, 2", "-1, 3", "5, 5"}) + void testOutOfRangeQueriesReturnZero(int start, int end) { + assertEquals(0, treeOf(1, 2, 3, 4, 5).getSum(start, end)); + } + + @Test + void testConstructorRejectsInvalidSize() { + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(0, new int[] {1, 2, 3})); + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(-1, new int[] {1, 2, 3})); + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(4, new int[] {1, 2, 3})); + } + + @Test + void testConstructorRejectsNullArray() { + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(3, null)); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17}) + void testMatchesBruteForceForVariousSizes(int size) { + int[] values = new int[size]; + for (int i = 0; i < size; i++) { + values[i] = i * 3 - 4; + } + SegmentTree tree = new SegmentTree(size, values.clone()); + + for (int start = 0; start < size; start++) { + int expected = 0; + for (int end = start; end < size; end++) { + expected += values[end]; + assertEquals(expected, tree.getSum(start, end), "sum of [" + start + ", " + end + "] with size " + size); + } + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTreeTest.java new file mode 100644 index 000000000000..c5973168438e --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/ThreadedBinaryTreeTest.java @@ -0,0 +1,50 @@ +/* + * TheAlgorithms (https://github.com/TheAlgorithms/Java) + * Author: Shewale41 + * This file is licensed under the MIT License. + */ + +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Basic tests for ThreadedBinaryTree inorder traversal. + */ +public class ThreadedBinaryTreeTest { + + @Test + public void testInorderTraversalSimple() { + ThreadedBinaryTree tree = new ThreadedBinaryTree(); + tree.insert(50); + tree.insert(30); + tree.insert(70); + tree.insert(20); + tree.insert(40); + tree.insert(60); + tree.insert(80); + + List expected = List.of(20, 30, 40, 50, 60, 70, 80); + List actual = tree.inorderTraversal(); + + assertEquals(expected, actual); + } + + @Test + public void testInorderWithDuplicates() { + ThreadedBinaryTree tree = new ThreadedBinaryTree(); + tree.insert(5); + tree.insert(3); + tree.insert(7); + tree.insert(7); // duplicate + tree.insert(2); + + List expected = List.of(2, 3, 5, 7, 7); + List actual = tree.inorderTraversal(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java b/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java index 09ada594faca..52b74a7a1faf 100644 --- a/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java +++ b/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ public void searchAndNotFound() { treap.insert(3); treap.insert(8); treap.insert(1); - assertEquals(null, treap.search(4)); + assertNull(treap.search(4)); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java new file mode 100644 index 000000000000..592170673a3a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java @@ -0,0 +1,117 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class WaveletTreeTest { + + @Test + public void testRank() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + // x = 1 + assertEquals(1, wt.rank(1, 1)); // In [5, 1], '1' appears 1 time + assertEquals(2, wt.rank(1, 4)); // In [5, 1, 2, 5, 1], '1' appears 2 times + assertEquals(0, wt.rank(1, 0)); // In [5], '1' appears 0 times + + // x = 5 + assertEquals(1, wt.rank(5, 0)); // In [5], '5' appears 1 time + assertEquals(1, wt.rank(5, 2)); // In [5, 1, 2], '5' appears 1 time + assertEquals(2, wt.rank(5, 4)); // In [5, 1, 2, 5, 1], '5' appears 2 times + + // Out of bounds / invalid value + assertEquals(0, wt.rank(10, 4)); // '10' is not in the array + assertEquals(0, wt.rank(5, -1)); // Invalid end index + } + + @Test + public void testSelect() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + assertEquals(1, wt.select(1, 1)); // 1st '1' is at index 1 + assertEquals(4, wt.select(1, 2)); // 2nd '1' is at index 4 + + assertEquals(0, wt.select(5, 1)); // 1st '5' is at index 0 + assertEquals(3, wt.select(5, 2)); // 2nd '5' is at index 3 + + assertEquals(2, wt.select(2, 1)); // 1st '2' is at index 2 + + assertEquals(-1, wt.select(5, 3)); // 3rd '5' doesn't exist + assertEquals(-1, wt.select(10, 1)); // '10' doesn't exist + assertEquals(-1, wt.select(5, 0)); // invalid k + } + + @Test + public void testKthSmallest() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + // Array: [5, 1, 2, 5, 1] -> Sorted: [1, 1, 2, 5, 5] + assertEquals(1, wt.kthSmallest(0, 4, 1)); // 1st smallest in [5, 1, 2, 5, 1] is 1 + assertEquals(1, wt.kthSmallest(0, 4, 2)); // 2nd smallest in [5, 1, 2, 5, 1] is 1 + assertEquals(2, wt.kthSmallest(0, 4, 3)); // 3rd smallest in [5, 1, 2, 5, 1] is 2 + assertEquals(5, wt.kthSmallest(0, 4, 4)); // 4th smallest in [5, 1, 2, 5, 1] is 5 + assertEquals(5, wt.kthSmallest(0, 4, 5)); // 5th smallest in [5, 1, 2, 5, 1] is 5 + + // Subarray: arr[1..3] = [1, 2, 5] -> Sorted: [1, 2, 5] + assertEquals(1, wt.kthSmallest(1, 3, 1)); // 1st smallest in [1, 2, 5] is 1 + assertEquals(2, wt.kthSmallest(1, 3, 2)); // 2nd smallest in [1, 2, 5] is 2 + assertEquals(5, wt.kthSmallest(1, 3, 3)); // 3rd smallest in [1, 2, 5] is 5 + + // Invalid ranges / arguments + assertEquals(-1, wt.kthSmallest(4, 2, 1)); // Invalid range (left > right) + assertEquals(-1, wt.kthSmallest(0, 4, 10)); // k > range length + assertEquals(-1, wt.kthSmallest(0, 4, 0)); // k < 1 + } + + @Test + public void testEmptyAndSingleElementArray() { + WaveletTree wtEmpty = new WaveletTree(new int[] {}); + assertEquals(0, wtEmpty.rank(1, 0)); + assertEquals(-1, wtEmpty.select(1, 1)); + assertEquals(-1, wtEmpty.kthSmallest(0, 0, 1)); + + WaveletTree wtSingle = new WaveletTree(new int[] {42}); + assertEquals(1, wtSingle.rank(42, 0)); + assertEquals(0, wtSingle.rank(42, -1)); + assertEquals(0, wtSingle.select(42, 1)); + assertEquals(-1, wtSingle.select(42, 2)); + assertEquals(42, wtSingle.kthSmallest(0, 0, 1)); + } + + @Test + public void testNullArrayAndCustomBounds() { + WaveletTree wtNull = new WaveletTree(null); + assertEquals(0, wtNull.rank(1, 0)); + + WaveletTree wtNullCustom = new WaveletTree(null, 1, 5); + assertEquals(-1, wtNullCustom.select(1, 1)); + + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wtCustom = new WaveletTree(arr, 1, 10); + assertEquals(2, wtCustom.rank(5, 4)); + assertEquals(0, wtCustom.rank(4, 4)); // Query an element inside bounds but not in array + assertEquals(0, wtCustom.rank(10, 4)); // Query upper bound + } + + @Test + public void testNegativeValues() { + int[] arr = {-5, 10, -2, 0, -5}; + WaveletTree wt = new WaveletTree(arr); + + assertEquals(2, wt.rank(-5, 4)); + assertEquals(1, wt.rank(0, 3)); + + assertEquals(0, wt.select(-5, 1)); + assertEquals(4, wt.select(-5, 2)); + assertEquals(3, wt.select(0, 1)); + + // Sorted: [-5, -5, -2, 0, 10] + assertEquals(-5, wt.kthSmallest(0, 4, 1)); + assertEquals(-2, wt.kthSmallest(0, 4, 3)); + assertEquals(10, wt.kthSmallest(0, 4, 5)); + } +} diff --git a/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java b/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java index 38784228d68e..b25fd796b112 100644 --- a/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java +++ b/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java @@ -16,14 +16,6 @@ public void testBuildLocation() { assertEquals(4.0, point.y); } - @Test - public void testCreateLocation() { - ClosestPair cp = new ClosestPair(5); - ClosestPair.Location[] locations = cp.createLocation(5); - assertNotNull(locations); - assertEquals(5, locations.length); - } - @Test public void testXPartition() { ClosestPair cp = new ClosestPair(5); diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java new file mode 100644 index 000000000000..762fe86d4d65 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.dynamicprogramming; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the generalized DigitDP implementation. + */ +public class DigitDPTest { + + @Test + public void testDigitDPBasicRange() { + // Numbers between 1 and 20 with a digit sum of 5: 5, 14 + long result = DigitDP.countRangeWithDigitSum(1, 20, 5); + assertEquals(2, result); + } + + @Test + public void testDigitDPZeroBound() { + // Number 0 has a digit sum of 0 + long result = DigitDP.countRangeWithDigitSum(0, 0, 0); + assertEquals(1, result); + } + + @Test + public void testDigitDPLargeRange() { + // Count numbers between 1 and 100 with a digit sum of 9 + // 9, 18, 27, 36, 45, 54, 63, 72, 81, 90 (10 numbers) + long result = DigitDP.countRangeWithDigitSum(1, 100, 9); + assertEquals(10, result); + } + + @Test + public void testDigitDPNoMatches() { + // No numbers between 10 and 15 can have a digit sum of 20 + long result = DigitDP.countRangeWithDigitSum(10, 15, 20); + assertEquals(0, result); + } + + @Test + public void testDigitDPExceedsMaxSum() { + // Sum condition that exceeds max possible physical sum array constraints + // gracefully returns 0 + long result = DigitDP.countRangeWithDigitSum(1, 100, 200); + assertEquals(0, result); + } + + @Test + public void testDigitDPInvalidRange() { + // Lower bound greater than upper bound should evaluate gracefully to 0 + long result = DigitDP.countRangeWithDigitSum(50, 20, 5); + assertEquals(0, result); + } + + @Test + public void testDigitDPExceedsMaxSumEdgeCase() { + // Yeh test case target > MAX_DIGIT_SUM wali condition ko hit karega + long result = DigitDP.countRangeWithDigitSum(1, 100, 180); + assertEquals(0, result); + } + + @Test + public void testDigitDPMemoizationHit() { + // Badi range dene se overlapping subproblems bante hain, + // jisse memoization hit hogi aur coverage 100% ho jayegi. + long result1 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + long result2 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + assertEquals(result1, result2); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java index 40bbdff15ca6..91169c4cc9d8 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.dynamicprogramming; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -55,27 +56,24 @@ public void testLCSWithBothEmptyStrings() { public void testLCSWithNullFirstString() { String str1 = null; String str2 = "XYZ"; - String expected = null; // Should return null if first string is null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test public void testLCSWithNullSecondString() { String str1 = "ABC"; String str2 = null; - String expected = null; // Should return null if second string is null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test public void testLCSWithNullBothStrings() { String str1 = null; String str2 = null; - String expected = null; // Should return null if both strings are null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java new file mode 100644 index 000000000000..a1ee624e94d2 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java @@ -0,0 +1,54 @@ +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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class LongestPalindromicSubsequenceTest { + + @ParameterizedTest + @CsvSource({"BBABCBCAB, BACBCAB", "BABCBAB, BABCBAB", "A, A", "AA, AA", "AB, B"}) + void testLpsKnownCases(String input, String expectedLps) { + assertEquals(expectedLps, LongestPalindromicSubsequence.lps(input)); + } + + @Test + void testLpsEmptyString() { + assertEquals("", LongestPalindromicSubsequence.lps("")); + } + + @Test + void testLpsSingleCharacter() { + assertEquals("Z", LongestPalindromicSubsequence.lps("Z")); + } + + @Test + void testLpsAllSameCharacters() { + assertEquals("AAAA", LongestPalindromicSubsequence.lps("AAAA")); + } + + @Test + void testLpsAlreadyPalindrome() { + assertEquals("RACECAR", LongestPalindromicSubsequence.lps("RACECAR")); + } + + @Test + void testLpsNoRepeatingCharacters() { + assertEquals(1, LongestPalindromicSubsequence.lps("ABCDE").length()); + } + + @Test + void testLpsNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { LongestPalindromicSubsequence.lps(null); }); + } + + @Test + void testLpsResultIsActuallyPalindrome() { + String result = LongestPalindromicSubsequence.lps("BBABCBCAB"); + String reversed = new StringBuilder(result).reverse().toString(); + assertEquals(result, reversed); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java new file mode 100644 index 000000000000..17ff3ec728dc --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OptimalBinarySearchTreeTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testFindOptimalCost(int[] keys, int[] frequencies, long expectedCost) { + assertEquals(expectedCost, OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream validTestCases() { + return Stream.of(Arguments.of(new int[] {}, new int[] {}, 0L), Arguments.of(new int[] {15}, new int[] {9}, 9L), Arguments.of(new int[] {10, 12}, new int[] {34, 50}, 118L), Arguments.of(new int[] {20, 10, 30}, new int[] {50, 34, 8}, 134L), + Arguments.of(new int[] {12, 10, 20, 42, 25, 37}, new int[] {8, 34, 50, 3, 40, 30}, 324L), Arguments.of(new int[] {1, 2, 3}, new int[] {0, 0, 0}, 0L)); + } + + @ParameterizedTest + @MethodSource("crossCheckTestCases") + void testFindOptimalCostAgainstBruteForce(int[] keys, int[] frequencies) { + assertEquals(bruteForceOptimalCost(keys, frequencies), OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream crossCheckTestCases() { + return Stream.of(Arguments.of(new int[] {3, 1, 2}, new int[] {4, 2, 6}), Arguments.of(new int[] {5, 2, 8, 6}, new int[] {3, 7, 1, 4}), Arguments.of(new int[] {9, 4, 11, 2}, new int[] {1, 8, 2, 5})); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testFindOptimalCostInvalidInput(int[] keys, int[] frequencies) { + assertThrows(IllegalArgumentException.class, () -> OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream invalidTestCases() { + return Stream.of(Arguments.of(null, new int[] {}), Arguments.of(new int[] {}, null), Arguments.of(new int[] {1, 2}, new int[] {3}), Arguments.of(new int[] {1, 1}, new int[] {2, 3}), Arguments.of(new int[] {1, 2}, new int[] {3, -1})); + } + + private static long bruteForceOptimalCost(int[] keys, int[] frequencies) { + int[][] sortedNodes = new int[keys.length][2]; + for (int index = 0; index < keys.length; index++) { + sortedNodes[index][0] = keys[index]; + sortedNodes[index][1] = frequencies[index]; + } + Arrays.sort(sortedNodes, java.util.Comparator.comparingInt(node -> node[0])); + + int[] sortedFrequencies = new int[sortedNodes.length]; + for (int index = 0; index < sortedNodes.length; index++) { + sortedFrequencies[index] = sortedNodes[index][1]; + } + + return bruteForceOptimalCost(sortedFrequencies, 0, sortedFrequencies.length - 1, 1); + } + + private static long bruteForceOptimalCost(int[] frequencies, int start, int end, int depth) { + if (start > end) { + return 0L; + } + + long minimumCost = Long.MAX_VALUE; + for (int root = start; root <= end; root++) { + long currentCost = (long) depth * frequencies[root] + bruteForceOptimalCost(frequencies, start, root - 1, depth + 1) + bruteForceOptimalCost(frequencies, root + 1, end, depth + 1); + minimumCost = Math.min(minimumCost, currentCost); + } + return minimumCost; + } +} diff --git a/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java b/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java new file mode 100644 index 000000000000..9f60df51b65f --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java @@ -0,0 +1,101 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.geom.Point2D; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class LineIntersectionTest { + + @Test + void testCrossingSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(4, 4); + Point q1 = new Point(0, 4); + Point q2 = new Point(4, 0); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testParallelSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(3, 3); + Point q1 = new Point(0, 1); + Point q2 = new Point(3, 4); + + assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testTouchingAtEndpoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(2, 2); + Point q2 = new Point(4, 0); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testCollinearOverlapHasNoUniquePoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(4, 4); + Point q1 = new Point(2, 2); + Point q2 = new Point(6, 6); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testCollinearDisjointSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(3, 3); + Point q2 = new Point(5, 5); + + assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testCollinearSegmentsTouchingAtEndpointHaveUniquePoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(2, 2); + Point q2 = new Point(4, 4); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testVerticalAndHorizontalCrossingSegments() { + Point p1 = new Point(2, 0); + Point p2 = new Point(2, 5); + Point q1 = new Point(0, 3); + Point q2 = new Point(4, 3); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(3.0, intersection.orElseThrow().getY(), 1e-9); + } +} diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java new file mode 100644 index 000000000000..291be677d894 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -0,0 +1,61 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AccountMergeTest { + + @Test + void testMergeAccountsWithSharedEmails() { + List> accounts = List.of(List.of("abc", "abc@mail.com", "abx@mail.com"), List.of("abc", "abc@mail.com", "aby@mail.com"), List.of("Mary", "mary@mail.com"), List.of("John", "johnnybravo@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of(List.of("John", "johnnybravo@mail.com"), List.of("Mary", "mary@mail.com"), List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testAccountsWithSameNameButNoSharedEmailStaySeparate() { + List> accounts = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testEmptyInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(List.of())); + } + + @Test + void testNullInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(null)); + } + + @Test + void testTransitiveMergeAndDuplicateEmails() { + List> accounts = List.of(List.of("A", "a1@mail.com", "a2@mail.com"), List.of("A", "a2@mail.com", "a3@mail.com"), List.of("A", "a3@mail.com", "a4@mail.com", "a4@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of(List.of("A", "a1@mail.com", "a2@mail.com", "a3@mail.com", "a4@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testAccountsWithNoEmailsArePreserved() { + List> accounts = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); + + assertEquals(expected, merged); + } +} diff --git a/src/test/java/com/thealgorithms/graph/GomoryHuTreeTest.java b/src/test/java/com/thealgorithms/graph/GomoryHuTreeTest.java new file mode 100644 index 000000000000..241f23c0fa1d --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/GomoryHuTreeTest.java @@ -0,0 +1,132 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.random.RandomGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GomoryHuTreeTest { + + @Test + @DisplayName("Single node graph") + void singleNode() { + int[][] cap = {{0}}; + int[][] res = GomoryHuTree.buildTree(cap); + int[] parent = res[0]; + int[] weight = res[1]; + assertEquals(-1, parent[0]); + assertEquals(0, weight[0]); + } + + @Test + @DisplayName("Triangle undirected graph with known min-cuts") + void triangleGraph() { + // 0-1:3, 1-2:2, 0-2:4 + int[][] cap = new int[3][3]; + cap[0][1] = 3; + cap[1][0] = 3; + cap[1][2] = 2; + cap[2][1] = 2; + cap[0][2] = 4; + cap[2][0] = 4; + + int[][] tree = GomoryHuTree.buildTree(cap); + // validate all pairs via path-min-edge equals maxflow + validateAllPairs(cap, tree); + } + + @Test + @DisplayName("Random small undirected graphs compare to EdmondsKarp") + void randomSmallGraphs() { + Random rng = new Random(42); + for (int n = 2; n <= 6; n++) { + for (int iter = 0; iter < 10; iter++) { + int[][] cap = randSymmetricMatrix(n, 0, 5, rng); + int[][] tree = GomoryHuTree.buildTree(cap); + validateAllPairs(cap, tree); + } + } + } + + private static int[][] randSymmetricMatrix(int n, int lo, int hi, RandomGenerator rng) { + int[][] a = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + int w = rng.nextInt(hi - lo + 1) + lo; + a[i][j] = w; + a[j][i] = w; + } + } + // zero diagonal + for (int i = 0; i < n; i++) { + a[i][i] = 0; + } + return a; + } + + private static void validateAllPairs(int[][] cap, int[][] tree) { + int n = cap.length; + int[] parent = tree[0]; + int[] weight = tree[1]; + + // build adjacency list of tree without generic array creation + List> g = new ArrayList<>(); + for (int i = 0; i < n; i++) { + g.add(new ArrayList<>()); + } + for (int v = 1; v < n; v++) { + int u = parent[v]; + int w = weight[v]; + g.get(u).add(new int[] {v, w}); + g.get(v).add(new int[] {u, w}); + } + + for (int s = 0; s < n; s++) { + for (int t = s + 1; t < n; t++) { + int treeVal = minEdgeOnPath(g, s, t); + int flowVal = EdmondsKarp.maxFlow(cap, s, t); + assertEquals(flowVal, treeVal, "pair (" + s + "," + t + ")"); + } + } + } + + private static int minEdgeOnPath(List> g, int s, int t) { + // BFS to record parent and edge weight along the path, since it's a tree, unique path exists + int n = g.size(); + int[] parent = new int[n]; + int[] edgeW = new int[n]; + Arrays.fill(parent, -1); + Queue q = new ArrayDeque<>(); + q.add(s); + parent[s] = s; + while (!q.isEmpty()) { + int u = q.poll(); + if (u == t) { + break; + } + for (int[] e : g.get(u)) { + int v = e[0]; + int w = e[1]; + if (parent[v] == -1) { + parent[v] = u; + edgeW[v] = w; + q.add(v); + } + } + } + int cur = t; + int ans = Integer.MAX_VALUE; + while (cur != s) { + ans = Math.min(ans, edgeW[cur]); + cur = parent[cur]; + } + return ans == Integer.MAX_VALUE ? 0 : ans; + } +} diff --git a/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java index 612d09561b7b..e6a35566c587 100644 --- a/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java +++ b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java @@ -153,7 +153,7 @@ void testMultipleEdgesBetweenSameNodes() { HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); List result = solver.findEulerianPath(); - // Hava a Eulerian Path but not a Eulerian Circuit + // Has an Eulerian Path but not an Eulerian Circuit assertEquals(result, Arrays.asList(0, 1, 2, 0, 1)); } diff --git a/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java b/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java new file mode 100644 index 000000000000..8608bfb2dfc9 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java @@ -0,0 +1,207 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TarjanBridges}. + * + *

Tests cover a wide range of graph configurations including simple graphs, + * cycles, trees, disconnected components, multigraph-like structures, and + * various edge cases to ensure correct bridge detection.

+ */ +class TarjanBridgesTest { + + /** + * Helper to build a symmetric adjacency list for an undirected graph. + */ + private static List> buildGraph(int vertexCount, int[][] edges) { + List> adj = new ArrayList<>(); + for (int i = 0; i < vertexCount; i++) { + adj.add(new ArrayList<>()); + } + for (int[] edge : edges) { + adj.get(edge[0]).add(edge[1]); + adj.get(edge[1]).add(edge[0]); + } + return adj; + } + + /** + * Sorts bridges for deterministic comparison. + */ + private static void sortBridges(List bridges) { + bridges.sort(Comparator.comparingInt((int[] a) -> a[0]).thenComparingInt(a -> a[1])); + } + + @Test + void testSimpleGraphWithOneBridge() { + // Graph: 0-1-2-3 where 1-2 is the only bridge + // 0---1---2---3 + // | | + // +-------+ (via 0-2 would make cycle, but not here) + // Actually: 0-1 in a cycle with 0-1, and 2-3 in a cycle with 2-3 + // Let's use: 0--1--2 (linear chain). All edges are bridges. + List> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}}); + List bridges = TarjanBridges.findBridges(3, adj); + sortBridges(bridges); + assertEquals(2, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + assertEquals(1, bridges.get(1)[0]); + assertEquals(2, bridges.get(1)[1]); + } + + @Test + void testCycleGraphHasNoBridges() { + // Graph: 0-1-2-0 (triangle). No bridges. + List> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}, {2, 0}}); + List bridges = TarjanBridges.findBridges(3, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testTreeGraphAllEdgesAreBridges() { + // Tree: 0 + // / \ + // 1 2 + // / \ + // 3 4 + List> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {1, 3}, {1, 4}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(4, bridges.size()); + } + + @Test + void testGraphWithMixedBridgesAndCycles() { + // Graph: + // 0---1 + // | | + // 3---2---4---5 + // | + // 6 + // Cycle: 0-1-2-3-0 (no bridges within) + // Bridges: 2-4, 4-5, 5-6 + List> adj = buildGraph(7, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {2, 4}, {4, 5}, {5, 6}}); + List bridges = TarjanBridges.findBridges(7, adj); + sortBridges(bridges); + assertEquals(3, bridges.size()); + assertEquals(2, bridges.get(0)[0]); + assertEquals(4, bridges.get(0)[1]); + assertEquals(4, bridges.get(1)[0]); + assertEquals(5, bridges.get(1)[1]); + assertEquals(5, bridges.get(2)[0]); + assertEquals(6, bridges.get(2)[1]); + } + + @Test + void testDisconnectedGraphWithBridges() { + // Component 1: 0-1 (bridge) + // Component 2: 2-3-4-2 (cycle, no bridges) + List> adj = buildGraph(5, new int[][] {{0, 1}, {2, 3}, {3, 4}, {4, 2}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(1, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + } + + @Test + void testSingleVertex() { + List> adj = buildGraph(1, new int[][] {}); + List bridges = TarjanBridges.findBridges(1, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testTwoVerticesWithOneEdge() { + List> adj = buildGraph(2, new int[][] {{0, 1}}); + List bridges = TarjanBridges.findBridges(2, adj); + assertEquals(1, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + } + + @Test + void testEmptyGraph() { + List> adj = buildGraph(0, new int[][] {}); + List bridges = TarjanBridges.findBridges(0, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testIsolatedVertices() { + // 5 vertices, no edges β€” all isolated + List> adj = buildGraph(5, new int[][] {}); + List bridges = TarjanBridges.findBridges(5, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testLargeCycleNoBridges() { + // Cycle: 0-1-2-3-4-5-6-7-0 + int n = 8; + int[][] edges = new int[n][2]; + for (int i = 0; i < n; i++) { + edges[i] = new int[] {i, (i + 1) % n}; + } + List> adj = buildGraph(n, edges); + List bridges = TarjanBridges.findBridges(n, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testComplexGraphWithMultipleCyclesAndBridges() { + // Two cycles connected by a single bridge edge: + // Cycle A: 0-1-2-0 + // Cycle B: 3-4-5-3 + // Bridge: 2-3 + List> adj = buildGraph(6, new int[][] {{0, 1}, {1, 2}, {2, 0}, {3, 4}, {4, 5}, {5, 3}, {2, 3}}); + List bridges = TarjanBridges.findBridges(6, adj); + assertEquals(1, bridges.size()); + assertEquals(2, bridges.get(0)[0]); + assertEquals(3, bridges.get(0)[1]); + } + + @Test + void testNegativeVertexCountThrowsException() { + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(-1, new ArrayList<>())); + } + + @Test + void testNullAdjacencyListThrowsException() { + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(3, null)); + } + + @Test + void testMismatchedAdjacencyListSizeThrowsException() { + List> adj = buildGraph(2, new int[][] {{0, 1}}); + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(5, adj)); + } + + @Test + void testStarGraphAllEdgesAreBridges() { + // Star graph: center vertex 0 connected to 1, 2, 3, 4 + List> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {0, 3}, {0, 4}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(4, bridges.size()); + } + + @Test + void testBridgeBetweenTwoCycles() { + // Two squares connected by one bridge: + // Square 1: 0-1-2-3-0 + // Square 2: 4-5-6-7-4 + // Bridge: 3-4 + List> adj = buildGraph(8, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {4, 5}, {5, 6}, {6, 7}, {7, 4}, {3, 4}}); + List bridges = TarjanBridges.findBridges(8, adj); + assertEquals(1, bridges.size()); + assertEquals(3, bridges.get(0)[0]); + assertEquals(4, bridges.get(0)[1]); + } +} diff --git a/src/test/java/com/thealgorithms/io/BufferedReaderTest.java b/src/test/java/com/thealgorithms/io/BufferedReaderTest.java index 891c3066058e..088e86f8f7c5 100644 --- a/src/test/java/com/thealgorithms/io/BufferedReaderTest.java +++ b/src/test/java/com/thealgorithms/io/BufferedReaderTest.java @@ -17,15 +17,15 @@ public void testPeeks() throws IOException { BufferedReader reader = new BufferedReader(input); // read the first letter - assertEquals(reader.read(), 'H'); + assertEquals('H', reader.read()); len--; - assertEquals(reader.available(), len); + assertEquals(len, reader.available()); // position: H[e]llo!\nWorld! // reader.read() will be == 'e' - assertEquals(reader.peek(1), 'l'); - assertEquals(reader.peek(2), 'l'); // second l - assertEquals(reader.peek(3), 'o'); + assertEquals('l', reader.peek(1)); + assertEquals('l', reader.peek(2)); // second l + assertEquals('o', reader.peek(3)); } @Test @@ -38,21 +38,21 @@ public void testMixes() throws IOException { BufferedReader reader = new BufferedReader(input); // read the first letter - assertEquals(reader.read(), 'H'); // first letter + assertEquals('H', reader.read()); // first letter len--; - assertEquals(reader.peek(1), 'l'); // third later (second letter after 'H') - assertEquals(reader.read(), 'e'); // second letter + assertEquals('l', reader.peek(1)); // third later (second letter after 'H') + assertEquals('e', reader.read()); // second letter len--; - assertEquals(reader.available(), len); + assertEquals(len, reader.available()); // position: H[e]llo!\nWorld! - assertEquals(reader.peek(2), 'o'); // second l - assertEquals(reader.peek(3), '!'); - assertEquals(reader.peek(4), '\n'); + assertEquals('o', reader.peek(2)); // second l + assertEquals('!', reader.peek(3)); + assertEquals('\n', reader.peek(4)); - assertEquals(reader.read(), 'l'); // third letter - assertEquals(reader.peek(1), 'o'); // fourth letter + assertEquals('l', reader.read()); // third letter + assertEquals('o', reader.peek(1)); // fourth letter for (int i = 0; i < 6; i++) { reader.read(); @@ -74,23 +74,23 @@ public void testBlockPractical() throws IOException { ByteArrayInputStream input = new ByteArrayInputStream(bytes); BufferedReader reader = new BufferedReader(input); - assertEquals(reader.peek(), 'H'); - assertEquals(reader.read(), '!'); // read the first letter + assertEquals('H', reader.peek()); + assertEquals('!', reader.read()); // read the first letter len--; // this only reads the next 5 bytes (Hello) because // the default buffer size = 5 - assertEquals(new String(reader.readBlock()), "Hello"); + assertEquals("Hello", new String(reader.readBlock())); len -= 5; assertEquals(reader.available(), len); // maybe kind of a practical demonstration / use case if (reader.read() == '\n') { - assertEquals(reader.read(), 'W'); - assertEquals(reader.read(), 'o'); + assertEquals('W', reader.read()); + assertEquals('o', reader.read()); // the rest of the blocks - assertEquals(new String(reader.readBlock()), "rld!"); + assertEquals("rld!", new String(reader.readBlock())); } else { // should not reach throw new IOException("Something not right"); diff --git a/src/test/java/com/thealgorithms/machinelearning/KNearestNeighborsTest.java b/src/test/java/com/thealgorithms/machinelearning/KNearestNeighborsTest.java new file mode 100644 index 000000000000..85f732a08ecb --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/KNearestNeighborsTest.java @@ -0,0 +1,247 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class KNearestNeighborsTest { + + @Test + void predictsCorrectClassOnSeparableDataset() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + assertEquals(0, knn.predict(new double[] {1.5, 1.5})); + assertEquals(1, knn.predict(new double[] {8.5, 8.5})); + } + + @Test + void predictsBatchClassesOnSeparableDataset() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + double[][] samples = {{1.5, 1.5}, {8.5, 8.5}}; + int[] predictions = knn.predict(samples); + + assertArrayEquals(new int[] {0, 1}, predictions); + } + + @Test + void throwsExceptionWhenKIsNotPositive() { + assertThrows(IllegalArgumentException.class, () -> new KNearestNeighbors(-4)); + assertThrows(IllegalArgumentException.class, () -> new KNearestNeighbors(0)); + } + + @Test + void throwsExceptionWhenKIsGreaterThanNumberOfTrainingSamples() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + KNearestNeighbors knn = new KNearestNeighbors(7); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(features, labels)); + } + + @Test + void nullFeaturesArrayThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(null, new int[] {0, 1})); + } + + @Test + void emptyFeaturesArrayThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {}, new int[] {})); + } + + @Test + void nullLabelsArrayThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{1, 1}, {2, 2}}, null)); + } + + @Test + void emptyLabelsArrayThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{1, 1}, {2, 2}}, new int[] {})); + } + + @Test + void mismatchedFeatureAndLabelLengthsThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(3); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{0, 0}, {1, 1}, {8, 8}, {9, 9}}, new int[] {0, 0, 1})); + } + + @Test + void firstFeatureVectorNullThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {null, {1, 1}}, new int[] {0, 1})); + } + + @Test + void emptyFeatureVectorThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(1); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{}}, new int[] {0})); + } + + @Test + void nullFeatureSampleThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(2); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{0, 0}, null, {8, 8}}, new int[] {0, 0, 1})); + } + + @Test + void mismatchedDimensionThrowsIllegalArgumentException() { + KNearestNeighbors knn = new KNearestNeighbors(2); + + assertThrows(IllegalArgumentException.class, () -> knn.fit(new double[][] {{0, 0}, {1, 1, 2}, {8, 8}}, new int[] {0, 0, 1})); + } + + @Test + void predictBeforeFitThrowsIllegalStateException() { + KNearestNeighbors knn = new KNearestNeighbors(3); + + assertThrows(IllegalStateException.class, () -> knn.predict(new double[] {1, 1})); + } + + @Test + void nullTestPointThrowsIllegalArgumentException() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> knn.predict((double[]) null)); + assertThrows(IllegalArgumentException.class, () -> knn.predict((double[][]) null)); + } + + @Test + void mismatchedTestPointLengthThrowsIllegalArgumentException() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> knn.predict(new double[] {1.5, 1.5, 1.5})); + } + + @Test + void tieBreakReturnsSmallerLabel() { + double[][] features = {{0, 0}, {0, 2}, {2, 0}, {2, 2}}; + + int[] labels = {0, 1, 0, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(4); + knn.fit(features, labels); + + assertEquals(0, knn.predict(new double[] {1, 1})); + } + + @Test + void tieBreakKeepsCurrentWinnerWhenLabelIsGreater() { + double[][] features = {{0, 0}, {0, 2}, {2, 0}, {2, 2}}; + + int[] labels = {1, 0, 1, 0}; + + KNearestNeighbors knn = new KNearestNeighbors(4); + knn.fit(features, labels); + + assertEquals(0, knn.predict(new double[] {1, 1})); + } + + @Test + void nullBatchSampleThrowsIllegalArgumentException() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> knn.predict(new double[][] {{1.5, 1.5}, null})); + } + + @Test + void invalidBatchSampleThrowsIllegalArgumentException() { + double[][] features = { + {0, 0}, + {1, 1}, + {8, 8}, + {9, 9}, + }; + + int[] labels = {0, 0, 1, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(3); + knn.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> knn.predict(new double[][] {{1.5, 1.5}, {8.5, 8.5, 8.5}})); + } + + @Test + void fitMakesDefensiveCopyOfTrainingData() { + double[][] features = {{0, 0}, {10, 10}}; + + int[] labels = {0, 1}; + + KNearestNeighbors knn = new KNearestNeighbors(1); + knn.fit(features, labels); + + features[0][0] = 100; + labels[0] = 1; + + assertEquals(0, knn.predict(new double[] {0, 0})); + } +} diff --git a/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java b/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java new file mode 100644 index 000000000000..e8a130d0ccb2 --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java @@ -0,0 +1,95 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class LinearRegressionTest { + + private static final double DELTA = 0.1; + + @Test + void fitLearnsCorrectSlopeAndIntercept() { + double trueM = 2.5; + double trueB = 1.5; + + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] yTrain = new double[xTrain.length]; + for (int i = 0; i < xTrain.length; i++) { + yTrain[i] = (trueM * xTrain[i]) + trueB; + } + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + assertEquals(trueM, model.getSlope(), DELTA); + assertEquals(trueB, model.getIntercept(), DELTA); + } + + @Test + void predictMatchesExpectedOnUnseenData() { + double trueM = 2.5; + double trueB = 1.5; + + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] yTrain = new double[xTrain.length]; + for (int i = 0; i < xTrain.length; i++) { + yTrain[i] = (trueM * xTrain[i]) + trueB; + } + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + double[] testInputs = {0.0, 3.5, 7.0}; + for (double testX : testInputs) { + double expectedY = (trueM * testX) + trueB; + assertEquals(expectedY, model.predict(testX), DELTA); + } + } + + @Test + void calculateMSEIsNearZeroAfterTraining() { + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] yTrain = {3.0, 5.0, 7.0, 9.0, 11.0}; // y = 2x + 1 + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + assertEquals(0.0, model.calculateMSE(xTrain, yTrain), 0.01); + } + + @Test + void fitThrowsExceptionOnMismatchedArrayLengths() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {1.0, 2.0}; + double[] y = {1.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, y)); + } + + @Test + void fitThrowsExceptionOnEmptyArrays() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {}; + double[] y = {}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, y)); + } + + @Test + void fitThrowsExceptionOnNullX() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] y = {1.0, 2.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(null, y)); + } + + @Test + void fitThrowsExceptionOnNullY() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {1.0, 2.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, null)); + } +} diff --git a/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java new file mode 100644 index 000000000000..5ccd31147ac3 --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java @@ -0,0 +1,141 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MultinomialNaiveBayesClassifierTest { + + @Test + void predictsCorrectClassOnSeparableToyDataset() { + // Class 0 samples are dominated by feature 0; class 1 samples by feature 1. + double[][] features = { + {5, 1}, + {6, 0}, + {4, 1}, + {1, 5}, + {0, 6}, + {1, 4}, + }; + int[] labels = {0, 0, 0, 1, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + assertEquals(0, classifier.predict(new double[] {5, 0})); + assertEquals(1, classifier.predict(new double[] {0, 5})); + } + + @Test + void predictBatchMatchesIndividualPredictions() { + double[][] features = { + {3, 0}, + {2, 0}, + {0, 3}, + {0, 2}, + }; + int[] labels = {0, 0, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + double[][] samples = {{4, 0}, {0, 4}}; + int[] predictions = classifier.predict(samples); + + assertEquals(classifier.predict(samples[0]), predictions[0]); + assertEquals(classifier.predict(samples[1]), predictions[1]); + } + + @Test + void laplaceSmoothingKeepsZeroCountFeatureLogProbabilityFinite() { + // Feature index 1 never appears for class 0 in training data. + double[][] features = { + {2, 0}, + {3, 0}, + {0, 2}, + {0, 3}, + }; + int[] labels = {0, 0, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + // A sample that hits class 0's zero-count feature should still produce + // a finite, usable prediction instead of -Infinity collapsing the score. + int prediction = classifier.predict(new double[] {1, 1}); + assertTrue(prediction == 0 || prediction == 1); + } + + @Test + void predictBeforeFitThrowsIllegalStateException() { + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalStateException.class, () -> classifier.predict(new double[] {1, 2})); + } + + @Test + void nonPositiveAlphaThrowsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(0.0)); + assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(-1.0)); + } + + @Test + void mismatchedSampleLengthThrowsIllegalArgumentException() { + double[][] features = { + {1, 2}, + {3, 4}, + }; + int[] labels = {0, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> classifier.predict(new double[] {1, 2, 3})); + } + + @Test + void mismatchedFeatureAndLabelLengthsThrowsIllegalArgumentException() { + double[][] features = { + {1, 2}, + {3, 4}, + }; + int[] labels = {0}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels)); + } + + @Test + void emptyFeaturesArrayThrowsIllegalArgumentException() { + double[][] features = {}; + int[] labels = {}; + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels)); + } + + @Test + void refittingReplacesPreviousModelState() { + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + + double[][] firstFeatures = { + {5, 0, 0}, + {0, 5, 0}, + {0, 0, 5}, + }; + int[] firstLabels = {0, 1, 2}; + classifier.fit(firstFeatures, firstLabels); + + double[][] secondFeatures = { + {5, 0}, + {0, 5}, + }; + int[] secondLabels = {0, 1}; + classifier.fit(secondFeatures, secondLabels); + + // Class 2 existed in the first fit but not the second β€” it must not + // survive into predictions after refitting. + int prediction = classifier.predict(new double[] {2.5, 2.5}); + assertTrue(prediction == 0 || prediction == 1); + } +} diff --git a/src/test/java/com/thealgorithms/machinelearning/PerceptronTest.java b/src/test/java/com/thealgorithms/machinelearning/PerceptronTest.java new file mode 100644 index 000000000000..2dce0a0642ca --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/PerceptronTest.java @@ -0,0 +1,163 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PerceptronTest { + + @Test + void learnsAndFunction() { + double[][] features = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; + int[] labels = {0, 0, 0, 1}; + + Perceptron perceptron = new Perceptron(1.0, 20); + perceptron.fit(features, labels); + + assertArrayEquals(labels, perceptron.predict(features)); + assertTrue(perceptron.hasConverged()); + assertTrue(perceptron.getEpochsRun() <= 20); + } + + @Test + void learnsExpectedWeightsAndBias() { + Perceptron perceptron = new Perceptron(1.0, 20); + perceptron.fit(new double[][] {{0}, {1}}, new int[] {0, 1}); + + assertArrayEquals(new double[] {1.0}, perceptron.getWeights()); + assertEquals(-1.0, perceptron.getBias()); + assertTrue(perceptron.hasConverged()); + assertEquals(3, perceptron.getEpochsRun()); + } + + @Test + void predictsUnseenSamples() { + double[][] features = {{-2, -1}, {-1, -2}, {1, 2}, {2, 1}}; + int[] labels = {0, 0, 1, 1}; + + Perceptron perceptron = new Perceptron(0.5, 20); + perceptron.fit(features, labels); + + assertEquals(0, perceptron.predict(new double[] {-3, -1})); + assertEquals(1, perceptron.predict(new double[] {3, 1})); + } + + @Test + void batchPredictionMatchesIndividualPredictions() { + double[][] features = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; + int[] labels = {0, 0, 0, 1}; + double[][] samples = {{0, 0}, {1, 0}, {1, 1}}; + + Perceptron perceptron = new Perceptron(1.0, 20); + perceptron.fit(features, labels); + + assertArrayEquals(new int[] {0, 0, 1}, perceptron.predict(samples)); + int[] individualPredictions = {perceptron.predict(samples[0]), perceptron.predict(samples[1]), perceptron.predict(samples[2])}; + assertArrayEquals(individualPredictions, perceptron.predict(samples)); + } + + @Test + void emptyBatchProducesEmptyPrediction() { + Perceptron perceptron = new Perceptron(1.0, 10); + perceptron.fit(new double[][] {{0}}, new int[] {0}); + + assertArrayEquals(new int[] {}, perceptron.predict(new double[][] {})); + } + + @Test + void nonSeparableDataStopsAtEpochLimitWithoutConverging() { + double[][] features = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; + int[] labels = {0, 1, 1, 0}; + + Perceptron perceptron = new Perceptron(1.0, 8); + perceptron.fit(features, labels); + + Assertions.assertFalse(perceptron.hasConverged()); + assertEquals(8, perceptron.getEpochsRun()); + } + + @Test + void fittingResetsPreviousModel() { + Perceptron perceptron = new Perceptron(1.0, 20); + perceptron.fit(new double[][] {{0}, {1}}, new int[] {0, 1}); + perceptron.fit(new double[][] {{0}, {1}}, new int[] {1, 0}); + + assertArrayEquals(new int[] {1, 0}, perceptron.predict(new double[][] {{0}, {1}})); + } + + @Test + void weightsAreReturnedAsDefensiveCopy() { + Perceptron perceptron = new Perceptron(1.0, 10); + perceptron.fit(new double[][] {{0}, {1}}, new int[] {0, 1}); + + double[] weights = perceptron.getWeights(); + weights[0] = 1000; + + assertEquals(1, perceptron.predict(new double[] {1})); + } + + @Test + void predictionBeforeFitThrows() { + Perceptron perceptron = new Perceptron(1.0, 10); + + assertThrows(IllegalStateException.class, () -> perceptron.predict(new double[] {1})); + assertThrows(IllegalStateException.class, () -> perceptron.predict(new double[][] {})); + assertThrows(IllegalStateException.class, perceptron::getWeights); + assertThrows(IllegalStateException.class, perceptron::getBias); + assertThrows(IllegalStateException.class, perceptron::hasConverged); + assertThrows(IllegalStateException.class, perceptron::getEpochsRun); + } + + @Test + void invalidHyperparametersThrow() { + assertThrows(IllegalArgumentException.class, () -> new Perceptron(0.0, 10)); + assertThrows(IllegalArgumentException.class, () -> new Perceptron(-1.0, 10)); + assertThrows(IllegalArgumentException.class, () -> new Perceptron(Double.NaN, 10)); + assertThrows(IllegalArgumentException.class, () -> new Perceptron(Double.POSITIVE_INFINITY, 10)); + assertThrows(IllegalArgumentException.class, () -> new Perceptron(1.0, 0)); + assertThrows(IllegalArgumentException.class, () -> new Perceptron(1.0, -1)); + } + + @Test + void invalidTrainingDataThrows() { + Perceptron perceptron = new Perceptron(1.0, 10); + + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(null, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}}, null)); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {}, new int[] {})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}}, new int[] {})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}, {1}}, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}, {1, 2}}, new int[] {0, 1})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {null}, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}, null}, new int[] {0, 1})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{}}, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{0}}, new int[] {2})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{Double.NaN}}, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.fit(new double[][] {{Double.POSITIVE_INFINITY}}, new int[] {0})); + } + + @Test + void invalidPredictionDataThrows() { + Perceptron perceptron = new Perceptron(1.0, 10); + perceptron.fit(new double[][] {{0, 0}}, new int[] {0}); + + assertThrows(IllegalArgumentException.class, () -> perceptron.predict((double[]) null)); + assertThrows(IllegalArgumentException.class, () -> perceptron.predict(new double[] {0})); + assertThrows(IllegalArgumentException.class, () -> perceptron.predict(new double[] {0, Double.NaN})); + assertThrows(IllegalArgumentException.class, () -> perceptron.predict((double[][]) null)); + assertThrows(IllegalArgumentException.class, () -> perceptron.predict(new double[][] {{0, 0}, null})); + } + + @Test + void divergingTrainingIsRejectedAndLeavesClassifierUnfitted() { + Perceptron perceptron = new Perceptron(1.0e300, 5); + + assertThrows(ArithmeticException.class, () -> perceptron.fit(new double[][] {{1.0e300}, {1.0e300}}, new int[] {0, 1})); + assertThrows(IllegalStateException.class, perceptron::getWeights); + assertThrows(IllegalStateException.class, perceptron::getBias); + } +} diff --git a/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java b/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java index dfca757fd877..070ff4ae3147 100644 --- a/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java +++ b/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java @@ -11,12 +11,15 @@ public class AbsoluteMinTest { void testGetMinValue() { assertEquals(0, AbsoluteMin.getMinValue(4, 0, 16)); assertEquals(-2, AbsoluteMin.getMinValue(3, -10, -2)); + assertEquals(-2, AbsoluteMin.getMinValue(-3, -10, -2)); + assertEquals(2, AbsoluteMin.getMinValue(-3, -10, 2)); + assertEquals(2, AbsoluteMin.getMinValue(-5, 2)); + assertEquals(2, AbsoluteMin.getMinValue(2, -5)); } @Test void testGetMinValueWithNoArguments() { - Exception exception = assertThrows(IllegalArgumentException.class, AbsoluteMin::getMinValue); - assertEquals("Numbers array cannot be empty", exception.getMessage()); + assertThrows(IllegalArgumentException.class, AbsoluteMin::getMinValue); } @Test @@ -24,4 +27,13 @@ void testGetMinValueWithSameAbsoluteValues() { assertEquals(-5, AbsoluteMin.getMinValue(-5, 5)); assertEquals(-5, AbsoluteMin.getMinValue(5, -5)); } + + @Test + void testIntegerMinValueOverflow() { + assertEquals(1, AbsoluteMin.getMinValue(Integer.MIN_VALUE, 1)); + assertEquals(-1, AbsoluteMin.getMinValue(Integer.MIN_VALUE, -1)); + assertEquals(0, AbsoluteMin.getMinValue(Integer.MIN_VALUE, 0)); + assertEquals(Integer.MIN_VALUE, AbsoluteMin.getMinValue(Integer.MIN_VALUE)); + assertEquals(Integer.MAX_VALUE, AbsoluteMin.getMinValue(Integer.MIN_VALUE, Integer.MAX_VALUE)); + } } diff --git a/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java b/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java index 907d5cb45ef9..308f38ad9bd5 100644 --- a/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java +++ b/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java @@ -34,6 +34,6 @@ void testNegativeNumbers() { @Test void testMinIntEdgeCase() { - assertEquals(Integer.MIN_VALUE, AbsoluteValue.getAbsValue(Integer.MIN_VALUE)); + assertEquals(2147483648L, AbsoluteValue.getAbsValue(Integer.MIN_VALUE)); } } diff --git a/src/test/java/com/thealgorithms/maths/AbundantNumberTest.java b/src/test/java/com/thealgorithms/maths/AbundantNumberTest.java new file mode 100644 index 000000000000..5b35345afd02 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/AbundantNumberTest.java @@ -0,0 +1,31 @@ +package com.thealgorithms.maths; + +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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class AbundantNumberTest { + @ParameterizedTest + @CsvSource({"12", "66", "222", "444", "888", "2424"}) + void abundantNumbersTest(int n) { + assertTrue(AbundantNumber.isAbundant(n)); + assertTrue(AbundantNumber.isAbundantNumber(n)); + } + + @ParameterizedTest + @CsvSource({"1", "2", "6", "111", "333", "2222"}) + void nonAbundantNumbersTest(int n) { + assertFalse(AbundantNumber.isAbundant(n)); + assertFalse(AbundantNumber.isAbundantNumber(n)); + } + + @ParameterizedTest + @CsvSource({"0", "-1"}) + void throwsNegativeNumbersNotAllowed(int n) { + assertThrows(IllegalArgumentException.class, () -> AbundantNumber.isAbundant(n)); + assertThrows(IllegalArgumentException.class, () -> AbundantNumber.isAbundantNumber(n)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/AreaTest.java b/src/test/java/com/thealgorithms/maths/AreaTest.java index b28afb85fbc3..1c2fe53ff3f3 100644 --- a/src/test/java/com/thealgorithms/maths/AreaTest.java +++ b/src/test/java/com/thealgorithms/maths/AreaTest.java @@ -16,6 +16,11 @@ void testSurfaceAreaCube() { assertEquals(6.0, Area.surfaceAreaCube(1)); } + @Test + void testSurfaceAreaCuboid() { + assertEquals(214.0, Area.surfaceAreaCuboid(5, 6, 7)); + } + @Test void testSurfaceAreaSphere() { assertEquals(12.566370614359172, Area.surfaceAreaSphere(1)); @@ -70,6 +75,12 @@ void surfaceAreaCone() { void testAllIllegalInput() { assertAll(() -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCube(0)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(0, 1, 2)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(1, 0, 2)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(1, 2, 0)), () -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaSphere(0)), () diff --git a/src/test/java/com/thealgorithms/maths/BellNumbersTest.java b/src/test/java/com/thealgorithms/maths/BellNumbersTest.java new file mode 100644 index 000000000000..8dd83cf0f7a9 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/BellNumbersTest.java @@ -0,0 +1,53 @@ +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; + +class BellNumbersTest { + + @Test + void testStandardCases() { + // Base cases and small numbers + assertEquals(1, BellNumbers.compute(0)); + assertEquals(1, BellNumbers.compute(1)); + assertEquals(2, BellNumbers.compute(2)); + assertEquals(5, BellNumbers.compute(3)); + assertEquals(15, BellNumbers.compute(4)); + assertEquals(52, BellNumbers.compute(5)); + } + + @Test + void testMediumNumber() { + // B10 = 115,975 + assertEquals(115975, BellNumbers.compute(10)); + // B15 = 1,382,958,545 + assertEquals(1382958545L, BellNumbers.compute(15)); + } + + @Test + void testLargeNumber() { + // B20 = 51,724,158,235,372 + // We use the 'L' suffix to tell Java this is a long literal + assertEquals(51724158235372L, BellNumbers.compute(20)); + } + + @Test + void testMaxLongCapacity() { + // B25 is the largest Bell number that fits in a Java long (signed 64-bit) + // B25 = 4,638,590,332,229,999,353 + assertEquals(4638590332229999353L, BellNumbers.compute(25)); + } + + @Test + void testNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> BellNumbers.compute(-1)); + } + + @Test + void testOverflowProtection() { + // We expect an exception if the user asks for the impossible + assertThrows(IllegalArgumentException.class, () -> BellNumbers.compute(26)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java b/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java new file mode 100644 index 000000000000..02e964b53771 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java @@ -0,0 +1,34 @@ +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; + +public class ComplexNumberMultiplyTest { + + @Test + void testExample() { + assertEquals("0+2i", ComplexNumberMultiply.multiply("1+1i", "1+1i")); + } + + @Test + void testNegative() { + assertEquals("0+-2i", ComplexNumberMultiply.multiply("1+-1i", "1+-1i")); + } + + @Test + void testZero() { + assertEquals("0+0i", ComplexNumberMultiply.multiply("0+0i", "5+3i")); + } + + @Test + void testInvalidFormat() { + assertThrows(IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply("1+1", "1+1i")); + } + + @Test + void testNullInput() { + assertThrows(IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply(null, "1+1i")); + } +} diff --git a/src/test/java/com/thealgorithms/maths/CorrelationTest.java b/src/test/java/com/thealgorithms/maths/CorrelationTest.java new file mode 100644 index 000000000000..96867d56ad5e --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/CorrelationTest.java @@ -0,0 +1,51 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for Correlation class + */ +public class CorrelationTest { + + public static final double DELTA = 1e-9; + + // Regular correlation test + public void testCorrelationFirst() { + double[] x = {1, 2, 3, 4}; + double[] y = {7, 1, 4, 9}; + int n = 4; + assertEquals(0.3319700011, Correlation.correlation(x, y, n), DELTA); + } + + // Regular correlation test (zero correlation) + public void testCorrelationSecond() { + double[] x = {1, 2, 3, 4}; + double[] y = {5, 0, 9, 2}; + int n = 4; + assertEquals(0, Correlation.correlation(x, y, n), DELTA); + } + + // Correlation with a constant variable is taken to be zero + public void testCorrelationConstant() { + double[] x = {1, 2, 3}; + double[] y = {4, 4, 4}; + int n = 3; + assertEquals(0, Correlation.correlation(x, y, n), DELTA); + } + + // Linear dependence gives correlation 1 + public void testCorrelationLinearDependence() { + double[] x = {1, 2, 3, 4}; + double[] y = {6, 8, 10, 12}; + int n = 4; + assertEquals(1, Correlation.correlation(x, y, n), DELTA); + } + + // Inverse linear dependence gives correlation -1 + public void testCorrelationInverseLinearDependence() { + double[] x = {1, 2, 3, 4, 5}; + double[] y = {18, 15, 12, 9, 6}; + int n = 5; + assertEquals(-1, Correlation.correlation(x, y, n), DELTA); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java b/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java new file mode 100644 index 000000000000..54e9e861ad44 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java @@ -0,0 +1,32 @@ +package com.thealgorithms.maths; + +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 org.junit.jupiter.api.Test; + +class DisariumNumberTest { + + @Test + void testDisariumNumbers() { + assertTrue(DisariumNumber.isDisarium(1)); + assertTrue(DisariumNumber.isDisarium(89)); + assertTrue(DisariumNumber.isDisarium(135)); + assertTrue(DisariumNumber.isDisarium(175)); + assertTrue(DisariumNumber.isDisarium(518)); + } + + @Test + void testNonDisariumNumbers() { + assertFalse(DisariumNumber.isDisarium(10)); + assertFalse(DisariumNumber.isDisarium(100)); + assertFalse(DisariumNumber.isDisarium(200)); + } + + @Test + void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> DisariumNumber.isDisarium(0)); + assertThrows(IllegalArgumentException.class, () -> DisariumNumber.isDisarium(-5)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java b/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java new file mode 100644 index 000000000000..6bd124629740 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java @@ -0,0 +1,23 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class DistanceBetweenTwoPointsTest { + + @Test + void testDistanceSimple() { + assertEquals(5.0, DistanceBetweenTwoPoints.calculate(0, 0, 3, 4), 1e-9); + } + + @Test + void testDistanceNegativeCoordinates() { + assertEquals(5.0, DistanceBetweenTwoPoints.calculate(-1, -1, 2, 3), 1e-9); + } + + @Test + void testSamePoint() { + assertEquals(0.0, DistanceBetweenTwoPoints.calculate(2, 2, 2, 2), 1e-9); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java b/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java index 3a14b80dd4f9..66f3b7b03938 100644 --- a/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java +++ b/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java @@ -9,78 +9,78 @@ public class DistanceFormulaTest { @Test void euclideanTest1() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1, 1, 2, 2), 1.4142135623730951); + Assertions.assertEquals(1.4142135623730951, DistanceFormula.euclideanDistance(1, 1, 2, 2)); } @Test void euclideanTest2() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1, 3, 8, 0), 7.0710678118654755); + Assertions.assertEquals(7.0710678118654755, DistanceFormula.euclideanDistance(1, 3, 8, 0)); } @Test void euclideanTest3() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(2.4, 9.1, 55.1, 100), 110.91911467371168); + Assertions.assertEquals(110.91911467371168, DistanceFormula.euclideanDistance(2.4, 9.1, 55.1, 100)); } @Test void euclideanTest4() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1000, 13, 20000, 84), 19022.067605809836); + Assertions.assertEquals(19022.067605809836, DistanceFormula.euclideanDistance(1000, 13, 20000, 84)); } @Test public void manhattantest1() { - assertEquals(DistanceFormula.manhattanDistance(1, 2, 3, 4), 4); + assertEquals(4, DistanceFormula.manhattanDistance(1, 2, 3, 4)); } @Test public void manhattantest2() { - assertEquals(DistanceFormula.manhattanDistance(6.5, 8.4, 20.1, 13.6), 18.8); + assertEquals(18.8, DistanceFormula.manhattanDistance(6.5, 8.4, 20.1, 13.6)); } @Test public void manhattanTest3() { - assertEquals(DistanceFormula.manhattanDistance(10.112, 50, 8, 25.67), 26.442); + assertEquals(26.442, DistanceFormula.manhattanDistance(10.112, 50, 8, 25.67)); } @Test public void hammingTest1() { int[] array1 = {1, 1, 1, 1}; int[] array2 = {0, 0, 0, 0}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 4); + assertEquals(4, DistanceFormula.hammingDistance(array1, array2)); } @Test public void hammingTest2() { int[] array1 = {1, 1, 1, 1}; int[] array2 = {1, 1, 1, 1}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 0); + assertEquals(0, DistanceFormula.hammingDistance(array1, array2)); } @Test public void hammingTest3() { int[] array1 = {1, 0, 0, 1, 1, 0, 1, 1, 0}; int[] array2 = {0, 1, 0, 0, 1, 1, 1, 0, 0}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 5); + assertEquals(5, DistanceFormula.hammingDistance(array1, array2)); } @Test public void minkowskiTest1() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 1), 10); + assertEquals(10, DistanceFormula.minkowskiDistance(array1, array2, 1)); } @Test public void minkowskiTest2() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 2), 5.477225575051661); + assertEquals(5.477225575051661, DistanceFormula.minkowskiDistance(array1, array2, 2)); } @Test public void minkowskiTest3() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 3), 4.641588833612778); + assertEquals(4.641588833612778, DistanceFormula.minkowskiDistance(array1, array2, 3)); } } diff --git a/src/test/java/com/thealgorithms/maths/EvilNumberTest.java b/src/test/java/com/thealgorithms/maths/EvilNumberTest.java new file mode 100644 index 000000000000..e59171fad25f --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/EvilNumberTest.java @@ -0,0 +1,28 @@ +package com.thealgorithms.maths; + +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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class EvilNumberTest { + @ParameterizedTest + @CsvSource({"0", "3", "10", "129", "222", "500", "777", "1198"}) + void evilNumbersTest(int n) { + assertTrue(EvilNumber.isEvilNumber(n)); + } + + @ParameterizedTest + @CsvSource({"1", "7", "100", "333", "555", "1199"}) + void odiousNumbersTest(int n) { + assertFalse(EvilNumber.isEvilNumber(n)); + } + + @ParameterizedTest + @CsvSource({"-1"}) + void throwsNegativeNumbersNotAllowed(int n) { + assertThrows(IllegalArgumentException.class, () -> EvilNumber.isEvilNumber(n)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithmTest.java b/src/test/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithmTest.java new file mode 100644 index 000000000000..56c005fd51ae --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ExtendedEuclideanAlgorithmTest.java @@ -0,0 +1,47 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ExtendedEuclideanAlgorithmTest { + + /** + * Verifies that the returned values satisfy BΓ©zout's identity: a*x + b*y = + * gcd(a, b) + */ + private void verifyBezoutIdentity(long a, long b, long[] result) { + long gcd = result[0]; + long x = result[1]; + long y = result[2]; + assertEquals(a * x + b * y, gcd, "BΓ©zout's identity failed for gcd(" + a + ", " + b + ")"); + } + + @Test + public void testExtendedGCD() { + // Test case 1: General case gcd(30, 50) = 10 + long[] result1 = ExtendedEuclideanAlgorithm.extendedGCD(30, 50); + assertEquals(10, result1[0], "Test Case 1 Failed: gcd(30, 50) should be 10"); + verifyBezoutIdentity(30, 50, result1); + + // Test case 2: Another general case gcd(240, 46) = 2 + long[] result2 = ExtendedEuclideanAlgorithm.extendedGCD(240, 46); + assertEquals(2, result2[0], "Test Case 2 Failed: gcd(240, 46) should be 2"); + verifyBezoutIdentity(240, 46, result2); + + // Test case 3: Base case where b is 0, gcd(10, 0) = 10 + long[] result3 = ExtendedEuclideanAlgorithm.extendedGCD(10, 0); + assertEquals(10, result3[0], "Test Case 3 Failed: gcd(10, 0) should be 10"); + verifyBezoutIdentity(10, 0, result3); + + // Test case 4: Numbers are co-prime gcd(17, 13) = 1 + long[] result4 = ExtendedEuclideanAlgorithm.extendedGCD(17, 13); + assertEquals(1, result4[0], "Test Case 4 Failed: gcd(17, 13) should be 1"); + verifyBezoutIdentity(17, 13, result4); + + // Test case 5: One number is a multiple of the other gcd(100, 20) = 20 + long[] result5 = ExtendedEuclideanAlgorithm.extendedGCD(100, 20); + assertEquals(20, result5[0], "Test Case 5 Failed: gcd(100, 20) should be 20"); + verifyBezoutIdentity(100, 20, result5); + } +} diff --git a/src/test/java/com/thealgorithms/maths/FactorialTest.java b/src/test/java/com/thealgorithms/maths/FactorialTest.java index b38dc45589ee..a393136696e1 100644 --- a/src/test/java/com/thealgorithms/maths/FactorialTest.java +++ b/src/test/java/com/thealgorithms/maths/FactorialTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; import org.junit.jupiter.api.Test; public class FactorialTest { @@ -11,14 +12,14 @@ public class FactorialTest { @Test public void testWhenInvalidInoutProvidedShouldThrowException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Factorial.factorial(-1)); - assertEquals(exception.getMessage(), EXCEPTION_MESSAGE); + assertEquals(EXCEPTION_MESSAGE, exception.getMessage()); } @Test public void testCorrectFactorialCalculation() { - assertEquals(1, Factorial.factorial(0)); - assertEquals(1, Factorial.factorial(1)); - assertEquals(120, Factorial.factorial(5)); - assertEquals(3628800, Factorial.factorial(10)); + assertEquals(BigInteger.ONE, Factorial.factorial(0)); + assertEquals(BigInteger.ONE, Factorial.factorial(1)); + assertEquals(BigInteger.valueOf(120), Factorial.factorial(5)); + assertEquals(BigInteger.valueOf(3628800), Factorial.factorial(10)); } } diff --git a/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java b/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java new file mode 100644 index 000000000000..be5ddd7ee79e --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java @@ -0,0 +1,33 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class FriendlyNumberTest { + + @Test + public void testFriendlyNumbers() { + // 6 and 28 are friendly (abundancy index = 2) + assertTrue(FriendlyNumber.areFriendly(6, 28)); + // Every number is friendly with itself + assertTrue(FriendlyNumber.areFriendly(6, 6)); + assertTrue(FriendlyNumber.areFriendly(1, 1)); + } + + @Test + public void testNonFriendlyNumbers() { + assertFalse(FriendlyNumber.areFriendly(6, 10)); + assertFalse(FriendlyNumber.areFriendly(10, 15)); + assertFalse(FriendlyNumber.areFriendly(4, 9)); + } + + @Test + public void testInvalidInputs() { + assertFalse(FriendlyNumber.areFriendly(0, 6)); + assertFalse(FriendlyNumber.areFriendly(-1, 6)); + assertFalse(FriendlyNumber.areFriendly(6, -1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/GCDTest.java b/src/test/java/com/thealgorithms/maths/GCDTest.java index bac3f8f7596c..6bc870e94df9 100644 --- a/src/test/java/com/thealgorithms/maths/GCDTest.java +++ b/src/test/java/com/thealgorithms/maths/GCDTest.java @@ -6,57 +6,77 @@ public class GCDTest { @Test - void test1() { + void testNegativeAndZeroThrowsException() { Assertions.assertThrows(ArithmeticException.class, () -> GCD.gcd(-1, 0)); } @Test - void test2() { + void testPositiveAndNegativeThrowsException() { Assertions.assertThrows(ArithmeticException.class, () -> GCD.gcd(10, -2)); } @Test - void test3() { + void testBothNegativeThrowsException() { Assertions.assertThrows(ArithmeticException.class, () -> GCD.gcd(-5, -3)); } @Test - void test4() { - Assertions.assertEquals(GCD.gcd(0, 2), 2); + void testZeroAndPositiveReturnsPositive() { + Assertions.assertEquals(2, GCD.gcd(0, 2)); } @Test - void test5() { - Assertions.assertEquals(GCD.gcd(10, 0), 10); + void testPositiveAndZeroReturnsPositive() { + Assertions.assertEquals(10, GCD.gcd(10, 0)); } @Test - void test6() { - Assertions.assertEquals(GCD.gcd(1, 0), 1); + void testOneAndZeroReturnsOne() { + Assertions.assertEquals(1, GCD.gcd(1, 0)); } @Test - void test7() { - Assertions.assertEquals(GCD.gcd(9, 6), 3); + void testTwoPositiveNumbers() { + Assertions.assertEquals(3, GCD.gcd(9, 6)); } @Test - void test8() { - Assertions.assertEquals(GCD.gcd(48, 18, 30, 12), 6); + void testMultipleArgumentsGcd() { + Assertions.assertEquals(6, GCD.gcd(48, 18, 30, 12)); } @Test - void testArrayGcd1() { - Assertions.assertEquals(GCD.gcd(new int[] {9, 6}), 3); + void testArrayInputGcd() { + Assertions.assertEquals(3, GCD.gcd(new int[] {9, 6})); } @Test - void testArrayGcd2() { - Assertions.assertEquals(GCD.gcd(new int[] {2 * 3 * 5 * 7, 2 * 5 * 5 * 5, 2 * 5 * 11, 5 * 5 * 5 * 13}), 5); + void testArrayWithCommonFactor() { + Assertions.assertEquals(5, GCD.gcd(new int[] {2 * 3 * 5 * 7, 2 * 5 * 5 * 5, 2 * 5 * 11, 5 * 5 * 5 * 13})); } @Test - void testArrayGcdForEmptyInput() { - Assertions.assertEquals(GCD.gcd(new int[] {}), 0); + void testEmptyArrayReturnsZero() { + Assertions.assertEquals(0, GCD.gcd(new int[] {})); + } + + @Test + void testSameNumbers() { + Assertions.assertEquals(7, GCD.gcd(7, 7)); + } + + @Test + void testPrimeNumbersHaveGcdOne() { + Assertions.assertEquals(1, GCD.gcd(13, 17)); + } + + @Test + void testSingleElementArrayReturnsElement() { + Assertions.assertEquals(42, GCD.gcd(new int[] {42})); + } + + @Test + void testLargeNumbers() { + Assertions.assertEquals(12, GCD.gcd(123456, 789012)); } } diff --git a/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java b/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java new file mode 100644 index 000000000000..19558510f916 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class JacobsthalNumberTest { + + @Test + public void testBaseCases() { + assertEquals(0, JacobsthalNumber.jacobsthal(0)); + assertEquals(1, JacobsthalNumber.jacobsthal(1)); + } + + @Test + public void testKnownValues() { + assertEquals(1, JacobsthalNumber.jacobsthal(2)); + assertEquals(3, JacobsthalNumber.jacobsthal(3)); + assertEquals(5, JacobsthalNumber.jacobsthal(4)); + assertEquals(11, JacobsthalNumber.jacobsthal(5)); + assertEquals(21, JacobsthalNumber.jacobsthal(6)); + assertEquals(43, JacobsthalNumber.jacobsthal(7)); + assertEquals(85, JacobsthalNumber.jacobsthal(8)); + assertEquals(171, JacobsthalNumber.jacobsthal(9)); + assertEquals(341, JacobsthalNumber.jacobsthal(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> JacobsthalNumber.jacobsthal(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java index c4205985dbfd..885382e29ca2 100644 --- a/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java +++ b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java @@ -176,7 +176,7 @@ void testSolutionEquality() { assertEquals(solution1, solution2); assertNotEquals(solution3, solution1); assertEquals(solution1, solution1); - assertNotEquals(null, solution1); + assertNotNull(solution1); assertNotEquals("string", solution1); } @@ -217,7 +217,7 @@ void testGcdSolutionWrapperEquality() { assertEquals(wrapper1, wrapper2); assertNotEquals(wrapper3, wrapper1); assertEquals(wrapper1, wrapper1); - assertNotEquals(null, wrapper1); + assertNotNull(wrapper1); assertNotEquals("string", wrapper1); } diff --git a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java b/src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java similarity index 99% rename from src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java rename to src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java index 7c3ce6635aa0..5a3d1c64b379 100644 --- a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java +++ b/src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.others; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/test/java/com/thealgorithms/maths/LuckyNumberTest.java b/src/test/java/com/thealgorithms/maths/LuckyNumberTest.java new file mode 100644 index 000000000000..91904316b25c --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/LuckyNumberTest.java @@ -0,0 +1,32 @@ +package com.thealgorithms.maths; + +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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class LuckyNumberTest { + + @ParameterizedTest + @CsvSource({"1", "3", "13", "49", "109", "459", "949"}) + void luckyNumbersTest(int n) { + assertTrue(LuckyNumber.isLucky(n)); + assertTrue(LuckyNumber.isLuckyNumber(n)); + } + + @ParameterizedTest + @CsvSource({"2", "17", "100", "300", "700"}) + void nonLuckyNumbersTest(int n) { + assertFalse(LuckyNumber.isLucky(n)); + assertFalse(LuckyNumber.isLuckyNumber(n)); + } + + @ParameterizedTest + @CsvSource({"0", "-1"}) + void throwsNegativeNumbersNotAllowed(int n) { + assertThrows(IllegalArgumentException.class, () -> LuckyNumber.isLucky(n)); + assertThrows(IllegalArgumentException.class, () -> LuckyNumber.isLuckyNumber(n)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/MeansTest.java b/src/test/java/com/thealgorithms/maths/MeansTest.java index deee0a931910..853fdbea3963 100644 --- a/src/test/java/com/thealgorithms/maths/MeansTest.java +++ b/src/test/java/com/thealgorithms/maths/MeansTest.java @@ -172,6 +172,53 @@ void testHarmonicMeanWithLinkedList() { assertEquals(expected, Means.harmonic(numbers), EPSILON); } + // ========== Quadratic Mean Tests ========== + + @Test + void testQuadraticMeanThrowsExceptionForEmptyList() { + List numbers = new ArrayList<>(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.quadratic(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); + } + + @Test + void testQuadraticMeanSingleNumber() { + LinkedHashSet numbers = new LinkedHashSet<>(Arrays.asList(2.5)); + assertEquals(2.5, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanTwoNumbers() { + List numbers = Arrays.asList(1.0, 7.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanMultipleNumbers() { + Vector numbers = new Vector<>(Arrays.asList(1.0, 2.5, 3.0, 7.5, 10.0)); + double expected = Math.sqrt(34.5); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanThreeNumbers() { + List numbers = Arrays.asList(3.0, 6.0, 9.0); + double expected = Math.sqrt(42.0); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanIdenticalNumbers() { + List numbers = Arrays.asList(5.0, 5.0, 5.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanWithLinkedList() { + LinkedList numbers = new LinkedList<>(Arrays.asList(1.0, 5.0, 11.0)); + assertEquals(7.0, Means.quadratic(numbers), EPSILON); + } + // ========== Additional Edge Case Tests ========== @Test @@ -198,21 +245,25 @@ void testAllMeansConsistencyForIdenticalValues() { double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertEquals(7.5, arithmetic, EPSILON); assertEquals(7.5, geometric, EPSILON); assertEquals(7.5, harmonic, EPSILON); + assertEquals(7.5, quadratic, EPSILON); } @Test void testMeansRelationship() { - // For positive numbers, harmonic mean ≀ geometric mean ≀ arithmetic mean + // For positive numbers, harmonic mean ≀ geometric mean ≀ arithmetic mean ≀ quadratic mean List numbers = Arrays.asList(2.0, 4.0, 8.0); double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertTrue(harmonic <= geometric, "Harmonic mean should be ≀ geometric mean"); assertTrue(geometric <= arithmetic, "Geometric mean should be ≀ arithmetic mean"); + assertTrue(arithmetic <= quadratic, "Arithmetic mean should be ≀ quadratic mean"); } } diff --git a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java index 3fe58dadf8a5..1ee437b190c5 100644 --- a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java @@ -48,22 +48,22 @@ public void testGetWithSameObject() { var uglyNumbers = new NthUglyNumber(new int[] {7, 2, 5, 3}); for (final var tc : testCases.entrySet()) { - assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue()); + assertEquals(tc.getValue(), uglyNumbers.get(tc.getKey())); } - assertEquals(uglyNumbers.get(999), 385875); + assertEquals(385875, uglyNumbers.get(999)); } @Test public void testGetWithBase1() { var uglyNumbers = new NthUglyNumber(new int[] {1}); - assertEquals(uglyNumbers.get(10), 1); + assertEquals(1, uglyNumbers.get(10)); } @Test public void testGetWithBase2() { var uglyNumbers = new NthUglyNumber(new int[] {2}); - assertEquals(uglyNumbers.get(5), 32); + assertEquals(32, uglyNumbers.get(5)); } @Test diff --git a/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java b/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java new file mode 100644 index 000000000000..b9d7f04b4d0b --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java @@ -0,0 +1,36 @@ +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; + +/** + * @author Vraj Prajapati (@Rosander0) + */ +public class PadovanSequenceTest { + + @Test + public void testBaseCase() { + assertEquals(1, PadovanSequence.padovan(0)); + assertEquals(1, PadovanSequence.padovan(1)); + assertEquals(1, PadovanSequence.padovan(2)); + } + + @Test + public void testKnownValues() { + assertEquals(2, PadovanSequence.padovan(3)); + assertEquals(2, PadovanSequence.padovan(4)); + assertEquals(3, PadovanSequence.padovan(5)); + assertEquals(4, PadovanSequence.padovan(6)); + assertEquals(5, PadovanSequence.padovan(7)); + assertEquals(7, PadovanSequence.padovan(8)); + assertEquals(9, PadovanSequence.padovan(9)); + assertEquals(12, PadovanSequence.padovan(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> PadovanSequence.padovan(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java b/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java index a70100c0b913..4e4bd85d07b5 100644 --- a/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java @@ -25,6 +25,6 @@ public void testNumbersAreNotPalindromes() { @Test public void testIfNegativeInputThenExceptionExpected() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> PalindromeNumber.isPalindrome(-1)); - Assertions.assertEquals(exception.getMessage(), "Input parameter must not be negative!"); + Assertions.assertEquals("Input parameter must not be negative!", exception.getMessage()); } } diff --git a/src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java b/src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java similarity index 98% rename from src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java rename to src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java index 130cd19b47b1..2405da558700 100644 --- a/src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java +++ b/src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.misc; +package com.thealgorithms.maths; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java b/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java index 7649e21eb231..a9b78be88042 100644 --- a/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java +++ b/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java @@ -14,13 +14,13 @@ public class ParseIntegerTest { @Test public void testNullInput() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> ParseInteger.parseInt(null)); - Assertions.assertEquals(exception.getMessage(), NULL_PARAMETER_MESSAGE); + Assertions.assertEquals(NULL_PARAMETER_MESSAGE, exception.getMessage()); } @Test public void testEmptyInput() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> ParseInteger.parseInt("")); - Assertions.assertEquals(exception.getMessage(), EMPTY_PARAMETER_MESSAGE); + Assertions.assertEquals(EMPTY_PARAMETER_MESSAGE, exception.getMessage()); } @Test diff --git a/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java b/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java new file mode 100644 index 000000000000..0ec476dc0641 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PerrinNumberTest { + + @Test + public void testBaseCases() { + assertEquals(3, PerrinNumber.perrin(0)); + assertEquals(0, PerrinNumber.perrin(1)); + assertEquals(2, PerrinNumber.perrin(2)); + } + + @Test + public void testKnownValues() { + assertEquals(3, PerrinNumber.perrin(3)); + assertEquals(2, PerrinNumber.perrin(4)); + assertEquals(5, PerrinNumber.perrin(5)); + assertEquals(5, PerrinNumber.perrin(6)); + assertEquals(7, PerrinNumber.perrin(7)); + assertEquals(10, PerrinNumber.perrin(8)); + assertEquals(12, PerrinNumber.perrin(9)); + assertEquals(17, PerrinNumber.perrin(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> PerrinNumber.perrin(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/PowerOfFourTest.java b/src/test/java/com/thealgorithms/maths/PowerOfFourTest.java new file mode 100644 index 000000000000..c91f8b3cf1b5 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PowerOfFourTest.java @@ -0,0 +1,36 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PowerOfFourTest { + + @Test + void testPowersOfFour() { + assertTrue(PowerOfFour.isPowerOfFour(1)); + assertTrue(PowerOfFour.isPowerOfFour(4)); + assertTrue(PowerOfFour.isPowerOfFour(16)); + assertTrue(PowerOfFour.isPowerOfFour(64)); + assertTrue(PowerOfFour.isPowerOfFour(256)); + assertTrue(PowerOfFour.isPowerOfFour(1024)); + } + + @Test + void testNonPowersOfFour() { + assertFalse(PowerOfFour.isPowerOfFour(2)); + assertFalse(PowerOfFour.isPowerOfFour(3)); + assertFalse(PowerOfFour.isPowerOfFour(5)); + assertFalse(PowerOfFour.isPowerOfFour(8)); + assertFalse(PowerOfFour.isPowerOfFour(15)); + assertFalse(PowerOfFour.isPowerOfFour(32)); + } + + @Test + void testEdgeCases() { + assertFalse(PowerOfFour.isPowerOfFour(0)); + assertFalse(PowerOfFour.isPowerOfFour(-1)); + assertFalse(PowerOfFour.isPowerOfFour(-4)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java b/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java index a2046511ddf5..a6552d56783c 100644 --- a/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java +++ b/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java @@ -14,10 +14,10 @@ public void testSolveEquationRealRoots() { double c = 1.9; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 2); - Assertions.assertEquals(roots[0].real, -0.27810465435684306); + Assertions.assertEquals(2, roots.length, 2); + Assertions.assertEquals(-0.27810465435684306, roots[0].real); Assertions.assertNull(roots[0].imaginary); - Assertions.assertEquals(roots[1].real, -1.6266572504050616); + Assertions.assertEquals(-1.6266572504050616, roots[1].real); Assertions.assertNull(roots[1].imaginary); } @@ -29,8 +29,8 @@ public void testSolveEquationEqualRoots() { double c = 1; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 1); - Assertions.assertEquals(roots[0].real, -1); + Assertions.assertEquals(1, roots.length); + Assertions.assertEquals(-1, roots[0].real); } @Test @@ -41,10 +41,10 @@ public void testSolveEquationComplexRoots() { double c = 5.6; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 2); - Assertions.assertEquals(roots[0].real, -0.8695652173913044); - Assertions.assertEquals(roots[0].imaginary, 1.2956229935435948); - Assertions.assertEquals(roots[1].real, -0.8695652173913044); - Assertions.assertEquals(roots[1].imaginary, -1.2956229935435948); + Assertions.assertEquals(2, roots.length); + Assertions.assertEquals(-0.8695652173913044, roots[0].real); + Assertions.assertEquals(1.2956229935435948, roots[0].imaginary); + Assertions.assertEquals(-0.8695652173913044, roots[1].real); + Assertions.assertEquals(-1.2956229935435948, roots[1].imaginary); } } diff --git a/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java b/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java index c744614e5cfa..c5d47f2213a9 100644 --- a/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java +++ b/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java @@ -29,19 +29,19 @@ public TestCase(final int[] inInputArray, final int inSecondMin, final int inSec @Test public void testForEmptyInputArray() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMin(new int[] {})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_LEN_LESS_2); + assertEquals(EXP_MSG_ARR_LEN_LESS_2, exception.getMessage()); } @Test public void testForArrayWithSingleElement() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMax(new int[] {1})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_LEN_LESS_2); + assertEquals(EXP_MSG_ARR_LEN_LESS_2, exception.getMessage()); } @Test public void testForArrayWithSameElements() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMin(new int[] {1, 1, 1, 1})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_SAME_ELE); + assertEquals(EXP_MSG_ARR_SAME_ELE, exception.getMessage()); } @ParameterizedTest diff --git a/src/test/java/com/thealgorithms/maths/SieveOfEratosthenesTest.java b/src/test/java/com/thealgorithms/maths/SieveOfEratosthenesTest.java index ebbd5df712fc..5d491a493ee7 100644 --- a/src/test/java/com/thealgorithms/maths/SieveOfEratosthenesTest.java +++ b/src/test/java/com/thealgorithms/maths/SieveOfEratosthenesTest.java @@ -1,46 +1,64 @@ package com.thealgorithms.maths; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; +/** + * Test cases for Sieve of Eratosthenes algorithm + * + * @author Navadeep0007 + */ class SieveOfEratosthenesTest { + + @Test + void testPrimesUpTo10() { + List expected = Arrays.asList(2, 3, 5, 7); + assertEquals(expected, SieveOfEratosthenes.findPrimes(10)); + } + + @Test + void testPrimesUpTo30() { + List expected = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29); + assertEquals(expected, SieveOfEratosthenes.findPrimes(30)); + } + @Test - public void testfFindPrimesTill1() { - assertArrayEquals(new int[] {}, SieveOfEratosthenes.findPrimesTill(1)); + void testPrimesUpTo2() { + List expected = Arrays.asList(2); + assertEquals(expected, SieveOfEratosthenes.findPrimes(2)); } @Test - public void testfFindPrimesTill2() { - assertArrayEquals(new int[] {2}, SieveOfEratosthenes.findPrimesTill(2)); + void testPrimesUpTo1() { + assertTrue(SieveOfEratosthenes.findPrimes(1).isEmpty()); } @Test - public void testfFindPrimesTill4() { - var primesTill4 = new int[] {2, 3}; - assertArrayEquals(primesTill4, SieveOfEratosthenes.findPrimesTill(3)); - assertArrayEquals(primesTill4, SieveOfEratosthenes.findPrimesTill(4)); + void testPrimesUpTo0() { + assertTrue(SieveOfEratosthenes.findPrimes(0).isEmpty()); } @Test - public void testfFindPrimesTill40() { - var primesTill40 = new int[] {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}; - assertArrayEquals(primesTill40, SieveOfEratosthenes.findPrimesTill(37)); - assertArrayEquals(primesTill40, SieveOfEratosthenes.findPrimesTill(38)); - assertArrayEquals(primesTill40, SieveOfEratosthenes.findPrimesTill(39)); - assertArrayEquals(primesTill40, SieveOfEratosthenes.findPrimesTill(40)); + void testNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> { SieveOfEratosthenes.findPrimes(-1); }); } @Test - public void testfFindPrimesTill240() { - var primesTill240 = new int[] {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239}; - assertArrayEquals(primesTill240, SieveOfEratosthenes.findPrimesTill(239)); - assertArrayEquals(primesTill240, SieveOfEratosthenes.findPrimesTill(240)); + void testCountPrimes() { + assertEquals(4, SieveOfEratosthenes.countPrimes(10)); + assertEquals(25, SieveOfEratosthenes.countPrimes(100)); } @Test - public void testFindPrimesTillThrowsExceptionForNonPositiveInput() { - assertThrows(IllegalArgumentException.class, () -> SieveOfEratosthenes.findPrimesTill(0)); + void testLargeNumber() { + List primes = SieveOfEratosthenes.findPrimes(1000); + assertEquals(168, primes.size()); // There are 168 primes up to 1000 + assertEquals(2, primes.get(0)); // First prime + assertEquals(997, primes.get(primes.size() - 1)); // Last prime up to 1000 } } diff --git a/src/test/java/com/thealgorithms/maths/SigmoidActivationTest.java b/src/test/java/com/thealgorithms/maths/SigmoidActivationTest.java new file mode 100644 index 000000000000..aadf2c7ddc12 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SigmoidActivationTest.java @@ -0,0 +1,68 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class SigmoidActivationTest { + + @Test + public void calculationTest() { + assertEquals(0.5, SigmoidActivation.activate(0), 0.01, "1 case correct"); + assertEquals(0.73, SigmoidActivation.activate(1), 0.01, "2 case correct"); + assertEquals(0.26, SigmoidActivation.activate(-1), 0.01, "3 case correct"); + assertEquals(0.88, SigmoidActivation.activate(2), 0.01, "4 case correct"); + assertEquals(0.11, SigmoidActivation.activate(-2), 0.01, "5 case correct"); + + double[][] xBatch = new double[4][3]; + double[][] expectedX = new double[4][3]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 3; j++) { + xBatch[i][j] = 0; + } + } + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 3; j++) { + expectedX[i][j] = 0.5; + } + } + + assertTrue(Arrays.deepEquals(expectedX, SigmoidActivation.activate(xBatch)), "batch case correct"); + + assertEquals(0.25, SigmoidActivation.grad(0.5), 0.01, "grad calculation correct"); + + double[][] yBatch = new double[4][3]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 3; j++) { + yBatch[i][j] = 0.5; + } + } + + double[][] expectedY = new double[4][3]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 3; j++) { + expectedY[i][j] = 0.25; + } + } + assertTrue(Arrays.deepEquals(expectedY, SigmoidActivation.grad(yBatch)), "grad batch case correct"); + } + + @Test + public void willReturnNaN() { + double x = Double.NaN; + + assertTrue(Double.isNaN(SigmoidActivation.activate(x)), "returned NaN"); + } + + @Test + public void extremumNumbersOnActivate() { + double x = 777; + + assertEquals(1.0, SigmoidActivation.activate(x), 0.01, "big number case correct"); + assertEquals(0.0, SigmoidActivation.activate((-1) * x), 0.01, "small number case correct"); + } +} diff --git a/src/test/java/com/thealgorithms/maths/SmithNumberTest.java b/src/test/java/com/thealgorithms/maths/SmithNumberTest.java new file mode 100644 index 000000000000..4e2ba0b88e33 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SmithNumberTest.java @@ -0,0 +1,22 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SmithNumberTest { + + @ParameterizedTest + @CsvSource({"4", "22", "121", "562", "985", "4937775"}) + void positiveSmithNumbersTest(int n) { + assertTrue(SmithNumber.isSmithNumber(n)); + } + + @ParameterizedTest + @CsvSource({"2", "11", "100", "550", "999", "1234557"}) + void negativeSmithNumbersTest(int n) { + assertFalse(SmithNumber.isSmithNumber(n)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/SociableNumberTest.java b/src/test/java/com/thealgorithms/maths/SociableNumberTest.java new file mode 100644 index 000000000000..b4877cd761a7 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SociableNumberTest.java @@ -0,0 +1,56 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SociableNumber}. + * + * @author Vraj Prajapati (@Rosander0) + */ +public class SociableNumberTest { + + @Test + public void testSumOfProperDivisorsEdgeCases() { + assertEquals(0, SociableNumber.sumOfProperDivisors(0)); + assertEquals(0, SociableNumber.sumOfProperDivisors(-5)); + assertEquals(0, SociableNumber.sumOfProperDivisors(1)); + assertEquals(1, SociableNumber.sumOfProperDivisors(2)); + } + + @Test + public void testSociableCycleOfLengthFive() { + assertTrue(SociableNumber.isSociable(12496, 5)); + } + + @Test + public void testAmicableNumbersAreSociableOfLengthTwo() { + assertTrue(SociableNumber.isSociable(220, 2)); + assertTrue(SociableNumber.isSociable(284, 2)); + } + + @Test + public void testNonSociableNumbers() { + assertFalse(SociableNumber.isSociable(12, 5)); + assertFalse(SociableNumber.isSociable(10, 3)); + } + + @Test + public void testEarlyCycleReturn() { + // 220 has cycle length 2; requesting a different length should return + // false because it returns to the start too early. + assertFalse(SociableNumber.isSociable(220, 3)); + assertFalse(SociableNumber.isSociable(284, 4)); + assertFalse(SociableNumber.isSociable(12496, 3)); + } + + @Test + public void testInvalidInputs() { + assertFalse(SociableNumber.isSociable(0, 5)); + assertFalse(SociableNumber.isSociable(-1, 5)); + assertFalse(SociableNumber.isSociable(220, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java b/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java index 2c10d2d14f3e..4716d389a4ca 100644 --- a/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java +++ b/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java @@ -8,19 +8,19 @@ public class StandardDeviationTest { @Test void test1() { double[] t1 = new double[] {1, 1, 1, 1, 1}; - Assertions.assertEquals(StandardDeviation.stdDev(t1), 0.0); + Assertions.assertEquals(0.0, StandardDeviation.stdDev(t1)); } @Test void test2() { double[] t2 = new double[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - Assertions.assertEquals(StandardDeviation.stdDev(t2), 2.8722813232690143); + Assertions.assertEquals(2.8722813232690143, StandardDeviation.stdDev(t2)); } @Test void test3() { double[] t3 = new double[] {1.1, 8.5, 20.3, 2.4, 6.2}; - Assertions.assertEquals(StandardDeviation.stdDev(t3), 6.8308125431752265); + Assertions.assertEquals(6.8308125431752265, StandardDeviation.stdDev(t3)); } @Test @@ -32,6 +32,6 @@ void test4() { 100.00045, 56.7, }; - Assertions.assertEquals(StandardDeviation.stdDev(t4), 38.506117353865775); + Assertions.assertEquals(38.506117353865775, StandardDeviation.stdDev(t4)); } } diff --git a/src/test/java/com/thealgorithms/maths/StandardScoreTest.java b/src/test/java/com/thealgorithms/maths/StandardScoreTest.java index 436b1fd011c6..6858b87ad2c6 100644 --- a/src/test/java/com/thealgorithms/maths/StandardScoreTest.java +++ b/src/test/java/com/thealgorithms/maths/StandardScoreTest.java @@ -7,21 +7,21 @@ public class StandardScoreTest { @Test void test1() { - Assertions.assertEquals(StandardScore.zScore(2, 0, 5), 0.4); + Assertions.assertEquals(0.4, StandardScore.zScore(2, 0, 5)); } @Test void test2() { - Assertions.assertEquals(StandardScore.zScore(1, 1, 1), 0.0); + Assertions.assertEquals(0.0, StandardScore.zScore(1, 1, 1)); } @Test void test3() { - Assertions.assertEquals(StandardScore.zScore(2.5, 1.8, 0.7), 1.0); + Assertions.assertEquals(1.0, StandardScore.zScore(2.5, 1.8, 0.7)); } @Test void test4() { - Assertions.assertEquals(StandardScore.zScore(8.9, 3, 4.2), 1.4047619047619049); + Assertions.assertEquals(1.4047619047619049, StandardScore.zScore(8.9, 3, 4.2)); } } diff --git a/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java index 834fe61a049e..02b3f614ca9a 100644 --- a/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java +++ b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java @@ -1,13 +1,12 @@ 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; /** * Test class for SumOfSquares - * - * @author BEASTSHRIRAM */ class SumOfSquaresTest { @@ -65,4 +64,11 @@ void testEdgeCases() { // Test edge case assertEquals(1, SumOfSquares.minSquares(0)); // 0^2 } + + @Test + void testNegativeInput() { + // Negative inputs should throw IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> SumOfSquares.minSquares(-1)); + assertThrows(IllegalArgumentException.class, () -> SumOfSquares.minSquares(-10)); + } } diff --git a/src/test/java/com/thealgorithms/maths/VolumeTest.java b/src/test/java/com/thealgorithms/maths/VolumeTest.java index 7cd0c6716147..c0b02d6ba28e 100644 --- a/src/test/java/com/thealgorithms/maths/VolumeTest.java +++ b/src/test/java/com/thealgorithms/maths/VolumeTest.java @@ -1,6 +1,6 @@ package com.thealgorithms.maths; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -10,30 +10,39 @@ public class VolumeTest { public void volume() { /* test cube */ - assertTrue(Volume.volumeCube(7) == 343.0); + assertEquals(343.0, Volume.volumeCube(7)); /* test cuboid */ - assertTrue(Volume.volumeCuboid(2, 5, 7) == 70.0); + assertEquals(70.0, Volume.volumeCuboid(2, 5, 7)); /* test sphere */ - assertTrue(Volume.volumeSphere(7) == 1436.7550402417319); + assertEquals(1436.7550402417319, Volume.volumeSphere(7)); /* test cylinder */ - assertTrue(Volume.volumeCylinder(3, 7) == 197.92033717615698); + assertEquals(197.92033717615698, Volume.volumeCylinder(3, 7)); /* test hemisphere */ - assertTrue(Volume.volumeHemisphere(7) == 718.3775201208659); + assertEquals(718.3775201208659, Volume.volumeHemisphere(7)); /* test cone */ - assertTrue(Volume.volumeCone(3, 7) == 65.97344572538566); + assertEquals(65.97344572538566, Volume.volumeCone(3, 7)); /* test prism */ - assertTrue(Volume.volumePrism(10, 2) == 20.0); + assertEquals(20.0, Volume.volumePrism(10, 2)); /* test pyramid */ - assertTrue(Volume.volumePyramid(10, 3) == 10.0); + assertEquals(10.0, Volume.volumePyramid(10, 3)); /* test frustum */ - assertTrue(Volume.volumeFrustumOfCone(3, 5, 7) == 359.188760060433); + assertEquals(359.188760060433, Volume.volumeFrustumOfCone(3, 5, 7)); + + /* test pyramid frustum */ + assertEquals(140.0, Volume.volumeFrustumOfPyramid(6, 24, 10)); + + /* test torus */ + assertEquals(39.47841760435743, Volume.volumeTorus(2, 1)); + + /* test ellipsoid */ + assertEquals(25.1327412287183459, Volume.volumeEllipsoid(3, 2, 1)); } } diff --git a/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java b/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java index 734d02477ba2..98ac29e81a5b 100644 --- a/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java +++ b/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java @@ -5,6 +5,8 @@ import com.thealgorithms.maths.Prime.MobiusFunction; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class MobiusFunctionTest { @@ -152,4 +154,15 @@ void testMobiusFunction() { assertEquals(expectedValue, actualValue); } } + + /** + * Large inputs whose smallest square divisor test used to overflow, most notably + * {@code Integer.MAX_VALUE}, whose square wraps around to 1 and made every number look like it + * had a squared prime factor. + */ + @ParameterizedTest + @CsvSource({"2147483647, -1", "2147483646, 0", "2147483645, -1", "2147483644, 0", "2147483629, -1", "2147395600, 0", "1073741824, 0", "1073741789, -1", "999999937, -1", "999999999, 0", "2146689000, 0"}) + void testMobiusForLargeNumbers(int number, int expected) { + assertEquals(expected, MobiusFunction.mobius(number)); + } } diff --git a/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java b/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java new file mode 100644 index 000000000000..adc8fb717e57 --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java @@ -0,0 +1,115 @@ +package com.thealgorithms.matrix; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class QRDecompositionTest { + + private static final double DELTA = 1e-9; + + @Test + public void testQRDecomposition2x2() { + double[][] matrix = {{12, -51}, {6, 167}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + double[][] reconstructed = multiplyMatrices(q, r); + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], reconstructed[i], DELTA); + } + } + + @Test + public void testQRDecomposition3x3() { + double[][] matrix = {{1, 1, 0}, {1, 0, 1}, {0, 1, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + double[][] reconstructed = multiplyMatrices(q, r); + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], reconstructed[i], DELTA); + } + } + + @Test + public void testQROrthogonalColumns() { + double[][] matrix = {{1, 1, 0}, {1, 0, 1}, {0, 1, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + + for (int i = 0; i < q[0].length; i++) { + for (int j = i; j < q[0].length; j++) { + double dot = 0; + for (int k = 0; k < q.length; k++) { + dot += q[k][i] * q[k][j]; + } + if (i == j) { + assertArrayEquals(new double[] {1.0}, new double[] {dot}, DELTA); + } else { + assertArrayEquals(new double[] {0.0}, new double[] {dot}, DELTA); + } + } + } + } + + @Test + public void testRIsUpperTriangular() { + double[][] matrix = {{12, -51}, {6, 167}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] r = qr.getR(); + + for (int i = 1; i < r.length; i++) { + for (int j = 0; j < i; j++) { + assertArrayEquals(new double[] {0.0}, new double[] {r[i][j]}, DELTA); + } + } + } + + @Test + public void testQRDecompositionIdentityMatrix() { + double[][] matrix = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], q[i], DELTA); + assertArrayEquals(matrix[i], r[i], DELTA); + } + } + + @Test + public void testQRDecompositionRankDeficientThrows() { + double[][] matrix = {{1, 2}, {2, 4}}; + assertThrows(ArithmeticException.class, () -> QRDecomposition.decompose(matrix)); + } + + @Test + public void testQRDecompositionNullMatrixThrows() { + assertThrows(IllegalArgumentException.class, () -> QRDecomposition.decompose(null)); + } + + @Test + public void testQRDecompositionEmptyMatrixThrows() { + assertThrows(IllegalArgumentException.class, () -> QRDecomposition.decompose(new double[0][0])); + } + + private static double[][] multiplyMatrices(double[][] a, double[][] b) { + int m = a.length; + int n = b[0].length; + int k = a[0].length; + double[][] result = new double[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + for (int p = 0; p < k; p++) { + result[i][j] += a[i][p] * b[p][j]; + } + } + } + return result; + } +} diff --git a/src/test/java/com/thealgorithms/matrix/StochasticMatrixTest.java b/src/test/java/com/thealgorithms/matrix/StochasticMatrixTest.java new file mode 100644 index 000000000000..1bba918dadac --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/StochasticMatrixTest.java @@ -0,0 +1,28 @@ +package com.thealgorithms.matrix; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class StochasticMatrixTest { + + @Test + void testRowStochasticMatrix() { + double[][] matrix = {{0.2, 0.5, 0.3}, {0.1, 0.6, 0.3}}; + assertTrue(StochasticMatrix.isRowStochastic(matrix)); + assertFalse(StochasticMatrix.isColumnStochastic(matrix)); + } + + @Test + void testColumnStochasticMatrix() { + double[][] matrix = {{0.4, 0.2}, {0.6, 0.8}}; + assertTrue(StochasticMatrix.isColumnStochastic(matrix)); + } + + @Test + void testInvalidMatrix() { + double[][] matrix = {{0.5, -0.5}, {0.5, 1.5}}; + assertFalse(StochasticMatrix.isRowStochastic(matrix)); + } +} diff --git a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java index f41953035846..c4a74af0ba8b 100644 --- a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java @@ -17,7 +17,7 @@ public class MedianOfRunningArrayTest { public void testWhenInvalidInoutProvidedShouldThrowException() { var stream = new MedianOfRunningArrayInteger(); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, stream::getMedian); - assertEquals(exception.getMessage(), EXCEPTION_MESSAGE); + assertEquals(EXCEPTION_MESSAGE, exception.getMessage()); } @Test diff --git a/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java b/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java index 915b83e376b6..c1adafa18d9f 100644 --- a/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.misc; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,7 +68,7 @@ void testShuffleRetainsElements() { ShuffleArray.shuffle(arr); // Check that the shuffled array contains the same elements - assertTrue(arr.length == 5); + assertEquals(5, arr.length); for (int i = 1; i <= 5; i++) { assertTrue(contains(arr, i)); } 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: + *

    + *
  • D1: {@code i % 2 == 0} - whether the digit at index i is doubled
  • + *
  • D2: {@code temp > 9} - whether 9 is subtracted from the doubled digit
  • + *
+ * 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/others/NewManShanksPrimeTest.java b/src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java deleted file mode 100644 index 3b657e441b1c..000000000000 --- a/src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.thealgorithms.others; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.thealgorithms.dynamicprogramming.NewManShanksPrime; -import org.junit.jupiter.api.Test; - -public class NewManShanksPrimeTest { - - @Test - void testOne() { - assertTrue(NewManShanksPrime.nthManShanksPrime(1, 1)); - } - - @Test - void testTwo() { - assertTrue(NewManShanksPrime.nthManShanksPrime(2, 3)); - } - - @Test - void testThree() { - assertTrue(NewManShanksPrime.nthManShanksPrime(3, 7)); - } - - @Test - void testFour() { - assertTrue(NewManShanksPrime.nthManShanksPrime(4, 17)); - } - - @Test - void testFive() { - assertTrue(NewManShanksPrime.nthManShanksPrime(5, 41)); - } - - @Test - void testSix() { - assertTrue(NewManShanksPrime.nthManShanksPrime(6, 99)); - } - - @Test - void testSeven() { - assertTrue(NewManShanksPrime.nthManShanksPrime(7, 239)); - } - - @Test - void testEight() { - assertTrue(NewManShanksPrime.nthManShanksPrime(8, 577)); - } -} diff --git a/src/test/java/com/thealgorithms/others/PasswordGenTest.java b/src/test/java/com/thealgorithms/others/PasswordGenTest.java index 76492556e75f..4dcdf6b9cf4f 100644 --- a/src/test/java/com/thealgorithms/others/PasswordGenTest.java +++ b/src/test/java/com/thealgorithms/others/PasswordGenTest.java @@ -17,7 +17,7 @@ public void failGenerationWithSameMinMaxLengthTest() { @Test public void generateOneCharacterPassword() { String tempPassword = PasswordGen.generatePassword(1, 2); - assertTrue(tempPassword.length() == 1); + assertEquals(1, tempPassword.length()); } @Test diff --git a/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java b/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java deleted file mode 100644 index 669f928cd247..000000000000 --- a/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.thealgorithms.others.cn; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -public class HammingDistanceTest { - @Test - public void checkForDifferentBits() { - int answer = HammingDistance.compute("000", "011"); - Assertions.assertThat(answer).isEqualTo(2); - } - - /* - - 1 0 1 0 1 - 1 1 1 1 0 - ---------- - 0 1 0 1 1 - - - */ - @Test - public void checkForDifferentBitsLength() { - int answer = HammingDistance.compute("10101", "11110"); - Assertions.assertThat(answer).isEqualTo(3); - } - - @Test - public void checkForSameBits() { - String someBits = "111"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForLongDataBits() { - int answer = HammingDistance.compute("10010101101010000100110100", "00110100001011001100110101"); - Assertions.assertThat(answer).isEqualTo(7); - } - - @Test - public void mismatchDataBits() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("100010", "00011"); }); - - Assertions.assertThat(ex.getMessage()).contains("must have the same length"); - } - - @Test - public void mismatchDataBits2() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("1", "11"); }); - - Assertions.assertThat(ex.getMessage()).contains("must have the same length"); - } - - @Test - public void checkForLongDataBitsSame() { - String someBits = "10010101101010000100110100"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForEmptyInput() { - String someBits = ""; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForInputOfLength1() { - String someBits = "0"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void computeThrowsExceptionWhenInputsAreNotBitStrs() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("1A", "11"); }); - - Assertions.assertThat(ex.getMessage()).contains("must be a binary string"); - } -} diff --git a/src/test/java/com/thealgorithms/physics/RelativityTest.java b/src/test/java/com/thealgorithms/physics/RelativityTest.java new file mode 100644 index 000000000000..44c17bdbd40f --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/RelativityTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Relativity utility class. + */ +final class RelativityTest { + + // A small tolerance (delta) for comparing floating-point numbers + private static final double DELTA = 1e-6; + private static final double C = Relativity.SPEED_OF_LIGHT; + + @Test + @DisplayName("Test the gamma parameter") + void testGamma() { + double myGamma = Relativity.gamma(0.6 * C); + assertEquals(1.25, myGamma, DELTA); + } + + @Test + @DisplayName("Test the length contraction") + void testLengthContraction() { + double myLength = Relativity.lengthContraction(5.0, 0.8 * C); + assertEquals(3.0, myLength, DELTA); + } + + @Test + @DisplayName("Test the time dilation") + void testTimeDilation() { + double myTime = Relativity.timeDilation(4.0, 0.6 * C); + assertEquals(5.0, myTime, DELTA); + } + + @Test + @DisplayName("Test the velocity addition in the same direction") + void testVelocityAdditionSameDirection() { + double myVelocity = Relativity.velocityAddition(0.8 * C, 0.75 * C); + assertEquals(0.125 * C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test the velocity addition in different directions") + void testVelocityAdditionDifferentDirections() { + double myVelocity = Relativity.velocityAddition(0.8 * C, -0.75 * C); + assertEquals(0.96875 * C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test the velocity addition with the speed of light") + void testVelocityAdditionWithSpeedOfLight() { + double myVelocity = Relativity.velocityAddition(C, 0.7 * C); + assertEquals(C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test invalid inputs throw exception") + void testInvalidOrbitalVelocityInputs() { + assertThrows(IllegalArgumentException.class, () -> Relativity.gamma(1.2 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.gamma(-C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.lengthContraction(-1.0, 0.6 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.lengthContraction(1.0, 1.5 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.timeDilation(-5.0, -0.8 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.timeDilation(5.0, C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(0.3 * C, -C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(1.4 * C, 0.2 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(-0.4 * C, 1.2 * C)); + } +} diff --git a/src/test/java/com/thealgorithms/physics/SnellLawTest.java b/src/test/java/com/thealgorithms/physics/SnellLawTest.java new file mode 100644 index 000000000000..ddd5fb1d5af7 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/SnellLawTest.java @@ -0,0 +1,41 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class SnellLawTest { + + @Test + public void testRefractedAngle() { + double n1 = 1.0; // air + double n2 = 1.5; // glass + double theta1 = Math.toRadians(30); + + double theta2 = SnellLaw.refractedAngle(n1, n2, theta1); + + double expected = Math.asin(n1 / n2 * Math.sin(theta1)); + + assertEquals(expected, theta2, 1e-12); + } + + @Test + public void testTotalInternalReflection() { + double n1 = 1.5; + double n2 = 1.0; + double theta1 = Math.toRadians(60); // large angle + + assertThrows(IllegalArgumentException.class, () -> SnellLaw.refractedAngle(n1, n2, theta1)); + } + + @Test + public void testNoTotalInternalReflectionAtLowAngles() { + double n1 = 1.5; + double n2 = 1.0; + double theta1 = Math.toRadians(10); + + assertDoesNotThrow(() -> SnellLaw.refractedAngle(n1, n2, theta1)); + } +} diff --git a/src/test/java/com/thealgorithms/physics/ThinLensTest.java b/src/test/java/com/thealgorithms/physics/ThinLensTest.java new file mode 100644 index 000000000000..cf7e94676819 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/ThinLensTest.java @@ -0,0 +1,19 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ThinLensTest { + + @Test + void testConvexLensRealImage() { + double v = ThinLens.imageDistance(10, 20); + assertEquals(20, v, 1e-6); + } + + @Test + void testMagnification() { + assertEquals(2.0, ThinLens.magnification(20, 10), 1e-6); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java b/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java new file mode 100644 index 000000000000..88a480f25f1a --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java @@ -0,0 +1,110 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DifferenceArrayTest { + + @Test + void testStandardRangeUpdate() { + int[] input = {10, 20, 30, 40, 50}; + DifferenceArray da = new DifferenceArray(input); + + da.update(1, 3, 5); + + long[] expected = {10, 25, 35, 45, 50}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testMultipleOverlappingUpdates() { + int[] input = {10, 10, 10, 10, 10}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 2, 10); + da.update(2, 4, 20); + + long[] expected = {20, 20, 40, 30, 30}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testIntegerOverflowSafety() { + int[] input = {Integer.MAX_VALUE, 100}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 0, 100); + + long[] result = da.getResultArray(); + long expectedVal = (long) Integer.MAX_VALUE + 100; + + assertEquals(expectedVal, result[0]); + } + + @Test + void testFullRangeUpdate() { + int[] input = {1, 2, 3}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 2, 100); + + long[] expected = {101, 102, 103}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testBoundaryWriteOptimization() { + int[] input = {5, 5}; + DifferenceArray da = new DifferenceArray(input); + + da.update(1, 1, 5); + + long[] expected = {5, 10}; + + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testLargeMassiveUpdate() { + int[] input = {0}; + DifferenceArray da = new DifferenceArray(input); + + int iterations = 100000; + for (int i = 0; i < iterations; i++) { + da.update(0, 0, 1); + } + + assertEquals(100000L, da.getResultArray()[0]); + } + + @Test + void testNullInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new DifferenceArray(null)); + } + + @Test + void testEmptyInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new DifferenceArray(new int[] {})); + } + + @Test + void testInvalidRangeNegativeIndex() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(-1, 1, 5)); + } + + @Test + void testInvalidRangeOutOfBounds() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(0, 3, 5)); + } + + @Test + void testInvalidRangeStartGreaterThanEnd() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(2, 1, 5)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java b/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java new file mode 100644 index 000000000000..87feff859356 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java @@ -0,0 +1,92 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PrefixSum2DTest { + + @Test + @DisplayName("Test basic 3x3 square matrix") + void testStandardSquare() { + int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of top-left 2x2: {1,2, 4,5} -> 12 + assertEquals(12L, ps.sumRegion(0, 0, 1, 1)); + // Sum of bottom-right 2x2: {5,6, 8,9} -> 28 + assertEquals(28L, ps.sumRegion(1, 1, 2, 2)); + // Full matrix -> 45 + assertEquals(45L, ps.sumRegion(0, 0, 2, 2)); + } + + @Test + @DisplayName("Test rectangular matrix (more cols than rows)") + void testRectangularWide() { + int[][] matrix = {{1, 1, 1, 1}, {2, 2, 2, 2}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of first 3 columns of both rows -> (1*3) + (2*3) = 9 + assertEquals(9L, ps.sumRegion(0, 0, 1, 2)); + } + + @Test + @DisplayName("Test rectangular matrix (more rows than cols)") + void testRectangularTall() { + int[][] matrix = {{1}, {2}, {3}, {4}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of middle two elements -> 2+3 = 5 + assertEquals(5L, ps.sumRegion(1, 0, 2, 0)); + } + + @Test + @DisplayName("Test single element matrix") + void testSingleElement() { + int[][] matrix = {{100}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + assertEquals(100L, ps.sumRegion(0, 0, 0, 0)); + } + + @Test + @DisplayName("Test large numbers for overflow (Integer -> Long)") + void testLargeNumbers() { + // 2 billion. Two of these sum to > MAX_INT + int val = 2_000_000_000; + int[][] matrix = {{val, val}, {val, val}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // 4 * 2B = 8 Billion + assertEquals(8_000_000_000L, ps.sumRegion(0, 0, 1, 1)); + } + + @Test + @DisplayName("Test invalid inputs") + void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(null)); + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {})); // empty + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {{}})); // empty row + } + + @Test + @DisplayName("Test invalid query ranges") + void testInvalidRanges() { + int[][] matrix = {{1, 2}, {3, 4}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Negative indices + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(-1, 0, 0, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, -1, 0, 0)); + + // Out of bounds + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 2, 0)); // row2 too big + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 0, 2)); // col2 too big + + // Inverted ranges (start > end) + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(1, 0, 0, 0)); // row1 > row2 + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 1, 0, 0)); // col1 > col2 + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java b/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java new file mode 100644 index 000000000000..a421b62e9306 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java @@ -0,0 +1,80 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PrefixSumTest { + + @Test + @DisplayName("Test basic sum with positive integers") + void testStandardCase() { + int[] input = {1, 2, 3, 4, 5}; + PrefixSum ps = new PrefixSum(input); + + // Sum of range [0, 4] -> 15 + assertEquals(15L, ps.sumRange(0, 4)); + + // Sum of range [1, 3] -> 9 + assertEquals(9L, ps.sumRange(1, 3)); + } + + @Test + @DisplayName("Test array with negative numbers and zeros") + void testNegativeAndZeros() { + int[] input = {-2, 0, 3, -5, 2, -1}; + PrefixSum ps = new PrefixSum(input); + + assertEquals(1L, ps.sumRange(0, 2)); + assertEquals(-1L, ps.sumRange(2, 5)); + assertEquals(0L, ps.sumRange(1, 1)); + } + + @Test + @DisplayName("Test with large integers to verify overflow handling") + void testLargeNumbers() { + // Two values that fit in int, but their sum exceeds Integer.MAX_VALUE + // Integer.MAX_VALUE is approx 2.14 billion. + int val = 2_000_000_000; + int[] input = {val, val, val}; + PrefixSum ps = new PrefixSum(input); + + // Sum of three 2 billion values is 6 billion (fits in long, overflows int) + assertEquals(6_000_000_000L, ps.sumRange(0, 2)); + } + + @Test + @DisplayName("Test single element array") + void testSingleElement() { + int[] input = {42}; + PrefixSum ps = new PrefixSum(input); + assertEquals(42L, ps.sumRange(0, 0)); + } + + @Test + @DisplayName("Test constructor with null input") + void testNullInput() { + assertThrows(IllegalArgumentException.class, () -> new PrefixSum(null)); + } + + @Test + @DisplayName("Test empty array behavior") + void testEmptyArray() { + int[] input = {}; + PrefixSum ps = new PrefixSum(input); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 0)); + } + + @Test + @DisplayName("Test invalid range indices") + void testInvalidIndices() { + int[] input = {10, 20, 30}; + PrefixSum ps = new PrefixSum(input); + + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(-1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 3)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(2, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java b/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java new file mode 100644 index 000000000000..12072318ac74 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link RangeSumQuery}. + */ +class RangeSumQueryTest { + + @Test + void testBasicExample() { + int[] nums = {1, 2, 3, 4, 5}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(6, RangeSumQuery.sumRange(prefixSum, 0, 2)); // 1+2+3 + assertEquals(9, RangeSumQuery.sumRange(prefixSum, 1, 3)); // 2+3+4 + assertEquals(15, RangeSumQuery.sumRange(prefixSum, 0, 4)); // 1+2+3+4+5 + assertEquals(12, RangeSumQuery.sumRange(prefixSum, 2, 4)); // 3+4+5 + } + + @Test + void testSingleElement() { + int[] nums = {7}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(7, RangeSumQuery.sumRange(prefixSum, 0, 0)); + } + + @Test + void testAllZeros() { + int[] nums = {0, 0, 0, 0}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(0, RangeSumQuery.sumRange(prefixSum, 0, 3)); + assertEquals(0, RangeSumQuery.sumRange(prefixSum, 1, 2)); + } + + @Test + void testNegativeNumbers() { + int[] nums = {-1, 2, -3, 4}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(-2, RangeSumQuery.sumRange(prefixSum, 0, 2)); // -1+2-3 + assertEquals(3, RangeSumQuery.sumRange(prefixSum, 1, 3)); // 2-3+4 + } + + @Test + void testEmptyArrayThrowsException() { + int[] nums = {}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 0, 0)); + } + + @Test + void testNullArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.buildPrefixSum(null)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(null, 0, 0)); + } + + @Test + void testInvalidIndicesThrowsException() { + int[] nums = {1, 2, 3}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, -1, 2)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 1, 5)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 2, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java b/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java new file mode 100644 index 000000000000..68f85b713046 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java @@ -0,0 +1,59 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SubarraySumEqualsK}. + */ +class SubarraySumEqualsKTest { + + @Test + void testBasicExample() { + int[] nums = {1, 1, 1}; + int k = 2; + assertEquals(2, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testWithNegativeNumbers() { + int[] nums = {1, -1, 0}; + int k = 0; + assertEquals(3, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testSingleElementEqualToK() { + int[] nums = {5}; + int k = 5; + assertEquals(1, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testSingleElementNotEqualToK() { + int[] nums = {5}; + int k = 3; + assertEquals(0, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testAllZeros() { + int[] nums = {0, 0, 0}; + int k = 0; + assertEquals(6, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testEmptyArray() { + int[] nums = {}; + int k = 0; + assertEquals(0, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testNullArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> SubarraySumEqualsK.countSubarrays(null, 0)); + } +} diff --git a/src/test/java/com/thealgorithms/puzzlesandgames/SudokuTest.java b/src/test/java/com/thealgorithms/puzzlesandgames/SudokuTest.java deleted file mode 100644 index 7fb96dcf805f..000000000000 --- a/src/test/java/com/thealgorithms/puzzlesandgames/SudokuTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.thealgorithms.puzzlesandgames; - -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 org.junit.jupiter.api.Test; - -public class SudokuTest { - - @Test - void testIsSafe2() { - int[][] board = {{3, 0, 6, 5, 0, 8, 4, 0, 0}, {5, 2, 0, 0, 0, 0, 0, 0, 0}, {0, 8, 7, 0, 0, 0, 0, 3, 1}, {0, 0, 3, 0, 1, 0, 0, 8, 0}, {9, 0, 0, 8, 6, 3, 0, 0, 5}, {0, 5, 0, 0, 9, 0, 6, 0, 0}, {1, 3, 0, 0, 0, 0, 2, 5, 0}, {0, 0, 0, 0, 0, 0, 0, 7, 4}, {0, 0, 5, 2, 0, 6, 3, 0, 0}}; - - assertFalse(Sudoku.isSafe(board, 0, 1, 3)); - assertTrue(Sudoku.isSafe(board, 1, 2, 1)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> { Sudoku.isSafe(board, 10, 10, 5); }); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> { Sudoku.isSafe(board, -1, 0, 5); }); - } - - @Test - void testSolveSudoku() { - int[][] board = {{3, 0, 6, 5, 0, 8, 4, 0, 0}, {5, 2, 0, 0, 0, 0, 0, 0, 0}, {0, 8, 7, 0, 0, 0, 0, 3, 1}, {0, 0, 3, 0, 1, 0, 0, 8, 0}, {9, 0, 0, 8, 6, 3, 0, 0, 5}, {0, 5, 0, 0, 9, 0, 6, 0, 0}, {1, 3, 0, 0, 0, 0, 2, 5, 0}, {0, 0, 0, 0, 0, 0, 0, 7, 4}, {0, 0, 5, 2, 0, 6, 3, 0, 0}}; - - assertTrue(Sudoku.solveSudoku(board, board.length)); - assertEquals(1, board[0][1]); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> { Sudoku.solveSudoku(board, 10); }); - assertTrue(Sudoku.solveSudoku(board, -1)); - } - - @Test - void testUnsolvableSudoku() { - int[][] unsolvableBoard = {{5, 1, 6, 8, 4, 9, 7, 3, 2}, {3, 0, 7, 6, 0, 5, 0, 0, 0}, {8, 0, 9, 7, 0, 0, 0, 6, 5}, {1, 3, 5, 0, 6, 0, 9, 0, 7}, {4, 7, 2, 5, 9, 1, 0, 0, 6}, {9, 6, 8, 3, 7, 0, 0, 5, 0}, {2, 5, 3, 1, 8, 6, 0, 7, 4}, {6, 8, 4, 2, 5, 7, 3, 9, 0}, {7, 9, 1, 4, 3, 0, 5, 0, 0}}; - - assertFalse(Sudoku.solveSudoku(unsolvableBoard, unsolvableBoard.length)); - } -} diff --git a/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java b/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java index 42669eb03bb4..f0a2686d3e4b 100644 --- a/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java +++ b/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java @@ -1,14 +1,31 @@ package com.thealgorithms.puzzlesandgames; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class TowerOfHanoiTest { + @ParameterizedTest + @MethodSource("diskCountAndMoveCount") + void testMoveCountMatchesFormula(int disks, int expectedMoves) { + List result = new ArrayList<>(); + TowerOfHanoi.shift(disks, "A", "B", "C", result); + assertEquals(expectedMoves, result.size()); + } + + private static Stream diskCountAndMoveCount() { + return Stream.of(Arguments.of(1, 1), Arguments.of(2, 3), Arguments.of(3, 7), Arguments.of(4, 15), Arguments.of(5, 31), Arguments.of(10, 1023)); + } + @Test public void testHanoiWithOneDisc() { List result = new ArrayList<>(); @@ -39,6 +56,15 @@ public void testHanoiWithThreeDiscs() { assertEquals(expected, result); } + @Test + public void testHanoiWithDifferentPoles() { + List result = new ArrayList<>(); + TowerOfHanoi.shift(2, "X", "Y", "Z", result); + + List expected = List.of("Move 1 from X to Y", "Move 2 from X to Z", "Move 1 from Y to Z"); + assertEquals(expected, result); + } + @Test public void testHanoiWithZeroDiscs() { List result = new ArrayList<>(); @@ -47,4 +73,10 @@ public void testHanoiWithZeroDiscs() { // There should be no moves if there are 0 discs assertTrue(result.isEmpty()); } + + @Test + public void testHanoiWithNegativeDiscsThrows() { + List result = new ArrayList<>(); + assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.shift(-1, "Pole1", "Pole2", "Pole3", result)); + } } diff --git a/src/test/java/com/thealgorithms/maths/FactorialRecursionTest.java b/src/test/java/com/thealgorithms/recursion/FactorialRecursionTest.java similarity index 96% rename from src/test/java/com/thealgorithms/maths/FactorialRecursionTest.java rename to src/test/java/com/thealgorithms/recursion/FactorialRecursionTest.java index db18b46356b4..198fcd558f63 100644 --- a/src/test/java/com/thealgorithms/maths/FactorialRecursionTest.java +++ b/src/test/java/com/thealgorithms/recursion/FactorialRecursionTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.maths; +package com.thealgorithms.recursion; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java index 18f0afc6a0a6..dec2c86de9c7 100644 --- a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java +++ b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java @@ -117,7 +117,7 @@ public void binarySearch2dArrayTestTargetInMiddle() { int target = 8; // Assert that the requirement, that the target is in the middle row and middle column, is // fulfilled. - assertEquals(arr[arr.length / 2][arr[0].length / 2], target); + assertEquals(target, arr[arr.length / 2][arr[0].length / 2]); int[] ans = BinarySearch2dArray.binarySearch(arr, target); System.out.println(Arrays.toString(ans)); assertEquals(1, ans[0]); @@ -135,8 +135,8 @@ public void binarySearch2dArrayTestTargetAboveMiddleRowInMiddleColumn() { // Assert that the requirement, that he target is in the middle column, // in an array with an even number of columns, and on the row "above" the middle row. - assertEquals(arr[0].length % 2, 0); - assertEquals(arr[arr.length / 2 - 1][arr[0].length / 2], target); + assertEquals(0, arr[0].length % 2); + assertEquals(target, arr[arr.length / 2 - 1][arr[0].length / 2]); int[] ans = BinarySearch2dArray.binarySearch(arr, target); System.out.println(Arrays.toString(ans)); assertEquals(0, ans[0]); diff --git a/src/test/java/com/thealgorithms/searches/BinarySearchTest.java b/src/test/java/com/thealgorithms/searches/BinarySearchTest.java index bd4620a7fa7d..00bed165734e 100644 --- a/src/test/java/com/thealgorithms/searches/BinarySearchTest.java +++ b/src/test/java/com/thealgorithms/searches/BinarySearchTest.java @@ -105,4 +105,90 @@ void testBinarySearchLargeArray() { int expectedIndex = 9999; // Index of the last element assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * Test for binary search with null array. + */ + @Test + void testBinarySearchNullArray() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = null; + int key = 5; // Key to search + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in a null array."); + } + + /** + * Test for binary search with duplicate elements. + */ + @Test + void testBinarySearchWithDuplicates() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {1, 2, 2, 2, 3}; + int key = 2; // Element present multiple times + + int result = binarySearch.find(array, key); + assertEquals(2, array[result], "The returned index should contain the searched element."); + } + + /** + * Test for binary search where all elements are the same. + */ + @Test + void testBinarySearchAllElementsSame() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {5, 5, 5, 5, 5}; + int key = 5; // All elements match + + int result = binarySearch.find(array, key); + assertEquals(5, array[result], "The returned index should contain the searched element."); + } + + /** + * Test for binary search with negative numbers. + */ + @Test + void testBinarySearchNegativeNumbers() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {-10, -5, 0, 5, 10}; + int key = -5; // Element present + int expectedIndex = 1; // Index of the element + assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the element should be 1."); + } + + /** + * Test for binary search when key is smaller than all elements. + */ + @Test + void testBinarySearchKeySmallerThanAll() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {10, 20, 30}; + int key = 5; // Smaller than all elements + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for binary search when key is larger than all elements. + */ + @Test + void testBinarySearchKeyLargerThanAll() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {10, 20, 30}; + int key = 40; // Larger than all elements + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for binary search with String array. + */ + @Test + void testBinarySearchStrings() { + BinarySearch binarySearch = new BinarySearch(); + String[] array = {"apple", "banana", "cherry", "date"}; + String key = "cherry"; // Element present + int expectedIndex = 2; // Index of the element + assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the element should be 2."); + } } 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 c84da531e8a4..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. */ @@ -81,4 +112,68 @@ void testExponentialSearchLargeArray() { int expectedIndex = 9999; assertEquals(expectedIndex, exponentialSearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * An element sitting exactly on the doubling boundary used to be reported as missing, because + * the binary search was handed {@code range} as its exclusive upper bound instead of + * {@code range + 1}. + */ + @Test + void testExponentialSearchElementOnRangeBoundary() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {-25, -9, 8, 21}; + assertEquals(2, exponentialSearch.find(array, 8), "The index of the found element should be 2."); + } + + /** + * Every element must be found regardless of the array length. + */ + @Test + void testExponentialSearchFindsEveryElement() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, exponentialSearch.find(array, i * 2), "Element at index " + i + " should be found for length " + length + "."); + } + } + } + + /** + * A missing key has to yield -1 rather than the negative insertion point that + * {@link java.util.Arrays#binarySearch} returns. + */ + @Test + void testExponentialSearchNotFoundReturnsMinusOne() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {1, 3, 5, 7, 9, 11}; + assertEquals(-1, exponentialSearch.find(array, 4), "A key inside the range but absent should give -1."); + 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 801c33b1d09a..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(); @@ -121,4 +130,53 @@ void testFibonacciSearchLargeArray() { int expectedIndex = 9999; assertEquals(expectedIndex, fibonacciSearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * A key greater than every element used to throw {@link ArrayIndexOutOfBoundsException}, + * because the final probe read {@code array[offset + 1]} without checking the bound. + */ + @Test + void testFibonacciSearchKeyGreaterThanLastElement() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i; + } + assertEquals(-1, fibonacciSearch.find(array, length), "A key above the maximum should not be found for length " + length + "."); + } + } + + /** + * The final probe used reference equality, so a key that is equal but not identical to the + * stored element was reported as missing. Values above 127 are outside the {@link Integer} + * cache and therefore are not the same object as the boxed array element. + */ + @Test + void testFibonacciSearchFindsEqualButNotIdenticalKey() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + Integer[] array = {10, 20, 300}; + assertEquals(2, fibonacciSearch.find(array, Integer.valueOf(300)), "The index of the found element should be 2."); + + String[] words = {"a", "b", "c"}; + String equalButDistinct = new StringBuilder("c").toString(); + assertEquals(2, fibonacciSearch.find(words, equalButDistinct), "The index of the found element should be 2."); + } + + /** + * Every element must be found regardless of the array length. + */ + @Test + void testFibonacciSearchFindsEveryElement() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = 1000 + i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, fibonacciSearch.find(array, Integer.valueOf(1000 + i * 2)), "Element at index " + i + " should be found for length " + length + "."); + } + } + } } diff --git a/src/test/java/com/thealgorithms/searches/HowManyTimesRotatedTest.java b/src/test/java/com/thealgorithms/searches/HowManyTimesRotatedTest.java index 7d52e9fb4eca..a212bceeff85 100644 --- a/src/test/java/com/thealgorithms/searches/HowManyTimesRotatedTest.java +++ b/src/test/java/com/thealgorithms/searches/HowManyTimesRotatedTest.java @@ -2,7 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; public class HowManyTimesRotatedTest { @@ -13,4 +15,20 @@ public void testHowManyTimesRotated() { int[] arr2 = {15, 17, 2, 3, 5}; assertEquals(2, HowManyTimesRotated.rotated(arr2)); } + + /** An unrotated (already sorted) array should resolve to 0 rotations without hanging. */ + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + public void testHowManyTimesRotatedOnUnrotatedArray() { + int[] arr = {2, 5, 6, 8, 11, 12, 15, 18}; + assertEquals(0, HowManyTimesRotated.rotated(arr)); + } + + /** Arrays of size 1 and 2 should not throw ArrayIndexOutOfBoundsException. */ + @Test + public void testHowManyTimesRotatedOnSmallArrays() { + assertEquals(0, HowManyTimesRotated.rotated(new int[] {5})); + assertEquals(0, HowManyTimesRotated.rotated(new int[] {1, 2})); + assertEquals(1, HowManyTimesRotated.rotated(new int[] {2, 1})); + } } diff --git a/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java b/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java index b3b7e7ef129c..b7ef64125da8 100644 --- a/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java @@ -87,4 +87,15 @@ void testInterpolationSearchLargeNonUniformArray() { int key = 21; // Present in the array assertEquals(6, interpolationSearch.find(array, key), "The index of the found element should be 6."); } + + /** + * Test for interpolation search with specific sorted arrays that previously caused division by zero. + */ + @Test + void testInterpolationSearchDivisionByZeroEdgeCases() { + InterpolationSearch interpolationSearch = new InterpolationSearch(); + assertEquals(3, interpolationSearch.find(new int[] {0, 0, 0, 2}, 2)); + assertEquals(0, interpolationSearch.find(new int[] {2, 2, 2, 2}, 2)); + assertEquals(3, interpolationSearch.find(new int[] {0, 1, 2, 4}, 4)); + } } diff --git a/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java index b2e121ac1ba0..f291610b298b 100644 --- a/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java +++ b/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java @@ -87,6 +87,26 @@ void testBinarySearchEmptyArray() { assertEquals(-1, binarySearch.find(array, key), "The element should not be found in an empty array."); } + /** + * Test for binary search with a null array. + */ + @Test + void testBinarySearchNullArray() { + IterativeBinarySearch binarySearch = new IterativeBinarySearch(); + Integer key = 1; + assertEquals(-1, binarySearch.find(null, key), "The element should not be found in a null array."); + } + + /** + * Test for binary search with a null key. + */ + @Test + void testBinarySearchNullKey() { + IterativeBinarySearch binarySearch = new IterativeBinarySearch(); + Integer[] array = {1, 2, 4, 8, 16}; + assertEquals(-1, binarySearch.find(array, null), "A null search key should return -1."); + } + /** * Test for binary search on a large array. */ diff --git a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java index 3fa319b66a41..3ecbaeb14dbf 100644 --- a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java @@ -2,7 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; /** * Unit tests for the JumpSearch class. @@ -91,4 +93,86 @@ void testJumpSearchLargeArrayNotFound() { Integer key = 999; // Key not present assertEquals(-1, jumpSearch.find(array, key), "The element should not be found in the array."); } + + /** + * A key greater than every element used to make the jumping loop spin forever, because the + * cursor was clamped to the last index and therefore stopped advancing. + */ + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void testJumpSearchKeyGreaterThanLastElement() { + JumpSearch jumpSearch = new JumpSearch(); + Integer[] array = {1, 2, 3, 4}; + assertEquals(-1, jumpSearch.find(array, 5), "A key above the maximum should not be found."); + } + + /** + * The same regression across several lengths, since the jump size depends on the array length. + */ + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void testJumpSearchKeyGreaterThanLastElementForEveryLength() { + JumpSearch jumpSearch = new JumpSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i; + } + assertEquals(-1, jumpSearch.find(array, length), "A key above the maximum should not be found for length " + length + "."); + } + } + + /** + * Every element must be found regardless of the array length, including the ones that sit + * exactly on a jump boundary. + */ + @Test + void testJumpSearchFindsEveryElement() { + JumpSearch jumpSearch = new JumpSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, jumpSearch.find(array, i * 2), "Element at index " + i + " should be found for length " + length + "."); + } + } + } + + @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/KMPSearchTest.java b/src/test/java/com/thealgorithms/searches/KMPSearchTest.java index cb804ac6a6a3..216c5fcd7d2c 100644 --- a/src/test/java/com/thealgorithms/searches/KMPSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/KMPSearchTest.java @@ -14,7 +14,7 @@ public void kmpSearchTestLast() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 10); + assertEquals(10, value); } @Test @@ -25,7 +25,7 @@ public void kmpSearchTestFront() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 0); + assertEquals(0, value); } @Test @@ -36,7 +36,7 @@ public void kmpSearchTestMiddle() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 4); + assertEquals(4, value); } @Test @@ -47,7 +47,7 @@ public void kmpSearchTestNotFound() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 4); + assertEquals(4, value); } @Test @@ -58,6 +58,6 @@ public void kmpSearchTest4() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, -1); + assertEquals(-1, value); } } diff --git a/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java b/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java index 534c2a4487b2..c0d82489209f 100644 --- a/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java +++ b/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Random; import org.junit.jupiter.api.Test; class LinearSearchThreadTest { @@ -62,10 +63,11 @@ void testSearcherEmptySegment() throws InterruptedException { void testSearcherRandomNumbers() throws InterruptedException { int size = 200; int[] array = new int[size]; + Random random = new Random(); for (int i = 0; i < size; i++) { - array[i] = (int) (Math.random() * 100); + array[i] = random.nextInt(100); } - int target = array[(int) (Math.random() * size)]; // Randomly select a target that is present + final int target = array[random.nextInt(size)]; // Randomly select a target that is present Searcher searcher = new Searcher(array, 0, size, target); searcher.start(); searcher.join(); 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/PerfectBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java deleted file mode 100644 index 6eab20f45467..000000000000 --- a/src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.thealgorithms.searches; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -/** - * @author D Sunil (https://github.com/sunilnitdgp) - * @see PerfectBinarySearch - */ -public class PerfectBinarySearchTest { - - @Test - public void testIntegerBinarySearch() { - Integer[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - PerfectBinarySearch binarySearch = new PerfectBinarySearch<>(); - - // Test cases for elements present in the array - assertEquals(0, binarySearch.find(array, 1)); // First element - assertEquals(4, binarySearch.find(array, 5)); // Middle element - assertEquals(9, binarySearch.find(array, 10)); // Last element - assertEquals(6, binarySearch.find(array, 7)); // Element in the middle - - // Test cases for elements not in the array - assertEquals(-1, binarySearch.find(array, 0)); // Element before the array - assertEquals(-1, binarySearch.find(array, 11)); // Element after the array - assertEquals(-1, binarySearch.find(array, 100)); // Element not in the array - } - - @Test - public void testStringBinarySearch() { - String[] array = {"apple", "banana", "cherry", "date", "fig"}; - PerfectBinarySearch binarySearch = new PerfectBinarySearch<>(); - - // Test cases for elements not in the array - assertEquals(-1, binarySearch.find(array, "apricot")); // Element not in the array - assertEquals(-1, binarySearch.find(array, "bananaa")); // Element not in the array - - // Test cases for elements present in the array - assertEquals(0, binarySearch.find(array, "apple")); // First element - assertEquals(2, binarySearch.find(array, "cherry")); // Middle element - assertEquals(4, binarySearch.find(array, "fig")); // Last element - } -} diff --git a/src/test/java/com/thealgorithms/searches/QuickSelectTest.java b/src/test/java/com/thealgorithms/searches/QuickSelectTest.java index cf160b0ff4b5..4c96be76861a 100644 --- a/src/test/java/com/thealgorithms/searches/QuickSelectTest.java +++ b/src/test/java/com/thealgorithms/searches/QuickSelectTest.java @@ -172,7 +172,7 @@ void quickSelect70thPercentileOfManyElements() { void quickSelectMedianOfThreeCharacters() { List elements = Arrays.asList('X', 'Z', 'Y'); char actual = QuickSelect.select(elements, 1); - assertEquals(actual, 'Y'); + assertEquals('Y', actual); } @Test diff --git a/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java new file mode 100644 index 000000000000..f18a0805eb22 --- /dev/null +++ b/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java @@ -0,0 +1,99 @@ +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; + +class RotatedBinarySearchTest { + + @Test + void shouldFindElementInRotatedArrayLeftSide() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7}; + assertEquals(2, search.find(array, 10)); + } + + @Test + void shouldFindElementInRotatedArrayRightSide() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7}; + assertEquals(6, search.find(array, 2)); + } + + @Test + void shouldFindElementInNotRotatedArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {1, 2, 3, 4, 5, 6, 7}; + assertEquals(4, search.find(array, 5)); + } + + @Test + void shouldReturnMinusOneWhenNotFound() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {4, 5, 6, 7, 0, 1, 2}; + assertEquals(-1, search.find(array, 3)); + } + + @Test + void shouldHandleWhenMiddleIsGreaterThanKeyInRightSortedHalf() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {6, 7, 0, 1, 2, 3, 4, 5}; + assertEquals(2, search.find(array, 0)); + } + + @Test + void shouldHandleDuplicates() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {2, 2, 2, 3, 4, 2}; + int index = search.find(array, 3); + assertTrue(index >= 0 && index < array.length); + assertEquals(3, array[index]); + } + + @Test + void shouldReturnMinusOneForNullArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + assertEquals(-1, search.find(null, 1)); + } + + @Test + void shouldReturnMinusOneForEmptyArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + assertEquals(-1, search.find(new Integer[0], 1)); + } + + @Test + void shouldFindElementInSingleElementArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + assertEquals(0, search.find(new Integer[] {7}, 7)); + } + + @Test + void shouldReturnMinusOneWhenSingleElementArrayDoesNotContainKey() { + RotatedBinarySearch search = new RotatedBinarySearch(); + assertEquals(-1, search.find(new Integer[] {7}, 8)); + } + + @Test + void shouldFindElementAtRotationPivot() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {4, 5, 6, 7, 0, 1, 2}; + assertEquals(4, search.find(array, 0)); + } + + @Test + void shouldFindElementsAtBothEnds() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {4, 5, 6, 7, 0, 1, 2}; + assertEquals(0, search.find(array, 4)); + assertEquals(6, search.find(array, 2)); + } + + @Test + void shouldReturnMinusOneForMissingKeyInAllDuplicatesArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {2, 2, 2, 2, 2, 2, 2, 2}; + assertEquals(-1, search.find(array, 3)); + } +} 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/SortOrderAgnosticBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java deleted file mode 100644 index e2917733d1d9..000000000000 --- a/src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.thealgorithms.searches; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class SortOrderAgnosticBinarySearchTest { - - @Test - public void testAscending() { - int[] arr = {1, 2, 3, 4, 5}; // for ascending order. - int target = 2; - int ans = SortOrderAgnosticBinarySearch.find(arr, target); - int excepted = 1; - assertEquals(excepted, ans); - } - - @Test - public void testDescending() { - int[] arr = {5, 4, 3, 2, 1}; // for descending order. - int target = 2; - int ans = SortOrderAgnosticBinarySearch.find(arr, target); - int excepted = 3; - assertEquals(excepted, ans); - } -} 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/slidingwindow/CountDistinctElementsInWindowTest.java b/src/test/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindowTest.java new file mode 100644 index 000000000000..a6931bca99d2 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindowTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class CountDistinctElementsInWindowTest { + + @Test + public void testBasicCase() { + assertArrayEquals(new int[] {3, 2, 2}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 2, 3}, 3)); + } + + @Test + public void testAllSame() { + assertArrayEquals(new int[] {1, 1, 1}, CountDistinctElementsInWindow.countDistinct(new int[] {2, 2, 2, 2}, 2)); + } + + @Test + public void testAllDistinct() { + assertArrayEquals(new int[] {3, 3}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 4}, 3)); + } + + @Test + public void testWindowSizeEqualsArray() { + assertArrayEquals(new int[] {4}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 4}, 4)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> CountDistinctElementsInWindow.countDistinct(new int[] {}, 2)); + } +} diff --git a/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java b/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java new file mode 100644 index 000000000000..71bf24cc9e30 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java @@ -0,0 +1,55 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class CountNiceSubarraysTest { + @Test + void testExampleCase() { + int[] nums = {1, 1, 2, 1, 1}; + assertEquals(2, CountNiceSubarrays.countNiceSubarrays(nums, 3)); + } + + @Test + void testAllEvenNumbers() { + int[] nums = {2, 4, 6, 8}; + assertEquals(0, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testSingleOdd() { + int[] nums = {1}; + assertEquals(1, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testMultipleChoices() { + int[] nums = {2, 2, 1, 2, 2, 1, 2}; + assertEquals(6, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testTrailingEvenNumbers() { + int[] nums = {1, 2, 2, 2}; + assertEquals(4, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testMultipleWindowShrinks() { + int[] nums = {1, 1, 1, 1}; + assertEquals(3, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testEvensBetweenOdds() { + int[] nums = {2, 1, 2, 1, 2}; + assertEquals(4, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testShrinkWithTrailingEvens() { + int[] nums = {2, 2, 1, 2, 2, 1, 2, 2}; + assertEquals(9, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java b/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java index 60c4bbe9d342..2ee30c44e282 100644 --- a/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java @@ -1,8 +1,17 @@ package com.thealgorithms.sorts; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + public class BitonicSortTest extends SortingAlgorithmTest { @Override SortAlgorithm getSortAlgorithm() { return new BitonicSort(); } + + @Test + void shouldRejectNullArray() { + assertThrows(IllegalArgumentException.class, () -> getSortAlgorithm().sort((Integer[]) null)); + } } diff --git a/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java new file mode 100644 index 000000000000..454d0bd26929 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java @@ -0,0 +1,93 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Arrays; +import java.util.Random; +import org.junit.jupiter.api.Test; + +/** + * JUnit 5 test class for {@link ConcurrentMergeSort}. + */ +public class ConcurrentMergeSortTest { + + @Test + public void testAlreadySortedArray() { + int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Already sorted array should remain unchanged."); + } + + @Test + public void testReverseSortedArray() { + int[] array = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Reverse sorted array should be sorted correctly."); + } + + @Test + public void testIdenticalElementsArray() { + int[] array = {5, 5, 5, 5, 5, 5, 5}; + int[] expected = {5, 5, 5, 5, 5, 5, 5}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Array with identical elements should be sorted correctly (unchanged)."); + } + + @Test + public void testLargeRandomArray() { + int size = 100_000; + int[] array = new int[size]; + int[] expected = new int[size]; + // Using a fixed seed for deterministic testing + Random random = new Random(42); + + for (int i = 0; i < size; i++) { + int value = random.nextInt(); + array[i] = value; + expected[i] = value; + } + + // Generate the expected result using Java's highly optimized built-in sort + Arrays.sort(expected); + + // This will easily trigger the concurrency threshold (8192) in the implementation + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Large random array should be sorted correctly utilizing concurrency."); + } + + @Test + public void testEmptyArray() { + int[] array = {}; + int[] expected = {}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Empty array should be handled without errors."); + } + + @Test + public void testSingleElementArray() { + int[] array = {42}; + int[] expected = {42}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Single element array should be handled without errors."); + } + + @Test + public void testNullArray() { + int[] array = null; + ConcurrentMergeSort.sort(array); + org.junit.jupiter.api.Assertions.assertNull(array, "Null array should be handled without errors."); + } +} 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()) { diff --git a/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java b/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java new file mode 100644 index 000000000000..49783f332e75 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class LibrarySortTest { + + @Test + public void testBasicSort() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 3, 1, 4, 2})); + } + + @Test + public void testAlreadySorted() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {1, 2, 3, 4, 5})); + } + + @Test + public void testReverseSorted() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 4, 3, 2, 1})); + } + + @Test + public void testDuplicates() { + assertArrayEquals(new int[] {1, 2, 2, 3, 3}, LibrarySort.sort(new int[] {3, 2, 1, 3, 2})); + } + + @Test + public void testSingleElement() { + assertArrayEquals(new int[] {1}, LibrarySort.sort(new int[] {1})); + } + + @Test + public void testEmptyArray() { + assertArrayEquals(new int[] {}, LibrarySort.sort(new int[] {})); + } + + @Test + public void testNullArray() { + assertThrows(IllegalArgumentException.class, () -> LibrarySort.sort(null)); + } + + // --- Added to cover branches the tests above never reach --- + + @Test + public void testShiftLeftWhenRightSideIsFull() { + // Right side of the target slot is completely occupied, forcing a left shift. + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6}, LibrarySort.sort(new int[] {0, 1, 2, 6, 4, 5, 3})); + } + + @Test + public void testTieBreakPrefersRightWhenDistancesEqual() { + // A gap exists on both sides at equal distance; algorithm should favor the right shift. + assertArrayEquals(new int[] {0, 1, 2, 3}, LibrarySort.sort(new int[] {0, 1, 3, 2})); + } + + @Test + public void testRightSearchRunsOffTheEnd() { + // No gap anywhere to the right of the target slot, all the way to the array's end. + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 7, 6})); + } + + @Test + public void testInsertAtEndWithNoTrailingGap() { + // A new global maximum arrives with no trailing gap left, forcing insertAtEnd(). + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 6, 7})); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java b/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java new file mode 100644 index 000000000000..8df0502e80e7 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java @@ -0,0 +1,8 @@ +package com.thealgorithms.sorts; + +public class SmoothSortTest extends SortingAlgorithmTest { + @Override + SortAlgorithm getSortAlgorithm() { + return new SmoothSort(); + } +} 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)); + } } diff --git a/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java b/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java index d5588b2b968e..e19f5b928263 100644 --- a/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java @@ -58,7 +58,7 @@ public void failureTest() { Exception exception = assertThrows(RuntimeException.class, () -> TopologicalSort.sort(graph)); String expected = "This graph contains a cycle. No linear ordering is possible. " + "Back edge: 6 -> 2"; - assertEquals(exception.getMessage(), expected); + assertEquals(expected, exception.getMessage()); } @Test void testEmptyGraph() { diff --git a/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java b/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java new file mode 100644 index 000000000000..91da746447a8 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java @@ -0,0 +1,19 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class TournamentSortTest extends SortingAlgorithmTest { + + @Test + void shouldAcceptWhenNullArrayIsPassed() { + Integer[] array = null; + assertNull(getSortAlgorithm().sort(array)); + } + + @Override + SortAlgorithm getSortAlgorithm() { + return new TournamentSort(); + } +} 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 = {}; diff --git a/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java b/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java index c5d57d63cf38..0d8b6acf9043 100644 --- a/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.sorts; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Arrays; import org.junit.jupiter.api.Test; @@ -70,4 +71,22 @@ void wiggleTestStrings() { wiggleSort.sort(values); assertArrayEquals(values, result); } + + @Test + void wiggleTestNonWiggleSortableOddArrayThrows() { + // [1, 2, 2] is not wiggle-sortable: the median 2 appears ceil(3 / 2) = 2 times + // but is not the smallest value, so sorting must fail instead of returning + // a wrongly ordered array + WiggleSort wiggleSort = new WiggleSort(); + Integer[] values = {1, 2, 2}; + assertThrows(IllegalArgumentException.class, () -> wiggleSort.sort(values)); + } + + @Test + void wiggleTestTooManyDuplicatesThrows() { + // more than half of the values are the same, which can never be wiggle-sorted + WiggleSort wiggleSort = new WiggleSort(); + Integer[] values = {2, 2, 2, 1}; + assertThrows(IllegalArgumentException.class, () -> wiggleSort.sort(values)); + } } diff --git a/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java b/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java new file mode 100644 index 000000000000..2e4ea74691da --- /dev/null +++ b/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.stacks; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class StockSpanProblemTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testCalculateSpan(int[] prices, int[] expectedSpans) { + assertArrayEquals(expectedSpans, StockSpanProblem.calculateSpan(prices)); + } + + private static Stream validTestCases() { + return Stream.of(Arguments.of(new int[] {10, 4, 5, 90, 120, 80}, new int[] {1, 1, 2, 4, 5, 1}), Arguments.of(new int[] {100, 50, 60, 70, 80, 90}, new int[] {1, 1, 2, 3, 4, 5}), Arguments.of(new int[] {5, 4, 3, 2, 1}, new int[] {1, 1, 1, 1, 1}), + Arguments.of(new int[] {1, 2, 3, 4, 5}, new int[] {1, 2, 3, 4, 5}), Arguments.of(new int[] {10, 20, 30, 40, 50}, new int[] {1, 2, 3, 4, 5}), Arguments.of(new int[] {100, 80, 60, 70, 60, 75, 85}, new int[] {1, 1, 1, 2, 1, 4, 6}), + Arguments.of(new int[] {7, 7, 7, 7}, new int[] {1, 2, 3, 4}), Arguments.of(new int[] {}, new int[] {}), Arguments.of(new int[] {42}, new int[] {1})); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testCalculateSpanInvalidInput(int[] prices) { + assertThrows(IllegalArgumentException.class, () -> StockSpanProblem.calculateSpan(prices)); + } + + private static Stream invalidTestCases() { + return Stream.of(Arguments.of((int[]) null)); + } +} diff --git a/src/test/java/com/thealgorithms/stacks/ValidParenthesesTest.java b/src/test/java/com/thealgorithms/stacks/ValidParenthesesTest.java new file mode 100644 index 000000000000..39014780caa9 --- /dev/null +++ b/src/test/java/com/thealgorithms/stacks/ValidParenthesesTest.java @@ -0,0 +1,32 @@ +package com.thealgorithms.stacks; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ValidParenthesesTest { + + @Test + void testValidParentheses() { + assertTrue(ValidParentheses.isValid("()")); + assertTrue(ValidParentheses.isValid("()[]{}")); + assertTrue(ValidParentheses.isValid("{[]}")); + assertTrue(ValidParentheses.isValid("")); + } + + @Test + void testInvalidParentheses() { + assertFalse(ValidParentheses.isValid("(]")); + assertFalse(ValidParentheses.isValid("([)]")); + assertFalse(ValidParentheses.isValid("{{{")); + assertFalse(ValidParentheses.isValid("}")); + assertFalse(ValidParentheses.isValid("(")); + } + + @Test + void testNullAndOddLength() { + assertFalse(ValidParentheses.isValid(null)); + assertFalse(ValidParentheses.isValid("(()")); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/AdwinTest.java b/src/test/java/com/thealgorithms/streaming/AdwinTest.java new file mode 100644 index 000000000000..0208f32cdaaf --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/AdwinTest.java @@ -0,0 +1,212 @@ +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.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 AdwinTest { + + private static double mean(double[] values) { + double sum = 0.0; + for (double value : values) { + sum += value; + } + return sum / values.length; + } + + private static double variance(double[] values) { + double mean = mean(values); + double sum = 0.0; + for (double value : values) { + sum += (value - mean) * (value - mean); + } + return sum / values.length; + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 1.0, -0.5, 2.0, Double.NaN}) + void rejectsInvalidDeltas(double delta) { + assertThrows(IllegalArgumentException.class, () -> new Adwin(delta)); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + Adwin window = new Adwin(); + assertThrows(IllegalArgumentException.class, () -> window.accept(value)); + } + + @Test + void startsEmpty() { + Adwin window = new Adwin(); + + assertEquals(0, window.width()); + assertEquals(0.0, window.estimate()); + assertEquals(0.0, window.variance()); + assertEquals(0, window.bucketCount()); + assertEquals(0, window.changeCount()); + assertEquals(0.002, window.delta()); + } + + @Test + @DisplayName("a constant stream is never cut and the window holds all of it") + void keepsEverythingOnAConstantStream() { + Adwin window = new Adwin(); + + for (int i = 0; i < 1000; i++) { + assertFalse(window.accept(3.0)); + } + + assertEquals(1000, window.width()); + assertEquals(1000, window.count()); + assertEquals(3.0, window.estimate()); + assertEquals(0.0, window.variance()); + assertEquals(0, window.changeCount()); + } + + @Test + @DisplayName("the histogram reproduces the mean and the variance of the window exactly") + void agreesWithADirectComputation() { + Adwin window = new Adwin(); + Random random = new Random(11L); + double[] data = new double[2000]; + + for (int i = 0; i < data.length; i++) { + data[i] = random.nextGaussian(); + assertFalse(window.accept(data[i]), "stationary noise must not be cut, sample " + i); + } + + assertEquals(data.length, window.width()); + assertEquals(mean(data), window.estimate(), 1e-9); + assertEquals(variance(data), window.variance(), 1e-9); + } + + @Test + @DisplayName("a step in the level cuts the window quickly") + void findsAStep() { + Adwin window = new Adwin(); + Random random = new Random(5L); + + int firstCut = -1; + for (int i = 0; i < 1200; i++) { + double sample = 0.1 * random.nextGaussian() + (i < 1000 ? 0.0 : 1.0); + boolean cut = window.accept(sample); + if (cut && firstCut < 0) { + firstCut = i; + } + } + + assertTrue(firstCut >= 1000, "the window must not be cut before the step, cut at " + firstCut); + assertTrue(firstCut < 1050, "the step should be found quickly, cut at " + firstCut); + assertEquals(200, window.width(), "the window should hold exactly the samples after the step"); + assertEquals(1.0, window.estimate(), 0.05); + } + + @Test + @DisplayName("the window follows a level that changes several times") + void tracksSeveralRegimes() { + Adwin window = new Adwin(); + Random random = new Random(23L); + + double[] levels = {0.0, 5.0, -3.0, 10.0}; + for (double level : levels) { + for (int i = 0; i < 400; i++) { + window.accept(level + 0.2 * random.nextGaussian()); + } + assertEquals(level, window.estimate(), 0.1, "the estimate should have caught up with " + level); + } + + assertTrue(window.changeCount() > 0); + } + + @Test + @DisplayName("the window never holds more than the stream") + void theWidthNeverExceedsTheCount() { + Adwin window = new Adwin(); + Random random = new Random(31L); + + for (int i = 0; i < 500; i++) { + window.accept(random.nextGaussian() + i / 100.0); + assertTrue(window.width() <= window.count()); + assertTrue(window.width() >= 1); + } + } + + @Test + @DisplayName("the number of buckets grows like the logarithm of the width") + void keepsALogarithmicNumberOfBuckets() { + Adwin window = new Adwin(); + for (int i = 0; i < 5000; i++) { + window.accept(1.0); + } + + int levels = 64 - Long.numberOfLeadingZeros(window.width()); + int bound = (Adwin.MAX_BUCKETS + 1) * (levels + 1); + + assertEquals(5000, window.width()); + assertTrue(window.bucketCount() <= bound, "held " + window.bucketCount() + " buckets, bound was " + bound); + } + + @Test + @DisplayName("a smaller delta cuts less eagerly") + void deltaControlsHowEagerlyTheWindowIsCut() { + Adwin eager = new Adwin(0.5); + Adwin careful = new Adwin(1e-9); + Random random = new Random(41L); + + for (int i = 0; i < 600; i++) { + double sample = random.nextGaussian() + (i < 300 ? 0.0 : 0.3); + eager.accept(sample); + careful.accept(sample); + } + + assertTrue(eager.changeCount() >= careful.changeCount(), "eager cut " + eager.changeCount() + ", careful cut " + careful.changeCount()); + } + + @Test + void scanReportsOneVerdictPerSample() { + Adwin window = new Adwin(); + + boolean[] cuts = window.scan(new double[] {1.0, 1.0, 1.0, 1.0}); + + assertEquals(4, cuts.length); + assertEquals(4, window.count()); + } + + @Test + void resetEmptiesTheWindow() { + Adwin window = new Adwin(); + Random random = new Random(3L); + for (int i = 0; i < 300; i++) { + window.accept(random.nextGaussian()); + } + + window.reset(); + + assertEquals(0, window.width()); + assertEquals(0, window.count()); + assertEquals(0, window.changeCount()); + assertEquals(0, window.bucketCount()); + assertEquals(0.0, window.estimate()); + assertFalse(window.accept(1.0)); + assertEquals(1.0, window.estimate()); + } + + @Test + void toStringMentionsTheState() { + Adwin window = new Adwin(); + window.accept(2.0); + + String text = window.toString(); + + assertTrue(text.contains("Adwin")); + assertTrue(text.contains("width=1")); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/ComplementaryFilterTest.java b/src/test/java/com/thealgorithms/streaming/ComplementaryFilterTest.java new file mode 100644 index 000000000000..0dc1db1a970d --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/ComplementaryFilterTest.java @@ -0,0 +1,258 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 ComplementaryFilterTest { + + private static final double COEFFICIENT = 0.98; + private static final double INTERVAL = 0.01; + + private static double standardDeviation(double[] values, double around) { + double sum = 0.0; + for (double value : values) { + sum += (value - around) * (value - around); + } + return Math.sqrt(sum / values.length); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 1.0, -0.5, 1.5, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsACoefficientOutsideTheUnitInterval(double coefficient) { + assertThrows(IllegalArgumentException.class, () -> new ComplementaryFilter(coefficient)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidTimeConstants(double value) { + assertThrows(IllegalArgumentException.class, () -> ComplementaryFilter.ofTimeConstant(value, INTERVAL)); + assertThrows(IllegalArgumentException.class, () -> ComplementaryFilter.ofTimeConstant(0.5, value)); + assertThrows(IllegalArgumentException.class, () -> new ComplementaryFilter().timeConstant(value)); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteReadings(double value) { + ComplementaryFilter filter = new ComplementaryFilter(); + + assertThrows(IllegalArgumentException.class, () -> filter.accept(value, 0.0)); + assertThrows(IllegalArgumentException.class, () -> filter.accept(0.0, value)); + assertThrows(IllegalArgumentException.class, () -> filter.reset(value)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsAnInvalidElapsedTime(double elapsed) { + ComplementaryFilter filter = new ComplementaryFilter(); + + assertThrows(IllegalArgumentException.class, () -> filter.accept(0.0, 0.0, elapsed)); + } + + @Test + void rejectsMismatchedRecordings() { + ComplementaryFilter filter = new ComplementaryFilter(); + + assertThrows(IllegalArgumentException.class, () -> filter.scan(new double[] {1.0, 2.0}, new double[] {1.0})); + } + + @Test + void exposesItsConfiguration() { + ComplementaryFilter filter = new ComplementaryFilter(0.9); + + assertEquals(0.9, filter.coefficient()); + assertEquals(0.98, new ComplementaryFilter().coefficient()); + assertEquals(0, filter.count()); + assertEquals(0.0, filter.value()); + } + + @Test + @DisplayName("the first pair is answered with the reference, because there is nothing to integrate yet") + void seedsWithTheFirstReference() { + ComplementaryFilter filter = new ComplementaryFilter(); + + assertEquals(7.5, filter.accept(100.0, 7.5), 1e-12); + assertEquals(1, filter.count()); + } + + @Test + @DisplayName("the coefficient and the time constant are two ways of saying the same thing") + void theTimeConstantRoundTrips() { + ComplementaryFilter filter = ComplementaryFilter.ofTimeConstant(0.5, INTERVAL); + + assertEquals(0.5, filter.timeConstant(INTERVAL), 1e-12); + assertEquals(0.5 / (0.5 + INTERVAL), filter.coefficient(), 1e-12); + assertEquals(COEFFICIENT * INTERVAL / (1 - COEFFICIENT), new ComplementaryFilter(COEFFICIENT).timeConstant(INTERVAL), 1e-12); + } + + @Test + @DisplayName("with nothing to integrate the estimate decays onto the reference geometrically") + void convergesOnTheReference() { + ComplementaryFilter filter = new ComplementaryFilter(COEFFICIENT); + filter.reset(10.0); + + for (int step = 1; step <= 200; step++) { + double value = filter.accept(0.0, 0.0); + assertEquals(10.0 * Math.pow(COEFFICIENT, step), value, 1e-9, "step " + step); + } + } + + @Test + @DisplayName("a biased rate leaves a bounded error of tau times the bias, where integration alone would run away") + void boundsTheDriftOfTheRate() { + double bias = 0.1; + ComplementaryFilter filter = new ComplementaryFilter(COEFFICIENT); + filter.reset(0.0); + + double integrated = 0.0; + for (int step = 0; step < 5000; step++) { + filter.accept(bias, 0.0, INTERVAL); + integrated += bias * INTERVAL; + } + + double expected = filter.timeConstant(INTERVAL) * bias; + assertEquals(expected, filter.value(), 1e-9, "the steady state error must be exactly tau * bias"); + assertEquals(0.049, filter.value(), 1e-6); + assertEquals(5.0, integrated, 1e-9, "plain integration of the same bias walks away"); + } + + @Test + @DisplayName("noise on the reference is cut down, roughly by the factor the theory promises") + void rejectsNoiseOnTheReference() { + Random random = new Random(7L); + ComplementaryFilter filter = new ComplementaryFilter(COEFFICIENT); + double[] references = new double[20000]; + double[] estimates = new double[references.length]; + + for (int i = 0; i < references.length; i++) { + references[i] = random.nextGaussian(); + estimates[i] = filter.accept(0.0, references[i], INTERVAL); + } + + double referenceSpread = standardDeviation(references, 0.0); + double estimateSpread = standardDeviation(estimates, 0.0); + double promised = Math.sqrt((1 - COEFFICIENT) / (1 + COEFFICIENT)); + + assertEquals(promised, estimateSpread / referenceSpread, 0.02, "the spread should shrink by sqrt((1-a)/(1+a))"); + } + + @Test + @DisplayName("a moving signal is followed without lag, which a low pass on the reference alone cannot do") + void followsARampWithoutLag() { + Random random = new Random(11L); + double slope = 1.0; + ComplementaryFilter filter = new ComplementaryFilter(COEFFICIENT); + + double lowPass = 0.0; + double filterError = 0.0; + double lowPassError = 0.0; + int steps = 4000; + + for (int i = 0; i < steps; i++) { + double truth = slope * i * INTERVAL; + double reference = truth + 0.05 * random.nextGaussian(); + double estimate = filter.accept(slope, reference, INTERVAL); + lowPass = i == 0 ? reference : COEFFICIENT * lowPass + (1 - COEFFICIENT) * reference; + + if (i > steps / 2) { + filterError += Math.abs(estimate - truth); + lowPassError += Math.abs(lowPass - truth); + } + } + + int counted = steps - steps / 2 - 1; + double filterMean = filterError / counted; + double lowPassMean = lowPassError / counted; + + assertTrue(filterMean < 0.02, "the complementary filter should sit on the ramp, it was off by " + filterMean); + assertEquals(filter.timeConstant(INTERVAL) * slope, lowPassMean, 0.02, "the low pass should lag by tau * slope"); + assertTrue(lowPassMean > 10 * filterMean, "the lag should dwarf the error of the complementary filter"); + } + + @Test + @DisplayName("the coefficient decides how quickly the reference takes over") + void theCoefficientDecidesWhichSensorWins() { + ComplementaryFilter trustsTheRate = new ComplementaryFilter(0.999); + ComplementaryFilter trustsTheReference = new ComplementaryFilter(0.001); + trustsTheRate.reset(0.0); + trustsTheReference.reset(0.0); + + for (int i = 0; i < 100; i++) { + trustsTheRate.accept(0.0, 100.0, INTERVAL); + trustsTheReference.accept(0.0, 100.0, INTERVAL); + } + + assertEquals(100.0 * (1 - Math.pow(0.999, 100)), trustsTheRate.value(), 1e-9); + assertTrue(trustsTheRate.value() < 10.0, "a second is a tenth of its time constant, so it has barely moved"); + assertEquals(100.0, trustsTheReference.value(), 1e-9, "the other one is on the reference from the first sample"); + assertTrue(trustsTheRate.timeConstant(INTERVAL) > 1000 * trustsTheReference.timeConstant(INTERVAL)); + } + + @Test + @DisplayName("the elapsed time scales how much of the rate is taken in") + void theElapsedTimeScalesTheIntegration() { + ComplementaryFilter fast = new ComplementaryFilter(COEFFICIENT); + ComplementaryFilter slow = new ComplementaryFilter(COEFFICIENT); + fast.reset(0.0); + slow.reset(0.0); + + fast.accept(2.0, 0.0, 0.5); + slow.accept(2.0, 0.0, 1.0); + + assertEquals(COEFFICIENT * 1.0, fast.value(), 1e-12); + assertEquals(COEFFICIENT * 2.0, slow.value(), 1e-12); + } + + @Test + void scanReportsOneEstimatePerSample() { + ComplementaryFilter filter = new ComplementaryFilter(); + + double[] estimates = filter.scan(new double[] {0.0, 0.0, 0.0}, new double[] {1.0, 1.0, 1.0}); + + assertEquals(3, estimates.length); + assertEquals(3, filter.count()); + assertEquals(1.0, estimates[2], 1e-12); + } + + @Test + void resetForgetsEverything() { + ComplementaryFilter filter = new ComplementaryFilter(); + filter.scan(new double[] {1.0, 1.0}, new double[] {5.0, 5.0}); + + filter.reset(); + + assertEquals(0, filter.count()); + assertEquals(0.0, filter.value()); + assertEquals(9.0, filter.accept(100.0, 9.0), 1e-12, "the next reference seeds the estimate again"); + } + + @Test + @DisplayName("restarting from a known estimate keeps it instead of waiting for the next reference") + void resetCanCarryOnFromAValue() { + ComplementaryFilter filter = new ComplementaryFilter(COEFFICIENT); + + filter.reset(4.0); + + assertEquals(4.0, filter.value(), 1e-12); + assertEquals(1, filter.count()); + assertEquals(COEFFICIENT * 4.0, filter.accept(0.0, 0.0), 1e-12); + } + + @Test + void toStringMentionsTheState() { + ComplementaryFilter filter = new ComplementaryFilter(0.9); + filter.accept(0.0, 2.0); + + String text = filter.toString(); + + assertTrue(text.contains("ComplementaryFilter")); + assertTrue(text.contains("coefficient=0.9")); + assertTrue(text.contains("samples=1")); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/CusumDetectorTest.java b/src/test/java/com/thealgorithms/streaming/CusumDetectorTest.java new file mode 100644 index 000000000000..996d71d219bc --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/CusumDetectorTest.java @@ -0,0 +1,190 @@ +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.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 CusumDetectorTest { + + /** + * Index of the first alarm in a sequence of verdicts, or {@code -1} if there is none. + */ + private static int firstAlarm(ShiftSignal[] signals) { + for (int i = 0; i < signals.length; i++) { + if (signals[i].isAlarm()) { + return i; + } + } + return -1; + } + + @Test + void rejectsInvalidConfiguration() { + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(Double.NaN, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, -1.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, -0.5, 5.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, 0.5, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, 0.5, Double.NaN)); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + CusumDetector detector = new CusumDetector(0.0, 1.0); + assertThrows(IllegalArgumentException.class, () -> detector.accept(value)); + } + + @Test + void exposesItsConfiguration() { + CusumDetector detector = new CusumDetector(20.0, 0.5, 0.4, 4.0); + assertEquals(20.0, detector.target()); + assertEquals(0.5, detector.standardDeviation()); + assertEquals(0.4, detector.allowance()); + assertEquals(4.0, detector.threshold()); + assertEquals(ShiftSignal.NONE, detector.lastSignal()); + assertEquals(0L, detector.count()); + assertEquals(0L, detector.alarmCount()); + } + + @Test + @DisplayName("a stream sitting exactly on target never raises an alarm") + void staysQuietOnTarget() { + CusumDetector detector = new CusumDetector(10.0, 1.0); + for (int i = 0; i < 100_000; i++) { + assertEquals(ShiftSignal.NONE, detector.accept(10.0)); + } + assertEquals(0.0, detector.upperSum()); + assertEquals(0.0, detector.lowerSum()); + assertEquals(0L, detector.alarmCount()); + assertEquals(100_000L, detector.count()); + } + + @Test + @DisplayName("the toll keeps in-control noise from accumulating") + void staysMostlyQuietOnInControlNoise() { + Random random = new Random(20240517L); + CusumDetector detector = new CusumDetector(0.0, 1.0, 0.5, 8.0); + for (int i = 0; i < 2_000; i++) { + detector.accept(random.nextGaussian()); + } + assertTrue(detector.alarmCount() <= 2, "raised " + detector.alarmCount() + " false alarms in 2000 samples"); + } + + @Test + @DisplayName("a persistent upward shift is detected within a handful of samples") + void detectsAnUpwardShift() { + double[] signal = new double[60]; + Random random = new Random(11L); + for (int i = 0; i < signal.length; i++) { + signal[i] = (i < 30 ? 0.0 : 2.0) + 0.1 * random.nextGaussian(); + } + + CusumDetector detector = new CusumDetector(0.0, 1.0, 0.5, 5.0); + ShiftSignal[] verdicts = detector.scan(signal); + int alarm = firstAlarm(verdicts); + assertTrue(alarm >= 30, "alarmed before the shift, at index " + alarm); + assertTrue(alarm <= 35, "took too long to alarm, index " + alarm); + assertEquals(ShiftSignal.UPWARD, verdicts[alarm]); + } + + @Test + void detectsADownwardShift() { + double[] signal = new double[60]; + Random random = new Random(12L); + for (int i = 0; i < signal.length; i++) { + signal[i] = (i < 30 ? 100.0 : 96.0) + 0.5 * random.nextGaussian(); + } + + CusumDetector detector = new CusumDetector(100.0, 1.0, 0.5, 5.0); + ShiftSignal[] verdicts = detector.scan(signal); + int alarm = firstAlarm(verdicts); + assertTrue(alarm >= 30 && alarm <= 35, "alarm at index " + alarm); + assertEquals(ShiftSignal.DOWNWARD, verdicts[alarm]); + } + + @Test + @DisplayName("catches a drift far too small to see in any single sample") + void detectsASmallPersistentDrift() { + double[] signal = new double[400]; + Random random = new Random(13L); + for (int i = 0; i < signal.length; i++) { + // The stream is noise free until the drift starts, so an early alarm can only be a + // reaction to the drift itself. + signal[i] = i < 200 ? 0.0 : 0.5 + random.nextGaussian(); + } + + CusumDetector detector = new CusumDetector(0.0, 1.0, 0.25, 5.0); + int alarm = firstAlarm(detector.scan(signal)); + assertTrue(alarm >= 200, "alarmed before the drift, at index " + alarm); + assertTrue(alarm < 260, "a half sigma drift should be caught quickly, but took until " + alarm); + } + + @Test + void sumsNeverGoNegativeAndClearOnAlarm() { + CusumDetector detector = new CusumDetector(0.0, 1.0, 0.5, 3.0); + for (int i = 0; i < 50; i++) { + detector.accept(-5.0); + assertTrue(detector.upperSum() >= 0.0); + assertTrue(detector.lowerSum() >= 0.0); + } + assertTrue(detector.alarmCount() > 1, "a sustained shift should keep alarming"); + } + + @Test + void scanReportsOneVerdictPerSample() { + CusumDetector detector = new CusumDetector(0.0, 1.0); + ShiftSignal[] signals = detector.scan(new double[] {0.0, 0.0, 0.0}); + assertEquals(3, signals.length); + for (ShiftSignal signal : signals) { + assertEquals(ShiftSignal.NONE, signal); + assertFalse(signal.isAlarm()); + } + } + + @Test + void resetClearsTheEvidence() { + CusumDetector detector = new CusumDetector(0.0, 1.0, 0.5, 5.0); + for (int i = 0; i < 10; i++) { + detector.accept(1.5); + } + assertTrue(detector.upperSum() > 0.0 || detector.alarmCount() > 0); + + detector.reset(); + assertEquals(0.0, detector.upperSum()); + assertEquals(0.0, detector.lowerSum()); + assertEquals(0L, detector.count()); + assertEquals(0L, detector.alarmCount()); + assertEquals(ShiftSignal.NONE, detector.lastSignal()); + } + + @Test + void toStringMentionsTheState() { + CusumDetector detector = new CusumDetector(7.0, 1.0); + assertTrue(detector.toString().contains("target=7.0"), detector.toString()); + } + + @Test + void shiftSignalDescribesItself() { + assertFalse(ShiftSignal.NONE.isAlarm()); + assertTrue(ShiftSignal.UPWARD.isAlarm()); + assertTrue(ShiftSignal.DOWNWARD.isAlarm()); + assertEquals(3, ShiftSignal.values().length); + assertEquals(ShiftSignal.UPWARD, ShiftSignal.valueOf("UPWARD")); + } + + @Test + void rejectsNonFiniteConfiguration() { + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, Double.NaN, 5.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, Double.POSITIVE_INFINITY, 5.0)); + assertThrows(IllegalArgumentException.class, () -> new CusumDetector(0.0, 1.0, 0.5, Double.POSITIVE_INFINITY)); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/DynamicTimeWarpingTest.java b/src/test/java/com/thealgorithms/streaming/DynamicTimeWarpingTest.java new file mode 100644 index 000000000000..5be3b21eb101 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/DynamicTimeWarpingTest.java @@ -0,0 +1,221 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 DynamicTimeWarpingTest { + + private static double manhattan(double[] first, double[] second) { + double sum = 0.0; + for (int i = 0; i < first.length; i++) { + sum += Math.abs(first[i] - second[i]); + } + return sum; + } + + private static double[] sine(int length, double period, double phase) { + double[] series = new double[length]; + for (int i = 0; i < length; i++) { + series[i] = Math.sin(2 * Math.PI * i / period + phase); + } + return series; + } + + @Test + void rejectsEmptySeries() { + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.distance(new double[0], new double[] {1.0})); + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.distance(new double[] {1.0}, new double[0])); + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.path(new double[0], new double[] {1.0})); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + double[] good = {1.0, 2.0, 3.0}; + double[] bad = {1.0, value, 3.0}; + + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.distance(bad, good)); + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.distance(good, bad)); + } + + @Test + @DisplayName("a band narrower than the difference in length admits no alignment") + void rejectsABandThatIsTooNarrow() { + double[] shorter = {1.0, 2.0, 3.0}; + double[] longer = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + + assertThrows(IllegalArgumentException.class, () -> DynamicTimeWarping.distance(shorter, longer, 2)); + assertEquals(6.0, DynamicTimeWarping.distance(shorter, longer, 3), 1e-12, "the last point of the short series has to carry 3, 4, 5 and 6"); + } + + @Test + void aSeriesIsAtNoDistanceFromItself() { + double[] series = {1.0, 4.0, 2.0, 8.0, 3.0}; + + assertEquals(0.0, DynamicTimeWarping.distance(series, series)); + } + + @Test + @DisplayName("two flat series a fixed distance apart cost that distance once per sample") + void measuresAConstantOffset() { + double[] first = {1.0, 1.0, 1.0, 1.0}; + double[] second = {3.0, 3.0, 3.0, 3.0}; + + assertEquals(8.0, DynamicTimeWarping.distance(first, second), 1e-12, "the diagonal is the shortest path and every cell costs 2"); + } + + @Test + @DisplayName("a series shifted in value is cheaper than sample by sample, because warping reuses points") + void warpingBeatsTheStraightComparisonOnARamp() { + double[] first = {1.0, 2.0, 3.0, 4.0}; + double[] second = {3.0, 4.0, 5.0, 6.0}; + + assertEquals(6.0, DynamicTimeWarping.distance(first, second), 1e-12); + assertEquals(8.0, manhattan(first, second), 1e-12); + } + + @Test + void isSymmetric() { + Random random = new Random(3L); + for (int trial = 0; trial < 20; trial++) { + double[] first = new double[10 + random.nextInt(20)]; + double[] second = new double[10 + random.nextInt(20)]; + for (int i = 0; i < first.length; i++) { + first[i] = random.nextGaussian(); + } + for (int i = 0; i < second.length; i++) { + second[i] = random.nextGaussian(); + } + + assertEquals(DynamicTimeWarping.distance(first, second), DynamicTimeWarping.distance(second, first), 1e-9); + } + } + + @Test + @DisplayName("warping never costs more than matching sample by sample") + void neverExceedsTheStraightComparison() { + Random random = new Random(11L); + for (int trial = 0; trial < 50; trial++) { + double[] first = new double[30]; + double[] second = new double[30]; + for (int i = 0; i < first.length; i++) { + first[i] = random.nextGaussian(); + second[i] = random.nextGaussian(); + } + + assertTrue(DynamicTimeWarping.distance(first, second) <= manhattan(first, second) + 1e-9); + } + } + + @Test + @DisplayName("a band of zero forces the straight comparison") + void aBandOfZeroIsTheStraightComparison() { + double[] first = {1.0, 5.0, 2.0, 8.0}; + double[] second = {2.0, 4.0, 4.0, 7.0}; + + assertEquals(manhattan(first, second), DynamicTimeWarping.distance(first, second, 0), 1e-12); + } + + @Test + @DisplayName("a narrower band can only cost more") + void aNarrowerBandCostsAtLeastAsMuch() { + double[] first = sine(60, 12.0, 0.0); + double[] second = sine(60, 12.0, 0.9); + + double free = DynamicTimeWarping.distance(first, second); + double banded = DynamicTimeWarping.distance(first, second, 3); + double tight = DynamicTimeWarping.distance(first, second, 1); + + assertTrue(banded >= free - 1e-9, "banded " + banded + " should not be below free " + free); + assertTrue(tight >= banded - 1e-9, "tight " + tight + " should not be below banded " + banded); + } + + @Test + @DisplayName("a shift in time costs almost nothing, where a straight comparison is fooled") + void absorbsAShiftInTime() { + double[] first = sine(60, 12.0, 0.0); + double[] second = sine(60, 12.0, Math.PI / 3); + + double warping = DynamicTimeWarping.distance(first, second); + double straight = manhattan(first, second); + + assertTrue(warping < 0.25 * straight, "warping " + warping + " against straight " + straight); + } + + @Test + @DisplayName("a series stretched in time still matches the original") + void absorbsAStretch() { + double[] original = sine(40, 10.0, 0.0); + double[] stretched = new double[80]; + for (int i = 0; i < stretched.length; i++) { + stretched[i] = original[i / 2]; + } + + double warping = DynamicTimeWarping.distance(original, stretched); + + assertTrue(warping < 1.0, "a stretched copy should be close, but was " + warping); + } + + @Test + @DisplayName("the path runs from corner to corner without ever going backwards") + void thePathIsMonotoneAndComplete() { + double[] first = sine(30, 8.0, 0.0); + double[] second = sine(45, 12.0, 0.4); + + int[][] alignment = DynamicTimeWarping.path(first, second); + + assertEquals(0, alignment[0][0]); + assertEquals(0, alignment[0][1]); + assertEquals(first.length - 1, alignment[alignment.length - 1][0]); + assertEquals(second.length - 1, alignment[alignment.length - 1][1]); + for (int step = 1; step < alignment.length; step++) { + int rowStep = alignment[step][0] - alignment[step - 1][0]; + int columnStep = alignment[step][1] - alignment[step - 1][1]; + assertTrue(rowStep >= 0 && rowStep <= 1, "the path stepped " + rowStep + " rows"); + assertTrue(columnStep >= 0 && columnStep <= 1, "the path stepped " + columnStep + " columns"); + assertTrue(rowStep + columnStep > 0, "the path stood still"); + } + } + + @Test + @DisplayName("the cost of the path is the distance") + void thePathCostsWhatTheDistanceSays() { + double[] first = sine(25, 7.0, 0.0); + double[] second = sine(33, 9.0, 0.2); + + int[][] alignment = DynamicTimeWarping.path(first, second); + double cost = 0.0; + for (int[] pair : alignment) { + cost += Math.abs(first[pair[0]] - second[pair[1]]); + } + + assertEquals(DynamicTimeWarping.distance(first, second), cost, 1e-9); + } + + @Test + void handlesSeriesOfOneSample() { + assertEquals(3.0, DynamicTimeWarping.distance(new double[] {1.0}, new double[] {4.0}), 1e-12); + assertEquals(1, DynamicTimeWarping.path(new double[] {1.0}, new double[] {4.0}).length); + } + + @Test + void leavesTheSeriesUntouched() { + double[] first = {1.0, 2.0, 3.0}; + double[] second = {4.0, 5.0}; + double[] firstCopy = first.clone(); + double[] secondCopy = second.clone(); + + DynamicTimeWarping.distance(first, second); + DynamicTimeWarping.path(first, second); + + org.junit.jupiter.api.Assertions.assertArrayEquals(firstCopy, first); + org.junit.jupiter.api.Assertions.assertArrayEquals(secondCopy, second); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/EwmaChangeDetectorTest.java b/src/test/java/com/thealgorithms/streaming/EwmaChangeDetectorTest.java new file mode 100644 index 000000000000..37b1688b424f --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/EwmaChangeDetectorTest.java @@ -0,0 +1,209 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 EwmaChangeDetectorTest { + + /** + * Index of the first alarm in a sequence of verdicts, or {@code -1} if there is none. + */ + private static int firstAlarm(ShiftSignal[] signals) { + for (int i = 0; i < signals.length; i++) { + if (signals[i].isAlarm()) { + return i; + } + } + return -1; + } + + private static double[] stepSignal(int length, int stepAt, double before, double after) { + double[] signal = new double[length]; + for (int i = 0; i < length; i++) { + signal[i] = i < stepAt ? before : after; + } + return signal; + } + + @Test + void rejectsInvalidConfiguration() { + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(Double.NaN, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, -2.0)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 1.0, 0.2, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 1.0, 0.0, 3.0)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 1.0, 1.5, 3.0)); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0); + assertThrows(IllegalArgumentException.class, () -> detector.accept(value)); + } + + @Test + void exposesItsConfiguration() { + EwmaChangeDetector detector = new EwmaChangeDetector(20.0, 0.5, 0.3, 2.5); + assertEquals(20.0, detector.target()); + assertEquals(0.5, detector.standardDeviation()); + assertEquals(0.3, detector.alpha()); + assertEquals(2.5, detector.width()); + assertEquals(20.0, detector.statistic()); + assertEquals(ShiftSignal.NONE, detector.lastSignal()); + assertEquals(0L, detector.count()); + assertEquals(0L, detector.alarmCount()); + } + + @Test + @DisplayName("the defaults are the customary alpha of 0.2 and a band of three sigmas") + void usesCustomaryDefaults() { + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0); + assertEquals(EwmaChangeDetector.DEFAULT_ALPHA, detector.alpha()); + assertEquals(EwmaChangeDetector.DEFAULT_WIDTH, detector.width()); + } + + @Test + @DisplayName("a stream sitting exactly on target never raises an alarm") + void staysQuietOnTarget() { + EwmaChangeDetector detector = new EwmaChangeDetector(5.0, 1.0); + for (int i = 0; i < 10_000; i++) { + assertEquals(ShiftSignal.NONE, detector.accept(5.0)); + } + assertEquals(5.0, detector.statistic(), 1e-12); + assertEquals(0L, detector.alarmCount()); + assertEquals(10_000L, detector.count()); + } + + @Test + @DisplayName("the control band widens during the warm-up and then settles") + void controlLimitApproachesItsAsymptote() { + double alpha = 0.2; + double width = 3.0; + double sigma = 2.0; + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, sigma, alpha, width); + + double asymptote = width * sigma * Math.sqrt(alpha / (2.0 - alpha)); + assertEquals(0.0, detector.controlLimit(), 1e-12); + + double previous = 0.0; + for (int i = 0; i < 40; i++) { + detector.accept(0.0); + double limit = detector.controlLimit(); + assertTrue(limit > previous, "the limit stopped growing at step " + i); + assertTrue(limit < asymptote, "the exact limit stays below its asymptote, but was " + limit); + previous = limit; + } + + for (int i = 0; i < 200; i++) { + detector.accept(0.0); + } + assertEquals(asymptote, detector.controlLimit(), 1e-9); + } + + @Test + void detectsAnUpwardShift() { + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0, 0.2, 3.0); + ShiftSignal[] verdicts = detector.scan(stepSignal(60, 30, 0.0, 2.0)); + + int alarm = firstAlarm(verdicts); + assertTrue(alarm >= 30, "alarmed before the shift, at index " + alarm); + assertTrue(alarm <= 36, "took too long to alarm, index " + alarm); + assertEquals(ShiftSignal.UPWARD, verdicts[alarm]); + } + + @Test + void detectsADownwardShift() { + EwmaChangeDetector detector = new EwmaChangeDetector(100.0, 2.0, 0.2, 3.0); + ShiftSignal[] verdicts = detector.scan(stepSignal(60, 30, 100.0, 94.0)); + + int alarm = firstAlarm(verdicts); + assertTrue(alarm >= 30 && alarm <= 36, "alarm at index " + alarm); + assertEquals(ShiftSignal.DOWNWARD, verdicts[alarm]); + } + + @Test + @DisplayName("an alarm returns the statistic to the target so the detector does not latch") + void resetsTheStatisticOnAnAlarm() { + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0, 0.2, 3.0); + ShiftSignal[] verdicts = detector.scan(stepSignal(40, 0, 2.0, 2.0)); + + assertTrue(firstAlarm(verdicts) >= 0); + assertTrue(detector.alarmCount() > 1, "a sustained shift should keep alarming"); + for (int i = 0; i < verdicts.length; i++) { + if (verdicts[i].isAlarm() && i + 1 < verdicts.length) { + assertEquals(ShiftSignal.NONE, verdicts[i + 1], "the sample right after an alarm restarts from the target"); + } + } + } + + @Test + @DisplayName("in-control noise rarely trips the band") + void staysMostlyQuietOnInControlNoise() { + Random random = new Random(20240517L); + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0, 0.2, 4.0); + for (int i = 0; i < 2_000; i++) { + detector.accept(random.nextGaussian()); + } + assertTrue(detector.alarmCount() <= 3, "raised " + detector.alarmCount() + " false alarms in 2000 samples"); + } + + @Test + @DisplayName("a shift buried in noise is still found") + void detectsAShiftInNoisyData() { + Random random = new Random(4242L); + double[] signal = new double[400]; + for (int i = 0; i < signal.length; i++) { + signal[i] = (i < 200 ? 0.0 : 1.5) + random.nextGaussian(); + } + + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0, 0.2, 4.0); + ShiftSignal[] verdicts = detector.scan(signal); + int alarm = firstAlarm(verdicts); + assertTrue(alarm >= 200, "alarmed before the shift, at index " + alarm); + assertTrue(alarm < 220, "the shift should be found quickly, but took until " + alarm); + assertEquals(ShiftSignal.UPWARD, verdicts[alarm]); + } + + @Test + void scanReportsOneVerdictPerSample() { + EwmaChangeDetector detector = new EwmaChangeDetector(0.0, 1.0); + ShiftSignal[] verdicts = detector.scan(new double[] {0.0, 0.0, 0.0}); + assertEquals(3, verdicts.length); + assertEquals(3L, detector.count()); + } + + @Test + void resetRestoresTheInitialState() { + EwmaChangeDetector detector = new EwmaChangeDetector(4.0, 1.0, 0.5, 3.0); + detector.scan(stepSignal(20, 0, 20.0, 20.0)); + detector.reset(); + + assertEquals(4.0, detector.statistic()); + assertEquals(0.0, detector.controlLimit(), 1e-12); + assertEquals(0L, detector.count()); + assertEquals(0L, detector.alarmCount()); + assertEquals(ShiftSignal.NONE, detector.lastSignal()); + } + + @Test + void toStringMentionsTheState() { + EwmaChangeDetector detector = new EwmaChangeDetector(7.0, 1.0); + assertTrue(detector.toString().contains("target=7.0"), detector.toString()); + } + + @Test + void rejectsNonFiniteConfiguration() { + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, Double.POSITIVE_INFINITY)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 1.0, 0.2, Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> new EwmaChangeDetector(0.0, 1.0, 0.2, Double.POSITIVE_INFINITY)); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/ExponentialMovingAverageTest.java b/src/test/java/com/thealgorithms/streaming/ExponentialMovingAverageTest.java new file mode 100644 index 000000000000..83931dbec1b1 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/ExponentialMovingAverageTest.java @@ -0,0 +1,180 @@ +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.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 ExponentialMovingAverageTest { + + @ParameterizedTest + @ValueSource(doubles = {0.0, -0.5, 1.5, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidAlpha(double alpha) { + assertThrows(IllegalArgumentException.class, () -> ExponentialMovingAverage.ofAlpha(alpha)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 0.5, -3.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidSpan(double span) { + assertThrows(IllegalArgumentException.class, () -> ExponentialMovingAverage.ofSpan(span)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidHalfLife(double halfLife) { + assertThrows(IllegalArgumentException.class, () -> ExponentialMovingAverage.ofHalfLife(halfLife)); + } + + @Test + void rejectsNonFiniteSeed() { + assertThrows(IllegalArgumentException.class, () -> ExponentialMovingAverage.ofAlpha(0.5, Double.NaN)); + } + + @Test + void spanAndHalfLifeTranslateIntoAlpha() { + assertEquals(0.2, ExponentialMovingAverage.ofSpan(9.0).alpha(), 1e-12); + assertEquals(1.0, ExponentialMovingAverage.ofSpan(1.0).alpha(), 1e-12); + assertEquals(0.5, ExponentialMovingAverage.ofHalfLife(1.0).alpha(), 1e-12); + assertEquals(1.0 - Math.pow(0.5, 0.1), ExponentialMovingAverage.ofHalfLife(10.0).alpha(), 1e-12); + } + + @Test + void queriesBeforeTheFirstSampleFail() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.5); + assertFalse(average.isInitialized()); + assertThrows(IllegalStateException.class, average::value); + assertThrows(IllegalStateException.class, average::variance); + assertThrows(IllegalStateException.class, average::standardDeviation); + } + + @Test + @DisplayName("the first sample seeds the average instead of being pulled towards zero") + void seedsWithTheFirstSample() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.1); + assertEquals(100.0, average.add(100.0)); + assertTrue(average.isInitialized()); + assertEquals(1L, average.count()); + } + + @Test + void followsTheRecurrence() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.5); + assertEquals(1.0, average.add(1.0), 1e-12); + assertEquals(1.5, average.add(2.0), 1e-12); + assertEquals(2.25, average.add(3.0), 1e-12); + assertEquals(3L, average.count()); + } + + @Test + void anExplicitSeedIsUsedRightAway() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.5, 0.0); + assertTrue(average.isInitialized()); + assertEquals(0.0, average.value()); + assertEquals(0.5, average.add(1.0), 1e-12); + assertEquals(1L, average.count(), "the seed is not counted as a sample"); + } + + @Test + void alphaOfOneKeepsOnlyTheLatestSample() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(1.0); + average.addAll(1.0, 2.0, 3.0); + assertEquals(3.0, average.value(), 1e-12); + assertEquals(0.0, average.variance(), 1e-12); + } + + @Test + void trackedVarianceFollowsTheRecurrence() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.5); + average.add(0.0); + assertEquals(0.0, average.variance(), 1e-12); + average.add(2.0); + assertEquals(1.0, average.variance(), 1e-12); + average.add(0.0); + assertEquals(0.75, average.variance(), 1e-12); + assertEquals(Math.sqrt(0.75), average.standardDeviation(), 1e-12); + } + + @Test + void aConstantSignalHasNoSpread() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofSpan(10.0); + for (int i = 0; i < 100; i++) { + average.add(7.0); + } + assertEquals(7.0, average.value(), 1e-12); + assertEquals(0.0, average.variance(), 1e-12); + } + + @Test + @DisplayName("a step in the signal is tracked, the faster the larger alpha is") + void tracksAStep() { + ExponentialMovingAverage fast = ExponentialMovingAverage.ofAlpha(0.5); + ExponentialMovingAverage slow = ExponentialMovingAverage.ofAlpha(0.05); + fast.add(0.0); + slow.add(0.0); + for (int i = 0; i < 10; i++) { + fast.add(10.0); + slow.add(10.0); + } + assertTrue(fast.value() > slow.value(), "fast=" + fast.value() + " slow=" + slow.value()); + assertEquals(10.0, fast.value(), 0.05); + assertTrue(slow.value() < 5.0); + + for (int i = 0; i < 500; i++) { + slow.add(10.0); + } + assertEquals(10.0, slow.value(), 1e-6); + } + + @Test + @DisplayName("on stationary noise the estimates sit close to the true mean and variance") + void approximatesTheStationaryMoments() { + Random random = new Random(987L); + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.01); + for (int i = 0; i < 200_000; i++) { + average.add(5.0 + 2.0 * random.nextGaussian()); + } + assertEquals(5.0, average.value(), 0.5); + assertEquals(4.0, average.variance(), 2.0); + } + + @Test + void resetReturnsToTheInitialState() { + ExponentialMovingAverage plain = ExponentialMovingAverage.ofAlpha(0.5); + plain.addAll(1.0, 2.0, 3.0); + plain.reset(); + assertFalse(plain.isInitialized()); + assertEquals(0L, plain.count()); + + ExponentialMovingAverage seeded = ExponentialMovingAverage.ofAlpha(0.5, 42.0); + seeded.addAll(1.0, 2.0); + seeded.reset(); + assertTrue(seeded.isInitialized()); + assertEquals(42.0, seeded.value()); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.3); + assertThrows(IllegalArgumentException.class, () -> average.add(value)); + } + + @Test + void toStringMentionsTheState() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.25); + average.add(4.0); + assertTrue(average.toString().contains("alpha=0.25"), average.toString()); + } + + @Test + void toStringWorksBeforeTheFirstSample() { + ExponentialMovingAverage average = ExponentialMovingAverage.ofAlpha(0.25); + assertTrue(average.toString().contains("value=NaN"), average.toString()); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/ExtendedKalmanFilterTest.java b/src/test/java/com/thealgorithms/streaming/ExtendedKalmanFilterTest.java new file mode 100644 index 000000000000..d0286aac4a36 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/ExtendedKalmanFilterTest.java @@ -0,0 +1,273 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ExtendedKalmanFilterTest { + + private static final double OFFSET = 5.0; + private static final double INTERVAL = 0.1; + + private static ExtendedKalmanFilter scalarFilter(double estimate, double variance) { + return new ExtendedKalmanFilter(new double[] {estimate}, new double[][] {{variance}}); + } + + /** The range a sensor at the origin sees of a target passing at a fixed lateral offset. */ + private static double range(double position) { + return Math.hypot(position, OFFSET); + } + + @Test + void rejectsAnEmptyOrMalformedStart() { + assertThrows(IllegalArgumentException.class, () -> new ExtendedKalmanFilter(new double[0], new double[0][0])); + assertThrows(IllegalArgumentException.class, () -> new ExtendedKalmanFilter(new double[] {1.0, 2.0}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> new ExtendedKalmanFilter(new double[] {1.0}, new double[][] {{1.0, 0.0}})); + assertThrows(IllegalArgumentException.class, () -> new ExtendedKalmanFilter(new double[] {Double.NaN}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> new ExtendedKalmanFilter(new double[] {1.0}, new double[][] {{Double.POSITIVE_INFINITY}})); + } + + @Test + void rejectsMisshapenModels() { + ExtendedKalmanFilter filter = scalarFilter(0.0, 1.0); + + assertThrows(IllegalArgumentException.class, () -> filter.predict(new double[][] {{1.0, 0.0}}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.predict(new double[][] {{1.0}}, new double[][] {{1.0, 0.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[0], new double[0][0], new double[0][0])); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[] {1.0}, new double[][] {{1.0}}, new double[][] {{1.0, 0.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[] {1.0}, new double[][] {{1.0, 1.0}}, new double[][] {{1.0}})); + } + + @Test + @DisplayName("a model that returns the wrong shape or a non-finite value is caught") + void rejectsAModelThatMisbehaves() { + ExtendedKalmanFilter filter = scalarFilter(0.0, 1.0); + + assertThrows(IllegalArgumentException.class, () -> filter.predict(current -> new double[] {1.0, 2.0}, current -> new double[][] {{1.0}}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.predict(current -> new double[] {Double.NaN}, current -> new double[][] {{1.0}}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[] {1.0}, current -> new double[] {1.0, 2.0}, current -> new double[][] {{1.0}}, new double[][] {{1.0}})); + } + + @Test + @DisplayName("a measurement that carries no information is reported rather than silently poisoning the state") + void rejectsASingularInnovationCovariance() { + ExtendedKalmanFilter filter = scalarFilter(0.0, 0.0); + + assertThrows(ArithmeticException.class, () -> filter.update(new double[] {1.0}, new double[][] {{1.0}}, new double[][] {{0.0}})); + } + + @Test + @DisplayName("one update of a one dimensional filter is the textbook half way step") + void reproducesTheTextbookScalarUpdate() { + ExtendedKalmanFilter filter = scalarFilter(0.0, 1.0); + + double[] corrected = filter.update(new double[] {1.0}, new double[][] {{1.0}}, new double[][] {{1.0}}); + + assertEquals(0.5, corrected[0], 1e-12, "with equal confidence the estimate lands in the middle"); + assertEquals(0.5, filter.variance(0), 1e-12); + assertEquals(0.5, filter.lastGain()[0][0], 1e-12); + assertEquals(1.0, filter.lastInnovation()[0], 1e-12); + } + + @Test + @DisplayName("on a linear model the filter is the scalar Kalman recursion, to the last digit") + void matchesTheScalarKalmanRecursion() { + double processNoise = 0.01; + double measurementNoise = 0.25; + ExtendedKalmanFilter filter = scalarFilter(0.0, 1.0); + double estimate = 0.0; + double variance = 1.0; + + Random random = new Random(5L); + for (int step = 0; step < 500; step++) { + filter.predict(new double[][] {{1.0}}, new double[][] {{processNoise}}); + variance += processNoise; + + double measurement = random.nextGaussian(); + filter.update(new double[] {measurement}, new double[][] {{1.0}}, new double[][] {{measurementNoise}}); + double gain = variance / (variance + measurementNoise); + estimate += gain * (measurement - estimate); + variance = (1 - gain) * variance * (1 - gain) + gain * measurementNoise * gain; + + assertEquals(estimate, filter.state()[0], 1e-12, "step " + step); + assertEquals(variance, filter.variance(0), 1e-12, "step " + step); + } + } + + @Test + @DisplayName("the Joseph form keeps the covariance symmetric over a long run") + void keepsTheCovarianceSymmetric() { + Random random = new Random(11L); + ExtendedKalmanFilter filter = new ExtendedKalmanFilter(new double[] {0.0, 0.0, 0.0}, new double[][] {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}); + double[][] transition = {{1.0, INTERVAL, 0.0}, {0.0, 1.0, INTERVAL}, {0.0, 0.0, 1.0}}; + double[][] processNoise = {{1e-4, 0.0, 0.0}, {0.0, 1e-4, 0.0}, {0.0, 0.0, 1e-4}}; + double[][] model = {{1.0, 0.0, 0.0}}; + double[][] measurementNoise = {{0.5}}; + + for (int step = 0; step < 500; step++) { + filter.predict(transition, processNoise); + filter.update(new double[] {random.nextGaussian()}, model, measurementNoise); + + double[][] covariance = filter.covariance(); + for (int i = 0; i < covariance.length; i++) { + assertTrue(covariance[i][i] >= 0.0, "a variance went negative at step " + step); + for (int j = 0; j < i; j++) { + assertEquals(covariance[i][j], covariance[j][i], 1e-12, "the covariance lost its symmetry at step " + step); + } + } + } + } + + @Test + @DisplayName("repeated measurements of something that does not move shrink the uncertainty towards zero") + void convergesOnAConstant() { + Random random = new Random(3L); + ExtendedKalmanFilter filter = scalarFilter(0.0, 100.0); + double truth = 7.0; + double previous = Double.MAX_VALUE; + + for (int step = 0; step < 2000; step++) { + filter.update(new double[] {truth + random.nextGaussian()}, new double[][] {{1.0}}, new double[][] {{1.0}}); + double variance = filter.variance(0); + assertTrue(variance < previous, "the uncertainty must never grow without process noise, step " + step); + previous = variance; + } + + assertEquals(truth, filter.state()[0], 0.1); + assertTrue(filter.variance(0) < 0.001, "after 2000 measurements the variance was still " + filter.variance(0)); + } + + @Test + @DisplayName("a target seen only as a range is tracked through the nonlinear measurement") + void tracksATargetThroughARangeOnlySensor() { + Random random = new Random(17L); + double[][] transition = {{1.0, INTERVAL}, {0.0, 1.0}}; + double[][] processNoise = {{1e-6, 0.0}, {0.0, 1e-4}}; + double[][] measurementNoise = {{0.04}}; + ExtendedKalmanFilter filter = new ExtendedKalmanFilter(new double[] {1.0, 0.5}, new double[][] {{4.0, 0.0}, {0.0, 4.0}}); + + double position = 2.0; + double velocity = 1.0; + double filterError = 0.0; + double rawError = 0.0; + int counted = 0; + + for (int step = 0; step < 400; step++) { + position += velocity * INTERVAL; + double measurement = range(position) + 0.2 * random.nextGaussian(); + + filter.predict(transition, processNoise); + filter.update(new double[] {measurement}, state -> new double[] {range(state[0])}, state -> new double[][] {{state[0] / range(state[0]), 0.0}}, measurementNoise); + + if (step > 200) { + filterError += Math.abs(filter.state()[0] - position); + rawError += Math.abs(Math.sqrt(Math.max(0.0, measurement * measurement - OFFSET * OFFSET)) - position); + counted++; + } + } + + double filtered = filterError / counted; + double raw = rawError / counted; + assertTrue(filtered < 0.2, "the filtered position was off by " + filtered); + assertTrue(filtered < raw / 2, "filtering should beat inverting each measurement on its own, " + filtered + " against " + raw); + assertEquals(velocity, filter.state()[1], 0.2, "the velocity is never measured, only inferred"); + } + + @Test + @DisplayName("the linear overloads are the nonlinear ones with a matrix for a model") + void theLinearOverloadsAgreeWithTheFunctionalOnes() { + double[][] transition = {{1.0, INTERVAL}, {0.0, 1.0}}; + double[][] processNoise = {{0.01, 0.0}, {0.0, 0.01}}; + double[][] model = {{1.0, 0.0}}; + double[][] measurementNoise = {{0.5}}; + double[] start = {1.0, 2.0}; + double[][] startCovariance = {{1.0, 0.0}, {0.0, 1.0}}; + + ExtendedKalmanFilter linear = new ExtendedKalmanFilter(start, startCovariance); + ExtendedKalmanFilter functional = new ExtendedKalmanFilter(start, startCovariance); + + for (int step = 0; step < 50; step++) { + linear.predict(transition, processNoise); + functional.predict(state -> new double[] {state[0] + INTERVAL * state[1], state[1]}, state -> transition, processNoise); + + double measurement = 0.1 * step; + linear.update(new double[] {measurement}, model, measurementNoise); + functional.update(new double[] {measurement}, state -> new double[] {state[0]}, state -> model, measurementNoise); + + Assertions.assertArrayEquals(linear.state(), functional.state(), 1e-12, "step " + step); + } + } + + @Test + @DisplayName("prediction without measurements only widens the uncertainty") + void predictionAloneAddsUncertainty() { + ExtendedKalmanFilter filter = scalarFilter(3.0, 1.0); + + for (int step = 1; step <= 10; step++) { + filter.predict(new double[][] {{1.0}}, new double[][] {{0.5}}); + + assertEquals(3.0, filter.state()[0], 1e-12, "a unit transition must not move the estimate"); + assertEquals(1.0 + 0.5 * step, filter.variance(0), 1e-12); + } + assertEquals(10, filter.predictionCount()); + assertEquals(0, filter.updateCount()); + } + + @Test + void theStateAndCovarianceAreCopiedOut() { + ExtendedKalmanFilter filter = scalarFilter(1.0, 2.0); + + filter.state()[0] = 99.0; + filter.covariance()[0][0] = 99.0; + filter.update(new double[] {1.0}, new double[][] {{1.0}}, new double[][] {{1.0}}); + filter.lastGain()[0][0] = 99.0; + filter.lastInnovation()[0] = 99.0; + + assertTrue(filter.state()[0] < 2.0); + assertTrue(filter.covariance()[0][0] < 2.0); + assertTrue(filter.lastGain()[0][0] < 1.0); + } + + @Test + void exposesItsShapeAndCounters() { + ExtendedKalmanFilter filter = new ExtendedKalmanFilter(new double[] {0.0, 0.0}, new double[][] {{1.0, 0.0}, {0.0, 1.0}}); + + assertEquals(2, filter.stateSize()); + assertEquals(0, filter.predictionCount()); + assertEquals(0, filter.updateCount()); + assertEquals(0, filter.lastInnovation().length); + assertThrows(IllegalArgumentException.class, () -> filter.variance(2)); + assertThrows(IllegalArgumentException.class, () -> filter.variance(-1)); + } + + @Test + void resetRestartsFromAKnownEstimate() { + ExtendedKalmanFilter filter = scalarFilter(0.0, 1.0); + filter.update(new double[] {5.0}, new double[][] {{1.0}}, new double[][] {{1.0}}); + + filter.reset(new double[] {-2.0}, new double[][] {{9.0}}); + + assertEquals(-2.0, filter.state()[0], 1e-12); + assertEquals(9.0, filter.variance(0), 1e-12); + assertEquals(0, filter.updateCount()); + assertEquals(0, filter.lastInnovation().length); + assertThrows(IllegalArgumentException.class, () -> filter.reset(new double[] {1.0, 2.0}, new double[][] {{1.0}})); + } + + @Test + void toStringMentionsTheState() { + ExtendedKalmanFilter filter = scalarFilter(1.5, 1.0); + filter.predict(new double[][] {{1.0}}, new double[][] {{0.1}}); + + String text = filter.toString(); + + assertTrue(text.contains("ExtendedKalmanFilter")); + assertTrue(text.contains("predictions=1")); + assertTrue(text.contains("1.5")); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/GeneralizedEsdTest.java b/src/test/java/com/thealgorithms/streaming/GeneralizedEsdTest.java new file mode 100644 index 000000000000..cd5e1db492d9 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/GeneralizedEsdTest.java @@ -0,0 +1,212 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Assertions; +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 GeneralizedEsdTest { + + /** + * The sample Rosner published with the test, also used as the worked example in the NIST + * engineering statistics handbook. + */ + private static final double[] ROSNER_SAMPLE = {-0.25, 0.68, 0.94, 1.15, 1.20, 1.26, 1.26, 1.34, 1.38, 1.43, 1.49, 1.49, 1.55, 1.56, 1.58, 1.65, 1.69, 1.70, 1.76, 1.77, 1.81, 1.91, 1.94, 1.96, 1.99, 2.06, 2.09, 2.10, 2.14, 2.15, 2.23, 2.24, 2.26, 2.35, 2.37, 2.40, 2.47, 2.54, 2.62, 2.64, 2.90, + 2.92, 2.92, 2.93, 3.21, 3.26, 3.30, 3.59, 3.68, 4.30, 4.64, 5.34, 5.42, 6.01}; + + private static double[] gaussianSample(int size, long seed) { + Random random = new Random(seed); + double[] sample = new double[size]; + for (int i = 0; i < size; i++) { + sample[i] = 100.0 + 5.0 * random.nextGaussian(); + } + return sample; + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 1.0, -0.1, 1.5, Double.NaN}) + void rejectsInvalidSignificanceLevels(double level) { + assertThrows(IllegalArgumentException.class, () -> new GeneralizedEsd(level)); + } + + @Test + void rejectsSamplesThatAreTooSmall() { + GeneralizedEsd test = new GeneralizedEsd(); + + assertThrows(IllegalArgumentException.class, () -> test.findOutliers(new double[] {1.0, 2.0}, 1)); + } + + @Test + void rejectsAnOutlierBoundOutOfRange() { + GeneralizedEsd test = new GeneralizedEsd(); + double[] sample = gaussianSample(20, 1L); + + assertThrows(IllegalArgumentException.class, () -> test.findOutliers(sample, 0)); + assertThrows(IllegalArgumentException.class, () -> test.findOutliers(sample, 19)); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + GeneralizedEsd test = new GeneralizedEsd(); + double[] sample = gaussianSample(20, 2L); + sample[7] = value; + + assertThrows(IllegalArgumentException.class, () -> test.findOutliers(sample, 3)); + } + + @Test + void exposesItsConfiguration() { + assertEquals(0.05, new GeneralizedEsd().significanceLevel()); + assertEquals(0.01, new GeneralizedEsd(0.01).significanceLevel()); + } + + @Test + @DisplayName("the critical values match the table published with the test") + void reproducesThePublishedCriticalValues() { + GeneralizedEsd test = new GeneralizedEsd(0.05); + + assertEquals(3.159, test.criticalValue(54, 1), 5e-4); + assertEquals(3.151, test.criticalValue(54, 2), 5e-4); + assertEquals(3.144, test.criticalValue(54, 3), 5e-4); + assertEquals(3.085, test.criticalValue(54, 10), 5e-4); + } + + @Test + @DisplayName("Rosner's own sample of 54 points holds exactly three outliers") + void reproducesTheWorkedExample() { + GeneralizedEsd test = new GeneralizedEsd(0.05); + + int[] outliers = test.findOutliers(ROSNER_SAMPLE, 10); + + assertArrayEqualsSorted(new int[] {51, 52, 53}, outliers); + assertEquals(5.34, ROSNER_SAMPLE[outliers[0]]); + assertEquals(5.42, ROSNER_SAMPLE[outliers[1]]); + assertEquals(6.01, ROSNER_SAMPLE[outliers[2]]); + } + + @Test + @DisplayName("the answer is the last test that succeeds, not the first") + void findsOutliersThatMaskEachOther() { + double[] sample = gaussianSample(40, 0L); + sample[10] = 160.0; + sample[20] = 161.0; + sample[30] = 162.0; + + int[] outliers = new GeneralizedEsd(0.05).findOutliers(sample, 5); + + assertArrayEqualsSorted(new int[] {10, 20, 30}, outliers); + } + + @Test + void findsASingleOutlier() { + double[] sample = gaussianSample(30, 4L); + sample[13] = 200.0; + + int[] outliers = new GeneralizedEsd().findOutliers(sample, 5); + + assertArrayEqualsSorted(new int[] {13}, outliers); + } + + @Test + @DisplayName("clean data holds no outliers") + void findsNothingInCleanData() { + for (long seed = 0; seed < 12; seed++) { + double[] sample = gaussianSample(60, seed); + + int[] outliers = new GeneralizedEsd(0.001).findOutliers(sample, 6); + + assertEquals(0, outliers.length, "seed " + seed + " reported " + Arrays.toString(outliers)); + } + } + + @Test + @DisplayName("the significance level is a rate, not a promise: clean data does report the odd outlier") + void falseAlarmsFollowTheSignificanceLevel() { + int reporting = 0; + for (long seed = 0; seed < 12; seed++) { + if (new GeneralizedEsd(0.05).findOutliers(gaussianSample(60, seed), 6).length > 0) { + reporting++; + } + } + + assertTrue(reporting > 0, "at five percent some clean sample is expected to report something"); + assertTrue(reporting <= 4, "but it must stay rare, " + reporting + " of 12 samples reported"); + } + + @Test + @DisplayName("a sample with no spread at all has no outliers") + void handlesAConstantSample() { + double[] sample = new double[20]; + Arrays.fill(sample, 3.0); + + assertEquals(0, new GeneralizedEsd().findOutliers(sample, 5).length); + } + + @Test + void leavesTheSampleUntouched() { + double[] sample = gaussianSample(30, 5L); + sample[3] = 500.0; + double[] copy = Arrays.copyOf(sample, sample.length); + + new GeneralizedEsd().findOutliers(sample, 4); + + Assertions.assertArrayEquals(copy, sample); + } + + @Test + @DisplayName("a stricter level reports at most as many outliers as a looser one") + void aStricterLevelIsMoreConservative() { + double[] sample = gaussianSample(50, 6L); + sample[5] = 125.0; + sample[6] = 126.0; + + int loose = new GeneralizedEsd(0.10).findOutliers(sample, 8).length; + int strict = new GeneralizedEsd(0.001).findOutliers(sample, 8).length; + + assertTrue(strict <= loose, "strict found " + strict + ", loose found " + loose); + } + + @Test + @DisplayName("the bound on the number of outliers is respected") + void neverReportsMoreThanTheBound() { + double[] sample = gaussianSample(40, 7L); + for (int i = 0; i < 6; i++) { + sample[i] = 300.0 + i; + } + + int[] outliers = new GeneralizedEsd().findOutliers(sample, 2); + + assertTrue(outliers.length <= 2); + } + + @Test + void findsOutliersOnBothSides() { + double[] sample = gaussianSample(40, 8L); + sample[4] = 300.0; + sample[24] = -150.0; + + int[] outliers = new GeneralizedEsd().findOutliers(sample, 4); + + assertArrayEqualsSorted(new int[] {4, 24}, outliers); + } + + @Test + void toStringMentionsTheLevel() { + String text = new GeneralizedEsd(0.05).toString(); + + assertTrue(text.contains("GeneralizedEsd")); + assertTrue(text.contains("0.05")); + } + + private static void assertArrayEqualsSorted(int[] expected, int[] actual) { + Assertions.assertArrayEquals(expected, actual, "expected " + Arrays.toString(expected) + " but got " + Arrays.toString(actual)); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/HampelFilterTest.java b/src/test/java/com/thealgorithms/streaming/HampelFilterTest.java new file mode 100644 index 000000000000..fe33656eb5da --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/HampelFilterTest.java @@ -0,0 +1,207 @@ +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.Assertions; +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 HampelFilterTest { + + private static double median(double[] values) { + double[] sorted = values.clone(); + Arrays.sort(sorted); + int middle = sorted.length / 2; + return sorted.length % 2 != 0 ? sorted[middle] : 0.5 * (sorted[middle - 1] + sorted[middle]); + } + + /** + * Median absolute deviation of the window ending at {@code index}, computed the obvious way. + */ + private static double bruteForceMad(double[] signal, int index, int windowSize) { + int from = Math.max(0, index - windowSize + 1); + double[] window = Arrays.copyOfRange(signal, from, index + 1); + double windowMedian = median(window); + double[] deviations = new double[window.length]; + for (int i = 0; i < window.length; i++) { + deviations[i] = Math.abs(window[i] - windowMedian); + } + return median(deviations); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void rejectsNonPositiveWindowSizes(int windowSize) { + assertThrows(IllegalArgumentException.class, () -> new HampelFilter(windowSize)); + } + + @ParameterizedTest + @ValueSource(doubles = {-1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsInvalidThresholds(double threshold) { + assertThrows(IllegalArgumentException.class, () -> new HampelFilter(5, threshold)); + } + + @Test + void queriesBeforeTheFirstSampleFail() { + HampelFilter filter = new HampelFilter(5); + assertEquals(5, filter.windowSize()); + assertEquals(HampelFilter.DEFAULT_THRESHOLD, filter.threshold()); + assertEquals(0, filter.size()); + assertEquals(0L, filter.outlierCount()); + assertFalse(filter.lastWasOutlier()); + assertThrows(IllegalStateException.class, filter::median); + assertThrows(IllegalStateException.class, filter::medianAbsoluteDeviation); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + HampelFilter filter = new HampelFilter(5); + assertThrows(IllegalArgumentException.class, () -> filter.accept(value)); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 5, 8, 15}) + @DisplayName("the median absolute deviation agrees with a brute force computation") + void madMatchesBruteForce(int windowSize) { + Random random = new Random(20240517L + windowSize); + double[] signal = new double[2_000]; + for (int i = 0; i < signal.length; i++) { + signal[i] = random.nextInt(100) - 50; + } + + HampelFilter filter = new HampelFilter(windowSize, 3.0); + for (int i = 0; i < signal.length; i++) { + filter.accept(signal[i]); + assertEquals(bruteForceMad(signal, i, windowSize), filter.medianAbsoluteDeviation(), 1e-12, "at index " + i); + } + } + + @Test + @DisplayName("a single spike is flagged and replaced by the local median") + void replacesASpike() { + HampelFilter filter = new HampelFilter(5, 3.0); + double[] signal = {5.0, 5.1, 4.9, 5.0, 5.05, 50.0, 5.0, 5.1}; + double[] filtered = filter.filter(signal); + + assertEquals(1L, filter.outlierCount()); + assertTrue(filtered[5] < 6.0, "the spike should have been replaced, but stayed at " + filtered[5]); + for (int i = 0; i < signal.length; i++) { + if (i != 5) { + assertEquals(signal[i], filtered[i], 0.0, "clean sample at index " + i + " must pass through"); + } + } + } + + @Test + void detectOutliersReportsWithoutChangingTheSignal() { + HampelFilter filter = new HampelFilter(5, 3.0); + double[] signal = {5.0, 5.1, 4.9, 5.0, 5.05, 50.0, 5.0, 5.1}; + boolean[] flags = filter.detectOutliers(signal); + + boolean[] expected = new boolean[signal.length]; + expected[5] = true; + Assertions.assertArrayEquals(expected, flags); + assertEquals(1L, filter.outlierCount()); + } + + @Test + @DisplayName("a clean ramp contains no outliers, since the MAD grows with the slope") + void leavesASmoothRampAlone() { + HampelFilter filter = new HampelFilter(5, 3.0); + double[] ramp = new double[200]; + for (int i = 0; i < ramp.length; i++) { + ramp[i] = i; + } + Assertions.assertArrayEquals(ramp, filter.filter(ramp), 0.0); + assertEquals(0L, filter.outlierCount()); + } + + @Test + void leavesAConstantSignalAlone() { + HampelFilter filter = new HampelFilter(7); + for (int i = 0; i < 100; i++) { + assertEquals(3.0, filter.accept(3.0)); + } + assertEquals(0L, filter.outlierCount()); + assertEquals(0.0, filter.medianAbsoluteDeviation()); + } + + @Test + @DisplayName("only a few samples of clean Gaussian noise are flagged") + void staysQuietOnCleanNoise() { + Random random = new Random(4242L); + HampelFilter filter = new HampelFilter(11, 3.0); + for (int i = 0; i < 5_000; i++) { + filter.accept(random.nextGaussian()); + } + assertTrue(filter.outlierCount() < 500, "flagged " + filter.outlierCount() + " of 5000 clean samples"); + } + + @Test + @DisplayName("spikes buried in noise are caught") + void catchesSpikesInNoisyData() { + Random random = new Random(7L); + double[] signal = new double[1_000]; + for (int i = 0; i < signal.length; i++) { + signal[i] = 20.0 + random.nextGaussian(); + } + int[] spikes = {100, 300, 700}; + for (int spike : spikes) { + signal[spike] = 200.0; + } + + HampelFilter filter = new HampelFilter(9, 3.0); + double[] filtered = filter.filter(signal); + for (int spike : spikes) { + assertTrue(filtered[spike] < 30.0, "spike at " + spike + " survived as " + filtered[spike]); + } + } + + @Test + @DisplayName("a zero MAD makes the identifier maximally strict") + void flagsAnyDeviationWhenTheMadIsZero() { + HampelFilter filter = new HampelFilter(5, 3.0); + for (int i = 0; i < 5; i++) { + filter.accept(5.0); + } + assertEquals(0.0, filter.medianAbsoluteDeviation()); + assertEquals(5.0, filter.accept(5.5)); + assertTrue(filter.lastWasOutlier()); + } + + @Test + void aThresholdOfZeroFlagsEverythingOffTheMedian() { + HampelFilter filter = new HampelFilter(3, 0.0); + filter.accept(1.0); + filter.accept(2.0); + assertTrue(filter.lastWasOutlier()); + assertEquals(1.5, filter.median(), 1e-12); + } + + @Test + void resetForgetsTheWindowAndTheCounters() { + HampelFilter filter = new HampelFilter(5, 3.0); + filter.filter(new double[] {5.0, 5.1, 4.9, 5.0, 5.05, 50.0}); + assertEquals(1L, filter.outlierCount()); + + filter.reset(); + assertEquals(0, filter.size()); + assertEquals(0L, filter.outlierCount()); + assertFalse(filter.lastWasOutlier()); + assertThrows(IllegalStateException.class, filter::median); + } + + @Test + void toStringMentionsTheState() { + HampelFilter filter = new HampelFilter(5, 2.5); + assertTrue(filter.toString().contains("threshold=2.5"), filter.toString()); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/KalmanFilterTest.java b/src/test/java/com/thealgorithms/streaming/KalmanFilterTest.java new file mode 100644 index 000000000000..14037eb79476 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/KalmanFilterTest.java @@ -0,0 +1,229 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 KalmanFilterTest { + + private static double rootMeanSquareError(double[] values, double truth) { + double sum = 0.0; + for (double value : values) { + sum += (value - truth) * (value - truth); + } + return Math.sqrt(sum / values.length); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsNonFiniteInitialEstimate(double initialEstimate) { + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(initialEstimate, 1.0, 0.1, 1.0)); + } + + @Test + void rejectsInvalidVariances() { + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, -1.0, 0.1, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, 1.0, -0.1, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, 1.0, 0.1, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, 1.0, 0.1, Double.NaN)); + } + + @Test + void rejectsNonFiniteInputs() { + KalmanFilter filter = new KalmanFilter(0.0, 1.0, 0.1, 1.0); + assertThrows(IllegalArgumentException.class, () -> filter.predict(Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> filter.update(Double.POSITIVE_INFINITY)); + assertThrows(IllegalArgumentException.class, () -> filter.update(1.0, 0.0)); + } + + @Test + @DisplayName("prediction adds uncertainty, correction removes it") + void covarianceGrowsOnPredictAndShrinksOnUpdate() { + KalmanFilter filter = new KalmanFilter(0.0, 1.0, 0.25, 1.0); + assertEquals(0.0, filter.lastGain()); + + filter.predict(); + assertEquals(1.25, filter.errorCovariance(), 1e-12); + + filter.update(2.0); + assertTrue(filter.errorCovariance() < 1.25); + assertTrue(filter.lastGain() > 0.0 && filter.lastGain() < 1.0); + } + + @Test + @DisplayName("the gain is the share of the measurement that gets believed") + void gainFollowsTheClosedForm() { + KalmanFilter filter = new KalmanFilter(0.0, 1.0, 0.0, 1.0); + filter.update(10.0); + assertEquals(0.5, filter.lastGain(), 1e-12); + assertEquals(5.0, filter.estimate(), 1e-12); + assertEquals(0.5, filter.errorCovariance(), 1e-12); + } + + @Test + void aControlInputShiftsTheEstimate() { + KalmanFilter filter = new KalmanFilter(100.0, 1.0, 0.1, 1.0); + assertEquals(103.0, filter.predict(3.0), 1e-12); + assertEquals(103.0, filter.estimate(), 1e-12); + } + + @Test + @DisplayName("with no process noise and no prior information the filter is the running mean") + void degeneratesIntoTheRunningMean() { + KalmanFilter filter = new KalmanFilter(0.0, 1e12, 0.0, 1.0); + Random random = new Random(2024L); + + double sum = 0.0; + for (int i = 1; i <= 200; i++) { + double measurement = random.nextGaussian(); + sum += measurement; + filter.update(measurement); + assertEquals(sum / i, filter.estimate(), 1e-6, "after " + i + " measurements"); + } + } + + @Test + @DisplayName("filtering a noisy constant beats using the raw measurements") + void reducesNoiseOnAConstantSignal() { + double truth = 12.5; + double noise = 2.0; + Random random = new Random(555L); + + double[] measurements = new double[400]; + for (int i = 0; i < measurements.length; i++) { + measurements[i] = truth + noise * random.nextGaussian(); + } + + KalmanFilter filter = new KalmanFilter(0.0, 100.0, 1e-4, noise * noise); + double[] filtered = filter.filter(measurements); + + assertEquals(measurements.length, filtered.length); + assertEquals(truth, filter.estimate(), 0.3); + assertTrue(rootMeanSquareError(filtered, truth) < rootMeanSquareError(measurements, truth) / 2.0); + } + + @Test + @DisplayName("a moving quantity is tracked, and more process noise means faster tracking") + void tracksAMovingSignal() { + Random random = new Random(31L); + double[] measurements = new double[200]; + for (int i = 0; i < measurements.length; i++) { + measurements[i] = i * 0.5 + random.nextGaussian(); + } + + KalmanFilter agile = new KalmanFilter(0.0, 1.0, 1.0, 1.0); + KalmanFilter sluggish = new KalmanFilter(0.0, 1.0, 1e-6, 1.0); + agile.filter(measurements); + sluggish.filter(measurements); + + double truth = 199 * 0.5; + assertTrue(Math.abs(agile.estimate() - truth) < Math.abs(sluggish.estimate() - truth), "agile=" + agile.estimate() + " sluggish=" + sluggish.estimate()); + assertEquals(truth, agile.estimate(), 3.0); + } + + @Test + @DisplayName("updating once per sensor reproduces the closed form inverse-variance fusion") + void sequentialUpdatesFuseSensors() { + double preciseReading = 10.0; + double preciseVariance = 0.25; + double coarseReading = 14.0; + double coarseVariance = 4.0; + + KalmanFilter filter = new KalmanFilter(0.0, 1e15, 0.0, 1.0); + filter.update(preciseReading, preciseVariance); + filter.update(coarseReading, coarseVariance); + + KalmanFilter.Estimate fused = KalmanFilter.fuse(new double[] {preciseReading, coarseReading}, new double[] {preciseVariance, coarseVariance}); + assertEquals(fused.value(), filter.estimate(), 1e-6); + assertEquals(fused.variance(), filter.errorCovariance(), 1e-6); + } + + @Test + void fusionWeightsSensorsByTheirPrecision() { + KalmanFilter.Estimate equal = KalmanFilter.fuse(new double[] {1.0, 3.0}, new double[] {1.0, 1.0}); + assertEquals(2.0, equal.value(), 1e-12); + assertEquals(0.5, equal.variance(), 1e-12); + + KalmanFilter.Estimate skewed = KalmanFilter.fuse(new double[] {1.0, 3.0}, new double[] {0.01, 1.0}); + assertTrue(skewed.value() < 1.1, "the precise sensor should dominate, but got " + skewed.value()); + assertTrue(skewed.variance() < 0.01, "fusing can only reduce the variance, but got " + skewed.variance()); + } + + @Test + void fusionOfASingleSensorReturnsIt() { + KalmanFilter.Estimate single = KalmanFilter.fuse(new double[] {7.0}, new double[] {2.0}); + assertEquals(7.0, single.value(), 1e-12); + assertEquals(2.0, single.variance(), 1e-12); + } + + @Test + void fusionRejectsMalformedInput() { + assertThrows(IllegalArgumentException.class, () -> KalmanFilter.fuse(new double[0], new double[0])); + assertThrows(IllegalArgumentException.class, () -> KalmanFilter.fuse(new double[] {1.0}, new double[] {1.0, 2.0})); + assertThrows(IllegalArgumentException.class, () -> KalmanFilter.fuse(new double[] {1.0}, new double[] {0.0})); + assertThrows(IllegalArgumentException.class, () -> KalmanFilter.fuse(new double[] {Double.NaN}, new double[] {1.0})); + } + + @Test + @DisplayName("fusing two sensors is better than trusting either of them alone") + void fusionBeatsEitherSensorAlone() { + double truth = 3.0; + Random random = new Random(97L); + double barometerNoise = 1.0; + double gpsNoise = 3.0; + + KalmanFilter filter = new KalmanFilter(0.0, 100.0, 1e-6, barometerNoise * barometerNoise); + double[] fused = new double[300]; + KalmanFilter barometerFilter = new KalmanFilter(0.0, 100.0, 1e-6, barometerNoise * barometerNoise); + + for (int i = 0; i < fused.length; i++) { + double barometer = truth + barometerNoise * random.nextGaussian(); + double gps = truth + gpsNoise * random.nextGaussian(); + + filter.predict(); + filter.update(barometer, barometerNoise * barometerNoise); + filter.update(gps, gpsNoise * gpsNoise); + fused[i] = filter.estimate(); + + barometerFilter.predict(); + barometerFilter.update(barometer); + } + + assertTrue(filter.errorCovariance() < barometerFilter.errorCovariance(), "fusing must not increase the uncertainty"); + assertEquals(truth, fused[fused.length - 1], 0.5); + } + + @Test + void resetRestoresTheInitialState() { + KalmanFilter filter = new KalmanFilter(5.0, 2.0, 0.1, 1.0); + assertEquals(0.1, filter.processNoise()); + assertEquals(1.0, filter.measurementNoise()); + + filter.filter(100.0); + filter.reset(); + + assertEquals(5.0, filter.estimate()); + assertEquals(2.0, filter.errorCovariance()); + assertEquals(0.0, filter.lastGain()); + } + + @Test + void toStringMentionsTheState() { + KalmanFilter filter = new KalmanFilter(1.0, 1.0, 0.1, 1.0); + assertTrue(filter.toString().contains("estimate=1.0"), filter.toString()); + } + + @Test + void rejectsNonFiniteVariances() { + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, Double.NaN, 0.1, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, Double.POSITIVE_INFINITY, 0.1, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, 1.0, Double.NaN, 1.0)); + assertThrows(IllegalArgumentException.class, () -> new KalmanFilter(0.0, 1.0, 0.1, Double.POSITIVE_INFINITY)); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/MedianFilterTest.java b/src/test/java/com/thealgorithms/streaming/MedianFilterTest.java new file mode 100644 index 000000000000..a176000022cc --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/MedianFilterTest.java @@ -0,0 +1,172 @@ +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.Assertions; +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 MedianFilterTest { + + /** + * Median of the last {@code windowSize} samples of {@code signal} ending at {@code index}, + * computed the obvious way. + */ + private static double bruteForceMedian(double[] signal, int index, int windowSize) { + int from = Math.max(0, index - windowSize + 1); + double[] window = Arrays.copyOfRange(signal, from, index + 1); + Arrays.sort(window); + int middle = window.length / 2; + return window.length % 2 != 0 ? window[middle] : 0.5 * (window[middle - 1] + window[middle]); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, Integer.MIN_VALUE}) + void rejectsNonPositiveWindowSizes(int windowSize) { + assertThrows(IllegalArgumentException.class, () -> new MedianFilter(windowSize)); + } + + @Test + void queriesBeforeTheFirstSampleFail() { + MedianFilter filter = new MedianFilter(3); + assertTrue(filter.isEmpty()); + assertFalse(filter.isFull()); + assertEquals(0, filter.size()); + assertEquals(3, filter.windowSize()); + assertEquals(0, filter.sortedWindow().length); + assertThrows(IllegalStateException.class, filter::median); + assertThrows(IllegalStateException.class, filter::min); + assertThrows(IllegalStateException.class, filter::max); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + MedianFilter filter = new MedianFilter(3); + assertThrows(IllegalArgumentException.class, () -> filter.accept(value)); + } + + @Test + @DisplayName("before the window fills up the median is taken over what has arrived") + void warmsUpGracefully() { + MedianFilter filter = new MedianFilter(5); + assertEquals(4.0, filter.accept(4.0)); + assertEquals(3.0, filter.accept(2.0), 1e-12); + assertEquals(4.0, filter.accept(9.0), 1e-12); + assertEquals(3, filter.size()); + assertFalse(filter.isFull()); + } + + @Test + void aWindowOfOneIsTheIdentity() { + MedianFilter filter = new MedianFilter(1); + assertEquals(5.0, filter.accept(5.0)); + assertEquals(-3.0, filter.accept(-3.0)); + assertTrue(filter.isFull()); + assertEquals(-3.0, filter.min()); + assertEquals(-3.0, filter.max()); + } + + @Test + @DisplayName("an even window averages the two middle samples") + void handlesEvenWindows() { + MedianFilter filter = new MedianFilter(4); + filter.accept(1.0); + filter.accept(2.0); + filter.accept(3.0); + assertEquals(2.5, filter.accept(4.0), 1e-12); + assertEquals(3.5, filter.accept(5.0), 1e-12); + } + + @Test + @DisplayName("a lone spike is removed, an edge is preserved") + void removesSpikesButKeepsEdges() { + MedianFilter filter = new MedianFilter(3); + double[] signal = {1.0, 1.0, 100.0, 1.0, 1.0, 9.0, 9.0, 9.0}; + double[] filtered = filter.filter(signal); + Assertions.assertArrayEquals(new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 9.0, 9.0}, filtered, 1e-12); + } + + @Test + void reportsTheExtremesOfTheWindow() { + MedianFilter filter = new MedianFilter(3); + filter.filter(new double[] {5.0, 1.0, 9.0}); + assertEquals(1.0, filter.min()); + assertEquals(9.0, filter.max()); + Assertions.assertArrayEquals(new double[] {1.0, 5.0, 9.0}, filter.sortedWindow(), 0.0); + + filter.accept(7.0); + assertEquals(1.0, filter.min()); + assertEquals(9.0, filter.max()); + Assertions.assertArrayEquals(new double[] {1.0, 7.0, 9.0}, filter.sortedWindow(), 0.0); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 7, 16, 31}) + @DisplayName("agrees with a brute force median for every window size") + void matchesBruteForce(int windowSize) { + Random random = new Random(20240517L + windowSize); + double[] signal = new double[3_000]; + for (int i = 0; i < signal.length; i++) { + signal[i] = random.nextInt(50); + } + + MedianFilter filter = new MedianFilter(windowSize); + for (int i = 0; i < signal.length; i++) { + assertEquals(bruteForceMedian(signal, i, windowSize), filter.accept(signal[i]), 1e-12, "at index " + i); + assertEquals(Math.min(i + 1, windowSize), filter.size()); + } + } + + @Test + @DisplayName("handles duplicates, which is where a sorted mirror of the window can go wrong") + void handlesHeavyDuplication() { + Random random = new Random(11L); + double[] signal = new double[2_000]; + for (int i = 0; i < signal.length; i++) { + signal[i] = random.nextInt(3); + } + + MedianFilter filter = new MedianFilter(5); + for (int i = 0; i < signal.length; i++) { + assertEquals(bruteForceMedian(signal, i, 5), filter.accept(signal[i]), 1e-12, "at index " + i); + } + } + + @Test + void resetEmptiesTheWindow() { + MedianFilter filter = new MedianFilter(3); + filter.filter(new double[] {1.0, 2.0, 3.0}); + filter.reset(); + + assertTrue(filter.isEmpty()); + assertThrows(IllegalStateException.class, filter::median); + assertEquals(8.0, filter.accept(8.0)); + } + + @Test + void toStringMentionsTheState() { + MedianFilter filter = new MedianFilter(3); + assertTrue(filter.toString().contains("size=0"), filter.toString()); + filter.accept(2.0); + assertTrue(filter.toString().contains("median=2.0"), filter.toString()); + } + + @Test + void reportsWhetherItHoldsSamples() { + MedianFilter filter = new MedianFilter(2); + assertTrue(filter.isEmpty()); + filter.accept(1.0); + assertFalse(filter.isEmpty()); + assertFalse(filter.isFull()); + filter.accept(2.0); + assertTrue(filter.isFull()); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/P2QuantileEstimatorTest.java b/src/test/java/com/thealgorithms/streaming/P2QuantileEstimatorTest.java new file mode 100644 index 000000000000..777e00d62d66 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/P2QuantileEstimatorTest.java @@ -0,0 +1,206 @@ +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 P2QuantileEstimatorTest { + + /** + * Exact quantile of a sample, by linear interpolation between the surrounding order statistics. + */ + private static double exactQuantile(double[] sortedValues, double probability) { + double rank = probability * (sortedValues.length - 1); + int lower = (int) Math.floor(rank); + int upper = Math.min(lower + 1, sortedValues.length - 1); + return sortedValues[lower] + (rank - lower) * (sortedValues[upper] - sortedValues[lower]); + } + + private static double[] shuffledUniformSample(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 = {0.0, 1.0, -0.1, 1.5, Double.NaN}) + void rejectsInvalidProbabilities(double probability) { + assertThrows(IllegalArgumentException.class, () -> new P2QuantileEstimator(probability)); + } + + @Test + void queriesBeforeTheFirstSampleFail() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + assertTrue(estimator.isEmpty()); + assertEquals(0L, estimator.count()); + assertEquals(0.5, estimator.probability()); + assertThrows(IllegalStateException.class, estimator::quantile); + assertThrows(IllegalStateException.class, estimator::min); + assertThrows(IllegalStateException.class, estimator::max); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + assertThrows(IllegalArgumentException.class, () -> estimator.add(value)); + } + + @Test + @DisplayName("the first five samples are answered exactly") + void isExactDuringWarmUp() { + P2QuantileEstimator median = new P2QuantileEstimator(0.5); + median.add(3.0); + assertEquals(3.0, median.quantile()); + median.add(1.0); + assertEquals(2.0, median.quantile(), 1e-12); + median.add(2.0); + assertEquals(2.0, median.quantile(), 1e-12); + median.add(10.0); + assertEquals(2.5, median.quantile(), 1e-12); + assertEquals(1.0, median.min()); + assertEquals(10.0, median.max()); + assertEquals(4L, median.count()); + } + + @Test + void tracksTheExtremesExactly() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + double[] values = shuffledUniformSample(5_000, 7L); + estimator.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + assertEquals(sorted[0], estimator.min(), 0.0); + assertEquals(sorted[sorted.length - 1], estimator.max(), 0.0); + assertEquals(5_000L, estimator.count()); + } + + @Test + void aConstantStreamHasAConstantQuantile() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.9); + for (int i = 0; i < 1_000; i++) { + estimator.add(7.0); + } + assertEquals(7.0, estimator.quantile(), 1e-12); + } + + @ParameterizedTest + @ValueSource(doubles = {0.05, 0.25, 0.5, 0.75, 0.9, 0.99}) + @DisplayName("estimates a uniform stream within a percent of the exact quantile") + void approximatesUniformQuantiles(double probability) { + double[] values = shuffledUniformSample(100_000, 20240517L); + P2QuantileEstimator estimator = new P2QuantileEstimator(probability); + estimator.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + assertEquals(exactQuantile(sorted, probability), estimator.quantile(), 0.01); + } + + @Test + @DisplayName("estimates the tail of a skewed stream") + void approximatesTheTailOfAnExponentialStream() { + Random random = new Random(31337L); + double[] values = new double[100_000]; + for (int i = 0; i < values.length; i++) { + values[i] = -Math.log(1.0 - random.nextDouble()) * 10.0; + } + + P2QuantileEstimator estimator = new P2QuantileEstimator(0.95); + estimator.addAll(values); + + double[] sorted = values.clone(); + Arrays.sort(sorted); + double expected = exactQuantile(sorted, 0.95); + assertEquals(expected, estimator.quantile(), 0.05 * expected); + } + + @Test + @DisplayName("stays sane even when the stream arrives already sorted, the worst case for P-square") + void handlesMonotonicInput() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + for (int i = 1; i <= 10_000; i++) { + estimator.add(i); + } + assertEquals(5_000.0, estimator.quantile(), 500.0); + assertEquals(1.0, estimator.min()); + assertEquals(10_000.0, estimator.max()); + } + + @Test + void estimatesOfDifferentQuantilesStayOrdered() { + double[] values = shuffledUniformSample(50_000, 99L); + P2QuantileEstimator low = new P2QuantileEstimator(0.25); + P2QuantileEstimator middle = new P2QuantileEstimator(0.5); + P2QuantileEstimator high = new P2QuantileEstimator(0.75); + low.addAll(values); + middle.addAll(values); + high.addAll(values); + + assertTrue(low.quantile() < middle.quantile()); + assertTrue(middle.quantile() < high.quantile()); + } + + @Test + void resetForgetsEverything() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + estimator.addAll(shuffledUniformSample(1_000, 5L)); + estimator.reset(); + + assertTrue(estimator.isEmpty()); + assertThrows(IllegalStateException.class, estimator::quantile); + + estimator.addAll(1.0, 2.0, 3.0); + assertEquals(2.0, estimator.quantile(), 1e-12); + } + + @Test + void toStringMentionsTheEstimate() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + assertTrue(estimator.toString().contains("count=0"), estimator.toString()); + estimator.addAll(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + assertFalse(estimator.isEmpty()); + assertTrue(estimator.toString().contains("quantile="), estimator.toString()); + } + + @Test + @DisplayName("violent jumps force the parabolic prediction to fall back to a linear one") + void survivesWildlyJumpingInput() { + P2QuantileEstimator estimator = new P2QuantileEstimator(0.5); + double value = 1.0; + for (int i = 0; i < 5_000; i++) { + value = i % 2 == 0 ? value * 3.0 + 1.0 : 1.0 / (i + 1); + estimator.add(value); + assertTrue(estimator.quantile() >= estimator.min(), "the estimate fell below the minimum"); + assertTrue(estimator.quantile() <= estimator.max(), "the estimate rose above the maximum"); + } + } + + @ParameterizedTest + @ValueSource(doubles = {0.01, 0.5, 0.99}) + @DisplayName("lands exactly on the textbook quantile of a ramp, which pins the marker convention") + void isExactOnARamp(double probability) { + // The markers are numbered from one, as in the paper. That origin is arithmetically irrelevant + // - only differences of positions are ever used - but the desired positions are grown by + // repeated addition, so a different origin rounds differently and flips the strict comparisons + // that pick between the parabolic and the linear prediction. On a ramp, whose quantiles are + // whole numbers, that shows up as an answer off by one. + P2QuantileEstimator estimator = new P2QuantileEstimator(probability); + for (int i = 0; i <= 1_700; i++) { + estimator.add(i); + } + assertEquals(probability * 1_700, estimator.quantile(), 1e-9); + } +} 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; + } + } +} diff --git a/src/test/java/com/thealgorithms/streaming/UnscentedKalmanFilterTest.java b/src/test/java/com/thealgorithms/streaming/UnscentedKalmanFilterTest.java new file mode 100644 index 000000000000..fa35d48af8fd --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/UnscentedKalmanFilterTest.java @@ -0,0 +1,286 @@ +package com.thealgorithms.streaming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 UnscentedKalmanFilterTest { + + private static final double OFFSET = 5.0; + private static final double INTERVAL = 0.1; + private static final double[][] IDENTITY_2 = {{1.0, 0.0}, {0.0, 1.0}}; + + private static double range(double position) { + return Math.hypot(position, OFFSET); + } + + @Test + void rejectsAnEmptyOrMalformedStart() { + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[0], new double[0][0])); + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {1.0, 2.0}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {Double.NaN}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {1.0}, new double[][] {{Double.POSITIVE_INFINITY}})); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY}) + void rejectsAnInvalidAlpha(double alpha) { + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}, alpha, 2.0, 0.0)); + } + + @Test + void rejectsAnInvalidBetaOrKappa() { + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}, 1.0, -0.5, 0.0)); + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}, 1.0, 2.0, Double.NaN)); + } + + @Test + @DisplayName("parameters that collapse the sigma points onto the mean are refused") + void rejectsParametersThatLeaveNoSpread() { + assertThrows(IllegalArgumentException.class, () -> new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}, 1.0, 2.0, -1.0)); + } + + @ParameterizedTest + @ValueSource(doubles = {0.001, 0.5, 1.0, 2.0}) + @DisplayName("the mean weights sum to one for any spread") + void theMeanWeightsSumToOne(double alpha) { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0, 0.0, 0.0}, new double[][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}, alpha, 2.0, 0.0); + + double sum = 0.0; + for (double weight : filter.meanWeights()) { + sum += weight; + } + + assertEquals(1.0, sum, 1e-9); + assertEquals(7, filter.meanWeights().length); + assertEquals(7, filter.covarianceWeights().length); + } + + @Test + @DisplayName("the sigma points carry exactly the mean and the covariance of the estimate") + void theSigmaPointsReproduceTheEstimate() { + double[] mean = {1.0, -2.0}; + double[][] covariance = {{2.0, 0.6}, {0.6, 1.0}}; + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(mean, covariance); + + double[][] points = filter.sigmaPoints(); + double[] meanWeights = filter.meanWeights(); + double[] covarianceWeights = filter.covarianceWeights(); + + double[] recovered = new double[2]; + for (int i = 0; i < points.length; i++) { + for (int j = 0; j < 2; j++) { + recovered[j] += meanWeights[i] * points[i][j]; + } + } + double[][] spread = new double[2][2]; + for (int i = 0; i < points.length; i++) { + for (int r = 0; r < 2; r++) { + for (int c = 0; c < 2; c++) { + spread[r][c] += covarianceWeights[i] * (points[i][r] - mean[r]) * (points[i][c] - mean[c]); + } + } + } + + assertEquals(5, points.length); + for (int r = 0; r < 2; r++) { + assertEquals(mean[r], recovered[r], 1e-12); + for (int c = 0; c < 2; c++) { + assertEquals(covariance[r][c], spread[r][c], 1e-12); + } + } + } + + @Test + @DisplayName("the mean and the variance of x squared come out exact, where the extended filter misses both") + void isExactOnAQuadratic() { + double mu = 3.0; + double sigma = 0.5; + double trueMean = mu * mu + sigma * sigma; + double trueVariance = 4 * mu * mu * sigma * sigma + 2 * Math.pow(sigma, 4); + + UnscentedKalmanFilter unscented = new UnscentedKalmanFilter(new double[] {mu}, new double[][] {{sigma * sigma}}); + unscented.predict(state -> new double[] {state[0] * state[0]}, new double[][] {{0.0}}); + + ExtendedKalmanFilter extended = new ExtendedKalmanFilter(new double[] {mu}, new double[][] {{sigma * sigma}}); + extended.predict(state -> new double[] {state[0] * state[0]}, state -> new double[][] {{2 * state[0]}}, new double[][] {{0.0}}); + + assertEquals(trueMean, unscented.state()[0], 1e-12); + assertEquals(trueVariance, unscented.variance(0), 1e-12); + assertEquals(mu * mu, extended.state()[0], 1e-12, "the extended filter drops the sigma squared the spread contributes"); + assertEquals(4 * mu * mu * sigma * sigma, extended.variance(0), 1e-12, "and the 2 sigma^4 of the variance"); + } + + @Test + @DisplayName("on a linear model it is the Kalman filter, step for step") + void agreesWithTheKalmanFilterOnALinearModel() { + double[][] transition = {{1.0, INTERVAL}, {0.0, 1.0}}; + double[][] processNoise = {{1e-3, 0.0}, {0.0, 1e-3}}; + double[][] measurementNoise = {{0.25}}; + UnscentedKalmanFilter unscented = new UnscentedKalmanFilter(new double[] {0.0, 1.0}, IDENTITY_2); + ExtendedKalmanFilter extended = new ExtendedKalmanFilter(new double[] {0.0, 1.0}, IDENTITY_2); + Random random = new Random(3L); + + for (int step = 0; step < 500; step++) { + unscented.predict(state -> new double[] {state[0] + INTERVAL * state[1], state[1]}, processNoise); + extended.predict(transition, processNoise); + + double measurement = 0.1 * step + 0.5 * random.nextGaussian(); + unscented.update(new double[] {measurement}, state -> new double[] {state[0]}, measurementNoise); + extended.update(new double[] {measurement}, new double[][] {{1.0, 0.0}}, measurementNoise); + + for (int i = 0; i < 2; i++) { + assertEquals(extended.state()[i], unscented.state()[i], 1e-9, "state at step " + step); + for (int j = 0; j < 2; j++) { + assertEquals(extended.covariance()[i][j], unscented.covariance()[i][j], 1e-9, "covariance at step " + step); + } + } + } + } + + @Test + @DisplayName("a target seen only as a range is tracked without writing a single derivative") + void tracksATargetThroughARangeOnlySensor() { + Random random = new Random(17L); + double[][] processNoise = {{1e-6, 0.0}, {0.0, 1e-4}}; + double[][] measurementNoise = {{0.04}}; + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {1.0, 0.5}, new double[][] {{4.0, 0.0}, {0.0, 4.0}}); + + double position = 2.0; + double velocity = 1.0; + double error = 0.0; + int counted = 0; + + for (int step = 0; step < 400; step++) { + position += velocity * INTERVAL; + double measurement = range(position) + 0.2 * random.nextGaussian(); + + filter.predict(state -> new double[] {state[0] + INTERVAL * state[1], state[1]}, processNoise); + filter.update(new double[] {measurement}, state -> new double[] {range(state[0])}, measurementNoise); + + if (step > 200) { + error += Math.abs(filter.state()[0] - position); + counted++; + } + } + + assertTrue(error / counted < 0.2, "the filtered position was off by " + error / counted); + assertEquals(velocity, filter.state()[1], 0.2, "the velocity is never measured, only inferred"); + } + + @Test + @DisplayName("the covariance stays symmetric and its variances positive over a long nonlinear run") + void keepsTheCovarianceHealthy() { + Random random = new Random(29L); + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {2.0, 1.0}, IDENTITY_2); + double[][] processNoise = {{1e-4, 0.0}, {0.0, 1e-4}}; + double[][] measurementNoise = {{0.1}}; + + for (int step = 0; step < 500; step++) { + filter.predict(state -> new double[] {state[0] + INTERVAL * Math.sin(state[1]), state[1]}, processNoise); + filter.update(new double[] {range(2.0 + 0.01 * step) + 0.3 * random.nextGaussian()}, state -> new double[] {range(state[0])}, measurementNoise); + + double[][] covariance = filter.covariance(); + assertEquals(covariance[0][1], covariance[1][0], 1e-15, "the covariance lost its symmetry at step " + step); + assertTrue(covariance[0][0] > 0.0 && covariance[1][1] > 0.0, "a variance stopped being positive at step " + step); + } + } + + @Test + @DisplayName("prediction through the identity only adds the process noise") + void predictionAloneAddsTheProcessNoise() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {3.0}, new double[][] {{1.0}}); + + for (int step = 1; step <= 10; step++) { + filter.predict(state -> state.clone(), new double[][] {{0.5}}); + + assertEquals(3.0, filter.state()[0], 1e-12); + assertEquals(1.0 + 0.5 * step, filter.variance(0), 1e-12); + } + assertEquals(10, filter.predictionCount()); + assertEquals(0, filter.updateCount()); + } + + @Test + @DisplayName("a covariance that is not positive definite has no square root, and the step is refused") + void refusesACovarianceThatIsNotPositiveDefinite() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0, 0.0}, new double[][] {{1.0, 2.0}, {2.0, 1.0}}); + + assertThrows(ArithmeticException.class, filter::sigmaPoints); + assertThrows(ArithmeticException.class, () -> filter.predict(state -> state, IDENTITY_2)); + } + + @Test + @DisplayName("a measurement that carries no information is reported rather than dividing by zero") + void refusesASingularInnovationCovariance() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}); + + assertThrows(ArithmeticException.class, () -> filter.update(new double[] {1.0}, state -> new double[] {1.0}, new double[][] {{0.0}})); + } + + @Test + void rejectsAModelThatMisbehaves() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}); + + assertThrows(IllegalArgumentException.class, () -> filter.predict(state -> new double[] {1.0, 2.0}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.predict(state -> new double[] {Double.NaN}, new double[][] {{1.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.predict(state -> state, new double[][] {{1.0, 0.0}})); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[0], state -> state, new double[0][0])); + assertThrows(IllegalArgumentException.class, () -> filter.update(new double[] {1.0}, state -> new double[] {1.0, 2.0}, new double[][] {{1.0}})); + } + + @Test + void theStateAndCovarianceAreCopiedOut() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {1.0}, new double[][] {{2.0}}); + + filter.state()[0] = 99.0; + filter.covariance()[0][0] = 99.0; + filter.meanWeights()[0] = 99.0; + + assertEquals(1.0, filter.state()[0], 1e-12); + assertEquals(2.0, filter.variance(0), 1e-12); + assertTrue(filter.meanWeights()[0] < 1.0); + } + + @Test + void exposesItsConfiguration() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0, 0.0}, IDENTITY_2, 0.5, 2.0, 1.0); + + assertEquals(2, filter.stateSize()); + assertEquals(0.5, filter.alpha()); + assertEquals(2.0, filter.beta()); + assertEquals(1.0, filter.kappa()); + assertEquals(0, filter.lastInnovation().length); + assertThrows(IllegalArgumentException.class, () -> filter.variance(2)); + } + + @Test + void resetRestartsFromAKnownEstimate() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {0.0}, new double[][] {{1.0}}); + filter.update(new double[] {5.0}, state -> state.clone(), new double[][] {{1.0}}); + + filter.reset(new double[] {-2.0}, new double[][] {{9.0}}); + + assertEquals(-2.0, filter.state()[0], 1e-12); + assertEquals(9.0, filter.variance(0), 1e-12); + assertEquals(0, filter.updateCount()); + assertEquals(0, filter.lastInnovation().length); + } + + @Test + void toStringMentionsTheState() { + UnscentedKalmanFilter filter = new UnscentedKalmanFilter(new double[] {1.5}, new double[][] {{1.0}}); + + String text = filter.toString(); + + assertTrue(text.contains("UnscentedKalmanFilter")); + assertTrue(text.contains("1.5")); + assertTrue(text.contains("beta=2.0")); + } +} diff --git a/src/test/java/com/thealgorithms/streaming/WelfordAlgorithmTest.java b/src/test/java/com/thealgorithms/streaming/WelfordAlgorithmTest.java new file mode 100644 index 000000000000..5031e9de6e62 --- /dev/null +++ b/src/test/java/com/thealgorithms/streaming/WelfordAlgorithmTest.java @@ -0,0 +1,243 @@ +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.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 WelfordAlgorithmTest { + + private static final double[] TEXTBOOK_SAMPLE = {2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0}; + + private static double twoPassPopulationVariance(double... values) { + double mean = 0.0; + for (double value : values) { + mean += value; + } + mean /= values.length; + + double sumOfSquares = 0.0; + for (double value : values) { + sumOfSquares += (value - mean) * (value - mean); + } + return sumOfSquares / values.length; + } + + @Test + void emptyAccumulatorReportsNothing() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + assertTrue(stats.isEmpty()); + assertEquals(0L, stats.count()); + assertEquals(0.0, stats.sum()); + assertEquals(0.0, stats.sumOfSquaredDeviations()); + assertTrue(Double.isNaN(stats.mean())); + assertTrue(Double.isNaN(stats.populationVariance())); + assertTrue(Double.isNaN(stats.sampleVariance())); + assertTrue(Double.isNaN(stats.sampleStandardDeviation())); + assertTrue(Double.isNaN(stats.standardError())); + } + + @Test + void singleSampleHasNoSpread() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.add(42.0); + assertFalse(stats.isEmpty()); + assertEquals(1L, stats.count()); + assertEquals(42.0, stats.mean()); + assertEquals(42.0, stats.sum()); + assertEquals(0.0, stats.populationVariance()); + assertTrue(Double.isNaN(stats.sampleVariance())); + } + + @Test + @DisplayName("reproduces the textbook values of a known sample") + void matchesKnownValues() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(TEXTBOOK_SAMPLE); + + assertEquals(8L, stats.count()); + assertEquals(5.0, stats.mean(), 1e-12); + assertEquals(40.0, stats.sum(), 1e-12); + assertEquals(32.0, stats.sumOfSquaredDeviations(), 1e-12); + assertEquals(4.0, stats.populationVariance(), 1e-12); + assertEquals(2.0, stats.populationStandardDeviation(), 1e-12); + assertEquals(32.0 / 7.0, stats.sampleVariance(), 1e-12); + assertEquals(Math.sqrt(32.0 / 7.0), stats.sampleStandardDeviation(), 1e-12); + assertEquals(Math.sqrt(32.0 / 7.0 / 8.0), stats.standardError(), 1e-12); + } + + @Test + @DisplayName("stays accurate where the naive sum of squares collapses") + void survivesLargeOffsets() { + double offset = 1e9; + double[] values = {offset + 4.0, offset + 7.0, offset + 13.0, offset + 16.0}; + + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(values); + + // The naive formula computes sum(x^2) - n * mean^2 on numbers of order 1e18 and loses every + // significant digit of the answer; Welford's recurrence never forms them. + double naive = 0.0; + for (double value : values) { + naive += value * value; + } + naive = (naive - values.length * stats.mean() * stats.mean()) / (values.length - 1); + + assertEquals(30.0, stats.sampleVariance(), 1e-6); + assertTrue(Math.abs(naive - 30.0) > 1.0, "the naive formula is expected to be far off, but returned " + naive); + } + + @ParameterizedTest + @ValueSource(ints = {2, 3, 10, 1_000}) + void agreesWithATwoPassComputation(int sampleCount) { + Random random = new Random(4242L + sampleCount); + double[] values = new double[sampleCount]; + for (int i = 0; i < sampleCount; i++) { + values[i] = random.nextGaussian() * 17.0 + 3.0; + } + + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(values); + + double expectedVariance = twoPassPopulationVariance(values); + assertEquals(expectedVariance, stats.populationVariance(), 1e-9 * Math.max(1.0, expectedVariance)); + } + + @Test + void removeUndoesAdd() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(TEXTBOOK_SAMPLE); + stats.add(1000.0); + stats.remove(1000.0); + + assertEquals(8L, stats.count()); + assertEquals(5.0, stats.mean(), 1e-9); + assertEquals(4.0, stats.populationVariance(), 1e-9); + } + + @Test + void removingTheLastSampleEmptiesTheAccumulator() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.add(3.0); + stats.remove(3.0); + assertTrue(stats.isEmpty()); + assertEquals(0.0, stats.sumOfSquaredDeviations()); + } + + @Test + void removeOnAnEmptyAccumulatorFails() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + assertThrows(IllegalStateException.class, () -> stats.remove(1.0)); + } + + @Test + @DisplayName("add plus remove turns the accumulator into a sliding window") + void supportsSlidingWindows() { + int windowSize = 20; + Random random = new Random(1234L); + double[] signal = new double[500]; + for (int i = 0; i < signal.length; i++) { + signal[i] = random.nextGaussian() * 5.0 + 100.0; + } + + WelfordAlgorithm window = new WelfordAlgorithm(); + for (int i = 0; i < signal.length; i++) { + window.add(signal[i]); + if (window.count() > windowSize) { + window.remove(signal[i - windowSize]); + } + if (i >= windowSize) { + double[] expectedWindow = new double[windowSize]; + System.arraycopy(signal, i - windowSize + 1, expectedWindow, 0, windowSize); + + WelfordAlgorithm reference = new WelfordAlgorithm(); + reference.addAll(expectedWindow); + assertEquals(windowSize, window.count()); + assertEquals(reference.mean(), window.mean(), 1e-9); + assertEquals(reference.populationVariance(), window.populationVariance(), 1e-8); + } + } + } + + @Test + void mergeCombinesPartialSummaries() { + Random random = new Random(20240517L); + double[] values = new double[1_000]; + for (int i = 0; i < values.length; i++) { + values[i] = random.nextGaussian(); + } + + WelfordAlgorithm whole = new WelfordAlgorithm(); + WelfordAlgorithm left = new WelfordAlgorithm(); + WelfordAlgorithm right = new WelfordAlgorithm(); + for (int i = 0; i < values.length; i++) { + whole.add(values[i]); + if (i < 337) { + left.add(values[i]); + } else { + right.add(values[i]); + } + } + + WelfordAlgorithm merged = WelfordAlgorithm.merge(left, right); + assertEquals(whole.count(), merged.count()); + assertEquals(whole.mean(), merged.mean(), 1e-12); + assertEquals(whole.sampleVariance(), merged.sampleVariance(), 1e-12); + } + + @Test + void mergeHandlesEmptyOperands() { + WelfordAlgorithm empty = new WelfordAlgorithm(); + WelfordAlgorithm filled = new WelfordAlgorithm(); + filled.addAll(TEXTBOOK_SAMPLE); + + assertTrue(WelfordAlgorithm.merge(empty, empty).isEmpty()); + assertEquals(filled.mean(), WelfordAlgorithm.merge(empty, filled).mean(), 1e-12); + assertEquals(filled.mean(), WelfordAlgorithm.merge(filled, empty).mean(), 1e-12); + assertEquals(filled.sampleVariance(), WelfordAlgorithm.merge(filled, empty).sampleVariance(), 1e-12); + } + + @Test + void clearForgetsEverything() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(TEXTBOOK_SAMPLE); + stats.clear(); + assertTrue(stats.isEmpty()); + assertTrue(Double.isNaN(stats.mean())); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) + void rejectsNonFiniteSamples(double value) { + WelfordAlgorithm stats = new WelfordAlgorithm(); + assertThrows(IllegalArgumentException.class, () -> stats.add(value)); + stats.add(1.0); + assertThrows(IllegalArgumentException.class, () -> stats.remove(value)); + } + + @Test + void toStringMentionsTheSummary() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.addAll(TEXTBOOK_SAMPLE); + assertTrue(stats.toString().contains("count=8"), stats.toString()); + } + + @Test + @DisplayName("removing a sample far from the mean cannot push the variance below zero") + void removalNeverYieldsANegativeVariance() { + WelfordAlgorithm stats = new WelfordAlgorithm(); + stats.add(1e16); + stats.add(1.0); + stats.remove(1e16); + + assertEquals(1L, stats.count()); + assertTrue(stats.sumOfSquaredDeviations() >= 0.0, "got " + stats.sumOfSquaredDeviations()); + assertTrue(stats.populationVariance() >= 0.0); + } +} diff --git a/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java b/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java index 7b41e11ef22f..0c7d7e4701cf 100644 --- a/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java +++ b/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java @@ -1,15 +1,45 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -public class AlphabeticalTest { +@DisplayName("Alphabetical.isAlphabetical()") +class AlphabeticalTest { - @ParameterizedTest(name = "\"{0}\" β†’ Expected: {1}") - @CsvSource({"'abcdefghijklmno', true", "'abcdxxxyzzzz', true", "'123a', false", "'abcABC', false", "'abcdefghikjlmno', false", "'aBC', true", "'abc', true", "'xyzabc', false", "'abcxyz', true", "'', false", "'1', false"}) - void testIsAlphabetical(String input, boolean expected) { - assertEquals(expected, Alphabetical.isAlphabetical(input)); + static Stream testCases() { + // Workaround for SpotBugs false positive (NAB_NEEDLESS_BOOLEAN_CONSTANT_CONVERSION) + // due to JUnit Arguments.of(Object...) auto-boxing + return Stream.of(arguments("", Boolean.FALSE, "Should return false for empty string"), arguments(" ", Boolean.FALSE, "Should return false for blank string"), arguments("a1b2", Boolean.FALSE, "Should return false when string contains numbers"), + arguments("abc!DEF", Boolean.FALSE, "Should return false when string contains symbols"), arguments("#abc", Boolean.FALSE, "Should return false when first character is not a letter"), arguments("abc", Boolean.TRUE, "Should return true for non-decreasing order"), + arguments("aBcD", Boolean.TRUE, "Should return true for mixed case increasing sequence"), arguments("a", Boolean.TRUE, "Should return true for single letter"), arguments("'", Boolean.FALSE, "Should return false for single symbol"), + arguments("aabbcc", Boolean.TRUE, "Should return true for repeated letters"), arguments("cba", Boolean.FALSE, "Should return false when order decreases"), arguments("abzba", Boolean.FALSE, "Should return false when middle breaks order")); + } + + private void assertAlphabetical(String input, boolean expected, String message) { + // Arrange & Act + boolean result = Alphabetical.isAlphabetical(input); + + // Assert + assertEquals(expected, result, message); + } + + @Test + @DisplayName("Should return false for null input") + void nullInputTest() { + assertAlphabetical(null, false, "Should return false for null input"); + } + + @ParameterizedTest(name = "{2}") + @MethodSource("testCases") + @DisplayName("Alphabetical cases") + void isAlphabeticalTest(String input, boolean expected, String message) { + assertAlphabetical(input, expected, message); } } diff --git a/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java index 9e8ae9e9f153..4cd55a4d7410 100644 --- a/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java +++ b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java @@ -1,9 +1,11 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class AlternativeStringArrangeTest { @@ -20,4 +22,15 @@ private static Stream provideTestData() { void arrangeTest(String input1, String input2, String expected) { assertEquals(expected, AlternativeStringArrange.arrange(input1, input2)); } + + @ParameterizedTest(name = "null input ({0}, {1}) should throw IllegalArgumentException") + @MethodSource("provideNullInputs") + void arrangeThrowsOnNullInput(String input1, String input2) { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> AlternativeStringArrange.arrange(input1, input2)); + assertEquals("Input strings must not be null", ex.getMessage()); + } + + private static Stream provideNullInputs() { + return Stream.of(Arguments.of(null, "abc"), Arguments.of("abc", null), Arguments.of(null, null)); + } } 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)); + } +} 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"); diff --git a/src/test/java/com/thealgorithms/strings/KMPTest.java b/src/test/java/com/thealgorithms/strings/KMPTest.java new file mode 100644 index 000000000000..9fa5f398d420 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/KMPTest.java @@ -0,0 +1,29 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class KMPTest { + + @Test + public void testNullInputs() { + assertEquals(List.of(), KMP.kmpMatcher(null, "A")); + assertEquals(List.of(), KMP.kmpMatcher("A", null)); + assertEquals(List.of(), KMP.kmpMatcher(null, null)); + } + + @Test + public void testKMPMatcher() { + assertEquals(List.of(0, 1), KMP.kmpMatcher("AAAAABAAABA", "AAAA")); + assertEquals(List.of(0, 3), KMP.kmpMatcher("ABCABC", "ABC")); + assertEquals(List.of(10), KMP.kmpMatcher("ABABDABACDABABCABAB", "ABABCABAB")); + assertEquals(List.of(), KMP.kmpMatcher("ABCDE", "FGH")); + assertEquals(List.of(), KMP.kmpMatcher("A", "AA")); + assertEquals(List.of(0, 1, 2), KMP.kmpMatcher("AAA", "A")); + assertEquals(List.of(0), KMP.kmpMatcher("A", "A")); + assertEquals(List.of(), KMP.kmpMatcher("", "A")); + assertEquals(List.of(), KMP.kmpMatcher("A", "")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java new file mode 100644 index 000000000000..c22cc77df18a --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java @@ -0,0 +1,75 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class KasaiAlgorithmTest { + + @Test + public void testKasaiBanana() { + String text = "banana"; + // Suffixes: + // 0: banana + // 1: anana + // 2: nana + // 3: ana + // 4: na + // 5: a + // + // Sorted Suffixes: + // 5: a + // 3: ana + // 1: anana + // 0: banana + // 4: na + // 2: nana + int[] suffixArr = {5, 3, 1, 0, 4, 2}; + + int[] expectedLcp = {1, 3, 0, 0, 2, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiAaaa() { + String text = "aaaa"; + // Sorted Suffixes: + // 3: a + // 2: aa + // 1: aaa + // 0: aaaa + int[] suffixArr = {3, 2, 1, 0}; + int[] expectedLcp = {1, 2, 3, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiEmptyString() { + assertArrayEquals(new int[0], KasaiAlgorithm.kasai("", new int[0])); + } + + @Test + public void testKasaiSingleChar() { + assertArrayEquals(new int[] {0}, KasaiAlgorithm.kasai("A", new int[] {0})); + } + + @Test + public void testKasaiNullTextOrSuffixArray() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai(null, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", null)); + } + + @Test + public void testKasaiInvalidSuffixArrayLength() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {0, 1})); + } + + @Test + public void testKasaiInvalidSuffixArrayIndex() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {1})); // Out of bounds + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {-1})); // Out of bounds + } +} diff --git a/src/test/java/com/thealgorithms/strings/LengthOfLastWordTest.java b/src/test/java/com/thealgorithms/strings/LengthOfLastWordTest.java new file mode 100644 index 000000000000..46a0a6eb0008 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LengthOfLastWordTest.java @@ -0,0 +1,18 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class LengthOfLastWordTest { + @Test + public void testLengthOfLastWord() { + assertEquals(5, new LengthOfLastWord().lengthOfLastWord("Hello World")); + assertEquals(4, new LengthOfLastWord().lengthOfLastWord(" fly me to the moon ")); + assertEquals(6, new LengthOfLastWord().lengthOfLastWord("luffy is still joyboy")); + assertEquals(5, new LengthOfLastWord().lengthOfLastWord("Hello")); + assertEquals(0, new LengthOfLastWord().lengthOfLastWord(" ")); + assertEquals(0, new LengthOfLastWord().lengthOfLastWord("")); + assertEquals(3, new LengthOfLastWord().lengthOfLastWord("JUST LIE ")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java new file mode 100644 index 000000000000..ea62a6b64741 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java @@ -0,0 +1,131 @@ +package com.thealgorithms.strings; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; +import org.junit.jupiter.api.Test; + +public class LongestCommonSubstringTest { + + @Test + public void testNullOrEmptyInputs() { + assertEquals("", LongestCommonSubstring.longestCommonSubstring(null, "abc")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", null)); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("", "abc")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "")); + } + + @Test + public void testNormalSubstrings() { + assertEquals("cde", LongestCommonSubstring.longestCommonSubstring("abcdef", "zcdemf")); + assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abc", "abc")); + assertEquals("cdef", LongestCommonSubstring.longestCommonSubstring("abcdef", "cdefgh")); + } + + @Test + public void testSingleCharacterAndNoMatch() { + assertEquals("a", LongestCommonSubstring.longestCommonSubstring("a", "a")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "xyz")); + } + + @Test + public void testMultipleMatchesFirstLongest() { + // Keeps the first matched longest substring when lengths are tied + assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abcXdef", "abcYdef")); + } + + // NEW + + @Test + public void testSpacesAndSpecialCharacters() { + assertEquals(" Hello World! ", LongestCommonSubstring.longestCommonSubstring("123 Hello World! 456", "ABC Hello World! XYZ")); + assertEquals("@#$%^", LongestCommonSubstring.longestCommonSubstring("test@#$%^123", "abc@#$%^xyz")); + } + + @Test + public void testCoincidenceAtBoundaries() { + // Match at the beginning + assertEquals("PREFIX_", LongestCommonSubstring.longestCommonSubstring("PREFIX_12345", "PREFIX_67890")); + // Match at the end + assertEquals("_SUFFIX", LongestCommonSubstring.longestCommonSubstring("12345_SUFFIX", "67890_SUFFIX")); + } + + @Test + public void testRepeatedPatterns() { + assertEquals("anabanana", LongestCommonSubstring.longestCommonSubstring("bananabanana", "anabanana")); + } + + @Test + public void testLargeInputsPerformanceAndTimeout() { + // Generate two 3,000-character strings containing a common substring in the middle + int size = 3000; + StringBuilder sb1 = new StringBuilder(size); + StringBuilder sb2 = new StringBuilder(size); + + for (int i = 0; i < 1000; i++) { + sb1.append('A'); + sb2.append('B'); + } + + String commonPart = "COMMON_LONG_SUBSTRING_TEST_1234567890"; + sb1.append(commonPart); + sb2.append(commonPart); + + for (int i = 0; i < 1500; i++) { + sb1.append('X'); + sb2.append('Y'); + } + + // Verify that the algorithm completes within 2 seconds + assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { + String result = LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString()); + assertEquals(commonPart, result); + }); + } + + @Test + public void testVeryLargeInputsPerformance() { + // Generate two very large strings (4,000 characters each) + int size = 4000; + StringBuilder sb1 = new StringBuilder(size); + StringBuilder sb2 = new StringBuilder(size); + + for (int i = 0; i < size; i++) { + sb1.append('A'); + sb2.append('B'); + } + + assertTimeoutPreemptively(Duration.ofSeconds(2), () -> { assertEquals("", LongestCommonSubstring.longestCommonSubstring(sb1.toString(), sb2.toString())); }); + } + + @Test + public void testCaseSensitivityAndUnicode() { + // Case sensitivity test + assertEquals("ABC", LongestCommonSubstring.longestCommonSubstring("ABCdef", "123ABCxyz")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "ABC")); + + // Full substring containment + assertEquals("inside", LongestCommonSubstring.longestCommonSubstring("inside", "text_inside_here")); + + // Unicode characters + assertEquals("_Ñéíóú_", LongestCommonSubstring.longestCommonSubstring("hola_Ñéíóú_mundo", "test_Ñéíóú_abc")); + } + + @Test + public void testWhitespaceAndControlCharacters() { + // Test with newlines and tabs + assertEquals("\t\n", LongestCommonSubstring.longestCommonSubstring("start\t\nend", "begin\t\nfinish")); + + // Test with multiple consecutive spaces + assertEquals(" ", LongestCommonSubstring.longestCommonSubstring("a b", "x y")); + } + + @Test + public void testOverlappingSubstrings() { + // Test overlapping matches like "AAAA" in "AAAAA" vs "AAAA" + assertEquals("AAAA", LongestCommonSubstring.longestCommonSubstring("AAAAA", "AAAA")); + assertEquals("ABAB", LongestCommonSubstring.longestCommonSubstring("ABABAB", "CABAB")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java deleted file mode 100644 index aa13c0f4a474..000000000000 --- a/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.thealgorithms.strings; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class LongestPalindromicSubstringTest { - - @ParameterizedTest - @MethodSource("provideTestCasesForLongestPalindrome") - void testLongestPalindrome(String input, String expected) { - assertEquals(expected, LongestPalindromicSubstring.longestPalindrome(input)); - } - - private static Stream provideTestCasesForLongestPalindrome() { - return Stream.of(Arguments.of("babad", "bab"), Arguments.of("cbbd", "bb"), Arguments.of("a", "a"), Arguments.of("", ""), Arguments.of("abc", "a"), Arguments.of(null, ""), Arguments.of("aaaaa", "aaaaa")); - } -} diff --git a/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java new file mode 100644 index 000000000000..366f6863340d --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java @@ -0,0 +1,33 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LongestRepeatedSubstringTest { + + @ParameterizedTest(name = "\"{0}\" -> \"{1}\"") + @MethodSource("provideTestCases") + void testLongestRepeatedSubstring(String input, String expected) { + assertEquals(expected, LongestRepeatedSubstring.longestRepeatedSubstring(input)); + } + + private static Stream provideTestCases() { + return Stream.of(Arguments.of("banana", "ana"), Arguments.of("abcabc", "abc"), Arguments.of("aaaa", "aaa"), Arguments.of("abcd", ""), Arguments.of("a", ""), Arguments.of("", ""), Arguments.of(null, ""), Arguments.of("aab", "a"), Arguments.of("aa", "a"), Arguments.of("mississippi", "issi")); + } + + @ParameterizedTest(name = "\"{0}\" -> LCP={1}") + @MethodSource("provideLcpTestCases") + void testBuildLcpArray(String input, int[] expectedLcp) { + int[] suffixArray = SuffixArray.buildSuffixArray(input); + assertArrayEquals(expectedLcp, LongestRepeatedSubstring.buildLcpArray(input, suffixArray)); + } + + private static Stream provideLcpTestCases() { + return Stream.of(Arguments.of("banana", new int[] {1, 3, 0, 0, 2}), Arguments.of("ab", new int[] {0})); + } +} 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)); + } +} diff --git a/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java b/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java new file mode 100644 index 000000000000..016f72ff9d7f --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class MoveHashToEndTest { + + @Test + void testBasicCase() { + assertEquals("helllo###", MoveHashToEnd.moveHashToEnd("h#e#l#llo")); + } + + @Test + void testNoHash() { + assertEquals("hello", MoveHashToEnd.moveHashToEnd("hello")); + } + + @Test + void testAllHashes() { + assertEquals("###", MoveHashToEnd.moveHashToEnd("###")); + } + + @Test + void testHashAtEnd() { + assertEquals("hello#", MoveHashToEnd.moveHashToEnd("hello#")); + } + + @Test + void testHashAtStart() { + assertEquals("hello#", MoveHashToEnd.moveHashToEnd("#hello")); + } + + @Test + void testEmptyString() { + assertEquals("", MoveHashToEnd.moveHashToEnd("")); + } + + @Test + void testNullInput() { + assertNull(MoveHashToEnd.moveHashToEnd(null)); + } + + @Test + void testSingleHash() { + assertEquals("#", MoveHashToEnd.moveHashToEnd("#")); + } + + @Test + void testSingleNonHashChar() { + assertEquals("a", MoveHashToEnd.moveHashToEnd("a")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/RabinKarpTest.java b/src/test/java/com/thealgorithms/strings/RabinKarpTest.java new file mode 100644 index 000000000000..6dfd099e9bca --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/RabinKarpTest.java @@ -0,0 +1,46 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class RabinKarpTest { + + @Test + public void testNullInputs() { + assertEquals(List.of(), RabinKarp.search(null, "A")); + assertEquals(List.of(), RabinKarp.search("A", null)); + assertEquals(List.of(), RabinKarp.search(null, null)); + } + + @Test + public void testHashCollision() { + // 'a' = 97. (char)198 % 101 = 97. + // For length 1, h = 1. p = 97. t = 198 % 101 = 97. + // Collision occurs, loop checks characters: 198 != 97, breaks. + char collisionChar = (char) 198; + String text = String.valueOf(collisionChar); + String pattern = "a"; + assertEquals(List.of(), RabinKarp.search(text, pattern)); + } + + @Test + public void testSearchWithCustomQ() { + // Using a different prime + assertEquals(List.of(0, 1), RabinKarp.search("AAAA", "AAA", 13)); + } + + @Test + public void testRabinKarpSearch() { + assertEquals(List.of(0, 1), RabinKarp.search("AAAAABAAABA", "AAAA")); + assertEquals(List.of(0, 3), RabinKarp.search("ABCABC", "ABC")); + assertEquals(List.of(10), RabinKarp.search("ABABDABACDABABCABAB", "ABABCABAB")); + assertEquals(List.of(), RabinKarp.search("ABCDE", "FGH")); + assertEquals(List.of(), RabinKarp.search("A", "AA")); + assertEquals(List.of(0, 1, 2), RabinKarp.search("AAA", "A")); + assertEquals(List.of(0), RabinKarp.search("A", "A")); + assertEquals(List.of(), RabinKarp.search("", "A")); + assertEquals(List.of(), RabinKarp.search("A", "")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java b/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java new file mode 100644 index 000000000000..3beb2e83399b --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java @@ -0,0 +1,28 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class RemoveStarsTest { + + @Test + void testExampleCase() { + assertEquals("lecoe", RemoveStars.removeStars("leet**cod*e")); + } + + @Test + void testAllStars() { + assertEquals("", RemoveStars.removeStars("abc***")); + } + + @Test + void testNoStars() { + assertEquals("hello", RemoveStars.removeStars("hello")); + } + + @Test + void testSingleCharacter() { + assertEquals("", RemoveStars.removeStars("a*")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/TitleCaseTest.java b/src/test/java/com/thealgorithms/strings/TitleCaseTest.java new file mode 100644 index 000000000000..37bb44ed2c7b --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/TitleCaseTest.java @@ -0,0 +1,39 @@ +package com.thealgorithms.strings; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TitleCaseTest { + + @Test + public void testNullOrEmptyInputs() { + assertEquals("", TitleCase.toTitleCase(null)); + assertEquals("", TitleCase.toTitleCase("")); + } + + @Test + public void testSingleWord() { + assertEquals("Hello", TitleCase.toTitleCase("hello")); + assertEquals("Hello", TitleCase.toTitleCase("HELLO")); + assertEquals("A", TitleCase.toTitleCase("a")); + } + + @Test + public void testMultipleWords() { + assertEquals("The Quick Brown Fox", TitleCase.toTitleCase("the quick brown fox")); + assertEquals("The Quick Brown Fox", TitleCase.toTitleCase("THE QUICK BROWN FOX")); + assertEquals("Already Title Case", TitleCase.toTitleCase("already Title Case")); + } + + @Test + public void testWhitespace() { + assertEquals(" Spaces ", TitleCase.toTitleCase(" spaces ")); + } + + @Test + public void testQuotedWords() { + assertEquals("\"Hello\"", TitleCase.toTitleCase("\"hello\"")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java b/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java new file mode 100644 index 000000000000..42b2d04ff265 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java @@ -0,0 +1,34 @@ +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 java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TopKFrequentWordsTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testFindTopKFrequentWords(String[] words, int k, List expected) { + assertEquals(expected, TopKFrequentWords.findTopKFrequentWords(words, k)); + } + + static Stream validTestCases() { + return Stream.of(Arguments.of(new String[] {"i", "love", "leetcode", "i", "love", "coding"}, 2, List.of("i", "love")), Arguments.of(new String[] {"the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"}, 4, List.of("the", "is", "sunny", "day")), + Arguments.of(new String[] {"bbb", "aaa", "bbb", "aaa", "ccc"}, 2, List.of("aaa", "bbb")), Arguments.of(new String[] {"one", "two", "three"}, 10, List.of("one", "three", "two")), Arguments.of(new String[] {}, 3, List.of()), Arguments.of(new String[] {"x", "x", "y"}, 0, List.of())); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testFindTopKFrequentWordsInvalidInput(String[] words, int k) { + assertThrows(IllegalArgumentException.class, () -> TopKFrequentWords.findTopKFrequentWords(words, k)); + } + + static Stream invalidTestCases() { + return Stream.of(Arguments.of((String[]) null, 1), Arguments.of(new String[] {"a", null, "b"}, 2), Arguments.of(new String[] {"a"}, -1)); + } +} diff --git a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java b/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java deleted file mode 100644 index 411b11e743b8..000000000000 --- a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.thealgorithms.strings; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -public class ValidParenthesesTest { - - @ParameterizedTest(name = "Input: \"{0}\" β†’ Expected: {1}") - @CsvSource({"'()', true", "'()[]{}', true", "'(]', false", "'{[]}', true", "'([{}])', true", "'([)]', false", "'', true", "'(', false", "')', false", "'{{{{}}}}', true", "'[({})]', true", "'[(])', false", "'[', false", "']', false", "'()()()()', true", "'(()', false", "'())', false", - "'{[()()]()}', true"}) - void - testIsValid(String input, boolean expected) { - assertEquals(expected, ValidParentheses.isValid(input)); - } - - @Test - void testNullInputThrows() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(null)); - assertEquals("Input string cannot be null", ex.getMessage()); - } - - @ParameterizedTest(name = "Input: \"{0}\" β†’ throws IllegalArgumentException") - @CsvSource({"'a'", "'()a'", "'[123]'", "'{hello}'", "'( )'", "'\t'", "'\n'", "'@#$%'"}) - void testInvalidCharactersThrow(String input) { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(input)); - assertTrue(ex.getMessage().startsWith("Unexpected character")); - } -} diff --git a/src/test/java/com/thealgorithms/strings/WordLadderTest.java b/src/test/java/com/thealgorithms/strings/WordLadderTest.java index 221953411da7..c029940abfb0 100644 --- a/src/test/java/com/thealgorithms/strings/WordLadderTest.java +++ b/src/test/java/com/thealgorithms/strings/WordLadderTest.java @@ -24,7 +24,7 @@ public class WordLadderTest { public void testWordLadder() { List wordList1 = Arrays.asList("hot", "dot", "dog", "lot", "log", "cog"); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList1), 5); + assertEquals(5, WordLadder.ladderLength("hit", "cog", wordList1)); } /** @@ -39,7 +39,7 @@ public void testWordLadder() { public void testWordLadder2() { List wordList2 = Arrays.asList("hot", "dot", "dog", "lot", "log"); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList2), 0); + assertEquals(0, WordLadder.ladderLength("hit", "cog", wordList2)); } /** @@ -54,7 +54,7 @@ public void testWordLadder2() { public void testWordLadder3() { List wordList3 = emptyList(); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList3), 0); + assertEquals(0, WordLadder.ladderLength("hit", "cog", wordList3)); } @ParameterizedTest diff --git a/src/test/java/com/thealgorithms/strings/ZAlgorithmTest.java b/src/test/java/com/thealgorithms/strings/ZAlgorithmTest.java new file mode 100644 index 000000000000..df749ed9a8b5 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/ZAlgorithmTest.java @@ -0,0 +1,25 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ZAlgorithmTest { + + @Test + void testZFunction() { + int[] z = ZAlgorithm.zFunction("aaaaa"); + assertArrayEquals(new int[] {0, 4, 3, 2, 1}, z); + } + + @Test + void testSearchFound() { + assertEquals(2, ZAlgorithm.search("abcabca", "cab")); + } + + @Test + void testSearchNotFound() { + assertEquals(-1, ZAlgorithm.search("abcdef", "gh")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java b/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java index 2cbbfe3d2dd8..9bf118c9b844 100644 --- a/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java +++ b/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java @@ -9,8 +9,8 @@ public class ZigZagPatternTest { public void testZigZagPattern() { String input1 = "HelloWorldFromJava"; String input2 = "javaIsAProgrammingLanguage"; - Assertions.assertEquals(ZigZagPattern.encode(input1, 4), "HooeWrrmalolFJvlda"); - Assertions.assertEquals(ZigZagPattern.encode(input2, 4), "jAaLgasPrmgaaevIrgmnnuaoig"); + Assertions.assertEquals("HooeWrrmalolFJvlda", ZigZagPattern.encode(input1, 4)); + Assertions.assertEquals("jAaLgasPrmgaaevIrgmnnuaoig", ZigZagPattern.encode(input2, 4)); // Edge cases Assertions.assertEquals("ABC", ZigZagPattern.encode("ABC", 1)); // Single row Assertions.assertEquals("A", ZigZagPattern.encode("A", 2)); // numRows > length of string