Skip to content

Merge Intervals & Group Anagrams ​

Classic array manipulation problems frequently asked in interviews and top tech companies

These two problems represent fundamental patterns in coding interviews: interval manipulation and hash-based grouping. Mastering these patterns unlocks solutions to dozens of related problems.


Merge Intervals ​

Problem Statement ​

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

LeetCode #56 - Medium

Example ​

Input: [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]

Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].
Input: [[1,4],[4,5]]
Output: [[1,5]]

Explanation: Intervals [1,4] and [4,5] are considered overlapping (they share endpoint 4).

Approach ​

Key Insight: Sorting intervals by start time guarantees that we can detect overlaps in a single linear scan without checking every possible pair.

Algorithm Steps:

  1. Sort the intervals by their start times - O(n log n)
  2. Initialize result with the first interval
  3. Iterate through remaining intervals:
    • If current interval overlaps with the last merged interval (start <= last_end), merge them by updating the end to max(last_end, current_end)
    • Otherwise, add the current interval to result
  4. Return the merged intervals

Overlap Detection: Two intervals [a, b] and [c, d] overlap if c <= b (when sorted by start time).

Mermaid Diagram ​

Solution ​

python
def merge(intervals: list[list[int]]) -> list[list[int]]:
    """
    Merge all overlapping intervals.

    Time: O(n log n) for sorting
    Space: O(n) for result (O(log n) for sorting in-place)
    """
    if not intervals:
        return []

    # Sort by start time
    intervals.sort(key=lambda x: x[0])
    result = [intervals[0]]

    for start, end in intervals[1:]:
        last_end = result[-1][1]

        if start <= last_end:  # Overlapping
            result[-1][1] = max(last_end, end)
        else:
            result.append([start, end])

    return result
java
public int[][] merge(int[][] intervals) {
    if (intervals == null || intervals.length == 0) return new int[0][0];

    // Sort by start time
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
    List<int[]> result = new ArrayList<>();
    result.add(intervals[0]);

    for (int i = 1; i < intervals.length; i++) {
        int start = intervals[i][0];
        int end   = intervals[i][1];
        int lastEnd = result.get(result.size() - 1)[1];

        if (start <= lastEnd) {  // Overlapping
            result.get(result.size() - 1)[1] = Math.max(lastEnd, end);
        } else {
            result.add(intervals[i]);
        }
    }

    return result.toArray(new int[0][]);
}

Complexity: Time O(n log n) · Space O(n)

  • Time: Sorting dominates at O(n log n); the subsequent linear merge scan is O(n)
  • Space: Result array stores up to n intervals; sorting uses O(log n) auxiliary space
python
# Alternative: More explicit version
def merge_verbose(intervals: list[list[int]]) -> list[list[int]]:
    """Same logic with clearer variable names."""
    if not intervals:
        return []

    # Sort intervals by start time
    sorted_intervals = sorted(intervals, key=lambda x: x[0])
    merged = []

    for interval in sorted_intervals:
        # If merged is empty or no overlap with last interval
        if not merged or interval[0] > merged[-1][1]:
            merged.append(interval)
        else:
            # Overlap exists - merge by extending the end time
            merged[-1][1] = max(merged[-1][1], interval[1])

    return merged

Complexity: Time O(n log n) · Space O(n)

  • Time: Sorting step is O(n log n); merging loop iterates through all n intervals in O(n)
  • Space: Merged array can hold all n intervals in worst case (no overlaps)

Complexity ​

AspectComplexityExplanation
TimeO(n log n)Dominated by sorting; merging is O(n)
SpaceO(n)For storing the result array

Note: If intervals are already sorted, time complexity reduces to O(n).

Google Follow-ups ​

1. What if intervals are already sorted? ​

If pre-sorted, skip the sorting step and achieve O(n) time complexity:

python
def merge_sorted(intervals: list[list[int]]) -> list[list[int]]:
    """Assumes intervals are already sorted by start time."""
    if not intervals:
        return []

    result = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= result[-1][1]:
            result[-1][1] = max(result[-1][1], end)
        else:
            result.append([start, end])
    return result

Complexity: Time O(n) · Space O(n)

  • Time: Linear scan through intervals since they are already sorted (no sorting step needed)
  • Space: Result array stores up to n intervals

2. Insert a new interval into sorted non-overlapping intervals ​

This is LeetCode #57 (Insert Interval) - a common interview question:

