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..c5f200c12836 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: diff --git a/.github/workflows/clang-format-lint.yml b/.github/workflows/clang-format-lint.yml index ca014e115282..dc0c9754ed1b 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@v6 - 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 new file mode 100644 index 000000000000..6deea88f0daf --- /dev/null +++ b/.github/workflows/close-failed-prs.yml @@ -0,0 +1,156 @@ +name: Close stale PRs with failed workflows + +on: + schedule: + - cron: '0 3 * * *' # runs daily at 03:00 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + close-stale: + runs-on: ubuntu-latest + steps: + - name: Close stale PRs + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const mainBranches = ['main', 'master']; + const cutoffDays = 14; + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - cutoffDays); + + console.log(`Checking PRs older than: ${cutoff.toISOString()}`); + + try { + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'updated', + direction: 'asc', + per_page: 100 + }); + + console.log(`Found ${prs.length} open PRs to check`); + + for (const pr of prs) { + try { + const updated = new Date(pr.updated_at); + + if (updated > cutoff) { + console.log(`⏩ Skipping PR #${pr.number} - updated recently`); + continue; + } + + console.log(`🔍 Checking PR #${pr.number}: "${pr.title}"`); + + // Get commits + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + per_page: 100 + }); + + const meaningfulCommits = commits.filter(c => { + const msg = c.commit.message.toLowerCase(); + const date = new Date(c.commit.committer.date); + const isMergeFromMain = mainBranches.some(branch => + msg.startsWith(`merge branch '${branch}'`) || + msg.includes(`merge remote-tracking branch '${branch}'`) + ); + + return !isMergeFromMain && date > cutoff; + }); + + // Get checks with error handling + let hasFailedChecks = false; + let allChecksCompleted = false; + let hasChecks = false; + + try { + const { data: checks } = await github.rest.checks.listForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: pr.head.sha + }); + + hasChecks = checks.check_runs.length > 0; + hasFailedChecks = checks.check_runs.some(c => c.conclusion === 'failure'); + allChecksCompleted = checks.check_runs.every(c => + c.status === 'completed' || c.status === 'skipped' + ); + } catch (error) { + console.log(`⚠️ Could not fetch checks for PR #${pr.number}: ${error.message}`); + } + + // Get workflow runs with error handling + let hasFailedWorkflows = false; + let allWorkflowsCompleted = false; + let hasWorkflows = false; + + try { + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: pr.head.sha, + per_page: 50 + }); + + hasWorkflows = runs.workflow_runs.length > 0; + hasFailedWorkflows = runs.workflow_runs.some(r => r.conclusion === 'failure'); + allWorkflowsCompleted = runs.workflow_runs.every(r => + ['completed', 'skipped', 'cancelled'].includes(r.status) + ); + + console.log(`PR #${pr.number}: ${runs.workflow_runs.length} workflow runs found`); + + } catch (error) { + console.log(`⚠️ Could not fetch workflow runs for PR #${pr.number}: ${error.message}`); + } + + console.log(`PR #${pr.number}: ${meaningfulCommits.length} meaningful commits`); + console.log(`Checks - has: ${hasChecks}, failed: ${hasFailedChecks}, completed: ${allChecksCompleted}`); + console.log(`Workflows - has: ${hasWorkflows}, failed: ${hasFailedWorkflows}, completed: ${allWorkflowsCompleted}`); + + // Combine conditions - only consider if we actually have checks/workflows + const hasAnyFailure = (hasChecks && hasFailedChecks) || (hasWorkflows && hasFailedWorkflows); + const allCompleted = (!hasChecks || allChecksCompleted) && (!hasWorkflows || allWorkflowsCompleted); + + if (meaningfulCommits.length === 0 && hasAnyFailure && allCompleted) { + console.log(`✅ Closing PR #${pr.number} (${pr.title})`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: `This pull request has been automatically closed because its workflows or checks failed and it has been inactive for more than ${cutoffDays} days. Please fix the workflows and reopen if you'd like to continue. Merging from main/master alone does not count as activity.` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: 'closed' + }); + + console.log(`✅ Successfully closed PR #${pr.number}`); + } else { + console.log(`⏩ Not closing PR #${pr.number} - conditions not met`); + } + + } catch (prError) { + console.error(`❌ Error processing PR #${pr.number}: ${prError.message}`); + continue; + } + } + + } catch (error) { + console.error(`❌ Fatal error: ${error.message}`); + throw error; + } diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ef4953afc954..152d0d766fd2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 @@ -30,7 +30,7 @@ jobs: distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 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@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:java-kotlin" @@ -52,15 +52,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:actions" ... diff --git a/.github/workflows/infer.yml b/.github/workflows/infer.yml index 3df7c4b1fc9e..9d4dcf63000b 100644 --- a/.github/workflows/infer.yml +++ b/.github/workflows/infer.yml @@ -15,7 +15,7 @@ jobs: run_infer: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 @@ -33,7 +33,7 @@ jobs: - name: Cache infer build id: cache-infer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: infer key: ${{ runner.os }}-infer-${{ env.year_week }} @@ -44,7 +44,7 @@ jobs: cd .. git clone https://github.com/facebook/infer.git cd infer - git checkout 01aaa268f9d38723ba69c139e10f9e2a04b40b1c + git checkout 02c2c43b71e4c5110c0be841e66153942fda06c9 ./build-infer.sh java cp -r infer ../Java diff --git a/.github/workflows/project_structure.yml b/.github/workflows/project_structure.yml index f9fb82a2781c..5aadc6353791 100644 --- a/.github/workflows/project_structure.yml +++ b/.github/workflows/project_structure.yml @@ -15,7 +15,7 @@ jobs: check_structure: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.13' diff --git a/.github/workflows/update-directorymd.yml b/.github/workflows/update-directorymd.yml index 6692a884a867..101d82427e38 100644 --- a/.github/workflows/update-directorymd.yml +++ b/.github/workflows/update-directorymd.yml @@ -1,4 +1,4 @@ -name: Generate Directory Markdown +name: Generate Directory Markdown on: push: @@ -14,7 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Run Directory Tree Generator uses: DenizAltunkapan/directory-tree-generator@v2 diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index ef76c6fc68a6..a8951da7de26 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -1,4 +1,4 @@ -FROM gitpod/workspace-java-21:2025-08-25-18-17-39 +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..239172177b38 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,18 @@ "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/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 13d1fe39c168..deaf59636fa4 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,14 +24,19 @@ - 📄 [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) - 📄 [ClearLeftmostSetBit](src/main/java/com/thealgorithms/bitmanipulation/ClearLeftmostSetBit.java) + - 📄 [CountBitsFlip](src/main/java/com/thealgorithms/bitmanipulation/CountBitsFlip.java) - 📄 [CountLeadingZeros](src/main/java/com/thealgorithms/bitmanipulation/CountLeadingZeros.java) - 📄 [CountSetBits](src/main/java/com/thealgorithms/bitmanipulation/CountSetBits.java) - 📄 [FindNthBit](src/main/java/com/thealgorithms/bitmanipulation/FindNthBit.java) @@ -74,6 +80,8 @@ - 📄 [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) - 📄 [ProductCipher](src/main/java/com/thealgorithms/ciphers/ProductCipher.java) @@ -89,14 +97,25 @@ - 📄 [CompositeLFSR](src/main/java/com/thealgorithms/ciphers/a5/CompositeLFSR.java) - 📄 [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** - 📄 [AffineConverter](src/main/java/com/thealgorithms/conversions/AffineConverter.java) - 📄 [AnyBaseToAnyBase](src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java) - 📄 [AnyBaseToDecimal](src/main/java/com/thealgorithms/conversions/AnyBaseToDecimal.java) - 📄 [AnytoAny](src/main/java/com/thealgorithms/conversions/AnytoAny.java) + - 📄 [Base64](src/main/java/com/thealgorithms/conversions/Base64.java) - 📄 [BinaryToDecimal](src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java) - 📄 [BinaryToHexadecimal](src/main/java/com/thealgorithms/conversions/BinaryToHexadecimal.java) - 📄 [BinaryToOctal](src/main/java/com/thealgorithms/conversions/BinaryToOctal.java) + - 📄 [CoordinateConverter](src/main/java/com/thealgorithms/conversions/CoordinateConverter.java) - 📄 [DecimalToAnyBase](src/main/java/com/thealgorithms/conversions/DecimalToAnyBase.java) - 📄 [DecimalToBinary](src/main/java/com/thealgorithms/conversions/DecimalToBinary.java) - 📄 [DecimalToHexadecimal](src/main/java/com/thealgorithms/conversions/DecimalToHexadecimal.java) @@ -117,6 +136,8 @@ - 📄 [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) - 📄 [UnitsConverter](src/main/java/com/thealgorithms/conversions/UnitsConverter.java) @@ -156,6 +177,7 @@ - 📄 [BoruvkaAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/BoruvkaAlgorithm.java) - 📄 [ConnectedComponent](src/main/java/com/thealgorithms/datastructures/graphs/ConnectedComponent.java) - 📄 [Cycles](src/main/java/com/thealgorithms/datastructures/graphs/Cycles.java) + - 📄 [DialsAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java) - 📄 [DijkstraAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java) - 📄 [DijkstraOptimizedAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java) - 📄 [EdmondsBlossomAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/EdmondsBlossomAlgorithm.java) @@ -170,6 +192,7 @@ - 📄 [MatrixGraphs](src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java) - 📄 [PrimMST](src/main/java/com/thealgorithms/datastructures/graphs/PrimMST.java) - 📄 [TarjansAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/TarjansAlgorithm.java) + - 📄 [TwoSat](src/main/java/com/thealgorithms/datastructures/graphs/TwoSat.java) - 📄 [UndirectedAdjacencyListGraph](src/main/java/com/thealgorithms/datastructures/graphs/UndirectedAdjacencyListGraph.java) - 📄 [WelshPowell](src/main/java/com/thealgorithms/datastructures/graphs/WelshPowell.java) - 📁 **hashmap** @@ -178,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) @@ -189,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) @@ -198,10 +223,12 @@ - 📄 [MinPriorityQueue](src/main/java/com/thealgorithms/datastructures/heaps/MinPriorityQueue.java) - 📁 **lists** - 📄 [CircleLinkedList](src/main/java/com/thealgorithms/datastructures/lists/CircleLinkedList.java) + - 📄 [CircularDoublyLinkedList](src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java) - 📄 [CountSinglyLinkedListRecursion](src/main/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursion.java) - 📄 [CreateAndDetectLoop](src/main/java/com/thealgorithms/datastructures/lists/CreateAndDetectLoop.java) - 📄 [CursorLinkedList](src/main/java/com/thealgorithms/datastructures/lists/CursorLinkedList.java) - 📄 [DoublyLinkedList](src/main/java/com/thealgorithms/datastructures/lists/DoublyLinkedList.java) + - 📄 [FlattenMultilevelLinkedList](src/main/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedList.java) - 📄 [MergeKSortedLinkedList](src/main/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedList.java) - 📄 [MergeSortedArrayList](src/main/java/com/thealgorithms/datastructures/lists/MergeSortedArrayList.java) - 📄 [MergeSortedSinglyLinkedList](src/main/java/com/thealgorithms/datastructures/lists/MergeSortedSinglyLinkedList.java) @@ -214,6 +241,7 @@ - 📄 [SinglyLinkedListNode](src/main/java/com/thealgorithms/datastructures/lists/SinglyLinkedListNode.java) - 📄 [SkipList](src/main/java/com/thealgorithms/datastructures/lists/SkipList.java) - 📄 [SortedLinkedList](src/main/java/com/thealgorithms/datastructures/lists/SortedLinkedList.java) + - 📄 [TortoiseHareAlgo](src/main/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgo.java) - 📁 **queues** - 📄 [CircularQueue](src/main/java/com/thealgorithms/datastructures/queues/CircularQueue.java) - 📄 [Deque](src/main/java/com/thealgorithms/datastructures/queues/Deque.java) @@ -240,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) @@ -261,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) @@ -298,6 +329,7 @@ - 📄 [ClimbingStairs](src/main/java/com/thealgorithms/dynamicprogramming/ClimbingStairs.java) - 📄 [CoinChange](src/main/java/com/thealgorithms/dynamicprogramming/CoinChange.java) - 📄 [CountFriendsPairing](src/main/java/com/thealgorithms/dynamicprogramming/CountFriendsPairing.java) + - 📄 [DamerauLevenshteinDistance](src/main/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistance.java) - 📄 [DiceThrow](src/main/java/com/thealgorithms/dynamicprogramming/DiceThrow.java) - 📄 [EditDistance](src/main/java/com/thealgorithms/dynamicprogramming/EditDistance.java) - 📄 [EggDropping](src/main/java/com/thealgorithms/dynamicprogramming/EggDropping.java) @@ -318,9 +350,11 @@ - 📄 [LongestValidParentheses](src/main/java/com/thealgorithms/dynamicprogramming/LongestValidParentheses.java) - 📄 [MatrixChainMultiplication](src/main/java/com/thealgorithms/dynamicprogramming/MatrixChainMultiplication.java) - 📄 [MatrixChainRecursiveTopDownMemoisation](src/main/java/com/thealgorithms/dynamicprogramming/MatrixChainRecursiveTopDownMemoisation.java) + - 📄 [MaximumProductSubarray](src/main/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarray.java) - 📄 [MaximumSumOfNonAdjacentElements](src/main/java/com/thealgorithms/dynamicprogramming/MaximumSumOfNonAdjacentElements.java) - 📄 [MinimumPathSum](src/main/java/com/thealgorithms/dynamicprogramming/MinimumPathSum.java) - 📄 [MinimumSumPartition](src/main/java/com/thealgorithms/dynamicprogramming/MinimumSumPartition.java) + - 📄 [NeedlemanWunsch](src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java) - 📄 [NewManShanksPrime](src/main/java/com/thealgorithms/dynamicprogramming/NewManShanksPrime.java) - 📄 [OptimalJobScheduling](src/main/java/com/thealgorithms/dynamicprogramming/OptimalJobScheduling.java) - 📄 [PalindromicPartitioning](src/main/java/com/thealgorithms/dynamicprogramming/PalindromicPartitioning.java) @@ -328,6 +362,7 @@ - 📄 [RegexMatching](src/main/java/com/thealgorithms/dynamicprogramming/RegexMatching.java) - 📄 [RodCutting](src/main/java/com/thealgorithms/dynamicprogramming/RodCutting.java) - 📄 [ShortestCommonSupersequenceLength](src/main/java/com/thealgorithms/dynamicprogramming/ShortestCommonSupersequenceLength.java) + - 📄 [SmithWaterman](src/main/java/com/thealgorithms/dynamicprogramming/SmithWaterman.java) - 📄 [SubsetCount](src/main/java/com/thealgorithms/dynamicprogramming/SubsetCount.java) - 📄 [SubsetSum](src/main/java/com/thealgorithms/dynamicprogramming/SubsetSum.java) - 📄 [SubsetSumSpaceOptimized](src/main/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimized.java) @@ -339,18 +374,34 @@ - 📄 [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) - 📄 [MidpointEllipse](src/main/java/com/thealgorithms/geometry/MidpointEllipse.java) - 📄 [Point](src/main/java/com/thealgorithms/geometry/Point.java) + - 📄 [WusLine](src/main/java/com/thealgorithms/geometry/WusLine.java) - 📁 **graph** + - 📄 [BronKerbosch](src/main/java/com/thealgorithms/graph/BronKerbosch.java) - 📄 [ConstrainedShortestPath](src/main/java/com/thealgorithms/graph/ConstrainedShortestPath.java) + - 📄 [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) + - 📄 [ZeroOneBfs](src/main/java/com/thealgorithms/graph/ZeroOneBfs.java) - 📁 **greedyalgorithms** - 📄 [ActivitySelection](src/main/java/com/thealgorithms/greedyalgorithms/ActivitySelection.java) - 📄 [BandwidthAllocation](src/main/java/com/thealgorithms/greedyalgorithms/BandwidthAllocation.java) @@ -380,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) @@ -391,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) @@ -403,11 +456,13 @@ - 📄 [DistanceFormula](src/main/java/com/thealgorithms/maths/DistanceFormula.java) - 📄 [DudeneyNumber](src/main/java/com/thealgorithms/maths/DudeneyNumber.java) - 📄 [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) @@ -425,7 +480,9 @@ - 📄 [GCDRecursion](src/main/java/com/thealgorithms/maths/GCDRecursion.java) - 📄 [Gaussian](src/main/java/com/thealgorithms/maths/Gaussian.java) - 📄 [GenericRoot](src/main/java/com/thealgorithms/maths/GenericRoot.java) + - 📄 [GermainPrimeAndSafePrime](src/main/java/com/thealgorithms/maths/GermainPrimeAndSafePrime.java) - 📄 [GoldbachConjecture](src/main/java/com/thealgorithms/maths/GoldbachConjecture.java) + - 📄 [HappyNumber](src/main/java/com/thealgorithms/maths/HappyNumber.java) - 📄 [HarshadNumber](src/main/java/com/thealgorithms/maths/HarshadNumber.java) - 📄 [HeronsFormula](src/main/java/com/thealgorithms/maths/HeronsFormula.java) - 📄 [JosephusProblem](src/main/java/com/thealgorithms/maths/JosephusProblem.java) @@ -439,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) @@ -446,9 +504,11 @@ - 📄 [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) + - 📄 [NumberPersistence](src/main/java/com/thealgorithms/maths/NumberPersistence.java) - 📄 [PalindromeNumber](src/main/java/com/thealgorithms/maths/PalindromeNumber.java) - 📄 [ParseInteger](src/main/java/com/thealgorithms/maths/ParseInteger.java) - 📄 [PascalTriangle](src/main/java/com/thealgorithms/maths/PascalTriangle.java) @@ -456,9 +516,11 @@ - 📄 [PerfectNumber](src/main/java/com/thealgorithms/maths/PerfectNumber.java) - 📄 [PerfectSquare](src/main/java/com/thealgorithms/maths/PerfectSquare.java) - 📄 [Perimeter](src/main/java/com/thealgorithms/maths/Perimeter.java) + - 📄 [PiApproximation](src/main/java/com/thealgorithms/maths/PiApproximation.java) - 📄 [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** @@ -474,8 +536,10 @@ - 📄 [ReverseNumber](src/main/java/com/thealgorithms/maths/ReverseNumber.java) - 📄 [RomanNumeralUtil](src/main/java/com/thealgorithms/maths/RomanNumeralUtil.java) - 📄 [SecondMinMax](src/main/java/com/thealgorithms/maths/SecondMinMax.java) + - 📄 [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) @@ -485,6 +549,7 @@ - 📄 [SumOfArithmeticSeries](src/main/java/com/thealgorithms/maths/SumOfArithmeticSeries.java) - 📄 [SumOfDigits](src/main/java/com/thealgorithms/maths/SumOfDigits.java) - 📄 [SumOfOddNumbers](src/main/java/com/thealgorithms/maths/SumOfOddNumbers.java) + - 📄 [SumOfSquares](src/main/java/com/thealgorithms/maths/SumOfSquares.java) - 📄 [SumWithoutArithmeticOperators](src/main/java/com/thealgorithms/maths/SumWithoutArithmeticOperators.java) - 📄 [TrinomialTriangle](src/main/java/com/thealgorithms/maths/TrinomialTriangle.java) - 📄 [TwinPrime](src/main/java/com/thealgorithms/maths/TwinPrime.java) @@ -492,8 +557,10 @@ - 📄 [VampireNumber](src/main/java/com/thealgorithms/maths/VampireNumber.java) - 📄 [VectorCrossProduct](src/main/java/com/thealgorithms/maths/VectorCrossProduct.java) - 📄 [Volume](src/main/java/com/thealgorithms/maths/Volume.java) + - 📄 [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) @@ -502,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** @@ -537,12 +605,11 @@ - 📄 [Dijkstra](src/main/java/com/thealgorithms/others/Dijkstra.java) - 📄 [FloydTriangle](src/main/java/com/thealgorithms/others/FloydTriangle.java) - 📄 [GaussLegendre](src/main/java/com/thealgorithms/others/GaussLegendre.java) - - 📄 [HappyNumbersSeq](src/main/java/com/thealgorithms/others/HappyNumbersSeq.java) - 📄 [Huffman](src/main/java/com/thealgorithms/others/Huffman.java) - 📄 [Implementing_auto_completing_features_using_trie](src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java) - 📄 [InsertDeleteInArray](src/main/java/com/thealgorithms/others/InsertDeleteInArray.java) + - 📄 [IterativeFloodFill](src/main/java/com/thealgorithms/others/IterativeFloodFill.java) - 📄 [KochSnowflake](src/main/java/com/thealgorithms/others/KochSnowflake.java) - - 📄 [Krishnamurthy](src/main/java/com/thealgorithms/others/Krishnamurthy.java) - 📄 [LineSweep](src/main/java/com/thealgorithms/others/LineSweep.java) - 📄 [LinearCongruentialGenerator](src/main/java/com/thealgorithms/others/LinearCongruentialGenerator.java) - 📄 [LowestBasePalindrome](src/main/java/com/thealgorithms/others/LowestBasePalindrome.java) @@ -551,18 +618,28 @@ - 📄 [MaximumSumOfDistinctSubarraysWithLengthK](src/main/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthK.java) - 📄 [MemoryManagementAlgorithms](src/main/java/com/thealgorithms/others/MemoryManagementAlgorithms.java) - 📄 [MiniMaxAlgorithm](src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java) + - 📄 [MosAlgorithm](src/main/java/com/thealgorithms/others/MosAlgorithm.java) - 📄 [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** @@ -573,8 +650,11 @@ - 📄 [RandomizedQuickSort](src/main/java/com/thealgorithms/randomized/RandomizedQuickSort.java) - 📄 [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) - 📁 **scheduling** - 📄 [AgingScheduling](src/main/java/com/thealgorithms/scheduling/AgingScheduling.java) - 📄 [EDFScheduling](src/main/java/com/thealgorithms/scheduling/EDFScheduling.java) @@ -608,7 +688,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) @@ -629,6 +709,7 @@ - 📄 [RowColumnWiseSorted2dArrayBinarySearch](src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java) - 📄 [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) @@ -640,6 +721,7 @@ - 📄 [MaxSumKSizeSubarray](src/main/java/com/thealgorithms/slidingwindow/MaxSumKSizeSubarray.java) - 📄 [MaximumSlidingWindow](src/main/java/com/thealgorithms/slidingwindow/MaximumSlidingWindow.java) - 📄 [MinSumKSizeSubarray](src/main/java/com/thealgorithms/slidingwindow/MinSumKSizeSubarray.java) + - 📄 [MinimumWindowSubstring](src/main/java/com/thealgorithms/slidingwindow/MinimumWindowSubstring.java) - 📄 [ShortestCoprimeSegment](src/main/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegment.java) - 📁 **sorts** - 📄 [AdaptiveMergeSort](src/main/java/com/thealgorithms/sorts/AdaptiveMergeSort.java) @@ -672,6 +754,7 @@ - 📄 [PancakeSort](src/main/java/com/thealgorithms/sorts/PancakeSort.java) - 📄 [PatienceSort](src/main/java/com/thealgorithms/sorts/PatienceSort.java) - 📄 [PigeonholeSort](src/main/java/com/thealgorithms/sorts/PigeonholeSort.java) + - 📄 [PriorityQueueSort](src/main/java/com/thealgorithms/sorts/PriorityQueueSort.java) - 📄 [QuickSort](src/main/java/com/thealgorithms/sorts/QuickSort.java) - 📄 [RadixSort](src/main/java/com/thealgorithms/sorts/RadixSort.java) - 📄 [SelectionSort](src/main/java/com/thealgorithms/sorts/SelectionSort.java) @@ -714,9 +797,12 @@ - 📄 [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) + - 📄 [AlternativeStringArrange](src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java) - 📄 [Anagrams](src/main/java/com/thealgorithms/strings/Anagrams.java) - 📄 [CharactersSame](src/main/java/com/thealgorithms/strings/CharactersSame.java) - 📄 [CheckVowels](src/main/java/com/thealgorithms/strings/CheckVowels.java) @@ -724,8 +810,10 @@ - 📄 [CountWords](src/main/java/com/thealgorithms/strings/CountWords.java) - 📄 [HammingDistance](src/main/java/com/thealgorithms/strings/HammingDistance.java) - 📄 [HorspoolSearch](src/main/java/com/thealgorithms/strings/HorspoolSearch.java) + - 📄 [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) @@ -748,6 +836,7 @@ - 📄 [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** @@ -762,6 +851,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) @@ -773,14 +863,19 @@ - 📄 [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) - 📄 [ClearLeftmostSetBitTest](src/test/java/com/thealgorithms/bitmanipulation/ClearLeftmostSetBitTest.java) + - 📄 [CountBitsFlipTest](src/test/java/com/thealgorithms/bitmanipulation/CountBitsFlipTest.java) - 📄 [CountLeadingZerosTest](src/test/java/com/thealgorithms/bitmanipulation/CountLeadingZerosTest.java) - 📄 [CountSetBitsTest](src/test/java/com/thealgorithms/bitmanipulation/CountSetBitsTest.java) - 📄 [FindNthBitTest](src/test/java/com/thealgorithms/bitmanipulation/FindNthBitTest.java) @@ -823,6 +918,8 @@ - 📄 [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) - 📄 [RSATest](src/test/java/com/thealgorithms/ciphers/RSATest.java) @@ -834,13 +931,24 @@ - 📄 [A5CipherTest](src/test/java/com/thealgorithms/ciphers/a5/A5CipherTest.java) - 📄 [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** - 📄 [AffineConverterTest](src/test/java/com/thealgorithms/conversions/AffineConverterTest.java) - 📄 [AnyBaseToDecimalTest](src/test/java/com/thealgorithms/conversions/AnyBaseToDecimalTest.java) - 📄 [AnytoAnyTest](src/test/java/com/thealgorithms/conversions/AnytoAnyTest.java) + - 📄 [Base64Test](src/test/java/com/thealgorithms/conversions/Base64Test.java) - 📄 [BinaryToDecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java) - 📄 [BinaryToHexadecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToHexadecimalTest.java) - 📄 [BinaryToOctalTest](src/test/java/com/thealgorithms/conversions/BinaryToOctalTest.java) + - 📄 [CoordinateConverterTest](src/test/java/com/thealgorithms/conversions/CoordinateConverterTest.java) - 📄 [DecimalToAnyBaseTest](src/test/java/com/thealgorithms/conversions/DecimalToAnyBaseTest.java) - 📄 [DecimalToBinaryTest](src/test/java/com/thealgorithms/conversions/DecimalToBinaryTest.java) - 📄 [DecimalToHexadecimalTest](src/test/java/com/thealgorithms/conversions/DecimalToHexadecimalTest.java) @@ -860,6 +968,8 @@ - 📄 [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) - 📄 [UnitsConverterTest](src/test/java/com/thealgorithms/conversions/UnitsConverterTest.java) @@ -892,8 +1002,11 @@ - 📄 [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) - 📄 [EdmondsBlossomAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/EdmondsBlossomAlgorithmTest.java) @@ -907,6 +1020,7 @@ - 📄 [MatrixGraphsTest](src/test/java/com/thealgorithms/datastructures/graphs/MatrixGraphsTest.java) - 📄 [PrimMSTTest](src/test/java/com/thealgorithms/datastructures/graphs/PrimMSTTest.java) - 📄 [TarjansAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/TarjansAlgorithmTest.java) + - 📄 [TwoSatTest](src/test/java/com/thealgorithms/datastructures/graphs/TwoSatTest.java) - 📄 [WelshPowellTest](src/test/java/com/thealgorithms/datastructures/graphs/WelshPowellTest.java) - 📁 **hashmap** - 📁 **hashing** @@ -914,6 +1028,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) @@ -922,6 +1037,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) @@ -931,9 +1047,11 @@ - 📄 [MinPriorityQueueTest](src/test/java/com/thealgorithms/datastructures/heaps/MinPriorityQueueTest.java) - 📁 **lists** - 📄 [CircleLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/CircleLinkedListTest.java) + - 📄 [CircularDoublyLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java) - 📄 [CountSinglyLinkedListRecursionTest](src/test/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursionTest.java) - 📄 [CreateAndDetectLoopTest](src/test/java/com/thealgorithms/datastructures/lists/CreateAndDetectLoopTest.java) - 📄 [CursorLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/CursorLinkedListTest.java) + - 📄 [FlattenMultilevelLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedListTest.java) - 📄 [MergeKSortedLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedListTest.java) - 📄 [MergeSortedArrayListTest](src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java) - 📄 [MergeSortedSinglyLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/MergeSortedSinglyLinkedListTest.java) @@ -944,6 +1062,7 @@ - 📄 [SinglyLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/SinglyLinkedListTest.java) - 📄 [SkipListTest](src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java) - 📄 [SortedLinkedListTest](src/test/java/com/thealgorithms/datastructures/lists/SortedLinkedListTest.java) + - 📄 [TortoiseHareAlgoTest](src/test/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgoTest.java) - 📁 **queues** - 📄 [CircularQueueTest](src/test/java/com/thealgorithms/datastructures/queues/CircularQueueTest.java) - 📄 [DequeTest](src/test/java/com/thealgorithms/datastructures/queues/DequeTest.java) @@ -964,11 +1083,14 @@ - 📄 [AVLTreeTest](src/test/java/com/thealgorithms/datastructures/trees/AVLTreeTest.java) - 📄 [BSTFromSortedArrayTest](src/test/java/com/thealgorithms/datastructures/trees/BSTFromSortedArrayTest.java) - 📄 [BSTIterativeTest](src/test/java/com/thealgorithms/datastructures/trees/BSTIterativeTest.java) + - 📄 [BSTRecursiveGenericTest](src/test/java/com/thealgorithms/datastructures/trees/BSTRecursiveGenericTest.java) - 📄 [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) @@ -982,11 +1104,15 @@ - 📄 [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) - 📄 [VerticalOrderTraversalTest](src/test/java/com/thealgorithms/datastructures/trees/VerticalOrderTraversalTest.java) - 📄 [ZigzagTraversalTest](src/test/java/com/thealgorithms/datastructures/trees/ZigzagTraversalTest.java) + - 📁 **devutils** + - 📁 **entities** + - 📄 [ProcessDetailsTest](src/test/java/com/thealgorithms/devutils/entities/ProcessDetailsTest.java) - 📁 **divideandconquer** - 📄 [BinaryExponentiationTest](src/test/java/com/thealgorithms/divideandconquer/BinaryExponentiationTest.java) - 📄 [ClosestPairTest](src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java) @@ -1007,6 +1133,7 @@ - 📄 [CoinChangeTest](src/test/java/com/thealgorithms/dynamicprogramming/CoinChangeTest.java) - 📄 [CountFriendsPairingTest](src/test/java/com/thealgorithms/dynamicprogramming/CountFriendsPairingTest.java) - 📄 [DPTest](src/test/java/com/thealgorithms/dynamicprogramming/DPTest.java) + - 📄 [DamerauLevenshteinDistanceTest](src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java) - 📄 [EditDistanceTest](src/test/java/com/thealgorithms/dynamicprogramming/EditDistanceTest.java) - 📄 [EggDroppingTest](src/test/java/com/thealgorithms/dynamicprogramming/EggDroppingTest.java) - 📄 [FibonacciTest](src/test/java/com/thealgorithms/dynamicprogramming/FibonacciTest.java) @@ -1025,9 +1152,11 @@ - 📄 [LongestValidParenthesesTest](src/test/java/com/thealgorithms/dynamicprogramming/LongestValidParenthesesTest.java) - 📄 [MatrixChainMultiplicationTest](src/test/java/com/thealgorithms/dynamicprogramming/MatrixChainMultiplicationTest.java) - 📄 [MatrixChainRecursiveTopDownMemoisationTest](src/test/java/com/thealgorithms/dynamicprogramming/MatrixChainRecursiveTopDownMemoisationTest.java) + - 📄 [MaximumProductSubarrayTest](src/test/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarrayTest.java) - 📄 [MaximumSumOfNonAdjacentElementsTest](src/test/java/com/thealgorithms/dynamicprogramming/MaximumSumOfNonAdjacentElementsTest.java) - 📄 [MinimumPathSumTest](src/test/java/com/thealgorithms/dynamicprogramming/MinimumPathSumTest.java) - 📄 [MinimumSumPartitionTest](src/test/java/com/thealgorithms/dynamicprogramming/MinimumSumPartitionTest.java) + - 📄 [NeedlemanWunschTest](src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java) - 📄 [NewManShanksPrimeTest](src/test/java/com/thealgorithms/dynamicprogramming/NewManShanksPrimeTest.java) - 📄 [OptimalJobSchedulingTest](src/test/java/com/thealgorithms/dynamicprogramming/OptimalJobSchedulingTest.java) - 📄 [PalindromicPartitioningTest](src/test/java/com/thealgorithms/dynamicprogramming/PalindromicPartitioningTest.java) @@ -1035,6 +1164,7 @@ - 📄 [RegexMatchingTest](src/test/java/com/thealgorithms/dynamicprogramming/RegexMatchingTest.java) - 📄 [RodCuttingTest](src/test/java/com/thealgorithms/dynamicprogramming/RodCuttingTest.java) - 📄 [ShortestCommonSupersequenceLengthTest](src/test/java/com/thealgorithms/dynamicprogramming/ShortestCommonSupersequenceLengthTest.java) + - 📄 [SmithWatermanTest](src/test/java/com/thealgorithms/dynamicprogramming/SmithWatermanTest.java) - 📄 [SubsetCountTest](src/test/java/com/thealgorithms/dynamicprogramming/SubsetCountTest.java) - 📄 [SubsetSumSpaceOptimizedTest](src/test/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimizedTest.java) - 📄 [SubsetSumTest](src/test/java/com/thealgorithms/dynamicprogramming/SubsetSumTest.java) @@ -1046,18 +1176,34 @@ - 📄 [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) - 📄 [MidpointEllipseTest](src/test/java/com/thealgorithms/geometry/MidpointEllipseTest.java) - 📄 [PointTest](src/test/java/com/thealgorithms/geometry/PointTest.java) + - 📄 [WusLineTest](src/test/java/com/thealgorithms/geometry/WusLineTest.java) - 📁 **graph** + - 📄 [BronKerboschTest](src/test/java/com/thealgorithms/graph/BronKerboschTest.java) - 📄 [ConstrainedShortestPathTest](src/test/java/com/thealgorithms/graph/ConstrainedShortestPathTest.java) + - 📄 [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) + - 📄 [ZeroOneBfsTest](src/test/java/com/thealgorithms/graph/ZeroOneBfsTest.java) - 📁 **greedyalgorithms** - 📄 [ActivitySelectionTest](src/test/java/com/thealgorithms/greedyalgorithms/ActivitySelectionTest.java) - 📄 [BandwidthAllocationTest](src/test/java/com/thealgorithms/greedyalgorithms/BandwidthAllocationTest.java) @@ -1084,6 +1230,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) @@ -1095,6 +1242,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) @@ -1106,9 +1254,11 @@ - 📄 [DistanceFormulaTest](src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java) - 📄 [DudeneyNumberTest](src/test/java/com/thealgorithms/maths/DudeneyNumberTest.java) - 📄 [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) @@ -1127,26 +1277,34 @@ - 📄 [GCDTest](src/test/java/com/thealgorithms/maths/GCDTest.java) - 📄 [GaussianTest](src/test/java/com/thealgorithms/maths/GaussianTest.java) - 📄 [GenericRootTest](src/test/java/com/thealgorithms/maths/GenericRootTest.java) + - 📄 [GermainPrimeAndSafePrimeTest](src/test/java/com/thealgorithms/maths/GermainPrimeAndSafePrimeTest.java) - 📄 [GoldbachConjectureTest](src/test/java/com/thealgorithms/maths/GoldbachConjectureTest.java) + - 📄 [HappyNumberTest](src/test/java/com/thealgorithms/maths/HappyNumberTest.java) - 📄 [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) - 📄 [KrishnamurthyNumberTest](src/test/java/com/thealgorithms/maths/KrishnamurthyNumberTest.java) - 📄 [LeastCommonMultipleTest](src/test/java/com/thealgorithms/maths/LeastCommonMultipleTest.java) - 📄 [LeonardoNumberTest](src/test/java/com/thealgorithms/maths/LeonardoNumberTest.java) + - 📄 [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) + - 📄 [NumberPersistenceTest](src/test/java/com/thealgorithms/maths/NumberPersistenceTest.java) - 📄 [PalindromeNumberTest](src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java) - 📄 [ParseIntegerTest](src/test/java/com/thealgorithms/maths/ParseIntegerTest.java) - 📄 [PascalTriangleTest](src/test/java/com/thealgorithms/maths/PascalTriangleTest.java) @@ -1154,8 +1312,10 @@ - 📄 [PerfectNumberTest](src/test/java/com/thealgorithms/maths/PerfectNumberTest.java) - 📄 [PerfectSquareTest](src/test/java/com/thealgorithms/maths/PerfectSquareTest.java) - 📄 [PerimeterTest](src/test/java/com/thealgorithms/maths/PerimeterTest.java) + - 📄 [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) @@ -1163,7 +1323,9 @@ - 📄 [QuadraticEquationSolverTest](src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java) - 📄 [ReverseNumberTest](src/test/java/com/thealgorithms/maths/ReverseNumberTest.java) - 📄 [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) @@ -1174,12 +1336,14 @@ - 📄 [SumOfArithmeticSeriesTest](src/test/java/com/thealgorithms/maths/SumOfArithmeticSeriesTest.java) - 📄 [SumOfDigitsTest](src/test/java/com/thealgorithms/maths/SumOfDigitsTest.java) - 📄 [SumOfOddNumbersTest](src/test/java/com/thealgorithms/maths/SumOfOddNumbersTest.java) + - 📄 [SumOfSquaresTest](src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java) - 📄 [SumWithoutArithmeticOperatorsTest](src/test/java/com/thealgorithms/maths/SumWithoutArithmeticOperatorsTest.java) - 📄 [TestArmstrong](src/test/java/com/thealgorithms/maths/TestArmstrong.java) - 📄 [TwinPrimeTest](src/test/java/com/thealgorithms/maths/TwinPrimeTest.java) - 📄 [UniformNumbersTest](src/test/java/com/thealgorithms/maths/UniformNumbersTest.java) - 📄 [VampireNumberTest](src/test/java/com/thealgorithms/maths/VampireNumberTest.java) - 📄 [VolumeTest](src/test/java/com/thealgorithms/maths/VolumeTest.java) + - 📄 [ZellersCongruenceTest](src/test/java/com/thealgorithms/maths/ZellersCongruenceTest.java) - 📁 **prime** - 📄 [LiouvilleLambdaFunctionTest](src/test/java/com/thealgorithms/maths/prime/LiouvilleLambdaFunctionTest.java) - 📄 [MillerRabinPrimalityCheckTest](src/test/java/com/thealgorithms/maths/prime/MillerRabinPrimalityCheckTest.java) @@ -1188,14 +1352,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) @@ -1219,23 +1385,39 @@ - 📄 [CountFriendsPairingTest](src/test/java/com/thealgorithms/others/CountFriendsPairingTest.java) - 📄 [FirstFitCPUTest](src/test/java/com/thealgorithms/others/FirstFitCPUTest.java) - 📄 [FloydTriangleTest](src/test/java/com/thealgorithms/others/FloydTriangleTest.java) + - 📄 [HuffmanTest](src/test/java/com/thealgorithms/others/HuffmanTest.java) + - 📄 [InsertDeleteInArrayTest](src/test/java/com/thealgorithms/others/InsertDeleteInArrayTest.java) + - 📄 [IterativeFloodFillTest](src/test/java/com/thealgorithms/others/IterativeFloodFillTest.java) - 📄 [KadaneAlogrithmTest](src/test/java/com/thealgorithms/others/KadaneAlogrithmTest.java) - 📄 [LineSweepTest](src/test/java/com/thealgorithms/others/LineSweepTest.java) - 📄 [LinkListSortTest](src/test/java/com/thealgorithms/others/LinkListSortTest.java) - 📄 [LowestBasePalindromeTest](src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java) - 📄 [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** @@ -1246,8 +1428,11 @@ - 📄 [RandomizedQuickSortTest](src/test/java/com/thealgorithms/randomized/RandomizedQuickSortTest.java) - 📄 [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) - 📁 **scheduling** - 📄 [AgingSchedulingTest](src/test/java/com/thealgorithms/scheduling/AgingSchedulingTest.java) - 📄 [EDFSchedulingTest](src/test/java/com/thealgorithms/scheduling/EDFSchedulingTest.java) @@ -1302,6 +1487,7 @@ - 📄 [RowColumnWiseSorted2dArrayBinarySearchTest](src/test/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearchTest.java) - 📄 [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) @@ -1314,6 +1500,7 @@ - 📄 [MaxSumKSizeSubarrayTest](src/test/java/com/thealgorithms/slidingwindow/MaxSumKSizeSubarrayTest.java) - 📄 [MaximumSlidingWindowTest](src/test/java/com/thealgorithms/slidingwindow/MaximumSlidingWindowTest.java) - 📄 [MinSumKSizeSubarrayTest](src/test/java/com/thealgorithms/slidingwindow/MinSumKSizeSubarrayTest.java) + - 📄 [MinimumWindowSubstringTest](src/test/java/com/thealgorithms/slidingwindow/MinimumWindowSubstringTest.java) - 📄 [ShortestCoprimeSegmentTest](src/test/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegmentTest.java) - 📁 **sorts** - 📄 [AdaptiveMergeSortTest](src/test/java/com/thealgorithms/sorts/AdaptiveMergeSortTest.java) @@ -1345,6 +1532,7 @@ - 📄 [PancakeSortTest](src/test/java/com/thealgorithms/sorts/PancakeSortTest.java) - 📄 [PatienceSortTest](src/test/java/com/thealgorithms/sorts/PatienceSortTest.java) - 📄 [PigeonholeSortTest](src/test/java/com/thealgorithms/sorts/PigeonholeSortTest.java) + - 📄 [PriorityQueueSortTest](src/test/java/com/thealgorithms/sorts/PriorityQueueSortTest.java) - 📄 [QuickSortTest](src/test/java/com/thealgorithms/sorts/QuickSortTest.java) - 📄 [RadixSortTest](src/test/java/com/thealgorithms/sorts/RadixSortTest.java) - 📄 [SelectionSortRecursiveTest](src/test/java/com/thealgorithms/sorts/SelectionSortRecursiveTest.java) @@ -1386,9 +1574,12 @@ - 📄 [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) + - 📄 [AlternativeStringArrangeTest](src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java) - 📄 [AnagramsTest](src/test/java/com/thealgorithms/strings/AnagramsTest.java) - 📄 [CharactersSameTest](src/test/java/com/thealgorithms/strings/CharactersSameTest.java) - 📄 [CheckVowelsTest](src/test/java/com/thealgorithms/strings/CheckVowelsTest.java) @@ -1396,7 +1587,9 @@ - 📄 [CountWordsTest](src/test/java/com/thealgorithms/strings/CountWordsTest.java) - 📄 [HammingDistanceTest](src/test/java/com/thealgorithms/strings/HammingDistanceTest.java) - 📄 [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) @@ -1418,6 +1611,7 @@ - 📄 [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/pmd-exclude.properties b/pmd-exclude.properties index 4c0ed625d884..a3c95b12fa4b 100644 --- a/pmd-exclude.properties +++ b/pmd-exclude.properties @@ -88,12 +88,14 @@ com.thealgorithms.others.LinearCongruentialGenerator=UselessMainMethod com.thealgorithms.others.Luhn=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.others.Mandelbrot=UselessMainMethod,UselessParentheses com.thealgorithms.others.MiniMaxAlgorithm=UselessMainMethod,UselessParentheses +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.others.Verhoeff=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.puzzlesandgames.Sudoku=UselessMainMethod +com.thealgorithms.recursion.DiceThrower=UselessMainMethod com.thealgorithms.searches.HowManyTimesRotated=UselessMainMethod com.thealgorithms.searches.InterpolationSearch=UselessParentheses com.thealgorithms.searches.KMPSearch=UselessParentheses diff --git a/pom.xml b/pom.xml index 14a0375fe170..59a497427c0d 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ UTF-8 21 21 - 3.27.4 + 3.27.6 @@ -20,7 +20,7 @@ org.junit junit-bom - 5.13.4 + 6.0.1 pom import @@ -42,13 +42,13 @@ org.mockito mockito-core - 5.19.0 + 5.21.0 test org.apache.commons commons-lang3 - 3.18.0 + 3.20.0 org.apache.commons @@ -69,10 +69,9 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.14.1 - 21 - 21 + 21 -Xlint:all -Xlint:-auxiliaryclass @@ -83,7 +82,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 @@ -113,14 +112,14 @@ com.puppycrawl.tools checkstyle - 11.0.0 + 12.3.1 com.github.spotbugs spotbugs-maven-plugin - 4.9.6.0 + 4.9.8.2 spotbugs-exclude.xml true @@ -128,7 +127,7 @@ com.mebigfatguy.fb-contrib fb-contrib - 7.6.14 + 7.7.2 com.h3xstream.findsecbugs @@ -141,7 +140,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.27.0 + 3.28.0 /rulesets/java/maven-pmd-plugin-default.xml @@ -156,4 +155,4 @@ - + \ No newline at end of file diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 4f327a443781..3e2f1ff84ca8 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -80,9 +80,6 @@ - - - @@ -204,6 +201,12 @@ + + + + + + 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/CombinationSum.java b/src/main/java/com/thealgorithms/backtracking/CombinationSum.java new file mode 100644 index 000000000000..09b99032bdc1 --- /dev/null +++ b/src/main/java/com/thealgorithms/backtracking/CombinationSum.java @@ -0,0 +1,48 @@ +package com.thealgorithms.backtracking; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Backtracking: pick/not-pick with reuse of candidates. */ +public final class CombinationSum { + private CombinationSum() { + throw new UnsupportedOperationException("Utility class"); + } + + public static List> combinationSum(int[] candidates, int target) { + List> results = new ArrayList<>(); + if (candidates == null || candidates.length == 0) { + return results; + } + + // Sort to help with pruning duplicates and early termination + Arrays.sort(candidates); + backtrack(candidates, target, 0, new ArrayList<>(), results); + return results; + } + + private static void backtrack(int[] candidates, int remaining, int start, List combination, List> results) { + if (remaining == 0) { + // Found valid combination; add a copy + results.add(new ArrayList<>(combination)); + return; + } + + for (int i = start; i < candidates.length; i++) { + int candidate = candidates[i]; + + // If candidate is greater than remaining target, further candidates (sorted) will also be too big + if (candidate > remaining) { + break; + } + + // include candidate + combination.add(candidate); + // Because we can reuse the same element, we pass i (not i + 1) + backtrack(candidates, remaining - candidate, i, combination, results); + // backtrack: remove last + combination.remove(combination.size() - 1); + } + } +} diff --git a/src/main/java/com/thealgorithms/backtracking/FloodFill.java b/src/main/java/com/thealgorithms/backtracking/FloodFill.java index c8219ca8ba7e..0f31a9c5a30e 100644 --- a/src/main/java/com/thealgorithms/backtracking/FloodFill.java +++ b/src/main/java/com/thealgorithms/backtracking/FloodFill.java @@ -12,8 +12,8 @@ private FloodFill() { * Get the color at the given coordinates of a 2D image * * @param image The image to be filled - * @param x The x co-ordinate of which color is to be obtained - * @param y The y co-ordinate of which color is to be obtained + * @param x The x coordinate of which color is to be obtained + * @param y The y coordinate of which color is to be obtained */ public static int getPixel(final int[][] image, final int x, final int y) { @@ -24,8 +24,8 @@ public static int getPixel(final int[][] image, final int x, final int y) { * Put the color at the given coordinates of a 2D image * * @param image The image to be filled - * @param x The x co-ordinate at which color is to be filled - * @param y The y co-ordinate at which color is to be filled + * @param x The x coordinate at which color is to be filled + * @param y The y coordinate at which color is to be filled */ public static void putPixel(final int[][] image, final int x, final int y, final int newColor) { image[x][y] = newColor; @@ -35,8 +35,8 @@ public static void putPixel(final int[][] image, final int x, final int y, final * Fill the 2D image with new color * * @param image The image to be filled - * @param x The x co-ordinate at which color is to be filled - * @param y The y co-ordinate at which color is to be filled + * @param x The x coordinate at which color is to be filled + * @param y The y coordinate at which color is to be filled * @param newColor The new color which to be filled in the image * @param oldColor The old color which is to be replaced in the image */ 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/bitmanipulation/BitRotate.java b/src/main/java/com/thealgorithms/bitmanipulation/BitRotate.java new file mode 100644 index 000000000000..226e09e78d1f --- /dev/null +++ b/src/main/java/com/thealgorithms/bitmanipulation/BitRotate.java @@ -0,0 +1,83 @@ +package com.thealgorithms.bitmanipulation; + +/** + * Utility class for performing circular bit rotations on 32-bit integers. + * Bit rotation is a circular shift operation where bits shifted out on one end + * are reinserted on the opposite end. + * + *

This class provides methods for both left and right circular rotations, + * supporting only 32-bit integer operations with proper shift normalization + * and error handling.

+ * + * @see Bit Rotation + */ +public final class BitRotate { + + /** + * Private constructor to prevent instantiation. + * This is a utility class with only static methods. + */ + private BitRotate() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Performs a circular left rotation (left shift) on a 32-bit integer. + * Bits shifted out from the left side are inserted on the right side. + * + * @param value the 32-bit integer value to rotate + * @param shift the number of positions to rotate left (must be non-negative) + * @return the result of left rotating the value by the specified shift amount + * @throws IllegalArgumentException if shift is negative + * + * @example + * // Binary: 10000000 00000000 00000000 00000001 + * rotateLeft(0x80000001, 1) + * // Returns: 3 (binary: 00000000 00000000 00000000 00000011) + */ + public static int rotateLeft(int value, int shift) { + if (shift < 0) { + throw new IllegalArgumentException("Shift amount cannot be negative: " + shift); + } + + // Normalize shift to the range [0, 31] using modulo 32 + shift = shift % 32; + + if (shift == 0) { + return value; + } + + // Left rotation: (value << shift) | (value >>> (32 - shift)) + return (value << shift) | (value >>> (32 - shift)); + } + + /** + * Performs a circular right rotation (right shift) on a 32-bit integer. + * Bits shifted out from the right side are inserted on the left side. + * + * @param value the 32-bit integer value to rotate + * @param shift the number of positions to rotate right (must be non-negative) + * @return the result of right rotating the value by the specified shift amount + * @throws IllegalArgumentException if shift is negative + * + * @example + * // Binary: 00000000 00000000 00000000 00000011 + * rotateRight(3, 1) + * // Returns: -2147483647 (binary: 10000000 00000000 00000000 00000001) + */ + public static int rotateRight(int value, int shift) { + if (shift < 0) { + throw new IllegalArgumentException("Shift amount cannot be negative: " + shift); + } + + // Normalize shift to the range [0, 31] using modulo 32 + shift = shift % 32; + + if (shift == 0) { + return value; + } + + // Right rotation: (value >>> shift) | (value << (32 - shift)) + return (value >>> shift) | (value << (32 - shift)); + } +} diff --git a/src/main/java/com/thealgorithms/bitmanipulation/BitSwap.java b/src/main/java/com/thealgorithms/bitmanipulation/BitSwap.java index d8c207567ba6..634c9e7b3b44 100644 --- a/src/main/java/com/thealgorithms/bitmanipulation/BitSwap.java +++ b/src/main/java/com/thealgorithms/bitmanipulation/BitSwap.java @@ -17,12 +17,15 @@ private BitSwap() { * @return The modified value with swapped bits * @throws IllegalArgumentException if either position is negative or ≥ 32 */ + public static int bitSwap(int data, final int posA, final int posB) { if (posA < 0 || posA >= Integer.SIZE || posB < 0 || posB >= Integer.SIZE) { throw new IllegalArgumentException("Bit positions must be between 0 and 31"); } - if (SingleBitOperations.getBit(data, posA) != SingleBitOperations.getBit(data, posB)) { + boolean bitA = ((data >> posA) & 1) != 0; + boolean bitB = ((data >> posB) & 1) != 0; + if (bitA != bitB) { data ^= (1 << posA) ^ (1 << posB); } return data; diff --git a/src/main/java/com/thealgorithms/bitmanipulation/BitwiseGCD.java b/src/main/java/com/thealgorithms/bitmanipulation/BitwiseGCD.java new file mode 100644 index 000000000000..516563459256 --- /dev/null +++ b/src/main/java/com/thealgorithms/bitmanipulation/BitwiseGCD.java @@ -0,0 +1,147 @@ +package com.thealgorithms.bitmanipulation; + +import java.math.BigInteger; + +/** + * Bitwise GCD implementation with full-range support utilities. + * + *

This class provides a fast binary (Stein's) GCD implementation for {@code long} + * inputs and a BigInteger-backed API for full 2's-complement range support (including + * {@code Long.MIN_VALUE}). The {@code long} implementation is efficient and avoids + * division/modulo operations. For edge-cases that overflow signed-64-bit ranges + * (e.g., gcd(Long.MIN_VALUE, 0) = 2^63), use the BigInteger API {@code gcdBig}. + * + *

Behaviour: + *

    + *
  • {@code gcd(long,long)} : returns non-negative {@code long} gcd for inputs whose + * absolute values fit in signed {@code long} (i.e., not causing an unsigned 2^63 result). + * If the true gcd does not fit in a signed {@code long} (for example gcd(Long.MIN_VALUE,0) = 2^63) + * this method will delegate to BigInteger and throw {@link ArithmeticException} if the + * BigInteger result does not fit into a signed {@code long}.
  • + *
  • {@code gcdBig(BigInteger, BigInteger)} : returns the exact gcd as a {@link BigInteger} + * and works for the full signed-64-bit range and beyond.
  • + *
+ */ +public final class BitwiseGCD { + + private BitwiseGCD() { + } + + /** + * Computes GCD of two long values using Stein's algorithm (binary GCD). + *

Handles negative inputs. If either input is {@code Long.MIN_VALUE} the + * method delegates to the BigInteger implementation and will throw {@link ArithmeticException} + * if the result cannot be represented as a signed {@code long}. + * + * @param a first value (may be negative) + * @param b second value (may be negative) + * @return non-negative gcd as a {@code long} + * @throws ArithmeticException when the exact gcd does not fit into a signed {@code long} + */ + public static long gcd(long a, long b) { + // Trivial cases + if (a == 0L) { + return absOrThrowIfOverflow(b); + } + if (b == 0L) { + return absOrThrowIfOverflow(a); + } + + // If either is Long.MIN_VALUE, absolute value doesn't fit into signed long. + if (a == Long.MIN_VALUE || b == Long.MIN_VALUE) { + // Delegate to BigInteger and try to return a long if it fits + BigInteger g = gcdBig(BigInteger.valueOf(a), BigInteger.valueOf(b)); + return g.longValueExact(); + } + + // Work with non-negative long values now (safe because we excluded Long.MIN_VALUE) + a = (a < 0) ? -a : a; + b = (b < 0) ? -b : b; + + // Count common factors of 2 + int commonTwos = Long.numberOfTrailingZeros(a | b); + + // Remove all factors of 2 from a + a >>= Long.numberOfTrailingZeros(a); + + while (b != 0L) { + // Remove all factors of 2 from b + b >>= Long.numberOfTrailingZeros(b); + + // Now both a and b are odd. Ensure a <= b + if (a > b) { + long tmp = a; + a = b; + b = tmp; + } + + // b >= a; subtract a from b (result is even) + b = b - a; + } + + // Restore common powers of two + return a << commonTwos; + } + + /** + * Helper to return absolute value of x unless x == Long.MIN_VALUE, in which + * case we delegate to BigInteger and throw to indicate overflow. + */ + private static long absOrThrowIfOverflow(long x) { + if (x == Long.MIN_VALUE) { + // |Long.MIN_VALUE| = 2^63 which does not fit into signed long + throw new ArithmeticException("Absolute value of Long.MIN_VALUE does not fit into signed long. Use gcdBig() for full-range support."); + } + return (x < 0) ? -x : x; + } + + /** + * Computes GCD for an array of {@code long} values. Returns 0 for empty/null arrays. + * If any intermediate gcd cannot be represented in signed long (rare), an ArithmeticException + * will be thrown. + */ + public static long gcd(long... values) { + + if (values == null || values.length == 0) { + return 0L; + } + long result = values[0]; + for (int i = 1; i < values.length; i++) { + result = gcd(result, values[i]); + if (result == 1L) { + return 1L; // early exit + } + } + return result; + } + + /** + * BigInteger-backed gcd that works for the full integer range (and beyond). + * This is the recommended method when inputs may be Long.MIN_VALUE or when you + * need an exact result even if it is greater than Long.MAX_VALUE. + * @param a first value (may be negative) + * @param b second value (may be negative) + * @return non-negative gcd as a {@link BigInteger} + */ + public static BigInteger gcdBig(BigInteger a, BigInteger b) { + + if (a == null || b == null) { + throw new NullPointerException("Arguments must not be null"); + } + return a.abs().gcd(b.abs()); + } + + /** + * Convenience overload that accepts signed-64 inputs and returns BigInteger gcd. + */ + public static BigInteger gcdBig(long a, long b) { + return gcdBig(BigInteger.valueOf(a), BigInteger.valueOf(b)); + } + + /** + * int overload for convenience. + */ + public static int gcd(int a, int b) { + return (int) gcd((long) a, (long) b); + } +} diff --git a/src/main/java/com/thealgorithms/bitmanipulation/CountBitsFlip.java b/src/main/java/com/thealgorithms/bitmanipulation/CountBitsFlip.java new file mode 100644 index 000000000000..8d2c757e5e0a --- /dev/null +++ b/src/main/java/com/thealgorithms/bitmanipulation/CountBitsFlip.java @@ -0,0 +1,63 @@ +package com.thealgorithms.bitmanipulation; + +/** + * Implementation to count number of bits to be flipped to convert A to B + * + * Problem: Given two numbers A and B, count the number of bits needed to be + * flipped to convert A to B. + * + * Example: + * A = 10 (01010 in binary) + * B = 20 (10100 in binary) + * XOR = 30 (11110 in binary) - positions where bits differ + * Answer: 4 bits need to be flipped + * + * Time Complexity: O(log n) - where n is the number of set bits + * Space Complexity: O(1) + * + *@author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class CountBitsFlip { + + private CountBitsFlip() { + throw new AssertionError("No instances."); + } + + /** + * Counts the number of bits that need to be flipped to convert a to b + * + * Algorithm: + * 1. XOR a and b to get positions where bits differ + * 2. Count the number of set bits in the XOR result + * 3. Use Brian Kernighan's algorithm: n & (n-1) removes rightmost set bit + * + * @param a the source number + * @param b the target number + * @return the number of bits to flip to convert A to B + */ + public static long countBitsFlip(long a, long b) { + int count = 0; + + // XOR gives us positions where bits differ + long xorResult = a ^ b; + + // Count set bits using Brian Kernighan's algorithm + while (xorResult != 0) { + xorResult = xorResult & (xorResult - 1); // Remove rightmost set bit + count++; + } + + return count; + } + + /** + * Alternative implementation using Long.bitCount(). + * + * @param a the source number + * @param b the target number + * @return the number of bits to flip to convert a to b + */ + public static long countBitsFlipAlternative(long a, long b) { + return Long.bitCount(a ^ b); + } +} 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/AES.java b/src/main/java/com/thealgorithms/ciphers/AES.java index 1c283f6b7655..df51eba55310 100644 --- a/src/main/java/com/thealgorithms/ciphers/AES.java +++ b/src/main/java/com/thealgorithms/ciphers/AES.java @@ -2738,7 +2738,7 @@ public static BigInteger decrypt(BigInteger cipherText, BigInteger key) { public static void main(String[] args) { try (Scanner input = new Scanner(System.in)) { - System.out.println("Enter (e) letter for encrpyt or (d) letter for decrypt :"); + System.out.println("Enter (e) letter for encrypt or (d) letter for decrypt :"); char choice = input.nextLine().charAt(0); String in; switch (choice) { 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/PermutationCipher.java b/src/main/java/com/thealgorithms/ciphers/PermutationCipher.java new file mode 100644 index 000000000000..ce443545db1d --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/PermutationCipher.java @@ -0,0 +1,194 @@ +package com.thealgorithms.ciphers; + +import java.util.HashSet; +import java.util.Set; + +/** + * A Java implementation of Permutation Cipher. + * It is a type of transposition cipher in which the plaintext is divided into blocks + * and the characters within each block are rearranged according to a fixed permutation key. + * + * For example, with key {3, 1, 2} and plaintext "HELLO", the text is divided into blocks + * of 3 characters: "HEL" and "LO" (with padding). The characters are then rearranged + * according to the key positions. + * + * @author GitHub Copilot + */ +public class PermutationCipher { + + private static final char PADDING_CHAR = 'X'; + + /** + * Encrypts the given plaintext using the permutation cipher with the specified key. + * + * @param plaintext the text to encrypt + * @param key the permutation key (array of integers representing positions) + * @return the encrypted text + * @throws IllegalArgumentException if the key is invalid + */ + public String encrypt(String plaintext, int[] key) { + validateKey(key); + + if (plaintext == null || plaintext.isEmpty()) { + return plaintext; + } + + // Remove spaces and convert to uppercase for consistent processing + String cleanText = plaintext.replaceAll("\\s+", "").toUpperCase(); + + // Pad the text to make it divisible by key length + String paddedText = padText(cleanText, key.length); + + StringBuilder encrypted = new StringBuilder(); + + // Process text in blocks of key length + for (int i = 0; i < paddedText.length(); i += key.length) { + String block = paddedText.substring(i, Math.min(i + key.length, paddedText.length())); + encrypted.append(permuteBlock(block, key)); + } + + return encrypted.toString(); + } + + /** + * Decrypts the given ciphertext using the permutation cipher with the specified key. + * + * @param ciphertext the text to decrypt + * @param key the permutation key (array of integers representing positions) + * @return the decrypted text + * @throws IllegalArgumentException if the key is invalid + */ + public String decrypt(String ciphertext, int[] key) { + validateKey(key); + + if (ciphertext == null || ciphertext.isEmpty()) { + return ciphertext; + } + + // Create the inverse permutation + int[] inverseKey = createInverseKey(key); + + StringBuilder decrypted = new StringBuilder(); + + // Process text in blocks of key length + for (int i = 0; i < ciphertext.length(); i += key.length) { + String block = ciphertext.substring(i, Math.min(i + key.length, ciphertext.length())); + decrypted.append(permuteBlock(block, inverseKey)); + } + + // Remove padding characters from the end + return removePadding(decrypted.toString()); + } + /** + * Validates that the permutation key is valid. + * A valid key must contain all integers from 1 to n exactly once, where n is the key length. + * + * @param key the permutation key to validate + * @throws IllegalArgumentException if the key is invalid + */ + private void validateKey(int[] key) { + if (key == null || key.length == 0) { + throw new IllegalArgumentException("Key cannot be null or empty"); + } + + Set keySet = new HashSet<>(); + for (int position : key) { + if (position < 1 || position > key.length) { + throw new IllegalArgumentException("Key must contain integers from 1 to " + key.length); + } + if (!keySet.add(position)) { + throw new IllegalArgumentException("Key must contain each position exactly once"); + } + } + } + + /** + * Pads the text with padding characters to make its length divisible by the block size. + * + * @param text the text to pad + * @param blockSize the size of each block + * @return the padded text + */ + private String padText(String text, int blockSize) { + int remainder = text.length() % blockSize; + if (remainder == 0) { + return text; + } + + int paddingNeeded = blockSize - remainder; + StringBuilder padded = new StringBuilder(text); + for (int i = 0; i < paddingNeeded; i++) { + padded.append(PADDING_CHAR); + } + + return padded.toString(); + } + /** + * Applies the permutation to a single block of text. + * + * @param block the block to permute + * @param key the permutation key + * @return the permuted block + */ + private String permuteBlock(String block, int[] key) { + if (block.length() != key.length) { + // Handle case where block is shorter than key (shouldn't happen with proper padding) + block = padText(block, key.length); + } + + char[] result = new char[key.length]; + char[] blockChars = block.toCharArray(); + + for (int i = 0; i < key.length; i++) { + // Key positions are 1-based, so subtract 1 for 0-based array indexing + result[i] = blockChars[key[i] - 1]; + } + + return new String(result); + } + + /** + * Creates the inverse permutation key for decryption. + * + * @param key the original permutation key + * @return the inverse key + */ + private int[] createInverseKey(int[] key) { + int[] inverse = new int[key.length]; + + for (int i = 0; i < key.length; i++) { + // The inverse key maps each position to where it should go + inverse[key[i] - 1] = i + 1; + } + + return inverse; + } + + /** + * Removes padding characters from the end of the decrypted text. + * + * @param text the text to remove padding from + * @return the text without padding + */ + private String removePadding(String text) { + if (text.isEmpty()) { + return text; + } + + int i = text.length() - 1; + while (i >= 0 && text.charAt(i) == PADDING_CHAR) { + i--; + } + + return text.substring(0, i + 1); + } + + /** + * Gets the padding character used by this cipher. + * + * @return the padding character + */ + public char getPaddingChar() { + return PADDING_CHAR; + } +} diff --git a/src/main/java/com/thealgorithms/compression/ArithmeticCoding.java b/src/main/java/com/thealgorithms/compression/ArithmeticCoding.java new file mode 100644 index 000000000000..b5ccf359d1be --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/ArithmeticCoding.java @@ -0,0 +1,157 @@ +package com.thealgorithms.compression; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of the Arithmetic Coding algorithm. + * + *

+ * Arithmetic coding is a form of entropy encoding used in lossless data + * compression. It encodes an entire message into a single number, a fraction n + * where (0.0 <= n < 1.0). Unlike Huffman coding, which assigns a specific + * bit sequence to each symbol, arithmetic coding represents the message as a + * sub-interval of the [0, 1) interval. + *

+ * + *

+ * This implementation uses BigDecimal for precision to handle the shrinking + * intervals, making it suitable for educational purposes to demonstrate the + * core logic. + *

+ * + *

+ * Time Complexity: O(n*m) for compression and decompression where n is the + * length of the input and m is the number of unique symbols, due to the need + * to calculate symbol probabilities. + *

+ * + *

+ * References: + *

+ *

+ */ +public final class ArithmeticCoding { + + private ArithmeticCoding() { + } + + /** + * Compresses a string using the Arithmetic Coding algorithm. + * + * @param uncompressed The string to be compressed. + * @return The compressed representation as a BigDecimal number. + * @throws IllegalArgumentException if the input string is null or empty. + */ + public static BigDecimal compress(String uncompressed) { + if (uncompressed == null || uncompressed.isEmpty()) { + throw new IllegalArgumentException("Input string cannot be null or empty."); + } + + Map probabilityTable = calculateProbabilities(uncompressed); + + BigDecimal low = BigDecimal.ZERO; + BigDecimal high = BigDecimal.ONE; + + for (char symbol : uncompressed.toCharArray()) { + BigDecimal range = high.subtract(low); + Symbol sym = probabilityTable.get(symbol); + + high = low.add(range.multiply(sym.high())); + low = low.add(range.multiply(sym.low())); + } + + return low; // Return the lower bound of the final interval + } + + /** + * Decompresses a BigDecimal number back into the original string. + * + * @param compressed The compressed BigDecimal number. + * @param length The length of the original uncompressed string. + * @param probabilityTable The probability table used during compression. + * @return The original, uncompressed string. + */ + public static String decompress(BigDecimal compressed, int length, Map probabilityTable) { + StringBuilder decompressed = new StringBuilder(); + + // Create a sorted list of symbols for deterministic decompression, matching the + // order used in calculateProbabilities + List> sortedSymbols = new ArrayList<>(probabilityTable.entrySet()); + sortedSymbols.sort(Map.Entry.comparingByKey()); + + BigDecimal low = BigDecimal.ZERO; + BigDecimal high = BigDecimal.ONE; + + for (int i = 0; i < length; i++) { + BigDecimal range = high.subtract(low); + + // Find which symbol the compressed value falls into + for (Map.Entry entry : sortedSymbols) { + Symbol sym = entry.getValue(); + + // Calculate the actual range for this symbol in the current interval + BigDecimal symLow = low.add(range.multiply(sym.low())); + BigDecimal symHigh = low.add(range.multiply(sym.high())); + + // Check if the compressed value falls within this symbol's range + if (compressed.compareTo(symLow) >= 0 && compressed.compareTo(symHigh) < 0) { + decompressed.append(entry.getKey()); + + // Update the interval for the next iteration + low = symLow; + high = symHigh; + break; + } + } + } + + return decompressed.toString(); + } + + /** + * Calculates the frequency and probability range for each character in the + * input string in a deterministic order. + * + * @param text The input string. + * @return A map from each character to a Symbol object containing its + * probability range. + */ + public static Map calculateProbabilities(String text) { + Map frequencies = new HashMap<>(); + for (char c : text.toCharArray()) { + frequencies.put(c, frequencies.getOrDefault(c, 0) + 1); + } + + // Sort the characters to ensure a deterministic order for the probability table + List sortedKeys = new ArrayList<>(frequencies.keySet()); + Collections.sort(sortedKeys); + + Map probabilityTable = new HashMap<>(); + BigDecimal currentLow = BigDecimal.ZERO; + int total = text.length(); + + for (char symbol : sortedKeys) { + BigDecimal probability = BigDecimal.valueOf(frequencies.get(symbol)).divide(BigDecimal.valueOf(total), MathContext.DECIMAL128); + BigDecimal high = currentLow.add(probability); + probabilityTable.put(symbol, new Symbol(currentLow, high)); + currentLow = high; + } + + return probabilityTable; + } + + /** + * Helper class to store the probability range [low, high) for a symbol. + */ + public record Symbol(BigDecimal low, BigDecimal high) { + } +} diff --git a/src/main/java/com/thealgorithms/compression/BurrowsWheelerTransform.java b/src/main/java/com/thealgorithms/compression/BurrowsWheelerTransform.java new file mode 100644 index 000000000000..a148517e5b55 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/BurrowsWheelerTransform.java @@ -0,0 +1,220 @@ +package com.thealgorithms.compression; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the Burrows-Wheeler Transform (BWT) and its inverse. + *

+ * BWT is a reversible data transformation algorithm that rearranges a string into runs of + * similar characters. While not a compression algorithm itself, it significantly improves + * the compressibility of data for subsequent algorithms like Move-to-Front encoding and + * Run-Length Encoding. + *

+ * + *

The transform works by: + *

    + *
  1. Generating all rotations of the input string
  2. + *
  3. Sorting these rotations lexicographically
  4. + *
  5. Taking the last column of the sorted matrix as output
  6. + *
  7. Recording the index of the original string in the sorted matrix
  8. + *
+ *

+ * + *

Important: The input string should end with a unique end-of-string marker + * (typically '$') that: + *

    + *
  • Does not appear anywhere else in the text
  • + *
  • Is lexicographically smaller than all other characters
  • + *
  • Ensures unique rotations and enables correct inverse transformation
  • + *
+ * Without this marker, the inverse transform may not correctly reconstruct the original string. + *

+ * + *

Time Complexity: + *

    + *
  • Forward transform: O(n² log n) where n is the string length
  • + *
  • Inverse transform: O(n) using the LF-mapping technique
  • + *
+ *

+ * + *

Example:

+ *
+ * Input:  "banana$"
+ * Output: BWTResult("annb$aa", 4)
+ *         - "annb$aa" is the transformed string (groups similar characters)
+ *         - 4 is the index of the original string in the sorted rotations
+ * 
+ * + * @see Burrows–Wheeler transform (Wikipedia) + */ +public final class BurrowsWheelerTransform { + + private BurrowsWheelerTransform() { + } + + /** + * A container for the result of the forward BWT. + *

+ * Contains the transformed string and the index of the original string + * in the sorted rotations matrix, both of which are required for the + * inverse transformation. + *

+ */ + public static class BWTResult { + /** The transformed string (last column of the sorted rotation matrix) */ + public final String transformed; + + /** The index of the original string in the sorted rotations matrix */ + public final int originalIndex; + + /** + * Constructs a BWTResult with the transformed string and original index. + * + * @param transformed the transformed string (L-column) + * @param originalIndex the index of the original string in sorted rotations + */ + public BWTResult(String transformed, int originalIndex) { + this.transformed = transformed; + this.originalIndex = originalIndex; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BWTResult bwtResult = (BWTResult) obj; + return originalIndex == bwtResult.originalIndex && transformed.equals(bwtResult.transformed); + } + + @Override + public int hashCode() { + return 31 * transformed.hashCode() + originalIndex; + } + + @Override + public String toString() { + return "BWTResult[transformed=" + transformed + ", originalIndex=" + originalIndex + "]"; + } + } + + /** + * Performs the forward Burrows-Wheeler Transform on the input string. + *

+ * The algorithm generates all cyclic rotations of the input, sorts them + * lexicographically, and returns the last column of this sorted matrix + * along with the position of the original string. + *

+ * + *

Note: It is strongly recommended that the input string ends with + * a unique end-of-string marker (e.g., '$') that is lexicographically smaller + * than any other character in the string. This ensures correct inversion.

+ * + * @param text the input string to transform; must not be {@code null} + * @return a {@link BWTResult} object containing the transformed string (L-column) + * and the index of the original string in the sorted rotations matrix; + * returns {@code BWTResult("", -1)} for empty input + * @throws NullPointerException if {@code text} is {@code null} + */ + public static BWTResult transform(String text) { + if (text == null || text.isEmpty()) { + return new BWTResult("", -1); + } + + int n = text.length(); + + // Generate all rotations of the input string + String[] rotations = new String[n]; + for (int i = 0; i < n; i++) { + rotations[i] = text.substring(i) + text.substring(0, i); + } + + // Sort rotations lexicographically + Arrays.sort(rotations); + int originalIndex = Arrays.binarySearch(rotations, text); + StringBuilder lastColumn = new StringBuilder(n); + for (int i = 0; i < n; i++) { + lastColumn.append(rotations[i].charAt(n - 1)); + } + + return new BWTResult(lastColumn.toString(), originalIndex); + } + + /** + * Performs the inverse Burrows-Wheeler Transform using the LF-mapping technique. + *

+ * The LF-mapping (Last-First mapping) is an efficient method to reconstruct + * the original string from the BWT output without explicitly reconstructing + * the entire sorted rotations matrix. + *

+ * + *

The algorithm works by: + *

    + *
  1. Creating the first column by sorting the BWT string
  2. + *
  3. Building a mapping from first column indices to last column indices
  4. + *
  5. Following this mapping starting from the original index to reconstruct the string
  6. + *
+ *

+ * + * @param bwtString the transformed string (L-column) from the forward transform; must not be {@code null} + * @param originalIndex the index of the original string row from the forward transform; + * use -1 for empty strings + * @return the original, untransformed string; returns empty string if input is empty or {@code originalIndex} is -1 + * @throws NullPointerException if {@code bwtString} is {@code null} + * @throws IllegalArgumentException if {@code originalIndex} is out of valid range (except -1) + */ + public static String inverseTransform(String bwtString, int originalIndex) { + if (bwtString == null || bwtString.isEmpty() || originalIndex == -1) { + return ""; + } + + int n = bwtString.length(); + if (originalIndex < 0 || originalIndex >= n) { + throw new IllegalArgumentException("Original index must be between 0 and " + (n - 1) + ", got: " + originalIndex); + } + + char[] lastColumn = bwtString.toCharArray(); + char[] firstColumn = bwtString.toCharArray(); + Arrays.sort(firstColumn); + + // Create the "next" array for LF-mapping. + // next[i] stores the row index in the last column that corresponds to firstColumn[i] + int[] next = new int[n]; + + // Track the count of each character seen so far in the last column + Map countMap = new HashMap<>(); + + // Store the first occurrence index of each character in the first column + Map firstOccurrence = new HashMap<>(); + + for (int i = 0; i < n; i++) { + if (!firstOccurrence.containsKey(firstColumn[i])) { + firstOccurrence.put(firstColumn[i], i); + } + } + + // Build the LF-mapping + for (int i = 0; i < n; i++) { + char c = lastColumn[i]; + int count = countMap.getOrDefault(c, 0); + int firstIndex = firstOccurrence.get(c); + next[firstIndex + count] = i; + countMap.put(c, count + 1); + } + + // Reconstruct the original string by following the LF-mapping + StringBuilder originalString = new StringBuilder(n); + int currentRow = originalIndex; + for (int i = 0; i < n; i++) { + originalString.append(firstColumn[currentRow]); + currentRow = next[currentRow]; + } + + return originalString.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/LZ77.java b/src/main/java/com/thealgorithms/compression/LZ77.java new file mode 100644 index 000000000000..d02307aa57b5 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/LZ77.java @@ -0,0 +1,168 @@ +package com.thealgorithms.compression; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of the Lempel-Ziv 77 (LZ77) compression algorithm. + *

+ * LZ77 is a lossless data compression algorithm that works by finding repeated + * occurrences of data in a sliding window. It replaces subsequent occurrences + * with references (offset, length) to the first occurrence within the window. + *

+ *

+ * This implementation uses a simple sliding window and lookahead buffer approach. + * Output format is a sequence of tuples (offset, length, next_character). + *

+ *

+ * Time Complexity: O(n*W) in this naive implementation, where n is the input length + * and W is the window size, due to the search for the longest match. More advanced + * data structures (like suffix trees) can improve this. + *

+ *

+ * References: + *

+ *

+ */ +public final class LZ77 { + + private static final int DEFAULT_WINDOW_SIZE = 4096; + private static final int DEFAULT_LOOKAHEAD_BUFFER_SIZE = 16; + private static final char END_OF_STREAM = '\u0000'; + private LZ77() { + } + + /** + * Represents a token in the LZ77 compressed output. + * Stores the offset back into the window, the length of the match, + * and the next character after the match (or END_OF_STREAM if at end). + */ + public record Token(int offset, int length, char nextChar) { + } + + /** + * Compresses the input text using the LZ77 algorithm. + * + * @param text The input string to compress. Must not be null. + * @param windowSize The size of the sliding window (search buffer). Must be positive. + * @param lookaheadBufferSize The size of the lookahead buffer. Must be positive. + * @return A list of {@link Token} objects representing the compressed data. + * @throws IllegalArgumentException if windowSize or lookaheadBufferSize are not positive. + */ + public static List compress(String text, int windowSize, int lookaheadBufferSize) { + if (text == null) { + return new ArrayList<>(); + } + if (windowSize <= 0 || lookaheadBufferSize <= 0) { + throw new IllegalArgumentException("Window size and lookahead buffer size must be positive."); + } + + List compressedOutput = new ArrayList<>(); + int currentPosition = 0; + + while (currentPosition < text.length()) { + int bestMatchDistance = 0; + int bestMatchLength = 0; + + // Define the start of the search window + int searchBufferStart = Math.max(0, currentPosition - windowSize); + // Define the end of the lookahead buffer (don't go past text length) + int lookaheadEnd = Math.min(currentPosition + lookaheadBufferSize, text.length()); + + // Search for the longest match in the window + for (int i = searchBufferStart; i < currentPosition; i++) { + int currentMatchLength = 0; + + // Check how far the match extends into the lookahead buffer + // This allows for overlapping matches (e.g., "aaa" can match with offset 1) + while (currentPosition + currentMatchLength < lookaheadEnd) { + int sourceIndex = i + currentMatchLength; + + // Handle overlapping matches (run-length encoding within LZ77) + // When we've matched beyond our starting position, wrap around using modulo + if (sourceIndex >= currentPosition) { + int offset = currentPosition - i; + sourceIndex = i + (currentMatchLength % offset); + } + + if (text.charAt(sourceIndex) == text.charAt(currentPosition + currentMatchLength)) { + currentMatchLength++; + } else { + break; + } + } + + // If this match is longer than the best found so far + if (currentMatchLength > bestMatchLength) { + bestMatchLength = currentMatchLength; + bestMatchDistance = currentPosition - i; // Calculate offset from current position + } + } + + char nextChar; + if (currentPosition + bestMatchLength < text.length()) { + nextChar = text.charAt(currentPosition + bestMatchLength); + } else { + nextChar = END_OF_STREAM; + } + + // Add the token to the output + compressedOutput.add(new Token(bestMatchDistance, bestMatchLength, nextChar)); + + // Move the current position forward + // If we're at the end and had a match, just move by the match length + if (nextChar == END_OF_STREAM) { + currentPosition += bestMatchLength; + } else { + currentPosition += bestMatchLength + 1; + } + } + + return compressedOutput; + } + + /** + * Compresses the input text using the LZ77 algorithm with default buffer sizes. + * + * @param text The input string to compress. Must not be null. + * @return A list of {@link Token} objects representing the compressed data. + */ + public static List compress(String text) { + return compress(text, DEFAULT_WINDOW_SIZE, DEFAULT_LOOKAHEAD_BUFFER_SIZE); + } + + /** + * Decompresses a list of LZ77 tokens back into the original string. + * + * @param compressedData The list of {@link Token} objects. Must not be null. + * @return The original, uncompressed string. + */ + public static String decompress(List compressedData) { + if (compressedData == null) { + return ""; + } + + StringBuilder decompressedText = new StringBuilder(); + + for (Token token : compressedData) { + // Copy matched characters from the sliding window + if (token.length > 0) { + int startIndex = decompressedText.length() - token.offset; + + // Handle overlapping matches (e.g., when length > offset) + for (int i = 0; i < token.length; i++) { + decompressedText.append(decompressedText.charAt(startIndex + i)); + } + } + + // Append the next character (if not END_OF_STREAM) + if (token.nextChar != END_OF_STREAM) { + decompressedText.append(token.nextChar); + } + } + + return decompressedText.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/LZ78.java b/src/main/java/com/thealgorithms/compression/LZ78.java new file mode 100644 index 000000000000..904c379cc2a2 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/LZ78.java @@ -0,0 +1,136 @@ +package com.thealgorithms.compression; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of the Lempel-Ziv 78 (LZ78) compression algorithm. + *

+ * LZ78 is a dictionary-based lossless data compression algorithm. It processes + * input data sequentially, building a dictionary of phrases encountered so far. + * It outputs pairs (dictionary_index, next_character), representing + * the longest match found in the dictionary plus the character that follows it. + *

+ *

+ * This implementation builds the dictionary dynamically during compression. + * The dictionary index 0 represents the empty string (no prefix). + *

+ *

+ * Time Complexity: O(n) on average for compression and decompression, assuming + * efficient dictionary lookups (using a HashMap), where n is the + * length of the input string. + *

+ *

+ * References: + *

+ *

+ */ +public final class LZ78 { + + /** + * Special character used to mark end of stream when needed. + */ + private static final char END_OF_STREAM = '\u0000'; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private LZ78() { + } + + /** + * Represents a token in the LZ78 compressed output. + * Stores the index of the matching prefix in the dictionary and the next character. + * Index 0 represents the empty string (no prefix). + */ + public record Token(int index, char nextChar) { + } + + /** + * A node in the dictionary trie structure. + * Each node represents a phrase and can have child nodes for extended phrases. + */ + private static final class TrieNode { + Map children = new HashMap<>(); + int index = -1; // -1 means not assigned yet + } + + /** + * Compresses the input text using the LZ78 algorithm. + * + * @param text The input string to compress. Must not be null. + * @return A list of {@link Token} objects representing the compressed data. + */ + public static List compress(String text) { + if (text == null || text.isEmpty()) { + return new ArrayList<>(); + } + + List compressedOutput = new ArrayList<>(); + TrieNode root = new TrieNode(); + int nextDictionaryIndex = 1; + + TrieNode currentNode = root; + int lastMatchedIndex = 0; + + for (int i = 0; i < text.length(); i++) { + char currentChar = text.charAt(i); + + if (currentNode.children.containsKey(currentChar)) { + currentNode = currentNode.children.get(currentChar); + lastMatchedIndex = currentNode.index; + } else { + // Output: (index of longest matching prefix, current character) + compressedOutput.add(new Token(lastMatchedIndex, currentChar)); + + TrieNode newNode = new TrieNode(); + newNode.index = nextDictionaryIndex++; + currentNode.children.put(currentChar, newNode); + + currentNode = root; + lastMatchedIndex = 0; + } + } + + // Handle remaining phrase at end of input + if (currentNode != root) { + compressedOutput.add(new Token(lastMatchedIndex, END_OF_STREAM)); + } + + return compressedOutput; + } + + /** + * Decompresses a list of LZ78 tokens back into the original string. + * + * @param compressedData The list of {@link Token} objects. Must not be null. + * @return The original, uncompressed string. + */ + public static String decompress(List compressedData) { + if (compressedData == null || compressedData.isEmpty()) { + return ""; + } + + StringBuilder decompressedText = new StringBuilder(); + Map dictionary = new HashMap<>(); + int nextDictionaryIndex = 1; + + for (Token token : compressedData) { + String prefix = (token.index == 0) ? "" : dictionary.get(token.index); + + if (token.nextChar == END_OF_STREAM) { + decompressedText.append(prefix); + } else { + String currentPhrase = prefix + token.nextChar; + decompressedText.append(currentPhrase); + dictionary.put(nextDictionaryIndex++, currentPhrase); + } + } + + return decompressedText.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/LZW.java b/src/main/java/com/thealgorithms/compression/LZW.java new file mode 100644 index 000000000000..c8383815ad4f --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/LZW.java @@ -0,0 +1,136 @@ +package com.thealgorithms.compression; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of the Lempel-Ziv-Welch (LZW) algorithm. + * + *

+ * LZW is a universal lossless data compression algorithm created by Abraham + * Lempel, Jacob Ziv, and Terry Welch. It works by building a dictionary of + * strings encountered during compression and replacing occurrences of those + * strings with a shorter code. + *

+ * + *

+ * This implementation handles standard ASCII characters and provides methods for + * both compression and decompression. + *

    + *
  • Compressing "TOBEORNOTTOBEORTOBEORNOT" results in a list of integer + * codes.
  • + *
  • Decompressing that list of codes results back in the original + * string.
  • + *
+ *

+ * + *

+ * Time Complexity: O(n) for both compression and decompression, where n is the + * length of the input string. + *

+ * + *

+ * References: + *

+ *

+ */ +public final class LZW { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private LZW() { + } + + /** + * Compresses a string using the LZW algorithm. + * + * @param uncompressed The string to be compressed. Can be null. + * @return A list of integers representing the compressed data. Returns an empty + * list if the input is null or empty. + */ + public static List compress(String uncompressed) { + if (uncompressed == null || uncompressed.isEmpty()) { + return new ArrayList<>(); + } + + // Initialize dictionary with single characters (ASCII 0-255) + int dictSize = 256; + Map dictionary = new HashMap<>(); + for (int i = 0; i < dictSize; i++) { + dictionary.put("" + (char) i, i); + } + + String w = ""; + List result = new ArrayList<>(); + for (char c : uncompressed.toCharArray()) { + String wc = w + c; + if (dictionary.containsKey(wc)) { + // If the new string is in the dictionary, extend the current string + w = wc; + } else { + // Otherwise, output the code for the current string + result.add(dictionary.get(w)); + // Add the new string to the dictionary + dictionary.put(wc, dictSize++); + // Start a new current string + w = "" + c; + } + } + + // Output the code for the last remaining string + result.add(dictionary.get(w)); + return result; + } + + /** + * Decompresses a list of integers back into a string using the LZW algorithm. + * + * @param compressed A list of integers representing the compressed data. Can be + * null. + * @return The original, uncompressed string. Returns an empty string if the + * input is null or empty. + */ + public static String decompress(List compressed) { + if (compressed == null || compressed.isEmpty()) { + return ""; + } + + // Initialize dictionary with single characters (ASCII 0-255) + int dictSize = 256; + Map dictionary = new HashMap<>(); + for (int i = 0; i < dictSize; i++) { + dictionary.put(i, "" + (char) i); + } + + // Decompress the first code + String w = "" + (char) (int) compressed.removeFirst(); + StringBuilder result = new StringBuilder(w); + + for (int k : compressed) { + String entry; + if (dictionary.containsKey(k)) { + // The code is in the dictionary + entry = dictionary.get(k); + } else if (k == dictSize) { + // Special case for sequences like "ababab" + entry = w + w.charAt(0); + } else { + throw new IllegalArgumentException("Bad compressed k: " + k); + } + + result.append(entry); + + // Add new sequence to the dictionary + dictionary.put(dictSize++, w + entry.charAt(0)); + + w = entry; + } + return result.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/MoveToFront.java b/src/main/java/com/thealgorithms/compression/MoveToFront.java new file mode 100644 index 000000000000..fa8976df8262 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/MoveToFront.java @@ -0,0 +1,164 @@ +package com.thealgorithms.compression; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Implementation of the Move-to-Front (MTF) transform and its inverse. + *

+ * MTF is a data transformation algorithm that encodes each symbol in the input + * as its current position in a dynamically-maintained list, then moves that symbol + * to the front of the list. This transformation is particularly effective when used + * after the Burrows-Wheeler Transform (BWT), as BWT groups similar characters together. + *

+ * + *

The transform converts runs of repeated characters into sequences of small integers + * (often zeros), which are highly compressible by subsequent entropy encoding algorithms + * like Run-Length Encoding (RLE) or Huffman coding. This technique is used in the + * bzip2 compression algorithm. + *

+ * + *

How it works: + *

    + *
  1. Maintain a list of symbols (the alphabet), initially in a fixed order
  2. + *
  3. For each input symbol: + *
      + *
    • Output its current index in the list
    • + *
    • Move that symbol to the front of the list
    • + *
    + *
  4. + *
+ * This means frequently occurring symbols quickly move to the front and are encoded + * with small indices (often 0), while rare symbols remain near the back. + *

+ * + *

Time Complexity: + *

    + *
  • Forward transform: O(n × m) where n is input length and m is alphabet size
  • + *
  • Inverse transform: O(n × m)
  • + *
+ * Note: Using {@link LinkedList} for O(1) insertions and O(m) search operations. + *

+ * + *

Example:

+ *
+ * Input:    "annb$aa"
+ * Alphabet: "$abn" (initial order)
+ * Output:   [1, 3, 0, 3, 3, 3, 0]
+ *
+ * Step-by-step:
+ * - 'a': index 1 in [$,a,b,n] → output 1, list becomes [a,$,b,n]
+ * - 'n': index 3 in [a,$,b,n] → output 3, list becomes [n,a,$,b]
+ * - 'n': index 0 in [n,a,$,b] → output 0, list stays [n,a,$,b]
+ * - 'b': index 3 in [n,a,$,b] → output 3, list becomes [b,n,a,$]
+ * - etc.
+ *
+ * Notice how repeated 'n' characters produce zeros after the first occurrence!
+ * 
+ * + * @see Move-to-front transform (Wikipedia) + */ +public final class MoveToFront { + + private MoveToFront() { + } + + /** + * Performs the forward Move-to-Front transform. + *

+ * Converts the input string into a list of integers, where each integer represents + * the position of the corresponding character in a dynamically-maintained alphabet list. + *

+ * + *

Note: All characters in the input text must exist in the provided alphabet, + * otherwise an {@link IllegalArgumentException} is thrown. The alphabet should contain + * all unique characters that may appear in the input.

+ * + * @param text the input string to transform; if empty, returns an empty list + * @param initialAlphabet a string containing the initial ordered set of symbols + * (e.g., "$abn" or the full ASCII set); must not be empty + * when {@code text} is non-empty + * @return a list of integers representing the transformed data, where each integer + * is the index of the corresponding input character in the current alphabet state + * @throws IllegalArgumentException if {@code text} is non-empty and {@code initialAlphabet} + * is {@code null} or empty + * @throws IllegalArgumentException if any character in {@code text} is not found in + * {@code initialAlphabet} + */ + public static List transform(String text, String initialAlphabet) { + if (text == null || text.isEmpty()) { + return new ArrayList<>(); + } + if (initialAlphabet == null || initialAlphabet.isEmpty()) { + throw new IllegalArgumentException("Alphabet cannot be null or empty when text is not empty."); + } + + List output = new ArrayList<>(text.length()); + + // Use LinkedList for O(1) add-to-front and O(n) remove operations + // This is more efficient than ArrayList for the move-to-front pattern + List alphabet = initialAlphabet.chars().mapToObj(c -> (char) c).collect(Collectors.toCollection(LinkedList::new)); + + for (char c : text.toCharArray()) { + int index = alphabet.indexOf(c); + if (index == -1) { + throw new IllegalArgumentException("Symbol '" + c + "' not found in the initial alphabet."); + } + + output.add(index); + + // Move the character to the front + Character symbol = alphabet.remove(index); + alphabet.addFirst(symbol); + } + return output; + } + + /** + * Performs the inverse Move-to-Front transform. + *

+ * Reconstructs the original string from the list of indices produced by the + * forward transform. This requires the exact same initial alphabet that was + * used in the forward transform. + *

+ * + *

Important: The {@code initialAlphabet} parameter must be identical + * to the one used in the forward transform, including character order, or the + * output will be incorrect.

+ * + * @param indices The list of integers from the forward transform. + * @param initialAlphabet the exact same initial alphabet string used for the forward transform; + * if {@code null} or empty, returns an empty string + * @return the original, untransformed string + * @throws IllegalArgumentException if any index in {@code indices} is negative or + * exceeds the current alphabet size + */ + public static String inverseTransform(Collection indices, String initialAlphabet) { + if (indices == null || indices.isEmpty() || initialAlphabet == null || initialAlphabet.isEmpty()) { + return ""; + } + + StringBuilder output = new StringBuilder(indices.size()); + + // Use LinkedList for O(1) add-to-front and O(n) remove operations + List alphabet = initialAlphabet.chars().mapToObj(c -> (char) c).collect(Collectors.toCollection(LinkedList::new)); + + for (int index : indices) { + if (index < 0 || index >= alphabet.size()) { + throw new IllegalArgumentException("Index " + index + " is out of bounds for the current alphabet of size " + alphabet.size() + "."); + } + + // Get the symbol at the index + char symbol = alphabet.get(index); + output.append(symbol); + + // Move the symbol to the front (mirroring the forward transform) + alphabet.remove(index); + alphabet.addFirst(symbol); + } + return output.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/RunLengthEncoding.java b/src/main/java/com/thealgorithms/compression/RunLengthEncoding.java new file mode 100644 index 000000000000..8d065f4648df --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/RunLengthEncoding.java @@ -0,0 +1,87 @@ +package com.thealgorithms.compression; + +/** + * An implementation of the Run-Length Encoding (RLE) algorithm. + * + *

Run-Length Encoding is a simple form of lossless data compression in which + * runs of data (sequences in which the same data value occurs in many + * consecutive data elements) are stored as a single data value and count, + * rather than as the original run. + * + *

This implementation provides methods for both compressing and decompressing + * a string. For example: + *

    + *
  • Compressing "AAAABBBCCDAA" results in "4A3B2C1D2A".
  • + *
  • Decompressing "4A3B2C1D2A" results in "AAAABBBCCDAA".
  • + *
+ * + *

Time Complexity: O(n) for both compression and decompression, where n is the + * length of the input string. + * + *

References: + *

+ */ +public final class RunLengthEncoding { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private RunLengthEncoding() { + } + + /** + * Compresses a string using the Run-Length Encoding algorithm. + * + * @param text The string to be compressed. Must not be null. + * @return The compressed string. Returns an empty string if the input is empty. + */ + public static String compress(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + + StringBuilder compressed = new StringBuilder(); + int count = 1; + + for (int i = 0; i < text.length(); i++) { + // Check if it's the last character or if the next character is different + if (i == text.length() - 1 || text.charAt(i) != text.charAt(i + 1)) { + compressed.append(count); + compressed.append(text.charAt(i)); + count = 1; // Reset count for the new character + } else { + count++; + } + } + return compressed.toString(); + } + + /** + * Decompresses a string that was compressed using the Run-Length Encoding algorithm. + * + * @param compressedText The compressed string. Must not be null. + * @return The original, uncompressed string. + */ + public static String decompress(String compressedText) { + if (compressedText == null || compressedText.isEmpty()) { + return ""; + } + + StringBuilder decompressed = new StringBuilder(); + int count = 0; + + for (char ch : compressedText.toCharArray()) { + if (Character.isDigit(ch)) { + // Build the number for runs of 10 or more (e.g., "12A") + count = count * 10 + ch - '0'; + } else { + // Append the character 'count' times + decompressed.append(String.valueOf(ch).repeat(Math.max(0, count))); + count = 0; // Reset count for the next sequence + } + } + return decompressed.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/compression/ShannonFano.java b/src/main/java/com/thealgorithms/compression/ShannonFano.java new file mode 100644 index 000000000000..aa5d7ad91b2f --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/ShannonFano.java @@ -0,0 +1,159 @@ +package com.thealgorithms.compression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * An implementation of the Shannon-Fano algorithm for generating prefix codes. + * + *

Shannon-Fano coding is an entropy encoding technique for lossless data + * compression. It assigns variable-length codes to symbols based on their + * frequencies of occurrence. It is a precursor to Huffman coding and works by + * recursively partitioning a sorted list of symbols into two sub-lists with + * nearly equal total frequencies. + * + *

The algorithm works as follows: + *

    + *
  1. Count the frequency of each symbol in the input data.
  2. + *
  3. Sort the symbols in descending order of their frequencies.
  4. + *
  5. Recursively divide the list of symbols into two parts with sums of + * frequencies as close as possible to each other.
  6. + *
  7. Assign a '0' bit to the codes in the first part and a '1' bit to the codes + * in the second part.
  8. + *
  9. Repeat the process for each part until a part contains only one symbol.
  10. + *
+ * + *

Time Complexity: O(n^2) in this implementation due to the partitioning logic, + * or O(n log n) if a more optimized partitioning strategy is used. + * Sorting takes O(n log n), where n is the number of unique symbols. + * + *

References: + *

+ */ +public final class ShannonFano { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private ShannonFano() { + } + + /** + * A private inner class to represent a symbol and its frequency. + * Implements Comparable to allow sorting based on frequency. + */ + private static class Symbol implements Comparable { + final char character; + final int frequency; + String code = ""; + + Symbol(char character, int frequency) { + this.character = character; + this.frequency = frequency; + } + + @Override + public int compareTo(Symbol other) { + return Integer.compare(other.frequency, this.frequency); // Sort descending + } + } + + /** + * Generates Shannon-Fano codes for the symbols in a given text. + * + * @param text The input string for which to generate codes. Must not be null. + * @return A map where keys are characters and values are their corresponding Shannon-Fano codes. + */ + public static Map generateCodes(String text) { + if (text == null || text.isEmpty()) { + return Collections.emptyMap(); + } + + Map frequencyMap = new HashMap<>(); + for (char c : text.toCharArray()) { + frequencyMap.put(c, frequencyMap.getOrDefault(c, 0) + 1); + } + + List symbols = new ArrayList<>(); + for (Map.Entry entry : frequencyMap.entrySet()) { + symbols.add(new Symbol(entry.getKey(), entry.getValue())); + } + + Collections.sort(symbols); + + // Special case: only one unique symbol + if (symbols.size() == 1) { + symbols.getFirst().code = "0"; + } else { + buildCodeTree(symbols, 0, symbols.size() - 1, ""); + } + + return symbols.stream().collect(Collectors.toMap(s -> s.character, s -> s.code)); + } + + /** + * Recursively builds the Shannon-Fano code tree by partitioning the list of symbols. + * Uses index-based approach to avoid sublist creation issues. + * + * @param symbols The sorted list of symbols to be processed. + * @param start The start index of the current partition. + * @param end The end index of the current partition (inclusive). + * @param prefix The current prefix code being built for the symbols in this partition. + */ + private static void buildCodeTree(List symbols, int start, int end, String prefix) { + // The initial check in generateCodes ensures start <= end is always true here. + // The base case is when a partition has only one symbol. + if (start == end) { + symbols.get(start).code = prefix; + return; + } + + // Find the optimal split point + int splitIndex = findSplitIndex(symbols, start, end); + + // Recursively process left and right partitions with updated prefixes + buildCodeTree(symbols, start, splitIndex, prefix + "0"); + buildCodeTree(symbols, splitIndex + 1, end, prefix + "1"); + } + + /** + * Finds the index that splits the range into two parts with the most balanced frequency sums. + * This method tries every possible split point and returns the index that minimizes the + * absolute difference between the two partition sums. + * + * @param symbols The sorted list of symbols. + * @param start The start index of the range. + * @param end The end index of the range (inclusive). + * @return The index of the last element in the first partition. + */ + private static int findSplitIndex(List symbols, int start, int end) { + // Calculate total frequency for the entire range + long totalFrequency = 0; + for (int i = start; i <= end; i++) { + totalFrequency += symbols.get(i).frequency; + } + + long leftSum = 0; + long minDifference = Long.MAX_VALUE; + int splitIndex = start; + + // Try every possible split point and find the one with minimum difference + for (int i = start; i < end; i++) { + leftSum += symbols.get(i).frequency; + long rightSum = totalFrequency - leftSum; + long difference = Math.abs(leftSum - rightSum); + + if (difference < minDifference) { + minDifference = difference; + splitIndex = i; + } + } + return splitIndex; + } +} diff --git a/src/main/java/com/thealgorithms/conversions/Base64.java b/src/main/java/com/thealgorithms/conversions/Base64.java new file mode 100644 index 000000000000..5219c4ba7f4e --- /dev/null +++ b/src/main/java/com/thealgorithms/conversions/Base64.java @@ -0,0 +1,196 @@ +package com.thealgorithms.conversions; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Base64 is a group of binary-to-text encoding schemes that represent binary data + * in an ASCII string format by translating it into a radix-64 representation. + * Each base64 digit represents exactly 6 bits of data. + * + * Base64 encoding is commonly used when there is a need to encode binary data + * that needs to be stored and transferred over media that are designed to deal + * with textual data. + * + * Wikipedia Reference: https://en.wikipedia.org/wiki/Base64 + * Author: Nithin U. + * Github: https://github.com/NithinU2802 + */ + +public final class Base64 { + + // Base64 character set + private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static final char PADDING_CHAR = '='; + + private Base64() { + } + + /** + * Encodes the given byte array to a Base64 encoded string. + * + * @param input the byte array to encode + * @return the Base64 encoded string + * @throws IllegalArgumentException if input is null + */ + public static String encode(byte[] input) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + + if (input.length == 0) { + return ""; + } + + StringBuilder result = new StringBuilder(); + int padding = 0; + + // Process input in groups of 3 bytes + for (int i = 0; i < input.length; i += 3) { + // Get up to 3 bytes + int byte1 = input[i] & 0xFF; + int byte2 = (i + 1 < input.length) ? (input[i + 1] & 0xFF) : 0; + int byte3 = (i + 2 < input.length) ? (input[i + 2] & 0xFF) : 0; + + // Calculate padding needed + if (i + 1 >= input.length) { + padding = 2; + } else if (i + 2 >= input.length) { + padding = 1; + } + + // Combine 3 bytes into a 24-bit number + int combined = (byte1 << 16) | (byte2 << 8) | byte3; + + // Extract four 6-bit groups + result.append(BASE64_CHARS.charAt((combined >> 18) & 0x3F)); + result.append(BASE64_CHARS.charAt((combined >> 12) & 0x3F)); + result.append(BASE64_CHARS.charAt((combined >> 6) & 0x3F)); + result.append(BASE64_CHARS.charAt(combined & 0x3F)); + } + + // Replace padding characters + if (padding > 0) { + result.setLength(result.length() - padding); + for (int i = 0; i < padding; i++) { + result.append(PADDING_CHAR); + } + } + + return result.toString(); + } + + /** + * Encodes the given string to a Base64 encoded string using UTF-8 encoding. + * + * @param input the string to encode + * @return the Base64 encoded string + * @throws IllegalArgumentException if input is null + */ + public static String encode(String input) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + + return encode(input.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Decodes the given Base64 encoded string to a byte array. + * + * @param input the Base64 encoded string to decode + * @return the decoded byte array + * @throws IllegalArgumentException if input is null or contains invalid Base64 characters + */ + public static byte[] decode(String input) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + + if (input.isEmpty()) { + return new byte[0]; + } + + // Strict RFC 4648 compliance: length must be a multiple of 4 + if (input.length() % 4 != 0) { + throw new IllegalArgumentException("Invalid Base64 input length; must be multiple of 4"); + } + + // 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)"); + } + + List result = new ArrayList<>(); + + // Process input in groups of 4 characters + for (int i = 0; i < input.length(); i += 4) { + // Get up to 4 characters + int char1 = getBase64Value(input.charAt(i)); + int char2 = getBase64Value(input.charAt(i + 1)); + int char3 = input.charAt(i + 2) == '=' ? 0 : getBase64Value(input.charAt(i + 2)); + int char4 = input.charAt(i + 3) == '=' ? 0 : getBase64Value(input.charAt(i + 3)); + + // Combine four 6-bit groups into a 24-bit number + int combined = (char1 << 18) | (char2 << 12) | (char3 << 6) | char4; + + // Extract three 8-bit bytes + result.add((byte) ((combined >> 16) & 0xFF)); + if (input.charAt(i + 2) != '=') { + result.add((byte) ((combined >> 8) & 0xFF)); + } + if (input.charAt(i + 3) != '=') { + result.add((byte) (combined & 0xFF)); + } + } + + // Convert List to byte[] + byte[] resultArray = new byte[result.size()]; + for (int i = 0; i < result.size(); i++) { + resultArray[i] = result.get(i); + } + + return resultArray; + } + + /** + * Decodes the given Base64 encoded string to a string using UTF-8 encoding. + * + * @param input the Base64 encoded string to decode + * @return the decoded string + * @throws IllegalArgumentException if input is null or contains invalid Base64 characters + */ + public static String decodeToString(String input) { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + + byte[] decodedBytes = decode(input); + return new String(decodedBytes, StandardCharsets.UTF_8); + } + + /** + * Gets the numeric value of a Base64 character. + * + * @param c the Base64 character + * @return the numeric value (0-63) + * @throws IllegalArgumentException if character is not a valid Base64 character + */ + private static int getBase64Value(char c) { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } else if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } else if (c >= '0' && c <= '9') { + return c - '0' + 52; + } else if (c == '+') { + return 62; + } else if (c == '/') { + return 63; + } else { + throw new IllegalArgumentException("Invalid Base64 character: " + c); + } + } +} diff --git a/src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java b/src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java index 36c0790e565f..b3783ab284b2 100644 --- a/src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java +++ b/src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java @@ -30,4 +30,30 @@ public static long binaryToDecimal(long binaryNumber) { } return decimalValue; } + + /** + * Converts a binary String to its decimal equivalent using bitwise operators. + * + * @param binary The binary number to convert. + * @return The decimal equivalent of the binary number. + * @throws IllegalArgumentException If the binary number contains digits other than 0 and 1. + */ + public static long binaryStringToDecimal(String binary) { + boolean isNegative = binary.charAt(0) == '-'; + if (isNegative) { + binary = binary.substring(1); + } + + long decimalValue = 0; + + for (char bit : binary.toCharArray()) { + if (bit != '0' && bit != '1') { + throw new IllegalArgumentException("Incorrect binary digit: " + bit); + } + // shift left by 1 (multiply by 2) and add bit value + decimalValue = (decimalValue << 1) | (bit - '0'); + } + + return isNegative ? -decimalValue : decimalValue; + } } diff --git a/src/main/java/com/thealgorithms/conversions/CoordinateConverter.java b/src/main/java/com/thealgorithms/conversions/CoordinateConverter.java new file mode 100644 index 000000000000..2766a3a1cf89 --- /dev/null +++ b/src/main/java/com/thealgorithms/conversions/CoordinateConverter.java @@ -0,0 +1,57 @@ +package com.thealgorithms.conversions; + +/** + * A utility class to convert between Cartesian and Polar coordinate systems. + * + *

This class provides methods to perform the following conversions: + *

    + *
  • Cartesian to Polar coordinates
  • + *
  • Polar to Cartesian coordinates
  • + *
+ * + *

The class is final and cannot be instantiated. + */ +public final class CoordinateConverter { + + private CoordinateConverter() { + // Prevent instantiation + } + + /** + * Converts Cartesian coordinates to Polar coordinates. + * + * @param x the x-coordinate in the Cartesian system; must be a finite number + * @param y the y-coordinate in the Cartesian system; must be a finite number + * @return an array where the first element is the radius (r) and the second element is the angle (theta) in degrees + * @throws IllegalArgumentException if x or y is not a finite number + */ + public static double[] cartesianToPolar(double x, double y) { + if (!Double.isFinite(x) || !Double.isFinite(y)) { + throw new IllegalArgumentException("x and y must be finite numbers."); + } + double r = Math.sqrt(x * x + y * y); + double theta = Math.toDegrees(Math.atan2(y, x)); + return new double[] {r, theta}; + } + + /** + * Converts Polar coordinates to Cartesian coordinates. + * + * @param r the radius in the Polar system; must be non-negative + * @param thetaDegrees the angle (theta) in degrees in the Polar system; must be a finite number + * @return an array where the first element is the x-coordinate and the second element is the y-coordinate in the Cartesian system + * @throws IllegalArgumentException if r is negative or thetaDegrees is not a finite number + */ + public static double[] polarToCartesian(double r, double thetaDegrees) { + if (r < 0) { + throw new IllegalArgumentException("Radius (r) must be non-negative."); + } + if (!Double.isFinite(thetaDegrees)) { + throw new IllegalArgumentException("Theta (angle) must be a finite number."); + } + double theta = Math.toRadians(thetaDegrees); + double x = r * Math.cos(theta); + double y = r * Math.sin(theta); + return new double[] {x, y}; + } +} 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/conversions/TimeConverter.java b/src/main/java/com/thealgorithms/conversions/TimeConverter.java new file mode 100644 index 000000000000..41cae37d7ad1 --- /dev/null +++ b/src/main/java/com/thealgorithms/conversions/TimeConverter.java @@ -0,0 +1,97 @@ +package com.thealgorithms.conversions; + +import java.util.Locale; +import java.util.Map; + +/** + * A utility class to convert between different units of time. + * + *

This class supports conversions between the following units: + *

    + *
  • seconds
  • + *
  • minutes
  • + *
  • hours
  • + *
  • days
  • + *
  • weeks
  • + *
  • months (approximated as 30.44 days)
  • + *
  • years (approximated as 365.25 days)
  • + *
+ * + *

The conversion is based on predefined constants in seconds. + * Results are rounded to three decimal places for consistency. + * + *

This class is final and cannot be instantiated. + * + * @see Wikipedia: Unit of time + */ +public final class TimeConverter { + + private TimeConverter() { + // Prevent instantiation + } + + /** + * Supported time units with their equivalent in seconds. + */ + private enum TimeUnit { + SECONDS(1.0), + MINUTES(60.0), + HOURS(3600.0), + DAYS(86400.0), + WEEKS(604800.0), + MONTHS(2629800.0), // 30.44 days + YEARS(31557600.0); // 365.25 days + + private final double seconds; + + TimeUnit(double seconds) { + this.seconds = seconds; + } + + public double toSeconds(double value) { + return value * seconds; + } + + public double fromSeconds(double secondsValue) { + return secondsValue / seconds; + } + } + + private static final Map UNIT_LOOKUP + = Map.ofEntries(Map.entry("seconds", TimeUnit.SECONDS), Map.entry("minutes", TimeUnit.MINUTES), Map.entry("hours", TimeUnit.HOURS), Map.entry("days", TimeUnit.DAYS), Map.entry("weeks", TimeUnit.WEEKS), Map.entry("months", TimeUnit.MONTHS), Map.entry("years", TimeUnit.YEARS)); + + /** + * Converts a time value from one unit to another. + * + * @param timeValue the numeric value of time to convert; must be non-negative + * @param unitFrom the unit of the input value (e.g., "minutes", "hours") + * @param unitTo the unit to convert into (e.g., "seconds", "days") + * @return the converted value in the target unit, rounded to three decimals + * @throws IllegalArgumentException if {@code timeValue} is negative + * @throws IllegalArgumentException if either {@code unitFrom} or {@code unitTo} is not supported + */ + public static double convertTime(double timeValue, String unitFrom, String unitTo) { + if (timeValue < 0) { + throw new IllegalArgumentException("timeValue must be a non-negative number."); + } + + TimeUnit from = resolveUnit(unitFrom); + TimeUnit to = resolveUnit(unitTo); + + double secondsValue = from.toSeconds(timeValue); + double converted = to.fromSeconds(secondsValue); + + return Math.round(converted * 1000.0) / 1000.0; + } + + private static TimeUnit resolveUnit(String unit) { + if (unit == null) { + throw new IllegalArgumentException("Unit cannot be null."); + } + TimeUnit resolved = UNIT_LOOKUP.get(unit.toLowerCase(Locale.ROOT)); + if (resolved == null) { + throw new IllegalArgumentException("Invalid unit '" + unit + "'. Supported units are: " + UNIT_LOOKUP.keySet()); + } + return resolved; + } +} diff --git a/src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java b/src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java index 30030de6c1bd..50726380621a 100644 --- a/src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java +++ b/src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java @@ -16,7 +16,7 @@ private TurkishToLatinConversion() { * 2. Replace all turkish characters with their corresponding latin characters * 3. Return the converted string * - * @param param String paramter + * @param param String parameter * @return String */ public static String convertTurkishToLatin(String param) { diff --git a/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java b/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java index d60b95110fc2..90625ad1c902 100644 --- a/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java +++ b/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java @@ -1,12 +1,15 @@ package com.thealgorithms.datastructures.bloomfilter; +import java.util.Arrays; import java.util.BitSet; /** * A generic BloomFilter implementation for probabilistic membership checking. *

- * Bloom filters are space-efficient data structures that provide a fast way to test whether an - * element is a member of a set. They may produce false positives, indicating an element is + * Bloom filters are space-efficient data structures that provide a fast way to + * test whether an + * element is a member of a set. They may produce false positives, indicating an + * element is * in the set when it is not, but they will never produce false negatives. *

* @@ -20,11 +23,14 @@ public class BloomFilter { private final Hash[] hashFunctions; /** - * Constructs a BloomFilter with a specified number of hash functions and bit array size. + * Constructs a BloomFilter with a specified number of hash functions and bit + * array size. * * @param numberOfHashFunctions the number of hash functions to use - * @param bitArraySize the size of the bit array, which determines the capacity of the filter - * @throws IllegalArgumentException if numberOfHashFunctions or bitArraySize is less than 1 + * @param bitArraySize the size of the bit array, which determines the + * capacity of the filter + * @throws IllegalArgumentException if numberOfHashFunctions or bitArraySize is + * less than 1 */ @SuppressWarnings("unchecked") public BloomFilter(int numberOfHashFunctions, int bitArraySize) { @@ -38,7 +44,8 @@ public BloomFilter(int numberOfHashFunctions, int bitArraySize) { } /** - * Initializes the hash functions with unique indices to ensure different hashing. + * Initializes the hash functions with unique indices to ensure different + * hashing. */ private void initializeHashFunctions() { for (int i = 0; i < numberOfHashFunctions; i++) { @@ -49,7 +56,8 @@ private void initializeHashFunctions() { /** * Inserts an element into the Bloom filter. *

- * This method hashes the element using all defined hash functions and sets the corresponding + * This method hashes the element using all defined hash functions and sets the + * corresponding * bits in the bit array. *

* @@ -65,13 +73,16 @@ public void insert(T key) { /** * Checks if an element might be in the Bloom filter. *

- * This method checks the bits at the positions computed by each hash function. If any of these - * bits are not set, the element is definitely not in the filter. If all bits are set, the element + * This method checks the bits at the positions computed by each hash function. + * If any of these + * bits are not set, the element is definitely not in the filter. If all bits + * are set, the element * might be in the filter. *

* * @param key the element to check for membership in the Bloom filter - * @return {@code true} if the element might be in the Bloom filter, {@code false} if it is definitely not + * @return {@code true} if the element might be in the Bloom filter, + * {@code false} if it is definitely not */ public boolean contains(T key) { for (Hash hash : hashFunctions) { @@ -86,7 +97,8 @@ public boolean contains(T key) { /** * Inner class representing a hash function used by the Bloom filter. *

- * Each instance of this class represents a different hash function based on its index. + * Each instance of this class represents a different hash function based on its + * index. *

* * @param The type of elements to be hashed. @@ -115,7 +127,7 @@ private static class Hash { * @return the computed hash value */ public int compute(T key) { - return index * asciiString(String.valueOf(key)); + return index * contentHash(key); } /** @@ -135,5 +147,31 @@ private int asciiString(String word) { } return sum; } + + /** + * Computes a content-based hash for arrays; falls back to ASCII-sum of String value otherwise. + */ + private int contentHash(Object key) { + if (key instanceof int[]) { + return Arrays.hashCode((int[]) key); + } else if (key instanceof long[]) { + return Arrays.hashCode((long[]) key); + } else if (key instanceof byte[]) { + return Arrays.hashCode((byte[]) key); + } else if (key instanceof short[]) { + return Arrays.hashCode((short[]) key); + } else if (key instanceof char[]) { + return Arrays.hashCode((char[]) key); + } else if (key instanceof boolean[]) { + return Arrays.hashCode((boolean[]) key); + } else if (key instanceof float[]) { + return Arrays.hashCode((float[]) key); + } else if (key instanceof double[]) { + return Arrays.hashCode((double[]) key); + } else if (key instanceof Object[]) { + return Arrays.deepHashCode((Object[]) key); + } + return asciiString(String.valueOf(key)); + } } } diff --git a/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java b/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java index f0d8ea8f7ff3..bf2b928ec33c 100644 --- a/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java +++ b/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java @@ -132,7 +132,7 @@ public void put(K key, V value) { private void addNodeWithUpdatedFrequency(Node node) { if (tail != null && head != null) { Node temp = this.head; - while (temp != null) { + while (true) { if (temp.frequency > node.frequency) { if (temp == head) { node.next = temp; diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/BellmanFord.java b/src/main/java/com/thealgorithms/datastructures/graphs/BellmanFord.java index 47c5f0d0b98e..5184dae58d28 100644 --- a/src/main/java/com/thealgorithms/datastructures/graphs/BellmanFord.java +++ b/src/main/java/com/thealgorithms/datastructures/graphs/BellmanFord.java @@ -160,7 +160,7 @@ public void show(int source, int end, break; } } - if (neg == 0) { // Go ahead and show results of computaion + if (neg == 0) { // Go ahead and show results of computation System.out.println("Distance is: " + dist[end]); System.out.println("Path followed:"); System.out.print(source + " "); diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java new file mode 100644 index 000000000000..da3fba88c618 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java @@ -0,0 +1,114 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * An implementation of Dial's Algorithm for the single-source shortest path problem. + * This algorithm is an optimization of Dijkstra's algorithm and is particularly + * efficient for graphs with small, non-negative integer edge weights. + * + * It uses a bucket queue (implemented here as a List of HashSets) to store vertices, + * where each bucket corresponds to a specific distance from the source. This is more + * efficient than a standard priority queue when the range of edge weights is small. + * + * Time Complexity: O(E + W * V), where E is the number of edges, V is the number + * of vertices, and W is the maximum weight of any edge. + * + * @see Wikipedia - Dial's Algorithm + */ +public final class DialsAlgorithm { + /** + * Private constructor to prevent instantiation of this utility class. + */ + private DialsAlgorithm() { + } + /** + * Represents an edge in the graph, connecting to a destination vertex with a given weight. + */ + public static class Edge { + private final int destination; + private final int weight; + + public Edge(int destination, int weight) { + this.destination = destination; + this.weight = weight; + } + + public int getDestination() { + return destination; + } + + public int getWeight() { + return weight; + } + } + /** + * Finds the shortest paths from a source vertex to all other vertices in a weighted graph. + * + * @param graph The graph represented as an adjacency list. + * @param source The source vertex to start from (0-indexed). + * @param maxEdgeWeight The maximum weight of any single edge in the graph. + * @return An array of integers where the value at each index `i` is the + * shortest distance from the source to vertex `i`. Unreachable vertices + * will have a value of Integer.MAX_VALUE. + * @throws IllegalArgumentException if the source vertex is out of bounds. + */ + public static int[] run(List> graph, int source, int maxEdgeWeight) { + int numVertices = graph.size(); + if (source < 0 || source >= numVertices) { + throw new IllegalArgumentException("Source vertex is out of bounds."); + } + + // Initialize distances array + int[] distances = new int[numVertices]; + Arrays.fill(distances, Integer.MAX_VALUE); + distances[source] = 0; + + // The bucket queue. Size is determined by the max possible path length. + int maxPathWeight = maxEdgeWeight * (numVertices > 0 ? numVertices - 1 : 0); + List> buckets = new ArrayList<>(maxPathWeight + 1); + for (int i = 0; i <= maxPathWeight; i++) { + buckets.add(new HashSet<>()); + } + + // Add the source vertex to the first bucket + buckets.get(0).add(source); + + // Process buckets in increasing order of distance + for (int d = 0; d <= maxPathWeight; d++) { + // Process all vertices in the current bucket + while (!buckets.get(d).isEmpty()) { + // Get and remove a vertex from the current bucket + int u = buckets.get(d).iterator().next(); + buckets.get(d).remove(u); + + // If we've found a shorter path already, skip + if (d > distances[u]) { + continue; + } + + // Relax all adjacent edges + for (Edge edge : graph.get(u)) { + int v = edge.getDestination(); + int weight = edge.getWeight(); + + // If a shorter path to v is found + if (distances[u] != Integer.MAX_VALUE && distances[u] + weight < distances[v]) { + // If v was already in a bucket, remove it from the old one + if (distances[v] != Integer.MAX_VALUE) { + buckets.get(distances[v]).remove(v); + } + // Update distance and move v to the new bucket + distances[v] = distances[u] + weight; + buckets.get(distances[v]).add(v); + } + } + } + } + return distances; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java b/src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java index c1d47df457da..a54b0b75e4dc 100644 --- a/src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java +++ b/src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java @@ -141,7 +141,7 @@ private int[][] adjacency() { * * @param from the parent vertex to check for adjacency * @param to the child vertex to check for adjacency - * @return whether or not the vertices are adjancent + * @return whether or not the vertices are adjacent */ private boolean adjacencyOfEdgeDoesExist(int from, int to) { return (this.adjacency()[from][to] != AdjacencyMatrixGraph.EDGE_NONE); @@ -162,7 +162,7 @@ public boolean vertexDoesExist(int aVertex) { * * @param from the parent vertex to check for adjacency * @param to the child vertex to check for adjacency - * @return whether or not the vertices are adjancent + * @return whether or not the vertices are adjacent */ public boolean edgeDoesExist(int from, int to) { if (this.vertexDoesExist(from) && this.vertexDoesExist(to)) { diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/TwoSat.java b/src/main/java/com/thealgorithms/datastructures/graphs/TwoSat.java new file mode 100644 index 000000000000..835e3880428f --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/TwoSat.java @@ -0,0 +1,265 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Stack; + +/** + * This class implements a solution to the 2-SAT (2-Satisfiability) problem + * using Kosaraju's algorithm to find strongly connected components (SCCs) + * in the implication graph. + * + *

+ * Brief Idea: + *

+ * + *
+ * 1. From each clause (a ∨ b), we can derive implications:
+ *      (¬a → b) and (¬b → a)
+ *
+ * 2. We construct an implication graph using these implications.
+ *
+ * 3. For each variable x, its negation ¬x is also represented as a node.
+ *    If x and ¬x belong to the same SCC, the expression is unsatisfiable.
+ *
+ * 4. Otherwise, we assign truth values based on the SCC order:
+ *    If SCC(x) > SCC(¬x), then x = true; otherwise, x = false.
+ * 
+ * + *

+ * Complexities: + *

+ *
    + *
  • Time Complexity: O(n + m)
  • + *
  • Space Complexity: O(n + m)
  • + *
+ * where {@code n} is the number of variables and {@code m} is the number of + * clauses. + * + *

+ * Usage Example: + *

+ * + *
+ * TwoSat twoSat = new TwoSat(5); // Initialize with 5 variables: x1, x2, x3, x4, x5
+ *
+ * // Add clauses
+ * twoSat.addClause(1, false, 2, false); // (x1 ∨ x2)
+ * twoSat.addClause(3, true, 2, false); // (¬x3 ∨ x2)
+ * twoSat.addClause(4, false, 5, true); // (x4 ∨ ¬x5)
+ *
+ * twoSat.solve(); // Solve the problem
+ *
+ * if (twoSat.isSolutionExists()) {
+ *     boolean[] solution = twoSat.getSolutions();
+ *     for (int i = 1; i <= 5; i++) {
+ *         System.out.println("x" + i + " = " + solution[i]);
+ *     }
+ * }
+ * 
+ *

Reference

+ * CP Algorithm

+ * Wikipedia - 2 SAT + * @author Shoyeb Ansari + * + * @see Kosaraju + */ +class TwoSat { + + /** Number of variables in the boolean expression. */ + private final int numberOfVariables; + + /** Implication graph built from the boolean clauses. */ + private final ArrayList[] graph; + + /** Transposed implication graph used in Kosaraju's algorithm. */ + private final ArrayList[] graphTranspose; + + /** Stores one valid truth assignment for all variables (1-indexed). */ + private final boolean[] variableAssignments; + + /** Indicates whether a valid solution exists. */ + private boolean hasSolution = true; + + /** Tracks whether the {@code solve()} method has been called. */ + private boolean isSolved = false; + + /** + * Initializes the TwoSat solver with the given number of variables. + * + * @param numberOfVariables the number of boolean variables + * @throws IllegalArgumentException if the number of variables is negative + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + TwoSat(int numberOfVariables) { + if (numberOfVariables < 0) { + throw new IllegalArgumentException("Number of variables cannot be negative."); + } + this.numberOfVariables = numberOfVariables; + int n = 2 * numberOfVariables + 1; + + graph = (ArrayList[]) new ArrayList[n]; + graphTranspose = (ArrayList[]) new ArrayList[n]; + for (int i = 0; i < n; i++) { + graph[i] = new ArrayList<>(); + graphTranspose[i] = new ArrayList<>(); + } + variableAssignments = new boolean[numberOfVariables + 1]; + } + + /** + * Adds a clause of the form (a ∨ b) to the boolean expression. + * + *

+ * Example: To add (¬x₁ ∨ x₂), call: + *

+ * + *
{@code
+     * addClause(1, true, 2, false);
+     * }
+ * + * @param a the first variable (1 ≤ a ≤ numberOfVariables) + * @param isNegateA {@code true} if variable {@code a} is negated + * @param b the second variable (1 ≤ b ≤ numberOfVariables) + * @param isNegateB {@code true} if variable {@code b} is negated + * @throws IllegalArgumentException if {@code a} or {@code b} are out of range + */ + void addClause(int a, boolean isNegateA, int b, boolean isNegateB) { + if (a <= 0 || a > numberOfVariables) { + throw new IllegalArgumentException("Variable number must be between 1 and " + numberOfVariables); + } + if (b <= 0 || b > numberOfVariables) { + throw new IllegalArgumentException("Variable number must be between 1 and " + numberOfVariables); + } + + a = isNegateA ? negate(a) : a; + b = isNegateB ? negate(b) : b; + int notA = negate(a); + int notB = negate(b); + + // Add implications: (¬a → b) and (¬b → a) + graph[notA].add(b); + graph[notB].add(a); + + // Build transpose graph + graphTranspose[b].add(notA); + graphTranspose[a].add(notB); + } + + /** + * Solves the 2-SAT problem using Kosaraju's algorithm to find SCCs + * and determines whether a satisfying assignment exists. + */ + void solve() { + isSolved = true; + int n = 2 * numberOfVariables + 1; + + boolean[] visited = new boolean[n]; + int[] component = new int[n]; + Stack topologicalOrder = new Stack<>(); + + // Step 1: Perform DFS to get topological order + for (int i = 1; i < n; i++) { + if (!visited[i]) { + dfsForTopologicalOrder(i, visited, topologicalOrder); + } + } + + Arrays.fill(visited, false); + int sccId = 0; + + // Step 2: Find SCCs on transposed graph + while (!topologicalOrder.isEmpty()) { + int node = topologicalOrder.pop(); + if (!visited[node]) { + dfsForScc(node, visited, component, sccId); + sccId++; + } + } + + // Step 3: Check for contradictions and assign values + for (int i = 1; i <= numberOfVariables; i++) { + int notI = negate(i); + if (component[i] == component[notI]) { + hasSolution = false; + return; + } + // If SCC(i) > SCC(¬i), then variable i is true. + variableAssignments[i] = component[i] > component[notI]; + } + } + + /** + * Returns whether the given boolean formula is satisfiable. + * + * @return {@code true} if a solution exists; {@code false} otherwise + * @throws Error if called before {@link #solve()} + */ + boolean isSolutionExists() { + if (!isSolved) { + throw new Error("Please call solve() before checking for a solution."); + } + return hasSolution; + } + + /** + * Returns one valid assignment of variables that satisfies the boolean formula. + * + * @return a boolean array where {@code result[i]} represents the truth value of + * variable {@code xᵢ} + * @throws Error if called before {@link #solve()} or if no solution exists + */ + boolean[] getSolutions() { + if (!isSolved) { + throw new Error("Please call solve() before fetching the solution."); + } + if (!hasSolution) { + throw new Error("No satisfying assignment exists for the given expression."); + } + return variableAssignments.clone(); + } + + /** Performs DFS to compute topological order. */ + private void dfsForTopologicalOrder(int u, boolean[] visited, Stack topologicalOrder) { + visited[u] = true; + for (int v : graph[u]) { + if (!visited[v]) { + dfsForTopologicalOrder(v, visited, topologicalOrder); + } + } + topologicalOrder.push(u); + } + + /** Performs DFS on the transposed graph to identify SCCs. */ + private void dfsForScc(int u, boolean[] visited, int[] component, int sccId) { + visited[u] = true; + component[u] = sccId; + for (int v : graphTranspose[u]) { + if (!visited[v]) { + dfsForScc(v, visited, component, sccId); + } + } + } + + /** + * Returns the index representing the negation of the given variable. + * + *

+ * Mapping rule: + *

+ * + *
+     * For a variable i:
+     *     negate(i) = i + n
+     * For a negated variable (i + n):
+     *     negate(i + n) = i
+     * where n = numberOfVariables
+     * 
+ * + * @param a the variable index + * @return the index representing its negation + */ + private int negate(int a) { + return a <= numberOfVariables ? a + numberOfVariables : a - numberOfVariables; + } +} 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/FibonacciHeap.java b/src/main/java/com/thealgorithms/datastructures/heaps/FibonacciHeap.java index 7a263fc08ac5..834de9c77881 100644 --- a/src/main/java/com/thealgorithms/datastructures/heaps/FibonacciHeap.java +++ b/src/main/java/com/thealgorithms/datastructures/heaps/FibonacciHeap.java @@ -387,7 +387,7 @@ public class HeapNode { private HeapNode parent; /* - * a constructor for a heapNode withe key @param (key) + * a constructor for a heapNode with key @param (key) * prev == next == this * parent == child == null */ 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/CircularDoublyLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java new file mode 100644 index 000000000000..fbb48854c449 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java @@ -0,0 +1,118 @@ +package com.thealgorithms.datastructures.lists; + +/** + * This class is a circular doubly linked list implementation. In a circular + * doubly linked list, + * the last node points back to the first node and the first node points back to + * the last node, + * creating a circular chain in both directions. + * + * This implementation includes basic operations such as appending elements to + * the end, + * removing elements from a specified position, and converting the list to a + * string representation. + * + * @param the type of elements held in this list + */ +public class CircularDoublyLinkedList { + static final class Node { + Node next; + Node prev; + E value; + + private Node(E value, Node next, Node prev) { + this.value = value; + this.next = next; + this.prev = prev; + } + } + + private int size; + Node head = null; + + /** + * Initializes a new circular doubly linked list. A dummy head node is used for + * simplicity, + * pointing initially to itself to ensure the list is never empty. + */ + public CircularDoublyLinkedList() { + head = new Node<>(null, null, null); + head.next = head; + head.prev = head; + size = 0; + } + + /** + * Returns the current size of the list. + * + * @return the number of elements in the list + */ + public int getSize() { + return size; + } + + /** + * Appends a new element to the end of the list. Throws a NullPointerException + * if + * a null value is provided. + * + * @param value the value to append to the list + * @throws NullPointerException if the value is null + */ + public void append(E value) { + if (value == null) { + throw new NullPointerException("Cannot add null element to the list"); + } + Node newNode = new Node<>(value, head, head.prev); + head.prev.next = newNode; + head.prev = newNode; + size++; + } + + /** + * Returns a string representation of the list in the format "[ element1, + * element2, ... ]". + * An empty list is represented as "[]". + * + * @return the string representation of the list + */ + public String toString() { + if (size == 0) { + return "[]"; + } + StringBuilder sb = new StringBuilder("[ "); + Node current = head.next; + while (current != head) { + sb.append(current.value); + if (current.next != head) { + sb.append(", "); + } + current = current.next; + } + sb.append(" ]"); + return sb.toString(); + } + + /** + * Removes and returns the element at the specified position in the list. + * Throws an IndexOutOfBoundsException if the position is invalid. + * + * @param pos the position of the element to remove + * @return the value of the removed element - pop operation + * @throws IndexOutOfBoundsException if the position is out of range + */ + public E remove(int pos) { + if (pos >= size || pos < 0) { + throw new IndexOutOfBoundsException("Position out of bounds"); + } + Node current = head.next; + for (int i = 0; i < pos; i++) { + current = current.next; + } + current.prev.next = current.next; + current.next.prev = current.prev; + E removedValue = current.value; + size--; + return removedValue; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedList.java new file mode 100644 index 000000000000..3c4106f178b9 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedList.java @@ -0,0 +1,92 @@ +package com.thealgorithms.datastructures.lists; +/** + * Implements an algorithm to flatten a multilevel linked list. + * + * In this specific problem structure, each node has a `next` pointer (to the + * next node at the same level) and a `child` pointer (which points to the head + * of another sorted linked list). The goal is to merge all these lists into a + * single, vertically sorted linked list using the `child` pointer. + * + * The approach is a recursive one that leverages a merge utility, similar to + * the merge step in Merge Sort. It recursively flattens the list starting from + * the rightmost node and merges each node's child list with the already + * flattened list to its right. + * @see GeeksforGeeks: Flattening a Linked List + */ +public final class FlattenMultilevelLinkedList { + /** + * Private constructor to prevent instantiation of this utility class. + */ + private FlattenMultilevelLinkedList() { + } + /** + * Node represents an element in the multilevel linked list. It contains the + * integer data, a reference to the next node at the same level, and a + * reference to the head of a child list. + */ + static class Node { + int data; + Node next; + Node child; + + Node(int data) { + this.data = data; + this.next = null; + this.child = null; + } + } + + /** + * Merges two sorted linked lists (connected via the `child` pointer). + * This is a helper function for the main flatten algorithm. + * + * @param a The head of the first sorted list. + * @param b The head of the second sorted list. + * @return The head of the merged sorted list. + */ + private static Node merge(Node a, Node b) { + // If one of the lists is empty, return the other. + if (a == null) { + return b; + } + if (b == null) { + return a; + } + + Node result; + + // Choose the smaller value as the new head. + if (a.data < b.data) { + result = a; + result.child = merge(a.child, b); + } else { + result = b; + result.child = merge(a, b.child); + } + result.next = null; // Ensure the merged list has no `next` pointers. + return result; + } + + /** + * Flattens a multilevel linked list into a single sorted list. + * The flattened list is connected using the `child` pointers. + * + * @param head The head of the top-level list (connected via `next` pointers). + * @return The head of the fully flattened and sorted list. + */ + public static Node flatten(Node head) { + // Base case: if the list is empty or has only one node, it's already flattened. + if (head == null || head.next == null) { + return head; + } + + // Recursively flatten the list starting from the next node. + head.next = flatten(head.next); + + // Now, merge the current list (head's child list) with the flattened rest of the list. + head = merge(head, head.next); + + // Return the head of the fully merged list. + return head; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/lists/README.md b/src/main/java/com/thealgorithms/datastructures/lists/README.md index 6aefa4c98e6d..5a0a43b923e1 100644 --- a/src/main/java/com/thealgorithms/datastructures/lists/README.md +++ b/src/main/java/com/thealgorithms/datastructures/lists/README.md @@ -28,5 +28,6 @@ The `next` variable points to the next node in the data structure and value stor 4. `CreateAndDetectLoop.java` : Create and detect a loop in a linked list. 5. `DoublyLinkedList.java` : A modification of singly linked list which has a `prev` pointer to point to the previous node. 6. `MergeKSortedLinkedlist.java` : Merges K sorted linked list with mergesort (mergesort is also the most efficient sorting algorithm for linked list). -7. `RandomNode.java` : Selects a random node from given linked list and diplays it. +7. `RandomNode.java` : Selects a random node from given linked list and displays it. 8. `SkipList.java` : Data Structure used for storing a sorted list of elements with help of a Linked list hierarchy that connects to subsequences of elements. +9. `TortoiseHareAlgo.java` : Finds the middle element of a linked list using the fast and slow pointer (Tortoise-Hare) algorithm. diff --git a/src/main/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgo.java b/src/main/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgo.java new file mode 100644 index 000000000000..9d803003c658 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgo.java @@ -0,0 +1,63 @@ +package com.thealgorithms.datastructures.lists; + +public class TortoiseHareAlgo { + static final class Node { + Node next; + E value; + + private Node(E value, Node next) { + this.value = value; + this.next = next; + } + } + + private Node head = null; + + public TortoiseHareAlgo() { + head = null; + } + + public void append(E value) { + Node newNode = new Node<>(value, null); + if (head == null) { + head = newNode; + return; + } + Node current = head; + while (current.next != null) { + current = current.next; + } + current.next = newNode; + } + + public E getMiddle() { + if (head == null) { + return null; + } + + Node slow = head; + Node fast = head; + + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + return slow.value; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + Node current = head; + while (current != null) { + sb.append(current.value); + if (current.next != null) { + sb.append(", "); + } + current = current.next; + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java b/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java index a5ca48670f2c..b877a5843b98 100644 --- a/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java +++ b/src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java @@ -9,7 +9,7 @@ * give numbers that are bigger, a higher priority. Queues in theory have no * fixed size but when using an array implementation it does. *

- * Additional contibutions made by: PuneetTri(https://github.com/PuneetTri) + * Additional contributions made by: PuneetTri(https://github.com/PuneetTri) */ class PriorityQueue { @@ -32,8 +32,8 @@ class PriorityQueue { PriorityQueue() { /* If capacity is not defined, default size of 11 would be used - * capacity=max+1 because we cant access 0th element of PQ, and to - * accomodate (max)th elements we need capacity to be max+1. + * capacity=max+1 because we can't access 0th element of PQ, and to + * accommodate (max)th elements we need capacity to be max+1. * Parent is at position k, child at position (k*2,k*2+1), if we * use position 0 in our queue, its child would be at: * (0*2, 0*2+1) -> (0,0). This is why we start at position 1 @@ -127,7 +127,7 @@ public int remove() { if (isEmpty()) { throw new RuntimeException("Queue is Empty"); } else { - int max = queueArray[1]; // By defintion of our max-heap, value at queueArray[1] pos is + int max = queueArray[1]; // By definition of our max-heap, value at queueArray[1] pos is // the greatest // Swap max and last element diff --git a/src/main/java/com/thealgorithms/datastructures/trees/AVLSimple.java b/src/main/java/com/thealgorithms/datastructures/trees/AVLSimple.java index e0309122cc12..07fc5c87b6c4 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/AVLSimple.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/AVLSimple.java @@ -1,7 +1,7 @@ package com.thealgorithms.datastructures.trees; /* -* Avl is algo that balance itself while adding new alues to tree +* Avl is algo that balance itself while adding new values to tree * by rotating branches of binary tree and make itself Binary seaarch tree * there are four cases which has to tackle * rotating - left right ,left left,right right,right left diff --git a/src/main/java/com/thealgorithms/datastructures/trees/BSTRecursiveGeneric.java b/src/main/java/com/thealgorithms/datastructures/trees/BSTRecursiveGeneric.java index 5c1334ffa0e8..2c94224ddeb4 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/BSTRecursiveGeneric.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/BSTRecursiveGeneric.java @@ -13,6 +13,7 @@ *

* * @author [Madhur Panwar](git-Madhur Panwar) + * @author [Udaya Krishnan M](git-Udaya Krishnan M) {added prettyDisplay() method} */ public class BSTRecursiveGeneric> { @@ -28,6 +29,29 @@ public BSTRecursiveGeneric() { root = null; } + /** + * Displays the tree is a structured format + */ + public void prettyDisplay() { + prettyDisplay(root, 0); + } + + private void prettyDisplay(Node node, int level) { + if (node == null) { + return; + } + prettyDisplay(node.right, level + 1); + if (level != 0) { + for (int i = 0; i < level - 1; i++) { + System.out.print("|\t"); + } + System.out.println("|---->" + node.data); + } else { + System.out.println(node.data); + } + prettyDisplay(node.left, level + 1); + } + /** * main function for testing */ @@ -38,7 +62,12 @@ public static void main(String[] args) { integerTree.add(5); integerTree.add(10); - integerTree.add(9); + integerTree.add(-9); + integerTree.add(4); + integerTree.add(3); + integerTree.add(1); + System.out.println("Pretty Display of current tree is:"); + integerTree.prettyDisplay(); assert !integerTree.find(4) : "4 is not yet present in BST"; assert integerTree.find(10) @@ -54,16 +83,21 @@ public static void main(String[] args) { assert integerTree.find(70) : "70 was inserted but not found"; /* - Will print in following order - 5 10 20 70 + Will print in following order + 5 10 20 70 */ + System.out.println("Pretty Display of current tree is:"); + integerTree.prettyDisplay(); integerTree.inorder(); + System.out.println("Pretty Display of current tree is:"); + integerTree.prettyDisplay(); System.out.println(); System.out.println("Testing for string data..."); // String BSTRecursiveGeneric stringTree = new BSTRecursiveGeneric(); stringTree.add("banana"); + stringTree.add("apple"); stringTree.add("pineapple"); stringTree.add("date"); assert !stringTree.find("girl") @@ -80,11 +114,15 @@ public static void main(String[] args) { stringTree.add("hills"); assert stringTree.find("hills") : "hills was inserted but not found"; + System.out.println("Pretty Display of current tree is:"); + stringTree.prettyDisplay(); /* Will print in following order banana hills india pineapple */ stringTree.inorder(); + System.out.println("Pretty Display of current tree is:"); + stringTree.prettyDisplay(); } /** 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/CeilInBinarySearchTree.java b/src/main/java/com/thealgorithms/datastructures/trees/CeilInBinarySearchTree.java index 214e111b9f1a..fff84111663b 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/CeilInBinarySearchTree.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/CeilInBinarySearchTree.java @@ -21,7 +21,7 @@ * * Solution 1: Brute Force Solution: Do an inorder traversal and save result * into an array. Iterate over the array to get an element equal to or greater - * than current key. Time Complexity: O(n) Space Complexity: O(n) for auxillary + * than current key. Time Complexity: O(n) Space Complexity: O(n) for auxiliary * array to save inorder representation of tree. *

*

@@ -29,7 +29,7 @@ * into an array.Since array is sorted do a binary search over the array to get * an element equal to or greater than current key. Time Complexity: O(n) for * traversal of tree and O(lg(n)) for binary search in array. Total = O(n) Space - * Complexity: O(n) for auxillary array to save inorder representation of tree. + * Complexity: O(n) for auxiliary array to save inorder representation of tree. *

*

* Solution 3: Optimal We can do a DFS search on given tree in following 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/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/devutils/nodes/LargeTreeNode.java b/src/main/java/com/thealgorithms/devutils/nodes/LargeTreeNode.java index 95d53ecb1f7a..d69288f72200 100644 --- a/src/main/java/com/thealgorithms/devutils/nodes/LargeTreeNode.java +++ b/src/main/java/com/thealgorithms/devutils/nodes/LargeTreeNode.java @@ -3,7 +3,7 @@ import java.util.Collection; /** - * {@link TreeNode} extension that holds a {@link Collection} of refrences to + * {@link TreeNode} extension that holds a {@link Collection} of references to * child Nodes. * * @param The type of the data held in the Node. @@ -18,7 +18,7 @@ public class LargeTreeNode extends TreeNode { private Collection> childNodes; /** - * Empty contructor. + * Empty constructor. */ public LargeTreeNode() { super(); diff --git a/src/main/java/com/thealgorithms/devutils/nodes/SimpleNode.java b/src/main/java/com/thealgorithms/devutils/nodes/SimpleNode.java index 769ffc2a9a96..bf3e795dd74b 100644 --- a/src/main/java/com/thealgorithms/devutils/nodes/SimpleNode.java +++ b/src/main/java/com/thealgorithms/devutils/nodes/SimpleNode.java @@ -15,7 +15,7 @@ public class SimpleNode extends Node { private SimpleNode nextNode; /** - * Empty contructor. + * Empty constructor. */ public SimpleNode() { super(); diff --git a/src/main/java/com/thealgorithms/devutils/nodes/SimpleTreeNode.java b/src/main/java/com/thealgorithms/devutils/nodes/SimpleTreeNode.java index 215f01a6ef59..eefffacee8d6 100644 --- a/src/main/java/com/thealgorithms/devutils/nodes/SimpleTreeNode.java +++ b/src/main/java/com/thealgorithms/devutils/nodes/SimpleTreeNode.java @@ -11,16 +11,16 @@ public class SimpleTreeNode extends TreeNode { /** - * Refrence to the child Node on the left. + * Reference to the child Node on the left. */ private SimpleTreeNode leftNode; /** - * Refrence to the child Node on the right. + * Reference to the child Node on the right. */ private SimpleTreeNode rightNode; /** - * Empty contructor. + * Empty constructor. */ public SimpleTreeNode() { super(); diff --git a/src/main/java/com/thealgorithms/devutils/nodes/TreeNode.java b/src/main/java/com/thealgorithms/devutils/nodes/TreeNode.java index 13c9212306c1..1639bec6e5a0 100644 --- a/src/main/java/com/thealgorithms/devutils/nodes/TreeNode.java +++ b/src/main/java/com/thealgorithms/devutils/nodes/TreeNode.java @@ -12,7 +12,7 @@ public abstract class TreeNode extends Node { /** - * Refernce to the parent Node. + * Reference to the parent Node. */ private TreeNode parentNode; /** @@ -21,7 +21,7 @@ public abstract class TreeNode extends Node { private int depth; /** - * Empty contructor. + * Empty constructor. */ public TreeNode() { super(); diff --git a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java index cd26f9213651..4c9c40c83174 100644 --- a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java +++ b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java @@ -182,7 +182,7 @@ public double closestPair(final Location[] a, final int indexNum) { double minLeftArea; // Minimum length of left array double minRightArea; // Minimum length of right array - double minValue; // Minimum lengt + double minValue; // Minimum length minLeftArea = closestPair(leftArray, divideX); // recursive closestPair minRightArea = closestPair(rightArray, indexNum - divideX); diff --git a/src/main/java/com/thealgorithms/divideandconquer/SkylineAlgorithm.java b/src/main/java/com/thealgorithms/divideandconquer/SkylineAlgorithm.java index 610b1b78a36a..0e8d9442138c 100644 --- a/src/main/java/com/thealgorithms/divideandconquer/SkylineAlgorithm.java +++ b/src/main/java/com/thealgorithms/divideandconquer/SkylineAlgorithm.java @@ -161,7 +161,7 @@ public int getY() { * function dominates the argument point. * * @param p1 the point that is compared - * @return true if the point wich calls the function dominates p1 false + * @return true if the point which calls the function dominates p1 false * otherwise. */ public boolean dominates(Point p1) { diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/BoundaryFill.java b/src/main/java/com/thealgorithms/dynamicprogramming/BoundaryFill.java index 8494492f293f..ccd54ee4349a 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/BoundaryFill.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/BoundaryFill.java @@ -12,8 +12,8 @@ private BoundaryFill() { * Get the color at the given co-odrinates of a 2D image * * @param image The image to be filled - * @param xCoordinate The x co-ordinate of which color is to be obtained - * @param yCoordinate The y co-ordinate of which color is to be obtained + * @param xCoordinate The x coordinate of which color is to be obtained + * @param yCoordinate The y coordinate of which color is to be obtained */ public static int getPixel(int[][] image, int xCoordinate, int yCoordinate) { return image[xCoordinate][yCoordinate]; @@ -23,8 +23,8 @@ public static int getPixel(int[][] image, int xCoordinate, int yCoordinate) { * Put the color at the given co-odrinates of a 2D image * * @param image The image to be filed - * @param xCoordinate The x co-ordinate at which color is to be filled - * @param yCoordinate The y co-ordinate at which color is to be filled + * @param xCoordinate The x coordinate at which color is to be filled + * @param yCoordinate The y coordinate at which color is to be filled */ public static void putPixel(int[][] image, int xCoordinate, int yCoordinate, int newColor) { image[xCoordinate][yCoordinate] = newColor; @@ -34,8 +34,8 @@ public static void putPixel(int[][] image, int xCoordinate, int yCoordinate, int * Fill the 2D image with new color * * @param image The image to be filed - * @param xCoordinate The x co-ordinate at which color is to be filled - * @param yCoordinate The y co-ordinate at which color is to be filled + * @param xCoordinate The x coordinate at which color is to be filled + * @param yCoordinate The y coordinate at which color is to be filled * @param newColor The new color which to be filled in the image * @param boundaryColor The old color which is to be replaced in the image */ diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistance.java b/src/main/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistance.java new file mode 100644 index 000000000000..9721d4ab0ad5 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistance.java @@ -0,0 +1,185 @@ +package com.thealgorithms.dynamicprogramming; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the full Damerau–Levenshtein distance algorithm. + * + * This algorithm calculates the minimum number of operations required + * to transform one string into another. Supported operations are: + * insertion, deletion, substitution, and transposition of adjacent characters. + * + * Unlike the restricted version (OSA), this implementation allows multiple + * edits on the same substring, computing the true edit distance. + * + * Time Complexity: O(n * m * max(n, m)) + * Space Complexity: O(n * m) + */ +public final class DamerauLevenshteinDistance { + + private DamerauLevenshteinDistance() { + // Utility class + } + + /** + * Computes the full Damerau–Levenshtein distance between two strings. + * + * @param s1 the first string + * @param s2 the second string + * @return the minimum edit distance between the two strings + * @throws IllegalArgumentException if either input string is null + */ + public static int distance(String s1, String s2) { + validateInputs(s1, s2); + + int n = s1.length(); + int m = s2.length(); + + Map charLastPosition = buildCharacterMap(s1, s2); + int[][] dp = initializeTable(n, m); + + fillTable(s1, s2, dp, charLastPosition); + + return dp[n + 1][m + 1]; + } + + /** + * Validates that both input strings are not null. + * + * @param s1 the first string to validate + * @param s2 the second string to validate + * @throws IllegalArgumentException if either string is null + */ + private static void validateInputs(String s1, String s2) { + if (s1 == null || s2 == null) { + throw new IllegalArgumentException("Input strings must not be null."); + } + } + + /** + * Builds a character map containing all unique characters from both strings. + * Each character is initialized with a position value of 0. + * + * This map is used to track the last occurrence position of each character + * during the distance computation, which is essential for handling transpositions. + * + * @param s1 the first string + * @param s2 the second string + * @return a map containing all unique characters from both strings, initialized to 0 + */ + private static Map buildCharacterMap(String s1, String s2) { + Map charMap = new HashMap<>(); + for (char c : s1.toCharArray()) { + charMap.putIfAbsent(c, 0); + } + for (char c : s2.toCharArray()) { + charMap.putIfAbsent(c, 0); + } + return charMap; + } + + /** + * Initializes the dynamic programming table for the algorithm. + * + * The table has dimensions (n+2) x (m+2) where n and m are the lengths + * of the input strings. The extra rows and columns are used to handle + * the transposition operation correctly. + * + * The first row and column are initialized with the maximum possible distance, + * while the second row and column represent the base case of transforming + * from an empty string. + * + * @param n the length of the first string + * @param m the length of the second string + * @return an initialized DP table ready for computation + */ + private static int[][] initializeTable(int n, int m) { + int maxDist = n + m; + int[][] dp = new int[n + 2][m + 2]; + + dp[0][0] = maxDist; + + for (int i = 0; i <= n; i++) { + dp[i + 1][0] = maxDist; + dp[i + 1][1] = i; + } + + for (int j = 0; j <= m; j++) { + dp[0][j + 1] = maxDist; + dp[1][j + 1] = j; + } + + return dp; + } + + /** + * Fills the dynamic programming table by computing the minimum edit distance + * for each substring pair. + * + * This method implements the core algorithm logic, iterating through both strings + * and computing the minimum cost of transforming substrings. It considers all + * four operations: insertion, deletion, substitution, and transposition. + * + * The character position map is updated as we progress through the first string + * to enable efficient transposition cost calculation. + * + * @param s1 the first string + * @param s2 the second string + * @param dp the dynamic programming table to fill + * @param charLastPosition map tracking the last position of each character in s1 + */ + private static void fillTable(String s1, String s2, int[][] dp, Map charLastPosition) { + int n = s1.length(); + int m = s2.length(); + + for (int i = 1; i <= n; i++) { + int lastMatchCol = 0; + + for (int j = 1; j <= m; j++) { + char char1 = s1.charAt(i - 1); + char char2 = s2.charAt(j - 1); + + int lastMatchRow = charLastPosition.get(char2); + int cost = (char1 == char2) ? 0 : 1; + + if (char1 == char2) { + lastMatchCol = j; + } + + dp[i + 1][j + 1] = computeMinimumCost(dp, i, j, lastMatchRow, lastMatchCol, cost); + } + + charLastPosition.put(s1.charAt(i - 1), i); + } + } + + /** + * Computes the minimum cost among all possible operations at the current position. + * + * This method evaluates four possible operations: + * 1. Substitution: replace character at position i with character at position j + * 2. Insertion: insert character from s2 at position j + * 3. Deletion: delete character from s1 at position i + * 4. Transposition: swap characters that have been seen before + * + * The transposition cost accounts for the gap between the current position + * and the last position where matching characters were found. + * + * @param dp the dynamic programming table + * @param i the current position in the first string (1-indexed in the DP table) + * @param j the current position in the second string (1-indexed in the DP table) + * @param lastMatchRow the row index where the current character of s2 last appeared in s1 + * @param lastMatchCol the column index where the current character of s1 last matched in s2 + * @param cost the substitution cost (0 if characters match, 1 otherwise) + * @return the minimum cost among all operations + */ + private static int computeMinimumCost(int[][] dp, int i, int j, int lastMatchRow, int lastMatchCol, int cost) { + int substitution = dp[i][j] + cost; + int insertion = dp[i + 1][j] + 1; + int deletion = dp[i][j + 1] + 1; + int transposition = dp[lastMatchRow][lastMatchCol] + i - lastMatchRow - 1 + 1 + j - lastMatchCol - 1; + + return Math.min(Math.min(substitution, insertion), Math.min(deletion, transposition)); + } +} 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/MaximumProductSubarray.java b/src/main/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarray.java new file mode 100644 index 000000000000..1c16f612e98f --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarray.java @@ -0,0 +1,58 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * The MaximumProductSubarray class implements the algorithm to find the + * maximum product of a contiguous subarray within a given array of integers. + * + *

Given an array of integers (which may contain positive numbers, negative + * numbers, and zeros), this algorithm finds the contiguous subarray that has + * the largest product. The algorithm handles negative numbers efficiently by + * tracking both maximum and minimum products, since a negative number can turn + * a minimum product into a maximum product.

+ * + *

This implementation uses a dynamic programming approach that runs in O(n) + * time complexity and O(1) space complexity, making it highly efficient for + * large arrays.

+ */ +public final class MaximumProductSubarray { + + private MaximumProductSubarray() { + // Prevent instantiation + } + + /** + * Finds the maximum product of any contiguous subarray in the given array. + * + * @param nums an array of integers which may contain positive, negative, + * and zero values. + * @return the maximum product of a contiguous subarray. Returns 0 if the + * array is null or empty. + */ + public static int maxProduct(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + + long maxProduct = nums[0]; + long currentMax = nums[0]; + long currentMin = nums[0]; + + for (int i = 1; i < nums.length; i++) { + // Swap currentMax and currentMin if current number is negative + if (nums[i] < 0) { + long temp = currentMax; + currentMax = currentMin; + currentMin = temp; + } + + // Update currentMax and currentMin + currentMax = Math.max(nums[i], currentMax * nums[i]); + currentMin = Math.min(nums[i], currentMin * nums[i]); + + // Update global max product + maxProduct = Math.max(maxProduct, currentMax); + } + + return (int) maxProduct; + } +} diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java b/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java new file mode 100644 index 000000000000..13b640cf0d04 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/NeedlemanWunsch.java @@ -0,0 +1,60 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * The Needleman–Wunsch algorithm performs global sequence alignment between two strings. + * It computes the optimal alignment score using dynamic programming, + * given a scoring scheme for matches, mismatches, and gaps. + * + * Time Complexity: O(n * m) + * Space Complexity: O(n * m) + */ +public final class NeedlemanWunsch { + + private NeedlemanWunsch() { + // Utility Class + } + + /** + * Computes the Needleman–Wunsch global alignment score between two strings. + * + * @param s1 the first string + * @param s2 the second string + * @param matchScore score for a character match + * @param mismatchPenalty penalty for a mismatch (should be negative) + * @param gapPenalty penalty for inserting a gap (should be negative) + * @return the optimal alignment score + */ + public static int align(String s1, String s2, int matchScore, int mismatchPenalty, int gapPenalty) { + if (s1 == null || s2 == null) { + throw new IllegalArgumentException("Input strings must not be null."); + } + + int n = s1.length(); + int m = s2.length(); + + int[][] dp = new int[n + 1][m + 1]; + + // Initialize gap penalties for first row and column + for (int i = 0; i <= n; i++) { + dp[i][0] = i * gapPenalty; + } + for (int j = 0; j <= m; j++) { + dp[0][j] = j * gapPenalty; + } + + // Fill the DP matrix + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + int matchOrMismatch = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? matchScore : mismatchPenalty; + + dp[i][j] = Math.max(Math.max(dp[i - 1][j - 1] + matchOrMismatch, // match/mismatch + dp[i - 1][j] + gapPenalty // deletion (gap in s2) + ), + dp[i][j - 1] + gapPenalty // insertion (gap in s1) + ); + } + } + + return dp[n][m]; + } +} diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java index 49c4a0a3a008..8c72fa347f50 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java @@ -1,3 +1,5 @@ +package com.thealgorithms.dynamicprogramming; +import java.util.Arrays; /** * @author Md Asif Joardar * @@ -13,11 +15,6 @@ * * The time complexity of the solution is O(n × sum) and requires O(n × sum) space */ - -package com.thealgorithms.dynamicprogramming; - -import java.util.Arrays; - public final class PartitionProblem { private PartitionProblem() { } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/SmithWaterman.java b/src/main/java/com/thealgorithms/dynamicprogramming/SmithWaterman.java new file mode 100644 index 000000000000..c0d66f68b502 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/SmithWaterman.java @@ -0,0 +1,56 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * Smith–Waterman algorithm for local sequence alignment. + * Finds the highest scoring local alignment between substrings of two sequences. + * + * Time Complexity: O(n * m) + * Space Complexity: O(n * m) + */ +public final class SmithWaterman { + + private SmithWaterman() { + // Utility Class + } + + /** + * Computes the Smith–Waterman local alignment score between two strings. + * + * @param s1 first string + * @param s2 second string + * @param matchScore score for a match + * @param mismatchPenalty penalty for mismatch (negative) + * @param gapPenalty penalty for insertion/deletion (negative) + * @return the maximum local alignment score + */ + public static int align(String s1, String s2, int matchScore, int mismatchPenalty, int gapPenalty) { + if (s1 == null || s2 == null) { + throw new IllegalArgumentException("Input strings must not be null."); + } + + int n = s1.length(); + int m = s2.length(); + int maxScore = 0; + + int[][] dp = new int[n + 1][m + 1]; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + int matchOrMismatch = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? matchScore : mismatchPenalty; + + dp[i][j] = Math.max(0, + Math.max(Math.max(dp[i - 1][j - 1] + matchOrMismatch, // match/mismatch + dp[i - 1][j] + gapPenalty // deletion + ), + dp[i][j - 1] + gapPenalty // insertion + )); + + if (dp[i][j] > maxScore) { + maxScore = dp[i][j]; + } + } + } + + return maxScore; + } +} diff --git a/src/main/java/com/thealgorithms/geometry/BentleyOttmann.java b/src/main/java/com/thealgorithms/geometry/BentleyOttmann.java new file mode 100644 index 000000000000..a11acb87dd7a --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/BentleyOttmann.java @@ -0,0 +1,423 @@ +package com.thealgorithms.geometry; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Implementation of the Bentley–Ottmann algorithm for finding all intersection + * points among a set of line segments in O((n + k) log n) time. + * + *

Uses a sweep-line approach with an event queue and status structure to + * efficiently detect intersections in 2D plane geometry.

+ * + * @see + * Bentley–Ottmann algorithm + */ +public final class BentleyOttmann { + + private BentleyOttmann() { + } + + private static final double EPS = 1e-9; + private static double currentSweepX; + + /** + * Represents a line segment with two endpoints. + */ + public static class Segment { + final Point2D.Double p1; + final Point2D.Double p2; + final int id; // Unique identifier for each segment + + Segment(Point2D.Double p1, Point2D.Double p2) { + this.p1 = p1; + this.p2 = p2; + this.id = segmentCounter++; + } + + private static int segmentCounter = 0; + + /** + * Computes the y-coordinate of this segment at a given x value. + */ + double getY(double x) { + if (Math.abs(p2.x - p1.x) < EPS) { + // Vertical segment: return midpoint y + return (p1.y + p2.y) / 2.0; + } + double t = (x - p1.x) / (p2.x - p1.x); + return p1.y + t * (p2.y - p1.y); + } + + Point2D.Double leftPoint() { + return p1.x < p2.x ? p1 : p1.x > p2.x ? p2 : p1.y < p2.y ? p1 : p2; + } + + Point2D.Double rightPoint() { + return p1.x > p2.x ? p1 : p1.x < p2.x ? p2 : p1.y > p2.y ? p1 : p2; + } + + @Override + public String toString() { + return String.format("S%d[(%.2f, %.2f), (%.2f, %.2f)]", id, p1.x, p1.y, p2.x, p2.y); + } + } + + /** + * Event types for the sweep line algorithm. + */ + private enum EventType { START, END, INTERSECTION } + + /** + * Represents an event in the event queue. + */ + private static class Event implements Comparable { + final Point2D.Double point; + final EventType type; + final Set segments; // Segments involved in this event + + Event(Point2D.Double point, EventType type) { + this.point = point; + this.type = type; + this.segments = new HashSet<>(); + } + + void addSegment(Segment s) { + segments.add(s); + } + + @Override + public int compareTo(Event other) { + // Sort by x-coordinate, then by y-coordinate + int cmp = Double.compare(this.point.x, other.point.x); + if (cmp == 0) { + cmp = Double.compare(this.point.y, other.point.y); + } + if (cmp == 0) { + // Process END events before START events at same point + cmp = this.type.compareTo(other.type); + } + return cmp; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Event e)) { + return false; + } + return pointsEqual(this.point, e.point); + } + + @Override + public int hashCode() { + return Objects.hash(Math.round(point.x * 1e6), Math.round(point.y * 1e6)); + } + } + + /** + * Comparator for segments in the status structure (sweep line). + * Orders segments by their y-coordinate at the current sweep line position. + */ + private static final class StatusComparator implements Comparator { + @Override + public int compare(Segment s1, Segment s2) { + if (s1.id == s2.id) { + return 0; + } + + double y1 = s1.getY(currentSweepX); + double y2 = s2.getY(currentSweepX); + + int cmp = Double.compare(y1, y2); + if (Math.abs(y1 - y2) < EPS) { + // If y-coordinates are equal, use segment id for consistency + return Integer.compare(s1.id, s2.id); + } + return cmp; + } + } + + /** + * Finds all intersection points among a set of line segments. + * + *

An intersection point is reported when two or more segments cross or touch. + * For overlapping segments, only actual crossing/touching points are reported, + * not all points along the overlap.

+ * + * @param segments list of line segments represented as pairs of points + * @return a set of intersection points where segments meet or cross + * @throws IllegalArgumentException if the list is null or contains null points + */ + public static Set findIntersections(List segments) { + if (segments == null) { + throw new IllegalArgumentException("Segment list must not be null"); + } + + Segment.segmentCounter = 0; // Reset counter + Set intersections = new HashSet<>(); + PriorityQueue eventQueue = new PriorityQueue<>(); + TreeSet status = new TreeSet<>(new StatusComparator()); + Map eventMap = new HashMap<>(); + + // Initialize event queue with segment start and end points + for (Segment s : segments) { + Point2D.Double left = s.leftPoint(); + Point2D.Double right = s.rightPoint(); + + Event startEvent = getOrCreateEvent(eventMap, left, EventType.START); + startEvent.addSegment(s); + + Event endEvent = getOrCreateEvent(eventMap, right, EventType.END); + endEvent.addSegment(s); + } + + // Add all unique events to the queue + for (Event e : eventMap.values()) { + if (!e.segments.isEmpty()) { + eventQueue.add(e); + } + } + + // Process events + while (!eventQueue.isEmpty()) { + Event event = eventQueue.poll(); + currentSweepX = event.point.x; + + handleEvent(event, status, eventQueue, eventMap, intersections); + } + + return intersections; + } + + private static Event getOrCreateEvent(Map eventMap, Point2D.Double point, EventType type) { + // Find existing event at this point + for (Map.Entry entry : eventMap.entrySet()) { + if (pointsEqual(entry.getKey(), point)) { + return entry.getValue(); + } + } + // Create new event + Event event = new Event(point, type); + eventMap.put(point, event); + return event; + } + + private static void handleEvent(Event event, TreeSet status, PriorityQueue eventQueue, Map eventMap, Set intersections) { + Point2D.Double p = event.point; + Set segmentsAtPoint = new HashSet<>(event.segments); + + // Check segments in status structure (much smaller than allSegments) + for (Segment s : status) { + if (pointsEqual(s.p1, p) || pointsEqual(s.p2, p) || (onSegment(s, p) && !pointsEqual(s.p1, p) && !pointsEqual(s.p2, p))) { + segmentsAtPoint.add(s); + } + } + + // If 2 or more segments meet at this point, it's an intersection + if (segmentsAtPoint.size() >= 2) { + intersections.add(p); + } + + // Categorize segments + Set upperSegs = new HashSet<>(); // Segments starting at p + Set lowerSegs = new HashSet<>(); // Segments ending at p + Set containingSegs = new HashSet<>(); // Segments containing p in interior + + for (Segment s : segmentsAtPoint) { + if (pointsEqual(s.leftPoint(), p)) { + upperSegs.add(s); + } else if (pointsEqual(s.rightPoint(), p)) { + lowerSegs.add(s); + } else { + containingSegs.add(s); + } + } + + // Remove ending segments and segments containing p from status + status.removeAll(lowerSegs); + status.removeAll(containingSegs); + + // Update sweep line position slightly past the event + currentSweepX = p.x + EPS; + + // Add starting segments and re-add containing segments + status.addAll(upperSegs); + status.addAll(containingSegs); + + if (upperSegs.isEmpty() && containingSegs.isEmpty()) { + // Find neighbors and check for new intersections + Segment sl = getNeighbor(status, lowerSegs, true); + Segment sr = getNeighbor(status, lowerSegs, false); + if (sl != null && sr != null) { + findNewEvent(sl, sr, p, eventQueue, eventMap); + } + } else { + Set unionSegs = new HashSet<>(upperSegs); + unionSegs.addAll(containingSegs); + + Segment leftmost = getLeftmost(unionSegs, status); + Segment rightmost = getRightmost(unionSegs, status); + + if (leftmost != null) { + Segment sl = status.lower(leftmost); + if (sl != null) { + findNewEvent(sl, leftmost, p, eventQueue, eventMap); + } + } + + if (rightmost != null) { + Segment sr = status.higher(rightmost); + if (sr != null) { + findNewEvent(rightmost, sr, p, eventQueue, eventMap); + } + } + } + } + + private static Segment getNeighbor(NavigableSet status, Set removed, boolean lower) { + if (removed.isEmpty()) { + return null; + } + Segment ref = removed.iterator().next(); + return lower ? status.lower(ref) : status.higher(ref); + } + + private static Segment getLeftmost(Set segments, SortedSet status) { + Segment leftmost = null; + for (Segment s : segments) { + if (leftmost == null || Objects.requireNonNull(status.comparator()).compare(s, leftmost) < 0) { + leftmost = s; + } + } + return leftmost; + } + + private static Segment getRightmost(Set segments, SortedSet status) { + Segment rightmost = null; + for (Segment s : segments) { + if (status.comparator() != null && (rightmost == null || status.comparator().compare(s, rightmost) > 0)) { + rightmost = s; + } + } + return rightmost; + } + + private static void findNewEvent(Segment s1, Segment s2, Point2D.Double currentPoint, PriorityQueue eventQueue, Map eventMap) { + Point2D.Double intersection = getIntersection(s1, s2); + + if (intersection != null && intersection.x > currentPoint.x - EPS && !pointsEqual(intersection, currentPoint)) { + + // Check if event already exists + boolean exists = false; + for (Map.Entry entry : eventMap.entrySet()) { + if (pointsEqual(entry.getKey(), intersection)) { + exists = true; + Event existingEvent = entry.getValue(); + existingEvent.addSegment(s1); + existingEvent.addSegment(s2); + break; + } + } + + if (!exists) { + Event newEvent = new Event(intersection, EventType.INTERSECTION); + newEvent.addSegment(s1); + newEvent.addSegment(s2); + eventMap.put(intersection, newEvent); + eventQueue.add(newEvent); + } + } + } + + private static Point2D.Double getIntersection(Segment s1, Segment s2) { + double x1 = s1.p1.x; + double y1 = s1.p1.y; + double x2 = s1.p2.x; + double y2 = s1.p2.y; + double x3 = s2.p1.x; + double y3 = s2.p1.y; + double x4 = s2.p2.x; + double y4 = s2.p2.y; + + double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + + if (Math.abs(denom) < EPS) { + // Parallel or collinear + if (areCollinear(s1, s2)) { + // For collinear segments, check if they overlap + // Return any overlapping point + List overlapPoints = new ArrayList<>(); + + if (onSegment(s1, s2.p1)) { + overlapPoints.add(s2.p1); + } + if (onSegment(s1, s2.p2)) { + overlapPoints.add(s2.p2); + } + if (onSegment(s2, s1.p1)) { + overlapPoints.add(s1.p1); + } + if (onSegment(s2, s1.p2)) { + overlapPoints.add(s1.p2); + } + + // Remove duplicates and return the first point + if (!overlapPoints.isEmpty()) { + // Find the point that's not an endpoint of both segments + for (Point2D.Double pt : overlapPoints) { + boolean isS1Endpoint = pointsEqual(pt, s1.p1) || pointsEqual(pt, s1.p2); + boolean isS2Endpoint = pointsEqual(pt, s2.p1) || pointsEqual(pt, s2.p2); + + // If it's an endpoint of both, it's a touching point + if (isS1Endpoint && isS2Endpoint) { + return pt; + } + } + // Return the first overlap point + return overlapPoints.getFirst(); + } + } + return null; + } + + double t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; + double u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; + + if (t >= -EPS && t <= 1 + EPS && u >= -EPS && u <= 1 + EPS) { + double px = x1 + t * (x2 - x1); + double py = y1 + t * (y2 - y1); + return new Point2D.Double(px, py); + } + + return null; + } + + private static boolean areCollinear(Segment s1, Segment s2) { + double cross1 = crossProduct(s1.p1, s1.p2, s2.p1); + double cross2 = crossProduct(s1.p1, s1.p2, s2.p2); + return Math.abs(cross1) < EPS && Math.abs(cross2) < EPS; + } + + private static double crossProduct(Point2D.Double a, Point2D.Double b, Point2D.Double c) { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); + } + + private static boolean onSegment(Segment s, Point2D.Double p) { + return p.x >= Math.min(s.p1.x, s.p2.x) - EPS && p.x <= Math.max(s.p1.x, s.p2.x) + EPS && p.y >= Math.min(s.p1.y, s.p2.y) - EPS && p.y <= Math.max(s.p1.y, s.p2.y) + EPS && Math.abs(crossProduct(s.p1, s.p2, p)) < EPS; + } + + private static boolean pointsEqual(Point2D.Double p1, Point2D.Double p2) { + return Math.abs(p1.x - p2.x) < EPS && Math.abs(p1.y - p2.y) < EPS; + } +} diff --git a/src/main/java/com/thealgorithms/geometry/ConvexHull.java b/src/main/java/com/thealgorithms/geometry/ConvexHull.java index 17f400854c64..d44d20c68807 100644 --- a/src/main/java/com/thealgorithms/geometry/ConvexHull.java +++ b/src/main/java/com/thealgorithms/geometry/ConvexHull.java @@ -61,11 +61,24 @@ public static List convexHullBruteForce(List points) { return new ArrayList<>(convexSet); } + /** + * Computes the convex hull using a recursive divide-and-conquer approach. + * Returns points in counter-clockwise order starting from the bottom-most, left-most point. + * + * @param points the input points + * @return the convex hull points in counter-clockwise order + */ public static List convexHullRecursive(List points) { + if (points.size() < 3) { + List result = new ArrayList<>(points); + Collections.sort(result); + return result; + } + Collections.sort(points); Set convexSet = new HashSet<>(); - Point leftMostPoint = points.get(0); - Point rightMostPoint = points.get(points.size() - 1); + Point leftMostPoint = points.getFirst(); + Point rightMostPoint = points.getLast(); convexSet.add(leftMostPoint); convexSet.add(rightMostPoint); @@ -85,9 +98,8 @@ public static List convexHullRecursive(List points) { constructHull(upperHull, leftMostPoint, rightMostPoint, convexSet); constructHull(lowerHull, rightMostPoint, leftMostPoint, convexSet); - List result = new ArrayList<>(convexSet); - Collections.sort(result); - return result; + // Convert to list and sort in counter-clockwise order + return sortCounterClockwise(new ArrayList<>(convexSet)); } private static void constructHull(Collection points, Point left, Point right, Set convexSet) { @@ -114,4 +126,82 @@ private static void constructHull(Collection points, Point left, Point ri } } } + + /** + * Sorts convex hull points in counter-clockwise order starting from + * the bottom-most, left-most point. + * + * @param hullPoints the unsorted convex hull points + * @return the points sorted in counter-clockwise order + */ + private static List sortCounterClockwise(List hullPoints) { + if (hullPoints.size() <= 2) { + Collections.sort(hullPoints); + return hullPoints; + } + + // Find the bottom-most, left-most point (pivot) + Point pivot = hullPoints.getFirst(); + for (Point p : hullPoints) { + if (p.y() < pivot.y() || (p.y() == pivot.y() && p.x() < pivot.x())) { + pivot = p; + } + } + + // Sort other points by polar angle with respect to pivot + final Point finalPivot = pivot; + List sorted = new ArrayList<>(hullPoints); + sorted.remove(finalPivot); + + sorted.sort((p1, p2) -> { + int crossProduct = Point.orientation(finalPivot, p1, p2); + + if (crossProduct == 0) { + // Collinear points: sort by distance from pivot (closer first for convex hull) + long dist1 = distanceSquared(finalPivot, p1); + long dist2 = distanceSquared(finalPivot, p2); + return Long.compare(dist1, dist2); + } + + // Positive cross product means p2 is counter-clockwise from p1 + // We want counter-clockwise order, so if p2 is CCW from p1, p1 should come first + return -crossProduct; + }); + + // Build result with pivot first, filtering out intermediate collinear points + List result = new ArrayList<>(); + result.add(finalPivot); + + if (!sorted.isEmpty()) { + // This loop iterates through the points sorted by angle. + // For points that are collinear with the pivot, we only want the one that is farthest away. + // The sort places closer points first. + for (int i = 0; i < sorted.size() - 1; i++) { + // Check the orientation of the pivot, the current point, and the next point. + int orientation = Point.orientation(finalPivot, sorted.get(i), sorted.get(i + 1)); + + // If the orientation is not 0, it means the next point (i+1) is at a new angle. + // Therefore, the current point (i) must be the farthest point at its angle. We keep it. + if (orientation != 0) { + result.add(sorted.get(i)); + } + // If the orientation is 0, the points are collinear. We discard the current point (i) + // because it is closer to the pivot than the next point (i+1). + } + // Always add the very last point from the sorted list. It is either the only point + // at its angle, or it's the farthest among a set of collinear points. + result.add(sorted.getLast()); + } + + return result; + } + + /** + * Computes the squared distance between two points to avoid floating point operations. + */ + private static long distanceSquared(Point p1, Point p2) { + long dx = (long) p1.x() - p2.x(); + long dy = (long) p1.y() - p2.y(); + return dx * dx + dy * dy; + } } diff --git a/src/main/java/com/thealgorithms/geometry/DDALine.java b/src/main/java/com/thealgorithms/geometry/DDALine.java new file mode 100644 index 000000000000..cb24951f398a --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/DDALine.java @@ -0,0 +1,54 @@ +package com.thealgorithms.geometry; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@code DDALine} class implements the Digital Differential Analyzer (DDA) + * line drawing algorithm. It computes points along a line between two given + * endpoints using floating-point arithmetic. + * + * The algorithm is straightforward but less efficient compared to + * Bresenham’s line algorithm, since it relies on floating-point operations. + * + * For more information, please visit {@link https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)} + */ +public final class DDALine { + + private DDALine() { + // Prevent instantiation + } + + /** + * Finds the list of points forming a line between two endpoints using DDA. + * + * @param x0 the x-coordinate of the starting point + * @param y0 the y-coordinate of the starting point + * @param x1 the x-coordinate of the ending point + * @param y1 the y-coordinate of the ending point + * @return an unmodifiable {@code List} containing all points on the line + */ + public static List findLine(int x0, int y0, int x1, int y1) { + int dx = x1 - x0; + int dy = y1 - y0; + + int steps = Math.max(Math.abs(dx), Math.abs(dy)); // number of steps + + double xIncrement = dx / (double) steps; + double yIncrement = dy / (double) steps; + + double x = x0; + double y = y0; + + List line = new ArrayList<>(steps + 1); + + for (int i = 0; i <= steps; i++) { + line.add(new Point((int) Math.round(x), (int) Math.round(y))); + x += xIncrement; + y += yIncrement; + } + + return line; + } +} diff --git a/src/main/java/com/thealgorithms/geometry/Haversine.java b/src/main/java/com/thealgorithms/geometry/Haversine.java new file mode 100644 index 000000000000..fa1799e9f076 --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/Haversine.java @@ -0,0 +1,49 @@ +package com.thealgorithms.geometry; +/** + * This Class implements the Haversine formula to calculate the distance between two points on a sphere (like Earth) from their latitudes and longitudes. + * + * The Haversine formula is used in navigation and mapping to find the great-circle distance, + * which is the shortest distance between two points along the surface of a sphere. It is often + * used to calculate the "as the crow flies" distance between two geographical locations. + * + * The formula is reliable for all distances, including small ones, and avoids issues with + * numerical instability that can affect other methods. + * + * @see "https://en.wikipedia.org/wiki/Haversine_formula" - Wikipedia + */ +public final class Haversine { + + // Average radius of Earth in kilometers + private static final double EARTH_RADIUS_KM = 6371.0; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Haversine() { + } + + /** + * Calculates the great-circle distance between two points on the earth + * (specified in decimal degrees). + * + * @param lat1 Latitude of the first point in decimal degrees. + * @param lon1 Longitude of the first point in decimal degrees. + * @param lat2 Latitude of the second point in decimal degrees. + * @param lon2 Longitude of the second point in decimal degrees. + * @return The distance between the two points in kilometers. + */ + public static double haversine(double lat1, double lon1, double lat2, double lon2) { + // Convert latitude and longitude from degrees to radians + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double lat1Rad = Math.toRadians(lat1); + double lat2Rad = Math.toRadians(lat2); + + // Apply the Haversine formula + double a = Math.pow(Math.sin(dLat / 2), 2) + Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1Rad) * Math.cos(lat2Rad); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS_KM * c; + } +} diff --git a/src/main/java/com/thealgorithms/geometry/WusLine.java b/src/main/java/com/thealgorithms/geometry/WusLine.java new file mode 100644 index 000000000000..3539daaf6e5a --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/WusLine.java @@ -0,0 +1,235 @@ +package com.thealgorithms.geometry; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@code WusLine} class implements Xiaolin Wu's line drawing algorithm, + * which produces anti-aliased lines by varying pixel brightness + * according to the line's proximity to pixel centers. + * + * This implementation returns the pixel coordinates along with + * their associated intensity values (in range [0.0, 1.0]), allowing + * rendering systems to blend accordingly. + * + * The algorithm works by: + * - Computing a line's intersection with pixel boundaries + * - Assigning intensity values based on distance from pixel centers + * - Drawing pairs of pixels perpendicular to the line's direction + * + * Reference: Xiaolin Wu, "An Efficient Antialiasing Technique", + * Computer Graphics (SIGGRAPH '91 Proceedings). + * + */ +public final class WusLine { + + private WusLine() { + // Utility class; prevent instantiation. + } + + /** + * Represents a pixel and its intensity for anti-aliased rendering. + * + * The intensity value determines how bright the pixel should be drawn, + * with 1.0 being fully opaque and 0.0 being fully transparent. + */ + public static class Pixel { + /** The pixel's coordinate on the screen. */ + public final Point point; + + /** The pixel's intensity value, clamped to the range [0.0, 1.0]. */ + public final double intensity; + + /** + * Constructs a new Pixel with the given coordinates and intensity. + * + * @param x the x-coordinate of the pixel + * @param y the y-coordinate of the pixel + * @param intensity the brightness/opacity of the pixel, will be clamped to [0.0, 1.0] + */ + public Pixel(int x, int y, double intensity) { + this.point = new Point(x, y); + this.intensity = Math.clamp(intensity, 0.0, 1.0); + } + } + + /** + * Internal class to hold processed endpoint data. + */ + private static class EndpointData { + final int xPixel; + final int yPixel; + final double yEnd; + final double xGap; + + EndpointData(int xPixel, int yPixel, double yEnd, double xGap) { + this.xPixel = xPixel; + this.yPixel = yPixel; + this.yEnd = yEnd; + this.xGap = xGap; + } + } + + /** + * Draws an anti-aliased line using Wu's algorithm. + * + * The algorithm produces smooth lines by drawing pairs of pixels at each + * x-coordinate (or y-coordinate for steep lines), with intensities based on + * the line's distance from pixel centers. + * + * @param x0 the x-coordinate of the line's start point + * @param y0 the y-coordinate of the line's start point + * @param x1 the x-coordinate of the line's end point + * @param y1 the y-coordinate of the line's end point + * @return a list of {@link Pixel} objects representing the anti-aliased line, + * ordered from start to end + */ + public static List drawLine(int x0, int y0, int x1, int y1) { + List pixels = new ArrayList<>(); + + // Determine if the line is steep (more vertical than horizontal) + boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0); + + if (steep) { + // For steep lines, swap x and y coordinates to iterate along y-axis + int temp = x0; + x0 = y0; + y0 = temp; + + temp = x1; + x1 = y1; + y1 = temp; + } + + if (x0 > x1) { + // Ensure we always draw from left to right + int temp = x0; + x0 = x1; + x1 = temp; + + temp = y0; + y0 = y1; + y1 = temp; + } + + // Calculate the line's slope + double deltaX = x1 - (double) x0; + double deltaY = y1 - (double) y0; + double gradient = (deltaX == 0) ? 1.0 : deltaY / deltaX; + + // Process the first endpoint + EndpointData firstEndpoint = processEndpoint(x0, y0, gradient, true); + addEndpointPixels(pixels, firstEndpoint, steep); + + // Process the second endpoint + EndpointData secondEndpoint = processEndpoint(x1, y1, gradient, false); + addEndpointPixels(pixels, secondEndpoint, steep); + + // Draw the main line between endpoints + drawMainLine(pixels, firstEndpoint, secondEndpoint, gradient, steep); + + return pixels; + } + + /** + * Processes a line endpoint to determine its pixel coordinates and intensities. + * + * @param x the x-coordinate of the endpoint + * @param y the y-coordinate of the endpoint + * @param gradient the slope of the line + * @param isStart true if this is the start endpoint, false if it's the end + * @return an {@link EndpointData} object containing processed endpoint information + */ + private static EndpointData processEndpoint(double x, double y, double gradient, boolean isStart) { + double xEnd = round(x); + double yEnd = y + gradient * (xEnd - x); + double xGap = isStart ? rfpart(x + 0.5) : fpart(x + 0.5); + + int xPixel = (int) xEnd; + int yPixel = (int) Math.floor(yEnd); + + return new EndpointData(xPixel, yPixel, yEnd, xGap); + } + + /** + * Adds the two endpoint pixels (one above, one below the line) to the pixel list. + * + * @param pixels the list to add pixels to + * @param endpoint the endpoint data containing coordinates and gaps + * @param steep true if the line is steep (coordinates should be swapped) + */ + private static void addEndpointPixels(List pixels, EndpointData endpoint, boolean steep) { + double fractionalY = fpart(endpoint.yEnd); + double complementFractionalY = rfpart(endpoint.yEnd); + + if (steep) { + pixels.add(new Pixel(endpoint.yPixel, endpoint.xPixel, complementFractionalY * endpoint.xGap)); + pixels.add(new Pixel(endpoint.yPixel + 1, endpoint.xPixel, fractionalY * endpoint.xGap)); + } else { + pixels.add(new Pixel(endpoint.xPixel, endpoint.yPixel, complementFractionalY * endpoint.xGap)); + pixels.add(new Pixel(endpoint.xPixel, endpoint.yPixel + 1, fractionalY * endpoint.xGap)); + } + } + + /** + * Draws the main portion of the line between the two endpoints. + * + * @param pixels the list to add pixels to + * @param firstEndpoint the processed start endpoint + * @param secondEndpoint the processed end endpoint + * @param gradient the slope of the line + * @param steep true if the line is steep (coordinates should be swapped) + */ + private static void drawMainLine(List pixels, EndpointData firstEndpoint, EndpointData secondEndpoint, double gradient, boolean steep) { + // Start y-intersection after the first endpoint + double intersectionY = firstEndpoint.yEnd + gradient; + + // Iterate through x-coordinates between the endpoints + for (int x = firstEndpoint.xPixel + 1; x < secondEndpoint.xPixel; x++) { + int yFloor = (int) Math.floor(intersectionY); + double fractionalPart = fpart(intersectionY); + double complementFractionalPart = rfpart(intersectionY); + + if (steep) { + pixels.add(new Pixel(yFloor, x, complementFractionalPart)); + pixels.add(new Pixel(yFloor + 1, x, fractionalPart)); + } else { + pixels.add(new Pixel(x, yFloor, complementFractionalPart)); + pixels.add(new Pixel(x, yFloor + 1, fractionalPart)); + } + + intersectionY += gradient; + } + } + + /** + * Returns the fractional part of a number. + * + * @param x the input number + * @return the fractional part (always in range [0.0, 1.0)) + */ + private static double fpart(double x) { + return x - Math.floor(x); + } + + /** + * Returns the reverse fractional part of a number (1 - fractional part). + * + * @param x the input number + * @return 1.0 minus the fractional part (always in range (0.0, 1.0]) + */ + private static double rfpart(double x) { + return 1.0 - fpart(x); + } + + /** + * Rounds a number to the nearest integer. + * + * @param x the input number + * @return the nearest integer value as a double + */ + private static double round(double x) { + return Math.floor(x + 0.5); + } +} diff --git a/src/main/java/com/thealgorithms/graph/BronKerbosch.java b/src/main/java/com/thealgorithms/graph/BronKerbosch.java new file mode 100644 index 000000000000..0510d9bcf494 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/BronKerbosch.java @@ -0,0 +1,114 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implementation of the Bron–Kerbosch algorithm with pivoting for enumerating all maximal cliques + * in an undirected graph. + * + *

The input graph is represented as an adjacency list where {@code adjacency.get(u)} returns the + * set of vertices adjacent to {@code u}. The algorithm runs in time proportional to the number of + * maximal cliques produced and is widely used for clique enumeration problems.

+ * + * @author Wikipedia: Bron–Kerbosch algorithm + */ +public final class BronKerbosch { + + private BronKerbosch() { + } + + /** + * Finds all maximal cliques of the provided graph. + * + * @param adjacency adjacency list where {@code adjacency.size()} equals the number of vertices + * @return a list containing every maximal clique, each represented as a {@link Set} of vertices + * @throws IllegalArgumentException if the adjacency list is {@code null}, contains {@code null} + * entries, or references invalid vertices + */ + public static List> findMaximalCliques(List> adjacency) { + if (adjacency == null) { + throw new IllegalArgumentException("Adjacency list must not be null"); + } + + int n = adjacency.size(); + List> graph = new ArrayList<>(n); + for (int u = 0; u < n; u++) { + Set neighbors = adjacency.get(u); + if (neighbors == null) { + throw new IllegalArgumentException("Adjacency list must not contain null sets"); + } + Set copy = new HashSet<>(); + for (int v : neighbors) { + if (v < 0 || v >= n) { + throw new IllegalArgumentException("Neighbor index out of bounds: " + v); + } + if (v != u) { + copy.add(v); + } + } + graph.add(copy); + } + + Set r = new HashSet<>(); + Set p = new HashSet<>(); + Set x = new HashSet<>(); + for (int v = 0; v < n; v++) { + p.add(v); + } + + List> cliques = new ArrayList<>(); + bronKerboschPivot(r, p, x, graph, cliques); + return cliques; + } + + private static void bronKerboschPivot(Set r, Set p, Set x, List> graph, List> cliques) { + if (p.isEmpty() && x.isEmpty()) { + cliques.add(new HashSet<>(r)); + return; + } + + int pivot = choosePivot(p, x, graph); + Set candidates = new HashSet<>(p); + if (pivot != -1) { + candidates.removeAll(graph.get(pivot)); + } + + for (Integer v : candidates) { + r.add(v); + Set newP = intersection(p, graph.get(v)); + Set newX = intersection(x, graph.get(v)); + bronKerboschPivot(r, newP, newX, graph, cliques); + r.remove(v); + p.remove(v); + x.add(v); + } + } + + private static int choosePivot(Set p, Set x, List> graph) { + int pivot = -1; + int maxDegree = -1; + Set union = new HashSet<>(p); + union.addAll(x); + for (Integer v : union) { + int degree = graph.get(v).size(); + if (degree > maxDegree) { + maxDegree = degree; + pivot = v; + } + } + return pivot; + } + + private static Set intersection(Set base, Set neighbors) { + Set result = new HashSet<>(); + for (Integer v : base) { + if (neighbors.contains(v)) { + result.add(v); + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/graph/Dinic.java b/src/main/java/com/thealgorithms/graph/Dinic.java new file mode 100644 index 000000000000..c45bd62ddfbb --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/Dinic.java @@ -0,0 +1,119 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Dinic's algorithm for computing maximum flow in a directed graph. + * + *

Time complexity: O(E * V^2) in the worst case, but typically faster in practice + * and near O(E * sqrt(V)) for unit networks.

+ * + *

The graph is represented using a capacity matrix where capacity[u][v] is the + * capacity of the directed edge u -> v. Capacities must be non-negative. + * The algorithm builds level graphs using BFS and finds blocking flows using DFS + * with current-edge optimization.

+ * + *

This implementation mirrors the API and validation style of + * {@link EdmondsKarp#maxFlow(int[][], int, int)} for consistency.

+ * + * @see Wikipedia: Dinic's algorithm + */ +public final class Dinic { + private Dinic() { + } + + /** + * Computes the maximum flow from source to sink using Dinic's algorithm. + * + * @param capacity square capacity matrix (n x n); entries must be >= 0 + * @param source source vertex index in [0, n) + * @param sink sink vertex index in [0, n) + * @return the maximum flow value + * @throws IllegalArgumentException if the input matrix is null/non-square/has negatives or + * indices invalid + */ + public static int maxFlow(int[][] capacity, int source, int sink) { + if (capacity == null || capacity.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + final int n = capacity.length; + for (int i = 0; i < n; i++) { + if (capacity[i] == null || capacity[i].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int j = 0; j < n; j++) { + if (capacity[i][j] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + if (source < 0 || sink < 0 || source >= n || sink >= n) { + throw new IllegalArgumentException("Source and sink must be valid vertex indices"); + } + if (source == sink) { + return 0; + } + + // residual capacities + int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + int[] level = new int[n]; + int flow = 0; + while (bfsBuildLevelGraph(residual, source, sink, level)) { + int[] next = new int[n]; // current-edge optimization + int pushed; + do { + pushed = dfsBlocking(residual, level, next, source, sink, Integer.MAX_VALUE); + flow += pushed; + } while (pushed > 0); + } + return flow; + } + + private static boolean bfsBuildLevelGraph(int[][] residual, int source, int sink, int[] level) { + Arrays.fill(level, -1); + level[source] = 0; + 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 && level[v] == -1) { + level[v] = level[u] + 1; + if (v == sink) { + return true; + } + q.add(v); + } + } + } + return level[sink] != -1; + } + + private static int dfsBlocking(int[][] residual, int[] level, int[] next, int u, int sink, int f) { + if (u == sink) { + return f; + } + final int n = residual.length; + for (int v = next[u]; v < n; v++, next[u] = v) { + if (residual[u][v] <= 0) { + continue; + } + if (level[v] != level[u] + 1) { + continue; + } + int pushed = dfsBlocking(residual, level, next, v, sink, Math.min(f, residual[u][v])); + if (pushed > 0) { + residual[u][v] -= pushed; + residual[v][u] += pushed; + return pushed; + } + } + return 0; + } +} diff --git a/src/main/java/com/thealgorithms/graph/Edmonds.java b/src/main/java/com/thealgorithms/graph/Edmonds.java new file mode 100644 index 000000000000..4ddb8f9ff544 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/Edmonds.java @@ -0,0 +1,201 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An implementation of Edmonds's algorithm (also known as the Chu–Liu/Edmonds algorithm) + * for finding a Minimum Spanning Arborescence (MSA). + * + *

An MSA is a directed graph equivalent of a Minimum Spanning Tree. It is a tree rooted + * at a specific vertex 'r' that reaches all other vertices, such that the sum of the + * weights of its edges is minimized. + * + *

The algorithm works recursively: + *

    + *
  1. For each vertex other than the root, select the incoming edge with the minimum weight.
  2. + *
  3. If the selected edges form a spanning arborescence, it is the MSA.
  4. + *
  5. If cycles are formed, contract each cycle into a new "supernode".
  6. + *
  7. Modify the weights of edges entering the new supernode.
  8. + *
  9. Recursively call the algorithm on the contracted graph.
  10. + *
  11. The final cost is the sum of the initial edge selections and the result of the recursive call.
  12. + *
+ * + *

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

References: + *

+ */ +public final class Edmonds { + + private Edmonds() { + } + + /** + * Represents a directed weighted edge in the graph. + */ + public static class Edge { + final int from; + final int to; + final long weight; + + /** + * Constructs a directed edge. + * + * @param from source vertex + * @param to destination vertex + * @param weight edge weight + */ + public Edge(int from, int to, long weight) { + this.from = from; + this.to = to; + this.weight = weight; + } + } + + /** + * Computes the total weight of the Minimum Spanning Arborescence of a directed, + * weighted graph from a given root. + * + * @param numVertices the number of vertices, labeled {@code 0..numVertices-1} + * @param edges list of directed edges in the graph + * @param root the root vertex + * @return the total weight of the MSA. Returns -1 if not all vertices are reachable + * from the root or if a valid arborescence cannot be formed. + * @throws IllegalArgumentException if {@code numVertices <= 0} or {@code root} is out of range. + */ + public static long findMinimumSpanningArborescence(int numVertices, List edges, int root) { + if (root < 0 || root >= numVertices) { + throw new IllegalArgumentException("Invalid number of vertices or root"); + } + if (numVertices == 1) { + return 0; + } + + return findMSARecursive(numVertices, edges, root); + } + + /** + * Recursive helper method for finding MSA. + */ + private static long findMSARecursive(int n, List edges, int root) { + long[] minWeightEdge = new long[n]; + int[] predecessor = new int[n]; + Arrays.fill(minWeightEdge, Long.MAX_VALUE); + Arrays.fill(predecessor, -1); + + for (Edge edge : edges) { + if (edge.to != root && edge.weight < minWeightEdge[edge.to]) { + minWeightEdge[edge.to] = edge.weight; + predecessor[edge.to] = edge.from; + } + } + // Check if all non-root nodes are reachable + for (int i = 0; i < n; i++) { + if (i != root && minWeightEdge[i] == Long.MAX_VALUE) { + return -1; // No spanning arborescence exists + } + } + int[] cycleId = new int[n]; + Arrays.fill(cycleId, -1); + boolean[] visited = new boolean[n]; + int cycleCount = 0; + + for (int i = 0; i < n; i++) { + if (visited[i]) { + continue; + } + + List path = new ArrayList<>(); + int curr = i; + + // Follow predecessor chain + while (curr != -1 && !visited[curr]) { + visited[curr] = true; + path.add(curr); + curr = predecessor[curr]; + } + + // If we hit a visited node, check if it forms a cycle + if (curr != -1) { + boolean inCycle = false; + for (int node : path) { + if (node == curr) { + inCycle = true; + } + if (inCycle) { + cycleId[node] = cycleCount; + } + } + if (inCycle) { + cycleCount++; + } + } + } + if (cycleCount == 0) { + long totalWeight = 0; + for (int i = 0; i < n; i++) { + if (i != root) { + totalWeight += minWeightEdge[i]; + } + } + return totalWeight; + } + long cycleWeightSum = 0; + for (int i = 0; i < n; i++) { + if (cycleId[i] >= 0) { + cycleWeightSum += minWeightEdge[i]; + } + } + + // Map old nodes to new nodes (cycles become supernodes) + int[] newNodeMap = new int[n]; + int[] cycleToNewNode = new int[cycleCount]; + int newN = 0; + + // Assign new node IDs to cycles first + for (int i = 0; i < cycleCount; i++) { + cycleToNewNode[i] = newN++; + } + + // Assign new node IDs to non-cycle nodes + for (int i = 0; i < n; i++) { + if (cycleId[i] == -1) { + newNodeMap[i] = newN++; + } else { + newNodeMap[i] = cycleToNewNode[cycleId[i]]; + } + } + + int newRoot = newNodeMap[root]; + + // Build contracted graph + List newEdges = new ArrayList<>(); + for (Edge edge : edges) { + int uCycleId = cycleId[edge.from]; + int vCycleId = cycleId[edge.to]; + + // Skip edges internal to a cycle + if (uCycleId >= 0 && uCycleId == vCycleId) { + continue; + } + + int newU = newNodeMap[edge.from]; + int newV = newNodeMap[edge.to]; + + long newWeight = edge.weight; + // Adjust weight for edges entering a cycle + if (vCycleId >= 0) { + newWeight -= minWeightEdge[edge.to]; + } + + if (newU != newV) { + newEdges.add(new Edge(newU, newV, newWeight)); + } + } + return cycleWeightSum + findMSARecursive(newN, newEdges, newRoot); + } +} diff --git a/src/main/java/com/thealgorithms/graph/EdmondsKarp.java b/src/main/java/com/thealgorithms/graph/EdmondsKarp.java new file mode 100644 index 000000000000..59e7b09cb49c --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/EdmondsKarp.java @@ -0,0 +1,107 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Implementation of the Edmonds–Karp algorithm for computing the maximum flow of a directed graph. + *

+ * The algorithm runs in O(V * E^2) time and is a specific implementation of the Ford–Fulkerson + * method where the augmenting paths are found using breadth-first search (BFS) to ensure the + * shortest augmenting paths (in terms of the number of edges) are used. + *

+ * + *

The graph is represented with a capacity matrix where {@code capacity[u][v]} denotes the + * capacity of the edge from {@code u} to {@code v}. Negative capacities are not allowed.

+ * + * @author Wikipedia: EdmondsKarp algorithm + */ +public final class EdmondsKarp { + + private EdmondsKarp() { + } + + /** + * Computes the maximum flow from {@code source} to {@code sink} in the provided capacity matrix. + * + * @param capacity the capacity matrix representing the directed graph; must be square and non-null + * @param source the source vertex index + * @param sink the sink vertex index + * @return the value of the maximum flow between {@code source} and {@code sink} + * @throws IllegalArgumentException if the matrix is {@code null}, not square, contains negative + * capacities, or if {@code source} / {@code sink} indices are invalid + */ + public static int maxFlow(int[][] capacity, int source, int sink) { + if (capacity == null || capacity.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + + final int n = capacity.length; + for (int row = 0; row < n; row++) { + if (capacity[row] == null || capacity[row].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int col = 0; col < n; col++) { + if (capacity[row][col] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + + if (source < 0 || source >= n || sink < 0 || sink >= n) { + throw new IllegalArgumentException("Source and sink must be valid vertex indices"); + } + if (source == sink) { + return 0; + } + + final int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + final 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; + } + + return maxFlow; + } + + private static boolean bfs(int[][] residual, int source, int sink, int[] parent) { + Arrays.fill(parent, -1); + parent[source] = source; + + Queue queue = new ArrayDeque<>(); + queue.add(source); + + while (!queue.isEmpty()) { + int u = queue.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; + } + queue.add(v); + } + } + } + return false; + } +} 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/HierholzerAlgorithm.java b/src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java new file mode 100644 index 000000000000..a804f77d7fa6 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java @@ -0,0 +1,140 @@ +package com.thealgorithms.graph; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * Implementation of Hierholzer's algorithm to find an Eulerian Circuit in an undirected graph. + *

+ * An Eulerian circuit is a trail in a graph that visits every edge exactly once, + * starting and ending at the same vertex. This algorithm finds such a circuit if one exists. + *

+ *

+ * This implementation is designed for an undirected graph. For a valid Eulerian + * circuit to exist, the graph must satisfy two conditions: + *

    + *
  1. All vertices with a non-zero degree must be part of a single connected component.
  2. + *
  3. Every vertex must have an even degree (an even number of edges connected to it).
  4. + *
+ *

+ *

+ * The algorithm runs in O(E + V) time, where E is the number of edges and V is the number of vertices. + * The graph is represented by a Map where keys are vertices and values are a LinkedList of adjacent vertices. + *

+ * + * @see Wikipedia: Hierholzer's algorithm + */ +public final class HierholzerAlgorithm { + + private final Map> graph; + + public HierholzerAlgorithm(Map> graph) { + this.graph = (graph == null) ? new HashMap<>() : graph; + } + + public boolean hasEulerianCircuit() { + if (graph.isEmpty()) { + return true; + } + + for (List neighbors : graph.values()) { + if (neighbors.size() % 2 != 0) { + return false; + } + } + + return isCoherentlyConnected(); + } + + public List findEulerianCircuit() { + if (!hasEulerianCircuit()) { + return Collections.emptyList(); + } + + Map> tempGraph = new HashMap<>(); + for (Map.Entry> entry : graph.entrySet()) { + tempGraph.put(entry.getKey(), new LinkedList<>(entry.getValue())); + } + + Stack currentPath = new Stack<>(); + LinkedList circuit = new LinkedList<>(); + + int startVertex = -1; + for (Map.Entry> entry : tempGraph.entrySet()) { + if (!entry.getValue().isEmpty()) { + startVertex = entry.getKey(); + break; + } + } + + if (startVertex == -1) { + if (graph.isEmpty()) { + return Collections.emptyList(); + } + return Collections.singletonList(graph.keySet().iterator().next()); + } + + currentPath.push(startVertex); + + while (!currentPath.isEmpty()) { + int currentVertex = currentPath.peek(); + + if (tempGraph.containsKey(currentVertex) && !tempGraph.get(currentVertex).isEmpty()) { + int nextVertex = tempGraph.get(currentVertex).pollFirst(); + tempGraph.get(nextVertex).remove(Integer.valueOf(currentVertex)); + currentPath.push(nextVertex); + } else { + circuit.addFirst(currentVertex); + currentPath.pop(); + } + } + + return circuit; + } + + private boolean isCoherentlyConnected() { + if (graph.isEmpty()) { + return true; + } + + Set visited = new HashSet<>(); + int startNode = -1; + + for (Map.Entry> entry : graph.entrySet()) { + if (!entry.getValue().isEmpty()) { + startNode = entry.getKey(); + break; + } + } + + if (startNode == -1) { + return true; + } + + dfs(startNode, visited); + + for (Map.Entry> entry : graph.entrySet()) { + if (!entry.getValue().isEmpty() && !visited.contains(entry.getKey())) { + return false; + } + } + return true; + } + + private void dfs(int u, Set visited) { + visited.add(u); + if (graph.containsKey(u)) { + for (int v : graph.get(u)) { + if (!visited.contains(v)) { + dfs(v, visited); + } + } + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java b/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java new file mode 100644 index 000000000000..82da5c8163e5 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java @@ -0,0 +1,303 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +/** + * Implementation of Hierholzer's Algorithm for finding an Eulerian Path or Circuit + * in a directed graph. + * + *

+ * An Eulerian Circuit is a path that starts and ends at the same vertex + * and visits every edge exactly once. + *

+ * + *

+ * An Eulerian Path visits every edge exactly once but may start and end + * at different vertices. + *

+ * + *

+ * Algorithm Summary:
+ * 1. Compute indegree and outdegree for all vertices.
+ * 2. Check if the graph satisfies Eulerian path or circuit conditions.
+ * 3. Verify that all vertices with non-zero degree are weakly connected (undirected connectivity).
+ * 4. Use Hierholzer’s algorithm to build the path by exploring unused edges iteratively. + *

+ * + *

+ * Time Complexity: O(E + V).
+ * Space Complexity: O(V + E). + *

+ * + * @author Wikipedia: Hierholzer algorithm + */ +public class HierholzerEulerianPath { + + /** + * Simple directed graph represented by adjacency lists. + */ + public static class Graph { + private final List> adjacencyList; + + /** + * Constructs a graph with a given number of vertices. + * + * @param numNodes number of vertices + */ + public Graph(int numNodes) { + adjacencyList = new ArrayList<>(); + for (int i = 0; i < numNodes; i++) { + adjacencyList.add(new ArrayList<>()); + } + } + + /** + * Adds a directed edge from vertex {@code from} to vertex {@code to}. + * + * @param from source vertex + * @param to destination vertex + */ + public void addEdge(int from, int to) { + adjacencyList.get(from).add(to); + } + + /** + * Returns a list of outgoing edges from the given vertex. + * + * @param node vertex index + * @return list of destination vertices + */ + public List getEdges(int node) { + return adjacencyList.get(node); + } + + /** + * Returns the number of vertices in the graph. + * + * @return number of vertices + */ + public int getNumNodes() { + return adjacencyList.size(); + } + } + + private final Graph graph; + + /** + * Creates a Hierholzer solver for the given graph. + * + * @param graph directed graph + */ + public HierholzerEulerianPath(Graph graph) { + this.graph = graph; + } + + /** + * Finds an Eulerian Path or Circuit using Hierholzer’s Algorithm. + * + * @return list of vertices representing the Eulerian Path/Circuit, + * or an empty list if none exists + */ + public List findEulerianPath() { + int n = graph.getNumNodes(); + + // empty graph -> no path + if (n == 0) { + return new ArrayList<>(); + } + + int[] inDegree = new int[n]; + int[] outDegree = new int[n]; + int edgeCount = computeDegrees(inDegree, outDegree); + + // no edges -> single vertex response requested by tests: [0] + if (edgeCount == 0) { + return Collections.singletonList(0); + } + + int startNode = determineStartNode(inDegree, outDegree); + if (startNode == -1) { + return new ArrayList<>(); + } + + if (!allNonZeroDegreeVerticesWeaklyConnected(startNode, n, outDegree, inDegree)) { + return new ArrayList<>(); + } + + List path = buildHierholzerPath(startNode, n); + if (path.size() != edgeCount + 1) { + return new ArrayList<>(); + } + + return rotateEulerianCircuitIfNeeded(path, outDegree, inDegree); + } + + private int computeDegrees(int[] inDegree, int[] outDegree) { + int edgeCount = 0; + for (int u = 0; u < graph.getNumNodes(); u++) { + for (int v : graph.getEdges(u)) { + outDegree[u]++; + inDegree[v]++; + edgeCount++; + } + } + return edgeCount; + } + + private int determineStartNode(int[] inDegree, int[] outDegree) { + int n = graph.getNumNodes(); + int startNode = -1; + int startCount = 0; + int endCount = 0; + + for (int i = 0; i < n; i++) { + int diff = outDegree[i] - inDegree[i]; + if (diff == 1) { + startNode = i; + startCount++; + } else if (diff == -1) { + endCount++; + } else if (Math.abs(diff) > 1) { + return -1; + } + } + + if (!((startCount == 1 && endCount == 1) || (startCount == 0 && endCount == 0))) { + return -1; + } + + if (startNode == -1) { + for (int i = 0; i < n; i++) { + if (outDegree[i] > 0) { + startNode = i; + break; + } + } + } + return startNode; + } + + private List buildHierholzerPath(int startNode, int n) { + List> tempAdj = new ArrayList<>(); + for (int i = 0; i < n; i++) { + tempAdj.add(new ArrayDeque<>(graph.getEdges(i))); + } + + Deque stack = new ArrayDeque<>(); + List path = new ArrayList<>(); + stack.push(startNode); + + while (!stack.isEmpty()) { + int u = stack.peek(); + if (!tempAdj.get(u).isEmpty()) { + stack.push(tempAdj.get(u).pollFirst()); + } else { + path.add(stack.pop()); + } + } + + Collections.reverse(path); + return path; + } + + private List rotateEulerianCircuitIfNeeded(List path, int[] outDegree, int[] inDegree) { + int startCount = 0; + int endCount = 0; + for (int i = 0; i < outDegree.length; i++) { + int diff = outDegree[i] - inDegree[i]; + if (diff == 1) { + startCount++; + } else if (diff == -1) { + endCount++; + } + } + + if (startCount == 0 && endCount == 0 && !path.isEmpty()) { + int preferredStart = -1; + for (int i = 0; i < outDegree.length; i++) { + if (outDegree[i] > 0) { + preferredStart = i; + break; + } + } + + if (preferredStart != -1 && path.get(0) != preferredStart) { + int idx = 0; + for (Integer node : path) { // replaced indexed loop + if (node == preferredStart) { + break; + } + idx++; + } + + if (idx > 0) { + List rotated = new ArrayList<>(); + int currentIndex = 0; + for (Integer node : path) { // replaced indexed loop + if (currentIndex >= idx) { + rotated.add(node); + } + currentIndex++; + } + currentIndex = 0; + for (Integer node : path) { // replaced indexed loop + if (currentIndex < idx) { + rotated.add(node); + } + currentIndex++; + } + path = rotated; + } + } + } + return path; + } + + /** + * Checks weak connectivity (undirected) among vertices that have non-zero degree. + * + * @param startNode node to start DFS from (must be a vertex with non-zero degree) + * @param n number of vertices + * @param outDegree out-degree array + * @param inDegree in-degree array + * @return true if all vertices having non-zero degree belong to a single weak component + */ + private boolean allNonZeroDegreeVerticesWeaklyConnected(int startNode, int n, int[] outDegree, int[] inDegree) { + boolean[] visited = new boolean[n]; + Deque stack = new ArrayDeque<>(); + stack.push(startNode); + visited[startNode] = true; + + while (!stack.isEmpty()) { + int u = stack.pop(); + for (int v : graph.getEdges(u)) { + if (!visited[v]) { + visited[v] = true; + stack.push(v); + } + } + for (int x = 0; x < n; x++) { + if (!visited[x]) { + for (int y : graph.getEdges(x)) { + if (y == u) { + visited[x] = true; + stack.push(x); + break; + } + } + } + } + } + + for (int i = 0; i < n; i++) { + if (outDegree[i] + inDegree[i] > 0 && !visited[i]) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java new file mode 100644 index 000000000000..84f5671c4f9d --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java @@ -0,0 +1,150 @@ +package com.thealgorithms.graph; + +import java.util.Arrays; + +/** + * Hungarian algorithm (a.k.a. Kuhn–Munkres) for the Assignment Problem. + * + *

Given an n x m cost matrix (n tasks, m workers), finds a minimum-cost + * one-to-one assignment. If the matrix is rectangular, the algorithm pads to a + * square internally. Costs must be finite non-negative integers. + * + *

Time complexity: O(n^3) with n = max(rows, cols). + * + *

API returns the assignment as an array where {@code assignment[i]} is the + * column chosen for row i (or -1 if unassigned when rows != cols), and a total + * minimal cost. + * + * @see Wikipedia: Hungarian algorithm + */ +public final class HungarianAlgorithm { + + private HungarianAlgorithm() { + } + + /** Result holder for the Hungarian algorithm. */ + public static final class Result { + public final int[] assignment; // assignment[row] = col or -1 + public final int minCost; + + public Result(int[] assignment, int minCost) { + this.assignment = assignment; + this.minCost = minCost; + } + } + + /** + * Solves the assignment problem for a non-negative cost matrix. + * + * @param cost an r x c matrix of non-negative costs + * @return Result with row-to-column assignment and minimal total cost + * @throws IllegalArgumentException for null/empty or negative costs + */ + public static Result solve(int[][] cost) { + validate(cost); + int rows = cost.length; + int cols = cost[0].length; + int n = Math.max(rows, cols); + + // Build square matrix with padding 0 for missing cells + int[][] a = new int[n][n]; + for (int i = 0; i < n; i++) { + if (i < rows) { + for (int j = 0; j < n; j++) { + a[i][j] = (j < cols) ? cost[i][j] : 0; + } + } else { + Arrays.fill(a[i], 0); + } + } + + // Potentials and matching arrays + int[] u = new int[n + 1]; + int[] v = new int[n + 1]; + int[] p = new int[n + 1]; + int[] way = new int[n + 1]; + + for (int i = 1; i <= n; i++) { + p[0] = i; + int j0 = 0; + int[] minv = new int[n + 1]; + boolean[] used = new boolean[n + 1]; + Arrays.fill(minv, Integer.MAX_VALUE); + Arrays.fill(used, false); + do { + used[j0] = true; + int i0 = p[j0]; + int delta = Integer.MAX_VALUE; + int j1 = 0; + for (int j = 1; j <= n; j++) { + if (!used[j]) { + int cur = a[i0 - 1][j - 1] - u[i0] - v[j]; + if (cur < minv[j]) { + minv[j] = cur; + way[j] = j0; + } + if (minv[j] < delta) { + delta = minv[j]; + j1 = j; + } + } + } + for (int j = 0; j <= n; j++) { + if (used[j]) { + u[p[j]] += delta; + v[j] -= delta; + } else { + minv[j] -= delta; + } + } + j0 = j1; + } while (p[j0] != 0); + do { + int j1 = way[j0]; + p[j0] = p[j1]; + j0 = j1; + } while (j0 != 0); + } + + int[] matchColForRow = new int[n]; + Arrays.fill(matchColForRow, -1); + for (int j = 1; j <= n; j++) { + if (p[j] != 0) { + matchColForRow[p[j] - 1] = j - 1; + } + } + + // Build assignment for original rows only, ignore padded rows + int[] assignment = new int[rows]; + Arrays.fill(assignment, -1); + int total = 0; + for (int i = 0; i < rows; i++) { + int j = matchColForRow[i]; + if (j >= 0 && j < cols) { + assignment[i] = j; + total += cost[i][j]; + } + } + return new Result(assignment, total); + } + + private static void validate(int[][] cost) { + if (cost == null || cost.length == 0) { + throw new IllegalArgumentException("Cost matrix must not be null or empty"); + } + int c = cost[0].length; + if (c == 0) { + throw new IllegalArgumentException("Cost matrix must have at least 1 column"); + } + for (int i = 0; i < cost.length; i++) { + if (cost[i] == null || cost[i].length != c) { + throw new IllegalArgumentException("Cost matrix must be rectangular with equal row lengths"); + } + for (int j = 0; j < c; j++) { + if (cost[i][j] < 0) { + throw new IllegalArgumentException("Costs must be non-negative"); + } + } + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/PushRelabel.java b/src/main/java/com/thealgorithms/graph/PushRelabel.java new file mode 100644 index 000000000000..1bfb5ceacce0 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/PushRelabel.java @@ -0,0 +1,162 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Push–Relabel (Relabel-to-Front variant simplified to array scanning) for maximum flow. + * + *

Input graph is a capacity matrix where {@code capacity[u][v]} is the capacity of the edge + * {@code u -> v}. Capacities must be non-negative. Vertices are indexed in {@code [0, n)}. + * + *

Time complexity: O(V^3) in the worst case for the array-based variant; typically fast in + * practice. This implementation uses a residual network over an adjacency-matrix representation. + * + *

The API mirrors {@link EdmondsKarp#maxFlow(int[][], int, int)} and {@link Dinic#maxFlow(int[][], int, int)}. + * + * @see Wikipedia: Push–Relabel maximum flow algorithm + */ +public final class PushRelabel { + + private PushRelabel() { + } + + /** + * Computes the maximum flow from {@code source} to {@code sink} using Push–Relabel. + * + * @param capacity square capacity matrix (n x n); entries must be >= 0 + * @param source source vertex index in [0, n) + * @param sink sink vertex index in [0, n) + * @return the maximum flow value + * @throws IllegalArgumentException if inputs are invalid + */ + public static int maxFlow(int[][] capacity, int source, int sink) { + validate(capacity, source, sink); + final int n = capacity.length; + if (source == sink) { + return 0; + } + + int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + int[] height = new int[n]; + int[] excess = new int[n]; + int[] nextNeighbor = new int[n]; + + // Preflow initialization + height[source] = n; + for (int v = 0; v < n; v++) { + int cap = residual[source][v]; + if (cap > 0) { + residual[source][v] -= cap; + residual[v][source] += cap; + excess[v] += cap; + excess[source] -= cap; + } + } + + // Active queue contains vertices (except source/sink) with positive excess + Queue active = new ArrayDeque<>(); + for (int v = 0; v < n; v++) { + if (v != source && v != sink && excess[v] > 0) { + active.add(v); + } + } + + State state = new State(residual, height, excess, nextNeighbor, source, sink, active); + + while (!active.isEmpty()) { + int u = active.poll(); + discharge(u, state); + if (excess[u] > 0) { + // still active after discharge; push to back + active.add(u); + } + } + + // Total flow equals excess at sink + return excess[sink]; + } + + private static void discharge(int u, State s) { + final int n = s.residual.length; + while (s.excess[u] > 0) { + if (s.nextNeighbor[u] >= n) { + relabel(u, s.residual, s.height); + s.nextNeighbor[u] = 0; + continue; + } + int v = s.nextNeighbor[u]; + if (s.residual[u][v] > 0 && s.height[u] == s.height[v] + 1) { + int delta = Math.min(s.excess[u], s.residual[u][v]); + s.residual[u][v] -= delta; + s.residual[v][u] += delta; + s.excess[u] -= delta; + int prevExcessV = s.excess[v]; + s.excess[v] += delta; + if (v != s.source && v != s.sink && prevExcessV == 0) { + s.active.add(v); + } + } else { + s.nextNeighbor[u]++; + } + } + } + + private static final class State { + final int[][] residual; + final int[] height; + final int[] excess; + final int[] nextNeighbor; + final int source; + final int sink; + final Queue active; + + State(int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue active) { + this.residual = residual; + this.height = height; + this.excess = excess; + this.nextNeighbor = nextNeighbor; + this.source = source; + this.sink = sink; + this.active = active; + } + } + + private static void relabel(int u, int[][] residual, int[] height) { + final int n = residual.length; + int minHeight = Integer.MAX_VALUE; + for (int v = 0; v < n; v++) { + if (residual[u][v] > 0) { + minHeight = Math.min(minHeight, height[v]); + } + } + if (minHeight < Integer.MAX_VALUE) { + height[u] = minHeight + 1; + } + } + + private static void validate(int[][] capacity, int source, int sink) { + if (capacity == null || capacity.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + int n = capacity.length; + for (int i = 0; i < n; i++) { + if (capacity[i] == null || capacity[i].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int j = 0; j < n; j++) { + if (capacity[i][j] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + if (source < 0 || sink < 0 || source >= n || sink >= n) { + throw new IllegalArgumentException("Source and sink must be valid vertex indices"); + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/StoerWagner.java b/src/main/java/com/thealgorithms/graph/StoerWagner.java new file mode 100644 index 000000000000..b204834c431a --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/StoerWagner.java @@ -0,0 +1,78 @@ +package com.thealgorithms.graph; + +/** + * An implementation of the Stoer-Wagner algorithm to find the global minimum cut of an undirected, weighted graph. + * A minimum cut is a partition of the graph's vertices into two disjoint sets with the minimum possible edge weight + * sum connecting the two sets. + * + * Wikipedia: https://en.wikipedia.org/wiki/Stoer%E2%80%93Wagner_algorithm + * Time Complexity: O(V^3) where V is the number of vertices. + */ +public class StoerWagner { + + /** + * Finds the minimum cut in the given undirected, weighted graph. + * + * @param graph An adjacency matrix representing the graph. graph[i][j] is the weight of the edge between i and j. + * @return The weight of the minimum cut. + */ + public int findMinCut(int[][] graph) { + int n = graph.length; + if (n < 2) { + return 0; + } + + int[][] currentGraph = new int[n][n]; + for (int i = 0; i < n; i++) { + System.arraycopy(graph[i], 0, currentGraph[i], 0, n); + } + + int minCut = Integer.MAX_VALUE; + boolean[] merged = new boolean[n]; + + for (int phase = 0; phase < n - 1; phase++) { + boolean[] inSetA = new boolean[n]; + int[] weights = new int[n]; + int prev = -1; + int last = -1; + + for (int i = 0; i < n - phase; i++) { + int maxWeight = -1; + int currentVertex = -1; + + for (int j = 0; j < n; j++) { + if (!merged[j] && !inSetA[j] && weights[j] > maxWeight) { + maxWeight = weights[j]; + currentVertex = j; + } + } + + if (currentVertex == -1) { + // This can happen if the graph is disconnected. + return 0; + } + + prev = last; + last = currentVertex; + inSetA[last] = true; + + for (int j = 0; j < n; j++) { + if (!merged[j] && !inSetA[j]) { + weights[j] += currentGraph[last][j]; + } + } + } + + minCut = Math.min(minCut, weights[last]); + + // Merge 'last' vertex into 'prev' vertex + for (int i = 0; i < n; i++) { + currentGraph[prev][i] += currentGraph[last][i]; + currentGraph[i][prev] = currentGraph[prev][i]; + } + merged[last] = true; + } + + return minCut; + } +} diff --git a/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java b/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java new file mode 100644 index 000000000000..dfc8386de6ce --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java @@ -0,0 +1,263 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; + +/** + * Yen's algorithm for finding K loopless shortest paths in a directed graph with non-negative edge weights. + * + *

Input is an adjacency matrix of edge weights. A value of -1 indicates no edge. + * All existing edge weights must be non-negative. Zero-weight edges are allowed.

+ * + *

References: + * - Wikipedia: Yen's algorithm (https://en.wikipedia.org/wiki/Yen%27s_algorithm) + * - Dijkstra's algorithm for the base shortest path computation.

+ */ +public final class YensKShortestPaths { + + private YensKShortestPaths() { + } + + private static final int NO_EDGE = -1; + private static final long INF_COST = Long.MAX_VALUE / 4; + + /** + * Compute up to k loopless shortest paths from src to dst using Yen's algorithm. + * + * @param weights adjacency matrix; weights[u][v] = -1 means no edge; otherwise non-negative edge weight + * @param src source vertex index + * @param dst destination vertex index + * @param k maximum number of paths to return (k >= 1) + * @return list of paths, each path is a list of vertex indices in order from src to dst + * @throws IllegalArgumentException on invalid inputs (null, non-square, negatives on existing edges, bad indices, k < 1) + */ + public static List> kShortestPaths(int[][] weights, int src, int dst, int k) { + validate(weights, src, dst, k); + final int n = weights.length; + // Make a defensive copy to avoid mutating caller's matrix + int[][] weightsCopy = new int[n][n]; + for (int i = 0; i < n; i++) { + weightsCopy[i] = Arrays.copyOf(weights[i], n); + } + + List shortestPaths = new ArrayList<>(); + PriorityQueue candidates = new PriorityQueue<>(); // min-heap by cost then lexicographic nodes + Set seen = new HashSet<>(); // deduplicate candidate paths by node sequence key + + Path first = dijkstra(weightsCopy, src, dst, new boolean[n]); + if (first == null) { + return List.of(); + } + shortestPaths.add(first); + + for (int kIdx = 1; kIdx < k; kIdx++) { + Path lastPath = shortestPaths.get(kIdx - 1); + List lastNodes = lastPath.nodes; + for (int i = 0; i < lastNodes.size() - 1; i++) { + int spurNode = lastNodes.get(i); + List rootPath = lastNodes.subList(0, i + 1); + + // Build modified graph: remove edges that would recreate same root + next edge as any A path + int[][] modifiedWeights = cloneMatrix(weightsCopy); + + for (Path p : shortestPaths) { + if (startsWith(p.nodes, rootPath) && p.nodes.size() > i + 1) { + int u = p.nodes.get(i); + int v = p.nodes.get(i + 1); + modifiedWeights[u][v] = NO_EDGE; // remove edge + } + } + // Prevent revisiting nodes in rootPath (loopless constraint), except spurNode itself + boolean[] blocked = new boolean[n]; + for (int j = 0; j < rootPath.size() - 1; j++) { + blocked[rootPath.get(j)] = true; + } + + Path spurPath = dijkstra(modifiedWeights, spurNode, dst, blocked); + if (spurPath != null) { + // concatenate rootPath (excluding spurNode at end) + spurPath + List totalNodes = new ArrayList<>(rootPath); + // spurPath.nodes starts with spurNode; avoid duplication + for (int idx = 1; idx < spurPath.nodes.size(); idx++) { + totalNodes.add(spurPath.nodes.get(idx)); + } + long rootCost = pathCost(weightsCopy, rootPath); + long totalCost = rootCost + spurPath.cost; // spurPath.cost covers from spurNode to dst + Path candidate = new Path(totalNodes, totalCost); + String key = candidate.key(); + if (seen.add(key)) { + candidates.add(candidate); + } + } + } + if (candidates.isEmpty()) { + break; + } + shortestPaths.add(candidates.poll()); + } + + // Map to list of node indices for output + List> result = new ArrayList<>(shortestPaths.size()); + for (Path p : shortestPaths) { + result.add(new ArrayList<>(p.nodes)); + } + return result; + } + + private static void validate(int[][] weights, int src, int dst, int k) { + if (weights == null || weights.length == 0) { + throw new IllegalArgumentException("Weights matrix must not be null or empty"); + } + int n = weights.length; + for (int i = 0; i < n; i++) { + if (weights[i] == null || weights[i].length != n) { + throw new IllegalArgumentException("Weights matrix must be square"); + } + for (int j = 0; j < n; j++) { + int val = weights[i][j]; + if (val < NO_EDGE) { + throw new IllegalArgumentException("Weights must be -1 (no edge) or >= 0"); + } + } + } + if (src < 0 || dst < 0 || src >= n || dst >= n) { + throw new IllegalArgumentException("Invalid src/dst indices"); + } + if (k < 1) { + throw new IllegalArgumentException("k must be >= 1"); + } + } + + private static boolean startsWith(List list, List prefix) { + if (prefix.size() > list.size()) { + return false; + } + for (int i = 0; i < prefix.size(); i++) { + if (!Objects.equals(list.get(i), prefix.get(i))) { + return false; + } + } + return true; + } + + private static int[][] cloneMatrix(int[][] a) { + int n = a.length; + int[][] b = new int[n][n]; + for (int i = 0; i < n; i++) { + b[i] = Arrays.copyOf(a[i], n); + } + return b; + } + + private static long pathCost(int[][] weights, List nodes) { + long cost = 0; + for (int i = 0; i + 1 < nodes.size(); i++) { + int u = nodes.get(i); + int v = nodes.get(i + 1); + int edgeCost = weights[u][v]; + if (edgeCost < 0) { + return INF_COST; // invalid + } + cost += edgeCost; + } + return cost; + } + + private static Path dijkstra(int[][] weights, int src, int dst, boolean[] blocked) { + int n = weights.length; + final long inf = INF_COST; + long[] dist = new long[n]; + int[] parent = new int[n]; + Arrays.fill(dist, inf); + Arrays.fill(parent, -1); + PriorityQueue queue = new PriorityQueue<>(); + if (blocked[src]) { + return null; + } + dist[src] = 0; + queue.add(new Node(src, 0)); + while (!queue.isEmpty()) { + Node current = queue.poll(); + if (current.dist != dist[current.u]) { + continue; + } + if (current.u == dst) { + break; + } + for (int v = 0; v < n; v++) { + int edgeWeight = weights[current.u][v]; + if (edgeWeight >= 0 && !blocked[v]) { + long newDist = current.dist + edgeWeight; + if (newDist < dist[v]) { + dist[v] = newDist; + parent[v] = current.u; + queue.add(new Node(v, newDist)); + } + } + } + } + if (dist[dst] >= inf) { + // If src==dst and not blocked, the path is trivial with cost 0 + if (src == dst) { + List nodes = new ArrayList<>(); + nodes.add(src); + return new Path(nodes, 0); + } + return null; + } + // Reconstruct path + List nodes = new ArrayList<>(); + int cur = dst; + while (cur != -1) { + nodes.add(0, cur); + cur = parent[cur]; + } + return new Path(nodes, dist[dst]); + } + + private static final class Node implements Comparable { + final int u; + final long dist; + Node(int u, long dist) { + this.u = u; + this.dist = dist; + } + public int compareTo(Node o) { + return Long.compare(this.dist, o.dist); + } + } + + private static final class Path implements Comparable { + final List nodes; + final long cost; + Path(List nodes, long cost) { + this.nodes = nodes; + this.cost = cost; + } + String key() { + return nodes.toString(); + } + @Override + public int compareTo(Path o) { + int costCmp = Long.compare(this.cost, o.cost); + if (costCmp != 0) { + return costCmp; + } + // tie-break lexicographically on nodes + int minLength = Math.min(this.nodes.size(), o.nodes.size()); + for (int i = 0; i < minLength; i++) { + int aNode = this.nodes.get(i); + int bNode = o.nodes.get(i); + if (aNode != bNode) { + return Integer.compare(aNode, bNode); + } + } + return Integer.compare(this.nodes.size(), o.nodes.size()); + } + } +} diff --git a/src/main/java/com/thealgorithms/graph/ZeroOneBfs.java b/src/main/java/com/thealgorithms/graph/ZeroOneBfs.java new file mode 100644 index 000000000000..53181a654215 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/ZeroOneBfs.java @@ -0,0 +1,77 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +/** + * 0-1 BFS for shortest paths on graphs with edges weighted 0 or 1. + * + *

Time Complexity: O(V + E). Space Complexity: O(V). + * + *

References: + *

    + *
  • https://cp-algorithms.com/graph/01_bfs.html
  • + *
+ */ +public final class ZeroOneBfs { + + private ZeroOneBfs() { + // Utility class; do not instantiate. + } + + /** + * Computes shortest distances from {@code src} in a graph whose edges have weight 0 or 1. + * + * @param n the number of vertices, labeled {@code 0..n-1} + * @param adj adjacency list; for each vertex u, {@code adj.get(u)} is a list of pairs + * {@code (v, w)} where {@code v} is a neighbor and {@code w} is 0 or 1 + * @param src the source vertex + * @return an array of distances; {@code Integer.MAX_VALUE} denotes unreachable + * @throws IllegalArgumentException if {@code n < 0}, {@code src} is out of range, + * or any edge has weight other than 0 or 1 + */ + public static int[] shortestPaths(int n, List> adj, int src) { + if (n < 0 || src < 0 || src >= n) { + throw new IllegalArgumentException("Invalid n or src"); + } + int[] dist = new int[n]; + Arrays.fill(dist, Integer.MAX_VALUE); + Deque dq = new ArrayDeque<>(); + + dist[src] = 0; + dq.addFirst(src); + + while (!dq.isEmpty()) { + int u = dq.pollFirst(); + List edges = adj.get(u); + if (edges == null) { + continue; + } + for (int[] e : edges) { + if (e == null || e.length < 2) { + continue; + } + int v = e[0]; + int w = e[1]; + if (v < 0 || v >= n) { + continue; // ignore bad edges + } + if (w != 0 && w != 1) { + throw new IllegalArgumentException("Edge weight must be 0 or 1"); + } + int nd = dist[u] + w; + if (nd < dist[v]) { + dist[v] = nd; + if (w == 0) { + dq.addFirst(v); + } else { + dq.addLast(v); + } + } + } + } + return dist; + } +} 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 7a06fd5e5fa0..1eba6666dde3 100644 --- a/src/main/java/com/thealgorithms/maths/Area.java +++ b/src/main/java/com/thealgorithms/maths/Area.java @@ -48,6 +48,25 @@ public static double surfaceAreaSphere(final double radius) { return 4 * Math.PI * radius * radius; } + /** + * Calculate the surface area of a pyramid with a square base. + * + * @param sideLength side length of the square base + * @param slantHeight slant height of the pyramid + * @return surface area of the given pyramid + */ + public static double surfaceAreaPyramid(final double sideLength, final double slantHeight) { + if (sideLength <= 0) { + throw new IllegalArgumentException("Must be a positive sideLength"); + } + if (slantHeight <= 0) { + throw new IllegalArgumentException("Must be a positive slantHeight"); + } + double baseArea = sideLength * sideLength; + double lateralSurfaceArea = 2 * sideLength * slantHeight; + return baseArea + lateralSurfaceArea; + } + /** * Calculate the area of a rectangle. * @@ -77,7 +96,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); } diff --git a/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java b/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java index faec049b08a7..1de39adbc18a 100644 --- a/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java +++ b/src/main/java/com/thealgorithms/maths/BinomialCoefficient.java @@ -1,8 +1,8 @@ package com.thealgorithms.maths; /* - * Java program for Binomial Cofficients - * Binomial Cofficients: A binomial cofficient C(n,k) gives number ways + * Java program for Binomial Coefficients + * Binomial Coefficients: A binomial coefficient C(n,k) gives number ways * in which k objects can be chosen from n objects. * Wikipedia: https://en.wikipedia.org/wiki/Binomial_coefficient * diff --git a/src/main/java/com/thealgorithms/maths/ChebyshevIteration.java b/src/main/java/com/thealgorithms/maths/ChebyshevIteration.java new file mode 100644 index 000000000000..bc30f1ba6e7e --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/ChebyshevIteration.java @@ -0,0 +1,181 @@ +package com.thealgorithms.maths; + +/** + * In numerical analysis, Chebyshev iteration is an iterative method for solving + * systems of linear equations Ax = b. It is designed for systems where the + * matrix A is symmetric positive-definite (SPD). + * + *

+ * This method is a "polynomial acceleration" method, meaning it finds the + * optimal polynomial to apply to the residual to accelerate convergence. + * + *

+ * It requires knowledge of the bounds of the eigenvalues of the matrix A: + * m(A) (smallest eigenvalue) and M(A) (largest eigenvalue). + * + *

+ * Wikipedia: https://en.wikipedia.org/wiki/Chebyshev_iteration + * + * @author Mitrajit Ghorui(KeyKyrios) + */ +public final class ChebyshevIteration { + + private ChebyshevIteration() { + } + + /** + * Solves the linear system Ax = b using the Chebyshev iteration method. + * + *

+ * NOTE: The matrix A *must* be symmetric positive-definite (SPD) for this + * algorithm to converge. + * + * @param a The matrix A (must be square, SPD). + * @param b The vector b. + * @param x0 The initial guess vector. + * @param minEigenvalue The smallest eigenvalue of A (m(A)). + * @param maxEigenvalue The largest eigenvalue of A (M(A)). + * @param maxIterations The maximum number of iterations to perform. + * @param tolerance The desired tolerance for the residual norm. + * @return The solution vector x. + * @throws IllegalArgumentException if matrix/vector dimensions are + * incompatible, + * if maxIterations <= 0, or if eigenvalues are invalid (e.g., minEigenvalue + * <= 0, maxEigenvalue <= minEigenvalue). + */ + public static double[] solve(double[][] a, double[] b, double[] x0, double minEigenvalue, double maxEigenvalue, int maxIterations, double tolerance) { + validateInputs(a, b, x0, minEigenvalue, maxEigenvalue, maxIterations, tolerance); + + int n = b.length; + double[] x = x0.clone(); + double[] r = vectorSubtract(b, matrixVectorMultiply(a, x)); + double[] p = new double[n]; + + double d = (maxEigenvalue + minEigenvalue) / 2.0; + double c = (maxEigenvalue - minEigenvalue) / 2.0; + + double alpha = 0.0; + double alphaPrev = 0.0; + + for (int k = 0; k < maxIterations; k++) { + double residualNorm = vectorNorm(r); + if (residualNorm < tolerance) { + return x; // Solution converged + } + + if (k == 0) { + alpha = 1.0 / d; + System.arraycopy(r, 0, p, 0, n); // p = r + } else { + double beta = c * alphaPrev / 2.0 * (c * alphaPrev / 2.0); + alpha = 1.0 / (d - beta / alphaPrev); + double[] pUpdate = scalarMultiply(beta / alphaPrev, p); + p = vectorAdd(r, pUpdate); // p = r + (beta / alphaPrev) * p + } + + double[] xUpdate = scalarMultiply(alpha, p); + x = vectorAdd(x, xUpdate); // x = x + alpha * p + + // Recompute residual for accuracy + r = vectorSubtract(b, matrixVectorMultiply(a, x)); + alphaPrev = alpha; + } + + return x; // Return best guess after maxIterations + } + + /** + * Validates the inputs for the Chebyshev solver. + */ + private static void validateInputs(double[][] a, double[] b, double[] x0, double minEigenvalue, double maxEigenvalue, int maxIterations, double tolerance) { + int n = a.length; + if (n == 0) { + throw new IllegalArgumentException("Matrix A cannot be empty."); + } + if (n != a[0].length) { + throw new IllegalArgumentException("Matrix A must be square."); + } + if (n != b.length) { + throw new IllegalArgumentException("Matrix A and vector b dimensions do not match."); + } + if (n != x0.length) { + throw new IllegalArgumentException("Matrix A and vector x0 dimensions do not match."); + } + if (minEigenvalue <= 0) { + throw new IllegalArgumentException("Smallest eigenvalue must be positive (matrix must be positive-definite)."); + } + if (maxEigenvalue <= minEigenvalue) { + throw new IllegalArgumentException("Max eigenvalue must be strictly greater than min eigenvalue."); + } + if (maxIterations <= 0) { + throw new IllegalArgumentException("Max iterations must be positive."); + } + if (tolerance <= 0) { + throw new IllegalArgumentException("Tolerance must be positive."); + } + } + + // --- Vector/Matrix Helper Methods --- + /** + * Computes the product of a matrix A and a vector v (Av). + */ + private static double[] matrixVectorMultiply(double[][] a, double[] v) { + int n = a.length; + double[] result = new double[n]; + for (int i = 0; i < n; i++) { + double sum = 0; + for (int j = 0; j < n; j++) { + sum += a[i][j] * v[j]; + } + result[i] = sum; + } + return result; + } + + /** + * Computes the subtraction of two vectors (v1 - v2). + */ + private static double[] vectorSubtract(double[] v1, double[] v2) { + int n = v1.length; + double[] result = new double[n]; + for (int i = 0; i < n; i++) { + result[i] = v1[i] - v2[i]; + } + return result; + } + + /** + * Computes the addition of two vectors (v1 + v2). + */ + private static double[] vectorAdd(double[] v1, double[] v2) { + int n = v1.length; + double[] result = new double[n]; + for (int i = 0; i < n; i++) { + result[i] = v1[i] + v2[i]; + } + return result; + } + + /** + * Computes the product of a scalar and a vector (s * v). + */ + private static double[] scalarMultiply(double scalar, double[] v) { + int n = v.length; + double[] result = new double[n]; + for (int i = 0; i < n; i++) { + result[i] = scalar * v[i]; + } + return result; + } + + /** + * Computes the L2 norm (Euclidean norm) of a vector. + */ + private static double vectorNorm(double[] v) { + double sumOfSquares = 0; + for (double val : v) { + sumOfSquares += val * val; + } + return Math.sqrt(sumOfSquares); + } +} diff --git a/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java b/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java new file mode 100644 index 000000000000..9a03f4e21d17 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/EulerPseudoprime.java @@ -0,0 +1,108 @@ +package com.thealgorithms.maths; + +import java.math.BigInteger; +import java.util.Random; + +/** + * The {@code EulerPseudoprime} class implements the Euler primality test. + * + * It is based on Euler’s criterion: + * For an odd prime number {@code n} and any integer {@code a} coprime to {@code n}: + * a^((n-1)/2) ≡ (a/n) (mod n) + * where (a/n) is the Jacobi symbol. + * + * This algorithm is a stronger probabilistic test than Fermat’s test. + * It may still incorrectly identify a composite as “probably prime” (Euler pseudoprime), + * but such cases are rare. + */ +public final class EulerPseudoprime { + + private EulerPseudoprime() { + // Private constructor to prevent instantiation. + } + + private static final Random RANDOM = new Random(1); + + /** + * Performs the Euler primality test for a given number. + * + * @param n number to test (must be > 2 and odd) + * @param trials number of random bases to test + * @return {@code true} if {@code n} passes all Euler tests (probably prime), + * {@code false} if composite. + */ + public static boolean isProbablePrime(BigInteger n, int trials) { + if (n.compareTo(BigInteger.TWO) < 0) { + return false; + } + if (n.equals(BigInteger.TWO) || n.equals(BigInteger.valueOf(3))) { + return true; + } + if (n.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + return false; + } + + for (int i = 0; i < trials; i++) { + BigInteger a = uniformRandom(BigInteger.TWO, n.subtract(BigInteger.TWO)); + BigInteger jacobi = BigInteger.valueOf(jacobiSymbol(a, n)); + if (jacobi.equals(BigInteger.ZERO)) { + return false; + } + + BigInteger exp = n.subtract(BigInteger.ONE).divide(BigInteger.TWO); + BigInteger modExp = a.modPow(exp, n); + + // Euler's criterion: a^((n-1)/2) ≡ (a/n) (mod n) + if (!modExp.equals(jacobi.mod(n))) { + return false; // definitely composite + } + } + return true; // probably prime + } + + /** + * Computes the Jacobi symbol (a/n). + * Assumes n is positive and odd. + */ + public static int jacobiSymbol(BigInteger a, BigInteger n) { + if (n.signum() <= 0 || n.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + throw new IllegalArgumentException("n must be positive and odd."); + } + + int result = 1; + a = a.mod(n); + + while (a.compareTo(BigInteger.ZERO) != 0) { + while (a.mod(BigInteger.TWO).equals(BigInteger.ZERO)) { + a = a.divide(BigInteger.TWO); + BigInteger nMod8 = n.mod(BigInteger.valueOf(8)); + if (nMod8.equals(BigInteger.valueOf(3)) || nMod8.equals(BigInteger.valueOf(5))) { + result = -result; + } + } + + BigInteger temp = a; + a = n; + n = temp; + + if (a.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3)) && n.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3))) { + result = -result; + } + + a = a.mod(n); + } + + return n.equals(BigInteger.ONE) ? result : 0; + } + + /** + * Generates a random BigInteger between {@code min} and {@code max}, inclusive. + */ + private static BigInteger uniformRandom(BigInteger min, BigInteger max) { + BigInteger result; + do { + result = new BigInteger(max.bitLength(), RANDOM); + } while (result.compareTo(min) < 0 || result.compareTo(max) > 0); + return result; + } +} 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..419133702fd4 --- /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 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/GermainPrimeAndSafePrime.java b/src/main/java/com/thealgorithms/maths/GermainPrimeAndSafePrime.java new file mode 100644 index 000000000000..180bbdfe9ac3 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/GermainPrimeAndSafePrime.java @@ -0,0 +1,63 @@ +package com.thealgorithms.maths; + +import com.thealgorithms.maths.Prime.PrimeCheck; + +/** + * A utility class to check whether a number is a Germain prime or a Safe prime. + * + *

This class provides methods to: + *

    + *
  • Check if a number is a Germain prime
  • + *
  • Check if a number is a Safe prime
  • + *
+ * + *

Definitions: + *

    + *
  • A Germain prime is a prime number p such that 2p + 1 is also prime.
  • + *
  • A Safe prime is a prime number p such that (p - 1) / 2 is also prime.
  • + *
+ * + *

This class is final and cannot be instantiated. + * + * @see Wikipedia: Safe and Sophie Germain primes + */ +public final class GermainPrimeAndSafePrime { + + // Private constructor to prevent instantiation + private GermainPrimeAndSafePrime() { + } + + /** + * Checks if a number is a Germain prime. + * + *

A Germain prime is a prime number p such that 2p + 1 is also prime. + * + * @param number the number to check; must be a positive integer + * @return {@code true} if the number is a Germain prime, {@code false} otherwise + * @throws IllegalArgumentException if the input number is less than 1 + */ + public static boolean isGermainPrime(int number) { + if (number < 1) { + throw new IllegalArgumentException("Input value must be a positive integer. Input value: " + number); + } + // A number is a Germain prime if it is prime and 2 * number + 1 is also prime + return PrimeCheck.isPrime(number) && PrimeCheck.isPrime(2 * number + 1); + } + + /** + * Checks if a number is a Safe prime. + * + *

A Safe prime is a prime number p such that (p - 1) / 2 is also prime. + * + * @param number the number to check; must be a positive integer + * @return {@code true} if the number is a Safe prime, {@code false} otherwise + * @throws IllegalArgumentException if the input number is less than 1 + */ + public static boolean isSafePrime(int number) { + if (number < 1) { + throw new IllegalArgumentException("Input value must be a positive integer. Input value: " + number); + } + // A number is a Safe prime if it is prime, (number - 1) is even, and (number - 1) / 2 is prime + return ((number - 1) % 2 == 0) && PrimeCheck.isPrime(number) && PrimeCheck.isPrime((number - 1) / 2); + } +} diff --git a/src/main/java/com/thealgorithms/maths/HappyNumber.java b/src/main/java/com/thealgorithms/maths/HappyNumber.java new file mode 100644 index 000000000000..bfe746953b33 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/HappyNumber.java @@ -0,0 +1,57 @@ +package com.thealgorithms.maths; + +/** + * A Happy Number is defined as a number which eventually reaches 1 when replaced + * by the sum of the squares of each digit. + * If it falls into a cycle that does not include 1, then it is not a happy number. + + * Example: + * 19 → 1² + 9² = 82 + * 82 → 8² + 2² = 68 + * 68 → 6² + 8² = 100 + * 100 → 1² + 0² + 0² = 1 → Happy Number! + */ +public final class HappyNumber { + + private HappyNumber() { + } + + /** + * Checks whether the given number is a Happy Number. + * Uses Floyd’s Cycle Detection algorithm (tortoise and hare method) + * to detect loops efficiently. + * + * @param n The number to check + * @return true if n is a Happy Number, false otherwise + */ + public static boolean isHappy(int n) { + int slow = n; + int fast = n; + + do { + slow = sumOfSquares(slow); // move 1 step + fast = sumOfSquares(sumOfSquares(fast)); // move 2 steps + } while (slow != fast); + + return slow == 1; // If cycle ends in 1 → Happy number + } + + /** + * Calculates the sum of squares of the digits of a number. + * + * Example: + * num = 82 → 8² + 2² = 64 + 4 = 68 + * + * @param num The number to calculate sum of squares of digits + * @return The sum of squares of the digits + */ + private static int sumOfSquares(int num) { + int sum = 0; + while (num > 0) { + int digit = num % 10; + sum += digit * digit; + num /= 10; + } + return sum; + } +} diff --git a/src/main/java/com/thealgorithms/maths/HarshadNumber.java b/src/main/java/com/thealgorithms/maths/HarshadNumber.java index 5792e925a8aa..abe21cb045ae 100644 --- a/src/main/java/com/thealgorithms/maths/HarshadNumber.java +++ b/src/main/java/com/thealgorithms/maths/HarshadNumber.java @@ -1,49 +1,78 @@ package com.thealgorithms.maths; -// Wikipedia for Harshad Number : https://en.wikipedia.org/wiki/Harshad_number - +/** + * A Harshad number (or Niven number) in a given number base is an integer that + * is divisible by the sum of its digits. + * For example, 18 is a Harshad number because 18 is divisible by (1 + 8) = 9. + * The name "Harshad" comes from the Sanskrit words "harṣa" (joy) and "da" + * (give), meaning "joy-giver". + * + * @author Hardvan + * @see Harshad Number - + * Wikipedia + */ public final class HarshadNumber { private HarshadNumber() { } /** - * A function to check if a number is Harshad number or not + * Checks if a number is a Harshad number. + * A Harshad number is a positive integer that is divisible by the sum of its + * digits. * - * @param n The number to be checked - * @return {@code true} if {@code a} is Harshad number, otherwise + * @param n the number to be checked (must be positive) + * @return {@code true} if {@code n} is a Harshad number, otherwise * {@code false} + * @throws IllegalArgumentException if {@code n} is less than or equal to 0 */ public static boolean isHarshad(long n) { if (n <= 0) { - return false; + throw new IllegalArgumentException("Input must be a positive integer. Received: " + n); } - long t = n; + long temp = n; long sumOfDigits = 0; - while (t > 0) { - sumOfDigits += t % 10; - t /= 10; + while (temp > 0) { + sumOfDigits += temp % 10; + temp /= 10; } return n % sumOfDigits == 0; } /** - * A function to check if a number is Harshad number or not + * Checks if a number represented as a string is a Harshad number. + * A Harshad number is a positive integer that is divisible by the sum of its + * digits. * - * @param s The number in String to be checked - * @return {@code true} if {@code a} is Harshad number, otherwise + * @param s the string representation of the number to be checked + * @return {@code true} if the number is a Harshad number, otherwise * {@code false} + * @throws IllegalArgumentException if {@code s} is null, empty, or represents a + * non-positive integer + * @throws NumberFormatException if {@code s} cannot be parsed as a long */ public static boolean isHarshad(String s) { - final Long n = Long.valueOf(s); + if (s == null || s.isEmpty()) { + throw new IllegalArgumentException("Input string cannot be null or empty"); + } + + final long n; + try { + n = Long.parseLong(s); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Input string must be a valid integer: " + s, e); + } + if (n <= 0) { - return false; + throw new IllegalArgumentException("Input must be a positive integer. Received: " + n); } int sumOfDigits = 0; for (char ch : s.toCharArray()) { - sumOfDigits += ch - '0'; + if (Character.isDigit(ch)) { + sumOfDigits += ch - '0'; + } } return n % sumOfDigits == 0; diff --git a/src/main/java/com/thealgorithms/maths/HeronsFormula.java b/src/main/java/com/thealgorithms/maths/HeronsFormula.java index 5baee715d1ec..10438e2888b9 100644 --- a/src/main/java/com/thealgorithms/maths/HeronsFormula.java +++ b/src/main/java/com/thealgorithms/maths/HeronsFormula.java @@ -1,33 +1,76 @@ package com.thealgorithms.maths; /** - * Wikipedia for HeronsFormula => https://en.wikipedia.org/wiki/Heron%27s_formula - * Find the area of a triangle using only side lengths + * Heron's Formula implementation for calculating the area of a triangle given + * its three side lengths. + *

+ * Heron's Formula states that the area of a triangle whose sides have lengths + * a, b, and c is: + * Area = √(s(s - a)(s - b)(s - c)) + * where s is the semi-perimeter of the triangle: s = (a + b + c) / 2 + *

+ * + * @see Heron's + * Formula - Wikipedia + * @author satyabarghav */ - public final class HeronsFormula { - /* - * A function to get the Area of a Triangle using Heron's Formula - * @param s1,s2,s3 => the three sides of the Triangle - * @return area using the formula (√(s(s – s1)(s – s2)(s – s3))) - * here s is called semi-perimeter and it is the half of the perimeter (i.e; s = (s1+s2+s3)/2) - * @author satyabarghav + /** + * Private constructor to prevent instantiation of this utility class. */ private HeronsFormula() { } + /** + * Checks if all three side lengths are positive. + * + * @param a the length of the first side + * @param b the length of the second side + * @param c the length of the third side + * @return true if all sides are positive, false otherwise + */ private static boolean areAllSidesPositive(final double a, final double b, final double c) { return a > 0 && b > 0 && c > 0; } + /** + * Checks if the given side lengths satisfy the triangle inequality theorem. + *

+ * The triangle inequality theorem states that the sum of any two sides + * of a triangle must be greater than the third side. + *

+ * + * @param a the length of the first side + * @param b the length of the second side + * @param c the length of the third side + * @return true if the sides can form a valid triangle, false otherwise + */ private static boolean canFormTriangle(final double a, final double b, final double c) { return a + b > c && b + c > a && c + a > b; } + /** + * Calculates the area of a triangle using Heron's Formula. + *

+ * Given three side lengths a, b, and c, the area is computed as: + * Area = √(s(s - a)(s - b)(s - c)) + * where s is the semi-perimeter: s = (a + b + c) / 2 + *

+ * + * @param a the length of the first side (must be positive) + * @param b the length of the second side (must be positive) + * @param c the length of the third side (must be positive) + * @return the area of the triangle + * @throws IllegalArgumentException if any side length is non-positive or if the + * sides cannot form a valid triangle + */ public static double herons(final double a, final double b, final double c) { - if (!areAllSidesPositive(a, b, c) || !canFormTriangle(a, b, c)) { - throw new IllegalArgumentException("Triangle can't be formed with the given side lengths"); + if (!areAllSidesPositive(a, b, c)) { + throw new IllegalArgumentException("All side lengths must be positive"); + } + if (!canFormTriangle(a, b, c)) { + throw new IllegalArgumentException("Triangle cannot be formed with the given side lengths (violates triangle inequality)"); } final double s = (a + b + c) / 2.0; return Math.sqrt((s) * (s - a) * (s - b) * (s - c)); diff --git a/src/main/java/com/thealgorithms/maths/JugglerSequence.java b/src/main/java/com/thealgorithms/maths/JugglerSequence.java index 702310a1f295..a459b4b6d4bb 100644 --- a/src/main/java/com/thealgorithms/maths/JugglerSequence.java +++ b/src/main/java/com/thealgorithms/maths/JugglerSequence.java @@ -43,7 +43,7 @@ public static void jugglerSequence(int inputNumber) { seq.add(n + ""); } String res = String.join(",", seq); - System.out.println(res); + System.out.print(res + "\n"); } // Driver code diff --git a/src/main/java/com/thealgorithms/maths/KaprekarNumbers.java b/src/main/java/com/thealgorithms/maths/KaprekarNumbers.java index eb9750f9ce08..99842e2f4f5e 100644 --- a/src/main/java/com/thealgorithms/maths/KaprekarNumbers.java +++ b/src/main/java/com/thealgorithms/maths/KaprekarNumbers.java @@ -4,23 +4,51 @@ import java.util.ArrayList; import java.util.List; +/** + * Utility class for identifying and working with Kaprekar Numbers. + *

+ * A Kaprekar number is a positive integer with the following property: + * If you square it, then split the resulting number into two parts (right part + * has same number of + * digits as the original number, left part has the remaining digits), and + * finally add the two + * parts together, you get the original number. + *

+ * For example: + *

    + *
  • 9: 9² = 81 → 8 + 1 = 9 (Kaprekar number)
  • + *
  • 45: 45² = 2025 → 20 + 25 = 45 (Kaprekar number)
  • + *
  • 297: 297² = 88209 → 88 + 209 = 297 (Kaprekar number)
  • + *
+ *

+ * Note: The right part can have leading zeros, but must not be all zeros. + * + * @see Kaprekar Number + * - Wikipedia + * @author TheAlgorithms (https://github.com/TheAlgorithms) + */ public final class KaprekarNumbers { private KaprekarNumbers() { } - /* This program demonstrates if a given number is Kaprekar Number or not. - Kaprekar Number: A Kaprekar number is an n-digit number which its square can be split into - two parts where the right part has n digits and sum of these parts is equal to the original - number. */ - - // Provides a list of kaprekarNumber in a range - public static List kaprekarNumberInRange(long start, long end) throws Exception { - long n = end - start; - if (n < 0) { - throw new Exception("Invalid range"); + /** + * Finds all Kaprekar numbers within a given range (inclusive). + * + * @param start the starting number of the range (inclusive) + * @param end the ending number of the range (inclusive) + * @return a list of all Kaprekar numbers in the specified range + * @throws IllegalArgumentException if start is greater than end or if start is + * negative + */ + public static List kaprekarNumberInRange(long start, long end) { + if (start > end) { + throw new IllegalArgumentException("Start must be less than or equal to end. Given start: " + start + ", end: " + end); + } + if (start < 0) { + throw new IllegalArgumentException("Start must be non-negative. Given start: " + start); } - ArrayList list = new ArrayList<>(); + ArrayList list = new ArrayList<>(); for (long i = start; i <= end; i++) { if (isKaprekarNumber(i)) { list.add(i); @@ -30,24 +58,60 @@ public static List kaprekarNumberInRange(long start, long end) throws Exce return list; } - // Checks whether a given number is Kaprekar Number or not + /** + * Checks whether a given number is a Kaprekar number. + *

+ * The algorithm works as follows: + *

    + *
  1. Square the number
  2. + *
  3. Split the squared number into two parts: left and right
  4. + *
  5. The right part has the same number of digits as the original number
  6. + *
  7. Add the left and right parts
  8. + *
  9. If the sum equals the original number, it's a Kaprekar number
  10. + *
+ *

+ * Special handling is required for numbers whose squares contain zeros. + * + * @param num the number to check + * @return true if the number is a Kaprekar number, false otherwise + * @throws IllegalArgumentException if num is negative + */ public static boolean isKaprekarNumber(long num) { + if (num < 0) { + throw new IllegalArgumentException("Number must be non-negative. Given: " + num); + } + + if (num == 0 || num == 1) { + return true; + } + String number = Long.toString(num); BigInteger originalNumber = BigInteger.valueOf(num); BigInteger numberSquared = originalNumber.multiply(originalNumber); - if (number.length() == numberSquared.toString().length()) { - return number.equals(numberSquared.toString()); - } else { - BigInteger leftDigits1 = BigInteger.ZERO; - BigInteger leftDigits2; - if (numberSquared.toString().contains("0")) { - leftDigits1 = new BigInteger(numberSquared.toString().substring(0, numberSquared.toString().indexOf("0"))); - } - leftDigits2 = new BigInteger(numberSquared.toString().substring(0, (numberSquared.toString().length() - number.length()))); - BigInteger rightDigits = new BigInteger(numberSquared.toString().substring(numberSquared.toString().length() - number.length())); - String x = leftDigits1.add(rightDigits).toString(); - String y = leftDigits2.add(rightDigits).toString(); - return (number.equals(x)) || (number.equals(y)); + String squaredStr = numberSquared.toString(); + + // Special case: if the squared number has the same length as the original + if (number.length() == squaredStr.length()) { + return number.equals(squaredStr); + } + + // Calculate the split position + int splitPos = squaredStr.length() - number.length(); + + // Split the squared number into left and right parts + String leftPart = squaredStr.substring(0, splitPos); + String rightPart = squaredStr.substring(splitPos); + + // Parse the parts as BigInteger (handles empty left part as zero) + BigInteger leftNum = leftPart.isEmpty() ? BigInteger.ZERO : new BigInteger(leftPart); + BigInteger rightNum = new BigInteger(rightPart); + + // Check if right part is all zeros (invalid for Kaprekar numbers except 1) + if (rightNum.equals(BigInteger.ZERO)) { + return false; } + + // Check if the sum equals the original number + return leftNum.add(rightNum).equals(originalNumber); } } diff --git a/src/main/java/com/thealgorithms/maths/KeithNumber.java b/src/main/java/com/thealgorithms/maths/KeithNumber.java index 1756cfbae91b..eb7f800d378f 100644 --- a/src/main/java/com/thealgorithms/maths/KeithNumber.java +++ b/src/main/java/com/thealgorithms/maths/KeithNumber.java @@ -4,57 +4,98 @@ import java.util.Collections; import java.util.Scanner; -final class KeithNumber { +/** + * A Keith number is an n-digit positive integer where the sequence formed by + * starting with its digits and repeatedly adding the previous n terms, + * eventually reaches the number itself. + * + *

+ * For example: + *

    + *
  • 14 is a Keith number: 1, 4, 5, 9, 14
  • + *
  • 19 is a Keith number: 1, 9, 10, 19
  • + *
  • 28 is a Keith number: 2, 8, 10, 18, 28
  • + *
  • 197 is a Keith number: 1, 9, 7, 17, 33, 57, 107, 197
  • + *
+ * + * @see Keith Number - + * Wikipedia + * @see Keith Number - + * MathWorld + */ +public final class KeithNumber { private KeithNumber() { } - // user-defined function that checks if the given number is Keith or not - static boolean isKeith(int x) { - // List stores all the digits of the X + /** + * Checks if a given number is a Keith number. + * + *

+ * The algorithm works as follows: + *

    + *
  1. Extract all digits of the number and store them in a list
  2. + *
  3. Generate subsequent terms by summing the last n digits
  4. + *
  5. Continue until a term equals or exceeds the original number
  6. + *
  7. If a term equals the number, it is a Keith number
  8. + *
+ * + * @param number the number to check (must be positive) + * @return {@code true} if the number is a Keith number, {@code false} otherwise + * @throws IllegalArgumentException if the number is not positive + */ + public static boolean isKeith(int number) { + if (number <= 0) { + throw new IllegalArgumentException("Number must be positive"); + } + + // Extract digits and store them in the list ArrayList terms = new ArrayList<>(); - // n denotes the number of digits - int temp = x; - int n = 0; - // executes until the condition becomes false + int temp = number; + int digitCount = 0; + while (temp > 0) { - // determines the last digit of the number and add it to the List terms.add(temp % 10); - // removes the last digit temp = temp / 10; - // increments the number of digits (n) by 1 - n++; + digitCount++; } - // reverse the List + + // Reverse the list to get digits in correct order Collections.reverse(terms); + + // Generate subsequent terms in the sequence int nextTerm = 0; - int i = n; - // finds next term for the series - // loop executes until the condition returns true - while (nextTerm < x) { + int currentIndex = digitCount; + + while (nextTerm < number) { nextTerm = 0; - // next term is the sum of previous n terms (it depends on number of digits the number - // has) - for (int j = 1; j <= n; j++) { - nextTerm = nextTerm + terms.get(i - j); + // Sum the last 'digitCount' terms + for (int j = 1; j <= digitCount; j++) { + nextTerm = nextTerm + terms.get(currentIndex - j); } terms.add(nextTerm); - i++; + currentIndex++; } - // when the control comes out of the while loop, there will be two conditions: - // either nextTerm will be equal to x or greater than x - // if equal, the given number is Keith, else not - return (nextTerm == x); + + // Check if the generated term equals the original number + return (nextTerm == number); } - // driver code + /** + * Main method for demonstrating Keith number detection. + * Reads a number from standard input and checks if it is a Keith number. + * + * @param args command line arguments (not used) + */ public static void main(String[] args) { - Scanner in = new Scanner(System.in); - int n = in.nextInt(); - if (isKeith(n)) { - System.out.println("Yes, the given number is a Keith number."); + Scanner scanner = new Scanner(System.in); + System.out.print("Enter a positive integer: "); + int number = scanner.nextInt(); + + if (isKeith(number)) { + System.out.println("Yes, " + number + " is a Keith number."); } else { - System.out.println("No, the given number is not a Keith number."); + System.out.println("No, " + number + " is not a Keith number."); } - in.close(); + scanner.close(); } } diff --git a/src/main/java/com/thealgorithms/maths/KrishnamurthyNumber.java b/src/main/java/com/thealgorithms/maths/KrishnamurthyNumber.java index 03f18aca786f..a00ef7e3b15d 100644 --- a/src/main/java/com/thealgorithms/maths/KrishnamurthyNumber.java +++ b/src/main/java/com/thealgorithms/maths/KrishnamurthyNumber.java @@ -3,10 +3,25 @@ /** * Utility class for checking if a number is a Krishnamurthy number. * - * A Krishnamurthy number (also known as a Strong number) is a number whose sum of the factorials of its digits is equal to the number itself. + *

+ * A Krishnamurthy number (also known as a Strong number or Factorion) is a + * number + * whose sum of the factorials of its digits is equal to the number itself. + *

* - * For example, 145 is a Krishnamurthy number because 1! + 4! + 5! = 1 + 24 + 120 = 145. + *

+ * For example, 145 is a Krishnamurthy number because 1! + 4! + 5! = 1 + 24 + + * 120 = 145. + *

+ * + *

+ * The only Krishnamurthy numbers in base 10 are: 1, 2, 145, and 40585. + *

+ * + *

* Example usage: + *

+ * *
  * boolean isKrishnamurthy = KrishnamurthyNumber.isKrishnamurthy(145);
  * System.out.println(isKrishnamurthy); // Output: true
@@ -14,40 +29,43 @@
  * isKrishnamurthy = KrishnamurthyNumber.isKrishnamurthy(123);
  * System.out.println(isKrishnamurthy); // Output: false
  * 
+ * + * @see Factorion + * (Wikipedia) */ public final class KrishnamurthyNumber { + // Pre-computed factorials for digits 0-9 to improve performance + private static final int[] FACTORIALS = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}; + private KrishnamurthyNumber() { } /** * Checks if a number is a Krishnamurthy number. * - * @param n The number to check + *

+ * A number is a Krishnamurthy number if the sum of the factorials of its digits + * equals the number itself. + *

+ * + * @param n the number to check * @return true if the number is a Krishnamurthy number, false otherwise */ public static boolean isKrishnamurthy(int n) { - int tmp = n; - int s = 0; - if (n <= 0) { return false; - } else { - while (n != 0) { - // initialising the variable fact that will store the factorials of the digits - int fact = 1; - // computing factorial of each digit - for (int i = 1; i <= n % 10; i++) { - fact = fact * i; - } - // computing the sum of the factorials - s = s + fact; - // discarding the digit for which factorial has been calculated - n = n / 10; - } + } - // evaluating if sum of the factorials of the digits equals the number itself - return tmp == s; + int original = n; + int sum = 0; + + while (n != 0) { + int digit = n % 10; + sum = sum + FACTORIALS[digit]; + n = n / 10; } + + return sum == original; } } diff --git a/src/main/java/com/thealgorithms/maths/LeonardoNumber.java b/src/main/java/com/thealgorithms/maths/LeonardoNumber.java index bbeec052777f..9ac3691a6285 100644 --- a/src/main/java/com/thealgorithms/maths/LeonardoNumber.java +++ b/src/main/java/com/thealgorithms/maths/LeonardoNumber.java @@ -1,25 +1,79 @@ package com.thealgorithms.maths; /** - * https://en.wikipedia.org/wiki/Leonardo_number + * Utility class for calculating Leonardo Numbers. + *

+ * Leonardo numbers are a sequence of numbers defined by the recurrence: + * L(n) = L(n-1) + L(n-2) + 1, with L(0) = 1 and L(1) = 1 + *

+ * The sequence begins: 1, 1, 3, 5, 9, 15, 25, 41, 67, 109, 177, ... + *

+ * This class provides both a recursive implementation and an optimized + * iterative + * implementation for calculating Leonardo numbers. + * + * @see Leonardo Number + * - Wikipedia + * @see OEIS A001595 */ public final class LeonardoNumber { private LeonardoNumber() { } /** - * Calculate nth Leonardo Number (1, 1, 3, 5, 9, 15, 25, 41, 67, 109, 177, ...) + * Calculates the nth Leonardo Number using recursion. + *

+ * Time Complexity: O(2^n) - exponential due to repeated calculations + * Space Complexity: O(n) - due to recursion stack + *

+ * Note: This method is not recommended for large values of n due to exponential + * time complexity. + * Consider using {@link #leonardoNumberIterative(int)} for better performance. * - * @param n the index of Leonardo Number to calculate - * @return nth number of Leonardo sequences + * @param n the index of the Leonardo Number to calculate (must be non-negative) + * @return the nth Leonardo Number + * @throws IllegalArgumentException if n is negative */ public static int leonardoNumber(int n) { if (n < 0) { - throw new ArithmeticException(); + throw new IllegalArgumentException("Input must be non-negative. Received: " + n); } if (n == 0 || n == 1) { return 1; } - return (leonardoNumber(n - 1) + leonardoNumber(n - 2) + 1); + return leonardoNumber(n - 1) + leonardoNumber(n - 2) + 1; + } + + /** + * Calculates the nth Leonardo Number using an iterative approach. + *

+ * This method provides better performance than the recursive version for large + * values of n. + *

+ * Time Complexity: O(n) + * Space Complexity: O(1) + * + * @param n the index of the Leonardo Number to calculate (must be non-negative) + * @return the nth Leonardo Number + * @throws IllegalArgumentException if n is negative + */ + public static int leonardoNumberIterative(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative. Received: " + n); + } + if (n == 0 || n == 1) { + return 1; + } + + int previous = 1; + int current = 1; + + for (int i = 2; i <= n; i++) { + int next = current + previous + 1; + previous = current; + current = next; + } + + return current; } } diff --git a/src/main/java/com/thealgorithms/maths/LinearDiophantineEquationsSolver.java b/src/main/java/com/thealgorithms/maths/LinearDiophantineEquationsSolver.java index a50cfb218283..a95e7053e210 100644 --- a/src/main/java/com/thealgorithms/maths/LinearDiophantineEquationsSolver.java +++ b/src/main/java/com/thealgorithms/maths/LinearDiophantineEquationsSolver.java @@ -2,20 +2,77 @@ import java.util.Objects; +/** + * A solver for linear Diophantine equations of the form ax + by = c. + *

+ * A linear Diophantine equation is an equation in which only integer solutions + * are allowed. + * This solver uses the Extended Euclidean Algorithm to find integer solutions + * (x, y) + * for equations of the form ax + by = c, where a, b, and c are integers. + *

+ *

+ * The equation has solutions if and only if gcd(a, b) divides c. + * If solutions exist, this solver finds one particular solution. + *

+ * + * @see Diophantine + * Equation + * @see Extended + * Euclidean Algorithm + */ public final class LinearDiophantineEquationsSolver { private LinearDiophantineEquationsSolver() { } + /** + * Demonstrates the solver with a sample equation: 3x + 4y = 7. + * + * @param args command line arguments (not used) + */ public static void main(String[] args) { // 3x + 4y = 7 final var toSolve = new Equation(3, 4, 7); System.out.println(findAnySolution(toSolve)); } + /** + * Finds any integer solution to the linear Diophantine equation ax + by = c. + *

+ * The method returns one of three types of solutions: + *

    + *
  • A specific solution (x, y) if solutions exist
  • + *
  • {@link Solution#NO_SOLUTION} if no integer solutions exist
  • + *
  • {@link Solution#INFINITE_SOLUTIONS} if the equation is 0x + 0y = 0
  • + *
+ *

+ * + * @param equation the linear Diophantine equation to solve + * @return a Solution object containing the result + * @throws NullPointerException if equation is null + */ public static Solution findAnySolution(final Equation equation) { if (equation.a() == 0 && equation.b() == 0 && equation.c() == 0) { return Solution.INFINITE_SOLUTIONS; } + if (equation.a() == 0 && equation.b() == 0) { + return Solution.NO_SOLUTION; + } + if (equation.a() == 0) { + if (equation.c() % equation.b() == 0) { + return new Solution(0, equation.c() / equation.b()); + } else { + return Solution.NO_SOLUTION; + } + } + if (equation.b() == 0) { + if (equation.c() % equation.a() == 0) { + return new Solution(equation.c() / equation.a(), 0); + } else { + return Solution.NO_SOLUTION; + } + } final var stub = new GcdSolutionWrapper(0, new Solution(0, 0)); final var gcdSolution = gcd(equation.a(), equation.b(), stub); if (equation.c() % gcdSolution.getGcd() != 0) { @@ -29,43 +86,100 @@ public static Solution findAnySolution(final Equation equation) { return toReturn; } + /** + * Computes the GCD of two integers using the Extended Euclidean Algorithm. + *

+ * This method also finds coefficients x and y such that ax + by = gcd(a, b). + * The coefficients are stored in the 'previous' wrapper object. + *

+ * + * @param a the first integer + * @param b the second integer + * @param previous a wrapper to store the solution coefficients + * @return a GcdSolutionWrapper containing the GCD and coefficients + */ private static GcdSolutionWrapper gcd(final int a, final int b, final GcdSolutionWrapper previous) { if (b == 0) { return new GcdSolutionWrapper(a, new Solution(1, 0)); } // stub wrapper becomes the `previous` of the next recursive call final var stubWrapper = new GcdSolutionWrapper(0, new Solution(0, 0)); - final var next = /* recursive call */ gcd(b, a % b, stubWrapper); + final var next = gcd(b, a % b, stubWrapper); previous.getSolution().setX(next.getSolution().getY()); previous.getSolution().setY(next.getSolution().getX() - (a / b) * (next.getSolution().getY())); previous.setGcd(next.getGcd()); return new GcdSolutionWrapper(next.getGcd(), previous.getSolution()); } + /** + * Represents a solution (x, y) to a linear Diophantine equation. + *

+ * Special instances: + *

    + *
  • {@link #NO_SOLUTION} - indicates no integer solutions exist
  • + *
  • {@link #INFINITE_SOLUTIONS} - indicates infinitely many solutions + * exist
  • + *
+ *

+ */ public static final class Solution { + /** + * Singleton instance representing the case where no solution exists. + */ public static final Solution NO_SOLUTION = new Solution(Integer.MAX_VALUE, Integer.MAX_VALUE); + + /** + * Singleton instance representing the case where infinite solutions exist. + */ public static final Solution INFINITE_SOLUTIONS = new Solution(Integer.MIN_VALUE, Integer.MIN_VALUE); + private int x; private int y; + /** + * Constructs a solution with the given x and y values. + * + * @param x the x coordinate of the solution + * @param y the y coordinate of the solution + */ public Solution(int x, int y) { this.x = x; this.y = y; } + /** + * Gets the x value of this solution. + * + * @return the x value + */ public int getX() { return x; } + /** + * Gets the y value of this solution. + * + * @return the y value + */ public int getY() { return y; } + /** + * Sets the x value of this solution. + * + * @param x the new x value + */ public void setX(int x) { this.x = x; } + /** + * Sets the y value of this solution. + * + * @param y the new y value + */ public void setY(int y) { this.y = y; } @@ -95,14 +209,35 @@ public String toString() { } } + /** + * Represents a linear Diophantine equation of the form ax + by = c. + * + * @param a the coefficient of x + * @param b the coefficient of y + * @param c the constant term + */ public record Equation(int a, int b, int c) { } + /** + * A wrapper class that holds both the GCD and the solution coefficients + * from the Extended Euclidean Algorithm. + *

+ * This class is used internally to pass results between recursive calls + * of the GCD computation. + *

+ */ public static final class GcdSolutionWrapper { private int gcd; private Solution solution; + /** + * Constructs a GcdSolutionWrapper with the given GCD and solution. + * + * @param gcd the greatest common divisor + * @param solution the solution coefficients + */ public GcdSolutionWrapper(int gcd, Solution solution) { this.gcd = gcd; this.solution = solution; @@ -120,18 +255,38 @@ public boolean equals(Object obj) { return (this.gcd == that.gcd && Objects.equals(this.solution, that.solution)); } + /** + * Gets the GCD value. + * + * @return the GCD + */ public int getGcd() { return gcd; } + /** + * Sets the GCD value. + * + * @param gcd the new GCD value + */ public void setGcd(int gcd) { this.gcd = gcd; } + /** + * Gets the solution coefficients. + * + * @return the solution + */ public Solution getSolution() { return solution; } + /** + * Sets the solution coefficients. + * + * @param solution the new solution + */ public void setSolution(Solution solution) { this.solution = solution; } diff --git a/src/main/java/com/thealgorithms/maths/LucasSeries.java b/src/main/java/com/thealgorithms/maths/LucasSeries.java index e277c511f317..90a35f0d6259 100644 --- a/src/main/java/com/thealgorithms/maths/LucasSeries.java +++ b/src/main/java/com/thealgorithms/maths/LucasSeries.java @@ -1,38 +1,69 @@ package com.thealgorithms.maths; /** - * https://en.wikipedia.org/wiki/Lucas_number + * Utility class for calculating Lucas numbers. + * The Lucas sequence is similar to the Fibonacci sequence but starts with 2 and + * 1. + * The sequence follows: L(n) = L(n-1) + L(n-2) + * Starting values: L(1) = 2, L(2) = 1 + * Sequence: 2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123, ... + * + * @see Lucas Number + * @author TheAlgorithms Contributors */ public final class LucasSeries { private LucasSeries() { } /** - * Calculate nth number of Lucas Series(2, 1, 3, 4, 7, 11, 18, 29, 47, 76, - * 123, ....) using recursion + * Calculate the nth Lucas number using recursion. + * Time Complexity: O(2^n) - exponential due to recursive calls + * Space Complexity: O(n) - recursion depth * - * @param n nth - * @return nth number of Lucas Series + * @param n the position in the Lucas sequence (1-indexed, must be positive) + * @return the nth Lucas number + * @throws IllegalArgumentException if n is less than 1 */ public static int lucasSeries(int n) { - return n == 1 ? 2 : n == 2 ? 1 : lucasSeries(n - 1) + lucasSeries(n - 2); + if (n < 1) { + throw new IllegalArgumentException("Input must be a positive integer. Provided: " + n); + } + if (n == 1) { + return 2; + } + if (n == 2) { + return 1; + } + return lucasSeries(n - 1) + lucasSeries(n - 2); } /** - * Calculate nth number of Lucas Series(2, 1, 3, 4, 7, 11, 18, 29, 47, 76, - * 123, ....) using iteration + * Calculate the nth Lucas number using iteration. + * Time Complexity: O(n) - single loop through n iterations + * Space Complexity: O(1) - constant space usage * - * @param n nth - * @return nth number of lucas series + * @param n the position in the Lucas sequence (1-indexed, must be positive) + * @return the nth Lucas number + * @throws IllegalArgumentException if n is less than 1 */ public static int lucasSeriesIteration(int n) { + if (n < 1) { + throw new IllegalArgumentException("Input must be a positive integer. Provided: " + n); + } + if (n == 1) { + return 2; + } + if (n == 2) { + return 1; + } + int previous = 2; int current = 1; - for (int i = 1; i < n; i++) { + for (int i = 2; i < n; i++) { int next = previous + current; previous = current; current = next; } - return previous; + return current; } } 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/Means.java b/src/main/java/com/thealgorithms/maths/Means.java index dccc820b172e..5445a3caebc7 100644 --- a/src/main/java/com/thealgorithms/maths/Means.java +++ b/src/main/java/com/thealgorithms/maths/Means.java @@ -4,9 +4,27 @@ import org.apache.commons.collections4.IterableUtils; /** - * https://en.wikipedia.org/wiki/Mean + * Utility class for computing various types of statistical means. *

- * by: Punit Patel + * This class provides static methods to calculate different types of means + * (averages) + * from a collection of numbers. All methods accept any {@link Iterable} + * collection of + * {@link Double} values and return the computed mean as a {@link Double}. + *

+ * + *

+ * Supported means: + *

    + *
  • Arithmetic Mean: The sum of all values divided by the count
  • + *
  • Geometric Mean: The nth root of the product of n values
  • + *
  • Harmonic Mean: The reciprocal of the arithmetic mean of + * reciprocals
  • + *
+ *

+ * + * @see Mean (Wikipedia) + * @author Punit Patel */ public final class Means { @@ -14,41 +32,90 @@ private Means() { } /** - * @brief computes the [Arithmetic Mean](https://en.wikipedia.org/wiki/Arithmetic_mean) of the input - * @param numbers the input numbers - * @throws IllegalArgumentException empty input + * Computes the arithmetic mean (average) of the given numbers. + *

+ * The arithmetic mean is calculated as: (x₁ + x₂ + ... + xₙ) / n + *

+ *

+ * Example: For numbers [2, 4, 6], the arithmetic mean is (2+4+6)/3 = 4.0 + *

+ * + * @param numbers the input numbers (must not be empty) * @return the arithmetic mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Arithmetic + * Mean */ public static Double arithmetic(final Iterable numbers) { checkIfNotEmpty(numbers); - return StreamSupport.stream(numbers.spliterator(), false).reduce((x, y) -> x + y).get() / IterableUtils.size(numbers); + double sum = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + y); + int size = IterableUtils.size(numbers); + return sum / size; } /** - * @brief computes the [Geometric Mean](https://en.wikipedia.org/wiki/Geometric_mean) of the input - * @param numbers the input numbers - * @throws IllegalArgumentException empty input + * Computes the geometric mean of the given numbers. + *

+ * The geometric mean is calculated as: ⁿ√(x₁ × x₂ × ... × xₙ) + *

+ *

+ * Example: For numbers [2, 8], the geometric mean is √(2×8) = √16 = 4.0 + *

+ *

+ * Note: This method may produce unexpected results for negative numbers, + * as it computes the real-valued nth root which may not exist for negative + * products. + *

+ * + * @param numbers the input numbers (must not be empty) * @return the geometric mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Geometric + * Mean */ public static Double geometric(final Iterable numbers) { checkIfNotEmpty(numbers); - return Math.pow(StreamSupport.stream(numbers.spliterator(), false).reduce((x, y) -> x * y).get(), 1d / IterableUtils.size(numbers)); + double product = StreamSupport.stream(numbers.spliterator(), false).reduce(1d, (x, y) -> x * y); + int size = IterableUtils.size(numbers); + return Math.pow(product, 1.0 / size); } /** - * @brief computes the [Harmonic Mean](https://en.wikipedia.org/wiki/Harmonic_mean) of the input - * @param numbers the input numbers - * @throws IllegalArgumentException empty input + * Computes the harmonic mean of the given numbers. + *

+ * The harmonic mean is calculated as: n / (1/x₁ + 1/x₂ + ... + 1/xₙ) + *

+ *

+ * Example: For numbers [1, 2, 4], the harmonic mean is 3/(1/1 + 1/2 + 1/4) = + * 3/1.75 ≈ 1.714 + *

+ *

+ * Note: This method will produce unexpected results if any input number is + * zero, + * as it involves computing reciprocals. + *

+ * + * @param numbers the input numbers (must not be empty) * @return the harmonic mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Harmonic Mean */ public static Double harmonic(final Iterable numbers) { checkIfNotEmpty(numbers); - return IterableUtils.size(numbers) / StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + 1d / y); + double sumOfReciprocals = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + 1d / y); + int size = IterableUtils.size(numbers); + return size / sumOfReciprocals; } + /** + * Validates that the input iterable is not empty. + * + * @param numbers the input numbers to validate + * @throws IllegalArgumentException if the input is empty + */ private static void checkIfNotEmpty(final Iterable numbers) { if (!numbers.iterator().hasNext()) { - throw new IllegalArgumentException("Emtpy list given for Mean computation."); + throw new IllegalArgumentException("Empty list given for Mean computation."); } } } diff --git a/src/main/java/com/thealgorithms/maths/Median.java b/src/main/java/com/thealgorithms/maths/Median.java index fd2aab84e4e9..5e1a4bc3a8d9 100644 --- a/src/main/java/com/thealgorithms/maths/Median.java +++ b/src/main/java/com/thealgorithms/maths/Median.java @@ -3,17 +3,35 @@ import java.util.Arrays; /** - * Wikipedia: https://en.wikipedia.org/wiki/Median + * Utility class for calculating the median of an array of integers. + * The median is the middle value in a sorted list of numbers. If the list has + * an even number + * of elements, the median is the average of the two middle values. + * + *

+ * Time Complexity: O(n log n) due to sorting + *

+ * Space Complexity: O(1) if sorting is done in-place + * + * @see Median (Wikipedia) + * @see Mean, + * Median, and Mode Review */ public final class Median { private Median() { } /** - * Calculate average median - * @param values sorted numbers to find median of - * @return median of given {@code values} - * @throws IllegalArgumentException If the input array is empty or null. + * Calculates the median of an array of integers. + * The array is sorted internally, so the original order is not preserved. + * For arrays with an odd number of elements, returns the middle element. + * For arrays with an even number of elements, returns the average of the two + * middle elements. + * + * @param values the array of integers to find the median of (can be unsorted) + * @return the median value as a double + * @throws IllegalArgumentException if the input array is empty or null */ public static double median(int[] values) { if (values == null || values.length == 0) { @@ -22,6 +40,10 @@ public static double median(int[] values) { Arrays.sort(values); int length = values.length; - return length % 2 == 0 ? (values[length / 2] + values[length / 2 - 1]) / 2.0 : values[length / 2]; + if (length % 2 == 0) { + return (values[length / 2] + values[length / 2 - 1]) / 2.0; + } else { + return values[length / 2]; + } } } diff --git a/src/main/java/com/thealgorithms/maths/Neville.java b/src/main/java/com/thealgorithms/maths/Neville.java new file mode 100644 index 000000000000..ca45f2e8a042 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/Neville.java @@ -0,0 +1,61 @@ +package com.thealgorithms.maths; + +import java.util.HashSet; +import java.util.Set; + +/** + * In numerical analysis, Neville's algorithm is an algorithm used for + * polynomial interpolation. Given n+1 points, there is a unique polynomial of + * degree at most n that passes through all the points. Neville's algorithm + * computes the value of this polynomial at a given point. + * + *

+ * Wikipedia: https://en.wikipedia.org/wiki/Neville%27s_algorithm + * + * @author Mitrajit Ghorui(KeyKyrios) + */ +public final class Neville { + + private Neville() { + } + + /** + * Evaluates the polynomial that passes through the given points at a + * specific x-coordinate. + * + * @param x The x-coordinates of the points. Must be the same length as y. + * @param y The y-coordinates of the points. Must be the same length as x. + * @param target The x-coordinate at which to evaluate the polynomial. + * @return The interpolated y-value at the target x-coordinate. + * @throws IllegalArgumentException if the lengths of x and y arrays are + * different, if the arrays are empty, or if x-coordinates are not unique. + */ + public static double interpolate(double[] x, double[] y, double target) { + if (x.length != y.length) { + throw new IllegalArgumentException("x and y arrays must have the same length."); + } + if (x.length == 0) { + throw new IllegalArgumentException("Input arrays cannot be empty."); + } + + // Check for duplicate x-coordinates to prevent division by zero + Set seenX = new HashSet<>(); + for (double val : x) { + if (!seenX.add(val)) { + throw new IllegalArgumentException("Input x-coordinates must be unique."); + } + } + + int n = x.length; + double[] p = new double[n]; + System.arraycopy(y, 0, p, 0, n); // Initialize p with y values + + for (int k = 1; k < n; k++) { + for (int i = 0; i < n - k; i++) { + p[i] = ((target - x[i + k]) * p[i] + (x[i] - target) * p[i + 1]) / (x[i] - x[i + k]); + } + } + + return p[0]; + } +} diff --git a/src/main/java/com/thealgorithms/maths/NumberPersistence.java b/src/main/java/com/thealgorithms/maths/NumberPersistence.java new file mode 100644 index 000000000000..9cc6667dca02 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/NumberPersistence.java @@ -0,0 +1,79 @@ +package com.thealgorithms.maths; + +/** + * A utility class for calculating the persistence of a number. + * + *

This class provides methods to calculate: + *

    + *
  • Multiplicative persistence: The number of steps required to reduce a number to a single digit by multiplying its digits.
  • + *
  • Additive persistence: The number of steps required to reduce a number to a single digit by summing its digits.
  • + *
+ * + *

This class is final and cannot be instantiated. + * + * @see Wikipedia: Persistence of a number + */ +public final class NumberPersistence { + + // Private constructor to prevent instantiation + private NumberPersistence() { + } + + /** + * Calculates the multiplicative persistence of a given number. + * + *

Multiplicative persistence is the number of steps required to reduce a number to a single digit + * by multiplying its digits repeatedly. + * + * @param num the number to calculate persistence for; must be non-negative + * @return the multiplicative persistence of the number + * @throws IllegalArgumentException if the input number is negative + */ + public static int multiplicativePersistence(int num) { + if (num < 0) { + throw new IllegalArgumentException("multiplicativePersistence() does not accept negative values"); + } + + int steps = 0; + while (num >= 10) { + int product = 1; + int temp = num; + while (temp > 0) { + product *= temp % 10; + temp /= 10; + } + num = product; + steps++; + } + return steps; + } + + /** + * Calculates the additive persistence of a given number. + * + *

Additive persistence is the number of steps required to reduce a number to a single digit + * by summing its digits repeatedly. + * + * @param num the number to calculate persistence for; must be non-negative + * @return the additive persistence of the number + * @throws IllegalArgumentException if the input number is negative + */ + public static int additivePersistence(int num) { + if (num < 0) { + throw new IllegalArgumentException("additivePersistence() does not accept negative values"); + } + + int steps = 0; + while (num >= 10) { + int sum = 0; + int temp = num; + while (temp > 0) { + sum += temp % 10; + temp /= 10; + } + num = sum; + steps++; + } + return steps; + } +} 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/PiApproximation.java b/src/main/java/com/thealgorithms/maths/PiApproximation.java new file mode 100644 index 000000000000..1be945d7ef24 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PiApproximation.java @@ -0,0 +1,74 @@ +package com.thealgorithms.maths; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Implementation to calculate an estimate of the number π (Pi). + * + * We take a random point P with coordinates (x, y) such that 0 ≤ x ≤ 1 and 0 ≤ y ≤ 1. + * If x² + y² ≤ 1, then the point is inside the quarter disk of radius 1, + * else the point is outside. We know that the probability of the point being + * inside the quarter disk is equal to π/4. + * + * + * @author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class PiApproximation { + + private PiApproximation() { + throw new AssertionError("No instances."); + } + + /** + * Structure representing a point with coordinates (x, y) + * where 0 ≤ x ≤ 1 and 0 ≤ y ≤ 1. + */ + static class Point { + double x; + double y; + + Point(double x, double y) { + this.x = x; + this.y = y; + } + } + + /** + * This function uses the points in a given list (drawn at random) + * to return an approximation of the number π. + * + * @param pts List of points where each point contains x and y coordinates + * @return An estimate of the number π + */ + public static double approximatePi(List pts) { + double count = 0; // Points in circle + + for (Point p : pts) { + if ((p.x * p.x) + (p.y * p.y) <= 1) { + count++; + } + } + + return 4.0 * count / pts.size(); + } + + /** + * Generates random points for testing the Pi approximation. + * + * @param numPoints Number of random points to generate + * @return List of random points + */ + public static List generateRandomPoints(int numPoints) { + List points = new ArrayList<>(); + Random rand = new Random(); + + for (int i = 0; i < numPoints; i++) { + double x = rand.nextDouble(); // Random value between 0 and 1 + double y = rand.nextDouble(); // Random value between 0 and 1 + points.add(new Point(x, y)); + } + + return points; + } +} 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/SieveOfAtkin.java b/src/main/java/com/thealgorithms/maths/SieveOfAtkin.java new file mode 100644 index 000000000000..780dd81dac7c --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SieveOfAtkin.java @@ -0,0 +1,143 @@ +package com.thealgorithms.maths; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of the Sieve of Atkin, an optimized algorithm to generate + * all prime numbers up to a given limit. + * + * The Sieve of Atkin uses quadratic forms and modular arithmetic to identify + * prime candidates, then eliminates multiples of squares. It is more efficient + * than the Sieve of Eratosthenes for large limits. + */ +public final class SieveOfAtkin { + + private SieveOfAtkin() { + // Utility class; prevent instantiation + } + + /** + * Generates a list of all prime numbers up to the specified limit + * using the Sieve of Atkin algorithm. + * + * @param limit the upper bound up to which primes are generated; must be zero or positive + * @return a list of prime numbers up to the limit; empty if the limit is less than 2 + */ + public static List generatePrimes(int limit) { + if (limit < 1) { + return List.of(); + } + + boolean[] sieve = new boolean[limit + 1]; + int sqrtLimit = (int) Math.sqrt(limit); + + markQuadraticResidues(limit, sqrtLimit, sieve); + eliminateMultiplesOfSquares(limit, sqrtLimit, sieve); + + List primes = new ArrayList<>(); + if (limit >= 2) { + primes.add(2); + } + if (limit >= 3) { + primes.add(3); + } + + for (int i = 5; i <= limit; i++) { + if (sieve[i]) { + primes.add(i); + } + } + + return primes; + } + + /** + * Marks numbers in the sieve as prime candidates based on quadratic residues. + * + * This method iterates over all x and y up to sqrt(limit) and applies + * the three quadratic forms used in the Sieve of Atkin. Numbers satisfying + * the modulo conditions are toggled in the sieve array. + * + * @param limit the upper bound for primes + * @param sqrtLimit square root of the limit + * @param sieve boolean array representing potential primes + */ + private static void markQuadraticResidues(int limit, int sqrtLimit, boolean[] sieve) { + for (int x = 1; x <= sqrtLimit; x++) { + for (int y = 1; y <= sqrtLimit; y++) { + applyQuadraticForm(4 * x * x + y * y, limit, sieve, 1, 5); + applyQuadraticForm(3 * x * x + y * y, limit, sieve, 7); + applyQuadraticForm(3 * x * x - y * y, limit, sieve, 11, x > y); + } + } + } + + /** + * Toggles the sieve entry for a number if it satisfies one modulo condition. + * + * @param n the number to check + * @param limit upper bound of primes + * @param sieve boolean array representing potential primes + * @param modulo the modulo condition number to check + */ + private static void applyQuadraticForm(int n, int limit, boolean[] sieve, int modulo) { + if (n <= limit && n % 12 == modulo) { + sieve[n] ^= true; + } + } + + /** + * Toggles the sieve entry for a number if it satisfies either of two modulo conditions. + * + * @param n the number to check + * @param limit upper bound of primes + * @param sieve boolean array representing potential primes + * @param modulo1 first modulo condition number to check + * @param modulo2 second modulo condition number to check + */ + private static void applyQuadraticForm(int n, int limit, boolean[] sieve, int modulo1, int modulo2) { + if (n <= limit && (n % 12 == modulo1 || n % 12 == modulo2)) { + sieve[n] ^= true; + } + } + + /** + * Toggles the sieve entry for a number if it satisfies the modulo condition and an additional boolean condition. + * + * This version is used for the quadratic form 3*x*x - y*y, which requires x > y. + * + * @param n the number to check + * @param limit upper bound of primes + * @param sieve boolean array representing potential primes + * @param modulo the modulo condition number to check + * @param condition an additional boolean condition that must be true + */ + private static void applyQuadraticForm(int n, int limit, boolean[] sieve, int modulo, boolean condition) { + if (condition && n <= limit && n % 12 == modulo) { + sieve[n] ^= true; + } + } + + /** + * Eliminates numbers that are multiples of squares from the sieve. + * + * All numbers that are multiples of i*i (where i is marked as prime) are + * marked non-prime to finalize the sieve. This ensures only actual primes remain. + * + * @param limit the upper bound for primes + * @param sqrtLimit square root of the limit + * @param sieve boolean array representing potential primes + */ + private static void eliminateMultiplesOfSquares(int limit, int sqrtLimit, boolean[] sieve) { + for (int i = 5; i <= sqrtLimit; i++) { + if (!sieve[i]) { + continue; + } + int square = i * i; + for (int j = square; j <= limit; j += square) { + sieve[j] = false; + } + } + } +} 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/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/SumOfSquares.java b/src/main/java/com/thealgorithms/maths/SumOfSquares.java new file mode 100644 index 000000000000..c050d5a75f7b --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SumOfSquares.java @@ -0,0 +1,53 @@ +package com.thealgorithms.maths; + +/** + * Implementation of Lagrange's Four Square Theorem + * Find minimum number of perfect squares that sum to given number + * + * @see Lagrange's Four Square Theorem + * @author BEASTSHRIRAM + */ +public final class SumOfSquares { + + private SumOfSquares() { + // Utility class + } + + /** + * Find minimum number of perfect squares that sum to n + * + * @param n the target number + * @return minimum number of squares needed + */ + public static int minSquares(int n) { + if (isPerfectSquare(n)) { + return 1; + } + + for (int i = 1; i * i <= n; i++) { + int remaining = n - i * i; + if (isPerfectSquare(remaining)) { + return 2; + } + } + + // Legendre's three-square theorem + int temp = n; + while (temp % 4 == 0) { + temp /= 4; + } + if (temp % 8 == 7) { + return 4; + } + + return 3; + } + + private static boolean isPerfectSquare(int n) { + if (n < 0) { + return false; + } + int root = (int) Math.sqrt(n); + return root * root == n; + } +} diff --git a/src/main/java/com/thealgorithms/maths/ZellersCongruence.java b/src/main/java/com/thealgorithms/maths/ZellersCongruence.java new file mode 100644 index 000000000000..95ed061ac17f --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/ZellersCongruence.java @@ -0,0 +1,107 @@ +package com.thealgorithms.maths; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.util.Objects; + +/** + * A utility class for calculating the day of the week for a given date using Zeller's Congruence. + * + *

Zeller's Congruence is an algorithm devised by Christian Zeller in the 19th century to calculate + * the day of the week for any Julian or Gregorian calendar date. The input date must be in the format + * "MM-DD-YYYY" or "MM/DD/YYYY". + * + *

This class is final and cannot be instantiated. + * + * @see Wikipedia: Zeller's Congruence + */ +public final class ZellersCongruence { + + private static final String[] DAYS = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; + + // Private constructor to prevent instantiation + private ZellersCongruence() { + } + + /** + * Calculates the day of the week for a given date using Zeller's Congruence. + * + *

The algorithm works for both Julian and Gregorian calendar dates. The input date must be + * in the format "MM-DD-YYYY" or "MM/DD/YYYY". + * + * @param input the date in the format "MM-DD-YYYY" or "MM/DD/YYYY" + * @return a string indicating the day of the week for the given date + * @throws IllegalArgumentException if the input format is invalid, the date is invalid, + * or the year is out of range + */ + public static String calculateDay(String input) { + if (input == null || input.length() != 10) { + throw new IllegalArgumentException("Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY."); + } + + int month = parsePart(input.substring(0, 2), 1, 12, "Month must be between 1 and 12."); + char sep1 = input.charAt(2); + validateSeparator(sep1); + + int day = parsePart(input.substring(3, 5), 1, 31, "Day must be between 1 and 31."); + char sep2 = input.charAt(5); + validateSeparator(sep2); + + int year = parsePart(input.substring(6, 10), 46, 8499, "Year must be between 46 and 8499."); + + try { + Objects.requireNonNull(LocalDate.of(year, month, day)); + } catch (DateTimeException e) { + throw new IllegalArgumentException("Invalid date.", e); + } + if (month <= 2) { + year -= 1; + month += 12; + } + + int century = year / 100; + int yearOfCentury = year % 100; + int t = (int) (2.6 * month - 5.39); + int u = century / 4; + int v = yearOfCentury / 4; + int f = (int) Math.round((day + yearOfCentury + t + u + v - 2 * century) % 7.0); + + int correctedDay = (f + 7) % 7; + + return "The date " + input + " falls on a " + DAYS[correctedDay] + "."; + } + + /** + * Parses a part of the date string and validates its range. + * + * @param part the substring to parse + * @param min the minimum valid value + * @param max the maximum valid value + * @param error the error message to throw if validation fails + * @return the parsed integer value + * @throws IllegalArgumentException if the part is not a valid number or is out of range + */ + private static int parsePart(String part, int min, int max, String error) { + try { + int value = Integer.parseInt(part); + if (value < min || value > max) { + throw new IllegalArgumentException(error); + } + return value; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid numeric part: " + part, e); + } + } + + /** + * Validates the separator character in the date string. + * + * @param sep the separator character + * @throws IllegalArgumentException if the separator is not '-' or '/' + */ + private static void validateSeparator(char sep) { + if (sep != '-' && sep != '/') { + throw new IllegalArgumentException("Date separator must be '-' or '/'."); + } + } +} diff --git a/src/main/java/com/thealgorithms/matrix/LUDecomposition.java b/src/main/java/com/thealgorithms/matrix/LUDecomposition.java new file mode 100644 index 000000000000..e41aaa201338 --- /dev/null +++ b/src/main/java/com/thealgorithms/matrix/LUDecomposition.java @@ -0,0 +1,88 @@ +package com.thealgorithms.matrix; + +/** + * LU Decomposition algorithm + * -------------------------- + * Decomposes a square matrix a into a product of two matrices: + * a = l * u + * where: + * - l is a lower triangular matrix with 1s on its diagonal + * - u is an upper triangular matrix + * + * Reference: + * https://en.wikipedia.org/wiki/lu_decomposition + */ +public final class LUDecomposition { + + private LUDecomposition() { + } + + /** + * A helper class to store both l and u matrices + */ + public static class LU { + double[][] l; + double[][] u; + + LU(double[][] l, double[][] u) { + this.l = l; + this.u = u; + } + } + + /** + * Performs LU Decomposition on a square matrix a + * + * @param a input square matrix + * @return LU object containing l and u matrices + */ + public static LU decompose(double[][] a) { + int n = a.length; + double[][] l = new double[n][n]; + double[][] u = new double[n][n]; + + for (int i = 0; i < n; i++) { + // upper triangular matrix + for (int k = i; k < n; k++) { + double sum = 0; + for (int j = 0; j < i; j++) { + sum += l[i][j] * u[j][k]; + } + u[i][k] = a[i][k] - sum; + } + + // lower triangular matrix + for (int k = i; k < n; k++) { + if (i == k) { + l[i][i] = 1; // diagonal as 1 + } else { + double sum = 0; + for (int j = 0; j < i; j++) { + sum += l[k][j] * u[j][i]; + } + l[k][i] = (a[k][i] - sum) / u[i][i]; + } + } + } + + return new LU(l, u); + } + + /** + * Utility function to print a matrix + * + * @param m matrix to print + */ + public static void printMatrix(double[][] m) { + for (double[] row : m) { + System.out.print("["); + for (int j = 0; j < row.length; j++) { + System.out.printf("%7.3f", row[j]); + if (j < row.length - 1) { + System.out.print(", "); + } + } + System.out.println("]"); + } + } +} diff --git a/src/main/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrder.java b/src/main/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrder.java index 2757da1f9023..4ae5970a9574 100644 --- a/src/main/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrder.java +++ b/src/main/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrder.java @@ -3,17 +3,41 @@ import java.util.ArrayList; import java.util.List; +/** + * Utility class to print a matrix in spiral order. + *

+ * Given a 2D array (matrix), this class provides a method to return the + * elements + * of the matrix in spiral order, starting from the top-left corner and moving + * clockwise. + *

+ * + * @author Sadiul Hakim (https://github.com/sadiul-hakim) + */ public class PrintAMatrixInSpiralOrder { + /** - * Search a key in row and column wise sorted matrix + * Returns the elements of the given matrix in spiral order. + * + * @param matrix the 2D array to traverse in spiral order + * @param row the number of rows in the matrix + * @param col the number of columns in the matrix + * @return a list containing the elements of the matrix in spiral order * - * @param matrix matrix to be searched - * @param row number of rows matrix has - * @param col number of columns matrix has - * @author Sadiul Hakim : https://github.com/sadiul-hakim + *

+ * Example: + * + *

+     * int[][] matrix = {
+     *   {1, 2, 3},
+     *   {4, 5, 6},
+     *   {7, 8, 9}
+     * };
+     * print(matrix, 3, 3) returns [1, 2, 3, 6, 9, 8, 7, 4, 5]
+     *         
+ *

*/ public List print(int[][] matrix, int row, int col) { - // r traverses matrix row wise from first int r = 0; // c traverses matrix column wise from first diff --git a/src/main/java/com/thealgorithms/matrix/RotateMatrixBy90Degrees.java b/src/main/java/com/thealgorithms/matrix/RotateMatrixBy90Degrees.java index 9a7f255282ac..7ee8a1a9f0ed 100644 --- a/src/main/java/com/thealgorithms/matrix/RotateMatrixBy90Degrees.java +++ b/src/main/java/com/thealgorithms/matrix/RotateMatrixBy90Degrees.java @@ -40,7 +40,7 @@ static void printMatrix(int[][] arr) { } /** - * Class containing the algo to roate matrix by 90 degree + * Class containing the algo to rotate matrix by 90 degree */ final class Rotate { private Rotate() { 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/BankersAlgorithm.java b/src/main/java/com/thealgorithms/others/BankersAlgorithm.java index 836526529374..5abf633a1c12 100644 --- a/src/main/java/com/thealgorithms/others/BankersAlgorithm.java +++ b/src/main/java/com/thealgorithms/others/BankersAlgorithm.java @@ -3,7 +3,7 @@ import java.util.Scanner; /** - * This file contains an implementation of BANKER'S ALGORITM Wikipedia: + * This file contains an implementation of BANKER'S ALGORITHM Wikipedia: * https://en.wikipedia.org/wiki/Banker%27s_algorithm * * The algorithm for finding out whether or not a system is in a safe state can diff --git a/src/main/java/com/thealgorithms/others/CRCAlgorithm.java b/src/main/java/com/thealgorithms/others/CRCAlgorithm.java index 00ddc86be820..2d0be15e0a7b 100644 --- a/src/main/java/com/thealgorithms/others/CRCAlgorithm.java +++ b/src/main/java/com/thealgorithms/others/CRCAlgorithm.java @@ -95,7 +95,7 @@ public int getCorrectMess() { /** * Resets some of the object's values, used on the main function, so that it - * can be re-used, in order not to waste too much memory and time, by + * can be reused, in order not to waste too much memory and time, by * creating new objects. */ public void refactor() { @@ -171,7 +171,7 @@ public void divideMessageWithP(boolean check) { * Once the message is transmitted, some of it's elements, is possible to * change from 1 to 0, or from 0 to 1, because of the Bit Error Rate (ber). * For every element of the message, a random double number is created. If - * that number is smaller than ber, then the spesific element changes. On + * that number is smaller than ber, then the specific element changes. On * the other hand, if it's bigger than ber, it does not. Based on these * changes. the boolean variable messageChanged, gets the value: true, or * false. diff --git a/src/main/java/com/thealgorithms/others/GaussLegendre.java b/src/main/java/com/thealgorithms/others/GaussLegendre.java index b56b51f158fb..acf76ae3b192 100644 --- a/src/main/java/com/thealgorithms/others/GaussLegendre.java +++ b/src/main/java/com/thealgorithms/others/GaussLegendre.java @@ -1,7 +1,7 @@ package com.thealgorithms.others; /** - * Guass Legendre Algorithm ref + * Gauss Legendre Algorithm ref * https://en.wikipedia.org/wiki/Gauss–Legendre_algorithm * * @author AKS1996 diff --git a/src/main/java/com/thealgorithms/others/HappyNumbersSeq.java b/src/main/java/com/thealgorithms/others/HappyNumbersSeq.java deleted file mode 100644 index 0ae1e451bc6a..000000000000 --- a/src/main/java/com/thealgorithms/others/HappyNumbersSeq.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.thealgorithms.others; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Scanner; -import java.util.Set; - -public final class HappyNumbersSeq { - private HappyNumbersSeq() { - } - - private static final Set CYCLE_NUMS = new HashSet<>(Arrays.asList(4, 16, 20, 37, 58, 145)); - - public static void main(String[] args) { - Scanner in = new Scanner(System.in); - System.out.print("Enter number: "); - int n = in.nextInt(); - while (n != 1 && !isSad(n)) { - System.out.print(n + " "); - n = sumSquares(n); - } - String res = n == 1 ? "1 Happy number" : "Sad number"; - System.out.println(res); - in.close(); - } - - private static int sumSquares(int n) { - int s = 0; - for (; n > 0; n /= 10) { - int r = n % 10; - s += r * r; - } - return s; - } - - private static boolean isSad(int n) { - return CYCLE_NUMS.contains(n); - } -} diff --git a/src/main/java/com/thealgorithms/others/Huffman.java b/src/main/java/com/thealgorithms/others/Huffman.java index 4fdee5d5e70e..22e75da502b5 100644 --- a/src/main/java/com/thealgorithms/others/Huffman.java +++ b/src/main/java/com/thealgorithms/others/Huffman.java @@ -1,125 +1,211 @@ package com.thealgorithms.others; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; import java.util.PriorityQueue; -import java.util.Scanner; -// node class is the basic structure -// of each node present in the Huffman - tree. +/** + * Node class representing a node in the Huffman tree. + * Each node contains a character, its frequency, and references to left and + * right children. + */ class HuffmanNode { - int data; char c; - HuffmanNode left; HuffmanNode right; -} -// comparator class helps to compare the node -// on the basis of one of its attribute. -// Here we will be compared -// on the basis of data values of the nodes. -class MyComparator implements Comparator { + /** + * Constructor for HuffmanNode. + * + * @param c the character stored in this node + * @param data the frequency of the character + */ + HuffmanNode(char c, int data) { + this.c = c; + this.data = data; + this.left = null; + this.right = null; + } + + /** + * Default constructor for HuffmanNode. + */ + HuffmanNode() { + this.left = null; + this.right = null; + } +} +/** + * Comparator class for comparing HuffmanNode objects based on their frequency + * data. + * Used to maintain min-heap property in the priority queue. + */ +class HuffmanComparator implements Comparator { + @Override public int compare(HuffmanNode x, HuffmanNode y) { - return x.data - y.data; + return Integer.compare(x.data, y.data); } } +/** + * Implementation of Huffman Coding algorithm for data compression. + * Huffman Coding is a greedy algorithm that assigns variable-length codes to + * characters + * based on their frequency of occurrence. Characters with higher frequency get + * shorter codes. + * + *

+ * Time Complexity: O(n log n) where n is the number of unique characters + * Space Complexity: O(n) + * + * @see Huffman + * Coding + */ public final class Huffman { private Huffman() { } - // recursive function to print the - // huffman-code through the tree traversal. - // Here s is the huffman - code generated. - public static void printCode(HuffmanNode root, String s) { - // base case; if the left and right are null - // then its a leaf node and we print - // the code s generated by traversing the tree. - if (root.left == null && root.right == null && Character.isLetter(root.c)) { - // c is the character in the node - System.out.println(root.c + ":" + s); - - return; + /** + * Builds a Huffman tree from the given character array and their frequencies. + * + * @param charArray array of characters + * @param charFreq array of frequencies corresponding to the characters + * @return root node of the Huffman tree + * @throws IllegalArgumentException if arrays are null, empty, or have different + * lengths + */ + public static HuffmanNode buildHuffmanTree(char[] charArray, int[] charFreq) { + if (charArray == null || charFreq == null) { + throw new IllegalArgumentException("Character array and frequency array cannot be null"); + } + if (charArray.length == 0 || charFreq.length == 0) { + throw new IllegalArgumentException("Character array and frequency array cannot be empty"); + } + if (charArray.length != charFreq.length) { + throw new IllegalArgumentException("Character array and frequency array must have the same length"); } - // if we go to left then add "0" to the code. - // if we go to the right add"1" to the code. - // recursive calls for left and - // right sub-tree of the generated tree. - printCode(root.left, s + "0"); - printCode(root.right, s + "1"); - } + int n = charArray.length; + PriorityQueue priorityQueue = new PriorityQueue<>(n, new HuffmanComparator()); - // main function - public static void main(String[] args) { - Scanner s = new Scanner(System.in); + // Create leaf nodes and add to priority queue + for (int i = 0; i < n; i++) { + if (charFreq[i] < 0) { + throw new IllegalArgumentException("Frequencies must be non-negative"); + } + HuffmanNode node = new HuffmanNode(charArray[i], charFreq[i]); + priorityQueue.add(node); + } - // number of characters. - int n = 6; - char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f'}; - int[] charfreq = {5, 9, 12, 13, 16, 45}; + // Build the Huffman tree + while (priorityQueue.size() > 1) { + HuffmanNode left = priorityQueue.poll(); + HuffmanNode right = priorityQueue.poll(); - // creating a priority queue q. - // makes a min-priority queue(min-heap). - PriorityQueue q = new PriorityQueue(n, new MyComparator()); + HuffmanNode parent = new HuffmanNode(); + parent.data = left.data + right.data; + parent.c = '-'; + parent.left = left; + parent.right = right; - for (int i = 0; i < n; i++) { - // creating a Huffman node object - // and add it to the priority queue. - HuffmanNode hn = new HuffmanNode(); + priorityQueue.add(parent); + } - hn.c = charArray[i]; - hn.data = charfreq[i]; + return priorityQueue.poll(); + } - hn.left = null; - hn.right = null; + /** + * Generates Huffman codes for all characters in the tree. + * + * @param root root node of the Huffman tree + * @return map of characters to their Huffman codes + */ + public static Map generateCodes(HuffmanNode root) { + Map huffmanCodes = new HashMap<>(); + if (root != null) { + generateCodesHelper(root, "", huffmanCodes); + } + return huffmanCodes; + } - // add functions adds - // the huffman node to the queue. - q.add(hn); + /** + * Helper method to recursively generate Huffman codes by traversing the tree. + * + * @param node current node in the tree + * @param code current code being built + * @param huffmanCodes map to store character-to-code mappings + */ + private static void generateCodesHelper(HuffmanNode node, String code, Map huffmanCodes) { + if (node == null) { + return; } - // create a root node - HuffmanNode root = null; + // If it's a leaf node, store the code + if (node.left == null && node.right == null && Character.isLetter(node.c)) { + huffmanCodes.put(node.c, code.isEmpty() ? "0" : code); + return; + } - // Here we will extract the two minimum value - // from the heap each time until - // its size reduces to 1, extract until - // all the nodes are extracted. - while (q.size() > 1) { - // first min extract. - HuffmanNode x = q.peek(); - q.poll(); + // Traverse left with '0' and right with '1' + if (node.left != null) { + generateCodesHelper(node.left, code + "0", huffmanCodes); + } + if (node.right != null) { + generateCodesHelper(node.right, code + "1", huffmanCodes); + } + } - // second min extarct. - HuffmanNode y = q.peek(); - q.poll(); + /** + * Prints Huffman codes for all characters in the tree. + * This method is kept for backward compatibility and demonstration purposes. + * + * @param root root node of the Huffman tree + * @param code current code being built (initially empty string) + */ + public static void printCode(HuffmanNode root, String code) { + if (root == null) { + return; + } - // new node f which is equal - HuffmanNode f = new HuffmanNode(); + // If it's a leaf node, print the code + if (root.left == null && root.right == null && Character.isLetter(root.c)) { + System.out.println(root.c + ":" + code); + return; + } - // to the sum of the frequency of the two nodes - // assigning values to the f node. - f.data = x.data + y.data; - f.c = '-'; + // Traverse left with '0' and right with '1' + if (root.left != null) { + printCode(root.left, code + "0"); + } + if (root.right != null) { + printCode(root.right, code + "1"); + } + } - // first extracted node as left child. - f.left = x; + /** + * Demonstrates the Huffman coding algorithm with sample data. + * + * @param args command line arguments (not used) + */ + public static void main(String[] args) { + // Sample characters and their frequencies + char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f'}; + int[] charFreq = {5, 9, 12, 13, 16, 45}; - // second extracted node as the right child. - f.right = y; + System.out.println("Characters: a, b, c, d, e, f"); + System.out.println("Frequencies: 5, 9, 12, 13, 16, 45"); + System.out.println("\nHuffman Codes:"); - // marking the f node as the root node. - root = f; + // Build Huffman tree + HuffmanNode root = buildHuffmanTree(charArray, charFreq); - // add this node to the priority-queue. - q.add(f); + // Generate and print Huffman codes + Map codes = generateCodes(root); + for (Map.Entry entry : codes.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); } - - // print the codes by traversing the tree - printCode(root, ""); - s.close(); } } diff --git a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java b/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java index bb88c7e3ae2f..7a1a7aadd805 100644 --- a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java +++ b/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java @@ -97,7 +97,7 @@ static void suggestionsRec(TrieNode root, String currPrefix) { } } - // Fucntion to print suggestions for + // Function to print suggestions for // given query prefix. static int printAutoSuggestions(TrieNode root, final String query) { TrieNode pCrawl = root; diff --git a/src/main/java/com/thealgorithms/others/InsertDeleteInArray.java b/src/main/java/com/thealgorithms/others/InsertDeleteInArray.java index 15093549871b..06a2539ee8b7 100644 --- a/src/main/java/com/thealgorithms/others/InsertDeleteInArray.java +++ b/src/main/java/com/thealgorithms/others/InsertDeleteInArray.java @@ -1,51 +1,172 @@ package com.thealgorithms.others; +import java.util.Arrays; import java.util.Scanner; +/** + * Utility class for performing insert and delete operations on arrays. + *

+ * This class demonstrates how to insert an element at a specific position and + * delete an element from a specific position in an integer array. Since arrays + * in Java have fixed size, insertion creates a new array with increased size, + * and deletion shifts elements to fill the gap. + *

+ * + *

+ * Time Complexity: + *

+ *
    + *
  • Insert: O(n) - requires copying elements to new array
  • + *
  • Delete: O(n) - requires shifting elements
  • + *
+ * + *

+ * Space Complexity: + *

+ *
    + *
  • Insert: O(n) - new array of size n+1
  • + *
  • Delete: O(1) - in-place modification (excluding result array)
  • + *
+ * + * @author TheAlgorithms community + * @see Array + * Data Structure + */ public final class InsertDeleteInArray { private InsertDeleteInArray() { } + /** + * Inserts an element at the specified position in the array. + *

+ * Creates a new array with size = original array size + 1. + * Elements at positions <= insertPos retain their positions, + * while elements at positions > insertPos are shifted right by one position. + *

+ * + * @param array the original array + * @param element the element to be inserted + * @param position the index at which the element should be inserted (0-based) + * @return a new array with the element inserted at the specified position + * @throws IllegalArgumentException if position is negative or greater than + * array length + * @throws IllegalArgumentException if array is null + */ + public static int[] insertElement(int[] array, int element, int position) { + if (array == null) { + throw new IllegalArgumentException("Array cannot be null"); + } + if (position < 0 || position > array.length) { + throw new IllegalArgumentException("Position must be between 0 and " + array.length); + } + + int[] newArray = new int[array.length + 1]; + + // Copy elements before insertion position + System.arraycopy(array, 0, newArray, 0, position); + + // Insert the new element + newArray[position] = element; + + // Copy remaining elements after insertion position + System.arraycopy(array, position, newArray, position + 1, array.length - position); + + return newArray; + } + + /** + * Deletes an element at the specified position from the array. + *

+ * Creates a new array with size = original array size - 1. + * Elements after the deletion position are shifted left by one position. + *

+ * + * @param array the original array + * @param position the index of the element to be deleted (0-based) + * @return a new array with the element at the specified position removed + * @throws IllegalArgumentException if position is negative or greater than or + * equal to array length + * @throws IllegalArgumentException if array is null or empty + */ + public static int[] deleteElement(int[] array, int position) { + if (array == null) { + throw new IllegalArgumentException("Array cannot be null"); + } + if (array.length == 0) { + throw new IllegalArgumentException("Array is empty"); + } + if (position < 0 || position >= array.length) { + throw new IllegalArgumentException("Position must be between 0 and " + (array.length - 1)); + } + + int[] newArray = new int[array.length - 1]; + + // Copy elements before deletion position + System.arraycopy(array, 0, newArray, 0, position); + + // Copy elements after deletion position + System.arraycopy(array, position + 1, newArray, position, array.length - position - 1); + + return newArray; + } + + /** + * Main method demonstrating insert and delete operations on an array. + *

+ * This method interactively: + *

    + *
  1. Takes array size and elements as input
  2. + *
  3. Inserts a new element at a specified position
  4. + *
  5. Deletes an element from a specified position
  6. + *
  7. Displays the array after each operation
  8. + *
+ *

+ * + * @param args command line arguments (not used) + */ public static void main(String[] args) { - try (Scanner s = new Scanner(System.in)) { - System.out.println("Enter the size of the array"); - int size = s.nextInt(); - int[] a = new int[size]; - int i; - - // To enter the initial elements - for (i = 0; i < size; i++) { - System.out.println("Enter the element"); - a[i] = s.nextInt(); - } + try (Scanner scanner = new Scanner(System.in)) { + // Input: array size and elements + System.out.println("Enter the size of the array:"); + int size = scanner.nextInt(); - // To insert a new element(we are creating a new array) - System.out.println("Enter the index at which the element should be inserted"); - int insertPos = s.nextInt(); - System.out.println("Enter the element to be inserted"); - int ins = s.nextInt(); - int size2 = size + 1; - int[] b = new int[size2]; - for (i = 0; i < size2; i++) { - if (i <= insertPos) { - b[i] = a[i]; - } else { - b[i] = a[i - 1]; - } + if (size <= 0) { + System.out.println("Array size must be positive"); + return; } - b[insertPos] = ins; - for (i = 0; i < size2; i++) { - System.out.println(b[i]); + + int[] array = new int[size]; + + System.out.println("Enter " + size + " elements:"); + for (int i = 0; i < size; i++) { + array[i] = scanner.nextInt(); } - // To delete an element given the index - System.out.println("Enter the index at which element is to be deleted"); - int delPos = s.nextInt(); - for (i = delPos; i < size2 - 1; i++) { - b[i] = b[i + 1]; + System.out.println("Original array: " + Arrays.toString(array)); + + // Insert operation + System.out.println("\nEnter the index at which the element should be inserted (0-" + size + "):"); + int insertPos = scanner.nextInt(); + System.out.println("Enter the element to be inserted:"); + int elementToInsert = scanner.nextInt(); + + try { + array = insertElement(array, elementToInsert, insertPos); + System.out.println("Array after insertion: " + Arrays.toString(array)); + } catch (IllegalArgumentException e) { + System.out.println("Error during insertion: " + e.getMessage()); + return; } - for (i = 0; i < size2 - 1; i++) { - System.out.println(b[i]); + + // Delete operation + System.out.println("\nEnter the index at which element is to be deleted (0-" + (array.length - 1) + "):"); + int deletePos = scanner.nextInt(); + + try { + array = deleteElement(array, deletePos); + System.out.println("Array after deletion: " + Arrays.toString(array)); + } catch (IllegalArgumentException e) { + System.out.println("Error during deletion: " + e.getMessage()); } } } diff --git a/src/main/java/com/thealgorithms/others/IterativeFloodFill.java b/src/main/java/com/thealgorithms/others/IterativeFloodFill.java new file mode 100644 index 000000000000..3f685f418a3d --- /dev/null +++ b/src/main/java/com/thealgorithms/others/IterativeFloodFill.java @@ -0,0 +1,102 @@ +package com.thealgorithms.others; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Implementation of the Flood Fill algorithm using an iterative BFS (Breadth-First Search) approach. + * + *

The Flood Fill algorithm is used to fill connected areas in an image with a new color, starting from a specified point. + * This implementation uses an iterative BFS approach with a queue + * instead of recursion to avoid stack overflow issues with large images.

+ * + *

Implementation Features:

+ *
    + *
  • Supports 8-connected filling (horizontal, vertical, and diagonal directions)
  • + *
  • Uses BFS traversal through {@link java.util.Queue}
  • + *
  • Includes nested {@code Point} class to represent pixel coordinates
  • + *
  • Iterative approach avoids stack overflow for large images
  • + *
+ * + *

Time Complexity: O(M × N) where M and N are the dimensions of the image

+ *

Space Complexity: O(M × N) in the worst case the queue stores every pixel

+ * + * @see Flood Fill Algorithm - GeeksforGeeks + * @see Flood Fill Algorithm - Wikipedia + */ +public final class IterativeFloodFill { + private IterativeFloodFill() { + } + + /** + * Iteratively fill the 2D image with new color + * + * @param image The image to be filled + * @param x The x coordinate at which color is to be filled + * @param y The y coordinate at which color is to be filled + * @param newColor The new color which to be filled in the image + * @param oldColor The old color which is to be replaced in the image + * @see FloodFill BFS + */ + public static void floodFill(final int[][] image, final int x, final int y, final int newColor, final int oldColor) { + if (image.length == 0 || image[0].length == 0 || newColor == oldColor || shouldSkipPixel(image, x, y, oldColor)) { + return; + } + + Queue queue = new LinkedList<>(); + queue.add(new Point(x, y)); + + int[] dx = {0, 0, -1, 1, 1, -1, 1, -1}; + int[] dy = {-1, 1, 0, 0, -1, 1, 1, -1}; + + while (!queue.isEmpty()) { + Point currPoint = queue.poll(); + + if (shouldSkipPixel(image, currPoint.x, currPoint.y, oldColor)) { + continue; + } + + image[currPoint.x][currPoint.y] = newColor; + + for (int i = 0; i < 8; i++) { + int curX = currPoint.x + dx[i]; + int curY = currPoint.y + dy[i]; + + if (!shouldSkipPixel(image, curX, curY, oldColor)) { + queue.add(new Point(curX, curY)); + } + } + } + } + + /** + * Represents a point in 2D space with integer coordinates. + */ + private static class Point { + final int x; + final int y; + + Point(final int x, final int y) { + this.x = x; + this.y = y; + } + } + + /** + * Checks if a pixel should be skipped during flood fill operation. + * + * @param image The image to get boundaries + * @param x The x coordinate of pixel to check + * @param y The y coordinate of pixel to check + * @param oldColor The old color which is to be replaced in the image + * @return {@code true} if pixel should be skipped, else {@code false} + */ + private static boolean shouldSkipPixel(final int[][] image, final int x, final int y, final int oldColor) { + + if (x < 0 || x >= image.length || y < 0 || y >= image[0].length || image[x][y] != oldColor) { + return true; + } + + return false; + } +} diff --git a/src/main/java/com/thealgorithms/others/Krishnamurthy.java b/src/main/java/com/thealgorithms/others/Krishnamurthy.java deleted file mode 100644 index 8e5ba7c6f1c7..000000000000 --- a/src/main/java/com/thealgorithms/others/Krishnamurthy.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.thealgorithms.others; - -import java.util.Scanner; - -final class Krishnamurthy { - private Krishnamurthy() { - } - - static int fact(int n) { - int i; - int p = 1; - for (i = n; i >= 1; i--) { - p = p * i; - } - return p; - } - - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); - int a; - int b; - int s = 0; - System.out.print("Enter the number : "); - a = sc.nextInt(); - int n = a; - while (a > 0) { - b = a % 10; - s = s + fact(b); - a = a / 10; - } - if (s == n) { - System.out.print(n + " is a krishnamurthy number"); - } else { - System.out.print(n + " is not a krishnamurthy number"); - } - sc.close(); - } -} diff --git a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java b/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java index 76d1ed4aba1d..a3ca8d6f6db8 100644 --- a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java +++ b/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java @@ -4,8 +4,26 @@ import java.util.List; /** - * @brief Class for finding the lowest base in which a given integer is a palindrome. - cf. https://oeis.org/A016026 + * Utility class for finding the lowest base in which a given integer is a + * palindrome. + *

+ * A number is a palindrome in a given base if its representation in that base + * reads the same + * forwards and backwards. For example, 15 in base 2 is 1111, which is + * palindromic. + * This class provides methods to check palindromic properties and find the + * smallest base + * where a number becomes palindromic. + *

+ *

+ * Example: The number 15 in base 2 is represented as [1,1,1,1], which is + * palindromic. + * The number 10 in base 3 is represented as [1,0,1], which is also palindromic. + *

+ * + * @see
OEIS A016026 - Smallest base in which + * n is palindromic + * @author TheAlgorithms Contributors */ public final class LowestBasePalindrome { private LowestBasePalindrome() { @@ -37,12 +55,18 @@ private static void checkNumber(int number) { /** * Computes the digits of a given number in a specified base. + *

+ * The digits are returned in reverse order (least significant digit first). + * For example, the number 13 in base 2 produces [1,0,1,1] representing 1101 in + * binary. + *

* - * @param number the number to be converted - * @param base the base to be used for the conversion - * @return a list of digits representing the number in the given base, with the most - * significant digit at the end of the list - * @throws IllegalArgumentException if the number is negative or the base is less than 2 + * @param number the number to be converted (must be non-negative) + * @param base the base to be used for the conversion (must be greater than 1) + * @return a list of digits representing the number in the given base, with the + * least significant digit at the beginning of the list + * @throws IllegalArgumentException if the number is negative or the base is + * less than 2 */ public static List computeDigitsInBase(int number, int base) { checkNumber(number); @@ -58,6 +82,10 @@ public static List computeDigitsInBase(int number, int base) { /** * Checks if a list of integers is palindromic. + *

+ * A list is palindromic if it reads the same forwards and backwards. + * For example, [1,2,1] is palindromic, but [1,2,3] is not. + *

* * @param list the list of integers to be checked * @return {@code true} if the list is a palindrome, {@code false} otherwise @@ -73,12 +101,29 @@ public static boolean isPalindromic(List list) { } /** - * Checks if the representation of a given number in a specified base is palindromic. + * Checks if the representation of a given number in a specified base is + * palindromic. + *

+ * This method first validates the input, then applies optimization: if the + * number + * ends with 0 in the given base (i.e., divisible by the base), it cannot be + * palindromic + * as palindromes cannot start with 0. + *

+ *

+ * Examples: + * - 101 in base 10 is palindromic (101) + * - 15 in base 2 is palindromic (1111) + * - 10 in base 3 is palindromic (101) + *

* - * @param number the number to be checked - * @param base the base in which the number will be represented - * @return {@code true} if the number is palindromic in the specified base, {@code false} otherwise - * @throws IllegalArgumentException if the number is negative or the base is less than 2 + * @param number the number to be checked (must be non-negative) + * @param base the base in which the number will be represented (must be + * greater than 1) + * @return {@code true} if the number is palindromic in the specified base, + * {@code false} otherwise + * @throws IllegalArgumentException if the number is negative or the base is + * less than 2 */ public static boolean isPalindromicInBase(int number, int base) { checkNumber(number); @@ -89,7 +134,8 @@ public static boolean isPalindromicInBase(int number, int base) { } if (number % base == 0) { - // If the last digit of the number in the given base is 0, it can't be palindromic + // If the last digit of the number in the given base is 0, it can't be + // palindromic return false; } @@ -97,10 +143,29 @@ public static boolean isPalindromicInBase(int number, int base) { } /** - * Finds the smallest base in which the representation of a given number is palindromic. + * Finds the smallest base in which the representation of a given number is + * palindromic. + *

+ * This method iteratively checks bases starting from 2 until it finds one where + * the number is palindromic. For any number n ≥ 2, the number is always + * palindromic + * in base n-1 (represented as [1, 1]), so this algorithm is guaranteed to + * terminate. + *

+ *

+ * Time Complexity: O(n * log(n)) in the worst case, where we check each base + * and + * convert the number to that base. + *

+ *

+ * Examples: + * - lowestBasePalindrome(15) returns 2 (15 in base 2 is 1111) + * - lowestBasePalindrome(10) returns 3 (10 in base 3 is 101) + * - lowestBasePalindrome(11) returns 10 (11 in base 10 is 11) + *

* - * @param number the number to be checked - * @return the smallest base in which the number is a palindrome + * @param number the number to be checked (must be non-negative) + * @return the smallest base in which the number is a palindrome (base ≥ 2) * @throws IllegalArgumentException if the number is negative */ public static int lowestBasePalindrome(int number) { diff --git a/src/main/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthK.java b/src/main/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthK.java index c05f1af4e327..dec813dd3213 100644 --- a/src/main/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthK.java +++ b/src/main/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthK.java @@ -1,14 +1,26 @@ package com.thealgorithms.others; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; /** - * References: https://en.wikipedia.org/wiki/Streaming_algorithm + * Algorithm to find the maximum sum of a subarray of size K with all distinct + * elements. * - * This model involves computing the maximum sum of subarrays of a fixed size \( K \) from a stream of integers. - * As the stream progresses, elements from the end of the window are removed, and new elements from the stream are added. + * This implementation uses a sliding window approach with a hash map to + * efficiently + * track element frequencies within the current window. The algorithm maintains + * a window + * of size K and slides it across the array, ensuring all elements in the window + * are distinct. * + * Time Complexity: O(n) where n is the length of the input array + * Space Complexity: O(k) for storing elements in the hash map + * + * @see Streaming + * Algorithm + * @see Sliding + * Window * @author Swarga-codes (https://github.com/Swarga-codes) */ public final class MaximumSumOfDistinctSubarraysWithLengthK { @@ -16,54 +28,62 @@ private MaximumSumOfDistinctSubarraysWithLengthK() { } /** - * Finds the maximum sum of a subarray of size K consisting of distinct elements. + * Finds the maximum sum of a subarray of size K consisting of distinct + * elements. * - * @param k The size of the subarray. - * @param nums The array from which subarrays will be considered. + * The algorithm uses a sliding window technique with a frequency map to track + * the count of each element in the current window. A window is valid only if + * all K elements are distinct (frequency map size equals K). * - * @return The maximum sum of any distinct-element subarray of size K. If no such subarray exists, returns 0. + * @param k The size of the subarray. Must be non-negative. + * @param nums The array from which subarrays will be considered. + * @return The maximum sum of any distinct-element subarray of size K. + * Returns 0 if no such subarray exists or if k is 0 or negative. + * @throws IllegalArgumentException if k is negative */ public static long maximumSubarraySum(int k, int... nums) { - if (nums.length < k) { + if (k <= 0 || nums == null || nums.length < k) { return 0; } - long masSum = 0; // Variable to store the maximum sum of distinct subarrays - long currentSum = 0; // Variable to store the sum of the current subarray - Set currentSet = new HashSet<>(); // Set to track distinct elements in the current subarray - // Initialize the first window + long maxSum = 0; + long currentSum = 0; + Map frequencyMap = new HashMap<>(); + + // Initialize the first window of size k for (int i = 0; i < k; i++) { currentSum += nums[i]; - currentSet.add(nums[i]); + frequencyMap.put(nums[i], frequencyMap.getOrDefault(nums[i], 0) + 1); } - // If the first window contains distinct elements, update maxSum - if (currentSet.size() == k) { - masSum = currentSum; + + // Check if the first window has all distinct elements + if (frequencyMap.size() == k) { + maxSum = currentSum; } + // Slide the window across the array - for (int i = 1; i < nums.length - k + 1; i++) { - // Update the sum by removing the element that is sliding out and adding the new element - currentSum = currentSum - nums[i - 1]; - currentSum = currentSum + nums[i + k - 1]; - int j = i; - boolean flag = false; // flag value which says that the subarray contains distinct elements - while (j < i + k && currentSet.size() < k) { - if (nums[i - 1] == nums[j]) { - flag = true; - break; - } else { - j++; - } - } - if (!flag) { - currentSet.remove(nums[i - 1]); + for (int i = k; i < nums.length; i++) { + // Remove the leftmost element from the window + int leftElement = nums[i - k]; + currentSum -= leftElement; + int leftFrequency = frequencyMap.get(leftElement); + if (leftFrequency == 1) { + frequencyMap.remove(leftElement); + } else { + frequencyMap.put(leftElement, leftFrequency - 1); } - currentSet.add(nums[i + k - 1]); - // If the current window has distinct elements, compare and possibly update maxSum - if (currentSet.size() == k && masSum < currentSum) { - masSum = currentSum; + + // Add the new rightmost element to the window + int rightElement = nums[i]; + currentSum += rightElement; + frequencyMap.put(rightElement, frequencyMap.getOrDefault(rightElement, 0) + 1); + + // If all elements in the window are distinct, update maxSum if needed + if (frequencyMap.size() == k && currentSum > maxSum) { + maxSum = currentSum; } } - return masSum; // the final maximum sum + + return maxSum; } } diff --git a/src/main/java/com/thealgorithms/others/MemoryManagementAlgorithms.java b/src/main/java/com/thealgorithms/others/MemoryManagementAlgorithms.java index 0924b8569942..40a5f6a7a767 100644 --- a/src/main/java/com/thealgorithms/others/MemoryManagementAlgorithms.java +++ b/src/main/java/com/thealgorithms/others/MemoryManagementAlgorithms.java @@ -16,7 +16,7 @@ public abstract class MemoryManagementAlgorithms { * blocks available. * @param sizeOfProcesses: an int array that contains the sizes of the * processes we need memory blocks for. - * @return the ArrayList filled with Integers repressenting the memory + * @return the ArrayList filled with Integers representing the memory * allocation that took place. */ public abstract ArrayList fitProcess(int[] sizeOfBlocks, int[] sizeOfProcesses); @@ -91,7 +91,7 @@ private static int findBestFit(int[] blockSizes, int processSize) { * blocks available. * @param sizeOfProcesses: an int array that contains the sizes of the * processes we need memory blocks for. - * @return the ArrayList filled with Integers repressenting the memory + * @return the ArrayList filled with Integers representing the memory * allocation that took place. */ public ArrayList fitProcess(int[] sizeOfBlocks, int[] sizeOfProcesses) { @@ -149,7 +149,7 @@ private static int findWorstFit(int[] blockSizes, int processSize) { * blocks available. * @param sizeOfProcesses: an int array that contains the sizes of the * processes we need memory blocks for. - * @return the ArrayList filled with Integers repressenting the memory + * @return the ArrayList filled with Integers representing the memory * allocation that took place. */ public ArrayList fitProcess(int[] sizeOfBlocks, int[] sizeOfProcesses) { @@ -201,7 +201,7 @@ private static int findFirstFit(int[] blockSizes, int processSize) { * blocks available. * @param sizeOfProcesses: an int array that contains the sizes of the * processes we need memory blocks for. - * @return the ArrayList filled with Integers repressenting the memory + * @return the ArrayList filled with Integers representing the memory * allocation that took place. */ public ArrayList fitProcess(int[] sizeOfBlocks, int[] sizeOfProcesses) { @@ -262,7 +262,7 @@ private int findNextFit(int[] blockSizes, int processSize) { * blocks available. * @param sizeOfProcesses: an int array that contains the sizes of the * processes we need memory blocks for. - * @return the ArrayList filled with Integers repressenting the memory + * @return the ArrayList filled with Integers representing the memory * allocation that took place. */ public ArrayList fitProcess(int[] sizeOfBlocks, int[] sizeOfProcesses) { diff --git a/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java b/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java index cd2cd02ab908..28dc980034f3 100644 --- a/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java +++ b/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java @@ -4,54 +4,99 @@ import java.util.Random; /** - * MiniMax is an algorithm used int artificial intelligence and game theory for - * minimizing the possible loss for the worst case scenario. + * MiniMax is an algorithm used in artificial intelligence and game theory for + * minimizing the possible loss for the worst case scenario. It is commonly used + * in two-player turn-based games such as Tic-Tac-Toe, Chess, and Checkers. * - * See more (https://en.wikipedia.org/wiki/Minimax, - * https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-1-introduction/). + *

+ * The algorithm simulates all possible moves in a game tree and chooses the + * move that minimizes the maximum possible loss. The algorithm assumes both + * players play optimally. + * + *

+ * Time Complexity: O(b^d) where b is the branching factor and d is the depth + *

+ * Space Complexity: O(d) for the recursive call stack + * + *

+ * See more: + *

* * @author aitofi (https://github.com/aitorfi) */ -public class MiniMaxAlgorithm { +public final class MiniMaxAlgorithm { + + private static final Random RANDOM = new Random(); /** * Game tree represented as an int array containing scores. Each array - * element is a leaf node. + * element is a leaf node. The array length must be a power of 2. */ private int[] scores; + + /** + * The height of the game tree, calculated as log2(scores.length). + */ private int height; /** - * Initializes the scores with 8 random leaf nodes + * Initializes the MiniMaxAlgorithm with 8 random leaf nodes (2^3 = 8). + * Each score is a random integer between 1 and 99 inclusive. */ public MiniMaxAlgorithm() { - scores = getRandomScores(3, 99); - height = log2(scores.length); + this(getRandomScores(3, 99)); } + /** + * Initializes the MiniMaxAlgorithm with the provided scores. + * + * @param scores An array of scores representing leaf nodes. The length must be + * a power of 2. + * @throws IllegalArgumentException if the scores array length is not a power of + * 2 + */ + public MiniMaxAlgorithm(int[] scores) { + if (!isPowerOfTwo(scores.length)) { + throw new IllegalArgumentException("The number of scores must be a power of 2."); + } + this.scores = Arrays.copyOf(scores, scores.length); + this.height = log2(scores.length); + } + + /** + * Demonstrates the MiniMax algorithm with a random game tree. + * + * @param args Command line arguments (not used) + */ public static void main(String[] args) { - MiniMaxAlgorithm miniMaxAlgorith = new MiniMaxAlgorithm(); + MiniMaxAlgorithm miniMaxAlgorithm = new MiniMaxAlgorithm(); boolean isMaximizer = true; // Specifies the player that goes first. - boolean verbose = true; // True to show each players choices. int bestScore; - bestScore = miniMaxAlgorith.miniMax(0, isMaximizer, 0, verbose); + bestScore = miniMaxAlgorithm.miniMax(0, isMaximizer, 0, true); - if (verbose) { - System.out.println(); - } - - System.out.println(Arrays.toString(miniMaxAlgorith.getScores())); + System.out.println(); + System.out.println(Arrays.toString(miniMaxAlgorithm.getScores())); System.out.println("The best score for " + (isMaximizer ? "Maximizer" : "Minimizer") + " is " + bestScore); } /** * Returns the optimal score assuming that both players play their best. * - * @param depth Indicates how deep we are into the game tree. - * @param isMaximizer True if it is maximizers turn; otherwise false. - * @param index Index of the leaf node that is being evaluated. - * @param verbose True to show each players choices. + *

+ * This method recursively evaluates the game tree using the minimax algorithm. + * At each level, the maximizer tries to maximize the score while the minimizer + * tries to minimize it. + * + * @param depth The current depth in the game tree (0 at root). + * @param isMaximizer True if it is the maximizer's turn; false for minimizer. + * @param index Index of the current node in the game tree. + * @param verbose True to print each player's choice during evaluation. * @return The optimal score for the player that made the first move. */ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { @@ -75,7 +120,7 @@ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { } // Leaf nodes can be sequentially inspected by - // recurssively multiplying (0 * 2) and ((0 * 2) + 1): + // recursively multiplying (0 * 2) and ((0 * 2) + 1): // (0 x 2) = 0; ((0 x 2) + 1) = 1 // (1 x 2) = 2; ((1 x 2) + 1) = 3 // (2 x 2) = 4; ((2 x 2) + 1) = 5 ... @@ -87,41 +132,73 @@ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { } /** - * Returns an array of random numbers which lenght is a power of 2. + * Returns an array of random numbers whose length is a power of 2. * - * @param size The power of 2 that will determine the lenght of the array. - * @param maxScore The maximum possible score. - * @return An array of random numbers. + * @param size The power of 2 that will determine the length of the array + * (array length = 2^size). + * @param maxScore The maximum possible score (scores will be between 1 and + * maxScore inclusive). + * @return An array of random numbers with length 2^size. */ public static int[] getRandomScores(int size, int maxScore) { int[] randomScores = new int[(int) Math.pow(2, size)]; - Random rand = new Random(); for (int i = 0; i < randomScores.length; i++) { - randomScores[i] = rand.nextInt(maxScore) + 1; + randomScores[i] = RANDOM.nextInt(maxScore) + 1; } return randomScores; } - // A utility function to find Log n in base 2 + /** + * Calculates the logarithm base 2 of a number. + * + * @param n The number to calculate log2 for (must be a power of 2). + * @return The log2 of n. + */ private int log2(int n) { return (n == 1) ? 0 : log2(n / 2) + 1; } + /** + * Checks if a number is a power of 2. + * + * @param n The number to check. + * @return True if n is a power of 2, false otherwise. + */ + private boolean isPowerOfTwo(int n) { + return n > 0 && (n & (n - 1)) == 0; + } + + /** + * Sets the scores array for the game tree. + * + * @param scores The array of scores. Length must be a power of 2. + * @throws IllegalArgumentException if the scores array length is not a power of + * 2 + */ public void setScores(int[] scores) { - if (scores.length % 1 == 0) { - this.scores = scores; - height = log2(this.scores.length); - } else { - System.out.println("The number of scores must be a power of 2."); + if (!isPowerOfTwo(scores.length)) { + throw new IllegalArgumentException("The number of scores must be a power of 2."); } + this.scores = Arrays.copyOf(scores, scores.length); + height = log2(this.scores.length); } + /** + * Returns a copy of the scores array. + * + * @return A copy of the scores array. + */ public int[] getScores() { - return scores; + return Arrays.copyOf(scores, scores.length); } + /** + * Returns the height of the game tree. + * + * @return The height of the game tree (log2 of the number of leaf nodes). + */ public int getHeight() { return height; } diff --git a/src/main/java/com/thealgorithms/others/MosAlgorithm.java b/src/main/java/com/thealgorithms/others/MosAlgorithm.java new file mode 100644 index 000000000000..2d2778339a7a --- /dev/null +++ b/src/main/java/com/thealgorithms/others/MosAlgorithm.java @@ -0,0 +1,260 @@ +package com.thealgorithms.others; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * Mo's Algorithm (Square Root Decomposition) for offline range queries + * + * Mo's Algorithm is used to answer range queries efficiently when: + * 1. Queries can be processed offline (all queries known beforehand) + * 2. We can efficiently add/remove elements from current range + * 3. The problem has optimal substructure for range operations + * + * Time Complexity: O((N + Q) * sqrt(N)) where N = array size, Q = number of queries + * Space Complexity: O(N + Q) + * + * @see Mo's Algorithm + * @author BEASTSHRIRAM + */ +public final class MosAlgorithm { + + /** + * Query structure to store range queries + */ + public static class Query { + public final int left; + public final int right; + public final int index; // Original index of query + public int result; // Result of the query + + public Query(int left, int right, int index) { + this.left = left; + this.right = right; + this.index = index; + this.result = 0; + } + } + + private MosAlgorithm() { + // Utility class + } + + /** + * Solves range sum queries using Mo's Algorithm + * + * @param arr the input array + * @param queries array of queries to process + * @return array of results corresponding to each query + */ + public static int[] solveRangeSumQueries(int[] arr, Query[] queries) { + if (arr == null || queries == null || arr.length == 0) { + return new int[0]; + } + + int n = arr.length; + int blockSize = (int) Math.sqrt(n); + + // Sort queries using Mo's ordering + Arrays.sort(queries, new MoComparator(blockSize)); + + // Initialize variables for current range + int currentLeft = 0; + int currentRight = -1; + int currentSum = 0; + + // Process each query + for (Query query : queries) { + // Expand or shrink the current range to match query range + + // Expand right boundary + while (currentRight < query.right) { + currentRight++; + currentSum += arr[currentRight]; + } + + // Shrink right boundary + while (currentRight > query.right) { + currentSum -= arr[currentRight]; + currentRight--; + } + + // Expand left boundary + while (currentLeft < query.left) { + currentSum -= arr[currentLeft]; + currentLeft++; + } + + // Shrink left boundary + while (currentLeft > query.left) { + currentLeft--; + currentSum += arr[currentLeft]; + } + + // Store the result + query.result = currentSum; + } + + // Extract results in original query order + int[] results = new int[queries.length]; + for (Query query : queries) { + results[query.index] = query.result; + } + + return results; + } + + /** + * Solves range frequency queries using Mo's Algorithm + * Example: Count occurrences of a specific value in range [L, R] + * + * @param arr the input array + * @param queries array of queries to process + * @param targetValue the value to count in each range + * @return array of results corresponding to each query + */ + public static int[] solveRangeFrequencyQueries(int[] arr, Query[] queries, int targetValue) { + if (arr == null || queries == null || arr.length == 0) { + return new int[0]; + } + + int n = arr.length; + int blockSize = (int) Math.sqrt(n); + + // Sort queries using Mo's ordering + Arrays.sort(queries, new MoComparator(blockSize)); + + // Initialize variables for current range + int currentLeft = 0; + int currentRight = -1; + int currentCount = 0; + + // Process each query + for (Query query : queries) { + // Expand right boundary + while (currentRight < query.right) { + currentRight++; + if (arr[currentRight] == targetValue) { + currentCount++; + } + } + + // Shrink right boundary + while (currentRight > query.right) { + if (arr[currentRight] == targetValue) { + currentCount--; + } + currentRight--; + } + + // Expand left boundary + while (currentLeft < query.left) { + if (arr[currentLeft] == targetValue) { + currentCount--; + } + currentLeft++; + } + + // Shrink left boundary + while (currentLeft > query.left) { + currentLeft--; + if (arr[currentLeft] == targetValue) { + currentCount++; + } + } + + // Store the result + query.result = currentCount; + } + + // Extract results in original query order + int[] results = new int[queries.length]; + for (Query query : queries) { + results[query.index] = query.result; + } + + return results; + } + + /** + * Comparator for Mo's Algorithm query ordering + * Queries are sorted by block of left endpoint, then by right endpoint + */ + private static class MoComparator implements Comparator { + private final int blockSize; + + MoComparator(int blockSize) { + this.blockSize = blockSize; + } + + @Override + public int compare(Query a, Query b) { + int blockA = a.left / blockSize; + int blockB = b.left / blockSize; + + if (blockA != blockB) { + return Integer.compare(blockA, blockB); + } + + // For odd blocks, sort right in ascending order + // For even blocks, sort right in descending order (optimization) + if ((blockA & 1) == 1) { + return Integer.compare(a.right, b.right); + } else { + return Integer.compare(b.right, a.right); + } + } + } + + /** + * Demo method showing usage of Mo's Algorithm + * + * @param args command line arguments + */ + public static void main(String[] args) { + // Example: Range sum queries + int[] arr = {1, 3, 5, 2, 7, 6, 3, 1, 4, 8}; + + Query[] queries = { + new Query(0, 2, 0), // Sum of elements from index 0 to 2: 1+3+5 = 9 + new Query(1, 4, 1), // Sum of elements from index 1 to 4: 3+5+2+7 = 17 + new Query(2, 6, 2), // Sum of elements from index 2 to 6: 5+2+7+6+3 = 23 + new Query(3, 8, 3) // Sum of elements from index 3 to 8: 2+7+6+3+1+4 = 23 + }; + + System.out.println("Array: " + Arrays.toString(arr)); + System.out.println("Range Sum Queries:"); + + // Store original queries for display + Query[] originalQueries = new Query[queries.length]; + for (int i = 0; i < queries.length; i++) { + originalQueries[i] = new Query(queries[i].left, queries[i].right, queries[i].index); + } + + int[] results = solveRangeSumQueries(arr, queries); + + for (int i = 0; i < originalQueries.length; i++) { + System.out.printf("Query %d: Sum of range [%d, %d] = %d%n", i, originalQueries[i].left, originalQueries[i].right, results[i]); + } + + // Example: Range frequency queries + System.out.println("\nRange Frequency Queries (count of value 3):"); + Query[] freqQueries = { + new Query(0, 5, 0), // Count of 3 in range [0, 5]: 1 occurrence + new Query(2, 8, 1), // Count of 3 in range [2, 8]: 2 occurrences + new Query(6, 9, 2) // Count of 3 in range [6, 9]: 1 occurrence + }; + + // Store original frequency queries for display + Query[] originalFreqQueries = new Query[freqQueries.length]; + for (int i = 0; i < freqQueries.length; i++) { + originalFreqQueries[i] = new Query(freqQueries[i].left, freqQueries[i].right, freqQueries[i].index); + } + + int[] freqResults = solveRangeFrequencyQueries(arr, freqQueries, 3); + + for (int i = 0; i < originalFreqQueries.length; i++) { + System.out.printf("Query %d: Count of 3 in range [%d, %d] = %d%n", i, originalFreqQueries[i].left, originalFreqQueries[i].right, freqResults[i]); + } + } +} diff --git a/src/main/java/com/thealgorithms/others/PageRank.java b/src/main/java/com/thealgorithms/others/PageRank.java index c7be7a9882bc..2899b80bcee8 100644 --- a/src/main/java/com/thealgorithms/others/PageRank.java +++ b/src/main/java/com/thealgorithms/others/PageRank.java @@ -2,94 +2,306 @@ import java.util.Scanner; -class PageRank { +/** + * PageRank Algorithm Implementation + * + *

+ * The PageRank algorithm is used by Google Search to rank web pages in their + * search engine + * results. It was named after Larry Page, one of the founders of Google. + * PageRank is a way of + * measuring the importance of website pages. + * + *

+ * Algorithm: 1. Initialize PageRank values for all pages to 1/N (where N is the + * total number + * of pages) 2. For each iteration: - For each page, calculate the new PageRank + * by summing the + * contributions from all incoming links - Apply the damping factor: PR(page) = + * (1-d) + d * + * sum(PR(incoming_page) / outgoing_links(incoming_page)) 3. Repeat until + * convergence + * + * @see PageRank Algorithm + */ +public final class PageRank { + private static final int MAX_NODES = 10; + private static final double DEFAULT_DAMPING_FACTOR = 0.85; + private static final int DEFAULT_ITERATIONS = 2; + + private int[][] adjacencyMatrix; + private double[] pageRankValues; + private int nodeCount; + + /** + * Constructor to initialize PageRank with specified number of nodes + * + * @param numberOfNodes the number of nodes/pages in the graph + * @throws IllegalArgumentException if numberOfNodes is less than 1 or greater + * than MAX_NODES + */ + public PageRank(int numberOfNodes) { + if (numberOfNodes < 1 || numberOfNodes > MAX_NODES) { + throw new IllegalArgumentException("Number of nodes must be between 1 and " + MAX_NODES); + } + this.nodeCount = numberOfNodes; + this.adjacencyMatrix = new int[MAX_NODES][MAX_NODES]; + this.pageRankValues = new double[MAX_NODES]; + } + + /** + * Default constructor for interactive mode + */ + public PageRank() { + this.adjacencyMatrix = new int[MAX_NODES][MAX_NODES]; + this.pageRankValues = new double[MAX_NODES]; + } + + /** + * Main method for interactive PageRank calculation + * + * @param args command line arguments (not used) + */ public static void main(String[] args) { - int nodes; - int i; - int j; - Scanner in = new Scanner(System.in); - System.out.print("Enter the Number of WebPages: "); - nodes = in.nextInt(); - PageRank p = new PageRank(); - System.out.println("Enter the Adjacency Matrix with 1->PATH & 0->NO PATH Between two WebPages: "); - for (i = 1; i <= nodes; i++) { - for (j = 1; j <= nodes; j++) { - p.path[i][j] = in.nextInt(); - if (j == i) { - p.path[i][j] = 0; + try (Scanner scanner = new Scanner(System.in)) { + System.out.print("Enter the Number of WebPages: "); + int nodes = scanner.nextInt(); + + PageRank pageRank = new PageRank(nodes); + System.out.println("Enter the Adjacency Matrix with 1->PATH & 0->NO PATH Between two WebPages: "); + + for (int i = 1; i <= nodes; i++) { + for (int j = 1; j <= nodes; j++) { + int value = scanner.nextInt(); + pageRank.setEdge(i, j, value); } } + + pageRank.calculatePageRank(nodes, DEFAULT_DAMPING_FACTOR, DEFAULT_ITERATIONS, true); } - p.calc(nodes); } - public int[][] path = new int[10][10]; - public double[] pagerank = new double[10]; + /** + * Sets an edge in the adjacency matrix + * + * @param from source node (1-indexed) + * @param to destination node (1-indexed) + * @param value 1 if edge exists, 0 otherwise + */ + public void setEdge(int from, int to, int value) { + if (from == to) { + adjacencyMatrix[from][to] = 0; // No self-loops + } else { + adjacencyMatrix[from][to] = value; + } + } - public void calc(double totalNodes) { - double initialPageRank; - double outgoingLinks = 0; - double dampingFactor = 0.85; - double[] tempPageRank = new double[10]; - int externalNodeNumber; - int internalNodeNumber; - int k = 1; // For Traversing - int iterationStep = 1; - initialPageRank = 1 / totalNodes; - System.out.printf(" Total Number of Nodes :" + totalNodes + "\t Initial PageRank of All Nodes :" + initialPageRank + "\n"); + /** + * Sets the adjacency matrix for the graph + * + * @param matrix the adjacency matrix (1-indexed) + */ + public void setAdjacencyMatrix(int[][] matrix) { + for (int i = 1; i <= nodeCount; i++) { + for (int j = 1; j <= nodeCount; j++) { + setEdge(i, j, matrix[i][j]); + } + } + } - // 0th ITERATION _ OR _ INITIALIZATION PHASE // - for (k = 1; k <= totalNodes; k++) { - this.pagerank[k] = initialPageRank; + /** + * Gets the PageRank value for a specific node + * + * @param node the node index (1-indexed) + * @return the PageRank value + */ + public double getPageRank(int node) { + if (node < 1 || node > nodeCount) { + throw new IllegalArgumentException("Node index out of bounds"); } - System.out.print("\n Initial PageRank Values , 0th Step \n"); + return pageRankValues[node]; + } + + /** + * Gets all PageRank values + * + * @return array of PageRank values (1-indexed) + */ + public double[] getAllPageRanks() { + return pageRankValues.clone(); + } + + /** + * Calculates PageRank using the default damping factor and iterations + * + * @param totalNodes the total number of nodes + * @return array of PageRank values + */ + public double[] calculatePageRank(int totalNodes) { + return calculatePageRank(totalNodes, DEFAULT_DAMPING_FACTOR, DEFAULT_ITERATIONS, false); + } + + /** + * Calculates PageRank with custom parameters + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor (typically 0.85) + * @param iterations number of iterations to perform + * @param verbose whether to print detailed output + * @return array of PageRank values + */ + public double[] calculatePageRank(int totalNodes, double dampingFactor, int iterations, boolean verbose) { + validateInputParameters(totalNodes, dampingFactor, iterations); - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); + this.nodeCount = totalNodes; + double initialPageRank = 1.0 / totalNodes; + + if (verbose) { + System.out.printf("Total Number of Nodes: %d\tInitial PageRank of All Nodes: %.6f%n", totalNodes, initialPageRank); } - while (iterationStep <= 2) { // Iterations - // Store the PageRank for All Nodes in Temporary Array - for (k = 1; k <= totalNodes; k++) { - tempPageRank[k] = this.pagerank[k]; - this.pagerank[k] = 0; - } + initializePageRanks(totalNodes, initialPageRank, verbose); + performIterations(totalNodes, dampingFactor, iterations, verbose); - for (internalNodeNumber = 1; internalNodeNumber <= totalNodes; internalNodeNumber++) { - for (externalNodeNumber = 1; externalNodeNumber <= totalNodes; externalNodeNumber++) { - if (this.path[externalNodeNumber][internalNodeNumber] == 1) { - k = 1; - outgoingLinks = 0; // Count the Number of Outgoing Links for each externalNodeNumber - while (k <= totalNodes) { - if (this.path[externalNodeNumber][k] == 1) { - outgoingLinks = outgoingLinks + 1; // Counter for Outgoing Links - } - k = k + 1; - } - // Calculate PageRank - this.pagerank[internalNodeNumber] += tempPageRank[externalNodeNumber] * (1 / outgoingLinks); - } - } - System.out.printf("\n After " + iterationStep + "th Step \n"); + if (verbose) { + System.out.println("\nFinal PageRank:"); + printPageRanks(totalNodes); + } - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); - } + return pageRankValues.clone(); + } + + /** + * Validates input parameters for PageRank calculation + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + * @param iterations number of iterations + * @throws IllegalArgumentException if parameters are invalid + */ + private void validateInputParameters(int totalNodes, double dampingFactor, int iterations) { + if (totalNodes < 1 || totalNodes > MAX_NODES) { + throw new IllegalArgumentException("Total nodes must be between 1 and " + MAX_NODES); + } + if (dampingFactor < 0 || dampingFactor > 1) { + throw new IllegalArgumentException("Damping factor must be between 0 and 1"); + } + if (iterations < 1) { + throw new IllegalArgumentException("Iterations must be at least 1"); + } + } + + /** + * Initializes PageRank values for all nodes + * + * @param totalNodes the total number of nodes + * @param initialPageRank the initial PageRank value + * @param verbose whether to print output + */ + private void initializePageRanks(int totalNodes, double initialPageRank, boolean verbose) { + for (int i = 1; i <= totalNodes; i++) { + pageRankValues[i] = initialPageRank; + } + + if (verbose) { + System.out.println("\nInitial PageRank Values, 0th Step"); + printPageRanks(totalNodes); + } + } - iterationStep = iterationStep + 1; + /** + * Performs the iterative PageRank calculation + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + * @param iterations number of iterations + * @param verbose whether to print output + */ + private void performIterations(int totalNodes, double dampingFactor, int iterations, boolean verbose) { + for (int iteration = 1; iteration <= iterations; iteration++) { + double[] tempPageRank = storeCurrentPageRanks(totalNodes); + calculateNewPageRanks(totalNodes, tempPageRank); + applyDampingFactor(totalNodes, dampingFactor); + + if (verbose) { + System.out.printf("%nAfter %d iteration(s)%n", iteration); + printPageRanks(totalNodes); } + } + } - // Add the Damping Factor to PageRank - for (k = 1; k <= totalNodes; k++) { - this.pagerank[k] = (1 - dampingFactor) + dampingFactor * this.pagerank[k]; + /** + * Stores current PageRank values in a temporary array + * + * @param totalNodes the total number of nodes + * @return temporary array with current PageRank values + */ + private double[] storeCurrentPageRanks(int totalNodes) { + double[] tempPageRank = new double[MAX_NODES]; + for (int i = 1; i <= totalNodes; i++) { + tempPageRank[i] = pageRankValues[i]; + pageRankValues[i] = 0; + } + return tempPageRank; + } + + /** + * Calculates new PageRank values based on incoming links + * + * @param totalNodes the total number of nodes + * @param tempPageRank temporary array with previous PageRank values + */ + private void calculateNewPageRanks(int totalNodes, double[] tempPageRank) { + for (int targetNode = 1; targetNode <= totalNodes; targetNode++) { + for (int sourceNode = 1; sourceNode <= totalNodes; sourceNode++) { + if (adjacencyMatrix[sourceNode][targetNode] == 1) { + int outgoingLinks = countOutgoingLinks(sourceNode, totalNodes); + if (outgoingLinks > 0) { + pageRankValues[targetNode] += tempPageRank[sourceNode] / outgoingLinks; + } + } } + } + } - // Display PageRank - System.out.print("\n Final Page Rank : \n"); - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); + /** + * Applies the damping factor to all PageRank values + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + */ + private void applyDampingFactor(int totalNodes, double dampingFactor) { + for (int i = 1; i <= totalNodes; i++) { + pageRankValues[i] = (1 - dampingFactor) + dampingFactor * pageRankValues[i]; + } + } + + /** + * Counts the number of outgoing links from a node + * + * @param node the source node (1-indexed) + * @param totalNodes total number of nodes + * @return the count of outgoing links + */ + private int countOutgoingLinks(int node, int totalNodes) { + int count = 0; + for (int i = 1; i <= totalNodes; i++) { + if (adjacencyMatrix[node][i] == 1) { + count++; } } + return count; + } + + /** + * Prints the PageRank values for all nodes + * + * @param totalNodes the total number of nodes + */ + private void printPageRanks(int totalNodes) { + for (int i = 1; i <= totalNodes; i++) { + System.out.printf("PageRank of %d: %.6f%n", i, pageRankValues[i]); + } } } diff --git a/src/main/java/com/thealgorithms/others/PerlinNoise.java b/src/main/java/com/thealgorithms/others/PerlinNoise.java index e6551ed6b9ee..d97e3395ff18 100644 --- a/src/main/java/com/thealgorithms/others/PerlinNoise.java +++ b/src/main/java/com/thealgorithms/others/PerlinNoise.java @@ -4,99 +4,156 @@ import java.util.Scanner; /** - * For detailed info and implementation see: Perlin-Noise + * Utility for generating 2D value-noise blended across octaves (commonly known + * as Perlin-like noise). + * + *

+ * The implementation follows the classic approach of: + *

    + *
  1. Generate a base grid of random values in [0, 1).
  2. + *
  3. For each octave k, compute a layer by bilinear interpolation of the base + * grid + * at period 2^k.
  4. + *
  5. Blend all layers from coarse to fine using a geometric series of + * amplitudes + * controlled by {@code persistence}, then normalize to [0, 1].
  6. + *
+ * + *

+ * For background see: + * Perlin Noise. + * + *

+ * Constraints and notes: + *

    + *
  • {@code width} and {@code height} should be positive.
  • + *
  • {@code octaveCount} must be at least 1 (0 would lead to a division by + * zero).
  • + *
  • {@code persistence} should be in (0, 1], typical values around + * 0.5–0.8.
  • + *
  • Given the same seed and parameters, results are deterministic.
  • + *
*/ + public final class PerlinNoise { private PerlinNoise() { } /** - * @param width width of noise array - * @param height height of noise array - * @param octaveCount numbers of layers used for blending noise - * @param persistence value of impact each layer get while blending - * @param seed used for randomizer - * @return float array containing calculated "Perlin-Noise" values + * Generate a 2D array of blended noise values normalized to [0, 1]. + * + * @param width width of the noise array (columns) + * @param height height of the noise array (rows) + * @param octaveCount number of octaves (layers) to blend; must be >= 1 + * @param persistence per-octave amplitude multiplier in (0, 1] + * @param seed seed for the random base grid + * @return a {@code width x height} array containing blended noise values in [0, + * 1] */ static float[][] generatePerlinNoise(int width, int height, int octaveCount, float persistence, long seed) { - final float[][] base = new float[width][height]; - final float[][] perlinNoise = new float[width][height]; - final float[][][] noiseLayers = new float[octaveCount][][]; + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width and height must be > 0"); + } + if (octaveCount < 1) { + throw new IllegalArgumentException("octaveCount must be >= 1"); + } + if (!(persistence > 0f && persistence <= 1f)) { // using > to exclude 0 and NaN + throw new IllegalArgumentException("persistence must be in (0, 1]"); + } + final float[][] base = createBaseGrid(width, height, seed); + final float[][][] layers = createLayers(base, width, height, octaveCount); + return blendAndNormalize(layers, width, height, persistence); + } + + /** Create the base random lattice values in [0,1). */ + static float[][] createBaseGrid(int width, int height, long seed) { + final float[][] base = new float[width][height]; Random random = new Random(seed); - // fill base array with random values as base for noise for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { base[x][y] = random.nextFloat(); } } + return base; + } - // calculate octaves with different roughness + /** Pre-compute each octave layer at increasing frequency. */ + static float[][][] createLayers(float[][] base, int width, int height, int octaveCount) { + final float[][][] noiseLayers = new float[octaveCount][][]; for (int octave = 0; octave < octaveCount; octave++) { noiseLayers[octave] = generatePerlinNoiseLayer(base, width, height, octave); } + return noiseLayers; + } + /** Blend layers using geometric amplitudes and normalize to [0,1]. */ + static float[][] blendAndNormalize(float[][][] layers, int width, int height, float persistence) { + final int octaveCount = layers.length; + final float[][] out = new float[width][height]; float amplitude = 1f; float totalAmplitude = 0f; - // calculate perlin noise by blending each layer together with specific persistence for (int octave = octaveCount - 1; octave >= 0; octave--) { amplitude *= persistence; totalAmplitude += amplitude; - + final float[][] layer = layers[octave]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - // adding each value of the noise layer to the noise - // by increasing amplitude the rougher noises will have more impact - perlinNoise[x][y] += noiseLayers[octave][x][y] * amplitude; + out[x][y] += layer[x][y] * amplitude; } } } - // normalize values so that they stay between 0..1 + if (totalAmplitude <= 0f || Float.isInfinite(totalAmplitude) || Float.isNaN(totalAmplitude)) { + throw new IllegalStateException("Invalid totalAmplitude computed during normalization"); + } + + final float invTotal = 1f / totalAmplitude; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - perlinNoise[x][y] /= totalAmplitude; + out[x][y] *= invTotal; } } - - return perlinNoise; + return out; } /** - * @param base base random float array - * @param width width of noise array + * Generate a single octave layer by bilinear interpolation of a base grid at a + * given octave (period = 2^octave). + * + * @param base base random float array of size {@code width x height} + * @param width width of noise array * @param height height of noise array - * @param octave current layer - * @return float array containing calculated "Perlin-Noise-Layer" values + * @param octave current octave (0 for period 1, 1 for period 2, ...) + * @return float array containing the octave's interpolated values */ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, int octave) { float[][] perlinNoiseLayer = new float[width][height]; - // calculate period (wavelength) for different shapes + // Calculate period (wavelength) for different shapes. int period = 1 << octave; // 2^k float frequency = 1f / period; // 1/2^k for (int x = 0; x < width; x++) { - // calculates the horizontal sampling indices + // Calculate the horizontal sampling indices. int x0 = (x / period) * period; int x1 = (x0 + period) % width; - float horizintalBlend = (x - x0) * frequency; + float horizontalBlend = (x - x0) * frequency; for (int y = 0; y < height; y++) { - // calculates the vertical sampling indices + // Calculate the vertical sampling indices. int y0 = (y / period) * period; int y1 = (y0 + period) % height; float verticalBlend = (y - y0) * frequency; - // blend top corners - float top = interpolate(base[x0][y0], base[x1][y0], horizintalBlend); + // Blend top corners. + float top = interpolate(base[x0][y0], base[x1][y0], horizontalBlend); - // blend bottom corners - float bottom = interpolate(base[x0][y1], base[x1][y1], horizintalBlend); + // Blend bottom corners. + float bottom = interpolate(base[x0][y1], base[x1][y1], horizontalBlend); - // blend top and bottom interpolation to get the final blend value for this cell + // Blend top and bottom interpolation to get the final value for this cell. perlinNoiseLayer[x][y] = interpolate(top, bottom, verticalBlend); } } @@ -105,16 +162,21 @@ static float[][] generatePerlinNoiseLayer(float[][] base, int width, int height, } /** - * @param a value of point a - * @param b value of point b - * @param alpha determine which value has more impact (closer to 0 -> a, - * closer to 1 -> b) - * @return interpolated value + * Linear interpolation between two values. + * + * @param a value at alpha = 0 + * @param b value at alpha = 1 + * @param alpha interpolation factor in [0, 1] + * @return interpolated value {@code (1 - alpha) * a + alpha * b} */ static float interpolate(float a, float b, float alpha) { return a * (1 - alpha) + alpha * b; } + /** + * Small demo that prints a text representation of the noise using a provided + * character set. + */ public static void main(String[] args) { Scanner in = new Scanner(System.in); @@ -148,7 +210,7 @@ public static void main(String[] args) { final char[] chars = charset.toCharArray(); final int length = chars.length; final float step = 1f / length; - // output based on charset + // Output based on charset thresholds. for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { float value = step; diff --git a/src/main/java/com/thealgorithms/others/PrintAMatrixInSpiralOrder.java b/src/main/java/com/thealgorithms/others/PrintAMatrixInSpiralOrder.java deleted file mode 100644 index abfdd006879e..000000000000 --- a/src/main/java/com/thealgorithms/others/PrintAMatrixInSpiralOrder.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.thealgorithms.others; - -import java.util.ArrayList; -import java.util.List; - -public class PrintAMatrixInSpiralOrder { - /** - * Search a key in row and column wise sorted matrix - * - * @param matrix matrix to be searched - * @param row number of rows matrix has - * @param col number of columns matrix has - * @author Sadiul Hakim : https://github.com/sadiul-hakim - */ - public List print(int[][] matrix, int row, int col) { - // r traverses matrix row wise from first - int r = 0; - // c traverses matrix column wise from first - int c = 0; - int i; - List result = new ArrayList<>(); - while (r < row && c < col) { - // print first row of matrix - for (i = c; i < col; i++) { - result.add(matrix[r][i]); - } - // increase r by one because first row printed - r++; - // print last column - for (i = r; i < row; i++) { - result.add(matrix[i][col - 1]); - } - // decrease col by one because last column has been printed - col--; - // print rows from last except printed elements - if (r < row) { - for (i = col - 1; i >= c; i--) { - result.add(matrix[row - 1][i]); - } - row--; - } - // print columns from first except printed elements - if (c < col) { - for (i = row - 1; i >= r; i--) { - result.add(matrix[i][c]); - } - c++; - } - } - return result; - } -} diff --git a/src/main/java/com/thealgorithms/physics/CoulombsLaw.java b/src/main/java/com/thealgorithms/physics/CoulombsLaw.java new file mode 100644 index 000000000000..3a3ad3e0d223 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/CoulombsLaw.java @@ -0,0 +1,80 @@ +package com.thealgorithms.physics; + +/** + * Implements Coulomb's Law for electrostatics. + * Provides simple static methods to calculate electrostatic force and circular orbit velocity. + * + * @author [Priyanshu Singh](https://github.com/Priyanshu1303d) + * @see Wikipedia + */ +public final class CoulombsLaw { + + /** Coulomb's constant in N·m²/C² */ + public static final double COULOMBS_CONSTANT = 8.9875517923e9; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private CoulombsLaw() { + } + + /** + * Calculates the electrostatic force vector exerted by one charge on another. + * The returned vector is the force *on* the second charge (q2). + * + * @param q1 Charge of the first particle (in Coulombs). + * @param x1 X-position of the first particle (m). + * @param y1 Y-position of the first particle (m). + * @param q2 Charge of the second particle (in Coulombs). + * @param x2 X-position of the second particle (m). + * @param y2 Y-position of the second particle (m). + * @return A double array `[fx, fy]` representing the force vector on the second charge. + */ + public static double[] calculateForceVector(double q1, double x1, double y1, double q2, double x2, double y2) { + // Vector from 1 to 2 + double dx = x2 - x1; + double dy = y2 - y1; + double distanceSq = dx * dx + dy * dy; + + // If particles are at the same position, force is zero to avoid division by zero. + if (distanceSq == 0) { + return new double[] {0, 0}; + } + + double distance = Math.sqrt(distanceSq); + // Force magnitude: k * (q1 * q2) / r^2 + // A positive result is repulsive (pushes q2 away from q1). + // A negative result is attractive (pulls q2 toward q1). + double forceMagnitude = COULOMBS_CONSTANT * q1 * q2 / distanceSq; + + // Calculate the components of the force vector + // (dx / distance) is the unit vector pointing from 1 to 2. + double fx = forceMagnitude * (dx / distance); + double fy = forceMagnitude * (dy / distance); + + return new double[] {fx, fy}; + } + + /** + * Calculates the speed required for a stable circular orbit of a charged particle + * around a central charge (e.g., an electron orbiting a nucleus). + * + * @param centralCharge The charge of the central body (in Coulombs). + * @param orbitingCharge The charge of the orbiting body (in Coulombs). + * @param orbitingMass The mass of the orbiting body (in kg). + * @param radius The radius of the orbit (in m). + * @return The orbital speed (in m/s). + * @throws IllegalArgumentException if mass or radius are not positive. + */ + public static double calculateCircularOrbitVelocity(double centralCharge, double orbitingCharge, double orbitingMass, double radius) { + if (orbitingMass <= 0 || radius <= 0) { + throw new IllegalArgumentException("Orbiting mass and radius must be positive."); + } + + // We only need the magnitude of the force, which is always positive. + double forceMagnitude = Math.abs(COULOMBS_CONSTANT * centralCharge * orbitingCharge) / (radius * radius); + + // F_c = m * v^2 / r => v = sqrt(F_c * r / m) + return Math.sqrt(forceMagnitude * radius / orbitingMass); + } +} diff --git a/src/main/java/com/thealgorithms/physics/DampedOscillator.java b/src/main/java/com/thealgorithms/physics/DampedOscillator.java new file mode 100644 index 000000000000..84028b628e77 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/DampedOscillator.java @@ -0,0 +1,109 @@ +package com.thealgorithms.physics; + +/** + * Models a damped harmonic oscillator, capturing the behavior of a mass-spring-damper system. + * + *

The system is defined by the second-order differential equation: + * x'' + 2 * gamma * x' + omega₀² * x = 0 + * where: + *

    + *
  • omega₀ is the natural (undamped) angular frequency in radians per second.
  • + *
  • gamma is the damping coefficient in inverse seconds.
  • + *
+ * + *

This implementation provides: + *

    + *
  • An analytical solution for the underdamped case (γ < ω₀).
  • + *
  • A numerical integrator based on the explicit Euler method for simulation purposes.
  • + *
+ * + *

Usage Example: + *

{@code
+ * DampedOscillator oscillator = new DampedOscillator(10.0, 0.5);
+ * double displacement = oscillator.displacementAnalytical(1.0, 0.0, 0.1);
+ * double[] nextState = oscillator.stepEuler(new double[]{1.0, 0.0}, 0.001);
+ * }
+ * + * @author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class DampedOscillator { + + /** Natural (undamped) angular frequency (rad/s). */ + private final double omega0; + + /** Damping coefficient (s⁻¹). */ + private final double gamma; + + private DampedOscillator() { + throw new AssertionError("No instances."); + } + + /** + * Constructs a damped oscillator model. + * + * @param omega0 the natural frequency (rad/s), must be positive + * @param gamma the damping coefficient (s⁻¹), must be non-negative + * @throws IllegalArgumentException if parameters are invalid + */ + public DampedOscillator(double omega0, double gamma) { + if (omega0 <= 0) { + throw new IllegalArgumentException("Natural frequency must be positive."); + } + if (gamma < 0) { + throw new IllegalArgumentException("Damping coefficient must be non-negative."); + } + this.omega0 = omega0; + this.gamma = gamma; + } + + /** + * Computes the analytical displacement of an underdamped oscillator. + * Formula: x(t) = A * exp(-γt) * cos(ω_d t + φ) + * + * @param amplitude the initial amplitude A + * @param phase the initial phase φ (radians) + * @param time the time t (seconds) + * @return the displacement x(t) + */ + public double displacementAnalytical(double amplitude, double phase, double time) { + double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma)); + return amplitude * Math.exp(-gamma * time) * Math.cos(omegaD * time + phase); + } + + /** + * Performs a single integration step using the explicit Euler method. + * State vector format: [x, v], where v = dx/dt. + * + * @param state the current state [x, v] + * @param dt the time step (seconds) + * @return the next state [x_next, v_next] + * @throws IllegalArgumentException if the state array is invalid or dt is non-positive + */ + public double[] stepEuler(double[] state, double dt) { + if (state == null || state.length != 2) { + throw new IllegalArgumentException("State must be a non-null array of length 2."); + } + if (dt <= 0) { + throw new IllegalArgumentException("Time step must be positive."); + } + + double x = state[0]; + double v = state[1]; + double acceleration = -2.0 * gamma * v - omega0 * omega0 * x; + + double xNext = x + dt * v; + double vNext = v + dt * acceleration; + + return new double[] {xNext, vNext}; + } + + /** @return the natural (undamped) angular frequency (rad/s). */ + public double getOmega0() { + return omega0; + } + + /** @return the damping coefficient (s⁻¹). */ + public double getGamma() { + return gamma; + } +} diff --git a/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java new file mode 100644 index 000000000000..399c3f1e041f --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/ElasticCollision2D.java @@ -0,0 +1,73 @@ +package com.thealgorithms.physics; + +/** + * 2D Elastic collision between two circular bodies + * Based on principles of conservation of momentum and kinetic energy. + * + * @author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class ElasticCollision2D { + + private ElasticCollision2D() { + throw new AssertionError("No instances. Utility class"); + } + + public static class Body { + public double x; + public double y; + public double vx; + public double vy; + public double mass; + public double radius; + + public Body(double x, double y, double vx, double vy, double mass, double radius) { + this.x = x; + this.y = y; + this.vx = vx; + this.vy = vy; + this.mass = mass; + this.radius = radius; + } + } + + /** + * Resolve instantaneous elastic collision between two circular bodies. + * + * @param a first body + * @param b second body + */ + public static void resolveCollision(Body a, Body b) { + double dx = b.x - a.x; + double dy = b.y - a.y; + double dist = Math.hypot(dx, dy); + + if (dist == 0) { + return; // overlapping + } + + double nx = dx / dist; + double ny = dy / dist; + + // relative velocity along normal + double rv = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny; + + if (rv > 0) { + return; // moving apart + } + + // impulse with masses + double m1 = a.mass; + double m2 = b.mass; + + double j = -(1 + 1.0) * rv / (1.0 / m1 + 1.0 / m2); + + // impulse vector + double impulseX = j * nx; + double impulseY = j * ny; + + a.vx -= impulseX / m1; + a.vy -= impulseY / m1; + b.vx += impulseX / m2; + b.vy += impulseY / m2; + } +} diff --git a/src/main/java/com/thealgorithms/physics/Gravitation.java b/src/main/java/com/thealgorithms/physics/Gravitation.java new file mode 100644 index 000000000000..292fdc195f85 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/Gravitation.java @@ -0,0 +1,66 @@ +package com.thealgorithms.physics; + +/** + * Implements Newton's Law of Universal Gravitation. + * Provides simple static methods to calculate gravitational force and circular orbit velocity. + * + * @author [Priyanshu Kumar Singh](https://github.com/Priyanshu1303d) + * @see Wikipedia + */ +public final class Gravitation { + + /** Gravitational constant in m^3 kg^-1 s^-2 */ + public static final double GRAVITATIONAL_CONSTANT = 6.67430e-11; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Gravitation() { + } + + /** + * Calculates the gravitational force vector exerted by one body on another. + * + * @param m1 Mass of the first body (kg). + * @param x1 X-position of the first body (m). + * @param y1 Y-position of the first body (m). + * @param m2 Mass of the second body (kg). + * @param x2 X-position of the second body (m). + * @param y2 Y-position of the second body (m). + * @return A double array `[fx, fy]` representing the force vector on the second body. + */ + public static double[] calculateGravitationalForce(double m1, double x1, double y1, double m2, double x2, double y2) { + double dx = x1 - x2; + double dy = y1 - y2; + double distanceSq = dx * dx + dy * dy; + + // If bodies are at the same position, force is zero to avoid division by zero. + if (distanceSq == 0) { + return new double[] {0, 0}; + } + + double distance = Math.sqrt(distanceSq); + double forceMagnitude = GRAVITATIONAL_CONSTANT * m1 * m2 / distanceSq; + + // Calculate the components of the force vector + double fx = forceMagnitude * (dx / distance); + double fy = forceMagnitude * (dy / distance); + + return new double[] {fx, fy}; + } + + /** + * Calculates the speed required for a stable circular orbit. + * + * @param centralMass The mass of the central body (kg). + * @param radius The radius of the orbit (m). + * @return The orbital speed (m/s). + * @throws IllegalArgumentException if mass or radius are not positive. + */ + public static double calculateCircularOrbitVelocity(double centralMass, double radius) { + if (centralMass <= 0 || radius <= 0) { + throw new IllegalArgumentException("Mass and radius must be positive."); + } + return Math.sqrt(GRAVITATIONAL_CONSTANT * centralMass / radius); + } +} diff --git a/src/main/java/com/thealgorithms/physics/GroundToGroundProjectileMotion.java b/src/main/java/com/thealgorithms/physics/GroundToGroundProjectileMotion.java new file mode 100644 index 000000000000..a8d7ac63a186 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/GroundToGroundProjectileMotion.java @@ -0,0 +1,90 @@ +package com.thealgorithms.physics; + +/** + * Ground to ground projectile motion calculator + * + * Ground to ground projectile motion is when a projectile's trajectory + * starts at the ground, reaches the apex, then falls back on the ground. + * + * @author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class GroundToGroundProjectileMotion { + + private GroundToGroundProjectileMotion() { + throw new AssertionError("No instances."); + } + + /** Standard gravity constant (m/s^2) */ + private static final double GRAVITY = 9.80665; + + /** + * Convert degrees to radians + * + * @param degrees Angle in degrees + * @return Angle in radians + */ + private static double degreesToRadians(double degrees) { + return degrees * (Math.PI / 180.0); + } + + /** + * Calculate the time of flight + * + * @param initialVelocity The starting velocity of the projectile (m/s) + * @param angle The angle that the projectile is launched at in degrees + * @return The time that the projectile is in the air for (seconds) + */ + public static double timeOfFlight(double initialVelocity, double angle) { + return timeOfFlight(initialVelocity, angle, GRAVITY); + } + + /** + * Calculate the time of flight with custom gravity + * + * @param initialVelocity The starting velocity of the projectile (m/s) + * @param angle The angle that the projectile is launched at in degrees + * @param gravity The value used for the gravity constant (m/s^2) + * @return The time that the projectile is in the air for (seconds) + */ + public static double timeOfFlight(double initialVelocity, double angle, double gravity) { + double viy = initialVelocity * Math.sin(degreesToRadians(angle)); + return 2.0 * viy / gravity; + } + + /** + * Calculate the horizontal distance that the projectile travels + * + * @param initialVelocity The starting velocity of the projectile (m/s) + * @param angle The angle that the projectile is launched at in degrees + * @param time The time that the projectile is in the air (seconds) + * @return Horizontal distance that the projectile travels (meters) + */ + public static double horizontalRange(double initialVelocity, double angle, double time) { + double vix = initialVelocity * Math.cos(degreesToRadians(angle)); + return vix * time; + } + + /** + * Calculate the max height of the projectile + * + * @param initialVelocity The starting velocity of the projectile (m/s) + * @param angle The angle that the projectile is launched at in degrees + * @return The max height that the projectile reaches (meters) + */ + public static double maxHeight(double initialVelocity, double angle) { + return maxHeight(initialVelocity, angle, GRAVITY); + } + + /** + * Calculate the max height of the projectile with custom gravity + * + * @param initialVelocity The starting velocity of the projectile (m/s) + * @param angle The angle that the projectile is launched at in degrees + * @param gravity The value used for the gravity constant (m/s^2) + * @return The max height that the projectile reaches (meters) + */ + public static double maxHeight(double initialVelocity, double angle, double gravity) { + double viy = initialVelocity * Math.sin(degreesToRadians(angle)); + return Math.pow(viy, 2) / (2.0 * gravity); + } +} diff --git a/src/main/java/com/thealgorithms/physics/Kinematics.java b/src/main/java/com/thealgorithms/physics/Kinematics.java new file mode 100644 index 000000000000..d017fe787afd --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/Kinematics.java @@ -0,0 +1,69 @@ +package com.thealgorithms.physics; +/** + * Implements the fundamental "SUVAT" equations for motion + * under constant acceleration. + * + * @author [Priyanshu Kumar Singh](https://github.com/Priyanshu1303d) + * @see Wikipedia + */ +public final class Kinematics { + private Kinematics() { + } + + /** + * Calculates the final velocity (v) of an object. + * Formula: v = u + at + * + * @param u Initial velocity (m/s). + * @param a Constant acceleration (m/s^2). + * @param t Time elapsed (s). + * @return The final velocity (m/s). + */ + + public static double calculateFinalVelocity(double u, double a, double t) { + return u + a * t; + } + + /** + * Calculates the displacement (s) of an object. + * Formula: s = ut + 0.5 * a * t^2 + * + * @param u Initial velocity (m/s). + * @param a Constant acceleration (m/s^2). + * @param t Time elapsed (s). + * @return The displacement (m). + */ + + public static double calculateDisplacement(double u, double a, double t) { + return u * t + 0.5 * a * t * t; + } + + /** + * Calculates the displacement (s) of an object. + * Formula: v^2 = u^2 + 2 * a * s + * + * @param u Initial velocity (m/s). + * @param a Constant acceleration (m/s^2). + * @param s Displacement (m). + * @return The final velocity squared (m/s)^2. + */ + + public static double calculateFinalVelocitySquared(double u, double a, double s) { + return u * u + 2 * a * s; + } + + /** + * Calculates the displacement (s) using the average velocity. + * Formula: s = (u + v) / 2 * t + * + * @param u Initial velocity (m/s). + * @param v Final velocity (m/s). + * @param t Time elapsed (s). + * @return The displacement (m). + */ + + public static double calculateDisplacementFromVelocities(double u, double v, double t) { + double velocitySum = u + v; + return velocitySum / 2 * t; + } +} diff --git a/src/main/java/com/thealgorithms/physics/ProjectileMotion.java b/src/main/java/com/thealgorithms/physics/ProjectileMotion.java new file mode 100644 index 000000000000..cfc79547922c --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/ProjectileMotion.java @@ -0,0 +1,96 @@ +package com.thealgorithms.physics; + +/** + * + * This implementation calculates the flight path of a projectile launched from any INITIAL HEIGHT. + * It is a more flexible version of the ground-to-ground model. + * + * @see Wikipedia - Projectile Motion + * @author [Priyanshu Kumar Singh](https://github.com/Priyanshu1303d) + */ +public final class ProjectileMotion { + + private ProjectileMotion() { + } + + /** Standard Earth gravity constant*/ + private static final double GRAVITY = 9.80665; + + /** + * A simple container for the results of a projectile motion calculation. + */ + public static final class Result { + private final double timeOfFlight; + private final double horizontalRange; + private final double maxHeight; + + public Result(double timeOfFlight, double horizontalRange, double maxHeight) { + this.timeOfFlight = timeOfFlight; + this.horizontalRange = horizontalRange; + this.maxHeight = maxHeight; + } + + /** @return The total time the projectile is in the air (seconds). */ + public double getTimeOfFlight() { + return timeOfFlight; + } + + /** @return The total horizontal distance traveled (meters). */ + public double getHorizontalRange() { + return horizontalRange; + } + + /** @return The maximum vertical height from the ground (meters). */ + public double getMaxHeight() { + return maxHeight; + } + } + + /** + * Calculates projectile trajectory using standard Earth gravity. + * + * @param initialVelocity Initial speed of the projectile (m/s). + * @param launchAngleDegrees Launch angle from the horizontal (degrees). + * @param initialHeight Starting height of the projectile (m). + * @return A {@link Result} object with the trajectory data. + */ + public static Result calculateTrajectory(double initialVelocity, double launchAngleDegrees, double initialHeight) { + return calculateTrajectory(initialVelocity, launchAngleDegrees, initialHeight, GRAVITY); + } + + /** + * Calculates projectile trajectory with a custom gravity value. + * + * @param initialVelocity Initial speed (m/s). Must be non-negative. + * @param launchAngleDegrees Launch angle (degrees). + * @param initialHeight Starting height (m). Must be non-negative. + * @param gravity Acceleration due to gravity (m/s^2). Must be positive. + * @return A {@link Result} object with the trajectory data. + */ + public static Result calculateTrajectory(double initialVelocity, double launchAngleDegrees, double initialHeight, double gravity) { + if (initialVelocity < 0 || initialHeight < 0 || gravity <= 0) { + throw new IllegalArgumentException("Velocity, height, and gravity must be non-negative, and gravity must be positive."); + } + + double launchAngleRadians = Math.toRadians(launchAngleDegrees); + double initialVerticalVelocity = initialVelocity * Math.sin(launchAngleRadians); // Initial vertical velocity + double initialHorizontalVelocity = initialVelocity * Math.cos(launchAngleRadians); // Initial horizontal velocity + + // Correctly calculate total time of flight using the quadratic formula for vertical motion. + // y(t) = y0 + initialVerticalVelocity*t - 0.5*g*t^2. We solve for t when y(t) = 0. + double totalTimeOfFlight = (initialVerticalVelocity + Math.sqrt(initialVerticalVelocity * initialVerticalVelocity + 2 * gravity * initialHeight)) / gravity; + + // Calculate max height. If launched downwards, max height is the initial height. + double maxHeight; + if (initialVerticalVelocity > 0) { + double heightGained = initialVerticalVelocity * initialVerticalVelocity / (2 * gravity); + maxHeight = initialHeight + heightGained; + } else { + maxHeight = initialHeight; + } + + double horizontalRange = initialHorizontalVelocity * totalTimeOfFlight; + + return new Result(totalTimeOfFlight, horizontalRange, maxHeight); + } +} diff --git a/src/main/java/com/thealgorithms/physics/SimplePendulumRK4.java b/src/main/java/com/thealgorithms/physics/SimplePendulumRK4.java new file mode 100644 index 000000000000..6de69c103b5a --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/SimplePendulumRK4.java @@ -0,0 +1,126 @@ +package com.thealgorithms.physics; + +/** + * Simulates a simple pendulum using the Runge-Kutta 4th order method. + * The pendulum is modeled with the nonlinear differential equation. + * + * @author [Yash Rajput](https://github.com/the-yash-rajput) + */ +public final class SimplePendulumRK4 { + + private SimplePendulumRK4() { + throw new AssertionError("No instances."); + } + + private final double length; // meters + private final double g; // acceleration due to gravity (m/s^2) + + /** + * Constructs a simple pendulum simulator. + * + * @param length the length of the pendulum in meters + * @param g the acceleration due to gravity in m/s^2 + */ + public SimplePendulumRK4(double length, double g) { + if (length <= 0) { + throw new IllegalArgumentException("Length must be positive"); + } + if (g <= 0) { + throw new IllegalArgumentException("Gravity must be positive"); + } + this.length = length; + this.g = g; + } + + /** + * Computes the derivatives of the state vector. + * State: [theta, omega] where theta is angle and omega is angular velocity. + * + * @param state the current state [theta, omega] + * @return the derivatives [dtheta/dt, domega/dt] + */ + private double[] derivatives(double[] state) { + double theta = state[0]; + double omega = state[1]; + double dtheta = omega; + double domega = -(g / length) * Math.sin(theta); + return new double[] {dtheta, domega}; + } + + /** + * Performs one time step using the RK4 method. + * + * @param state the current state [theta, omega] + * @param dt the time step size + * @return the new state after time dt + */ + public double[] stepRK4(double[] state, double dt) { + if (state == null || state.length != 2) { + throw new IllegalArgumentException("State must be array of length 2"); + } + if (dt <= 0) { + throw new IllegalArgumentException("Time step must be positive"); + } + + double[] k1 = derivatives(state); + double[] s2 = new double[] {state[0] + 0.5 * dt * k1[0], state[1] + 0.5 * dt * k1[1]}; + + double[] k2 = derivatives(s2); + double[] s3 = new double[] {state[0] + 0.5 * dt * k2[0], state[1] + 0.5 * dt * k2[1]}; + + double[] k3 = derivatives(s3); + double[] s4 = new double[] {state[0] + dt * k3[0], state[1] + dt * k3[1]}; + + double[] k4 = derivatives(s4); + + double thetaNext = state[0] + dt / 6.0 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]); + double omegaNext = state[1] + dt / 6.0 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]); + + return new double[] {thetaNext, omegaNext}; + } + + /** + * Simulates the pendulum for a given duration. + * + * @param initialState the initial state [theta, omega] + * @param dt the time step size + * @param steps the number of steps to simulate + * @return array of states at each step + */ + public double[][] simulate(double[] initialState, double dt, int steps) { + double[][] trajectory = new double[steps + 1][2]; + trajectory[0] = initialState.clone(); + + double[] currentState = initialState.clone(); + for (int i = 1; i <= steps; i++) { + currentState = stepRK4(currentState, dt); + trajectory[i] = currentState.clone(); + } + + return trajectory; + } + + /** + * Calculates the total energy of the pendulum. + * E = (1/2) * m * L^2 * omega^2 + m * g * L * (1 - cos(theta)) + * We use m = 1 for simplicity. + * + * @param state the current state [theta, omega] + * @return the total energy + */ + public double calculateEnergy(double[] state) { + double theta = state[0]; + double omega = state[1]; + double kineticEnergy = 0.5 * length * length * omega * omega; + double potentialEnergy = g * length * (1 - Math.cos(theta)); + return kineticEnergy + potentialEnergy; + } + + public double getLength() { + return length; + } + + public double getGravity() { + return g; + } +} 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/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/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/recursion/DiceThrower.java b/src/main/java/com/thealgorithms/recursion/DiceThrower.java new file mode 100644 index 000000000000..f46d82213aaa --- /dev/null +++ b/src/main/java/com/thealgorithms/recursion/DiceThrower.java @@ -0,0 +1,113 @@ +package com.thealgorithms.recursion; + +import java.util.ArrayList; +import java.util.List; + +/** + * DiceThrower - Generates all possible dice roll combinations that sum to a target + * + * This algorithm uses recursive backtracking to find all combinations of dice rolls + * (faces 1-6) that sum to a given target value. + * + * Example: If target = 4, possible combinations include: + * - "1111" (1+1+1+1 = 4) + * - "13" (1+3 = 4) + * - "22" (2+2 = 4) + * - "4" (4 = 4) + * + * @author BEASTSHRIRAM + * @see Backtracking Algorithm + */ +public final class DiceThrower { + + private DiceThrower() { + // Utility class + } + + /** + * Returns all possible dice roll combinations that sum to the target + * + * @param target the target sum to achieve with dice rolls + * @return list of all possible combinations as strings + */ + public static List getDiceCombinations(int target) { + if (target < 0) { + throw new IllegalArgumentException("Target must be non-negative"); + } + return generateCombinations("", target); + } + + /** + * Prints all possible dice roll combinations that sum to the target + * + * @param target the target sum to achieve with dice rolls + */ + public static void printDiceCombinations(int target) { + if (target < 0) { + throw new IllegalArgumentException("Target must be non-negative"); + } + printCombinations("", target); + } + + /** + * Recursive helper method to generate all combinations + * + * @param current the current combination being built + * @param remaining the remaining sum needed + * @return list of all combinations from this state + */ + private static List generateCombinations(String current, int remaining) { + List combinations = new ArrayList<>(); + + // Base case: if remaining sum is 0, we found a valid combination + if (remaining == 0) { + combinations.add(current); + return combinations; + } + + // Try all possible dice faces (1-6), but not more than remaining sum + for (int face = 1; face <= 6 && face <= remaining; face++) { + List subCombinations = generateCombinations(current + face, remaining - face); + combinations.addAll(subCombinations); + } + + return combinations; + } + + /** + * Recursive helper method to print all combinations + * + * @param current the current combination being built + * @param remaining the remaining sum needed + */ + private static void printCombinations(String current, int remaining) { + // Base case: if remaining sum is 0, we found a valid combination + if (remaining == 0) { + System.out.println(current); + return; + } + + // Try all possible dice faces (1-6), but not more than remaining sum + for (int face = 1; face <= 6 && face <= remaining; face++) { + printCombinations(current + face, remaining - face); + } + } + + /** + * Demo method to show usage + * + * @param args command line arguments + */ + public static void main(String[] args) { + int target = 4; + + System.out.println("All dice combinations that sum to " + target + ":"); + List combinations = getDiceCombinations(target); + + for (String combination : combinations) { + System.out.println(combination); + } + + System.out.println("\nTotal combinations: " + combinations.size()); + } +} 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..9bc6da2f7443 100644 --- a/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java +++ b/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java @@ -12,10 +12,12 @@ private FibonacciSeries() { throw new UnsupportedOperationException("Utility class"); } 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/recursion/SylvesterSequence.java b/src/main/java/com/thealgorithms/recursion/SylvesterSequence.java new file mode 100644 index 000000000000..2b39f66c7936 --- /dev/null +++ b/src/main/java/com/thealgorithms/recursion/SylvesterSequence.java @@ -0,0 +1,50 @@ +package com.thealgorithms.recursion; + +import java.math.BigInteger; + +/** + * A utility class for calculating numbers in Sylvester's sequence. + * + *

Sylvester's sequence is a sequence of integers where each term is calculated + * using the formula: + *

+ * a(n) = a(n-1) * (a(n-1) - 1) + 1
+ * 
+ * with the first term being 2. + * + *

This class is final and cannot be instantiated. + * + * @see Wikipedia: Sylvester sequence + */ +public final class SylvesterSequence { + + // Private constructor to prevent instantiation + private SylvesterSequence() { + } + + /** + * Calculates the nth number in Sylvester's sequence. + * + *

The sequence is defined recursively, with the first term being 2: + *

+     * a(1) = 2
+     * a(n) = a(n-1) * (a(n-1) - 1) + 1 for n > 1
+     * 
+ * + * @param n the position in the sequence (must be greater than 0) + * @return the nth number in Sylvester's sequence + * @throws IllegalArgumentException if n is less than or equal to 0 + */ + public static BigInteger sylvester(int n) { + if (n <= 0) { + throw new IllegalArgumentException("sylvester() does not accept negative numbers or zero."); + } + if (n == 1) { + return BigInteger.valueOf(2); + } else { + BigInteger prev = sylvester(n - 1); + // Sylvester sequence formula: a(n) = a(n-1) * (a(n-1) - 1) + 1 + return prev.multiply(prev.subtract(BigInteger.ONE)).add(BigInteger.ONE); + } + } +} diff --git a/src/main/java/com/thealgorithms/searches/ExponentalSearch.java b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java similarity index 100% rename from src/main/java/com/thealgorithms/searches/ExponentalSearch.java rename to src/main/java/com/thealgorithms/searches/ExponentialSearch.java diff --git a/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java b/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java index b40c7554d84b..ecdcd36adf21 100644 --- a/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java +++ b/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java @@ -13,7 +13,7 @@ * In this two pointers are taken, the first points to the 0th row and the second one points to end * column, and then the element corresponding to the pointers placed in the array is compared with * the target that either its equal, greater or smaller than the target. If the element is equal to - * the target, the co-ordinates of that element is returned i.e. an array of the two pointers will + * the target, the coordinates of that element is returned i.e. an array of the two pointers will * be returned, else if the target is greater than corresponding element then the pointer pointing * to the 0th row will be incremented by 1, else if the target is lesser than the corresponding * element then the pointer pointing to the end column will be decremented by 1. And if the element diff --git a/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java b/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java new file mode 100644 index 000000000000..1a5903a5d134 --- /dev/null +++ b/src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java @@ -0,0 +1,99 @@ +package com.thealgorithms.searches; + +import com.thealgorithms.devutils.searches.SearchAlgorithm; + +/** + * Sentinel Linear Search is a variation of linear search that eliminates the + * need to check the array bounds in each iteration by placing the search key + * at the end of the array as a sentinel value. + * + *

+ * The algorithm works by: + * 1. Storing the last element of the array + * 2. Placing the search key at the last position (sentinel) + * 3. Searching from the beginning without bound checking + * 4. If found before the last position, return the index + * 5. If found at the last position, check if it was originally there + * + *

+ * Time Complexity: + * - Best case: O(1) - when the element is at the first position + * - Average case: O(n) - when the element is in the middle + * - Worst case: O(n) - when the element is not present + * + *

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

+ * Advantages over regular linear search: + * - Reduces the number of comparisons by eliminating bound checking + * - Slightly more efficient in practice due to fewer conditional checks + * + * @author TheAlgorithms Contributors + * @see LinearSearch + * @see SearchAlgorithm + */ +public class SentinelLinearSearch implements SearchAlgorithm { + /** + * Performs sentinel linear search on the given array. + * + * @param array the array to search in + * @param key the element to search for + * @param the type of elements in the array, must be Comparable + * @return the index of the first occurrence of the key, or -1 if not found + * @throws IllegalArgumentException if the array is null + */ + @Override + public > int find(T[] array, T key) { + if (array == null) { + throw new IllegalArgumentException("Array cannot be null"); + } + + if (array.length == 0) { + return -1; + } + + if (key == null) { + return findNull(array); + } + + // Store the last element + T lastElement = array[array.length - 1]; + + // Place the sentinel (search key) at the end + array[array.length - 1] = key; + + int i = 0; + // Search without bound checking since sentinel guarantees we'll find the key + while (array[i].compareTo(key) != 0) { + i++; + } + + // Restore the original last element + array[array.length - 1] = lastElement; + + // Check if we found the key before the sentinel position + // or if the original last element was the key we were looking for + if (i < array.length - 1 || (lastElement != null && lastElement.compareTo(key) == 0)) { + return i; + } + + return -1; // Key not found + } + + /** + * Helper method to find null values in the array. + * + * @param array the array to search in + * @param the type of elements in the array + * @return the index of the first null element, or -1 if not found + */ + private > int findNull(T[] array) { + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/com/thealgorithms/slidingwindow/MinimumWindowSubstring.java b/src/main/java/com/thealgorithms/slidingwindow/MinimumWindowSubstring.java new file mode 100644 index 000000000000..c1a5ac067ab5 --- /dev/null +++ b/src/main/java/com/thealgorithms/slidingwindow/MinimumWindowSubstring.java @@ -0,0 +1,69 @@ +package com.thealgorithms.slidingwindow; + +import java.util.HashMap; + +/** + * Finds the minimum window substring in 's' that contains all characters of 't'. + * + * Worst-case performance O(n) + * Best-case performance O(n) + * Average performance O(n) + * Worst-case space complexity O(1) + * + * @author https://github.com/Chiefpatwal + */ +public final class MinimumWindowSubstring { + // Prevent instantiation + private MinimumWindowSubstring() { + } + + /** + * Finds the minimum window substring of 's' containing all characters of 't'. + * + * @param s The input string to search within. + * @param t The string with required characters. + * @return The minimum window substring, or empty string if not found. + */ + public static String minWindow(String s, String t) { + if (s.length() < t.length()) { + return ""; + } + + HashMap tFreq = new HashMap<>(); + for (char c : t.toCharArray()) { + tFreq.put(c, tFreq.getOrDefault(c, 0) + 1); + } + + HashMap windowFreq = new HashMap<>(); + int left = 0; + int right = 0; + int minLen = Integer.MAX_VALUE; + int count = 0; + String result = ""; + + while (right < s.length()) { + char c = s.charAt(right); + windowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1); + + if (tFreq.containsKey(c) && windowFreq.get(c).intValue() <= tFreq.get(c).intValue()) { + count++; + } + + while (count == t.length()) { + if (right - left + 1 < minLen) { + minLen = right - left + 1; + result = s.substring(left, right + 1); + } + + char leftChar = s.charAt(left); + windowFreq.put(leftChar, windowFreq.get(leftChar) - 1); + if (tFreq.containsKey(leftChar) && windowFreq.get(leftChar) < tFreq.get(leftChar)) { + count--; + } + left++; + } + right++; + } + return result; + } +} 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/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..fdbfd9cd1cfa 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 { /** diff --git a/src/main/java/com/thealgorithms/sorts/LinkListSort.java b/src/main/java/com/thealgorithms/sorts/LinkListSort.java index 800d78f36549..d8fd76a86236 100644 --- a/src/main/java/com/thealgorithms/sorts/LinkListSort.java +++ b/src/main/java/com/thealgorithms/sorts/LinkListSort.java @@ -106,7 +106,7 @@ public static boolean isSorted(int[] p, int option) { // The given array and the expected array is checked if both are same then true // is displayed else false is displayed default: - // default is used incase user puts a unauthorized value + // default is used in case user puts a unauthorized value System.out.println("Wrong choice"); } // Switch case is used to call the classes as per the user requirement diff --git a/src/main/java/com/thealgorithms/sorts/MergeSort.java b/src/main/java/com/thealgorithms/sorts/MergeSort.java index 86a184f67b26..f7a7c8da004d 100644 --- a/src/main/java/com/thealgorithms/sorts/MergeSort.java +++ b/src/main/java/com/thealgorithms/sorts/MergeSort.java @@ -13,11 +13,16 @@ class MergeSort implements SortAlgorithm { private Comparable[] aux; /** - * 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) { diff --git a/src/main/java/com/thealgorithms/sorts/PriorityQueueSort.java b/src/main/java/com/thealgorithms/sorts/PriorityQueueSort.java new file mode 100644 index 000000000000..16f13050e8b3 --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/PriorityQueueSort.java @@ -0,0 +1,49 @@ +package com.thealgorithms.sorts; + +import java.util.PriorityQueue; + +/** + * Sorts an array using Java's PriorityQueue (Min-Heap). + * + *

Example: Input: [7, 2, 9, 4, 1] Output: [1, 2, 4, 7, 9] + * + *

Time Complexity: + * - Inserting n elements into the PriorityQueue → O(n log n) + * - Polling n elements → O(n log n) + * - Total: O(n log n) + * + *

Space Complexity: O(n) for the PriorityQueue + * + * @see + * Heap / PriorityQueue + */ +public final class PriorityQueueSort { + + // Private constructor to prevent instantiation (utility class) + private PriorityQueueSort() { + } + + /** + * Sorts the given array in ascending order using a PriorityQueue. + * + * @param arr the array to be sorted + * @return the sorted array (in-place) + */ + public static int[] sort(int[] arr) { + if (arr == null || arr.length == 0) { + return arr; + } + + PriorityQueue pq = new PriorityQueue<>(); + for (int num : arr) { + pq.offer(num); + } + + int i = 0; + while (!pq.isEmpty()) { + arr[i++] = pq.poll(); + } + + return arr; + } +} 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..2d1814441701 100644 --- a/src/main/java/com/thealgorithms/sorts/SelectionSort.java +++ b/src/main/java/com/thealgorithms/sorts/SelectionSort.java @@ -2,11 +2,16 @@ public class SelectionSort implements SortAlgorithm { /** - * Sorts an array of comparable elements in increasing order using the selection sort algorithm. + * Generic Selection Sort algorithm. * - * @param array the array to be sorted - * @param the class of array elements - * @return the sorted array + * Time Complexity: + * - Best case: O(n^2) + * - Average case: O(n^2) + * - Worst case: O(n^2) + * + * Space Complexity: O(1) – in-place sorting. + * + * @see SortAlgorithm */ @Override public > T[] sort(T[] array) { 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/stacks/TrappingRainwater.java b/src/main/java/com/thealgorithms/stacks/TrappingRainwater.java new file mode 100644 index 000000000000..072665061b8e --- /dev/null +++ b/src/main/java/com/thealgorithms/stacks/TrappingRainwater.java @@ -0,0 +1,48 @@ +package com.thealgorithms.stacks; +/** + * Trapping Rainwater Problem + * Given an array of non-negative integers representing the height of bars, + * compute how much water it can trap after raining. + * + * Example: + * Input: [4,2,0,3,2,5] + * Output: 9 + * + * Time Complexity: O(n) + * Space Complexity: O(1) + * + * Reference: https://en.wikipedia.org/wiki/Trapping_rain_water + */ +public final class TrappingRainwater { + + private TrappingRainwater() { + throw new UnsupportedOperationException("Utility class"); + } + + public static int trap(int[] height) { + int left = 0; + int right = height.length - 1; + int leftMax = 0; + int rightMax = 0; + int result = 0; + + while (left < right) { + if (height[left] < height[right]) { + if (height[left] >= leftMax) { + leftMax = height[left]; + } else { + result += leftMax - height[left]; + } + left++; + } else { + if (height[right] >= rightMax) { + rightMax = height[right]; + } else { + result += rightMax - height[right]; + } + right--; + } + } + return result; + } +} 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/strings/AlternativeStringArrange.java b/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java new file mode 100644 index 000000000000..cf736dbd8cab --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/AlternativeStringArrange.java @@ -0,0 +1,48 @@ +package com.thealgorithms.strings; + +/** + * This class provides a method to arrange two strings by alternating their characters. + * If one string is longer, the remaining characters of the longer string are appended at the end. + *

+ * Example: + * Input: "abc", "12345" + * Output: "a1b2c345" + *

+ * Input: "abcd", "12" + * Output: "a1b2cd" + * + * @author Milad Sadeghi + */ +public final class AlternativeStringArrange { + + // Private constructor to prevent instantiation + private AlternativeStringArrange() { + } + + /** + * Arranges two strings by alternating their characters. + * + * @param firstString the first input string + * @param secondString the second input string + * @return a new string with characters from both strings arranged alternately + */ + public static String arrange(String firstString, String secondString) { + StringBuilder result = new StringBuilder(); + int length1 = firstString.length(); + int length2 = secondString.length(); + int minLength = Math.min(length1, length2); + + for (int i = 0; i < minLength; i++) { + result.append(firstString.charAt(i)); + result.append(secondString.charAt(i)); + } + + if (length1 > length2) { + result.append(firstString.substring(minLength)); + } else if (length2 > length1) { + result.append(secondString.substring(minLength)); + } + + return result.toString(); + } +} diff --git a/src/main/java/com/thealgorithms/strings/Isogram.java b/src/main/java/com/thealgorithms/strings/Isogram.java new file mode 100644 index 000000000000..b37f085b075d --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/Isogram.java @@ -0,0 +1,92 @@ +package com.thealgorithms.strings; + +import java.util.HashSet; +import java.util.Set; + +/** + * An isogram (also called heterogram or nonpattern word) is a word in which no + * letter of the word occurs more than once. Each character appears exactly + * once. + * + * For example, the word "uncopyrightable" is the longest common English isogram + * with 15 unique letters. Other examples include "dermatoglyphics" (15 + * letters), + * "background" (10 letters), "python" (6 letters), and "keyboard" (8 letters). + * But words like "hello" and "programming" are not isograms because some + * letters + * appear multiple times ('l' appears twice in "hello", while 'r', 'm', 'g' + * repeat + * in "programming"). + * + * Isograms are particularly valuable in creating substitution ciphers and are + * studied in recreational linguistics. A perfect pangram, which uses all 26 + * letters + * of the alphabet exactly once, is a special type of isogram. + * + * Reference from https://en.wikipedia.org/wiki/Heterogram_(literature)#Isograms + */ +public final class Isogram { + /** + * Private constructor to prevent instantiation of utility class. + */ + private Isogram() { + } + + /** + * Checks if a string is an isogram using boolean array approach. + * + * Time Complexity: O(n) + * Space Complexity: O(1) + * + * @param str the input string + * @return true if the string is an isogram, false otherwise + * @throws IllegalArgumentException if the string contains non-alphabetic + * characters + */ + public static boolean isAlphabeticIsogram(String str) { + if (str == null || str.isEmpty()) { + return true; + } + + str = str.toLowerCase(); + + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch < 'a' || ch > 'z') { + throw new IllegalArgumentException("Input contains non-alphabetic character: '" + ch + "'"); + } + } + + boolean[] seenChars = new boolean[26]; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + int index = ch - 'a'; + if (seenChars[index]) { + return false; + } + seenChars[index] = true; + } + return true; + } + + /** + * Checks if a string is an isogram using length comparison approach. + * Time Complexity: O(n) + * Space Complexity: O(k) where k is the number of unique characters + * + * @param str the input string + * @return true if the string is an isogram, false otherwise + */ + public static boolean isFullIsogram(String str) { + if (str == null || str.isEmpty()) { + return true; + } + str = str.toLowerCase(); + + Set uniqueChars = new HashSet<>(); + for (char ch : str.toCharArray()) { + uniqueChars.add(ch); + } + return uniqueChars.size() == str.length(); + } +} 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/Pangram.java b/src/main/java/com/thealgorithms/strings/Pangram.java index 01307b28f6c6..a92c282d7e52 100644 --- a/src/main/java/com/thealgorithms/strings/Pangram.java +++ b/src/main/java/com/thealgorithms/strings/Pangram.java @@ -60,7 +60,7 @@ public static boolean isPangram(String s) { } /** - * Checks if a String is Pangram or not by checking if each alhpabet is present or not + * Checks if a String is Pangram or not by checking if each alphabet is present or not * * @param s The String to check * @return {@code true} if s is a Pangram, otherwise {@code false} 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/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/test/java/com/thealgorithms/backtracking/CombinationSumTest.java b/src/test/java/com/thealgorithms/backtracking/CombinationSumTest.java new file mode 100644 index 000000000000..986c71acebe8 --- /dev/null +++ b/src/test/java/com/thealgorithms/backtracking/CombinationSumTest.java @@ -0,0 +1,29 @@ +package com.thealgorithms.backtracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.Test; + +class CombinationSumTest { + private static List> norm(Iterable> x) { + List> y = new ArrayList<>(); + for (var p : x) { + var q = new ArrayList<>(p); + q.sort(Integer::compare); + y.add(q); + } + y.sort(Comparator.>comparingInt(List::size).thenComparing(Object::toString)); + return y; + } + + @Test + void sample() { + int[] candidates = {2, 3, 6, 7}; + int target = 7; + var expected = List.of(List.of(2, 2, 3), List.of(7)); + assertEquals(norm(expected), norm(CombinationSum.combinationSum(candidates, target))); + } +} 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/BitRotateTest.java b/src/test/java/com/thealgorithms/bitmanipulation/BitRotateTest.java new file mode 100644 index 000000000000..0595ae5a73e1 --- /dev/null +++ b/src/test/java/com/thealgorithms/bitmanipulation/BitRotateTest.java @@ -0,0 +1,205 @@ +package com.thealgorithms.bitmanipulation; + +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 static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for BitRotate class covering typical, boundary, and edge cases. + * Tests verify correct behavior for 32-bit circular bit rotations. + * + * @author Yajunesh + */ +public class BitRotateTest { + + // ===== rotateLeft Tests ===== + + @Test + public void testRotateLeftBasic() { + // Basic left rotation + assertEquals(0b00000000_00000000_00000000_00000010, BitRotate.rotateLeft(1, 1)); + assertEquals(0b00000000_00000000_00000000_00000100, BitRotate.rotateLeft(1, 2)); + assertEquals(0b00000000_00000000_00000000_00001000, BitRotate.rotateLeft(1, 3)); + } + + @Test + public void testRotateLeftWithCarry() { + // Test bits carrying from left to right + // Binary: 10000000_00000000_00000000_00000001 + int value = 0x80000001; + // After left rotate by 1: 00000000_00000000_00000000_00000011 + assertEquals(3, BitRotate.rotateLeft(value, 1)); + + // Binary: 11000000_00000000_00000000_00000000 + value = 0xC0000000; + // After left rotate by 1: 10000000_00000000_00000000_00000001 + assertEquals(0x80000001, BitRotate.rotateLeft(value, 1)); + } + + @Test + public void testRotateLeftShift32() { + // Shift of 32 should be same as shift of 0 (modulo behavior) + int value = 0x12345678; + assertEquals(value, BitRotate.rotateLeft(value, 32)); + assertEquals(value, BitRotate.rotateLeft(value, 64)); + assertEquals(value, BitRotate.rotateLeft(value, 96)); + } + + @Test + public void testRotateLeftShiftNormalization() { + // Test that shifts > 32 are properly normalized + int value = 1; + assertEquals(BitRotate.rotateLeft(value, 1), BitRotate.rotateLeft(value, 33)); + assertEquals(BitRotate.rotateLeft(value, 5), BitRotate.rotateLeft(value, 37)); + } + + @Test + public void testRotateLeftZeroShift() { + // Zero shift should return original value + int value = 0xABCD1234; + assertEquals(value, BitRotate.rotateLeft(value, 0)); + } + + // ===== rotateRight Tests ===== + + @Test + public void testRotateRightBasic() { + // Basic right rotation + assertEquals(0b10000000_00000000_00000000_00000000, BitRotate.rotateRight(1, 1)); + assertEquals(0b01000000_00000000_00000000_00000000, BitRotate.rotateRight(1, 2)); + assertEquals(0b00100000_00000000_00000000_00000000, BitRotate.rotateRight(1, 3)); + } + + @Test + public void testRotateRightWithCarry() { + // Test bits carrying from right to left + // Binary: 00000000_00000000_00000000_00000011 + int value = 3; + // After right rotate by 1: 10000000_00000000_00000000_00000001 + assertEquals(0x80000001, BitRotate.rotateRight(value, 1)); + + // Binary: 00000000_00000000_00000000_00000001 + value = 1; + // After right rotate by 1: 10000000_00000000_00000000_00000000 + assertEquals(0x80000000, BitRotate.rotateRight(value, 1)); + } + + @Test + public void testRotateRightShift32() { + // Shift of 32 should be same as shift of 0 (modulo behavior) + int value = 0x9ABCDEF0; + assertEquals(value, BitRotate.rotateRight(value, 32)); + assertEquals(value, BitRotate.rotateRight(value, 64)); + assertEquals(value, BitRotate.rotateRight(value, 96)); + } + + @Test + public void testRotateRightShiftNormalization() { + // Test that shifts > 32 are properly normalized + int value = 1; + assertEquals(BitRotate.rotateRight(value, 1), BitRotate.rotateRight(value, 33)); + assertEquals(BitRotate.rotateRight(value, 7), BitRotate.rotateRight(value, 39)); + } + + @Test + public void testRotateRightZeroShift() { + // Zero shift should return original value + int value = 0xDEADBEEF; + assertEquals(value, BitRotate.rotateRight(value, 0)); + } + + // ===== Edge Case Tests ===== + + @Test + public void testRotateLeftMaxValue() { + // Test with maximum integer value + int value = Integer.MAX_VALUE; // 0x7FFFFFFF + int rotated = BitRotate.rotateLeft(value, 1); + // MAX_VALUE << 1 should become 0xFFFFFFFE, but with rotation it becomes different + assertEquals(0xFFFFFFFE, rotated); + } + + @Test + public void testRotateRightMinValue() { + // Test with minimum integer value (treated as unsigned) + int value = Integer.MIN_VALUE; // 0x80000000 + int rotated = BitRotate.rotateRight(value, 1); + // MIN_VALUE >>> 1 should become 0x40000000, but with rotation from left + assertEquals(0x40000000, rotated); + } + + @Test + public void testRotateAllOnes() { + // Test with all bits set + int value = 0xFFFFFFFF; // All ones + assertEquals(value, BitRotate.rotateLeft(value, 13)); + assertEquals(value, BitRotate.rotateRight(value, 27)); + } + + @Test + public void testRotateAllZeros() { + // Test with all bits zero + int value = 0x00000000; + assertEquals(value, BitRotate.rotateLeft(value, 15)); + assertEquals(value, BitRotate.rotateRight(value, 19)); + } + + // ===== Exception Tests ===== + + @Test + public void testRotateLeftNegativeShift() { + // Negative shifts should throw IllegalArgumentException + Exception exception = assertThrows(IllegalArgumentException.class, () -> BitRotate.rotateLeft(42, -1)); + assertTrue(exception.getMessage().contains("negative")); + } + + @Test + public void testRotateRightNegativeShift() { + // Negative shifts should throw IllegalArgumentException + Exception exception = assertThrows(IllegalArgumentException.class, () -> BitRotate.rotateRight(42, -5)); + assertTrue(exception.getMessage().contains("negative")); + } + + // ===== Complementary Operations Test ===== + + @Test + public void testRotateLeftRightComposition() { + // Rotating left then right by same amount should return original value + int original = 0x12345678; + int shift = 7; + + int leftRotated = BitRotate.rotateLeft(original, shift); + int restored = BitRotate.rotateRight(leftRotated, shift); + + assertEquals(original, restored); + } + + @Test + public void testRotateRightLeftComposition() { + // Rotating right then left by same amount should return original value + int original = 0x9ABCDEF0; + int shift = 13; + + int rightRotated = BitRotate.rotateRight(original, shift); + int restored = BitRotate.rotateLeft(rightRotated, shift); + + assertEquals(original, restored); + } + + @Test + public void testRotateLeft31IsSameAsRotateRight1() { + // Rotating left by 31 should be same as rotating right by 1 + int value = 0x55555555; + assertEquals(BitRotate.rotateLeft(value, 31), BitRotate.rotateRight(value, 1)); + } + + @Test + public void testTraversals() { + // Test that methods don't throw exceptions + assertDoesNotThrow(() -> BitRotate.rotateLeft(1, 1)); + assertDoesNotThrow(() -> BitRotate.rotateRight(1, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/bitmanipulation/BitwiseGCDTest.java b/src/test/java/com/thealgorithms/bitmanipulation/BitwiseGCDTest.java new file mode 100644 index 000000000000..207fdc1e57a0 --- /dev/null +++ b/src/test/java/com/thealgorithms/bitmanipulation/BitwiseGCDTest.java @@ -0,0 +1,110 @@ +package com.thealgorithms.bitmanipulation; + +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 BitwiseGCDTest { + + @Test + public void testGcdBasic() { + assertEquals(6L, BitwiseGCD.gcd(48L, 18L)); + } + + @Test + public void testGcdZeroAndNonZero() { + assertEquals(5L, BitwiseGCD.gcd(0L, 5L)); + assertEquals(5L, BitwiseGCD.gcd(5L, 0L)); + } + + @Test + public void testGcdBothZero() { + assertEquals(0L, BitwiseGCD.gcd(0L, 0L)); + } + + @Test + public void testGcdNegativeInputs() { + assertEquals(6L, BitwiseGCD.gcd(-48L, 18L)); + assertEquals(6L, BitwiseGCD.gcd(48L, -18L)); + assertEquals(6L, BitwiseGCD.gcd(-48L, -18L)); + } + + @Test + public void testGcdIntOverload() { + assertEquals(6, BitwiseGCD.gcd(48, 18)); + } + + @Test + public void testGcdArray() { + long[] values = {48L, 18L, 6L}; + assertEquals(6L, BitwiseGCD.gcd(values)); + } + + @Test + public void testGcdEmptyArray() { + long[] empty = {}; + assertEquals(0L, BitwiseGCD.gcd(empty)); + } + + @Test + public void testGcdCoprime() { + assertEquals(1L, BitwiseGCD.gcd(17L, 13L)); + } + + @Test + public void testGcdPowersOfTwo() { + assertEquals(1024L, BitwiseGCD.gcd(1L << 20, 1L << 10)); + } + + @Test + public void testGcdLargeNumbers() { + assertEquals(6L, BitwiseGCD.gcd(270L, 192L)); + } + + @Test + public void testGcdEarlyExitArray() { + long[] manyCoprimes = {7L, 11L, 13L, 17L, 19L, 23L, 29L}; + assertEquals(1L, BitwiseGCD.gcd(manyCoprimes)); + } + + @Test + public void testGcdSameNumbers() { + assertEquals(42L, BitwiseGCD.gcd(42L, 42L)); + } + + @Test + public void testGcdLongMinValueBigInteger() { + // gcd(Long.MIN_VALUE, 0) = |Long.MIN_VALUE| = 2^63; must use BigInteger to represent it + BigInteger expected = BigInteger.ONE.shiftLeft(63); // 2^63 + assertEquals(expected, BitwiseGCD.gcdBig(Long.MIN_VALUE, 0L)); + } + + @Test + public void testGcdLongMinValueLongOverloadThrows() { + // The long overload cannot return 2^63 as a positive signed long, so it must throw + assertThrows(ArithmeticException.class, () -> BitwiseGCD.gcd(Long.MIN_VALUE, 0L)); + } + + @Test + public void testGcdWithLongMinAndOther() { + // gcd(Long.MIN_VALUE, 2^10) should be 2^10 + long p = 1L << 10; + BigInteger expected = BigInteger.valueOf(p); + assertEquals(expected, BitwiseGCD.gcdBig(Long.MIN_VALUE, p)); + } + + @Test + public void testGcdWithBothLongMin() { + // gcd(Long.MIN_VALUE, Long.MIN_VALUE) = 2^63 + BigInteger expected = BigInteger.ONE.shiftLeft(63); + assertEquals(expected, BitwiseGCD.gcdBig(Long.MIN_VALUE, Long.MIN_VALUE)); + } + + @Test + public void testGcdEdgeCasesMixed() { + assertEquals(1L, BitwiseGCD.gcd(1L, Long.MAX_VALUE)); + assertEquals(1L, BitwiseGCD.gcd(Long.MAX_VALUE, 1L)); + } +} diff --git a/src/test/java/com/thealgorithms/bitmanipulation/CountBitsFlipTest.java b/src/test/java/com/thealgorithms/bitmanipulation/CountBitsFlipTest.java new file mode 100644 index 000000000000..f33fa0fefbc0 --- /dev/null +++ b/src/test/java/com/thealgorithms/bitmanipulation/CountBitsFlipTest.java @@ -0,0 +1,94 @@ +package com.thealgorithms.bitmanipulation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Random; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for CountBitsFlip. + * Covers: + * - simple examples + * - zeros and identical values + * - negative numbers and two's complement edge cases + * - Long.MIN_VALUE / Long.MAX_VALUE + * - randomized consistency checks between two implementations + */ +@DisplayName("CountBitsFlip Tests") +class CountBitsFlipTest { + + @Test + @DisplayName("Example: A=10, B=20 => 4 bits") + void exampleTenTwenty() { + long a = 10L; + long b = 20L; + long expected = 4L; + assertEquals(expected, CountBitsFlip.countBitsFlip(a, b), "Brian Kernighan implementation should return 4"); + assertEquals(expected, CountBitsFlip.countBitsFlipAlternative(a, b), "Long.bitCount implementation should return 4"); + } + + @Test + @DisplayName("Identical values => 0 bits") + void identicalValues() { + long a = 123456789L; + long b = 123456789L; + long expected = 0L; + assertEquals(expected, CountBitsFlip.countBitsFlip(a, b)); + assertEquals(expected, CountBitsFlip.countBitsFlipAlternative(a, b)); + } + + @Test + @DisplayName("Both zeros => 0 bits") + void bothZeros() { + assertEquals(0L, CountBitsFlip.countBitsFlip(0L, 0L)); + assertEquals(0L, CountBitsFlip.countBitsFlipAlternative(0L, 0L)); + } + + @Test + @DisplayName("Small example: A=15 (1111), B=8 (1000) => 3 bits") + void smallExample() { + long a = 15L; // 1111 + long b = 8L; // 1000 + long expected = 3L; // differs in three low bits + assertEquals(expected, CountBitsFlip.countBitsFlip(a, b)); + assertEquals(expected, CountBitsFlip.countBitsFlipAlternative(a, b)); + } + + @Test + @DisplayName("Negative values: -1 vs 0 => 64 bits (two's complement all ones)") + void negativeVsZero() { + long a = -1L; + long b = 0L; + long expected = 64L; // all 64 bits differ + assertEquals(expected, CountBitsFlip.countBitsFlip(a, b)); + assertEquals(expected, CountBitsFlip.countBitsFlipAlternative(a, b)); + } + + @Test + @DisplayName("Long.MIN_VALUE vs Long.MAX_VALUE => 64 bits") + void minMaxLongs() { + long a = Long.MIN_VALUE; + long b = Long.MAX_VALUE; + long expected = 64L; // MAX ^ MIN yields all ones on 64-bit long + assertEquals(expected, CountBitsFlip.countBitsFlip(a, b)); + assertEquals(expected, CountBitsFlip.countBitsFlipAlternative(a, b)); + } + + @Test + @DisplayName("Randomized consistency: both implementations agree across many pairs") + void randomizedConsistency() { + final int iterations = 1000; + final Random rnd = new Random(12345L); // deterministic seed for reproducibility + + for (int i = 0; i < iterations; i++) { + long a = rnd.nextLong(); + long b = rnd.nextLong(); + + long res1 = CountBitsFlip.countBitsFlip(a, b); + long res2 = CountBitsFlip.countBitsFlipAlternative(a, b); + + assertEquals(res2, res1, () -> String.format("Mismatch for a=%d, b=%d: impl1=%d, impl2=%d", a, b, res1, res2)); + } + } +} 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/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 new file mode 100644 index 000000000000..4ba6787cc97e --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java @@ -0,0 +1,323 @@ +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; + +class PermutationCipherTest { + + private final PermutationCipher cipher = new PermutationCipher(); + + @Test + void testBasicEncryption() { + // given + String plaintext = "HELLO"; + int[] key = {3, 1, 2}; // Move 3rd position to 1st, 1st to 2nd, 2nd to 3rd + + // when + String encrypted = cipher.encrypt(plaintext, key); + + // then + // "HELLO" becomes "HEL" + "LOX" (padded) + // "HEL" with key {3,1,2} becomes "LHE" (L=3rd, H=1st, E=2nd) + // "LOX" with key {3,1,2} becomes "XLO" (X=3rd, L=1st, O=2nd) + assertEquals("LHEXLO", encrypted); + } + + @Test + void testBasicDecryption() { + // given + String ciphertext = "LHEXLO"; + int[] key = {3, 1, 2}; + + // when + String decrypted = cipher.decrypt(ciphertext, key); + + // then + assertEquals("HELLO", decrypted); + } + + @Test + void testEncryptDecryptRoundTrip() { + // given + String plaintext = "THIS IS A TEST MESSAGE"; + int[] key = {4, 2, 1, 3}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("THISISATESTMESSAGE", decrypted); // Spaces are removed during encryption + } + + @Test + void testSingleCharacterKey() { + // given + String plaintext = "ABCDEF"; + int[] key = {1}; // Identity permutation + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("ABCDEF", encrypted); // Should remain unchanged + assertEquals("ABCDEF", decrypted); + } + + @Test + void testLargerKey() { + // given + String plaintext = "PERMUTATION"; + int[] key = {5, 3, 1, 4, 2}; // 5-character permutation + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("PERMUTATION", decrypted); + } + + @Test + void testExactBlockSize() { + // given + String plaintext = "ABCDEF"; // Length 6, divisible by key length 3 + int[] key = {2, 3, 1}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("ABCDEF", decrypted); + } + + @Test + void testEmptyString() { + // given + String plaintext = ""; + int[] key = {2, 1, 3}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("", encrypted); + assertEquals("", decrypted); + } + + @Test + void testNullString() { + // given + String plaintext = null; + int[] key = {2, 1, 3}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals(null, encrypted); + assertEquals(null, decrypted); + } + + @Test + void testStringWithSpaces() { + // given + String plaintext = "A B C D E F"; + int[] key = {2, 1}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("ABCDEF", decrypted); // Spaces should be removed + } + + @Test + void testLowercaseConversion() { + // given + String plaintext = "hello world"; + int[] key = {3, 1, 2}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("HELLOWORLD", decrypted); // Should be converted to uppercase + } + + @Test + void testInvalidKeyNull() { + // given + String plaintext = "HELLO"; + int[] key = null; + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testInvalidKeyEmpty() { + // given + String plaintext = "HELLO"; + int[] key = {}; + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testInvalidKeyOutOfRange() { + // given + String plaintext = "HELLO"; + int[] key = {1, 2, 4}; // 4 is out of range for key length 3 + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testInvalidKeyZero() { + // given + String plaintext = "HELLO"; + int[] key = {0, 1, 2}; // 0 is invalid (should be 1-based) + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testInvalidKeyDuplicate() { + // given + String plaintext = "HELLO"; + int[] key = {1, 2, 2}; // Duplicate position + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testInvalidKeyMissingPosition() { + // given + String plaintext = "HELLO"; + int[] key = {1, 3}; // Missing position 2 + + // when & then + assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key)); + assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key)); + } + + @Test + void testReverseKey() { + // given + String plaintext = "ABCD"; + int[] key = {4, 3, 2, 1}; // Reverse order + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("DCBA", encrypted); // Should be reversed + assertEquals("ABCD", decrypted); + } + + @Test + void testSpecificExampleFromDescription() { + // given + String plaintext = "HELLO"; + int[] key = {3, 1, 2}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + + // then + // Block 1: "HEL" -> positions {3,1,2} -> "LHE" + // Block 2: "LOX" -> positions {3,1,2} -> "XLO" + assertEquals("LHEXLO", encrypted); + // Verify decryption + String decrypted = cipher.decrypt(encrypted, key); + assertEquals("HELLO", decrypted); + } + + @Test + void testPaddingCharacterGetter() { + // when + char paddingChar = cipher.getPaddingChar(); + + // then + assertEquals('X', paddingChar); + } + + @Test + void testLongText() { + // given + String plaintext = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"; + int[] key = {4, 1, 3, 2}; + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("THEQUICKBROWNFOXJUMPSOVERTHELAZYDOG", decrypted); + } + + @Test + void testIdentityPermutation() { + // given + String plaintext = "IDENTITY"; + int[] key = {1, 2, 3, 4}; // Identity permutation + + // when + String encrypted = cipher.encrypt(plaintext, key); + String decrypted = cipher.decrypt(encrypted, key); + + // then + assertEquals("IDENTITY", encrypted); // Should remain unchanged + assertEquals("IDENTITY", decrypted); + } + + @Test + void testEmptyStringRemovePadding() { + // given - Test to cover line 178 (empty string case in removePadding) + String ciphertext = ""; + int[] key = {2, 1, 3}; + + // when + String decrypted = cipher.decrypt(ciphertext, key); + + // then + assertEquals("", decrypted); // Should return empty string directly + } + + @Test + void testBlockShorterThanKey() { + // given - Test to cover line 139 (block length != key length case) + // This is a defensive case where permuteBlock might receive a block shorter than key + // We can test this by manually creating a scenario with malformed ciphertext + String malformedCiphertext = "AB"; // Length 2, but key length is 3 + int[] key = {3, 1, 2}; // Key length is 3 + + // when - This should trigger the padding logic in permuteBlock during decryption + String decrypted = cipher.decrypt(malformedCiphertext, key); + + // then - The method should handle the short block gracefully + // "AB" gets padded to "ABX", then permuted with inverse key {2,3,1} + // inverse key {2,3,1} means: pos 2→1st, pos 3→2nd, pos 1→3rd = "BXA" + // Padding removal only removes trailing X's, so "BXA" remains as is + assertEquals("BXA", decrypted); + } +} diff --git a/src/test/java/com/thealgorithms/compression/ArithmeticCodingTest.java b/src/test/java/com/thealgorithms/compression/ArithmeticCodingTest.java new file mode 100644 index 000000000000..8e51fe5eb463 --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/ArithmeticCodingTest.java @@ -0,0 +1,154 @@ +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 java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ArithmeticCodingTest { + + @Test + void testThrowsExceptionForNullOrEmptyInput() { + // Test that null input throws IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> ArithmeticCoding.compress(null)); + + // Test that empty string throws IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> ArithmeticCoding.compress("")); + } + + @Test + void testCompressionAndDecompressionSimple() { + String original = "BABA"; + Map probTable = ArithmeticCoding.calculateProbabilities(original); + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Verify that compression produces a valid number in [0, 1) + assertNotNull(compressed); + assertTrue(compressed.compareTo(BigDecimal.ZERO) >= 0); + assertTrue(compressed.compareTo(BigDecimal.ONE) < 0); + + // Verify decompression restores the original string + String decompressed = ArithmeticCoding.decompress(compressed, original.length(), probTable); + assertEquals(original, decompressed); + } + + @Test + void testSymmetryWithComplexString() { + String original = "THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG"; + Map probTable = ArithmeticCoding.calculateProbabilities(original); + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Verify compression produces a number in valid range + assertTrue(compressed.compareTo(BigDecimal.ZERO) >= 0); + assertTrue(compressed.compareTo(BigDecimal.ONE) < 0); + + // Verify symmetry: decompress(compress(x)) == x + String decompressed = ArithmeticCoding.decompress(compressed, original.length(), probTable); + assertEquals(original, decompressed); + } + + @Test + void testSymmetryWithRepetitions() { + String original = "MISSISSIPPI"; + Map probTable = ArithmeticCoding.calculateProbabilities(original); + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Verify compression produces a number in valid range + assertTrue(compressed.compareTo(BigDecimal.ZERO) >= 0); + assertTrue(compressed.compareTo(BigDecimal.ONE) < 0); + + // Verify the compression-decompression cycle + String decompressed = ArithmeticCoding.decompress(compressed, original.length(), probTable); + assertEquals(original, decompressed); + } + + @Test + void testSingleCharacterString() { + String original = "AAAAA"; + Map probTable = ArithmeticCoding.calculateProbabilities(original); + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Even with a single unique character, compression should work + assertTrue(compressed.compareTo(BigDecimal.ZERO) >= 0); + assertTrue(compressed.compareTo(BigDecimal.ONE) < 0); + + String decompressed = ArithmeticCoding.decompress(compressed, original.length(), probTable); + assertEquals(original, decompressed); + } + + @Test + void testCompressionOutputDemo() { + // Demonstrate actual compression output similar to LZW test + String original = "BABA"; + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Example: "BABA" compresses to approximately 0.625 + // This shows that the entire message is encoded as a single number + System.out.println("Original: " + original); + System.out.println("Compressed to: " + compressed); + System.out.println("Compression: " + original.length() + " characters -> 1 BigDecimal number"); + + // Verify the compressed value is in valid range [0, 1) + assertTrue(compressed.compareTo(BigDecimal.ZERO) >= 0); + assertTrue(compressed.compareTo(BigDecimal.ONE) < 0); + } + + @Test + void testProbabilityTableCalculation() { + // Test that probability table is calculated correctly + String text = "AABBC"; + Map probTable = ArithmeticCoding.calculateProbabilities(text); + + // Verify all characters are in the table + assertTrue(probTable.containsKey('A')); + assertTrue(probTable.containsKey('B')); + assertTrue(probTable.containsKey('C')); + + // Verify probability ranges are valid + for (ArithmeticCoding.Symbol symbol : probTable.values()) { + assertTrue(symbol.low().compareTo(BigDecimal.ZERO) >= 0); + assertTrue(symbol.high().compareTo(BigDecimal.ONE) <= 0); + assertTrue(symbol.low().compareTo(symbol.high()) < 0); + } + } + + @Test + void testDecompressionWithMismatchedProbabilityTable() { + // Test decompression with a probability table that doesn't match the original + String original = "ABCD"; + BigDecimal compressed = ArithmeticCoding.compress(original); + + // Create a different probability table (for "XYZ" instead of "ABCD") + Map wrongProbTable = ArithmeticCoding.calculateProbabilities("XYZ"); + + // Decompression with wrong probability table should produce incorrect output + String decompressed = ArithmeticCoding.decompress(compressed, original.length(), wrongProbTable); + + // The decompressed string will be different from original (likely all 'X', 'Y', or 'Z') + // This tests the edge case where the compressed value doesn't fall into expected ranges + assertNotNull(decompressed); + assertEquals(original.length(), decompressed.length()); + } + + @Test + void testDecompressionWithValueOutsideSymbolRanges() { + // Create a custom probability table + Map probTable = new HashMap<>(); + probTable.put('A', new ArithmeticCoding.Symbol(new BigDecimal("0.0"), new BigDecimal("0.5"))); + probTable.put('B', new ArithmeticCoding.Symbol(new BigDecimal("0.5"), new BigDecimal("1.0"))); + + // Use a compressed value that should decode properly + BigDecimal compressed = new BigDecimal("0.25"); // Falls in 'A' range + + String decompressed = ArithmeticCoding.decompress(compressed, 3, probTable); + + // Verify decompression completes (even if result might not be meaningful) + assertNotNull(decompressed); + assertEquals(3, decompressed.length()); + } +} diff --git a/src/test/java/com/thealgorithms/compression/BurrowsWheelerTransformTest.java b/src/test/java/com/thealgorithms/compression/BurrowsWheelerTransformTest.java new file mode 100644 index 000000000000..b6e10e0d796d --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/BurrowsWheelerTransformTest.java @@ -0,0 +1,124 @@ +package com.thealgorithms.compression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class BurrowsWheelerTransformTest { + + @Test + public void testTransformAndInverseBanana() { + String original = "banana$"; + BurrowsWheelerTransform.BWTResult expectedTransform = new BurrowsWheelerTransform.BWTResult("annb$aa", 4); + + // Test forward transform + BurrowsWheelerTransform.BWTResult actualTransform = BurrowsWheelerTransform.transform(original); + assertEquals(expectedTransform, actualTransform); + + // Test inverse transform + String reconstructed = BurrowsWheelerTransform.inverseTransform(actualTransform.transformed, actualTransform.originalIndex); + assertEquals(original, reconstructed); + } + + @Test + public void testTransformAndInverseAbracadabra() { + String original = "abracadabra$"; + BurrowsWheelerTransform.BWTResult expectedTransform = new BurrowsWheelerTransform.BWTResult("ard$rcaaaabb", 3); + + // Test forward transform + BurrowsWheelerTransform.BWTResult actualTransform = BurrowsWheelerTransform.transform(original); + assertEquals(expectedTransform, actualTransform); + + // Test inverse transform + String reconstructed = BurrowsWheelerTransform.inverseTransform(actualTransform.transformed, actualTransform.originalIndex); + assertEquals(original, reconstructed); + } + + @Test + public void testTransformAndInverseSixMixPixFix() { + String original = "SIX.MIX.PIX.FIX$"; + BurrowsWheelerTransform.BWTResult expectedTransform = new BurrowsWheelerTransform.BWTResult("XXXX.FPSM..$IIII", 11); + + // Test forward transform + BurrowsWheelerTransform.BWTResult actualTransform = BurrowsWheelerTransform.transform(original); + assertEquals(expectedTransform, actualTransform); + + // Test inverse transform + String reconstructed = BurrowsWheelerTransform.inverseTransform(actualTransform.transformed, actualTransform.originalIndex); + assertEquals(original, reconstructed); + } + + @Test + public void testEmptyString() { + String original = ""; + BurrowsWheelerTransform.BWTResult expectedTransform = new BurrowsWheelerTransform.BWTResult("", -1); + + BurrowsWheelerTransform.BWTResult actualTransform = BurrowsWheelerTransform.transform(original); + assertEquals(expectedTransform, actualTransform); + + String reconstructed = BurrowsWheelerTransform.inverseTransform(actualTransform.transformed, actualTransform.originalIndex); + assertEquals(original, reconstructed); + } + + @Test + public void testSingleCharacter() { + String original = "a"; + BurrowsWheelerTransform.BWTResult expectedTransform = new BurrowsWheelerTransform.BWTResult("a", 0); + + BurrowsWheelerTransform.BWTResult actualTransform = BurrowsWheelerTransform.transform(original); + assertEquals(expectedTransform, actualTransform); + + String reconstructed = BurrowsWheelerTransform.inverseTransform(actualTransform.transformed, actualTransform.originalIndex); + assertEquals(original, reconstructed); + } + + @Test + public void testTransformNull() { + assertEquals(new BurrowsWheelerTransform.BWTResult("", -1), BurrowsWheelerTransform.transform(null)); + } + + @Test + public void testInverseTransformNullString() { + // bwtString == null + assertEquals("", BurrowsWheelerTransform.inverseTransform(null, 1)); + // bwtString.isEmpty() + assertEquals("", BurrowsWheelerTransform.inverseTransform("", 0)); + } + + @Test + public void testInverseTransformIndexOutOfBounds() { + String bwt = "annb$aa"; + int n = bwt.length(); // n = 7 + + // originalIndex >= n + assertThrows(IllegalArgumentException.class, () -> BurrowsWheelerTransform.inverseTransform(bwt, n)); + assertThrows(IllegalArgumentException.class, () -> BurrowsWheelerTransform.inverseTransform(bwt, 8)); + + // originalIndex < 0 + assertThrows(IllegalArgumentException.class, () -> BurrowsWheelerTransform.inverseTransform(bwt, -2)); + } + + @Test + public void testBWTResultHelpers() { + BurrowsWheelerTransform.BWTResult res1 = new BurrowsWheelerTransform.BWTResult("annb$aa", 4); + BurrowsWheelerTransform.BWTResult res2 = new BurrowsWheelerTransform.BWTResult("annb$aa", 4); + BurrowsWheelerTransform.BWTResult res3 = new BurrowsWheelerTransform.BWTResult("other", 4); + BurrowsWheelerTransform.BWTResult res4 = new BurrowsWheelerTransform.BWTResult("annb$aa", 1); + + assertEquals(res1, res1); + assertEquals(res1, res2); + assertNotEquals(res1, null); // obj == null + assertNotEquals(res1, new Object()); // different class + assertNotEquals(res1, res3); // different transformed + assertNotEquals(res1, res4); // different originalIndex + + assertEquals(res1.hashCode(), res2.hashCode()); + assertNotEquals(res1.hashCode(), res3.hashCode()); + + assertTrue(res1.toString().contains("annb$aa")); + assertTrue(res1.toString().contains("originalIndex=4")); + } +} diff --git a/src/test/java/com/thealgorithms/compression/LZ77Test.java b/src/test/java/com/thealgorithms/compression/LZ77Test.java new file mode 100644 index 000000000000..86732d48a54a --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/LZ77Test.java @@ -0,0 +1,223 @@ +package com.thealgorithms.compression; + +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.DisplayName; +import org.junit.jupiter.api.Test; + +class LZ77Test { + + @Test + @DisplayName("Test compression and decompression of a simple repeating string") + void testSimpleRepeatingString() { + String original = "ababcbababaa"; + List compressed = LZ77.compress(original, 10, 4); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test compression and decompression of a string with no repeats initially") + void testNoInitialRepeats() { + String original = "abcdefgh"; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test compression and decompression of a longer example") + void testLongerExample() { + String original = "TOBEORNOTTOBEORTOBEORNOT"; + List compressed = LZ77.compress(original, 20, 10); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test empty string compression and decompression") + void testEmptyString() { + String original = ""; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + assertTrue(compressed.isEmpty()); + } + + @Test + @DisplayName("Test null string compression") + void testNullStringCompress() { + List compressed = LZ77.compress(null); + assertTrue(compressed.isEmpty()); + } + + @Test + @DisplayName("Test null list decompression") + void testNullListDecompress() { + String decompressed = LZ77.decompress(null); + assertEquals("", decompressed); + } + + @Test + @DisplayName("Test invalid buffer sizes throw exception") + void testInvalidBufferSizes() { + assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 0, 5)); + assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 5, 0)); + assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", -1, 5)); + assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 5, -1)); + } + + @Test + @DisplayName("Test string with all same characters") + void testAllSameCharacters() { + String original = "AAAAAA"; + List compressed = LZ77.compress(original, 10, 5); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve good compression for repeated characters + assertTrue(compressed.size() < original.length()); + } + + @Test + @DisplayName("Test string with all unique characters") + void testAllUniqueCharacters() { + String original = "abcdefghijklmnop"; + List compressed = LZ77.compress(original, 10, 5); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + + // No compression expected for unique characters + assertEquals(original.length(), compressed.size()); + } + + @Test + @DisplayName("Test single character string") + void testSingleCharacter() { + String original = "a"; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + assertEquals(1, compressed.size()); + } + + @Test + @DisplayName("Test match that goes exactly to the end") + void testMatchToEnd() { + String original = "abcabc"; + List compressed = LZ77.compress(original, 10, 10); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with very small window size") + void testSmallWindowSize() { + String original = "ababababab"; + List compressed = LZ77.compress(original, 2, 4); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with very small lookahead buffer") + void testSmallLookaheadBuffer() { + String original = "ababcbababaa"; + List compressed = LZ77.compress(original, 10, 2); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test repeating pattern at the end") + void testRepeatingPatternAtEnd() { + String original = "xyzabcabcabcabc"; + List compressed = LZ77.compress(original, 15, 8); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test overlapping matches (run-length encoding case)") + void testOverlappingMatches() { + String original = "aaaaaa"; + List compressed = LZ77.compress(original, 10, 10); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test complex pattern with multiple repeats") + void testComplexPattern() { + String original = "abcabcabcxyzxyzxyz"; + List compressed = LZ77.compress(original, 20, 10); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with special characters") + void testSpecialCharacters() { + String original = "hello world! @#$%^&*()"; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with numbers") + void testWithNumbers() { + String original = "1234567890123456"; + List compressed = LZ77.compress(original, 15, 8); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test long repeating sequence") + void testLongRepeatingSequence() { + String original = "abcdefgh".repeat(10); + List compressed = LZ77.compress(original, 50, 20); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve significant compression + assertTrue(compressed.size() < original.length() / 2); + } + + @Test + @DisplayName("Test compression effectiveness") + void testCompressionEffectiveness() { + String original = "ababababababab"; + List compressed = LZ77.compress(original, 20, 10); + + // Verify that compression actually reduces the data size + // Each token represents potentially multiple characters + assertTrue(compressed.size() <= original.length()); + + // Verify decompression + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with mixed case letters") + void testMixedCase() { + String original = "AaBbCcAaBbCc"; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test default parameters") + void testDefaultParameters() { + String original = "This is a test string with some repeated patterns. This is repeated."; + List compressed = LZ77.compress(original); + String decompressed = LZ77.decompress(compressed); + assertEquals(original, decompressed); + } +} diff --git a/src/test/java/com/thealgorithms/compression/LZ78Test.java b/src/test/java/com/thealgorithms/compression/LZ78Test.java new file mode 100644 index 000000000000..7889b50b76f3 --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/LZ78Test.java @@ -0,0 +1,295 @@ +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; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LZ78Test { + + @Test + @DisplayName("Test compression and decompression of a simple repeating string") + void testSimpleRepeatingString() { + String original = "ababcbababaa"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test compression and decompression example ABAABABAABAB") + void testStandardExample() { + String original = "ABAABABAABAB"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Verify the compression produces expected tokens + // Expected: (0,A)(0,B)(1,A)(2,B)(3,A)(4,B) + // Where dictionary builds as: 1:A, 2:B, 3:AA, 4:BA, 5:ABA, 6:BAB + assertEquals(6, compressed.size()); + assertEquals(0, compressed.get(0).index()); + assertEquals('A', compressed.get(0).nextChar()); + assertEquals(0, compressed.get(1).index()); + assertEquals('B', compressed.get(1).nextChar()); + assertEquals(1, compressed.get(2).index()); + assertEquals('A', compressed.get(2).nextChar()); + } + + @Test + @DisplayName("Test compression and decompression of a longer example") + void testLongerExample() { + String original = "TOBEORNOTTOBEORTOBEORNOT"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test empty string compression and decompression") + void testEmptyString() { + String original = ""; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + assertTrue(compressed.isEmpty()); + } + + @Test + @DisplayName("Test null string compression") + void testNullStringCompress() { + List compressed = LZ78.compress(null); + assertTrue(compressed.isEmpty()); + } + + @Test + @DisplayName("Test null list decompression") + void testNullListDecompress() { + String decompressed = LZ78.decompress(null); + assertEquals("", decompressed); + } + + @Test + @DisplayName("Test string with all same characters") + void testAllSameCharacters() { + String original = "AAAAAA"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve good compression: (0,A)(1,A)(2,A)... + assertTrue(compressed.size() <= 4); // Builds: A, AA, AAA, etc. + } + + @Test + @DisplayName("Test string with all unique characters") + void testAllUniqueCharacters() { + String original = "abcdefg"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // No compression for unique characters + assertEquals(original.length(), compressed.size()); + + // Each token should have index 0 (empty prefix) + for (LZ78.Token token : compressed) { + assertEquals(0, token.index()); + } + } + + @Test + @DisplayName("Test single character string") + void testSingleCharacter() { + String original = "a"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + assertEquals(1, compressed.size()); + assertEquals(0, compressed.getFirst().index()); + assertEquals('a', compressed.getFirst().nextChar()); + } + + @Test + @DisplayName("Test two character string") + void testTwoCharacters() { + String original = "ab"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + assertEquals(2, compressed.size()); + } + + @Test + @DisplayName("Test repeating pairs") + void testRepeatingPairs() { + String original = "ababab"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Should compress well: (0,a)(0,b)(1,b) or similar + assertTrue(compressed.size() < original.length()); + } + + @Test + @DisplayName("Test growing patterns") + void testGrowingPatterns() { + String original = "abcabcdabcde"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test dictionary building correctness") + void testDictionaryBuilding() { + String original = "aabaabaab"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Verify first few tokens + // Expected pattern: (0,a)(1,b)(2,a)(3,b) building dictionary 1:a, 2:ab, 3:aa, 4:aab + assertTrue(compressed.size() > 0); + assertEquals(0, compressed.getFirst().index()); // First char always has index 0 + } + + @Test + @DisplayName("Test with special characters") + void testSpecialCharacters() { + String original = "hello world! hello!"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test with numbers") + void testWithNumbers() { + String original = "1234512345"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve compression + assertTrue(compressed.size() < original.length()); + } + + @Test + @DisplayName("Test long repeating sequence") + void testLongRepeatingSequence() { + String original = "abcdefgh".repeat(5); + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // LZ78 should achieve some compression for repeating sequences + assertTrue(compressed.size() < original.length(), "Compressed size should be less than original length"); + } + + @Test + @DisplayName("Test alternating characters") + void testAlternatingCharacters() { + String original = "ababababab"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test compression effectiveness") + void testCompressionEffectiveness() { + String original = "the quick brown fox jumps over the lazy dog the quick brown fox"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve some compression due to repeated phrases + assertTrue(compressed.size() < original.length()); + } + + @Test + @DisplayName("Test with mixed case letters") + void testMixedCase() { + String original = "AaBbCcAaBbCc"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test palindrome string") + void testPalindrome() { + String original = "abccba"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test highly compressible pattern") + void testHighlyCompressible() { + String original = "aaaaaaaaaa"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + + // Should achieve excellent compression ratio + assertTrue(compressed.size() <= 4); + } + + @Test + @DisplayName("Test empty list decompression") + void testEmptyListDecompress() { + List compressed = List.of(); + String decompressed = LZ78.decompress(compressed); + assertEquals("", decompressed); + } + + @Test + @DisplayName("Test binary-like pattern") + void testBinaryPattern() { + String original = "0101010101"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test nested patterns") + void testNestedPatterns() { + String original = "abcabcdefabcdefghi"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test whitespace handling") + void testWhitespace() { + String original = "a b c a b c"; + List compressed = LZ78.compress(original); + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + @DisplayName("Test token structure correctness") + void testTokenStructure() { + String original = "abc"; + List compressed = LZ78.compress(original); + + // All tokens should have valid indices (>= 0) + for (LZ78.Token token : compressed) { + assertTrue(token.index() >= 0); + assertNotNull(token.nextChar()); + } + + String decompressed = LZ78.decompress(compressed); + assertEquals(original, decompressed); + } +} diff --git a/src/test/java/com/thealgorithms/compression/LZWTest.java b/src/test/java/com/thealgorithms/compression/LZWTest.java new file mode 100644 index 000000000000..7f0c7503c822 --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/LZWTest.java @@ -0,0 +1,104 @@ +package com.thealgorithms.compression; + +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.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class LZWTest { + + @Test + void testNullAndEmptyInputs() { + // Test that a null input to compress returns an empty list + assertTrue(LZW.compress(null).isEmpty()); + + // Test that a null input to decompress returns an empty string + assertEquals("", LZW.decompress(null)); + + // Test that an empty input to compress returns an empty list + assertTrue(LZW.compress("").isEmpty()); + + // Test that an empty input to decompress returns an empty string + assertEquals("", LZW.decompress(Collections.emptyList())); + } + + @Test + void testCompressionAndDecompressionWithSimpleString() { + // Test a classic example string + String original = "TOBEORNOTTOBEORTOBEORNOT"; + List compressed = LZW.compress(original); + + // Create the expected output list + List expectedOutput = List.of(84, 79, 66, 69, 79, 82, 78, 79, 84, 256, 258, 260, 265, 259, 261, 263); + + // This assertion will fail if the output is not what we expect + assertEquals(expectedOutput, compressed); + + // This assertion ensures the decompressed string is correct + String decompressed = LZW.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + void testCompressionWithRepeatedChars() { + // Test a string with long runs of the same character + String original = "AAAAABBBBBAAAAA"; + List compressed = LZW.compress(original); + String decompressed = LZW.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + void testCompressionWithUniqueChars() { + // Test a string with no repetitions + String original = "ABCDEFG"; + List compressed = LZW.compress(original); + String decompressed = LZW.decompress(compressed); + assertEquals(original, decompressed); + } + + @Test + void testSymmetry() { + // Test that compressing and then decompressing a complex string returns the + // original + String original = "THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG"; + List compressed = LZW.compress(original); + String decompressed = LZW.decompress(compressed); + assertEquals(original, decompressed); + + // Another symmetry test with special characters and patterns + String original2 = "ababcbababa"; + List compressed2 = LZW.compress(original2); + String decompressed2 = LZW.decompress(compressed2); + assertEquals(original2, decompressed2); + } + + @Test + void testInvalidCompressedData() { + // Test that decompressing with an invalid code throws IllegalArgumentException + // Create a list with a code that doesn't exist in the dictionary + List invalidCompressed = new ArrayList<>(); + invalidCompressed.add(65); // 'A' - valid + invalidCompressed.add(999); // Invalid code (not in dictionary) + + // This should throw IllegalArgumentException with message "Bad compressed k: 999" + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> LZW.decompress(invalidCompressed)); + + assertTrue(exception.getMessage().contains("Bad compressed k: 999")); + } + + @Test + void testDecompressionWithGapInDictionary() { + // Test with codes that skip dictionary entries + List invalidCompressed = new ArrayList<>(); + invalidCompressed.add(84); // 'T' - valid + invalidCompressed.add(500); // Way beyond current dictionary size + + // This should throw IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> LZW.decompress(invalidCompressed)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/MoveToFrontTest.java b/src/test/java/com/thealgorithms/compression/MoveToFrontTest.java new file mode 100644 index 000000000000..42ef6c9cd675 --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/MoveToFrontTest.java @@ -0,0 +1,92 @@ +package com.thealgorithms.compression; + +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; + +public class MoveToFrontTest { + + @Test + public void testTransformAndInverseBananaExample() { + String original = "annb$aa"; + String alphabet = "$abn"; + List expectedTransform = List.of(1, 3, 0, 3, 3, 3, 0); + + // Test forward transform + List actualTransform = MoveToFront.transform(original, alphabet); + assertEquals(expectedTransform, actualTransform); + + // Test inverse transform + String reconstructed = MoveToFront.inverseTransform(actualTransform, alphabet); + assertEquals(original, reconstructed); + } + + @Test + public void testTransformAndInverseCabaaExample() { + String original = "cabaa"; + String alphabet = "abcdef"; + List expectedTransform = List.of(2, 1, 2, 1, 0); + + // Test forward transform + List actualTransform = MoveToFront.transform(original, alphabet); + assertEquals(expectedTransform, actualTransform); + + // Test inverse transform + String reconstructed = MoveToFront.inverseTransform(actualTransform, alphabet); + assertEquals(original, reconstructed); + } + + @Test + public void testEmptyInput() { + String original = ""; + String alphabet = "abc"; + List expectedTransform = List.of(); + + List actualTransform = MoveToFront.transform(original, alphabet); + assertEquals(expectedTransform, actualTransform); + + String reconstructed = MoveToFront.inverseTransform(actualTransform, alphabet); + assertEquals(original, reconstructed); + } + + @Test + public void testEmptyAlphabet() { + assertThrows(IllegalArgumentException.class, () -> MoveToFront.transform("abc", "")); + + assertEquals("", MoveToFront.inverseTransform(List.of(1, 2), "")); + } + + @Test + public void testSymbolNotInAlphabet() { + // 'd' is not in "abc" + assertThrows(IllegalArgumentException.class, () -> MoveToFront.transform("abd", "abc")); + } + + @Test + public void testIndexOutOfBounds() { + // Index 5 is out of bounds for alphabet "abc" + // 1. test index >= alphabet.size() + assertThrows(IllegalArgumentException.class, () -> MoveToFront.inverseTransform(List.of(1, 2, 5), "abc")); + + // 2. test index < 0 + assertThrows(IllegalArgumentException.class, () -> MoveToFront.inverseTransform(List.of(1, -1, 2), "abc")); + } + + @Test + public void testTransformNull() { + List expected = List.of(); + assertEquals(expected, MoveToFront.transform(null, "abc")); + assertThrows(IllegalArgumentException.class, () -> MoveToFront.transform("abc", null)); + } + + @Test + public void testInverseTransformNulls() { + // 1. test indices == null + assertEquals("", MoveToFront.inverseTransform(null, "abc")); + + // 2. test initialAlphabet == null + assertEquals("", MoveToFront.inverseTransform(List.of(1, 2), null)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/RunLengthEncodingTest.java b/src/test/java/com/thealgorithms/compression/RunLengthEncodingTest.java new file mode 100644 index 000000000000..049a7fac9ae9 --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/RunLengthEncodingTest.java @@ -0,0 +1,90 @@ +package com.thealgorithms.compression; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class RunLengthEncodingTest { + + @Test + void testNullInputs() { + // Test that a null input to compress returns an empty string + assertEquals("", RunLengthEncoding.compress(null)); + + // Test that a null input to decompress returns an empty string + assertEquals("", RunLengthEncoding.decompress(null)); + } + + @Test + void testCompressionSimple() { + // Test a typical string with multiple runs + String input = "AAAABBBCCDAA"; + String expected = "4A3B2C1D2A"; + assertEquals(expected, RunLengthEncoding.compress(input)); + } + + @Test + void testCompressionWithNoRuns() { + // Test a string with no consecutive characters + String input = "ABCDE"; + String expected = "1A1B1C1D1E"; + assertEquals(expected, RunLengthEncoding.compress(input)); + } + + @Test + void testCompressionEdgeCases() { + // Test with an empty string + assertEquals("", RunLengthEncoding.compress("")); + + // Test with a single character + assertEquals("1A", RunLengthEncoding.compress("A")); + + // Test with a long run of a single character + assertEquals("10Z", RunLengthEncoding.compress("ZZZZZZZZZZ")); + } + + @Test + void testDecompressionSimple() { + // Test decompression of a typical RLE string + String input = "4A3B2C1D2A"; + String expected = "AAAABBBCCDAA"; + assertEquals(expected, RunLengthEncoding.decompress(input)); + } + + @Test + void testDecompressionWithNoRuns() { + // Test decompression of a string with single characters + String input = "1A1B1C1D1E"; + String expected = "ABCDE"; + assertEquals(expected, RunLengthEncoding.decompress(input)); + } + + @Test + void testDecompressionWithMultiDigitCount() { + // Test decompression where a run count is greater than 9 + String input = "12A1B3C"; + String expected = "AAAAAAAAAAAABCCC"; + assertEquals(expected, RunLengthEncoding.decompress(input)); + } + + @Test + void testDecompressionEdgeCases() { + // Test with an empty string + assertEquals("", RunLengthEncoding.decompress("")); + + // Test with a single character run + assertEquals("A", RunLengthEncoding.decompress("1A")); + } + + @Test + void testSymmetry() { + // Test that compressing and then decompressing returns the original string + String original1 = "WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB"; + String compressed = RunLengthEncoding.compress(original1); + String decompressed = RunLengthEncoding.decompress(compressed); + assertEquals(original1, decompressed); + + String original2 = "A"; + assertEquals(original2, RunLengthEncoding.decompress(RunLengthEncoding.compress(original2))); + } +} diff --git a/src/test/java/com/thealgorithms/compression/ShannonFanoTest.java b/src/test/java/com/thealgorithms/compression/ShannonFanoTest.java new file mode 100644 index 000000000000..ce34088dacca --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/ShannonFanoTest.java @@ -0,0 +1,71 @@ +package com.thealgorithms.compression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ShannonFanoTest { + + @Test + void testNullInput() { + // Test with a null string, should return an empty map + assertTrue(ShannonFano.generateCodes(null).isEmpty()); + } + + @Test + void testSimpleString() { + // A simple string to test basic code generation + String text = "AAABBC"; + Map codes = ShannonFano.generateCodes(text); + + assertEquals(3, codes.size()); + assertEquals("0", codes.get('A')); + assertEquals("10", codes.get('B')); + assertEquals("11", codes.get('C')); + } + + @Test + void testExampleFromStringIssue() { + // Example from the original issue proposal: A:15, B:7, C:6, D:6, E:5 + // The code finds a more optimal split: {A,B} | {C,D,E} -> |22-17|=5 + // instead of {A} | {B,C,D,E} -> |15-24|=9. + String text = "AAAAAAAAAAAAAAABBBBBBBCCCCCCDDDDDDEEEEE"; + Map codes = ShannonFano.generateCodes(text); + + assertEquals(5, codes.size()); + assertEquals("00", codes.get('A')); + assertEquals("01", codes.get('B')); + assertEquals("10", codes.get('C')); + assertEquals("110", codes.get('D')); + assertEquals("111", codes.get('E')); + } + + @Test + void testEdgeCases() { + // Test with an empty string + assertTrue(ShannonFano.generateCodes("").isEmpty()); + + // Test with a single character + Map singleCharCodes = ShannonFano.generateCodes("AAAAA"); + assertEquals(1, singleCharCodes.size()); + assertEquals("0", singleCharCodes.get('A')); // A single symbol gets code "0" + + // Test with all unique characters + String uniqueCharsText = "ABCDEF"; + Map uniqueCharCodes = ShannonFano.generateCodes(uniqueCharsText); + assertEquals(6, uniqueCharCodes.size()); + // Check that codes are unique and have varying lengths as expected + assertEquals(6, uniqueCharCodes.values().stream().distinct().count()); + } + + @Test + void testStringWithTwoChars() { + String text = "ABABAB"; + Map codes = ShannonFano.generateCodes(text); + + assertEquals(2, codes.size()); + assertTrue(codes.get('A').equals("0") && codes.get('B').equals("1") || codes.get('A').equals("1") && codes.get('B').equals("0")); + } +} diff --git a/src/test/java/com/thealgorithms/conversions/Base64Test.java b/src/test/java/com/thealgorithms/conversions/Base64Test.java new file mode 100644 index 000000000000..fbc220c0ca95 --- /dev/null +++ b/src/test/java/com/thealgorithms/conversions/Base64Test.java @@ -0,0 +1,183 @@ +package com.thealgorithms.conversions; + +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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Test cases for Base64 encoding and decoding. + * + * Author: Nithin U. + * Github: https://github.com/NithinU2802 + */ + +class Base64Test { + + @Test + void testBase64Alphabet() { + // Test that all Base64 characters are handled correctly + String allChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + String encoded = Base64.encode(allChars); + String decoded = Base64.decodeToString(encoded); + assertEquals(allChars, decoded); + } + + @ParameterizedTest + @CsvSource({"'', ''", "A, QQ==", "AB, QUI=", "ABC, QUJD", "ABCD, QUJDRA==", "Hello, SGVsbG8=", "'Hello World', SGVsbG8gV29ybGQ=", "'Hello, World!', 'SGVsbG8sIFdvcmxkIQ=='", "'The quick brown fox jumps over the lazy dog', 'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='", + "123456789, MTIzNDU2Nzg5", "'Base64 encoding test', 'QmFzZTY0IGVuY29kaW5nIHRlc3Q='"}) + void + testStringEncoding(String input, String expected) { + assertEquals(expected, Base64.encode(input)); + } + + @ParameterizedTest + @CsvSource({"'', ''", "QQ==, A", "QUI=, AB", "QUJD, ABC", "QUJDRA==, ABCD", "SGVsbG8=, Hello", "'SGVsbG8gV29ybGQ=', 'Hello World'", "'SGVsbG8sIFdvcmxkIQ==', 'Hello, World!'", "'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==', 'The quick brown fox jumps over the lazy dog'", + "MTIzNDU2Nzg5, 123456789", "'QmFzZTY0IGVuY29kaW5nIHRlc3Q=', 'Base64 encoding test'"}) + void + testStringDecoding(String input, String expected) { + assertEquals(expected, Base64.decodeToString(input)); + } + + @Test + void testByteArrayEncoding() { + byte[] input = {72, 101, 108, 108, 111}; + String expected = "SGVsbG8="; + assertEquals(expected, Base64.encode(input)); + } + + @Test + void testByteArrayDecoding() { + String input = "SGVsbG8="; + byte[] expected = {72, 101, 108, 108, 111}; + assertArrayEquals(expected, Base64.decode(input)); + } + + @Test + void testRoundTripEncoding() { + String[] testStrings = {"", "A", "AB", "ABC", "Hello, World!", "The quick brown fox jumps over the lazy dog", "1234567890", "Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?", + "Unicode: வணக்கம்", // Tamil for "Hello" + "Multi-line\nstring\rwith\tdifferent\nwhitespace"}; + + for (String original : testStrings) { + String encoded = Base64.encode(original); + String decoded = Base64.decodeToString(encoded); + assertEquals(original, decoded, "Round trip failed for: " + original); + } + } + + @Test + void testRoundTripByteArrayEncoding() { + byte[][] testArrays = {{}, {0}, {-1}, {0, 1, 2, 3, 4, 5}, {-128, -1, 0, 1, 127}, {72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}}; + + for (byte[] original : testArrays) { + String encoded = Base64.encode(original); + byte[] decoded = Base64.decode(encoded); + assertArrayEquals(original, decoded, "Round trip failed for byte array"); + } + } + + @Test + void testBinaryData() { + // Test with binary data that might contain null bytes + byte[] binaryData = new byte[256]; + for (int i = 0; i < 256; i++) { + binaryData[i] = (byte) i; + } + + String encoded = Base64.encode(binaryData); + byte[] decoded = Base64.decode(encoded); + assertArrayEquals(binaryData, decoded); + } + + @Test + void testNullInputEncoding() { + assertThrows(IllegalArgumentException.class, () -> Base64.encode((String) null)); + assertThrows(IllegalArgumentException.class, () -> Base64.encode((byte[]) null)); + } + + @Test + void testNullInputDecoding() { + assertThrows(IllegalArgumentException.class, () -> Base64.decode(null)); + assertThrows(IllegalArgumentException.class, () -> Base64.decodeToString(null)); + } + + @Test + void testInvalidBase64Characters() { + assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8@")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8#")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8$")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8%")); + } + + @Test + void testInvalidLength() { + // Length must be multiple of 4 + assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQ")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQQ")); + } + + @Test + void testInvalidPaddingPosition() { + // '=' can only appear at the end + assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=QQ")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=Q=")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("=QQQ")); + } + + @Test + void testPaddingVariations() { + // Test different padding scenarios '=' + assertEquals("A", Base64.decodeToString("QQ==")); + assertEquals("AB", Base64.decodeToString("QUI=")); + assertEquals("ABC", Base64.decodeToString("QUJD")); + } + + @Test + void testPaddingConsistency() { + // Ensure that strings requiring different amounts of padding encode/decode correctly + String[] testCases = {"A", "AB", "ABC", "ABCD", "ABCDE", "ABCDEF"}; + + for (String test : testCases) { + String encoded = Base64.encode(test); + String decoded = Base64.decodeToString(encoded); + assertEquals(test, decoded); + + // Verify padding is correct + int expectedPadding = (3 - (test.length() % 3)) % 3; + int actualPadding = 0; + for (int i = encoded.length() - 1; i >= 0 && encoded.charAt(i) == '='; i--) { + actualPadding++; + } + assertEquals(expectedPadding, actualPadding, "Incorrect padding for: " + test); + } + } + + @Test + void testLargeData() { + // Test with larger data to ensure scalability + StringBuilder largeString = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + largeString.append("This is a test string for Base64 encoding. "); + } + + String original = largeString.toString(); + String encoded = Base64.encode(original); + String decoded = Base64.decodeToString(encoded); + assertEquals(original, decoded); + } + + @Test + void testEmptyAndSingleCharacter() { + // Test edge cases + assertEquals("", Base64.encode("")); + assertEquals("", Base64.decodeToString("")); + + assertEquals("QQ==", Base64.encode("A")); + assertEquals("A", Base64.decodeToString("QQ==")); + } +} diff --git a/src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java b/src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java index 9045d100285e..e11a86b4c006 100644 --- a/src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java +++ b/src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java @@ -18,6 +18,12 @@ public void testBinaryToDecimal() { assertEquals(5, BinaryToDecimal.binaryToDecimal(101)); assertEquals(63, BinaryToDecimal.binaryToDecimal(111111)); assertEquals(512, BinaryToDecimal.binaryToDecimal(1000000000)); + + assertEquals(0, BinaryToDecimal.binaryStringToDecimal("0")); + assertEquals(1, BinaryToDecimal.binaryStringToDecimal("1")); + assertEquals(5, BinaryToDecimal.binaryStringToDecimal("101")); + assertEquals(63, BinaryToDecimal.binaryStringToDecimal("111111")); + assertEquals(512, BinaryToDecimal.binaryStringToDecimal("1000000000")); } @Test @@ -25,6 +31,9 @@ public void testBinaryToDecimal() { public void testNegativeBinaryToDecimal() { assertEquals(-1, BinaryToDecimal.binaryToDecimal(-1)); assertEquals(-42, BinaryToDecimal.binaryToDecimal(-101010)); + + assertEquals(-1, BinaryToDecimal.binaryStringToDecimal("-1")); + assertEquals(-42, BinaryToDecimal.binaryStringToDecimal("-101010")); } @Test @@ -32,11 +41,16 @@ public void testNegativeBinaryToDecimal() { public void testLargeBinaryToDecimal() { assertEquals(262144L, BinaryToDecimal.binaryToDecimal(1000000000000000000L)); assertEquals(524287L, BinaryToDecimal.binaryToDecimal(1111111111111111111L)); + + assertEquals(262144L, BinaryToDecimal.binaryStringToDecimal("1000000000000000000")); + assertEquals(524287L, BinaryToDecimal.binaryStringToDecimal("1111111111111111111")); } @ParameterizedTest @CsvSource({"2", "1234", "11112", "101021"}) void testNotCorrectBinaryInput(long binaryNumber) { assertThrows(IllegalArgumentException.class, () -> BinaryToDecimal.binaryToDecimal(binaryNumber)); + + assertThrows(IllegalArgumentException.class, () -> BinaryToDecimal.binaryStringToDecimal(Long.toString(binaryNumber))); } } diff --git a/src/test/java/com/thealgorithms/conversions/CoordinateConverterTest.java b/src/test/java/com/thealgorithms/conversions/CoordinateConverterTest.java new file mode 100644 index 000000000000..e41e9160478d --- /dev/null +++ b/src/test/java/com/thealgorithms/conversions/CoordinateConverterTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.conversions; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class CoordinateConverterTest { + + @ParameterizedTest + @CsvSource({"0, 0, 0, 0", "1, 0, 1, 0", "0, 1, 1, 90", "-1, 0, 1, 180", "0, -1, 1, -90", "3, 4, 5, 53.13010235415599"}) + void testCartesianToPolar(double x, double y, double expectedR, double expectedTheta) { + assertArrayEquals(new double[] {expectedR, expectedTheta}, CoordinateConverter.cartesianToPolar(x, y), 1e-9); + } + + @ParameterizedTest + @CsvSource({"1, 0, 1, 0", "1, 90, 0, 1", "1, 180, -1, 0", "1, -90, 0, -1", "5, 53.13010235415599, 3, 4"}) + void testPolarToCartesian(double r, double theta, double expectedX, double expectedY) { + assertArrayEquals(new double[] {expectedX, expectedY}, CoordinateConverter.polarToCartesian(r, theta), 1e-9); + } + + @ParameterizedTest + @CsvSource({"NaN, 1", "1, NaN", "Infinity, 1", "1, Infinity", "-Infinity, 1", "1, -Infinity"}) + void testCartesianToPolarInvalidInputs(double x, double y) { + assertThrows(IllegalArgumentException.class, () -> CoordinateConverter.cartesianToPolar(x, y)); + } + + @ParameterizedTest + @CsvSource({"-1, 0", "1, NaN", "1, Infinity", "1, -Infinity"}) + void testPolarToCartesianInvalidInputs(double r, double theta) { + assertThrows(IllegalArgumentException.class, () -> CoordinateConverter.polarToCartesian(r, theta)); + } +} 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/conversions/TimeConverterTest.java b/src/test/java/com/thealgorithms/conversions/TimeConverterTest.java new file mode 100644 index 000000000000..4e10318d4563 --- /dev/null +++ b/src/test/java/com/thealgorithms/conversions/TimeConverterTest.java @@ -0,0 +1,69 @@ +package com.thealgorithms.conversions; + +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.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.MethodSource; + +class TimeConverterTest { + + @ParameterizedTest(name = "{0} {1} -> {2} {3}") + @CsvSource({"60, seconds, minutes, 1", "120, seconds, minutes, 2", "2, minutes, seconds, 120", "2, hours, minutes, 120", "1, days, hours, 24", "1, weeks, days, 7", "1, months, days, 30.438", "1, years, days, 365.25", "3600, seconds, hours, 1", "86400, seconds, days, 1", + "604800, seconds, weeks, 1", "31557600, seconds, years, 1"}) + void + testValidConversions(double value, String from, String to, double expected) { + assertEquals(expected, TimeConverter.convertTime(value, from, to)); + } + + @Test + @DisplayName("Zero conversion returns zero") + void testZeroValue() { + assertEquals(0.0, TimeConverter.convertTime(0, "seconds", "hours")); + } + + @Test + @DisplayName("Same-unit conversion returns original value") + void testSameUnitConversion() { + assertEquals(123.456, TimeConverter.convertTime(123.456, "minutes", "minutes")); + } + + @Test + @DisplayName("Negative value throws exception") + void testNegativeValue() { + assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(-5, "seconds", "minutes")); + } + + @ParameterizedTest + @CsvSource({"lightyears, seconds", "minutes, centuries"}) + void testInvalidUnits(String from, String to) { + assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, from, to)); + } + + @Test + @DisplayName("Null unit throws exception") + void testNullUnit() { + assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, null, "seconds")); + + assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, "minutes", null)); + + assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, null, null)); + } + + static Stream roundTripCases() { + return Stream.of(org.junit.jupiter.params.provider.Arguments.of(1.0, "hours", "minutes"), org.junit.jupiter.params.provider.Arguments.of(2.5, "days", "hours"), org.junit.jupiter.params.provider.Arguments.of(1000, "seconds", "minutes")); + } + + @ParameterizedTest + @MethodSource("roundTripCases") + @DisplayName("Round-trip conversion returns original value") + void testRoundTripConversion(double value, String from, String to) { + double converted = TimeConverter.convertTime(value, from, to); + double roundTrip = TimeConverter.convertTime(converted, to, from); + assertEquals(Math.round(value * 1000.0) / 1000.0, Math.round(roundTrip * 1000.0) / 1000.0, 0.05); + } +} 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/DialsAlgorithmTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java new file mode 100644 index 000000000000..2350455d4329 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java @@ -0,0 +1,88 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class DialsAlgorithmTest { + + private List> graph; + private static final int NUM_VERTICES = 6; + private static final int MAX_EDGE_WEIGHT = 10; + + @BeforeEach + void setUp() { + graph = new ArrayList<>(); + for (int i = 0; i < NUM_VERTICES; i++) { + graph.add(new ArrayList<>()); + } + } + + private void addEdge(int u, int v, int weight) { + graph.get(u).add(new DialsAlgorithm.Edge(v, weight)); + } + + @Test + @DisplayName("Test with a simple connected graph") + void testSimpleGraph() { + // Build graph from a standard example + addEdge(0, 1, 2); + addEdge(0, 2, 4); + addEdge(1, 2, 1); + addEdge(1, 3, 7); + addEdge(2, 4, 3); + addEdge(3, 5, 1); + addEdge(4, 3, 2); + addEdge(4, 5, 5); + + int[] expectedDistances = {0, 2, 3, 8, 6, 9}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with a disconnected node") + void testDisconnectedNode() { + addEdge(0, 1, 5); + addEdge(1, 2, 5); + // Node 3, 4, 5 are disconnected + + int[] expectedDistances = {0, 5, 10, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with source as destination") + void testSourceIsDestination() { + addEdge(0, 1, 10); + int[] expectedDistances = {0, 10, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + // Run with source 0 + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test graph with multiple paths to a node") + void testMultiplePaths() { + addEdge(0, 1, 10); + addEdge(0, 2, 3); + addEdge(2, 1, 2); // Shorter path to 1 is via 2 (3+2=5) + + int[] expectedDistances = {0, 5, 3, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with an invalid source vertex") + void testInvalidSource() { + assertThrows(IllegalArgumentException.class, () -> DialsAlgorithm.run(graph, -1, MAX_EDGE_WEIGHT)); + assertThrows(IllegalArgumentException.class, () -> DialsAlgorithm.run(graph, NUM_VERTICES, MAX_EDGE_WEIGHT)); + } +} 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/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/TwoSatTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/TwoSatTest.java new file mode 100644 index 000000000000..76b5aa8a780a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/TwoSatTest.java @@ -0,0 +1,125 @@ +package com.thealgorithms.datastructures.graphs; + +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; + +/** + * Testcases for 2-SAT. + * Please note thea while checking for boolean assignments always keep n + 1 elements and the first element should be always false. + */ +public class TwoSatTest { + private TwoSat twoSat; + + /** + * Case 1: Basic satisfiable case. + * Simple 3 clauses with consistent assignments. + */ + @Test + public void testSatisfiableBasicCase() { + twoSat = new TwoSat(5); + + twoSat.addClause(1, false, 2, false); // (x1 ∨ x2) + twoSat.addClause(3, true, 2, false); // (¬x3 ∨ x2) + twoSat.addClause(4, false, 5, true); // (x4 ∨ ¬x5) + + twoSat.solve(); + + assertTrue(twoSat.isSolutionExists(), "Expected solution to exist"); + boolean[] expected = {false, true, true, true, true, true}; + assertArrayEquals(expected, twoSat.getSolutions()); + } + + /** + * Case 2: Unsatisfiable due to direct contradiction. + * (x1 ∨ x1) ∧ (¬x1 ∨ ¬x1) makes x1 and ¬x1 both required. + */ + @Test + public void testUnsatisfiableContradiction() { + twoSat = new TwoSat(1); + + twoSat.addClause(1, false, 1, false); // (x1 ∨ x1) + twoSat.addClause(1, true, 1, true); // (¬x1 ∨ ¬x1) + + twoSat.solve(); + + assertFalse(twoSat.isSolutionExists(), "Expected no solution (contradiction)"); + } + + /** + * Case 3: Single variable, trivially satisfiable. + * Only (x1 ∨ x1) exists. + */ + @Test + public void testSingleVariableTrivialSatisfiable() { + twoSat = new TwoSat(1); + + twoSat.addClause(1, false, 1, false); // (x1 ∨ x1) + + twoSat.solve(); + + assertTrue(twoSat.isSolutionExists(), "Expected solution to exist"); + boolean[] expected = {false, true}; + assertArrayEquals(expected, twoSat.getSolutions()); + } + + /** + * Case 4: Larger satisfiable system with dependencies. + * (x1 ∨ x2), (¬x2 ∨ x3), (¬x3 ∨ x4), (¬x4 ∨ x5) + */ + @Test + public void testChainedDependenciesSatisfiable() { + twoSat = new TwoSat(5); + + twoSat.addClause(1, false, 2, false); + twoSat.addClause(2, true, 3, false); + twoSat.addClause(3, true, 4, false); + twoSat.addClause(4, true, 5, false); + + twoSat.solve(); + + assertTrue(twoSat.isSolutionExists(), "Expected solution to exist"); + boolean[] solution = twoSat.getSolutions(); + for (int i = 1; i <= 5; i++) { + assertTrue(solution[i], "Expected x" + i + " to be true"); + } + } + + /** + * Case 5: Contradiction due to dependency cycle. + * (x1 ∨ x2), (¬x1 ∨ ¬x2), (x1 ∨ ¬x2), (¬x1 ∨ x2) + * These clauses form a circular dependency making it impossible. + */ + @Test + public void testUnsatisfiableCycle() { + twoSat = new TwoSat(2); + + twoSat.addClause(1, false, 2, false); + twoSat.addClause(1, true, 2, true); + twoSat.addClause(1, false, 2, true); + twoSat.addClause(1, true, 2, false); + + twoSat.solve(); + + assertFalse(twoSat.isSolutionExists(), "Expected no solution due to contradictory cycle"); + } + + /** + * Testcase from CSES + */ + @Test + public void test6() { + twoSat = new TwoSat(2); + + twoSat.addClause(1, true, 2, false); + twoSat.addClause(2, true, 1, false); + twoSat.addClause(1, true, 1, true); + twoSat.addClause(2, false, 2, false); + + twoSat.solve(); + + assertFalse(twoSat.isSolutionExists(), "Expected no solution."); + } +} 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/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/CircularDoublyLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java new file mode 100644 index 000000000000..faa2765a3264 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java @@ -0,0 +1,120 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CircularDoublyLinkedListTest { + + private CircularDoublyLinkedList list; + + @BeforeEach + public void setUp() { + list = new CircularDoublyLinkedList<>(); + } + + @Test + public void testInitialSize() { + assertEquals(0, list.getSize(), "Initial size should be 0."); + } + + @Test + public void testAppendAndSize() { + list.append(10); + list.append(20); + list.append(30); + + assertEquals(3, list.getSize(), "Size after appends should be 3."); + assertEquals("[ 10, 20, 30 ]", list.toString(), "List content should match appended values."); + } + + @Test + public void testRemove() { + list.append(10); + list.append(20); + list.append(30); + + int removed = list.remove(1); + assertEquals(20, removed, "Removed element at index 1 should be 20."); + + assertEquals("[ 10, 30 ]", list.toString(), "List content should reflect removal."); + assertEquals(2, list.getSize(), "Size after removal should be 2."); + + removed = list.remove(0); + assertEquals(10, removed, "Removed element at index 0 should be 10."); + assertEquals("[ 30 ]", list.toString(), "List content should reflect second removal."); + assertEquals(1, list.getSize(), "Size after second removal should be 1."); + } + + @Test + public void testRemoveInvalidIndex() { + list.append(10); + list.append(20); + + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(2), "Removing at invalid index 2 should throw exception."); + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(-1), "Removing at negative index should throw exception."); + } + + @Test + public void testToStringEmpty() { + assertEquals("[]", list.toString(), "Empty list should display as []."); + } + + @Test + public void testSingleElement() { + list.append(10); + + assertEquals(1, list.getSize(), "Size after adding single element should be 1."); + assertEquals("[ 10 ]", list.toString(), "Single element list string should be formatted correctly."); + int removed = list.remove(0); + assertEquals(10, removed, "Removed element should be the one appended."); + assertEquals("[]", list.toString(), "List should be empty after removing last element."); + assertEquals(0, list.getSize(), "Size after removing last element should be 0."); + } + + @Test + public void testNullAppend() { + assertThrows(NullPointerException.class, () -> list.append(null), "Appending null should throw NullPointerException."); + } + + @Test + public void testRemoveLastPosition() { + list.append(10); + list.append(20); + list.append(30); + int removed = list.remove(list.getSize() - 1); + assertEquals(30, removed, "Last element removed should be 30."); + assertEquals(2, list.getSize(), "Size should decrease after removing last element."); + } + + @Test + public void testRemoveFromEmptyThrows() { + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(0), "Remove from empty list should throw."); + } + + @Test + public void testRepeatedAppendAndRemove() { + for (int i = 0; i < 100; i++) { + list.append(i); + } + assertEquals(100, list.getSize()); + + for (int i = 99; i >= 0; i--) { + int removed = list.remove(i); + assertEquals(i, removed, "Removed element should match appended value."); + } + assertEquals(0, list.getSize(), "List should be empty after all removes."); + } + + @Test + public void testToStringAfterMultipleRemoves() { + list.append(1); + list.append(2); + list.append(3); + list.remove(2); + list.remove(0); + assertEquals("[ 2 ]", list.toString(), "ToString should correctly represent remaining elements."); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedListTest.java new file mode 100644 index 000000000000..667f9fcf5700 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/FlattenMultilevelLinkedListTest.java @@ -0,0 +1,112 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +/** + * Unit tests for the FlattenMultilevelLinkedList class. + * This class tests the flattening logic with various list structures, + * including null lists, simple lists, and complex multilevel lists. + */ +final class FlattenMultilevelLinkedListTest { + + // A helper function to convert a flattened list (connected by child pointers) + // into a standard Java List for easy comparison. + private List toList(FlattenMultilevelLinkedList.Node head) { + List list = new ArrayList<>(); + FlattenMultilevelLinkedList.Node current = head; + while (current != null) { + list.add(current.data); + current = current.child; + } + return list; + } + + @Test + @DisplayName("Test with a null list") + void testFlattenNullList() { + assertNull(FlattenMultilevelLinkedList.flatten(null)); + } + + @Test + @DisplayName("Test with a simple, single-level list") + void testFlattenSingleLevelList() { + // Create a simple list: 1 -> 2 -> 3 + FlattenMultilevelLinkedList.Node head = new FlattenMultilevelLinkedList.Node(1); + head.next = new FlattenMultilevelLinkedList.Node(2); + head.next.next = new FlattenMultilevelLinkedList.Node(3); + + // Flatten the list + FlattenMultilevelLinkedList.Node flattenedHead = FlattenMultilevelLinkedList.flatten(head); + + // Expected output: 1 -> 2 -> 3 (vertically) + List expected = List.of(1, 2, 3); + assertEquals(expected, toList(flattenedHead)); + } + + @Test + @DisplayName("Test with a complex multilevel list") + void testFlattenComplexMultilevelList() { + // Create the multilevel structure from the problem description + // 5 -> 10 -> 19 -> 28 + // | | | | + // 7 20 22 35 + // | | | + // 8 50 40 + // | | + // 30 45 + FlattenMultilevelLinkedList.Node head = new FlattenMultilevelLinkedList.Node(5); + head.child = new FlattenMultilevelLinkedList.Node(7); + head.child.child = new FlattenMultilevelLinkedList.Node(8); + head.child.child.child = new FlattenMultilevelLinkedList.Node(30); + + head.next = new FlattenMultilevelLinkedList.Node(10); + head.next.child = new FlattenMultilevelLinkedList.Node(20); + + head.next.next = new FlattenMultilevelLinkedList.Node(19); + head.next.next.child = new FlattenMultilevelLinkedList.Node(22); + head.next.next.child.child = new FlattenMultilevelLinkedList.Node(50); + + head.next.next.next = new FlattenMultilevelLinkedList.Node(28); + head.next.next.next.child = new FlattenMultilevelLinkedList.Node(35); + head.next.next.next.child.child = new FlattenMultilevelLinkedList.Node(40); + head.next.next.next.child.child.child = new FlattenMultilevelLinkedList.Node(45); + + // Flatten the list + FlattenMultilevelLinkedList.Node flattenedHead = FlattenMultilevelLinkedList.flatten(head); + + // Expected sorted output + List expected = List.of(5, 7, 8, 10, 19, 20, 22, 28, 30, 35, 40, 45, 50); + assertEquals(expected, toList(flattenedHead)); + } + + @Test + @DisplayName("Test with some empty child lists") + void testFlattenWithEmptyChildLists() { + // Create a list: 5 -> 10 -> 12 + // | | + // 7 11 + // | + // 9 + FlattenMultilevelLinkedList.Node head = new FlattenMultilevelLinkedList.Node(5); + head.child = new FlattenMultilevelLinkedList.Node(7); + head.child.child = new FlattenMultilevelLinkedList.Node(9); + + head.next = new FlattenMultilevelLinkedList.Node(10); // No child list + head.next.child = null; + + head.next.next = new FlattenMultilevelLinkedList.Node(12); + head.next.next.child = new FlattenMultilevelLinkedList.Node(16); + + // Flatten the list + FlattenMultilevelLinkedList.Node flattenedHead = FlattenMultilevelLinkedList.flatten(head); + + // Expected sorted output + List expected = List.of(5, 7, 9, 10, 12, 16); + assertEquals(expected, toList(flattenedHead)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SinglyLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SinglyLinkedListTest.java index f80c6b5055f0..fde52b982385 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/SinglyLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/SinglyLinkedListTest.java @@ -214,10 +214,10 @@ void recursiveReverseListTest() { @Test void readWithEnhancedForLoopTest() { - final var expeced = new ArrayList(Arrays.asList(10, 20, 30)); + final var expected = new ArrayList(Arrays.asList(10, 20, 30)); SinglyLinkedList list = new SinglyLinkedList(); - for (final var x : expeced) { + for (final var x : expected) { list.insert(x); } @@ -226,7 +226,7 @@ void readWithEnhancedForLoopTest() { readElements.add(x); } - assertEquals(readElements, expeced); + assertEquals(readElements, expected); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgoTest.java b/src/test/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgoTest.java new file mode 100644 index 000000000000..2804534c988c --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/TortoiseHareAlgoTest.java @@ -0,0 +1,46 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class TortoiseHareAlgoTest { + + @Test + void testAppendAndToString() { + TortoiseHareAlgo list = new TortoiseHareAlgo<>(); + list.append(10); + list.append(20); + list.append(30); + assertEquals("[10, 20, 30]", list.toString()); + } + + @Test + void testGetMiddleOdd() { + TortoiseHareAlgo list = new TortoiseHareAlgo<>(); + list.append(1); + list.append(2); + list.append(3); + list.append(4); + list.append(5); + assertEquals(3, list.getMiddle()); + } + + @Test + void testGetMiddleEven() { + TortoiseHareAlgo list = new TortoiseHareAlgo<>(); + list.append(1); + list.append(2); + list.append(3); + list.append(4); + assertEquals(3, list.getMiddle()); // returns second middle + } + + @Test + void testEmptyList() { + TortoiseHareAlgo list = new TortoiseHareAlgo<>(); + assertNull(list.getMiddle()); + assertEquals("[]", list.toString()); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/BSTRecursiveGenericTest.java b/src/test/java/com/thealgorithms/datastructures/trees/BSTRecursiveGenericTest.java new file mode 100644 index 000000000000..5ec0080c5fb8 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/BSTRecursiveGenericTest.java @@ -0,0 +1,169 @@ +package com.thealgorithms.datastructures.trees; + +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.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for BSTRecursiveGeneric class. + * Covers insertion, deletion, search, traversal, sorting, and display. + * + * Author: Udaya Krishnan M + * GitHub: https://github.com/UdayaKrishnanM/ + */ +class BSTRecursiveGenericTest { + + private BSTRecursiveGeneric intTree; + private BSTRecursiveGeneric stringTree; + + /** + * Initializes test trees before each test. + */ + @BeforeEach + void setUp() { + intTree = new BSTRecursiveGeneric<>(); + stringTree = new BSTRecursiveGeneric<>(); + } + + /** + * Tests insertion and search of integer values. + */ + @Test + void testAddAndFindInteger() { + intTree.add(10); + intTree.add(5); + intTree.add(15); + assertTrue(intTree.find(10)); + assertTrue(intTree.find(5)); + assertTrue(intTree.find(15)); + assertFalse(intTree.find(20)); + } + + /** + * Tests insertion and search of string values. + */ + @Test + void testAddAndFindString() { + stringTree.add("apple"); + stringTree.add("banana"); + stringTree.add("cherry"); + assertTrue(stringTree.find("banana")); + assertFalse(stringTree.find("date")); + } + + /** + * Tests deletion of existing and non-existing elements. + */ + @Test + void testRemoveElements() { + intTree.add(10); + intTree.add(5); + intTree.add(15); + assertTrue(intTree.find(5)); + intTree.remove(5); + assertFalse(intTree.find(5)); + intTree.remove(100); // non-existent + assertFalse(intTree.find(100)); + } + + /** + * Tests inorder traversal output. + */ + @Test + void testInorderTraversal() { + intTree.add(20); + intTree.add(10); + intTree.add(30); + intTree.inorder(); // visually verify output + assertTrue(true); + } + + /** + * Tests preorder traversal output. + */ + @Test + void testPreorderTraversal() { + intTree.add(20); + intTree.add(10); + intTree.add(30); + intTree.preorder(); // visually verify output + assertTrue(true); + } + + /** + * Tests postorder traversal output. + */ + @Test + void testPostorderTraversal() { + intTree.add(20); + intTree.add(10); + intTree.add(30); + intTree.postorder(); // visually verify output + assertTrue(true); + } + + /** + * Tests inorderSort returns sorted list. + */ + @Test + void testInorderSort() { + intTree.add(30); + intTree.add(10); + intTree.add(20); + List sorted = intTree.inorderSort(); + assertEquals(List.of(10, 20, 30), sorted); + } + + /** + * Tests prettyDisplay method for visual tree structure. + */ + @Test + void testPrettyDisplay() { + intTree.add(50); + intTree.add(30); + intTree.add(70); + intTree.add(20); + intTree.add(40); + intTree.add(60); + intTree.add(80); + intTree.prettyDisplay(); // visually verify output + assertTrue(true); + } + + /** + * Tests edge case: empty tree. + */ + @Test + void testEmptyTree() { + assertFalse(intTree.find(1)); + List sorted = intTree.inorderSort(); + assertTrue(sorted.isEmpty()); + } + + /** + * Tests edge case: single node tree. + */ + @Test + void testSingleNodeTree() { + intTree.add(42); + assertTrue(intTree.find(42)); + intTree.remove(42); + assertFalse(intTree.find(42)); + } + + /** + * Tests duplicate insertions. + */ + @Test + void testDuplicateInsertions() { + intTree.add(10); + intTree.add(10); + intTree.add(10); + List sorted = intTree.inorderSort(); + assertEquals(List.of(10), sorted); // assuming duplicates are ignored + } +} 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/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/devutils/entities/ProcessDetailsTest.java b/src/test/java/com/thealgorithms/devutils/entities/ProcessDetailsTest.java new file mode 100644 index 000000000000..b5c96c601e4f --- /dev/null +++ b/src/test/java/com/thealgorithms/devutils/entities/ProcessDetailsTest.java @@ -0,0 +1,288 @@ +package com.thealgorithms.devutils.entities; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for ProcessDetails + * Tests the ProcessDetails entity used in scheduling algorithms + * + * @author Sourav Saha (yashsaha555) + */ +class ProcessDetailsTest { + + private ProcessDetails processWithPriority; + private ProcessDetails processWithoutPriority; + + @BeforeEach + void setUp() { + // Initialize test objects before each test + processWithPriority = new ProcessDetails("P1", 0, 10, 5); + processWithoutPriority = new ProcessDetails("P2", 2, 8); + } + + @Test + void testConstructorWithPriority() { + // Test constructor with priority parameter + ProcessDetails process = new ProcessDetails("P3", 1, 15, 3); + + assertEquals("P3", process.getProcessId()); + assertEquals(1, process.getArrivalTime()); + assertEquals(15, process.getBurstTime()); + assertEquals(3, process.getPriority()); + assertEquals(0, process.getWaitingTime()); // Default value + assertEquals(0, process.getTurnAroundTimeTime()); // Default value + } + + @Test + void testConstructorWithoutPriority() { + // Test constructor without priority parameter + ProcessDetails process = new ProcessDetails("P4", 3, 12); + + assertEquals("P4", process.getProcessId()); + assertEquals(3, process.getArrivalTime()); + assertEquals(12, process.getBurstTime()); + assertEquals(0, process.getPriority()); // Default value + assertEquals(0, process.getWaitingTime()); // Default value + assertEquals(0, process.getTurnAroundTimeTime()); // Default value + } + + @Test + void testGetProcessId() { + assertEquals("P1", processWithPriority.getProcessId()); + assertEquals("P2", processWithoutPriority.getProcessId()); + } + + @Test + void testGetArrivalTime() { + assertEquals(0, processWithPriority.getArrivalTime()); + assertEquals(2, processWithoutPriority.getArrivalTime()); + } + + @Test + void testGetBurstTime() { + assertEquals(10, processWithPriority.getBurstTime()); + assertEquals(8, processWithoutPriority.getBurstTime()); + } + + @Test + void testGetWaitingTime() { + // Initial waiting time should be 0 + assertEquals(0, processWithPriority.getWaitingTime()); + assertEquals(0, processWithoutPriority.getWaitingTime()); + } + + @Test + void testGetTurnAroundTimeTime() { + // Initial turnaround time should be 0 + assertEquals(0, processWithPriority.getTurnAroundTimeTime()); + assertEquals(0, processWithoutPriority.getTurnAroundTimeTime()); + } + + @Test + void testGetPriority() { + assertEquals(5, processWithPriority.getPriority()); + assertEquals(0, processWithoutPriority.getPriority()); // Default for constructor without priority + } + + @Test + void testSetProcessId() { + processWithPriority.setProcessId("NewP1"); + assertEquals("NewP1", processWithPriority.getProcessId()); + + // Test setting null process ID + processWithPriority.setProcessId(null); + assertNull(processWithPriority.getProcessId()); + + // Test setting empty process ID + processWithPriority.setProcessId(""); + assertEquals("", processWithPriority.getProcessId()); + } + + @Test + void testSetArrivalTime() { + processWithPriority.setArrivalTime(5); + assertEquals(5, processWithPriority.getArrivalTime()); + + // Test setting negative arrival time + processWithPriority.setArrivalTime(-1); + assertEquals(-1, processWithPriority.getArrivalTime()); + + // Test setting zero arrival time + processWithPriority.setArrivalTime(0); + assertEquals(0, processWithPriority.getArrivalTime()); + } + + @Test + void testSetBurstTime() { + processWithPriority.setBurstTime(20); + assertEquals(20, processWithPriority.getBurstTime()); + + // Test setting zero burst time + processWithPriority.setBurstTime(0); + assertEquals(0, processWithPriority.getBurstTime()); + + // Test setting very large burst time + processWithPriority.setBurstTime(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, processWithPriority.getBurstTime()); + } + + @Test + void testSetWaitingTime() { + processWithPriority.setWaitingTime(15); + assertEquals(15, processWithPriority.getWaitingTime()); + + // Test setting negative waiting time + processWithPriority.setWaitingTime(-5); + assertEquals(-5, processWithPriority.getWaitingTime()); + + // Test setting zero waiting time + processWithPriority.setWaitingTime(0); + assertEquals(0, processWithPriority.getWaitingTime()); + } + + @Test + void testSetTurnAroundTimeTime() { + processWithPriority.setTurnAroundTimeTime(25); + assertEquals(25, processWithPriority.getTurnAroundTimeTime()); + + // Test setting negative turnaround time + processWithPriority.setTurnAroundTimeTime(-10); + assertEquals(-10, processWithPriority.getTurnAroundTimeTime()); + + // Test setting zero turnaround time + processWithPriority.setTurnAroundTimeTime(0); + assertEquals(0, processWithPriority.getTurnAroundTimeTime()); + } + + @Test + void testCompleteProcessLifecycle() { + // Test a complete process lifecycle with realistic scheduling values + ProcessDetails process = new ProcessDetails("P5", 0, 10, 2); + + // Simulate process execution + process.setWaitingTime(5); // Process waited 5 time units + process.setTurnAroundTimeTime(15); // Total time from arrival to completion + + assertEquals("P5", process.getProcessId()); + assertEquals(0, process.getArrivalTime()); + assertEquals(10, process.getBurstTime()); + assertEquals(5, process.getWaitingTime()); + assertEquals(15, process.getTurnAroundTimeTime()); + assertEquals(2, process.getPriority()); + } + + @Test + void testProcessWithMinimumValues() { + // Test process with minimum possible values + ProcessDetails process = new ProcessDetails("", 0, 1, 0); + + assertEquals("", process.getProcessId()); + assertEquals(0, process.getArrivalTime()); + assertEquals(1, process.getBurstTime()); + assertEquals(0, process.getPriority()); + } + + @Test + void testProcessWithMaximumValues() { + // Test process with large values + ProcessDetails process = new ProcessDetails("LongProcessName", Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + assertEquals("LongProcessName", process.getProcessId()); + assertEquals(Integer.MAX_VALUE, process.getArrivalTime()); + assertEquals(Integer.MAX_VALUE, process.getBurstTime()); + assertEquals(Integer.MAX_VALUE, process.getPriority()); + } + + @Test + void testProcessModificationAfterCreation() { + // Test that all fields can be modified after object creation + ProcessDetails process = new ProcessDetails("Original", 1, 5, 3); + + // Modify all fields + process.setProcessId("Modified"); + process.setArrivalTime(10); + process.setBurstTime(20); + process.setWaitingTime(8); + process.setTurnAroundTimeTime(28); + + // Verify all modifications + assertEquals("Modified", process.getProcessId()); + assertEquals(10, process.getArrivalTime()); + assertEquals(20, process.getBurstTime()); + assertEquals(8, process.getWaitingTime()); + assertEquals(28, process.getTurnAroundTimeTime()); + assertEquals(3, process.getPriority()); // Priority has no setter, should remain unchanged + } + + @Test + void testMultipleProcessesIndependence() { + // Test that multiple ProcessDetails objects are independent + ProcessDetails process1 = new ProcessDetails("P1", 0, 5, 1); + ProcessDetails process2 = new ProcessDetails("P2", 2, 8, 2); + + // Modify first process + process1.setWaitingTime(10); + process1.setTurnAroundTimeTime(15); + + // Verify first process was modified correctly + assertEquals("P1", process1.getProcessId()); + assertEquals(0, process1.getArrivalTime()); + assertEquals(5, process1.getBurstTime()); + assertEquals(1, process1.getPriority()); + assertEquals(10, process1.getWaitingTime()); + assertEquals(15, process1.getTurnAroundTimeTime()); + + // Verify second process is unchanged + assertEquals("P2", process2.getProcessId()); + assertEquals(2, process2.getArrivalTime()); + assertEquals(8, process2.getBurstTime()); + assertEquals(2, process2.getPriority()); + assertEquals(0, process2.getWaitingTime()); + assertEquals(0, process2.getTurnAroundTimeTime()); + } + + @Test + void testConstructorParameterOrder() { + // Test that constructor parameters are assigned to correct fields + ProcessDetails process = new ProcessDetails("TestProcess", 123, 456, 789); + + assertEquals("TestProcess", process.getProcessId()); + assertEquals(123, process.getArrivalTime()); + assertEquals(456, process.getBurstTime()); + assertEquals(789, process.getPriority()); + } + + @Test + void testTypicalSchedulingScenario() { + // Test a typical scheduling scenario with multiple processes + ProcessDetails[] processes = {new ProcessDetails("P1", 0, 8, 3), new ProcessDetails("P2", 1, 4, 1), new ProcessDetails("P3", 2, 9, 4), new ProcessDetails("P4", 3, 5, 2)}; + + // Simulate FCFS scheduling calculations + int currentTime = 0; + for (ProcessDetails process : processes) { + if (currentTime < process.getArrivalTime()) { + currentTime = process.getArrivalTime(); + } + process.setWaitingTime(currentTime - process.getArrivalTime()); + currentTime += process.getBurstTime(); + process.setTurnAroundTimeTime(process.getWaitingTime() + process.getBurstTime()); + } + + // Verify calculations + assertEquals(0, processes[0].getWaitingTime()); // P1: arrives at 0, starts immediately + assertEquals(8, processes[0].getTurnAroundTimeTime()); // P1: 0 + 8 + + assertEquals(7, processes[1].getWaitingTime()); // P2: arrives at 1, starts at 8 + assertEquals(11, processes[1].getTurnAroundTimeTime()); // P2: 7 + 4 + + assertEquals(10, processes[2].getWaitingTime()); // P3: arrives at 2, starts at 12 + assertEquals(19, processes[2].getTurnAroundTimeTime()); // P3: 10 + 9 + + assertEquals(18, processes[3].getWaitingTime()); // P4: arrives at 3, starts at 21 + assertEquals(23, processes[3].getTurnAroundTimeTime()); // P4: 18 + 5 + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java new file mode 100644 index 000000000000..063d7bef6e1b --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java @@ -0,0 +1,194 @@ +package com.thealgorithms.dynamicprogramming; + +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.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@code DamerauLevenshteinDistance} class. + * Tests cover edge cases, basic operations, and complex transposition scenarios. + */ +class DamerauLevenshteinDistanceTest { + + @Test + @DisplayName("Should throw exception for null first string") + void testNullFirstString() { + assertThrows(IllegalArgumentException.class, () -> { DamerauLevenshteinDistance.distance(null, "test"); }); + } + + @Test + @DisplayName("Should throw exception for null second string") + void testNullSecondString() { + assertThrows(IllegalArgumentException.class, () -> { DamerauLevenshteinDistance.distance("test", null); }); + } + + @Test + @DisplayName("Should throw exception for both null strings") + void testBothNullStrings() { + assertThrows(IllegalArgumentException.class, () -> { DamerauLevenshteinDistance.distance(null, null); }); + } + + @Test + @DisplayName("Should return 0 for identical strings") + void testIdenticalStrings() { + assertEquals(0, DamerauLevenshteinDistance.distance("", "")); + assertEquals(0, DamerauLevenshteinDistance.distance("a", "a")); + assertEquals(0, DamerauLevenshteinDistance.distance("abc", "abc")); + assertEquals(0, DamerauLevenshteinDistance.distance("hello", "hello")); + } + + @Test + @DisplayName("Should return length when one string is empty") + void testEmptyStrings() { + assertEquals(3, DamerauLevenshteinDistance.distance("", "abc")); + assertEquals(5, DamerauLevenshteinDistance.distance("hello", "")); + assertEquals(0, DamerauLevenshteinDistance.distance("", "")); + } + + @Test + @DisplayName("Should handle single character insertions") + void testSingleInsertion() { + assertEquals(1, DamerauLevenshteinDistance.distance("cat", "cats")); + assertEquals(1, DamerauLevenshteinDistance.distance("ab", "abc")); + assertEquals(1, DamerauLevenshteinDistance.distance("", "a")); + } + + @Test + @DisplayName("Should handle single character deletions") + void testSingleDeletion() { + assertEquals(1, DamerauLevenshteinDistance.distance("cats", "cat")); + assertEquals(1, DamerauLevenshteinDistance.distance("abc", "ab")); + assertEquals(1, DamerauLevenshteinDistance.distance("a", "")); + } + + @Test + @DisplayName("Should handle single character substitutions") + void testSingleSubstitution() { + assertEquals(1, DamerauLevenshteinDistance.distance("cat", "bat")); + assertEquals(1, DamerauLevenshteinDistance.distance("abc", "adc")); + assertEquals(1, DamerauLevenshteinDistance.distance("x", "y")); + } + + @Test + @DisplayName("Should handle adjacent character transpositions") + void testAdjacentTransposition() { + assertEquals(1, DamerauLevenshteinDistance.distance("ab", "ba")); + assertEquals(1, DamerauLevenshteinDistance.distance("abc", "bac")); + assertEquals(1, DamerauLevenshteinDistance.distance("hello", "ehllo")); + } + + @Test + @DisplayName("Should correctly compute distance for CA to ABC") + void testCAtoABC() { + // This is the critical test case that differentiates full DL from OSA + // Full DL: 2 (insert A at start, insert B in middle) + // OSA would give: 3 + assertEquals(2, DamerauLevenshteinDistance.distance("CA", "ABC")); + } + + @Test + @DisplayName("Should handle non-adjacent transpositions") + void testNonAdjacentTransposition() { + assertEquals(2, DamerauLevenshteinDistance.distance("abc", "cba")); + assertEquals(3, DamerauLevenshteinDistance.distance("abcd", "dcba")); + } + + @Test + @DisplayName("Should handle multiple operations") + void testMultipleOperations() { + assertEquals(3, DamerauLevenshteinDistance.distance("kitten", "sitting")); + assertEquals(3, DamerauLevenshteinDistance.distance("saturday", "sunday")); + assertEquals(5, DamerauLevenshteinDistance.distance("intention", "execution")); + } + + @Test + @DisplayName("Should handle completely different strings") + void testCompletelyDifferentStrings() { + assertEquals(3, DamerauLevenshteinDistance.distance("abc", "xyz")); + assertEquals(4, DamerauLevenshteinDistance.distance("hello", "world")); + } + + @Test + @DisplayName("Should handle strings with repeated characters") + void testRepeatedCharacters() { + assertEquals(0, DamerauLevenshteinDistance.distance("aaa", "aaa")); + assertEquals(1, DamerauLevenshteinDistance.distance("aaa", "aab")); + assertEquals(1, DamerauLevenshteinDistance.distance("aaa", "aba")); + } + + @Test + @DisplayName("Should be symmetric") + void testSymmetry() { + assertEquals(DamerauLevenshteinDistance.distance("abc", "def"), DamerauLevenshteinDistance.distance("def", "abc")); + assertEquals(DamerauLevenshteinDistance.distance("hello", "world"), DamerauLevenshteinDistance.distance("world", "hello")); + } + + @Test + @DisplayName("Should handle case sensitivity") + void testCaseSensitivity() { + assertEquals(1, DamerauLevenshteinDistance.distance("Hello", "hello")); + assertEquals(5, DamerauLevenshteinDistance.distance("HELLO", "hello")); + } + + @Test + @DisplayName("Should handle single character strings") + void testSingleCharacterStrings() { + assertEquals(1, DamerauLevenshteinDistance.distance("a", "b")); + assertEquals(0, DamerauLevenshteinDistance.distance("a", "a")); + assertEquals(2, DamerauLevenshteinDistance.distance("a", "abc")); + } + + @Test + @DisplayName("Should handle long strings efficiently") + void testLongStrings() { + String s1 = "abcdefghijklmnopqrstuvwxyz"; + String s2 = "abcdefghijklmnopqrstuvwxyz"; + assertEquals(0, DamerauLevenshteinDistance.distance(s1, s2)); + + String s3 = "abcdefghijklmnopqrstuvwxyz"; + String s4 = "zyxwvutsrqponmlkjihgfedcba"; + assertEquals(25, DamerauLevenshteinDistance.distance(s3, s4)); + } + + @Test + @DisplayName("Should satisfy triangle inequality") + void testTriangleInequality() { + // d(a,c) <= d(a,b) + d(b,c) + String a = "cat"; + String b = "hat"; + String c = "rat"; + + int ab = DamerauLevenshteinDistance.distance(a, b); + int bc = DamerauLevenshteinDistance.distance(b, c); + int ac = DamerauLevenshteinDistance.distance(a, c); + + assertTrue(ac <= ab + bc); + } + + @Test + @DisplayName("Should handle special characters") + void testSpecialCharacters() { + assertEquals(0, DamerauLevenshteinDistance.distance("hello!", "hello!")); + assertEquals(1, DamerauLevenshteinDistance.distance("hello!", "hello?")); + assertEquals(1, DamerauLevenshteinDistance.distance("a@b", "a#b")); + } + + @Test + @DisplayName("Should handle numeric strings") + void testNumericStrings() { + assertEquals(1, DamerauLevenshteinDistance.distance("123", "124")); + assertEquals(1, DamerauLevenshteinDistance.distance("123", "213")); + assertEquals(0, DamerauLevenshteinDistance.distance("999", "999")); + } + + @Test + @DisplayName("Should handle unicode characters") + void testUnicodeCharacters() { + assertEquals(0, DamerauLevenshteinDistance.distance("café", "café")); + assertEquals(1, DamerauLevenshteinDistance.distance("café", "cafe")); + assertEquals(0, DamerauLevenshteinDistance.distance("你好", "你好")); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarrayTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarrayTest.java new file mode 100644 index 000000000000..0f499bf2a2f7 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/MaximumProductSubarrayTest.java @@ -0,0 +1,152 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class MaximumProductSubarrayTest { + + /** + * Test case for an array with all positive numbers. + * The expected maximum product is the product of all elements. + */ + @Test + void testAllPositiveNumbers() { + int[] nums = {2, 3, 4}; + int expected = 24; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 24."); + } + + /** + * Test case for an array with positive and negative numbers. + * The expected maximum product is 24 (subarray [2, -3, -4]). + */ + @Test + void testMixedPositiveAndNegative() { + int[] nums = {2, -3, -4, 1}; + int expected = 24; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 24."); + } + + /** + * Test case for an array containing zeros. + * The expected maximum product is 24 (subarray [4, 6]). + */ + @Test + void testArrayWithZeros() { + int[] nums = {2, 3, 0, 4, 6}; + int expected = 24; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 24."); + } + + /** + * Test case for an array with a single element. + * The expected maximum product is the element itself. + */ + @Test + void testSingleElement() { + int[] nums = {5}; + int expected = 5; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 5."); + } + + /** + * Test case for an array with all negative numbers. + * The expected maximum product is 12 (subarray [-3, -4]). + */ + @Test + void testAllNegativeNumbers() { + int[] nums = {-2, -3, -4}; + int expected = 12; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 12."); + } + + /** + * Test case for an array with negative numbers where odd count of negatives + * breaks the chain. The expected maximum product is 60 (subarray [-2, -3, 10]). + */ + @Test + void testOddNegativeNumbers() { + int[] nums = {-2, -3, 10, -1}; + int expected = 60; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 60."); + } + + /** + * Test case for an empty array. + * The expected maximum product is 0. + */ + @Test + void testEmptyArray() { + int[] nums = {}; + int expected = 0; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 0 for an empty array."); + } + + /** + * Test case for a null array. + * The expected maximum product is 0. + */ + @Test + void testNullArray() { + int[] nums = null; + int expected = 0; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 0 for a null array."); + } + + /** + * Test case for an array with alternating positive and negative numbers. + * The expected maximum product is 6 (subarray [2, 3]). + */ + @Test + void testAlternatingNumbers() { + int[] nums = {2, 3, -2, 4}; + int expected = 6; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 6."); + } + + /** + * Test case for an array with large positive and negative numbers. + * The expected maximum product is 360 (subarray [6, -3, -20]). + */ + @Test + void testLargeNumbers() { + int[] nums = {6, -3, -20, 0, 5}; + int expected = 360; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 360."); + } + + /** + * Test case for an array with single negative number. + * The expected maximum product is the negative number itself. + */ + @Test + void testSingleNegativeElement() { + int[] nums = {-8}; + int expected = -8; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be -8."); + } + + /** + * Test case for an array with multiple zeros. + * The expected maximum product is 6 (subarray [2, 3]). + */ + @Test + void testMultipleZeros() { + int[] nums = {0, 2, 3, 0, 4}; + int expected = 6; + int actual = MaximumProductSubarray.maxProduct(nums); + assertEquals(expected, actual, "The maximum product should be 6."); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java new file mode 100644 index 000000000000..f7ba5eea8bce --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/NeedlemanWunschTest.java @@ -0,0 +1,60 @@ +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; + +/** + * Unit Tests for the {@code NeedlemanWunsch} class + */ +class NeedlemanWunschTest { + + @Test + void testIdenticalStrings() { + int score = NeedlemanWunsch.align("GATTACA", "GATTACA", 1, -1, -2); + assertEquals(7, score); // All matches, 7*1 + } + + @Test + void testSimpleMismatch() { + int score = NeedlemanWunsch.align("GATTACA", "GACTATA", 1, -1, -2); + assertEquals(3, score); + } + + @Test + void testInsertion() { + int score = NeedlemanWunsch.align("GATTACA", "GATACA", 1, -1, -2); + // One deletion (gap penalty) + assertEquals(4, score); + } + + @Test + void testEmptyStrings() { + assertEquals(0, NeedlemanWunsch.align("", "", 1, -1, -2)); + } + + @Test + void testOneEmpty() { + assertEquals(-14, NeedlemanWunsch.align("GATTACA", "", 1, -1, -2)); // 7 gaps × -2 + } + + @Test + void testGapHeavyAlignment() { + int score = NeedlemanWunsch.align("AAAA", "AA", 1, -1, -2); + assertEquals(-2, score); // Two matches (2*1) + two gaps (2*-2) + } + + @ParameterizedTest + @CsvSource({"null,ABC", "ABC,null", "null,null"}) + void testNullInputs(String s1, String s2) { + // Interpret "null" literal as Java null + String first = "null".equals(s1) ? null : s1; + String second = "null".equals(s2) ? null : s2; + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> NeedlemanWunsch.align(first, second, 1, -1, -2)); + assertEquals("Input strings must not be null.", ex.getMessage()); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/SmithWatermanTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/SmithWatermanTest.java new file mode 100644 index 000000000000..46c47e376989 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/SmithWatermanTest.java @@ -0,0 +1,53 @@ +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; + +/** + * Unit tests for the {@code SmithWaterman} class. + */ +class SmithWatermanTest { + + @Test + void testIdenticalStrings() { + int score = SmithWaterman.align("GATTACA", "GATTACA", 2, -1, -2); + assertEquals(14, score); // full match, 7*2 + } + + @Test + void testPartialMatch() { + int score = SmithWaterman.align("GATTACA", "TTAC", 2, -1, -2); + assertEquals(8, score); // best local alignment "TTAC" + } + + @Test + void testNoMatch() { + int score = SmithWaterman.align("AAAA", "TTTT", 1, -1, -2); + assertEquals(0, score); // no alignment worth keeping + } + + @Test + void testInsertionDeletion() { + int score = SmithWaterman.align("ACGT", "ACGGT", 1, -1, -2); + assertEquals(3, score); // local alignment "ACG" + } + + @Test + void testEmptyStrings() { + assertEquals(0, SmithWaterman.align("", "", 1, -1, -2)); + } + + @ParameterizedTest + @CsvSource({"null,ABC", "ABC,null", "null,null"}) + void testNullInputs(String s1, String s2) { + String first = "null".equals(s1) ? null : s1; + String second = "null".equals(s2) ? null : s2; + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> SmithWaterman.align(first, second, 1, -1, -2)); + assertEquals("Input strings must not be null.", ex.getMessage()); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/BentleyOttmannTest.java b/src/test/java/com/thealgorithms/geometry/BentleyOttmannTest.java new file mode 100644 index 000000000000..99240566a2f3 --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/BentleyOttmannTest.java @@ -0,0 +1,324 @@ +package com.thealgorithms.geometry; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive unit tests for {@link BentleyOttmann}. + * + *

This test suite validates the correctness of the Bentley–Ottmann algorithm + * implementation by checking intersection points between multiple line segment configurations.

+ * + *

Test cases include typical, edge, degenerate geometrical setups, and performance tests.

+ */ +public class BentleyOttmannTest { + + private static final double EPS = 1e-6; + + @Test + void testSingleIntersection() { + List segments = List.of(newSegment(1, 1, 5, 5), newSegment(1, 5, 5, 1)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertEquals(1, intersections.size()); + Assertions.assertTrue(containsPoint(intersections, 3.0, 3.0)); + } + + @Test + void testVerticalIntersection() { + List segments = List.of(newSegment(3, 0, 3, 6), newSegment(1, 1, 5, 5)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertEquals(1, intersections.size()); + Assertions.assertTrue(containsPoint(intersections, 3.0, 3.0)); + } + + @Test + void testNoIntersection() { + List segments = List.of(newSegment(0, 0, 1, 1), newSegment(2, 2, 3, 3)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(intersections.isEmpty()); + } + + @Test + void testCoincidentSegments() { + List segments = List.of(newSegment(1, 1, 5, 5), newSegment(1, 1, 5, 5)); + + Set intersections = BentleyOttmann.findIntersections(segments); + + Assertions.assertEquals(2, intersections.size(), "Two identical segments should report 2 intersection points (both endpoints)"); + Assertions.assertTrue(containsPoint(intersections, 1.0, 1.0)); + Assertions.assertTrue(containsPoint(intersections, 5.0, 5.0)); + } + + @Test + void testHorizontalIntersection() { + List segments = List.of(newSegment(0, 2, 4, 2), newSegment(2, 0, 2, 4)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0)); + } + + @Test + void testEmptyList() { + List segments = List.of(); + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(intersections.isEmpty()); + } + + @Test + void testSingleSegment() { + List segments = List.of(newSegment(0, 0, 5, 5)); + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(intersections.isEmpty()); + } + + @Test + void testNullListThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> BentleyOttmann.findIntersections(null)); + } + + @Test + void testParallelSegments() { + // Test 1: Parallel diagonal segments + List diagonalSegments = List.of(newSegment(0, 0, 4, 4), newSegment(1, 0, 5, 4), newSegment(2, 0, 6, 4)); + Assertions.assertTrue(BentleyOttmann.findIntersections(diagonalSegments).isEmpty()); + + // Test 2: Parallel vertical segments + List verticalSegments = List.of(newSegment(1, 0, 1, 5), newSegment(2, 0, 2, 5), newSegment(3, 0, 3, 5)); + Assertions.assertTrue(BentleyOttmann.findIntersections(verticalSegments).isEmpty()); + + // Test 3: Parallel horizontal segments + List horizontalSegments = List.of(newSegment(0, 1, 5, 1), newSegment(0, 2, 5, 2), newSegment(0, 3, 5, 3)); + Assertions.assertTrue(BentleyOttmann.findIntersections(horizontalSegments).isEmpty()); + } + + @Test + void testTouchingEndpoints() { + List segments = List.of(newSegment(0, 0, 2, 2), newSegment(2, 2, 4, 0)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertEquals(1, intersections.size()); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0)); + } + + @Test + void testOverlappingCollinearSegments() { + List segments = List.of(newSegment(0, 0, 4, 4), newSegment(2, 2, 6, 6)); + + Set intersections = BentleyOttmann.findIntersections(segments); + // Overlapping collinear segments share the point (2,2) where second starts + // and (4,4) where first ends - at least one should be detected + Assertions.assertFalse(intersections.isEmpty(), "Should find at least one overlap point"); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0) || containsPoint(intersections, 4.0, 4.0), "Should contain either (2,2) or (4,4)"); + } + + @Test + void testMultipleSegmentsAtOnePoint() { + // Star pattern: 4 segments meeting at (2, 2) + List segments = List.of(newSegment(0, 2, 4, 2), // horizontal + newSegment(2, 0, 2, 4), // vertical + newSegment(0, 0, 4, 4), // diagonal / + newSegment(0, 4, 4, 0) // diagonal \ + ); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0)); + // All segments meet at (2, 2), so should be reported once + Assertions.assertEquals(1, intersections.size()); + } + + @Test + void testGridPattern() { + // 3x3 grid: should have 9 intersection points + List segments = new ArrayList<>(); + + // Vertical lines at x = 0, 1, 2 + for (int i = 0; i <= 2; i++) { + segments.add(newSegment(i, 0, i, 2)); + } + + // Horizontal lines at y = 0, 1, 2 + for (int i = 0; i <= 2; i++) { + segments.add(newSegment(0, i, 2, i)); + } + + Set intersections = BentleyOttmann.findIntersections(segments); + + // Each vertical line crosses each horizontal line + // 3 vertical × 3 horizontal = 9 intersections + Assertions.assertEquals(9, intersections.size(), "3x3 grid should have 9 intersections"); + + // Verify all grid points are present + for (int x = 0; x <= 2; x++) { + for (int y = 0; y <= 2; y++) { + Assertions.assertTrue(containsPoint(intersections, x, y), String.format("Grid point (%d, %d) should be present", x, y)); + } + } + } + + @Test + void testTriangleIntersections() { + // Three segments forming a triangle + List segments = List.of(newSegment(0, 0, 4, 0), // base + newSegment(0, 0, 2, 3), // left side + newSegment(4, 0, 2, 3) // right side + ); + + Set intersections = BentleyOttmann.findIntersections(segments); + // Triangle vertices are intersections + Assertions.assertTrue(containsPoint(intersections, 0.0, 0.0)); + Assertions.assertTrue(containsPoint(intersections, 4.0, 0.0)); + Assertions.assertTrue(containsPoint(intersections, 2.0, 3.0)); + Assertions.assertEquals(3, intersections.size()); + } + + @Test + void testCrossingDiagonals() { + // X pattern with multiple crossings + List segments = List.of(newSegment(0, 0, 10, 10), newSegment(0, 10, 10, 0), newSegment(5, 0, 5, 10), newSegment(0, 5, 10, 5)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(containsPoint(intersections, 5.0, 5.0), "Center point should be present"); + Assertions.assertEquals(1, intersections.size()); + } + + @Test + void testVerySmallSegments() { + List segments = List.of(newSegment(0.001, 0.001, 0.002, 0.002), newSegment(0.001, 0.002, 0.002, 0.001)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertEquals(1, intersections.size()); + Assertions.assertTrue(containsPoint(intersections, 0.0015, 0.0015)); + } + + @Test + void testSegmentsShareCommonPoint() { + List segmentsSameStart = List.of(newSegment(0, 0, 4, 4), newSegment(0, 0, 4, -4), newSegment(0, 0, -4, 4)); + + Set intersectionsSameStart = BentleyOttmann.findIntersections(segmentsSameStart); + Assertions.assertTrue(containsPoint(intersectionsSameStart, 0.0, 0.0)); + List segmentsSameEnd = List.of(newSegment(0, 0, 4, 4), newSegment(8, 4, 4, 4), newSegment(4, 8, 4, 4)); + + Set intersectionsSameEnd = BentleyOttmann.findIntersections(segmentsSameEnd); + Assertions.assertTrue(containsPoint(intersectionsSameEnd, 4.0, 4.0)); + } + + @Test + void testSegmentsAtAngles() { + // Segments at 45, 90, 135 degrees + List segments = List.of(newSegment(0, 2, 4, 2), // horizontal + newSegment(2, 0, 2, 4), // vertical + newSegment(0, 0, 4, 4), // 45 degrees + newSegment(0, 4, 4, 0) // 135 degrees + ); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0)); + } + + @Test + void testPerformanceWithManySegments() { + // Generate 100 random segments + Random random = new Random(42); // Fixed seed for reproducibility + List segments = new ArrayList<>(); + + for (int i = 0; i < 100; i++) { + double x1 = random.nextDouble() * 100; + double y1 = random.nextDouble() * 100; + double x2 = random.nextDouble() * 100; + double y2 = random.nextDouble() * 100; + segments.add(newSegment(x1, y1, x2, y2)); + } + + long startTime = System.currentTimeMillis(); + Set intersections = BentleyOttmann.findIntersections(segments); + long endTime = System.currentTimeMillis(); + + long duration = endTime - startTime; + + // Should complete in reasonable time (< 1 second for 100 segments) + Assertions.assertTrue(duration < 1000, "Algorithm should complete in less than 1 second for 100 segments. Took: " + duration + "ms"); + + // Just verify it returns a valid result + Assertions.assertNotNull(intersections); + System.out.println("Performance test: 100 segments processed in " + duration + "ms, found " + intersections.size() + " intersections"); + } + + @Test + void testIssueExample() { + // Example from the GitHub issue + List segments = List.of(newSegment(1, 1, 5, 5), // Segment A + newSegment(1, 5, 5, 1), // Segment B + newSegment(3, 0, 3, 6) // Segment C + ); + + Set intersections = BentleyOttmann.findIntersections(segments); + + // Expected output: [(3, 3)] + Assertions.assertEquals(1, intersections.size(), "Should find exactly one intersection"); + Assertions.assertTrue(containsPoint(intersections, 3.0, 3.0), "Intersection should be at (3, 3)"); + } + + @Test + void testEventTypeOrdering() { + // Multiple events at the same point with different types + List segments = List.of(newSegment(2, 2, 6, 2), // ends at (2,2) + newSegment(0, 2, 2, 2), // ends at (2,2) + newSegment(2, 2, 2, 6), // starts at (2,2) + newSegment(2, 0, 2, 2) // ends at (2,2) + ); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0)); + } + + @Test + void testCollinearOverlapWithInteriorPoint() { + // Test collinear segments where one segment's interior overlaps another + List segments = List.of(newSegment(0, 0, 6, 6), newSegment(2, 2, 4, 4)); + Set intersections = BentleyOttmann.findIntersections(segments); + + // Should find at least one overlap point (where segments touch/overlap) + Assertions.assertFalse(intersections.isEmpty(), "Should find overlap points for collinear segments"); + Assertions.assertTrue(containsPoint(intersections, 2.0, 2.0) || containsPoint(intersections, 4.0, 4.0), "Should contain overlap boundary point"); + } + + @Test + void testCollinearTouchingAtBothEndpoints() { + // Test collinear segments that touch at both endpoints + // This triggers the "endpoint of both" logic (line 354-355) + List segments = List.of(newSegment(0, 0, 4, 4), newSegment(4, 4, 8, 8)); + + Set intersections = BentleyOttmann.findIntersections(segments); + Assertions.assertEquals(1, intersections.size()); + Assertions.assertTrue(containsPoint(intersections, 4.0, 4.0), "Should find touching point"); + } + + @Test + void testCollinearOverlapPartialInterior() { + // Test case where segments overlap but one point is inside, one is endpoint + List segments = List.of(newSegment(0, 0, 5, 5), newSegment(3, 3, 7, 7)); + + Set intersections = BentleyOttmann.findIntersections(segments); + + // Should detect the overlap region + Assertions.assertFalse(intersections.isEmpty()); + // The algorithm should return at least one of the boundary points + Assertions.assertTrue(containsPoint(intersections, 3.0, 3.0) || containsPoint(intersections, 5.0, 5.0)); + } + + private static BentleyOttmann.Segment newSegment(double x1, double y1, double x2, double y2) { + return new BentleyOttmann.Segment(new Point2D.Double(x1, y1), new Point2D.Double(x2, y2)); + } + + private static boolean containsPoint(Set points, double x, double y) { + return points.stream().anyMatch(p -> Math.abs(p.x - x) < EPS && Math.abs(p.y - y) < EPS); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java index e3e32e43c6de..d3ca0df65829 100644 --- a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java +++ b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java @@ -1,7 +1,9 @@ package com.thealgorithms.geometry; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -10,14 +12,17 @@ public class ConvexHullTest { @Test void testConvexHullBruteForce() { + // Test 1: Triangle with intermediate point List points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); List expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); + // Test 2: Collinear points points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0)); expected = Arrays.asList(new Point(0, 0), new Point(10, 0)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); + // Test 3: Complex polygon points = Arrays.asList(new Point(0, 3), new Point(2, 2), new Point(1, 1), new Point(2, 1), new Point(3, 0), new Point(0, 0), new Point(3, 3), new Point(2, -1), new Point(2, -4), new Point(1, -3)); expected = Arrays.asList(new Point(2, -4), new Point(1, -3), new Point(0, 0), new Point(3, 0), new Point(0, 3), new Point(3, 3)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); @@ -25,16 +30,109 @@ void testConvexHullBruteForce() { @Test void testConvexHullRecursive() { + // Test 1: Triangle - CCW order starting from bottom-left + // The algorithm includes (1,0) as it's detected as an extreme point List points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); + List result = ConvexHull.convexHullRecursive(points); List expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order"); + // Test 2: Collinear points points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0)); + result = ConvexHull.convexHullRecursive(points); expected = Arrays.asList(new Point(0, 0), new Point(10, 0)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + assertEquals(expected, result); + // Test 3: Complex polygon + // Convex hull vertices in CCW order from bottom-most point (2,-4): + // (2,-4) -> (3,0) -> (3,3) -> (0,3) -> (0,0) -> (1,-3) -> back to (2,-4) points = Arrays.asList(new Point(0, 3), new Point(2, 2), new Point(1, 1), new Point(2, 1), new Point(3, 0), new Point(0, 0), new Point(3, 3), new Point(2, -1), new Point(2, -4), new Point(1, -3)); - expected = Arrays.asList(new Point(2, -4), new Point(1, -3), new Point(0, 0), new Point(3, 0), new Point(0, 3), new Point(3, 3)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + result = ConvexHull.convexHullRecursive(points); + expected = Arrays.asList(new Point(2, -4), new Point(3, 0), new Point(3, 3), new Point(0, 3), new Point(0, 0), new Point(1, -3)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order"); + } + + @Test + void testConvexHullRecursiveAdditionalCases() { + // Test 4: Square (all corners on hull) + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); + List result = ConvexHull.convexHullRecursive(points); + List expected = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Square points should be in CCW order"); + + // Test 5: Pentagon with interior point + points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(5, 3), new Point(2, 5), new Point(-1, 3), new Point(2, 2) // (2,2) is interior + ); + result = ConvexHull.convexHullRecursive(points); + // CCW from (0,0): (0,0) -> (4,0) -> (5,3) -> (2,5) -> (-1,3) + expected = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(5, 3), new Point(2, 5), new Point(-1, 3)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Pentagon points should be in CCW order"); + + // Test 6: Simple triangle (clearly convex) + points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); + result = ConvexHull.convexHullRecursive(points); + expected = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Triangle points should be in CCW order"); + } + + /** + * Helper method to verify if points are in counter-clockwise order. + * Uses the signed area method: positive area means CCW. + */ + private boolean isCounterClockwise(List points) { + if (points.size() < 3) { + return true; // Less than 3 points, trivially true + } + + long signedArea = 0; + for (int i = 0; i < points.size(); i++) { + Point p1 = points.get(i); + Point p2 = points.get((i + 1) % points.size()); + signedArea += (long) p1.x() * p2.y() - (long) p2.x() * p1.y(); + } + + return signedArea > 0; // Positive signed area means counter-clockwise + } + + @Test + void testRecursiveHullForCoverage() { + // 1. Test the base cases of the convexHullRecursive method (covering scenarios with < 3 input points). + + // Test Case: 0 points + List pointsEmpty = new ArrayList<>(); + List resultEmpty = ConvexHull.convexHullRecursive(pointsEmpty); + assertTrue(resultEmpty.isEmpty(), "Should return an empty list for an empty input list"); + + // Test Case: 1 point + List pointsOne = List.of(new Point(5, 5)); + // Pass a new ArrayList because the original method modifies the input list. + List resultOne = ConvexHull.convexHullRecursive(new ArrayList<>(pointsOne)); + List expectedOne = List.of(new Point(5, 5)); + assertEquals(expectedOne, resultOne, "Should return the single point for a single-point input"); + + // Test Case: 2 points + List pointsTwo = Arrays.asList(new Point(10, 1), new Point(0, 0)); + List resultTwo = ConvexHull.convexHullRecursive(new ArrayList<>(pointsTwo)); + List expectedTwo = Arrays.asList(new Point(0, 0), new Point(10, 1)); // Should return the two points, sorted. + assertEquals(expectedTwo, resultTwo, "Should return the two sorted points for a two-point input"); + + // 2. Test the logic for handling collinear points in the sortCounterClockwise method. + + // Construct a scenario where multiple collinear points lie on an edge of the convex hull. + // The expected convex hull vertices are (0,0), (10,0), and (5,5). + // When (0,0) is used as the pivot for polar angle sorting, (5,0) and (10,0) are collinear. + // This will trigger the crossProduct == 0 branch in the sortCounterClockwise method. + List pointsWithCollinearOnHull = Arrays.asList(new Point(0, 0), new Point(5, 0), new Point(10, 0), new Point(5, 5), new Point(2, 2)); + + List resultCollinear = ConvexHull.convexHullRecursive(new ArrayList<>(pointsWithCollinearOnHull)); + List expectedCollinear = Arrays.asList(new Point(0, 0), new Point(10, 0), new Point(5, 5)); + + assertEquals(expectedCollinear, resultCollinear, "Should correctly handle collinear points on the hull edge"); + assertTrue(isCounterClockwise(resultCollinear), "The result of the collinear test should be in counter-clockwise order"); } } diff --git a/src/test/java/com/thealgorithms/geometry/DDALineTest.java b/src/test/java/com/thealgorithms/geometry/DDALineTest.java new file mode 100644 index 000000000000..d3d639aa6468 --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/DDALineTest.java @@ -0,0 +1,29 @@ +package com.thealgorithms.geometry; + +import java.awt.Point; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * The {@code DDALineTest} class contains unit tests for the + * {@code DDALine} class, specifically testing the {@code findLine} method. + */ +class DDALineTest { + + static Stream linePointsProvider() { + return Stream.of(Arguments.of(0, 0, 5, 5, List.of(new Point(0, 0), new Point(1, 1), new Point(2, 2), new Point(3, 3), new Point(4, 4), new Point(5, 5))), Arguments.of(0, 0, 5, 0, List.of(new Point(0, 0), new Point(1, 0), new Point(2, 0), new Point(3, 0), new Point(4, 0), new Point(5, 0))), + Arguments.of(0, 0, 0, 5, List.of(new Point(0, 0), new Point(0, 1), new Point(0, 2), new Point(0, 3), new Point(0, 4), new Point(0, 5))), Arguments.of(-2, -2, -5, -5, List.of(new Point(-2, -2), new Point(-3, -3), new Point(-4, -4), new Point(-5, -5))), + Arguments.of(1, 1, 1, 1, List.of(new Point(1, 1))), Arguments.of(0, 0, 1, 5, List.of(new Point(0, 0), new Point(0, 1), new Point(0, 2), new Point(1, 3), new Point(1, 4), new Point(1, 5)))); + } + + @ParameterizedTest + @MethodSource("linePointsProvider") + void testFindLine(int x0, int y0, int x1, int y1, List expected) { + List actual = DDALine.findLine(x0, y0, x1, y1); + Assertions.assertEquals(expected, actual, "The DDA algorithm should generate the expected ordered points."); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/HaversineTest.java b/src/test/java/com/thealgorithms/geometry/HaversineTest.java new file mode 100644 index 000000000000..b4dcca30a8ac --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/HaversineTest.java @@ -0,0 +1,60 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Unit tests for the Haversine formula implementation. + * This class uses parameterized tests to verify the distance calculation + * between various geographical coordinates. + */ +final class HaversineTest { + + // A small tolerance for comparing double values, since floating-point + // arithmetic is not always exact. A 1km tolerance is reasonable for these distances. + private static final double DELTA = 1.0; + + /** + * Provides test cases for the haversine distance calculation. + * Each argument contains: lat1, lon1, lat2, lon2, and the expected distance in kilometers. + * + * @return a stream of arguments for the parameterized test. + */ + static Stream haversineTestProvider() { + return Stream.of( + // Case 1: Distance between Paris, France and Tokyo, Japan + Arguments.of(48.8566, 2.3522, 35.6895, 139.6917, 9712.0), + + // Case 2: Distance between New York, USA and London, UK + Arguments.of(40.7128, -74.0060, 51.5074, -0.1278, 5570.0), + + // Case 3: Zero distance (same point) + Arguments.of(52.5200, 13.4050, 52.5200, 13.4050, 0.0), + + // Case 4: Antipodal points (opposite sides of the Earth) + // Should be approximately half the Earth's circumference (PI * radius) + Arguments.of(0.0, 0.0, 0.0, 180.0, 20015.0)); + } + + /** + * Tests the haversine method with various sets of coordinates. + * + * @param lat1 Latitude of the first point. + * @param lon1 Longitude of the first point. + * @param lat2 Latitude of the second point. + * @param lon2 Longitude of the second point. + * @param expectedDistance The expected distance in kilometers. + */ + @ParameterizedTest + @MethodSource("haversineTestProvider") + @DisplayName("Test Haversine distance calculation for various coordinates") + void testHaversine(double lat1, double lon1, double lat2, double lon2, double expectedDistance) { + double actualDistance = Haversine.haversine(lat1, lon1, lat2, lon2); + assertEquals(expectedDistance, actualDistance, DELTA); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/WusLineTest.java b/src/test/java/com/thealgorithms/geometry/WusLineTest.java new file mode 100644 index 000000000000..0b7f36859e69 --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/WusLineTest.java @@ -0,0 +1,90 @@ +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.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link WusLine} class. + */ +class WusLineTest { + + @Test + void testSimpleLineProducesPixels() { + List pixels = WusLine.drawLine(2, 2, 6, 4); + assertFalse(pixels.isEmpty(), "Line should produce non-empty pixel list"); + } + + @Test + void testEndpointsIncluded() { + List pixels = WusLine.drawLine(0, 0, 5, 3); + boolean hasStart = pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(0, 0))); + boolean hasEnd = pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(5, 3))); + assertTrue(hasStart, "Start point should be represented in the pixel list"); + assertTrue(hasEnd, "End point should be represented in the pixel list"); + } + + @Test + void testIntensityInRange() { + List pixels = WusLine.drawLine(1, 1, 8, 5); + for (WusLine.Pixel pixel : pixels) { + assertTrue(pixel.intensity >= 0.0 && pixel.intensity <= 1.0, "Intensity must be clamped between 0.0 and 1.0"); + } + } + + @Test + void testReversedEndpointsProducesSameLine() { + List forward = WusLine.drawLine(2, 2, 10, 5); + List backward = WusLine.drawLine(10, 5, 2, 2); + + // They should cover same coordinates (ignoring order) + var forwardPoints = forward.stream().map(p -> p.point).collect(java.util.stream.Collectors.toSet()); + var backwardPoints = backward.stream().map(p -> p.point).collect(java.util.stream.Collectors.toSet()); + + assertEquals(forwardPoints, backwardPoints, "Reversing endpoints should yield same line pixels"); + } + + @Test + void testSteepLineHasProperCoverage() { + // Steep line: Δy > Δx + List pixels = WusLine.drawLine(3, 2, 5, 10); + assertFalse(pixels.isEmpty()); + // Expect increasing y values + long increasing = 0; + for (int i = 1; i < pixels.size(); i++) { + if (pixels.get(i).point.y >= pixels.get(i - 1).point.y) { + increasing++; + } + } + assertTrue(increasing > pixels.size() / 2, "Steep line should have increasing y coordinates"); + } + + @Test + void testZeroLengthLineUsesDefaultGradient() { + // same start and end -> dx == 0 -> gradient should take the (dx == 0) ? 1.0 branch + List pixels = WusLine.drawLine(3, 3, 3, 3); + + // sanity checks: we produced pixels and the exact point is present + assertFalse(pixels.isEmpty(), "Zero-length line should produce at least one pixel"); + assertTrue(pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(3, 3))), "Pixel list should include the single-point coordinate (3,3)"); + } + + @Test + void testHorizontalLineIntensityStable() { + List pixels = WusLine.drawLine(1, 5, 8, 5); + + // For each x, take the max intensity among pixels with that x (the visible intensity for the column) + java.util.Map maxIntensityByX = pixels.stream() + .collect(java.util.stream.Collectors.groupingBy(p -> p.point.x, java.util.stream.Collectors.mapping(p -> p.intensity, java.util.stream.Collectors.maxBy(Double::compareTo)))) + .entrySet() + .stream() + .collect(java.util.stream.Collectors.toMap(java.util.Map.Entry::getKey, e -> e.getValue().orElse(0.0))); + + double avgMaxIntensity = maxIntensityByX.values().stream().mapToDouble(Double::doubleValue).average().orElse(0.0); + + assertTrue(avgMaxIntensity > 0.5, "Average of the maximum per-x intensities should be > 0.5 for a horizontal line"); + } +} diff --git a/src/test/java/com/thealgorithms/graph/BronKerboschTest.java b/src/test/java/com/thealgorithms/graph/BronKerboschTest.java new file mode 100644 index 000000000000..54c91c6ac1fd --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/BronKerboschTest.java @@ -0,0 +1,79 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BronKerboschTest { + + @Test + @DisplayName("Complete graph returns single clique") + void completeGraph() { + List> adjacency = buildGraph(4); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 0, 2); + addUndirectedEdge(adjacency, 0, 3); + addUndirectedEdge(adjacency, 1, 2); + addUndirectedEdge(adjacency, 1, 3); + addUndirectedEdge(adjacency, 2, 3); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + assertEquals(1, cliques.size()); + assertEquals(Set.of(0, 1, 2, 3), cliques.get(0)); + } + + @Test + @DisplayName("Path graph produces individual edges") + void pathGraph() { + List> adjacency = buildGraph(3); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 1, 2); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + Set> result = new HashSet<>(cliques); + Set> expected = Set.of(Set.of(0, 1), Set.of(1, 2)); + assertEquals(expected, result); + } + + @Test + @DisplayName("Disconnected graph finds cliques per component") + void disconnectedGraph() { + List> adjacency = buildGraph(5); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 0, 2); + addUndirectedEdge(adjacency, 1, 2); + addUndirectedEdge(adjacency, 3, 4); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + Set> result = new HashSet<>(cliques); + Set> expected = Set.of(Set.of(0, 1, 2), Set.of(3, 4)); + assertEquals(expected, result); + } + + @Test + @DisplayName("Null neighbor set triggers exception") + void nullNeighborSet() { + List> adjacency = new ArrayList<>(); + adjacency.add(null); + assertThrows(IllegalArgumentException.class, () -> BronKerbosch.findMaximalCliques(adjacency)); + } + + private static List> buildGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new HashSet<>()); + } + return graph; + } + + private static void addUndirectedEdge(List> graph, int u, int v) { + graph.get(u).add(v); + graph.get(v).add(u); + } +} diff --git a/src/test/java/com/thealgorithms/graph/DinicTest.java b/src/test/java/com/thealgorithms/graph/DinicTest.java new file mode 100644 index 000000000000..912f787519fa --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/DinicTest.java @@ -0,0 +1,71 @@ +package com.thealgorithms.graph; + +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 DinicTest { + @Test + @DisplayName("Classic CLRS network yields max flow 23 (Dinic)") + void clrsExample() { + int[][] capacity = {{0, 16, 13, 0, 0, 0}, {0, 0, 10, 12, 0, 0}, {0, 4, 0, 0, 14, 0}, {0, 0, 9, 0, 0, 20}, {0, 0, 0, 7, 0, 4}, {0, 0, 0, 0, 0, 0}}; + int maxFlow = Dinic.maxFlow(capacity, 0, 5); + assertEquals(23, maxFlow); + } + + @Test + @DisplayName("Disconnected network has zero flow (Dinic)") + void disconnectedGraph() { + int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + int maxFlow = Dinic.maxFlow(capacity, 0, 2); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Source equals sink returns zero (Dinic)") + void sourceEqualsSink() { + int[][] capacity = {{0, 5}, {0, 0}}; + int maxFlow = Dinic.maxFlow(capacity, 0, 0); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Invalid matrix throws exception (Dinic)") + void invalidMatrix() { + int[][] capacity = {{0, 1}, {1}}; + assertThrows(IllegalArgumentException.class, () -> Dinic.maxFlow(capacity, 0, 1)); + } + + @Test + @DisplayName("Dinic matches Edmonds-Karp on random small graphs") + void parityWithEdmondsKarp() { + java.util.Random rnd = new java.util.Random(42); + for (int n = 3; n <= 7; n++) { + for (int it = 0; it < 25; it++) { + int[][] cap = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + if (i != j && rnd.nextDouble() < 0.35) { + cap[i][j] = rnd.nextInt(10); // capacities 0..9 + } + } + } + int s = 0; + int t = n - 1; + int f1 = Dinic.maxFlow(copyMatrix(cap), s, t); + int f2 = EdmondsKarp.maxFlow(cap, s, t); + assertEquals(f2, f1); + } + } + } + + private static int[][] copyMatrix(int[][] a) { + int[][] b = new int[a.length][a.length]; + for (int i = 0; i < a.length; i++) { + b[i] = java.util.Arrays.copyOf(a[i], a[i].length); + } + return b; + } +} diff --git a/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java b/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java new file mode 100644 index 000000000000..55aeda381031 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java @@ -0,0 +1,48 @@ +package com.thealgorithms.graph; + +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 EdmondsKarpTest { + + @Test + @DisplayName("Classic CLRS network yields max flow 23") + void clrsExample() { + int[][] capacity = {{0, 16, 13, 0, 0, 0}, {0, 0, 10, 12, 0, 0}, {0, 4, 0, 0, 14, 0}, {0, 0, 9, 0, 0, 20}, {0, 0, 0, 7, 0, 4}, {0, 0, 0, 0, 0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 5); + assertEquals(23, maxFlow); + } + + @Test + @DisplayName("Disconnected network has zero flow") + void disconnectedGraph() { + int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 2); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Source equals sink returns zero") + void sourceEqualsSink() { + int[][] capacity = {{0, 5}, {0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 0); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Invalid matrix throws exception") + void invalidMatrix() { + int[][] capacity = {{0, 1}, {1}}; + assertThrows(IllegalArgumentException.class, () -> EdmondsKarp.maxFlow(capacity, 0, 1)); + } + + @Test + @DisplayName("Negative capacity is rejected") + void negativeCapacity() { + int[][] capacity = {{0, -1}, {0, 0}}; + assertThrows(IllegalArgumentException.class, () -> EdmondsKarp.maxFlow(capacity, 0, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/graph/EdmondsTest.java b/src/test/java/com/thealgorithms/graph/EdmondsTest.java new file mode 100644 index 000000000000..ab5740c94217 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/EdmondsTest.java @@ -0,0 +1,172 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class EdmondsTest { + + @Test + void testSimpleGraphNoCycle() { + int n = 4; + int root = 0; + List edges = new ArrayList<>(); + edges.add(new Edmonds.Edge(0, 1, 10)); + edges.add(new Edmonds.Edge(0, 2, 1)); + edges.add(new Edmonds.Edge(2, 1, 2)); + edges.add(new Edmonds.Edge(2, 3, 5)); + + // Expected arborescence edges: (0,2), (2,1), (2,3) + // Weights: 1 + 2 + 5 = 8 + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(8, result); + } + + @Test + void testGraphWithOneCycle() { + int n = 4; + int root = 0; + List edges = new ArrayList<>(); + edges.add(new Edmonds.Edge(0, 1, 10)); + edges.add(new Edmonds.Edge(2, 1, 4)); + edges.add(new Edmonds.Edge(1, 2, 5)); + edges.add(new Edmonds.Edge(2, 3, 6)); + + // Min edges: (2,1, w=4), (1,2, w=5), (2,3, w=6) + // Cycle: 1 -> 2 -> 1, cost = 4 + 5 = 9 + // Contract {1,2} to C. + // New edge (0,C) with w = 10 - min_in(1) = 10 - 4 = 6 + // New edge (C,3) with w = 6 + // Contracted MSA cost = 6 + 6 = 12 + // Total cost = cycle_cost + contracted_msa_cost = 9 + 12 = 21 + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(21, result); + } + + @Test + void testComplexGraphWithCycle() { + int n = 6; + int root = 0; + List edges = new ArrayList<>(); + edges.add(new Edmonds.Edge(0, 1, 10)); + edges.add(new Edmonds.Edge(0, 2, 20)); + edges.add(new Edmonds.Edge(1, 2, 5)); + edges.add(new Edmonds.Edge(2, 3, 10)); + edges.add(new Edmonds.Edge(3, 1, 3)); + edges.add(new Edmonds.Edge(1, 4, 7)); + edges.add(new Edmonds.Edge(3, 4, 2)); + edges.add(new Edmonds.Edge(4, 5, 5)); + + // Min edges: (3,1,3), (1,2,5), (2,3,10), (3,4,2), (4,5,5) + // Cycle: 1->2->3->1, cost = 5+10+3=18 + // Contract {1,2,3} to C. + // Edge (0,1,10) -> (0,C), w = 10-3=7 + // Edge (0,2,20) -> (0,C), w = 20-5=15. Min is 7. + // Edge (1,4,7) -> (C,4,7) + // Edge (3,4,2) -> (C,4,2). Min is 2. + // Edge (4,5,5) -> (4,5,5) + // Contracted MSA: (0,C,7), (C,4,2), (4,5,5). Cost = 7+2+5=14 + // Total cost = 18 + 14 = 32 + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(32, result); + } + + @Test + void testUnreachableNode() { + int n = 4; + int root = 0; + List edges = new ArrayList<>(); + edges.add(new Edmonds.Edge(0, 1, 10)); + edges.add(new Edmonds.Edge(2, 3, 5)); // Node 2 and 3 are unreachable from root 0 + + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(-1, result); + } + + @Test + void testNoEdgesToNonRootNodes() { + int n = 3; + int root = 0; + List edges = new ArrayList<>(); + edges.add(new Edmonds.Edge(0, 1, 10)); // Node 2 is unreachable + + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(-1, result); + } + + @Test + void testSingleNode() { + int n = 1; + int root = 0; + List edges = new ArrayList<>(); + + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(0, result); + } + + @Test + void testInvalidInputThrowsException() { + List edges = new ArrayList<>(); + + assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(0, edges, 0)); + assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, -1)); + assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, 5)); + } + + @Test + void testCoverageForEdgeSelectionLogic() { + int n = 3; + int root = 0; + List edges = new ArrayList<>(); + + // This will cover the `edge.weight < minWeightEdge[edge.to]` being false. + edges.add(new Edmonds.Edge(0, 1, 10)); + edges.add(new Edmonds.Edge(2, 1, 20)); + + // This will cover the `edge.to != root` being false. + edges.add(new Edmonds.Edge(1, 0, 100)); + + // A regular edge to make the graph complete + edges.add(new Edmonds.Edge(0, 2, 5)); + + // Expected MSA: (0,1, w=10) and (0,2, w=5). Total weight = 15. + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(15, result); + } + + @Test + void testCoverageForContractedSelfLoop() { + int n = 4; + int root = 0; + List edges = new ArrayList<>(); + + // Connect root to the cycle components + edges.add(new Edmonds.Edge(0, 1, 20)); + + // Create a cycle 1 -> 2 -> 1 + edges.add(new Edmonds.Edge(1, 2, 5)); + edges.add(new Edmonds.Edge(2, 1, 5)); + + // This is the CRITICAL edge for coverage: + // It connects two nodes (1 and 2) that are part of the SAME cycle. + // After contracting cycle {1, 2} into a supernode C, this edge becomes (C, C), + // which means newU == newV. This will trigger the `false` branch of the `if`. + edges.add(new Edmonds.Edge(1, 1, 100)); // Also a self-loop on a cycle node. + + // Add another edge to ensure node 3 is reachable + edges.add(new Edmonds.Edge(1, 3, 10)); + + // Cycle {1,2} has cost 5+5=10. + // Contract {1,2} to supernode C. + // Edge (0,1,20) becomes (0,C, w=20-5=15). + // Edge (1,3,10) becomes (C,3, w=10). + // Edge (1,1,100) is discarded because newU == newV. + // Cost of contracted graph = 15 + 10 = 25. + // Total cost = cycle cost + contracted cost = 10 + 25 = 35. + long result = Edmonds.findMinimumSpanningArborescence(n, edges, root); + assertEquals(35, result); + } +} 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/HierholzerAlgorithmTest.java b/src/test/java/com/thealgorithms/graph/HierholzerAlgorithmTest.java new file mode 100644 index 000000000000..4dadb206d134 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HierholzerAlgorithmTest.java @@ -0,0 +1,60 @@ +package com.thealgorithms.graph; + +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.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class HierholzerAlgorithmTest { + + @Test + public void testFindsEulerianCircuitInSimpleTriangleGraph() { + Map> graph = new HashMap<>(); + graph.put(0, new LinkedList<>(Arrays.asList(1, 2))); + graph.put(1, new LinkedList<>(Arrays.asList(0, 2))); + graph.put(2, new LinkedList<>(Arrays.asList(0, 1))); + HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph); + assertTrue(algorithm.hasEulerianCircuit()); + List circuit = algorithm.findEulerianCircuit(); + assertEquals(4, circuit.size()); + assertEquals(circuit.get(0), circuit.get(circuit.size() - 1)); + } + + @Test + public void testFailsForGraphWithOddDegreeVertices() { + Map> graph = new HashMap<>(); + graph.put(0, new LinkedList<>(Collections.singletonList(1))); + graph.put(1, new LinkedList<>(Collections.singletonList(0))); + HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph); + assertFalse(algorithm.hasEulerianCircuit()); + assertTrue(algorithm.findEulerianCircuit().isEmpty()); + } + + @Test + public void testFailsForDisconnectedGraph() { + Map> graph = new HashMap<>(); + graph.put(0, new LinkedList<>(Arrays.asList(1, 2))); + graph.put(1, new LinkedList<>(Arrays.asList(0, 2))); + graph.put(2, new LinkedList<>(Arrays.asList(0, 1))); + graph.put(3, new LinkedList<>(Arrays.asList(4, 5))); + graph.put(4, new LinkedList<>(Arrays.asList(3, 5))); + graph.put(5, new LinkedList<>(Arrays.asList(3, 4))); + HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph); + assertFalse(algorithm.hasEulerianCircuit()); + } + + @Test + public void testHandlesEmptyGraph() { + Map> graph = new HashMap<>(); + HierholzerAlgorithm algorithm = new HierholzerAlgorithm(graph); + assertTrue(algorithm.hasEulerianCircuit()); + assertTrue(algorithm.findEulerianCircuit().isEmpty()); + } +} diff --git a/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java new file mode 100644 index 000000000000..612d09561b7b --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java @@ -0,0 +1,169 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link HierholzerEulerianPath}. + * + * This test suite validates Hierholzer's Algorithm implementation + * for finding Eulerian Paths and Circuits in directed graphs. + * + *

Coverage includes: + *

    + *
  • Basic Eulerian Circuit
  • + *
  • Eulerian Path
  • + *
  • Disconnected graphs
  • + *
  • Single-node graphs
  • + *
  • Graphs with no edges
  • + *
  • Graphs that do not have any Eulerian Path/Circuit
  • + *
+ *

+ */ +class HierholzerEulerianPathTest { + + @Test + void testSimpleEulerianCircuit() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Circuit: [0, 1, 2, 0] + List expected = Arrays.asList(0, 1, 2, 0); + assertEquals(expected, result); + } + + @Test + void testEulerianPathDifferentStartEnd() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Path: [0, 1, 2, 3, 1] + List expected = Arrays.asList(0, 1, 2, 3, 1); + assertEquals(expected, result); + } + + @Test + void testNoEulerianPathExists() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + // Edge 2->0 missing, so it's not Eulerian Circuit + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + assertEquals(result, Arrays.asList(0, 1, 2)); + } + + @Test + void testDisconnectedGraph() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4); + graph.addEdge(0, 1); + graph.addEdge(2, 3); // disconnected component + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Disconnected graph cannot have an Eulerian path + assertTrue(result.isEmpty()); + } + + @Test + void testGraphWithSelfLoop() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 0); // self loop + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Circuit with self-loop included: [0, 0, 1, 2, 0] + assertEquals(Arrays.asList(0, 0, 1, 2, 0), result); + } + + @Test + void testSingleNodeNoEdges() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Only one vertex and no edges + assertEquals(Collections.singletonList(0), result); + } + + @Test + void testSingleNodeWithLoop() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1); + graph.addEdge(0, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian circuit on a single node with a self-loop + assertEquals(Arrays.asList(0, 0), result); + } + + @Test + void testComplexEulerianCircuit() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(5); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + graph.addEdge(4, 0); + graph.addEdge(1, 3); + graph.addEdge(3, 1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Verify all edges are used + int totalEdges = 7; + assertEquals(totalEdges + 1, result.size(), "Path must contain all edges + 1 vertices"); + assertEquals(result.get(0), result.get(result.size() - 1), "Must form a circuit"); + } + + @Test + void testMultipleEdgesBetweenSameNodes() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Hava a Eulerian Path but not a Eulerian Circuit + assertEquals(result, Arrays.asList(0, 1, 2, 0, 1)); + } + + @Test + void testCoverageForEmptyGraph() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(0); + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Empty graph has no vertices or path + assertTrue(result.isEmpty()); + } +} diff --git a/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java new file mode 100644 index 000000000000..1344934b62e4 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java @@ -0,0 +1,44 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HungarianAlgorithmTest { + + @Test + @DisplayName("Classic 3x3 example: minimal cost 5 with assignment [1,0,2]") + void classicSquareExample() { + int[][] cost = {{4, 1, 3}, {2, 0, 5}, {3, 2, 2}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(5, res.minCost); + assertArrayEquals(new int[] {1, 0, 2}, res.assignment); + } + + @Test + @DisplayName("Rectangular (more rows than cols): pads to square and returns -1 for unassigned rows") + void rectangularMoreRows() { + int[][] cost = {{7, 3}, {2, 8}, {5, 1}}; + // Optimal selects any 2 rows: choose row1->col0 (2) and row2->col1 (1) => total 3 + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(3, res.minCost); + // Two rows assigned to 2 columns; one row remains -1. + int assigned = 0; + for (int a : res.assignment) { + if (a >= 0) { + assigned++; + } + } + assertEquals(2, assigned); + } + + @Test + @DisplayName("Zero diagonal yields zero total cost") + void zeroDiagonal() { + int[][] cost = {{0, 5, 9}, {4, 0, 7}, {3, 6, 0}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(0, res.minCost); + } +} diff --git a/src/test/java/com/thealgorithms/graph/PushRelabelTest.java b/src/test/java/com/thealgorithms/graph/PushRelabelTest.java new file mode 100644 index 000000000000..b0021ec805b8 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/PushRelabelTest.java @@ -0,0 +1,66 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PushRelabelTest { + + @Test + @DisplayName("Classic CLRS network yields max flow 23 (PushRelabel)") + void clrsExample() { + int[][] capacity = {{0, 16, 13, 0, 0, 0}, {0, 0, 10, 12, 0, 0}, {0, 4, 0, 0, 14, 0}, {0, 0, 9, 0, 0, 20}, {0, 0, 0, 7, 0, 4}, {0, 0, 0, 0, 0, 0}}; + int maxFlow = PushRelabel.maxFlow(capacity, 0, 5); + assertEquals(23, maxFlow); + } + + @Test + @DisplayName("Disconnected network has zero flow (PushRelabel)") + void disconnectedGraph() { + int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + int maxFlow = PushRelabel.maxFlow(capacity, 0, 2); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Source equals sink returns zero (PushRelabel)") + void sourceEqualsSink() { + int[][] capacity = {{0, 5}, {0, 0}}; + int maxFlow = PushRelabel.maxFlow(capacity, 0, 0); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("PushRelabel matches Dinic and EdmondsKarp on random small graphs") + void parityWithOtherMaxFlow() { + java.util.Random rnd = new java.util.Random(42); + for (int n = 3; n <= 7; n++) { + for (int it = 0; it < 25; it++) { + int[][] cap = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + if (i != j && rnd.nextDouble() < 0.35) { + cap[i][j] = rnd.nextInt(10); // capacities 0..9 + } + } + } + int s = 0; + int t = n - 1; + int fPushRelabel = PushRelabel.maxFlow(copyMatrix(cap), s, t); + int fDinic = Dinic.maxFlow(copyMatrix(cap), s, t); + int fEdmondsKarp = EdmondsKarp.maxFlow(cap, s, t); + assertEquals(fDinic, fPushRelabel); + assertEquals(fEdmondsKarp, fPushRelabel); + } + } + } + + private static int[][] copyMatrix(int[][] a) { + int[][] b = new int[a.length][a.length]; + for (int i = 0; i < a.length; i++) { + b[i] = java.util.Arrays.copyOf(a[i], a[i].length); + } + return b; + } +} diff --git a/src/test/java/com/thealgorithms/graph/StoerWagnerTest.java b/src/test/java/com/thealgorithms/graph/StoerWagnerTest.java new file mode 100644 index 000000000000..894d99687d1d --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/StoerWagnerTest.java @@ -0,0 +1,64 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the StoerWagner global minimum cut algorithm. + * + * These tests verify correctness of the implementation across + * several graph configurations: simple, complete, disconnected, + * and small edge cases. + */ +public class StoerWagnerTest { + + @Test + public void testSimpleGraph() { + int[][] graph = {{0, 3, 2, 0}, {3, 0, 1, 4}, {2, 1, 0, 5}, {0, 4, 5, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(5, algo.findMinCut(graph)); // Correct minimum cut = 5 + } + + @Test + public void testTriangleGraph() { + int[][] graph = {{0, 2, 3}, {2, 0, 4}, {3, 4, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(5, algo.findMinCut(graph)); // min cut = 5 + } + + @Test + public void testDisconnectedGraph() { + int[][] graph = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(0, algo.findMinCut(graph)); // Disconnected graph => cut = 0 + } + + @Test + public void testCompleteGraph() { + int[][] graph = {{0, 1, 1, 1}, {1, 0, 1, 1}, {1, 1, 0, 1}, {1, 1, 1, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(3, algo.findMinCut(graph)); // Each vertex connected to all others + } + + @Test + public void testSingleVertex() { + int[][] graph = {{0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(0, algo.findMinCut(graph)); // Only one vertex + } + + @Test + public void testTwoVertices() { + int[][] graph = {{0, 7}, {7, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(7, algo.findMinCut(graph)); // Only one edge, cut weight = 7 + } + + @Test + public void testSquareGraphWithDiagonal() { + int[][] graph = {{0, 2, 0, 2}, {2, 0, 3, 0}, {0, 3, 0, 4}, {2, 0, 4, 0}}; + StoerWagner algo = new StoerWagner(); + assertEquals(4, algo.findMinCut(graph)); // verified manually + } +} diff --git a/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java b/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java new file mode 100644 index 000000000000..1de37a310939 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java @@ -0,0 +1,76 @@ +package com.thealgorithms.graph; + +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.DisplayName; +import org.junit.jupiter.api.Test; + +class YensKShortestPathsTest { + + @Test + @DisplayName("Basic K-shortest paths on small directed graph") + void basicKPaths() { + // Graph (directed) with non-negative weights, -1 = no edge + // 0 -> 1 (1), 0 -> 2 (2), 1 -> 3 (1), 2 -> 3 (1), 0 -> 3 (5), 1 -> 2 (1) + int n = 4; + int[][] w = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + w[i][j] = -1; + } + } + w[0][1] = 1; + w[0][2] = 2; + w[1][3] = 1; + w[2][3] = 1; + w[0][3] = 5; + w[1][2] = 1; + + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 3, 3); + // Expected K=3 loopless shortest paths from 0 to 3, ordered by total cost: + // 1) 0-1-3 (cost 2) + // 2) 0-2-3 (cost 3) + // 3) 0-1-2-3 (cost 3) -> tie with 0-2-3; tie-broken lexicographically by nodes + assertEquals(3, paths.size()); + assertEquals(List.of(0, 1, 3), paths.get(0)); + assertEquals(List.of(0, 1, 2, 3), paths.get(1)); // lexicographically before [0,2,3] + assertEquals(List.of(0, 2, 3), paths.get(2)); + } + + @Test + @DisplayName("K larger than available paths returns only existing ones") + void kLargerThanAvailable() { + int[][] w = {{-1, 1, -1}, {-1, -1, 1}, {-1, -1, -1}}; + // Only one simple path 0->1->2 + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 2, 5); + assertEquals(1, paths.size()); + assertEquals(List.of(0, 1, 2), paths.get(0)); + } + + @Test + @DisplayName("No path returns empty list") + void noPath() { + int[][] w = {{-1, -1}, {-1, -1}}; + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 1, 3); + assertEquals(0, paths.size()); + } + + @Test + @DisplayName("Source equals destination returns trivial path") + void sourceEqualsDestination() { + int[][] w = {{-1, 1}, {-1, -1}}; + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 0, 2); + // First path is [0] + assertEquals(1, paths.size()); + assertEquals(List.of(0), paths.get(0)); + } + + @Test + @DisplayName("Negative weight entries (other than -1) are rejected") + void negativeWeightsRejected() { + int[][] w = {{-1, -2}, {-1, -1}}; + assertThrows(IllegalArgumentException.class, () -> YensKShortestPaths.kShortestPaths(w, 0, 1, 2)); + } +} diff --git a/src/test/java/com/thealgorithms/graph/ZeroOneBfsTest.java b/src/test/java/com/thealgorithms/graph/ZeroOneBfsTest.java new file mode 100644 index 000000000000..5de9eadf0930 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/ZeroOneBfsTest.java @@ -0,0 +1,68 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ZeroOneBfsTest { + + // Helper to build adjacency list with capacity n + private static List> makeAdj(int n) { + List> adj = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + adj.add(new ArrayList<>()); + } + return adj; + } + + @Test + void simpleLineGraph() { + int n = 4; + List> adj = makeAdj(n); + // 0 --0--> 1 --1--> 2 --0--> 3 + adj.get(0).add(new int[] {1, 0}); + adj.get(1).add(new int[] {2, 1}); + adj.get(2).add(new int[] {3, 0}); + + int[] dist = ZeroOneBfs.shortestPaths(n, adj, 0); + assertArrayEquals(new int[] {0, 0, 1, 1}, dist); + } + + @Test + void parallelEdgesPreferZero() { + int n = 3; + List> adj = makeAdj(n); + // Two edges 0->1: weight 1 and weight 0. Algorithm should choose 0. + adj.get(0).add(new int[] {1, 1}); + adj.get(0).add(new int[] {1, 0}); + adj.get(1).add(new int[] {2, 1}); + + int[] dist = ZeroOneBfs.shortestPaths(n, adj, 0); + assertArrayEquals(new int[] {0, 0, 1}, dist); + } + + @Test + void unreachableNodes() { + int n = 3; + List> adj = makeAdj(n); + adj.get(0).add(new int[] {1, 0}); + int[] dist = ZeroOneBfs.shortestPaths(n, adj, 0); + // node 2 unreachable -> Integer.MAX_VALUE + assertArrayEquals(new int[] {0, 0, Integer.MAX_VALUE}, dist); + } + + @Test + void invalidArgs() { + int n = 2; + List> adj = makeAdj(n); + // invalid weight + adj.get(0).add(new int[] {1, 2}); + assertThrows(IllegalArgumentException.class, () -> ZeroOneBfs.shortestPaths(n, adj, 0)); + // invalid src + assertThrows(IllegalArgumentException.class, () -> ZeroOneBfs.shortestPaths(n, adj, -1)); + assertThrows(IllegalArgumentException.class, () -> ZeroOneBfs.shortestPaths(n, adj, 2)); + } +} 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/ChebyshevIterationTest.java b/src/test/java/com/thealgorithms/maths/ChebyshevIterationTest.java new file mode 100644 index 000000000000..d5cf83818fa4 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ChebyshevIterationTest.java @@ -0,0 +1,105 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class ChebyshevIterationTest { + + @Test + public void testSolveSimple2x2Diagonal() { + double[][] a = {{2, 0}, {0, 1}}; + double[] b = {2, 2}; + double[] x0 = {0, 0}; + double minEig = 1.0; + double maxEig = 2.0; + int maxIter = 50; + double tol = 1e-9; + double[] expected = {1.0, 2.0}; + + double[] result = ChebyshevIteration.solve(a, b, x0, minEig, maxEig, maxIter, tol); + assertArrayEquals(expected, result, 1e-9); + } + + @Test + public void testSolve2x2Symmetric() { + double[][] a = {{4, 1}, {1, 3}}; + double[] b = {1, 2}; + double[] x0 = {0, 0}; + double minEig = (7.0 - Math.sqrt(5.0)) / 2.0; + double maxEig = (7.0 + Math.sqrt(5.0)) / 2.0; + int maxIter = 100; + double tol = 1e-10; + double[] expected = {1.0 / 11.0, 7.0 / 11.0}; + + double[] result = ChebyshevIteration.solve(a, b, x0, minEig, maxEig, maxIter, tol); + assertArrayEquals(expected, result, 1e-9); + } + + @Test + public void testAlreadyAtSolution() { + double[][] a = {{2, 0}, {0, 1}}; + double[] b = {2, 2}; + double[] x0 = {1, 2}; + double minEig = 1.0; + double maxEig = 2.0; + int maxIter = 10; + double tol = 1e-5; + double[] expected = {1.0, 2.0}; + + double[] result = ChebyshevIteration.solve(a, b, x0, minEig, maxEig, maxIter, tol); + assertArrayEquals(expected, result, 0.0); + } + + @Test + public void testMismatchedDimensionsAB() { + double[][] a = {{1, 0}, {0, 1}}; + double[] b = {1}; + double[] x0 = {0, 0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 2, 10, 1e-5)); + } + + @Test + public void testMismatchedDimensionsAX() { + double[][] a = {{1, 0}, {0, 1}}; + double[] b = {1, 1}; + double[] x0 = {0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 2, 10, 1e-5)); + } + + @Test + public void testNonSquareMatrix() { + double[][] a = {{1, 0, 0}, {0, 1, 0}}; + double[] b = {1, 1}; + double[] x0 = {0, 0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 2, 10, 1e-5)); + } + + @Test + public void testInvalidEigenvalues() { + double[][] a = {{1, 0}, {0, 1}}; + double[] b = {1, 1}; + double[] x0 = {0, 0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 2, 1, 10, 1e-5)); + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 1, 10, 1e-5)); + } + + @Test + public void testNonPositiveDefinite() { + double[][] a = {{1, 0}, {0, 1}}; + double[] b = {1, 1}; + double[] x0 = {0, 0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 0, 1, 10, 1e-5)); + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, -1, 1, 10, 1e-5)); + } + + @Test + public void testInvalidIterationCount() { + double[][] a = {{1, 0}, {0, 1}}; + double[] b = {1, 1}; + double[] x0 = {0, 0}; + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 2, 0, 1e-5)); + assertThrows(IllegalArgumentException.class, () -> ChebyshevIteration.solve(a, b, x0, 1, 2, -1, 1e-5)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java b/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java new file mode 100644 index 000000000000..b4ee0cf0659c --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/EulerPseudoprimeTest.java @@ -0,0 +1,93 @@ +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 static org.mockito.ArgumentMatchers.any; + +import java.math.BigInteger; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class EulerPseudoprimeTest { + + @Test + void testPrimeNumbers() { + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(7), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(13), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(101), 5)); + } + + @Test + void testCompositeNumbers() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(9), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(21), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(221), 5)); + } + + @Test + void testEvenNumbers() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(4), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(100), 5)); + } + + @Test + void testEdgeCases() { + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(0), 5)); + assertFalse(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(1), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(2), 5)); + assertTrue(EulerPseudoprime.isProbablePrime(BigInteger.valueOf(3), 5)); + } + + @Test + void testIsProbablePrimeWhenJacobiSymbolIsZero() { + try (MockedStatic mockedPrimality = Mockito.mockStatic(EulerPseudoprime.class, Mockito.CALLS_REAL_METHODS)) { + + // Mock jacobiSymbol to return 0 to test the branch + mockedPrimality.when(() -> EulerPseudoprime.jacobiSymbol(any(BigInteger.class), any(BigInteger.class))).thenReturn(0); + + boolean result = EulerPseudoprime.isProbablePrime(BigInteger.valueOf(15), 1); + + assertFalse(result); + } + } + + @Test + void testJacobiSymbolThrowsForEvenOrNonPositiveN() throws Exception { + var method = EulerPseudoprime.class.getDeclaredMethod("jacobiSymbol", BigInteger.class, BigInteger.class); + + // Helper lambda to unwrap InvocationTargetException + Runnable invokeJacobi = () -> { + try { + method.invoke(null, BigInteger.valueOf(2), BigInteger.valueOf(8)); + } catch (Exception e) { + // unwrap + Throwable cause = e.getCause(); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } else { + throw new RuntimeException(e); + } + } + }; + + // Now check that it actually throws + assertThrows(IllegalArgumentException.class, invokeJacobi::run); + + // Another case: non-positive n + Runnable invokeJacobi2 = () -> { + try { + method.invoke(null, BigInteger.valueOf(5), BigInteger.valueOf(-3)); + } catch (Exception e) { + Throwable cause = e.getCause(); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } else { + throw new RuntimeException(e); + } + } + }; + assertThrows(IllegalArgumentException.class, invokeJacobi2::run); + } +} 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/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/GermainPrimeAndSafePrimeTest.java b/src/test/java/com/thealgorithms/maths/GermainPrimeAndSafePrimeTest.java new file mode 100644 index 000000000000..069d1156295f --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/GermainPrimeAndSafePrimeTest.java @@ -0,0 +1,49 @@ +package com.thealgorithms.maths; + +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.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class GermainPrimeAndSafePrimeTest { + + static Stream provideNumbersForGermainPrimes() { + return Stream.of(Arguments.of(2, Boolean.TRUE), Arguments.of(3, Boolean.TRUE), Arguments.of(5, Boolean.TRUE), Arguments.of(11, Boolean.TRUE), Arguments.of(23, Boolean.TRUE), Arguments.of(293, Boolean.TRUE), Arguments.of(4, Boolean.FALSE), Arguments.of(7, Boolean.FALSE), + Arguments.of(9, Boolean.FALSE), Arguments.of(1, Boolean.FALSE)); + } + + static Stream provideNumbersForSafePrimes() { + return Stream.of(Arguments.of(5, Boolean.TRUE), Arguments.of(7, Boolean.TRUE), Arguments.of(11, Boolean.TRUE), Arguments.of(23, Boolean.TRUE), Arguments.of(1283, Boolean.TRUE), Arguments.of(4, Boolean.FALSE), Arguments.of(13, Boolean.FALSE), Arguments.of(9, Boolean.FALSE), + Arguments.of(1, Boolean.FALSE)); + } + + static Stream provideNegativeNumbers() { + return Stream.of(-10, -1, 0); + } + + @ParameterizedTest + @MethodSource("provideNumbersForGermainPrimes") + @DisplayName("Check whether a number is a Germain prime") + void testValidGermainPrimes(int number, boolean expected) { + assertEquals(expected, GermainPrimeAndSafePrime.isGermainPrime(number)); + } + + @ParameterizedTest + @MethodSource("provideNumbersForSafePrimes") + @DisplayName("Check whether a number is a Safe prime") + void testValidSafePrimes(int number, boolean expected) { + assertEquals(expected, GermainPrimeAndSafePrime.isSafePrime(number)); + } + + @ParameterizedTest + @MethodSource("provideNegativeNumbers") + @DisplayName("Negative numbers and zero should throw IllegalArgumentException") + void testNegativeNumbersThrowException(int number) { + assertThrows(IllegalArgumentException.class, () -> GermainPrimeAndSafePrime.isGermainPrime(number)); + assertThrows(IllegalArgumentException.class, () -> GermainPrimeAndSafePrime.isSafePrime(number)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/HappyNumberTest.java b/src/test/java/com/thealgorithms/maths/HappyNumberTest.java new file mode 100644 index 000000000000..4b7cb795406f --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/HappyNumberTest.java @@ -0,0 +1,32 @@ +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; + +public class HappyNumberTest { + + @Test + void testHappyNumbers() { + // Known happy numbers + assertTrue(HappyNumber.isHappy(1)); + assertTrue(HappyNumber.isHappy(7)); + assertTrue(HappyNumber.isHappy(19)); + assertTrue(HappyNumber.isHappy(100)); + } + + @Test + void testUnhappyNumbers() { + // Known unhappy numbers + assertFalse(HappyNumber.isHappy(2)); + assertFalse(HappyNumber.isHappy(4)); + assertFalse(HappyNumber.isHappy(20)); + } + + @Test + void testLargeNumber() { + // Just to check behavior with larger input + assertTrue(HappyNumber.isHappy(1000000)); // reduces to 1 eventually + } +} diff --git a/src/test/java/com/thealgorithms/maths/HarshadNumberTest.java b/src/test/java/com/thealgorithms/maths/HarshadNumberTest.java index af1c459f3d7f..299e6bd78a99 100644 --- a/src/test/java/com/thealgorithms/maths/HarshadNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/HarshadNumberTest.java @@ -1,25 +1,135 @@ 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.Assertions; import org.junit.jupiter.api.Test; -public class HarshadNumberTest { +/** + * Test class for {@link HarshadNumber}. + * Tests various scenarios including positive cases, edge cases, and exception + * handling. + */ +class HarshadNumberTest { + + @Test + void testValidHarshadNumbers() { + // Single digit Harshad numbers (all single digits except 0 are Harshad numbers) + Assertions.assertTrue(HarshadNumber.isHarshad(1)); + Assertions.assertTrue(HarshadNumber.isHarshad(2)); + Assertions.assertTrue(HarshadNumber.isHarshad(3)); + Assertions.assertTrue(HarshadNumber.isHarshad(4)); + Assertions.assertTrue(HarshadNumber.isHarshad(5)); + Assertions.assertTrue(HarshadNumber.isHarshad(6)); + Assertions.assertTrue(HarshadNumber.isHarshad(7)); + Assertions.assertTrue(HarshadNumber.isHarshad(8)); + Assertions.assertTrue(HarshadNumber.isHarshad(9)); + + // Two digit Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad(10)); // 10 / (1 + 0) = 10 + Assertions.assertTrue(HarshadNumber.isHarshad(12)); // 12 / (1 + 2) = 4 + Assertions.assertTrue(HarshadNumber.isHarshad(18)); // 18 / (1 + 8) = 2 + Assertions.assertTrue(HarshadNumber.isHarshad(20)); // 20 / (2 + 0) = 10 + Assertions.assertTrue(HarshadNumber.isHarshad(21)); // 21 / (2 + 1) = 7 + + // Three digit Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad(100)); // 100 / (1 + 0 + 0) = 100 + Assertions.assertTrue(HarshadNumber.isHarshad(102)); // 102 / (1 + 0 + 2) = 34 + Assertions.assertTrue(HarshadNumber.isHarshad(108)); // 108 / (1 + 0 + 8) = 12 + + // Large Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad(1000)); // 1000 / (1 + 0 + 0 + 0) = 1000 + Assertions.assertTrue(HarshadNumber.isHarshad(1002)); // 1002 / (1 + 0 + 0 + 2) = 334 + Assertions.assertTrue(HarshadNumber.isHarshad(999999999)); // 999999999 / (9*9) = 12345679 + } @Test - public void harshadNumber() { + void testInvalidHarshadNumbers() { + // Numbers that are not Harshad numbers + Assertions.assertFalse(HarshadNumber.isHarshad(11)); // 11 / (1 + 1) = 5.5 + Assertions.assertFalse(HarshadNumber.isHarshad(13)); // 13 / (1 + 3) = 3.25 + Assertions.assertFalse(HarshadNumber.isHarshad(17)); // 17 / (1 + 7) = 2.125 + Assertions.assertFalse(HarshadNumber.isHarshad(19)); // 19 / (1 + 9) = 1.9 + Assertions.assertFalse(HarshadNumber.isHarshad(23)); // 23 / (2 + 3) = 4.6 + Assertions.assertFalse(HarshadNumber.isHarshad(101)); // 101 / (1 + 0 + 1) = 50.5 + } - assertTrue(HarshadNumber.isHarshad(18)); - assertFalse(HarshadNumber.isHarshad(-18)); - assertFalse(HarshadNumber.isHarshad(19)); - assertTrue(HarshadNumber.isHarshad(999999999)); - assertFalse(HarshadNumber.isHarshad(0)); + @Test + void testZeroThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(0)); + } - assertTrue(HarshadNumber.isHarshad("18")); - assertFalse(HarshadNumber.isHarshad("-18")); - assertFalse(HarshadNumber.isHarshad("19")); - assertTrue(HarshadNumber.isHarshad("999999999")); - assertTrue(HarshadNumber.isHarshad("99999999999100")); + @Test + void testNegativeNumbersThrowException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(-1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(-18)); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(-100)); + } + + @Test + void testValidHarshadNumbersWithString() { + // Single digit Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad("1")); + Assertions.assertTrue(HarshadNumber.isHarshad("2")); + Assertions.assertTrue(HarshadNumber.isHarshad("9")); + + // Two digit Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad("10")); + Assertions.assertTrue(HarshadNumber.isHarshad("12")); + Assertions.assertTrue(HarshadNumber.isHarshad("18")); + + // Large Harshad numbers + Assertions.assertTrue(HarshadNumber.isHarshad("1000")); + Assertions.assertTrue(HarshadNumber.isHarshad("999999999")); + Assertions.assertTrue(HarshadNumber.isHarshad("99999999999100")); + } + + @Test + void testInvalidHarshadNumbersWithString() { + // Numbers that are not Harshad numbers + Assertions.assertFalse(HarshadNumber.isHarshad("11")); + Assertions.assertFalse(HarshadNumber.isHarshad("13")); + Assertions.assertFalse(HarshadNumber.isHarshad("19")); + Assertions.assertFalse(HarshadNumber.isHarshad("23")); + } + + @Test + void testStringWithZeroThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("0")); + } + + @Test + void testStringWithNegativeNumbersThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("-1")); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("-18")); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("-100")); + } + + @Test + void testNullStringThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(null)); + } + + @Test + void testEmptyStringThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("")); + } + + @Test + void testInvalidStringThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("abc")); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("12.5")); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad("12a")); + Assertions.assertThrows(IllegalArgumentException.class, () -> HarshadNumber.isHarshad(" 12 ")); + } + + @Test + void testMaxLongValue() { + // Test with a large number close to Long.MAX_VALUE + long largeHarshadCandidate = 9223372036854775800L; + // This specific number may or may not be Harshad, just testing it doesn't crash + try { + HarshadNumber.isHarshad(largeHarshadCandidate); + } catch (Exception e) { + Assertions.fail("Should not throw exception for valid large numbers"); + } } } diff --git a/src/test/java/com/thealgorithms/maths/HeronsFormulaTest.java b/src/test/java/com/thealgorithms/maths/HeronsFormulaTest.java index 22cecf4dc960..5175c6348c9f 100644 --- a/src/test/java/com/thealgorithms/maths/HeronsFormulaTest.java +++ b/src/test/java/com/thealgorithms/maths/HeronsFormulaTest.java @@ -3,37 +3,141 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class HeronsFormulaTest { +/** + * Test cases for {@link HeronsFormula}. + */ +class HeronsFormulaTest { + + private static final double EPSILON = 1e-10; + + @Test + void testRightTriangleThreeFourFive() { + Assertions.assertEquals(6.0, HeronsFormula.herons(3, 4, 5), EPSILON); + } + + @Test + void testTriangleTwentyFourThirtyEighteen() { + Assertions.assertEquals(216.0, HeronsFormula.herons(24, 30, 18), EPSILON); + } + + @Test + void testEquilateralTriangle() { + Assertions.assertEquals(0.4330127018922193, HeronsFormula.herons(1, 1, 1), EPSILON); + } + + @Test + void testScaleneTriangleFourFiveEight() { + Assertions.assertEquals(8.181534085976786, HeronsFormula.herons(4, 5, 8), EPSILON); + } + + @Test + void testEquilateralTriangleLargeSides() { + final double side = 10.0; + final double expectedArea = Math.sqrt(3) / 4 * side * side; + Assertions.assertEquals(expectedArea, HeronsFormula.herons(side, side, side), EPSILON); + } @Test - void test1() { - Assertions.assertEquals(HeronsFormula.herons(3, 4, 5), 6.0); + void testIsoscelesTriangle() { + Assertions.assertEquals(12.0, HeronsFormula.herons(5, 5, 6), EPSILON); } @Test - void test2() { - Assertions.assertEquals(HeronsFormula.herons(24, 30, 18), 216.0); + void testSmallTriangle() { + Assertions.assertEquals(0.4330127018922193, HeronsFormula.herons(1.0, 1.0, 1.0), EPSILON); } @Test - void test3() { - Assertions.assertEquals(HeronsFormula.herons(1, 1, 1), 0.4330127018922193); + void testLargeTriangle() { + Assertions.assertEquals(600.0, HeronsFormula.herons(30, 40, 50), EPSILON); } @Test - void test4() { - Assertions.assertEquals(HeronsFormula.herons(4, 5, 8), 8.181534085976786); + void testDecimalSides() { + final double area = HeronsFormula.herons(2.5, 3.5, 4.0); + Assertions.assertTrue(area > 0); + Assertions.assertEquals(4.330127018922194, area, EPSILON); } @Test - public void testCalculateAreaWithInvalidInput() { + void testDegenerateTriangleEqualToSumOfOtherTwo() { Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 2, 3); }); + } + + @Test + void testDegenerateTriangleVariant2() { Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(2, 1, 3); }); + } + + @Test + void testDegenerateTriangleVariant3() { Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(3, 2, 1); }); + } + + @Test + void testDegenerateTriangleVariant4() { Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 3, 2); }); + } - Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 1, 0); }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 0, 1); }); + @Test + void testInvalidTriangleSideGreaterThanSum() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 1, 5); }); + } + + @Test + void testZeroFirstSide() { Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(0, 1, 1); }); } + + @Test + void testZeroSecondSide() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 0, 1); }); + } + + @Test + void testZeroThirdSide() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(1, 1, 0); }); + } + + @Test + void testNegativeFirstSide() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(-1, 2, 2); }); + } + + @Test + void testNegativeSecondSide() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(2, -1, 2); }); + } + + @Test + void testNegativeThirdSide() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(2, 2, -1); }); + } + + @Test + void testAllNegativeSides() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(-1, -2, -3); }); + } + + @Test + void testAllZeroSides() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { HeronsFormula.herons(0, 0, 0); }); + } + + @Test + void testVerySmallTriangle() { + final double result = HeronsFormula.herons(0.001, 0.001, 0.001); + Assertions.assertTrue(result > 0); + Assertions.assertTrue(result < 0.001); + } + + @Test + void testRightTriangleFiveTwelveThirteen() { + Assertions.assertEquals(30.0, HeronsFormula.herons(5, 12, 13), EPSILON); + } + + @Test + void testRightTriangleEightFifteenSeventeen() { + Assertions.assertEquals(60.0, HeronsFormula.herons(8, 15, 17), EPSILON); + } } diff --git a/src/test/java/com/thealgorithms/maths/JugglerSequenceTest.java b/src/test/java/com/thealgorithms/maths/JugglerSequenceTest.java new file mode 100644 index 000000000000..9f3e80eee2b6 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/JugglerSequenceTest.java @@ -0,0 +1,42 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.junit.jupiter.api.Test; + +class JugglerSequenceTest { + + @Test + void testJugglerSequenceWithThree() { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + JugglerSequence.jugglerSequence(3); + assertEquals("3,5,11,36,6,2,1\n", outContent.toString()); + } + + @Test + void testJugglerSequenceWithTwo() { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + JugglerSequence.jugglerSequence(2); + assertEquals("2,1\n", outContent.toString()); + } + + @Test + void testJugglerSequenceWithNine() { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + JugglerSequence.jugglerSequence(9); + assertEquals("9,27,140,11,36,6,2,1\n", outContent.toString()); + } + + @Test + void testJugglerSequenceWithOne() { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + JugglerSequence.jugglerSequence(1); + assertEquals("1\n", outContent.toString()); + } +} diff --git a/src/test/java/com/thealgorithms/maths/KaprekarNumbersTest.java b/src/test/java/com/thealgorithms/maths/KaprekarNumbersTest.java index 05e58cf88e22..a3cd7500b30c 100644 --- a/src/test/java/com/thealgorithms/maths/KaprekarNumbersTest.java +++ b/src/test/java/com/thealgorithms/maths/KaprekarNumbersTest.java @@ -1,85 +1,188 @@ 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; -public class KaprekarNumbersTest { +/** + * Test class for {@link KaprekarNumbers}. + * Tests various Kaprekar numbers and edge cases to ensure full coverage. + */ +class KaprekarNumbersTest { @Test - void testFor1() { + void testZeroIsKaprekarNumber() { + assertTrue(KaprekarNumbers.isKaprekarNumber(0)); + } + + @Test + void testOneIsKaprekarNumber() { assertTrue(KaprekarNumbers.isKaprekarNumber(1)); } @Test - void testFor45() { + void testNineIsKaprekarNumber() { + // 9^2 = 81, 8 + 1 = 9 + assertTrue(KaprekarNumbers.isKaprekarNumber(9)); + } + + @Test + void testFortyFiveIsKaprekarNumber() { + // 45^2 = 2025, 20 + 25 = 45 assertTrue(KaprekarNumbers.isKaprekarNumber(45)); } @Test - void testFor297() { + void testFiftyFiveIsKaprekarNumber() { + // 55^2 = 3025, 30 + 25 = 55 + assertTrue(KaprekarNumbers.isKaprekarNumber(55)); + } + + @Test + void testNinetyNineIsKaprekarNumber() { + // 99^2 = 9801, 98 + 01 = 99 + assertTrue(KaprekarNumbers.isKaprekarNumber(99)); + } + + @Test + void testTwoNinetySevenIsKaprekarNumber() { + // 297^2 = 88209, 88 + 209 = 297 assertTrue(KaprekarNumbers.isKaprekarNumber(297)); } @Test - void testFor2223() { + void testSevenZeroThreeIsKaprekarNumber() { + // 703^2 = 494209, 494 + 209 = 703 + assertTrue(KaprekarNumbers.isKaprekarNumber(703)); + } + + @Test + void testNineNineNineIsKaprekarNumber() { + // 999^2 = 998001, 998 + 001 = 999 + assertTrue(KaprekarNumbers.isKaprekarNumber(999)); + } + + @Test + void testTwoTwoTwoThreeIsKaprekarNumber() { + // 2223^2 = 4941729, 4941 + 729 = 5670 (not directly obvious) + // Actually: 494 + 1729 = 2223 assertTrue(KaprekarNumbers.isKaprekarNumber(2223)); } @Test - void testFor857143() { + void testEightFiveSevenOneFortyThreeIsKaprekarNumber() { assertTrue(KaprekarNumbers.isKaprekarNumber(857143)); } @Test - void testFor3() { + void testTwoIsNotKaprekarNumber() { + assertFalse(KaprekarNumbers.isKaprekarNumber(2)); + } + + @Test + void testThreeIsNotKaprekarNumber() { assertFalse(KaprekarNumbers.isKaprekarNumber(3)); } @Test - void testFor26() { + void testTenIsNotKaprekarNumber() { + assertFalse(KaprekarNumbers.isKaprekarNumber(10)); + } + + @Test + void testTwentySixIsNotKaprekarNumber() { assertFalse(KaprekarNumbers.isKaprekarNumber(26)); } @Test - void testFor98() { + void testNinetyEightIsNotKaprekarNumber() { assertFalse(KaprekarNumbers.isKaprekarNumber(98)); } @Test - void testForRangeOfNumber() { - try { - List rangedNumbers = KaprekarNumbers.kaprekarNumberInRange(1, 100000); - long[] allTheNumbers = { - 1, - 9, - 45, - 55, - 99, - 297, - 703, - 999, - 2223, - 2728, - 4950, - 5050, - 7272, - 7777, - 9999, - 17344, - 22222, - 77778, - 82656, - 95121, - 99999, - }; - for (long i : allTheNumbers) { - assert rangedNumbers.contains(i); - } - } catch (Exception e) { - assert false; - } + void testOneHundredIsNotKaprekarNumber() { + assertFalse(KaprekarNumbers.isKaprekarNumber(100)); + } + + @Test + void testNegativeNumberThrowsException() { + assertThrows(IllegalArgumentException.class, () -> KaprekarNumbers.isKaprekarNumber(-5)); + } + + @Test + void testKaprekarNumbersInSmallRange() { + List result = KaprekarNumbers.kaprekarNumberInRange(1, 10); + List expected = Arrays.asList(1L, 9L); + assertEquals(expected, result); + } + + @Test + void testKaprekarNumbersInMediumRange() { + List result = KaprekarNumbers.kaprekarNumberInRange(1, 100); + List expected = Arrays.asList(1L, 9L, 45L, 55L, 99L); + assertEquals(expected, result); + } + + @Test + void testKaprekarNumbersInLargeRange() { + List rangedNumbers = KaprekarNumbers.kaprekarNumberInRange(1, 100000); + List expectedNumbers = Arrays.asList(1L, 9L, 45L, 55L, 99L, 297L, 703L, 999L, 2223L, 2728L, 4950L, 5050L, 7272L, 7777L, 9999L, 17344L, 22222L, 77778L, 82656L, 95121L, 99999L); + assertEquals(expectedNumbers, rangedNumbers); + } + + @Test + void testKaprekarNumbersInSingleElementRange() { + List result = KaprekarNumbers.kaprekarNumberInRange(9, 9); + List expected = Arrays.asList(9L); + assertEquals(expected, result); + } + + @Test + void testKaprekarNumbersInRangeWithNoKaprekarNumbers() { + List result = KaprekarNumbers.kaprekarNumberInRange(2, 8); + assertTrue(result.isEmpty()); + } + + @Test + void testKaprekarNumbersInRangeStartingFromZero() { + List result = KaprekarNumbers.kaprekarNumberInRange(0, 5); + List expected = Arrays.asList(0L, 1L); + assertEquals(expected, result); + } + + @Test + void testInvalidRangeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> KaprekarNumbers.kaprekarNumberInRange(100, 50)); + } + + @Test + void testNegativeStartThrowsException() { + assertThrows(IllegalArgumentException.class, () -> KaprekarNumbers.kaprekarNumberInRange(-10, 100)); + } + + @Test + void testEmptyRange() { + List result = KaprekarNumbers.kaprekarNumberInRange(10, 44); + assertTrue(result.isEmpty()); + } + + @Test + void testLargeKaprekarNumber() { + // Test a larger known Kaprekar number + assertTrue(KaprekarNumbers.isKaprekarNumber(142857)); + } + + @Test + void testFourDigitKaprekarNumbers() { + // Test some 4-digit Kaprekar numbers + assertTrue(KaprekarNumbers.isKaprekarNumber(2728)); + assertTrue(KaprekarNumbers.isKaprekarNumber(4950)); + assertTrue(KaprekarNumbers.isKaprekarNumber(5050)); + assertTrue(KaprekarNumbers.isKaprekarNumber(7272)); } } diff --git a/src/test/java/com/thealgorithms/maths/KeithNumberTest.java b/src/test/java/com/thealgorithms/maths/KeithNumberTest.java new file mode 100644 index 000000000000..cac6b925e5c1 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/KeithNumberTest.java @@ -0,0 +1,153 @@ +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; + +/** + * Test cases for {@link KeithNumber}. + */ +class KeithNumberTest { + + /** + * Tests single-digit Keith numbers. + * All single-digit numbers (1-9) are Keith numbers by definition. + */ + @Test + void testSingleDigitKeithNumbers() { + assertTrue(KeithNumber.isKeith(1)); + assertTrue(KeithNumber.isKeith(2)); + assertTrue(KeithNumber.isKeith(3)); + assertTrue(KeithNumber.isKeith(4)); + assertTrue(KeithNumber.isKeith(5)); + assertTrue(KeithNumber.isKeith(6)); + assertTrue(KeithNumber.isKeith(7)); + assertTrue(KeithNumber.isKeith(8)); + assertTrue(KeithNumber.isKeith(9)); + } + + /** + * Tests two-digit Keith numbers. + * Known two-digit Keith numbers: 14, 19, 28, 47, 61, 75. + */ + @Test + void testTwoDigitKeithNumbers() { + assertTrue(KeithNumber.isKeith(14)); + assertTrue(KeithNumber.isKeith(19)); + assertTrue(KeithNumber.isKeith(28)); + assertTrue(KeithNumber.isKeith(47)); + assertTrue(KeithNumber.isKeith(61)); + assertTrue(KeithNumber.isKeith(75)); + } + + /** + * Tests three-digit Keith numbers. + * Known three-digit Keith numbers: 197, 742. + */ + @Test + void testThreeDigitKeithNumbers() { + assertTrue(KeithNumber.isKeith(197)); + assertTrue(KeithNumber.isKeith(742)); + } + + /** + * Tests four-digit Keith numbers. + * Known four-digit Keith numbers: 1104, 1537, 2208, 2580, 3684, 4788, 7385, + * 7647, 7909. + */ + @Test + void testFourDigitKeithNumbers() { + assertTrue(KeithNumber.isKeith(1104)); + assertTrue(KeithNumber.isKeith(1537)); + assertTrue(KeithNumber.isKeith(2208)); + } + + /** + * Tests two-digit non-Keith numbers. + */ + @Test + void testTwoDigitNonKeithNumbers() { + assertFalse(KeithNumber.isKeith(10)); + assertFalse(KeithNumber.isKeith(11)); + assertFalse(KeithNumber.isKeith(12)); + assertFalse(KeithNumber.isKeith(13)); + assertFalse(KeithNumber.isKeith(15)); + assertFalse(KeithNumber.isKeith(20)); + assertFalse(KeithNumber.isKeith(30)); + assertFalse(KeithNumber.isKeith(50)); + } + + /** + * Tests three-digit non-Keith numbers. + */ + @Test + void testThreeDigitNonKeithNumbers() { + assertFalse(KeithNumber.isKeith(100)); + assertFalse(KeithNumber.isKeith(123)); + assertFalse(KeithNumber.isKeith(196)); + assertFalse(KeithNumber.isKeith(198)); + assertFalse(KeithNumber.isKeith(456)); + assertFalse(KeithNumber.isKeith(741)); + assertFalse(KeithNumber.isKeith(743)); + assertFalse(KeithNumber.isKeith(999)); + } + + /** + * Tests validation for edge case 14 in detail. + * 14 is a Keith number: 1, 4, 5 (1+4), 9 (4+5), 14 (5+9). + */ + @Test + void testKeithNumber14() { + assertTrue(KeithNumber.isKeith(14)); + } + + /** + * Tests validation for edge case 197 in detail. + * 197 is a Keith number: 1, 9, 7, 17, 33, 57, 107, 197. + */ + @Test + void testKeithNumber197() { + assertTrue(KeithNumber.isKeith(197)); + } + + /** + * Tests that zero throws an IllegalArgumentException. + */ + @Test + void testZeroThrowsException() { + assertThrows(IllegalArgumentException.class, () -> KeithNumber.isKeith(0)); + } + + /** + * Tests that negative numbers throw an IllegalArgumentException. + */ + @Test + void testNegativeNumbersThrowException() { + assertThrows(IllegalArgumentException.class, () -> KeithNumber.isKeith(-1)); + assertThrows(IllegalArgumentException.class, () -> KeithNumber.isKeith(-14)); + assertThrows(IllegalArgumentException.class, () -> KeithNumber.isKeith(-100)); + } + + /** + * Tests various edge cases with larger numbers. + */ + @Test + void testLargerNumbers() { + assertTrue(KeithNumber.isKeith(2208)); + assertFalse(KeithNumber.isKeith(2207)); + assertFalse(KeithNumber.isKeith(2209)); + } + + /** + * Tests the expected behavior with all two-digit Keith numbers. + */ + @Test + void testAllKnownTwoDigitKeithNumbers() { + int[] knownKeithNumbers = {14, 19, 28, 47, 61, 75}; + for (int number : knownKeithNumbers) { + assertTrue(KeithNumber.isKeith(number), "Expected " + number + " to be a Keith number"); + } + } +} diff --git a/src/test/java/com/thealgorithms/maths/KrishnamurthyNumberTest.java b/src/test/java/com/thealgorithms/maths/KrishnamurthyNumberTest.java index 595acde2b5d8..3c9d4f886b3d 100644 --- a/src/test/java/com/thealgorithms/maths/KrishnamurthyNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/KrishnamurthyNumberTest.java @@ -6,57 +6,115 @@ import org.junit.jupiter.api.Test; /** - * Unit tests for the KrishnamurthyNumber class. + * Unit tests for the {@link KrishnamurthyNumber} class. */ -public class KrishnamurthyNumberTest { +class KrishnamurthyNumberTest { /** - * Test the isKrishnamurthy method with a known Krishnamurthy number. + * Test with known Krishnamurthy number 145. + * 1! + 4! + 5! = 1 + 24 + 120 = 145 */ @Test - public void testIsKrishnamurthyTrue() { + void testIsKrishnamurthyWith145() { assertTrue(KrishnamurthyNumber.isKrishnamurthy(145)); } /** - * Test the isKrishnamurthy method with a number that is not a Krishnamurthy number. + * Test with a number that is not a Krishnamurthy number. */ @Test - public void testIsKrishnamurthyFalse() { + void testIsKrishnamurthyWithNonKrishnamurthyNumber() { assertFalse(KrishnamurthyNumber.isKrishnamurthy(123)); } /** - * Test the isKrishnamurthy method with zero. + * Test with zero, which is not a Krishnamurthy number. */ @Test - public void testIsKrishnamurthyZero() { + void testIsKrishnamurthyWithZero() { assertFalse(KrishnamurthyNumber.isKrishnamurthy(0)); } /** - * Test the isKrishnamurthy method with a negative number. + * Test with negative numbers, which cannot be Krishnamurthy numbers. */ @Test - public void testIsKrishnamurthyNegative() { + void testIsKrishnamurthyWithNegativeNumbers() { + assertFalse(KrishnamurthyNumber.isKrishnamurthy(-1)); assertFalse(KrishnamurthyNumber.isKrishnamurthy(-145)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(-100)); } /** - * Test the isKrishnamurthy method with a single-digit Krishnamurthy number. + * Test with single-digit Krishnamurthy numbers. + * 1! = 1 and 2! = 2, so both 1 and 2 are Krishnamurthy numbers. */ @Test - public void testIsKrishnamurthySingleDigitTrue() { + void testIsKrishnamurthyWithSingleDigitKrishnamurthyNumbers() { assertTrue(KrishnamurthyNumber.isKrishnamurthy(1)); assertTrue(KrishnamurthyNumber.isKrishnamurthy(2)); } /** - * Test the isKrishnamurthy method with a single-digit number that is not a Krishnamurthy number. + * Test with single-digit numbers that are not Krishnamurthy numbers. */ @Test - public void testIsKrishnamurthySingleDigitFalse() { + void testIsKrishnamurthyWithSingleDigitNonKrishnamurthyNumbers() { assertFalse(KrishnamurthyNumber.isKrishnamurthy(3)); assertFalse(KrishnamurthyNumber.isKrishnamurthy(4)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(5)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(6)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(7)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(8)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(9)); + } + + /** + * Test with the largest Krishnamurthy number: 40585. + * 4! + 0! + 5! + 8! + 5! = 24 + 1 + 120 + 40320 + 120 = 40585 + */ + @Test + void testIsKrishnamurthyWithLargestKrishnamurthyNumber() { + assertTrue(KrishnamurthyNumber.isKrishnamurthy(40585)); + } + + /** + * Test with various non-Krishnamurthy numbers. + */ + @Test + void testIsKrishnamurthyWithVariousNonKrishnamurthyNumbers() { + assertFalse(KrishnamurthyNumber.isKrishnamurthy(10)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(50)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(100)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(144)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(146)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(150)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(200)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(999)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(1000)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(40584)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(40586)); + } + + /** + * Test with numbers close to known Krishnamurthy numbers. + */ + @Test + void testIsKrishnamurthyWithNumbersCloseToKrishnamurthy() { + assertFalse(KrishnamurthyNumber.isKrishnamurthy(144)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(146)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(40584)); + assertFalse(KrishnamurthyNumber.isKrishnamurthy(40586)); + } + + /** + * Test with all known Krishnamurthy numbers in base 10. + */ + @Test + void testAllKnownKrishnamurthyNumbers() { + assertTrue(KrishnamurthyNumber.isKrishnamurthy(1)); + assertTrue(KrishnamurthyNumber.isKrishnamurthy(2)); + assertTrue(KrishnamurthyNumber.isKrishnamurthy(145)); + assertTrue(KrishnamurthyNumber.isKrishnamurthy(40585)); } } diff --git a/src/test/java/com/thealgorithms/maths/LeonardoNumberTest.java b/src/test/java/com/thealgorithms/maths/LeonardoNumberTest.java index 705f1a1006fa..baf4540cf239 100644 --- a/src/test/java/com/thealgorithms/maths/LeonardoNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/LeonardoNumberTest.java @@ -1,29 +1,171 @@ 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.Assertions; import org.junit.jupiter.api.Test; -public class LeonardoNumberTest { +/** + * Test cases for {@link LeonardoNumber} class. + *

+ * Tests both recursive and iterative implementations with various input values + * including edge cases and boundary conditions. + */ +class LeonardoNumberTest { + + // Tests for recursive implementation + + @Test + void testLeonardoNumberNegative() { + Assertions.assertThrows(IllegalArgumentException.class, () -> LeonardoNumber.leonardoNumber(-1)); + } + + @Test + void testLeonardoNumberNegativeLarge() { + Assertions.assertThrows(IllegalArgumentException.class, () -> LeonardoNumber.leonardoNumber(-100)); + } + + @Test + void testLeonardoNumberZero() { + Assertions.assertEquals(1, LeonardoNumber.leonardoNumber(0)); + } + + @Test + void testLeonardoNumberOne() { + Assertions.assertEquals(1, LeonardoNumber.leonardoNumber(1)); + } + + @Test + void testLeonardoNumberTwo() { + Assertions.assertEquals(3, LeonardoNumber.leonardoNumber(2)); + } + + @Test + void testLeonardoNumberThree() { + Assertions.assertEquals(5, LeonardoNumber.leonardoNumber(3)); + } + + @Test + void testLeonardoNumberFour() { + Assertions.assertEquals(9, LeonardoNumber.leonardoNumber(4)); + } + + @Test + void testLeonardoNumberFive() { + Assertions.assertEquals(15, LeonardoNumber.leonardoNumber(5)); + } + + @Test + void testLeonardoNumberSix() { + Assertions.assertEquals(25, LeonardoNumber.leonardoNumber(6)); + } + + @Test + void testLeonardoNumberSeven() { + Assertions.assertEquals(41, LeonardoNumber.leonardoNumber(7)); + } + + @Test + void testLeonardoNumberEight() { + Assertions.assertEquals(67, LeonardoNumber.leonardoNumber(8)); + } + + @Test + void testLeonardoNumberTen() { + Assertions.assertEquals(177, LeonardoNumber.leonardoNumber(10)); + } + + @Test + void testLeonardoNumberFifteen() { + Assertions.assertEquals(1973, LeonardoNumber.leonardoNumber(15)); + } + @Test - void leonardoNumberNegative() { - assertThrows(ArithmeticException.class, () -> LeonardoNumber.leonardoNumber(-1)); + void testLeonardoNumberTwenty() { + Assertions.assertEquals(21891, LeonardoNumber.leonardoNumber(20)); } + + // Tests for iterative implementation + + @Test + void testLeonardoNumberIterativeNegative() { + Assertions.assertThrows(IllegalArgumentException.class, () -> LeonardoNumber.leonardoNumberIterative(-1)); + } + + @Test + void testLeonardoNumberIterativeNegativeLarge() { + Assertions.assertThrows(IllegalArgumentException.class, () -> LeonardoNumber.leonardoNumberIterative(-50)); + } + + @Test + void testLeonardoNumberIterativeZero() { + Assertions.assertEquals(1, LeonardoNumber.leonardoNumberIterative(0)); + } + + @Test + void testLeonardoNumberIterativeOne() { + Assertions.assertEquals(1, LeonardoNumber.leonardoNumberIterative(1)); + } + + @Test + void testLeonardoNumberIterativeTwo() { + Assertions.assertEquals(3, LeonardoNumber.leonardoNumberIterative(2)); + } + + @Test + void testLeonardoNumberIterativeThree() { + Assertions.assertEquals(5, LeonardoNumber.leonardoNumberIterative(3)); + } + + @Test + void testLeonardoNumberIterativeFour() { + Assertions.assertEquals(9, LeonardoNumber.leonardoNumberIterative(4)); + } + @Test - void leonardoNumberZero() { - assertEquals(1, LeonardoNumber.leonardoNumber(0)); + void testLeonardoNumberIterativeFive() { + Assertions.assertEquals(15, LeonardoNumber.leonardoNumberIterative(5)); } + @Test - void leonardoNumberOne() { - assertEquals(1, LeonardoNumber.leonardoNumber(1)); + void testLeonardoNumberIterativeSix() { + Assertions.assertEquals(25, LeonardoNumber.leonardoNumberIterative(6)); } + @Test - void leonardoNumberFive() { - assertEquals(15, LeonardoNumber.leonardoNumber(5)); + void testLeonardoNumberIterativeSeven() { + Assertions.assertEquals(41, LeonardoNumber.leonardoNumberIterative(7)); } + + @Test + void testLeonardoNumberIterativeEight() { + Assertions.assertEquals(67, LeonardoNumber.leonardoNumberIterative(8)); + } + + @Test + void testLeonardoNumberIterativeTen() { + Assertions.assertEquals(177, LeonardoNumber.leonardoNumberIterative(10)); + } + + @Test + void testLeonardoNumberIterativeFifteen() { + Assertions.assertEquals(1973, LeonardoNumber.leonardoNumberIterative(15)); + } + + @Test + void testLeonardoNumberIterativeTwenty() { + Assertions.assertEquals(21891, LeonardoNumber.leonardoNumberIterative(20)); + } + + @Test + void testLeonardoNumberIterativeTwentyFive() { + Assertions.assertEquals(242785, LeonardoNumber.leonardoNumberIterative(25)); + } + + // Consistency tests between recursive and iterative implementations + @Test - void leonardoNumberTwenty() { - assertEquals(21891, LeonardoNumber.leonardoNumber(20)); + void testConsistencyBetweenImplementations() { + for (int i = 0; i <= 15; i++) { + Assertions.assertEquals(LeonardoNumber.leonardoNumber(i), LeonardoNumber.leonardoNumberIterative(i), "Mismatch at index " + i); + } } } diff --git a/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java new file mode 100644 index 000000000000..c4205985dbfd --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java @@ -0,0 +1,330 @@ +package com.thealgorithms.maths; + +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.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Test class for LinearDiophantineEquationsSolver. + * Tests various cases including: + * - Equations with solutions + * - Equations with no solutions + * - Special cases (zero coefficients, infinite solutions) + * - Edge cases (negative coefficients, large numbers) + */ +class LinearDiophantineEquationsSolverTest { + + /** + * Tests the example equation 3x + 4y = 7. + * Expected solution: x = -9, y = 8 (or other valid solutions). + */ + @Test + void testBasicEquation() { + final var equation = new LinearDiophantineEquationsSolver.Equation(3, 4, 7); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + // Verify that the solution satisfies the equation + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with no solution: 2x + 4y = 5. + * Since gcd(2, 4) = 2 and 2 does not divide 5, no solution exists. + */ + @Test + void testNoSolution() { + final var equation = new LinearDiophantineEquationsSolver.Equation(2, 4, 5); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertEquals(LinearDiophantineEquationsSolver.Solution.NO_SOLUTION, solution); + } + + /** + * Tests the trivial equation 0x + 0y = 0. + * This has infinite solutions. + */ + @Test + void testInfiniteSolutions() { + final var equation = new LinearDiophantineEquationsSolver.Equation(0, 0, 0); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertEquals(LinearDiophantineEquationsSolver.Solution.INFINITE_SOLUTIONS, solution); + } + + /** + * Tests an equation where a = 0: 0x + 5y = 10. + * Expected solution: x = 0, y = 2. + */ + @Test + void testZeroCoefficient() { + final var equation = new LinearDiophantineEquationsSolver.Equation(0, 5, 10); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation where b = 0: 3x + 0y = 9. + * Expected solution: x = 3, y can be anything (solver will return y = 0). + */ + @Test + void testZeroCoefficientB() { + final var equation = new LinearDiophantineEquationsSolver.Equation(3, 0, 9); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with negative coefficients: -3x + 4y = 7. + */ + @Test + void testNegativeCoefficients() { + final var equation = new LinearDiophantineEquationsSolver.Equation(-3, 4, 7); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with negative result: 3x + 4y = -7. + */ + @Test + void testNegativeResult() { + final var equation = new LinearDiophantineEquationsSolver.Equation(3, 4, -7); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with coprime coefficients: 7x + 11y = 1. + * Since gcd(7, 11) = 1, a solution exists. + */ + @Test + void testCoprimeCoefficients() { + final var equation = new LinearDiophantineEquationsSolver.Equation(7, 11, 1); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with larger coefficients: 12x + 18y = 30. + * Since gcd(12, 18) = 6 and 6 divides 30, a solution exists. + */ + @Test + void testLargerCoefficients() { + final var equation = new LinearDiophantineEquationsSolver.Equation(12, 18, 30); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation that has no solution due to GCD: 6x + 9y = 5. + * Since gcd(6, 9) = 3 and 3 does not divide 5, no solution exists. + */ + @Test + void testNoSolutionGcdCheck() { + final var equation = new LinearDiophantineEquationsSolver.Equation(6, 9, 5); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertEquals(LinearDiophantineEquationsSolver.Solution.NO_SOLUTION, solution); + } + + /** + * Tests the equation x + y = 1. + * Simple case where gcd(1, 1) = 1. + */ + @Test + void testSimpleCase() { + final var equation = new LinearDiophantineEquationsSolver.Equation(1, 1, 1); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests Solution equality. + */ + @Test + void testSolutionEquality() { + final var solution1 = new LinearDiophantineEquationsSolver.Solution(3, 5); + final var solution2 = new LinearDiophantineEquationsSolver.Solution(3, 5); + final var solution3 = new LinearDiophantineEquationsSolver.Solution(3, 6); + + assertEquals(solution1, solution2); + assertNotEquals(solution3, solution1); + assertEquals(solution1, solution1); + assertNotEquals(null, solution1); + assertNotEquals("string", solution1); + } + + /** + * Tests Solution hashCode. + */ + @Test + void testSolutionHashCode() { + final var solution1 = new LinearDiophantineEquationsSolver.Solution(3, 5); + final var solution2 = new LinearDiophantineEquationsSolver.Solution(3, 5); + + assertEquals(solution1.hashCode(), solution2.hashCode()); + } + + /** + * Tests Solution toString. + */ + @Test + void testSolutionToString() { + final var solution = new LinearDiophantineEquationsSolver.Solution(3, 5); + final var str = solution.toString(); + + assertTrue(str.contains("3")); + assertTrue(str.contains("5")); + assertTrue(str.contains("Solution")); + } + + /** + * Tests GcdSolutionWrapper equality. + */ + @Test + void testGcdSolutionWrapperEquality() { + final var solution = new LinearDiophantineEquationsSolver.Solution(1, 2); + final var wrapper1 = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + final var wrapper2 = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + final var wrapper3 = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(6, solution); + + assertEquals(wrapper1, wrapper2); + assertNotEquals(wrapper3, wrapper1); + assertEquals(wrapper1, wrapper1); + assertNotEquals(null, wrapper1); + assertNotEquals("string", wrapper1); + } + + /** + * Tests GcdSolutionWrapper hashCode. + */ + @Test + void testGcdSolutionWrapperHashCode() { + final var solution = new LinearDiophantineEquationsSolver.Solution(1, 2); + final var wrapper1 = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + final var wrapper2 = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + + assertEquals(wrapper1.hashCode(), wrapper2.hashCode()); + } + + /** + * Tests GcdSolutionWrapper toString. + */ + @Test + void testGcdSolutionWrapperToString() { + final var solution = new LinearDiophantineEquationsSolver.Solution(1, 2); + final var wrapper = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + final var str = wrapper.toString(); + + assertTrue(str.contains("5")); + assertTrue(str.contains("GcdSolutionWrapper")); + } + + /** + * Tests Equation record functionality. + */ + @Test + void testEquationRecord() { + final var equation = new LinearDiophantineEquationsSolver.Equation(3, 4, 7); + + assertEquals(3, equation.a()); + assertEquals(4, equation.b()); + assertEquals(7, equation.c()); + } + + /** + * Tests an equation with c = 0: 3x + 4y = 0. + * Expected solution: x = 0, y = 0. + */ + @Test + void testZeroResult() { + final var equation = new LinearDiophantineEquationsSolver.Equation(3, 4, 0); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests Solution setters. + */ + @Test + void testSolutionSetters() { + final var solution = new LinearDiophantineEquationsSolver.Solution(1, 2); + + solution.setX(10); + solution.setY(20); + + assertEquals(10, solution.getX()); + assertEquals(20, solution.getY()); + } + + /** + * Tests GcdSolutionWrapper setters. + */ + @Test + void testGcdSolutionWrapperSetters() { + final var solution = new LinearDiophantineEquationsSolver.Solution(1, 2); + final var wrapper = new LinearDiophantineEquationsSolver.GcdSolutionWrapper(5, solution); + + final var newSolution = new LinearDiophantineEquationsSolver.Solution(3, 4); + wrapper.setGcd(10); + wrapper.setSolution(newSolution); + + assertEquals(10, wrapper.getGcd()); + assertEquals(newSolution, wrapper.getSolution()); + } + + /** + * Tests an equation with both coefficients negative: -3x - 4y = -7. + */ + @Test + void testBothCoefficientsNegative() { + final var equation = new LinearDiophantineEquationsSolver.Equation(-3, -4, -7); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } + + /** + * Tests an equation with large prime coefficients: 97x + 101y = 198. + */ + @Test + void testLargePrimeCoefficients() { + final var equation = new LinearDiophantineEquationsSolver.Equation(97, 101, 198); + final var solution = LinearDiophantineEquationsSolver.findAnySolution(equation); + + assertNotNull(solution); + int result = equation.a() * solution.getX() + equation.b() * solution.getY(); + assertEquals(equation.c(), result); + } +} diff --git a/src/test/java/com/thealgorithms/maths/LucasSeriesTest.java b/src/test/java/com/thealgorithms/maths/LucasSeriesTest.java index 3576268c5d0c..4b6c1e41ecb6 100644 --- a/src/test/java/com/thealgorithms/maths/LucasSeriesTest.java +++ b/src/test/java/com/thealgorithms/maths/LucasSeriesTest.java @@ -1,28 +1,158 @@ 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 LucasSeriesTest { +/** + * Test cases for {@link LucasSeries} class. + * Tests both recursive and iterative implementations for correctness, + * edge cases, and error handling. + */ +class LucasSeriesTest { + + /** + * Test the first Lucas number L(1) = 2 + */ @Test - void lucasSeriesTwo() { + void testFirstLucasNumber() { assertEquals(2, LucasSeries.lucasSeries(1)); assertEquals(2, LucasSeries.lucasSeriesIteration(1)); } + + /** + * Test the second Lucas number L(2) = 1 + */ @Test - void lucasSeriesOne() { + void testSecondLucasNumber() { assertEquals(1, LucasSeries.lucasSeries(2)); assertEquals(1, LucasSeries.lucasSeriesIteration(2)); } + + /** + * Test the third Lucas number L(3) = 3 + */ + @Test + void testThirdLucasNumber() { + assertEquals(3, LucasSeries.lucasSeries(3)); + assertEquals(3, LucasSeries.lucasSeriesIteration(3)); + } + + /** + * Test the fourth Lucas number L(4) = 4 + */ @Test - void lucasSeriesSeven() { + void testFourthLucasNumber() { + assertEquals(4, LucasSeries.lucasSeries(4)); + assertEquals(4, LucasSeries.lucasSeriesIteration(4)); + } + + /** + * Test the fifth Lucas number L(5) = 7 + */ + @Test + void testFifthLucasNumber() { assertEquals(7, LucasSeries.lucasSeries(5)); assertEquals(7, LucasSeries.lucasSeriesIteration(5)); } + + /** + * Test the sixth Lucas number L(6) = 11 + */ + @Test + void testSixthLucasNumber() { + assertEquals(11, LucasSeries.lucasSeries(6)); + assertEquals(11, LucasSeries.lucasSeriesIteration(6)); + } + + /** + * Test the seventh Lucas number L(7) = 18 + */ + @Test + void testSeventhLucasNumber() { + assertEquals(18, LucasSeries.lucasSeries(7)); + assertEquals(18, LucasSeries.lucasSeriesIteration(7)); + } + + /** + * Test the eighth Lucas number L(8) = 29 + */ + @Test + void testEighthLucasNumber() { + assertEquals(29, LucasSeries.lucasSeries(8)); + assertEquals(29, LucasSeries.lucasSeriesIteration(8)); + } + + /** + * Test the ninth Lucas number L(9) = 47 + */ @Test - void lucasSeriesEleven() { + void testNinthLucasNumber() { + assertEquals(47, LucasSeries.lucasSeries(9)); + assertEquals(47, LucasSeries.lucasSeriesIteration(9)); + } + + /** + * Test the tenth Lucas number L(10) = 76 + */ + @Test + void testTenthLucasNumber() { + assertEquals(76, LucasSeries.lucasSeries(10)); + assertEquals(76, LucasSeries.lucasSeriesIteration(10)); + } + + /** + * Test the eleventh Lucas number L(11) = 123 + */ + @Test + void testEleventhLucasNumber() { assertEquals(123, LucasSeries.lucasSeries(11)); assertEquals(123, LucasSeries.lucasSeriesIteration(11)); } + + /** + * Test larger Lucas numbers to ensure correctness + */ + @Test + void testLargerLucasNumbers() { + assertEquals(199, LucasSeries.lucasSeries(12)); + assertEquals(199, LucasSeries.lucasSeriesIteration(12)); + assertEquals(322, LucasSeries.lucasSeries(13)); + assertEquals(322, LucasSeries.lucasSeriesIteration(13)); + assertEquals(521, LucasSeries.lucasSeries(14)); + assertEquals(521, LucasSeries.lucasSeriesIteration(14)); + assertEquals(843, LucasSeries.lucasSeries(15)); + assertEquals(843, LucasSeries.lucasSeriesIteration(15)); + } + + /** + * Test that both methods produce the same results + */ + @Test + void testRecursiveAndIterativeConsistency() { + for (int i = 1; i <= 15; i++) { + assertEquals(LucasSeries.lucasSeries(i), LucasSeries.lucasSeriesIteration(i), "Mismatch at position " + i); + } + } + + /** + * Test invalid input: zero + */ + @Test + void testZeroInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeries(0)); + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeriesIteration(0)); + } + + /** + * Test invalid input: negative number + */ + @Test + void testNegativeInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeries(-1)); + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeriesIteration(-1)); + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeries(-5)); + assertThrows(IllegalArgumentException.class, () -> LucasSeries.lucasSeriesIteration(-5)); + } } 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 4b3a5df44b34..deee0a931910 100644 --- a/src/test/java/com/thealgorithms/maths/MeansTest.java +++ b/src/test/java/com/thealgorithms/maths/MeansTest.java @@ -2,70 +2,217 @@ 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.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.TreeSet; import java.util.Vector; -import org.assertj.core.util.Lists; -import org.assertj.core.util.Sets; import org.junit.jupiter.api.Test; +/** + * Test class for {@link Means}. + *

+ * This class provides comprehensive test coverage for all mean calculation + * methods, + * including edge cases, various collection types, and error conditions. + *

+ */ class MeansTest { + private static final double EPSILON = 1e-9; + + // ========== Arithmetic Mean Tests ========== + @Test - void arithmeticMeanZeroNumbers() throws IllegalArgumentException { + void testArithmeticMeanThrowsExceptionForEmptyList() { List numbers = new ArrayList<>(); - assertThrows(IllegalArgumentException.class, () -> Means.arithmetic(numbers)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.arithmetic(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); + } + + @Test + void testArithmeticMeanSingleNumber() { + List numbers = Arrays.asList(2.5); + assertEquals(2.5, Means.arithmetic(numbers), EPSILON); + } + + @Test + void testArithmeticMeanTwoNumbers() { + List numbers = Arrays.asList(2.0, 4.0); + assertEquals(3.0, Means.arithmetic(numbers), EPSILON); } @Test - void geometricMeanZeroNumbers() throws IllegalArgumentException { + void testArithmeticMeanMultipleNumbers() { + List numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0); + assertEquals(3.0, Means.arithmetic(numbers), EPSILON); + } + + @Test + void testArithmeticMeanWithTreeSet() { + Set numbers = new TreeSet<>(Arrays.asList(1.0, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5)); + assertEquals(44.0, Means.arithmetic(numbers), EPSILON); + } + + @Test + void testArithmeticMeanWithNegativeNumbers() { + List numbers = Arrays.asList(-5.0, -3.0, -1.0, 1.0, 3.0, 5.0); + assertEquals(0.0, Means.arithmetic(numbers), EPSILON); + } + + @Test + void testArithmeticMeanWithDecimalNumbers() { + List numbers = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5); + assertEquals(3.3, Means.arithmetic(numbers), EPSILON); + } + + @Test + void testArithmeticMeanWithVector() { + Vector numbers = new Vector<>(Arrays.asList(10.0, 20.0, 30.0)); + assertEquals(20.0, Means.arithmetic(numbers), EPSILON); + } + + // ========== Geometric Mean Tests ========== + + @Test + void testGeometricMeanThrowsExceptionForEmptyList() { List numbers = new ArrayList<>(); - assertThrows(IllegalArgumentException.class, () -> Means.geometric(numbers)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.geometric(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); } @Test - void harmonicMeanZeroNumbers() throws IllegalArgumentException { + void testGeometricMeanSingleNumber() { + Set numbers = new LinkedHashSet<>(Arrays.asList(2.5)); + assertEquals(2.5, Means.geometric(numbers), EPSILON); + } + + @Test + void testGeometricMeanTwoNumbers() { + List numbers = Arrays.asList(2.0, 8.0); + assertEquals(4.0, Means.geometric(numbers), EPSILON); + } + + @Test + void testGeometricMeanMultipleNumbers() { + LinkedList numbers = new LinkedList<>(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.25)); + assertEquals(2.6426195539300585, Means.geometric(numbers), EPSILON); + } + + @Test + void testGeometricMeanPerfectSquares() { + List numbers = Arrays.asList(1.0, 4.0, 9.0, 16.0); + double expected = Math.pow(1.0 * 4.0 * 9.0 * 16.0, 1.0 / 4.0); + assertEquals(expected, Means.geometric(numbers), EPSILON); + } + + @Test + void testGeometricMeanIdenticalNumbers() { + List numbers = Arrays.asList(5.0, 5.0, 5.0, 5.0); + assertEquals(5.0, Means.geometric(numbers), EPSILON); + } + + @Test + void testGeometricMeanWithLinkedHashSet() { + LinkedHashSet numbers = new LinkedHashSet<>(Arrays.asList(2.0, 4.0, 8.0)); + double expected = Math.pow(2.0 * 4.0 * 8.0, 1.0 / 3.0); + assertEquals(expected, Means.geometric(numbers), EPSILON); + } + + // ========== Harmonic Mean Tests ========== + + @Test + void testHarmonicMeanThrowsExceptionForEmptyList() { List numbers = new ArrayList<>(); - assertThrows(IllegalArgumentException.class, () -> Means.harmonic(numbers)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.harmonic(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); + } + + @Test + void testHarmonicMeanSingleNumber() { + LinkedHashSet numbers = new LinkedHashSet<>(Arrays.asList(2.5)); + assertEquals(2.5, Means.harmonic(numbers), EPSILON); } @Test - void arithmeticMeanSingleNumber() { - List numbers = Lists.newArrayList(2.5); - assertEquals(2.5, Means.arithmetic(numbers)); + void testHarmonicMeanTwoNumbers() { + List numbers = Arrays.asList(2.0, 4.0); + double expected = 2.0 / (1.0 / 2.0 + 1.0 / 4.0); + assertEquals(expected, Means.harmonic(numbers), EPSILON); } @Test - void geometricMeanSingleNumber() { - Set numbers = Sets.newHashSet(Lists.newArrayList(2.5)); - assertEquals(2.5, Means.geometric(numbers)); + void testHarmonicMeanMultipleNumbers() { + Vector numbers = new Vector<>(Arrays.asList(1.0, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5)); + assertEquals(4.6697322801074135, Means.harmonic(numbers), EPSILON); } @Test - void harmonicMeanSingleNumber() { - LinkedHashSet numbers = Sets.newLinkedHashSet(2.5); - assertEquals(2.5, Means.harmonic(numbers)); + void testHarmonicMeanThreeNumbers() { + List numbers = Arrays.asList(1.0, 2.0, 4.0); + double expected = 3.0 / (1.0 / 1.0 + 1.0 / 2.0 + 1.0 / 4.0); + assertEquals(expected, Means.harmonic(numbers), EPSILON); } @Test - void arithmeticMeanMultipleNumbers() { - Set numbers = Sets.newTreeSet(1d, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5); - assertEquals(44, Means.arithmetic(numbers)); + void testHarmonicMeanIdenticalNumbers() { + List numbers = Arrays.asList(6.0, 6.0, 6.0); + assertEquals(6.0, Means.harmonic(numbers), EPSILON); } @Test - void geometricMeanMultipleNumbers() { - LinkedList numbers = new LinkedList<>(Lists.newArrayList(1d, 2d, 3d, 4d, 5d, 6d, 1.25)); - assertEquals(2.6426195539300585, Means.geometric(numbers)); + void testHarmonicMeanWithLinkedList() { + LinkedList numbers = new LinkedList<>(Arrays.asList(3.0, 6.0, 9.0)); + double expected = 3.0 / (1.0 / 3.0 + 1.0 / 6.0 + 1.0 / 9.0); + assertEquals(expected, Means.harmonic(numbers), EPSILON); + } + + // ========== Additional Edge Case Tests ========== + + @Test + void testArithmeticMeanWithVeryLargeNumbers() { + List numbers = Arrays.asList(1e100, 2e100, 3e100); + assertEquals(2e100, Means.arithmetic(numbers), 1e90); } @Test - void harmonicMeanMultipleNumbers() { - Vector numbers = new Vector<>(Lists.newArrayList(1d, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5)); - assertEquals(4.6697322801074135, Means.harmonic(numbers)); + void testArithmeticMeanWithVerySmallNumbers() { + List numbers = Arrays.asList(1e-100, 2e-100, 3e-100); + assertEquals(2e-100, Means.arithmetic(numbers), 1e-110); + } + + @Test + void testGeometricMeanWithOnes() { + List numbers = Arrays.asList(1.0, 1.0, 1.0, 1.0); + assertEquals(1.0, Means.geometric(numbers), EPSILON); + } + + @Test + void testAllMeansConsistencyForIdenticalValues() { + List numbers = Arrays.asList(7.5, 7.5, 7.5, 7.5); + double arithmetic = Means.arithmetic(numbers); + double geometric = Means.geometric(numbers); + double harmonic = Means.harmonic(numbers); + + assertEquals(7.5, arithmetic, EPSILON); + assertEquals(7.5, geometric, EPSILON); + assertEquals(7.5, harmonic, EPSILON); + } + + @Test + void testMeansRelationship() { + // For positive numbers, harmonic mean ≤ geometric mean ≤ arithmetic 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); + + assertTrue(harmonic <= geometric, "Harmonic mean should be ≤ geometric mean"); + assertTrue(geometric <= arithmetic, "Geometric mean should be ≤ arithmetic mean"); } } diff --git a/src/test/java/com/thealgorithms/maths/MedianTest.java b/src/test/java/com/thealgorithms/maths/MedianTest.java index 560feb695792..f42fe8bb17ee 100644 --- a/src/test/java/com/thealgorithms/maths/MedianTest.java +++ b/src/test/java/com/thealgorithms/maths/MedianTest.java @@ -5,40 +5,153 @@ import org.junit.jupiter.api.Test; -public class MedianTest { +/** + * Test class for {@link Median}. + * Tests various scenarios including edge cases, odd/even length arrays, + * negative values, and unsorted inputs. + */ +class MedianTest { + @Test - void medianSingleValue() { + void testMedianSingleValue() { int[] arr = {0}; assertEquals(0, Median.median(arr)); } @Test - void medianTwoValues() { + void testMedianSinglePositiveValue() { + int[] arr = {42}; + assertEquals(42, Median.median(arr)); + } + + @Test + void testMedianSingleNegativeValue() { + int[] arr = {-15}; + assertEquals(-15, Median.median(arr)); + } + + @Test + void testMedianTwoValues() { int[] arr = {1, 2}; assertEquals(1.5, Median.median(arr)); } @Test - void medianThreeValues() { + void testMedianTwoIdenticalValues() { + int[] arr = {5, 5}; + assertEquals(5.0, Median.median(arr)); + } + + @Test + void testMedianThreeValues() { int[] arr = {1, 2, 3}; assertEquals(2, Median.median(arr)); } @Test - void medianDecimalValueReturn() { + void testMedianThreeUnsortedValues() { + int[] arr = {3, 1, 2}; + assertEquals(2, Median.median(arr)); + } + + @Test + void testMedianDecimalValueReturn() { int[] arr = {1, 2, 3, 4, 5, 6, 8, 9}; assertEquals(4.5, Median.median(arr)); } @Test - void medianNegativeValues() { + void testMedianNegativeValues() { int[] arr = {-27, -16, -7, -4, -2, -1}; assertEquals(-5.5, Median.median(arr)); } @Test - void medianEmptyArrayThrows() { + void testMedianMixedPositiveAndNegativeValues() { + int[] arr = {-10, -5, 0, 5, 10}; + assertEquals(0, Median.median(arr)); + } + + @Test + void testMedianMixedUnsortedValues() { + int[] arr = {10, -5, 0, 5, -10}; + assertEquals(0, Median.median(arr)); + } + + @Test + void testMedianLargeOddArray() { + int[] arr = {9, 7, 5, 3, 1, 2, 4, 6, 8}; + assertEquals(5, Median.median(arr)); + } + + @Test + void testMedianLargeEvenArray() { + int[] arr = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; + assertEquals(55.0, Median.median(arr)); + } + + @Test + void testMedianAllSameValues() { + int[] arr = {7, 7, 7, 7, 7}; + assertEquals(7.0, Median.median(arr)); + } + + @Test + void testMedianWithZeros() { + int[] arr = {0, 0, 0, 0, 0}; + assertEquals(0.0, Median.median(arr)); + } + + @Test + void testMedianAlreadySorted() { + int[] arr = {1, 2, 3, 4, 5}; + assertEquals(3, Median.median(arr)); + } + + @Test + void testMedianReverseSorted() { + int[] arr = {5, 4, 3, 2, 1}; + assertEquals(3, Median.median(arr)); + } + + @Test + void testMedianWithDuplicates() { + int[] arr = {1, 2, 2, 3, 3, 3, 4}; + assertEquals(3, Median.median(arr)); + } + + @Test + void testMedianEmptyArrayThrows() { int[] arr = {}; assertThrows(IllegalArgumentException.class, () -> Median.median(arr)); } + + @Test + void testMedianNullArrayThrows() { + assertThrows(IllegalArgumentException.class, () -> Median.median(null)); + } + + @Test + void testMedianExtremeValues() { + int[] arr = {Integer.MAX_VALUE, Integer.MIN_VALUE}; + assertEquals(-0.5, Median.median(arr)); + } + + @Test + void testMedianTwoNegativeValues() { + int[] arr = {-10, -20}; + assertEquals(-15.0, Median.median(arr)); + } + + @Test + void testMedianFourValuesEven() { + int[] arr = {1, 2, 3, 4}; + assertEquals(2.5, Median.median(arr)); + } + + @Test + void testMedianFiveValuesOdd() { + int[] arr = {10, 20, 30, 40, 50}; + assertEquals(30.0, Median.median(arr)); + } } diff --git a/src/test/java/com/thealgorithms/maths/NevilleTest.java b/src/test/java/com/thealgorithms/maths/NevilleTest.java new file mode 100644 index 000000000000..234fb2d65ce4 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/NevilleTest.java @@ -0,0 +1,69 @@ +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 NevilleTest { + + @Test + public void testInterpolateLinear() { + // Test with a simple linear function y = 2x + 1 + // Points (0, 1) and (2, 5) + double[] x = {0, 2}; + double[] y = {1, 5}; + // We want to find y when x = 1, which should be 3 + double target = 1; + double expected = 3.0; + assertEquals(expected, Neville.interpolate(x, y, target), 1e-9); + } + + @Test + public void testInterpolateQuadratic() { + // Test with a quadratic function y = x^2 + // Points (0, 0), (1, 1), (3, 9) + double[] x = {0, 1, 3}; + double[] y = {0, 1, 9}; + // We want to find y when x = 2, which should be 4 + double target = 2; + double expected = 4.0; + assertEquals(expected, Neville.interpolate(x, y, target), 1e-9); + } + + @Test + public void testInterpolateWithNegativeNumbers() { + // Test with y = x^2 - 2x + 1 + // Points (-1, 4), (0, 1), (2, 1) + double[] x = {-1, 0, 2}; + double[] y = {4, 1, 1}; + // We want to find y when x = 1, which should be 0 + double target = 1; + double expected = 0.0; + assertEquals(expected, Neville.interpolate(x, y, target), 1e-9); + } + + @Test + public void testMismatchedArrayLengths() { + double[] x = {1, 2}; + double[] y = {1}; + double target = 1.5; + assertThrows(IllegalArgumentException.class, () -> Neville.interpolate(x, y, target)); + } + + @Test + public void testEmptyArrays() { + double[] x = {}; + double[] y = {}; + double target = 1; + assertThrows(IllegalArgumentException.class, () -> Neville.interpolate(x, y, target)); + } + + @Test + public void testDuplicateXCoordinates() { + double[] x = {1, 2, 1}; + double[] y = {5, 8, 3}; + double target = 1.5; + assertThrows(IllegalArgumentException.class, () -> Neville.interpolate(x, y, target)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/NumberPersistenceTest.java b/src/test/java/com/thealgorithms/maths/NumberPersistenceTest.java new file mode 100644 index 000000000000..3f7e70167fc2 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/NumberPersistenceTest.java @@ -0,0 +1,42 @@ +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.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class NumberPersistenceTest { + + @ParameterizedTest(name = "multiplicativePersistence({0}) = {1}") + @CsvSource({"0, 0", "7, 0", "217, 2", "39, 3", "999, 4"}) + @DisplayName("Test multiplicative persistence with valid inputs") + void testMultiplicativePersistenceValid(int input, int expected) { + assertEquals(expected, NumberPersistence.multiplicativePersistence(input)); + } + + @ParameterizedTest(name = "multiplicativePersistence({0}) throws IllegalArgumentException") + @ValueSource(ints = {-1, -100, -9999}) + @DisplayName("Test multiplicative persistence with negative numbers") + void testMultiplicativePersistenceNegative(int input) { + Exception exception = assertThrows(IllegalArgumentException.class, () -> NumberPersistence.multiplicativePersistence(input)); + assertEquals("multiplicativePersistence() does not accept negative values", exception.getMessage()); + } + + @ParameterizedTest(name = "additivePersistence({0}) = {1}") + @CsvSource({"0, 0", "5, 0", "199, 3", "999, 2", "1234, 2"}) + @DisplayName("Test additive persistence with valid inputs") + void testAdditivePersistenceValid(int input, int expected) { + assertEquals(expected, NumberPersistence.additivePersistence(input)); + } + + @ParameterizedTest(name = "additivePersistence({0}) throws IllegalArgumentException") + @ValueSource(ints = {-1, -100, -9999}) + @DisplayName("Test additive persistence with negative numbers") + void testAdditivePersistenceNegative(int input) { + Exception exception = assertThrows(IllegalArgumentException.class, () -> NumberPersistence.additivePersistence(input)); + assertEquals("additivePersistence() does not accept negative values", exception.getMessage()); + } +} diff --git a/src/test/java/com/thealgorithms/maths/PiApproximationTest.java b/src/test/java/com/thealgorithms/maths/PiApproximationTest.java new file mode 100644 index 000000000000..47da3b46a336 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PiApproximationTest.java @@ -0,0 +1,169 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PiApproximationTest { + + private static final double DELTA = 0.5; // Tolerance for Pi approximation + private static final double TIGHT_DELTA = 0.1; // Tighter tolerance for large samples + + /** + * Test with known points that are all inside the quarter circle. + */ + @Test + public void testAllPointsInside() { + List points = new ArrayList<>(); + points.add(new PiApproximation.Point(0.0, 0.0)); // Origin + points.add(new PiApproximation.Point(0.5, 0.5)); // Inside + points.add(new PiApproximation.Point(0.3, 0.3)); // Inside + + double result = PiApproximation.approximatePi(points); + // All points inside, so result should be 4.0 + assertEquals(4.0, result, 0.001); + } + + /** + * Test with known points that are all outside the quarter circle. + */ + @Test + public void testAllPointsOutside() { + List points = new ArrayList<>(); + points.add(new PiApproximation.Point(1.0, 1.0)); // Corner - outside + points.add(new PiApproximation.Point(0.9, 0.9)); // Outside + + double result = PiApproximation.approximatePi(points); + // No points inside, so result should be 0.0 + assertEquals(0.0, result, 0.001); + } + + /** + * Test with mixed points (some inside, some outside). + */ + @Test + public void testMixedPoints() { + List points = new ArrayList<>(); + // Inside points + points.add(new PiApproximation.Point(0.0, 0.0)); + points.add(new PiApproximation.Point(0.5, 0.5)); + // Outside points + points.add(new PiApproximation.Point(1.0, 1.0)); + points.add(new PiApproximation.Point(0.9, 0.9)); + + double result = PiApproximation.approximatePi(points); + // 2 out of 4 points inside: 4 * 2/4 = 2.0 + assertEquals(2.0, result, 0.001); + } + + /** + * Test with boundary point (on the circle). + */ + @Test + public void testBoundaryPoint() { + List points = new ArrayList<>(); + points.add(new PiApproximation.Point(1.0, 0.0)); // On circle: x² + y² = 1 + points.add(new PiApproximation.Point(0.0, 1.0)); // On circle + + double result = PiApproximation.approximatePi(points); + // Boundary points should be counted as inside (≤ 1) + assertEquals(4.0, result, 0.001); + } + + /** + * Test with small random sample (moderate accuracy expected). + */ + @Test + public void testSmallRandomSample() { + List points = PiApproximation.generateRandomPoints(1000); + double result = PiApproximation.approximatePi(points); + + // With 1000 points, result should be reasonably close to π + assertEquals(Math.PI, result, DELTA); + } + + /** + * Test with large random sample (better accuracy expected). + */ + @Test + public void testLargeRandomSample() { + List points = PiApproximation.generateRandomPoints(100000); + double result = PiApproximation.approximatePi(points); + + // With 100000 points, result should be very close to π + assertEquals(Math.PI, result, TIGHT_DELTA); + } + + /** + * Test that result is always positive. + */ + @Test + public void testResultIsPositive() { + List points = PiApproximation.generateRandomPoints(1000); + double result = PiApproximation.approximatePi(points); + + assertTrue(result >= 0, "Pi approximation should be positive"); + } + + /** + * Test that result is bounded (0 ≤ result ≤ 4). + */ + @Test + public void testResultIsBounded() { + List points = PiApproximation.generateRandomPoints(1000); + double result = PiApproximation.approximatePi(points); + + assertTrue(result >= 0 && result <= 4, "Pi approximation should be between 0 and 4"); + } + + /** + * Test with single point inside. + */ + @Test + public void testSinglePointInside() { + List points = new ArrayList<>(); + points.add(new PiApproximation.Point(0.0, 0.0)); + + double result = PiApproximation.approximatePi(points); + assertEquals(4.0, result, 0.001); + } + + /** + * Test with single point outside. + */ + @Test + public void testSinglePointOutside() { + List points = new ArrayList<>(); + points.add(new PiApproximation.Point(1.0, 1.0)); + + double result = PiApproximation.approximatePi(points); + assertEquals(0.0, result, 0.001); + } + + /** + * Test that generated points are within valid range [0, 1]. + */ + @Test + public void testGeneratedPointsInRange() { + List points = PiApproximation.generateRandomPoints(100); + + for (PiApproximation.Point p : points) { + assertTrue(p.x >= 0 && p.x <= 1, "X coordinate should be between 0 and 1"); + assertTrue(p.y >= 0 && p.y <= 1, "Y coordinate should be between 0 and 1"); + } + } + + /** + * Test that the correct number of points are generated. + */ + @Test + public void testCorrectNumberOfPointsGenerated() { + int expectedSize = 500; + List points = PiApproximation.generateRandomPoints(expectedSize); + + assertEquals(expectedSize, points.size()); + } +} 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/SieveOfAtkinTest.java b/src/test/java/com/thealgorithms/maths/SieveOfAtkinTest.java new file mode 100644 index 000000000000..8eca45c9abb8 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SieveOfAtkinTest.java @@ -0,0 +1,47 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@code SieveOfAtkin} class. + */ +class SieveOfAtkinTest { + + @Test + void testGeneratePrimesLimit10() { + List primes = SieveOfAtkin.generatePrimes(10); + // Assert the full expected list of primes + List expected = List.of(2, 3, 5, 7); + assertEquals(expected, primes, "Primes up to 10 should match expected list"); + } + + @Test + void testGeneratePrimesLimit2() { + List primes = SieveOfAtkin.generatePrimes(2); + List expected = List.of(2); + assertEquals(expected, primes, "Primes up to 2 should include 2"); + } + + @Test + void testGeneratePrimesLimit1() { + List primes = SieveOfAtkin.generatePrimes(1); + assertTrue(primes.isEmpty(), "Primes list should be empty when limit < 2"); + } + + @Test + void testGeneratePrimesLimit50() { + List primes = SieveOfAtkin.generatePrimes(50); + List expected = List.of(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47); + assertEquals(expected, primes, "Primes up to 50 should match expected list"); + } + + @Test + void testGeneratePrimesNegativeLimit() { + List primes = SieveOfAtkin.generatePrimes(-10); + assertTrue(primes.isEmpty(), "Primes list should be empty for negative limit"); + } +} 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/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/SumOfSquaresTest.java b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java new file mode 100644 index 000000000000..834fe61a049e --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java @@ -0,0 +1,68 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Test class for SumOfSquares + * + * @author BEASTSHRIRAM + */ +class SumOfSquaresTest { + + @Test + void testPerfectSquares() { + // Perfect squares should return 1 + assertEquals(1, SumOfSquares.minSquares(1)); // 1^2 + assertEquals(1, SumOfSquares.minSquares(4)); // 2^2 + assertEquals(1, SumOfSquares.minSquares(9)); // 3^2 + assertEquals(1, SumOfSquares.minSquares(16)); // 4^2 + assertEquals(1, SumOfSquares.minSquares(25)); // 5^2 + } + + @Test + void testTwoSquares() { + // Numbers that can be expressed as sum of two squares + assertEquals(2, SumOfSquares.minSquares(2)); // 1^2 + 1^2 + assertEquals(2, SumOfSquares.minSquares(5)); // 1^2 + 2^2 + assertEquals(2, SumOfSquares.minSquares(8)); // 2^2 + 2^2 + assertEquals(2, SumOfSquares.minSquares(10)); // 1^2 + 3^2 + assertEquals(2, SumOfSquares.minSquares(13)); // 2^2 + 3^2 + } + + @Test + void testThreeSquares() { + // Numbers that require exactly three squares + assertEquals(3, SumOfSquares.minSquares(3)); // 1^2 + 1^2 + 1^2 + assertEquals(3, SumOfSquares.minSquares(6)); // 1^2 + 1^2 + 2^2 + assertEquals(3, SumOfSquares.minSquares(11)); // 1^2 + 1^2 + 3^2 + assertEquals(3, SumOfSquares.minSquares(12)); // 2^2 + 2^2 + 2^2 + assertEquals(3, SumOfSquares.minSquares(14)); // 1^2 + 2^2 + 3^2 + } + + @Test + void testFourSquares() { + // Numbers that require exactly four squares (form 4^a * (8b + 7)) + assertEquals(4, SumOfSquares.minSquares(7)); // 1^2 + 1^2 + 1^2 + 2^2 + assertEquals(4, SumOfSquares.minSquares(15)); // 1^2 + 1^2 + 2^2 + 3^2 + assertEquals(4, SumOfSquares.minSquares(23)); // 1^2 + 1^2 + 3^2 + 3^2 + assertEquals(4, SumOfSquares.minSquares(28)); // 4 * 7, so needs 4 squares + assertEquals(4, SumOfSquares.minSquares(31)); // 1^2 + 2^2 + 3^2 + 3^2 + } + + @Test + void testLargerNumbers() { + // Test some larger numbers + assertEquals(1, SumOfSquares.minSquares(100)); // 10^2 + assertEquals(2, SumOfSquares.minSquares(65)); // 1^2 + 8^2 + assertEquals(3, SumOfSquares.minSquares(19)); // 1^2 + 3^2 + 3^2 + assertEquals(4, SumOfSquares.minSquares(60)); // 4 * 15, and 15 = 8*1 + 7 + } + + @Test + void testEdgeCases() { + // Test edge case + assertEquals(1, SumOfSquares.minSquares(0)); // 0^2 + } +} diff --git a/src/test/java/com/thealgorithms/maths/ZellersCongruenceTest.java b/src/test/java/com/thealgorithms/maths/ZellersCongruenceTest.java new file mode 100644 index 000000000000..71bcbdc5ed9f --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ZellersCongruenceTest.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 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 ZellersCongruenceTest { + + static Stream validDates() { + return Stream.of(Arguments.of("01-01-2000", "Saturday"), Arguments.of("12-25-2021", "Saturday"), Arguments.of("07-04-1776", "Thursday"), Arguments.of("02-29-2020", "Saturday"), Arguments.of("03-01-1900", "Thursday"), Arguments.of("03/01/1900", "Thursday")); + } + + static Stream invalidDates() { + return Stream.of(Arguments.of("13-01-2000", "Month must be between 1 and 12."), Arguments.of("02-30-2020", "Invalid date."), Arguments.of("00-15-2020", "Month must be between 1 and 12."), Arguments.of("01-01-0000", "Year must be between 46 and 8499."), + Arguments.of("01/01/200", "Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY."), Arguments.of("01@01>2000", "Date separator must be '-' or '/'."), Arguments.of("aa-01-1900", "Invalid numeric part: aa"), + Arguments.of(null, "Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY.")); + } + + @ParameterizedTest + @MethodSource("validDates") + void testValidDates(String inputDate, String expectedDay) { + String result = ZellersCongruence.calculateDay(inputDate); + assertEquals("The date " + inputDate + " falls on a " + expectedDay + ".", result); + } + + @ParameterizedTest + @MethodSource("invalidDates") + void testInvalidDates(String inputDate, String expectedErrorMessage) { + Exception exception = assertThrows(IllegalArgumentException.class, () -> ZellersCongruence.calculateDay(inputDate)); + assertEquals(expectedErrorMessage, exception.getMessage()); + } +} diff --git a/src/test/java/com/thealgorithms/matrix/LUDecompositionTest.java b/src/test/java/com/thealgorithms/matrix/LUDecompositionTest.java new file mode 100644 index 000000000000..d3cc6d64bf42 --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/LUDecompositionTest.java @@ -0,0 +1,40 @@ +package com.thealgorithms.matrix; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import org.junit.jupiter.api.Test; + +public class LUDecompositionTest { + + @Test + public void testLUDecomposition() { + double[][] a = {{4, 3}, {6, 3}}; + + // Perform LU decomposition + LUDecomposition.LU lu = LUDecomposition.decompose(a); + double[][] l = lu.l; + double[][] u = lu.u; + + // Reconstruct a from l and u + double[][] reconstructed = multiplyMatrices(l, u); + + // Assert that reconstructed matrix matches original a + for (int i = 0; i < a.length; i++) { + assertArrayEquals(a[i], reconstructed[i], 1e-9); + } + } + + // Helper method to multiply two matrices + private double[][] multiplyMatrices(double[][] a, double[][] b) { + int n = a.length; + double[][] c = new double[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + for (int k = 0; k < n; k++) { + c[i][j] += a[i][k] * b[k][j]; + } + } + } + return c; + } +} diff --git a/src/test/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrderTest.java b/src/test/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrderTest.java new file mode 100644 index 000000000000..745fdf98f193 --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/PrintAMatrixInSpiralOrderTest.java @@ -0,0 +1,83 @@ +package com.thealgorithms.matrix; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PrintAMatrixInSpiralOrderTest { + + private final PrintAMatrixInSpiralOrder spiralPrinter = new PrintAMatrixInSpiralOrder(); + + @Test + void testSquareMatrix() { + int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + List expected = Arrays.asList(1, 2, 3, 6, 9, 8, 7, 4, 5); + assertEquals(expected, spiralPrinter.print(matrix, 3, 3)); + } + + @Test + void testRectangularMatrixMoreRows() { + int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}}; + List expected = Arrays.asList(1, 2, 3, 6, 9, 12, 11, 10, 7, 4, 5, 8); + assertEquals(expected, spiralPrinter.print(matrix, 4, 3)); + } + + @Test + void testRectangularMatrixMoreCols() { + int[][] matrix = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; + List expected = Arrays.asList(1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7); + assertEquals(expected, spiralPrinter.print(matrix, 3, 4)); + } + + @Test + void testSingleRow() { + int[][] matrix = {{1, 2, 3, 4}}; + List expected = Arrays.asList(1, 2, 3, 4); + assertEquals(expected, spiralPrinter.print(matrix, 1, 4)); + } + + @Test + void testSingleColumn() { + int[][] matrix = {{1}, {2}, {3}}; + List expected = Arrays.asList(1, 2, 3); + assertEquals(expected, spiralPrinter.print(matrix, 3, 1)); + } + + @Test + void testEmptyMatrix() { + int[][] matrix = new int[0][0]; + List expected = Collections.emptyList(); + assertEquals(expected, spiralPrinter.print(matrix, 0, 0)); + } + + @Test + void testOneElementMatrix() { + int[][] matrix = {{42}}; + List expected = Collections.singletonList(42); + assertEquals(expected, spiralPrinter.print(matrix, 1, 1)); + } + + @Test + void testMatrixWithNegativeNumbers() { + int[][] matrix = {{-1, -2}, {-3, -4}}; + List expected = Arrays.asList(-1, -2, -4, -3); + assertEquals(expected, spiralPrinter.print(matrix, 2, 2)); + } + + @Test + void testLargeSquareMatrix() { + int[][] matrix = {{3, 4, 5, 6, 7}, {8, 9, 10, 11, 12}, {14, 15, 16, 17, 18}, {23, 24, 25, 26, 27}, {30, 31, 32, 33, 34}}; + List expected = Arrays.asList(3, 4, 5, 6, 7, 12, 18, 27, 34, 33, 32, 31, 30, 23, 14, 8, 9, 10, 11, 17, 26, 25, 24, 15, 16); + assertEquals(expected, spiralPrinter.print(matrix, 5, 5)); + } + + @Test + void testSingleRowWithTwoElements() { + int[][] matrix = {{2, 2}}; + List expected = Arrays.asList(2, 2); + assertEquals(expected, spiralPrinter.print(matrix, 1, 2)); + } +} 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/matrix/TestPrintMatrixInSpiralOrder.java b/src/test/java/com/thealgorithms/matrix/TestPrintMatrixInSpiralOrder.java deleted file mode 100644 index bb415a5861a8..000000000000 --- a/src/test/java/com/thealgorithms/matrix/TestPrintMatrixInSpiralOrder.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.thealgorithms.matrix; - -import static org.junit.jupiter.api.Assertions.assertIterableEquals; - -import java.util.List; -import org.junit.jupiter.api.Test; - -public class TestPrintMatrixInSpiralOrder { - @Test - public void testOne() { - int[][] matrix = {{3, 4, 5, 6, 7}, {8, 9, 10, 11, 12}, {14, 15, 16, 17, 18}, {23, 24, 25, 26, 27}, {30, 31, 32, 33, 34}}; - var printClass = new PrintAMatrixInSpiralOrder(); - List res = printClass.print(matrix, matrix.length, matrix[0].length); - List list = List.of(3, 4, 5, 6, 7, 12, 18, 27, 34, 33, 32, 31, 30, 23, 14, 8, 9, 10, 11, 17, 26, 25, 24, 15, 16); - assertIterableEquals(res, list); - } - - @Test - public void testTwo() { - int[][] matrix = {{2, 2}}; - var printClass = new PrintAMatrixInSpiralOrder(); - List res = printClass.print(matrix, matrix.length, matrix[0].length); - List list = List.of(2, 2); - assertIterableEquals(res, list); - } -} diff --git a/src/test/java/com/thealgorithms/others/HuffmanTest.java b/src/test/java/com/thealgorithms/others/HuffmanTest.java new file mode 100644 index 000000000000..aa16f6493506 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/HuffmanTest.java @@ -0,0 +1,223 @@ +package com.thealgorithms.others; + +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test class for Huffman coding algorithm. + * Tests various scenarios including normal cases, edge cases, and error + * conditions. + */ +class HuffmanTest { + + @Test + void testBuildHuffmanTreeWithBasicInput() { + char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f'}; + int[] charFreq = {5, 9, 12, 13, 16, 45}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + + Assertions.assertNotNull(root); + Assertions.assertEquals(100, root.data); // Total frequency + } + + @Test + void testGenerateCodesWithBasicInput() { + char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f'}; + int[] charFreq = {5, 9, 12, 13, 16, 45}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(6, codes.size()); + + // Verify that all characters have codes + for (char c : charArray) { + Assertions.assertTrue(codes.containsKey(c), "Missing code for character: " + c); + Assertions.assertNotNull(codes.get(c), "Null code for character: " + c); + } + + // Verify that higher frequency characters have shorter codes + // 'f' has the highest frequency (45), so it should have one of the shortest + // codes + Assertions.assertTrue(codes.get('f').length() <= codes.get('a').length()); + } + + @Test + void testSingleCharacter() { + char[] charArray = {'a'}; + int[] charFreq = {10}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(1, codes.size()); + Assertions.assertEquals("0", codes.get('a')); // Single character gets code "0" + } + + @Test + void testTwoCharacters() { + char[] charArray = {'a', 'b'}; + int[] charFreq = {3, 7}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(2, codes.size()); + + // Verify both characters have codes + Assertions.assertTrue(codes.containsKey('a')); + Assertions.assertTrue(codes.containsKey('b')); + + // Verify codes are different + Assertions.assertNotEquals(codes.get('a'), codes.get('b')); + } + + @Test + void testEqualFrequencies() { + char[] charArray = {'a', 'b', 'c'}; + int[] charFreq = {5, 5, 5}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(3, codes.size()); + + // Verify all characters have codes + for (char c : charArray) { + Assertions.assertTrue(codes.containsKey(c)); + } + } + + @Test + void testLargeFrequencyDifference() { + char[] charArray = {'a', 'b', 'c'}; + int[] charFreq = {1, 10, 100}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(3, codes.size()); + + // Character 'c' with highest frequency should have shortest code + Assertions.assertTrue(codes.get('c').length() <= codes.get('b').length()); + Assertions.assertTrue(codes.get('c').length() <= codes.get('a').length()); + } + + @Test + void testNullCharacterArray() { + int[] charFreq = {5, 9, 12}; + + Assertions.assertThrows(IllegalArgumentException.class, () -> { Huffman.buildHuffmanTree(null, charFreq); }); + } + + @Test + void testNullFrequencyArray() { + char[] charArray = {'a', 'b', 'c'}; + + Assertions.assertThrows(IllegalArgumentException.class, () -> { Huffman.buildHuffmanTree(charArray, null); }); + } + + @Test + void testEmptyArrays() { + char[] charArray = {}; + int[] charFreq = {}; + + Assertions.assertThrows(IllegalArgumentException.class, () -> { Huffman.buildHuffmanTree(charArray, charFreq); }); + } + + @Test + void testMismatchedArrayLengths() { + char[] charArray = {'a', 'b', 'c'}; + int[] charFreq = {5, 9}; + + Assertions.assertThrows(IllegalArgumentException.class, () -> { Huffman.buildHuffmanTree(charArray, charFreq); }); + } + + @Test + void testNegativeFrequency() { + char[] charArray = {'a', 'b', 'c'}; + int[] charFreq = {5, -9, 12}; + + Assertions.assertThrows(IllegalArgumentException.class, () -> { Huffman.buildHuffmanTree(charArray, charFreq); }); + } + + @Test + void testZeroFrequency() { + char[] charArray = {'a', 'b', 'c'}; + int[] charFreq = {0, 5, 10}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(3, codes.size()); + Assertions.assertTrue(codes.containsKey('a')); // Even with 0 frequency, character should have a code + } + + @Test + void testGenerateCodesWithNullRoot() { + Map codes = Huffman.generateCodes(null); + + Assertions.assertNotNull(codes); + Assertions.assertTrue(codes.isEmpty()); + } + + @Test + void testPrefixProperty() { + // Verify that no code is a prefix of another (Huffman property) + char[] charArray = {'a', 'b', 'c', 'd', 'e'}; + int[] charFreq = {5, 9, 12, 13, 16}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + // Check that no code is a prefix of another + for (Map.Entry entry1 : codes.entrySet()) { + for (Map.Entry entry2 : codes.entrySet()) { + if (!entry1.getKey().equals(entry2.getKey())) { + String code1 = entry1.getValue(); + String code2 = entry2.getValue(); + Assertions.assertTrue(!code1.startsWith(code2) && !code2.startsWith(code1), "Code " + code1 + " is a prefix of " + code2); + } + } + } + } + + @Test + void testBinaryCodesOnly() { + // Verify that all codes contain only '0' and '1' + char[] charArray = {'a', 'b', 'c', 'd'}; + int[] charFreq = {1, 2, 3, 4}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + for (String code : codes.values()) { + Assertions.assertTrue(code.matches("[01]+"), "Code contains non-binary characters: " + code); + } + } + + @Test + void testMultipleCharactersWithLargeAlphabet() { + char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}; + int[] charFreq = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; + + HuffmanNode root = Huffman.buildHuffmanTree(charArray, charFreq); + Map codes = Huffman.generateCodes(root); + + Assertions.assertNotNull(codes); + Assertions.assertEquals(10, codes.size()); + + // Verify all characters have codes + for (char c : charArray) { + Assertions.assertTrue(codes.containsKey(c)); + } + } +} diff --git a/src/test/java/com/thealgorithms/others/InsertDeleteInArrayTest.java b/src/test/java/com/thealgorithms/others/InsertDeleteInArrayTest.java new file mode 100644 index 000000000000..e8e49980de13 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/InsertDeleteInArrayTest.java @@ -0,0 +1,220 @@ +package com.thealgorithms.others; + +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; + +/** + * Test cases for {@link InsertDeleteInArray} class. + *

+ * Tests cover: + *

    + *
  • Insert operations at various positions
  • + *
  • Delete operations at various positions
  • + *
  • Edge cases (empty arrays, single element, boundary positions)
  • + *
  • Error conditions (null arrays, invalid positions)
  • + *
+ *

+ */ +class InsertDeleteInArrayTest { + + // ========== Insert Element Tests ========== + + @Test + void testInsertAtBeginning() { + int[] array = {2, 3, 4, 5}; + int[] result = InsertDeleteInArray.insertElement(array, 1, 0); + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, result); + } + + @Test + void testInsertAtEnd() { + int[] array = {1, 2, 3, 4}; + int[] result = InsertDeleteInArray.insertElement(array, 5, 4); + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, result); + } + + @Test + void testInsertInMiddle() { + int[] array = {1, 2, 4, 5}; + int[] result = InsertDeleteInArray.insertElement(array, 3, 2); + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, result); + } + + @Test + void testInsertIntoEmptyArray() { + int[] array = {}; + int[] result = InsertDeleteInArray.insertElement(array, 42, 0); + assertArrayEquals(new int[] {42}, result); + } + + @Test + void testInsertIntoSingleElementArray() { + int[] array = {5}; + int[] result = InsertDeleteInArray.insertElement(array, 3, 0); + assertArrayEquals(new int[] {3, 5}, result); + } + + @Test + void testInsertNegativeNumber() { + int[] array = {1, 2, 3}; + int[] result = InsertDeleteInArray.insertElement(array, -10, 1); + assertArrayEquals(new int[] {1, -10, 2, 3}, result); + } + + @Test + void testInsertZero() { + int[] array = {1, 2, 3}; + int[] result = InsertDeleteInArray.insertElement(array, 0, 1); + assertArrayEquals(new int[] {1, 0, 2, 3}, result); + } + + @Test + void testInsertWithNullArray() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.insertElement(null, 5, 0)); + assertEquals("Array cannot be null", exception.getMessage()); + } + + @Test + void testInsertWithNegativePosition() { + int[] array = {1, 2, 3}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.insertElement(array, 5, -1)); + assertEquals("Position must be between 0 and 3", exception.getMessage()); + } + + @Test + void testInsertWithPositionTooLarge() { + int[] array = {1, 2, 3}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.insertElement(array, 5, 4)); + assertEquals("Position must be between 0 and 3", exception.getMessage()); + } + + // ========== Delete Element Tests ========== + + @Test + void testDeleteFromBeginning() { + int[] array = {1, 2, 3, 4, 5}; + int[] result = InsertDeleteInArray.deleteElement(array, 0); + assertArrayEquals(new int[] {2, 3, 4, 5}, result); + } + + @Test + void testDeleteFromEnd() { + int[] array = {1, 2, 3, 4, 5}; + int[] result = InsertDeleteInArray.deleteElement(array, 4); + assertArrayEquals(new int[] {1, 2, 3, 4}, result); + } + + @Test + void testDeleteFromMiddle() { + int[] array = {1, 2, 3, 4, 5}; + int[] result = InsertDeleteInArray.deleteElement(array, 2); + assertArrayEquals(new int[] {1, 2, 4, 5}, result); + } + + @Test + void testDeleteFromSingleElementArray() { + int[] array = {42}; + int[] result = InsertDeleteInArray.deleteElement(array, 0); + assertArrayEquals(new int[] {}, result); + } + + @Test + void testDeleteFromTwoElementArray() { + int[] array = {10, 20}; + int[] result = InsertDeleteInArray.deleteElement(array, 1); + assertArrayEquals(new int[] {10}, result); + } + + @Test + void testDeleteWithNullArray() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.deleteElement(null, 0)); + assertEquals("Array cannot be null", exception.getMessage()); + } + + @Test + void testDeleteFromEmptyArray() { + int[] array = {}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.deleteElement(array, 0)); + assertEquals("Array is empty", exception.getMessage()); + } + + @Test + void testDeleteWithNegativePosition() { + int[] array = {1, 2, 3}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.deleteElement(array, -1)); + assertEquals("Position must be between 0 and 2", exception.getMessage()); + } + + @Test + void testDeleteWithPositionTooLarge() { + int[] array = {1, 2, 3}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.deleteElement(array, 3)); + assertEquals("Position must be between 0 and 2", exception.getMessage()); + } + + @Test + void testDeleteWithPositionEqualToLength() { + int[] array = {1, 2, 3, 4, 5}; + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> InsertDeleteInArray.deleteElement(array, 5)); + assertEquals("Position must be between 0 and 4", exception.getMessage()); + } + + // ========== Combined Operations Tests ========== + + @Test + void testInsertThenDelete() { + int[] array = {1, 2, 3}; + int[] afterInsert = InsertDeleteInArray.insertElement(array, 99, 1); + assertArrayEquals(new int[] {1, 99, 2, 3}, afterInsert); + int[] afterDelete = InsertDeleteInArray.deleteElement(afterInsert, 1); + assertArrayEquals(new int[] {1, 2, 3}, afterDelete); + } + + @Test + void testMultipleInsertions() { + int[] array = {1, 3, 5}; + array = InsertDeleteInArray.insertElement(array, 2, 1); + assertArrayEquals(new int[] {1, 2, 3, 5}, array); + array = InsertDeleteInArray.insertElement(array, 4, 3); + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, array); + } + + @Test + void testMultipleDeletions() { + int[] array = {1, 2, 3, 4, 5}; + array = InsertDeleteInArray.deleteElement(array, 2); + assertArrayEquals(new int[] {1, 2, 4, 5}, array); + array = InsertDeleteInArray.deleteElement(array, 0); + assertArrayEquals(new int[] {2, 4, 5}, array); + } + + @Test + void testLargeArray() { + int[] array = new int[100]; + for (int i = 0; i < 100; i++) { + array[i] = i; + } + int[] result = InsertDeleteInArray.insertElement(array, 999, 50); + assertEquals(101, result.length); + assertEquals(999, result[50]); + assertEquals(49, result[49]); + assertEquals(50, result[51]); + } + + @Test + void testArrayWithDuplicates() { + int[] array = {1, 2, 2, 3, 2}; + int[] result = InsertDeleteInArray.deleteElement(array, 1); + assertArrayEquals(new int[] {1, 2, 3, 2}, result); + } + + @Test + void testNegativeNumbers() { + int[] array = {-5, -3, -1, 0, 1}; + int[] result = InsertDeleteInArray.insertElement(array, -2, 2); + assertArrayEquals(new int[] {-5, -3, -2, -1, 0, 1}, result); + } +} diff --git a/src/test/java/com/thealgorithms/others/IterativeFloodFillTest.java b/src/test/java/com/thealgorithms/others/IterativeFloodFillTest.java new file mode 100644 index 000000000000..560f1df68e81 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/IterativeFloodFillTest.java @@ -0,0 +1,163 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class IterativeFloodFillTest { + + @Test + void testForEmptyImage() { + int[][] image = {}; + int[][] expected = {}; + + IterativeFloodFill.floodFill(image, 4, 5, 3, 2); + assertArrayEquals(expected, image); + } + + @Test + void testForSingleElementImage() { + int[][] image = {{1}}; + int[][] expected = {{3}}; + + IterativeFloodFill.floodFill(image, 0, 0, 3, 1); + assertArrayEquals(expected, image); + } + + @Test + void testForEmptyRow() { + int[][] image = {{}}; + int[][] expected = {{}}; + + IterativeFloodFill.floodFill(image, 4, 5, 3, 2); + assertArrayEquals(expected, image); + } + + @Test + void testForImageOne() { + int[][] image = { + {0, 0, 0, 0, 0, 0, 0}, + {0, 3, 3, 3, 3, 0, 0}, + {0, 3, 1, 1, 5, 0, 0}, + {0, 3, 1, 1, 5, 5, 3}, + {0, 3, 5, 5, 1, 1, 3}, + {0, 0, 0, 5, 1, 1, 3}, + {0, 0, 0, 3, 3, 3, 3}, + }; + + int[][] expected = { + {0, 0, 0, 0, 0, 0, 0}, + {0, 3, 3, 3, 3, 0, 0}, + {0, 3, 2, 2, 5, 0, 0}, + {0, 3, 2, 2, 5, 5, 3}, + {0, 3, 5, 5, 2, 2, 3}, + {0, 0, 0, 5, 2, 2, 3}, + {0, 0, 0, 3, 3, 3, 3}, + }; + + IterativeFloodFill.floodFill(image, 2, 2, 2, 1); + assertArrayEquals(expected, image); + } + + @Test + void testForImageTwo() { + int[][] image = { + {0, 0, 1, 1, 0, 0, 0}, + {1, 1, 3, 3, 3, 0, 0}, + {1, 3, 1, 1, 5, 0, 0}, + {0, 3, 1, 1, 5, 5, 3}, + {0, 3, 5, 5, 1, 1, 3}, + {0, 0, 0, 5, 1, 1, 3}, + {0, 0, 0, 1, 3, 1, 3}, + }; + + int[][] expected = { + {0, 0, 2, 2, 0, 0, 0}, + {2, 2, 3, 3, 3, 0, 0}, + {2, 3, 2, 2, 5, 0, 0}, + {0, 3, 2, 2, 5, 5, 3}, + {0, 3, 5, 5, 2, 2, 3}, + {0, 0, 0, 5, 2, 2, 3}, + {0, 0, 0, 2, 3, 2, 3}, + }; + + IterativeFloodFill.floodFill(image, 2, 2, 2, 1); + assertArrayEquals(expected, image); + } + + @Test + void testForImageThree() { + int[][] image = { + {1, 1, 2, 3, 1, 1, 1}, + {1, 0, 0, 1, 0, 0, 1}, + {1, 1, 1, 0, 3, 1, 2}, + }; + + int[][] expected = { + {4, 4, 2, 3, 4, 4, 4}, + {4, 0, 0, 4, 0, 0, 4}, + {4, 4, 4, 0, 3, 4, 2}, + }; + + IterativeFloodFill.floodFill(image, 0, 1, 4, 1); + assertArrayEquals(expected, image); + } + + @Test + void testForSameNewAndOldColor() { + int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + IterativeFloodFill.floodFill(image, 0, 1, 1, 1); + assertArrayEquals(expected, image); + } + + @Test + void testForBigImage() { + int[][] image = new int[100][100]; + + assertDoesNotThrow(() -> IterativeFloodFill.floodFill(image, 0, 0, 1, 0)); + } + + @Test + void testForBelowZeroX() { + int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + IterativeFloodFill.floodFill(image, -1, 1, 1, 0); + assertArrayEquals(expected, image); + } + + @Test + void testForBelowZeroY() { + int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + IterativeFloodFill.floodFill(image, 1, -1, 1, 0); + assertArrayEquals(expected, image); + } + + @Test + void testForAboveBoundaryX() { + int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + IterativeFloodFill.floodFill(image, 100, 1, 1, 0); + assertArrayEquals(expected, image); + } + + @Test + void testForAboveBoundaryY() { + int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}}; + + IterativeFloodFill.floodFill(image, 1, 100, 1, 0); + assertArrayEquals(expected, image); + } +} diff --git a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java b/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java index 1014f39a26bc..7c3ce6635aa0 100644 --- a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java +++ b/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java @@ -1,53 +1,80 @@ package com.thealgorithms.others; -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.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +/** + * Comprehensive test suite for {@link LowestBasePalindrome}. + * Tests all public methods including edge cases and exception handling. + * + * @author TheAlgorithms Contributors + */ public class LowestBasePalindromeTest { @ParameterizedTest @MethodSource("provideListsForIsPalindromicPositive") public void testIsPalindromicPositive(List list) { - assertTrue(LowestBasePalindrome.isPalindromic(list)); + Assertions.assertTrue(LowestBasePalindrome.isPalindromic(list)); } @ParameterizedTest @MethodSource("provideListsForIsPalindromicNegative") public void testIsPalindromicNegative(List list) { - assertFalse(LowestBasePalindrome.isPalindromic(list)); + Assertions.assertFalse(LowestBasePalindrome.isPalindromic(list)); } @ParameterizedTest @MethodSource("provideNumbersAndBasesForIsPalindromicInBasePositive") public void testIsPalindromicInBasePositive(int number, int base) { - assertTrue(LowestBasePalindrome.isPalindromicInBase(number, base)); + Assertions.assertTrue(LowestBasePalindrome.isPalindromicInBase(number, base)); } @ParameterizedTest @MethodSource("provideNumbersAndBasesForIsPalindromicInBaseNegative") public void testIsPalindromicInBaseNegative(int number, int base) { - assertFalse(LowestBasePalindrome.isPalindromicInBase(number, base)); + Assertions.assertFalse(LowestBasePalindrome.isPalindromicInBase(number, base)); } @ParameterizedTest @MethodSource("provideNumbersAndBasesForExceptions") public void testIsPalindromicInBaseThrowsException(int number, int base) { - org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> LowestBasePalindrome.isPalindromicInBase(number, base)); + Assertions.assertThrows(IllegalArgumentException.class, () -> LowestBasePalindrome.isPalindromicInBase(number, base)); } @ParameterizedTest @MethodSource("provideNumbersForLowestBasePalindrome") public void testLowestBasePalindrome(int number, int expectedBase) { - assertEquals(expectedBase, LowestBasePalindrome.lowestBasePalindrome(number)); + Assertions.assertEquals(expectedBase, LowestBasePalindrome.lowestBasePalindrome(number)); + } + + @ParameterizedTest + @MethodSource("provideNumbersForComputeDigitsInBase") + public void testComputeDigitsInBase(int number, int base, List expectedDigits) { + Assertions.assertEquals(expectedDigits, LowestBasePalindrome.computeDigitsInBase(number, base)); + } + + @ParameterizedTest + @MethodSource("provideInvalidNumbersForComputeDigits") + public void testComputeDigitsInBaseThrowsExceptionForNegativeNumber(int number, int base) { + Assertions.assertThrows(IllegalArgumentException.class, () -> LowestBasePalindrome.computeDigitsInBase(number, base)); + } + + @ParameterizedTest + @MethodSource("provideInvalidBasesForComputeDigits") + public void testComputeDigitsInBaseThrowsExceptionForInvalidBase(int number, int base) { + Assertions.assertThrows(IllegalArgumentException.class, () -> LowestBasePalindrome.computeDigitsInBase(number, base)); + } + + @ParameterizedTest + @MethodSource("provideNegativeNumbersForLowestBasePalindrome") + public void testLowestBasePalindromeThrowsExceptionForNegativeNumber(int number) { + Assertions.assertThrows(IllegalArgumentException.class, () -> LowestBasePalindrome.lowestBasePalindrome(number)); } private static Stream provideListsForIsPalindromicPositive() { @@ -74,4 +101,21 @@ private static Stream provideNumbersForLowestBasePalindrome() { return Stream.of(Arguments.of(0, 2), Arguments.of(1, 2), Arguments.of(2, 3), Arguments.of(3, 2), Arguments.of(10, 3), Arguments.of(11, 10), Arguments.of(15, 2), Arguments.of(39, 12), Arguments.of(44, 10), Arguments.of(58, 28), Arguments.of(69, 22), Arguments.of(79, 78), Arguments.of(87, 28), Arguments.of(90, 14), Arguments.of(5591, 37), Arguments.of(5895, 130), Arguments.of(9950, 198), Arguments.of(9974, 4986)); } + + private static Stream provideNumbersForComputeDigitsInBase() { + return Stream.of(Arguments.of(0, 2, new ArrayList<>()), Arguments.of(5, 2, Arrays.asList(1, 0, 1)), Arguments.of(13, 2, Arrays.asList(1, 0, 1, 1)), Arguments.of(10, 3, Arrays.asList(1, 0, 1)), Arguments.of(15, 2, Arrays.asList(1, 1, 1, 1)), Arguments.of(101, 10, Arrays.asList(1, 0, 1)), + Arguments.of(255, 16, Arrays.asList(15, 15)), Arguments.of(100, 10, Arrays.asList(0, 0, 1))); + } + + private static Stream provideInvalidNumbersForComputeDigits() { + return Stream.of(Arguments.of(-1, 2), Arguments.of(-10, 10), Arguments.of(-100, 5)); + } + + private static Stream provideInvalidBasesForComputeDigits() { + return Stream.of(Arguments.of(10, 1), Arguments.of(5, 0), Arguments.of(100, -1)); + } + + private static Stream provideNegativeNumbersForLowestBasePalindrome() { + return Stream.of(Arguments.of(-1), Arguments.of(-10), Arguments.of(-100)); + } } diff --git a/src/test/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthKTest.java b/src/test/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthKTest.java index f360e3f53546..1a42f1815a96 100644 --- a/src/test/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthKTest.java +++ b/src/test/java/com/thealgorithms/others/MaximumSumOfDistinctSubarraysWithLengthKTest.java @@ -3,20 +3,157 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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 MaximumSumOfDistinctSubarraysWithLengthKTest { +/** + * Test class for {@link MaximumSumOfDistinctSubarraysWithLengthK}. + * + * This class contains comprehensive test cases to verify the correctness of the + * maximum subarray sum algorithm with distinct elements constraint. + */ +class MaximumSumOfDistinctSubarraysWithLengthKTest { + /** + * Parameterized test for various input scenarios. + * + * @param expected the expected maximum sum + * @param k the subarray size + * @param arr the input array + */ @ParameterizedTest @MethodSource("inputStream") - void testMaximumSubarraySum(int expected, int k, int[] arr) { + void testMaximumSubarraySum(long expected, int k, int[] arr) { assertEquals(expected, MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(k, arr)); } + /** + * Provides test cases for the parameterized test. + * + * Test cases cover: + * - Normal cases with distinct and duplicate elements + * - Edge cases (empty array, k = 0, k > array length) + * - Single element arrays + * - Arrays with all duplicates + * - Negative numbers + * - Large sums + * + * @return stream of test arguments + */ private static Stream inputStream() { - return Stream.of(Arguments.of(15, 3, new int[] {1, 5, 4, 2, 9, 9, 9}), Arguments.of(0, 3, new int[] {4, 4, 4}), Arguments.of(12, 3, new int[] {9, 9, 9, 1, 2, 3}), Arguments.of(0, 0, new int[] {9, 9, 9}), Arguments.of(0, 5, new int[] {9, 9, 9}), Arguments.of(9, 1, new int[] {9, 2, 3, 7}), - Arguments.of(15, 5, new int[] {1, 2, 3, 4, 5}), Arguments.of(6, 3, new int[] {-1, 2, 3, 1, -2, 4}), Arguments.of(10, 1, new int[] {10}), Arguments.of(0, 2, new int[] {7, 7, 7, 7}), Arguments.of(0, 3, new int[] {}), Arguments.of(0, 10, new int[] {1, 2, 3})); + return Stream.of( + // Normal case: [5, 4, 2] has distinct elements with sum 11, but [4, 2, 9] also + // distinct with sum 15 + Arguments.of(15L, 3, new int[] {1, 5, 4, 2, 9, 9, 9}), + // All elements are same, no distinct subarray of size 3 + Arguments.of(0L, 3, new int[] {4, 4, 4}), + // First three have duplicates, but [1, 2, 3] are distinct with sum 6, wait + // [9,1,2] has sum 12 + Arguments.of(12L, 3, new int[] {9, 9, 9, 1, 2, 3}), + // k = 0, should return 0 + Arguments.of(0L, 0, new int[] {9, 9, 9}), + // k > array length, should return 0 + Arguments.of(0L, 5, new int[] {9, 9, 9}), + // k = 1, single element (always distinct) + Arguments.of(9L, 1, new int[] {9, 2, 3, 7}), + // All distinct elements, size matches array + Arguments.of(15L, 5, new int[] {1, 2, 3, 4, 5}), + // Array with negative numbers + Arguments.of(6L, 3, new int[] {-1, 2, 3, 1, -2, 4}), + // Single element array + Arguments.of(10L, 1, new int[] {10}), + // All duplicates with k = 2 + Arguments.of(0L, 2, new int[] {7, 7, 7, 7}), + // Empty array + Arguments.of(0L, 3, new int[] {}), + // k much larger than array length + Arguments.of(0L, 10, new int[] {1, 2, 3})); + } + + /** + * Test with a larger array and larger k value. + */ + @Test + void testLargerArray() { + int[] arr = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(5, arr); + // Maximum sum with 5 distinct elements: [6,7,8,9,10] = 40 + assertEquals(40L, result); + } + + /** + * Test with negative k value. + */ + @Test + void testNegativeK() { + int[] arr = new int[] {1, 2, 3, 4, 5}; + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(-1, arr); + assertEquals(0L, result); + } + + /** + * Test with null array. + */ + @Test + void testNullArray() { + int[] nullArray = null; + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(3, new int[][] {nullArray}[0]); + assertEquals(0L, result); + } + + /** + * Test with array containing duplicates at boundaries. + */ + @Test + void testDuplicatesAtBoundaries() { + int[] arr = new int[] {1, 1, 2, 3, 4, 4}; + // [2, 3, 4] is the only valid window with sum 9 + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(3, arr); + assertEquals(9L, result); + } + + /** + * Test with large numbers to verify long return type. + */ + @Test + void testLargeNumbers() { + int[] arr = new int[] {1000000, 2000000, 3000000, 4000000}; + // All elements are distinct, max sum with k=3 is [2000000, 3000000, 4000000] = + // 9000000 + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(3, arr); + assertEquals(9000000L, result); + } + + /** + * Test where multiple windows have the same maximum sum. + */ + @Test + void testMultipleMaxWindows() { + int[] arr = new int[] {1, 2, 3, 4, 3, 2, 1}; + // Windows [1,2,3], [2,3,4], [4,3,2], [3,2,1] - max is [2,3,4] = 9 + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(3, arr); + assertEquals(9L, result); + } + + /** + * Test with only two elements and k=2. + */ + @Test + void testTwoElementsDistinct() { + int[] arr = new int[] {5, 10}; + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(2, arr); + assertEquals(15L, result); + } + + /** + * Test with only two elements (duplicates) and k=2. + */ + @Test + void testTwoElementsDuplicate() { + int[] arr = new int[] {5, 5}; + long result = MaximumSumOfDistinctSubarraysWithLengthK.maximumSubarraySum(2, arr); + assertEquals(0L, result); } } diff --git a/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java b/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java new file mode 100644 index 000000000000..4e81c8b7e34f --- /dev/null +++ b/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java @@ -0,0 +1,344 @@ +package com.thealgorithms.others; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for MiniMaxAlgorithm + * Tests the minimax algorithm implementation for game tree evaluation + */ +class MiniMaxAlgorithmTest { + + private MiniMaxAlgorithm miniMax; + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + void setUp() { + miniMax = new MiniMaxAlgorithm(); + System.setOut(new PrintStream(outputStream)); + } + + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + + @Test + void testConstructorCreatesValidScores() { + // The default constructor should create scores array of length 8 (2^3) + Assertions.assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(3, miniMax.getHeight()); + + // All scores should be positive (between 1 and 99) + for (int score : miniMax.getScores()) { + Assertions.assertTrue(score >= 1 && score <= 99); + } + } + + @Test + void testConstructorWithValidScores() { + int[] validScores = {10, 20, 30, 40}; + MiniMaxAlgorithm customMiniMax = new MiniMaxAlgorithm(validScores); + + Assertions.assertArrayEquals(validScores, customMiniMax.getScores()); + Assertions.assertEquals(2, customMiniMax.getHeight()); // log2(4) = 2 + } + + @Test + void testConstructorWithInvalidScoresThrowsException() { + int[] invalidScores = {10, 20, 30}; // Length 3 is not a power of 2 + Assertions.assertThrows(IllegalArgumentException.class, () -> new MiniMaxAlgorithm(invalidScores)); + } + + @Test + void testConstructorDoesNotModifyOriginalArray() { + int[] originalScores = {10, 20, 30, 40}; + int[] copyOfOriginal = {10, 20, 30, 40}; + MiniMaxAlgorithm customMiniMax = new MiniMaxAlgorithm(originalScores); + + // Modify the original array + originalScores[0] = 999; + + // Constructor should have made a copy, so internal state should be unchanged + Assertions.assertArrayEquals(copyOfOriginal, customMiniMax.getScores()); + } + + @Test + void testSetScoresWithValidPowerOfTwo() { + int[] validScores = {10, 20, 30, 40}; + miniMax.setScores(validScores); + + Assertions.assertArrayEquals(validScores, miniMax.getScores()); + Assertions.assertEquals(2, miniMax.getHeight()); // log2(4) = 2 + } + + @Test + void testSetScoresWithInvalidLength() { + int[] invalidScores = {10, 20, 30}; // Length 3 is not a power of 2 + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores)); + + // Scores should remain unchanged (original length 8) + Assertions.assertEquals(8, miniMax.getScores().length); + } + + @Test + void testSetScoresWithZeroLength() { + int[] emptyScores = {}; // Length 0 is not a power of 2 + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyScores)); + + // Scores should remain unchanged (original length 8) + Assertions.assertEquals(8, miniMax.getScores().length); + } + + @Test + void testSetScoresWithVariousInvalidLengths() { + // Test multiple invalid lengths to ensure isPowerOfTwo function is fully + // covered + int[][] invalidScoreArrays = { + {1, 2, 3, 4, 5}, // Length 5 + {1, 2, 3, 4, 5, 6}, // Length 6 + {1, 2, 3, 4, 5, 6, 7}, // Length 7 + new int[9], // Length 9 + new int[10], // Length 10 + new int[15] // Length 15 + }; + + for (int[] invalidScores : invalidScoreArrays) { + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores), "Failed for array length: " + invalidScores.length); + } + + // Scores should remain unchanged (original length 8) + Assertions.assertEquals(8, miniMax.getScores().length); + } + + @Test + void testSetScoresWithSingleElement() { + int[] singleScore = {42}; + miniMax.setScores(singleScore); + + Assertions.assertArrayEquals(singleScore, miniMax.getScores()); + Assertions.assertEquals(0, miniMax.getHeight()); // log2(1) = 0 + } + + @Test + void testMiniMaxWithKnownScores() { + // Test with a known game tree: [3, 12, 8, 2] + int[] testScores = {3, 12, 8, 2}; + miniMax.setScores(testScores); + + // Maximizer starts: should choose max(min(3,12), min(8,2)) = max(3, 2) = 3 + int result = miniMax.miniMax(0, true, 0, false); + Assertions.assertEquals(3, result); + } + + @Test + void testMiniMaxWithMinimizerFirst() { + // Test with minimizer starting first + int[] testScores = {3, 12, 8, 2}; + miniMax.setScores(testScores); + + // Minimizer starts: should choose min(max(3,12), max(8,2)) = min(12, 8) = 8 + int result = miniMax.miniMax(0, false, 0, false); + Assertions.assertEquals(8, result); + } + + @Test + void testMiniMaxWithLargerTree() { + // Test with 8 elements: [5, 6, 7, 4, 5, 3, 6, 2] + int[] testScores = {5, 6, 7, 4, 5, 3, 6, 2}; + miniMax.setScores(testScores); + + // Maximizer starts + int result = miniMax.miniMax(0, true, 0, false); + // Expected: max(min(max(5,6), max(7,4)), min(max(5,3), max(6,2))) + // = max(min(6, 7), min(5, 6)) = max(6, 5) = 6 + Assertions.assertEquals(6, result); + } + + @Test + void testMiniMaxVerboseOutput() { + int[] testScores = {3, 12, 8, 2}; + miniMax.setScores(testScores); + + miniMax.miniMax(0, true, 0, true); + + String output = outputStream.toString(); + Assertions.assertTrue(output.contains("Maximizer")); + Assertions.assertTrue(output.contains("Minimizer")); + Assertions.assertTrue(output.contains("chooses")); + } + + @Test + void testGetRandomScoresLength() { + int[] randomScores = MiniMaxAlgorithm.getRandomScores(4, 50); + Assertions.assertEquals(16, randomScores.length); // 2^4 = 16 + + // All scores should be between 1 and 50 + for (int score : randomScores) { + Assertions.assertTrue(score >= 1 && score <= 50); + } + } + + @Test + void testGetRandomScoresWithDifferentParameters() { + int[] randomScores = MiniMaxAlgorithm.getRandomScores(2, 10); + Assertions.assertEquals(4, randomScores.length); // 2^2 = 4 + + // All scores should be between 1 and 10 + for (int score : randomScores) { + Assertions.assertTrue(score >= 1 && score <= 10); + } + } + + @Test + void testMainMethod() { + // Test that main method runs without errors + Assertions.assertDoesNotThrow(() -> MiniMaxAlgorithm.main(new String[] {})); + + String output = outputStream.toString(); + Assertions.assertTrue(output.contains("The best score for")); + Assertions.assertTrue(output.contains("Maximizer")); + } + + @Test + void testHeightCalculation() { + // Test height calculation for different array sizes + int[] scores2 = {1, 2}; + miniMax.setScores(scores2); + Assertions.assertEquals(1, miniMax.getHeight()); // log2(2) = 1 + + int[] scores16 = new int[16]; + miniMax.setScores(scores16); + Assertions.assertEquals(4, miniMax.getHeight()); // log2(16) = 4 + } + + @Test + void testEdgeCaseWithZeroScores() { + int[] zeroScores = {0, 0, 0, 0}; + miniMax.setScores(zeroScores); + + int result = miniMax.miniMax(0, true, 0, false); + Assertions.assertEquals(0, result); + } + + @Test + void testEdgeCaseWithNegativeScores() { + int[] negativeScores = {-5, -2, -8, -1}; + miniMax.setScores(negativeScores); + + // Tree evaluation with maximizer first: + // Level 1 (minimizer): min(-5,-2) = -5, min(-8,-1) = -8 + // Level 0 (maximizer): max(-5, -8) = -5 + int result = miniMax.miniMax(0, true, 0, false); + Assertions.assertEquals(-5, result); + } + + @Test + void testSetScoresWithNegativeLength() { + // This test ensures the first condition of isPowerOfTwo (n > 0) is tested + // Although we can't directly create an array with negative length, + // we can test edge cases around zero and ensure proper validation + + // Test with array length 0 (edge case for n > 0 condition) + int[] emptyArray = new int[0]; + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyArray)); + + Assertions.assertEquals(8, miniMax.getScores().length); // Should remain unchanged + } + + @Test + void testSetScoresWithLargePowerOfTwo() { + // Test with a large power of 2 to ensure the algorithm works correctly + int[] largeValidScores = new int[32]; // 32 = 2^5 + for (int i = 0; i < largeValidScores.length; i++) { + largeValidScores[i] = i + 1; + } + + miniMax.setScores(largeValidScores); + Assertions.assertArrayEquals(largeValidScores, miniMax.getScores()); + Assertions.assertEquals(5, miniMax.getHeight()); // log2(32) = 5 + } + + @Test + void testSetScoresValidEdgeCases() { + // Test valid powers of 2 to ensure isPowerOfTwo returns true correctly + int[][] validPowersOf2 = { + new int[1], // 1 = 2^0 + new int[2], // 2 = 2^1 + new int[4], // 4 = 2^2 + new int[8], // 8 = 2^3 + new int[16], // 16 = 2^4 + new int[64] // 64 = 2^6 + }; + + int[] expectedHeights = {0, 1, 2, 3, 4, 6}; + + for (int i = 0; i < validPowersOf2.length; i++) { + miniMax.setScores(validPowersOf2[i]); + Assertions.assertEquals(validPowersOf2[i].length, miniMax.getScores().length, "Failed for array length: " + validPowersOf2[i].length); + Assertions.assertEquals(expectedHeights[i], miniMax.getHeight(), "Height calculation failed for array length: " + validPowersOf2[i].length); + } + } + + @Test + void testGetScoresReturnsDefensiveCopy() { + int[] originalScores = {10, 20, 30, 40}; + miniMax.setScores(originalScores); + + // Get the scores and modify them + int[] retrievedScores = miniMax.getScores(); + retrievedScores[0] = 999; + + // Internal state should remain unchanged + Assertions.assertEquals(10, miniMax.getScores()[0]); + } + + @Test + void testSetScoresCreatesDefensiveCopy() { + int[] originalScores = {10, 20, 30, 40}; + miniMax.setScores(originalScores); + + // Modify the original array after setting + originalScores[0] = 999; + + // Internal state should remain unchanged + Assertions.assertEquals(10, miniMax.getScores()[0]); + } + + @Test + void testMiniMaxWithAllSameScores() { + int[] sameScores = {5, 5, 5, 5}; + miniMax.setScores(sameScores); + + // When all scores are the same, result should be that score + int result = miniMax.miniMax(0, true, 0, false); + Assertions.assertEquals(5, result); + } + + @Test + void testMiniMaxAtDifferentDepths() { + int[] testScores = {3, 12, 8, 2, 14, 5, 2, 9}; + miniMax.setScores(testScores); + + // Test maximizer first + int result = miniMax.miniMax(0, true, 0, false); + // Expected: max(min(max(3,12), max(8,2)), min(max(14,5), max(2,9))) + // = max(min(12, 8), min(14, 9)) = max(8, 9) = 9 + Assertions.assertEquals(9, result); + } + + @Test + void testMiniMaxWithMinIntAndMaxInt() { + int[] extremeScores = {Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 1}; + miniMax.setScores(extremeScores); + + int result = miniMax.miniMax(0, true, 0, false); + // Expected: max(min(MIN, MAX), min(0, 1)) = max(MIN, 0) = 0 + Assertions.assertEquals(0, result); + } +} diff --git a/src/test/java/com/thealgorithms/others/MosAlgorithmTest.java b/src/test/java/com/thealgorithms/others/MosAlgorithmTest.java new file mode 100644 index 000000000000..ac931eb0a2b9 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/MosAlgorithmTest.java @@ -0,0 +1,202 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.junit.jupiter.api.Test; + +/** + * Test class for MosAlgorithm + * + * @author BEASTSHRIRAM + */ +class MosAlgorithmTest { + + @Test + void testRangeSumQueriesBasic() { + int[] arr = {1, 3, 5, 2, 7}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 2, 0), // Sum of [1, 3, 5] = 9 + new MosAlgorithm.Query(1, 3, 1), // Sum of [3, 5, 2] = 10 + new MosAlgorithm.Query(2, 4, 2) // Sum of [5, 2, 7] = 14 + }; + + int[] expected = {9, 10, 14}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeSumQueriesSingleElement() { + int[] arr = {5, 10, 15, 20}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 0, 0), // Sum of [5] = 5 + new MosAlgorithm.Query(1, 1, 1), // Sum of [10] = 10 + new MosAlgorithm.Query(2, 2, 2), // Sum of [15] = 15 + new MosAlgorithm.Query(3, 3, 3) // Sum of [20] = 20 + }; + + int[] expected = {5, 10, 15, 20}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeSumQueriesFullArray() { + int[] arr = {1, 2, 3, 4, 5}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 4, 0) // Sum of entire array = 15 + }; + + int[] expected = {15}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeSumQueriesOverlapping() { + int[] arr = {2, 4, 6, 8, 10}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 2, 0), // Sum of [2, 4, 6] = 12 + new MosAlgorithm.Query(1, 3, 1), // Sum of [4, 6, 8] = 18 + new MosAlgorithm.Query(2, 4, 2) // Sum of [6, 8, 10] = 24 + }; + + int[] expected = {12, 18, 24}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeFrequencyQueriesBasic() { + int[] arr = {1, 2, 2, 1, 3, 2, 1}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 3, 0), // Count of 2 in [1, 2, 2, 1] = 2 + new MosAlgorithm.Query(1, 5, 1), // Count of 2 in [2, 2, 1, 3, 2] = 3 + new MosAlgorithm.Query(4, 6, 2) // Count of 2 in [3, 2, 1] = 1 + }; + + int[] expected = {2, 3, 1}; + int[] results = MosAlgorithm.solveRangeFrequencyQueries(arr, queries, 2); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeFrequencyQueriesNoMatch() { + int[] arr = {1, 3, 5, 7, 9}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 2, 0), // Count of 2 in [1, 3, 5] = 0 + new MosAlgorithm.Query(1, 4, 1) // Count of 2 in [3, 5, 7, 9] = 0 + }; + + int[] expected = {0, 0}; + int[] results = MosAlgorithm.solveRangeFrequencyQueries(arr, queries, 2); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeFrequencyQueriesAllMatch() { + int[] arr = {5, 5, 5, 5, 5}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 2, 0), // Count of 5 in [5, 5, 5] = 3 + new MosAlgorithm.Query(1, 3, 1), // Count of 5 in [5, 5, 5] = 3 + new MosAlgorithm.Query(0, 4, 2) // Count of 5 in [5, 5, 5, 5, 5] = 5 + }; + + int[] expected = {3, 3, 5}; + int[] results = MosAlgorithm.solveRangeFrequencyQueries(arr, queries, 5); + + assertArrayEquals(expected, results); + } + + @Test + void testEmptyArray() { + int[] arr = {}; + MosAlgorithm.Query[] queries = {}; + + int[] expected = {}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testNullInputs() { + int[] results1 = MosAlgorithm.solveRangeSumQueries(null, null); + assertArrayEquals(new int[0], results1); + + int[] results2 = MosAlgorithm.solveRangeFrequencyQueries(null, null, 1); + assertArrayEquals(new int[0], results2); + } + + @Test + void testQueryStructure() { + MosAlgorithm.Query query = new MosAlgorithm.Query(1, 5, 0); + + assertEquals(1, query.left); + assertEquals(5, query.right); + assertEquals(0, query.index); + assertEquals(0, query.result); // Default value + } + + @Test + void testLargerArray() { + int[] arr = {1, 4, 2, 8, 5, 7, 3, 6, 9, 10}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 4, 0), // Sum of [1, 4, 2, 8, 5] = 20 + new MosAlgorithm.Query(2, 7, 1), // Sum of [2, 8, 5, 7, 3, 6] = 31 + new MosAlgorithm.Query(5, 9, 2), // Sum of [7, 3, 6, 9, 10] = 35 + new MosAlgorithm.Query(1, 8, 3) // Sum of [4, 2, 8, 5, 7, 3, 6, 9] = 44 + }; + + int[] expected = {20, 31, 35, 44}; + int[] results = MosAlgorithm.solveRangeSumQueries(arr, queries); + + assertArrayEquals(expected, results); + } + + @Test + void testRangeFrequencyWithDuplicates() { + int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3}; + MosAlgorithm.Query[] queries = { + new MosAlgorithm.Query(0, 5, 0), // Count of 1 in [3, 1, 4, 1, 5, 9] = 2 + new MosAlgorithm.Query(3, 9, 1), // Count of 1 in [1, 5, 9, 2, 6, 5, 3] = 1 + new MosAlgorithm.Query(0, 9, 2) // Count of 1 in entire array = 2 + }; + + int[] expected = {2, 1, 2}; + int[] results = MosAlgorithm.solveRangeFrequencyQueries(arr, queries, 1); + + assertArrayEquals(expected, results); + } + + @Test + void testMainMethod() { + // Capture System.out + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outputStream)); + + try { + // Test main method + MosAlgorithm.main(new String[] {}); + String output = outputStream.toString(); + + // Verify expected output contains demonstration + assertTrue(output.contains("Range Sum Queries:")); + assertTrue(output.contains("Range Frequency Queries (count of value 3):")); + assertTrue(output.contains("Array: [1, 3, 5, 2, 7, 6, 3, 1, 4, 8]")); + } finally { + System.setOut(originalOut); + } + } +} diff --git a/src/test/java/com/thealgorithms/others/PageRankTest.java b/src/test/java/com/thealgorithms/others/PageRankTest.java new file mode 100644 index 000000000000..de96403dd1fc --- /dev/null +++ b/src/test/java/com/thealgorithms/others/PageRankTest.java @@ -0,0 +1,319 @@ +package com.thealgorithms.others; + +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 org.junit.jupiter.api.Test; + +/** + * Test class for PageRank algorithm implementation + * + * @author Hardvan + */ +class PageRankTest { + + private static final double EPSILON = 0.0001; // Tolerance for floating point comparisons + + /** + * Test basic PageRank calculation with a simple 3-node graph + * Graph: 1 -> 2, 2 -> 3, 3 -> 1 + */ + @Test + void testSimpleThreeNodeGraph() { + PageRank pageRank = new PageRank(3); + + // Create a simple circular graph: 1 -> 2 -> 3 -> 1 + int[][] adjacencyMatrix = new int[10][10]; + adjacencyMatrix[1][2] = 1; // Node 1 links to Node 2 + adjacencyMatrix[2][3] = 1; // Node 2 links to Node 3 + adjacencyMatrix[3][1] = 1; // Node 3 links to Node 1 + + pageRank.setAdjacencyMatrix(adjacencyMatrix); + double[] result = pageRank.calculatePageRank(3); + + // All nodes should have equal PageRank in a circular graph + assertNotNull(result); + assertEquals(result[1], result[2], EPSILON); + assertEquals(result[2], result[3], EPSILON); + } + + /** + * Test PageRank with a two-node graph where one node points to another + */ + @Test + void testTwoNodeGraph() { + PageRank pageRank = new PageRank(2); + + // Node 1 links to Node 2 + pageRank.setEdge(1, 2, 1); + + double[] result = pageRank.calculatePageRank(2); + + // Node 2 should have higher PageRank than Node 1 (after 2 iterations) + assertNotNull(result); + assertEquals(0.2775, result[2], EPSILON); + assertEquals(0.15, result[1], EPSILON); + } + + /** + * Test PageRank with a single node (no links) + */ + @Test + void testSingleNode() { + PageRank pageRank = new PageRank(1); + + double[] result = pageRank.calculatePageRank(1); + + // Single node should have (1-d) = 0.15 after applying damping + assertNotNull(result); + assertEquals(0.15, result[1], EPSILON); + } + + /** + * Test PageRank with a hub-and-spoke configuration + * Node 1 is the hub, pointing to nodes 2, 3, and 4 + */ + @Test + void testHubAndSpokeGraph() { + PageRank pageRank = new PageRank(4); + + // Hub node (1) links to all other nodes + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(1, 3, 1); + pageRank.setEdge(1, 4, 1); + + // All spokes link back to hub + pageRank.setEdge(2, 1, 1); + pageRank.setEdge(3, 1, 1); + pageRank.setEdge(4, 1, 1); + + double[] result = pageRank.calculatePageRank(4); + + assertNotNull(result); + // Hub should have higher PageRank + assertEquals(result[2], result[3], EPSILON); + assertEquals(result[3], result[4], EPSILON); + } + + /** + * Test PageRank with multiple iterations + */ + @Test + void testMultipleIterations() { + PageRank pageRank = new PageRank(3); + + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 1, 1); + + double[] result2Iterations = pageRank.calculatePageRank(3, 0.85, 2, false); + double[] result5Iterations = pageRank.calculatePageRank(3, 0.85, 5, false); + + assertNotNull(result2Iterations); + assertNotNull(result5Iterations); + } + + /** + * Test getPageRank method for individual node + */ + @Test + void testGetPageRank() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + pageRank.calculatePageRank(2); + + double node1PageRank = pageRank.getPageRank(1); + double node2PageRank = pageRank.getPageRank(2); + + assertEquals(0.15, node1PageRank, EPSILON); + assertEquals(0.2775, node2PageRank, EPSILON); + } + + /** + * Test getAllPageRanks method + */ + @Test + void testGetAllPageRanks() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + pageRank.calculatePageRank(2); + + double[] allPageRanks = pageRank.getAllPageRanks(); + + assertNotNull(allPageRanks); + assertEquals(0.15, allPageRanks[1], EPSILON); + assertEquals(0.2775, allPageRanks[2], EPSILON); + } + + /** + * Test that self-loops are not allowed + */ + @Test + void testNoSelfLoops() { + PageRank pageRank = new PageRank(2); + + // Try to set a self-loop + pageRank.setEdge(1, 1, 1); + pageRank.setEdge(1, 2, 1); + + double[] result = pageRank.calculatePageRank(2); + + assertNotNull(result); + // Self-loop should be ignored + } + + /** + * Test exception when node count is too small + */ + @Test + void testInvalidNodeCountTooSmall() { + assertThrows(IllegalArgumentException.class, () -> new PageRank(0)); + } + + /** + * Test exception when node count is too large + */ + @Test + void testInvalidNodeCountTooLarge() { + assertThrows(IllegalArgumentException.class, () -> new PageRank(11)); + } + + /** + * Test exception for invalid damping factor (negative) + */ + @Test + void testInvalidDampingFactorNegative() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, -0.1, 2, false)); + } + + /** + * Test exception for invalid damping factor (greater than 1) + */ + @Test + void testInvalidDampingFactorTooLarge() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, 1.5, 2, false)); + } + + /** + * Test exception for invalid iterations (less than 1) + */ + @Test + void testInvalidIterations() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, 0.85, 0, false)); + } + + /** + * Test exception when getting PageRank for invalid node + */ + @Test + void testGetPageRankInvalidNode() { + PageRank pageRank = new PageRank(2); + pageRank.calculatePageRank(2); + + assertThrows(IllegalArgumentException.class, () -> pageRank.getPageRank(3)); + } + + /** + * Test exception when getting PageRank for node less than 1 + */ + @Test + void testGetPageRankNodeLessThanOne() { + PageRank pageRank = new PageRank(2); + pageRank.calculatePageRank(2); + + assertThrows(IllegalArgumentException.class, () -> pageRank.getPageRank(0)); + } + + /** + * Test complex graph with multiple incoming and outgoing links + */ + @Test + void testComplexGraph() { + PageRank pageRank = new PageRank(4); + + // Create a more complex graph + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(1, 3, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 4, 1); + pageRank.setEdge(4, 1, 1); + + double[] result = pageRank.calculatePageRank(4); + + assertNotNull(result); + // Node 3 should have high PageRank (receives links from nodes 1 and 2) + // After 2 iterations, the sum will not equal total nodes + double sum = result[1] + result[2] + result[3] + result[4]; + assertEquals(1.8325, sum, EPSILON); + } + + /** + * Test that PageRank values sum after 2 iterations + */ + @Test + void testPageRankSum() { + PageRank pageRank = new PageRank(5); + + // Create arbitrary graph + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 4, 1); + pageRank.setEdge(4, 5, 1); + pageRank.setEdge(5, 1, 1); + + double[] result = pageRank.calculatePageRank(5); + + double sum = 0; + for (int i = 1; i <= 5; i++) { + sum += result[i]; + } + + // Sum after 2 iterations with default damping factor + assertEquals(2.11, sum, EPSILON); + } + + /** + * Test graph with isolated node (no incoming or outgoing links) + */ + @Test + void testGraphWithIsolatedNode() { + PageRank pageRank = new PageRank(3); + + // Node 1 and 2 are connected, Node 3 is isolated + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 1, 1); + + double[] result = pageRank.calculatePageRank(3); + + assertNotNull(result); + // Isolated node should have some PageRank due to damping factor + assertEquals(0.15, result[3], EPSILON); + } + + /** + * Test verbose mode (should not throw exception) + */ + @Test + void testVerboseMode() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + + // This should execute without throwing an exception + double[] result = pageRank.calculatePageRank(2, 0.85, 2, true); + + assertNotNull(result); + } +} diff --git a/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java new file mode 100644 index 000000000000..88c043ad9aa3 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/PerlinNoiseTest.java @@ -0,0 +1,103 @@ +package com.thealgorithms.others; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PerlinNoiseTest { + + @Test + @DisplayName("generatePerlinNoise returns array with correct dimensions") + void testDimensions() { + int w = 8; + int h = 6; + float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 4, 0.6f, 123L); + assertThat(noise).hasDimensions(w, h); + } + + @Test + @DisplayName("All values are within [0,1] after normalization") + void testRange() { + int w = 16; + int h = 16; + float[][] noise = PerlinNoise.generatePerlinNoise(w, h, 5, 0.7f, 42L); + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(noise[x][y]).isBetween(0f, 1f); + } + } + } + + @Test + @DisplayName("Deterministic for same parameters and seed") + void testDeterminism() { + int w = 10; + int h = 10; + long seed = 98765L; + float[][] a = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed); + float[][] b = PerlinNoise.generatePerlinNoise(w, h, 3, 0.5f, seed); + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(a[x][y]).isEqualTo(b[x][y]); + } + } + } + + @Test + @DisplayName("Different seeds produce different outputs (probabilistically)") + void testDifferentSeeds() { + int w = 12; + int h = 12; + float[][] a = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 1L); + float[][] b = PerlinNoise.generatePerlinNoise(w, h, 4, 0.8f, 2L); + + // Count exact equalities; expect very few or none. + int equalCount = 0; + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + if (Float.compare(a[x][y], b[x][y]) == 0) { + equalCount++; + } + } + } + assertThat(equalCount).isLessThan(w * h / 10); // less than 10% equal exact values + } + + @Test + @DisplayName("Interpolation endpoints are respected") + void testInterpolateEndpoints() { + assertThat(PerlinNoise.interpolate(0f, 1f, 0f)).isEqualTo(0f); + assertThat(PerlinNoise.interpolate(0f, 1f, 1f)).isEqualTo(1f); + assertThat(PerlinNoise.interpolate(0.2f, 0.8f, 0.5f)).isEqualTo(0.5f); + } + + @Test + @DisplayName("Single octave reduces to bilinear interpolation of base grid") + void testSingleOctaveLayer() { + int w = 8; + int h = 8; + long seed = 7L; + float[][] base = PerlinNoise.createBaseGrid(w, h, seed); + float[][] layer = PerlinNoise.generatePerlinNoiseLayer(base, w, h, 0); // period=1 + // With period = 1, x0=x, x1=(x+1)%w etc. Values should be smooth and within + // [0,1] + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + assertThat(layer[x][y]).isBetween(0f, 1f); + } + } + } + + @Test + @DisplayName("Invalid inputs are rejected") + void testInvalidInputs() { + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(0, 5, 1, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, -1, 1, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 0, 0.5f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 0f, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, Float.NaN, 1L)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PerlinNoise.generatePerlinNoise(5, 5, 1, 1.1f, 1L)).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/com/thealgorithms/others/TestPrintMatrixInSpiralOrder.java b/src/test/java/com/thealgorithms/others/TestPrintMatrixInSpiralOrder.java deleted file mode 100644 index d35d4bb60c73..000000000000 --- a/src/test/java/com/thealgorithms/others/TestPrintMatrixInSpiralOrder.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.thealgorithms.others; - -import static org.junit.jupiter.api.Assertions.assertIterableEquals; - -import java.util.List; -import org.junit.jupiter.api.Test; - -public class TestPrintMatrixInSpiralOrder { - @Test - public void testOne() { - int[][] matrix = {{3, 4, 5, 6, 7}, {8, 9, 10, 11, 12}, {14, 15, 16, 17, 18}, {23, 24, 25, 26, 27}, {30, 31, 32, 33, 34}}; - var printClass = new PrintAMatrixInSpiralOrder(); - List res = printClass.print(matrix, matrix.length, matrix[0].length); - List list = List.of(3, 4, 5, 6, 7, 12, 18, 27, 34, 33, 32, 31, 30, 23, 14, 8, 9, 10, 11, 17, 26, 25, 24, 15, 16); - assertIterableEquals(res, list); - } - - @Test - public void testTwo() { - int[][] matrix = {{2, 2}}; - var printClass = new PrintAMatrixInSpiralOrder(); - List res = printClass.print(matrix, matrix.length, matrix[0].length); - List list = List.of(2, 2); - assertIterableEquals(res, list); - } -} diff --git a/src/test/java/com/thealgorithms/physics/CoulombsLawTest.java b/src/test/java/com/thealgorithms/physics/CoulombsLawTest.java new file mode 100644 index 000000000000..9829e703bd10 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/CoulombsLawTest.java @@ -0,0 +1,100 @@ +package com.thealgorithms.physics; + +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.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the CoulombsLaw utility class. + */ +final class CoulombsLawTest { + + // A small tolerance (delta) for comparing floating-point numbers + private static final double DELTA = 1e-9; + private static final double K = CoulombsLaw.COULOMBS_CONSTANT; + + @Test + @DisplayName("Test repulsive force between two charges on the x-axis") + void testSimpleRepulsiveForce() { + // Two positive 1C charges, 1 meter apart. + // Force on q2 should be F = K*1*1 / 1^2 = K, directed away from q1 (positive x) + double[] forceOnB = CoulombsLaw.calculateForceVector(1.0, 0, 0, 1.0, 1, 0); + assertArrayEquals(new double[] {K, 0.0}, forceOnB, DELTA); + + // Force on q1 should be equal and opposite (negative x) + double[] forceOnA = CoulombsLaw.calculateForceVector(1.0, 1, 0, 1.0, 0, 0); + assertArrayEquals(new double[] {-K, 0.0}, forceOnA, DELTA); + } + + @Test + @DisplayName("Test attractive force between two charges on the x-axis") + void testSimpleAttractiveForce() { + // One positive 1C, one negative -1C, 1 meter apart. + // Force on q2 should be F = K*1*(-1) / 1^2 = -K, directed toward q1 (negative x) + double[] forceOnB = CoulombsLaw.calculateForceVector(1.0, 0, 0, -1.0, 1, 0); + assertArrayEquals(new double[] {-K, 0.0}, forceOnB, DELTA); + } + + @Test + @DisplayName("Test electrostatic force in a 2D plane (repulsive)") + void test2DRepulsiveForce() { + // q1 at (0,0) with charge +2C + // q2 at (3,4) with charge +1C + // Distance is 5 meters. + double magnitude = K * 2.0 * 1.0 / 25.0; // 2K/25 + // Unit vector from 1 to 2 is (3/5, 4/5) + double expectedFx = magnitude * (3.0 / 5.0); // 6K / 125 + double expectedFy = magnitude * (4.0 / 5.0); // 8K / 125 + + double[] forceOnB = CoulombsLaw.calculateForceVector(2.0, 0, 0, 1.0, 3, 4); + assertArrayEquals(new double[] {expectedFx, expectedFy}, forceOnB, DELTA); + } + + @Test + @DisplayName("Test overlapping charges should result in zero force") + void testOverlappingCharges() { + double[] force = CoulombsLaw.calculateForceVector(1.0, 1.5, -2.5, -1.0, 1.5, -2.5); + assertArrayEquals(new double[] {0.0, 0.0}, force, DELTA); + } + + @Test + @DisplayName("Test circular orbit velocity with simple values") + void testCircularOrbitVelocity() { + // v = sqrt( (K*1*1 / 1^2) * 1 / 1 ) = sqrt(K) + double velocity = CoulombsLaw.calculateCircularOrbitVelocity(1.0, 1.0, 1.0, 1.0); + assertEquals(Math.sqrt(K), velocity, DELTA); + } + + @Test + @DisplayName("Test orbital velocity for a Hydrogen atom (Bohr model)") + void testHydrogenAtomVelocity() { + // Charge of a proton + double protonCharge = 1.602176634e-19; + // Charge of an electron + double electronCharge = -1.602176634e-19; + // Mass of an electron + double electronMass = 9.1093837e-31; + // Bohr radius (avg distance) + double bohrRadius = 5.29177e-11; + + double expectedVelocity = 2.1876917e6; + + double velocity = CoulombsLaw.calculateCircularOrbitVelocity(protonCharge, electronCharge, electronMass, bohrRadius); + // Use a wider delta for this real-world calculation + assertEquals(expectedVelocity, velocity, 1.0); + } + + @Test + @DisplayName("Test invalid inputs for orbital velocity throw exception") + void testInvalidOrbitalVelocityInputs() { + // Non-positive mass + assertThrows(IllegalArgumentException.class, () -> CoulombsLaw.calculateCircularOrbitVelocity(1, 1, 0, 100)); + assertThrows(IllegalArgumentException.class, () -> CoulombsLaw.calculateCircularOrbitVelocity(1, 1, -1, 100)); + // Non-positive radius + assertThrows(IllegalArgumentException.class, () -> CoulombsLaw.calculateCircularOrbitVelocity(1, 1, 1, 0)); + assertThrows(IllegalArgumentException.class, () -> CoulombsLaw.calculateCircularOrbitVelocity(1, 1, 1, -100)); + } +} diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java new file mode 100644 index 000000000000..4b3e9fafe063 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -0,0 +1,143 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertAll; +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 {@link DampedOscillator}. + * + *

Tests focus on: + *

    + *
  • Constructor validation
  • + *
  • Analytical displacement for underdamped and overdamped parameterizations
  • + *
  • Basic numeric integration sanity using explicit Euler for small step sizes
  • + *
  • Method argument validation (null/invalid inputs)
  • + *
+ */ +@DisplayName("DampedOscillator — unit tests") +public class DampedOscillatorTest { + + private static final double TOLERANCE = 1e-3; + + @Test + @DisplayName("Constructor rejects invalid parameters") + void constructorValidation() { + assertAll("invalid-constructor-params", + () + -> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(0.0, 0.1), "omega0 == 0 should throw"), + () -> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(-1.0, 0.1), "negative omega0 should throw"), () -> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(1.0, -0.1), "negative gamma should throw")); + } + + @Test + @DisplayName("Analytical displacement matches expected formula for underdamped case") + void analyticalUnderdamped() { + double omega0 = 10.0; + double gamma = 0.5; + DampedOscillator d = new DampedOscillator(omega0, gamma); + + double a = 1.0; + double phi = 0.2; + double t = 0.123; + + // expected: a * exp(-gamma * t) * cos(omega_d * t + phi) + double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma)); + double expected = a * Math.exp(-gamma * t) * Math.cos(omegaD * t + phi); + + double actual = d.displacementAnalytical(a, phi, t); + assertEquals(expected, actual, 1e-12, "Analytical underdamped displacement should match closed-form value"); + } + + @Test + @DisplayName("Analytical displacement gracefully handles overdamped parameters (omegaD -> 0)") + void analyticalOverdamped() { + double omega0 = 1.0; + double gamma = 2.0; // gamma > omega0 => omega_d = 0 in our implementation (Math.max) + DampedOscillator d = new DampedOscillator(omega0, gamma); + + double a = 2.0; + double phi = Math.PI / 4.0; + double t = 0.5; + + // With omegaD forced to 0 by implementation, expected simplifies to: + double expected = a * Math.exp(-gamma * t) * Math.cos(phi); + double actual = d.displacementAnalytical(a, phi, t); + + assertEquals(expected, actual, 1e-12, "Overdamped handling should reduce to exponential * cos(phase)"); + } + + @Test + @DisplayName("Explicit Euler step approximates analytical solution for small dt over short time") + void eulerApproximatesAnalyticalSmallDt() { + double omega0 = 10.0; + double gamma = 0.5; + DampedOscillator d = new DampedOscillator(omega0, gamma); + + double a = 1.0; + double phi = 0.0; + + // initial conditions consistent with amplitude a and zero phase: + // x(0) = a, v(0) = -a * gamma * cos(phi) + a * omegaD * sin(phi) + double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma)); + double x0 = a * Math.cos(phi); + double v0 = -a * gamma * Math.cos(phi) - a * omegaD * Math.sin(phi); // small general form + + double dt = 1e-4; + int steps = 1000; // simulate to t = 0.1s + double tFinal = steps * dt; + + double[] state = new double[] {x0, v0}; + for (int i = 0; i < steps; i++) { + state = d.stepEuler(state, dt); + } + + double analyticAtT = d.displacementAnalytical(a, phi, tFinal); + double numericAtT = state[0]; + + // Euler is low-order — allow a small tolerance but assert it remains close for small dt + short time. + assertEquals(analyticAtT, numericAtT, TOLERANCE, String.format("Numeric Euler should approximate analytical solution at t=%.6f (tolerance=%g)", tFinal, TOLERANCE)); + } + + @Test + @DisplayName("stepEuler validates inputs and throws on null/invalid dt/state") + void eulerInputValidation() { + DampedOscillator d = new DampedOscillator(5.0, 0.1); + + assertAll("invalid-stepEuler-args", + () + -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(null, 0.01), "null state should throw"), + () + -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0}, 0.01), "state array with invalid length should throw"), + () -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0, 0.0}, 0.0), "non-positive dt should throw"), () -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0, 0.0}, -1e-3), "negative dt should throw")); + } + + @Test + @DisplayName("Getter methods return configured parameters") + void gettersReturnConfiguration() { + double omega0 = Math.PI; + double gamma = 0.01; + DampedOscillator d = new DampedOscillator(omega0, gamma); + + assertAll("getters", () -> assertEquals(omega0, d.getOmega0(), 0.0, "getOmega0 should return configured omega0"), () -> assertEquals(gamma, d.getGamma(), 0.0, "getGamma should return configured gamma")); + } + + @Test + @DisplayName("Analytical displacement at t=0 returns initial amplitude * cos(phase)") + void analyticalAtZeroTime() { + double omega0 = 5.0; + double gamma = 0.2; + DampedOscillator d = new DampedOscillator(omega0, gamma); + + double a = 2.0; + double phi = Math.PI / 3.0; + double t = 0.0; + + double expected = a * Math.cos(phi); + double actual = d.displacementAnalytical(a, phi, t); + + assertEquals(expected, actual, 1e-12, "Displacement at t=0 should be a * cos(phase)"); + } +} diff --git a/src/test/java/com/thealgorithms/physics/ElasticCollision2DTest.java b/src/test/java/com/thealgorithms/physics/ElasticCollision2DTest.java new file mode 100644 index 000000000000..480e5da23db1 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/ElasticCollision2DTest.java @@ -0,0 +1,91 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ElasticCollision2DTest { + + @Test + void testEqualMassHeadOnCollision() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, 1, 0, 1.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(1, 0, -1, 0, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + assertEquals(-1.0, a.vx, 1e-6); + assertEquals(0.0, a.vy, 1e-6); + assertEquals(1.0, b.vx, 1e-6); + assertEquals(0.0, b.vy, 1e-6); + } + + @Test + void testUnequalMassHeadOnCollision() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, 2, 0, 2.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(1, 0, -1, 0, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + // 1D head-on collision results + assertEquals(0.0, a.vx, 1e-6); + assertEquals(0.0, a.vy, 1e-6); + assertEquals(3.0, b.vx, 1e-6); + assertEquals(0.0, b.vy, 1e-6); + } + + @Test + void testMovingApartNoCollision() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, -1, 0, 1.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(1, 0, 1, 0, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + assertEquals(-1.0, a.vx, 1e-6); + assertEquals(0.0, a.vy, 1e-6); + assertEquals(1.0, b.vx, 1e-6); + assertEquals(0.0, b.vy, 1e-6); + } + + @Test + void testGlancingCollision() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, 1, 1, 1.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(1, 1, -1, -0.5, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + // Ensure relative velocity along normal is reversed + double nx = (b.x - a.x) / Math.hypot(b.x - a.x, b.y - a.y); + double ny = (b.y - a.y) / Math.hypot(b.x - a.x, b.y - a.y); + double relVelAfter = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny; + + assertTrue(relVelAfter > 0); + } + + @Test + void testOverlappingBodies() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, 1, 0, 1.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(0, 0, -1, 0, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + // Should not crash, velocities may remain unchanged + assertEquals(1.0, a.vx, 1e-6); + assertEquals(0.0, a.vy, 1e-6); + assertEquals(-1.0, b.vx, 1e-6); + assertEquals(0.0, b.vy, 1e-6); + } + + @Test + void testStationaryBodyHit() { + ElasticCollision2D.Body a = new ElasticCollision2D.Body(0, 0, 2, 0, 1.0, 0.5); + ElasticCollision2D.Body b = new ElasticCollision2D.Body(1, 0, 0, 0, 1.0, 0.5); + + ElasticCollision2D.resolveCollision(a, b); + + assertEquals(0.0, a.vx, 1e-6); + assertEquals(0.0, a.vy, 1e-6); + assertEquals(2.0, b.vx, 1e-6); + assertEquals(0.0, b.vy, 1e-6); + } +} diff --git a/src/test/java/com/thealgorithms/physics/GravitationTest.java b/src/test/java/com/thealgorithms/physics/GravitationTest.java new file mode 100644 index 000000000000..9094e289e79a --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/GravitationTest.java @@ -0,0 +1,83 @@ +package com.thealgorithms.physics; + +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.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Gravitation utility class. + */ +final class GravitationTest { + + // A small tolerance (delta) for comparing floating-point numbers + private static final double DELTA = 1e-9; + private static final double G = Gravitation.GRAVITATIONAL_CONSTANT; + + @Test + @DisplayName("Test gravitational force between two bodies on the x-axis") + void testSimpleForceCalculation() { + // Force on body 2 should be F = G*1*1 / 1^2 = G, directed towards body 1 (negative x) + double[] forceOnB = Gravitation.calculateGravitationalForce(1.0, 0, 0, 1.0, 1, 0); + assertArrayEquals(new double[] {-G, 0.0}, forceOnB, DELTA); + + // Force on body 1 should be equal and opposite (positive x) + double[] forceOnA = Gravitation.calculateGravitationalForce(1.0, 1, 0, 1.0, 0, 0); + assertArrayEquals(new double[] {G, 0.0}, forceOnA, DELTA); + } + + @Test + @DisplayName("Test gravitational force in a 2D plane") + void test2DForceCalculation() { + // Body 1 at (0,0) with mass 2kg + // Body 2 at (3,4) with mass 1kg + // Distance is sqrt(3^2 + 4^2) = 5 meters + double magnitude = 2.0 * G / 25.0; // G * 2 * 1 / 5^2 + // Unit vector from 2 to 1 is (-3/5, -4/5) + double expectedFx = magnitude * -3.0 / 5.0; // -6G / 125 + double expectedFy = magnitude * -4.0 / 5.0; // -8G / 125 + + double[] forceOnB = Gravitation.calculateGravitationalForce(2.0, 0, 0, 1.0, 3, 4); + assertArrayEquals(new double[] {expectedFx, expectedFy}, forceOnB, DELTA); + } + + @Test + @DisplayName("Test overlapping bodies should result in zero force") + void testOverlappingBodies() { + double[] force = Gravitation.calculateGravitationalForce(1000.0, 1.5, -2.5, 500.0, 1.5, -2.5); + assertArrayEquals(new double[] {0.0, 0.0}, force, DELTA); + } + + @Test + @DisplayName("Test circular orbit velocity with simple values") + void testCircularOrbitVelocity() { + // v = sqrt(G*1/1) = sqrt(G) + double velocity = Gravitation.calculateCircularOrbitVelocity(1.0, 1.0); + assertEquals(Math.sqrt(G), velocity, DELTA); + } + + @Test + @DisplayName("Test orbital velocity with real-world-ish values (LEO)") + void testEarthOrbitVelocity() { + // Mass of Earth ~5.972e24 kg + // Radius of LEO ~6,771,000 m (Earth radius + 400km) + double earthMass = 5.972e24; + double leoRadius = 6.771e6; + // FIX: Updated expected value to match the high-precision calculation + double expectedVelocity = 7672.4904; + + double velocity = Gravitation.calculateCircularOrbitVelocity(earthMass, leoRadius); + assertEquals(expectedVelocity, velocity, 0.0001); // Use a larger delta for big numbers + } + + @Test + @DisplayName("Test invalid inputs for orbital velocity throw exception") + void testInvalidOrbitalVelocityInputs() { + assertThrows(IllegalArgumentException.class, () -> Gravitation.calculateCircularOrbitVelocity(0, 100)); + assertThrows(IllegalArgumentException.class, () -> Gravitation.calculateCircularOrbitVelocity(-1000, 100)); + assertThrows(IllegalArgumentException.class, () -> Gravitation.calculateCircularOrbitVelocity(1000, 0)); + assertThrows(IllegalArgumentException.class, () -> Gravitation.calculateCircularOrbitVelocity(1000, -100)); + } +} diff --git a/src/test/java/com/thealgorithms/physics/GroundToGroundProjectileMotionTest.java b/src/test/java/com/thealgorithms/physics/GroundToGroundProjectileMotionTest.java new file mode 100644 index 000000000000..9d7e11777983 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/GroundToGroundProjectileMotionTest.java @@ -0,0 +1,149 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * JUnit test class for GroundToGroundProjectileMotion + * + * Contains unit tests for projectile motion calculations using JUnit 5 + */ +public class GroundToGroundProjectileMotionTest { + + private static final double EPSILON = 0.001; // Tolerance for floating point comparison + + @Test + @DisplayName("Test time of flight calculation") + public void testTimeOfFlight() { + // Arrange + double initialVelocity = 5.0; + double angle = 40.0; + double expectedTimeOfFlight = 0.655; + + // Act + double flightTimeOutput = GroundToGroundProjectileMotion.timeOfFlight(initialVelocity, angle); + flightTimeOutput = Math.round(flightTimeOutput * 1000.0) / 1000.0; + + // Assert + assertEquals(expectedTimeOfFlight, flightTimeOutput, EPSILON, "Time of flight should be " + expectedTimeOfFlight + " seconds"); + + System.out.println("Projectile Flight Time Test"); + System.out.println("Input Initial Velocity: " + initialVelocity + " m/s"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Expected Output: " + expectedTimeOfFlight + " seconds"); + System.out.println("Actual Output: " + flightTimeOutput + " seconds"); + System.out.println("TEST PASSED\n"); + } + + @Test + @DisplayName("Test horizontal range calculation") + public void testHorizontalRange() { + // Arrange + double initialVelocity = 5.0; + double angle = 40.0; + double flightTime = 0.655; + double expectedHorizontalRange = 2.51; + + // Act + double horizontalRangeOutput = GroundToGroundProjectileMotion.horizontalRange(initialVelocity, angle, flightTime); + horizontalRangeOutput = Math.round(horizontalRangeOutput * 100.0) / 100.0; + + // Assert + assertEquals(expectedHorizontalRange, horizontalRangeOutput, EPSILON, "Horizontal range should be " + expectedHorizontalRange + " meters"); + + System.out.println("Projectile Horizontal Range Test"); + System.out.println("Input Initial Velocity: " + initialVelocity + " m/s"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Input Time Of Flight: " + flightTime + " seconds"); + System.out.println("Expected Output: " + expectedHorizontalRange + " meters"); + System.out.println("Actual Output: " + horizontalRangeOutput + " meters"); + System.out.println("TEST PASSED\n"); + } + + @Test + @DisplayName("Test max height calculation") + public void testMaxHeight() { + // Arrange + double initialVelocity = 5.0; + double angle = 40.0; + double expectedMaxHeight = 0.527; // Updated to match actual calculation + + // Act + double maxHeightOutput = GroundToGroundProjectileMotion.maxHeight(initialVelocity, angle); + maxHeightOutput = Math.round(maxHeightOutput * 1000.0) / 1000.0; + + // Assert + assertEquals(expectedMaxHeight, maxHeightOutput, EPSILON, "Max height should be " + expectedMaxHeight + " meters"); + + System.out.println("Projectile Max Height Test"); + System.out.println("Input Initial Velocity: " + initialVelocity + " m/s"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Expected Output: " + expectedMaxHeight + " meters"); + System.out.println("Actual Output: " + maxHeightOutput + " meters"); + System.out.println("TEST PASSED\n"); + } + + @Test + @DisplayName("Test time of flight with custom gravity") + public void testTimeOfFlightWithCustomGravity() { + // Arrange + double initialVelocity = 10.0; + double angle = 45.0; + double customGravity = 1.62; // Moon gravity (m/s^2) + + // Act + double flightTime = GroundToGroundProjectileMotion.timeOfFlight(initialVelocity, angle, customGravity); + + // Assert + assertTrue(flightTime > 0, "Flight time should be positive"); + assertTrue(flightTime > 8.0, "Flight time on moon should be longer than on Earth"); + + System.out.println("Custom Gravity Test (Moon)"); + System.out.println("Input Initial Velocity: " + initialVelocity + " m/s"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Gravity: " + customGravity + " m/s^2"); + System.out.println("Flight Time: " + flightTime + " seconds"); + System.out.println("TEST PASSED\n"); + } + + @Test + @DisplayName("Test projectile at 90 degrees (straight up)") + public void testVerticalProjectile() { + // Arrange + double initialVelocity = 20.0; + double angle = 90.0; + + // Act + double horizontalRange = GroundToGroundProjectileMotion.horizontalRange(initialVelocity, angle, 1.0); + + // Assert + assertEquals(0.0, horizontalRange, EPSILON, "Horizontal range should be zero for vertical launch"); + + System.out.println("Vertical Projectile Test"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Horizontal Range: " + horizontalRange + " meters"); + System.out.println("TEST PASSED\n"); + } + + @Test + @DisplayName("Test projectile at 0 degrees (horizontal)") + public void testHorizontalProjectile() { + // Arrange + double initialVelocity = 15.0; + double angle = 0.0; + + // Act + double maxHeight = GroundToGroundProjectileMotion.maxHeight(initialVelocity, angle); + + // Assert + assertEquals(0.0, maxHeight, EPSILON, "Max height should be zero for horizontal launch"); + + System.out.println("Horizontal Projectile Test"); + System.out.println("Input Angle: " + angle + " degrees"); + System.out.println("Max Height: " + maxHeight + " meters"); + System.out.println("TEST PASSED\n"); + } +} diff --git a/src/test/java/com/thealgorithms/physics/KinematicsTest.java b/src/test/java/com/thealgorithms/physics/KinematicsTest.java new file mode 100644 index 000000000000..c5274b0814a7 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/KinematicsTest.java @@ -0,0 +1,48 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Kinematics utility class. + */ + +public final class KinematicsTest { + // A small tolerance for comparing floating-point numbers + private static final double DELTA = 1e-9; + @Test + @DisplayName("Test final velocity: v = u + at") + void testCalculateFinalVelocity() { + assertEquals(20.0, Kinematics.calculateFinalVelocity(10.0, 2.0, 5.0), DELTA); + } + + @Test + @DisplayName("Test displacement: s = ut + 0.5at^2") + void testCalculateDisplacement() { + assertEquals(75.0, Kinematics.calculateDisplacement(10.0, 2.0, 5.0), DELTA); + } + + @Test + @DisplayName("Test final velocity squared: v^2 = u^2 + 2as") + void testCalculateFinalVelocitySquared() { + assertEquals(400.0, Kinematics.calculateFinalVelocitySquared(10.0, 2.0, 75.0), DELTA); + } + + @Test + @DisplayName("Test displacement from average velocity: s = (u+v)/2 * t") + void testCalculateDisplacementFromVelocities() { + assertEquals(75.0, Kinematics.calculateDisplacementFromVelocities(10.0, 20.0, 5.0), DELTA); + } + + @Test + @DisplayName("Test with negative acceleration (deceleration)") + void testDeceleration() { + assertEquals(10.0, Kinematics.calculateFinalVelocity(30.0, -4.0, 5.0), DELTA); + assertEquals(100.0, Kinematics.calculateDisplacement(30.0, -4.0, 5.0), DELTA); + + assertEquals(100.0, Kinematics.calculateFinalVelocitySquared(30.0, -4.0, 100.0), DELTA); + assertEquals(100.0, Kinematics.calculateDisplacementFromVelocities(30.0, 10.0, 5.0), DELTA); + } +} diff --git a/src/test/java/com/thealgorithms/physics/ProjectileMotionTest.java b/src/test/java/com/thealgorithms/physics/ProjectileMotionTest.java new file mode 100644 index 000000000000..25e7cfa87f9e --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/ProjectileMotionTest.java @@ -0,0 +1,69 @@ +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; + +/** + * Test class for the general-purpose ProjectileMotion calculator. + * + */ +final class ProjectileMotionTest { + + private static final double DELTA = 1e-4; // Tolerance for comparing double values + + @Test + @DisplayName("Test ground-to-ground launch (initial height is zero)") + void testGroundToGroundLaunch() { + ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(50, 30, 0); + assertEquals(5.0986, result.getTimeOfFlight(), DELTA); + assertEquals(220.7750, result.getHorizontalRange(), DELTA); + assertEquals(31.8661, result.getMaxHeight(), DELTA); + } + + @Test + @DisplayName("Test launch from an elevated position") + void testElevatedLaunch() { + ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(30, 45, 100); + assertEquals(7.1705, result.getTimeOfFlight(), DELTA); + assertEquals(152.1091, result.getHorizontalRange(), DELTA); + assertEquals(122.9436, result.getMaxHeight(), DELTA); // Final corrected value + } + + @Test + @DisplayName("Test launch straight up (90 degrees)") + void testVerticalLaunch() { + ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(40, 90, 20); + assertEquals(8.6303, result.getTimeOfFlight(), DELTA); + assertEquals(0.0, result.getHorizontalRange(), DELTA); + assertEquals(101.5773, result.getMaxHeight(), DELTA); + } + + @Test + @DisplayName("Test horizontal launch from a height (0 degrees)") + void testHorizontalLaunch() { + ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(25, 0, 80); + assertEquals(4.0392, result.getTimeOfFlight(), DELTA); + assertEquals(100.9809, result.getHorizontalRange(), DELTA); + assertEquals(80.0, result.getMaxHeight(), DELTA); + } + + @Test + @DisplayName("Test downward launch from a height (negative angle)") + void testDownwardLaunchFromHeight() { + ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(20, -30, 100); + assertEquals(3.6100, result.getTimeOfFlight(), DELTA); + assertEquals(62.5268, result.getHorizontalRange(), DELTA); + assertEquals(100.0, result.getMaxHeight(), DELTA); + } + + @Test + @DisplayName("Test invalid arguments throw an exception") + void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(-10, 45, 100)); + assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(10, 45, -100)); + assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(10, 45, 100, 0)); + } +} diff --git a/src/test/java/com/thealgorithms/physics/SimplePendulumRK4Test.java b/src/test/java/com/thealgorithms/physics/SimplePendulumRK4Test.java new file mode 100644 index 000000000000..bec4ad8cbbd7 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/SimplePendulumRK4Test.java @@ -0,0 +1,261 @@ +package com.thealgorithms.physics; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Test class for SimplePendulumRK4. + * Tests numerical accuracy, physical correctness, and edge cases. + */ +class SimplePendulumRK4Test { + + private static final double EPSILON = 1e-6; + private static final double ENERGY_DRIFT_TOLERANCE = 1e-3; + @Test + @DisplayName("Test constructor creates valid pendulum") + void testConstructor() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.5, 9.81); + Assertions.assertNotNull(pendulum); + Assertions.assertEquals(1.5, pendulum.getLength(), EPSILON); + Assertions.assertEquals(9.81, pendulum.getGravity(), EPSILON); + } + + @Test + @DisplayName("Test constructor rejects negative length") + void testConstructorNegativeLength() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(-1.0, 9.81); }); + } + + @Test + @DisplayName("Test constructor rejects negative gravity") + void testConstructorNegativeGravity() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(1.0, -9.81); }); + } + + @Test + @DisplayName("Test constructor rejects zero length") + void testConstructorZeroLength() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(0.0, 9.81); }); + } + + @Test + @DisplayName("Test getters return correct values") + void testGetters() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(2.5, 10.0); + Assertions.assertEquals(2.5, pendulum.getLength(), EPSILON); + Assertions.assertEquals(10.0, pendulum.getGravity(), EPSILON); + } + + @Test + @DisplayName("Test single RK4 step returns valid state") + void testSingleStep() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {0.1, 0.0}; + double[] newState = pendulum.stepRK4(state, 0.01); + + Assertions.assertNotNull(newState); + Assertions.assertEquals(2, newState.length); + } + + @Test + @DisplayName("Test equilibrium stability (pendulum at rest stays at rest)") + void testEquilibrium() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {0.0, 0.0}; + + for (int i = 0; i < 100; i++) { + state = pendulum.stepRK4(state, 0.01); + } + + Assertions.assertEquals(0.0, state[0], EPSILON, "Theta should remain at equilibrium"); + Assertions.assertEquals(0.0, state[1], EPSILON, "Omega should remain zero"); + } + + @Test + @DisplayName("Test small angle oscillation returns to initial position") + void testSmallAngleOscillation() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double initialAngle = Math.toRadians(5.0); + double[] state = {initialAngle, 0.0}; + double dt = 0.01; + + // Theoretical period for small angles + double expectedPeriod = 2 * Math.PI * Math.sqrt(1.0 / 9.81); + int stepsPerPeriod = (int) (expectedPeriod / dt); + + double[][] trajectory = pendulum.simulate(state, dt, stepsPerPeriod); + double finalTheta = trajectory[stepsPerPeriod][0]; + + // After one period, should return close to initial position + double error = Math.abs(finalTheta - initialAngle) / Math.abs(initialAngle); + Assertions.assertTrue(error < 0.05, "Small angle approximation error should be < 5%"); + } + + @Test + @DisplayName("Test large angle oscillation is symmetric") + void testLargeAngleOscillation() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.toRadians(120.0), 0.0}; + + double[][] trajectory = pendulum.simulate(state, 0.01, 500); + + double maxTheta = Double.NEGATIVE_INFINITY; + double minTheta = Double.POSITIVE_INFINITY; + + for (double[] s : trajectory) { + maxTheta = Math.max(maxTheta, s[0]); + minTheta = Math.min(minTheta, s[0]); + } + + Assertions.assertTrue(maxTheta > 0, "Should have positive excursions"); + Assertions.assertTrue(minTheta < 0, "Should have negative excursions"); + + // Check symmetry + double asymmetry = Math.abs((maxTheta + minTheta) / maxTheta); + Assertions.assertTrue(asymmetry < 0.1, "Oscillation should be symmetric"); + } + + @Test + @DisplayName("Test energy conservation for small angle") + void testEnergyConservationSmallAngle() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.toRadians(15.0), 0.0}; + + double initialEnergy = pendulum.calculateEnergy(state); + + for (int i = 0; i < 1000; i++) { + state = pendulum.stepRK4(state, 0.01); + } + + double finalEnergy = pendulum.calculateEnergy(state); + double drift = Math.abs(finalEnergy - initialEnergy) / initialEnergy; + + Assertions.assertTrue(drift < ENERGY_DRIFT_TOLERANCE, "Energy drift should be < 0.1%, got: " + (drift * 100) + "%"); + } + + @Test + @DisplayName("Test energy conservation for large angle") + void testEnergyConservationLargeAngle() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.toRadians(90.0), 0.0}; + + double initialEnergy = pendulum.calculateEnergy(state); + + for (int i = 0; i < 1000; i++) { + state = pendulum.stepRK4(state, 0.01); + } + + double finalEnergy = pendulum.calculateEnergy(state); + double drift = Math.abs(finalEnergy - initialEnergy) / initialEnergy; + + Assertions.assertTrue(drift < ENERGY_DRIFT_TOLERANCE, "Energy drift should be < 0.1%, got: " + (drift * 100) + "%"); + } + + @Test + @DisplayName("Test simulate method returns correct trajectory") + void testSimulate() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] initialState = {Math.toRadians(20.0), 0.0}; + int steps = 100; + + double[][] trajectory = pendulum.simulate(initialState, 0.01, steps); + + Assertions.assertEquals(steps + 1, trajectory.length, "Trajectory should have steps + 1 entries"); + Assertions.assertArrayEquals(initialState, trajectory[0], EPSILON, "First entry should match initial state"); + + // Verify state changes over time + boolean changed = false; + for (int i = 1; i <= steps; i++) { + if (Math.abs(trajectory[i][0] - initialState[0]) > EPSILON) { + changed = true; + break; + } + } + Assertions.assertTrue(changed, "Simulation should progress from initial state"); + } + + @Test + @DisplayName("Test energy calculation at equilibrium") + void testEnergyAtEquilibrium() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {0.0, 0.0}; + double energy = pendulum.calculateEnergy(state); + Assertions.assertEquals(0.0, energy, EPSILON, "Energy at equilibrium should be zero"); + } + + @Test + @DisplayName("Test energy calculation at maximum angle") + void testEnergyAtMaxAngle() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.PI / 2, 0.0}; + double energy = pendulum.calculateEnergy(state); + Assertions.assertTrue(energy > 0, "Energy should be positive at max angle"); + } + + @Test + @DisplayName("Test energy calculation with angular velocity") + void testEnergyWithVelocity() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {0.0, 1.0}; + double energy = pendulum.calculateEnergy(state); + Assertions.assertTrue(energy > 0, "Energy should be positive with velocity"); + } + + @Test + @DisplayName("Test stepRK4 rejects null state") + void testStepRejectsNullState() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(null, 0.01); }); + } + + @Test + @DisplayName("Test stepRK4 rejects invalid state length") + void testStepRejectsInvalidStateLength() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(new double[] {0.1}, 0.01); }); + } + + @Test + @DisplayName("Test stepRK4 rejects negative time step") + void testStepRejectsNegativeTimeStep() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(new double[] {0.1, 0.2}, -0.01); }); + } + + @Test + @DisplayName("Test extreme condition: very large angle") + void testExtremeLargeAngle() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.toRadians(179.0), 0.0}; + double[] result = pendulum.stepRK4(state, 0.01); + + Assertions.assertNotNull(result); + Assertions.assertTrue(Double.isFinite(result[0]), "Should handle large angles without NaN"); + Assertions.assertTrue(Double.isFinite(result[1]), "Should handle large angles without NaN"); + } + + @Test + @DisplayName("Test extreme condition: high angular velocity") + void testExtremeHighVelocity() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {0.0, 10.0}; + double[] result = pendulum.stepRK4(state, 0.01); + + Assertions.assertNotNull(result); + Assertions.assertTrue(Double.isFinite(result[0]), "Should handle high velocity without NaN"); + Assertions.assertTrue(Double.isFinite(result[1]), "Should handle high velocity without NaN"); + } + + @Test + @DisplayName("Test extreme condition: very small time step") + void testExtremeSmallTimeStep() { + SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81); + double[] state = {Math.toRadians(10.0), 0.0}; + double[] result = pendulum.stepRK4(state, 1e-6); + + Assertions.assertNotNull(result); + Assertions.assertTrue(Double.isFinite(result[0]), "Should handle small time steps without NaN"); + Assertions.assertTrue(Double.isFinite(result[1]), "Should handle small time steps without NaN"); + } +} 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/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/recursion/DiceThrowerTest.java b/src/test/java/com/thealgorithms/recursion/DiceThrowerTest.java new file mode 100644 index 000000000000..0804e9bfff25 --- /dev/null +++ b/src/test/java/com/thealgorithms/recursion/DiceThrowerTest.java @@ -0,0 +1,223 @@ +package com.thealgorithms.recursion; + +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.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Test class for DiceThrower + * + * @author BEASTSHRIRAM + */ +class DiceThrowerTest { + + @Test + void testTargetZero() { + List result = DiceThrower.getDiceCombinations(0); + assertEquals(1, result.size()); + assertEquals("", result.get(0)); + } + + @Test + void testTargetOne() { + List result = DiceThrower.getDiceCombinations(1); + assertEquals(1, result.size()); + assertEquals("1", result.get(0)); + } + + @Test + void testTargetTwo() { + List result = DiceThrower.getDiceCombinations(2); + assertEquals(2, result.size()); + assertTrue(result.contains("11")); + assertTrue(result.contains("2")); + } + + @Test + void testTargetThree() { + List result = DiceThrower.getDiceCombinations(3); + assertEquals(4, result.size()); + assertTrue(result.contains("111")); + assertTrue(result.contains("12")); + assertTrue(result.contains("21")); + assertTrue(result.contains("3")); + } + + @Test + void testTargetFour() { + List result = DiceThrower.getDiceCombinations(4); + assertEquals(8, result.size()); + assertTrue(result.contains("1111")); + assertTrue(result.contains("112")); + assertTrue(result.contains("121")); + assertTrue(result.contains("13")); + assertTrue(result.contains("211")); + assertTrue(result.contains("22")); + assertTrue(result.contains("31")); + assertTrue(result.contains("4")); + } + + @Test + void testTargetSix() { + List result = DiceThrower.getDiceCombinations(6); + assertEquals(32, result.size()); + assertTrue(result.contains("6")); + assertTrue(result.contains("33")); + assertTrue(result.contains("222")); + assertTrue(result.contains("111111")); + } + + @Test + void testTargetSeven() { + List result = DiceThrower.getDiceCombinations(7); + // Should include combinations like 61, 52, 43, 331, 322, 2221, etc. + assertTrue(result.size() > 0); + assertTrue(result.contains("61")); + assertTrue(result.contains("16")); + assertTrue(result.contains("52")); + assertTrue(result.contains("43")); + } + + @Test + void testLargerTarget() { + List result = DiceThrower.getDiceCombinations(10); + assertTrue(result.size() > 0); + // All results should sum to 10 + for (String combination : result) { + int sum = 0; + for (char c : combination.toCharArray()) { + sum += Character.getNumericValue(c); + } + assertEquals(10, sum); + } + } + + @Test + void testNegativeTarget() { + assertThrows(IllegalArgumentException.class, () -> { DiceThrower.getDiceCombinations(-1); }); + } + + @Test + void testNegativeTargetPrint() { + assertThrows(IllegalArgumentException.class, () -> { DiceThrower.printDiceCombinations(-1); }); + } + + @Test + void testAllCombinationsValid() { + List result = DiceThrower.getDiceCombinations(5); + + for (String combination : result) { + // Check that each character is a valid dice face (1-6) + for (char c : combination.toCharArray()) { + int face = Character.getNumericValue(c); + assertTrue(face >= 1 && face <= 6, "Invalid dice face: " + face); + } + + // Check that the sum equals the target + int sum = 0; + for (char c : combination.toCharArray()) { + sum += Character.getNumericValue(c); + } + assertEquals(5, sum, "Combination " + combination + " does not sum to 5"); + } + } + + @Test + void testPrintDiceCombinations() { + // Capture System.out + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outputStream)); + + try { + // Test printing combinations for target 3 + DiceThrower.printDiceCombinations(3); + String output = outputStream.toString(); + + // Verify all expected combinations are printed + assertTrue(output.contains("111")); + assertTrue(output.contains("12")); + assertTrue(output.contains("21")); + assertTrue(output.contains("3")); + + // Count number of lines (combinations) + String[] lines = output.trim().split("\n"); + assertEquals(4, lines.length); + } finally { + // Restore System.out + System.setOut(originalOut); + } + } + + @Test + void testPrintDiceCombinationsZero() { + // Capture System.out + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outputStream)); + + try { + DiceThrower.printDiceCombinations(0); + String output = outputStream.toString(); + + // Should print empty string (one line) + assertEquals("", output.trim()); + } finally { + System.setOut(originalOut); + } + } + + @Test + void testMainMethod() { + // Capture System.out + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outputStream)); + + try { + // Test main method + DiceThrower.main(new String[] {}); + String output = outputStream.toString(); + + // Verify expected output contains header and combinations + assertTrue(output.contains("All dice combinations that sum to 4:")); + assertTrue(output.contains("Total combinations: 8")); + assertTrue(output.contains("1111")); + assertTrue(output.contains("22")); + assertTrue(output.contains("4")); + } finally { + System.setOut(originalOut); + } + } + + @Test + void testEdgeCaseTargetFive() { + List result = DiceThrower.getDiceCombinations(5); + assertEquals(16, result.size()); + + // Test specific combinations exist + assertTrue(result.contains("11111")); + assertTrue(result.contains("1112")); + assertTrue(result.contains("122")); + assertTrue(result.contains("14")); + assertTrue(result.contains("23")); + assertTrue(result.contains("5")); + } + + @Test + void testTargetGreaterThanSix() { + List result = DiceThrower.getDiceCombinations(8); + assertTrue(result.size() > 0); + + // Verify some expected combinations + assertTrue(result.contains("62")); + assertTrue(result.contains("53")); + assertTrue(result.contains("44")); + assertTrue(result.contains("2222")); + } +} 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/recursion/SylvesterSequenceTest.java b/src/test/java/com/thealgorithms/recursion/SylvesterSequenceTest.java new file mode 100644 index 000000000000..3ea5641ac484 --- /dev/null +++ b/src/test/java/com/thealgorithms/recursion/SylvesterSequenceTest.java @@ -0,0 +1,51 @@ +package com.thealgorithms.recursion; + +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.math.BigInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SylvesterSequenceTest { + + /** + * Provides test cases for valid Sylvester sequence numbers. + * Format: { n, expectedValue } + */ + static Stream validSylvesterNumbers() { + return Stream.of(new Object[] {1, BigInteger.valueOf(2)}, new Object[] {2, BigInteger.valueOf(3)}, new Object[] {3, BigInteger.valueOf(7)}, new Object[] {4, BigInteger.valueOf(43)}, new Object[] {5, BigInteger.valueOf(1807)}, new Object[] {6, new BigInteger("3263443")}, + new Object[] {7, new BigInteger("10650056950807")}, new Object[] {8, new BigInteger("113423713055421844361000443")}); + } + + @ParameterizedTest + @MethodSource("validSylvesterNumbers") + void testSylvesterValidNumbers(int n, BigInteger expected) { + assertEquals(expected, SylvesterSequence.sylvester(n), "Sylvester sequence value mismatch for n=" + n); + } + + /** + * Test edge case for n <= 0 which should throw IllegalArgumentException + */ + @ParameterizedTest + @ValueSource(ints = {0, -1, -10, -100}) + void testSylvesterInvalidZero(int n) { + assertThrows(IllegalArgumentException.class, () -> SylvesterSequence.sylvester(n)); + } + + /** + * Test a larger number to ensure no overflow occurs. + */ + @Test + void testSylvesterLargeNumber() { + int n = 10; + BigInteger result = SylvesterSequence.sylvester(n); + assertNotNull(result); + assertTrue(result.compareTo(BigInteger.ZERO) > 0, "Result should be positive"); + } +} diff --git a/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java b/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java new file mode 100644 index 000000000000..0da8e1c83997 --- /dev/null +++ b/src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java @@ -0,0 +1,225 @@ +package com.thealgorithms.searches; + +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; + +/** + * Unit tests for the SentinelLinearSearch class. + */ +class SentinelLinearSearchTest { + + /** + * Test for finding an element present in the array. + */ + @Test + void testSentinelLinearSearchFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + Integer key = 5; // Element to find + assertEquals(5, sentinelLinearSearch.find(array, key), "The index of the found element should be 5."); + } + + /** + * Test for finding the first element in the array. + */ + @Test + void testSentinelLinearSearchFirstElement() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + Integer key = 0; // First element + assertEquals(0, sentinelLinearSearch.find(array, key), "The index of the first element should be 0."); + } + + /** + * Test for finding the last element in the array. + */ + @Test + void testSentinelLinearSearchLastElement() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + Integer key = 10; // Last element + assertEquals(10, sentinelLinearSearch.find(array, key), "The index of the last element should be 10."); + } + + /** + * Test for finding an element not present in the array. + */ + @Test + void testSentinelLinearSearchNotFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + Integer key = -1; // Element not in the array + assertEquals(-1, sentinelLinearSearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for finding an element in an empty array. + */ + @Test + void testSentinelLinearSearchEmptyArray() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {}; // Empty array + Integer key = 1; // Key not present + assertEquals(-1, sentinelLinearSearch.find(array, key), "The element should not be found in an empty array."); + } + + /** + * Test for finding an element in a single-element array when present. + */ + @Test + void testSentinelLinearSearchSingleElementFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {42}; // Single element array + Integer key = 42; // Element present + assertEquals(0, sentinelLinearSearch.find(array, key), "The element should be found at index 0."); + } + + /** + * Test for finding an element in a single-element array when not present. + */ + @Test + void testSentinelLinearSearchSingleElementNotFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {42}; // Single element array + Integer key = 24; // Element not present + assertEquals(-1, sentinelLinearSearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for finding multiple occurrences of the same element in the array. + */ + @Test + void testSentinelLinearSearchMultipleOccurrences() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, 2, 3, 4, 5, 3, 6, 7, 3}; // 3 occurs multiple times + Integer key = 3; // Key to find + assertEquals(2, sentinelLinearSearch.find(array, key), "The index of the first occurrence of the element should be 2."); + } + + /** + * Test for finding an element in a large array. + */ + @Test + void testSentinelLinearSearchLargeArray() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = new Integer[1000]; + for (int i = 0; i < array.length; i++) { + array[i] = i; // Fill the array with integers 0 to 999 + } + Integer key = 256; // Present in the array + assertEquals(256, sentinelLinearSearch.find(array, key), "The index of the found element should be 256."); + } + + /** + * Test for finding an element in a large array when it is not present. + */ + @Test + void testSentinelLinearSearchLargeArrayNotFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = new Integer[1000]; + for (int i = 0; i < array.length; i++) { + array[i] = i; // Fill the array with integers 0 to 999 + } + Integer key = 1001; // Key not present + assertEquals(-1, sentinelLinearSearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for performance with random large array. + */ + @Test + void testSentinelLinearSearchRandomArray() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Random random = new Random(); + Integer[] array = random.ints(0, 1000).distinct().limit(1000).boxed().toArray(Integer[] ::new); + Integer key = array[random.nextInt(array.length)]; // Key should be in the array + assertEquals(java.util.Arrays.asList(array).indexOf(key), sentinelLinearSearch.find(array, key), "The index of the found element should match."); + } + + /** + * Test for handling null array. + */ + @Test + void testSentinelLinearSearchNullArray() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = null; // Null array + Integer key = 1; // Any key + assertThrows(IllegalArgumentException.class, () -> sentinelLinearSearch.find(array, key), "Should throw IllegalArgumentException for null array."); + } + + /** + * Test for handling null key in array with null elements. + */ + @Test + void testSentinelLinearSearchNullKey() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, null, 3, 4, null}; // Array with null elements + Integer key = null; // Null key + assertEquals(1, sentinelLinearSearch.find(array, key), "The index of the first null element should be 1."); + } + + /** + * Test for handling null key when not present in array. + */ + @Test + void testSentinelLinearSearchNullKeyNotFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, 2, 3, 4, 5}; // Array without null elements + Integer key = null; // Null key + assertEquals(-1, sentinelLinearSearch.find(array, key), "Null key should not be found in array without null elements."); + } + + /** + * Test with String array to verify generic functionality. + */ + @Test + void testSentinelLinearSearchStringArray() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + String[] array = {"apple", "banana", "cherry", "date", "elderberry"}; + String key = "cherry"; // Element to find + assertEquals(2, sentinelLinearSearch.find(array, key), "The index of 'cherry' should be 2."); + } + + /** + * Test with String array when element not found. + */ + @Test + void testSentinelLinearSearchStringArrayNotFound() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + String[] array = {"apple", "banana", "cherry", "date", "elderberry"}; + String key = "grape"; // Element not in array + assertEquals(-1, sentinelLinearSearch.find(array, key), "The element 'grape' should not be found in the array."); + } + + /** + * Test that the original array is not modified after search. + */ + @Test + void testSentinelLinearSearchArrayIntegrity() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, 2, 3, 4, 5}; + Integer[] originalArray = array.clone(); // Keep a copy of the original + Integer key = 3; // Element to find + + sentinelLinearSearch.find(array, key); + + // Verify array is unchanged + for (int i = 0; i < array.length; i++) { + assertEquals(originalArray[i], array[i], "Array should remain unchanged after search."); + } + } + + /** + * Test edge case where the key is the same as the last element. + */ + @Test + void testSentinelLinearSearchKeyEqualsLastElement() { + SentinelLinearSearch sentinelLinearSearch = new SentinelLinearSearch(); + Integer[] array = {1, 2, 3, 4, 5, 3}; // Last element is 3, and 3 also appears earlier + Integer key = 3; // Key equals last element + assertEquals(2, sentinelLinearSearch.find(array, key), "Should find the first occurrence at index 2, not the last."); + } +} diff --git a/src/test/java/com/thealgorithms/slidingwindow/MinimumWindowSubstringTest.java b/src/test/java/com/thealgorithms/slidingwindow/MinimumWindowSubstringTest.java new file mode 100644 index 000000000000..2c1534f4bfe6 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/MinimumWindowSubstringTest.java @@ -0,0 +1,35 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Finds the minimum window substring in {@code s} that contains all characters of {@code t}. + * + * @param s The input string to search within + * @param t The string with required characters + * @return The minimum window substring, or empty string if not found + * @author (https://github.com/Chiefpatwal) + */ +public class MinimumWindowSubstringTest { + + /** + * Tests for MinimumWindowSubstring.minWindow. + */ + @Test + public void testMinimumWindowSubstring() { + assertEquals("BANC", MinimumWindowSubstring.minWindow("ADOBECODEBANC", "ABC")); + assertEquals("a", MinimumWindowSubstring.minWindow("a", "a")); + assertEquals("", MinimumWindowSubstring.minWindow("a", "aa")); + assertEquals("", MinimumWindowSubstring.minWindow("ADOBECODEBANC", "XYZ")); + assertEquals("BC", MinimumWindowSubstring.minWindow("ABCDEF", "BC")); + assertEquals("q", MinimumWindowSubstring.minWindow("abcdefghijklmnopqrstuvwxyz", "q")); + assertEquals("", MinimumWindowSubstring.minWindow("zzzzzzzzz", "zzzzzzzzzz")); + assertEquals("abbbbbcdd", MinimumWindowSubstring.minWindow("aaaaaaaaaaaabbbbbcdd", "abcdd")); + assertEquals("ABCDEFG", MinimumWindowSubstring.minWindow("ABCDEFG", "ABCDEFG")); + assertEquals("", MinimumWindowSubstring.minWindow("abc", "A")); + assertEquals("A", MinimumWindowSubstring.minWindow("aAbBcC", "A")); + assertEquals("AABBC", MinimumWindowSubstring.minWindow("AAABBC", "AABC")); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/PriorityQueueSortTest.java b/src/test/java/com/thealgorithms/sorts/PriorityQueueSortTest.java new file mode 100644 index 000000000000..215431293227 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/PriorityQueueSortTest.java @@ -0,0 +1,56 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import org.junit.jupiter.api.Test; + +class PriorityQueueSortTest { + + @Test + void testNullArray() { + int[] input = null; + assertArrayEquals(null, PriorityQueueSort.sort(input)); + } + + @Test + void testSingleElementArray() { + int[] input = {5}; + int[] expected = {5}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } + + @Test + void testSortNormalArray() { + int[] input = {7, 2, 9, 4, 1}; + int[] expected = {1, 2, 4, 7, 9}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } + + @Test + void testEmptyArray() { + int[] input = {}; + int[] expected = {}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } + + @Test + void testNegativeNumbers() { + int[] input = {3, -1, 2, -5, 0}; + int[] expected = {-5, -1, 0, 2, 3}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } + + @Test + void testAlreadySortedArray() { + int[] input = {1, 2, 3, 4, 5}; + int[] expected = {1, 2, 3, 4, 5}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } + + @Test + void testArrayWithDuplicates() { + int[] input = {5, 1, 3, 3, 2, 5}; + int[] expected = {1, 2, 3, 3, 5, 5}; + assertArrayEquals(expected, PriorityQueueSort.sort(input)); + } +} diff --git a/src/test/java/com/thealgorithms/stacks/TrappingRainwaterTest.java b/src/test/java/com/thealgorithms/stacks/TrappingRainwaterTest.java new file mode 100644 index 000000000000..909be6cd46da --- /dev/null +++ b/src/test/java/com/thealgorithms/stacks/TrappingRainwaterTest.java @@ -0,0 +1,38 @@ +package com.thealgorithms.stacks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TrappingRainwaterTest { + + @Test + public void testExampleCase() { + int[] height = {4, 2, 0, 3, 2, 5}; + assertEquals(9, TrappingRainwater.trap(height)); + } + + @Test + public void testNoTrapping() { + int[] height = {1, 2, 3, 4, 5}; + assertEquals(0, TrappingRainwater.trap(height)); + } + + @Test + public void testFlatSurface() { + int[] height = {0, 0, 0, 0}; + assertEquals(0, TrappingRainwater.trap(height)); + } + + @Test + public void testSymmetricPit() { + int[] height = {3, 0, 2, 0, 3}; + assertEquals(7, TrappingRainwater.trap(height)); + } + + @Test + public void testSingleBar() { + int[] height = {5}; + assertEquals(0, TrappingRainwater.trap(height)); + } +} 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/strings/AlternativeStringArrangeTest.java b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java new file mode 100644 index 000000000000..9e8ae9e9f153 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java @@ -0,0 +1,23 @@ +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.MethodSource; + +class AlternativeStringArrangeTest { + + // Method to provide test data + private static Stream provideTestData() { + return Stream.of(new Object[] {"abc", "12345", "a1b2c345"}, new Object[] {"abcd", "12", "a1b2cd"}, new Object[] {"", "123", "123"}, new Object[] {"abc", "", "abc"}, new Object[] {"a", "1", "a1"}, new Object[] {"ab", "12", "a1b2"}, new Object[] {"abcdef", "123", "a1b2c3def"}, + new Object[] {"ab", "123456", "a1b23456"}); + } + + // Parameterized test using the provided test data + @ParameterizedTest(name = "{0} and {1} should return {2}") + @MethodSource("provideTestData") + void arrangeTest(String input1, String input2, String expected) { + assertEquals(expected, AlternativeStringArrange.arrange(input1, input2)); + } +} diff --git a/src/test/java/com/thealgorithms/strings/IsogramTest.java b/src/test/java/com/thealgorithms/strings/IsogramTest.java new file mode 100644 index 000000000000..9dd844dc7cf9 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/IsogramTest.java @@ -0,0 +1,117 @@ +package com.thealgorithms.strings; + +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.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class IsogramTest { + + record IsogramTestCase(String input, boolean expected) { + } + + private static Stream isAlphabeticIsogram() { + return Stream.of( + // Valid isograms (only checks letters) + new IsogramTestCase("uncopyrightable", true), new IsogramTestCase("dermatoglyphics", true), new IsogramTestCase("background", true), new IsogramTestCase("python", true), new IsogramTestCase("keyboard", true), new IsogramTestCase("clipboard", true), new IsogramTestCase("flowchart", true), + new IsogramTestCase("bankruptcy", true), new IsogramTestCase("computer", true), new IsogramTestCase("algorithms", true), + + // Not isograms - letters repeat + new IsogramTestCase("hello", false), new IsogramTestCase("programming", false), new IsogramTestCase("java", false), new IsogramTestCase("coffee", false), new IsogramTestCase("book", false), new IsogramTestCase("letter", false), new IsogramTestCase("mississippi", false), + new IsogramTestCase("google", false), + + // Edge cases + new IsogramTestCase("", true), new IsogramTestCase("a", true), new IsogramTestCase("ab", true), new IsogramTestCase("abc", true), new IsogramTestCase("aa", false), new IsogramTestCase("abcdefghijklmnopqrstuvwxyz", true), + + // Case insensitive + new IsogramTestCase("Python", true), new IsogramTestCase("BACKGROUND", true), new IsogramTestCase("Hello", false), new IsogramTestCase("PROGRAMMING", false)); + } + + private static Stream isFullIsogram() { + return Stream.of( + // Valid isograms (checks all characters) + new IsogramTestCase("uncopyrightable", true), new IsogramTestCase("dermatoglyphics", true), new IsogramTestCase("background", true), new IsogramTestCase("python", true), new IsogramTestCase("keyboard", true), new IsogramTestCase("clipboard", true), new IsogramTestCase("flowchart", true), + new IsogramTestCase("bankruptcy", true), new IsogramTestCase("computer", true), new IsogramTestCase("algorithms", true), + + // Not isograms - characters repeat + new IsogramTestCase("hello", false), new IsogramTestCase("programming", false), new IsogramTestCase("java", false), new IsogramTestCase("coffee", false), new IsogramTestCase("book", false), new IsogramTestCase("letter", false), new IsogramTestCase("mississippi", false), + new IsogramTestCase("google", false), + + // Edge cases + new IsogramTestCase("", true), new IsogramTestCase("a", true), new IsogramTestCase("ab", true), new IsogramTestCase("abc", true), new IsogramTestCase("aa", false), new IsogramTestCase("abcdefghijklmnopqrstuvwxyz", true), + + // Case insensitive + new IsogramTestCase("Python", true), new IsogramTestCase("BACKGROUND", true), new IsogramTestCase("Hello", false), new IsogramTestCase("PROGRAMMING", false), + + // Strings with symbols and numbers + new IsogramTestCase("abc@def", true), // all characters unique + new IsogramTestCase("test-case", false), // 't', 's', 'e' repeat + new IsogramTestCase("python123", true), // all characters unique + new IsogramTestCase("hello@123", false), // 'l' repeats + new IsogramTestCase("abc123!@#", true), // all characters unique + new IsogramTestCase("test123test", false), // 't', 'e', 's' repeat + new IsogramTestCase("1234567890", true), // all digits unique + new IsogramTestCase("12321", false), // '1' and '2' repeat + new IsogramTestCase("!@#$%^&*()", true) // all special characters unique + ); + } + + @ParameterizedTest + @MethodSource("isAlphabeticIsogram") + void testIsogramByArray(IsogramTestCase testCase) { + assertEquals(testCase.expected(), Isogram.isAlphabeticIsogram(testCase.input())); + } + + @ParameterizedTest + @MethodSource("isFullIsogram") + void testIsogramByLength(IsogramTestCase testCase) { + assertEquals(testCase.expected(), Isogram.isFullIsogram(testCase.input())); + } + + @Test + void testNullInputByArray() { + assertTrue(Isogram.isAlphabeticIsogram(null)); + } + + @Test + void testNullInputByLength() { + assertTrue(Isogram.isFullIsogram(null)); + } + + @Test + void testEmptyStringByArray() { + assertTrue(Isogram.isAlphabeticIsogram("")); + } + + @Test + void testEmptyStringByLength() { + assertTrue(Isogram.isFullIsogram("")); + } + + @Test + void testAlphabeticIsogramThrowsException() { + // Test that IllegalArgumentException is thrown for non-alphabetic characters + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("1")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("@")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("python!")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("123algorithm")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("hello123")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("!@@#$%^&*()")); + } + + @Test + void testFullIsogramWithMixedCharacters() { + // Test that full isogram method handles all character types without exceptions + assertTrue(Isogram.isFullIsogram("abc123")); + assertFalse(Isogram.isFullIsogram("test@email")); // 'e' repeats + assertFalse(Isogram.isFullIsogram("hello123")); // 'l' repeats + assertTrue(Isogram.isFullIsogram("1234567890")); + assertFalse(Isogram.isFullIsogram("12321")); // '1' and '2' repeat + assertTrue(Isogram.isFullIsogram("!@#$%^&*()")); + } +} 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/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")); + } +}