Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 46

Optimizing with Python Dictionaries

Recommended resources

Introduction

Hello! In our last lesson, we analyzed the performance of hash tables, discovering their key strength: an average-case time complexity of O(1) for lookups, insertions, and deletions. We contrasted this with the O(n) worst-case, which is rare in practice. This incredible average-case efficiency is why hash tables are a go-to data structure for performance-critical tasks.

Today, we'll move from theory to practice. This lesson's goal is to use Python's dictionary (a hash table implementation) to solve problems requiring fast lookups. You will learn to recognize common algorithmic patterns where a dictionary can drastically improve a solution's efficiency, a skill that is crucial for technical interviews.


A Quick Refresher on Python Dictionaries

Before we dive into problem-solving, let's quickly review the essentials of Python's dict. You've encountered dictionaries before, but mastering their use is key to leveraging their power.

A dictionary stores a collection of key-value pairs. Each unique key maps to a specific value, allowing for incredibly fast retrieval of that value if you know its key.

Dictionary in Python: Key-Value Pairs and Properties
This image provides a great visual summary. A dictionary is created with curly braces `{}`, containing `key: value` pairs. You access a value using its key, like `d['a']`. The keys must be unique and are typically strings or numbers.

To ensure you're comfortable with the syntax, the following video provides a clear, practical overview of the most common dictionary operations.

Python Tutorial for Beginners 5: Dictionaries - Working with Key-Value Pairs

This video from Corey Schafer's beginner series is an excellent refresher on working with dictionaries. Please watch the following segments to review the core operations.

Please watch the following parts: 00:35 - 02:09: Creating a dictionary and accessing values using square brackets []. 02:09 - 03:58: Safely accessing values using the .get() method to avoid errors with non-existent keys. 03:58 - 05:00: Adding new key-value pairs and updating existing ones. 07:09 - 09:00: Looping through keys, values, and key-value pairs (.items()).