python
def insert(intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]:
    """
    Insert a new interval into sorted non-overlapping intervals.
    Merge if necessary.
    """
    result = []
    i = 0
    n = len(intervals)

    # Add all intervals that come before newInterval
    while i < n and intervals[i][1] < newInterval[0]:
        result.append(intervals[i])
        i += 1

    # Merge all overlapping intervals with newInterval
    while i < n and intervals[i][0] <= newInterval[1]:
        newInterval[0] = min(newInterval[0], intervals[i][0])
        newInterval[1] = max(newInterval[1], intervals[i][1])
        i += 1
    result.append(newInterval)

    # Add remaining intervals
    while i < n:
        result.append(intervals[i])
        i += 1

    return result

Complexity: Time O(n) · Space O(n)

  • Time: Three sequential while loops each traverse at most n intervals total (amortized O(n))
  • Space: Result array stores up to n + 1 intervals (all original plus the new one)

3. Find minimum meeting rooms needed (Meeting Rooms II) ​

This is LeetCode #253 - frequently asked in interviews:

python
import heapq

def minMeetingRooms(intervals: list[list[int]]) -> int:
    """
    Find minimum number of meeting rooms required.
    Uses min-heap to track end times of ongoing meetings.
    """
    if not intervals:
        return 0

    # Sort by start time
    intervals.sort(key=lambda x: x[0])

    # Min-heap to track end times of meetings in progress
    heap = []
    heapq.heappush(heap, intervals[0][1])

    for start, end in intervals[1:]:
        # If earliest ending meeting ends before this one starts
        if heap[0] <= start:
            heapq.heappop(heap)  # Reuse that room
        heapq.heappush(heap, end)

    return len(heap)

Complexity: Time O(n log n) · Space O(n)

  • Time: Sorting is O(n log n); each of n meetings does at most one heap push and pop, each O(log n)
  • Space: Heap can grow to hold all n meeting end times if all meetings overlap
python
# Alternative: Sweep Line Algorithm
def minMeetingRooms_sweepline(intervals: list[list[int]]) -> int:
    """
    Sweep line approach - process start and end events chronologically.
    """
    events = []
    for start, end in intervals:
        events.append((start, 1))   # Meeting starts
        events.append((end, -1))    # Meeting ends

    events.sort()  # Sort by time, ends before starts if same time

    max_rooms = 0
    current_rooms = 0
    for time, delta in events:
        current_rooms += delta
        max_rooms = max(max_rooms, current_rooms)

    return max_rooms

Complexity: Time O(n log n) · Space O(n)

  • Time: Creating 2n events is O(n); sorting events is O(n log n); single sweep is O(n)
  • Space: Events list stores 2n tuples (start and end event for each meeting)

4. Employee Free Time ​

Find common free time slots across all employees:

python
def employeeFreeTime(schedule: list[list[list[int]]]) -> list[list[int]]:
    """Find free time common to all employees."""
    # Flatten and sort all intervals
    all_intervals = []
    for employee in schedule:
        all_intervals.extend(employee)
    all_intervals.sort(key=lambda x: x[0])

    # Merge overlapping work times
    merged = [all_intervals[0]]
    for start, end in all_intervals[1:]:
        if start <= merged[-1][1]:
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])

    # Gaps between merged intervals are free times
    free_time = []
    for i in range(1, len(merged)):
        free_time.append([merged[i-1][1], merged[i][0]])

    return free_time

Complexity: Time O(n log n) · Space O(n)

  • Time: Flattening all intervals is O(n); sorting is O(n log n); merging and finding gaps are O(n)
  • Space: Merged list and free_time list each can hold up to O(n) intervals

Group Anagrams ​

Problem Statement ​

Given an array of strings strs, group the anagrams together. You can return the answer in any order.

An anagram is a word or phrase formed by rearranging the letters of a different word or phrase, using all the original letters exactly once.

LeetCode #49 - Medium

Example ​

Input: ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

Explanation:
- "ate", "eat", "tea" are anagrams (same letters rearranged)
- "nat", "tan" are anagrams
- "bat" has no anagram in the list
Input: [""]
Output: [[""]]
Input: ["a"]
Output: [["a"]]

Approach ​

Key Insight: Two strings are anagrams if and only if they have the same characters with the same frequencies. We can use this as a unique "signature" to group strings.

Two Main Approaches:

  1. Sorted String as Key: Sort each string - anagrams produce identical sorted strings
  2. Character Count as Key: Count character frequencies - anagrams have identical frequency distributions

Mermaid Diagram ​

Solution ​

python
from collections import defaultdict

