From 0574dfb035bc8e9f1724dce6dc5bfd788a2b0c01 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Sun, 21 Apr 2013 15:16:58 +0000 Subject: [PATCH] [COLLECTIONS-422] Added CollectionUtils.permutations(Collection) and PermutationIterator. Thanks for Benoit Corne for the patch. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/collections/trunk@1470310 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 3 + src/changes/changes.xml | 4 + .../commons/collections4/CollectionUtils.java | 30 +++ .../iterators/PermutationIterator.java | 156 ++++++++++++++ .../collections4/CollectionUtilsTest.java | 19 ++ .../iterators/PermutationIteratorTest.java | 190 ++++++++++++++++++ 6 files changed, 402 insertions(+) create mode 100644 src/main/java/org/apache/commons/collections4/iterators/PermutationIterator.java create mode 100644 src/test/java/org/apache/commons/collections4/iterators/PermutationIteratorTest.java diff --git a/pom.xml b/pom.xml index 2170f481b..701b0df4c 100644 --- a/pom.xml +++ b/pom.xml @@ -177,6 +177,9 @@ Steve Clark + + Benoit Corne + Eric Crampton diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 5a527aa1f..c5fdaa365 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,10 @@ + + Added method "CollectionUtils#permutations(Collection)" and class "PermutationIterator" + to generate unordered permutations of a collection. + Added clarifying javadoc wrt runtime complexity of "AbstractDualBidiMap#retainAll". diff --git a/src/main/java/org/apache/commons/collections4/CollectionUtils.java b/src/main/java/org/apache/commons/collections4/CollectionUtils.java index e88635cae..ace7c9cb8 100644 --- a/src/main/java/org/apache/commons/collections4/CollectionUtils.java +++ b/src/main/java/org/apache/commons/collections4/CollectionUtils.java @@ -24,6 +24,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -37,6 +38,7 @@ import org.apache.commons.collections4.collection.UnmodifiableBoundedCollection; import org.apache.commons.collections4.collection.UnmodifiableCollection; import org.apache.commons.collections4.functors.Equator; import org.apache.commons.collections4.functors.TruePredicate; +import org.apache.commons.collections4.iterators.PermutationIterator; /** * Provides utility methods and decorators for {@link Collection} instances. @@ -1585,6 +1587,34 @@ public class CollectionUtils { } } + //----------------------------------------------------------------------- + + /** + * Returns a {@link Collection} of all the permutations of the input collection. + *

+ * NOTE: the number of permutations of a given collection is equal to n!, where + * n is the size of the collection. Thus, the resulting collection will become + * very large for collections > 10 (e.g. 10! = 3628800, 15! = 1307674368000). + *

+ * For larger collections it is advised to use a {@link PermutationIterator} to + * iterate over all permutations. + * + * @see PermutationIterator + * + * @param the element type + * @param collection the collection to create permutations for, may not be null + * @return an unordered collection of all permutations of the input collection + * @throws NullPointerException if collection is null + */ + public static Collection> permutations(final Collection collection) { + final PermutationIterator it = new PermutationIterator(collection); + final Collection> result = new LinkedList>(); + while (it.hasNext()) { + result.add(it.next()); + } + return result; + } + //----------------------------------------------------------------------- /** * Returns a collection containing all the elements in collection diff --git a/src/main/java/org/apache/commons/collections4/iterators/PermutationIterator.java b/src/main/java/org/apache/commons/collections4/iterators/PermutationIterator.java new file mode 100644 index 000000000..616007fbd --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/iterators/PermutationIterator.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.iterators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * This iterator creates permutations of an input collection, using the + * Steinhaus-Johnson-Trotter algorithm (also called plain changes). + *

+ * The iterator will return exactly n! permutations of the input collection. + * The {@code remove()} operation is not supported, and will throw an + * {@code UnsupportedOperationException}. + *

