Start learning free

LESSON 272 OF 437 · HASH TABLE

Intersection of Two Arrays in C++: Hash Map in O(n + m) Time

Practice O(n + m) Read the full explanation

Given two integer arrays first and second, return an array of their intersection. Each element in the result must be unique, and the result must be sorted in ascending order.

 

Example 1:

Input: first = [1,2,2,1], second = [2,2]
Output: [2]

Example 2:

Input: first = [4,9,5], second = [9,4,9,8,4]
Output: [4,9]
Explanation: 4 and 9 are in both arrays. Each appears twice in second, but only once in the result.

 

Constraints:

  • 1 <= first.length, second.length <= 1000
  • -1000 <= first[i], second[i] <= 1000
Note:

Your algorithm should run in O(n + m) time, plus the time to sort the shared numbers, where n and m are the lengths of first and second.

STEP 1 OF 12
    Ctrl + Enter to check

    C++ · HASH TABLE

    Intersection Of Two Arrays in C++, explained

    Problem overview

    The method intersection receives two vector<int> references, first and second. It returns a new vector with the numbers that appear in both. Each number appears once in the result however often it occurs in the inputs, and the result is sorted in ascending order. Each input holds between 1 and 1000 numbers, from -1000 to 1000.

    For first = [1, 2, 2, 1] and second = [2, 2] the answer is [2]. For first = [4, 9, 5] and second = [9, 4, 9, 8, 4] the answer is [4, 9]: both numbers occur in both vectors, 8 occurs only in second, and 5 only in first.

    The lesson asks for O(n + m) time, plus the time to sort the shared numbers, where n and m are the two lengths.

    Approach: remember one vector, then scan the other

    The brute-force way checks, for each number in one vector, whether it occurs anywhere in the other. That is O(n * m). A hash table answers the question "have I seen this number?" in constant time on average, so each vector needs to be walked only once.

    The plan has three parts:

    1. Put every number of the shorter vector into an unordered_map with a count of 0. Duplicates collapse into a single key, which gives the uniqueness the result needs.
    2. Walk the longer vector. For each number that is a key, add 1 to its count. Numbers that aren't keys are ignored. Afterwards, a key with a count above 0 appeared in both vectors.
    3. Collect those keys and sort them. A hash map hands out its entries in no particular order, while the answer must be ascending.

    On first = [4, 9, 5] and second = [9, 4, 9, 8, 4], the first vector is the shorter, so the map starts as 4: 0, 9: 0, 5: 0. Walking second gives 9: 2, 4: 2, with 8 skipped and 5 left at 0. The keys with a positive count are 4 and 9, and sorting them gives [4, 9].

    Putting the shorter vector into the map keeps the map small. It never holds more than min(n, m) keys.

    Step by step

    1. Copy both inputs into nums1 and nums2 with auto. The deduced type is vector<int>, so these are real copies and the next step can reorder them without touching the caller's vectors.
    2. Declare unordered_map<int, int> d. Keys are numbers from the shorter vector and values are match counts.
    3. Swap if nums1 is longer. After swap(nums1, nums2), nums1 is the shorter one.
    4. Loop over nums1 with int i.
    5. Set d[i] = 0. This registers each number once. A repeated number just sets the same key to 0 again.
    6. Loop over nums2, again with int i.
    7. Skip numbers that aren't keys. d.find(i) == d.end() means the number isn't in the map, so continue.
    8. Otherwise add one with d[i] = d[i] + 1. A key with a count above 0 now records a number found in both vectors.
    9. Declare vector<int> result to hold the answer.
    10. Loop over the map with the structured binding auto& [k, val], which names the key and the value of each entry.
    11. Keep the keys whose val is above 0 with result.push_back(k).
    12. Sort result and return it. The order of a hash map's entries is unspecified, so the sort is what makes the result ascending.

    The solution in C++

    class Solution {
    public:
        vector<int> intersection(vector<int>& first, vector<int>& second) {
            auto nums1 = first;
            auto nums2 = second;
            unordered_map<int, int> d;
            if (nums1.size() > nums2.size()) swap(nums1, nums2);
            for (int i : nums1) {
                d[i] = 0;
            }
            for (int i : nums2) {
                if (d.find(i) == d.end()) continue;
                else d[i] = d[i] + 1;
            }
            vector<int> result;
            for (auto& [k, val] : d) {
                if (val > 0) result.push_back(k);
            }
            sort(result.begin(), result.end());
            return result;
        }
    };
    

    C++ details worth a closer look:

    • auto copies. auto nums1 = first; deduces vector<int>, not a reference, so it makes a copy. Writing auto& nums1 = first; would bind a reference and the later swap would exchange the contents of the caller's vectors.
    • swap on vectors is cheap. swap(nums1, nums2) exchanges the internal buffers without copying the elements, so it takes constant time.
    • find does not insert. d.find(i) returns an iterator, and d.end() means "not found". Using d[i] for the test instead would insert every missing number with the value 0 and break the logic that only the shorter vector's numbers are keys.
    • Assignment through operator[]. d[i] = 0 inserts the key if it is missing and overwrites the value if it exists. Since the shorter vector is fully registered before the second loop starts, no count is lost.
    • Structured bindings. auto& [k, val] (C++17) unpacks each map entry, which is a pair<const int, int>. The key k is const because changing a key inside a hash map is not allowed. val is a reference to the stored count.
    • Result order. The loop over d visits entries in an unspecified order, so the final sort is required for the ascending result. Without it the output could be right on one input and wrong on another.

    Complexity

    Time: O(n + m) on average, plus the sort. Copying the vectors, filling the map and scanning the longer vector are each linear, and every hash operation takes constant time on average. Collecting the keys visits the map once. Sorting a result of r shared numbers costs O(r log r), which is where the lesson's "plus the time to sort the shared numbers" comes in. Since r is at most min(n, m), the sort never costs more than O(min(n, m) log min(n, m)).

    Space: O(n + m). The two copies take O(n + m). The map and the result each hold at most min(n, m) numbers.

    Edge cases and common mistakes

    • No common numbers. [1, 2] and [3, 4] leave every count at 0, so the result is an empty vector.
    • Duplicates. [1, 2, 2, 1] and [2, 2] give [2], not [2, 2]. The map has one key per distinct number, and the result is built from keys.
    • Inputs of different lengths. The swap makes the first loop always cover the shorter vector. The answer is the same either way, but the map stays small.
    • Negative numbers. Values from -1000 to 1000 work as map keys, and sort orders negative numbers before positive ones, so [-1, 0] and [0, -1] give [-1, 0].
    • Skipping the sort. An unordered_map doesn't remember insertion order or sort its keys, so the result must be sorted explicitly.
    • Using d[i] to test membership. It inserts missing keys. Use find (or count) to look without changing the map.

    Testing

    • first = [1, 2, 2, 1], second = [2, 2] gives [2].
    • first = [4, 9, 5], second = [9, 4, 9, 8, 4] gives [4, 9].
    • first = [1, 2], second = [3, 4] gives [].
    • first = [-1, 0, 7], second = [7, 0, -1, -1] gives [-1, 0, 7].
    Keep going with C++About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free