Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create combinations of values in Java?

I have the following map: Map<Integer,String[]> map = new HashMap<Integer,String[]>();

The keys are integers and the values are arrays (could also be replaced by lists).

Now, I would like to get all possible combinations of the values among the keys. For example, let's say the map contains the following entries:

key 1: "test1", "stackoverflow"
key 2: "test2", "wow"
key 3: "new"

The combinations consists of

("test1","test2","new")
("test1","wow","new")
("stackoverflow", "test2", "new")
("stackoverflow", "wow", "new")

For this I imagine a method boolean hasNext() which returns true if there is a next pair and a second method which just returns the next set of values (if any).

How can this be done? The map could also be replaced by an other data structure.

like image 633
machinery Avatar asked Mar 12 '16 17:03

machinery


1 Answers

The algorithm is essentially almost the same as the increment algorithm for decimal numbers ("x -> x+1").

Here the iterator class:

import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeSet;

public class CombinationsIterator implements Iterator<String[]> {

    // Immutable fields
    private final int combinationLength;
    private final String[][] values;
    private final int[] maxIndexes;

    // Mutable fields
    private final int[] currentIndexes;
    private boolean hasNext;

    public CombinationsIterator(final Map<Integer,String[]> map) {
        combinationLength = map.size();
        values = new String[combinationLength][];
        maxIndexes = new int[combinationLength];
        currentIndexes = new int[combinationLength];

        if (combinationLength == 0) {
            hasNext = false;
            return;
        }

        hasNext = true;

        // Reorganize the map to array.
        // Map is not actually needed and would unnecessarily complicate the algorithm.
        int valuesIndex = 0;
        for (final int key : new TreeSet<>(map.keySet())) {
            values[valuesIndex++] = map.get(key);
        }

        // Fill in the arrays of max indexes and current indexes.
        for (int i = 0; i < combinationLength; ++i) {
            if (values[i].length == 0) {
                // Set hasNext to false if at least one of the value-arrays is empty.
                // Stop the loop as the behavior of the iterator is already defined in this case:
                // the iterator will just return no combinations.
                hasNext = false;
                return;
            }

            maxIndexes[i] = values[i].length - 1;
            currentIndexes[i] = 0;
        }
    }

    @Override
    public boolean hasNext() {
        return hasNext;
    }

    @Override
    public String[] next() {
        if (!hasNext) {
            throw new NoSuchElementException("No more combinations are available");
        }
        final String[] combination = getCombinationByCurrentIndexes();
        nextIndexesCombination();
        return combination;
    }

    private String[] getCombinationByCurrentIndexes() {
        final String[] combination = new String[combinationLength];
        for (int i = 0; i < combinationLength; ++i) {
            combination[i] = values[i][currentIndexes[i]];
        }
        return combination;
    }

    private void nextIndexesCombination() {
        // A slightly modified "increment number by one" algorithm.

        // This loop seems more natural, but it would return combinations in a different order than in your example:
//      for (int i = 0; i < combinationLength; ++i) {

        // This loop returns combinations in the order which matches your example:
        for (int i = combinationLength - 1; i >= 0; --i) {
            if (currentIndexes[i] < maxIndexes[i]) {
                // Increment the current index
                ++currentIndexes[i];
                return;
            } else {
                // Current index at max: 
                // reset it to zero and "carry" to the next index
                currentIndexes[i] = 0;
            }
        }
        // If we are here, then all current indexes are at max, and there are no more combinations
        hasNext = false;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Remove operation is not supported");
    }

}

Here the sample usage:

final Map<Integer,String[]> map = new HashMap<Integer,String[]>();
map.put(1, new String[]{"test1", "stackoverflow"});
map.put(2, new String[]{"test2", "wow"});
map.put(3, new String[]{"new"});

final CombinationsIterator iterator = new CombinationsIterator(map);
while (iterator.hasNext()) {
    System.out.println(
        org.apache.commons.lang3.ArrayUtils.toString(iterator.next())
    );
}

It prints exactly what's specified in your example.


P.S. The map is actually not needed; it could be replaced by a simple array of arrays (or list of lists). The constructor would then get a bit simpler:

public CombinationsIterator(final String[][] array) {
    combinationLength = array.length;
    values = array;

    // ...

    // Reorganize the map to array - THIS CAN BE REMOVED.
like image 108
Alex Shesterov Avatar answered Sep 24 '22 08:09

Alex Shesterov