Solve Your First 3 Blind 75 Problems

Solve 3 Blind 75 problems using hash maps to optimize from O(n²) to O(n).

Introduction

30 Second Summary

Technical interviews love to test whether you can improve a slow solution into a fast one. The skill of spotting when a simple data structure swap can make your code 100x faster is what separates candidates who pass from those who don't.

In this project, you will solve 3 real Blind 75 interview problems using the Arrays & Hashing pattern. You'll start each problem with a brute force solution, then optimize it using hash maps and hash sets.

What You'll Build

A local Python project containing a problem-solving template and 3 fully-solved Blind 75 problems, each with both brute force and optimized solutions.

By the end of this project, you'll have:

  • A reusable 5-step problem-solving template you can apply to any DSA interview problem.
  • 3 solved Blind 75 problems (Contains Duplicate, Two Sum, Group Anagrams) with both brute force and optimal solutions.
  • A clear understanding of when and why to use hash maps to optimize from O(n^2) to O(n).
  • Secret Mission: Solve a 4th Blind 75 problem (Top K Frequent Elements) entirely on your own using the template.

Are there any prerequisites?

You should be comfortable with basic Python: functions, loops, lists, and dictionaries. You don't need any prior experience with data structures and algorithms or technical interview prep.

Set Up Your Project and Template

Every interview problem feels different on the surface, but the approach to solving them stays the same. Before you tackle any code, you need a repeatable system.

In this step, you'll create your project folder and build a 5-step problem-solving template. This template becomes your go-to framework for every Blind 75 problem you solve from here on out.

In this step, get ready to:
  • Create the project folder and open it in VS Code.
  • Write a reusable problem-solving template with 5 steps.
  • Run the template to confirm everything works.
Create the project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running this command:
mkdir blind75-arrays-hashing && cd blind75-arrays-hashing
  • Confirm the folder exists by running:
ls ~/Desktop

You should see blind75-arrays-hashing in the output.

Now open this folder in VS Code.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the blind75-arrays-hashing folder, then click Open.

Don't see the folder on your Desktop?

Make sure you ran the mkdir command after running cd ~/Desktop. If you skipped the first command, the folder may have been created in your home directory instead.

Help me find my project folder.

Write the problem-solving template

The template encodes a 5-step system that interviewers expect to see: understand the problem, write the simplest correct solution, analyze its complexity, optimize using a better data structure, then test.

  • In the VS Code Explorer sidebar, right-click in the empty space and select New File.
  • Name the file template.py and press Enter.
  • Copy the following code into template.py:
"""
Problem-Solving Template (5-Step Framework)
============================================
Use this template for every Blind 75 problem you solve.

Step 1: UNDERSTAND
- Restate the problem in your own words
- What are the inputs? What are the outputs?
- What are the constraints?
- What are the edge cases?

Step 2: BRUTE FORCE
- What is the simplest correct solution?
- Don't worry about efficiency yet

Step 3: ANALYZE
- What is the time complexity of the brute force?
- What is the space complexity?
- Where is the bottleneck?

Step 4: OPTIMIZE
- Can a hash map/set eliminate a nested loop?
- Can sorting help?
- Can I trade space for time?

Step 5: TEST
- Run through examples by hand
- Test edge cases
- Verify with assert statements
"""


def brute_force():
    """Write your brute force solution here."""
    pass


def optimal():
    """Write your optimized solution here."""
    pass


# --- Tests ---
if __name__ == "__main__":
    # Add your test cases here using assert statements
    # Example: assert optimal([1, 2, 3]) == expected_output
    print("All tests passed!")

What does this template do?

The big docstring at the top is your framework checklist. Each time you tackle a new problem, you'll fill in those 5 sections with your own thinking.

  • UNDERSTAND forces you to restate the problem and catch edge cases before writing code.
  • BRUTE FORCE gets you a working solution first. This proves you understand the problem.
  • ANALYZE identifies the time complexity bottleneck so you know what to fix.
  • OPTIMIZE is where hash maps and sets come in to replace slow nested loops.
  • TEST uses assert statements to verify your code automatically.

Why start with brute force?

