diff --git a/.github/workflows/close-failed-prs.yml b/.github/workflows/close-failed-prs.yml new file mode 100644 index 000000000000..f1bf3690f13f --- /dev/null +++ b/.github/workflows/close-failed-prs.yml @@ -0,0 +1,154 @@ +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 isMergeFromMain = mainBranches.some(branch => + msg.startsWith(`merge branch '${branch}'`) || + msg.includes(`merge remote-tracking branch '${branch}'`) + ); + return !isMergeFromMain; + }); + + // 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; + } \ No newline at end of file diff --git a/DIRECTORY.md b/DIRECTORY.md index b311b10fa177..47833a3f59f2 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -29,8 +29,10 @@ - 📄 [BcdConversion](src/main/java/com/thealgorithms/bitmanipulation/BcdConversion.java) - 📄 [BinaryPalindromeCheck](src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.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 +76,7 @@ - 📄 [ECC](src/main/java/com/thealgorithms/ciphers/ECC.java) - 📄 [HillCipher](src/main/java/com/thealgorithms/ciphers/HillCipher.java) - 📄 [MonoAlphabetic](src/main/java/com/thealgorithms/ciphers/MonoAlphabetic.java) + - 📄 [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,6 +92,9 @@ - 📄 [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** + - 📄 [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) @@ -98,6 +104,7 @@ - 📄 [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) @@ -118,6 +125,7 @@ - 📄 [PhoneticAlphabetConverter](src/main/java/com/thealgorithms/conversions/PhoneticAlphabetConverter.java) - 📄 [RgbHsvConversion](src/main/java/com/thealgorithms/conversions/RgbHsvConversion.java) - 📄 [RomanToInteger](src/main/java/com/thealgorithms/conversions/RomanToInteger.java) + - 📄 [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) @@ -157,6 +165,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) @@ -171,6 +180,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** @@ -199,10 +209,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) @@ -215,6 +227,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) @@ -299,6 +312,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) @@ -319,9 +333,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) @@ -329,6 +345,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) @@ -343,15 +360,25 @@ - 📄 [BresenhamLine](src/main/java/com/thealgorithms/geometry/BresenhamLine.java) - 📄 [ConvexHull](src/main/java/com/thealgorithms/geometry/ConvexHull.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) - 📄 [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) - 📄 [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) @@ -404,6 +431,7 @@ - 📄 [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) - 📄 [FFT](src/main/java/com/thealgorithms/maths/FFT.java) - 📄 [FFTBluestein](src/main/java/com/thealgorithms/maths/FFTBluestein.java) @@ -426,7 +454,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) @@ -450,6 +480,7 @@ - 📄 [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) @@ -457,6 +488,7 @@ - 📄 [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) @@ -475,6 +507,7 @@ - 📄 [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) - 📄 [SolovayStrassenPrimalityTest](src/main/java/com/thealgorithms/maths/SolovayStrassenPrimalityTest.java) @@ -486,6 +519,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) @@ -493,6 +527,7 @@ - 📄 [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) - 📄 [MatrixMultiplication](src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java) @@ -538,12 +573,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) @@ -552,6 +586,7 @@ - 📄 [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) @@ -562,6 +597,8 @@ - 📄 [Verhoeff](src/main/java/com/thealgorithms/others/Verhoeff.java) - 📁 **cn** - 📄 [HammingDistance](src/main/java/com/thealgorithms/others/cn/HammingDistance.java) + - 📁 **physics** + - 📄 [GroundToGroundProjectileMotion](src/main/java/com/thealgorithms/physics/GroundToGroundProjectileMotion.java) - 📁 **puzzlesandgames** - 📄 [Sudoku](src/main/java/com/thealgorithms/puzzlesandgames/Sudoku.java) - 📄 [TowerOfHanoi](src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java) @@ -574,8 +611,10 @@ - 📄 [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) - 📄 [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) @@ -630,6 +669,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) @@ -641,6 +681,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) @@ -719,6 +760,7 @@ - 📁 **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) @@ -726,6 +768,7 @@ - 📄 [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) - 📄 [LetterCombinationsOfPhoneNumber](src/main/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumber.java) @@ -781,8 +824,10 @@ - 📄 [BcdConversionTest](src/test/java/com/thealgorithms/bitmanipulation/BcdConversionTest.java) - 📄 [BinaryPalindromeCheckTest](src/test/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheckTest.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) @@ -825,6 +870,7 @@ - 📄 [ECCTest](src/test/java/com/thealgorithms/ciphers/ECCTest.java) - 📄 [HillCipherTest](src/test/java/com/thealgorithms/ciphers/HillCipherTest.java) - 📄 [MonoAlphabeticTest](src/test/java/com/thealgorithms/ciphers/MonoAlphabeticTest.java) + - 📄 [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) @@ -836,6 +882,9 @@ - 📄 [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** + - 📄 [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) @@ -844,6 +893,7 @@ - 📄 [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) @@ -863,6 +913,7 @@ - 📄 [OctalToHexadecimalTest](src/test/java/com/thealgorithms/conversions/OctalToHexadecimalTest.java) - 📄 [PhoneticAlphabetConverterTest](src/test/java/com/thealgorithms/conversions/PhoneticAlphabetConverterTest.java) - 📄 [RomanToIntegerTest](src/test/java/com/thealgorithms/conversions/RomanToIntegerTest.java) + - 📄 [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) @@ -897,6 +948,7 @@ - 📄 [AStarTest](src/test/java/com/thealgorithms/datastructures/graphs/AStarTest.java) - 📄 [BipartiteGraphDFSTest](src/test/java/com/thealgorithms/datastructures/graphs/BipartiteGraphDFSTest.java) - 📄 [BoruvkaAlgorithmTest](src/test/java/com/thealgorithms/datastructures/graphs/BoruvkaAlgorithmTest.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) @@ -910,6 +962,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** @@ -934,9 +987,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) @@ -947,6 +1002,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) @@ -967,6 +1023,7 @@ - 📄 [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) @@ -990,6 +1047,9 @@ - 📄 [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) @@ -1010,6 +1070,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) @@ -1028,9 +1089,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) @@ -1038,6 +1101,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) @@ -1052,15 +1116,24 @@ - 📄 [BresenhamLineTest](src/test/java/com/thealgorithms/geometry/BresenhamLineTest.java) - 📄 [ConvexHullTest](src/test/java/com/thealgorithms/geometry/ConvexHullTest.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) - 📄 [HopcroftKarpTest](src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java) - 📄 [PredecessorConstrainedDfsTest](src/test/java/com/thealgorithms/graph/PredecessorConstrainedDfsTest.java) + - 📄 [PushRelabelTest](src/test/java/com/thealgorithms/graph/PushRelabelTest.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) @@ -1109,6 +1182,7 @@ - 📄 [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) - 📄 [FFTTest](src/test/java/com/thealgorithms/maths/FFTTest.java) - 📄 [FactorialRecursionTest](src/test/java/com/thealgorithms/maths/FactorialRecursionTest.java) @@ -1130,15 +1204,19 @@ - 📄 [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) - 📄 [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) - 📄 [MathBuilderTest](src/test/java/com/thealgorithms/maths/MathBuilderTest.java) @@ -1150,6 +1228,7 @@ - 📄 [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) @@ -1157,6 +1236,7 @@ - 📄 [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) - 📄 [PowerOfTwoOrNotTest](src/test/java/com/thealgorithms/maths/PowerOfTwoOrNotTest.java) @@ -1166,6 +1246,7 @@ - 📄 [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) - 📄 [SolovayStrassenPrimalityTestTest](src/test/java/com/thealgorithms/maths/SolovayStrassenPrimalityTestTest.java) - 📄 [SquareFreeIntegerTest](src/test/java/com/thealgorithms/maths/SquareFreeIntegerTest.java) @@ -1177,12 +1258,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) @@ -1222,14 +1305,21 @@ - 📄 [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) @@ -1237,6 +1327,8 @@ - 📄 [WorstFitCPUTest](src/test/java/com/thealgorithms/others/WorstFitCPUTest.java) - 📁 **cn** - 📄 [HammingDistanceTest](src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java) + - 📁 **physics** + - 📄 [GroundToGroundProjectileMotionTest](src/test/java/com/thealgorithms/physics/GroundToGroundProjectileMotionTest.java) - 📁 **puzzlesandgames** - 📄 [SudokuTest](src/test/java/com/thealgorithms/puzzlesandgames/SudokuTest.java) - 📄 [TowerOfHanoiTest](src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java) @@ -1249,8 +1341,10 @@ - 📄 [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) - 📄 [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) @@ -1305,6 +1399,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) @@ -1317,6 +1412,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) @@ -1393,6 +1489,7 @@ - 📁 **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) @@ -1400,6 +1497,7 @@ - 📄 [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) - 📄 [LetterCombinationsOfPhoneNumberTest](src/test/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumberTest.java) - 📄 [LongestCommonPrefixTest](src/test/java/com/thealgorithms/strings/LongestCommonPrefixTest.java) diff --git a/pom.xml b/pom.xml index e0e3516c08eb..b7ca85e1407c 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 @@ -112,14 +112,14 @@ com.puppycrawl.tools checkstyle - 12.0.0 + 12.1.1 com.github.spotbugs spotbugs-maven-plugin - 4.9.6.0 + 4.9.8.1 spotbugs-exclude.xml true 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/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/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/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/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/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..5a19c3bfa990 100644 --- a/src/main/java/com/thealgorithms/datastructures/lists/README.md +++ b/src/main/java/com/thealgorithms/datastructures/lists/README.md @@ -30,3 +30,4 @@ The `next` variable points to the next node in the data structure and value stor 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. 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/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/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/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/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/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/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/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/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/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/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/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/MiniMaxAlgorithm.java b/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java index e0b96570a4fe..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,46 +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; } - // A utility function to check if a number is a power of 2 + /** + * 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 (!isPowerOfTwo(scores.length)) { - System.out.println("The number of scores must be a power of 2."); - return; + throw new IllegalArgumentException("The number of scores must be a power of 2."); } - this.scores = scores; + 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/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/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/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/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/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/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/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/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/TwoSatTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/TwoSatTest.java new file mode 100644 index 000000000000..15e77b357f83 --- /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 whlie 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/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/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/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/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/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/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/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/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/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/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/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/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 index 821eb3f16029..4e81c8b7e34f 100644 --- a/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java +++ b/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java @@ -1,12 +1,9 @@ package com.thealgorithms.others; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -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.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,56 +23,82 @@ void setUp() { 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) - assertEquals(8, miniMax.getScores().length); - assertEquals(3, miniMax.getHeight()); + 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()) { - assertTrue(score >= 1 && score <= 99); + 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); - assertArrayEquals(validScores, miniMax.getScores()); - assertEquals(2, miniMax.getHeight()); // log2(4) = 2 + 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 - miniMax.setScores(invalidScores); - - // Should print error message and not change the scores - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores)); // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test void testSetScoresWithZeroLength() { int[] emptyScores = {}; // Length 0 is not a power of 2 - miniMax.setScores(emptyScores); - - // Should print error message and not change the scores - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyScores)); // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test void testSetScoresWithVariousInvalidLengths() { - // Test multiple invalid lengths to ensure isPowerOfTwo function is fully covered + // 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 @@ -86,17 +109,11 @@ void testSetScoresWithVariousInvalidLengths() { }; for (int[] invalidScores : invalidScoreArrays) { - // Clear the output stream for each test - outputStream.reset(); - miniMax.setScores(invalidScores); - - // Should print error message for each invalid length - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2."), "Failed for array length: " + invalidScores.length); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores), "Failed for array length: " + invalidScores.length); } // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test @@ -104,8 +121,8 @@ void testSetScoresWithSingleElement() { int[] singleScore = {42}; miniMax.setScores(singleScore); - assertArrayEquals(singleScore, miniMax.getScores()); - assertEquals(0, miniMax.getHeight()); // log2(1) = 0 + Assertions.assertArrayEquals(singleScore, miniMax.getScores()); + Assertions.assertEquals(0, miniMax.getHeight()); // log2(1) = 0 } @Test @@ -116,7 +133,7 @@ void testMiniMaxWithKnownScores() { // Maximizer starts: should choose max(min(3,12), min(8,2)) = max(3, 2) = 3 int result = miniMax.miniMax(0, true, 0, false); - assertEquals(3, result); + Assertions.assertEquals(3, result); } @Test @@ -127,7 +144,7 @@ void testMiniMaxWithMinimizerFirst() { // Minimizer starts: should choose min(max(3,12), max(8,2)) = min(12, 8) = 8 int result = miniMax.miniMax(0, false, 0, false); - assertEquals(8, result); + Assertions.assertEquals(8, result); } @Test @@ -139,8 +156,8 @@ void testMiniMaxWithLargerTree() { // 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 - assertEquals(6, result); + // = max(min(6, 7), min(5, 6)) = max(6, 5) = 6 + Assertions.assertEquals(6, result); } @Test @@ -151,41 +168,41 @@ void testMiniMaxVerboseOutput() { miniMax.miniMax(0, true, 0, true); String output = outputStream.toString(); - assertTrue(output.contains("Maximizer")); - assertTrue(output.contains("Minimizer")); - assertTrue(output.contains("chooses")); + Assertions.assertTrue(output.contains("Maximizer")); + Assertions.assertTrue(output.contains("Minimizer")); + Assertions.assertTrue(output.contains("chooses")); } @Test void testGetRandomScoresLength() { int[] randomScores = MiniMaxAlgorithm.getRandomScores(4, 50); - assertEquals(16, randomScores.length); // 2^4 = 16 + Assertions.assertEquals(16, randomScores.length); // 2^4 = 16 // All scores should be between 1 and 50 for (int score : randomScores) { - assertTrue(score >= 1 && score <= 50); + Assertions.assertTrue(score >= 1 && score <= 50); } } @Test void testGetRandomScoresWithDifferentParameters() { int[] randomScores = MiniMaxAlgorithm.getRandomScores(2, 10); - assertEquals(4, randomScores.length); // 2^2 = 4 + Assertions.assertEquals(4, randomScores.length); // 2^2 = 4 // All scores should be between 1 and 10 for (int score : randomScores) { - assertTrue(score >= 1 && score <= 10); + Assertions.assertTrue(score >= 1 && score <= 10); } } @Test void testMainMethod() { // Test that main method runs without errors - assertDoesNotThrow(() -> MiniMaxAlgorithm.main(new String[] {})); + Assertions.assertDoesNotThrow(() -> MiniMaxAlgorithm.main(new String[] {})); String output = outputStream.toString(); - assertTrue(output.contains("The best score for")); - assertTrue(output.contains("Maximizer")); + Assertions.assertTrue(output.contains("The best score for")); + Assertions.assertTrue(output.contains("Maximizer")); } @Test @@ -193,11 +210,11 @@ void testHeightCalculation() { // Test height calculation for different array sizes int[] scores2 = {1, 2}; miniMax.setScores(scores2); - assertEquals(1, miniMax.getHeight()); // log2(2) = 1 + Assertions.assertEquals(1, miniMax.getHeight()); // log2(2) = 1 int[] scores16 = new int[16]; miniMax.setScores(scores16); - assertEquals(4, miniMax.getHeight()); // log2(16) = 4 + Assertions.assertEquals(4, miniMax.getHeight()); // log2(16) = 4 } @Test @@ -206,7 +223,7 @@ void testEdgeCaseWithZeroScores() { miniMax.setScores(zeroScores); int result = miniMax.miniMax(0, true, 0, false); - assertEquals(0, result); + Assertions.assertEquals(0, result); } @Test @@ -218,11 +235,7 @@ void testEdgeCaseWithNegativeScores() { // 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); - assertEquals(-5, result); - } - - void tearDown() { - System.setOut(originalOut); + Assertions.assertEquals(-5, result); } @Test @@ -233,12 +246,9 @@ void testSetScoresWithNegativeLength() { // Test with array length 0 (edge case for n > 0 condition) int[] emptyArray = new int[0]; - outputStream.reset(); - miniMax.setScores(emptyArray); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyArray)); - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); - assertEquals(8, miniMax.getScores().length); // Should remain unchanged + Assertions.assertEquals(8, miniMax.getScores().length); // Should remain unchanged } @Test @@ -250,8 +260,8 @@ void testSetScoresWithLargePowerOfTwo() { } miniMax.setScores(largeValidScores); - assertArrayEquals(largeValidScores, miniMax.getScores()); - assertEquals(5, miniMax.getHeight()); // log2(32) = 5 + Assertions.assertArrayEquals(largeValidScores, miniMax.getScores()); + Assertions.assertEquals(5, miniMax.getHeight()); // log2(32) = 5 } @Test @@ -270,8 +280,65 @@ void testSetScoresValidEdgeCases() { for (int i = 0; i < validPowersOf2.length; i++) { miniMax.setScores(validPowersOf2[i]); - assertEquals(validPowersOf2[i].length, miniMax.getScores().length, "Failed for array length: " + validPowersOf2[i].length); - assertEquals(expectedHeights[i], miniMax.getHeight(), "Height calculation failed for array length: " + validPowersOf2[i].length); + 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/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/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/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/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/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."); + } +}