Python • Algorithms and Complexity

Big-O for Lists & Dicts — Explain Big-O for loops, nested loops, and common operations (list/dict membership) (conceptual)

Learn to predict how your Python code slows down as data grows using Big-O notation.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 9 figures

SECTION 1

Historical Context & Motivation

Imagine you have a list of 10 names and you need to find one. No big deal — you can scan through them in a blink. Now imagine that list grows to 10 million names. Suddenly, the way you search matters a lot. Computer scientists needed a standard language to describe how the running time of an algorithm grows as the input gets larger. That language is called Big-O notation, and it has been a cornerstone of computer science since the mid-twentieth century.

1894 • Bachmann Introduces Big-O

German mathematician Paul Bachmann first used the Big-O symbol in his book on number theory to describe how functions grow relative to each other.

1976 • Knuth Popularizes Big-O in CS

Donald Knuth's landmark series The Art of Computer Programming established Big-O as the standard way to analyze algorithm efficiency.

1991 • Python Is Born

Guido van Rossum released Python, which uses lists and dictionaries as core data structures — each with distinct Big-O performance characteristics that every Python programmer should understand.

2000s • Big Data Makes Big-O Essential

As datasets grew from thousands to billions of records, choosing the right data structure — list vs. dictionary — became the difference between code that runs in seconds and code that never finishes.

The central question Big-O answers is simple: If I double the size of my input, how much longer will my program take? Understanding this helps you write code that scales gracefully, whether you are processing a classroom roster or a social media feed with millions of posts.

SECTION 2

Core Principles & Definitions

Big-O notation strips away the small details — the speed of your processor, the programming language you use — and focuses on the growth rate of an algorithm's running time. We use the letter n to represent the size of the input, such as the number of items in a list. Here are the foundational ideas you need.

1

O(1) — Constant Time

The operation takes the same amount of time no matter how large the input is. Accessing a list element by index (my_list[5]) or looking up a key in a dictionary are both O(1).
2

O(n) — Linear Time

The time grows in direct proportion to the input size. A single for loop that visits every element in a list of length n is O(n). Double the list, double the time.
3

O(n²) — Quadratic Time

A nested loop where both loops run n times performs n × n operations. If n doubles from 100 to 200, the work quadruples from 10,000 to 40,000.
4

Drop Constants & Lower Terms

Big-O ignores constant multipliers and smaller terms. An algorithm doing 3n + 50 operations is still O(n), because as n grows large, the shape of the growth is what matters.
5

Worst-Case Focus

Big-O typically describes the worst-case scenario. Even if your loop sometimes exits early, Big-O asks: what happens when the element is at the very end — or not present at all?
✦

KEY TAKEAWAY

Think of Big-O like measuring a road trip. You don't care whether you drive at 61 or 63 mph — you care whether the trip is across town (short) or across the country (long). Big-O tells you the shape of the journey, not the exact minute you arrive.

SECTION 3

Visual Explanation — Growth Curves

The most powerful way to understand Big-O is to see the growth curves side by side. The diagram below plots the number of operations on the vertical axis against the input size n on the horizontal axis. Notice how the O(n²) curve shoots upward dramatically compared to the steady climb of O(n) and the flat line of O(1).

Big-O Growth Curves ComparisonInput Size (n)Operations0n2n3n4nn2n3n4nO(1)O(n)O(n²)O(n log n)
The green flat line represents O(1) constant time — it never changes. The cyan diagonal is O(n) linear time. The pink curve that rockets upward is O(n²) quadratic time. The amber curve, O(n log n), sits between linear and quadratic.

This visual makes the key lesson crystal clear. An O(n²) algorithm might be fine for 100 items, but at 10,000 items it would need 100 million operations — while an O(n) algorithm would only need 10,000. Choosing the right approach, or the right data structure, can mean the difference between your program finishing in a fraction of a second and it freezing for hours.

SECTION 4

Mathematical Framework

