Hackerrank's Merging Communities
Simplifying


        for(int i = 0; i &lt; disjointSet.size(); i++) {
            Map&lt;Integer, Set&lt;Integer&gt;&gt; map = disjointSet.get(i);
            if(map.containsKey(first)) {
                firstSet = map.get(first);
            } else if(map.containsKey(second)) {
                secondSet = map.get(second);
            }
        }



Consider instead 

        for (Map&lt;Integer, Set&lt;Integer&gt;&gt; map : disjointSet) {
            if (firstSet == null) {
                firstSet = map.get(first);
            }

            if (secondSet == null) {
                secondSet = map.get(second);
            }

            if (firstSet != null &amp;&amp; secondSet != null) {
                break;
            }
        }


You don't need i.  Java can iterate over a collection directly, which saves us a line of code.  

You don't need to check containsKey before doing get.  If containsKey is false, then get will return null.  

You might as well stop as soon as both sets are found.  You don't need to keep going.  

A linear scan over all the values is something like \$\mathcal{O}(N \cdot Q)\$, assuming the number of merges is proportional to the number of queries.  Because you have to do one linear scan for each merge.  However, there is an alternative solution that is closer to \$\mathcal{O}(Q)\$.  

Alternative data structure


        DisjointSet ds = new DisjointSet();



Consider 

        DisjointSet ds = new DisjointSet(N + 1);


Now you can change from 


    private List&lt;Map&lt;Integer, Set&lt;Integer&gt;&gt;&gt; disjointSet;



to something fixed size.  The normal representation for a disjoint set is a parent pointer tree with ranks added.  In this case, we need to track the sizes and can use those to represent the rank.  Consider 

public class DisjointSet {

    private int[] parents;
    private int[] sizes;

    DisjointSet(int N) {
        parents = new int[N];
        sizes = new int[N];
    }

    public void makeSet(int i) {
        parents[i] = i;
        sizes[i] = 1;
    }

    public void union(int a, int b) {
        int first = find(a);
        int second = find(b);

        // if already part of the same set, no need to union
        if (first == second) {
            return;
        }

        if (sizes[first] &lt; sizes[second]) {
            parents[first] = second;
            sizes[second] += sizes[first];
        } else {
            parents[second] = first;
            sizes[first] += sizes[second];
        }
    }

    public int find(int i) {
        // if not the root
        if (parents[i] != i) {
            // Make the parent the root, so that it will recurse at most once
            // on subsequent calls.
            parents[i] = find(parents[i]);
        }

        // Return the root (as the parent is always the root by this point).
        return parents[i];
    }

    public int getSetSize(int i) {
        return sizes[find(parents[i])];
    }

}


The parents array stores the immediate parent of any element.  If an element is alone in a set, it is its own parent.  Root nodes (terminators) are also their own parents. 

For a root node, the sizes array holds the size of the set.  For other nodes, it holds junk data.  

This uses both union-by-rank and path compression for an optimal runtime.  It's also reasonably efficient in space, requiring only two integers for each element.