Search code examples
javaabstract-classanagram

Group Anagrams - LeetCode


Today I came across a solution for this problem: https://leetcode.com/problems/group-anagrams/submissions/

The author used AbstractList to solve this problem. And the solution looks somewhat like this

import java.util.AbstractList;

class Solution {
    private List<List<String>> result;
    private HashMap<String, List<String>> map = new HashMap<>();
    
    public List<List<String>> groupAnagrams(String[] strs) {
        
        return new AbstractList<>() {
            public List<String> get(int key) {
                if (result == null)
                    init(strs);
                return result.get(key);
            }
            
            public int size() {
                if (result == null)
                    init(strs);
                return result.size();
            }      
        };
    }
    
    private void init(String[] strs) {
        for (String str : strs) {
            char[] ch_map = str.toCharArray();
            Arrays.sort(ch_map);
            String key = String.valueOf(ch_map);

            if (!map.containsKey(key))
                map.put(key, new ArrayList<>());
            map.get(key).add(str);
        }

        result = new ArrayList<>(map.values());
    }
}

The produced test result is 0ms ~ 1ms.

If I removed

if (result == null)
    init(strs);

from get(int key) and int size(). Then I put the init(strs) to the start of the function like this:

...
    public List<List<String>> groupAnagrams(String[] strs) {
        init(strs);
        return new AbstractList<>() {
            public List<String> get(int key) {
                return result.get(key);
            }
            
            public int size() {
                return result.size();
            }      
        };
    }
...

The test result for this case is 10ms ~ 15ms.

I tried to print the number of time init(strs) is called, but it returned 1 time for both cases.

My question is why it is significantly faster when you put the init(strs) inside the AbstractClass ?


Solution

  • Theory

    The solution you've presented is simply a hack that works around the way LeetCode verifies the execution time. In case of the given exercise the whole run could look like the following:

    1. Prepare an array that will be passed to the test.
    2. Start counting time.
    3. Execute the solution provided by a user.
    4. Stop counting time.
    5. Verify the result returned by the solution.

    The only thing the "hacky" code is doing during the groupAnagrams method execution is creation of an anonymous class instance that is then returned. The actual evaluation of the logic required to complete the task is done only after the get() or size() method have been called, which is part of the verification stage and as assumed above - the time is not verified by LeetCode there.

    Practice

    I've prepared a GitHub repository with a simple project that verifies the differences between both solutions. As predicted - only small part of the work is done during "result list" calculation (time elapsed from the start in nanoseconds):

    Result calculated:      4833
    > Verification done:    47666
    

    While this is how it looks for the "standard" solution:

    Result calculated:      42916
    > Verification done:    44875
    

    Note

    Normally JMH should be used for benchmarking, but I wanted the verification to be as explicit as possible, so I've used simple printing of time differences in nanoseconds to console. JVM warmup was performed in the code I've prepared and the results seem to be reasonable.