+ * NOTE: in case an empty collection is provided, the iterator will + * return exactly one empty list as result, as 0! = 1. + * + * @param the type of the objects being permuted + * + * @version $Id$ + * @since 4.0 + */ +public class PermutationIterator implements Iterator> { + + /** + * Permutation is done on theses keys to handle equal objects. + */ + private int[] keys; + + /** + * Mapping between keys and objects. + */ + private Map objectMap; + + /** + * Direction table used in the algorithm: + *

+ */ + private boolean[] direction; + + /** + * Next permutation to return. When a permutation is requested + * this instance is provided and the next one is computed. + */ + private List nextPermutation; + + /** + * Standard constructor for this class. + * @param coll the collection to generate permutations for + * @throws NullPointerException if coll is null + */ + public PermutationIterator(final Collection coll) { + if (coll == null) { + throw new NullPointerException("The collection must not be null"); + } + + keys = new int[coll.size()]; + direction = new boolean[coll.size()]; + Arrays.fill(direction, false); + int value = 1; + objectMap = new HashMap(); + for (E e : coll) { + objectMap.put(value, e); + keys[value - 1] = value; + value++; + } + nextPermutation = new ArrayList(coll); + } + + /** + * Indicates if there are more permutation available. + * @return true if there are more permutations, otherwise false + */ + public boolean hasNext() { + return nextPermutation != null; + } + + /** + * Returns the next permutation of the input collection. + * @return a list of the permutator's elements representing a permutation + * @throws NoSuchElementException if there are no more permutations + */ + public List next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + // find the largest mobile integer k + int indexOfLargestMobileInteger = -1; + int largestKey = -1; + for (int i = 0; i < keys.length; i++) { + if ((direction[i] && i < keys.length - 1 && keys[i] > keys[i + 1]) || + (!direction[i] && i > 0 && keys[i] > keys[i - 1])) { + if (keys[i] > largestKey) { + largestKey = keys[i]; + indexOfLargestMobileInteger = i; + } + } + } + if (largestKey == -1) { + List toReturn = nextPermutation; + nextPermutation = null; + return toReturn; + } + + // swap k and the adjacent integer it is looking at + final int offset = direction[indexOfLargestMobileInteger] ? 1 : -1; + final int tmpKey = keys[indexOfLargestMobileInteger]; + keys[indexOfLargestMobileInteger] = keys[indexOfLargestMobileInteger + offset]; + keys[indexOfLargestMobileInteger + offset] = tmpKey; + boolean tmpDirection = direction[indexOfLargestMobileInteger]; + direction[indexOfLargestMobileInteger] = direction[indexOfLargestMobileInteger + offset]; + direction[indexOfLargestMobileInteger + offset] = tmpDirection; + + // reverse the direction of all integers larger than k and build the result + final List nextP = new ArrayList(); + for (int i = 0; i < keys.length; i++) { + if (keys[i] > largestKey) { + direction[i] = !direction[i]; + } + nextP.add(objectMap.get(keys[i])); + } + final List result = nextPermutation; + nextPermutation = nextP; + return result; + } + + public void remove() { + throw new UnsupportedOperationException("remove() is not supported"); + } + +} diff --git a/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java b/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java index e4780e4ad..fde4d00e3 100644 --- a/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java +++ b/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java @@ -1645,4 +1645,23 @@ public class CollectionUtilsTest extends MockTestCase { assertEquals("Merge two lists 2 - ignore duplicates", combinedList, result2); } + @Test(expected=NullPointerException.class) + public void testPermutationsWithNullCollection() { + CollectionUtils.permutations(null); + } + + @Test + public void testPermutations() { + List sample = collectionA.subList(0, 5); + Collection> permutations = CollectionUtils.permutations(sample); + + // result size = n! + int collSize = sample.size(); + int factorial = 1; + for (int i = 1; i <= collSize; i++) { + factorial *= i; + } + assertEquals(factorial, permutations.size()); + } + } diff --git a/src/test/java/org/apache/commons/collections4/iterators/PermutationIteratorTest.java b/src/test/java/org/apache/commons/collections4/iterators/PermutationIteratorTest.java new file mode 100644 index 000000000..f70e31586 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/iterators/PermutationIteratorTest.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.iterators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Test class for PermutationIterator. + * + * @version $Id$ + * @since 4.0 + */ +public class PermutationIteratorTest extends AbstractIteratorTest> { + + protected Character[] testArray = { 'A', 'B', 'C' }; + protected List testList; + + public PermutationIteratorTest(final String testName) { + super(testName); + } + + @Override + public void setUp() { + testList = new ArrayList(); + testList.addAll(Arrays.asList(testArray)); + } + + //----------------------------------------------------------------------- + + public boolean supportsRemove() { + return false; + } + + public boolean supportsEmptyIterator() { + return false; + } + + @Override + public PermutationIterator makeEmptyIterator() { + return new PermutationIterator(new ArrayList()); + } + + @Override + public PermutationIterator makeObject() { + return new PermutationIterator(testList); + } + + //----------------------------------------------------------------------- + + public void testPermutationResultSize() { + int factorial = 1; + for (int i = 0; i < 8; i++, factorial*=i) { + List list = new ArrayList(); + for (int j = 0; j < i; j++) { + list.add(j); + } + Iterator> it = new PermutationIterator(list); + int count = 0; + while (it.hasNext()) { + it.next(); + count++; + } + assertEquals(factorial, count); + } + } + + /** + * test checking that all the permutations are returned + */ + public void testPermutationExhaustivity() { + List perm1 = new ArrayList(); + List perm2 = new ArrayList(); + List perm3 = new ArrayList(); + List perm4 = new ArrayList(); + List perm5 = new ArrayList(); + List perm6 = new ArrayList(); + + perm1.add('A'); + perm2.add('A'); + perm3.add('B'); + perm4.add('B'); + perm5.add('C'); + perm6.add('C'); + + perm1.add('B'); + perm2.add('C'); + perm3.add('A'); + perm4.add('C'); + perm5.add('A'); + perm6.add('B'); + + perm1.add('C'); + perm2.add('B'); + perm3.add('C'); + perm4.add('A'); + perm5.add('B'); + perm6.add('A'); + + List> results = new ArrayList>(); + + PermutationIterator it = makeObject(); + while (it.hasNext()) { + List next = it.next(); + results.add(next); + } + //3! permutation for 3 elements + assertEquals(6, results.size()); + assertTrue(results.contains(perm1)); + assertTrue(results.contains(perm2)); + assertTrue(results.contains(perm3)); + assertTrue(results.contains(perm4)); + assertTrue(results.contains(perm5)); + assertTrue(results.contains(perm6)); + } + + /** + * test checking that all the permutations are returned only once. + */ + public void testPermutationUnicity() { + List> resultsList = new ArrayList>(); + Set> resultsSet = new HashSet>(); + + PermutationIterator it = makeObject(); + while (it.hasNext()) { + List permutation = it.next(); + resultsList.add(permutation); + resultsSet.add(permutation); + } + //3! permutation for 3 elements + assertEquals(6, resultsList.size()); + assertEquals(6, resultsSet.size()); + } + + public void testPermutationException() { + List> resultsList = new ArrayList>(); + + PermutationIterator it = makeObject(); + while (it.hasNext()) { + List permutation = it.next(); + resultsList.add(permutation); + } + //asking for another permutation should throw an exception + try { + it.next(); + fail(); + } catch (NoSuchElementException e) { + // expected + } + } + + public void testPermutatorHasMore() { + PermutationIterator it = makeObject(); + for (int i = 0; i < 6; i++) { + assertTrue(it.hasNext()); + it.next(); + } + assertFalse(it.hasNext()); + } + + public void testEmptyCollection() { + PermutationIterator it = makeEmptyIterator(); + // there is one permutation for an empty set: 0! = 1 + assertTrue(it.hasNext()); + + List nextPermutation = it.next(); + assertEquals(0, nextPermutation.size()); + + assertFalse(it.hasNext()); + } +} \ No newline at end of file