Skip to content
84 changes: 84 additions & 0 deletions src/main/java/com/thealgorithms/recursion/Permutations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.thealgorithms.recursion;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* This class provides methods to generate all permutations
* of a given array of any type using recursion.
* <p>
* Reference:
* https://en.wikipedia.org/wiki/Permutation
*/
public final class Permutations {

private Permutations() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private constructor is not tested — this is causing
2 uncovered lines in Codecov.

Add:
@test
void testConstructorThrowsException() {
assertThrows(UnsupportedOperationException.class, () -> {
var constructor = Permutations.class.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
});
}

throw new UnsupportedOperationException("Utility class");
}

/**
* Generates all permutations of a generic array.
*
* @param <T> the type of elements in the array
* @param items the input array
* @return list of all permutations
* @throws NullPointerException if items is null
* @throws IllegalArgumentException if any element in items is null
*/
public static <T> List<List<T>> permutations(T[] items) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned by @DenizAltunkapan, a Permutation class already
exists at backtracking/Permutation.java. Please align your
unique permutations logic into the existing implementation
instead of creating a separate class.

if (items == null) {
throw new NullPointerException("Input array cannot be null");
}
for (T item : items) {
if (item == null) {
throw new IllegalArgumentException("Array elements cannot be null");
}
}

List<List<T>> result = new ArrayList<>();
List<T> list = new ArrayList<>(Arrays.asList(items));
generatePermutations(0, list, result);
return result;
}

/**
* Recursive backtracking to generate permutations.
*
* @param <T> the type of elements
* @param index the current position being fixed
* @param list the working list of elements
* @param result the accumulated list of permutations
*/
private static <T> void generatePermutations(int index, List<T> list, List<List<T>> result) {
if (index == list.size()) {
result.add(new ArrayList<>(list));
return;
}

Set<T> seen = new HashSet<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HashSet relies on hashCode() and equals() for deduplication.
For custom objects that don't override these methods,
duplicates may not be detected correctly.
Consider documenting this assumption in JavaDoc or
using explicit comparison instead.

for (int i = index; i < list.size(); i++) {
if (seen.add(list.get(i))) { // skip duplicate values at this position
swap(list, index, i);
generatePermutations(index + 1, list, result);
swap(list, index, i); // backtrack
}
}
}

/**
* Swaps two elements in a list.
*
* @param <T> the type of elements
* @param list the list
* @param i first index
* @param j second index
*/
private static <T> void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
}
157 changes: 157 additions & 0 deletions src/test/java/com/thealgorithms/recursion/Permutations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.thealgorithms.recursion;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import org.junit.jupiter.api.Test;

class PermutationsTest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test file name should be PermutationsTest.java not
Permutations.java — as per TheAlgorithms naming convention.


// ─────────────────────────────────────────────
// Null / Invalid Input Tests
// ─────────────────────────────────────────────

@Test
void testNullArrayThrowsNullPointerException() {
assertThrows(NullPointerException.class, () -> Permutations.permutations((Integer[]) null));
}

@Test
void testArrayWithNullElementThrowsIllegalArgumentException() {
Integer[] items = {1, null, 3};
assertThrows(IllegalArgumentException.class, () -> Permutations.permutations(items));
}

// ─────────────────────────────────────────────
// Edge Case Tests
// ─────────────────────────────────────────────

@Test
void testEmptyArrayReturnsOneEmptyPermutation() {
Integer[] items = {};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(1, result.size());
assertTrue(result.get(0).isEmpty());
}

@Test
void testSingleElementReturnsOnePermutation() {
Integer[] items = {42};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(1, result.size());
assertEquals(List.of(42), result.get(0));
}

// ─────────────────────────────────────────────
// Integer Permutation Tests
// ─────────────────────────────────────────────

@Test
void testTwoIntegersReturnsTwoPermutations() {
Integer[] items = {1, 2};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(2, result.size());
assertTrue(result.contains(List.of(1, 2)));
assertTrue(result.contains(List.of(2, 1)));
}

@Test
void testThreeIntegersReturnsSixPermutations() {
Integer[] items = {1, 2, 3};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(6, result.size());
}

@Test
void testIntegerPermutationsContainAllExpectedOrders() {
Integer[] items = {1, 2, 3};
List<List<Integer>> result = Permutations.permutations(items);
assertTrue(result.contains(List.of(1, 2, 3)));
assertTrue(result.contains(List.of(1, 3, 2)));
assertTrue(result.contains(List.of(2, 1, 3)));
assertTrue(result.contains(List.of(2, 3, 1)));
assertTrue(result.contains(List.of(3, 1, 2)));
assertTrue(result.contains(List.of(3, 2, 1)));
}

// ─────────────────────────────────────────────
// Duplicate Handling Tests
// ─────────────────────────────────────────────

@Test
void testTwoDuplicateIntegersReturnsOnePermutation() {
Integer[] items = {1, 1};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(1, result.size());
assertEquals(List.of(1, 1), result.get(0));
}

@Test
void testArrayWithDuplicatesReturnsCorrectCount() {
Integer[] items = {1, 1, 2};
List<List<Integer>> result = Permutations.permutations(items);
// 3!/2! = 3 unique permutations
assertEquals(3, result.size());
assertTrue(result.contains(List.of(1, 1, 2)));
assertTrue(result.contains(List.of(1, 2, 1)));
assertTrue(result.contains(List.of(2, 1, 1)));
}

@Test
void testAllDuplicatesReturnsOnePermutation() {
Integer[] items = {5, 5, 5};
List<List<Integer>> result = Permutations.permutations(items);
assertEquals(1, result.size());
assertEquals(List.of(5, 5, 5), result.get(0));
}

// ─────────────────────────────────────────────
// String Permutation Tests
// ─────────────────────────────────────────────

@Test
void testTwoStringsReturnsTwoPermutations() {
String[] items = {"a", "b"};
List<List<String>> result = Permutations.permutations(items);
assertEquals(2, result.size());
assertTrue(result.contains(List.of("a", "b")));
assertTrue(result.contains(List.of("b", "a")));
}

@Test
void testThreeStringsReturnsSixPermutations() {
String[] items = {"x", "y", "z"};
List<List<String>> result = Permutations.permutations(items);
assertEquals(6, result.size());
}

@Test
void testDuplicateStringsReturnsCorrectCount() {
String[] items = {"a", "a", "b"};
List<List<String>> result = Permutations.permutations(items);
assertEquals(3, result.size());
assertTrue(result.contains(List.of("a", "a", "b")));
assertTrue(result.contains(List.of("a", "b", "a")));
assertTrue(result.contains(List.of("b", "a", "a")));
}

// ─────────────────────────────────────────────
// Character Permutation Tests
// ─────────────────────────────────────────────

@Test
void testCharacterPermutations() {
Character[] items = {'a', 'b', 'c'};
List<List<Character>> result = Permutations.permutations(items);
assertEquals(6, result.size());
}

@Test
void testDuplicateCharactersReturnsCorrectCount() {
Character[] items = {'a', 'a', 'b'};
List<List<Character>> result = Permutations.permutations(items);
assertEquals(3, result.size());
}
}
Loading