Interviewers explicitly want to see you start simple. A brute force solution proves you understand the problem and gives you a correct baseline to improve from.

Jumping straight to an optimal solution is risky. If you get stuck, you have nothing to show. Starting with brute force guarantees partial credit and makes optimization systematic.

  • Save template.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to running it.

ⓧ I'd like to double check the full code

Here's the complete template.py file. Make sure yours matches exactly:

"""
Problem-Solving Template (5-Step Framework)
============================================
Use this template for every Blind 75 problem you solve.

Step 1: UNDERSTAND
- Restate the problem in your own words
- What are the inputs? What are the outputs?
- What are the constraints?
- What are the edge cases?

Step 2: BRUTE FORCE
- What is the simplest correct solution?
- Don't worry about efficiency yet

Step 3: ANALYZE
- What is the time complexity of the brute force?
- What is the space complexity?
- Where is the bottleneck?

Step 4: OPTIMIZE
- Can a hash map/set eliminate a nested loop?
- Can sorting help?
- Can I trade space for time?

Step 5: TEST
- Run through examples by hand
- Test edge cases
- Verify with assert statements
"""


def brute_force():
    """Write your brute force solution here."""
    pass


def optimal():
    """Write your optimized solution here."""
    pass


# --- Tests ---
if __name__ == "__main__":
    # Add your test cases here using assert statements
    # Example: assert optimal([1, 2, 3]) == expected_output
    print("All tests passed!")
Run the template to verify your setup

Running the template now confirms that Python is working and your file has no syntax errors.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
  • Run your template by entering this command:
python3 template.py

You should see this output:

All tests passed!

Seeing an error instead?

  • If you see SyntaxError, check that the triple quotes """ at the top and bottom of the docstring match. A missing quote breaks the entire file.
  • If you see No such file or directory, your terminal is not in the blind75-arrays-hashing folder. Run cd ~/Desktop/blind75-arrays-hashing first.
  • If you see command not found: python3, Python may not be installed. Try running python --version instead (without the 3).

Help me fix my Python setup.

Your framework is locked and loaded. Next up, you'll use it to solve your first Blind 75 problem: Contains Duplicate.

Solve Contains Duplicate

Your template gives you a repeatable system for attacking any problem. Now it's time to put that system to work on a real Blind 75 interview question.

Contains Duplicate is one of the simplest problems in the Arrays & Hashing category. It's the perfect starting point because the brute force solution is obvious, and the optimization demonstrates exactly why hash sets exist.

In this step, get ready to:
  • Write the problem statement and complexity analysis for Contains Duplicate.
  • Implement both a brute force and an optimal hash set solution.
  • Run test cases to verify both solutions work correctly.
Create the file and write the brute force solution

The problem asks: given an integer array, return True if any value appears at least twice. Your first job is to restate the problem, identify constraints, and write the simplest correct solution.

  • In VS Code, create a new file called 01_contains_duplicate.py in your blind75-arrays-hashing folder.
  • Add the following problem statement and analysis as a docstring at the top of the file:
"""
Contains Duplicate (LeetCode 217) - Easy
=========================================

UNDERSTAND:
- Given an integer array nums, return True if any value appears
  at least twice, return False if every element is distinct.
- Input: list of integers
- Output: boolean
- Constraints: 1 <= nums.length <= 10^5, -10^9 <= nums[i] <= 10^9
- Edge cases: single element (always False), all same elements (True)

BRUTE FORCE:
- For each element, check every other element for a match
- Time: O(n^2), Space: O(1)