def groupAnagrams(strs: list[str]) -> list[list[str]]:
    """
    Group anagrams using sorted string as key.

    Time: O(n * k log k) where n = number of strings, k = max string length
    Space: O(n * k) for storing all strings in hash map
    """
    groups = defaultdict(list)

    for s in strs:
        # Use sorted string as key - all anagrams produce same sorted string
        key = ''.join(sorted(s))
        groups[key].append(s)

    return list(groups.values())
java
public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();

    for (String s : strs) {
        // Use sorted string as key - all anagrams produce same sorted string
        char[] chars = s.toCharArray();
        Arrays.sort(chars);
        String key = new String(chars);
        groups.computeIfAbsent(key, x -> new ArrayList<>()).add(s);
    }

    return new ArrayList<>(groups.values());
}

Complexity: Time O(n * k log k) · Space O(n * k)

  • Time: For each of n strings, sorting k characters takes O(k log k), giving O(n * k log k) total
  • Space: Hash map stores all n strings; each string and its sorted key use O(k) space
python
def groupAnagrams_v2(strs: list[str]) -> list[list[str]]:
    """
    Alternative: Use character count tuple as key.
    More efficient for long strings since counting is O(k) vs sorting O(k log k).

    Time: O(n * k) where n = number of strings, k = max string length
    Space: O(n * k)
    """
    groups = defaultdict(list)

    for s in strs:
        # Create frequency count array for 26 lowercase letters
        count = [0] * 26
        for c in s:
            count[ord(c) - ord('a')] += 1
        # Use tuple as key (lists are not hashable)
        groups[tuple(count)].append(s)

    return list(groups.values())

Complexity: Time O(n * k) · Space O(n * k)

  • Time: For each of n strings, counting k characters takes O(k), giving O(n * k) total
  • Space: Hash map stores all n strings plus 26-element count tuples for keys
python
def groupAnagrams_v3(strs: list[str]) -> list[list[str]]:
    """
    Alternative: Use Counter for cleaner code.
    Slightly less efficient but more readable.
    """
    from collections import Counter

    groups = defaultdict(list)

    for s in strs:
        # frozenset of Counter items as key
        # Note: This doesn't work if characters can repeat different times
        # Use tuple(sorted(Counter(s).items())) instead
        key = tuple(sorted(Counter(s).items()))
        groups[key].append(s)

    return list(groups.values())

Complexity: Time O(n * k log k) · Space O(n * k)

  • Time: Counter creation is O(k) per string, but sorted() on items adds O(u log u) where u is unique chars (up to k)
  • Space: Hash map stores all n strings; Counter objects and their sorted tuples use O(k) space each

Complexity ​

MethodTimeSpaceBest For
SortingO(n * k log k)O(n * k)Short strings, simple implementation
CountingO(n * k)O(n * k)Long strings, performance critical

Where:

  • n = number of strings
  • k = maximum length of a string

Google Follow-ups ​

1. How would you handle Unicode characters? ​

For Unicode, we cannot use a fixed 26-element array. Use a dictionary or Counter instead:

python
from collections import Counter, defaultdict

def groupAnagrams_unicode(strs: list[str]) -> list[list[str]]:
    """
    Handle Unicode characters using Counter.
    Works for any character set.
    """
    groups = defaultdict(list)

    for s in strs:
        # Counter works with any characters
        # Convert to sorted tuple of items for consistent hashing
        char_count = Counter(s)
        key = tuple(sorted(char_count.items()))
        groups[key].append(s)

    return list(groups.values())

Complexity: Time O(n * k log k) · Space O(n * k)

  • Time: For each string, Counter is O(k), and sorting items is O(u log u) where u is unique characters
  • Space: Hash map stores n strings and Counter-based tuple keys proportional to character diversity
python
# Example with Unicode
strs = ["cafe", "face", "cafe", "face"]  # e with accent
# Each unique character is counted separately

Additional Unicode Considerations:

  • Normalization: Different Unicode representations of the same character (e.g., e vs e with combining accent)
  • Case folding: Some languages have complex case rules
  • Solution: Use unicodedata.normalize() before processing
python
import unicodedata

def normalize_string(s: str) -> str:
    """Normalize Unicode string for consistent comparison."""
    return unicodedata.normalize('NFC', s.lower())

2. What if strings are very long? ​

For very long strings:

  1. Use counting instead of sorting: O(k) vs O(k log k)
  2. Stream processing: If strings are too long to fit in memory
  3. Probabilistic hashing: Use polynomial hash for approximate grouping
