Reputation: 43
I'm trying to generate all possible words of length 0 to 4 with letters a to z and A to Z. This is what I have using Java stream
static String[] letters
= " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
letters[0] = "";
static final int CAPACITY = 1 + 52 + 52*52 + 52*52*52 + 52*52*52*52;
static List<String> words
= Arrays.stream(LETTERS)
.flatMap(word -> Arrays.stream(LETTERS).map(word::concat))
.flatMap(word -> Arrays.stream(LETTERS).map(word::concat))
.flatMap(word -> Arrays.stream(LETTERS).map(word::concat))
.distinct()
.collect(Collectors.toCollection(() -> new ArrayList<>(CAPACITY)));
Is there another solution where I don't have to use distinct()
? I know I could do something like:
String[] letters
= "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
List<String> words = new ArrayList<>();
words.add("");
for(String letter1 : letters) {
words.add(letter1);
for(String letter2 : letters) {
words.add(letter1 + letter2)
for(String letter3 : letters) {
words.add(letter1 + letter2 + letter3);
for(String letter4 : letters) {
words.add(letter1 + letter2 + letter3 + letter4);
}
}
}
}
But I want to do it using Java stream.
(Edit) After your suggestions:
public static List<String> generateWords(int maxLength, String[]
letters) {
if (letters.length == 0)
return Arrays.asList("");
int capacity = IntStream.rangeClosed(0, maxLength)
.map(i -> (int) Math.pow(letters.length, i))
.sum();
return IntStream.range(0, capacity)
.mapToObj(i -> map(i, letters))
.collect(Collectors.toCollection(() -> new ArrayList<>(capacity)));
}
private static String map(int index, String[] letters) {
if(index == 0) return "";
return map((index - 1) / letters.length, letters)
+ letters[(index - 1) % letters.length];
}
Upvotes: 2
Views: 5174
Reputation: 43
I'm a bit late to the party since I just learned about Java 8 streams and lambdas a few days ago. Anyway, I saw your question and I decided to give it a try.
My solution involved using stream pipelining together with recursion. One of the aspects I like about streams is that you can chain them together. That's where the idea of calling them recursively to solve the permutation problem came from.
Hope you people like it and can give me some ideas on ways to improve it.
Edit: Thanks anonymous user for the suggested improvements. Now considering r = 1.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RecursivePermutation {
// Returns a stream of permutations of c.size()
// elements taken r at a time without repetition.
// Example: If C={1,2} and r=2
// Output: {(1,2), (2,1)}
// This recursive function uses the chain property of streams
public static <T> Stream<List<T>> permutations(List<T> c, int r){
if (r==1){
return c.stream()
.map(e -> Arrays.asList(e));
} else
if (r==2){
return c.stream()
.flatMap(
e1 -> c.stream() // e1: refers to an element of c
.filter(e2 -> !e1.equals(e2)) // e2: refers to an element of c
.map(e2 -> Arrays.asList(e1, e2))
);
} else {
return permutations(c, r-1)
.flatMap(
l -> c.stream()
.filter( e -> l.contains(e) == false)
.map(e -> {
List<T> out = new ArrayList<>();
out.addAll(l);
out.add(e);
return out;}
)
);
}
}
public static void main(String[] args) {
// Name Permutations
List<String> stringList = Arrays.asList("Joe", "Ana", "Pete", "Mark", "Lucy");
List<List<String>> sp =
permutations(stringList, 2).collect(Collectors.toList());
System.out.println(sp);
// Integers Permutations
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
List<List<Integer>> np =
permutations(numbers, 3).collect(Collectors.toList());
System.out.println(np);
}
}
Running this code will give you the following output:
[[Joe, Ana], [Joe, Pete], [Joe, Mark], [Joe, Lucy], [Ana, Joe], [Ana, Pete], [Ana, Mark], [Ana, Lucy], [Pete, Joe], [Pete, Ana], [Pete, Mark], [Pete, Lucy], [Mark, Joe], [Mark, Ana], [Mark, Pete], [Mark, Lucy], [Lucy, Joe], [Lucy, Ana], [Lucy, Pete], [Lucy, Mark]]
[[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 2, 6], [1, 2, 7], [1, 3, 2], [1, 3, 4], [1, 3, 5], [1, 3, 6], [1, 3, 7], [1, 4, 2], [1, 4, 3], [1, 4, 5], [1, 4, 6], [1, 4, 7], [1, 5, 2], [1, 5, 3], [1, 5, 4], [1, 5, 6], [1, 5, 7], [1, 6, 2], [1, 6, 3], [1, 6, 4], [1, 6, 5], [1, 6, 7], [1, 7, 2], [1, 7, 3], [1, 7, 4], [1, 7, 5], [1, 7, 6], [2, 1, 3], [2, 1, 4], [2, 1, 5], [2, 1, 6], [2, 1, 7], [2, 3, 1], [2, 3, 4], [2, 3, 5], [2, 3, 6], [2, 3, 7], [2, 4, 1], [2, 4, 3], [2, 4, 5], [2, 4, 6], [2, 4, 7], [2, 5, 1], [2, 5, 3], [2, 5, 4], [2, 5, 6], [2, 5, 7], [2, 6, 1], [2, 6, 3], [2, 6, 4], [2, 6, 5], [2, 6, 7], [2, 7, 1], [2, 7, 3], [2, 7, 4], [2, 7, 5], [2, 7, 6], [3, 1, 2], [3, 1, 4], [3, 1, 5], [3, 1, 6], [3, 1, 7], [3, 2, 1], [3, 2, 4], [3, 2, 5], [3, 2, 6], [3, 2, 7], [3, 4, 1], [3, 4, 2], [3, 4, 5], [3, 4, 6], [3, 4, 7], [3, 5, 1], [3, 5, 2], [3, 5, 4], [3, 5, 6], [3, 5, 7], [3, 6, 1], [3, 6, 2], [3, 6, 4], [3, 6, 5], [3, 6, 7], [3, 7, 1], [3, 7, 2], [3, 7, 4], [3, 7, 5], [3, 7, 6], [4, 1, 2], [4, 1, 3], [4, 1, 5], [4, 1, 6], [4, 1, 7], [4, 2, 1], [4, 2, 3], [4, 2, 5], [4, 2, 6], [4, 2, 7], [4, 3, 1], [4, 3, 2], [4, 3, 5], [4, 3, 6], [4, 3, 7], [4, 5, 1], [4, 5, 2], [4, 5, 3], [4, 5, 6], [4, 5, 7], [4, 6, 1], [4, 6, 2], [4, 6, 3], [4, 6, 5], [4, 6, 7], [4, 7, 1], [4, 7, 2], [4, 7, 3], [4, 7, 5], [4, 7, 6], [5, 1, 2], [5, 1, 3], [5, 1, 4], [5, 1, 6], [5, 1, 7], [5, 2, 1], [5, 2, 3], [5, 2, 4], [5, 2, 6], [5, 2, 7], [5, 3, 1], [5, 3, 2], [5, 3, 4], [5, 3, 6], [5, 3, 7], [5, 4, 1], [5, 4, 2], [5, 4, 3], [5, 4, 6], [5, 4, 7], [5, 6, 1], [5, 6, 2], [5, 6, 3], [5, 6, 4], [5, 6, 7], [5, 7, 1], [5, 7, 2], [5, 7, 3], [5, 7, 4], [5, 7, 6], [6, 1, 2], [6, 1, 3], [6, 1, 4], [6, 1, 5], [6, 1, 7], [6, 2, 1], [6, 2, 3], [6, 2, 4], [6, 2, 5], [6, 2, 7], [6, 3, 1], [6, 3, 2], [6, 3, 4], [6, 3, 5], [6, 3, 7], [6, 4, 1], [6, 4, 2], [6, 4, 3], [6, 4, 5], [6, 4, 7], [6, 5, 1], [6, 5, 2], [6, 5, 3], [6, 5, 4], [6, 5, 7], [6, 7, 1], [6, 7, 2], [6, 7, 3], [6, 7, 4], [6, 7, 5], [7, 1, 2], [7, 1, 3], [7, 1, 4], [7, 1, 5], [7, 1, 6], [7, 2, 1], [7, 2, 3], [7, 2, 4], [7, 2, 5], [7, 2, 6], [7, 3, 1], [7, 3, 2], [7, 3, 4], [7, 3, 5], [7, 3, 6], [7, 4, 1], [7, 4, 2], [7, 4, 3], [7, 4, 5], [7, 4, 6], [7, 5, 1], [7, 5, 2], [7, 5, 3], [7, 5, 4], [7, 5, 6], [7, 6, 1], [7, 6, 2], [7, 6, 3], [7, 6, 4], [7, 6, 5]]
Upvotes: 0
Reputation: 4555
Thanks for the cool question, really nice one!
The reason you need the distinct is the empty string, i.e. the words that are less than 4 characters long. For example, ...a
, ..a.
, .a..
, and a...
all result in a
, and you need the distinct to remove them. We should find a way around this, as distinct might mean a lot of comparisons, which might have quite an impact at such numbers.
It is not that hard to prevent creating duplicates: Do not add ""
to your letters.
Looking at your own approach, if you only stream the actual characters, and add ""
only after each flatMap
, you get exactly what you need:
Condsider letters = {a, b}, maxLength = 2
Start with Stream.of("")
1st flatMap: [a, b]
add "": [, a, b]
2nd flatMap: [a, b, aa, ab, ba, bb]
add "": [, a, b, aa, ab, ba, bb]
Here is a nice method that does exactly that:
private static List<String> maxWords(int maxLength, String[] letters)
{
if (letters.length == 0)
{
return Arrays.asList("");
}
Stream<String> s = Stream.of("");
int[] capacity = { 1 };
for (int i = 0; i < maxLength; i++)
{
s = Stream.concat(Stream.of(""), s.flatMap(word -> Arrays.stream(letters).map(word::concat)));
capacity[0] = capacity[0] + (int) Math.pow(letters.length, i + 1);
}
return s.collect(Collectors.toCollection(() -> new ArrayList<>(capacity[0])));
}
This alternative is based on Aaron's idea, which I like even more. We first enumerate from 0 to capacity-1, and then map each number to its corresponding word. I strongly recommend watching this video about the Library of Babel (from minute 17) for an explanation if you are interested.
private static List<String> maxWords2(int maxLength, String[] letters)
{
if (letters.length == 0)
{
return Arrays.asList("");
}
int capacity = IntStream.rangeClosed(0, maxLength)
.map(length -> (int) Math.pow(letters.length, length))
.sum();
return IntStream.range(0, capacity).mapToObj(i -> {
StringBuilder s = new StringBuilder();
while (i > 0)
{
i--;
s.append(letters[i % letters.length]);
i /= letters.length;
}
return s.reverse().toString();
}).collect(Collectors.toCollection(() -> new ArrayList<>(capacity)));
}
Upvotes: 4