OPTIMIZE:
- Use a set for O(1) lookup instead of inner loop
- As we iterate, check if number is already in set
- Time: O(n), Space: O(n)
"""

What does this docstring do?

This follows your 5-step template. The UNDERSTAND section restates the problem, names inputs/outputs, and lists edge cases. The BRUTE FORCE and OPTIMIZE sections plan both solutions before you write any code.

Writing this analysis first is what interviewers want to see. It proves you think before you code.

The analysis describes a brute force approach: for each element, check every other element. That's a nested loop.

  • Below the docstring, add the brute force function:
def contains_duplicate_brute(nums):
    """Brute force: check every pair. O(n^2) time, O(1) space."""
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False

What does this code do?

  • The outer loop picks each element one by one with index i.
  • The inner loop compares it against every element after it with index j. Starting at i + 1 avoids comparing an element to itself.
  • If any pair matches, return True immediately. If no pair matches after all comparisons, return False.
  • This is O(n^2) time complexity because for n elements, you check roughly n*n/2 pairs. It uses O(1) space because no extra data structures are needed.
  • Save 01_contains_duplicate.py.
  • Run the file to confirm there are no syntax errors:
python3 01_contains_duplicate.py

You should see no output and no errors. The file runs cleanly because the function is valid Python, even without test cases yet.

Seeing a SyntaxError?

  • Check that the triple-quoted docstring has matching opening and closing triple quotes.
  • Make sure the def line ends with a colon and the body is indented with 4 spaces.
  • Confirm there are two blank lines between the closing """ and the def keyword.

Help me fix a SyntaxError in my Contains Duplicate brute force function.

Write the optimal hash set solution

The brute force works, but it's slow. The nested loop is the bottleneck. Every time you check "have I seen this number before?", you scan the entire array again. A hash set answers that question in O(1) time.

  • Below the contains_duplicate_brute function, add the optimal solution:
def contains_duplicate(nums):
    """Optimal: use a hash set. O(n) time, O(n) space."""
    seen = set()
    for num in nums:
        if num in seen:
            return True
        seen.add(num)
    return False

What does this code do?

  • seen = set() creates an empty hash set to track numbers you've already encountered.
  • For each number, if num in seen checks membership in O(1) time. This replaces the entire inner loop.
  • If the number isn't in the set, seen.add(num) stores it for future lookups.
  • The trade-off: you use O(n) extra space (the set) to bring time down from O(n^2) to O(n). One loop instead of two nested loops.

Why is this the key pattern?

This is the core Arrays & Hashing insight you'll use again and again. Whenever you see a nested loop that searches for something, ask: "Can I store values in a hash set or hash map and look them up in O(1) instead?" That single question eliminates most O(n^2) brute forces.

  • Save 01_contains_duplicate.py. You won't see output yet. The next substep adds test cases that verify both solutions.
Add test cases and verify both solutions

Both functions are written, but you need proof they actually work. Assert statements crash the program if a test fails, giving you immediate feedback.

  • At the bottom of 01_contains_duplicate.py, add the test section:
# --- Tests ---
if __name__ == "__main__":
    # Test brute force
    assert contains_duplicate_brute([1, 2, 3, 1]) == True
    assert contains_duplicate_brute([1, 2, 3, 4]) == False
    assert contains_duplicate_brute([1, 1, 1, 3, 3, 4, 3, 2, 4, 2]) == True
    print("Brute force: all tests passed!")

    # Test optimal
    assert contains_duplicate([1, 2, 3, 1]) == True
    assert contains_duplicate([1, 2, 3, 4]) == False
    assert contains_duplicate([1, 1, 1, 3, 3, 4, 3, 2, 4, 2]) == True
    assert contains_duplicate([1]) == False
    print("Optimal: all tests passed!")

What do these tests cover?

  • [1, 2, 3, 1] has a duplicate (1 appears twice). Should return True.
  • [1, 2, 3, 4] has all unique elements. Should return False.
  • [1, 1, 1, 3, 3, 4, 3, 2, 4, 2] has multiple duplicates. Should return True.
  • [1] is an edge case. A single element can never have a duplicate. Should return False.

The if __name__ == "__main__": guard ensures tests only run when you execute the file directly, not when it's imported.

  • Save 01_contains_duplicate.py.
  • Run the file to verify all tests pass:
python3 01_contains_duplicate.py

You should see two lines of output: Brute force: all tests passed! and Optimal: all tests passed!.

Seeing an AssertionError?

  • Check that contains_duplicate_brute uses range(i + 1, len(nums)) for the inner loop, not range(len(nums)). Starting at i + 1 avoids comparing an element to itself.
  • In contains_duplicate, make sure seen.add(num) comes after the if num in seen check, not before it.
  • Verify your function names match exactly. Python is case-sensitive.

