diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e5622f7b51d..1b91763b2d53 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,14 +5,20 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 - package-ecosystem: "github-actions" directory: "/.github/workflows/" schedule: interval: "daily" + cooldown: + default-days: 7 - package-ecosystem: "maven" directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 ... diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5f200c12836..a1395d87ec8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,9 +8,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.7.0 with: java-version: 21 distribution: 'temurin' @@ -20,7 +20,7 @@ jobs: if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: fail_ci_if_error: true - name: Upload coverage to codecov (with token) @@ -28,7 +28,7 @@ jobs: github.repository == 'TheAlgorithms/Java' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/clang-format-lint.yml b/.github/workflows/clang-format-lint.yml index dc0c9754ed1b..622679f73842 100644 --- a/.github/workflows/clang-format-lint.yml +++ b/.github/workflows/clang-format-lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: DoozyX/clang-format-lint-action@v0.20 with: source: './src' diff --git a/.github/workflows/close-failed-prs.yml b/.github/workflows/close-failed-prs.yml index 6deea88f0daf..4013e87b6569 100644 --- a/.github/workflows/close-failed-prs.yml +++ b/.github/workflows/close-failed-prs.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Close stale PRs - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 152d0d766fd2..db2c7f5a1b46 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,16 +21,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.7.0 with: java-version: 21 distribution: 'temurin' - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.37.7 with: languages: 'java-kotlin' @@ -38,7 +38,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.37.7 with: category: "/language:java-kotlin" @@ -52,15 +52,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.37.7 with: languages: 'actions' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.37.7 with: category: "/language:actions" ... diff --git a/.github/workflows/infer.yml b/.github/workflows/infer.yml index 9d4dcf63000b..3bfc509ebf47 100644 --- a/.github/workflows/infer.yml +++ b/.github/workflows/infer.yml @@ -15,42 +15,16 @@ jobs: run_infer: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.7.0 with: java-version: 21 distribution: 'temurin' - - name: Set up OCaml - uses: ocaml/setup-ocaml@v3 - with: - ocaml-compiler: 5 - - - name: Get current year/weak - run: echo "year_week=$(date +'%Y_%U')" >> $GITHUB_ENV - - - name: Cache infer build - id: cache-infer - uses: actions/cache@v5 - with: - path: infer - key: ${{ runner.os }}-infer-${{ env.year_week }} - - - name: Build infer - if: steps.cache-infer.outputs.cache-hit != 'true' - run: | - cd .. - git clone https://github.com/facebook/infer.git - cd infer - git checkout 02c2c43b71e4c5110c0be841e66153942fda06c9 - ./build-infer.sh java - cp -r infer ../Java - - - name: Add infer to PATH - run: | - echo "infer/bin" >> $GITHUB_PATH + - name: Set up inferAdd commentMore actions + uses: srz-zumix/setup-infer@v1 - name: Display infer version run: | @@ -60,5 +34,5 @@ jobs: - name: Run infer run: | mvn clean - infer --fail-on-issue --print-logs --no-progress-bar -- mvn test + infer --java-version 21 --fail-on-issue --print-logs --no-progress-bar -- mvn test ... diff --git a/.github/workflows/project_structure.yml b/.github/workflows/project_structure.yml index 5aadc6353791..a70c3240ce08 100644 --- a/.github/workflows/project_structure.yml +++ b/.github/workflows/project_structure.yml @@ -15,8 +15,8 @@ jobs: check_structure: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7.0.0 with: python-version: '3.13' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bb613daf8f1d..b961d15c240e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/stale@v10 + - uses: actions/stale@v11.0.0 with: stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution!' close-issue-message: 'Please reopen this issue once you have made the required changes. If you need help, feel free to ask in our [Discord](https://the-algorithms.com/discord) server or ping one of the maintainers here. Thank you for your contribution!' diff --git a/.github/workflows/update-directorymd.yml b/.github/workflows/update-directorymd.yml index 101d82427e38..3977bfda86bf 100644 --- a/.github/workflows/update-directorymd.yml +++ b/.github/workflows/update-directorymd.yml @@ -1,4 +1,4 @@ -ο»Ώname: Generate Directory Markdown +name: Generate Directory Markdown on: push: @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false @@ -33,7 +33,7 @@ jobs: git diff --cached --quiet || git commit -m "Update DIRECTORY.md" - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.REPO_SCOPED_TOKEN }} branch: update-directory diff --git a/.inferconfig b/.inferconfig index 239172177b38..cf26212feac5 100644 --- a/.inferconfig +++ b/.inferconfig @@ -21,6 +21,7 @@ "src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java", "src/test/java/com/thealgorithms/datastructures/trees/KDTreeTest.java", "src/test/java/com/thealgorithms/datastructures/trees/LazySegmentTreeTest.java", + "src/test/java/com/thealgorithms/dynamicprogramming/DamerauLevenshteinDistanceTest.java", "src/test/java/com/thealgorithms/others/HuffmanTest.java", "src/test/java/com/thealgorithms/searches/QuickSelectTest.java", "src/test/java/com/thealgorithms/stacks/PostfixToInfixTest.java", diff --git a/DIRECTORY.md b/DIRECTORY.md index deaf59636fa4..37d9b3c295e2 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -626,8 +626,6 @@ - πŸ“„ [SkylineProblem](src/main/java/com/thealgorithms/others/SkylineProblem.java) - πŸ“„ [TwoPointers](src/main/java/com/thealgorithms/others/TwoPointers.java) - πŸ“„ [Verhoeff](src/main/java/com/thealgorithms/others/Verhoeff.java) - - πŸ“ **cn** - - πŸ“„ [HammingDistance](src/main/java/com/thealgorithms/others/cn/HammingDistance.java) - πŸ“ **physics** - πŸ“„ [CoulombsLaw](src/main/java/com/thealgorithms/physics/CoulombsLaw.java) - πŸ“„ [DampedOscillator](src/main/java/com/thealgorithms/physics/DampedOscillator.java) @@ -701,7 +699,6 @@ - πŸ“„ [LowerBound](src/main/java/com/thealgorithms/searches/LowerBound.java) - πŸ“„ [MonteCarloTreeSearch](src/main/java/com/thealgorithms/searches/MonteCarloTreeSearch.java) - πŸ“„ [OrderAgnosticBinarySearch](src/main/java/com/thealgorithms/searches/OrderAgnosticBinarySearch.java) - - πŸ“„ [PerfectBinarySearch](src/main/java/com/thealgorithms/searches/PerfectBinarySearch.java) - πŸ“„ [QuickSelect](src/main/java/com/thealgorithms/searches/QuickSelect.java) - πŸ“„ [RabinKarpAlgorithm](src/main/java/com/thealgorithms/searches/RabinKarpAlgorithm.java) - πŸ“„ [RandomSearch](src/main/java/com/thealgorithms/searches/RandomSearch.java) @@ -710,7 +707,6 @@ - πŸ“„ [SaddlebackSearch](src/main/java/com/thealgorithms/searches/SaddlebackSearch.java) - πŸ“„ [SearchInARowAndColWiseSortedMatrix](src/main/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrix.java) - πŸ“„ [SentinelLinearSearch](src/main/java/com/thealgorithms/searches/SentinelLinearSearch.java) - - πŸ“„ [SortOrderAgnosticBinarySearch](src/main/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearch.java) - πŸ“„ [SquareRootBinarySearch](src/main/java/com/thealgorithms/searches/SquareRootBinarySearch.java) - πŸ“„ [TernarySearch](src/main/java/com/thealgorithms/searches/TernarySearch.java) - πŸ“„ [UnionFind](src/main/java/com/thealgorithms/searches/UnionFind.java) @@ -817,10 +813,10 @@ - πŸ“„ [LetterCombinationsOfPhoneNumber](src/main/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumber.java) - πŸ“„ [LongestCommonPrefix](src/main/java/com/thealgorithms/strings/LongestCommonPrefix.java) - πŸ“„ [LongestNonRepetitiveSubstring](src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java) - - πŸ“„ [LongestPalindromicSubstring](src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java) - πŸ“„ [Lower](src/main/java/com/thealgorithms/strings/Lower.java) - πŸ“„ [Manacher](src/main/java/com/thealgorithms/strings/Manacher.java) - πŸ“„ [MyAtoi](src/main/java/com/thealgorithms/strings/MyAtoi.java) + - πŸ“„ [MoveHashToEnd](src/main/java/com/thealgorithms/strings/MoveHashToEnd.java) - πŸ“„ [Palindrome](src/main/java/com/thealgorithms/strings/Palindrome.java) - πŸ“„ [Pangram](src/main/java/com/thealgorithms/strings/Pangram.java) - πŸ“„ [PermuteString](src/main/java/com/thealgorithms/strings/PermuteString.java) @@ -834,7 +830,6 @@ - πŸ“„ [StringMatchFiniteAutomata](src/main/java/com/thealgorithms/strings/StringMatchFiniteAutomata.java) - πŸ“„ [SuffixArray](src/main/java/com/thealgorithms/strings/SuffixArray.java) - πŸ“„ [Upper](src/main/java/com/thealgorithms/strings/Upper.java) - - πŸ“„ [ValidParentheses](src/main/java/com/thealgorithms/strings/ValidParentheses.java) - πŸ“„ [WordLadder](src/main/java/com/thealgorithms/strings/WordLadder.java) - πŸ“„ [ZAlgorithm](src/main/java/com/thealgorithms/strings/ZAlgorithm.java) - πŸ“ **zigZagPattern** @@ -1395,7 +1390,6 @@ - πŸ“„ [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) @@ -1404,8 +1398,6 @@ - πŸ“„ [SkylineProblemTest](src/test/java/com/thealgorithms/others/SkylineProblemTest.java) - πŸ“„ [TwoPointersTest](src/test/java/com/thealgorithms/others/TwoPointersTest.java) - πŸ“„ [WorstFitCPUTest](src/test/java/com/thealgorithms/others/WorstFitCPUTest.java) - - πŸ“ **cn** - - πŸ“„ [HammingDistanceTest](src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java) - πŸ“ **physics** - πŸ“„ [CoulombsLawTest](src/test/java/com/thealgorithms/physics/CoulombsLawTest.java) - πŸ“„ [DampedOscillatorTest](src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java) @@ -1479,7 +1471,6 @@ - πŸ“„ [LowerBoundTest](src/test/java/com/thealgorithms/searches/LowerBoundTest.java) - πŸ“„ [MonteCarloTreeSearchTest](src/test/java/com/thealgorithms/searches/MonteCarloTreeSearchTest.java) - πŸ“„ [OrderAgnosticBinarySearchTest](src/test/java/com/thealgorithms/searches/OrderAgnosticBinarySearchTest.java) - - πŸ“„ [PerfectBinarySearchTest](src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java) - πŸ“„ [QuickSelectTest](src/test/java/com/thealgorithms/searches/QuickSelectTest.java) - πŸ“„ [RabinKarpAlgorithmTest](src/test/java/com/thealgorithms/searches/RabinKarpAlgorithmTest.java) - πŸ“„ [RandomSearchTest](src/test/java/com/thealgorithms/searches/RandomSearchTest.java) @@ -1488,7 +1479,6 @@ - πŸ“„ [SaddlebackSearchTest](src/test/java/com/thealgorithms/searches/SaddlebackSearchTest.java) - πŸ“„ [SearchInARowAndColWiseSortedMatrixTest](src/test/java/com/thealgorithms/searches/SearchInARowAndColWiseSortedMatrixTest.java) - πŸ“„ [SentinelLinearSearchTest](src/test/java/com/thealgorithms/searches/SentinelLinearSearchTest.java) - - πŸ“„ [SortOrderAgnosticBinarySearchTest](src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java) - πŸ“„ [SquareRootBinarySearchTest](src/test/java/com/thealgorithms/searches/SquareRootBinarySearchTest.java) - πŸ“„ [TernarySearchTest](src/test/java/com/thealgorithms/searches/TernarySearchTest.java) - πŸ“„ [TestSearchInARowAndColWiseSortedMatrix](src/test/java/com/thealgorithms/searches/TestSearchInARowAndColWiseSortedMatrix.java) @@ -1593,7 +1583,6 @@ - πŸ“„ [LetterCombinationsOfPhoneNumberTest](src/test/java/com/thealgorithms/strings/LetterCombinationsOfPhoneNumberTest.java) - πŸ“„ [LongestCommonPrefixTest](src/test/java/com/thealgorithms/strings/LongestCommonPrefixTest.java) - πŸ“„ [LongestNonRepetitiveSubstringTest](src/test/java/com/thealgorithms/strings/LongestNonRepetitiveSubstringTest.java) - - πŸ“„ [LongestPalindromicSubstringTest](src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java) - πŸ“„ [LowerTest](src/test/java/com/thealgorithms/strings/LowerTest.java) - πŸ“„ [ManacherTest](src/test/java/com/thealgorithms/strings/ManacherTest.java) - πŸ“„ [MyAtoiTest](src/test/java/com/thealgorithms/strings/MyAtoiTest.java) @@ -1609,7 +1598,6 @@ - πŸ“„ [StringMatchFiniteAutomataTest](src/test/java/com/thealgorithms/strings/StringMatchFiniteAutomataTest.java) - πŸ“„ [SuffixArrayTest](src/test/java/com/thealgorithms/strings/SuffixArrayTest.java) - πŸ“„ [UpperTest](src/test/java/com/thealgorithms/strings/UpperTest.java) - - πŸ“„ [ValidParenthesesTest](src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java) - πŸ“„ [WordLadderTest](src/test/java/com/thealgorithms/strings/WordLadderTest.java) - πŸ“„ [ZAlgorithmTest](src/test/java/com/thealgorithms/strings/ZAlgorithmTest.java) - πŸ“ **zigZagPattern** diff --git a/README-ko.md b/README-ko.md deleted file mode 100644 index 4f8cab92fc42..000000000000 --- a/README-ko.md +++ /dev/null @@ -1,191 +0,0 @@ -# μ•Œκ³ λ¦¬μ¦˜ - μžλ°” - -## 이 [개발브런치](https://github.com/TheAlgorithms/Java/tree/Development)λŠ” κΈ°μ‘΄ ν”„λ‘œμ νŠΈλ₯Ό Java ν”„λ‘œμ νŠΈ ꡬ쑰둜 μž¬κ°œλ°œν•˜κΈ° μœ„ν•΄ μž‘μ„±λ˜μ—ˆλ‹€. 기여도λ₯Ό μœ„ν•΄ 개발 μ§€μ‚¬λ‘œ μ „ν™˜ν•  수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ 이 문제λ₯Ό μ°Έμ‘°ν•˜μ‹­μ‹œμ˜€. μ»¨νŠΈλ¦¬λ·°μ…˜μ„ μœ„ν•΄ [개발브런치](https://github.com/TheAlgorithms/Java/tree/Development)둜 μ „ν™˜ν•  수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ [이 이슈](https://github.com/TheAlgorithms/Java/issues/474)λ₯Ό μ°Έκ³ ν•˜μ‹­μ‹œμ˜€. - -### μžλ°”λ‘œ κ΅¬ν˜„λœ λͺ¨λ“  μ•Œκ³ λ¦¬μ¦˜λ“€ (ꡐ윑용) - -이것듀은 단지 μ‹œλ²”μ„ μœ„ν•œ 것이닀. ν‘œμ€€ μžλ°” λΌμ΄λΈŒλŸ¬λ¦¬μ—λŠ” μ„±λŠ₯μƒμ˜ 이유둜 더 λ‚˜μ€ 것듀이 κ΅¬ν˜„λ˜μ–΄μžˆλ‹€ - -## μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜ - -### Bubble(버블 μ •λ ¬) - -![alt text][bubble-image] - -From [Wikipedia][bubble-wiki]: 버블 μ†ŒνŠΈ(sinking sor라고도 λΆˆλ¦¬μ›€)λŠ” 리슀트λ₯Ό 반볡적인 λ‹¨κ³„λ‘œ μ ‘κ·Όν•˜μ—¬ μ •λ ¬ν•œλ‹€. 각각의 짝을 λΉ„κ΅ν•˜λ©°, μˆœμ„œκ°€ 잘λͺ»λœ 경우 κ·Έμ ‘ν•œ μ•„μ΄ν…œλ“€μ„ μŠ€μ™‘ν•˜λŠ” μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. 더 이상 μŠ€μ™‘ν•  것이 없을 λ•ŒκΉŒμ§€ λ°˜λ³΅ν•˜λ©°, 반볡이 λλ‚¨μŒ λ¦¬μŠ€νŠΈκ°€ μ •λ ¬λ˜μ—ˆμŒμ„ μ˜λ―Έν•œλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n) -- 평균 μ„±λŠ₯ O(n^2) - -###### View the algorithm in [action][bubble-toptal] - -### Insertion(μ‚½μž… μ •λ ¬) - -![alt text][insertion-image] - -From [Wikipedia][insertion-wiki]: μ‚½μž… 정렬은 μ΅œμ’… μ •λ ¬λœ λ°°μ—΄(λ˜λŠ” 리슀트)을 ν•œλ²ˆμ— ν•˜λ‚˜μ”© κ΅¬μΆ•ν•˜λŠ” μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. 이것은 큰 λ¦¬μŠ€νŠΈμ—μ„œ 더 λ‚˜μ€ μ•Œκ³ λ¦¬μ¦˜μΈ 퀡 μ†ŒνŠΈ, νž™ μ†ŒνŠΈ, λ˜λŠ” λ¨Έμ§€ μ†ŒνŠΈλ³΄λ‹€ 훨씬 μ•ˆμ’‹μ€ νš¨μœ¨μ„ κ°€μ§„λ‹€. κ·Έλ¦Όμ—μ„œ 각 λ§‰λŒ€λŠ” μ •λ ¬ν•΄μ•Ό ν•˜λŠ” λ°°μ—΄μ˜ μš”μ†Œλ₯Ό λ‚˜νƒ€λ‚Έλ‹€. 상단과 두 번째 상단 λ§‰λŒ€μ˜ 첫 번째 κ΅μ°¨μ μ—μ„œ λ°œμƒν•˜λŠ” 것은 두 번째 μš”μ†Œκ°€ 첫 번째 μš”μ†Œλ³΄λ‹€ 더 높은 μš°μ„  μˆœμœ„λ₯Ό κ°€μ§€κΈ° λ•Œλ¬Έμ— λ§‰λŒ€λ‘œ ν‘œμ‹œλ˜λŠ” μ΄λŸ¬ν•œ μš”μ†Œλ₯Ό κ΅ν™˜ν•œ 것이닀. 이 방법을 λ°˜λ³΅ν•˜λ©΄ μ‚½μž… 정렬이 μ™„λ£Œλœλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n) -- 평균 O(n^2) - -###### View the algorithm in [action][insertion-toptal] - -### Merge(합병 μ •λ ¬) - -![alt text][merge-image] - -From [Wikipedia][merge-wiki]: 컴퓨터 κ³Όν•™μ—μ„œ, 합병 정렬은 효율적인, λ²”μš©μ μΈ, 비ꡐ 기반 μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. λŒ€λΆ€λΆ„μ˜ κ΅¬ν˜„μ€ μ•ˆμ •μ μΈ λΆ„λ₯˜λ₯Ό μ΄λ£¨λŠ”λ°, 이것은 κ΅¬ν˜„μ΄ μ •λ ¬λœ 좜λ ₯에 λ™μΌν•œ μš”μ†Œμ˜ μž…λ ₯ μˆœμ„œλ₯Ό μœ μ§€ν•œλ‹€λŠ” 것을 μ˜λ―Έν•œλ‹€. 합병 정렬은 1945년에 John von Neumann이 발λͺ…ν•œ λΆ„ν•  정볡 μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n log n) (일반적) -- 졜고의 μ„±λŠ₯ O(n log n) -- 평균 O(n log n) - -###### View the algorithm in [action][merge-toptal] - -### Quick(퀡 μ •λ ¬) - -![alt text][quick-image] - -From [Wikipedia][quick-wiki]: 퀡 μ •λ ¬sometimes called partition-exchange sort)은 효율적인 μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μœΌλ‘œ, λ°°μ—΄μ˜ μš”μ†Œλ₯Ό μˆœμ„œλŒ€λ‘œ μ •λ ¬ν•˜λŠ” 체계적인 방법 μ—­ν™œμ„ ν•œλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n log n) or O(n) with three-way partition -- 평균 O(n log n) - -###### View the algorithm in [action][quick-toptal] - -### Selection(선택 μ •λ ¬) - -![alt text][selection-image] - -From [Wikipedia][selection-wiki]: μ•Œκ³ λ¦¬μ¦˜ μž…λ ₯ 리슀트λ₯Ό 두 λΆ€λΆ„μœΌλ‘œ λ‚˜λˆˆλ‹€ : 첫 뢀뢄은 μ•„μ΄ν…œλ“€μ΄ 이미 μ™Όμͺ½μ—μ„œ 였λ₯Έμͺ½μœΌλ‘œ μ •λ ¬λ˜μ—ˆλ‹€. 그리고 남은 λΆ€λΆ„μ˜ μ•„μ΄ν…œλ“€μ€ λ‚˜λ¨Έμ§€ ν•­λͺ©μ„ μ°¨μ§€ν•˜λŠ” λ¦¬μŠ€νŠΈμ΄λ‹€. μ²˜μŒμ—λŠ” μ •λ ¬λœ λ¦¬μŠ€νŠΈλŠ” 곡백이고 λ‚˜λ¨Έμ§€κ°€ 전뢀이닀. 였λ₯΄μ°¨μˆœ(λ˜λŠ” λ‚΄λ¦Όμ°¨μˆœ) μ•Œκ³ λ¦¬μ¦˜μ€ κ°€μž₯ μž‘μ€ μš”μ†Œλ₯Ό μ •λ ¬λ˜μ§€ μ•Šμ€ λ¦¬μŠ€νŠΈμ—μ„œ μ°Ύκ³  정렬이 μ•ˆλœ κ°€μž₯ μ™Όμͺ½(μ •λ ¬λœ 리슀트) λ¦¬μŠ€νŠΈμ™€ λ°”κΎΌλ‹€. μ΄λ ‡κ²Œ 였λ₯Έμͺ½μœΌλ‘œ λ‚˜μ•„κ°„λ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n^2) -- 졜고의 μ„±λŠ₯ O(n^2) -- 평균 O(n^2) - -###### View the algorithm in [action][selection-toptal] - -### Shell(μ‰˜ μ •λ ¬) - -![alt text][shell-image] - -From [Wikipedia][shell-wiki]: μ‰˜ 정렬은 멀리 λ–¨μ–΄μ Έ μžˆλŠ” ν•­λͺ©μ˜ κ΅ν™˜μ„ ν—ˆμš©ν•˜λŠ” μ‚½μž… μ’…λ₯˜μ˜ μΌλ°˜ν™”μ΄λ‹€. κ·Έ μ•„μ΄λ””μ–΄λŠ” λͺ¨λ“  n번째 μš”μ†Œκ°€ μ •λ ¬λœ λͺ©λ‘μ„ μ œκ³΅ν•œλ‹€λŠ” 것을 κ³ λ €ν•˜μ—¬ μ–΄λŠ κ³³μ—μ„œλ“ μ§€ μ‹œμž‘ν•˜λ„λ‘ μš”μ†Œμ˜ λͺ©λ‘μ„ λ°°μ—΄ν•˜λŠ” 것이닀. μ΄λŸ¬ν•œ λͺ©λ‘μ€ h-sorted둜 μ•Œλ €μ Έ μžˆλ‹€. λ§ˆμ°¬κ°€μ§€λ‘œ, 각각 κ°œλ³„μ μœΌλ‘œ μ •λ ¬λœ h μΈν„°λ¦¬λΈŒ λͺ©λ‘μœΌλ‘œ κ°„μ£Όν•  수 μžˆλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(nlog2 2n) -- 졜고의 μ„±λŠ₯ O(n log n) -- Average case performance depends on gap sequence - -###### View the algorithm in [action][shell-toptal] - -### μ‹œκ°„ λ³΅μž‘μ„± κ·Έλž˜ν”„ - -μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ˜ λ³΅μž‘μ„± 비ꡐ (버블 μ •λ ¬, μ‚½μž… μ •λ ¬, 선택 μ •λ ¬) - -[λ³΅μž‘μ„± κ·Έλž˜ν”„](https://github.com/prateekiiest/Python/blob/master/sorts/sortinggraphs.png) - ---- - -## 검색 μ•Œκ³ λ¦¬μ¦˜ - -### Linear (μ„ ν˜• 탐색) - -![alt text][linear-image] - -From [Wikipedia][linear-wiki]: μ„ ν˜• 탐색 λ˜λŠ” 순차 탐색은 λͺ©λ‘ λ‚΄μ—μ„œ λͺ©ν‘œκ°’을 μ°ΎλŠ” 방법이닀. 일치 ν•­λͺ©μ΄ λ°œκ²¬λ˜κ±°λ‚˜ λͺ¨λ“  μš”μ†Œκ°€ 탐색될 λ•ŒκΉŒμ§€ λͺ©λ‘μ˜ 각 μš”μ†Œμ— λŒ€ν•΄ λͺ©ν‘œκ°’을 순차적으둜 κ²€μ‚¬ν•œλ‹€. -μ„ ν˜• 검색은 μ΅œμ•…μ˜ μ„ ν˜• μ‹œκ°„μœΌλ‘œ μ‹€ν–‰λ˜λ©° μ΅œλŒ€ n개의 λΉ„κ΅μ—μ„œ 이루어진닀. μ—¬κΈ°μ„œ n은 λͺ©λ‘μ˜ 길이닀. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(n) -- 졜고의 μ„±λŠ₯ O(1) -- 평균 O(n) -- μ΅œμ•…μ˜ 경우 곡간 λ³΅μž‘μ„± O(1) iterative - -### Binary (이진 탐색) - -![alt text][binary-image] - -From [Wikipedia][binary-wiki]: 이진 탐색, (also known as half-interval search or logarithmic search), 은 μ •λ ¬λœ λ°°μ—΄ λ‚΄μ—μ„œ λͺ©ν‘œκ°’μ˜ μœ„μΉ˜λ₯Ό μ°ΎλŠ” 검색 μ•Œκ³ λ¦¬μ¦˜μ΄λ‹€. λͺ©ν‘œκ°’을 λ°°μ—΄μ˜ 쀑간 μš”μ†Œμ™€ λΉ„κ΅ν•œλ‹€; λ§Œμ•½ λͺ©ν‘œκ°’이 λ™μΌν•˜μ§€ μ•ŠμœΌλ©΄, λͺ©ν‘œλ¬Όμ˜ 절반이 제거되고 검색이 성곡할 λ•ŒκΉŒμ§€ λ‚˜λ¨Έμ§€ μ ˆλ°˜μ—μ„œ μ†λœλ‹€. - -**속성** - -- μ΅œμ•…μ˜ μ„±λŠ₯ O(log n) -- 졜고의 μ„±λŠ₯ O(1) -- 평균 O(log n) -- μ΅œμ•…μ˜ 경우 곡간 λ³΅μž‘μ„± O(1) - -[bubble-toptal]: https://www.toptal.com/developers/sorting-algorithms/bubble-sort -[bubble-wiki]: https://en.wikipedia.org/wiki/Bubble_sort -[bubble-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Bubblesort-edited-color.svg/220px-Bubblesort-edited-color.svg.png "Bubble Sort" -[insertion-toptal]: https://www.toptal.com/developers/sorting-algorithms/insertion-sort -[insertion-wiki]: https://en.wikipedia.org/wiki/Insertion_sort -[insertion-image]: https://upload.wikimedia.org/wikipedia/commons/7/7e/Insertionsort-edited.png "Insertion Sort" -[quick-toptal]: https://www.toptal.com/developers/sorting-algorithms/quick-sort -[quick-wiki]: https://en.wikipedia.org/wiki/Quicksort -[quick-image]: https://upload.wikimedia.org/wikipedia/commons/6/6a/Sorting_quicksort_anim.gif "Quick Sort" -[merge-toptal]: https://www.toptal.com/developers/sorting-algorithms/merge-sort -[merge-wiki]: https://en.wikipedia.org/wiki/Merge_sort -[merge-image]: https://upload.wikimedia.org/wikipedia/commons/c/cc/Merge-sort-example-300px.gif "Merge Sort" -[selection-toptal]: https://www.toptal.com/developers/sorting-algorithms/selection-sort -[selection-wiki]: https://en.wikipedia.org/wiki/Selection_sort -[selection-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Selection_sort_animation.gif/250px-Selection_sort_animation.gif "Selection Sort Sort" -[shell-toptal]: https://www.toptal.com/developers/sorting-algorithms/shell-sort -[shell-wiki]: https://en.wikipedia.org/wiki/Shellsort -[shell-image]: https://upload.wikimedia.org/wikipedia/commons/d/d8/Sorting_shellsort_anim.gif "Shell Sort" -[linear-wiki]: https://en.wikipedia.org/wiki/Linear_search -[linear-image]: http://www.tutorialspoint.com/data_structures_algorithms/images/linear_search.gif -[binary-wiki]: https://en.wikipedia.org/wiki/Binary_search_algorithm -[binary-image]: https://upload.wikimedia.org/wikipedia/commons/f/f7/Binary_search_into_array.png - ---- - -## λ‚˜λ¨Έμ§€ μ•Œκ³ λ¦¬μ¦˜μ— λŒ€ν•œ 링크 - -| μ „ν™˜ | λ‹€μ΄λ‚˜λ―Ήν”„λ‘œκ·Έλž˜λ°(DP) | μ•”ν˜Έ | κ·Έ μ™Έ 것듀 | -| --------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | -| [Any Base to Any Base](Conversions/AnyBaseToAnyBase.java) | [Coin Change](DynamicProgramming/CoinChange.java) | [Caesar](Ciphers/Caesar.java) | [Heap Sort](Sorts/HeapSort.java) | -| [Any Base to Decimal](Conversions/AnyBaseToDecimal.java) | [Egg Dropping](DynamicProgramming/EggDropping.java) | [Columnar Transposition Cipher](Ciphers/ColumnarTranspositionCipher.java) | [Palindromic Prime Checker](Misc/PalindromePrime.java) | -| [Binary to Decimal](Conversions/BinaryToDecimal.java) | [Fibonacci](DynamicProgramming/Fibonacci.java) | [RSA](Ciphers/RSA.java) | More soon... | -| [Binary to HexaDecimal](Conversions/BinaryToHexadecimal.java) | [Kadane Algorithm](DynamicProgramming/KadaneAlgorithm.java) | more coming soon... | -| [Binary to Octal](Conversions/BinaryToOctal.java) | [Knapsack](DynamicProgramming/Knapsack.java) | -| [Decimal To Any Base](Conversions/DecimalToAnyBase.java) | [Longest Common Subsequence](DynamicProgramming/LongestCommonSubsequence.java) | -| [Decimal To Binary](Conversions/DecimalToBinary.java) | [Longest Increasing Subsequence](DynamicProgramming/LongestIncreasingSubsequence.java) | -| [Decimal To Hexadecimal](Conversions/DecimalToHexaDecimal.java) | [Rod Cutting](DynamicProgramming/RodCutting.java) | -| and much more... | and more... | - -### 자료 ꡬ쑰 - -| κ·Έλž˜ν”„ | νž™ | 리슀트 | 큐 | -| ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------- | -| | [빈 νž™ μ˜ˆμ™Έμ²˜λ¦¬](DataStructures/Heaps/EmptyHeapException.java) | [μ›ν˜• μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/CircleLinkedList.java) | [μ œλ„ˆλ¦­ μ–΄λ ˆμ΄ 리슀트 큐](DataStructures/Queues/GenericArrayListQueue.java) | -| | [νž™](DataStructures/Heaps/Heap.java) | [이쀑 μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/DoublyLinkedList.java) | [큐](DataStructures/Queues/Queues.java) | -| [κ·Έλž˜ν”„](DataStructures/Graphs/Graphs.java) | [νž™ μš”μ†Œ](DataStructures/Heaps/HeapElement.java) | [λ‹¨μˆœ μ—°κ²°λ¦¬μŠ€νŠΈ](DataStructures/Lists/SinglyLinkedList.java) | -| [크루슀칼 μ•Œκ³ λ¦¬μ¦˜](DataStructures/Graphs/Kruskal.java) | [μ΅œλŒ€νž™](DataStructures/Heaps/MaxHeap.java) | -| [ν–‰λ ¬ κ·Έλž˜ν”„](DataStructures/Graphs/MatrixGraphs.java) | [μ΅œμ†Œνž™](DataStructures/Heaps/MinHeap.java) | -| [ν”„λ¦Ό μ΅œμ†Œμ‹ μž₯트리](DataStructures/Graphs/PrimMST.java) | - -| μŠ€νƒ | 트리 | -| --------------------------------------------------------------- | ------------------------------------------------- | -| [λ…Έλ“œ μŠ€νƒ](DataStructures/Stacks/NodeStack.java) | [AVL 트리](DataStructures/Trees/AVLTree.java) | -| [μ—°κ²°λ¦¬μŠ€νŠΈ μŠ€νƒ](DataStructures/Stacks/StackOfLinkedList.java) | [이진 트리](DataStructures/Trees/BinaryTree.java) | -| [μŠ€νƒ](DataStructures/Stacks) | And much more... | - -- [Bags](DataStructures/Bags/Bag.java) -- [Buffer](DataStructures/Buffers/CircularBuffer.java) -- [HashMap](DataStructures/HashMap/Hashing/HashMap.java) -- diff --git a/pmd-exclude.properties b/pmd-exclude.properties index a3c95b12fa4b..26653d9dc17b 100644 --- a/pmd-exclude.properties +++ b/pmd-exclude.properties @@ -53,14 +53,11 @@ com.thealgorithms.maths.Gaussian=UselessParentheses com.thealgorithms.maths.GcdSolutionWrapper=UselessParentheses com.thealgorithms.maths.HeronsFormula=UselessParentheses com.thealgorithms.maths.JugglerSequence=UselessMainMethod -com.thealgorithms.maths.KaprekarNumbers=UselessParentheses com.thealgorithms.maths.KeithNumber=UselessMainMethod,UselessParentheses -com.thealgorithms.maths.LeonardoNumber=UselessParentheses com.thealgorithms.maths.LinearDiophantineEquationsSolver=UselessMainMethod,UselessParentheses com.thealgorithms.maths.MagicSquare=UselessMainMethod com.thealgorithms.maths.PiNilakantha=UselessMainMethod com.thealgorithms.maths.Prime.PrimeCheck=UselessMainMethod -com.thealgorithms.maths.PythagoreanTriple=UselessMainMethod com.thealgorithms.maths.RomanNumeralUtil=UselessParentheses com.thealgorithms.maths.SecondMinMax=UselessParentheses com.thealgorithms.maths.SecondMinMaxTest=UnnecessaryFullyQualifiedName @@ -71,7 +68,6 @@ com.thealgorithms.maths.TrinomialTriangle=UselessMainMethod,UselessParentheses com.thealgorithms.maths.VectorCrossProduct=UselessMainMethod com.thealgorithms.maths.Volume=UselessParentheses com.thealgorithms.matrix.RotateMatrixBy90Degrees=UselessMainMethod -com.thealgorithms.misc.Sparsity=UselessParentheses com.thealgorithms.others.BankersAlgorithm=UselessMainMethod com.thealgorithms.others.BrianKernighanAlgorithm=UselessMainMethod com.thealgorithms.others.CRC16=UselessMainMethod,UselessParentheses @@ -79,11 +75,9 @@ com.thealgorithms.others.CRC32=UselessMainMethod com.thealgorithms.others.Damm=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.others.Dijkstra=UselessMainMethod com.thealgorithms.others.GaussLegendre=UselessMainMethod -com.thealgorithms.others.HappyNumbersSeq=UselessMainMethod com.thealgorithms.others.Huffman=UselessMainMethod com.thealgorithms.others.InsertDeleteInArray=UselessMainMethod com.thealgorithms.others.KochSnowflake=UselessMainMethod -com.thealgorithms.others.Krishnamurthy=UselessMainMethod com.thealgorithms.others.LinearCongruentialGenerator=UselessMainMethod com.thealgorithms.others.Luhn=UnnecessaryFullyQualifiedName,UselessMainMethod com.thealgorithms.others.Mandelbrot=UselessMainMethod,UselessParentheses @@ -92,9 +86,8 @@ com.thealgorithms.others.MosAlgorithm=UselessMainMethod com.thealgorithms.others.PageRank=UselessMainMethod,UselessParentheses com.thealgorithms.others.PerlinNoise=UselessMainMethod,UselessParentheses com.thealgorithms.others.QueueUsingTwoStacks=UselessParentheses -com.thealgorithms.others.Trieac=UselessMainMethod,UselessParentheses +com.thealgorithms.datastructures.trees.TrieAutocomplete=UselessMainMethod,UselessParentheses com.thealgorithms.others.Verhoeff=UnnecessaryFullyQualifiedName,UselessMainMethod -com.thealgorithms.puzzlesandgames.Sudoku=UselessMainMethod com.thealgorithms.recursion.DiceThrower=UselessMainMethod com.thealgorithms.searches.HowManyTimesRotated=UselessMainMethod com.thealgorithms.searches.InterpolationSearch=UselessParentheses @@ -108,15 +101,11 @@ com.thealgorithms.sorts.MergeSortNoExtraSpace=UselessParentheses com.thealgorithms.sorts.RadixSort=UselessParentheses com.thealgorithms.sorts.TreeSort=UselessMainMethod com.thealgorithms.sorts.WiggleSort=UselessParentheses -com.thealgorithms.stacks.LargestRectangle=UselessMainMethod com.thealgorithms.stacks.MaximumMinimumWindow=UselessMainMethod com.thealgorithms.stacks.PostfixToInfix=UselessParentheses -com.thealgorithms.strings.Alphabetical=UselessMainMethod com.thealgorithms.strings.HorspoolSearch=UnnecessaryFullyQualifiedName,UselessParentheses -com.thealgorithms.strings.KMP=UselessMainMethod com.thealgorithms.strings.Lower=UselessMainMethod com.thealgorithms.strings.Palindrome=UselessParentheses com.thealgorithms.strings.Pangram=UselessMainMethod -com.thealgorithms.strings.RabinKarp=UselessMainMethod com.thealgorithms.strings.Rotation=UselessMainMethod com.thealgorithms.strings.Upper=UselessMainMethod diff --git a/pom.xml b/pom.xml index 73869d6dd942..df36cfb6cdc7 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ UTF-8 21 21 - 3.27.6 + 3.27.7 @@ -20,7 +20,7 @@ org.junit junit-bom - 6.0.1 + 6.1.3 pom import @@ -42,7 +42,7 @@ org.mockito mockito-core - 5.21.0 + 5.23.0 test @@ -53,7 +53,7 @@ org.apache.commons commons-collections4 - 4.5.0 + 4.6.0 @@ -61,7 +61,7 @@ maven-surefire-plugin - 3.5.4 + 3.5.6 @@ -69,7 +69,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.1 + 3.15.0 21 @@ -82,7 +82,7 @@ org.jacoco jacoco-maven-plugin - 0.8.14 + 0.8.15 @@ -112,14 +112,14 @@ com.puppycrawl.tools checkstyle - 13.0.0 + 13.10.0 com.github.spotbugs spotbugs-maven-plugin - 4.9.8.2 + 4.10.3.0 spotbugs-exclude.xml true @@ -127,7 +127,7 @@ com.mebigfatguy.fb-contrib fb-contrib - 7.7.2 + 7.7.4 com.h3xstream.findsecbugs diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 3e2f1ff84ca8..1c34e47fb651 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -8,18 +8,9 @@ - - - - - - - - - @@ -32,15 +23,9 @@ - - - - - - @@ -50,15 +35,9 @@ - - - - - - @@ -71,9 +50,6 @@ - - - @@ -84,7 +60,10 @@ - + + + + @@ -114,12 +93,6 @@ - - - - - - @@ -129,9 +102,6 @@ - - - @@ -150,9 +120,6 @@ - - - @@ -189,15 +156,9 @@ - - - - - - @@ -205,7 +166,10 @@ - + + + + diff --git a/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java b/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java index 0dd23e937953..4a9e954bd202 100644 --- a/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java +++ b/src/main/java/com/thealgorithms/audiofilters/EMAFilter.java @@ -3,16 +3,19 @@ /** * Exponential Moving Average (EMA) Filter for smoothing audio signals. * - *

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

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

Based on the definition from + *

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

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

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

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

Example: + *

+ *   maze = { {1, 0, 0, 0},
+ *            {1, 1, 0, 1},
+ *            {0, 1, 0, 0},
+ *            {0, 1, 1, 1} }
+ *   Output: ["DDRDRR", "DRDDRR"]  (two valid paths)
+ * 
+ * + * @see Maze solving algorithm + * @author the-Sunny-Sharma (GitHub) + */ +public final class RatInAMaze { + + private RatInAMaze() { + } + + /** + * Finds all paths from the top-left to the bottom-right of the given maze. + * + * @param maze an {@code n x n} binary matrix where {@code 1} = open, {@code 0} = blocked + * @return a sorted list of all valid path strings using directions D, L, R, U; + * an empty list if no path exists + * @throws IllegalArgumentException if the maze is null, empty, or not square + */ + public static List findPaths(final int[][] maze) { + if (maze == null || maze.length == 0) { + throw new IllegalArgumentException("Maze must not be null or empty."); + } + int n = maze.length; + for (int[] row : maze) { + if (row.length != n) { + throw new IllegalArgumentException("Maze must be a square (n x n) matrix."); + } + } + List results = new ArrayList<>(); + if (maze[0][0] == 0 || maze[n - 1][n - 1] == 0) { + return results; + } + boolean[][] visited = new boolean[n][n]; + solve(maze, 0, 0, n, "", visited, results); + return results; + } + + /** + * Recursive backtracking helper that explores all four directions. + * + * @param maze the binary maze + * @param row current row position + * @param col current column position + * @param n maze dimension + * @param path path string built so far + * @param visited tracks visited cells for the current path + * @param results accumulates complete paths + */ + private static void solve(final int[][] maze, final int row, final int col, final int n, final String path, final boolean[][] visited, final List results) { + // Base case: reached destination + if (row == n - 1 && col == n - 1) { + results.add(path); + return; + } + + // Mark current cell as visited + visited[row][col] = true; + + // Explore in alphabetical order: Down, Left, Right, Up + // Down + if (isSafe(maze, row + 1, col, n, visited)) { + solve(maze, row + 1, col, n, path + 'D', visited, results); + } + // Left + if (isSafe(maze, row, col - 1, n, visited)) { + solve(maze, row, col - 1, n, path + 'L', visited, results); + } + // Right + if (isSafe(maze, row, col + 1, n, visited)) { + solve(maze, row, col + 1, n, path + 'R', visited, results); + } + // Up + if (isSafe(maze, row - 1, col, n, visited)) { + solve(maze, row - 1, col, n, path + 'U', visited, results); + } + + // Backtrack: unmark current cell + visited[row][col] = false; + } + + /** + * Checks whether moving to {@code (row, col)} is valid. + * + * @param maze the binary maze + * @param row target row + * @param col target column + * @param n maze dimension + * @param visited tracks visited cells for the current path + * @return {@code true} if the cell is within bounds, open, and not yet visited + */ + private static boolean isSafe(final int[][] maze, final int row, final int col, final int n, final boolean[][] visited) { + return row >= 0 && row < n && col >= 0 && col < n && maze[row][col] == 1 && !visited[row][col]; + } +} diff --git a/src/main/java/com/thealgorithms/backtracking/WordSearch.java b/src/main/java/com/thealgorithms/backtracking/WordSearch.java index 174ca90ccaab..452f17b6ace6 100644 --- a/src/main/java/com/thealgorithms/backtracking/WordSearch.java +++ b/src/main/java/com/thealgorithms/backtracking/WordSearch.java @@ -35,22 +35,6 @@ * - Stack space for the recursive DFS function, where L is the maximum depth of recursion (length of the word). */ public class WordSearch { - private final int[] dx = {0, 0, 1, -1}; - private final int[] dy = {1, -1, 0, 0}; - private boolean[][] visited; - private char[][] board; - private String word; - - /** - * Checks if the given (x, y) coordinates are valid positions in the board. - * - * @param x The row index. - * @param y The column index. - * @return True if the coordinates are within the bounds of the board; false otherwise. - */ - private boolean isValid(int x, int y) { - return x >= 0 && x < board.length && y >= 0 && y < board[0].length; - } /** * Performs Depth First Search (DFS) from the cell (x, y) @@ -58,28 +42,27 @@ private boolean isValid(int x, int y) { * * @param x The current row index. * @param y The current column index. - * @param nextIdx The index of the next character in the word to be matched. + * @param idx The index of the next character in the word to be matched. * @return True if a valid path is found to match the remaining characters of the word; false otherwise. */ - private boolean doDFS(int x, int y, int nextIdx) { - visited[x][y] = true; - if (nextIdx == word.length()) { + + private boolean dfs(char[][] board, int x, int y, String word, int idx) { + if (idx == word.length()) { return true; } - for (int i = 0; i < 4; ++i) { - int xi = x + dx[i]; - int yi = y + dy[i]; - if (isValid(xi, yi) && board[xi][yi] == word.charAt(nextIdx) && !visited[xi][yi]) { - boolean exists = doDFS(xi, yi, nextIdx + 1); - if (exists) { - return true; - } - } + if (x < 0 || y < 0 || x >= board.length || y >= board[0].length || board[x][y] != word.charAt(idx)) { + return false; } - visited[x][y] = false; // Backtrack - return false; + char temp = board[x][y]; + board[x][y] = '#'; + + boolean found = dfs(board, x + 1, y, word, idx + 1) || dfs(board, x - 1, y, word, idx + 1) || dfs(board, x, y + 1, word, idx + 1) || dfs(board, x, y - 1, word, idx + 1); + + board[x][y] = temp; + + return found; } /** @@ -90,20 +73,21 @@ private boolean doDFS(int x, int y, int nextIdx) { * @param word The target word to search for in the board. * @return True if the word exists in the board; false otherwise. */ + public boolean exist(char[][] board, String word) { - this.board = board; - this.word = word; - for (int i = 0; i < board.length; ++i) { - for (int j = 0; j < board[0].length; ++j) { - if (board[i][j] == word.charAt(0)) { - visited = new boolean[board.length][board[0].length]; - boolean exists = doDFS(i, j, 1); - if (exists) { - return true; - } + + int m = board.length; + int n = board[0].length; + + // DFS search + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == word.charAt(0) && dfs(board, i, j, word, 0)) { + return true; } } } + return false; } } diff --git a/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java b/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java index 0d6fd140c720..5038d44079ec 100644 --- a/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java +++ b/src/main/java/com/thealgorithms/bitmanipulation/BinaryPalindromeCheck.java @@ -9,6 +9,12 @@ *

* * @author Hardvan + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ public final class BinaryPalindromeCheck { private BinaryPalindromeCheck() { diff --git a/src/main/java/com/thealgorithms/ciphers/AESEncryption.java b/src/main/java/com/thealgorithms/ciphers/AESEncryption.java index 14582205442f..6f155e73f47d 100644 --- a/src/main/java/com/thealgorithms/ciphers/AESEncryption.java +++ b/src/main/java/com/thealgorithms/ciphers/AESEncryption.java @@ -38,7 +38,7 @@ public static void main(String[] args) throws Exception { System.out.println("Original Text:" + plainText); System.out.println("AES Key (Hex Form):" + bytesToHex(secKey.getEncoded())); System.out.println("Encrypted Text (Hex Form):" + bytesToHex(cipherText)); - System.out.println("Descrypted Text:" + decryptedText); + System.out.println("Decryption successful. Decrypted text matches original: " + decryptedText.equals(plainText)); } /** diff --git a/src/main/java/com/thealgorithms/ciphers/Caesar.java b/src/main/java/com/thealgorithms/ciphers/Caesar.java index 23535bc2b5d2..7a5e70e6eb78 100644 --- a/src/main/java/com/thealgorithms/ciphers/Caesar.java +++ b/src/main/java/com/thealgorithms/ciphers/Caesar.java @@ -9,8 +9,8 @@ * @author khalil2535 */ public class Caesar { - private static char normalizeShift(final int shift) { - return (char) (shift % 26); + private static int normalizeShift(final int shift) { + return ((shift % 26) + 26) % 26; } /** @@ -22,21 +22,18 @@ private static char normalizeShift(final int shift) { public String encode(String message, int shift) { StringBuilder encoded = new StringBuilder(); - final char shiftChar = normalizeShift(shift); + final int shiftChar = normalizeShift(shift); final int length = message.length(); for (int i = 0; i < length; i++) { - // int current = message.charAt(i); //using char to shift characters because - // ascii - // is in-order latin alphabet - char current = message.charAt(i); // Java law : char + int = char + final char current = message.charAt(i); if (isCapitalLatinLetter(current)) { - current += shiftChar; - encoded.append((char) (current > 'Z' ? current - 26 : current)); // 26 = number of latin letters + final int shifted = current + shiftChar; + encoded.append((char) (shifted > 'Z' ? shifted - 26 : shifted)); // 26 = number of latin letters } else if (isSmallLatinLetter(current)) { - current += shiftChar; - encoded.append((char) (current > 'z' ? current - 26 : current)); // 26 = number of latin letters + final int shifted = current + shiftChar; + encoded.append((char) (shifted > 'z' ? shifted - 26 : shifted)); // 26 = number of latin letters } else { encoded.append(current); } @@ -53,17 +50,17 @@ public String encode(String message, int shift) { public String decode(String encryptedMessage, int shift) { StringBuilder decoded = new StringBuilder(); - final char shiftChar = normalizeShift(shift); + final int shiftChar = normalizeShift(shift); final int length = encryptedMessage.length(); for (int i = 0; i < length; i++) { - char current = encryptedMessage.charAt(i); + final char current = encryptedMessage.charAt(i); if (isCapitalLatinLetter(current)) { - current -= shiftChar; - decoded.append((char) (current < 'A' ? current + 26 : current)); // 26 = number of latin letters + final int shifted = current - shiftChar; + decoded.append((char) (shifted < 'A' ? shifted + 26 : shifted)); // 26 = number of latin letters } else if (isSmallLatinLetter(current)) { - current -= shiftChar; - decoded.append((char) (current < 'a' ? current + 26 : current)); // 26 = number of latin letters + final int shifted = current - shiftChar; + decoded.append((char) (shifted < 'a' ? shifted + 26 : shifted)); // 26 = number of latin letters } else { decoded.append(current); } diff --git a/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java new file mode 100644 index 000000000000..6383caa59b1f --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java @@ -0,0 +1,174 @@ +package com.thealgorithms.ciphers; + +import java.math.BigInteger; +import java.security.SecureRandom; + +/** + * ElGamal Encryption Algorithm Implementation. + * + *

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

+ * + *

+ * Key Features: + *

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

+ * + * @author Chahat Sandhu, singhc7 + * @see ElGamal Encryption (Wikipedia) + * @see Safe Primes + */ +public final class ElGamalCipher { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int PRIME_CERTAINTY = 40; + private static final int MIN_BIT_LENGTH = 256; + + private ElGamalCipher() { + } + + /** + * A container for the Public and Private keys. + * + * @param p The prime modulus. + * @param g The generator (primitive root). + * @param y The public key component (g^x mod p). + * @param x The private key. + */ + public record KeyPair(BigInteger p, BigInteger g, BigInteger y, BigInteger x) { + } + + /** + * Container for the encryption result. + * + * @param a The first component (g^k mod p). + * @param b The second component (y^k * m mod p). + */ + public record CipherText(BigInteger a, BigInteger b) { + } + + /** + * Generates a valid ElGamal KeyPair using a Safe Prime. + * + * @param bitLength The bit length of the prime modulus p. Must be at least 256. + * @return A valid KeyPair (p, g, y, x). + * @throws IllegalArgumentException if bitLength is too small. + */ + public static KeyPair generateKeys(int bitLength) { + if (bitLength < MIN_BIT_LENGTH) { + throw new IllegalArgumentException("Bit length must be at least " + MIN_BIT_LENGTH + " for security."); + } + + BigInteger p; + BigInteger q; + BigInteger g; + BigInteger x; + BigInteger y; + + // Generate Safe Prime p = 2q + 1 + do { + q = new BigInteger(bitLength - 1, PRIME_CERTAINTY, RANDOM); + p = q.multiply(BigInteger.TWO).add(BigInteger.ONE); + } while (!p.isProbablePrime(PRIME_CERTAINTY)); + + // Find a Generator g (Primitive Root modulo p) + do { + g = new BigInteger(bitLength, RANDOM).mod(p.subtract(BigInteger.TWO)).add(BigInteger.TWO); + } while (!isValidGenerator(g, p, q)); + + // Generate Private Key x in range [2, p-2] + do { + x = new BigInteger(bitLength, RANDOM); + } while (x.compareTo(BigInteger.TWO) < 0 || x.compareTo(p.subtract(BigInteger.TWO)) > 0); + + // Compute Public Key y = g^x mod p + y = g.modPow(x, p); + + return new KeyPair(p, g, y, x); + } + + /** + * Encrypts a message using the public key. + * + * @param message The message converted to BigInteger. + * @param p The prime modulus. + * @param g The generator. + * @param y The public key component. + * @return The CipherText pair (a, b). + * @throws IllegalArgumentException if inputs are null, negative, or message >= p. + */ + public static CipherText encrypt(BigInteger message, BigInteger p, BigInteger g, BigInteger y) { + if (message == null || p == null || g == null || y == null) { + throw new IllegalArgumentException("Inputs cannot be null."); + } + if (message.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Message must be non-negative."); + } + if (message.compareTo(p) >= 0) { + throw new IllegalArgumentException("Message must be smaller than the prime modulus p."); + } + + BigInteger k; + BigInteger pMinus1 = p.subtract(BigInteger.ONE); + + // Select ephemeral key k such that 1 < k < p-1 and gcd(k, p-1) = 1 + do { + k = new BigInteger(p.bitLength(), RANDOM); + } while (k.compareTo(BigInteger.ONE) <= 0 || k.compareTo(pMinus1) >= 0 || !k.gcd(pMinus1).equals(BigInteger.ONE)); + + BigInteger a = g.modPow(k, p); + BigInteger b = y.modPow(k, p).multiply(message).mod(p); + + return new CipherText(a, b); + } + + /** + * Decrypts a ciphertext using the private key. + * + * @param cipher The CipherText (a, b). + * @param x The private key. + * @param p The prime modulus. + * @return The decrypted message as BigInteger. + * @throws IllegalArgumentException if inputs are null. + */ + public static BigInteger decrypt(CipherText cipher, BigInteger x, BigInteger p) { + if (cipher == null || x == null || p == null) { + throw new IllegalArgumentException("Inputs cannot be null."); + } + + BigInteger a = cipher.a(); + BigInteger b = cipher.b(); + + BigInteger s = a.modPow(x, p); + BigInteger sInverse = s.modInverse(p); + + return b.multiply(sInverse).mod(p); + } + + /** + * Verifies if g is a valid generator for safe prime p = 2q + 1. + * + * @param g The candidate generator. + * @param p The safe prime. + * @param q The Sophie Germain prime (p-1)/2. + * @return True if g is a primitive root, False otherwise. + */ + private static boolean isValidGenerator(BigInteger g, BigInteger p, BigInteger q) { + // Fix: Must use braces {} for all if statements + if (g.equals(BigInteger.ONE)) { + return false; + } + if (g.modPow(BigInteger.TWO, p).equals(BigInteger.ONE)) { + return false; + } + return !g.modPow(q, p).equals(BigInteger.ONE); + } +} diff --git a/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java b/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java index f81252980468..324dc88e4f19 100644 --- a/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java +++ b/src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java @@ -1,7 +1,5 @@ package com.thealgorithms.ciphers; -import java.util.Arrays; - /** * The rail fence cipher (also called a zigzag cipher) is a classical type of transposition cipher. * It derives its name from the manner in which encryption is performed, in analogy to a fence built with horizontal rails. @@ -14,28 +12,27 @@ public class RailFenceCipher { // Encrypts the input string using the rail fence cipher method with the given number of rails. public String encrypt(String str, int rails) { + checkInput(str, rails); + // Base case of single rail or rails are more than the number of characters in the string if (rails == 1 || rails >= str.length()) { return str; } - // Boolean flag to determine if the movement is downward or upward in the rail matrix. + // Boolean flag to determine if the movement is downward or upward in the rail pattern. boolean down = true; - // Create a 2D array to represent the rails (rows) and the length of the string (columns). - char[][] strRail = new char[rails][str.length()]; - - // Initialize all positions in the rail matrix with a placeholder character ('\n'). + // Collect the characters of every rail separately. Using one buffer per rail (instead of a + // rails x length matrix with a placeholder character) keeps every character of the input, + // including characters that would otherwise be indistinguishable from the placeholder. + StringBuilder[] railBuffers = new StringBuilder[rails]; for (int i = 0; i < rails; i++) { - Arrays.fill(strRail[i], '\n'); + railBuffers[i] = new StringBuilder(); } - int row = 0; // Start at the first row - int col = 0; // Start at the first column + int row = 0; // Start at the first rail - int i = 0; - - // Fill the rail matrix with characters from the string based on the rail pattern. - while (col < str.length()) { + // Distribute the characters of the string over the rails following the zigzag pattern. + for (int i = 0; i < str.length(); i++) { // Change direction to down when at the first row. if (row == 0) { down = true; @@ -45,33 +42,28 @@ else if (row == rails - 1) { down = false; } - // Place the character in the current position of the rail matrix. - strRail[row][col] = str.charAt(i); - col++; // Move to the next column. + // Append the character to the rail it belongs to. + railBuffers[row].append(str.charAt(i)); // Move to the next row based on the direction. if (down) { row++; } else { row--; } - - i++; } - // Construct the encrypted string by reading characters row by row. - StringBuilder encryptedString = new StringBuilder(); - for (char[] chRow : strRail) { - for (char ch : chRow) { - if (ch != '\n') { - encryptedString.append(ch); - } - } + // Construct the encrypted string by reading the rails top to bottom. + StringBuilder encryptedString = new StringBuilder(str.length()); + for (StringBuilder railBuffer : railBuffers) { + encryptedString.append(railBuffer); } return encryptedString.toString(); } // Decrypts the input string using the rail fence cipher method with the given number of rails. public String decrypt(String str, int rails) { + checkInput(str, rails); + // Base case of single rail or rails are more than the number of characters in the string if (rails == 1 || rails >= str.length()) { return str; @@ -144,4 +136,14 @@ else if (row == rails - 1) { return decryptedString.toString(); } + + // Rejects inputs the zigzag pattern is not defined for. + private static void checkInput(String str, int rails) { + if (str == null) { + throw new IllegalArgumentException("Input string must not be null"); + } + if (rails <= 0) { + throw new IllegalArgumentException("Number of rails must be positive, but was " + rails); + } + } } diff --git a/src/main/java/com/thealgorithms/compression/HuffmanCoding.java b/src/main/java/com/thealgorithms/compression/HuffmanCoding.java new file mode 100644 index 000000000000..d7f9d58d2429 --- /dev/null +++ b/src/main/java/com/thealgorithms/compression/HuffmanCoding.java @@ -0,0 +1,253 @@ +package com.thealgorithms.compression; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.PriorityQueue; + +/** + * Huffman Coding Compression Algorithm Implementation. + *

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

+ *

+ * Key Features: + *

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

+ * @author Chahat Sandhu, singhc7 + * @see Huffman Coding (Wikipedia) + */ +public class HuffmanCoding { + + private Node root; + private final Map huffmanCodes; + + /** + * Represents a node within the Huffman Tree. + * Implements {@link Comparable} to allow sorting by frequency in a PriorityQueue. + */ + private static class Node implements Comparable { + final char ch; + final int freq; + final Node left; + final Node right; + + /** + * Constructs a leaf node containing a specific character and its frequency. + * + * @param ch The character stored in this leaf. + * @param freq The frequency of occurrence of the character. + */ + Node(char ch, int freq) { + this.ch = ch; + this.freq = freq; + this.left = null; + this.right = null; + } + + /** + * Constructs an internal node that merges two child nodes. + * The character is defaulted to the null character ('\0'). + * + * @param freq The combined frequency of the left and right child nodes. + * @param left The left child node. + * @param right The right child node. + */ + Node(int freq, Node left, Node right) { + this.ch = '\0'; + this.freq = freq; + this.left = left; + this.right = right; + } + + /** + * Determines if the current node is a leaf (contains no children). + * + * @return {@code true} if both left and right children are null, {@code false} otherwise. + */ + boolean isLeaf() { + return left == null && right == null; + } + + /** + * Compares this node with another node based on their frequencies. + * Used by the PriorityQueue to maintain the min-heap property. + * + * @param other The other Node to compare against. + * @return A negative integer, zero, or a positive integer as this node's frequency + * is less than, equal to, or greater than the specified node's frequency. + */ + @Override + public int compareTo(Node other) { + return Integer.compare(this.freq, other.freq); + } + } + + /** + * Initializes the Huffman Tree and generates immutable prefix-free codes + * based on the character frequencies in the provided text. + * + * @param text The input string used to calculate frequencies and build the optimal tree. + * If null or empty, an empty tree and dictionary are created. + */ + public HuffmanCoding(String text) { + if (text == null || text.isEmpty()) { + this.huffmanCodes = Collections.emptyMap(); + return; + } + + Map tempCodes = new HashMap<>(); + buildTree(text); + generateCodes(root, "", tempCodes); + + if (tempCodes.size() == 1) { + tempCodes.put(root.ch, "0"); + } + + this.huffmanCodes = Collections.unmodifiableMap(tempCodes); + } + + /** + * Computes character frequencies and constructs the Huffman Tree using a min-heap. + * The optimal tree is built by repeatedly extracting the two lowest-frequency nodes + * and merging them until a single root node remains. + * + * @param text The input text to analyze. + */ + private void buildTree(String text) { + Map freqMap = new HashMap<>(); + for (char c : text.toCharArray()) { + freqMap.put(c, freqMap.getOrDefault(c, 0) + 1); + } + + PriorityQueue pq = new PriorityQueue<>(); + for (Map.Entry entry : freqMap.entrySet()) { + pq.add(new Node(entry.getKey(), entry.getValue())); + } + + while (pq.size() > 1) { + Node left = pq.poll(); + Node right = pq.poll(); + pq.add(new Node(left.freq + right.freq, left, right)); + } + + root = pq.poll(); + } + + /** + * Recursively traverses the Huffman Tree to generate prefix-free binary codes. + * Left traversals append a '0' to the code, while right traversals append a '1'. + * + * @param node The current node in the traversal. + * @param code The accumulated binary string for the current path. + * @param map The temporary dictionary to populate with the final character-to-code mappings. + */ + private void generateCodes(Node node, String code, Map map) { + if (node == null) { + return; + } + if (node.isLeaf()) { + map.put(node.ch, code); + return; + } + generateCodes(node.left, code + "0", map); + generateCodes(node.right, code + "1", map); + } + + /** + * Encodes the given plaintext string into a binary string using the generated Huffman dictionary. + * + * @param text The plaintext string to compress. + * @return A string of '0's and '1's representing the compressed data. + * Returns an empty string if the input is null or empty. + * @throws IllegalStateException If attempting to encode when the Huffman tree is empty. + * @throws IllegalArgumentException If the input text contains a character not present + * in the original text used to build the tree. + */ + public String encode(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + if (root == null) { + throw new IllegalStateException("Huffman tree is empty."); + } + + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) { + if (!huffmanCodes.containsKey(c)) { + throw new IllegalArgumentException(String.format("Character '%c' (U+%04X) not found in Huffman dictionary.", c, (int) c)); + } + sb.append(huffmanCodes.get(c)); + } + return sb.toString(); + } + + /** + * Decodes the given binary string back into the original plaintext using the Huffman Tree. + * Validates the integrity of the binary payload during traversal. + * + * @param encodedText The binary string of '0's and '1's to decompress. + * @return The reconstructed plaintext string. Returns an empty string if the input is null or empty. + * @throws IllegalStateException If attempting to decode when the Huffman tree is empty. + * @throws IllegalArgumentException If the binary string contains characters other than '0' or '1', + * or if the sequence ends abruptly without reaching a leaf node. + */ + public String decode(String encodedText) { + if (encodedText == null || encodedText.isEmpty()) { + return ""; + } + if (root == null) { + throw new IllegalStateException("Huffman tree is empty."); + } + + StringBuilder sb = new StringBuilder(); + + if (root.isLeaf()) { + for (char bit : encodedText.toCharArray()) { + if (bit != '0') { + throw new IllegalArgumentException("Invalid binary sequence for single-character tree."); + } + sb.append(root.ch); + } + return sb.toString(); + } + + Node current = root; + for (char bit : encodedText.toCharArray()) { + if (bit != '0' && bit != '1') { + throw new IllegalArgumentException("Encoded text contains invalid characters: " + bit); + } + + current = (bit == '0') ? current.left : current.right; + + if (current.isLeaf()) { + sb.append(current.ch); + current = root; + } + } + + if (current != root) { + throw new IllegalArgumentException("Malformed encoded string: incomplete sequence ending."); + } + + return sb.toString(); + } + + /** + * Retrieves the generated Huffman dictionary mapping characters to their binary codes. + * + * @return An unmodifiable map containing the character-to-binary-code mappings to prevent + * external mutation of the algorithm's state. + */ + public Map getHuffmanCodes() { + return huffmanCodes; + } +} diff --git a/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java b/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java index 7a9448fd8fe7..314e7fba38a3 100644 --- a/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java +++ b/src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java @@ -1,3 +1,4 @@ + package com.thealgorithms.conversions; import java.util.Arrays; diff --git a/src/main/java/com/thealgorithms/conversions/Base64.java b/src/main/java/com/thealgorithms/conversions/Base64.java index 5219c4ba7f4e..fb4411b399a3 100644 --- a/src/main/java/com/thealgorithms/conversions/Base64.java +++ b/src/main/java/com/thealgorithms/conversions/Base64.java @@ -119,8 +119,15 @@ public static byte[] decode(String input) { // Validate padding: '=' can only appear at the end (last 1 or 2 chars) int firstPadding = input.indexOf('='); - if (firstPadding != -1 && firstPadding < input.length() - 2) { - throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)"); + if (firstPadding != -1) { + if (firstPadding < input.length() - 2) { + throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)"); + } + for (int i = firstPadding; i < input.length(); i++) { + if (input.charAt(i) != '=') { + throw new IllegalArgumentException("A padding '=' must not be followed by a non-padding character"); + } + } } List result = new ArrayList<>(); diff --git a/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java b/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java index cd5dc580b694..dbdb2d806209 100644 --- a/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java +++ b/src/main/java/com/thealgorithms/datastructures/dynamicarray/DynamicArray.java @@ -63,7 +63,8 @@ public void add(final E element) { * * @param index the index at which the element is to be placed * @param element the element to be inserted at the specified index - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the number of elements + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the number of elements */ public void put(final int index, E element) { if (index < 0) { @@ -82,7 +83,8 @@ public void put(final int index, E element) { * * @param index the index of the element to retrieve * @return the element at the specified index - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the current size + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the current size */ @SuppressWarnings("unchecked") public E get(final int index) { @@ -97,7 +99,8 @@ public E get(final int index) { * * @param index the index of the element to be removed * @return the element that was removed from the array - * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the current size + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or + * equal to the current size */ public E remove(final int index) { if (index < 0 || index >= size) { @@ -127,6 +130,21 @@ public boolean isEmpty() { return size == 0; } + /** + * Checks whether the array contains the specified element. + * + * @param element the element to check for + * @return true if the array contains the specified element, false otherwise + */ + public boolean contains(final E element) { + for (int i = 0; i < size; i++) { + if (Objects.equals(elements[i], element)) { + return true; + } + } + return false; + } + /** * Returns a sequential stream with this collection as its source. * @@ -137,7 +155,8 @@ public Stream stream() { } /** - * Ensures that the array has enough capacity to hold the specified number of elements. + * Ensures that the array has enough capacity to hold the specified number of + * elements. * * @param minCapacity the minimum capacity required */ @@ -150,7 +169,8 @@ private void ensureCapacity(int minCapacity) { /** * Removes the element at the specified index without resizing the array. - * This method shifts any subsequent elements to the left and clears the last element. + * This method shifts any subsequent elements to the left and clears the last + * element. * * @param index the index of the element to remove */ @@ -163,7 +183,8 @@ private void fastRemove(int index) { } /** - * Returns a string representation of the array, including only the elements that are currently stored. + * Returns a string representation of the array, including only the elements + * that are currently stored. * * @return a string containing the elements in the array */ @@ -227,7 +248,9 @@ public E next() { /** * Removes the last element returned by this iterator. * - * @throws IllegalStateException if the next method has not yet been called, or the remove method has already been called after the last call to the next method + * @throws IllegalStateException if the next method has not yet been called, or + * the remove method has already been called after + * the last call to the next method */ @Override public void remove() { @@ -242,7 +265,8 @@ public void remove() { /** * Checks for concurrent modifications to the array during iteration. * - * @throws ConcurrentModificationException if the array has been modified structurally + * @throws ConcurrentModificationException if the array has been modified + * structurally */ private void checkForComodification() { if (modCount != expectedModCount) { @@ -251,7 +275,8 @@ private void checkForComodification() { } /** - * Performs the given action for each remaining element in the iterator until all elements have been processed. + * Performs the given action for each remaining element in the iterator until + * all elements have been processed. * * @param action the action to be performed for each element * @throws NullPointerException if the specified action is null diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java index 70699a9461f7..1b5f765843be 100644 --- a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java +++ b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraAlgorithm.java @@ -1,6 +1,7 @@ package com.thealgorithms.datastructures.graphs; import java.util.Arrays; +import java.util.PriorityQueue; /** * Dijkstra's algorithm for finding the shortest path from a single source vertex to all other vertices in a graph. @@ -18,6 +19,21 @@ public DijkstraAlgorithm(int vertexCount) { this.vertexCount = vertexCount; } + private static class Node implements Comparable { + int id; + int distance; + + Node(int id, int distance) { + this.id = id; + this.distance = distance; + } + + @Override + public int compareTo(Node other) { + return Integer.compare(this.distance, other.distance); + } + } + /** * Executes Dijkstra's algorithm on the provided graph to find the shortest paths from the source vertex to all other vertices. * @@ -36,18 +52,25 @@ public int[] run(int[][] graph, int source) { int[] distances = new int[vertexCount]; boolean[] processed = new boolean[vertexCount]; + PriorityQueue unprocessed = new PriorityQueue<>(); Arrays.fill(distances, Integer.MAX_VALUE); - Arrays.fill(processed, false); distances[source] = 0; + unprocessed.add(new Node(source, 0)); + + while (!unprocessed.isEmpty()) { + Node current = unprocessed.poll(); + int u = current.id; - for (int count = 0; count < vertexCount - 1; count++) { - int u = getMinDistanceVertex(distances, processed); + if (processed[u]) { + continue; + } processed[u] = true; for (int v = 0; v < vertexCount; v++) { if (!processed[v] && graph[u][v] != 0 && distances[u] != Integer.MAX_VALUE && distances[u] + graph[u][v] < distances[v]) { distances[v] = distances[u] + graph[u][v]; + unprocessed.add(new Node(v, distances[v])); } } } @@ -56,27 +79,6 @@ public int[] run(int[][] graph, int source) { return distances; } - /** - * Finds the vertex with the minimum distance value from the set of vertices that have not yet been processed. - * - * @param distances The array of current shortest distances from the source vertex. - * @param processed The array indicating whether each vertex has been processed. - * @return The index of the vertex with the minimum distance value. - */ - private int getMinDistanceVertex(int[] distances, boolean[] processed) { - int min = Integer.MAX_VALUE; - int minIndex = -1; - - for (int v = 0; v < vertexCount; v++) { - if (!processed[v] && distances[v] <= min) { - min = distances[v]; - minIndex = v; - } - } - - return minIndex; - } - /** * Prints the shortest distances from the source vertex to all other vertices. * diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java deleted file mode 100644 index a686b808a970..000000000000 --- a/src/main/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithm.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.thealgorithms.datastructures.graphs; - -import java.util.Arrays; -import java.util.Set; -import java.util.TreeSet; -import org.apache.commons.lang3.tuple.Pair; - -/** - * Dijkstra's algorithm for finding the shortest path from a single source vertex to all other vertices in a graph. - */ -public class DijkstraOptimizedAlgorithm { - - private final int vertexCount; - - /** - * Constructs a Dijkstra object with the given number of vertices. - * - * @param vertexCount The number of vertices in the graph. - */ - public DijkstraOptimizedAlgorithm(int vertexCount) { - this.vertexCount = vertexCount; - } - - /** - * Executes Dijkstra's algorithm on the provided graph to find the shortest paths from the source vertex to all other vertices. - * - * The graph is represented as an adjacency matrix where {@code graph[i][j]} represents the weight of the edge from vertex {@code i} - * to vertex {@code j}. A value of 0 indicates no edge exists between the vertices. - * - * @param graph The graph represented as an adjacency matrix. - * @param source The source vertex. - * @return An array where the value at each index {@code i} represents the shortest distance from the source vertex to vertex {@code i}. - * @throws IllegalArgumentException if the source vertex is out of range. - */ - public int[] run(int[][] graph, int source) { - if (source < 0 || source >= vertexCount) { - throw new IllegalArgumentException("Incorrect source"); - } - - int[] distances = new int[vertexCount]; - boolean[] processed = new boolean[vertexCount]; - Set> unprocessed = new TreeSet<>(); - - Arrays.fill(distances, Integer.MAX_VALUE); - Arrays.fill(processed, false); - distances[source] = 0; - unprocessed.add(Pair.of(0, source)); - - while (!unprocessed.isEmpty()) { - Pair distanceAndU = unprocessed.iterator().next(); - unprocessed.remove(distanceAndU); - int u = distanceAndU.getRight(); - processed[u] = true; - - for (int v = 0; v < vertexCount; v++) { - if (!processed[v] && graph[u][v] != 0 && distances[u] != Integer.MAX_VALUE && distances[u] + graph[u][v] < distances[v]) { - unprocessed.remove(Pair.of(distances[v], v)); - distances[v] = distances[u] + graph[u][v]; - unprocessed.add(Pair.of(distances[v], v)); - } - } - } - - return distances; - } -} diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java b/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java new file mode 100644 index 000000000000..3ce8696f55ff --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/RottingOranges.java @@ -0,0 +1,114 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Multi-source Breadth-First Search (BFS) implementation for the Rotting Oranges problem. + * + *

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

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

Given a grid where: + *

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

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

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

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

+ * + *

This method does not modify the input list.

+ * + *

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

+ * + *

Complexity:

+ *
    + *
  • Time: {@code O(n)}
  • + *
  • Space: {@code O(1)}
  • + *
+ */ +public final class MiddleOfLinkedList { + + private MiddleOfLinkedList() { + } + + /** + * Returns the middle node of the list. + * + * @param head the head of the singly linked list; may be {@code null} + * @return the middle node (second middle for even-sized lists), or {@code null} if {@code head} is {@code null} + */ + public static SinglyLinkedListNode middleNode(final SinglyLinkedListNode head) { + if (head == null) { + return null; + } + + SinglyLinkedListNode slow = head; + SinglyLinkedListNode fast = head; + + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + return slow; + } +} diff --git a/src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java similarity index 84% rename from src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java rename to src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java index c81476eaec32..7bb16921b9ef 100644 --- a/src/main/java/com/thealgorithms/misc/PalindromeSinglyLinkedList.java +++ b/src/main/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedList.java @@ -1,4 +1,4 @@ -package com.thealgorithms.misc; +package com.thealgorithms.datastructures.lists; import java.util.Stack; @@ -9,6 +9,13 @@ * * See more: * https://www.geeksforgeeks.org/function-to-check-if-a-singly-linked-list-is-palindrome/ + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber */ @SuppressWarnings("rawtypes") public final class PalindromeSinglyLinkedList { diff --git a/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java new file mode 100644 index 000000000000..200c636ce1ab --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedList.java @@ -0,0 +1,105 @@ +package com.thealgorithms.datastructures.lists; + +import java.util.Objects; + +/** + * A Self-Organizing Linked List implementation using the Move-To-Front (MTF) strategy. + * When an element is searched, it is automatically moved to the head of the list + * to optimize subsequent lookups. + * + * @param the type of elements held in this list + */ +public class SelfOrganizingLinkedList { + + /** + * Node structure for the self-organizing linked list. + * + * @param the type of element held in this node + */ + private static class Node { + E value; + Node next; + + Node(E value) { + this.value = value; + this.next = null; + } + } + + private Node head; + private int size; + + public SelfOrganizingLinkedList() { + this.size = 0; + this.head = null; + } + + /** + * Inserts a new value at the end of the list. + * + * @param value the element to add + */ + public void insert(E value) { + Node newNode = new Node<>(value); + if (head == null) { + head = newNode; + } else { + Node temp = head; + while (temp.next != null) { + temp = temp.next; + } + temp.next = newNode; + } + size++; + } + + /** + * Searches for a value in the list. + * If found, moves the node to the front (head) of the list. + * + * @param key the value to search for + * @return true if the element is present, false otherwise + */ + public boolean search(E key) { + if (head == null) { + return false; + } + // If the key is already at the head, no pointers need to be rewired + if (Objects.equals(head.value, key)) { + return true; + } + + Node prev = head; + Node curr = head.next; + + while (curr != null && !Objects.equals(curr.value, key)) { + prev = curr; + curr = curr.next; + } + + if (curr == null) { + return false; + } + + // Unlink curr from its current position and move it to head + prev.next = curr.next; + curr.next = head; + head = curr; + return true; + } + + /** Gets the current head value of the list. */ + public E getHeadValue() { + return head != null ? head.value : null; + } + + /** Returns the size of the list. */ + public int getSize() { + return size; + } + + /** Returns true if the list contains no elements. */ + public boolean isEmpty() { + return size == 0; + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java b/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java new file mode 100644 index 000000000000..79275dcefe20 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursion.java @@ -0,0 +1,28 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.Queue; + +/** + * Reverse a queue using recursion. + */ +public final class ReverseQueueRecursion { + private ReverseQueueRecursion() { + // private constructor to prevent instantiation + } + + /** + * Reverses the given queue recursively. + * + * @param queue the queue to reverse + * @param the type of elements in the queue + */ + public static void reverseQueue(final Queue queue) { + if (queue == null || queue.isEmpty()) { + return; + } + + final T front = queue.poll(); + reverseQueue(queue); + queue.add(front); + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java b/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java new file mode 100644 index 000000000000..a943b0028974 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/queues/ThreadSafeQueue.java @@ -0,0 +1,186 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @brief Thread-safe bounded queue implementation using ReentrantLock and Condition variables + * @details A blocking queue that supports multiple producers and consumers. + * Uses a circular buffer internally with lock-based synchronization to ensure + * thread safety. Producers block when the queue is full, and consumers block + * when the queue is empty. + * @see Producer-Consumer Problem + */ +public class ThreadSafeQueue { + + private final Object[] buffer; + private final int capacity; + private int head; + private int tail; + private int count; + private final ReentrantLock lock; + private final Condition notFull; + private final Condition notEmpty; + + /** + * @brief Constructs a ThreadSafeQueue with the specified capacity + * @param capacity the maximum number of elements the queue can hold + * @throws IllegalArgumentException if capacity is less than or equal to zero + */ + public ThreadSafeQueue(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero."); + } + this.capacity = capacity; + this.buffer = new Object[capacity]; + this.head = 0; + this.tail = 0; + this.count = 0; + this.lock = new ReentrantLock(); + this.notFull = lock.newCondition(); + this.notEmpty = lock.newCondition(); + } + + /** + * @brief Adds an element to the tail of the queue, blocking if full + * @param item the element to add + * @throws InterruptedException if the thread is interrupted while waiting + * @throws IllegalArgumentException if the item is null + */ + public void enqueue(T item) throws InterruptedException { + if (item == null) { + throw new IllegalArgumentException("Cannot enqueue null item."); + } + + lock.lock(); + try { + while (count == capacity) { + notFull.await(); + } + buffer[tail] = item; + tail = (tail + 1) % capacity; + count++; + notEmpty.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * @brief Removes and returns the element at the head of the queue, blocking if empty + * @return the element at the head of the queue + * @throws InterruptedException if the thread is interrupted while waiting + */ + @SuppressWarnings("unchecked") + public T dequeue() throws InterruptedException { + lock.lock(); + try { + while (count == 0) { + notEmpty.await(); + } + T item = (T) buffer[head]; + buffer[head] = null; + head = (head + 1) % capacity; + count--; + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + /** + * @brief Adds an element to the tail of the queue without blocking + * @param item the element to add + * @return true if the element was added, false if the queue was full + * @throws IllegalArgumentException if the item is null + */ + public boolean offer(T item) { + if (item == null) { + throw new IllegalArgumentException("Cannot enqueue null item."); + } + + lock.lock(); + try { + if (count == capacity) { + return false; + } + buffer[tail] = item; + tail = (tail + 1) % capacity; + count++; + notEmpty.signalAll(); + return true; + } finally { + lock.unlock(); + } + } + + /** + * @brief Removes and returns the element at the head without blocking + * @return the element at the head, or null if the queue is empty + */ + @SuppressWarnings("unchecked") + public T poll() { + lock.lock(); + try { + if (count == 0) { + return null; + } + T item = (T) buffer[head]; + buffer[head] = null; + head = (head + 1) % capacity; + count--; + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + /** + * @brief Returns the number of elements in the queue + * @return the current size of the queue + */ + public int size() { + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } + + /** + * @brief Checks if the queue is empty + * @return true if the queue contains no elements + */ + public boolean isEmpty() { + lock.lock(); + try { + return count == 0; + } finally { + lock.unlock(); + } + } + + /** + * @brief Checks if the queue is full + * @return true if the queue has reached its capacity + */ + public boolean isFull() { + lock.lock(); + try { + return count == capacity; + } finally { + lock.unlock(); + } + } + + /** + * @brief Returns the maximum capacity of the queue + * @return the capacity + */ + public int capacity() { + return capacity; + } +} diff --git a/src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java b/src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java similarity index 99% rename from src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java rename to src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java index 236a23205180..ed67f9ae3394 100644 --- a/src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/HeavyLightDecomposition.java @@ -1,4 +1,4 @@ -package com.thealgorithms.tree; +package com.thealgorithms.datastructures.trees; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java index 57b3edc163ca..af6acb0cbb2b 100644 --- a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java @@ -8,13 +8,18 @@ public class SegmentTree { /* Constructor which takes the size of the array and the array as a parameter*/ public SegmentTree(int n, int[] arr) { + if (arr == null) { + throw new IllegalArgumentException("Input array must not be null"); + } + if (n <= 0 || n > arr.length) { + throw new IllegalArgumentException("Size must be in the range [1, " + arr.length + "], but was " + n); + } this.n = n; int x = (int) (Math.ceil(Math.log(n) / Math.log(2))); int segSize = 2 * (int) Math.pow(2, x) - 1; this.segTree = new int[segSize]; this.arr = arr; - this.n = n; constructTree(arr, 0, n - 1, 0); } @@ -47,7 +52,8 @@ private void updateTree(int start, int end, int index, int diff, int segIndex) { /* A function to update the value at a particular index*/ public void update(int index, int value) { - if (index < 0 || index > n) { + // Valid positions are 0..n-1; index == n is out of bounds and must not reach arr[index]. + if (index < 0 || index >= n) { return; } @@ -73,7 +79,8 @@ private int getSumTree(int start, int end, int qStart, int qEnd, int segIndex) { /* A function to query the sum of the subarray [start...end]*/ public int getSum(int start, int end) { - if (start < 0 || end > n || start > end) { + // The last queryable position is n-1, so end == n is an out of range query. + if (start < 0 || end >= n || start > end) { return 0; } return getSumTree(0, n - 1, start, end, 0); diff --git a/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java new file mode 100644 index 000000000000..40b9e8a73533 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/SegmentTree2D.java @@ -0,0 +1,201 @@ +package com.thealgorithms.datastructures.trees; + +/** + * 2D Segment Tree (Tree of Trees) implementation. + * This data structure supports point updates and submatrix sum queries + * in a 2D grid. It achieves this by nesting 1D Segment Trees within a 1D Segment Tree. + * + * Time Complexity: + * - Build/Initialization: O(N * M) + * - Point Update: O(log N * log M) + * - Submatrix Query: O(log N * log M) + * + * @see 2D Segment Tree + */ +public class SegmentTree2D { + + /** + * Represents a 1D Segment Tree. + * This is equivalent to your 'Sagara' struct. It manages the columns (X-axis). + */ + public static class SegmentTree1D { + private int n; + private final int[] tree; + + /** + * Initializes the 1D Segment Tree with the nearest power of 2. + * + * @param size The expected number of elements (columns). + */ + public SegmentTree1D(int size) { + n = 1; + while (n < size) { + n *= 2; + } + tree = new int[n * 2]; + } + + /** + * Recursively updates a point in the 1D tree. + */ + private void update(int index, int val, int node, int lx, int rx) { + if (rx - lx == 1) { + tree[node] = val; + return; + } + + int mid = lx + (rx - lx) / 2; + int leftChild = node * 2 + 1; + int rightChild = node * 2 + 2; + + if (index < mid) { + update(index, val, leftChild, lx, mid); + } else { + update(index, val, rightChild, mid, rx); + } + + tree[node] = tree[leftChild] + tree[rightChild]; + } + + /** + * Public wrapper to update a specific index. + * + * @param index The column index to update. + * @param val The new value. + */ + public void update(int index, int val) { + update(index, val, 0, 0, n); + } + + /** + * Retrieves the exact value at a specific leaf node. + * + * @param index The column index. + * @return The value at the given index. + */ + public int get(int index) { + return query(index, index + 1, 0, 0, n); + } + + /** + * Recursively queries the sum in a 1D range. + */ + private int query(int l, int r, int node, int lx, int rx) { + if (lx >= r || rx <= l) { + return 0; // Out of bounds + } + if (lx >= l && rx <= r) { + return tree[node]; // Fully inside + } + + int mid = lx + (rx - lx) / 2; + int leftSum = query(l, r, node * 2 + 1, lx, mid); + int rightSum = query(l, r, node * 2 + 2, mid, rx); + + return leftSum + rightSum; + } + + /** + * Public wrapper to query the sum in the range [l, r). + * + * @param l Left boundary (inclusive). + * @param r Right boundary (exclusive). + * @return The sum of the range. + */ + public int query(int l, int r) { + return query(l, r, 0, 0, n); + } + } + + // --- Start of 2D Segment Tree (equivalent to 'Sagara2D') --- + + private int n; + private final SegmentTree1D[] tree; + + /** + * Initializes the 2D Segment Tree. + * + * @param rows The number of rows in the matrix. + * @param cols The number of columns in the matrix. + */ + public SegmentTree2D(int rows, int cols) { + n = 1; + while (n < rows) { + n *= 2; + } + tree = new SegmentTree1D[n * 2]; + for (int i = 0; i < n * 2; i++) { + // Every node in the outer tree is a full 1D tree! + tree[i] = new SegmentTree1D(cols); + } + } + + /** + * Recursively updates a point in the 2D grid. + */ + private void update(int row, int col, int val, int node, int lx, int rx) { + if (rx - lx == 1) { + tree[node].update(col, val); + return; + } + + int mid = lx + (rx - lx) / 2; + int leftChild = node * 2 + 1; + int rightChild = node * 2 + 2; + + if (row < mid) { + update(row, col, val, leftChild, lx, mid); + } else { + update(row, col, val, rightChild, mid, rx); + } + + // The value of the current node's column is the sum of its children's column values + int leftVal = tree[leftChild].get(col); + int rightVal = tree[rightChild].get(col); + tree[node].update(col, leftVal + rightVal); + } + + /** + * Public wrapper to update a specific point (row, col). + * + * @param row The row index. + * @param col The column index. + * @param val The new value. + */ + public void update(int row, int col, int val) { + update(row, col, val, 0, 0, n); + } + + /** + * Recursively queries the sum in a submatrix. + */ + private int query(int top, int bottom, int left, int right, int node, int lx, int rx) { + if (lx >= bottom || rx <= top) { + return 0; // Out of bounds + } + if (lx >= top && rx <= bottom) { + // Fully inside the row range, so delegate the column query to the 1D tree + return tree[node].query(left, right); + } + + int mid = lx + (rx - lx) / 2; + int leftSum = query(top, bottom, left, right, node * 2 + 1, lx, mid); + int rightSum = query(top, bottom, left, right, node * 2 + 2, mid, rx); + + return leftSum + rightSum; + } + + /** + * Public wrapper to query the sum of a submatrix. + * Note: boundaries are [top, bottom) and [left, right). + * + * @param top Top row index (inclusive). + * @param bottom Bottom row index (exclusive). + * @param left Left column index (inclusive). + * @param right Right column index (exclusive). + * @return The sum of the submatrix. + */ + public int query(int top, int bottom, int left, int right) { + return query(top, bottom, left, right, 0, 0, n); + } +} diff --git a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java b/src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java similarity index 98% rename from src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java rename to src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java index 7a1a7aadd805..624e3d65bfc1 100644 --- a/src/main/java/com/thealgorithms/others/Implementing_auto_completing_features_using_trie.java +++ b/src/main/java/com/thealgorithms/datastructures/trees/TrieAutocomplete.java @@ -1,8 +1,8 @@ -package com.thealgorithms.others; +package com.thealgorithms.datastructures.trees; // Java Program to implement Auto-Complete // Feature using Trie -class Trieac { +class TrieAutocomplete { // Alphabet size (# of symbols) public static final int ALPHABET_SIZE = 26; diff --git a/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java b/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java new file mode 100644 index 000000000000..6feaa6f35048 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/trees/WaveletTree.java @@ -0,0 +1,235 @@ +package com.thealgorithms.datastructures.trees; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Wavelet Tree is a highly efficient data structure used to store sequences + * and answer queries like rank, select, and quantile in O(log(max_val - min_val)) time. + * This structure is particularly useful in competitive programming and text compression. + */ +public class WaveletTree { + + private class Node { + int low; + int high; + Node left; + Node right; + List leftCount; // Prefix sums of elements going to the left child + + /** + * Recursively constructs the tree nodes by partitioning the array. + * + * @param arr the subarray for the current node + * @param low the minimum possible value in the current node + * @param high the maximum possible value in the current node + */ + Node(int[] arr, int low, int high) { + this.low = low; + this.high = high; + + if (low == high) { + return; + } + + int mid = low + (high - low) / 2; + leftCount = new ArrayList<>(arr.length + 1); + leftCount.add(0); + + List leftArr = new ArrayList<>(); + List rightArr = new ArrayList<>(); + + for (int x : arr) { + if (x <= mid) { + leftArr.add(x); + leftCount.add(leftCount.get(leftCount.size() - 1) + 1); + } else { + rightArr.add(x); + leftCount.add(leftCount.get(leftCount.size() - 1)); + } + } + + if (!leftArr.isEmpty()) { + this.left = new Node(leftArr.stream().mapToInt(i -> i).toArray(), low, mid); + } + if (!rightArr.isEmpty()) { + this.right = new Node(rightArr.stream().mapToInt(i -> i).toArray(), mid + 1, high); + } + } + } + + private Node root; + private final int n; + + /** + * Constructs a Wavelet Tree from the given array. + * The min and max values are determined dynamically from the array. + * + * @param arr the input array + */ + public WaveletTree(int[] arr) { + if (arr == null || arr.length == 0) { + this.n = 0; + return; + } + this.n = arr.length; + int min = arr[0]; + int max = arr[0]; + for (int x : arr) { + if (x < min) { + min = x; + } + if (x > max) { + max = x; + } + } + root = new Node(arr, min, max); + } + + /** + * Constructs a Wavelet Tree from the given array with specific min and max values. + * + * @param arr the input array + * @param minValue the minimum possible value + * @param maxValue the maximum possible value + */ + public WaveletTree(int[] arr, int minValue, int maxValue) { + if (arr == null || arr.length == 0) { + this.n = 0; + return; + } + this.n = arr.length; + root = new Node(arr, minValue, maxValue); + } + + /** + * How many times does the number x appear in the array from index 0 to i (inclusive)? + * + * @param x the number to search for + * @param i the end index (0-based, inclusive) + * @return the number of occurrences of x in arr[0...i] + */ + public int rank(int x, int i) { + if (root == null || x < root.low || x > root.high || i < 0) { + return 0; + } + // If i is out of bounds, cap it at n - 1 + int endIdx = Math.min(i, n - 1); + return rank(root, x, endIdx + 1); + } + + private int rank(Node node, int x, int count) { + if (node == null || count == 0) { + return 0; + } + if (node.low == node.high) { + return count; + } + int mid = node.low + (node.high - node.low) / 2; + int leftC = node.leftCount.get(count); + if (x <= mid) { + return rank(node.left, x, leftC); + } else { + return rank(node.right, x, count - leftC); + } + } + + /** + * What is the 0-based index of the k-th occurrence of the number x in the array? + * + * @param x the number to search for + * @param k the occurrence count (1-based) + * @return the 0-based index in the original array, or -1 if x occurs less than k times + */ + public int select(int x, int k) { + if (root == null || x < root.low || x > root.high || k <= 0) { + return -1; + } + if (rank(x, n - 1) < k) { + return -1; + } + return select(root, x, k); + } + + private int select(Node node, int x, int k) { + if (node.low == node.high) { + return k - 1; // 0-based index within the imaginary array at the leaf + } + int mid = node.low + (node.high - node.low) / 2; + if (x <= mid) { + int posInLeft = select(node.left, x, k); + return binarySearchLeft(node.leftCount, posInLeft + 1); + } else { + int posInRight = select(node.right, x, k); + return binarySearchRight(node.leftCount, posInRight + 1); + } + } + + private int binarySearchLeft(List prefixSums, int k) { + int l = 1; + int r = prefixSums.size() - 1; + int ans = -1; + while (l <= r) { + int mid = l + (r - l) / 2; + if (prefixSums.get(mid) >= k) { + ans = mid; + r = mid - 1; + } else { + l = mid + 1; + } + } + return ans == -1 ? -1 : ans - 1; // Convert to 0-based index + } + + private int binarySearchRight(List prefixSums, int k) { + int l = 1; + int r = prefixSums.size() - 1; + int ans = -1; + while (l <= r) { + int mid = l + (r - l) / 2; + if (mid - prefixSums.get(mid) >= k) { + ans = mid; + r = mid - 1; + } else { + l = mid + 1; + } + } + return ans == -1 ? -1 : ans - 1; // Convert to 0-based index + } + + /** + * If you sort the subarray from index left to right, what would be the k-th smallest element? + * This query is also commonly known as the quantile query. + * + * @param left the start index of the subarray (0-based, inclusive) + * @param right the end index of the subarray (0-based, inclusive) + * @param k the rank of the smallest element (1-based, e.g., k=1 is the minimum) + * @return the k-th smallest element in the subarray, or -1 if invalid parameters + */ + public int kthSmallest(int left, int right, int k) { + if (root == null || left > right || left < 0 || k < 1 || k > right - left + 1) { + return -1; + } + return kthSmallest(root, left, right, k); + } + + private int kthSmallest(Node node, int left, int right, int k) { + if (node.low == node.high) { + return node.low; + } + + int countLeftInLMinus1 = (left == 0) ? 0 : node.leftCount.get(left); + int countLeftInR = node.leftCount.get(right + 1); + int elementsToLeft = countLeftInR - countLeftInLMinus1; + + if (k <= elementsToLeft) { + int newL = countLeftInLMinus1; + int newR = countLeftInR - 1; + return kthSmallest(node.left, newL, newR, k); + } else { + int newL = left - countLeftInLMinus1; + int newR = right - countLeftInR; + return kthSmallest(node.right, newL, newR, k - elementsToLeft); + } + } +} diff --git a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java index 4c9c40c83174..323098a99887 100644 --- a/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java +++ b/src/main/java/com/thealgorithms/divideandconquer/ClosestPair.java @@ -66,10 +66,6 @@ public static class Location { } } - public Location[] createLocation(int numberValues) { - return new Location[numberValues]; - } - public Location buildLocation(double x, double y) { return new Location(x, y); } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java new file mode 100644 index 000000000000..7dae7603fedc --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -0,0 +1,111 @@ +package com.thealgorithms.dynamicprogramming; +import java.util.Arrays; + +/** + * A generalized template for the Digit Dynamic Programming (Digit DP) + * technique. + * Digit DP is used to count numbers within a range [L, R] that satisfy specific + * digit properties. + * This specific implementation demonstrates counting the numbers whose digit + * sum equals a target value. + * + *

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

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

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

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberGoldenRation} - Closed-form expression using Binet's formula
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
+ * * @author Varun Upadhyay (https://github.com/varunu28) */ public final class Fibonacci { private Fibonacci() { diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java b/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java index 0b40d4559341..5c8a6a953f83 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequence.java @@ -1,58 +1,42 @@ package com.thealgorithms.dynamicprogramming; /** - * Algorithm explanation - * https://www.educative.io/edpresso/longest-palindromic-subsequence-algorithm + * Longest Palindromic Subsequence algorithm. + * A palindromic subsequence is a subsequence that reads the same forwards and backwards. + * This implementation finds the longest such subsequence by computing the LCS of the + * original string and its reverse. + * + * @see Wikipedia */ public final class LongestPalindromicSubsequence { private LongestPalindromicSubsequence() { } - public static void main(String[] args) { - String a = "BBABCBCAB"; - String b = "BABCBAB"; - - String aLPS = lps(a); - String bLPS = lps(b); - - System.out.println(a + " => " + aLPS); - System.out.println(b + " => " + bLPS); - } - - public static String lps(String original) throws IllegalArgumentException { - StringBuilder reverse = new StringBuilder(original); - reverse = reverse.reverse(); - return recursiveLPS(original, reverse.toString()); + /** + * Returns the longest palindromic subsequence of the given string. + * + * @param original the input string + * @return the longest palindromic subsequence + * @throws IllegalArgumentException if the input string is null + */ + public static String lps(String original) { + if (original == null) { + throw new IllegalArgumentException("Input string must not be null"); + } + String reverse = new StringBuilder(original).reverse().toString(); + return recursiveLPS(original, reverse); } private static String recursiveLPS(String original, String reverse) { - String bestResult = ""; - - // no more chars, then return empty - if (original.length() == 0 || reverse.length() == 0) { - bestResult = ""; - } else { - // if the last chars match, then remove it from both strings and recur - if (original.charAt(original.length() - 1) == reverse.charAt(reverse.length() - 1)) { - String bestSubResult = recursiveLPS(original.substring(0, original.length() - 1), reverse.substring(0, reverse.length() - 1)); - - bestResult = reverse.charAt(reverse.length() - 1) + bestSubResult; - } else { - // otherwise (1) ignore the last character of reverse, and recur on original and - // updated reverse again (2) ignore the last character of original and recur on the - // updated original and reverse again then select the best result from these two - // subproblems. - - String bestSubResult1 = recursiveLPS(original, reverse.substring(0, reverse.length() - 1)); - String bestSubResult2 = recursiveLPS(original.substring(0, original.length() - 1), reverse); - if (bestSubResult1.length() > bestSubResult2.length()) { - bestResult = bestSubResult1; - } else { - bestResult = bestSubResult2; - } - } + if (original.isEmpty() || reverse.isEmpty()) { + return ""; } - - return bestResult; + if (original.charAt(original.length() - 1) == reverse.charAt(reverse.length() - 1)) { + String bestSubResult = recursiveLPS(original.substring(0, original.length() - 1), reverse.substring(0, reverse.length() - 1)); + return reverse.charAt(reverse.length() - 1) + bestSubResult; + } + String sub1 = recursiveLPS(original, reverse.substring(0, reverse.length() - 1)); + String sub2 = recursiveLPS(original.substring(0, original.length() - 1), reverse); + return sub1.length() >= sub2.length() ? sub1 : sub2; } } diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java b/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java new file mode 100644 index 000000000000..428176ea6c40 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTree.java @@ -0,0 +1,130 @@ +package com.thealgorithms.dynamicprogramming; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * Computes the minimum search cost of an optimal binary search tree. + * + *

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

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

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

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

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

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

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

+ * + *

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

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

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

+ * + *

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

+ *

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

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

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

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

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

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

Space Complexity: O(n) + * + * @param amount the total amount to make change for + * @return list of coins used to make the amount + */ public static ArrayList coinChangeProblem(int amount) { // Define an array of coin denominations in descending order Integer[] coins = {1, 2, 5, 10, 20, 50, 100, 500, 2000}; diff --git a/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java b/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java new file mode 100644 index 000000000000..134d4eee8c4c --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/LinearRegression.java @@ -0,0 +1,102 @@ +package com.thealgorithms.machinelearning; + +/** + * A simple Linear Regression model implemented from scratch using Gradient Descent. + * + * @see Linear Regression (Wikipedia) + * @author Vraj Prajapati (Rosander0) + */ +public class LinearRegression { + private double m; // Slope (weight) + private double b; // Y-intercept (bias) + private final double learningRate; + private final int epochs; + + /** + * Constructs a Linear Regression model with the given hyperparameters. + * + * @param learningRate controls the step size during gradient descent + * @param epochs the number of iterations to train the model + */ + public LinearRegression(double learningRate, int epochs) { + this.learningRate = learningRate; + this.epochs = epochs; + this.m = 0.0; + this.b = 0.0; + } + + /** + * Trains the model on the provided dataset using batch gradient descent. + * + * @param x the input feature values + * @param y the corresponding target values + * @throws IllegalArgumentException if the arrays are null, empty, or of differing lengths + */ + public void fit(double[] x, double[] y) { + if (x == null || y == null || x.length != y.length || x.length == 0) { + throw new IllegalArgumentException("X and Y must be non-null, non-empty, and of the same length."); + } + + int n = x.length; + + for (int epoch = 0; epoch < epochs; epoch++) { + double mGradient = 0; + double bGradient = 0; + + // Calculate gradients across the entire dataset + for (int i = 0; i < n; i++) { + double prediction = (m * x[i]) + b; + double error = prediction - y[i]; + + // Partial derivatives of the Mean Squared Error cost function + mGradient += error * x[i]; + bGradient += error; + } + + // Average the gradients and update the parameters + m -= 2.0 / n * mGradient * learningRate; + b -= 2.0 / n * bGradient * learningRate; + } + } + + /** + * Predicts the output for a given input x. + * + * @param x the input value + * @return the predicted output + */ + public double predict(double x) { + return (m * x) + b; + } + + /** + * Calculates the Mean Squared Error of the model against a dataset. + * + * @param x the input feature values + * @param y the corresponding target values + * @return the mean squared error + */ + public double calculateMSE(double[] x, double[] y) { + double totalSquaredError = 0; + int n = x.length; + for (int i = 0; i < n; i++) { + double error = predict(x[i]) - y[i]; + totalSquaredError += error * error; + } + return totalSquaredError / n; + } + + /** + * @return the learned slope of the regression line + */ + public double getSlope() { + return m; + } + + /** + * @return the learned y-intercept of the regression line + */ + public double getIntercept() { + return b; + } +} diff --git a/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java new file mode 100644 index 000000000000..43e7e555814c --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java @@ -0,0 +1,143 @@ +package com.thealgorithms.machinelearning; + +import java.util.HashMap; +import java.util.Map; + +/** + * Multinomial Naive Bayes classifier. + * + *

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

Reference: + * Naive Bayes classifier + * + * @author Vraj Prajapati(Rosander0) + */ +public final class MultinomialNaiveBayesClassifier { + + private final double alpha; + private final Map logPriors; + private final Map logLikelihoods; + private int numFeatures; + + /** + * Constructs a classifier with the given Laplace smoothing parameter. + * + * @param alpha smoothing constant; must be greater than 0. A value of 1.0 + * corresponds to standard Laplace smoothing. + */ + public MultinomialNaiveBayesClassifier(double alpha) { + if (alpha <= 0) { + throw new IllegalArgumentException("alpha must be greater than 0"); + } + this.alpha = alpha; + this.logPriors = new HashMap<>(); + this.logLikelihoods = new HashMap<>(); + } + + /** Constructs a classifier using the standard Laplace smoothing constant of 1.0. */ + public MultinomialNaiveBayesClassifier() { + this(1.0); + } + + /** + * Fits the classifier on the given feature matrix and labels. + * + * @param features training samples, each row a vector of non-negative + * feature counts + * @param labels class label for each row of {@code features} + */ + public void fit(double[][] features, int[] labels) { + if (features.length == 0 || features.length != labels.length) { + throw new IllegalArgumentException("features and labels must be non-empty and of equal length"); + } + logPriors.clear(); + logLikelihoods.clear(); + numFeatures = features[0].length; + + Map classCounts = new HashMap<>(); + Map featureSums = new HashMap<>(); + Map totalFeatureCount = new HashMap<>(); + + for (int i = 0; i < features.length; i++) { + int label = labels[i]; + classCounts.merge(label, 1, Integer::sum); + double[] sums = featureSums.computeIfAbsent(label, k -> new double[numFeatures]); + double total = totalFeatureCount.getOrDefault(label, 0.0); + for (int j = 0; j < numFeatures; j++) { + sums[j] += features[i][j]; + total += features[i][j]; + } + totalFeatureCount.put(label, total); + } + + int totalSamples = features.length; + for (Map.Entry entry : featureSums.entrySet()) { + int label = entry.getKey(); + double[] sums = entry.getValue(); + int count = classCounts.getOrDefault(label, 0); + double total = totalFeatureCount.getOrDefault(label, 0.0); + + logPriors.put(label, Math.log((double) count / totalSamples)); + + double denom = total + alpha * numFeatures; + double[] logLikelihood = new double[numFeatures]; + for (int j = 0; j < numFeatures; j++) { + logLikelihood[j] = Math.log((sums[j] + alpha) / denom); + } + logLikelihoods.put(label, logLikelihood); + } + } + + /** + * Predicts the most likely class for a single sample. + * + * @param sample feature vector of non-negative counts + * @return the predicted class label + */ + public int predict(double[] sample) { + if (logPriors.isEmpty()) { + throw new IllegalStateException("classifier has not been fitted"); + } + if (sample.length != numFeatures) { + throw new IllegalArgumentException("sample length must match training feature count"); + } + + int bestLabel = -1; + double bestScore = Double.NEGATIVE_INFINITY; + + for (Map.Entry entry : logLikelihoods.entrySet()) { + int label = entry.getKey(); + double[] logLikelihood = entry.getValue(); + double score = logPriors.getOrDefault(label, Double.NEGATIVE_INFINITY); + for (int j = 0; j < numFeatures; j++) { + score += sample[j] * logLikelihood[j]; + } + if (score > bestScore) { + bestScore = score; + bestLabel = label; + } + } + return bestLabel; + } + + /** + * Predicts class labels for a batch of samples. + * + * @param samples feature vectors of non-negative counts + * @return predicted class label for each row of {@code samples} + */ + public int[] predict(double[][] samples) { + int[] predictions = new int[samples.length]; + for (int i = 0; i < samples.length; i++) { + predictions[i] = predict(samples[i]); + } + return predictions; + } +} diff --git a/src/main/java/com/thealgorithms/maths/AbsoluteMin.java b/src/main/java/com/thealgorithms/maths/AbsoluteMin.java index 1b9575a330dd..aab6fe0f426d 100644 --- a/src/main/java/com/thealgorithms/maths/AbsoluteMin.java +++ b/src/main/java/com/thealgorithms/maths/AbsoluteMin.java @@ -1,7 +1,5 @@ package com.thealgorithms.maths; -import java.util.Arrays; - public final class AbsoluteMin { private AbsoluteMin() { } @@ -13,14 +11,17 @@ private AbsoluteMin() { * @return The absolute min value */ public static int getMinValue(int... numbers) { - if (numbers.length == 0) { - throw new IllegalArgumentException("Numbers array cannot be empty"); + if (numbers == null || numbers.length == 0) { + throw new IllegalArgumentException("Numbers array cannot be empty or null"); } - var absMinWrapper = new Object() { int value = numbers[0]; }; - - Arrays.stream(numbers).skip(1).filter(number -> Math.abs(number) <= Math.abs(absMinWrapper.value)).forEach(number -> absMinWrapper.value = Math.min(absMinWrapper.value, number)); - - return absMinWrapper.value; + long absMin = numbers[0]; + for (int i = 1; i < numbers.length; i++) { + long current = numbers[i]; + if (Math.abs(current) < Math.abs(absMin) || (Math.abs(current) == Math.abs(absMin) && current < absMin)) { + absMin = current; + } + } + return (int) absMin; } } diff --git a/src/main/java/com/thealgorithms/maths/AbsoluteValue.java b/src/main/java/com/thealgorithms/maths/AbsoluteValue.java index b9279d5a244a..114eb71b1015 100644 --- a/src/main/java/com/thealgorithms/maths/AbsoluteValue.java +++ b/src/main/java/com/thealgorithms/maths/AbsoluteValue.java @@ -11,6 +11,6 @@ private AbsoluteValue() { * @return The absolute value of the {@code number} */ public static int getAbsValue(int number) { - return number < 0 ? -number : number; + return Math.abs(number); } } diff --git a/src/main/java/com/thealgorithms/maths/Area.java b/src/main/java/com/thealgorithms/maths/Area.java index 1eba6666dde3..84fc67159379 100644 --- a/src/main/java/com/thealgorithms/maths/Area.java +++ b/src/main/java/com/thealgorithms/maths/Area.java @@ -10,17 +10,17 @@ private Area() { /** * String of IllegalArgumentException for radius */ - private static final String POSITIVE_RADIUS = "Must be a positive radius"; + private static final String POSITIVE_RADIUS = "Radius must be greater than 0"; /** * String of IllegalArgumentException for height */ - private static final String POSITIVE_HEIGHT = "Must be a positive height"; + private static final String POSITIVE_HEIGHT = "Height must be greater than 0"; /** * String of IllegalArgumentException for base */ - private static final String POSITIVE_BASE = "Must be a positive base"; + private static final String POSITIVE_BASE = "Base must be greater than 0"; /** * Calculate the surface area of a cube. @@ -30,11 +30,32 @@ private Area() { */ public static double surfaceAreaCube(final double sideLength) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException("Side length must be greater than 0"); } return 6 * sideLength * sideLength; } + /** + * Calculate the surface area of a cuboid. + * + * @param length length of the cuboid + * @param width width of the cuboid + * @param height height of the cuboid + * @return surface area of given cuboid + */ + public static double surfaceAreaCuboid(final double length, double width, double height) { + if (length <= 0) { + throw new IllegalArgumentException("Length must be greater than 0"); + } + if (width <= 0) { + throw new IllegalArgumentException("Width must be greater than 0"); + } + if (height <= 0) { + throw new IllegalArgumentException("Height must be greater than 0"); + } + return 2 * (length * width + length * height + width * height); + } + /** * Calculate the surface area of a sphere. * @@ -57,10 +78,10 @@ public static double surfaceAreaSphere(final double radius) { */ public static double surfaceAreaPyramid(final double sideLength, final double slantHeight) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException(""); } if (slantHeight <= 0) { - throw new IllegalArgumentException("Must be a positive slantHeight"); + throw new IllegalArgumentException("slant height must be greater than 0"); } double baseArea = sideLength * sideLength; double lateralSurfaceArea = 2 * sideLength * slantHeight; @@ -76,10 +97,10 @@ public static double surfaceAreaPyramid(final double sideLength, final double sl */ public static double surfaceAreaRectangle(final double length, final double width) { if (length <= 0) { - throw new IllegalArgumentException("Must be a positive length"); + throw new IllegalArgumentException("Length must be greater than 0"); } if (width <= 0) { - throw new IllegalArgumentException("Must be a positive width"); + throw new IllegalArgumentException("Width must be greater than 0"); } return length * width; } @@ -109,7 +130,7 @@ public static double surfaceAreaCylinder(final double radius, final double heigh */ public static double surfaceAreaSquare(final double sideLength) { if (sideLength <= 0) { - throw new IllegalArgumentException("Must be a positive sideLength"); + throw new IllegalArgumentException("Side Length must be greater than 0"); } return sideLength * sideLength; } @@ -121,14 +142,14 @@ public static double surfaceAreaSquare(final double sideLength) { * @param height height of triangle * @return area of given triangle */ - public static double surfaceAreaTriangle(final double base, final double height) { - if (base <= 0) { + public static double surfaceAreaTriangle(final double baseLength, final double height) { + if (baseLength <= 0) { throw new IllegalArgumentException(POSITIVE_BASE); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return base * height / 2; + return baseLength * height / 2; } /** @@ -138,14 +159,14 @@ public static double surfaceAreaTriangle(final double base, final double height) * @param height height of a parallelogram * @return area of given parallelogram */ - public static double surfaceAreaParallelogram(final double base, final double height) { - if (base <= 0) { + public static double surfaceAreaParallelogram(final double baseLength, final double height) { + if (baseLength <= 0) { throw new IllegalArgumentException(POSITIVE_BASE); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return base * height; + return baseLength * height; } /** @@ -156,17 +177,17 @@ public static double surfaceAreaParallelogram(final double base, final double he * @param height height of trapezium * @return area of given trapezium */ - public static double surfaceAreaTrapezium(final double base1, final double base2, final double height) { - if (base1 <= 0) { + public static double surfaceAreaTrapezium(final double baseLength1, final double baseLength2, final double height) { + if (baseLength1 <= 0) { throw new IllegalArgumentException(POSITIVE_BASE + 1); } - if (base2 <= 0) { + if (baseLength2 <= 0) { throw new IllegalArgumentException(POSITIVE_BASE + 2); } if (height <= 0) { throw new IllegalArgumentException(POSITIVE_HEIGHT); } - return (base1 + base2) * height / 2; + return (baseLength1 + baseLength2) * height / 2; } /** diff --git a/src/main/java/com/thealgorithms/maths/Average.java b/src/main/java/com/thealgorithms/maths/Average.java index a550a7f6504d..cf55af509ccc 100644 --- a/src/main/java/com/thealgorithms/maths/Average.java +++ b/src/main/java/com/thealgorithms/maths/Average.java @@ -1,9 +1,16 @@ package com.thealgorithms.maths; +import java.util.Arrays; +import java.util.OptionalDouble; + /** * A utility class for computing the average of numeric arrays. - * This class provides static methods to calculate the average of arrays - * of both {@code double} and {@code int} values. + * + *

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

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

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

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

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

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

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

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

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

Reference: https://en.wikipedia.org/wiki/Euclidean_distance + */ +public final class DistanceBetweenTwoPoints { + + private DistanceBetweenTwoPoints() { + // Utility class; prevent instantiation + } + + /** + * Calculate the Euclidean distance between two points. + * + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @return Euclidean distance between the two points + */ + public static double calculate(final double x1, final double y1, final double x2, final double y2) { + final double deltaX = x2 - x1; + final double deltaY = y2 - y1; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } +} diff --git a/src/main/java/com/thealgorithms/maths/Factorial.java b/src/main/java/com/thealgorithms/maths/Factorial.java index 511cc1f84f05..8ad219a3066c 100644 --- a/src/main/java/com/thealgorithms/maths/Factorial.java +++ b/src/main/java/com/thealgorithms/maths/Factorial.java @@ -1,23 +1,19 @@ package com.thealgorithms.maths; +import java.math.BigInteger; + public final class Factorial { private Factorial() { } - /** - * Calculate factorial N using iteration - * - * @param n the number - * @return the factorial of {@code n} - */ - public static long factorial(int n) { + public static BigInteger factorial(int n) { if (n < 0) { throw new IllegalArgumentException("Input number cannot be negative"); } - long factorial = 1; - for (int i = 1; i <= n; ++i) { - factorial *= i; + BigInteger result = BigInteger.ONE; + for (int i = 1; i <= n; i++) { + result = result.multiply(BigInteger.valueOf(i)); } - return factorial; + return result; } } diff --git a/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java b/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java index 84390860ccc4..8a93580a4b72 100644 --- a/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java +++ b/src/main/java/com/thealgorithms/maths/FibonacciJavaStreams.java @@ -6,9 +6,23 @@ import java.util.stream.Stream; /** - * @author: caos321 - * @date: 14 October 2021 (Thursday) + * Calculates Fibonacci numbers using a functional programming paradigm with Java Streams. + *

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

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

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

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

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

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

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

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

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

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

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

    + *
  • {@link com.thealgorithms.maths.FibonacciLoop} - Standard Iterative (Loop) approach
  • + *
  • {@link com.thealgorithms.recursion.FibonacciSeries} - Naive Recursive approach
  • + *
  • {@link com.thealgorithms.dynamicprogramming.Fibonacci} - Dynamic Programming approaches (Memoization, Bottom-Up, Optimized)
  • + *
  • {@link com.thealgorithms.maths.FibonacciJavaStreams} - Functional approach using Java Streams
  • + *
  • {@link com.thealgorithms.maths.FibonacciNumberCheck} - Utility to check if a given number is a Fibonacci number
  • + *
  • {@link com.thealgorithms.matrix.matrixexponentiation.Fibonacci} - O(log n) Matrix Exponentiation approach
  • + *
* * @see Binet's formula on Wikipedia */ diff --git a/src/main/java/com/thealgorithms/maths/FriendlyNumber.java b/src/main/java/com/thealgorithms/maths/FriendlyNumber.java new file mode 100644 index 000000000000..900ce89295a4 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/FriendlyNumber.java @@ -0,0 +1,49 @@ +package com.thealgorithms.maths; + +/** + * Two numbers are Friendly if they share the same abundancy index, + * which is the ratio of the sum of divisors to the number itself. + * Example: 6 and 28 are friendly because sigma(6)/6 = 2 and sigma(28)/28 = 2 + * + * @see + * Wikipedia: Friendly Number + * + * @author Vraj Prajapati @Rosander0 + */ +public final class FriendlyNumber { + + private FriendlyNumber() { + // Utility class + } + + private static int sumOfDivisors(final int number) { + int sum = 0; + final int root = (int) Math.sqrt(number); + for (int i = 1; i <= root; i++) { + if (number % i == 0) { + sum += i; + final int other = number / i; + if (other != i) { + sum += other; + } + } + } + return sum; + } + + /** + * Checks whether two numbers are Friendly Numbers. + * + * @param a First number (must be positive) + * @param b Second number (must be positive) + * @return true if a and b are friendly numbers, false otherwise + */ + public static boolean areFriendly(final int a, final int b) { + if (a <= 0 || b <= 0) { + return false; + } + final long sigmaA = sumOfDivisors(a); + final long sigmaB = sumOfDivisors(b); + return sigmaA * b == sigmaB * a; + } +} diff --git a/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java b/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java new file mode 100644 index 000000000000..f4f2e23c3932 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/JacobsthalNumber.java @@ -0,0 +1,44 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +/** + * The Jacobsthal Sequence is a sequence of integers defined by the recurrence relation: + * J(n) = J(n-1) + 2*J(n-2) with initial values J(0) = 0, J(1) = 1. + * Example: 0, 1, 1, 3, 5, 11, 21, 43, 85, 171, 341... + * + * @see + * Wikipedia: Jacobsthal Number + */ +public final class JacobsthalNumber { + + private JacobsthalNumber() { + // Utility class + } + + /** + * Calculates the nth term of the Jacobsthal Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Jacobsthal Sequence + */ + public static long jacobsthal(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative!"); + } + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + long a = 0; + long b = 1; + long result = 0; + for (int i = 2; i <= n; i++) { + result = b + 2 * a; + a = b; + b = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java b/src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java similarity index 94% rename from src/main/java/com/thealgorithms/others/LowestBasePalindrome.java rename to src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java index a3ca8d6f6db8..4a79b4298fc4 100644 --- a/src/main/java/com/thealgorithms/others/LowestBasePalindrome.java +++ b/src/main/java/com/thealgorithms/maths/LowestBasePalindrome.java @@ -1,4 +1,4 @@ -package com.thealgorithms.others; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.List; @@ -23,6 +23,12 @@ * * @see OEIS A016026 - Smallest base in which * n is palindromic + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + * @see com.thealgorithms.maths.PalindromeNumber * @author TheAlgorithms Contributors */ public final class LowestBasePalindrome { diff --git a/src/main/java/com/thealgorithms/maths/Means.java b/src/main/java/com/thealgorithms/maths/Means.java index 5445a3caebc7..d77eb1d3f661 100644 --- a/src/main/java/com/thealgorithms/maths/Means.java +++ b/src/main/java/com/thealgorithms/maths/Means.java @@ -107,6 +107,28 @@ public static Double harmonic(final Iterable numbers) { return size / sumOfReciprocals; } + /** + * Computes the quadratic mean (root mean square) of the given numbers. + *

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

+ *

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

+ * + * @param numbers the input numbers (must not be empty) + * @return the quadratic mean of the input numbers + * @throws IllegalArgumentException if the input is empty + * @see Quadratic + * Mean + */ + public static Double quadratic(final Iterable numbers) { + checkIfNotEmpty(numbers); + double sumOfSquares = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + y * y); + int size = IterableUtils.size(numbers); + return Math.pow(sumOfSquares / size, 0.5); + } + /** * Validates that the input iterable is not empty. * diff --git a/src/main/java/com/thealgorithms/maths/PadovanSequence.java b/src/main/java/com/thealgorithms/maths/PadovanSequence.java new file mode 100644 index 000000000000..51e7d2441b15 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PadovanSequence.java @@ -0,0 +1,43 @@ +package com.thealgorithms.maths; + +/** + * The Padovan Sequence is a sequence of integers defined by the recurrence relation: + * P(n) = P(n-2) + P(n-3) with initial values P(0) = P(1) = P(2) = 1. + * Example: 1, 1, 1, 2, 2, 3, 4, 5, 7, 9, 12, 16, 21, 28, 37... + * + * @see + * Wikipedia: Padovan Sequence + * @author Vraj Prajapati (@Rosander0) + */ +public final class PadovanSequence { + + private PadovanSequence() { + // Utility class + } + + /** + * Calculates the nth term of the Padovan Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Padovan Sequence + */ + public static long padovan(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative. Received: " + n); + } + if (n <= 2) { + return 1; + } + long a = 1; + long b = 1; + long c = 1; + long result = 0; + for (int i = 3; i <= n; i++) { + result = a + b; + a = b; + b = c; + c = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/maths/PalindromeNumber.java b/src/main/java/com/thealgorithms/maths/PalindromeNumber.java index a22d63897b37..9543f83332a7 100644 --- a/src/main/java/com/thealgorithms/maths/PalindromeNumber.java +++ b/src/main/java/com/thealgorithms/maths/PalindromeNumber.java @@ -1,5 +1,16 @@ package com.thealgorithms.maths; +/** + * A class to check if a given number is a palindrome. + * A palindromic number is a number that remains the same when its digits are reversed. + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromePrime + */ public final class PalindromeNumber { private PalindromeNumber() { } diff --git a/src/main/java/com/thealgorithms/misc/PalindromePrime.java b/src/main/java/com/thealgorithms/maths/PalindromePrime.java similarity index 73% rename from src/main/java/com/thealgorithms/misc/PalindromePrime.java rename to src/main/java/com/thealgorithms/maths/PalindromePrime.java index 164e957a9d12..21b76acefee8 100644 --- a/src/main/java/com/thealgorithms/misc/PalindromePrime.java +++ b/src/main/java/com/thealgorithms/maths/PalindromePrime.java @@ -1,8 +1,19 @@ -package com.thealgorithms.misc; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.List; +/** + * A class to check and generate palindromic prime numbers. + * A palindromic prime is a prime number that is also a palindromic number. + * + * @see com.thealgorithms.strings.Palindrome + * @see com.thealgorithms.stacks.PalindromeWithStack + * @see com.thealgorithms.bitmanipulation.BinaryPalindromeCheck + * @see com.thealgorithms.maths.LowestBasePalindrome + * @see com.thealgorithms.datastructures.lists.PalindromeSinglyLinkedList + * @see com.thealgorithms.maths.PalindromeNumber + */ public final class PalindromePrime { private PalindromePrime() { } diff --git a/src/main/java/com/thealgorithms/maths/PerfectSquare.java b/src/main/java/com/thealgorithms/maths/PerfectSquare.java index e9318bd7d805..aec43062121a 100644 --- a/src/main/java/com/thealgorithms/maths/PerfectSquare.java +++ b/src/main/java/com/thealgorithms/maths/PerfectSquare.java @@ -15,6 +15,9 @@ private PerfectSquare() { * false */ public static boolean isPerfectSquare(final int number) { + if (number < 0) { + return false; + } final int sqrt = (int) Math.sqrt(number); return sqrt * sqrt == number; } @@ -27,6 +30,9 @@ public static boolean isPerfectSquare(final int number) { * {@code false} */ public static boolean isPerfectSquareUsingPow(long number) { + if (number < 0) { + return false; + } long a = (long) Math.pow(number, 1.0 / 2); return a * a == number; } diff --git a/src/main/java/com/thealgorithms/maths/PerrinNumber.java b/src/main/java/com/thealgorithms/maths/PerrinNumber.java new file mode 100644 index 000000000000..cee45a1c5538 --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/PerrinNumber.java @@ -0,0 +1,53 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +/** + * The Perrin Sequence is a sequence of integers defined by the recurrence relation: + * P(n) = P(n-2) + P(n-3) with initial values P(0) = 3, P(1) = 0, P(2) = 2. + * Example: 3, 0, 2, 3, 2, 5, 5, 7, 10, 12, 17, 22, 29, 39, 51... + * + * Note: The Perrin Sequence uses the same recurrence relation as the Padovan Sequence + * but has different initial values. + * + * @see + * Wikipedia: Perrin Number + * @see PadovanSequence + */ +public final class PerrinNumber { + + private PerrinNumber() { + // Utility class + } + + /** + * Calculates the nth term of the Perrin Sequence. + * + * @param n the index of the sequence (must be non-negative) + * @return the nth term of the Perrin Sequence + */ + public static long perrin(final int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative!"); + } + if (n == 0) { + return 3; + } + if (n == 1) { + return 0; + } + if (n == 2) { + return 2; + } + long a = 3; + long b = 0; + long c = 2; + long result = 0; + for (int i = 3; i <= n; i++) { + result = a + b; + a = b; + b = c; + c = result; + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java b/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java index 3d4e4eff0f03..ec1785a916c7 100644 --- a/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java +++ b/src/main/java/com/thealgorithms/maths/Prime/MobiusFunction.java @@ -31,27 +31,28 @@ public static int mobius(int number) { throw new IllegalArgumentException("Number must be greater than zero."); } - if (number == 1) { - // return 1 if number passed is less or is 1 - return 1; - } - int primeFactorCount = 0; + int remaining = number; - for (int i = 1; i <= number; i++) { - // find prime factors of number - if (number % i == 0 && PrimeCheck.isPrime(i)) { - // check if number is divisible by square of prime factor - if (number % (i * i) == 0) { - // if number is divisible by square of prime factor + /* Divide out every prime factor in turn. Trial division only has to run up to the square + root of the remaining value, and the multiplication is widened to long so that the bound + does not overflow for numbers close to Integer.MAX_VALUE. */ + for (int factor = 2; (long) factor * factor <= remaining; factor++) { + if (remaining % factor == 0) { + remaining /= factor; + if (remaining % factor == 0) { + // number is divisible by the square of this prime factor return 0; } - /*increment primeFactorCount by 1 - if number is not divisible by square of found prime factor*/ primeFactorCount++; } } + /* Whatever is left is either 1 or a single prime factor larger than the square root. */ + if (remaining > 1) { + primeFactorCount++; + } + return (primeFactorCount % 2 == 0) ? 1 : -1; } } diff --git a/src/main/java/com/thealgorithms/maths/SociableNumber.java b/src/main/java/com/thealgorithms/maths/SociableNumber.java new file mode 100644 index 000000000000..9ce644e61dfc --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/SociableNumber.java @@ -0,0 +1,67 @@ +package com.thealgorithms.maths; + +/** + * Sociable numbers are natural numbers that form a cyclic sequence where the + * sum of proper divisors of each number equals the next number in the sequence, + * with the sequence eventually returning to the starting number. + * Amicable numbers are a special case of sociable numbers with a cycle length of 2. + * Example: (12496, 14288, 15472, 14536, 14264) is a sociable cycle of length 5. + * + * @author Vraj Prajapati (@Rosander0) + * @see Wikipedia: Sociable Number + * @see AmicableNumber + */ +public final class SociableNumber { + + private SociableNumber() { + // Utility class + } + + /** + * Calculates the sum of proper divisors of a number + * (all divisors excluding the number itself). + * + * @param number the number to calculate proper divisors sum for + * @return sum of proper divisors, or 0 if number is less than or equal to 1 + */ + static int sumOfProperDivisors(final int number) { + if (number <= 1) { + return 0; + } + int sum = 1; // 1 is a proper divisor of every number > 1 + final int root = (int) Math.sqrt(number); + for (int i = 2; i <= root; i++) { + if (number % i == 0) { + final int other = number / i; + sum += i; + if (other != i) { + sum += other; + } + } + } + return sum; + } + + /** + * Checks whether a number is part of a sociable cycle of a given length. + * Starting from the given number, it follows the chain of proper divisor + * sums and checks if it returns to the starting number in exactly cycleLength steps. + * + * @param number the starting number (must be positive) + * @param cycleLength the expected cycle length (must be greater than 1) + * @return true if the number is part of a sociable cycle of given length, false otherwise + */ + public static boolean isSociable(final int number, final int cycleLength) { + if (number <= 0 || cycleLength <= 1) { + return false; + } + int current = number; + for (int i = 0; i < cycleLength; i++) { + current = sumOfProperDivisors(current); + if (current == number) { + return i == cycleLength - 1; + } + } + return false; + } +} diff --git a/src/main/java/com/thealgorithms/maths/SumOfSquares.java b/src/main/java/com/thealgorithms/maths/SumOfSquares.java index c050d5a75f7b..77acbcc2a609 100644 --- a/src/main/java/com/thealgorithms/maths/SumOfSquares.java +++ b/src/main/java/com/thealgorithms/maths/SumOfSquares.java @@ -5,7 +5,6 @@ * Find minimum number of perfect squares that sum to given number * * @see Lagrange's Four Square Theorem - * @author BEASTSHRIRAM */ public final class SumOfSquares { @@ -16,10 +15,15 @@ private SumOfSquares() { /** * Find minimum number of perfect squares that sum to n * - * @param n the target number + * @param n the target number (must be non-negative) * @return minimum number of squares needed + * @throws IllegalArgumentException if n is negative */ public static int minSquares(int n) { + if (n < 0) { + throw new IllegalArgumentException("Input must be non-negative"); + } + if (isPerfectSquare(n)) { return 1; } diff --git a/src/main/java/com/thealgorithms/maths/Volume.java b/src/main/java/com/thealgorithms/maths/Volume.java index 0f282b2abae2..488b921cae83 100644 --- a/src/main/java/com/thealgorithms/maths/Volume.java +++ b/src/main/java/com/thealgorithms/maths/Volume.java @@ -102,4 +102,39 @@ public static double volumePyramid(double baseArea, double height) { public static double volumeFrustumOfCone(double r1, double r2, double height) { return (Math.PI * height / 3) * (r1 * r1 + r2 * r2 + r1 * r2); } + + /** + * Calculate the volume of a frustum of a pyramid. + * + * @param upperBaseArea area of the upper base + * @param lowerBaseArea area of the lower base + * @param height height of the frustum + * @return volume of the frustum + */ + public static double volumeFrustumOfPyramid(double upperBaseArea, double lowerBaseArea, double height) { + return (upperBaseArea + lowerBaseArea + Math.sqrt(upperBaseArea * lowerBaseArea)) * height / 3; + } + + /** + * Calculate the volume of a torus. + * + * @param majorRadius major radius of a torus + * @param minorRadius minor radius of a torus + * @return volume of the torus + */ + public static double volumeTorus(double majorRadius, double minorRadius) { + return 2 * Math.PI * Math.PI * majorRadius * minorRadius * minorRadius; + } + + /** + * Calculate the volume of an ellipsoid. + * + * @param a first semi-axis of an ellipsoid + * @param b second semi-axis of an ellipsoid + * @param c third semi-axis of an ellipsoid + * @return volume of the ellipsoid + */ + public static double volumeEllipsoid(double a, double b, double c) { + return (4 * Math.PI * a * b * c) / 3; + } } diff --git a/src/main/java/com/thealgorithms/matrix/QRDecomposition.java b/src/main/java/com/thealgorithms/matrix/QRDecomposition.java new file mode 100644 index 000000000000..45f56bc14729 --- /dev/null +++ b/src/main/java/com/thealgorithms/matrix/QRDecomposition.java @@ -0,0 +1,149 @@ +package com.thealgorithms.matrix; + +/** + * @brief Implementation of QR Decomposition using the Gram-Schmidt process + * @details Decomposes a matrix A into an orthogonal matrix Q and an upper + * triangular matrix R such that A = Q * R. The Gram-Schmidt process + * orthogonalizes the columns of A to produce Q, and R is computed as Q^T * A. + * This decomposition is useful for solving linear least squares problems, + * eigenvalue computations, and numerical stability in linear algebra. + * @see QR Decomposition + */ +public final class QRDecomposition { + + private QRDecomposition() { + } + + /** + * A helper class to store both Q and R matrices + */ + public static class QR { + private final double[][] q; + private final double[][] r; + + QR(double[][] q, double[][] r) { + this.q = q; + this.r = r; + } + + public double[][] getQ() { + return q; + } + + public double[][] getR() { + return r; + } + } + + /** + * @brief Performs QR decomposition on a matrix using the Gram-Schmidt process + * @param matrix the input matrix (m x n) + * @return QR object containing orthogonal matrix Q (m x n) and upper triangular matrix R (n x n) + * @throws IllegalArgumentException if the matrix is null, empty, or has invalid rows + */ + public static QR decompose(double[][] matrix) { + validateInputMatrix(matrix); + + int m = matrix.length; + int n = matrix[0].length; + + double[][] q = new double[m][n]; + double[][] r = new double[n][n]; + + for (int j = 0; j < n; j++) { + double[] v = getColumn(matrix, j); + + for (int i = 0; i < j; i++) { + double[] qi = getColumn(q, i); + r[i][j] = dotProduct(qi, v); + v = subtractVectors(v, scalarMultiply(qi, r[i][j])); + } + + r[j][j] = norm(v); + if (r[j][j] == 0) { + throw new ArithmeticException("Matrix is rank deficient. Cannot perform QR decomposition."); + } + double[] qj = scalarMultiply(v, 1.0 / r[j][j]); + setColumn(q, j, qj); + } + + return new QR(q, r); + } + + private static double[] getColumn(double[][] matrix, int col) { + int m = matrix.length; + double[] column = new double[m]; + for (int i = 0; i < m; i++) { + column[i] = matrix[i][col]; + } + return column; + } + + private static void setColumn(double[][] matrix, int col, double[] column) { + for (int i = 0; i < matrix.length; i++) { + matrix[i][col] = column[i]; + } + } + + private static double dotProduct(double[] a, double[] b) { + double sum = 0; + for (int i = 0; i < a.length; i++) { + sum += a[i] * b[i]; + } + return sum; + } + + private static double[] subtractVectors(double[] a, double[] b) { + double[] result = new double[a.length]; + for (int i = 0; i < a.length; i++) { + result[i] = a[i] - b[i]; + } + return result; + } + + private static double[] scalarMultiply(double[] v, double scalar) { + double[] result = new double[v.length]; + for (int i = 0; i < v.length; i++) { + result[i] = v[i] * scalar; + } + return result; + } + + private static double norm(double[] v) { + return Math.sqrt(dotProduct(v, v)); + } + + private static void validateInputMatrix(double[][] matrix) { + if (matrix == null) { + throw new IllegalArgumentException("The input matrix cannot be null"); + } + if (matrix.length == 0) { + throw new IllegalArgumentException("The input matrix cannot be empty"); + } + if (!hasValidRows(matrix)) { + throw new IllegalArgumentException("The input matrix cannot have null or empty rows"); + } + if (isJaggedMatrix(matrix)) { + throw new IllegalArgumentException("The input matrix cannot be jagged"); + } + } + + private static boolean hasValidRows(double[][] matrix) { + for (double[] row : matrix) { + if (row == null || row.length == 0) { + return false; + } + } + return true; + } + + private static boolean isJaggedMatrix(double[][] matrix) { + int numColumns = matrix[0].length; + for (double[] row : matrix) { + if (row.length != numColumns) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/thealgorithms/others/Dijkstra.java b/src/main/java/com/thealgorithms/others/Dijkstra.java deleted file mode 100644 index a379100a2f3b..000000000000 --- a/src/main/java/com/thealgorithms/others/Dijkstra.java +++ /dev/null @@ -1,248 +0,0 @@ -package com.thealgorithms.others; - -import java.util.HashMap; -import java.util.Map; -import java.util.NavigableSet; -import java.util.TreeSet; -/** - * Dijkstra's algorithm,is a graph search algorithm that solves the - * single-source shortest path problem for a graph with nonnegative edge path - * costs, producing a shortest path tree. - * - *

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

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

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

+ * + *

+ * Key Operations: + *

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

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

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

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

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

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

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

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

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

+ * + *

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

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

+ * + *

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

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

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

+ * + *

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

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

+ * + *

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

+ * + *

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

+ * + * @see Prefix Sum (Wikipedia) + * @author Ruturaj Jadhav, ruturajjadhav07 + */ +public final class SubarraySumEqualsK { + + private SubarraySumEqualsK() { + // Utility class; prevent instantiation + } + + /** + * Counts the number of subarrays whose sum equals k. + * + * @param nums The input integer array. + * @param k The target sum. + * @return The number of continuous subarrays summing to k. + * @throws IllegalArgumentException if nums is null. + */ + public static int countSubarrays(int[] nums, int k) { + if (nums == null) { + throw new IllegalArgumentException("Input array cannot be null"); + } + + Map prefixSumFrequency = new HashMap<>(); + prefixSumFrequency.put(0L, 1); + + long prefixSum = 0; + int count = 0; + + for (int num : nums) { + prefixSum += num; + + long requiredSum = prefixSum - k; + count += prefixSumFrequency.getOrDefault(requiredSum, 0); + + prefixSumFrequency.put(prefixSum, prefixSumFrequency.getOrDefault(prefixSum, 0) + 1); + } + + return count; + } +} diff --git a/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java b/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java index 72e9a14ac070..d94bef69cd3a 100644 --- a/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java +++ b/src/main/java/com/thealgorithms/puzzlesandgames/TowerOfHanoi.java @@ -3,27 +3,32 @@ import java.util.List; /** - * The {@code TowerOfHanoi} class provides a recursive solution to the Tower of Hanoi puzzle. - * This puzzle involves moving a set of discs from one pole to another, following specific rules: + * Recursive solution to the Tower of Hanoi puzzle. + * + *

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

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

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

* *

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

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

+ * See Tower of Hanoi on Wikipedia. + *

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

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

*/ public static void shift(int n, String startPole, String intermediatePole, String endPole, List result) { - if (n != 0) { - // Move n-1 discs from startPole to intermediatePole - shift(n - 1, startPole, endPole, intermediatePole, result); + if (n < 0) { + throw new IllegalArgumentException("Number of discs must be non-negative"); + } + if (n == 0) { + return; + } - // Add the move of the nth disc from startPole to endPole - result.add(String.format("Move %d from %s to %s", n, startPole, endPole)); + // Move n-1 discs from startPole to intermediatePole + shift(n - 1, startPole, endPole, intermediatePole, result); - // Move the n-1 discs from intermediatePole to endPole - shift(n - 1, intermediatePole, startPole, endPole, result); - } + // Add the move of the nth disc from startPole to endPole + result.add(String.format("Move %d from %s to %s", n, startPole, endPole)); + + // Move the n-1 discs from intermediatePole to endPole + shift(n - 1, intermediatePole, startPole, endPole, result); } } diff --git a/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java b/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java index 9bc6da2f7443..404adfdf0cf1 100644 --- a/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java +++ b/src/main/java/com/thealgorithms/recursion/FibonacciSeries.java @@ -1,16 +1,36 @@ package com.thealgorithms.recursion; -/* - The Fibonacci series is a sequence of numbers where each number is the sum of the two preceding ones, - starting with 0 and 1. - NUMBER 0 1 2 3 4 5 6 7 8 9 10 ... - FIBONACCI 0 1 1 2 3 5 8 13 21 34 55 ... -*/ - +/** + * The Fibonacci series is a sequence of numbers where each number is the sum of the two preceding ones, + * starting with 0 and 1. + *

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

+ *

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

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

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

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

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

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

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

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

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

Example Usage: + *

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

How it works: + *

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

Time Complexity: O(log n) because we halve the search space each time. + * Space Complexity: O(log n) due to recursive call stack. * - * @param array The array to make the binary search - * @param key The number you are looking for - * @param left The lower bound - * @param right The upper bound - * @return the location of the key + * @param The type of elements (must be Comparable) + * @param array The sorted array to search in + * @param key The element we're looking for + * @param left The leftmost index of current search range (inclusive) + * @param right The rightmost index of current search range (inclusive) + * @return The index where key is located, or -1 if not found */ private > int search(T[] array, T key, int left, int right) { + // Base case: Search space is exhausted + // This happens when left pointer crosses right pointer if (right < left) { - return -1; // this means that the key not found + return -1; // Key not found in the array } - // find median - int median = (left + right) >>> 1; + + // Calculate middle index + // Using (left + right) / 2 could cause integer overflow for large arrays + // So we use: left + (right - left) / 2 which is mathematically equivalent + // but prevents overflow + int median = (left + right) >>> 1; // Unsigned right shift is faster division by 2 + + // Get the value at middle position for comparison int comp = key.compareTo(array[median]); + // Case 1: Found the target element at middle position if (comp == 0) { - return median; - } else if (comp < 0) { + return median; // Return the index where element was found + } + // Case 2: Target is smaller than middle element + // This means if target exists, it must be in the LEFT half + else if (comp < 0) { + // Recursively search the left half + // New search range: [left, median - 1] return search(array, key, left, median - 1); - } else { + } + // Case 3: Target is greater than middle element + // This means if target exists, it must be in the RIGHT half + else { + // Recursively search the right half + // New search range: [median + 1, right] return search(array, key, median + 1, right); } } diff --git a/src/main/java/com/thealgorithms/searches/ExponentialSearch.java b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java index 9187dcbc2f4b..e666b9148aaa 100644 --- a/src/main/java/com/thealgorithms/searches/ExponentialSearch.java +++ b/src/main/java/com/thealgorithms/searches/ExponentialSearch.java @@ -46,6 +46,9 @@ public > int find(T[] array, T key) { range = range * 2; } - return Arrays.binarySearch(array, range / 2, Math.min(range, array.length), key); + // The candidate block is the inclusive index range [range / 2, range], so the + // exclusive upper bound handed to binarySearch has to be range + 1. + final int index = Arrays.binarySearch(array, range / 2, Math.min(range + 1, array.length), key); + return index >= 0 ? index : -1; } } diff --git a/src/main/java/com/thealgorithms/searches/FibonacciSearch.java b/src/main/java/com/thealgorithms/searches/FibonacciSearch.java index 78dac0f0a712..fa91cd14a1af 100644 --- a/src/main/java/com/thealgorithms/searches/FibonacciSearch.java +++ b/src/main/java/com/thealgorithms/searches/FibonacciSearch.java @@ -69,7 +69,7 @@ public > int find(T[] array, T key) { } } - if (fibMinus1 == 1 && array[offset + 1] == key) { + if (fibMinus1 == 1 && offset + 1 < n && array[offset + 1].compareTo(key) == 0) { return offset + 1; } diff --git a/src/main/java/com/thealgorithms/searches/InterpolationSearch.java b/src/main/java/com/thealgorithms/searches/InterpolationSearch.java index 3ac6be25bf53..52dbc0b7e2c5 100644 --- a/src/main/java/com/thealgorithms/searches/InterpolationSearch.java +++ b/src/main/java/com/thealgorithms/searches/InterpolationSearch.java @@ -1,3 +1,4 @@ + package com.thealgorithms.searches; /** @@ -35,9 +36,12 @@ public int find(int[] array, int key) { // Since array is sorted, an element present // in array must be in range defined by corner while (start <= end && key >= array[start] && key <= array[end]) { + if (array[start] == array[end]) { + return start; + } // Probing the position with keeping // uniform distribution in mind. - int pos = start + (((end - start) / (array[end] - array[start])) * (key - array[start])); + int pos = start + (int) (((long) (end - start) * (key - array[start])) / ((long) array[end] - array[start])); // Condition of target found if (array[pos] == key) { diff --git a/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java b/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java index 05fab0534267..d051dbc7b823 100644 --- a/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java +++ b/src/main/java/com/thealgorithms/searches/IterativeBinarySearch.java @@ -3,50 +3,53 @@ import com.thealgorithms.devutils.searches.SearchAlgorithm; /** - * Binary search is one of the most popular algorithms This class represents - * iterative version {@link BinarySearch} Iterative binary search is likely to - * have lower constant factors because it doesn't involve the overhead of - * manipulating the call stack. But in java the recursive version can be - * optimized by the compiler to this version. + * Binary search is one of the most popular algorithms. + * This class represents the iterative version of {@link BinarySearch}. * - *

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

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

Performance: + *

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

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

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

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

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

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

+ * Edge Cases: + *

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

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

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

How it works step-by-step: + *

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

Example: + *

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

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

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

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

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

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

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

Complexity: + *

    + *
  • Time Complexity: $O(N \log N)$
  • + *
  • Space Complexity: $O(N)$
  • + *
+ */ +public final class ConcurrentMergeSort { + + private ConcurrentMergeSort() { + } + + /** + * Fallback threshold where the algorithm switches to standard sequential + * Merge Sort to prevent thread-creation overhead from ruining performance. + */ + private static final int SEQUENTIAL_THRESHOLD = 8192; + + /** + * Sorts the specified array of integers concurrently using Merge Sort. + * + * @param array the array to be sorted + */ + public static void sort(int[] array) { + if (array == null || array.length <= 1) { + return; + } + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + + // Calculate a safe maximum depth to prevent creating more tasks than the pool can handle. + // This effectively prevents thread starvation deadlock in fixed-size thread pools, + // by forcing leaf tasks to run sequentially and eventually complete. + int maxDepth = (int) (Math.log(availableProcessors) / Math.log(2)) + 1; + + ThreadPoolExecutor executor = new ThreadPoolExecutor(availableProcessors, availableProcessors, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); + + try { + int[] tempArray = new int[array.length]; + concurrentMergeSort(array, tempArray, 0, array.length - 1, executor, maxDepth); + } finally { + // Ensure the executor is gracefully shut down + executor.shutdown(); + } + } + + /** + * Recursively sorts the array utilizing the provided executor for concurrency. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + * @param executor the {@link ThreadPoolExecutor} to handle concurrent tasks + * @param depth the remaining depth for allowing concurrent execution + */ + private static void concurrentMergeSort(int[] array, int[] temp, int left, int right, ThreadPoolExecutor executor, int depth) { + int length = right - left + 1; + + // Switch to sequential sort if the array is small or we have reached the maximum concurrent depth + if (length < SEQUENTIAL_THRESHOLD || depth <= 0) { + sequentialMergeSort(array, temp, left, right); + return; + } + + int mid = left + (right - left) / 2; + + // Submit the left half for concurrent execution + CompletableFuture leftTask = CompletableFuture.runAsync(() -> concurrentMergeSort(array, temp, left, mid, executor, depth - 1), executor); + + // Process the right half in the current thread to optimize resource usage + concurrentMergeSort(array, temp, mid + 1, right, executor, depth - 1); + + // Wait for the concurrently executed left half to complete + leftTask.join(); + + merge(array, temp, left, mid, right); + } + + /** + * Sorts the specified sub-array sequentially using standard Merge Sort. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + */ + private static void sequentialMergeSort(int[] array, int[] temp, int left, int right) { + if (left >= right) { + return; + } + + int mid = left + (right - left) / 2; + sequentialMergeSort(array, temp, left, mid); + sequentialMergeSort(array, temp, mid + 1, right); + merge(array, temp, left, mid, right); + } + + /** + * Merges two sorted sub-arrays into a single sorted sub-array. + * + * @param array the original array containing the sub-arrays + * @param temp a temporary array used for merging + * @param left the starting index of the first sub-array + * @param mid the ending index of the first sub-array (and the partition point) + * @param right the ending index of the second sub-array + */ + private static void merge(int[] array, int[] temp, int left, int mid, int right) { + System.arraycopy(array, left, temp, left, right - left + 1); + + int i = left; + int j = mid + 1; + int k = left; + + while (i <= mid && j <= right) { + if (temp[i] <= temp[j]) { + array[k++] = temp[i++]; + } else { + array[k++] = temp[j++]; + } + } + + while (i <= mid) { + array[k++] = temp[i++]; + } + + // Remaining elements from the right half are already in their correct relative positions + } +} diff --git a/src/main/java/com/thealgorithms/sorts/InsertionSort.java b/src/main/java/com/thealgorithms/sorts/InsertionSort.java index fdbfd9cd1cfa..1e42f2a61271 100644 --- a/src/main/java/com/thealgorithms/sorts/InsertionSort.java +++ b/src/main/java/com/thealgorithms/sorts/InsertionSort.java @@ -33,30 +33,30 @@ public > T[] sort(T[] array) { } /** - * Sorts a subarray of the given array using the standard Insertion Sort algorithm. + * Sorts a subarray of the given items using the standard Insertion Sort algorithm. * - * @param array The array to be sorted - * @param lo The starting index of the subarray - * @param hi The ending index of the subarray (exclusive) - * @param The type of elements in the array, which must be comparable - * @return The sorted array + * @param items The items to be sorted + * @param startIndex The starting index of the subarray + * @param endIndex The ending index of the subarray (exclusive) + * @param The type of elements in the items, which must be comparable + * @return The sorted items */ - public > T[] sort(T[] array, final int lo, final int hi) { - if (array == null || lo >= hi) { - return array; + public > T[] sort(T[] items, final int startIndex, final int endIndex) { + if (items == null || startIndex >= endIndex) { + return items; } - for (int i = lo + 1; i < hi; i++) { - final T key = array[i]; + for (int i = startIndex + 1; i < endIndex; i++) { + final T key = items[i]; int j = i - 1; - while (j >= lo && SortUtils.less(key, array[j])) { - array[j + 1] = array[j]; + while (j >= startIndex && SortUtils.less(key, items[j])) { + items[j + 1] = items[j]; j--; } - array[j + 1] = key; + items[j + 1] = key; } - return array; + return items; } /** diff --git a/src/main/java/com/thealgorithms/sorts/LibrarySort.java b/src/main/java/com/thealgorithms/sorts/LibrarySort.java new file mode 100644 index 000000000000..28f16e3016ff --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/LibrarySort.java @@ -0,0 +1,207 @@ +package com.thealgorithms.sorts; + +import java.util.Arrays; + +/** + * Library Sort (also known as Gapped Insertion Sort) maintains a sparse + * working array with gaps distributed between elements, so that most + * insertions land directly in an empty gap without shifting anything. + * Elements are inserted in rounds that double in size (1, 2, 4, 8, ...); + * after each round the array is rebalanced so gaps are spread out evenly + * again for the next round. + * Time Complexity: O(n log n) expected, O(n^2) worst case if gaps collapse + * Space Complexity: O(n) + * + * @see + * Wikipedia: Library Sort + * @author Vraj Prajapati (@Rosander0) + */ +public final class LibrarySort { + + private static final int GAP_FACTOR = 2; + + private LibrarySort() { + // Utility class + } + + /** + * Sorts an array using the Library Sort algorithm. + * + * @param array the array to sort (must not be null) + * @return the sorted array + * @throws IllegalArgumentException if {@code array} is {@code null} + */ + public static int[] sort(final int[] array) { + if (array == null) { + throw new IllegalArgumentException("Input array must not be null."); + } + if (array.length <= 1) { + return array; + } + + final int n = array.length; + final int capacity = GAP_FACTOR * n; + final int[] data = new int[capacity]; + final boolean[] occupied = new boolean[capacity]; + + final int mid = capacity / 2; + data[mid] = array[0]; + occupied[mid] = true; + + int filled = 1; + int nextToInsert = 1; + int round = 0; + while (nextToInsert < n) { + final int roundSize = Math.min(1 << round, n - nextToInsert); + for (int i = 0; i < roundSize; i++) { + insert(data, occupied, array[nextToInsert + i]); + filled++; + } + nextToInsert += roundSize; + round++; + if (nextToInsert < n) { + rebalance(data, occupied, filled); + } + } + + int idx = 0; + for (int i = 0; i < capacity; i++) { + if (occupied[i]) { + array[idx++] = data[i]; + } + } + return array; + } + + /** + * Inserts {@code value} into the gapped array, placing it directly in an + * empty gap when possible, otherwise shifting toward the nearest gap. + */ + private static void insert(final int[] data, final boolean[] occupied, final int value) { + final int pos = findInsertionIndex(data, occupied, value); + if (pos >= data.length) { + insertAtEnd(data, occupied, value); + return; + } + + if (!occupied[pos]) { + data[pos] = value; + occupied[pos] = true; + return; + } + + int right = pos; + while (right < data.length && occupied[right]) { + right++; + } + int left = pos - 1; + while (left >= 0 && occupied[left]) { + left--; + } + + final boolean canGoRight = right < data.length; + final boolean canGoLeft = left >= 0; + + if (canGoRight && (!canGoLeft || (right - pos) <= (pos - left))) { + // Shift data[pos, right) one slot to the right, opening a gap at pos. + // occupied[pos] is untouched by the copy and was already true. + System.arraycopy(data, pos, data, pos + 1, right - pos); + occupied[right] = true; + data[pos] = value; + } else if (canGoLeft) { + // Shift data[left + 1, pos) one slot to the left, opening a gap at pos - 1. + // occupied[pos - 1] is untouched by the copy and was already true. + System.arraycopy(data, left + 1, data, left, pos - 1 - left); + occupied[left] = true; + data[pos - 1] = value; + } else { + // Unreachable in practice: canGoRight and canGoLeft can only both be false if + // every slot in this capacity-2n array is occupied, but at most n elements are + // ever present at once. Kept as a defensive guard against that invariant breaking. + throw new IllegalStateException("No gap available for insertion; rebalance too infrequent."); + } + } + + /** + * Handles insertion of a new global maximum, which must land after every + * currently occupied slot. Since there is no room to its right, this + * shifts occupied slots left into the nearest gap instead. + */ + private static void insertAtEnd(final int[] data, final boolean[] occupied, final int value) { + final int last = data.length - 1; + // occupied[last] is unreachable as false here: insertAtEnd() is only called when + // findInsertionIndex() returns data.length, which requires data[last] to already be + // occupied. Kept as a defensive guard in case that invariant is ever broken. + if (!occupied[last]) { + data[last] = value; + occupied[last] = true; + return; + } + int left = last - 1; + while (left >= 0 && occupied[left]) { + left--; + } + // left < 0 is unreachable in practice: at most n elements ever occupy this + // capacity-2n array, so fewer than half the slots left of `last` can be filled, + // guaranteeing a gap exists before the scan reaches index -1. + if (left < 0) { + throw new IllegalStateException("No gap available for insertion; rebalance too infrequent."); + } + // Shift data[left + 1, last] one slot to the left, opening a gap at last. + // occupied[last] is untouched by the copy and was already true. + System.arraycopy(data, left + 1, data, left, last - left); + occupied[left] = true; + data[last] = value; + } + + /** + * Finds the leftmost index at which {@code value} can be inserted so + * that occupied slots remain sorted. Empty slots are compared using the + * value of the nearest occupied slot at or after them, which is a + * monotonic function of index and therefore safe to binary search over. + */ + private static int findInsertionIndex(final int[] data, final boolean[] occupied, final int value) { + int lo = 0; + int hi = data.length; + while (lo < hi) { + final int mid = lo + (hi - lo) / 2; + final int probe = nearestOccupiedValueAtOrAfter(data, occupied, mid); + if (probe != Integer.MAX_VALUE && probe <= value) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; + } + + private static int nearestOccupiedValueAtOrAfter(final int[] data, final boolean[] occupied, final int index) { + for (int i = index; i < data.length; i++) { + if (occupied[i]) { + return data[i]; + } + } + return Integer.MAX_VALUE; + } + + /** + * Redistributes the {@code filled} occupied elements evenly across the + * full capacity of {@code data}, restoring uniform gaps between them. + */ + private static void rebalance(final int[] data, final boolean[] occupied, final int filled) { + final int capacity = data.length; + final int[] temp = new int[filled]; + int idx = 0; + for (int i = 0; i < capacity; i++) { + if (occupied[i]) { + temp[idx++] = data[i]; + } + } + Arrays.fill(occupied, false); + for (int k = 0; k < filled; k++) { + final int pos = (int) ((long) k * capacity / filled); + data[pos] = temp[k]; + occupied[pos] = true; + } + } +} diff --git a/src/main/java/com/thealgorithms/sorts/MergeSort.java b/src/main/java/com/thealgorithms/sorts/MergeSort.java index f7a7c8da004d..5db9c48b4f61 100644 --- a/src/main/java/com/thealgorithms/sorts/MergeSort.java +++ b/src/main/java/com/thealgorithms/sorts/MergeSort.java @@ -10,7 +10,7 @@ @SuppressWarnings("rawtypes") class MergeSort implements SortAlgorithm { - private Comparable[] aux; + private Comparable[] tempArray; /** * Generic merge sort algorithm. @@ -26,7 +26,7 @@ class MergeSort implements SortAlgorithm { */ @Override public > T[] sort(T[] unsorted) { - aux = new Comparable[unsorted.length]; + tempArray = new Comparable[unsorted.length]; doSort(unsorted, 0, unsorted.length - 1); return unsorted; } @@ -58,17 +58,17 @@ private > void doSort(T[] arr, int left, int right) { private > void merge(T[] arr, int left, int mid, int right) { int i = left; int j = mid + 1; - System.arraycopy(arr, left, aux, left, right + 1 - left); + System.arraycopy(arr, left, tempArray, left, right + 1 - left); for (int k = left; k <= right; k++) { if (j > right) { - arr[k] = (T) aux[i++]; + arr[k] = (T) tempArray[i++]; } else if (i > mid) { - arr[k] = (T) aux[j++]; - } else if (less(aux[j], aux[i])) { - arr[k] = (T) aux[j++]; + arr[k] = (T) tempArray[j++]; + } else if (less(tempArray[j], tempArray[i])) { + arr[k] = (T) tempArray[j++]; } else { - arr[k] = (T) aux[i++]; + arr[k] = (T) tempArray[i++]; } } } diff --git a/src/main/java/com/thealgorithms/sorts/PancakeSort.java b/src/main/java/com/thealgorithms/sorts/PancakeSort.java index 6079672a1d77..6522aefd7ae3 100644 --- a/src/main/java/com/thealgorithms/sorts/PancakeSort.java +++ b/src/main/java/com/thealgorithms/sorts/PancakeSort.java @@ -15,7 +15,7 @@ public > T[] sort(T[] array) { } for (int currentSize = 0; currentSize < array.length; currentSize++) { - int maxIndex = findMaxIndex(array, currentSize); + int maxIndex = findIndexOfMax(array, currentSize); SortUtils.flip(array, maxIndex, array.length - 1 - currentSize); } @@ -30,7 +30,7 @@ public > T[] sort(T[] array) { * @param the type of elements in the array * @return the index of the maximum element */ - private > int findMaxIndex(T[] array, int currentSize) { + private > int findIndexOfMax(T[] array, int currentSize) { T max = array[0]; int maxIndex = 0; for (int i = 0; i < array.length - currentSize; i++) { diff --git a/src/main/java/com/thealgorithms/sorts/SelectionSort.java b/src/main/java/com/thealgorithms/sorts/SelectionSort.java index 2d1814441701..e6d1a16a7af7 100644 --- a/src/main/java/com/thealgorithms/sorts/SelectionSort.java +++ b/src/main/java/com/thealgorithms/sorts/SelectionSort.java @@ -1,5 +1,10 @@ package com.thealgorithms.sorts; +/** + * Implementation of the Selection Sort algorithm. + * + * @see SortAlgorithm + */ public class SelectionSort implements SortAlgorithm { /** * Generic Selection Sort algorithm. @@ -11,11 +16,12 @@ public class SelectionSort implements SortAlgorithm { * * Space Complexity: O(1) – in-place sorting. * - * @see SortAlgorithm + * @param array the array to be sorted + * @param the type of elements in the array + * @return the sorted array */ @Override public > T[] sort(T[] array) { - for (int i = 0; i < array.length - 1; i++) { final int minIndex = findIndexOfMin(array, i); SortUtils.swap(array, i, minIndex); @@ -23,6 +29,14 @@ public > T[] sort(T[] array) { return array; } + /** + * Finds the index of the minimum element in the array starting from a given index. + * + * @param array the array to search + * @param startIndex the index to start searching from + * @param the type of elements in the array + * @return the index of the minimum element + */ private static > int findIndexOfMin(T[] array, final int startIndex) { int minIndex = startIndex; for (int i = startIndex + 1; i < array.length; i++) { diff --git a/src/main/java/com/thealgorithms/sorts/SmoothSort.java b/src/main/java/com/thealgorithms/sorts/SmoothSort.java new file mode 100644 index 000000000000..c45d6f1f02b2 --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/SmoothSort.java @@ -0,0 +1,168 @@ +package com.thealgorithms.sorts; + +/** + * Smooth Sort is an in-place, comparison-based sorting algorithm proposed by Edsger W. Dijkstra (1981). + * + *

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

Time Complexity: + *

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

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

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

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

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

Example: for prices [100, 80, 60, 70, 60, 75, 85], the spans are + * [1, 1, 1, 2, 1, 4, 6]. + */ +public final class StockSpanProblem { + private StockSpanProblem() { + } + + /** + * Calculates the stock span for each price in the input array. + * + * @param prices the stock prices + * @return the span for each day + * @throws IllegalArgumentException if the input array is null + */ + public static int[] calculateSpan(int[] prices) { + if (prices == null) { + throw new IllegalArgumentException("Input prices cannot be null"); + } + + int[] spans = new int[prices.length]; + Stack stack = new Stack<>(); + + // Small example: + // prices = [100, 80, 60, 70] + // spans = [ 1, 1, 1, 2] + // When we process 70, we pop 60 because 60 <= 70, so the span becomes 2. + // + // The stack stores indices of days with prices greater than the current day's price. + for (int index = 0; index < prices.length; index++) { + // Remove all previous days whose prices are less than or equal to the current price. + while (!stack.isEmpty() && prices[stack.peek()] <= prices[index]) { + stack.pop(); + } + + // If the stack is empty, there is no earlier day with a greater price, + // so the count will be from day 0 to this day (index + 1). + // + // Otherwise, the span is the number of days between + // the nearest earlier day with a greater price and the current day. + spans[index] = stack.isEmpty() ? index + 1 : index - stack.peek(); + + // Store the current index as a candidate for future span calculations. + stack.push(index); + } + + return spans; + } +} diff --git a/src/main/java/com/thealgorithms/strings/Alphabetical.java b/src/main/java/com/thealgorithms/strings/Alphabetical.java index ef2974eb427d..37b1fb068b44 100644 --- a/src/main/java/com/thealgorithms/strings/Alphabetical.java +++ b/src/main/java/com/thealgorithms/strings/Alphabetical.java @@ -1,32 +1,58 @@ package com.thealgorithms.strings; +import java.util.Locale; + /** - * Utility class for checking if a string's characters are in alphabetical order. + * Utility class for checking whether a string's characters are in non-decreasing + * lexicographical order based on Unicode code points (case-insensitive). + *

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

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

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

+ * Rules: + *

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

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

+ * + * @see LCP array - Wikipedia + */ +public final class KasaiAlgorithm { + + private KasaiAlgorithm() { + } + + /** + * Computes the LCP array using Kasai's algorithm. + * + * @param text the original string + * @param suffixArr the suffix array of the string + * @return the LCP array of length N, where LCP[i] is the length of the longest + * common prefix of the suffixes indexed by suffixArr[i] and suffixArr[i+1]. + * The last element LCP[N-1] is always 0. + * @throws IllegalArgumentException if text or suffixArr is null, or their lengths differ + */ + public static int[] kasai(String text, int[] suffixArr) { + if (text == null || suffixArr == null) { + throw new IllegalArgumentException("Text and suffix array must not be null."); + } + int n = text.length(); + if (suffixArr.length != n) { + throw new IllegalArgumentException("Suffix array length must match text length."); + } + if (n == 0) { + return new int[0]; + } + + // Compute the inverse suffix array + // invSuff[i] stores the index of the suffix text.substring(i) in the suffix array + int[] invSuff = new int[n]; + for (int i = 0; i < n; i++) { + if (suffixArr[i] < 0 || suffixArr[i] >= n) { + throw new IllegalArgumentException("Suffix array contains out-of-bounds index."); + } + invSuff[suffixArr[i]] = i; + } + + int[] lcp = new int[n]; + int k = 0; // Length of the longest common prefix + + for (int i = 0; i < n; i++) { + // Suffix at index i has not a next suffix in suffix array + int rank = invSuff[i]; + if (rank == n - 1) { + k = 0; + continue; + } + + int nextSuffixIndex = suffixArr[rank + 1]; + + // Directly match characters to find LCP + while (i + k < n && nextSuffixIndex + k < n && text.charAt(i + k) == text.charAt(nextSuffixIndex + k)) { + k++; + } + + lcp[rank] = k; + + // Delete the starting character from the string + if (k > 0) { + k--; + } + } + + return lcp; + } +} diff --git a/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java b/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java new file mode 100644 index 000000000000..b2190316aff2 --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LongestCommonSubstring.java @@ -0,0 +1,55 @@ +package com.thealgorithms.strings; + +/** + * Longest Common Substring finds the longest string that is a + * contiguous substring of two input strings. + * Example: "abcdef" and "zcdemf" -> "cde" + * + * @see + * Wikipedia: Longest Common Substring + * + * author: Vraj Prajapati @Rosander0 + */ +public final class LongestCommonSubstring { + + private LongestCommonSubstring() { + // Utility class + } + + /** + * Finds the longest common substring of two strings. + * + * @param a First input string + * @param b Second input string + * @return The longest common substring, or empty string if none exists. + * If multiple substrings share the maximum length, the first one found is returned. + */ + public static String longestCommonSubstring(final String a, final String b) { + if (a == null || b == null || a.isEmpty() || b.isEmpty()) { + return ""; + } + + int[][] dp = new int[a.length() + 1][b.length() + 1]; + int maxLength = 0; + int endIndex = 0; + + for (int i = 1; i <= a.length(); i++) { + for (int j = 1; j <= b.length(); j++) { + if (a.charAt(i - 1) == b.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + if (dp[i][j] > maxLength) { + maxLength = dp[i][j]; + endIndex = i; + } + } else { + dp[i][j] = 0; + } + } + } + + if (maxLength == 0) { + return ""; + } + return a.substring(endIndex - maxLength, endIndex); + } +} diff --git a/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java b/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java index 6808cd50602f..51e8dc6b02c3 100644 --- a/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java +++ b/src/main/java/com/thealgorithms/strings/LongestNonRepetitiveSubstring.java @@ -13,6 +13,12 @@ private LongestNonRepetitiveSubstring() { /** * Finds the length of the longest substring without repeating characters. * + * Uses the sliding window technique with a HashMap to track + * the last seen index of each character. + * + * Time Complexity: O(n), where n is the length of the input string. + * Space Complexity: O(min(n, m)), where m is the size of the character set. + * * @param s the input string * @return the length of the longest non-repetitive substring */ diff --git a/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java b/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java deleted file mode 100644 index ca500357ba77..000000000000 --- a/src/main/java/com/thealgorithms/strings/LongestPalindromicSubstring.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.thealgorithms.strings; - -final class LongestPalindromicSubstring { - private LongestPalindromicSubstring() { - } - - /** - * Finds the longest palindromic substring in the given string. - * - * @param s the input string - * @return the longest palindromic substring - */ - public static String longestPalindrome(String s) { - if (s == null || s.isEmpty()) { - return ""; - } - String maxStr = ""; - for (int i = 0; i < s.length(); ++i) { - for (int j = i; j < s.length(); ++j) { - if (isValid(s, i, j) && (j - i + 1 > maxStr.length())) { - maxStr = s.substring(i, j + 1); - } - } - } - return maxStr; - } - - private static boolean isValid(String s, int lo, int hi) { - int n = hi - lo + 1; - for (int i = 0; i < n / 2; ++i) { - if (s.charAt(lo + i) != s.charAt(hi - i)) { - return false; - } - } - return true; - } -} diff --git a/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java b/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java new file mode 100644 index 000000000000..87c9278fd4bf --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/LongestRepeatedSubstring.java @@ -0,0 +1,83 @@ +package com.thealgorithms.strings; + +/** + * Finds the longest substring that occurs at least twice in a given string. + * + *

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

+ * + *

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

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

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

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

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

Reference: + * https://en.wikipedia.org/wiki/Top-k_problem + * + */ +public final class TopKFrequentWords { + private TopKFrequentWords() { + } + + /** + * Finds the k most frequent words. + * + * @param words input array of words + * @param k number of words to return + * @return list of top-k words ordered by frequency then lexicographical order + * @throws IllegalArgumentException if words is null, k is negative, or words contains null + */ + public static List findTopKFrequentWords(String[] words, int k) { + if (words == null) { + throw new IllegalArgumentException("Input words array cannot be null."); + } + if (k < 0) { + throw new IllegalArgumentException("k cannot be negative."); + } + if (k == 0 || words.length == 0) { + return List.of(); + } + + Map frequency = new HashMap<>(); + for (String word : words) { + if (word == null) { + throw new IllegalArgumentException("Input words cannot contain null values."); + } + frequency.put(word, frequency.getOrDefault(word, 0) + 1); + } + + List candidates = new ArrayList<>(frequency.keySet()); + candidates.sort(Comparator.comparingInt(frequency::get).reversed().thenComparing(Comparator.naturalOrder())); + + int limit = Math.min(k, candidates.size()); + return new ArrayList<>(candidates.subList(0, limit)); + } +} diff --git a/src/main/java/com/thealgorithms/strings/ValidParentheses.java b/src/main/java/com/thealgorithms/strings/ValidParentheses.java deleted file mode 100644 index 25a72f379dec..000000000000 --- a/src/main/java/com/thealgorithms/strings/ValidParentheses.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.thealgorithms.strings; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Map; - -/** - * Validates if a given string has valid matching parentheses. - *

- * A string is considered valid if: - *

    - *
  • Open brackets are closed by the same type of brackets.
  • - *
  • Brackets are closed in the correct order.
  • - *
  • Every closing bracket has a corresponding open bracket of the same type.
  • - *
- * - * Allowed characters: '(', ')', '{', '}', '[', ']' - */ -public final class ValidParentheses { - private ValidParentheses() { - } - - private static final Map BRACKET_PAIRS = Map.of(')', '(', '}', '{', ']', '['); - - /** - * Checks if the input string has valid parentheses. - * - * @param s the string containing only bracket characters - * @return true if valid, false otherwise - * @throws IllegalArgumentException if the string contains invalid characters or is null - */ - public static boolean isValid(String s) { - if (s == null) { - throw new IllegalArgumentException("Input string cannot be null"); - } - - Deque stack = new ArrayDeque<>(); - - for (char c : s.toCharArray()) { - if (BRACKET_PAIRS.containsValue(c)) { - stack.push(c); // opening bracket - } else if (BRACKET_PAIRS.containsKey(c)) { - if (stack.isEmpty() || stack.pop() != BRACKET_PAIRS.get(c)) { - return false; - } - } else { - throw new IllegalArgumentException("Unexpected character: " + c); - } - } - - return stack.isEmpty(); - } -} diff --git a/src/test/java/com/thealgorithms/backtracking/CombinationTest.java b/src/test/java/com/thealgorithms/backtracking/CombinationTest.java index a9d1163f3ecd..5d2f99ccadf8 100644 --- a/src/test/java/com/thealgorithms/backtracking/CombinationTest.java +++ b/src/test/java/com/thealgorithms/backtracking/CombinationTest.java @@ -28,16 +28,16 @@ void testNoElement() { @Test void testLengthOne() { List> result = Combination.combination(new Integer[] {1, 2}, 1); - assertTrue(result.get(0).iterator().next() == 1); - assertTrue(result.get(1).iterator().next() == 2); + assertEquals(1, result.get(0).iterator().next()); + assertEquals(2, result.get(1).iterator().next()); } @Test void testLengthTwo() { List> result = Combination.combination(new Integer[] {1, 2}, 2); Integer[] arr = result.get(0).toArray(new Integer[2]); - assertTrue(arr[0] == 1); - assertTrue(arr[1] == 2); + assertEquals(1, arr[0]); + assertEquals(2, arr[1]); } @Test diff --git a/src/test/java/com/thealgorithms/backtracking/PermutationTest.java b/src/test/java/com/thealgorithms/backtracking/PermutationTest.java index 76a714829109..54747e5e73a1 100644 --- a/src/test/java/com/thealgorithms/backtracking/PermutationTest.java +++ b/src/test/java/com/thealgorithms/backtracking/PermutationTest.java @@ -12,13 +12,13 @@ public class PermutationTest { @Test void testNoElement() { List result = Permutation.permutation(new Integer[] {}); - assertEquals(result.get(0).length, 0); + assertEquals(0, result.get(0).length); } @Test void testSingleElement() { List result = Permutation.permutation(new Integer[] {1}); - assertEquals(result.get(0)[0], 1); + assertEquals(1, result.get(0)[0]); } @Test diff --git a/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java b/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java new file mode 100644 index 000000000000..ecd1f3c4dfae --- /dev/null +++ b/src/test/java/com/thealgorithms/backtracking/RatInAMazeTest.java @@ -0,0 +1,99 @@ +package com.thealgorithms.backtracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class RatInAMazeTest { + + @Test + void testMultiplePathsExist() { + int[][] maze = {{1, 0, 0, 0}, {1, 1, 0, 1}, {0, 1, 0, 0}, {0, 1, 1, 1}}; + + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() >= 1); + for (String path : paths) { + assertTrue(path.chars().allMatch(c -> "DLRU".indexOf(c) >= 0)); + } + } + + @Test + void testSinglePath() { + int[][] maze = {{1, 0, 0}, {1, 1, 0}, {0, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertEquals(1, paths.size()); + assertEquals("DRDR", paths.get(0)); + } + + @Test + void testNoPathExists() { + int[][] maze = {{1, 0, 0}, {0, 0, 0}, {0, 0, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testSourceBlocked() { + int[][] maze = {{0, 1}, {1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testDestinationBlocked() { + int[][] maze = {{1, 1}, {1, 0}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testSingleCellMazeOpen() { + int[][] maze = {{1}}; + List paths = RatInAMaze.findPaths(maze); + assertEquals(1, paths.size()); + assertEquals("", paths.get(0)); + } + + @Test + void testSingleCellMazeBlocked() { + int[][] maze = {{0}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.isEmpty()); + } + + @Test + void testNullMazeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(null)); + } + + @Test + void testEmptyMazeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(new int[][] {})); + } + + @Test + void testNonSquareMazeThrowsException() { + int[][] maze = {{1, 0, 1}, {1, 1, 1}}; + assertThrows(IllegalArgumentException.class, () -> RatInAMaze.findPaths(maze)); + } + + @Test + void testAllCellsOpen() { + int[][] maze = {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() > 1); + } + + @Test + void testLargerMazeWithPath() { + int[][] maze = {{1, 1, 1, 1}, {0, 1, 0, 1}, {0, 1, 0, 1}, {0, 1, 1, 1}}; + List paths = RatInAMaze.findPaths(maze); + assertTrue(paths.size() >= 1); + for (String path : paths) { + assertTrue(path.chars().allMatch(c -> "DLRU".indexOf(c) >= 0), "Path contains invalid characters: " + path); + } + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/CaesarTest.java b/src/test/java/com/thealgorithms/ciphers/CaesarTest.java index 7aa41c4cf423..c8b20ad8d8f9 100644 --- a/src/test/java/com/thealgorithms/ciphers/CaesarTest.java +++ b/src/test/java/com/thealgorithms/ciphers/CaesarTest.java @@ -32,6 +32,29 @@ void caesarDecryptTest() { assertEquals("Encrypt this text", cipherText); } + @Test + void caesarEncryptWithNegativeShiftTest() { + // a shift of -1 must wrap 'A' backwards to 'Z', like a shift of +25 would + assertEquals("Z", caesar.encode("A", -1)); + assertEquals("z", caesar.encode("a", -1)); + assertEquals("EBIIL", caesar.encode("HELLO", -3)); + } + + @Test + void caesarDecryptWithNegativeShiftTest() { + assertEquals("A", caesar.decode("Z", -1)); + assertEquals("HELLO", caesar.decode("EBIIL", -3)); + } + + @Test + void caesarNegativeShiftRoundTripTest() { + // encode followed by decode with the same shift must return the original text + for (int shift : new int[] {-1, -5, -25, -26, -27, -52}) { + String message = "The quick brown Fox"; + assertEquals(message, caesar.decode(caesar.encode(message, shift), shift)); + } + } + @Test void caesarBruteForce() { // given diff --git a/src/test/java/com/thealgorithms/ciphers/ECCTest.java b/src/test/java/com/thealgorithms/ciphers/ECCTest.java index 701f801af1c8..b78ba51f7c3e 100644 --- a/src/test/java/com/thealgorithms/ciphers/ECCTest.java +++ b/src/test/java/com/thealgorithms/ciphers/ECCTest.java @@ -37,7 +37,7 @@ void testEncrypt() { System.out.println("Base Point G: " + curve.getBasePoint()); // Verify that the ciphertext is not empty - assertEquals(cipherText.length, 2); // Check if the ciphertext contains two points (R and S) + assertEquals(2, cipherText.length); // Check if the ciphertext contains two points (R and S) // Output the encrypted coordinate points System.out.println("Encrypted Points:"); diff --git a/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java new file mode 100644 index 000000000000..63dec4846bbc --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java @@ -0,0 +1,145 @@ +package com.thealgorithms.ciphers; + +import java.math.BigInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Unit tests for ElGamalCipher. + * Includes property-based testing (homomorphism), probabilistic checks, + * and boundary validation. + */ +class ElGamalCipherTest { + + private static ElGamalCipher.KeyPair sharedKeys; + + @BeforeAll + static void setup() { + // Generate 256-bit keys for efficient unit testing + sharedKeys = ElGamalCipher.generateKeys(256); + } + + @Test + @DisplayName("Test Key Generation Validity") + void testKeyGeneration() { + Assertions.assertNotNull(sharedKeys.p()); + Assertions.assertNotNull(sharedKeys.g()); + Assertions.assertNotNull(sharedKeys.x()); + Assertions.assertNotNull(sharedKeys.y()); + + // Verify generator bounds: 1 < g < p + Assertions.assertTrue(sharedKeys.g().compareTo(BigInteger.ONE) > 0); + Assertions.assertTrue(sharedKeys.g().compareTo(sharedKeys.p()) < 0); + + // Verify private key bounds: 1 < x < p-1 + Assertions.assertTrue(sharedKeys.x().compareTo(BigInteger.ONE) > 0); + Assertions.assertTrue(sharedKeys.x().compareTo(sharedKeys.p().subtract(BigInteger.ONE)) < 0); + } + + @Test + @DisplayName("Security Check: Probabilistic Encryption") + void testSemanticSecurity() { + // Encrypting the same message twice MUST yield different ciphertexts + // due to the random ephemeral key 'k'. + BigInteger message = new BigInteger("123456789"); + + ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Check that the ephemeral keys (and thus 'a' components) were different + Assertions.assertNotEquals(c1.a(), c2.a(), "Ciphertexts must be randomized (Semantic Security violation)"); + Assertions.assertNotEquals(c1.b(), c2.b()); + + // But both must decrypt to the original message + Assertions.assertEquals(ElGamalCipher.decrypt(c1, sharedKeys.x(), sharedKeys.p()), message); + Assertions.assertEquals(ElGamalCipher.decrypt(c2, sharedKeys.x(), sharedKeys.p()), message); + } + + @ParameterizedTest + @MethodSource("provideMessages") + @DisplayName("Parameterized Test: Encrypt and Decrypt various messages") + void testEncryptDecrypt(String messageStr) { + BigInteger message = new BigInteger(messageStr.getBytes()); + + // Skip if message exceeds the test key size (256 bits) + if (message.compareTo(sharedKeys.p()) >= 0) { + return; + } + + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(message, decrypted, "Decrypted BigInteger must match original"); + Assertions.assertEquals(messageStr, new String(decrypted.toByteArray()), "Decrypted string must match original"); + } + + static Stream provideMessages() { + return Stream.of("Hello World", "TheAlgorithms", "A", "1234567890", "!@#$%^&*()"); + } + + @Test + @DisplayName("Edge Case: Message equals 0") + void testMessageZero() { + BigInteger zero = BigInteger.ZERO; + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(zero, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(zero, decrypted, "Should successfully encrypt/decrypt zero"); + } + + @Test + @DisplayName("Edge Case: Message equals p-1") + void testMessageMaxBound() { + BigInteger pMinus1 = sharedKeys.p().subtract(BigInteger.ONE); + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(pMinus1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p()); + + Assertions.assertEquals(pMinus1, decrypted, "Should successfully encrypt/decrypt p-1"); + } + + @Test + @DisplayName("Negative Test: Message >= p should fail") + void testMessageTooLarge() { + BigInteger tooLarge = sharedKeys.p(); + Assertions.assertThrows(IllegalArgumentException.class, () -> ElGamalCipher.encrypt(tooLarge, sharedKeys.p(), sharedKeys.g(), sharedKeys.y())); + } + + @Test + @DisplayName("Negative Test: Decrypt with wrong private key") + void testWrongKeyDecryption() { + BigInteger message = new BigInteger("99999"); + ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Generate a fake private key + BigInteger wrongX = sharedKeys.x().add(BigInteger.ONE); + + BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, wrongX, sharedKeys.p()); + + Assertions.assertNotEquals(message, decrypted, "Decryption with wrong key must yield incorrect result"); + } + + @Test + @DisplayName("Property Test: Multiplicative Homomorphism") + void testHomomorphism() { + BigInteger m1 = new BigInteger("50"); + BigInteger m2 = BigInteger.TEN; // Fix: Replaced new BigInteger("10") with BigInteger.TEN + + ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(m1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(m2, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()); + + // Multiply ciphertexts component-wise: (a1*a2, b1*b2) + BigInteger aNew = c1.a().multiply(c2.a()).mod(sharedKeys.p()); + BigInteger bNew = c1.b().multiply(c2.b()).mod(sharedKeys.p()); + ElGamalCipher.CipherText cCombined = new ElGamalCipher.CipherText(aNew, bNew); + + BigInteger decrypted = ElGamalCipher.decrypt(cCombined, sharedKeys.x(), sharedKeys.p()); + BigInteger expected = m1.multiply(m2).mod(sharedKeys.p()); + + Assertions.assertEquals(expected, decrypted, "Cipher must satisfy multiplicative homomorphism"); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java b/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java index 4ba6787cc97e..ecb7455c1ba2 100644 --- a/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java +++ b/src/test/java/com/thealgorithms/ciphers/PermutationCipherTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.ciphers; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -121,8 +122,8 @@ void testNullString() { String decrypted = cipher.decrypt(encrypted, key); // then - assertEquals(null, encrypted); - assertEquals(null, decrypted); + assertNull(encrypted); + assertNull(decrypted); } @Test diff --git a/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java b/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java new file mode 100644 index 000000000000..041f8c0dd4c1 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/RailFenceCipherTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class RailFenceCipherTest { + + private final RailFenceCipher railFenceCipher = new RailFenceCipher(); + + @Test + void testEncrypt() { + assertEquals("WECRLTEERDSOEEFEAOCAIVDEN", railFenceCipher.encrypt("WEAREDISCOVEREDFLEEATONCE", 3)); + } + + @Test + void testDecrypt() { + assertEquals("WEAREDISCOVEREDFLEEATONCE", railFenceCipher.decrypt("WECRLTEERDSOEEFEAOCAIVDEN", 3)); + } + + @ParameterizedTest + @CsvSource({"HELLOWORLD, 2", "HELLOWORLD, 3", "HELLOWORLD, 4", "ATTACKATDAWN, 5", "abcdefghij, 6"}) + void testRoundTrip(String message, int rails) { + assertEquals(message, railFenceCipher.decrypt(railFenceCipher.encrypt(message, rails), rails)); + } + + /** + * Every character of the input must survive encryption, including the ones that used to collide + * with the placeholder that marked unused cells of the rail matrix. + */ + @ParameterizedTest + @ValueSource(strings = {"ab\ncdef", "line1\nline2\nline3", "\n\n\n\n\n", "a\nb", "tabs\tand\nnewlines\r\n"}) + void testControlCharactersArePreserved(String message) { + for (int rails = 2; rails <= 5; rails++) { + String encrypted = railFenceCipher.encrypt(message, rails); + assertEquals(message.length(), encrypted.length(), "characters were dropped with " + rails + " rails"); + assertEquals(message, railFenceCipher.decrypt(encrypted, rails), "round trip failed with " + rails + " rails"); + } + } + + @Test + void testEncryptWithNewlineMatchesReferencePattern() { + // Rails of "ab\ncdef" with 3 rails: {a, d} / {b, c, e} / {\n, f} + assertEquals("adbce\nf", railFenceCipher.encrypt("ab\ncdef", 3)); + } + + @ParameterizedTest + @CsvSource({"HELLO, 1", "HELLO, 5", "HELLO, 9", "'', 1", "'', 4"}) + void testDegenerateRailCountsReturnInput(String message, int rails) { + assertEquals(message, railFenceCipher.encrypt(message, rails)); + assertEquals(message, railFenceCipher.decrypt(message, rails)); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, -7}) + void testNonPositiveRailCountThrows(int rails) { + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt("HELLO", rails)); + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt("HELLO", rails)); + } + + @Test + void testNullInputThrows() { + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt(null, 3)); + assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt(null, 3)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java b/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java new file mode 100644 index 000000000000..f919417899db --- /dev/null +++ b/src/test/java/com/thealgorithms/compression/HuffmanCodingTest.java @@ -0,0 +1,110 @@ +package com.thealgorithms.compression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class HuffmanCodingTest { + + @Test + void testStandardLifecycle() { + String input = "efficiency is key"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertNotNull(encoded); + assertTrue(encoded.matches("[01]+")); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testNullAndEmptyHandling() { + HuffmanCoding huffman = new HuffmanCoding(""); + assertEquals("", huffman.encode("")); + assertEquals("", huffman.decode("")); + + HuffmanCoding huffmanNull = new HuffmanCoding(null); + assertEquals("", huffmanNull.encode(null)); + assertEquals("", huffmanNull.decode(null)); + } + + @Test + void testSingleCharacterEdgeCase() { + String input = "aaaaa"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertEquals("00000", encoded); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testUnicodeAndSpecialCharacters() { + // Tests spacing, symbols, non-latin alphabets, and surrogate pairs (emojis) + String input = "Hello, World! πŸš€\nLine 2: こんにけは"; + HuffmanCoding huffman = new HuffmanCoding(input); + + String encoded = huffman.encode(input); + assertEquals(input, huffman.decode(encoded)); + } + + @Test + void testFailFastOnUnseenCharacter() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> huffman.encode("abcd") // 'd' was not in the original tree + ); + assertTrue(exception.getMessage().contains("not found in Huffman dictionary")); + } + + @Test + void testFailFastOnInvalidBinaryCharacter() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + String encoded = huffman.encode("abc"); + + // Inject a '2' into the binary stream + String corruptedEncoded = encoded + "2"; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> huffman.decode(corruptedEncoded)); + assertTrue(exception.getMessage().contains("contains invalid characters")); + } + + @Test + void testFailFastOnIncompleteSequence() { + HuffmanCoding huffman = new HuffmanCoding("abcd"); + String encoded = huffman.encode("abc"); + + // Truncate the last bit to simulate an incomplete byte/sequence transfer + String truncatedEncoded = encoded.substring(0, encoded.length() - 1); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> huffman.decode(truncatedEncoded)); + assertTrue(exception.getMessage().contains("incomplete sequence")); + } + + @Test + void testImmutabilityOfDictionary() { + HuffmanCoding huffman = new HuffmanCoding("abc"); + var codes = huffman.getHuffmanCodes(); + + assertThrows(UnsupportedOperationException.class, () -> codes.put('z', "0101")); + } + + @Test + void testStressVolume() { + StringBuilder sb = new StringBuilder(); + // Generate a 100,000 character string + for (int i = 0; i < 100000; i++) { + sb.append((char) ('a' + (i % 26))); + } + String largeInput = sb.toString(); + + HuffmanCoding huffman = new HuffmanCoding(largeInput); + String encoded = huffman.encode(largeInput); + + assertEquals(largeInput, huffman.decode(encoded)); + } +} diff --git a/src/test/java/com/thealgorithms/compression/LZ78Test.java b/src/test/java/com/thealgorithms/compression/LZ78Test.java index 7889b50b76f3..da1fd8d23318 100644 --- a/src/test/java/com/thealgorithms/compression/LZ78Test.java +++ b/src/test/java/com/thealgorithms/compression/LZ78Test.java @@ -1,7 +1,6 @@ package com.thealgorithms.compression; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -286,7 +285,6 @@ void testTokenStructure() { // All tokens should have valid indices (>= 0) for (LZ78.Token token : compressed) { assertTrue(token.index() >= 0); - assertNotNull(token.nextChar()); } String decompressed = LZ78.decompress(compressed); diff --git a/src/test/java/com/thealgorithms/conversions/Base64Test.java b/src/test/java/com/thealgorithms/conversions/Base64Test.java index fbc220c0ca95..cd0d6c8b38a8 100644 --- a/src/test/java/com/thealgorithms/conversions/Base64Test.java +++ b/src/test/java/com/thealgorithms/conversions/Base64Test.java @@ -127,6 +127,9 @@ void testInvalidPaddingPosition() { assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=QQ")); assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=Q=")); assertThrows(IllegalArgumentException.class, () -> Base64.decode("=QQQ")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQ=Q")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("AB=C")); + assertThrows(IllegalArgumentException.class, () -> Base64.decode("AB=A")); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java index 8212793dfb79..f85a4628a1ac 100644 --- a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java +++ b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java @@ -98,11 +98,9 @@ void testIterator() { @Test void testIteratorEmptyBag() { Bag bag = new Bag<>(); - int count = 0; - for (String ignored : bag) { - org.junit.jupiter.api.Assertions.fail("Iterator should not return any items for an empty bag"); + for (String item : bag) { + org.junit.jupiter.api.Assertions.fail("Iterator returned item for an empty bag:" + item); } - assertEquals(0, count, "Iterator should not traverse any items in an empty bag"); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java b/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java index 8fdc93e1ca22..39e3fa0abe77 100644 --- a/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/dynamicarray/DynamicArrayTest.java @@ -255,4 +255,23 @@ public void testCapacityDoubling() { assertEquals(3, array.getSize()); assertEquals("Charlie", array.get(2)); } + + @Test + public void testContains() { + DynamicArray array = new DynamicArray<>(); + array.add(1); + array.add(2); + array.add(3); + + assertTrue(array.contains(2)); + assertFalse(array.contains(5)); + } + + @Test + public void testContainsWithNull() { + DynamicArray array = new DynamicArray<>(); + array.add(null); + + assertTrue(array.contains(null)); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java deleted file mode 100644 index bf4e2828e069..000000000000 --- a/src/test/java/com/thealgorithms/datastructures/graphs/DijkstraOptimizedAlgorithmTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.thealgorithms.datastructures.graphs; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class DijkstraOptimizedAlgorithmTest { - - private DijkstraOptimizedAlgorithm dijkstraOptimizedAlgorithm; - private int[][] graph; - - @BeforeEach - void setUp() { - graph = new int[][] { - {0, 4, 0, 0, 0, 0, 0, 8, 0}, - {4, 0, 8, 0, 0, 0, 0, 11, 0}, - {0, 8, 0, 7, 0, 4, 0, 0, 2}, - {0, 0, 7, 0, 9, 14, 0, 0, 0}, - {0, 0, 0, 9, 0, 10, 0, 0, 0}, - {0, 0, 4, 14, 10, 0, 2, 0, 0}, - {0, 0, 0, 0, 0, 2, 0, 1, 6}, - {8, 11, 0, 0, 0, 0, 1, 0, 7}, - {0, 0, 2, 0, 0, 0, 6, 7, 0}, - }; - - dijkstraOptimizedAlgorithm = new DijkstraOptimizedAlgorithm(graph.length); - } - - @Test - void testRunAlgorithm() { - int[] expectedDistances = {0, 4, 12, 19, 21, 11, 9, 8, 14}; - assertArrayEquals(expectedDistances, dijkstraOptimizedAlgorithm.run(graph, 0)); - } - - @Test - void testGraphWithDisconnectedNodes() { - int[][] disconnectedGraph = { - {0, 3, 0, 0}, {3, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0} // Node 3 is disconnected - }; - - DijkstraOptimizedAlgorithm dijkstraDisconnected = new DijkstraOptimizedAlgorithm(disconnectedGraph.length); - - // Testing from vertex 0 - int[] expectedDistances = {0, 3, 4, Integer.MAX_VALUE}; // Node 3 is unreachable - assertArrayEquals(expectedDistances, dijkstraDisconnected.run(disconnectedGraph, 0)); - } - - @Test - void testSingleVertexGraph() { - int[][] singleVertexGraph = {{0}}; - DijkstraOptimizedAlgorithm dijkstraSingleVertex = new DijkstraOptimizedAlgorithm(1); - - int[] expectedDistances = {0}; // The only vertex's distance to itself is 0 - assertArrayEquals(expectedDistances, dijkstraSingleVertex.run(singleVertexGraph, 0)); - } - - @Test - void testInvalidSourceVertex() { - assertThrows(IllegalArgumentException.class, () -> dijkstraOptimizedAlgorithm.run(graph, -1)); - assertThrows(IllegalArgumentException.class, () -> dijkstraOptimizedAlgorithm.run(graph, graph.length)); - } -} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java new file mode 100644 index 000000000000..003f6fcee812 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/RottingOrangesTest.java @@ -0,0 +1,134 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class RottingOrangesTest { + + @Test + void testAllOrangesRotInSingleMinute() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } + + @Test + void testImpossibleToRotAllOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testNoFreshOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 2}, {2, 2}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testNoRottenOranges() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{1, 1}, {1, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testEmptyGrid() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testSingleRottenOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testSingleFreshOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testSingleFreshOrangeNextToRottenOrange() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1}}; + + assertEquals(1, rottingOranges.run(grid)); + } + + @Test + void testMultipleRottenSources() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 0, 2}, {1, 1, 1, 1}, {0, 1, 1, 1}}; + + assertEquals(3, rottingOranges.run(grid)); + } + + @Test + void testFreshOrangeBlockedByEmptyCells() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 0, 1}, {0, 0, 0}, {1, 0, 1}}; + + assertEquals(-1, rottingOranges.run(grid)); + } + + @Test + void testLinearSpread() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } + + @Test + void testVerticalSpread() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2}, {1}, {1}, {1}}; + + assertEquals(3, rottingOranges.run(grid)); + } + + @Test + void testGridWithOnlyEmptyCells() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{0, 0}, {0, 0}}; + + assertEquals(0, rottingOranges.run(grid)); + } + + @Test + void testComplexGrid() { + RottingOranges rottingOranges = new RottingOranges(); + + int[][] grid = {{2, 1, 1}, {1, 1, 1}, {1, 1, 1}}; + + assertEquals(4, rottingOranges.run(grid)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java index 5d1733a3e97c..6b6e670a258b 100644 --- a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayTest.java @@ -1,11 +1,9 @@ package com.thealgorithms.datastructures.hashmap.hashing; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class GenericHashMapUsingArrayTest { @@ -16,10 +14,10 @@ void testGenericHashmapWhichUsesArrayAndBothKeyAndValueAreStrings() { map.put("Nepal", "Kathmandu"); map.put("India", "New Delhi"); map.put("Australia", "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Kathmandu", map.get("Nepal")); - assertEquals("Sydney", map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Kathmandu", map.get("Nepal")); + Assertions.assertEquals("Sydney", map.get("Australia")); } @Test @@ -29,12 +27,12 @@ void testGenericHashmapWhichUsesArrayAndKeyIsStringValueIsInteger() { map.put("Nepal", 25); map.put("India", 101); map.put("Australia", 99); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals(25, map.get("Nepal")); - assertEquals(99, map.get("Australia")); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals(25, map.get("Nepal")); + Assertions.assertEquals(99, map.get("Australia")); map.remove("Nepal"); - assertFalse(map.containsKey("Nepal")); + Assertions.assertFalse(map.containsKey("Nepal")); } @Test @@ -44,11 +42,11 @@ void testGenericHashmapWhichUsesArrayAndKeyIsIntegerValueIsString() { map.put(34, "Kathmandu"); map.put(46, "New Delhi"); map.put(89, "Sydney"); - assertNotNull(map); - assertEquals(4, map.size()); - assertEquals("Sydney", map.get(89)); - assertEquals("Washington DC", map.get(101)); - assertTrue(map.containsKey(46)); + Assertions.assertNotNull(map); + Assertions.assertEquals(4, map.size()); + Assertions.assertEquals("Sydney", map.get(89)); + Assertions.assertEquals("Washington DC", map.get(101)); + Assertions.assertTrue(map.containsKey(46)); } @Test @@ -56,7 +54,7 @@ void testRemoveNonExistentKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.remove("Nepal"); // Attempting to remove a non-existent key - assertEquals(1, map.size()); // Size should remain the same + Assertions.assertEquals(1, map.size()); // Size should remain the same } @Test @@ -65,8 +63,8 @@ void testRehashing() { for (int i = 0; i < 20; i++) { map.put("Key" + i, "Value" + i); } - assertEquals(20, map.size()); // Ensure all items were added - assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash + Assertions.assertEquals(20, map.size()); // Ensure all items were added + Assertions.assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash } @Test @@ -74,7 +72,7 @@ void testUpdateValueForExistingKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); map.put("USA", "New Washington DC"); // Updating value for existing key - assertEquals("New Washington DC", map.get("USA")); + Assertions.assertEquals("New Washington DC", map.get("USA")); } @Test @@ -83,14 +81,154 @@ void testToStringMethod() { map.put("USA", "Washington DC"); map.put("Nepal", "Kathmandu"); String expected = "{USA : Washington DC, Nepal : Kathmandu}"; - assertEquals(expected, map.toString()); + Assertions.assertEquals(expected, map.toString()); } @Test void testContainsKey() { GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); map.put("USA", "Washington DC"); - assertTrue(map.containsKey("USA")); - assertFalse(map.containsKey("Nepal")); + Assertions.assertTrue(map.containsKey("USA")); + Assertions.assertFalse(map.containsKey("Nepal")); + } + + // ======= Added tests from the new version ======= + + @Test + void shouldThrowNullPointerExceptionForNullKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + String nullKey = null; // Use variable to avoid static analysis false positive + Assertions.assertThrows(NullPointerException.class, () -> map.put(nullKey, "value")); + } + + @Test + void shouldStoreNullValueForKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyWithNullValue", null); + Assertions.assertEquals(1, map.size()); + Assertions.assertNull(map.get("keyWithNullValue")); + // Note: containsKey returns false for null values due to implementation + Assertions.assertFalse(map.containsKey("keyWithNullValue")); + } + + @Test + void shouldHandleCollisionWhenKeysHashToSameBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(200, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleEmptyStringAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("", "valueForEmptyKey"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("valueForEmptyKey", map.get("")); + Assertions.assertTrue(map.containsKey("")); + } + + @Test + void shouldHandleEmptyStringAsValue() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("keyForEmptyValue", ""); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("", map.get("keyForEmptyValue")); + Assertions.assertTrue(map.containsKey("keyForEmptyValue")); + } + + @Test + void shouldHandleNegativeIntegerKeys() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(-1, 100); + map.put(-100, 200); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(-1)); + Assertions.assertEquals(200, map.get(-100)); + Assertions.assertTrue(map.containsKey(-1)); + Assertions.assertTrue(map.containsKey(-100)); + } + + @Test + void shouldHandleZeroAsKey() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(0, 100); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(100, map.get(0)); + Assertions.assertTrue(map.containsKey(0)); + } + + @Test + void shouldHandleStringWithSpecialCharacters() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put("key!@#$%^&*()", "value<>?/\\|"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value<>?/\\|", map.get("key!@#$%^&*()")); + Assertions.assertTrue(map.containsKey("key!@#$%^&*()")); + } + + @Test + void shouldHandleLongStrings() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + StringBuilder longKey = new StringBuilder(); + StringBuilder longValue = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + longKey.append("a"); + longValue.append("b"); + } + String key = longKey.toString(); + String value = longValue.toString(); + map.put(key, value); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals(value, map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "ab", "abc", "test", "longerString"}) + void shouldHandleKeysOfDifferentLengths(String key) { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + map.put(key, "value"); + Assertions.assertEquals(1, map.size()); + Assertions.assertEquals("value", map.get(key)); + Assertions.assertTrue(map.containsKey(key)); + } + + @Test + void shouldHandleUpdateOnExistingKeyInCollisionBucket() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + Integer key1 = 1; + Integer key2 = 17; + map.put(key1, 100); + map.put(key2, 200); + Assertions.assertEquals(2, map.size()); + map.put(key2, 999); + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals(100, map.get(key1)); + Assertions.assertEquals(999, map.get(key2)); + Assertions.assertTrue(map.containsKey(key1)); + Assertions.assertTrue(map.containsKey(key2)); + } + + @Test + void shouldHandleExactlyLoadFactorBoundary() { + GenericHashMapUsingArray map = new GenericHashMapUsingArray<>(); + // Fill exactly to load factor (12 items with capacity 16 and 0.75 load factor) + for (int i = 0; i < 12; i++) { + map.put(i, i * 10); + } + Assertions.assertEquals(12, map.size()); + // Act - This should trigger rehash on 13th item + map.put(12, 120); + // Assert - Rehash should have happened + Assertions.assertEquals(13, map.size()); + Assertions.assertEquals(120, map.get(12)); + Assertions.assertTrue(map.containsKey(12)); } } diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java index 44551a8adac6..ef7739a2e8a9 100644 --- a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/MapTest.java @@ -81,19 +81,19 @@ void containsTest() { @Test void sizeTest() { Map map = getMap(); - assertEquals(map.size(), 0); + assertEquals(0, map.size()); for (int i = -100; i < 100; i++) { map.put(i, String.valueOf(i)); } - assertEquals(map.size(), 200); + assertEquals(200, map.size()); for (int i = -50; i < 50; i++) { map.delete(i); } - assertEquals(map.size(), 100); + assertEquals(100, map.size()); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java b/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java index d04a9de8a94b..792969200c82 100644 --- a/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java +++ b/src/test/java/com/thealgorithms/datastructures/heaps/HeapElementTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -39,7 +40,7 @@ void testEquals() { assertEquals(element1, element2); // Same key and info assertNotEquals(element1, element3); // Different key - assertNotEquals(null, element1); // Check for null + assertNotNull(element1); assertNotEquals("String", element1); // Check for different type } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java index 5483bbcd0394..4390c0f5f2eb 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/MergeSortedArrayListTest.java @@ -2,98 +2,51 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class MergeSortedArrayListTest { - @Test - void testMergeTwoSortedLists() { - List listA = Arrays.asList(1, 3, 5, 7, 9); - List listB = Arrays.asList(2, 4, 6, 8, 10); + @ParameterizedTest(name = "{3}") + @MethodSource("provideMergeTestData") + void testMergeParameterizedScenarios(List listA, List listB, List expected, String scenarioName) { List result = new ArrayList<>(); - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - assertEquals(expected, result, "Merged list should be sorted and contain all elements from both input lists."); + assertEquals(expected, result, () -> "Failed scenario: " + scenarioName); } - @Test - void testMergeWithEmptyList() { - List listA = Arrays.asList(1, 2, 3); - List listB = new ArrayList<>(); // Empty list - List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 3); - assertEquals(expected, result, "Merged list should match listA when listB is empty."); - } - - @Test - void testMergeWithBothEmptyLists() { - List listA = new ArrayList<>(); // Empty list - List listB = new ArrayList<>(); // Empty list - List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - assertTrue(result.isEmpty(), "Merged list should be empty when both input lists are empty."); + private static Stream provideMergeTestData() { + return Stream.of(Arguments.of(Arrays.asList(1, 3, 5, 7, 9), Arrays.asList(2, 4, 6, 8, 10), Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), "Standard alternating sorted lists"), Arguments.of(Arrays.asList(1, 2, 3), new ArrayList<>(), Arrays.asList(1, 2, 3), "Merge with empty second list"), + Arguments.of(new ArrayList<>(), Arrays.asList(4, 5, 6), Arrays.asList(4, 5, 6), "Merge with empty first list"), Arguments.of(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), "Merge with both lists empty"), + Arguments.of(Arrays.asList(1, 2, 2, 3), Arrays.asList(2, 3, 4), Arrays.asList(1, 2, 2, 2, 3, 3, 4), "Handling duplicate elements gracefully"), + Arguments.of(Arrays.asList(-3, -1, 2), Arrays.asList(-2, 0, 3), Arrays.asList(-3, -2, -1, 0, 2, 3), "Handling negative numbers mixed with positive numbers")); } @Test - void testMergeWithDuplicateElements() { - List listA = Arrays.asList(1, 2, 2, 3); - List listB = Arrays.asList(2, 3, 4); + void testMergeThrowsExceptionWhenListAIsNull() { + List listB = Arrays.asList(1, 2, 3); List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(1, 2, 2, 2, 3, 3, 4); - assertEquals(expected, result, "Merged list should correctly handle and include duplicate elements."); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(null, listB, result)); } @Test - void testMergeWithNegativeAndPositiveNumbers() { - List listA = Arrays.asList(-3, -1, 2); - List listB = Arrays.asList(-2, 0, 3); + void testMergeThrowsExceptionWhenListBIsNull() { + List listA = Arrays.asList(1, 2, 3); List result = new ArrayList<>(); - - MergeSortedArrayList.merge(listA, listB, result); - - List expected = Arrays.asList(-3, -2, -1, 0, 2, 3); - assertEquals(expected, result, "Merged list should correctly handle negative and positive numbers."); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(listA, null, result)); } @Test - void testMergeThrowsExceptionOnNullInput() { - List listA = null; - List listB = Arrays.asList(1, 2, 3); - List result = new ArrayList<>(); - - List finalListB = listB; - List finalListA = listA; - List finalResult = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA, finalListB, finalResult), "Should throw NullPointerException if any input list is null."); - - listA = Arrays.asList(1, 2, 3); - listB = null; - List finalListA1 = listA; - List finalListB1 = listB; - List finalResult1 = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA1, finalListB1, finalResult1), "Should throw NullPointerException if any input list is null."); - - listA = Arrays.asList(1, 2, 3); - listB = Arrays.asList(4, 5, 6); - result = null; - List finalListA2 = listA; - List finalListB2 = listB; - List finalResult2 = result; - assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(finalListA2, finalListB2, finalResult2), "Should throw NullPointerException if the result collection is null."); + void testMergeThrowsExceptionWhenResultCollectionIsNull() { + List listA = Arrays.asList(1, 2, 3); + List listB = Arrays.asList(4, 5, 6); + assertThrows(NullPointerException.class, () -> MergeSortedArrayList.merge(listA, listB, null)); } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java new file mode 100644 index 000000000000..ba5614a07916 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/MiddleOfLinkedListTest.java @@ -0,0 +1,74 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Objects; +import org.junit.jupiter.api.Test; + +public class MiddleOfLinkedListTest { + + private static SinglyLinkedListNode listOf(int firstValue, int... remainingValues) { + SinglyLinkedListNode head = new SinglyLinkedListNode(firstValue); + SinglyLinkedListNode current = head; + + for (int i = 0; i < remainingValues.length; i++) { + current.next = new SinglyLinkedListNode(remainingValues[i]); + current = current.next; + } + return head; + } + + @Test + void middleNodeOddLength() { + SinglyLinkedListNode head = listOf(1, 2, 3, 4, 5); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(3, middle.value); + } + + @Test + void middleNodeEvenLengthReturnsSecondMiddle() { + SinglyLinkedListNode head = listOf(1, 2, 3, 4, 5, 6); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(4, middle.value); + } + + @Test + void middleNodeSingleElement() { + SinglyLinkedListNode head = listOf(42); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(42, middle.value); + } + + @Test + void middleNodeTwoElementsReturnsSecond() { + SinglyLinkedListNode head = listOf(10, 20); + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(head)); + assertEquals(20, middle.value); + } + + @Test + void middleNodeNullHead() { + assertNull(MiddleOfLinkedList.middleNode(null)); + } + + @Test + void middleNodeDoesNotModifyListStructure() { + SinglyLinkedListNode first = new SinglyLinkedListNode(1); + SinglyLinkedListNode second = new SinglyLinkedListNode(2); + SinglyLinkedListNode third = new SinglyLinkedListNode(3); + SinglyLinkedListNode fourth = new SinglyLinkedListNode(4); + + first.next = second; + second.next = third; + third.next = fourth; + + SinglyLinkedListNode middle = Objects.requireNonNull(MiddleOfLinkedList.middleNode(first)); + assertEquals(3, middle.value); + + assertEquals(second, first.next); + assertEquals(third, second.next); + assertEquals(fourth, third.next); + assertNull(fourth.next); + } +} diff --git a/src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java similarity index 98% rename from src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java rename to src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java index 0f0577d39094..10f6b8536b19 100644 --- a/src/test/java/com/thealgorithms/misc/PalindromeSinglyLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/PalindromeSinglyLinkedListTest.java @@ -1,9 +1,8 @@ -package com.thealgorithms.misc; +package com.thealgorithms.datastructures.lists; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.thealgorithms.datastructures.lists.SinglyLinkedList; import org.junit.jupiter.api.Test; public class PalindromeSinglyLinkedListTest { diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java new file mode 100644 index 000000000000..1d397cf09301 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/SelfOrganizingLinkedListTest.java @@ -0,0 +1,119 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SelfOrganizingLinkedListTest { + + private SelfOrganizingLinkedList list; + + @BeforeEach + void setUp() { + list = new SelfOrganizingLinkedList<>(); + } + + @Test + void testEmptyListAndGetters() { + assertTrue(list.isEmpty()); + assertEquals(0, list.getSize()); + assertNull(list.getHeadValue()); + assertFalse(list.search(10)); + } + + @Test + void testInsertAndSizeState() { + assertTrue(list.isEmpty()); + list.insert(10); + assertFalse(list.isEmpty()); + assertEquals(1, list.getSize()); + + list.insert(20); + assertEquals(2, list.getSize()); + } + + @Test + void testMoveMiddleElementToFrontPreservesFullListStructure() { + list.insert(10); + list.insert(20); + list.insert(30); + list.insert(40); + + // Initial order: [10, 20, 30, 40] + assertTrue(list.search(30)); + + // Expected order: [30, 10, 20, 40] + assertEquals(4, list.getSize()); + assertEquals(30, list.getHeadValue()); + + // Sequential head tracking to verify middle and tail pointers didn't break + assertTrue(list.search(10)); // [10, 30, 20, 40] + assertEquals(10, list.getHeadValue()); + + assertTrue(list.search(20)); // [20, 10, 30, 40] + assertEquals(20, list.getHeadValue()); + + assertTrue(list.search(40)); // [40, 20, 10, 30] + assertEquals(40, list.getHeadValue()); + assertEquals(4, list.getSize()); + } + + @Test + void testMoveLastElementToFrontPreservesFullListStructure() { + list.insert(10); + list.insert(20); + list.insert(30); + + // Search tail element '30' + assertTrue(list.search(30)); // Order becomes [30, 10, 20] + + assertEquals(30, list.getHeadValue()); + assertEquals(3, list.getSize()); + + // Verify remaining chain order [10, 20] + assertTrue(list.search(20)); // [20, 30, 10] + assertEquals(20, list.getHeadValue()); + + assertTrue(list.search(10)); // [10, 20, 30] + assertEquals(10, list.getHeadValue()); + assertEquals(3, list.getSize()); + } + + @Test + void testSearchNonExistentElementPreservesStructureAndSize() { + list.insert(10); + list.insert(20); + list.insert(30); + + assertFalse(list.search(99)); + assertEquals(3, list.getSize()); + assertEquals(10, list.getHeadValue()); + } + + @Test + void testDuplicateValuesMovesFirstMatchedToFront() { + list.insert(10); + list.insert(20); + list.insert(10); // Duplicate '10' at tail + list.insert(30); + + // Initial list state: [10, 20, 10, 30] + // Searching '10' hits the head immediately -> no re-linking + assertTrue(list.search(10)); + assertEquals(10, list.getHeadValue()); + assertEquals(4, list.getSize()); + + // Searching '20' moves middle element to head: [20, 10, 10, 30] + assertTrue(list.search(20)); + assertEquals(20, list.getHeadValue()); + + // Searching '10' moves the FIRST instance of '10' (index 1) to head: [10, 20, 10, 30] + assertTrue(list.search(10)); + assertEquals(10, list.getHeadValue()); + assertEquals(4, list.getSize()); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java index e97fe091c556..3bb8bbabb761 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java @@ -9,14 +9,14 @@ class PriorityQueuesTest { void testPQInsertion() { PriorityQueue myQueue = new PriorityQueue(4); myQueue.insert(2); - Assertions.assertEquals(myQueue.peek(), 2); + Assertions.assertEquals(2, myQueue.peek()); myQueue.insert(5); myQueue.insert(3); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(5, myQueue.peek()); myQueue.insert(10); - Assertions.assertEquals(myQueue.peek(), 10); + Assertions.assertEquals(10, myQueue.peek()); } @Test @@ -28,32 +28,32 @@ void testPQDeletion() { myQueue.insert(10); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(5, myQueue.peek()); myQueue.remove(); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 2); + Assertions.assertEquals(2, myQueue.peek()); } @Test void testPQExtra() { PriorityQueue myQueue = new PriorityQueue(4); - Assertions.assertEquals(myQueue.isEmpty(), true); - Assertions.assertEquals(myQueue.isFull(), false); + Assertions.assertTrue(myQueue.isEmpty()); + Assertions.assertFalse(myQueue.isFull()); myQueue.insert(2); myQueue.insert(5); - Assertions.assertEquals(myQueue.isFull(), false); + Assertions.assertFalse(myQueue.isFull()); myQueue.insert(3); myQueue.insert(10); - Assertions.assertEquals(myQueue.isEmpty(), false); - Assertions.assertEquals(myQueue.isFull(), true); + Assertions.assertFalse(myQueue.isEmpty()); + Assertions.assertTrue(myQueue.isFull()); myQueue.remove(); - Assertions.assertEquals(myQueue.getSize(), 3); - Assertions.assertEquals(myQueue.peek(), 5); + Assertions.assertEquals(3, myQueue.getSize()); + Assertions.assertEquals(5, myQueue.peek()); myQueue.remove(); myQueue.remove(); - Assertions.assertEquals(myQueue.peek(), 2); - Assertions.assertEquals(myQueue.getSize(), 1); + Assertions.assertEquals(2, myQueue.peek()); + Assertions.assertEquals(1, myQueue.getSize()); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java b/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java new file mode 100644 index 000000000000..e3abe15b6a46 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/queues/ReverseQueueRecursionTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.datastructures.queues; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedList; +import java.util.Queue; +import org.junit.jupiter.api.Test; + +class ReverseQueueRecursionTest { + @Test + void testReverseMultipleElements() { + Queue queue = new LinkedList<>(); + queue.add(1); + queue.add(2); + queue.add(3); + queue.add(4); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals(4, queue.poll()); + assertEquals(3, queue.poll()); + assertEquals(2, queue.poll()); + assertEquals(1, queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseSingleElement() { + Queue queue = new LinkedList<>(); + queue.add(42); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals(42, queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseEmptyQueue() { + Queue queue = new LinkedList<>(); + ReverseQueueRecursion.reverseQueue(queue); + assertTrue(queue.isEmpty()); + } + + @Test + void testReverseStringQueue() { + Queue queue = new LinkedList<>(); + queue.add("A"); + queue.add("B"); + queue.add("C"); + ReverseQueueRecursion.reverseQueue(queue); + assertEquals("C", queue.poll()); + assertEquals("B", queue.poll()); + assertEquals("A", queue.poll()); + assertTrue(queue.isEmpty()); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java b/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java new file mode 100644 index 000000000000..4c038c05b167 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/queues/ThreadSafeQueueTest.java @@ -0,0 +1,295 @@ +package com.thealgorithms.datastructures.queues; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ThreadSafeQueueTest { + + @Test + public void testEnqueueDequeue() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + + Assertions.assertEquals(3, queue.size()); + Assertions.assertEquals(1, queue.dequeue()); + Assertions.assertEquals(2, queue.dequeue()); + Assertions.assertEquals(3, queue.dequeue()); + Assertions.assertTrue(queue.isEmpty()); + } + + @Test + public void testOfferPoll() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.offer("a")); + Assertions.assertTrue(queue.offer("b")); + Assertions.assertFalse(queue.offer("c")); + + Assertions.assertEquals("a", queue.poll()); + Assertions.assertEquals("b", queue.poll()); + Assertions.assertNull(queue.poll()); + } + + @Test + public void testOfferRejectsWhenFull() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.offer(1)); + Assertions.assertTrue(queue.offer(2)); + Assertions.assertFalse(queue.offer(3)); + Assertions.assertEquals(2, queue.size()); + } + + @Test + public void testPollReturnsNullWhenEmpty() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertNull(queue.poll()); + } + + @Test + public void testEnqueueNullThrows() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertThrows(IllegalArgumentException.class, () -> queue.enqueue(null)); + } + + @Test + public void testOfferNullThrows() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + Assertions.assertThrows(IllegalArgumentException.class, () -> queue.offer(null)); + } + + @Test + public void testInvalidCapacityThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ThreadSafeQueue<>(0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ThreadSafeQueue<>(-1)); + } + + @Test + public void testIsEmptyAndIsFull() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(2); + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.enqueue(1); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.enqueue(2); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertTrue(queue.isFull()); + + queue.dequeue(); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + queue.dequeue(); + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + } + + @Test + public void testCapacity() { + ThreadSafeQueue queue = new ThreadSafeQueue<>(10); + Assertions.assertEquals(10, queue.capacity()); + } + + @Test + public void testCircularBufferWrapAround() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(3); + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + + Assertions.assertEquals(1, queue.dequeue()); + Assertions.assertEquals(2, queue.dequeue()); + + queue.enqueue(4); + queue.enqueue(5); + + Assertions.assertEquals(3, queue.dequeue()); + Assertions.assertEquals(4, queue.dequeue()); + Assertions.assertEquals(5, queue.dequeue()); + } + + @Test + public void testMultipleProducersSingleConsumer() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(100); + int numProducers = 4; + int itemsPerProducer = 250; + int totalItems = numProducers * itemsPerProducer; + CountDownLatch doneLatch = new CountDownLatch(numProducers); + List results = new ArrayList<>(); + + ExecutorService executor = Executors.newFixedThreadPool(numProducers + 1); + + for (int p = 0; p < numProducers; p++) { + final int producerId = p; + executor.submit(() -> { + try { + for (int i = 0; i < itemsPerProducer; i++) { + queue.enqueue(producerId * itemsPerProducer + i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + Thread consumerThread = new Thread(() -> { + try { + while (results.size() < totalItems) { + Integer item = queue.poll(); + if (item != null) { + synchronized (results) { + results.add(item); + } + } + } + } catch (Exception e) { + Thread.currentThread().interrupt(); + } + }); + consumerThread.start(); + + Assertions.assertTrue(doneLatch.await(10, TimeUnit.SECONDS)); + consumerThread.join(5000); + + Assertions.assertEquals(totalItems, results.size()); + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + public void testSingleProducerMultipleConsumers() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(50); + int numConsumers = 4; + int totalItems = 1000; + CountDownLatch doneLatch = new CountDownLatch(numConsumers); + AtomicInteger consumedCount = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(numConsumers + 1); + + executor.submit(() -> { + try { + for (int i = 0; i < totalItems; i++) { + queue.enqueue(i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + for (int c = 0; c < numConsumers; c++) { + executor.submit(() -> { + try { + while (consumedCount.get() < totalItems) { + Integer item = queue.poll(); + if (item != null) { + consumedCount.incrementAndGet(); + } + } + } finally { + doneLatch.countDown(); + } + }); + } + + Assertions.assertTrue(doneLatch.await(10, TimeUnit.SECONDS)); + Assertions.assertEquals(totalItems, consumedCount.get()); + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + public void testBlockingEnqueueWhenFull() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(1); + queue.enqueue(1); + + AtomicInteger blockedCount = new AtomicInteger(0); + Thread producer = new Thread(() -> { + try { + queue.enqueue(2); + blockedCount.incrementAndGet(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + producer.start(); + + Thread.sleep(100); + Assertions.assertEquals(1, queue.dequeue()); + + producer.join(2000); + Assertions.assertEquals(1, blockedCount.get()); + Assertions.assertEquals(2, queue.dequeue()); + } + + @Test + public void testBlockingDequeueWhenEmpty() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(5); + + AtomicInteger result = new AtomicInteger(-1); + Thread consumer = new Thread(() -> { + try { + result.set(queue.dequeue()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + consumer.start(); + + Thread.sleep(100); + queue.enqueue(42); + + consumer.join(2000); + Assertions.assertEquals(42, result.get()); + } + + @Test + public void testStressConcurrentAccess() throws InterruptedException { + ThreadSafeQueue queue = new ThreadSafeQueue<>(10); + int numThreads = 8; + int opsPerThread = 500; + CountDownLatch latch = new CountDownLatch(numThreads); + AtomicInteger enqueueCount = new AtomicInteger(0); + AtomicInteger dequeueCount = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + + for (int t = 0; t < numThreads; t++) { + final boolean isProducer = t % 2 == 0; + executor.submit(() -> { + try { + for (int i = 0; i < opsPerThread; i++) { + if (isProducer) { + if (queue.offer(i)) { + enqueueCount.incrementAndGet(); + } + } else { + if (queue.poll() != null) { + dequeueCount.incrementAndGet(); + } + } + } + } finally { + latch.countDown(); + } + }); + } + + Assertions.assertTrue(latch.await(10, TimeUnit.SECONDS)); + Assertions.assertTrue(enqueueCount.get() >= dequeueCount.get()); + Assertions.assertEquals(enqueueCount.get() - dequeueCount.get(), queue.size()); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } +} diff --git a/src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java b/src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java similarity index 97% rename from src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java rename to src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java index 29189290e1d4..f0cb1724f67c 100644 --- a/src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java +++ b/src/test/java/com/thealgorithms/datastructures/trees/HeavyLightDecompositionTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.tree; +package com.thealgorithms.datastructures.trees; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java new file mode 100644 index 000000000000..db081da2550a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTree2DTest.java @@ -0,0 +1,71 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class SegmentTree2DTest { + + @Test + void testInitialEmptyQueries() { + SegmentTree2D segmentTree = new SegmentTree2D(4, 4); + + // Initial tree should return 0 for any query + assertEquals(0, segmentTree.query(0, 4, 0, 4)); + assertEquals(0, segmentTree.query(1, 3, 1, 3)); + } + + @Test + void testUpdateAndPointQuery() { + SegmentTree2D segmentTree = new SegmentTree2D(5, 5); + + segmentTree.update(2, 3, 10); + segmentTree.update(0, 0, 5); + + // Querying single points [row, row+1) x [col, col+1) + assertEquals(10, segmentTree.query(2, 3, 3, 4)); + assertEquals(5, segmentTree.query(0, 1, 0, 1)); + + // Empty point should be 0 + assertEquals(0, segmentTree.query(1, 2, 1, 2)); + } + + @Test + void testSubmatrixQuery() { + SegmentTree2D segmentTree = new SegmentTree2D(4, 4); + + // Matrix simulation: + // [1, 2, 0, 0] + // [3, 4, 0, 0] + // [0, 0, 0, 0] + // [0, 0, 0, 0] + segmentTree.update(0, 0, 1); + segmentTree.update(0, 1, 2); + segmentTree.update(1, 0, 3); + segmentTree.update(1, 1, 4); + + // Top-left 2x2 sum: 1+2+3+4 = 10 + assertEquals(10, segmentTree.query(0, 2, 0, 2)); + + // First row sum: 1+2 = 3 + assertEquals(3, segmentTree.query(0, 1, 0, 4)); + + // Second column sum: 2+4 = 6 + assertEquals(6, segmentTree.query(0, 4, 1, 2)); + } + + @Test + void testUpdateOverwriting() { + SegmentTree2D segmentTree = new SegmentTree2D(3, 3); + + segmentTree.update(1, 1, 5); + assertEquals(5, segmentTree.query(1, 2, 1, 2)); + + // Overwrite the same point + segmentTree.update(1, 1, 20); + assertEquals(20, segmentTree.query(1, 2, 1, 2)); + + // Full matrix sum should just be this point + assertEquals(20, segmentTree.query(0, 3, 0, 3)); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java new file mode 100644 index 000000000000..196c32575709 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/SegmentTreeTest.java @@ -0,0 +1,96 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SegmentTreeTest { + + private static SegmentTree treeOf(int... values) { + return new SegmentTree(values.length, values); + } + + @ParameterizedTest + @CsvSource({"0, 4, 15", "0, 0, 1", "4, 4, 5", "1, 3, 9", "2, 4, 12"}) + void testRangeSums(int start, int end, int expected) { + assertEquals(expected, treeOf(1, 2, 3, 4, 5).getSum(start, end)); + } + + @Test + void testSingleElementTree() { + SegmentTree tree = treeOf(42); + assertEquals(42, tree.getSum(0, 0)); + tree.update(0, 7); + assertEquals(7, tree.getSum(0, 0)); + } + + @Test + void testUpdateIsReflectedInSubsequentQueries() { + SegmentTree tree = treeOf(1, 2, 3, 4, 5); + tree.update(2, 10); + assertEquals(22, tree.getSum(0, 4)); + assertEquals(16, tree.getSum(1, 3)); + tree.update(0, -1); + assertEquals(20, tree.getSum(0, 4)); + } + + @Test + void testNegativeValues() { + SegmentTree tree = treeOf(-5, 3, -2, 8); + assertEquals(4, tree.getSum(0, 3)); + assertEquals(-4, tree.getSum(0, 2)); + } + + /** + * index == n is past the last element, so it must be rejected by the guard instead of reaching + * the backing array and throwing {@link ArrayIndexOutOfBoundsException}. + */ + @ParameterizedTest + @ValueSource(ints = {5, 6, 100, -1}) + void testUpdateOutOfRangeIndexIsIgnored(int index) { + SegmentTree tree = treeOf(1, 2, 3, 4, 5); + assertDoesNotThrow(() -> tree.update(index, 99)); + assertEquals(15, tree.getSum(0, 4), "out of range update must not modify the tree"); + } + + @ParameterizedTest + @CsvSource({"0, 5", "0, 6", "3, 2", "-1, 3", "5, 5"}) + void testOutOfRangeQueriesReturnZero(int start, int end) { + assertEquals(0, treeOf(1, 2, 3, 4, 5).getSum(start, end)); + } + + @Test + void testConstructorRejectsInvalidSize() { + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(0, new int[] {1, 2, 3})); + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(-1, new int[] {1, 2, 3})); + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(4, new int[] {1, 2, 3})); + } + + @Test + void testConstructorRejectsNullArray() { + assertThrows(IllegalArgumentException.class, () -> new SegmentTree(3, null)); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17}) + void testMatchesBruteForceForVariousSizes(int size) { + int[] values = new int[size]; + for (int i = 0; i < size; i++) { + values[i] = i * 3 - 4; + } + SegmentTree tree = new SegmentTree(size, values.clone()); + + for (int start = 0; start < size; start++) { + int expected = 0; + for (int end = start; end < size; end++) { + expected += values[end]; + assertEquals(expected, tree.getSum(start, end), "sum of [" + start + ", " + end + "] with size " + size); + } + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java b/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java index 09ada594faca..52b74a7a1faf 100644 --- a/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java +++ b/src/test/java/com/thealgorithms/datastructures/trees/TreapTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ public void searchAndNotFound() { treap.insert(3); treap.insert(8); treap.insert(1); - assertEquals(null, treap.search(4)); + assertNull(treap.search(4)); } @Test diff --git a/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java b/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java new file mode 100644 index 000000000000..592170673a3a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/trees/WaveletTreeTest.java @@ -0,0 +1,117 @@ +package com.thealgorithms.datastructures.trees; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class WaveletTreeTest { + + @Test + public void testRank() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + // x = 1 + assertEquals(1, wt.rank(1, 1)); // In [5, 1], '1' appears 1 time + assertEquals(2, wt.rank(1, 4)); // In [5, 1, 2, 5, 1], '1' appears 2 times + assertEquals(0, wt.rank(1, 0)); // In [5], '1' appears 0 times + + // x = 5 + assertEquals(1, wt.rank(5, 0)); // In [5], '5' appears 1 time + assertEquals(1, wt.rank(5, 2)); // In [5, 1, 2], '5' appears 1 time + assertEquals(2, wt.rank(5, 4)); // In [5, 1, 2, 5, 1], '5' appears 2 times + + // Out of bounds / invalid value + assertEquals(0, wt.rank(10, 4)); // '10' is not in the array + assertEquals(0, wt.rank(5, -1)); // Invalid end index + } + + @Test + public void testSelect() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + assertEquals(1, wt.select(1, 1)); // 1st '1' is at index 1 + assertEquals(4, wt.select(1, 2)); // 2nd '1' is at index 4 + + assertEquals(0, wt.select(5, 1)); // 1st '5' is at index 0 + assertEquals(3, wt.select(5, 2)); // 2nd '5' is at index 3 + + assertEquals(2, wt.select(2, 1)); // 1st '2' is at index 2 + + assertEquals(-1, wt.select(5, 3)); // 3rd '5' doesn't exist + assertEquals(-1, wt.select(10, 1)); // '10' doesn't exist + assertEquals(-1, wt.select(5, 0)); // invalid k + } + + @Test + public void testKthSmallest() { + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wt = new WaveletTree(arr); + + // Array: [5, 1, 2, 5, 1] -> Sorted: [1, 1, 2, 5, 5] + assertEquals(1, wt.kthSmallest(0, 4, 1)); // 1st smallest in [5, 1, 2, 5, 1] is 1 + assertEquals(1, wt.kthSmallest(0, 4, 2)); // 2nd smallest in [5, 1, 2, 5, 1] is 1 + assertEquals(2, wt.kthSmallest(0, 4, 3)); // 3rd smallest in [5, 1, 2, 5, 1] is 2 + assertEquals(5, wt.kthSmallest(0, 4, 4)); // 4th smallest in [5, 1, 2, 5, 1] is 5 + assertEquals(5, wt.kthSmallest(0, 4, 5)); // 5th smallest in [5, 1, 2, 5, 1] is 5 + + // Subarray: arr[1..3] = [1, 2, 5] -> Sorted: [1, 2, 5] + assertEquals(1, wt.kthSmallest(1, 3, 1)); // 1st smallest in [1, 2, 5] is 1 + assertEquals(2, wt.kthSmallest(1, 3, 2)); // 2nd smallest in [1, 2, 5] is 2 + assertEquals(5, wt.kthSmallest(1, 3, 3)); // 3rd smallest in [1, 2, 5] is 5 + + // Invalid ranges / arguments + assertEquals(-1, wt.kthSmallest(4, 2, 1)); // Invalid range (left > right) + assertEquals(-1, wt.kthSmallest(0, 4, 10)); // k > range length + assertEquals(-1, wt.kthSmallest(0, 4, 0)); // k < 1 + } + + @Test + public void testEmptyAndSingleElementArray() { + WaveletTree wtEmpty = new WaveletTree(new int[] {}); + assertEquals(0, wtEmpty.rank(1, 0)); + assertEquals(-1, wtEmpty.select(1, 1)); + assertEquals(-1, wtEmpty.kthSmallest(0, 0, 1)); + + WaveletTree wtSingle = new WaveletTree(new int[] {42}); + assertEquals(1, wtSingle.rank(42, 0)); + assertEquals(0, wtSingle.rank(42, -1)); + assertEquals(0, wtSingle.select(42, 1)); + assertEquals(-1, wtSingle.select(42, 2)); + assertEquals(42, wtSingle.kthSmallest(0, 0, 1)); + } + + @Test + public void testNullArrayAndCustomBounds() { + WaveletTree wtNull = new WaveletTree(null); + assertEquals(0, wtNull.rank(1, 0)); + + WaveletTree wtNullCustom = new WaveletTree(null, 1, 5); + assertEquals(-1, wtNullCustom.select(1, 1)); + + int[] arr = {5, 1, 2, 5, 1}; + WaveletTree wtCustom = new WaveletTree(arr, 1, 10); + assertEquals(2, wtCustom.rank(5, 4)); + assertEquals(0, wtCustom.rank(4, 4)); // Query an element inside bounds but not in array + assertEquals(0, wtCustom.rank(10, 4)); // Query upper bound + } + + @Test + public void testNegativeValues() { + int[] arr = {-5, 10, -2, 0, -5}; + WaveletTree wt = new WaveletTree(arr); + + assertEquals(2, wt.rank(-5, 4)); + assertEquals(1, wt.rank(0, 3)); + + assertEquals(0, wt.select(-5, 1)); + assertEquals(4, wt.select(-5, 2)); + assertEquals(3, wt.select(0, 1)); + + // Sorted: [-5, -5, -2, 0, 10] + assertEquals(-5, wt.kthSmallest(0, 4, 1)); + assertEquals(-2, wt.kthSmallest(0, 4, 3)); + assertEquals(10, wt.kthSmallest(0, 4, 5)); + } +} diff --git a/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java b/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java index 38784228d68e..b25fd796b112 100644 --- a/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java +++ b/src/test/java/com/thealgorithms/divideandconquer/ClosestPairTest.java @@ -16,14 +16,6 @@ public void testBuildLocation() { assertEquals(4.0, point.y); } - @Test - public void testCreateLocation() { - ClosestPair cp = new ClosestPair(5); - ClosestPair.Location[] locations = cp.createLocation(5); - assertNotNull(locations); - assertEquals(5, locations.length); - } - @Test public void testXPartition() { ClosestPair cp = new ClosestPair(5); diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java new file mode 100644 index 000000000000..762fe86d4d65 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.dynamicprogramming; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the generalized DigitDP implementation. + */ +public class DigitDPTest { + + @Test + public void testDigitDPBasicRange() { + // Numbers between 1 and 20 with a digit sum of 5: 5, 14 + long result = DigitDP.countRangeWithDigitSum(1, 20, 5); + assertEquals(2, result); + } + + @Test + public void testDigitDPZeroBound() { + // Number 0 has a digit sum of 0 + long result = DigitDP.countRangeWithDigitSum(0, 0, 0); + assertEquals(1, result); + } + + @Test + public void testDigitDPLargeRange() { + // Count numbers between 1 and 100 with a digit sum of 9 + // 9, 18, 27, 36, 45, 54, 63, 72, 81, 90 (10 numbers) + long result = DigitDP.countRangeWithDigitSum(1, 100, 9); + assertEquals(10, result); + } + + @Test + public void testDigitDPNoMatches() { + // No numbers between 10 and 15 can have a digit sum of 20 + long result = DigitDP.countRangeWithDigitSum(10, 15, 20); + assertEquals(0, result); + } + + @Test + public void testDigitDPExceedsMaxSum() { + // Sum condition that exceeds max possible physical sum array constraints + // gracefully returns 0 + long result = DigitDP.countRangeWithDigitSum(1, 100, 200); + assertEquals(0, result); + } + + @Test + public void testDigitDPInvalidRange() { + // Lower bound greater than upper bound should evaluate gracefully to 0 + long result = DigitDP.countRangeWithDigitSum(50, 20, 5); + assertEquals(0, result); + } + + @Test + public void testDigitDPExceedsMaxSumEdgeCase() { + // Yeh test case target > MAX_DIGIT_SUM wali condition ko hit karega + long result = DigitDP.countRangeWithDigitSum(1, 100, 180); + assertEquals(0, result); + } + + @Test + public void testDigitDPMemoizationHit() { + // Badi range dene se overlapping subproblems bante hain, + // jisse memoization hit hogi aur coverage 100% ho jayegi. + long result1 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + long result2 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + assertEquals(result1, result2); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java index 40bbdff15ca6..91169c4cc9d8 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/LongestCommonSubsequenceTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.dynamicprogramming; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; @@ -55,27 +56,24 @@ public void testLCSWithBothEmptyStrings() { public void testLCSWithNullFirstString() { String str1 = null; String str2 = "XYZ"; - String expected = null; // Should return null if first string is null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test public void testLCSWithNullSecondString() { String str1 = "ABC"; String str2 = null; - String expected = null; // Should return null if second string is null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test public void testLCSWithNullBothStrings() { String str1 = null; String str2 = null; - String expected = null; // Should return null if both strings are null String result = LongestCommonSubsequence.getLCS(str1, str2); - assertEquals(expected, result); + assertNull(result); } @Test diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java new file mode 100644 index 000000000000..a1ee624e94d2 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/LongestPalindromicSubsequenceTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class LongestPalindromicSubsequenceTest { + + @ParameterizedTest + @CsvSource({"BBABCBCAB, BACBCAB", "BABCBAB, BABCBAB", "A, A", "AA, AA", "AB, B"}) + void testLpsKnownCases(String input, String expectedLps) { + assertEquals(expectedLps, LongestPalindromicSubsequence.lps(input)); + } + + @Test + void testLpsEmptyString() { + assertEquals("", LongestPalindromicSubsequence.lps("")); + } + + @Test + void testLpsSingleCharacter() { + assertEquals("Z", LongestPalindromicSubsequence.lps("Z")); + } + + @Test + void testLpsAllSameCharacters() { + assertEquals("AAAA", LongestPalindromicSubsequence.lps("AAAA")); + } + + @Test + void testLpsAlreadyPalindrome() { + assertEquals("RACECAR", LongestPalindromicSubsequence.lps("RACECAR")); + } + + @Test + void testLpsNoRepeatingCharacters() { + assertEquals(1, LongestPalindromicSubsequence.lps("ABCDE").length()); + } + + @Test + void testLpsNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { LongestPalindromicSubsequence.lps(null); }); + } + + @Test + void testLpsResultIsActuallyPalindrome() { + String result = LongestPalindromicSubsequence.lps("BBABCBCAB"); + String reversed = new StringBuilder(result).reverse().toString(); + assertEquals(result, reversed); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java new file mode 100644 index 000000000000..17ff3ec728dc --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/OptimalBinarySearchTreeTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OptimalBinarySearchTreeTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testFindOptimalCost(int[] keys, int[] frequencies, long expectedCost) { + assertEquals(expectedCost, OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream validTestCases() { + return Stream.of(Arguments.of(new int[] {}, new int[] {}, 0L), Arguments.of(new int[] {15}, new int[] {9}, 9L), Arguments.of(new int[] {10, 12}, new int[] {34, 50}, 118L), Arguments.of(new int[] {20, 10, 30}, new int[] {50, 34, 8}, 134L), + Arguments.of(new int[] {12, 10, 20, 42, 25, 37}, new int[] {8, 34, 50, 3, 40, 30}, 324L), Arguments.of(new int[] {1, 2, 3}, new int[] {0, 0, 0}, 0L)); + } + + @ParameterizedTest + @MethodSource("crossCheckTestCases") + void testFindOptimalCostAgainstBruteForce(int[] keys, int[] frequencies) { + assertEquals(bruteForceOptimalCost(keys, frequencies), OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream crossCheckTestCases() { + return Stream.of(Arguments.of(new int[] {3, 1, 2}, new int[] {4, 2, 6}), Arguments.of(new int[] {5, 2, 8, 6}, new int[] {3, 7, 1, 4}), Arguments.of(new int[] {9, 4, 11, 2}, new int[] {1, 8, 2, 5})); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testFindOptimalCostInvalidInput(int[] keys, int[] frequencies) { + assertThrows(IllegalArgumentException.class, () -> OptimalBinarySearchTree.findOptimalCost(keys, frequencies)); + } + + private static Stream invalidTestCases() { + return Stream.of(Arguments.of(null, new int[] {}), Arguments.of(new int[] {}, null), Arguments.of(new int[] {1, 2}, new int[] {3}), Arguments.of(new int[] {1, 1}, new int[] {2, 3}), Arguments.of(new int[] {1, 2}, new int[] {3, -1})); + } + + private static long bruteForceOptimalCost(int[] keys, int[] frequencies) { + int[][] sortedNodes = new int[keys.length][2]; + for (int index = 0; index < keys.length; index++) { + sortedNodes[index][0] = keys[index]; + sortedNodes[index][1] = frequencies[index]; + } + Arrays.sort(sortedNodes, java.util.Comparator.comparingInt(node -> node[0])); + + int[] sortedFrequencies = new int[sortedNodes.length]; + for (int index = 0; index < sortedNodes.length; index++) { + sortedFrequencies[index] = sortedNodes[index][1]; + } + + return bruteForceOptimalCost(sortedFrequencies, 0, sortedFrequencies.length - 1, 1); + } + + private static long bruteForceOptimalCost(int[] frequencies, int start, int end, int depth) { + if (start > end) { + return 0L; + } + + long minimumCost = Long.MAX_VALUE; + for (int root = start; root <= end; root++) { + long currentCost = (long) depth * frequencies[root] + bruteForceOptimalCost(frequencies, start, root - 1, depth + 1) + bruteForceOptimalCost(frequencies, root + 1, end, depth + 1); + minimumCost = Math.min(minimumCost, currentCost); + } + return minimumCost; + } +} diff --git a/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java b/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java new file mode 100644 index 000000000000..9f60df51b65f --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java @@ -0,0 +1,101 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.geom.Point2D; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class LineIntersectionTest { + + @Test + void testCrossingSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(4, 4); + Point q1 = new Point(0, 4); + Point q2 = new Point(4, 0); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testParallelSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(3, 3); + Point q1 = new Point(0, 1); + Point q2 = new Point(3, 4); + + assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testTouchingAtEndpoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(2, 2); + Point q2 = new Point(4, 0); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testCollinearOverlapHasNoUniquePoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(4, 4); + Point q1 = new Point(2, 2); + Point q2 = new Point(6, 6); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testCollinearDisjointSegments() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(3, 3); + Point q2 = new Point(5, 5); + + assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); + assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); + } + + @Test + void testCollinearSegmentsTouchingAtEndpointHaveUniquePoint() { + Point p1 = new Point(0, 0); + Point p2 = new Point(2, 2); + Point q1 = new Point(2, 2); + Point q2 = new Point(4, 4); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); + } + + @Test + void testVerticalAndHorizontalCrossingSegments() { + Point p1 = new Point(2, 0); + Point p2 = new Point(2, 5); + Point q1 = new Point(0, 3); + Point q2 = new Point(4, 3); + + assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); + Optional intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); + assertTrue(intersection.isPresent()); + assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); + assertEquals(3.0, intersection.orElseThrow().getY(), 1e-9); + } +} diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java new file mode 100644 index 000000000000..291be677d894 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -0,0 +1,61 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AccountMergeTest { + + @Test + void testMergeAccountsWithSharedEmails() { + List> accounts = List.of(List.of("abc", "abc@mail.com", "abx@mail.com"), List.of("abc", "abc@mail.com", "aby@mail.com"), List.of("Mary", "mary@mail.com"), List.of("John", "johnnybravo@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of(List.of("John", "johnnybravo@mail.com"), List.of("Mary", "mary@mail.com"), List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testAccountsWithSameNameButNoSharedEmailStaySeparate() { + List> accounts = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testEmptyInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(List.of())); + } + + @Test + void testNullInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(null)); + } + + @Test + void testTransitiveMergeAndDuplicateEmails() { + List> accounts = List.of(List.of("A", "a1@mail.com", "a2@mail.com"), List.of("A", "a2@mail.com", "a3@mail.com"), List.of("A", "a3@mail.com", "a4@mail.com", "a4@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of(List.of("A", "a1@mail.com", "a2@mail.com", "a3@mail.com", "a4@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testAccountsWithNoEmailsArePreserved() { + List> accounts = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); + + assertEquals(expected, merged); + } +} diff --git a/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java b/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java new file mode 100644 index 000000000000..8608bfb2dfc9 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java @@ -0,0 +1,207 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TarjanBridges}. + * + *

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

+ */ +class TarjanBridgesTest { + + /** + * Helper to build a symmetric adjacency list for an undirected graph. + */ + private static List> buildGraph(int vertexCount, int[][] edges) { + List> adj = new ArrayList<>(); + for (int i = 0; i < vertexCount; i++) { + adj.add(new ArrayList<>()); + } + for (int[] edge : edges) { + adj.get(edge[0]).add(edge[1]); + adj.get(edge[1]).add(edge[0]); + } + return adj; + } + + /** + * Sorts bridges for deterministic comparison. + */ + private static void sortBridges(List bridges) { + bridges.sort(Comparator.comparingInt((int[] a) -> a[0]).thenComparingInt(a -> a[1])); + } + + @Test + void testSimpleGraphWithOneBridge() { + // Graph: 0-1-2-3 where 1-2 is the only bridge + // 0---1---2---3 + // | | + // +-------+ (via 0-2 would make cycle, but not here) + // Actually: 0-1 in a cycle with 0-1, and 2-3 in a cycle with 2-3 + // Let's use: 0--1--2 (linear chain). All edges are bridges. + List> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}}); + List bridges = TarjanBridges.findBridges(3, adj); + sortBridges(bridges); + assertEquals(2, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + assertEquals(1, bridges.get(1)[0]); + assertEquals(2, bridges.get(1)[1]); + } + + @Test + void testCycleGraphHasNoBridges() { + // Graph: 0-1-2-0 (triangle). No bridges. + List> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}, {2, 0}}); + List bridges = TarjanBridges.findBridges(3, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testTreeGraphAllEdgesAreBridges() { + // Tree: 0 + // / \ + // 1 2 + // / \ + // 3 4 + List> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {1, 3}, {1, 4}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(4, bridges.size()); + } + + @Test + void testGraphWithMixedBridgesAndCycles() { + // Graph: + // 0---1 + // | | + // 3---2---4---5 + // | + // 6 + // Cycle: 0-1-2-3-0 (no bridges within) + // Bridges: 2-4, 4-5, 5-6 + List> adj = buildGraph(7, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {2, 4}, {4, 5}, {5, 6}}); + List bridges = TarjanBridges.findBridges(7, adj); + sortBridges(bridges); + assertEquals(3, bridges.size()); + assertEquals(2, bridges.get(0)[0]); + assertEquals(4, bridges.get(0)[1]); + assertEquals(4, bridges.get(1)[0]); + assertEquals(5, bridges.get(1)[1]); + assertEquals(5, bridges.get(2)[0]); + assertEquals(6, bridges.get(2)[1]); + } + + @Test + void testDisconnectedGraphWithBridges() { + // Component 1: 0-1 (bridge) + // Component 2: 2-3-4-2 (cycle, no bridges) + List> adj = buildGraph(5, new int[][] {{0, 1}, {2, 3}, {3, 4}, {4, 2}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(1, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + } + + @Test + void testSingleVertex() { + List> adj = buildGraph(1, new int[][] {}); + List bridges = TarjanBridges.findBridges(1, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testTwoVerticesWithOneEdge() { + List> adj = buildGraph(2, new int[][] {{0, 1}}); + List bridges = TarjanBridges.findBridges(2, adj); + assertEquals(1, bridges.size()); + assertEquals(0, bridges.get(0)[0]); + assertEquals(1, bridges.get(0)[1]); + } + + @Test + void testEmptyGraph() { + List> adj = buildGraph(0, new int[][] {}); + List bridges = TarjanBridges.findBridges(0, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testIsolatedVertices() { + // 5 vertices, no edges β€” all isolated + List> adj = buildGraph(5, new int[][] {}); + List bridges = TarjanBridges.findBridges(5, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testLargeCycleNoBridges() { + // Cycle: 0-1-2-3-4-5-6-7-0 + int n = 8; + int[][] edges = new int[n][2]; + for (int i = 0; i < n; i++) { + edges[i] = new int[] {i, (i + 1) % n}; + } + List> adj = buildGraph(n, edges); + List bridges = TarjanBridges.findBridges(n, adj); + assertTrue(bridges.isEmpty()); + } + + @Test + void testComplexGraphWithMultipleCyclesAndBridges() { + // Two cycles connected by a single bridge edge: + // Cycle A: 0-1-2-0 + // Cycle B: 3-4-5-3 + // Bridge: 2-3 + List> adj = buildGraph(6, new int[][] {{0, 1}, {1, 2}, {2, 0}, {3, 4}, {4, 5}, {5, 3}, {2, 3}}); + List bridges = TarjanBridges.findBridges(6, adj); + assertEquals(1, bridges.size()); + assertEquals(2, bridges.get(0)[0]); + assertEquals(3, bridges.get(0)[1]); + } + + @Test + void testNegativeVertexCountThrowsException() { + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(-1, new ArrayList<>())); + } + + @Test + void testNullAdjacencyListThrowsException() { + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(3, null)); + } + + @Test + void testMismatchedAdjacencyListSizeThrowsException() { + List> adj = buildGraph(2, new int[][] {{0, 1}}); + assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(5, adj)); + } + + @Test + void testStarGraphAllEdgesAreBridges() { + // Star graph: center vertex 0 connected to 1, 2, 3, 4 + List> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {0, 3}, {0, 4}}); + List bridges = TarjanBridges.findBridges(5, adj); + assertEquals(4, bridges.size()); + } + + @Test + void testBridgeBetweenTwoCycles() { + // Two squares connected by one bridge: + // Square 1: 0-1-2-3-0 + // Square 2: 4-5-6-7-4 + // Bridge: 3-4 + List> adj = buildGraph(8, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {4, 5}, {5, 6}, {6, 7}, {7, 4}, {3, 4}}); + List bridges = TarjanBridges.findBridges(8, adj); + assertEquals(1, bridges.size()); + assertEquals(3, bridges.get(0)[0]); + assertEquals(4, bridges.get(0)[1]); + } +} diff --git a/src/test/java/com/thealgorithms/io/BufferedReaderTest.java b/src/test/java/com/thealgorithms/io/BufferedReaderTest.java index 891c3066058e..088e86f8f7c5 100644 --- a/src/test/java/com/thealgorithms/io/BufferedReaderTest.java +++ b/src/test/java/com/thealgorithms/io/BufferedReaderTest.java @@ -17,15 +17,15 @@ public void testPeeks() throws IOException { BufferedReader reader = new BufferedReader(input); // read the first letter - assertEquals(reader.read(), 'H'); + assertEquals('H', reader.read()); len--; - assertEquals(reader.available(), len); + assertEquals(len, reader.available()); // position: H[e]llo!\nWorld! // reader.read() will be == 'e' - assertEquals(reader.peek(1), 'l'); - assertEquals(reader.peek(2), 'l'); // second l - assertEquals(reader.peek(3), 'o'); + assertEquals('l', reader.peek(1)); + assertEquals('l', reader.peek(2)); // second l + assertEquals('o', reader.peek(3)); } @Test @@ -38,21 +38,21 @@ public void testMixes() throws IOException { BufferedReader reader = new BufferedReader(input); // read the first letter - assertEquals(reader.read(), 'H'); // first letter + assertEquals('H', reader.read()); // first letter len--; - assertEquals(reader.peek(1), 'l'); // third later (second letter after 'H') - assertEquals(reader.read(), 'e'); // second letter + assertEquals('l', reader.peek(1)); // third later (second letter after 'H') + assertEquals('e', reader.read()); // second letter len--; - assertEquals(reader.available(), len); + assertEquals(len, reader.available()); // position: H[e]llo!\nWorld! - assertEquals(reader.peek(2), 'o'); // second l - assertEquals(reader.peek(3), '!'); - assertEquals(reader.peek(4), '\n'); + assertEquals('o', reader.peek(2)); // second l + assertEquals('!', reader.peek(3)); + assertEquals('\n', reader.peek(4)); - assertEquals(reader.read(), 'l'); // third letter - assertEquals(reader.peek(1), 'o'); // fourth letter + assertEquals('l', reader.read()); // third letter + assertEquals('o', reader.peek(1)); // fourth letter for (int i = 0; i < 6; i++) { reader.read(); @@ -74,23 +74,23 @@ public void testBlockPractical() throws IOException { ByteArrayInputStream input = new ByteArrayInputStream(bytes); BufferedReader reader = new BufferedReader(input); - assertEquals(reader.peek(), 'H'); - assertEquals(reader.read(), '!'); // read the first letter + assertEquals('H', reader.peek()); + assertEquals('!', reader.read()); // read the first letter len--; // this only reads the next 5 bytes (Hello) because // the default buffer size = 5 - assertEquals(new String(reader.readBlock()), "Hello"); + assertEquals("Hello", new String(reader.readBlock())); len -= 5; assertEquals(reader.available(), len); // maybe kind of a practical demonstration / use case if (reader.read() == '\n') { - assertEquals(reader.read(), 'W'); - assertEquals(reader.read(), 'o'); + assertEquals('W', reader.read()); + assertEquals('o', reader.read()); // the rest of the blocks - assertEquals(new String(reader.readBlock()), "rld!"); + assertEquals("rld!", new String(reader.readBlock())); } else { // should not reach throw new IOException("Something not right"); diff --git a/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java b/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java new file mode 100644 index 000000000000..e8a130d0ccb2 --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/LinearRegressionTest.java @@ -0,0 +1,95 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class LinearRegressionTest { + + private static final double DELTA = 0.1; + + @Test + void fitLearnsCorrectSlopeAndIntercept() { + double trueM = 2.5; + double trueB = 1.5; + + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] yTrain = new double[xTrain.length]; + for (int i = 0; i < xTrain.length; i++) { + yTrain[i] = (trueM * xTrain[i]) + trueB; + } + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + assertEquals(trueM, model.getSlope(), DELTA); + assertEquals(trueB, model.getIntercept(), DELTA); + } + + @Test + void predictMatchesExpectedOnUnseenData() { + double trueM = 2.5; + double trueB = 1.5; + + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] yTrain = new double[xTrain.length]; + for (int i = 0; i < xTrain.length; i++) { + yTrain[i] = (trueM * xTrain[i]) + trueB; + } + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + double[] testInputs = {0.0, 3.5, 7.0}; + for (double testX : testInputs) { + double expectedY = (trueM * testX) + trueB; + assertEquals(expectedY, model.predict(testX), DELTA); + } + } + + @Test + void calculateMSEIsNearZeroAfterTraining() { + double[] xTrain = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] yTrain = {3.0, 5.0, 7.0, 9.0, 11.0}; // y = 2x + 1 + + LinearRegression model = new LinearRegression(0.01, 1000); + model.fit(xTrain, yTrain); + + assertEquals(0.0, model.calculateMSE(xTrain, yTrain), 0.01); + } + + @Test + void fitThrowsExceptionOnMismatchedArrayLengths() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {1.0, 2.0}; + double[] y = {1.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, y)); + } + + @Test + void fitThrowsExceptionOnEmptyArrays() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {}; + double[] y = {}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, y)); + } + + @Test + void fitThrowsExceptionOnNullX() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] y = {1.0, 2.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(null, y)); + } + + @Test + void fitThrowsExceptionOnNullY() { + LinearRegression model = new LinearRegression(0.01, 100); + double[] x = {1.0, 2.0}; + + assertThrows(IllegalArgumentException.class, () -> model.fit(x, null)); + } +} diff --git a/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java new file mode 100644 index 000000000000..5ccd31147ac3 --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java @@ -0,0 +1,141 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MultinomialNaiveBayesClassifierTest { + + @Test + void predictsCorrectClassOnSeparableToyDataset() { + // Class 0 samples are dominated by feature 0; class 1 samples by feature 1. + double[][] features = { + {5, 1}, + {6, 0}, + {4, 1}, + {1, 5}, + {0, 6}, + {1, 4}, + }; + int[] labels = {0, 0, 0, 1, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + assertEquals(0, classifier.predict(new double[] {5, 0})); + assertEquals(1, classifier.predict(new double[] {0, 5})); + } + + @Test + void predictBatchMatchesIndividualPredictions() { + double[][] features = { + {3, 0}, + {2, 0}, + {0, 3}, + {0, 2}, + }; + int[] labels = {0, 0, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + double[][] samples = {{4, 0}, {0, 4}}; + int[] predictions = classifier.predict(samples); + + assertEquals(classifier.predict(samples[0]), predictions[0]); + assertEquals(classifier.predict(samples[1]), predictions[1]); + } + + @Test + void laplaceSmoothingKeepsZeroCountFeatureLogProbabilityFinite() { + // Feature index 1 never appears for class 0 in training data. + double[][] features = { + {2, 0}, + {3, 0}, + {0, 2}, + {0, 3}, + }; + int[] labels = {0, 0, 1, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + // A sample that hits class 0's zero-count feature should still produce + // a finite, usable prediction instead of -Infinity collapsing the score. + int prediction = classifier.predict(new double[] {1, 1}); + assertTrue(prediction == 0 || prediction == 1); + } + + @Test + void predictBeforeFitThrowsIllegalStateException() { + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalStateException.class, () -> classifier.predict(new double[] {1, 2})); + } + + @Test + void nonPositiveAlphaThrowsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(0.0)); + assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(-1.0)); + } + + @Test + void mismatchedSampleLengthThrowsIllegalArgumentException() { + double[][] features = { + {1, 2}, + {3, 4}, + }; + int[] labels = {0, 1}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + classifier.fit(features, labels); + + assertThrows(IllegalArgumentException.class, () -> classifier.predict(new double[] {1, 2, 3})); + } + + @Test + void mismatchedFeatureAndLabelLengthsThrowsIllegalArgumentException() { + double[][] features = { + {1, 2}, + {3, 4}, + }; + int[] labels = {0}; + + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels)); + } + + @Test + void emptyFeaturesArrayThrowsIllegalArgumentException() { + double[][] features = {}; + int[] labels = {}; + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels)); + } + + @Test + void refittingReplacesPreviousModelState() { + MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier(); + + double[][] firstFeatures = { + {5, 0, 0}, + {0, 5, 0}, + {0, 0, 5}, + }; + int[] firstLabels = {0, 1, 2}; + classifier.fit(firstFeatures, firstLabels); + + double[][] secondFeatures = { + {5, 0}, + {0, 5}, + }; + int[] secondLabels = {0, 1}; + classifier.fit(secondFeatures, secondLabels); + + // Class 2 existed in the first fit but not the second β€” it must not + // survive into predictions after refitting. + int prediction = classifier.predict(new double[] {2.5, 2.5}); + assertTrue(prediction == 0 || prediction == 1); + } +} diff --git a/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java b/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java index dfca757fd877..070ff4ae3147 100644 --- a/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java +++ b/src/test/java/com/thealgorithms/maths/AbsoluteMinTest.java @@ -11,12 +11,15 @@ public class AbsoluteMinTest { void testGetMinValue() { assertEquals(0, AbsoluteMin.getMinValue(4, 0, 16)); assertEquals(-2, AbsoluteMin.getMinValue(3, -10, -2)); + assertEquals(-2, AbsoluteMin.getMinValue(-3, -10, -2)); + assertEquals(2, AbsoluteMin.getMinValue(-3, -10, 2)); + assertEquals(2, AbsoluteMin.getMinValue(-5, 2)); + assertEquals(2, AbsoluteMin.getMinValue(2, -5)); } @Test void testGetMinValueWithNoArguments() { - Exception exception = assertThrows(IllegalArgumentException.class, AbsoluteMin::getMinValue); - assertEquals("Numbers array cannot be empty", exception.getMessage()); + assertThrows(IllegalArgumentException.class, AbsoluteMin::getMinValue); } @Test @@ -24,4 +27,13 @@ void testGetMinValueWithSameAbsoluteValues() { assertEquals(-5, AbsoluteMin.getMinValue(-5, 5)); assertEquals(-5, AbsoluteMin.getMinValue(5, -5)); } + + @Test + void testIntegerMinValueOverflow() { + assertEquals(1, AbsoluteMin.getMinValue(Integer.MIN_VALUE, 1)); + assertEquals(-1, AbsoluteMin.getMinValue(Integer.MIN_VALUE, -1)); + assertEquals(0, AbsoluteMin.getMinValue(Integer.MIN_VALUE, 0)); + assertEquals(Integer.MIN_VALUE, AbsoluteMin.getMinValue(Integer.MIN_VALUE)); + assertEquals(Integer.MAX_VALUE, AbsoluteMin.getMinValue(Integer.MIN_VALUE, Integer.MAX_VALUE)); + } } diff --git a/src/test/java/com/thealgorithms/maths/AreaTest.java b/src/test/java/com/thealgorithms/maths/AreaTest.java index b28afb85fbc3..1c2fe53ff3f3 100644 --- a/src/test/java/com/thealgorithms/maths/AreaTest.java +++ b/src/test/java/com/thealgorithms/maths/AreaTest.java @@ -16,6 +16,11 @@ void testSurfaceAreaCube() { assertEquals(6.0, Area.surfaceAreaCube(1)); } + @Test + void testSurfaceAreaCuboid() { + assertEquals(214.0, Area.surfaceAreaCuboid(5, 6, 7)); + } + @Test void testSurfaceAreaSphere() { assertEquals(12.566370614359172, Area.surfaceAreaSphere(1)); @@ -70,6 +75,12 @@ void surfaceAreaCone() { void testAllIllegalInput() { assertAll(() -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCube(0)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(0, 1, 2)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(1, 0, 2)), + () + -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaCuboid(1, 2, 0)), () -> assertThrows(IllegalArgumentException.class, () -> Area.surfaceAreaSphere(0)), () diff --git a/src/test/java/com/thealgorithms/maths/BellNumbersTest.java b/src/test/java/com/thealgorithms/maths/BellNumbersTest.java new file mode 100644 index 000000000000..8dd83cf0f7a9 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/BellNumbersTest.java @@ -0,0 +1,53 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class BellNumbersTest { + + @Test + void testStandardCases() { + // Base cases and small numbers + assertEquals(1, BellNumbers.compute(0)); + assertEquals(1, BellNumbers.compute(1)); + assertEquals(2, BellNumbers.compute(2)); + assertEquals(5, BellNumbers.compute(3)); + assertEquals(15, BellNumbers.compute(4)); + assertEquals(52, BellNumbers.compute(5)); + } + + @Test + void testMediumNumber() { + // B10 = 115,975 + assertEquals(115975, BellNumbers.compute(10)); + // B15 = 1,382,958,545 + assertEquals(1382958545L, BellNumbers.compute(15)); + } + + @Test + void testLargeNumber() { + // B20 = 51,724,158,235,372 + // We use the 'L' suffix to tell Java this is a long literal + assertEquals(51724158235372L, BellNumbers.compute(20)); + } + + @Test + void testMaxLongCapacity() { + // B25 is the largest Bell number that fits in a Java long (signed 64-bit) + // B25 = 4,638,590,332,229,999,353 + assertEquals(4638590332229999353L, BellNumbers.compute(25)); + } + + @Test + void testNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> BellNumbers.compute(-1)); + } + + @Test + void testOverflowProtection() { + // We expect an exception if the user asks for the impossible + assertThrows(IllegalArgumentException.class, () -> BellNumbers.compute(26)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java b/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java new file mode 100644 index 000000000000..02e964b53771 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ComplexNumberMultiplyTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class ComplexNumberMultiplyTest { + + @Test + void testExample() { + assertEquals("0+2i", ComplexNumberMultiply.multiply("1+1i", "1+1i")); + } + + @Test + void testNegative() { + assertEquals("0+-2i", ComplexNumberMultiply.multiply("1+-1i", "1+-1i")); + } + + @Test + void testZero() { + assertEquals("0+0i", ComplexNumberMultiply.multiply("0+0i", "5+3i")); + } + + @Test + void testInvalidFormat() { + assertThrows(IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply("1+1", "1+1i")); + } + + @Test + void testNullInput() { + assertThrows(IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply(null, "1+1i")); + } +} diff --git a/src/test/java/com/thealgorithms/maths/CorrelationTest.java b/src/test/java/com/thealgorithms/maths/CorrelationTest.java new file mode 100644 index 000000000000..96867d56ad5e --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/CorrelationTest.java @@ -0,0 +1,51 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test class for Correlation class + */ +public class CorrelationTest { + + public static final double DELTA = 1e-9; + + // Regular correlation test + public void testCorrelationFirst() { + double[] x = {1, 2, 3, 4}; + double[] y = {7, 1, 4, 9}; + int n = 4; + assertEquals(0.3319700011, Correlation.correlation(x, y, n), DELTA); + } + + // Regular correlation test (zero correlation) + public void testCorrelationSecond() { + double[] x = {1, 2, 3, 4}; + double[] y = {5, 0, 9, 2}; + int n = 4; + assertEquals(0, Correlation.correlation(x, y, n), DELTA); + } + + // Correlation with a constant variable is taken to be zero + public void testCorrelationConstant() { + double[] x = {1, 2, 3}; + double[] y = {4, 4, 4}; + int n = 3; + assertEquals(0, Correlation.correlation(x, y, n), DELTA); + } + + // Linear dependence gives correlation 1 + public void testCorrelationLinearDependence() { + double[] x = {1, 2, 3, 4}; + double[] y = {6, 8, 10, 12}; + int n = 4; + assertEquals(1, Correlation.correlation(x, y, n), DELTA); + } + + // Inverse linear dependence gives correlation -1 + public void testCorrelationInverseLinearDependence() { + double[] x = {1, 2, 3, 4, 5}; + double[] y = {18, 15, 12, 9, 6}; + int n = 5; + assertEquals(-1, Correlation.correlation(x, y, n), DELTA); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java b/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java new file mode 100644 index 000000000000..54e9e861ad44 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/DisariumNumberTest.java @@ -0,0 +1,32 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DisariumNumberTest { + + @Test + void testDisariumNumbers() { + assertTrue(DisariumNumber.isDisarium(1)); + assertTrue(DisariumNumber.isDisarium(89)); + assertTrue(DisariumNumber.isDisarium(135)); + assertTrue(DisariumNumber.isDisarium(175)); + assertTrue(DisariumNumber.isDisarium(518)); + } + + @Test + void testNonDisariumNumbers() { + assertFalse(DisariumNumber.isDisarium(10)); + assertFalse(DisariumNumber.isDisarium(100)); + assertFalse(DisariumNumber.isDisarium(200)); + } + + @Test + void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> DisariumNumber.isDisarium(0)); + assertThrows(IllegalArgumentException.class, () -> DisariumNumber.isDisarium(-5)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java b/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java new file mode 100644 index 000000000000..6bd124629740 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/DistanceBetweenTwoPointsTest.java @@ -0,0 +1,23 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class DistanceBetweenTwoPointsTest { + + @Test + void testDistanceSimple() { + assertEquals(5.0, DistanceBetweenTwoPoints.calculate(0, 0, 3, 4), 1e-9); + } + + @Test + void testDistanceNegativeCoordinates() { + assertEquals(5.0, DistanceBetweenTwoPoints.calculate(-1, -1, 2, 3), 1e-9); + } + + @Test + void testSamePoint() { + assertEquals(0.0, DistanceBetweenTwoPoints.calculate(2, 2, 2, 2), 1e-9); + } +} diff --git a/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java b/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java index 3a14b80dd4f9..66f3b7b03938 100644 --- a/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java +++ b/src/test/java/com/thealgorithms/maths/DistanceFormulaTest.java @@ -9,78 +9,78 @@ public class DistanceFormulaTest { @Test void euclideanTest1() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1, 1, 2, 2), 1.4142135623730951); + Assertions.assertEquals(1.4142135623730951, DistanceFormula.euclideanDistance(1, 1, 2, 2)); } @Test void euclideanTest2() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1, 3, 8, 0), 7.0710678118654755); + Assertions.assertEquals(7.0710678118654755, DistanceFormula.euclideanDistance(1, 3, 8, 0)); } @Test void euclideanTest3() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(2.4, 9.1, 55.1, 100), 110.91911467371168); + Assertions.assertEquals(110.91911467371168, DistanceFormula.euclideanDistance(2.4, 9.1, 55.1, 100)); } @Test void euclideanTest4() { - Assertions.assertEquals(DistanceFormula.euclideanDistance(1000, 13, 20000, 84), 19022.067605809836); + Assertions.assertEquals(19022.067605809836, DistanceFormula.euclideanDistance(1000, 13, 20000, 84)); } @Test public void manhattantest1() { - assertEquals(DistanceFormula.manhattanDistance(1, 2, 3, 4), 4); + assertEquals(4, DistanceFormula.manhattanDistance(1, 2, 3, 4)); } @Test public void manhattantest2() { - assertEquals(DistanceFormula.manhattanDistance(6.5, 8.4, 20.1, 13.6), 18.8); + assertEquals(18.8, DistanceFormula.manhattanDistance(6.5, 8.4, 20.1, 13.6)); } @Test public void manhattanTest3() { - assertEquals(DistanceFormula.manhattanDistance(10.112, 50, 8, 25.67), 26.442); + assertEquals(26.442, DistanceFormula.manhattanDistance(10.112, 50, 8, 25.67)); } @Test public void hammingTest1() { int[] array1 = {1, 1, 1, 1}; int[] array2 = {0, 0, 0, 0}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 4); + assertEquals(4, DistanceFormula.hammingDistance(array1, array2)); } @Test public void hammingTest2() { int[] array1 = {1, 1, 1, 1}; int[] array2 = {1, 1, 1, 1}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 0); + assertEquals(0, DistanceFormula.hammingDistance(array1, array2)); } @Test public void hammingTest3() { int[] array1 = {1, 0, 0, 1, 1, 0, 1, 1, 0}; int[] array2 = {0, 1, 0, 0, 1, 1, 1, 0, 0}; - assertEquals(DistanceFormula.hammingDistance(array1, array2), 5); + assertEquals(5, DistanceFormula.hammingDistance(array1, array2)); } @Test public void minkowskiTest1() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 1), 10); + assertEquals(10, DistanceFormula.minkowskiDistance(array1, array2, 1)); } @Test public void minkowskiTest2() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 2), 5.477225575051661); + assertEquals(5.477225575051661, DistanceFormula.minkowskiDistance(array1, array2, 2)); } @Test public void minkowskiTest3() { double[] array1 = {1, 3, 8, 5}; double[] array2 = {4, 2, 6, 9}; - assertEquals(DistanceFormula.minkowskiDistance(array1, array2, 3), 4.641588833612778); + assertEquals(4.641588833612778, DistanceFormula.minkowskiDistance(array1, array2, 3)); } } diff --git a/src/test/java/com/thealgorithms/maths/FactorialTest.java b/src/test/java/com/thealgorithms/maths/FactorialTest.java index b38dc45589ee..a393136696e1 100644 --- a/src/test/java/com/thealgorithms/maths/FactorialTest.java +++ b/src/test/java/com/thealgorithms/maths/FactorialTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; import org.junit.jupiter.api.Test; public class FactorialTest { @@ -11,14 +12,14 @@ public class FactorialTest { @Test public void testWhenInvalidInoutProvidedShouldThrowException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Factorial.factorial(-1)); - assertEquals(exception.getMessage(), EXCEPTION_MESSAGE); + assertEquals(EXCEPTION_MESSAGE, exception.getMessage()); } @Test public void testCorrectFactorialCalculation() { - assertEquals(1, Factorial.factorial(0)); - assertEquals(1, Factorial.factorial(1)); - assertEquals(120, Factorial.factorial(5)); - assertEquals(3628800, Factorial.factorial(10)); + assertEquals(BigInteger.ONE, Factorial.factorial(0)); + assertEquals(BigInteger.ONE, Factorial.factorial(1)); + assertEquals(BigInteger.valueOf(120), Factorial.factorial(5)); + assertEquals(BigInteger.valueOf(3628800), Factorial.factorial(10)); } } diff --git a/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java b/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java new file mode 100644 index 000000000000..be5ddd7ee79e --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/FriendlyNumberTest.java @@ -0,0 +1,33 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class FriendlyNumberTest { + + @Test + public void testFriendlyNumbers() { + // 6 and 28 are friendly (abundancy index = 2) + assertTrue(FriendlyNumber.areFriendly(6, 28)); + // Every number is friendly with itself + assertTrue(FriendlyNumber.areFriendly(6, 6)); + assertTrue(FriendlyNumber.areFriendly(1, 1)); + } + + @Test + public void testNonFriendlyNumbers() { + assertFalse(FriendlyNumber.areFriendly(6, 10)); + assertFalse(FriendlyNumber.areFriendly(10, 15)); + assertFalse(FriendlyNumber.areFriendly(4, 9)); + } + + @Test + public void testInvalidInputs() { + assertFalse(FriendlyNumber.areFriendly(0, 6)); + assertFalse(FriendlyNumber.areFriendly(-1, 6)); + assertFalse(FriendlyNumber.areFriendly(6, -1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java b/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java new file mode 100644 index 000000000000..19558510f916 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/JacobsthalNumberTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class JacobsthalNumberTest { + + @Test + public void testBaseCases() { + assertEquals(0, JacobsthalNumber.jacobsthal(0)); + assertEquals(1, JacobsthalNumber.jacobsthal(1)); + } + + @Test + public void testKnownValues() { + assertEquals(1, JacobsthalNumber.jacobsthal(2)); + assertEquals(3, JacobsthalNumber.jacobsthal(3)); + assertEquals(5, JacobsthalNumber.jacobsthal(4)); + assertEquals(11, JacobsthalNumber.jacobsthal(5)); + assertEquals(21, JacobsthalNumber.jacobsthal(6)); + assertEquals(43, JacobsthalNumber.jacobsthal(7)); + assertEquals(85, JacobsthalNumber.jacobsthal(8)); + assertEquals(171, JacobsthalNumber.jacobsthal(9)); + assertEquals(341, JacobsthalNumber.jacobsthal(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> JacobsthalNumber.jacobsthal(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java index c4205985dbfd..885382e29ca2 100644 --- a/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java +++ b/src/test/java/com/thealgorithms/maths/LinearDiophantineEquationsSolverTest.java @@ -176,7 +176,7 @@ void testSolutionEquality() { assertEquals(solution1, solution2); assertNotEquals(solution3, solution1); assertEquals(solution1, solution1); - assertNotEquals(null, solution1); + assertNotNull(solution1); assertNotEquals("string", solution1); } @@ -217,7 +217,7 @@ void testGcdSolutionWrapperEquality() { assertEquals(wrapper1, wrapper2); assertNotEquals(wrapper3, wrapper1); assertEquals(wrapper1, wrapper1); - assertNotEquals(null, wrapper1); + assertNotNull(wrapper1); assertNotEquals("string", wrapper1); } diff --git a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java b/src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java similarity index 99% rename from src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java rename to src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java index 7c3ce6635aa0..5a3d1c64b379 100644 --- a/src/test/java/com/thealgorithms/others/LowestBasePalindromeTest.java +++ b/src/test/java/com/thealgorithms/maths/LowestBasePalindromeTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.others; +package com.thealgorithms.maths; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/test/java/com/thealgorithms/maths/MeansTest.java b/src/test/java/com/thealgorithms/maths/MeansTest.java index deee0a931910..853fdbea3963 100644 --- a/src/test/java/com/thealgorithms/maths/MeansTest.java +++ b/src/test/java/com/thealgorithms/maths/MeansTest.java @@ -172,6 +172,53 @@ void testHarmonicMeanWithLinkedList() { assertEquals(expected, Means.harmonic(numbers), EPSILON); } + // ========== Quadratic Mean Tests ========== + + @Test + void testQuadraticMeanThrowsExceptionForEmptyList() { + List numbers = new ArrayList<>(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.quadratic(numbers)); + assertTrue(exception.getMessage().contains("Empty list")); + } + + @Test + void testQuadraticMeanSingleNumber() { + LinkedHashSet numbers = new LinkedHashSet<>(Arrays.asList(2.5)); + assertEquals(2.5, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanTwoNumbers() { + List numbers = Arrays.asList(1.0, 7.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanMultipleNumbers() { + Vector numbers = new Vector<>(Arrays.asList(1.0, 2.5, 3.0, 7.5, 10.0)); + double expected = Math.sqrt(34.5); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanThreeNumbers() { + List numbers = Arrays.asList(3.0, 6.0, 9.0); + double expected = Math.sqrt(42.0); + assertEquals(expected, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanIdenticalNumbers() { + List numbers = Arrays.asList(5.0, 5.0, 5.0); + assertEquals(5.0, Means.quadratic(numbers), EPSILON); + } + + @Test + void testQuadraticMeanWithLinkedList() { + LinkedList numbers = new LinkedList<>(Arrays.asList(1.0, 5.0, 11.0)); + assertEquals(7.0, Means.quadratic(numbers), EPSILON); + } + // ========== Additional Edge Case Tests ========== @Test @@ -198,21 +245,25 @@ void testAllMeansConsistencyForIdenticalValues() { double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertEquals(7.5, arithmetic, EPSILON); assertEquals(7.5, geometric, EPSILON); assertEquals(7.5, harmonic, EPSILON); + assertEquals(7.5, quadratic, EPSILON); } @Test void testMeansRelationship() { - // For positive numbers, harmonic mean ≀ geometric mean ≀ arithmetic mean + // For positive numbers, harmonic mean ≀ geometric mean ≀ arithmetic mean ≀ quadratic mean List numbers = Arrays.asList(2.0, 4.0, 8.0); double arithmetic = Means.arithmetic(numbers); double geometric = Means.geometric(numbers); double harmonic = Means.harmonic(numbers); + double quadratic = Means.quadratic(numbers); assertTrue(harmonic <= geometric, "Harmonic mean should be ≀ geometric mean"); assertTrue(geometric <= arithmetic, "Geometric mean should be ≀ arithmetic mean"); + assertTrue(arithmetic <= quadratic, "Arithmetic mean should be ≀ quadratic mean"); } } diff --git a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java index 3fe58dadf8a5..1ee437b190c5 100644 --- a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java @@ -48,22 +48,22 @@ public void testGetWithSameObject() { var uglyNumbers = new NthUglyNumber(new int[] {7, 2, 5, 3}); for (final var tc : testCases.entrySet()) { - assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue()); + assertEquals(tc.getValue(), uglyNumbers.get(tc.getKey())); } - assertEquals(uglyNumbers.get(999), 385875); + assertEquals(385875, uglyNumbers.get(999)); } @Test public void testGetWithBase1() { var uglyNumbers = new NthUglyNumber(new int[] {1}); - assertEquals(uglyNumbers.get(10), 1); + assertEquals(1, uglyNumbers.get(10)); } @Test public void testGetWithBase2() { var uglyNumbers = new NthUglyNumber(new int[] {2}); - assertEquals(uglyNumbers.get(5), 32); + assertEquals(32, uglyNumbers.get(5)); } @Test diff --git a/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java b/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java new file mode 100644 index 000000000000..b9d7f04b4d0b --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PadovanSequenceTest.java @@ -0,0 +1,36 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * @author Vraj Prajapati (@Rosander0) + */ +public class PadovanSequenceTest { + + @Test + public void testBaseCase() { + assertEquals(1, PadovanSequence.padovan(0)); + assertEquals(1, PadovanSequence.padovan(1)); + assertEquals(1, PadovanSequence.padovan(2)); + } + + @Test + public void testKnownValues() { + assertEquals(2, PadovanSequence.padovan(3)); + assertEquals(2, PadovanSequence.padovan(4)); + assertEquals(3, PadovanSequence.padovan(5)); + assertEquals(4, PadovanSequence.padovan(6)); + assertEquals(5, PadovanSequence.padovan(7)); + assertEquals(7, PadovanSequence.padovan(8)); + assertEquals(9, PadovanSequence.padovan(9)); + assertEquals(12, PadovanSequence.padovan(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> PadovanSequence.padovan(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java b/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java index a70100c0b913..4e4bd85d07b5 100644 --- a/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java +++ b/src/test/java/com/thealgorithms/maths/PalindromeNumberTest.java @@ -25,6 +25,6 @@ public void testNumbersAreNotPalindromes() { @Test public void testIfNegativeInputThenExceptionExpected() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> PalindromeNumber.isPalindrome(-1)); - Assertions.assertEquals(exception.getMessage(), "Input parameter must not be negative!"); + Assertions.assertEquals("Input parameter must not be negative!", exception.getMessage()); } } diff --git a/src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java b/src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java similarity index 98% rename from src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java rename to src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java index 130cd19b47b1..2405da558700 100644 --- a/src/test/java/com/thealgorithms/misc/PalindromePrimeTest.java +++ b/src/test/java/com/thealgorithms/maths/PalindromePrimeTest.java @@ -1,4 +1,4 @@ -package com.thealgorithms.misc; +package com.thealgorithms.maths; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java b/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java index 7649e21eb231..a9b78be88042 100644 --- a/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java +++ b/src/test/java/com/thealgorithms/maths/ParseIntegerTest.java @@ -14,13 +14,13 @@ public class ParseIntegerTest { @Test public void testNullInput() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> ParseInteger.parseInt(null)); - Assertions.assertEquals(exception.getMessage(), NULL_PARAMETER_MESSAGE); + Assertions.assertEquals(NULL_PARAMETER_MESSAGE, exception.getMessage()); } @Test public void testEmptyInput() { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> ParseInteger.parseInt("")); - Assertions.assertEquals(exception.getMessage(), EMPTY_PARAMETER_MESSAGE); + Assertions.assertEquals(EMPTY_PARAMETER_MESSAGE, exception.getMessage()); } @Test diff --git a/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java b/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java new file mode 100644 index 000000000000..0ec476dc0641 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/PerrinNumberTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.maths; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PerrinNumberTest { + + @Test + public void testBaseCases() { + assertEquals(3, PerrinNumber.perrin(0)); + assertEquals(0, PerrinNumber.perrin(1)); + assertEquals(2, PerrinNumber.perrin(2)); + } + + @Test + public void testKnownValues() { + assertEquals(3, PerrinNumber.perrin(3)); + assertEquals(2, PerrinNumber.perrin(4)); + assertEquals(5, PerrinNumber.perrin(5)); + assertEquals(5, PerrinNumber.perrin(6)); + assertEquals(7, PerrinNumber.perrin(7)); + assertEquals(10, PerrinNumber.perrin(8)); + assertEquals(12, PerrinNumber.perrin(9)); + assertEquals(17, PerrinNumber.perrin(10)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> PerrinNumber.perrin(-1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java b/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java index a2046511ddf5..a6552d56783c 100644 --- a/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java +++ b/src/test/java/com/thealgorithms/maths/QuadraticEquationSolverTest.java @@ -14,10 +14,10 @@ public void testSolveEquationRealRoots() { double c = 1.9; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 2); - Assertions.assertEquals(roots[0].real, -0.27810465435684306); + Assertions.assertEquals(2, roots.length, 2); + Assertions.assertEquals(-0.27810465435684306, roots[0].real); Assertions.assertNull(roots[0].imaginary); - Assertions.assertEquals(roots[1].real, -1.6266572504050616); + Assertions.assertEquals(-1.6266572504050616, roots[1].real); Assertions.assertNull(roots[1].imaginary); } @@ -29,8 +29,8 @@ public void testSolveEquationEqualRoots() { double c = 1; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 1); - Assertions.assertEquals(roots[0].real, -1); + Assertions.assertEquals(1, roots.length); + Assertions.assertEquals(-1, roots[0].real); } @Test @@ -41,10 +41,10 @@ public void testSolveEquationComplexRoots() { double c = 5.6; ComplexNumber[] roots = quadraticEquationSolver.solveEquation(a, b, c); - Assertions.assertEquals(roots.length, 2); - Assertions.assertEquals(roots[0].real, -0.8695652173913044); - Assertions.assertEquals(roots[0].imaginary, 1.2956229935435948); - Assertions.assertEquals(roots[1].real, -0.8695652173913044); - Assertions.assertEquals(roots[1].imaginary, -1.2956229935435948); + Assertions.assertEquals(2, roots.length); + Assertions.assertEquals(-0.8695652173913044, roots[0].real); + Assertions.assertEquals(1.2956229935435948, roots[0].imaginary); + Assertions.assertEquals(-0.8695652173913044, roots[1].real); + Assertions.assertEquals(-1.2956229935435948, roots[1].imaginary); } } diff --git a/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java b/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java index c744614e5cfa..c5d47f2213a9 100644 --- a/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java +++ b/src/test/java/com/thealgorithms/maths/SecondMinMaxTest.java @@ -29,19 +29,19 @@ public TestCase(final int[] inInputArray, final int inSecondMin, final int inSec @Test public void testForEmptyInputArray() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMin(new int[] {})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_LEN_LESS_2); + assertEquals(EXP_MSG_ARR_LEN_LESS_2, exception.getMessage()); } @Test public void testForArrayWithSingleElement() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMax(new int[] {1})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_LEN_LESS_2); + assertEquals(EXP_MSG_ARR_LEN_LESS_2, exception.getMessage()); } @Test public void testForArrayWithSameElements() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SecondMinMax.findSecondMin(new int[] {1, 1, 1, 1})); - assertEquals(exception.getMessage(), EXP_MSG_ARR_SAME_ELE); + assertEquals(EXP_MSG_ARR_SAME_ELE, exception.getMessage()); } @ParameterizedTest diff --git a/src/test/java/com/thealgorithms/maths/SociableNumberTest.java b/src/test/java/com/thealgorithms/maths/SociableNumberTest.java new file mode 100644 index 000000000000..b4877cd761a7 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/SociableNumberTest.java @@ -0,0 +1,56 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SociableNumber}. + * + * @author Vraj Prajapati (@Rosander0) + */ +public class SociableNumberTest { + + @Test + public void testSumOfProperDivisorsEdgeCases() { + assertEquals(0, SociableNumber.sumOfProperDivisors(0)); + assertEquals(0, SociableNumber.sumOfProperDivisors(-5)); + assertEquals(0, SociableNumber.sumOfProperDivisors(1)); + assertEquals(1, SociableNumber.sumOfProperDivisors(2)); + } + + @Test + public void testSociableCycleOfLengthFive() { + assertTrue(SociableNumber.isSociable(12496, 5)); + } + + @Test + public void testAmicableNumbersAreSociableOfLengthTwo() { + assertTrue(SociableNumber.isSociable(220, 2)); + assertTrue(SociableNumber.isSociable(284, 2)); + } + + @Test + public void testNonSociableNumbers() { + assertFalse(SociableNumber.isSociable(12, 5)); + assertFalse(SociableNumber.isSociable(10, 3)); + } + + @Test + public void testEarlyCycleReturn() { + // 220 has cycle length 2; requesting a different length should return + // false because it returns to the start too early. + assertFalse(SociableNumber.isSociable(220, 3)); + assertFalse(SociableNumber.isSociable(284, 4)); + assertFalse(SociableNumber.isSociable(12496, 3)); + } + + @Test + public void testInvalidInputs() { + assertFalse(SociableNumber.isSociable(0, 5)); + assertFalse(SociableNumber.isSociable(-1, 5)); + assertFalse(SociableNumber.isSociable(220, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java b/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java index 2c10d2d14f3e..4716d389a4ca 100644 --- a/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java +++ b/src/test/java/com/thealgorithms/maths/StandardDeviationTest.java @@ -8,19 +8,19 @@ public class StandardDeviationTest { @Test void test1() { double[] t1 = new double[] {1, 1, 1, 1, 1}; - Assertions.assertEquals(StandardDeviation.stdDev(t1), 0.0); + Assertions.assertEquals(0.0, StandardDeviation.stdDev(t1)); } @Test void test2() { double[] t2 = new double[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - Assertions.assertEquals(StandardDeviation.stdDev(t2), 2.8722813232690143); + Assertions.assertEquals(2.8722813232690143, StandardDeviation.stdDev(t2)); } @Test void test3() { double[] t3 = new double[] {1.1, 8.5, 20.3, 2.4, 6.2}; - Assertions.assertEquals(StandardDeviation.stdDev(t3), 6.8308125431752265); + Assertions.assertEquals(6.8308125431752265, StandardDeviation.stdDev(t3)); } @Test @@ -32,6 +32,6 @@ void test4() { 100.00045, 56.7, }; - Assertions.assertEquals(StandardDeviation.stdDev(t4), 38.506117353865775); + Assertions.assertEquals(38.506117353865775, StandardDeviation.stdDev(t4)); } } diff --git a/src/test/java/com/thealgorithms/maths/StandardScoreTest.java b/src/test/java/com/thealgorithms/maths/StandardScoreTest.java index 436b1fd011c6..6858b87ad2c6 100644 --- a/src/test/java/com/thealgorithms/maths/StandardScoreTest.java +++ b/src/test/java/com/thealgorithms/maths/StandardScoreTest.java @@ -7,21 +7,21 @@ public class StandardScoreTest { @Test void test1() { - Assertions.assertEquals(StandardScore.zScore(2, 0, 5), 0.4); + Assertions.assertEquals(0.4, StandardScore.zScore(2, 0, 5)); } @Test void test2() { - Assertions.assertEquals(StandardScore.zScore(1, 1, 1), 0.0); + Assertions.assertEquals(0.0, StandardScore.zScore(1, 1, 1)); } @Test void test3() { - Assertions.assertEquals(StandardScore.zScore(2.5, 1.8, 0.7), 1.0); + Assertions.assertEquals(1.0, StandardScore.zScore(2.5, 1.8, 0.7)); } @Test void test4() { - Assertions.assertEquals(StandardScore.zScore(8.9, 3, 4.2), 1.4047619047619049); + Assertions.assertEquals(1.4047619047619049, StandardScore.zScore(8.9, 3, 4.2)); } } diff --git a/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java index 834fe61a049e..02b3f614ca9a 100644 --- a/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java +++ b/src/test/java/com/thealgorithms/maths/SumOfSquaresTest.java @@ -1,13 +1,12 @@ package com.thealgorithms.maths; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; /** * Test class for SumOfSquares - * - * @author BEASTSHRIRAM */ class SumOfSquaresTest { @@ -65,4 +64,11 @@ void testEdgeCases() { // Test edge case assertEquals(1, SumOfSquares.minSquares(0)); // 0^2 } + + @Test + void testNegativeInput() { + // Negative inputs should throw IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> SumOfSquares.minSquares(-1)); + assertThrows(IllegalArgumentException.class, () -> SumOfSquares.minSquares(-10)); + } } diff --git a/src/test/java/com/thealgorithms/maths/VolumeTest.java b/src/test/java/com/thealgorithms/maths/VolumeTest.java index 7cd0c6716147..c0b02d6ba28e 100644 --- a/src/test/java/com/thealgorithms/maths/VolumeTest.java +++ b/src/test/java/com/thealgorithms/maths/VolumeTest.java @@ -1,6 +1,6 @@ package com.thealgorithms.maths; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -10,30 +10,39 @@ public class VolumeTest { public void volume() { /* test cube */ - assertTrue(Volume.volumeCube(7) == 343.0); + assertEquals(343.0, Volume.volumeCube(7)); /* test cuboid */ - assertTrue(Volume.volumeCuboid(2, 5, 7) == 70.0); + assertEquals(70.0, Volume.volumeCuboid(2, 5, 7)); /* test sphere */ - assertTrue(Volume.volumeSphere(7) == 1436.7550402417319); + assertEquals(1436.7550402417319, Volume.volumeSphere(7)); /* test cylinder */ - assertTrue(Volume.volumeCylinder(3, 7) == 197.92033717615698); + assertEquals(197.92033717615698, Volume.volumeCylinder(3, 7)); /* test hemisphere */ - assertTrue(Volume.volumeHemisphere(7) == 718.3775201208659); + assertEquals(718.3775201208659, Volume.volumeHemisphere(7)); /* test cone */ - assertTrue(Volume.volumeCone(3, 7) == 65.97344572538566); + assertEquals(65.97344572538566, Volume.volumeCone(3, 7)); /* test prism */ - assertTrue(Volume.volumePrism(10, 2) == 20.0); + assertEquals(20.0, Volume.volumePrism(10, 2)); /* test pyramid */ - assertTrue(Volume.volumePyramid(10, 3) == 10.0); + assertEquals(10.0, Volume.volumePyramid(10, 3)); /* test frustum */ - assertTrue(Volume.volumeFrustumOfCone(3, 5, 7) == 359.188760060433); + assertEquals(359.188760060433, Volume.volumeFrustumOfCone(3, 5, 7)); + + /* test pyramid frustum */ + assertEquals(140.0, Volume.volumeFrustumOfPyramid(6, 24, 10)); + + /* test torus */ + assertEquals(39.47841760435743, Volume.volumeTorus(2, 1)); + + /* test ellipsoid */ + assertEquals(25.1327412287183459, Volume.volumeEllipsoid(3, 2, 1)); } } diff --git a/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java b/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java index 734d02477ba2..98ac29e81a5b 100644 --- a/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java +++ b/src/test/java/com/thealgorithms/maths/prime/MobiusFunctionTest.java @@ -5,6 +5,8 @@ import com.thealgorithms.maths.Prime.MobiusFunction; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class MobiusFunctionTest { @@ -152,4 +154,15 @@ void testMobiusFunction() { assertEquals(expectedValue, actualValue); } } + + /** + * Large inputs whose smallest square divisor test used to overflow, most notably + * {@code Integer.MAX_VALUE}, whose square wraps around to 1 and made every number look like it + * had a squared prime factor. + */ + @ParameterizedTest + @CsvSource({"2147483647, -1", "2147483646, 0", "2147483645, -1", "2147483644, 0", "2147483629, -1", "2147395600, 0", "1073741824, 0", "1073741789, -1", "999999937, -1", "999999999, 0", "2146689000, 0"}) + void testMobiusForLargeNumbers(int number, int expected) { + assertEquals(expected, MobiusFunction.mobius(number)); + } } diff --git a/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java b/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java new file mode 100644 index 000000000000..adc8fb717e57 --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/QRDecompositionTest.java @@ -0,0 +1,115 @@ +package com.thealgorithms.matrix; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class QRDecompositionTest { + + private static final double DELTA = 1e-9; + + @Test + public void testQRDecomposition2x2() { + double[][] matrix = {{12, -51}, {6, 167}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + double[][] reconstructed = multiplyMatrices(q, r); + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], reconstructed[i], DELTA); + } + } + + @Test + public void testQRDecomposition3x3() { + double[][] matrix = {{1, 1, 0}, {1, 0, 1}, {0, 1, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + double[][] reconstructed = multiplyMatrices(q, r); + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], reconstructed[i], DELTA); + } + } + + @Test + public void testQROrthogonalColumns() { + double[][] matrix = {{1, 1, 0}, {1, 0, 1}, {0, 1, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + + for (int i = 0; i < q[0].length; i++) { + for (int j = i; j < q[0].length; j++) { + double dot = 0; + for (int k = 0; k < q.length; k++) { + dot += q[k][i] * q[k][j]; + } + if (i == j) { + assertArrayEquals(new double[] {1.0}, new double[] {dot}, DELTA); + } else { + assertArrayEquals(new double[] {0.0}, new double[] {dot}, DELTA); + } + } + } + } + + @Test + public void testRIsUpperTriangular() { + double[][] matrix = {{12, -51}, {6, 167}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] r = qr.getR(); + + for (int i = 1; i < r.length; i++) { + for (int j = 0; j < i; j++) { + assertArrayEquals(new double[] {0.0}, new double[] {r[i][j]}, DELTA); + } + } + } + + @Test + public void testQRDecompositionIdentityMatrix() { + double[][] matrix = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; + QRDecomposition.QR qr = QRDecomposition.decompose(matrix); + double[][] q = qr.getQ(); + double[][] r = qr.getR(); + + for (int i = 0; i < matrix.length; i++) { + assertArrayEquals(matrix[i], q[i], DELTA); + assertArrayEquals(matrix[i], r[i], DELTA); + } + } + + @Test + public void testQRDecompositionRankDeficientThrows() { + double[][] matrix = {{1, 2}, {2, 4}}; + assertThrows(ArithmeticException.class, () -> QRDecomposition.decompose(matrix)); + } + + @Test + public void testQRDecompositionNullMatrixThrows() { + assertThrows(IllegalArgumentException.class, () -> QRDecomposition.decompose(null)); + } + + @Test + public void testQRDecompositionEmptyMatrixThrows() { + assertThrows(IllegalArgumentException.class, () -> QRDecomposition.decompose(new double[0][0])); + } + + private static double[][] multiplyMatrices(double[][] a, double[][] b) { + int m = a.length; + int n = b[0].length; + int k = a[0].length; + double[][] result = new double[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + for (int p = 0; p < k; p++) { + result[i][j] += a[i][p] * b[p][j]; + } + } + } + return result; + } +} diff --git a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java index f41953035846..c4a74af0ba8b 100644 --- a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java @@ -17,7 +17,7 @@ public class MedianOfRunningArrayTest { public void testWhenInvalidInoutProvidedShouldThrowException() { var stream = new MedianOfRunningArrayInteger(); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, stream::getMedian); - assertEquals(exception.getMessage(), EXCEPTION_MESSAGE); + assertEquals(EXCEPTION_MESSAGE, exception.getMessage()); } @Test diff --git a/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java b/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java index 915b83e376b6..c1adafa18d9f 100644 --- a/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/ShuffleArrayTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.misc; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,7 +68,7 @@ void testShuffleRetainsElements() { ShuffleArray.shuffle(arr); // Check that the shuffled array contains the same elements - assertTrue(arr.length == 5); + assertEquals(5, arr.length); for (int i = 1; i <= 5; i++) { assertTrue(contains(arr, i)); } diff --git a/src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java b/src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java deleted file mode 100644 index 3b657e441b1c..000000000000 --- a/src/test/java/com/thealgorithms/others/NewManShanksPrimeTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.thealgorithms.others; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.thealgorithms.dynamicprogramming.NewManShanksPrime; -import org.junit.jupiter.api.Test; - -public class NewManShanksPrimeTest { - - @Test - void testOne() { - assertTrue(NewManShanksPrime.nthManShanksPrime(1, 1)); - } - - @Test - void testTwo() { - assertTrue(NewManShanksPrime.nthManShanksPrime(2, 3)); - } - - @Test - void testThree() { - assertTrue(NewManShanksPrime.nthManShanksPrime(3, 7)); - } - - @Test - void testFour() { - assertTrue(NewManShanksPrime.nthManShanksPrime(4, 17)); - } - - @Test - void testFive() { - assertTrue(NewManShanksPrime.nthManShanksPrime(5, 41)); - } - - @Test - void testSix() { - assertTrue(NewManShanksPrime.nthManShanksPrime(6, 99)); - } - - @Test - void testSeven() { - assertTrue(NewManShanksPrime.nthManShanksPrime(7, 239)); - } - - @Test - void testEight() { - assertTrue(NewManShanksPrime.nthManShanksPrime(8, 577)); - } -} diff --git a/src/test/java/com/thealgorithms/others/PasswordGenTest.java b/src/test/java/com/thealgorithms/others/PasswordGenTest.java index 76492556e75f..4dcdf6b9cf4f 100644 --- a/src/test/java/com/thealgorithms/others/PasswordGenTest.java +++ b/src/test/java/com/thealgorithms/others/PasswordGenTest.java @@ -17,7 +17,7 @@ public void failGenerationWithSameMinMaxLengthTest() { @Test public void generateOneCharacterPassword() { String tempPassword = PasswordGen.generatePassword(1, 2); - assertTrue(tempPassword.length() == 1); + assertEquals(1, tempPassword.length()); } @Test diff --git a/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java b/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java deleted file mode 100644 index 669f928cd247..000000000000 --- a/src/test/java/com/thealgorithms/others/cn/HammingDistanceTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.thealgorithms.others.cn; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -public class HammingDistanceTest { - @Test - public void checkForDifferentBits() { - int answer = HammingDistance.compute("000", "011"); - Assertions.assertThat(answer).isEqualTo(2); - } - - /* - - 1 0 1 0 1 - 1 1 1 1 0 - ---------- - 0 1 0 1 1 - - - */ - @Test - public void checkForDifferentBitsLength() { - int answer = HammingDistance.compute("10101", "11110"); - Assertions.assertThat(answer).isEqualTo(3); - } - - @Test - public void checkForSameBits() { - String someBits = "111"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForLongDataBits() { - int answer = HammingDistance.compute("10010101101010000100110100", "00110100001011001100110101"); - Assertions.assertThat(answer).isEqualTo(7); - } - - @Test - public void mismatchDataBits() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("100010", "00011"); }); - - Assertions.assertThat(ex.getMessage()).contains("must have the same length"); - } - - @Test - public void mismatchDataBits2() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("1", "11"); }); - - Assertions.assertThat(ex.getMessage()).contains("must have the same length"); - } - - @Test - public void checkForLongDataBitsSame() { - String someBits = "10010101101010000100110100"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForEmptyInput() { - String someBits = ""; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void checkForInputOfLength1() { - String someBits = "0"; - int answer = HammingDistance.compute(someBits, someBits); - Assertions.assertThat(answer).isEqualTo(0); - } - - @Test - public void computeThrowsExceptionWhenInputsAreNotBitStrs() { - Exception ex = org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> { HammingDistance.compute("1A", "11"); }); - - Assertions.assertThat(ex.getMessage()).contains("must be a binary string"); - } -} diff --git a/src/test/java/com/thealgorithms/physics/RelativityTest.java b/src/test/java/com/thealgorithms/physics/RelativityTest.java new file mode 100644 index 000000000000..44c17bdbd40f --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/RelativityTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Relativity utility class. + */ +final class RelativityTest { + + // A small tolerance (delta) for comparing floating-point numbers + private static final double DELTA = 1e-6; + private static final double C = Relativity.SPEED_OF_LIGHT; + + @Test + @DisplayName("Test the gamma parameter") + void testGamma() { + double myGamma = Relativity.gamma(0.6 * C); + assertEquals(1.25, myGamma, DELTA); + } + + @Test + @DisplayName("Test the length contraction") + void testLengthContraction() { + double myLength = Relativity.lengthContraction(5.0, 0.8 * C); + assertEquals(3.0, myLength, DELTA); + } + + @Test + @DisplayName("Test the time dilation") + void testTimeDilation() { + double myTime = Relativity.timeDilation(4.0, 0.6 * C); + assertEquals(5.0, myTime, DELTA); + } + + @Test + @DisplayName("Test the velocity addition in the same direction") + void testVelocityAdditionSameDirection() { + double myVelocity = Relativity.velocityAddition(0.8 * C, 0.75 * C); + assertEquals(0.125 * C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test the velocity addition in different directions") + void testVelocityAdditionDifferentDirections() { + double myVelocity = Relativity.velocityAddition(0.8 * C, -0.75 * C); + assertEquals(0.96875 * C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test the velocity addition with the speed of light") + void testVelocityAdditionWithSpeedOfLight() { + double myVelocity = Relativity.velocityAddition(C, 0.7 * C); + assertEquals(C, myVelocity, DELTA); + } + + @Test + @DisplayName("Test invalid inputs throw exception") + void testInvalidOrbitalVelocityInputs() { + assertThrows(IllegalArgumentException.class, () -> Relativity.gamma(1.2 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.gamma(-C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.lengthContraction(-1.0, 0.6 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.lengthContraction(1.0, 1.5 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.timeDilation(-5.0, -0.8 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.timeDilation(5.0, C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(0.3 * C, -C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(1.4 * C, 0.2 * C)); + assertThrows(IllegalArgumentException.class, () -> Relativity.velocityAddition(-0.4 * C, 1.2 * C)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java b/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java new file mode 100644 index 000000000000..88a480f25f1a --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/DifferenceArrayTest.java @@ -0,0 +1,110 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DifferenceArrayTest { + + @Test + void testStandardRangeUpdate() { + int[] input = {10, 20, 30, 40, 50}; + DifferenceArray da = new DifferenceArray(input); + + da.update(1, 3, 5); + + long[] expected = {10, 25, 35, 45, 50}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testMultipleOverlappingUpdates() { + int[] input = {10, 10, 10, 10, 10}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 2, 10); + da.update(2, 4, 20); + + long[] expected = {20, 20, 40, 30, 30}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testIntegerOverflowSafety() { + int[] input = {Integer.MAX_VALUE, 100}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 0, 100); + + long[] result = da.getResultArray(); + long expectedVal = (long) Integer.MAX_VALUE + 100; + + assertEquals(expectedVal, result[0]); + } + + @Test + void testFullRangeUpdate() { + int[] input = {1, 2, 3}; + DifferenceArray da = new DifferenceArray(input); + + da.update(0, 2, 100); + + long[] expected = {101, 102, 103}; + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testBoundaryWriteOptimization() { + int[] input = {5, 5}; + DifferenceArray da = new DifferenceArray(input); + + da.update(1, 1, 5); + + long[] expected = {5, 10}; + + assertArrayEquals(expected, da.getResultArray()); + } + + @Test + void testLargeMassiveUpdate() { + int[] input = {0}; + DifferenceArray da = new DifferenceArray(input); + + int iterations = 100000; + for (int i = 0; i < iterations; i++) { + da.update(0, 0, 1); + } + + assertEquals(100000L, da.getResultArray()[0]); + } + + @Test + void testNullInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new DifferenceArray(null)); + } + + @Test + void testEmptyInputThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new DifferenceArray(new int[] {})); + } + + @Test + void testInvalidRangeNegativeIndex() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(-1, 1, 5)); + } + + @Test + void testInvalidRangeOutOfBounds() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(0, 3, 5)); + } + + @Test + void testInvalidRangeStartGreaterThanEnd() { + DifferenceArray da = new DifferenceArray(new int[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, () -> da.update(2, 1, 5)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java b/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java new file mode 100644 index 000000000000..87feff859356 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java @@ -0,0 +1,92 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PrefixSum2DTest { + + @Test + @DisplayName("Test basic 3x3 square matrix") + void testStandardSquare() { + int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of top-left 2x2: {1,2, 4,5} -> 12 + assertEquals(12L, ps.sumRegion(0, 0, 1, 1)); + // Sum of bottom-right 2x2: {5,6, 8,9} -> 28 + assertEquals(28L, ps.sumRegion(1, 1, 2, 2)); + // Full matrix -> 45 + assertEquals(45L, ps.sumRegion(0, 0, 2, 2)); + } + + @Test + @DisplayName("Test rectangular matrix (more cols than rows)") + void testRectangularWide() { + int[][] matrix = {{1, 1, 1, 1}, {2, 2, 2, 2}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of first 3 columns of both rows -> (1*3) + (2*3) = 9 + assertEquals(9L, ps.sumRegion(0, 0, 1, 2)); + } + + @Test + @DisplayName("Test rectangular matrix (more rows than cols)") + void testRectangularTall() { + int[][] matrix = {{1}, {2}, {3}, {4}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Sum of middle two elements -> 2+3 = 5 + assertEquals(5L, ps.sumRegion(1, 0, 2, 0)); + } + + @Test + @DisplayName("Test single element matrix") + void testSingleElement() { + int[][] matrix = {{100}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + assertEquals(100L, ps.sumRegion(0, 0, 0, 0)); + } + + @Test + @DisplayName("Test large numbers for overflow (Integer -> Long)") + void testLargeNumbers() { + // 2 billion. Two of these sum to > MAX_INT + int val = 2_000_000_000; + int[][] matrix = {{val, val}, {val, val}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // 4 * 2B = 8 Billion + assertEquals(8_000_000_000L, ps.sumRegion(0, 0, 1, 1)); + } + + @Test + @DisplayName("Test invalid inputs") + void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(null)); + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {})); // empty + assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {{}})); // empty row + } + + @Test + @DisplayName("Test invalid query ranges") + void testInvalidRanges() { + int[][] matrix = {{1, 2}, {3, 4}}; + PrefixSum2D ps = new PrefixSum2D(matrix); + + // Negative indices + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(-1, 0, 0, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, -1, 0, 0)); + + // Out of bounds + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 2, 0)); // row2 too big + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 0, 2)); // col2 too big + + // Inverted ranges (start > end) + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(1, 0, 0, 0)); // row1 > row2 + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 1, 0, 0)); // col1 > col2 + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java b/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java new file mode 100644 index 000000000000..a421b62e9306 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java @@ -0,0 +1,80 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PrefixSumTest { + + @Test + @DisplayName("Test basic sum with positive integers") + void testStandardCase() { + int[] input = {1, 2, 3, 4, 5}; + PrefixSum ps = new PrefixSum(input); + + // Sum of range [0, 4] -> 15 + assertEquals(15L, ps.sumRange(0, 4)); + + // Sum of range [1, 3] -> 9 + assertEquals(9L, ps.sumRange(1, 3)); + } + + @Test + @DisplayName("Test array with negative numbers and zeros") + void testNegativeAndZeros() { + int[] input = {-2, 0, 3, -5, 2, -1}; + PrefixSum ps = new PrefixSum(input); + + assertEquals(1L, ps.sumRange(0, 2)); + assertEquals(-1L, ps.sumRange(2, 5)); + assertEquals(0L, ps.sumRange(1, 1)); + } + + @Test + @DisplayName("Test with large integers to verify overflow handling") + void testLargeNumbers() { + // Two values that fit in int, but their sum exceeds Integer.MAX_VALUE + // Integer.MAX_VALUE is approx 2.14 billion. + int val = 2_000_000_000; + int[] input = {val, val, val}; + PrefixSum ps = new PrefixSum(input); + + // Sum of three 2 billion values is 6 billion (fits in long, overflows int) + assertEquals(6_000_000_000L, ps.sumRange(0, 2)); + } + + @Test + @DisplayName("Test single element array") + void testSingleElement() { + int[] input = {42}; + PrefixSum ps = new PrefixSum(input); + assertEquals(42L, ps.sumRange(0, 0)); + } + + @Test + @DisplayName("Test constructor with null input") + void testNullInput() { + assertThrows(IllegalArgumentException.class, () -> new PrefixSum(null)); + } + + @Test + @DisplayName("Test empty array behavior") + void testEmptyArray() { + int[] input = {}; + PrefixSum ps = new PrefixSum(input); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 0)); + } + + @Test + @DisplayName("Test invalid range indices") + void testInvalidIndices() { + int[] input = {10, 20, 30}; + PrefixSum ps = new PrefixSum(input); + + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(-1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 3)); + assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(2, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java b/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java new file mode 100644 index 000000000000..12072318ac74 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/RangeSumQueryTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link RangeSumQuery}. + */ +class RangeSumQueryTest { + + @Test + void testBasicExample() { + int[] nums = {1, 2, 3, 4, 5}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(6, RangeSumQuery.sumRange(prefixSum, 0, 2)); // 1+2+3 + assertEquals(9, RangeSumQuery.sumRange(prefixSum, 1, 3)); // 2+3+4 + assertEquals(15, RangeSumQuery.sumRange(prefixSum, 0, 4)); // 1+2+3+4+5 + assertEquals(12, RangeSumQuery.sumRange(prefixSum, 2, 4)); // 3+4+5 + } + + @Test + void testSingleElement() { + int[] nums = {7}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(7, RangeSumQuery.sumRange(prefixSum, 0, 0)); + } + + @Test + void testAllZeros() { + int[] nums = {0, 0, 0, 0}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(0, RangeSumQuery.sumRange(prefixSum, 0, 3)); + assertEquals(0, RangeSumQuery.sumRange(prefixSum, 1, 2)); + } + + @Test + void testNegativeNumbers() { + int[] nums = {-1, 2, -3, 4}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertEquals(-2, RangeSumQuery.sumRange(prefixSum, 0, 2)); // -1+2-3 + assertEquals(3, RangeSumQuery.sumRange(prefixSum, 1, 3)); // 2-3+4 + } + + @Test + void testEmptyArrayThrowsException() { + int[] nums = {}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 0, 0)); + } + + @Test + void testNullArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.buildPrefixSum(null)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(null, 0, 0)); + } + + @Test + void testInvalidIndicesThrowsException() { + int[] nums = {1, 2, 3}; + int[] prefixSum = RangeSumQuery.buildPrefixSum(nums); + + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, -1, 2)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 1, 5)); + assertThrows(IllegalArgumentException.class, () -> RangeSumQuery.sumRange(prefixSum, 2, 1)); + } +} diff --git a/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java b/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java new file mode 100644 index 000000000000..68f85b713046 --- /dev/null +++ b/src/test/java/com/thealgorithms/prefixsum/SubarraySumEqualskTest.java @@ -0,0 +1,59 @@ +package com.thealgorithms.prefixsum; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SubarraySumEqualsK}. + */ +class SubarraySumEqualsKTest { + + @Test + void testBasicExample() { + int[] nums = {1, 1, 1}; + int k = 2; + assertEquals(2, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testWithNegativeNumbers() { + int[] nums = {1, -1, 0}; + int k = 0; + assertEquals(3, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testSingleElementEqualToK() { + int[] nums = {5}; + int k = 5; + assertEquals(1, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testSingleElementNotEqualToK() { + int[] nums = {5}; + int k = 3; + assertEquals(0, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testAllZeros() { + int[] nums = {0, 0, 0}; + int k = 0; + assertEquals(6, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testEmptyArray() { + int[] nums = {}; + int k = 0; + assertEquals(0, SubarraySumEqualsK.countSubarrays(nums, k)); + } + + @Test + void testNullArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> SubarraySumEqualsK.countSubarrays(null, 0)); + } +} diff --git a/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java b/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java index 42669eb03bb4..f0a2686d3e4b 100644 --- a/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java +++ b/src/test/java/com/thealgorithms/puzzlesandgames/TowerOfHanoiTest.java @@ -1,14 +1,31 @@ package com.thealgorithms.puzzlesandgames; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class TowerOfHanoiTest { + @ParameterizedTest + @MethodSource("diskCountAndMoveCount") + void testMoveCountMatchesFormula(int disks, int expectedMoves) { + List result = new ArrayList<>(); + TowerOfHanoi.shift(disks, "A", "B", "C", result); + assertEquals(expectedMoves, result.size()); + } + + private static Stream diskCountAndMoveCount() { + return Stream.of(Arguments.of(1, 1), Arguments.of(2, 3), Arguments.of(3, 7), Arguments.of(4, 15), Arguments.of(5, 31), Arguments.of(10, 1023)); + } + @Test public void testHanoiWithOneDisc() { List result = new ArrayList<>(); @@ -39,6 +56,15 @@ public void testHanoiWithThreeDiscs() { assertEquals(expected, result); } + @Test + public void testHanoiWithDifferentPoles() { + List result = new ArrayList<>(); + TowerOfHanoi.shift(2, "X", "Y", "Z", result); + + List expected = List.of("Move 1 from X to Y", "Move 2 from X to Z", "Move 1 from Y to Z"); + assertEquals(expected, result); + } + @Test public void testHanoiWithZeroDiscs() { List result = new ArrayList<>(); @@ -47,4 +73,10 @@ public void testHanoiWithZeroDiscs() { // There should be no moves if there are 0 discs assertTrue(result.isEmpty()); } + + @Test + public void testHanoiWithNegativeDiscsThrows() { + List result = new ArrayList<>(); + assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.shift(-1, "Pole1", "Pole2", "Pole3", result)); + } } diff --git a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java index 18f0afc6a0a6..dec2c86de9c7 100644 --- a/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java +++ b/src/test/java/com/thealgorithms/searches/BinarySearch2dArrayTest.java @@ -117,7 +117,7 @@ public void binarySearch2dArrayTestTargetInMiddle() { int target = 8; // Assert that the requirement, that the target is in the middle row and middle column, is // fulfilled. - assertEquals(arr[arr.length / 2][arr[0].length / 2], target); + assertEquals(target, arr[arr.length / 2][arr[0].length / 2]); int[] ans = BinarySearch2dArray.binarySearch(arr, target); System.out.println(Arrays.toString(ans)); assertEquals(1, ans[0]); @@ -135,8 +135,8 @@ public void binarySearch2dArrayTestTargetAboveMiddleRowInMiddleColumn() { // Assert that the requirement, that he target is in the middle column, // in an array with an even number of columns, and on the row "above" the middle row. - assertEquals(arr[0].length % 2, 0); - assertEquals(arr[arr.length / 2 - 1][arr[0].length / 2], target); + assertEquals(0, arr[0].length % 2); + assertEquals(target, arr[arr.length / 2 - 1][arr[0].length / 2]); int[] ans = BinarySearch2dArray.binarySearch(arr, target); System.out.println(Arrays.toString(ans)); assertEquals(0, ans[0]); diff --git a/src/test/java/com/thealgorithms/searches/BinarySearchTest.java b/src/test/java/com/thealgorithms/searches/BinarySearchTest.java index bd4620a7fa7d..00bed165734e 100644 --- a/src/test/java/com/thealgorithms/searches/BinarySearchTest.java +++ b/src/test/java/com/thealgorithms/searches/BinarySearchTest.java @@ -105,4 +105,90 @@ void testBinarySearchLargeArray() { int expectedIndex = 9999; // Index of the last element assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * Test for binary search with null array. + */ + @Test + void testBinarySearchNullArray() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = null; + int key = 5; // Key to search + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in a null array."); + } + + /** + * Test for binary search with duplicate elements. + */ + @Test + void testBinarySearchWithDuplicates() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {1, 2, 2, 2, 3}; + int key = 2; // Element present multiple times + + int result = binarySearch.find(array, key); + assertEquals(2, array[result], "The returned index should contain the searched element."); + } + + /** + * Test for binary search where all elements are the same. + */ + @Test + void testBinarySearchAllElementsSame() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {5, 5, 5, 5, 5}; + int key = 5; // All elements match + + int result = binarySearch.find(array, key); + assertEquals(5, array[result], "The returned index should contain the searched element."); + } + + /** + * Test for binary search with negative numbers. + */ + @Test + void testBinarySearchNegativeNumbers() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {-10, -5, 0, 5, 10}; + int key = -5; // Element present + int expectedIndex = 1; // Index of the element + assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the element should be 1."); + } + + /** + * Test for binary search when key is smaller than all elements. + */ + @Test + void testBinarySearchKeySmallerThanAll() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {10, 20, 30}; + int key = 5; // Smaller than all elements + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for binary search when key is larger than all elements. + */ + @Test + void testBinarySearchKeyLargerThanAll() { + BinarySearch binarySearch = new BinarySearch(); + Integer[] array = {10, 20, 30}; + int key = 40; // Larger than all elements + int expectedIndex = -1; // Key not found + assertEquals(expectedIndex, binarySearch.find(array, key), "The element should not be found in the array."); + } + + /** + * Test for binary search with String array. + */ + @Test + void testBinarySearchStrings() { + BinarySearch binarySearch = new BinarySearch(); + String[] array = {"apple", "banana", "cherry", "date"}; + String key = "cherry"; // Element present + int expectedIndex = 2; // Index of the element + assertEquals(expectedIndex, binarySearch.find(array, key), "The index of the element should be 2."); + } } diff --git a/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java b/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java index c84da531e8a4..c6b07ca2b4d5 100644 --- a/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/ExponentialSearchTest.java @@ -81,4 +81,46 @@ void testExponentialSearchLargeArray() { int expectedIndex = 9999; assertEquals(expectedIndex, exponentialSearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * An element sitting exactly on the doubling boundary used to be reported as missing, because + * the binary search was handed {@code range} as its exclusive upper bound instead of + * {@code range + 1}. + */ + @Test + void testExponentialSearchElementOnRangeBoundary() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {-25, -9, 8, 21}; + assertEquals(2, exponentialSearch.find(array, 8), "The index of the found element should be 2."); + } + + /** + * Every element must be found regardless of the array length. + */ + @Test + void testExponentialSearchFindsEveryElement() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, exponentialSearch.find(array, i * 2), "Element at index " + i + " should be found for length " + length + "."); + } + } + } + + /** + * A missing key has to yield -1 rather than the negative insertion point that + * {@link java.util.Arrays#binarySearch} returns. + */ + @Test + void testExponentialSearchNotFoundReturnsMinusOne() { + ExponentialSearch exponentialSearch = new ExponentialSearch(); + Integer[] array = {1, 3, 5, 7, 9, 11}; + assertEquals(-1, exponentialSearch.find(array, 4), "A key inside the range but absent should give -1."); + assertEquals(-1, exponentialSearch.find(array, 0), "A key below the minimum should give -1."); + assertEquals(-1, exponentialSearch.find(array, 12), "A key above the maximum should give -1."); + } } diff --git a/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java b/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java index 801c33b1d09a..04a270864223 100644 --- a/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/FibonacciSearchTest.java @@ -121,4 +121,53 @@ void testFibonacciSearchLargeArray() { int expectedIndex = 9999; assertEquals(expectedIndex, fibonacciSearch.find(array, key), "The index of the last element should be 9999."); } + + /** + * A key greater than every element used to throw {@link ArrayIndexOutOfBoundsException}, + * because the final probe read {@code array[offset + 1]} without checking the bound. + */ + @Test + void testFibonacciSearchKeyGreaterThanLastElement() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i; + } + assertEquals(-1, fibonacciSearch.find(array, length), "A key above the maximum should not be found for length " + length + "."); + } + } + + /** + * The final probe used reference equality, so a key that is equal but not identical to the + * stored element was reported as missing. Values above 127 are outside the {@link Integer} + * cache and therefore are not the same object as the boxed array element. + */ + @Test + void testFibonacciSearchFindsEqualButNotIdenticalKey() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + Integer[] array = {10, 20, 300}; + assertEquals(2, fibonacciSearch.find(array, Integer.valueOf(300)), "The index of the found element should be 2."); + + String[] words = {"a", "b", "c"}; + String equalButDistinct = new StringBuilder("c").toString(); + assertEquals(2, fibonacciSearch.find(words, equalButDistinct), "The index of the found element should be 2."); + } + + /** + * Every element must be found regardless of the array length. + */ + @Test + void testFibonacciSearchFindsEveryElement() { + FibonacciSearch fibonacciSearch = new FibonacciSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = 1000 + i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, fibonacciSearch.find(array, Integer.valueOf(1000 + i * 2)), "Element at index " + i + " should be found for length " + length + "."); + } + } + } } diff --git a/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java b/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java index b3b7e7ef129c..b7ef64125da8 100644 --- a/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/InterpolationSearchTest.java @@ -87,4 +87,15 @@ void testInterpolationSearchLargeNonUniformArray() { int key = 21; // Present in the array assertEquals(6, interpolationSearch.find(array, key), "The index of the found element should be 6."); } + + /** + * Test for interpolation search with specific sorted arrays that previously caused division by zero. + */ + @Test + void testInterpolationSearchDivisionByZeroEdgeCases() { + InterpolationSearch interpolationSearch = new InterpolationSearch(); + assertEquals(3, interpolationSearch.find(new int[] {0, 0, 0, 2}, 2)); + assertEquals(0, interpolationSearch.find(new int[] {2, 2, 2, 2}, 2)); + assertEquals(3, interpolationSearch.find(new int[] {0, 1, 2, 4}, 4)); + } } diff --git a/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java index b2e121ac1ba0..f291610b298b 100644 --- a/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java +++ b/src/test/java/com/thealgorithms/searches/IterativeBinarySearchTest.java @@ -87,6 +87,26 @@ void testBinarySearchEmptyArray() { assertEquals(-1, binarySearch.find(array, key), "The element should not be found in an empty array."); } + /** + * Test for binary search with a null array. + */ + @Test + void testBinarySearchNullArray() { + IterativeBinarySearch binarySearch = new IterativeBinarySearch(); + Integer key = 1; + assertEquals(-1, binarySearch.find(null, key), "The element should not be found in a null array."); + } + + /** + * Test for binary search with a null key. + */ + @Test + void testBinarySearchNullKey() { + IterativeBinarySearch binarySearch = new IterativeBinarySearch(); + Integer[] array = {1, 2, 4, 8, 16}; + assertEquals(-1, binarySearch.find(array, null), "A null search key should return -1."); + } + /** * Test for binary search on a large array. */ diff --git a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java index 3fa319b66a41..a5ce93b8a3af 100644 --- a/src/test/java/com/thealgorithms/searches/JumpSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/JumpSearchTest.java @@ -2,7 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; /** * Unit tests for the JumpSearch class. @@ -91,4 +93,50 @@ void testJumpSearchLargeArrayNotFound() { Integer key = 999; // Key not present assertEquals(-1, jumpSearch.find(array, key), "The element should not be found in the array."); } + + /** + * A key greater than every element used to make the jumping loop spin forever, because the + * cursor was clamped to the last index and therefore stopped advancing. + */ + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void testJumpSearchKeyGreaterThanLastElement() { + JumpSearch jumpSearch = new JumpSearch(); + Integer[] array = {1, 2, 3, 4}; + assertEquals(-1, jumpSearch.find(array, 5), "A key above the maximum should not be found."); + } + + /** + * The same regression across several lengths, since the jump size depends on the array length. + */ + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void testJumpSearchKeyGreaterThanLastElementForEveryLength() { + JumpSearch jumpSearch = new JumpSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i; + } + assertEquals(-1, jumpSearch.find(array, length), "A key above the maximum should not be found for length " + length + "."); + } + } + + /** + * Every element must be found regardless of the array length, including the ones that sit + * exactly on a jump boundary. + */ + @Test + void testJumpSearchFindsEveryElement() { + JumpSearch jumpSearch = new JumpSearch(); + for (int length = 1; length <= 50; length++) { + Integer[] array = new Integer[length]; + for (int i = 0; i < length; i++) { + array[i] = i * 2; + } + for (int i = 0; i < length; i++) { + assertEquals(i, jumpSearch.find(array, i * 2), "Element at index " + i + " should be found for length " + length + "."); + } + } + } } diff --git a/src/test/java/com/thealgorithms/searches/KMPSearchTest.java b/src/test/java/com/thealgorithms/searches/KMPSearchTest.java index cb804ac6a6a3..216c5fcd7d2c 100644 --- a/src/test/java/com/thealgorithms/searches/KMPSearchTest.java +++ b/src/test/java/com/thealgorithms/searches/KMPSearchTest.java @@ -14,7 +14,7 @@ public void kmpSearchTestLast() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 10); + assertEquals(10, value); } @Test @@ -25,7 +25,7 @@ public void kmpSearchTestFront() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 0); + assertEquals(0, value); } @Test @@ -36,7 +36,7 @@ public void kmpSearchTestMiddle() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 4); + assertEquals(4, value); } @Test @@ -47,7 +47,7 @@ public void kmpSearchTestNotFound() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, 4); + assertEquals(4, value); } @Test @@ -58,6 +58,6 @@ public void kmpSearchTest4() { KMPSearch kmpSearch = new KMPSearch(); int value = kmpSearch.kmpSearch(pat, txt); System.out.println(value); - assertEquals(value, -1); + assertEquals(-1, value); } } diff --git a/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java b/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java index 534c2a4487b2..c0d82489209f 100644 --- a/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java +++ b/src/test/java/com/thealgorithms/searches/LinearSearchThreadTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Random; import org.junit.jupiter.api.Test; class LinearSearchThreadTest { @@ -62,10 +63,11 @@ void testSearcherEmptySegment() throws InterruptedException { void testSearcherRandomNumbers() throws InterruptedException { int size = 200; int[] array = new int[size]; + Random random = new Random(); for (int i = 0; i < size; i++) { - array[i] = (int) (Math.random() * 100); + array[i] = random.nextInt(100); } - int target = array[(int) (Math.random() * size)]; // Randomly select a target that is present + final int target = array[random.nextInt(size)]; // Randomly select a target that is present Searcher searcher = new Searcher(array, 0, size, target); searcher.start(); searcher.join(); diff --git a/src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java deleted file mode 100644 index 6eab20f45467..000000000000 --- a/src/test/java/com/thealgorithms/searches/PerfectBinarySearchTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.thealgorithms.searches; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -/** - * @author D Sunil (https://github.com/sunilnitdgp) - * @see PerfectBinarySearch - */ -public class PerfectBinarySearchTest { - - @Test - public void testIntegerBinarySearch() { - Integer[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - PerfectBinarySearch binarySearch = new PerfectBinarySearch<>(); - - // Test cases for elements present in the array - assertEquals(0, binarySearch.find(array, 1)); // First element - assertEquals(4, binarySearch.find(array, 5)); // Middle element - assertEquals(9, binarySearch.find(array, 10)); // Last element - assertEquals(6, binarySearch.find(array, 7)); // Element in the middle - - // Test cases for elements not in the array - assertEquals(-1, binarySearch.find(array, 0)); // Element before the array - assertEquals(-1, binarySearch.find(array, 11)); // Element after the array - assertEquals(-1, binarySearch.find(array, 100)); // Element not in the array - } - - @Test - public void testStringBinarySearch() { - String[] array = {"apple", "banana", "cherry", "date", "fig"}; - PerfectBinarySearch binarySearch = new PerfectBinarySearch<>(); - - // Test cases for elements not in the array - assertEquals(-1, binarySearch.find(array, "apricot")); // Element not in the array - assertEquals(-1, binarySearch.find(array, "bananaa")); // Element not in the array - - // Test cases for elements present in the array - assertEquals(0, binarySearch.find(array, "apple")); // First element - assertEquals(2, binarySearch.find(array, "cherry")); // Middle element - assertEquals(4, binarySearch.find(array, "fig")); // Last element - } -} diff --git a/src/test/java/com/thealgorithms/searches/QuickSelectTest.java b/src/test/java/com/thealgorithms/searches/QuickSelectTest.java index cf160b0ff4b5..4c96be76861a 100644 --- a/src/test/java/com/thealgorithms/searches/QuickSelectTest.java +++ b/src/test/java/com/thealgorithms/searches/QuickSelectTest.java @@ -172,7 +172,7 @@ void quickSelect70thPercentileOfManyElements() { void quickSelectMedianOfThreeCharacters() { List elements = Arrays.asList('X', 'Z', 'Y'); char actual = QuickSelect.select(elements, 1); - assertEquals(actual, 'Y'); + assertEquals('Y', actual); } @Test diff --git a/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java new file mode 100644 index 000000000000..1e6ab4c37fcc --- /dev/null +++ b/src/test/java/com/thealgorithms/searches/RotatedBinarySearchTest.java @@ -0,0 +1,53 @@ +package com.thealgorithms.searches; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class RotatedBinarySearchTest { + + @Test + void shouldFindElementInRotatedArrayLeftSide() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7}; + assertEquals(2, search.find(array, 10)); + } + + @Test + void shouldFindElementInRotatedArrayRightSide() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7}; + assertEquals(6, search.find(array, 2)); + } + + @Test + void shouldFindElementInNotRotatedArray() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {1, 2, 3, 4, 5, 6, 7}; + assertEquals(4, search.find(array, 5)); + } + + @Test + void shouldReturnMinusOneWhenNotFound() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {4, 5, 6, 7, 0, 1, 2}; + assertEquals(-1, search.find(array, 3)); + } + + @Test + void shouldHandleWhenMiddleIsGreaterThanKeyInRightSortedHalf() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {6, 7, 0, 1, 2, 3, 4, 5}; + assertEquals(2, search.find(array, 0)); + } + + @Test + void shouldHandleDuplicates() { + RotatedBinarySearch search = new RotatedBinarySearch(); + Integer[] array = {2, 2, 2, 3, 4, 2}; + int index = search.find(array, 3); + assertTrue(index >= 0 && index < array.length); + assertEquals(3, array[index]); + } +} diff --git a/src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java deleted file mode 100644 index e2917733d1d9..000000000000 --- a/src/test/java/com/thealgorithms/searches/SortOrderAgnosticBinarySearchTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.thealgorithms.searches; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class SortOrderAgnosticBinarySearchTest { - - @Test - public void testAscending() { - int[] arr = {1, 2, 3, 4, 5}; // for ascending order. - int target = 2; - int ans = SortOrderAgnosticBinarySearch.find(arr, target); - int excepted = 1; - assertEquals(excepted, ans); - } - - @Test - public void testDescending() { - int[] arr = {5, 4, 3, 2, 1}; // for descending order. - int target = 2; - int ans = SortOrderAgnosticBinarySearch.find(arr, target); - int excepted = 3; - assertEquals(excepted, ans); - } -} diff --git a/src/test/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindowTest.java b/src/test/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindowTest.java new file mode 100644 index 000000000000..a6931bca99d2 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/CountDistinctElementsInWindowTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class CountDistinctElementsInWindowTest { + + @Test + public void testBasicCase() { + assertArrayEquals(new int[] {3, 2, 2}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 2, 3}, 3)); + } + + @Test + public void testAllSame() { + assertArrayEquals(new int[] {1, 1, 1}, CountDistinctElementsInWindow.countDistinct(new int[] {2, 2, 2, 2}, 2)); + } + + @Test + public void testAllDistinct() { + assertArrayEquals(new int[] {3, 3}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 4}, 3)); + } + + @Test + public void testWindowSizeEqualsArray() { + assertArrayEquals(new int[] {4}, CountDistinctElementsInWindow.countDistinct(new int[] {1, 2, 3, 4}, 4)); + } + + @Test + public void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> CountDistinctElementsInWindow.countDistinct(new int[] {}, 2)); + } +} diff --git a/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java b/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java new file mode 100644 index 000000000000..71bf24cc9e30 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/CountNiceSubarraysTest.java @@ -0,0 +1,55 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class CountNiceSubarraysTest { + @Test + void testExampleCase() { + int[] nums = {1, 1, 2, 1, 1}; + assertEquals(2, CountNiceSubarrays.countNiceSubarrays(nums, 3)); + } + + @Test + void testAllEvenNumbers() { + int[] nums = {2, 4, 6, 8}; + assertEquals(0, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testSingleOdd() { + int[] nums = {1}; + assertEquals(1, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testMultipleChoices() { + int[] nums = {2, 2, 1, 2, 2, 1, 2}; + assertEquals(6, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testTrailingEvenNumbers() { + int[] nums = {1, 2, 2, 2}; + assertEquals(4, CountNiceSubarrays.countNiceSubarrays(nums, 1)); + } + + @Test + void testMultipleWindowShrinks() { + int[] nums = {1, 1, 1, 1}; + assertEquals(3, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testEvensBetweenOdds() { + int[] nums = {2, 1, 2, 1, 2}; + assertEquals(4, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } + + @Test + void testShrinkWithTrailingEvens() { + int[] nums = {2, 2, 1, 2, 2, 1, 2, 2}; + assertEquals(9, CountNiceSubarrays.countNiceSubarrays(nums, 2)); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java b/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java index 60c4bbe9d342..2ee30c44e282 100644 --- a/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/BitonicSortTest.java @@ -1,8 +1,17 @@ package com.thealgorithms.sorts; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + public class BitonicSortTest extends SortingAlgorithmTest { @Override SortAlgorithm getSortAlgorithm() { return new BitonicSort(); } + + @Test + void shouldRejectNullArray() { + assertThrows(IllegalArgumentException.class, () -> getSortAlgorithm().sort((Integer[]) null)); + } } diff --git a/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java new file mode 100644 index 000000000000..454d0bd26929 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java @@ -0,0 +1,93 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Arrays; +import java.util.Random; +import org.junit.jupiter.api.Test; + +/** + * JUnit 5 test class for {@link ConcurrentMergeSort}. + */ +public class ConcurrentMergeSortTest { + + @Test + public void testAlreadySortedArray() { + int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Already sorted array should remain unchanged."); + } + + @Test + public void testReverseSortedArray() { + int[] array = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Reverse sorted array should be sorted correctly."); + } + + @Test + public void testIdenticalElementsArray() { + int[] array = {5, 5, 5, 5, 5, 5, 5}; + int[] expected = {5, 5, 5, 5, 5, 5, 5}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Array with identical elements should be sorted correctly (unchanged)."); + } + + @Test + public void testLargeRandomArray() { + int size = 100_000; + int[] array = new int[size]; + int[] expected = new int[size]; + // Using a fixed seed for deterministic testing + Random random = new Random(42); + + for (int i = 0; i < size; i++) { + int value = random.nextInt(); + array[i] = value; + expected[i] = value; + } + + // Generate the expected result using Java's highly optimized built-in sort + Arrays.sort(expected); + + // This will easily trigger the concurrency threshold (8192) in the implementation + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Large random array should be sorted correctly utilizing concurrency."); + } + + @Test + public void testEmptyArray() { + int[] array = {}; + int[] expected = {}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Empty array should be handled without errors."); + } + + @Test + public void testSingleElementArray() { + int[] array = {42}; + int[] expected = {42}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Single element array should be handled without errors."); + } + + @Test + public void testNullArray() { + int[] array = null; + ConcurrentMergeSort.sort(array); + org.junit.jupiter.api.Assertions.assertNull(array, "Null array should be handled without errors."); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java b/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java new file mode 100644 index 000000000000..49783f332e75 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/LibrarySortTest.java @@ -0,0 +1,70 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class LibrarySortTest { + + @Test + public void testBasicSort() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 3, 1, 4, 2})); + } + + @Test + public void testAlreadySorted() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {1, 2, 3, 4, 5})); + } + + @Test + public void testReverseSorted() { + assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 4, 3, 2, 1})); + } + + @Test + public void testDuplicates() { + assertArrayEquals(new int[] {1, 2, 2, 3, 3}, LibrarySort.sort(new int[] {3, 2, 1, 3, 2})); + } + + @Test + public void testSingleElement() { + assertArrayEquals(new int[] {1}, LibrarySort.sort(new int[] {1})); + } + + @Test + public void testEmptyArray() { + assertArrayEquals(new int[] {}, LibrarySort.sort(new int[] {})); + } + + @Test + public void testNullArray() { + assertThrows(IllegalArgumentException.class, () -> LibrarySort.sort(null)); + } + + // --- Added to cover branches the tests above never reach --- + + @Test + public void testShiftLeftWhenRightSideIsFull() { + // Right side of the target slot is completely occupied, forcing a left shift. + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6}, LibrarySort.sort(new int[] {0, 1, 2, 6, 4, 5, 3})); + } + + @Test + public void testTieBreakPrefersRightWhenDistancesEqual() { + // A gap exists on both sides at equal distance; algorithm should favor the right shift. + assertArrayEquals(new int[] {0, 1, 2, 3}, LibrarySort.sort(new int[] {0, 1, 3, 2})); + } + + @Test + public void testRightSearchRunsOffTheEnd() { + // No gap anywhere to the right of the target slot, all the way to the array's end. + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 7, 6})); + } + + @Test + public void testInsertAtEndWithNoTrailingGap() { + // A new global maximum arrives with no trailing gap left, forcing insertAtEnd(). + assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 6, 7})); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java b/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java new file mode 100644 index 000000000000..8df0502e80e7 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/SmoothSortTest.java @@ -0,0 +1,8 @@ +package com.thealgorithms.sorts; + +public class SmoothSortTest extends SortingAlgorithmTest { + @Override + SortAlgorithm getSortAlgorithm() { + return new SmoothSort(); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java b/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java index d5588b2b968e..e19f5b928263 100644 --- a/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/TopologicalSortTest.java @@ -58,7 +58,7 @@ public void failureTest() { Exception exception = assertThrows(RuntimeException.class, () -> TopologicalSort.sort(graph)); String expected = "This graph contains a cycle. No linear ordering is possible. " + "Back edge: 6 -> 2"; - assertEquals(exception.getMessage(), expected); + assertEquals(expected, exception.getMessage()); } @Test void testEmptyGraph() { diff --git a/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java b/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java new file mode 100644 index 000000000000..91da746447a8 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/TournamentSortTest.java @@ -0,0 +1,19 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class TournamentSortTest extends SortingAlgorithmTest { + + @Test + void shouldAcceptWhenNullArrayIsPassed() { + Integer[] array = null; + assertNull(getSortAlgorithm().sort(array)); + } + + @Override + SortAlgorithm getSortAlgorithm() { + return new TournamentSort(); + } +} diff --git a/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java b/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java index c5d57d63cf38..0d8b6acf9043 100644 --- a/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java +++ b/src/test/java/com/thealgorithms/sorts/WiggleSortTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.sorts; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Arrays; import org.junit.jupiter.api.Test; @@ -70,4 +71,22 @@ void wiggleTestStrings() { wiggleSort.sort(values); assertArrayEquals(values, result); } + + @Test + void wiggleTestNonWiggleSortableOddArrayThrows() { + // [1, 2, 2] is not wiggle-sortable: the median 2 appears ceil(3 / 2) = 2 times + // but is not the smallest value, so sorting must fail instead of returning + // a wrongly ordered array + WiggleSort wiggleSort = new WiggleSort(); + Integer[] values = {1, 2, 2}; + assertThrows(IllegalArgumentException.class, () -> wiggleSort.sort(values)); + } + + @Test + void wiggleTestTooManyDuplicatesThrows() { + // more than half of the values are the same, which can never be wiggle-sorted + WiggleSort wiggleSort = new WiggleSort(); + Integer[] values = {2, 2, 2, 1}; + assertThrows(IllegalArgumentException.class, () -> wiggleSort.sort(values)); + } } diff --git a/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java b/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java new file mode 100644 index 000000000000..2e4ea74691da --- /dev/null +++ b/src/test/java/com/thealgorithms/stacks/StockSpanProblemTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.stacks; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class StockSpanProblemTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testCalculateSpan(int[] prices, int[] expectedSpans) { + assertArrayEquals(expectedSpans, StockSpanProblem.calculateSpan(prices)); + } + + private static Stream validTestCases() { + return Stream.of(Arguments.of(new int[] {10, 4, 5, 90, 120, 80}, new int[] {1, 1, 2, 4, 5, 1}), Arguments.of(new int[] {100, 50, 60, 70, 80, 90}, new int[] {1, 1, 2, 3, 4, 5}), Arguments.of(new int[] {5, 4, 3, 2, 1}, new int[] {1, 1, 1, 1, 1}), + Arguments.of(new int[] {1, 2, 3, 4, 5}, new int[] {1, 2, 3, 4, 5}), Arguments.of(new int[] {10, 20, 30, 40, 50}, new int[] {1, 2, 3, 4, 5}), Arguments.of(new int[] {100, 80, 60, 70, 60, 75, 85}, new int[] {1, 1, 1, 2, 1, 4, 6}), + Arguments.of(new int[] {7, 7, 7, 7}, new int[] {1, 2, 3, 4}), Arguments.of(new int[] {}, new int[] {}), Arguments.of(new int[] {42}, new int[] {1})); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testCalculateSpanInvalidInput(int[] prices) { + assertThrows(IllegalArgumentException.class, () -> StockSpanProblem.calculateSpan(prices)); + } + + private static Stream invalidTestCases() { + return Stream.of(Arguments.of((int[]) null)); + } +} diff --git a/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java b/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java index 7b41e11ef22f..0c7d7e4701cf 100644 --- a/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java +++ b/src/test/java/com/thealgorithms/strings/AlphabeticalTest.java @@ -1,15 +1,45 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -public class AlphabeticalTest { +@DisplayName("Alphabetical.isAlphabetical()") +class AlphabeticalTest { - @ParameterizedTest(name = "\"{0}\" β†’ Expected: {1}") - @CsvSource({"'abcdefghijklmno', true", "'abcdxxxyzzzz', true", "'123a', false", "'abcABC', false", "'abcdefghikjlmno', false", "'aBC', true", "'abc', true", "'xyzabc', false", "'abcxyz', true", "'', false", "'1', false"}) - void testIsAlphabetical(String input, boolean expected) { - assertEquals(expected, Alphabetical.isAlphabetical(input)); + static Stream testCases() { + // Workaround for SpotBugs false positive (NAB_NEEDLESS_BOOLEAN_CONSTANT_CONVERSION) + // due to JUnit Arguments.of(Object...) auto-boxing + return Stream.of(arguments("", Boolean.FALSE, "Should return false for empty string"), arguments(" ", Boolean.FALSE, "Should return false for blank string"), arguments("a1b2", Boolean.FALSE, "Should return false when string contains numbers"), + arguments("abc!DEF", Boolean.FALSE, "Should return false when string contains symbols"), arguments("#abc", Boolean.FALSE, "Should return false when first character is not a letter"), arguments("abc", Boolean.TRUE, "Should return true for non-decreasing order"), + arguments("aBcD", Boolean.TRUE, "Should return true for mixed case increasing sequence"), arguments("a", Boolean.TRUE, "Should return true for single letter"), arguments("'", Boolean.FALSE, "Should return false for single symbol"), + arguments("aabbcc", Boolean.TRUE, "Should return true for repeated letters"), arguments("cba", Boolean.FALSE, "Should return false when order decreases"), arguments("abzba", Boolean.FALSE, "Should return false when middle breaks order")); + } + + private void assertAlphabetical(String input, boolean expected, String message) { + // Arrange & Act + boolean result = Alphabetical.isAlphabetical(input); + + // Assert + assertEquals(expected, result, message); + } + + @Test + @DisplayName("Should return false for null input") + void nullInputTest() { + assertAlphabetical(null, false, "Should return false for null input"); + } + + @ParameterizedTest(name = "{2}") + @MethodSource("testCases") + @DisplayName("Alphabetical cases") + void isAlphabeticalTest(String input, boolean expected, String message) { + assertAlphabetical(input, expected, message); } } diff --git a/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java index 9e8ae9e9f153..4cd55a4d7410 100644 --- a/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java +++ b/src/test/java/com/thealgorithms/strings/AlternativeStringArrangeTest.java @@ -1,9 +1,11 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class AlternativeStringArrangeTest { @@ -20,4 +22,15 @@ private static Stream provideTestData() { void arrangeTest(String input1, String input2, String expected) { assertEquals(expected, AlternativeStringArrange.arrange(input1, input2)); } + + @ParameterizedTest(name = "null input ({0}, {1}) should throw IllegalArgumentException") + @MethodSource("provideNullInputs") + void arrangeThrowsOnNullInput(String input1, String input2) { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> AlternativeStringArrange.arrange(input1, input2)); + assertEquals("Input strings must not be null", ex.getMessage()); + } + + private static Stream provideNullInputs() { + return Stream.of(Arguments.of(null, "abc"), Arguments.of("abc", null), Arguments.of(null, null)); + } } diff --git a/src/test/java/com/thealgorithms/strings/KMPTest.java b/src/test/java/com/thealgorithms/strings/KMPTest.java new file mode 100644 index 000000000000..9fa5f398d420 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/KMPTest.java @@ -0,0 +1,29 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class KMPTest { + + @Test + public void testNullInputs() { + assertEquals(List.of(), KMP.kmpMatcher(null, "A")); + assertEquals(List.of(), KMP.kmpMatcher("A", null)); + assertEquals(List.of(), KMP.kmpMatcher(null, null)); + } + + @Test + public void testKMPMatcher() { + assertEquals(List.of(0, 1), KMP.kmpMatcher("AAAAABAAABA", "AAAA")); + assertEquals(List.of(0, 3), KMP.kmpMatcher("ABCABC", "ABC")); + assertEquals(List.of(10), KMP.kmpMatcher("ABABDABACDABABCABAB", "ABABCABAB")); + assertEquals(List.of(), KMP.kmpMatcher("ABCDE", "FGH")); + assertEquals(List.of(), KMP.kmpMatcher("A", "AA")); + assertEquals(List.of(0, 1, 2), KMP.kmpMatcher("AAA", "A")); + assertEquals(List.of(0), KMP.kmpMatcher("A", "A")); + assertEquals(List.of(), KMP.kmpMatcher("", "A")); + assertEquals(List.of(), KMP.kmpMatcher("A", "")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java new file mode 100644 index 000000000000..c22cc77df18a --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/KasaiAlgorithmTest.java @@ -0,0 +1,75 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class KasaiAlgorithmTest { + + @Test + public void testKasaiBanana() { + String text = "banana"; + // Suffixes: + // 0: banana + // 1: anana + // 2: nana + // 3: ana + // 4: na + // 5: a + // + // Sorted Suffixes: + // 5: a + // 3: ana + // 1: anana + // 0: banana + // 4: na + // 2: nana + int[] suffixArr = {5, 3, 1, 0, 4, 2}; + + int[] expectedLcp = {1, 3, 0, 0, 2, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiAaaa() { + String text = "aaaa"; + // Sorted Suffixes: + // 3: a + // 2: aa + // 1: aaa + // 0: aaaa + int[] suffixArr = {3, 2, 1, 0}; + int[] expectedLcp = {1, 2, 3, 0}; + + assertArrayEquals(expectedLcp, KasaiAlgorithm.kasai(text, suffixArr)); + } + + @Test + public void testKasaiEmptyString() { + assertArrayEquals(new int[0], KasaiAlgorithm.kasai("", new int[0])); + } + + @Test + public void testKasaiSingleChar() { + assertArrayEquals(new int[] {0}, KasaiAlgorithm.kasai("A", new int[] {0})); + } + + @Test + public void testKasaiNullTextOrSuffixArray() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai(null, new int[] {0})); + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", null)); + } + + @Test + public void testKasaiInvalidSuffixArrayLength() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {0, 1})); + } + + @Test + public void testKasaiInvalidSuffixArrayIndex() { + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {1})); // Out of bounds + assertThrows(IllegalArgumentException.class, () -> KasaiAlgorithm.kasai("A", new int[] {-1})); // Out of bounds + } +} diff --git a/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java new file mode 100644 index 000000000000..e54abcf2f1f3 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LongestCommonSubstringTest.java @@ -0,0 +1,36 @@ +package com.thealgorithms.strings; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class LongestCommonSubstringTest { + + @Test + public void testNullOrEmptyInputs() { + assertEquals("", LongestCommonSubstring.longestCommonSubstring(null, "abc")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", null)); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("", "abc")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "")); + } + + @Test + public void testNormalSubstrings() { + assertEquals("cde", LongestCommonSubstring.longestCommonSubstring("abcdef", "zcdemf")); + assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abc", "abc")); + assertEquals("cdef", LongestCommonSubstring.longestCommonSubstring("abcdef", "cdefgh")); + } + + @Test + public void testSingleCharacterAndNoMatch() { + assertEquals("a", LongestCommonSubstring.longestCommonSubstring("a", "a")); + assertEquals("", LongestCommonSubstring.longestCommonSubstring("abc", "xyz")); + } + + @Test + public void testMultipleMatchesFirstLongest() { + // Keeps the first matched longest substring when lengths are tied + assertEquals("abc", LongestCommonSubstring.longestCommonSubstring("abcXdef", "abcYdef")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java deleted file mode 100644 index aa13c0f4a474..000000000000 --- a/src/test/java/com/thealgorithms/strings/LongestPalindromicSubstringTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.thealgorithms.strings; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class LongestPalindromicSubstringTest { - - @ParameterizedTest - @MethodSource("provideTestCasesForLongestPalindrome") - void testLongestPalindrome(String input, String expected) { - assertEquals(expected, LongestPalindromicSubstring.longestPalindrome(input)); - } - - private static Stream provideTestCasesForLongestPalindrome() { - return Stream.of(Arguments.of("babad", "bab"), Arguments.of("cbbd", "bb"), Arguments.of("a", "a"), Arguments.of("", ""), Arguments.of("abc", "a"), Arguments.of(null, ""), Arguments.of("aaaaa", "aaaaa")); - } -} diff --git a/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java b/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java new file mode 100644 index 000000000000..366f6863340d --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/LongestRepeatedSubstringTest.java @@ -0,0 +1,33 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LongestRepeatedSubstringTest { + + @ParameterizedTest(name = "\"{0}\" -> \"{1}\"") + @MethodSource("provideTestCases") + void testLongestRepeatedSubstring(String input, String expected) { + assertEquals(expected, LongestRepeatedSubstring.longestRepeatedSubstring(input)); + } + + private static Stream provideTestCases() { + return Stream.of(Arguments.of("banana", "ana"), Arguments.of("abcabc", "abc"), Arguments.of("aaaa", "aaa"), Arguments.of("abcd", ""), Arguments.of("a", ""), Arguments.of("", ""), Arguments.of(null, ""), Arguments.of("aab", "a"), Arguments.of("aa", "a"), Arguments.of("mississippi", "issi")); + } + + @ParameterizedTest(name = "\"{0}\" -> LCP={1}") + @MethodSource("provideLcpTestCases") + void testBuildLcpArray(String input, int[] expectedLcp) { + int[] suffixArray = SuffixArray.buildSuffixArray(input); + assertArrayEquals(expectedLcp, LongestRepeatedSubstring.buildLcpArray(input, suffixArray)); + } + + private static Stream provideLcpTestCases() { + return Stream.of(Arguments.of("banana", new int[] {1, 3, 0, 0, 2}), Arguments.of("ab", new int[] {0})); + } +} diff --git a/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java b/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java new file mode 100644 index 000000000000..016f72ff9d7f --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/MoveHashToEndTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class MoveHashToEndTest { + + @Test + void testBasicCase() { + assertEquals("helllo###", MoveHashToEnd.moveHashToEnd("h#e#l#llo")); + } + + @Test + void testNoHash() { + assertEquals("hello", MoveHashToEnd.moveHashToEnd("hello")); + } + + @Test + void testAllHashes() { + assertEquals("###", MoveHashToEnd.moveHashToEnd("###")); + } + + @Test + void testHashAtEnd() { + assertEquals("hello#", MoveHashToEnd.moveHashToEnd("hello#")); + } + + @Test + void testHashAtStart() { + assertEquals("hello#", MoveHashToEnd.moveHashToEnd("#hello")); + } + + @Test + void testEmptyString() { + assertEquals("", MoveHashToEnd.moveHashToEnd("")); + } + + @Test + void testNullInput() { + assertNull(MoveHashToEnd.moveHashToEnd(null)); + } + + @Test + void testSingleHash() { + assertEquals("#", MoveHashToEnd.moveHashToEnd("#")); + } + + @Test + void testSingleNonHashChar() { + assertEquals("a", MoveHashToEnd.moveHashToEnd("a")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/RabinKarpTest.java b/src/test/java/com/thealgorithms/strings/RabinKarpTest.java new file mode 100644 index 000000000000..6dfd099e9bca --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/RabinKarpTest.java @@ -0,0 +1,46 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class RabinKarpTest { + + @Test + public void testNullInputs() { + assertEquals(List.of(), RabinKarp.search(null, "A")); + assertEquals(List.of(), RabinKarp.search("A", null)); + assertEquals(List.of(), RabinKarp.search(null, null)); + } + + @Test + public void testHashCollision() { + // 'a' = 97. (char)198 % 101 = 97. + // For length 1, h = 1. p = 97. t = 198 % 101 = 97. + // Collision occurs, loop checks characters: 198 != 97, breaks. + char collisionChar = (char) 198; + String text = String.valueOf(collisionChar); + String pattern = "a"; + assertEquals(List.of(), RabinKarp.search(text, pattern)); + } + + @Test + public void testSearchWithCustomQ() { + // Using a different prime + assertEquals(List.of(0, 1), RabinKarp.search("AAAA", "AAA", 13)); + } + + @Test + public void testRabinKarpSearch() { + assertEquals(List.of(0, 1), RabinKarp.search("AAAAABAAABA", "AAAA")); + assertEquals(List.of(0, 3), RabinKarp.search("ABCABC", "ABC")); + assertEquals(List.of(10), RabinKarp.search("ABABDABACDABABCABAB", "ABABCABAB")); + assertEquals(List.of(), RabinKarp.search("ABCDE", "FGH")); + assertEquals(List.of(), RabinKarp.search("A", "AA")); + assertEquals(List.of(0, 1, 2), RabinKarp.search("AAA", "A")); + assertEquals(List.of(0), RabinKarp.search("A", "A")); + assertEquals(List.of(), RabinKarp.search("", "A")); + assertEquals(List.of(), RabinKarp.search("A", "")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java b/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java new file mode 100644 index 000000000000..3beb2e83399b --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/RemoveStarsTest.java @@ -0,0 +1,28 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class RemoveStarsTest { + + @Test + void testExampleCase() { + assertEquals("lecoe", RemoveStars.removeStars("leet**cod*e")); + } + + @Test + void testAllStars() { + assertEquals("", RemoveStars.removeStars("abc***")); + } + + @Test + void testNoStars() { + assertEquals("hello", RemoveStars.removeStars("hello")); + } + + @Test + void testSingleCharacter() { + assertEquals("", RemoveStars.removeStars("a*")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/TitleCaseTest.java b/src/test/java/com/thealgorithms/strings/TitleCaseTest.java new file mode 100644 index 000000000000..37bb44ed2c7b --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/TitleCaseTest.java @@ -0,0 +1,39 @@ +package com.thealgorithms.strings; +// author: Vraj Prajapati @Rosander0 + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TitleCaseTest { + + @Test + public void testNullOrEmptyInputs() { + assertEquals("", TitleCase.toTitleCase(null)); + assertEquals("", TitleCase.toTitleCase("")); + } + + @Test + public void testSingleWord() { + assertEquals("Hello", TitleCase.toTitleCase("hello")); + assertEquals("Hello", TitleCase.toTitleCase("HELLO")); + assertEquals("A", TitleCase.toTitleCase("a")); + } + + @Test + public void testMultipleWords() { + assertEquals("The Quick Brown Fox", TitleCase.toTitleCase("the quick brown fox")); + assertEquals("The Quick Brown Fox", TitleCase.toTitleCase("THE QUICK BROWN FOX")); + assertEquals("Already Title Case", TitleCase.toTitleCase("already Title Case")); + } + + @Test + public void testWhitespace() { + assertEquals(" Spaces ", TitleCase.toTitleCase(" spaces ")); + } + + @Test + public void testQuotedWords() { + assertEquals("\"Hello\"", TitleCase.toTitleCase("\"hello\"")); + } +} diff --git a/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java b/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java new file mode 100644 index 000000000000..42b2d04ff265 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/TopKFrequentWordsTest.java @@ -0,0 +1,34 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TopKFrequentWordsTest { + + @ParameterizedTest + @MethodSource("validTestCases") + void testFindTopKFrequentWords(String[] words, int k, List expected) { + assertEquals(expected, TopKFrequentWords.findTopKFrequentWords(words, k)); + } + + static Stream validTestCases() { + return Stream.of(Arguments.of(new String[] {"i", "love", "leetcode", "i", "love", "coding"}, 2, List.of("i", "love")), Arguments.of(new String[] {"the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"}, 4, List.of("the", "is", "sunny", "day")), + Arguments.of(new String[] {"bbb", "aaa", "bbb", "aaa", "ccc"}, 2, List.of("aaa", "bbb")), Arguments.of(new String[] {"one", "two", "three"}, 10, List.of("one", "three", "two")), Arguments.of(new String[] {}, 3, List.of()), Arguments.of(new String[] {"x", "x", "y"}, 0, List.of())); + } + + @ParameterizedTest + @MethodSource("invalidTestCases") + void testFindTopKFrequentWordsInvalidInput(String[] words, int k) { + assertThrows(IllegalArgumentException.class, () -> TopKFrequentWords.findTopKFrequentWords(words, k)); + } + + static Stream invalidTestCases() { + return Stream.of(Arguments.of((String[]) null, 1), Arguments.of(new String[] {"a", null, "b"}, 2), Arguments.of(new String[] {"a"}, -1)); + } +} diff --git a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java b/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java deleted file mode 100644 index 411b11e743b8..000000000000 --- a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.thealgorithms.strings; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -public class ValidParenthesesTest { - - @ParameterizedTest(name = "Input: \"{0}\" β†’ Expected: {1}") - @CsvSource({"'()', true", "'()[]{}', true", "'(]', false", "'{[]}', true", "'([{}])', true", "'([)]', false", "'', true", "'(', false", "')', false", "'{{{{}}}}', true", "'[({})]', true", "'[(])', false", "'[', false", "']', false", "'()()()()', true", "'(()', false", "'())', false", - "'{[()()]()}', true"}) - void - testIsValid(String input, boolean expected) { - assertEquals(expected, ValidParentheses.isValid(input)); - } - - @Test - void testNullInputThrows() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(null)); - assertEquals("Input string cannot be null", ex.getMessage()); - } - - @ParameterizedTest(name = "Input: \"{0}\" β†’ throws IllegalArgumentException") - @CsvSource({"'a'", "'()a'", "'[123]'", "'{hello}'", "'( )'", "'\t'", "'\n'", "'@#$%'"}) - void testInvalidCharactersThrow(String input) { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(input)); - assertTrue(ex.getMessage().startsWith("Unexpected character")); - } -} diff --git a/src/test/java/com/thealgorithms/strings/WordLadderTest.java b/src/test/java/com/thealgorithms/strings/WordLadderTest.java index 221953411da7..c029940abfb0 100644 --- a/src/test/java/com/thealgorithms/strings/WordLadderTest.java +++ b/src/test/java/com/thealgorithms/strings/WordLadderTest.java @@ -24,7 +24,7 @@ public class WordLadderTest { public void testWordLadder() { List wordList1 = Arrays.asList("hot", "dot", "dog", "lot", "log", "cog"); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList1), 5); + assertEquals(5, WordLadder.ladderLength("hit", "cog", wordList1)); } /** @@ -39,7 +39,7 @@ public void testWordLadder() { public void testWordLadder2() { List wordList2 = Arrays.asList("hot", "dot", "dog", "lot", "log"); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList2), 0); + assertEquals(0, WordLadder.ladderLength("hit", "cog", wordList2)); } /** @@ -54,7 +54,7 @@ public void testWordLadder2() { public void testWordLadder3() { List wordList3 = emptyList(); - assertEquals(WordLadder.ladderLength("hit", "cog", wordList3), 0); + assertEquals(0, WordLadder.ladderLength("hit", "cog", wordList3)); } @ParameterizedTest diff --git a/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java b/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java index 2cbbfe3d2dd8..9bf118c9b844 100644 --- a/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java +++ b/src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java @@ -9,8 +9,8 @@ public class ZigZagPatternTest { public void testZigZagPattern() { String input1 = "HelloWorldFromJava"; String input2 = "javaIsAProgrammingLanguage"; - Assertions.assertEquals(ZigZagPattern.encode(input1, 4), "HooeWrrmalolFJvlda"); - Assertions.assertEquals(ZigZagPattern.encode(input2, 4), "jAaLgasPrmgaaevIrgmnnuaoig"); + Assertions.assertEquals("HooeWrrmalolFJvlda", ZigZagPattern.encode(input1, 4)); + Assertions.assertEquals("jAaLgasPrmgaaevIrgmnnuaoig", ZigZagPattern.encode(input2, 4)); // Edge cases Assertions.assertEquals("ABC", ZigZagPattern.encode("ABC", 1)); // Single row Assertions.assertEquals("A", ZigZagPattern.encode("A", 2)); // numRows > length of string