Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine if a list composed of anagram elements in Java 8

I want to determine if a list is anagram or not using Java 8.

Example input:

"cat", "cta", "act", "atc", "tac", "tca"

I have written the following function that does the job but I am wondering if there is a better and elegant way to do this.

boolean isAnagram(String[] list) {
    long count = Stream.of(list)
            .map(String::toCharArray)
            .map(arr -> {
                Arrays.sort(arr);
                return arr;
            })
            .map(String::valueOf)
            .distinct()
            .count();
    return count == 1;

}

It seems I can't sort char array with Stream.sorted() method so that's why I used a second map operator. If there is some way that I can operate directly on char stream instead of Stream of char array, that would also help.

like image 974
Limonkufu Avatar asked Oct 29 '18 14:10

Limonkufu


3 Answers

Alternatively an updated version of your implementation that could work would be:

boolean isAnagram(String[] list) {
    return Stream.of(list) // Stream<String>
            .map(String::toCharArray) // Stream<char[]>
            .peek(Arrays::sort) // sort 
            .map(String::valueOf) // Stream<String>
            .distinct() //distinct
            .count() == 1;
}
like image 166
Naman Avatar answered Sep 19 '22 13:09

Naman


Instead of creating and sorting a char[] or int[], which can not be done inline and thus "breaks" the stream, you could get a Stream of the chars in the Strings and sort those before converting them to arrays. Note that this is an IntSteam, though, and String.valueOf(int[]) will include the array's memory address, which is not very useful here, so better use Arrays.toString in this case.

boolean anagrams = Stream.of(words)
        .map(String::chars).map(IntStream::sorted)
        .map(IntStream::toArray).map(Arrays::toString)
        .distinct().count() == 1;

Of course, you can also use map(s -> Arrays.toString(s.chars().sorted().toArray())) instead of the series of four maps. Not sure if there's a (significant) difference in speed, it's probably mainly a matter of taste.

Also, you could use IntBuffer.wrap to make the arrays comparable, which should be considerably faster than Arrays.toString (thanks to Holger in comments).

boolean anagrams = Stream.of(words)
        .map(s -> IntBuffer.wrap(s.chars().sorted().toArray()))
        .distinct().count() == 1;
like image 10
tobias_k Avatar answered Nov 18 '22 12:11

tobias_k


I would not deal with counting distinct values, as that’s not what you are interested in. What you want to know, is whether all elements are equal according to a special equality rule.

So when we create a method to convert a String to a canonical key (i.e. all characters sorted)

private CharBuffer canonical(String s) {
    char[] array = s.toCharArray();
    Arrays.sort(array);
    return CharBuffer.wrap(array);
}

we can simply check whether all subsequent elements are equal to the first one:

boolean isAnagram(String[] list) {
    if(list.length == 0) return false;
    return Arrays.stream(list, 1, list.length)
        .map(this::canonical)
        .allMatch(canonical(list[0])::equals);
}

Note that for method references of the form expression::name, the expression is evaluate once and the result captured, so canonical(list[0]) is evaluated only once for the entire stream operation and only equals invoked for every element.

Of course, you can also use the Stream API to create the canonical keys:

private IntBuffer canonical(String s) {
    return IntBuffer.wrap(s.chars().sorted().toArray());
}

(the isAnagram method does not need any change)

Note that CharBuffer and IntBuffer can be used as lightweight wrappers around arrays, like in this answer, and implement equals and hashCode appropriately, based on the actual array contents.

like image 8
Holger Avatar answered Nov 18 '22 11:11

Holger