Help me debug my Contains Duplicate solution.

✔️ Awesome, I've got everything!

Great. Double check you've saved 01_contains_duplicate.py and both test lines print in your terminal.

ⓧ I'd like to double check the full code

Here is the complete file. Compare it against yours character by character.

"""
Contains Duplicate (LeetCode 217) - Easy
=========================================

UNDERSTAND:
- Given an integer array nums, return True if any value appears
  at least twice, return False if every element is distinct.
- Input: list of integers
- Output: boolean
- Constraints: 1 <= nums.length <= 10^5, -10^9 <= nums[i] <= 10^9
- Edge cases: single element (always False), all same elements (True)

BRUTE FORCE:
- For each element, check every other element for a match
- Time: O(n^2), Space: O(1)

OPTIMIZE:
- Use a set for O(1) lookup instead of inner loop
- As we iterate, check if number is already in set
- Time: O(n), Space: O(n)
"""


def contains_duplicate_brute(nums):
    """Brute force: check every pair. O(n^2) time, O(1) space."""
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False


def contains_duplicate(nums):
    """Optimal: use a hash set. O(n) time, O(n) space."""
    seen = set()
    for num in nums:
        if num in seen:
            return True
        seen.add(num)
    return False


# --- Tests ---
if __name__ == "__main__":
    # Test brute force
    assert contains_duplicate_brute([1, 2, 3, 1]) == True
    assert contains_duplicate_brute([1, 2, 3, 4]) == False
    assert contains_duplicate_brute([1, 1, 1, 3, 3, 4, 3, 2, 4, 2]) == True
    print("Brute force: all tests passed!")

    # Test optimal
    assert contains_duplicate([1, 2, 3, 1]) == True
    assert contains_duplicate([1, 2, 3, 4]) == False
    assert contains_duplicate([1, 1, 1, 3, 3, 4, 3, 2, 4, 2]) == True
    assert contains_duplicate([1]) == False
    print("Optimal: all tests passed!")

You've solved your first Blind 75 problem with both approaches. Next up, you'll tackle Two Sum, where the hash map stores not just membership but also position information.

Solve Two Sum

In the previous step, you used a hash set to check membership in O(1) time. Now you'll tackle Two Sum, the most famous LeetCode problem of all time.

Two Sum introduces a slightly different pattern. Instead of just checking "have I seen this number before?", you need to check "have I seen the other number that would complete my pair?" This is called the complement lookup pattern, and it's the single most common hash map technique in interviews.

In this step, get ready to:
  • Write the brute force nested-loop solution for Two Sum.
  • Write the optimal hash map solution using complement lookup.
  • Run test cases to verify both solutions produce correct indices.
Create the file and write the brute force solution

Two Sum asks you to find two numbers in an array that add up to a target. You need to return their indices, not the numbers themselves. This small detail changes how you store information.

  • In VS Code, create a new file called 02_two_sum.py inside your blind75-arrays-hashing folder.
  • Add the problem statement docstring and brute force function by pasting this code:
"""
Two Sum (LeetCode 1) - Easy
=============================

UNDERSTAND:
- Given an array of integers nums and an integer target, return
  indices of the two numbers that add up to target.
- Each input has exactly one solution.
- You may not use the same element twice.
- Input: list of integers, target integer
- Output: list of two indices
- Constraints: 2 <= nums.length <= 10^4, -10^9 <= nums[i] <= 10^9
- Edge cases: two same numbers that sum to target [3,3] target=6

BRUTE FORCE:
- Check every pair of elements
- Time: O(n^2), Space: O(1)

OPTIMIZE:
- Use a hash map to store {value: index} as we iterate
- For each number, check if (target - number) exists in the map
- Time: O(n), Space: O(n)
"""


def two_sum_brute(nums, target):
    """Brute force: check every pair. O(n^2) time, O(1) space."""
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

What does this code do?

The UNDERSTAND section breaks down the problem. Notice that the output is indices (positions), not the values themselves. This means your solution needs to track where each number lives in the array.