You do not need calculus to understand Big-O — just some intuition about counting operations. The idea is to count how many times the most-repeated instruction runs as a function of n, the input size, and then keep only the dominant (fastest-growing) term.

SINGLE LOOP

foriinrange(n):→niterations→O(n)for i in range(n): → n iterations → O(n)

A single loop that runs from 0 to n − 1 executes its body n times. We say this is O(n) — linear time.

NESTED LOOP

foriinrange(n):forjinrange(n):→n×n=n2iterations→O(n2)for i in range(n): for j in range(n): → n × n = n² iterations → O(n²)

When one loop sits inside another and both iterate over all n elements, the inner body executes n² times total. This is quadratic time.

CONSTANT TIME OPERATION

mydict[key]→1operation→O(1)my_dict[key] → 1 operation → O(1)

Dictionary lookups use a hash table internally, which maps a key directly to a memory location. The time does not depend on how many items are in the dictionary.

SIMPLIFICATION RULE

3n² + 7n + 42 → drop constants & lower terms → O(n²)

When n is very large, the n² term dominates everything else. The 7n and 42 become negligible, and the leading coefficient 3 does not change the shape of the growth.

💡

Why Drop the Constants?

Big-O is about predicting behavior at scale. An algorithm that does 3n operations will always be faster than one that does n² operations — once n gets large enough. The constant 3 doesn't change that fundamental relationship. Think of it this way: driving 3× the speed limit is still linear with distance, while taking every possible detour grows exponentially.

SECTION 5

Lists vs. Dicts — Operation-by-Operation

Python's two most used data structures — lists and dictionaries — have very different performance profiles. The table below compares common operations and their Big-O complexities. Understanding these differences is one of the most practical skills you can develop as a Python programmer.

OperationList — Big-ODict — Big-OWhy?
Access by index / keyO(1)O(1)Both jump directly to the memory location
Membership test (x in ...)O(n)O(1)Lists must scan every element; dicts use hash lookup
Append / insert at endO(1)O(1)Both add to the end without shifting elements
Insert at beginningO(n)N/AList must shift every element one position to the right
Delete by valueO(n)O(1)List searches then shifts; dict hashes directly
Iterate over all itemsO(n)O(n)Both must visit every element

Big-O comparison of common list and dictionary operations in Python

Membership Test: List vs. DictionaryList: "Is 7 in here?" — O(n)3check1check9check5check7found!scans left → right (5 checks)Dict: "Is 7 in here?" — O(1)Hash Table0: 91: 12: 33: 54: 7 ✓hash(7)→ slot 4Jump directly — 1 step!Performance Impactn = 1,0001000n = 1,0001■ List scan■ Dict lookupDict is 1000× faster here
Top: a list must check each element one by one (scanning left to right) — that is O(n). Bottom: a dictionary uses a hash function to jump directly to the correct slot — that is O(1). The performance bar on the right shows the dramatic difference at n = 1,000.

The critical takeaway from this comparison is about the in keyword. Writing x in my_list forces Python to scan every element until it finds a match (or reaches the end), giving O(n). Writing x in my_dict triggers a hash-based lookup that takes O(1) on average. If you frequently need to test whether a value exists in a collection, a dictionary (or a set, which works the same way) is the right choice.

SECTION 6

Worked Example — Analyzing a Function's Big-O

Let's walk through a realistic Python function and determine its Big-O step by step. Suppose we want to find all pairs of numbers in a list that add up to a target value.