To summarize the most critical operations for problem-solving:

  • Create a dictionary: my_dict = {}
  • Add/Update an item: my_dict[key] = value
  • Access an item: value = my_dict[key] (causes an error if key doesn't exist)
  • Safely access an item: value = my_dict.get(key, default_value) (returns None or a specified default if key doesn't exist)
  • Check for a key: if key in my_dict:
  • Iterate over key-value pairs: for key, value in my_dict.items():

With this refresher, we're ready to see how these simple operations can lead to powerful algorithms.


Pattern 1: The "Look-Back" Problem

A very common algorithmic challenge involves processing a list of items while needing to quickly "look back" at the items you've already seen. A brute-force approach often involves a nested loop: for each item, you loop back through all previous items. This leads to an O(n²) solution.

A dictionary allows you to optimize this to O(n) by storing information about seen items. Instead of looping back, you perform a single, O(1) lookup in the dictionary.

The classic example is the "Two Sum" problem, a favorite in technical interviews.

Problem Statement: Given a list of numbers and a target number, find the indices of two numbers in the list that add up to the target.

Let's watch a video that perfectly explains how to solve this, first with the brute-force method and then with the optimized dictionary (or "hash map") approach.

Two Sum - Leetcode 1 - HashMap - Python

This video from NeetCode is one of the clearest explanations of the 'Two Sum' problem and its optimal solution. It directly demonstrates the value of using a hash map.

Watch the entire video. It first explains the O(n²) brute-force solution (00:00-01:49) and then walks through the logic and implementation of the far more efficient O(n) hash map solution (01:49-08:26).

Let's break down the logic of the O(n) hash map solution you just saw. We trade extra memory (to store the dictionary) for a massive speed-up.

Here's the algorithm step-by-step:

  1. Initialize an empty dictionary, let's call it prev_map. This will store number: index pairs for the numbers we've already seen.
  2. Loop through the input list nums, getting both the index i and the number n at each step.
  3. For each number n, calculate the difference required to reach the target: diff = target - n.
  4. Perform a fast lookup: Check if diff exists as a key in prev_map.
    • If diff is in prev_map, it means we have found our pair! The first number's index is prev_map[diff], and the second number's index is the current index, i. We can return these two indices.
    • If diff is not in prev_map, it means we haven't seen the other number needed for the pair yet. So, we add the current number and its index to our map: prev_map[n] = i. This prepares the map for future iterations.

By the time we visit the second number of the correct pair, the first number will have already been stored in the map, guaranteeing we find the solution in a single pass.


Pattern 2: Frequency Counting

Another extremely common use for dictionaries is to count the frequency of items in a list or string. The item becomes the key, and its count becomes the value.

Problem Statement: Given a list of items, find the "majority element," which is the element that appears more than n / 2 times, where n is the total number of items.

To solve this, you can iterate through the list and use a dictionary to keep a running count of each element.

Mastering Interview Problems Using Python Dictionaries

The article 'Mastering Interview Problems Using Python Dictionaries' from CodeSignal provides an excellent walkthrough of this frequency counting pattern with the 'Majority Vote Problem'.

Please read the section titled 'Problem 1: Majority Vote Problem with Dictionaries'. Focus on the 'Efficient Approach Explanation' and the step-by-step solution. Notice how the get method is used to simplify the code for incrementing the count.

The logic for frequency counting is straightforward and powerful:

  1. Initialize an empty dictionary, counts = {}.
  2. Iterate through each item in the input list.
  3. For each item:
    • Use counts.get(item, 0) to get the current count of the item. If the item isn't in the dictionary yet, this will return the default value, 0.
    • Increment the count and update the dictionary: counts[item] = counts.get(item, 0) + 1.
  4. After building the frequency map, you can iterate through it to find the item with the highest count or check for any other condition.

This pattern is not just for numbers; it works perfectly for counting characters in a string, words in a text, or any other hashable item.


Let's Practice: Your Turn

Now, let's solve a problem that combines these ideas.

Problem: Given a string, find the first character that does not repeat. For example, in "algorithm", the answer is 'a'. In "racecars", the answer is 'e'. If all characters repeat, return None.

Think about the steps:

  1. How can you efficiently count every character in the string? (Hint: Frequency Counting)
  2. Once you have the counts, how can you find the first one with a count of 1?

Try to outline the solution yourself before looking at the code below.

Click here to see the solution
def first_non_repeating_char(s):



    # Step 1: Build a frequency map (a dictionary) of characters.
    counts = {}
    for char in s:
        counts[char] = counts.get(char, 0) + 1
        



    # Step 2: Iterate through the string AGAIN to find the first character
    # with a count of 1 in our map. The second loop preserves the order.
    for char in s:
        if counts[char] == 1:
            return char
            



    # Step 3: If the loop finishes, no non-repeating character was found.
    return None




# --- Test Cases ---
print(f"'algorithm' -> '{first_non_repeating_char('algorithm')}'") # Expected: 'a'
print(f"'racecars' -> '{first_non_repeating_char('racecars')}'")   # Expected: 'e'
print(f"'aabbcc' -> '{first_non_repeating_char('aabbcc')}'")     # Expected: None

This solution requires two passes through the string, but since the loops are not nested, the time complexity is O(n) + O(n) = O(n), where n is the length of the string. The dictionary lookups (counts[char]) inside the loops are O(1), making the whole process highly efficient.


Conclusion

In this lesson, you bridged the gap between the theory of hash tables and their practical application using Python dictionaries. This is a significant step in your journey to mastering algorithms.

Key Takeaways:

  • Python's dict is an implementation of a hash table, providing O(1) average time complexity for lookups, insertions, and deletions.
  • This efficiency makes dictionaries ideal for optimizing algorithms that would otherwise require slow, O(n) searches within a loop.
  • You learned to identify and apply two fundamental problem-solving patterns:
    • The "Look-Back" Pattern: Storing seen items to avoid re-scanning previous elements (e.g., Two Sum).
    • Frequency Counting: Using a dictionary to efficiently count occurrences of items (e.g., Majority Element, First Non-Repeating Character).

Preview of the Next Lesson:

Hash tables are also the foundation for another useful Python data structure: the set. In the next lesson, we will explore what sets are, how they differ from dictionaries, and how to use them to solve problems involving uniqueness and membership testing.