The brute force uses the same nested-loop pattern as Contains Duplicate. For each element at index i, it checks every later element at index j to see if they add up to the target. If yes, return both indices.

  • Save 02_two_sum.py.
  • Confirm the file appears in the VS Code Explorer sidebar under your blind75-arrays-hashing folder.

Don't see the file in the sidebar?

Make sure you saved with Cmd+S (macOS) or Ctrl+S (Windows). If the file still doesn't appear, check that you created it inside the blind75-arrays-hashing folder and not a parent directory.

Help me figure out why my file isn't showing up in VS Code.

Write the optimal solution using a hash map

The brute force checks every pair, which means for each number you scan the entire remaining array looking for its partner. A dictionary can replace that inner scan with a single O(1) lookup.

  • In 02_two_sum.py, add the optimal function below the brute force function by pasting this code:

def two_sum(nums, target):
    """Optimal: hash map complement lookup. O(n) time, O(n) space."""
    seen = {}  # value -> index
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return []

What is the complement lookup pattern?

For each number, you calculate its complement: target - num. If the complement already exists in your seen dictionary, you've found your pair. If not, store the current number and its index for future lookups.

This flips the question from "does my pair exist somewhere ahead?" (requires inner loop) to "have I already seen my partner?" (requires one dictionary check). That single flip eliminates the nested loop entirely.

  • Save 02_two_sum.py.
  • You won't see a change yet. The next chunk adds test cases so you can run both solutions.
Add test cases and verify both solutions

Test cases confirm that both approaches return the correct pair of indices. The Three examples from LeetCode cover a normal case, a non-obvious pair, and two identical numbers.

  • At the bottom of 02_two_sum.py, add the test section by pasting this code:

# --- Tests ---
if __name__ == "__main__":
    # Test brute force
    assert two_sum_brute([2, 7, 11, 15], 9) == [0, 1]
    assert two_sum_brute([3, 2, 4], 6) == [1, 2]
    assert two_sum_brute([3, 3], 6) == [0, 1]
    print("Brute force: all tests passed!")

    # Test optimal
    assert two_sum([2, 7, 11, 15], 9) == [0, 1]
    assert two_sum([3, 2, 4], 6) == [1, 2]
    assert two_sum([3, 3], 6) == [0, 1]
    print("Optimal: all tests passed!")

What do these test cases cover?

  • [2, 7, 11, 15] with target 9: the first two elements are the answer. Checks the basic case.
  • [3, 2, 4] with target 6: the answer isn't at the start. Checks that you don't stop too early.
  • [3, 3] with target 6: two identical numbers. Checks the edge case where the same value appears twice at different indices.
  • Save 02_two_sum.py.
  • Run your file by entering this command in Terminal:
python3 02_two_sum.py
  • You should see two lines printed:
Brute force: all tests passed!
Optimal: all tests passed!

Seeing an AssertionError?

An AssertionError means one of your functions returned the wrong indices. Check that two_sum_brute returns [i, j] (a list, not a tuple) and that two_sum returns [seen[complement], i] with the earlier index first.

Help me debug my Two Sum solution.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete 02_two_sum.py file. Make sure yours matches exactly:

"""
Two Sum (LeetCode 1) - Easy
=============================

UNDERSTAND:
- Given an array of integers nums and an integer target, return
  indices of the two numbers that add up to target.
- Each input has exactly one solution.
- You may not use the same element twice.
- Input: list of integers, target integer
- Output: list of two indices
- Constraints: 2 <= nums.length <= 10^4, -10^9 <= nums[i] <= 10^9
- Edge cases: two same numbers that sum to target [3,3] target=6

BRUTE FORCE:
- Check every pair of elements
- Time: O(n^2), Space: O(1)

OPTIMIZE:
- Use a hash map to store {value: index} as we iterate
- For each number, check if (target - number) exists in the map
- Time: O(n), Space: O(n)
"""


def two_sum_brute(nums, target):
    """Brute force: check every pair. O(n^2) time, O(1) space."""
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []


def two_sum(nums, target):
    """Optimal: hash map complement lookup. O(n) time, O(n) space."""
    seen = {}  # value -> index
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return []