Find Pairs That Sum to a Target

  1. 1

    Step 1 — Read the Code

    Here is our function: def find_pairs(nums, target): pairs = [] for i in range(len(nums)): # Loop A for j in range(i+1, len(nums)): # Loop B if nums[i] + nums[j] == target: pairs.append((nums[i], nums[j])) return pairs We need to figure out how many times the innermost operation (the if check) executes when the list has n elements.
  2. 2

    Step 2 — Count the Iterations

    Loop A runs n times (i goes from 0 to n − 1). For each value of i, Loop B runs roughly n − i − 1 times. The total number of iterations is (n − 1) + (n − 2) + … + 1 + 0.
    Total iterations = n × (n − 1) / 2 = (n² − n) / 2
  3. 3

    Step 3 — Apply the Simplification Rule

    We drop the constant factor (1/2) and the lower-order term (−n). The expression (n² − n) / 2 simplifies under Big-O rules to just n².
    Big-O = O(n²)
  4. 4

    Step 4 — Interpret the Result

    This means if we double the list size from 1,000 to 2,000, the number of pair-checks roughly quadruples — from about 500,000 to about 2,000,000. For a list of 100,000 elements, we would need about 5 billion comparisons, which could take minutes.
  5. 5

    Step 5 — Consider a Better Approach

    We can rewrite this with a dictionary (or set) to achieve O(n): def find_pairs_fast(nums, target): seen = set() pairs = [] for num in nums: # One loop: O(n) complement = target - num if complement in seen: # Set lookup: O(1) pairs.append((complement, num)) seen.add(num) return pairs The single loop is O(n), and each set operation inside it is O(1), so the overall complexity is O(n) — a massive improvement.
    Optimized Big-O = O(n) — thanks to the set's O(1) membership test!

SECTION 7

Strengths & Limitations of Each Structure

If dictionaries are so fast for lookups, why not use them everywhere? The answer is that every data structure comes with tradeoffs. Lists and dictionaries serve different purposes, and picking the right one depends on what your program needs to do most often.

FactorListDictionary
Ordered?Yes — maintains insertion order and supports indexing by positionMaintains insertion order (Python 3.7+), but not indexed by position
Membership speedO(n) — must scan through elementsO(1) — uses hash table
Memory usageLower — stores values onlyHigher — stores keys, values, and hash metadata
Duplicate elementsAllowed — can hold [3, 3, 3]Keys must be unique
Best use caseOrdered collections, iteration, stacks, queuesFast lookups, counting, mapping relationships

Tradeoffs between lists and dictionaries

✦

KEY TAKEAWAY

Think of a list like a bookshelf where books are in a specific order — great for browsing from left to right, but finding a specific title means scanning every spine. A dictionary is like a library catalog system — you look up a call number and go straight to the shelf. The catalog costs extra space and setup, but finding any book is nearly instant.

SECTION 8

Connection to Advanced Complexity Theory

The Big-O concepts you have learned here form the foundation for deeper topics in algorithms and complexity. As you advance, you will encounter additional layers of analysis, more complex data structures, and entirely new classes of problems.

What You Know NowWhat Comes Next
Big-O describes worst-case growth rateBig-Ω (Omega) describes best-case; Big-Θ (Theta) describes tight bounds (average)
O(1), O(n), O(n²) for simple loopsO(log n) for binary search, O(n log n) for efficient sorting (merge sort, Timsort)
Lists and dicts as primary structuresTrees, heaps, graphs — each with unique Big-O profiles for insert, delete, and search
Counting loop iterations by handRecurrence relations and the Master Theorem for recursive algorithms
Time complexity (how long?)Space complexity (how much memory?) — sometimes you trade time for memory or vice versa

From introductory Big-O to advanced complexity topics

One especially exciting frontier is the P vs. NP problem — one of the biggest unsolved questions in computer science. It asks whether every problem whose solution can be verified quickly (in polynomial time) can also be solved quickly. The Clay Mathematics Institute offers a one-million-dollar prize for its resolution. Everything you've learned here — about counting operations and classifying growth rates — is the first step toward understanding problems like this.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why x in my_list is O(n) while x in my_dict is O(1). What fundamental difference between the two data structures causes this?
Show answer
A list stores elements in a sequence with no special organization by value. To check if x exists, Python must start at the beginning and compare x with every element until it finds a match or reaches the end — that is n comparisons in the worst case. A dictionary uses a hash table: it computes a hash of the key, which tells it exactly where to look in memory. This direct computation takes constant time regardless of how many keys exist. The fundamental difference is that dicts have an indexing mechanism (the hash function) that lists lack.

