I’ve been working on Leetcode questions for the past few days, and whenever possible, I’ve been experimenting with functional programming approaches, despite the fact that it might be slower compared to imperative programming.
As an example, when I tackled Leetcode question 49 - Group Anagrams, I chose to implement a functional programming solution. Below, you’ll find the code I came up with, including how I usually setup my test class for Leetcode purposes.
package dev.jcf.leetcode.arraysandhashing;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
public class Medium_49_GroupAnagramsTest {
@Test
void groupAnagramsTest() throws Exception {
List<List<String>> expected = Arrays.asList(
Arrays.asList("tan", "nat"),
Arrays.asList("eat", "tea", "ate"),
Arrays.asList("bat")
);
assertThat(Solution.groupAnagrams(new String[]{"eat", "tea", "tan", "ate", "nat", "bat"}))
.containsExactlyInAnyOrderElementsOf(expected);
}
@Test
void groupAnagramsEdgeCaseTest1() throws Exception {
List<List<String>> expected = Arrays.asList(
Arrays.asList("bdddddddddd"),
Arrays.asList("bbbbbbbbbbc")
);
assertThat(Solution.groupAnagrams(new String[]{"bdddddddddd", "bbbbbbbbbbc"}))
.containsExactlyInAnyOrderElementsOf(expected);
}
private static class Solution {
private static List<List<String>> groupAnagrams(String[] strs) {
return Arrays.stream(strs)
.map(it -> {
// create character array and count occurrence.
int[] chars = new int[26];
for (char c : it.toCharArray()) {
chars[c - 'a']++;
}
// create a map entry
return Map.entry(
// generate the hash as the map key
Arrays.stream(chars)
.mapToObj(String::valueOf)
.collect(Collectors.joining("#")),
// the value associated with the key
it);
}).collect(Collectors.groupingBy( // group string by their hashes
Map.Entry::getKey,
Collectors.mapping(
Map.Entry::getValue,
Collectors.toList())))
.values() // The following is to comply with the return type.
.stream()
.toList();
}
}
}
The approach here involves generating a hash for each string. This hash is computed by creating an array with 26 elements, counting the occurrence of each character, and then joining them together as a string with a # delimiter. The purpose of this delimiter is to ensure that inputs such as {“bdddddddddd”, “bbbbbbbbbbc”} will produce distinct hashes.
With the delimiter, the hashes respectively are:
0#1#0#10#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0
0#10#1#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0
In the first string, there is one ‘b’ and ten ’d’s, while the second string contains ten ‘b’s and one ‘c’.
Without the delimiter, the hashes will look the same despite them not being anagrams.
010100000000000000000000000 // Is this 10 b's and 1 c or 1 b and 1 d's?
010100000000000000000000000
After completing my solution, I realized it was too long and not as readable as I desired. I turned to ChatGPT for a refactor, and here’s what it came up with."
private static class Solution {
private static List<List<String>> groupAnagrams(String[] strs) {
return Arrays.stream(strs)
.collect(Collectors.groupingBy(
str -> {
int[] chars = new int[26];
for (char c : str.toCharArray()) {
chars[c - 'a']++;
}
return Arrays.toString(chars);
},
Collectors.toList()
))
.values()
.stream()
.toList();
}
}
I was surprised at how much of the code was improved. What surprised me the most was that the line Arrays.stream(chars).mapToObj(String::valueOf).collect(Collectors.joining("#"))
was replaced with Arrays.toString(chars)
. At first, I wondered about the delimiter, but it turns out that if you read the documentation of Arrays.toString(), it says the following:
The string representation consists of a list of the array’s elements, enclosed in square brackets ("[]"). Adjacent elements are separated by the characters “, " (a comma followed by a space).
This means the hashes for {"bdddddddddd","bbbbbbbbbbc"}
are the following:
[0, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
In closing, I will be continuing to practice answering Leetcode questions using functional programming, and with the assistance of ChatGPT, I’ll be able to discover new approaches I might not have considered on my own. Thanks ChatGPT. Please don’t take away our jobs.