# --- Tests ---
if __name__ == "__main__":
    # Test brute force
    assert two_sum_brute([2, 7, 11, 15], 9) == [0, 1]
    assert two_sum_brute([3, 2, 4], 6) == [1, 2]
    assert two_sum_brute([3, 3], 6) == [0, 1]
    print("Brute force: all tests passed!")

    # Test optimal
    assert two_sum([2, 7, 11, 15], 9) == [0, 1]
    assert two_sum([3, 2, 4], 6) == [1, 2]
    assert two_sum([3, 3], 6) == [0, 1]
    print("Optimal: all tests passed!")

Why is this faster than Contains Duplicate's pattern?

Contains Duplicate only needed to ask "have I seen this exact number?" A set was enough for that. Two Sum needs to ask "have I seen the number that would complete my pair?" That requires storing both the value AND its index, which is why you need a dictionary instead of a set.

The core insight is the same though. When you see O(n^2) caused by a nested search, ask yourself: can I store something in a hash map to make that search O(1)?

Two problems down. You've now seen the two most fundamental hash map patterns: membership checking (set) and value lookup (dictionary). Next up, you'll combine hash maps with a creative key design to solve a medium-difficulty problem.

Solve Group Anagrams

You've solved Two Sum by storing seen values in a dictionary for O(1) complement lookup. Now you'll tackle a Medium-difficulty problem that pushes your hash map skills further.

Group Anagrams asks you to cluster words that share the same letters. The interesting challenge here is designing the right dictionary key. Your brute force and optimal solutions will use two different keying strategies, and the difference reveals a powerful pattern for interview problems.

In this step, get ready to:
  • Write a brute force solution that sorts each string to create dictionary keys.
  • Write an optimal solution that uses character-count tuples as keys instead.
  • Test both solutions and understand the complexity trade-off.
Create the file and write the brute force solution

Group Anagrams is the first Medium problem in the Blind 75 Arrays & Hashing category. It combines string manipulation with creative hash map key design.

  • In VS Code, create a new file called 03_group_anagrams.py inside your blind75-arrays-hashing folder.
  • Add the problem statement docstring, import, and brute force solution by copying the following code into the file:
"""
Group Anagrams (LeetCode 49) - Medium
=======================================

UNDERSTAND:
- Given an array of strings, group the anagrams together.
- An anagram is a word formed by rearranging letters of another word.
- Return the answer in any order.
- Input: list of strings
- Output: list of lists of strings (grouped anagrams)
- Constraints: 1 <= strs.length <= 10^4, 0 <= strs[i].length <= 100,
  strs[i] consists of lowercase English letters
- Edge cases: empty string [""], single character ["a"]

BRUTE FORCE:
- Sort each string, use sorted string as dictionary key
- Time: O(n * k * log(k)) where n = number of strings, k = max string length
- Space: O(n * k)

OPTIMIZE:
- Use character count tuple as key instead of sorting
- Count frequency of each letter (26 slots for a-z)
- Time: O(n * k), Space: O(n * k)
"""

from collections import defaultdict


def group_anagrams_brute(strs):
    """Brute force: sort each string as key. O(n * k * log(k)) time."""
    groups = defaultdict(list)
    for s in strs:
        key = "".join(sorted(s))
        groups[key].append(s)
    return list(groups.values())

What does this code do?

The docstring follows the same 5-step framework from your template. It restates the problem, lists constraints, and outlines both approaches up front.

The brute force strategy is: if two words are anagrams, sorting their letters produces the same string. For example, "eat" and "tea" both sort to "aet". Using that sorted string as a dictionary key groups all anagrams together.

The defaultdict(list) automatically creates an empty list for any new key, so you can append without checking if the key exists first.

Why is the brute force O(n * k * log(k))?

You iterate through n strings. For each string, sorting costs O(k * log(k)) where k is the string length. Unlike Contains Duplicate and Two Sum, this brute force isn't O(n^2) because there's no nested loop over the input array. The bottleneck is the sorting step inside each iteration.

  • Save 03_group_anagrams.py.

Seeing a syntax error?