Question 2: BASIC CALCULATION

What is the Big-O of the following code? total = 0 for i in range(n): total += i print(total) Identify how many times each line executes and give the overall Big-O.
Show answer
The assignment total = 0 executes once — O(1). The for loop runs n times, and inside it the addition total += i also runs n times — O(n). The print statement runs once — O(1). Adding these together gives O(1) + O(n) + O(1). We drop the constants and lower-order terms, so the overall Big-O is O(n). Doubling n would roughly double the running time.

Question 3: INTERMEDIATE

Analyze the Big-O of this function: def mystery(lst): n = len(lst) for i in range(n): for j in range(n): print(lst[i], lst[j]) for k in range(n): print(lst[k]) Show the Big-O of each section and the overall result.
Show answer
The nested loops (i and j) each run n times, so the nested section performs n × n = n² print operations — that's O(n²). The third loop runs n times, contributing O(n). The total is O(n²) + O(n). Since n² grows faster than n, the dominant term is n². The overall Big-O is O(n²). The single loop at the end is essentially negligible compared to the nested loops for large n. For example, at n = 1,000, the nested part does 1,000,000 operations while the single loop only adds 1,000 more.

Question 4: APPLIED

You are building an app that checks whether each username in a list of 50,000 new sign-ups already exists in a database of 1,000,000 existing users. Your teammate wrote: existing = [...] # list of 1,000,000 usernames for name in new_signups: # 50,000 names if name in existing: # list membership print('taken') Estimate the total number of comparisons and suggest a faster approach.
Show answer
The outer loop runs 50,000 times. Each name in existing check on a list of 1,000,000 elements is O(n), meaning up to 1,000,000 comparisons per check. In the worst case, this is 50,000 × 1,000,000 = 50 billion comparisons — far too slow. The fix is to convert the existing users list to a set: existing_set = set(existing). This one-time conversion is O(n), and then each membership test becomes O(1). The total becomes 1,000,000 (to build the set) + 50,000 (to check all names) = about 1,050,000 operations — roughly 50,000 times faster than the original approach.

Question 5: CRITICAL THINKING

Consider this code that removes all duplicates from a list: def remove_dupes_v1(lst): result = [] for item in lst: if item not in result: result.append(item) return result (a) What is the Big-O of this function? Explain carefully. (b) Write a version using a dictionary or set that achieves better Big-O. (c) Under what circumstances might the O(n²) version actually be acceptable?
Show answer
(a) The outer loop runs n times. For each iteration, item not in result scans the result list, which can grow up to n elements. In the worst case (all unique elements), the check on iteration k scans k elements. The total is 0 + 1 + 2 + … + (n−1) = n(n−1)/2, which is O(n²). (b) A set-based version: def remove_dupes_v2(lst): seen = set() result = [] for item in lst: if item not in seen: seen.add(item) result.append(item) return result This is O(n) because both item not in seen and seen.add(item) are O(1). (c) The O(n²) version might be acceptable when n is very small (say, under 100), when you need to preserve the exact order and don't want to import extra logic, or when memory is extremely constrained and you cannot afford the extra set. Big-O describes behavior at scale — for tiny inputs, even 'slow' algorithms finish instantly.

SUMMARY

Lesson Summary

Summary

Big-O notation describes how an algorithm's running time grows as the input size n increases. A single for loop over n elements is O(n) — linear time. A nested loop where both levels iterate over n elements is O(n²) — quadratic time. When simplifying, always drop constants and lower-order terms to focus on the dominant growth rate.

The most impactful practical insight is the difference between list membership testing at O(n) and dictionary/set membership testing at O(1). Dictionaries achieve this speed through hash tables, which map keys directly to memory locations. When your code needs frequent lookups inside a loop, switching from a list to a set or dictionary can transform an O(n²) algorithm into an O(n) algorithm — a difference that matters enormously at scale.

Varsity Tutors • Python • Big-O for Lists & Dicts