python
def groupAnagrams_long_strings(strs: list[str]) -> list[list[str]]:
    """
    Optimized for very long strings using character counting.
    Avoids the O(k log k) sorting overhead.
    """
    groups = defaultdict(list)

    for s in strs:
        # Count characters in O(k) time
        count = [0] * 26
        for c in s:
            count[ord(c) - ord('a')] += 1
        groups[tuple(count)].append(s)

    return list(groups.values())

Complexity: Time O(n * k) · Space O(n * k)

  • Time: Character counting for each of n strings is O(k); no sorting needed with fixed 26-element array
  • Space: Hash map stores n strings; each key is a 26-element tuple (constant per string)

3. Find all anagrams in a string (Sliding Window) ​

Related problem - LeetCode #438:

python
def findAnagrams(s: str, p: str) -> list[int]:
    """
    Find all start indices of p's anagrams in s.
    Uses sliding window with character count comparison.
    """
    if len(p) > len(s):
        return []

    p_count = [0] * 26
    s_count = [0] * 26
    result = []

    # Initialize counts for first window
    for i in range(len(p)):
        p_count[ord(p[i]) - ord('a')] += 1
        s_count[ord(s[i]) - ord('a')] += 1

    if s_count == p_count:
        result.append(0)

    # Slide window
    for i in range(len(p), len(s)):
        # Add new character
        s_count[ord(s[i]) - ord('a')] += 1
        # Remove old character
        s_count[ord(s[i - len(p)]) - ord('a')] -= 1

        if s_count == p_count:
            result.append(i - len(p) + 1)

    return result

Complexity: Time O(n) · Space O(1)

  • Time: Initialization is O(p) for pattern; sliding window through s is O(n - p), comparing 26-element arrays is O(1)
  • Space: Two fixed 26-element count arrays regardless of input size

This problem was asked alongside Group Anagrams in a Google phone screen:

python
def generateAbbreviations(word: str) -> list[str]:
    """
    Generate all possible abbreviations of a word.
    Example: "word" -> ["word", "1ord", "w1rd", "wo1d", "wor1",
                        "2rd", "w2d", "wo2", "1o1d", ...]
    """
    result = []

    def backtrack(pos: int, current: str, count: int):
        if pos == len(word):
            if count > 0:
                current += str(count)
            result.append(current)
            return

        # Abbreviate current character
        backtrack(pos + 1, current, count + 1)

        # Keep current character
        if count > 0:
            current += str(count)
        backtrack(pos + 1, current + word[pos], 0)

    backtrack(0, "", 0)
    return result

Complexity: Time O(n * 2^n) · Space O(n)

  • Time: 2^n possible abbreviations (each character can be abbreviated or kept); each abbreviation takes O(n) to build
  • Space: Recursion stack depth is O(n); result stores 2^n strings but is considered output space

Common Patterns and Techniques ​

Hash Map Grouping Pattern ​

Both problems demonstrate the power of using hash maps with computed keys:

python
# Generic pattern for grouping by computed key
def group_by_key(items, key_func):
    groups = defaultdict(list)
    for item in items:
        key = key_func(item)
        groups[key].append(item)
    return list(groups.values())

# Usage for Group Anagrams
result = group_by_key(strs, lambda s: ''.join(sorted(s)))

Interval Processing Pattern ​

The Merge Intervals problem introduces the interval processing pattern used in many problems:

  1. Sort by start time
  2. Track current/merged state
  3. Update state based on overlap condition
  4. Handle edge cases (empty, single, touching intervals)

Past Interview References ​

Based on documented interview experiences:

  1. Merge Intervals Variation: "Given a sorted list of disjoint intervals and a new interval, merge them into a sorted list of disjoint intervals." - This tests understanding of the insert operation with pre-sorted input.

  2. Meeting Rooms II: Frequently asked as a follow-up to understand if candidates can extend interval concepts to resource allocation problems.

  3. Group Anagrams + Generalized Abbreviation: Asked together in a Google phone screen - tests both hashing skills and backtracking.

  4. Real-World Applications Discussed:

    • Calendar scheduling and conflict detection
    • CPU task scheduling
    • Genomic range queries
    • Spell checkers and word games (anagrams)

Key Takeaways ​

ProblemCore TechniqueKey Insight
Merge IntervalsSort + Linear ScanSorting by start enables O(n) merge
Group AnagramsHash Map GroupingAnagrams share a canonical form

Interview Tips:

  1. Start with the sorting approach for both - it is easier to explain and implement
  2. Discuss time/space trade-offs before optimizing
  3. Consider edge cases: empty input, single element, all overlapping/all anagrams
  4. Mention real-world applications to demonstrate deeper understanding

Sources ​