Make sure the triple-quoted docstring opens and closes with """ on its own line. A missing closing set of triple quotes will cause Python to treat the rest of the file as a string.

Also check that from collections import defaultdict is outside and below the docstring, not inside it.

Help me fix a syntax error in my group anagrams file.

Write the optimal solution using character counts

Sorting each string costs O(k * log(k)). You can eliminate that cost by counting character frequencies instead, which only costs O(k) per string.

  • Add the optimal solution below the brute force function in 03_group_anagrams.py:
def group_anagrams(strs):
    """Optimal: character count tuple as key. O(n * k) time."""
    groups = defaultdict(list)
    for s in strs:
        count = [0] * 26
        for char in s:
            count[ord(char) - ord("a")] += 1
        groups[tuple(count)].append(s)
    return list(groups.values())

What does this code do?

Instead of sorting, this creates a list of 26 zeros (one slot per letter a through z). It then counts how many times each letter appears in the string.

The expression ord(char) - ord("a") converts a letter to an index (0 for 'a', 1 for 'b', etc.). The final count list is converted to a tuple so it can serve as a dictionary key. Two anagrams always produce the same count tuple.

This is the key insight: sorting gives you a canonical form in O(k * log(k)), but counting gives you a canonical form in O(k). For long strings, this difference matters.

Why a tuple and not a list?

Python dictionaries require keys to be hashable. Lists are mutable and not hashable, so they can't be dictionary keys. Tuples are immutable and hashable, making them perfect for this pattern.

  • Save 03_group_anagrams.py.

Not sure where to place this function?

Add it directly below the group_anagrams_brute function, with two blank lines separating them. Make sure the def keyword starts at column 0 (no indentation).

Help me place my optimal function correctly in the file.

Add test cases and verify both solutions

Anagram grouping has a twist for testing: the groups can be returned in any order. You need a helper function that compares results regardless of ordering.

  • Add the test section at the bottom of 03_group_anagrams.py, below both solution functions:
# --- Tests ---
if __name__ == "__main__":
    # Helper to compare grouped results (order doesn't matter)
    def same_groups(result, expected):
        result_sorted = sorted([sorted(group) for group in result])
        expected_sorted = sorted([sorted(group) for group in expected])
        return result_sorted == expected_sorted

    # Test brute force
    result = group_anagrams_brute(["eat", "tea", "tan", "ate", "nat", "bat"])
    assert same_groups(result, [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]])

    result = group_anagrams_brute([""])
    assert same_groups(result, [[""]])

    result = group_anagrams_brute(["a"])
    assert same_groups(result, [["a"]])
    print("Brute force: all tests passed!")

    # Test optimal
    result = group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
    assert same_groups(result, [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]])

    result = group_anagrams([""])
    assert same_groups(result, [[""]])

    result = group_anagrams(["a"])
    assert same_groups(result, [["a"]])
    print("Optimal: all tests passed!")

What does this code do?

The same_groups() helper sorts both the groups and the items within each group. This lets you compare results without worrying about order. For example, [["tea", "eat"], ["bat"]] and [["bat"], ["eat", "tea"]] are both correct answers.

The test cases cover three scenarios: a standard input with multiple groups, an edge case with an empty string, and an edge case with a single character.

  • Save 03_group_anagrams.py.
  • Run the file in your Terminal by running this command:
python3 03_group_anagrams.py
  • You should see both Brute force: all tests passed! and Optimal: all tests passed! printed in the Terminal.

Seeing an AssertionError?

If the brute force tests fail, make sure you're using "".join(sorted(s)) as the key. Using just sorted(s) returns a list, which can't be a dictionary key.

If the optimal tests fail, double check that you're converting the count list to a tuple with tuple(count) before using it as a key. Also verify the expression is ord(char) - ord("a") with a lowercase "a".

Help me debug my Group Anagrams assertion error.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here is the complete 03_group_anagrams.py file:

"""
Group Anagrams (LeetCode 49) - Medium
=======================================

UNDERSTAND:
- Given an array of strings, group the anagrams together.
- An anagram is a word formed by rearranging letters of another word.
- Return the answer in any order.
- Input: list of strings
- Output: list of lists of strings (grouped anagrams)
- Constraints: 1 <= strs.length <= 10^4, 0 <= strs[i].length <= 100,
  strs[i] consists of lowercase English letters
