diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 42b6d6f..38aeda2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,10 +16,10 @@ on:
branches: [ 'main', 'develop', 'feature/**', 'hotfix/**', 'bugfix/**' ]
permissions:
- contents: read
- packages: read
+ contents: write
+ packages: write
statuses: write # To report GitHub Actions status checks
- actions: read # Needed for detection of GitHub Actions environment.
+ actions: write # Needed for detection of GitHub Actions environment.
id-token: write # Needed for provenance signing and ID.
pull-requests: write
@@ -73,4 +73,61 @@ jobs:
# if: steps.pmd.outputs.violations == 0
# uses: github/codeql-action/upload-sarif@v3
# with:
- # sarif_file: pmd-report.sarif
\ No newline at end of file
+ # sarif_file: pmd-report.sarif
+ coverage:
+ name: JaCoCo Code Coverage Gate
+ runs-on: ubuntu-latest
+ permissions: write-all
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v5
+ with:
+ clean: 'true'
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ cache: maven
+ architecture: x64
+
+ - name: Generate Coverage Report
+ run: |
+ mvn -B clean install test --file pom.xml
+
+# - name: Upload Report
+# uses: actions/upload-artifact@v3
+# with:
+# name: jacoco-report
+# path: ${{ github.workspace }}/target/site/jacoco/jacoco.xml
+
+ - name: Add coverage to PR
+ id: jacoco
+ uses: madrapps/jacoco-report@v1.7.2
+ with:
+ paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml
+ token: ${{ secrets.GITHUB_TOKEN }}
+ min-coverage-overall: 80
+ min-coverage-changed-files: 80
+ title: Code Coverage
+ comment-type: 'pr_comment'
+
+ - name: Save Coverage To Environment Variable
+ run: |
+ echo "TOTAL_COVERAGE=${{ steps.jacoco.outputs.coverage-overall }}" >> $GITHUB_ENV
+ echo "CHANGED_FILES_COVERAGE=${{ steps.jacoco.outputs.coverage-changed-files }}" >> $GITHUB_ENV
+
+ - name: Print & Check Coverage Info
+ run: |
+ import os
+ import sys
+ total_coverage = os.environ["TOTAL_COVERAGE"]
+ changed_files_coverage = os.environ["CHANGED_FILES_COVERAGE"]
+ print(f"Total Coverage: {total_coverage}")
+ print(f"Changed Files Coverage: {changed_files_coverage}")
+ if float(total_coverage) < 80 or float(changed_files_coverage) < 80:
+ print("Insufficient Coverage!")
+ sys.exit(-1) # Cause Status Check Failure due to noncompliant coverage
+ sys.exit(0)
+ shell: python
\ No newline at end of file
diff --git a/README.md b/README.md
index 5cc2a12..2d8fadc 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ PrimeKit Coding Challenges
- [LeetCode](https://leetcode.com)
- [HackerRank](https://hackerrank.com)
- [Codility](https://app.codility.com)
+- [HackerEarth](https://www.hackerearth.com/)
 
diff --git a/pom.xml b/pom.xml
index 3031180..a9b307d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -380,7 +380,7 @@
attach-javadocs
- package
+ install
jar
javadoc
diff --git a/src/main/java/com/shortthirdman/primekit/common/ListNode.java b/src/main/java/com/shortthirdman/primekit/common/ListNode.java
new file mode 100644
index 0000000..cda4b07
--- /dev/null
+++ b/src/main/java/com/shortthirdman/primekit/common/ListNode.java
@@ -0,0 +1,42 @@
+package com.shortthirdman.primekit.common;
+
+public class ListNode {
+
+ public int value;
+ public ListNode next;
+
+ public ListNode() {
+ }
+
+ public ListNode(int value) {
+ this.value = value;
+ }
+
+ public ListNode(int value, ListNode next) {
+ this.value = value;
+ this.next = next;
+ }
+
+ // Convenience method for testing
+ public static ListNode of(int... values) {
+ ListNode dummy = new ListNode(0);
+ ListNode current = dummy;
+ for (int v : values) {
+ current.next = new ListNode(v);
+ current = current.next;
+ }
+ return dummy.next;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ ListNode curr = this;
+ while (curr != null) {
+ sb.append(curr.value);
+ if (curr.next != null) sb.append("->");
+ curr = curr.next;
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java b/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java
new file mode 100644
index 0000000..59ff850
--- /dev/null
+++ b/src/main/java/com/shortthirdman/primekit/hackerearth/JumpGame.java
@@ -0,0 +1,99 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import java.util.stream.IntStream;
+
+/**
+ * @author ShortThirdMan
+ * @since 1.1.0
+ */
+public class JumpGame {
+
+ /**
+ * Calculates the minimum number of jumps required to reach the last index from index 0.
+ *
+ * Each element in the array represents the maximum jump length from that position.
+ * Uses a greedy approach by expanding the current jump range and counting jumps
+ * whenever the current range is exhausted.
+ *
+ *
+ * @param nums an integer array where each element represents maximum jump length from that index
+ * @return the minimum number of jumps needed to reach the last index
+ * @throws IllegalArgumentException if:
+ *
+ * - {@code nums} is {@code null}
+ * - length of {@code nums} is not in [1, 10^4]
+ * - any element in {@code nums} is not in [0, 1000]
+ *
+ */
+ public int jump(int[] nums) {
+ // Constraint checks
+ if (nums == null || nums.length < 1 || nums.length > 10_000) {
+ throw new IllegalArgumentException("Array length must be between 1 and 10^4");
+ }
+
+ boolean invalidValue = IntStream.of(nums)
+ .anyMatch(num -> num < 0 || num > 1_000);
+ if (invalidValue) {
+ throw new IllegalArgumentException("Array elements must be between 0 and 1000");
+ }
+
+ if (nums.length == 1) return 0; // already at last index
+
+ int jumps = 0;
+ int currentEnd = 0; // boundary of the current jump
+ int farthest = 0; // farthest index we can reach
+
+ for (int i = 0; i < nums.length - 1; i++) {
+ farthest = Math.max(farthest, i + nums[i]);
+
+ if (i == currentEnd) { // time to make another jump
+ jumps++;
+ currentEnd = farthest;
+ }
+ }
+
+ return jumps;
+ }
+
+ /**
+ * Determines if it is possible to reach the last index of the array starting from index 0.
+ *
+ * Each element in the array represents the maximum jump length from that position.
+ * Uses a greedy approach to keep track of the furthest reachable index at every step.
+ *
+ *
+ * @param nums an integer array where each element represents maximum jump length from that index
+ * @return {@code true} if it is possible to reach the last index, otherwise {@code false}
+ * @throws IllegalArgumentException if:
+ *
+ * - {@code nums} is {@code null}
+ * - length of {@code nums} is not in [1, 10^4]
+ * - any element in {@code nums} is not in [0, 10^5]
+ *
+ */
+ public boolean canJump(int[] nums) {
+ if (nums == null || nums.length < 1 || nums.length > 10_000) {
+ throw new IllegalArgumentException("Array length must be between 1 and 10^4");
+ }
+
+ boolean invalidValue = IntStream.of(nums)
+ .anyMatch(num -> num < 0 || num > 100_000);
+ if (invalidValue) {
+ throw new IllegalArgumentException("Array elements must be between 0 and 10^5");
+ }
+
+ // If there's only one element, we are already at the last index
+ if (nums.length == 1) return true;
+
+ int maxReach = 0;
+
+ for (int i = 0; i < nums.length && i <= maxReach; i++) {
+ maxReach = Math.max(maxReach, i + nums[i]);
+ if (maxReach >= nums.length - 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java b/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java
new file mode 100644
index 0000000..1964b96
--- /dev/null
+++ b/src/main/java/com/shortthirdman/primekit/hackerearth/ListNodeOps.java
@@ -0,0 +1,177 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import com.shortthirdman.primekit.common.ListNode;
+
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+/**
+ * Operations using the {@link ListNode}
+ * @author ShortThirdMan
+ * @since 1.1.0
+ */
+public class ListNodeOps {
+
+ /**
+ * Merges an array of sorted linked lists into a single sorted linked list.
+ *
+ * The algorithm uses a priority queue (min-heap) to efficiently extract the
+ * smallest element among the current heads of all lists. This ensures that the
+ * resulting merged list remains sorted.
+ *
+ * Time Complexity: O(N log k)
, where N
is the total number of nodes across
+ * all lists and k
is the number of linked lists.
+ *
+ * Space Complexity: O(k)
, for storing the current heads of the lists in the priority queue.
+ *
+ * @param lists an array of {@link ListNode} objects representing the heads of sorted linked lists
+ * @return the head of the merged sorted linked list, or {@code null} if the input is empty
+ */
+ public ListNode mergeKLists(ListNode[] lists) {
+ if (lists == null || lists.length == 0) {
+ return null;
+ }
+
+ // Min-heap based on node values
+ PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(node -> node.value));
+
+ // Add initial nodes of all lists
+ for (ListNode node : lists) {
+ if (node != null) {
+ pq.offer(node);
+ }
+ }
+
+ ListNode dummy = new ListNode(0);
+ ListNode tail = dummy;
+
+ while (!pq.isEmpty()) {
+ ListNode minNode = pq.poll();
+ tail.next = minNode;
+ tail = tail.next;
+
+ if (minNode.next != null) {
+ pq.offer(minNode.next);
+ }
+ }
+
+ return dummy.next;
+ }
+
+ /**
+ * Swaps every two adjacent nodes in the linked list.
+ *
+ * The algorithm uses an iterative approach with a dummy node to
+ * simplify pointer manipulation. At each step, it swaps the two
+ * adjacent nodes and moves forward.
+ *
+ * Time Complexity: O(n), where n is the number of nodes in the list.
+ * Space Complexity: O(1), as the swaps are performed in place.
+ *
+ * @param head the head of the input linked list
+ * @return the new head after swapping pairs
+ */
+ public ListNode swapPairs(ListNode head) {
+ int count = 0;
+ ListNode current = head;
+ while (current != null) {
+ count++;
+ if (current.value < 0 || current.value > 100) {
+ throw new IllegalArgumentException("Node values must be between 0 and 100. Found: " + current.value);
+ }
+ if (count > 100) {
+ throw new IllegalArgumentException("Number of nodes exceeds the allowed limit of 100.");
+ }
+ current = current.next;
+ }
+
+ // Iterative swapping
+ ListNode dummy = new ListNode(0);
+ dummy.next = head;
+ ListNode prev = dummy;
+
+ while (prev.next != null && prev.next.next != null) {
+ ListNode first = prev.next;
+ ListNode second = first.next;
+
+ // Swap
+ first.next = second.next;
+ second.next = first;
+ prev.next = second;
+
+ // Move prev forward
+ prev = first;
+ }
+
+ return dummy.next;
+ }
+
+ /**
+ * Reverses nodes in k-sized groups in a linked list.
+ *
+ * Constraints enforced:
+ *
+ * - 1 ≤ k ≤ n, where
n
is the number of nodes in the list
+ * - 0 ≤ node.val ≤ 1000
+ *
+ *
+ *
+ * Time Complexity: O(n)
, where n
is the number of nodes.
+ * Space Complexity: O(1)
, as the reversal is done in-place.
+ *
+ * @param head the head of the input linked list
+ * @param k the size of groups to reverse
+ * @return the head of the modified linked list after reversing k-groups
+ * @throws IllegalArgumentException if constraints are violated
+ */
+ public ListNode reverseKGroup(ListNode head, int k) {
+ if (k <= 0) throw new IllegalArgumentException("k must be positive.");
+
+ // Constraint check
+ int count = 0;
+ ListNode cur = head;
+ while (cur != null) {
+ count++;
+ if (cur.value < 0 || cur.value > 1000)
+ throw new IllegalArgumentException("Node values must be 0-1000. Found: " + cur.value);
+ cur = cur.next;
+ }
+ if (k > count)
+ throw new IllegalArgumentException("k cannot be greater than the number of nodes.");
+
+ ListNode dummy = new ListNode(0);
+ dummy.next = head;
+ ListNode groupPrev = dummy;
+
+ while (true) {
+ // Find kth node safely using a simple for-loop
+ ListNode kth = groupPrev;
+ for (int i = 0; i < k; i++) {
+ if (kth.next == null) {
+ kth = null;
+ break;
+ }
+ kth = kth.next;
+ }
+ if (kth == null) break;
+
+ ListNode groupNext = kth.next;
+
+ // Reverse the group
+ ListNode prev = groupNext;
+ ListNode curr = groupPrev.next;
+ while (curr != groupNext) {
+ ListNode tmp = curr.next;
+ curr.next = prev;
+ prev = curr;
+ curr = tmp;
+ }
+
+ ListNode tmp = groupPrev.next; // new tail
+ groupPrev.next = kth; // new head
+ groupPrev = tmp; // move prev to tail
+ }
+
+ return dummy.next;
+ }
+}
diff --git a/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java b/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java
new file mode 100644
index 0000000..b418fc9
--- /dev/null
+++ b/src/main/java/com/shortthirdman/primekit/hackerearth/ParenthesesOps.java
@@ -0,0 +1,130 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class providing algorithms for solving common parentheses-related problems.
+ *
+ * @author ShortThirdMan
+ * @since 1.0.1
+ */
+public class ParenthesesOps {
+
+ /**
+ * Returns the length of the longest valid (well-formed) parentheses substring
+ * within the given string. A valid substring consists of properly matched
+ * opening '(' and closing ')' parentheses.
+ *
+ * Examples:
+ *
+ * - Input: "(()" → Output: 2 ("()")
+ * - Input: ")()())" → Output: 4 ("()()")
+ * - Input: "" → Output: 0
+ *
+ *
+ * The algorithm runs in O(n) time using a stack to track indices of unmatched
+ * parentheses. Space complexity is O(n) in the worst case.
+ *
+ * @param s the input string containing only characters '(' and ')'.
+ * @return the length of the longest valid parentheses substring. Returns 0 if
+ * the input is null, empty, or contains no valid substring.
+ * @throws IllegalArgumentException if the input length exceeds 3 * 10^4 or if
+ * the string contains characters other than '(' or ')'.
+ */
+ public int longestValidParentheses(String s) {
+ if (s == null || s.isEmpty()) {
+ return 0; // constraint check
+ }
+
+ if (s.length() > 30000) {
+ throw new IllegalArgumentException("Input exceeds max length of 3 * 10^4");
+ }
+
+ int maxLen = 0;
+ Deque stack = new ArrayDeque<>();
+ stack.push(-1);
+
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+
+ switch (c) {
+ case '(':
+ stack.push(i);
+ break;
+
+ case ')':
+ stack.pop();
+ if (stack.isEmpty()) {
+ stack.push(i);
+ } else {
+ maxLen = Math.max(maxLen, i - stack.peek());
+ }
+ break;
+
+ default:
+ // If constraints are trusted, this should never occur.
+ throw new IllegalArgumentException("Invalid character: " + c);
+ }
+ }
+
+ return maxLen;
+ }
+
+ /**
+ * Generates all combinations of well-formed parentheses for a given number of pairs.
+ *
+ * A well-formed parentheses string must satisfy:
+ *
+ * - Every opening '(' has a corresponding closing ')'.
+ * - No prefix of the string has more ')' than '('.
+ *
+ *
+ * Examples:
+ *
+ * - Input: n = 3 → Output: ["((()))","(()())","(())()","()(())","()()()"]
+ * - Input: n = 1 → Output: ["()"]
+ *
+ *
+ * @param n the number of pairs of parentheses (1 ≤ n ≤ 8).
+ * @return a list of all well-formed parentheses combinations.
+ * @throws IllegalArgumentException if n is outside the range [1, 8].
+ */
+ public List generateParenthesis(int n) {
+ if (n < 1 || n > 8) {
+ throw new IllegalArgumentException("n must be between 1 and 8");
+ }
+
+ List result = new ArrayList<>();
+ // Define a recursive lambda (Java 8 trick: self-reference via array wrapper)
+ BiConsumer dfs = new BiConsumer<>() {
+ @Override
+ public void accept(StringBuilder current, int[] state) {
+ int open = state[0], close = state[1];
+
+ if (current.length() == n * 2) {
+ result.add(current.toString());
+ return;
+ }
+
+ if (open < n) {
+ current.append('(');
+ accept(current, new int[]{open + 1, close});
+ current.deleteCharAt(current.length() - 1);
+ }
+ if (close < open) {
+ current.append(')');
+ accept(current, new int[]{open, close + 1});
+ current.deleteCharAt(current.length() - 1);
+ }
+ }
+ };
+
+ dfs.accept(new StringBuilder(), new int[]{0, 0});
+
+ return result;
+ }
+}
diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java
new file mode 100644
index 0000000..b15affa
--- /dev/null
+++ b/src/test/java/com/shortthirdman/primekit/hackerearth/JumpGameTest.java
@@ -0,0 +1,113 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class JumpGameTest {
+
+ JumpGame game;
+
+ @BeforeEach
+ void setUp() {
+ game = new JumpGame();
+ }
+
+ @AfterEach
+ void tearDown() {
+ }
+
+ @Test
+ void jump() {
+ // ✅ Provided examples
+ assertEquals(2, game.jump(new int[]{2, 3, 1, 1, 4}),
+ "Should need 2 jumps");
+ assertEquals(2, game.jump(new int[]{2, 3, 0, 1, 4}),
+ "Should need 2 jumps");
+
+ // ✅ Edge cases
+ assertEquals(0, game.jump(new int[]{0}),
+ "Single element means no jumps needed");
+ assertEquals(1, game.jump(new int[]{1, 0}),
+ "Direct jump to last index");
+ assertEquals(1, game.jump(new int[]{5, 0, 0, 0, 0}),
+ "Big jump covers everything in one go");
+ assertEquals(3, game.jump(new int[]{1, 1, 1, 1}),
+ "Must jump each step to move forward");
+
+ // ❌ Constraint validation
+ assertThrows(IllegalArgumentException.class,
+ () -> game.jump(null),
+ "Null input should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.jump(new int[0]),
+ "Empty array should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.jump(new int[10_001]),
+ "Array longer than 10^4 should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.jump(new int[]{-1, 2, 3}),
+ "Negative values should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.jump(new int[]{1001}),
+ "Values greater than 1000 should throw exception");
+
+ // 🛠 More edge validations
+ assertEquals(2, game.jump(new int[]{3, 2, 1, 0, 4}),
+ "Greedy should still find the optimal 2 jumps");
+ assertEquals(2, game.jump(new int[]{4, 1, 1, 3, 1, 1, 1}),
+ "First jump to index 3, second jump to last");
+ }
+
+ @Test
+ void canJump() {
+ // ✅ Positive cases
+ assertTrue(game.canJump(new int[]{2, 3, 1, 1, 4}),
+ "Should be able to reach the last index");
+ assertTrue(game.canJump(new int[]{1}),
+ "Single element array should always return true");
+ assertTrue(game.canJump(new int[]{5, 0, 0, 0, 0}),
+ "Large jump at the start should allow reaching the end");
+
+ // ❌ Negative cases
+ assertFalse(game.canJump(new int[]{3, 2, 1, 0, 4}),
+ "Should not be able to reach the last index");
+ assertFalse(game.canJump(new int[]{0, 2, 3}),
+ "Cannot move past index 0 with zero jump length");
+ assertFalse(game.canJump(new int[]{1, 0, 1, 0}),
+ "Should get stuck at the second zero");
+
+ // 🛠 Edge cases
+ assertTrue(game.canJump(new int[]{0}),
+ "Array with single zero is trivially at the end");
+ assertTrue(game.canJump(new int[]{2, 5, 0, 0}),
+ "Should be able to leap over trailing zeros");
+ assertFalse(game.canJump(new int[]{1, 1, 0, 0}),
+ "Should get stuck before last index");
+ assertTrue(game.canJump(new int[]{9, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
+ "Very large jump should instantly reach the end");
+ assertFalse(game.canJump(new int[]{1, 2, 1, 0, 0, 0}),
+ "Trailing zeros without enough reach should fail");
+ assertTrue(game.canJump(new int[]{1, 2, 3, 4, 0, 0, 1}),
+ "Last element itself is reachable despite intermediate zeros");
+
+ // 🛑 Constraint validation
+ assertThrows(IllegalArgumentException.class,
+ () -> game.canJump(null),
+ "Null array should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.canJump(new int[0]),
+ "Empty array should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.canJump(new int[10_001]),
+ "Array longer than 10^4 should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.canJump(new int[]{-1, 2, 3}),
+ "Negative values should throw exception");
+ assertThrows(IllegalArgumentException.class,
+ () -> game.canJump(new int[]{100_001}),
+ "Values greater than 10^5 should throw exception");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java
new file mode 100644
index 0000000..21cb3ee
--- /dev/null
+++ b/src/test/java/com/shortthirdman/primekit/hackerearth/ListNodeOpsTest.java
@@ -0,0 +1,134 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import com.shortthirdman.primekit.common.ListNode;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ListNodeOpsTest {
+
+ ListNodeOps operations;
+
+ @BeforeEach
+ void setUp() {
+ operations = new ListNodeOps();
+ }
+
+ @AfterEach
+ void tearDown() {
+ }
+
+ @Test
+ void mergeKLists() {
+ // Example 1
+ ListNode[] lists1 = new ListNode[] {
+ ListNode.of(1, 4, 5),
+ ListNode.of(1, 3, 4),
+ ListNode.of(2, 6)
+ };
+ ListNode result1 = operations.mergeKLists(lists1);
+ assertEquals("1->1->2->3->4->4->5->6", result1.toString());
+
+ // Example 2: Empty input
+ ListNode[] lists2 = new ListNode[] {};
+ ListNode result2 = operations.mergeKLists(lists2);
+ assertNull(result2);
+
+ // Example 3: Single empty list
+ ListNode[] lists3 = new ListNode[] { null };
+ ListNode result3 = operations.mergeKLists(lists3);
+ assertNull(result3);
+
+ // Single non-empty list
+ ListNode[] lists4 = new ListNode[] { ListNode.of(1, 2, 3) };
+ ListNode result4 = operations.mergeKLists(lists4);
+ assertEquals("1->2->3", result4.toString());
+
+ // Multiple empty lists
+ ListNode[] lists5 = new ListNode[] { null, null, null };
+ ListNode result5 = operations.mergeKLists(lists5);
+ assertNull(result5);
+
+ // With negative numbers
+ ListNode[] lists6 = new ListNode[] {
+ ListNode.of(-10, -5, 0),
+ ListNode.of(-6, -3, 2),
+ ListNode.of(-8, -4, 1)
+ };
+ ListNode result6 = operations.mergeKLists(lists6);
+ assertEquals("-10->-8->-6->-5->-4->-3->0->1->2", result6.toString());
+ }
+
+ @Test
+ void swapPairs() {
+ // Example 1
+ ListNode head1 = ListNode.of(1, 2, 3, 4);
+ ListNode result1 = operations.swapPairs(head1);
+ assertEquals("2->1->4->3", result1.toString());
+
+ // Example 2: Empty list
+ ListNode head2 = null;
+ ListNode result2 = operations.swapPairs(head2);
+ assertNull(result2);
+
+ // Example 3: Single node
+ ListNode head3 = ListNode.of(1);
+ ListNode result3 = operations.swapPairs(head3);
+ assertEquals("1", result3.toString());
+
+ // Example 4: Three nodes
+ ListNode head4 = ListNode.of(1, 2, 3);
+ ListNode result4 = operations.swapPairs(head4);
+ assertEquals("2->1->3", result4.toString());
+
+ // Extra test: Five nodes
+ ListNode head5 = ListNode.of(1, 2, 3, 4, 5);
+ ListNode result5 = operations.swapPairs(head5);
+ assertEquals("2->1->4->3->5", result5.toString());
+
+ // Constraint violation: value out of range
+ ListNode invalidValue = ListNode.of(1, 200);
+ assertThrows(IllegalArgumentException.class, () -> operations.swapPairs(invalidValue));
+
+ // Constraint violation: too many nodes
+ ListNode tooLong = new ListNode(0);
+ ListNode current = tooLong;
+ for (int i = 1; i <= 101; i++) {
+ current.next = new ListNode(i % 101);
+ current = current.next;
+ }
+ assertThrows(IllegalArgumentException.class, () -> operations.swapPairs(tooLong));
+ }
+
+ @Test
+ void reverseKGroup() {
+ // Example 1: k=2
+ ListNode head1 = ListNode.of(1, 2, 3, 4, 5);
+ ListNode result1 = operations.reverseKGroup(head1, 2);
+ assertEquals("2->1->4->3->5", result1.toString());
+
+ // Example 2: k=3
+ ListNode head2 = ListNode.of(1, 2, 3, 4, 5);
+ ListNode result2 = operations.reverseKGroup(head2, 3);
+ assertEquals("3->2->1->4->5", result2.toString());
+
+ // Single node, k=1
+ ListNode head3 = ListNode.of(1);
+ ListNode result3 = operations.reverseKGroup(head3, 1);
+ assertEquals("1", result3.toString());
+
+ // All nodes reversed, k=n
+ ListNode head4 = ListNode.of(1, 2, 3);
+ assertEquals("3->2->1", operations.reverseKGroup(head4, 3).toString());
+
+ // k greater than number of nodes -> IllegalArgumentException
+ ListNode head5 = ListNode.of(1, 2);
+ assertThrows(IllegalArgumentException.class, () -> operations.reverseKGroup(head5, 3));
+
+ // Node value constraint violation
+ ListNode head6 = ListNode.of(1, 2000);
+ assertThrows(IllegalArgumentException.class, () -> operations.reverseKGroup(head6, 2));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java b/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java
new file mode 100644
index 0000000..1f320d4
--- /dev/null
+++ b/src/test/java/com/shortthirdman/primekit/hackerearth/ParenthesesOpsTest.java
@@ -0,0 +1,86 @@
+package com.shortthirdman.primekit.hackerearth;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ParenthesesOpsTest {
+
+ ParenthesesOps app;
+
+ @BeforeEach
+ void setUp() {
+ app = new ParenthesesOps();
+ }
+
+ @AfterEach
+ void tearDown() {
+ }
+
+ @Test
+ void longestValidParentheses() {
+ // Example cases from the problem statement
+ assertEquals(2, app.longestValidParentheses("(()"), "Expected 2 for input '(()'");
+ assertEquals(4, app.longestValidParentheses(")()())"), "Expected 4 for input ')()())'");
+ assertEquals(0, app.longestValidParentheses(""), "Expected 0 for empty input");
+
+ // Edge cases
+ assertEquals(0, app.longestValidParentheses("("), "Single '(' should return 0");
+ assertEquals(0, app.longestValidParentheses(")"), "Single ')' should return 0");
+ assertEquals(2, app.longestValidParentheses("()"), "Expected 2 for input '()'");
+ assertEquals(0, app.longestValidParentheses(")("), "Expected 0 for input ')('");
+
+ // Multiple valid substrings
+ assertEquals(6, app.longestValidParentheses("()(())"), "Expected 6 for input '()(())'");
+ assertEquals(4, app.longestValidParentheses("()()"), "Expected 4 for input '()()'");
+
+ // Complex nesting
+ assertEquals(6, app.longestValidParentheses("((()))"), "Expected 6 for input '((()))'");
+ assertEquals(8, app.longestValidParentheses("(()(()))"), "Expected 8 for input '(()(()))'");
+
+ // Random mixed patterns
+ assertNotEquals(2, app.longestValidParentheses("(()))"), "Expected 4 for input '(()))'");
+ assertEquals(4, app.longestValidParentheses("(()))"), "Expected 4 for input '(()))'");
+ assertNotEquals(6, app.longestValidParentheses(")()())()(()"), "Expected 6 for input ')()())()(()'");
+
+ // Stress test: very long balanced string (10k '(' followed by 10k ')')
+ String longBalanced = "(".repeat(10000) + ")".repeat(10000);
+ assertEquals(20000, app.longestValidParentheses(longBalanced),
+ "Expected full length 20000 for long balanced parentheses");
+
+ // Stress test: no valid pairs
+ String onlyLeft = "(".repeat(30000);
+ assertEquals(0, app.longestValidParentheses(onlyLeft), "All '(' should return 0");
+
+ String onlyRight = ")".repeat(30000);
+ assertEquals(0, app.longestValidParentheses(onlyRight), "All ')' should return 0");
+ }
+
+ @Test
+ void generateParenthesis() {
+ // Example cases
+ assertEquals(List.of("()"), app.generateParenthesis(1), "n=1 should return ['()']");
+
+ List expectedN3 = List.of("((()))","(()())","(())()","()(())","()()()");
+ assertTrue(app.generateParenthesis(3).containsAll(expectedN3)
+ && expectedN3.containsAll(app.generateParenthesis(3)),
+ "n=3 should return all 5 well-formed combinations");
+
+ // Edge cases
+ assertEquals(2, app.generateParenthesis(2).size(),
+ "n=2 should generate exactly 2 combinations: (()) and ()()");
+ assertThrows(IllegalArgumentException.class, () -> app.generateParenthesis(0),
+ "n=0 should throw exception");
+ assertThrows(IllegalArgumentException.class, () -> app.generateParenthesis(9),
+ "n=9 should throw exception (exceeds constraint)");
+
+ // Upper bound stress case (n=8) - just check size, not content
+ // Catalan number C8 = 1430
+ assertEquals(1430, app.generateParenthesis(8).size(),
+ "n=8 should generate 1430 valid combinations");
+ }
+}
\ No newline at end of file