Expert · Algorithms, data structures and testing · topic 45 of 50 · 30 min
Big-O and searching
Explanation
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.
| Big-O | Name | Example | Steps for n = 1,000,000 |
|---|---|---|---|
| O(1) | constant | my_dict[key], my_list[5] | 1 |
| O(log n) | logarithmic | binary search in a sorted list | about 20 |
| O(n) | linear | x in my_list, a single for loop | 1,000,000 |
| O(n log n) | n log n | sorted(), merge sort | about 20,000,000 |
| O(n^2) | quadratic | a loop inside a loop over the same list | 1,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).
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)) # -1Binary 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).
- Sorted list: [3, 8, 15, 23, 42, 57, 61, 78, 90]. Target: 61. low = 0, high = 8.
- Middle index (0 + 8) // 2 = 4, value 42. 61 is bigger, so low = 5.
- Middle index (5 + 8) // 2 = 6, value 61. Found it at index 6 after only 2 looks.
- If low ever becomes greater than high, the target is not in the list.
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)) # -1The 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.
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 placeWhy 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.
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")Watch the lesson
Big-O and searching · 1/6
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.
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"))Practice (0/3 done)
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).
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.
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
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.