- Edge cases: empty string [""], single character ["a"]

BRUTE FORCE:
- Sort each string, use sorted string as dictionary key
- Time: O(n * k * log(k)) where n = number of strings, k = max string length
- Space: O(n * k)

OPTIMIZE:
- Use character count tuple as key instead of sorting
- Count frequency of each letter (26 slots for a-z)
- Time: O(n * k), Space: O(n * k)
"""

from collections import defaultdict


def group_anagrams_brute(strs):
    """Brute force: sort each string as key. O(n * k * log(k)) time."""
    groups = defaultdict(list)
    for s in strs:
        key = "".join(sorted(s))
        groups[key].append(s)
    return list(groups.values())


def group_anagrams(strs):
    """Optimal: character count tuple as key. O(n * k) time."""
    groups = defaultdict(list)
    for s in strs:
        count = [0] * 26
        for char in s:
            count[ord(char) - ord("a")] += 1
        groups[tuple(count)].append(s)
    return list(groups.values())


# --- Tests ---
if __name__ == "__main__":
    # Helper to compare grouped results (order doesn't matter)
    def same_groups(result, expected):
        result_sorted = sorted([sorted(group) for group in result])
        expected_sorted = sorted([sorted(group) for group in expected])
        return result_sorted == expected_sorted

    # Test brute force
    result = group_anagrams_brute(["eat", "tea", "tan", "ate", "nat", "bat"])
    assert same_groups(result, [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]])

    result = group_anagrams_brute([""])
    assert same_groups(result, [[""]])

    result = group_anagrams_brute(["a"])
    assert same_groups(result, [["a"]])
    print("Brute force: all tests passed!")

    # Test optimal
    result = group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
    assert same_groups(result, [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]])

    result = group_anagrams([""])
    assert same_groups(result, [[""]])

    result = group_anagrams(["a"])
    assert same_groups(result, [["a"]])
    print("Optimal: all tests passed!")

You've now solved all three Blind 75 problems in this project. Each one used a hash map to eliminate expensive repeated work. Next up, put your skills to the test with a Secret Mission challenge.

Secret mission

Solve Top K Frequent Elements

You've solved 3 problems with guidance. Now it's time to fly solo. Use your 5-step template to tackle a 4th Blind 75 problem from scratch, proving you can break down and optimize a solution independently.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The blind75-arrays-hashing folder on your Desktop containing template.py, 01_contains_duplicate.py, 02_two_sum.py, 03_group_anagrams.py, and 04_top_k_frequent.py.

Keep everything running

No action needed. Choose this if you plan to keep solving more Blind 75 problems and want to add new solution files to the same folder.

  • Your blind75-arrays-hashing folder stays on your Desktop, ready for the next problem.
  • You can continue using template.py as your starting point for new solutions.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files sit on disk and use negligible storage.

  • Close the VS Code window to free up memory.
  • Close your Terminal window.
  • Your files remain in blind75-arrays-hashing on your Desktop whenever you want to return.

Delete - I don't want to use this again

Remove the project folder entirely to start fresh.

  • Close the folder in VS Code first.
  • Delete the project folder by running this command in Terminal:
rm -rf ~/Desktop/blind75-arrays-hashing
  • Confirm deletion by running:
ls ~/Desktop | grep blind75
  • You should see no output, confirming the folder is gone.

Nice Work!

Nice Work!

You just solved 3 Blind 75 problems from scratch, building both brute force and optimized solutions for each one. You now have a repeatable system for attacking any DSA interview problem.

You've learned how to:

  • Solve Contains Duplicate using a hash set for O(1) membership checking, replacing an O(n^2) nested loop with a single O(n) pass.
  • Solve Two Sum using a hash map for O(1) complement lookup, storing seen values and their indices for instant retrieval.
  • Solve Group Anagrams using a hash map with character-count tuple keys, avoiding O(k log k) sorting per string in favour of O(k) counting.
  • Secret Mission: Solved Top K Frequent Elements independently using frequency counting and bucket sort, achieving O(n) time by using array indices as frequencies.

Ready to quiz yourself?