Skip to content

Expert · Algorithms, data structures and testing · topic 45 of 50 · 30 min

Big-O and searching

Explanation

Big-O is a simple way to describe how much more work an algorithm does as the data grows: O(1) stays the same, O(n) grows in step with the data, O(log n) grows very slowly and O(n^2) explodes. Linear search checks items one by one; binary search halves a sorted list each step. Sets and dicts find items almost instantly because they use hashing.

What Big-O measures

An algorithm is a step-by-step method for solving a problem. Big-O notation describes how the number of steps grows when the input size grows. The input size is called n: the number of items in the list, students in the class, rows in the file. Big-O ignores small details and constant factors; it only cares about the shape of the growth.

Think of it like this: Finding a friend's house. O(1): you know the house number and walk straight to it. O(n): you knock on every door in the lane until you find them. O(log n): you look for a word in a printed dictionary by opening the middle and throwing away the half you do not need, again and again. O(n^2): every student in a class shakes hands with every other student; double the class and the handshakes go up four times.
Big-ONameExampleSteps for n = 1,000,000
O(1)constantmy_dict[key], my_list[5]1
O(log n)logarithmicbinary search in a sorted listabout 20
O(n)linearx in my_list, a single for loop1,000,000
O(n log n)n log nsorted(), merge sortabout 20,000,000
O(n^2)quadratica loop inside a loop over the same list1,000,000,000,000

Linear search

Linear search starts at the first item and checks each one until it finds the target or reaches the end. It works on any list, sorted or not. In the worst case (the item is last or missing) it checks all n items, so it is O(n).

python
def linear_search(items, target):
    for index, item in enumerate(items):
        if item == target:
            return index
    return -1                   # not found

roll_numbers = [104, 87, 230, 15, 66]
print(linear_search(roll_numbers, 15))    # 3
print(linear_search(roll_numbers, 999))   # -1
Returning -1 for "not found" is a common convention.

Binary search, step by step

Binary search only works on a sorted list, but it is dramatically faster. It looks at the middle item. If that is the target, done. If the target is smaller, the answer must be in the left half; if bigger, in the right half. Each step throws away half of what is left, so a million items need only about 20 steps. That is O(log n).

  1. Sorted list: [3, 8, 15, 23, 42, 57, 61, 78, 90]. Target: 61. low = 0, high = 8.
  2. Middle index (0 + 8) // 2 = 4, value 42. 61 is bigger, so low = 5.
  3. Middle index (5 + 8) // 2 = 6, value 61. Found it at index 6 after only 2 looks.
  4. If low ever becomes greater than high, the target is not in the list.
python
def binary_search(items, target):
    low, high = 0, len(items) - 1
    while low <= high:
        mid = (low + high) // 2
        if items[mid] == target:
            return mid
        if items[mid] < target:
            low = mid + 1          # target is in the right half
        else:
            high = mid - 1         # target is in the left half
    return -1

data = [3, 8, 15, 23, 42, 57, 61, 78, 90]
print(binary_search(data, 61))   # 6
print(binary_search(data, 5))    # -1
Notice the three pieces: low, high and mid, and the loop condition low <= high.
Careful: Binary search on an unsorted list gives wrong answers without any error. Sort first, or keep the list sorted as you add items. Sorting costs O(n log n), so it only pays off if you search many times.

The bisect module

Python's standard library already has binary search in the bisect module. bisect_left(a, x) returns the position where x would go in the sorted list a, to the left of any equal items. insort(a, x) inserts x in the right place so the list stays sorted.

python
from bisect import bisect_left, bisect_right, insort

marks = [35, 48, 52, 52, 67, 80, 91]
i = bisect_left(marks, 52)
print(i, marks[i] == 52)                 # 2 True: found
print(bisect_right(marks, 52) - bisect_left(marks, 52))   # 2 students got 52
insort(marks, 75)
print(marks)                             # 75 lands in the right place
bisect gives you a correct, fast binary search without writing the loop yourself.

Why sets and dicts are so fast

Sets and dicts use hashing. A hash function turns a key into a number, and that number says almost exactly where the item is stored. So checking "is x in my_set?" jumps straight to the right place instead of walking the whole collection. On average it is O(1), no matter how big the set is.

python
import time

ids_list = list(range(100_000))
ids_set = set(ids_list)

start = time.perf_counter()
for _ in range(100):
    -1 in ids_list                       # walks all 100,000 items each time
list_ms = (time.perf_counter() - start) * 1000

start = time.perf_counter()
for _ in range(100):
    -1 in ids_set                        # one jump each time
set_ms = (time.perf_counter() - start) * 1000

print("list:", round(list_ms, 2), "ms   set:", round(set_ms, 4), "ms")
The set lookup is thousands of times faster here, and the gap grows with more data.
Tip: If your code checks "x in something" many times inside a loop, convert that something to a set once before the loop. This one change often turns an O(n^2) program into an O(n) one.

Watch the lesson

Big-O and searching · 1/6

Big-O and searching

In this lesson you will learn how to talk about the speed of an algorithm, and how to search data fast.

Prefer a video by a teacher? Find free videos on “big o notation binary search python explained” on YouTube ↗

In real life: Checking roll numbers on an exam results site

On results day, lakhs of students type their roll numbers at the same moment. If the server searched a plain list of 20 lakh roll numbers one by one, every search could take millions of steps. Keeping the roll numbers in a dict (roll number to result) makes each lookup a single jump, so the site stays fast under heavy load.

python
results = {
    "2401001": "Pass - 412/500",
    "2401002": "Pass - 388/500",
    "2401003": "Compartment in Maths",
}

def check_result(roll):
    return results.get(roll, "Roll number not found")

print(check_result("2401002"))
print(check_result("9999999"))
A dict lookup is O(1) on average, whether there are 3 entries or 20 lakh.

Practice (0/3 done)

Task 1 · Code · Python

Write linear_search(items, target) that returns the index of the first item equal to target, or -1 if it is not there. Use a loop (do not use items.index).

Task 2 · Code · Python

Write binary_search(items, target) for a SORTED list. Return the index of target or -1. The checker uses a list of 100,000 numbers and counts how many items you read: binary search needs about 17 reads, so more than 40 means you are not halving.

Task 3 · Code · Python

Write count_between(sorted_marks, low, high) that returns how many marks are in the range low to high (both included). Use the bisect module: bisect_right(..., high) - bisect_left(..., low).

Quick quiz

Question 1

1.What must be true before you can use binary search?

Question 2

2.About how many steps does binary search need for 1,000,000 sorted items?

Question 3

3.A loop inside a loop, both over the same list of n items, is usually...

Question 4

4.Why is "x in my_set" faster than "x in my_list" for big collections?

Question 5

5.What does bisect_left([10, 20, 30], 25) return?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Testing your code