Skip to content
Free IDE

Fundamentals

Algorithm Complexity (Big-O) Explained with Python

Learn what Big-O notation is, count steps in Python and compare O(1), O(log n), O(n) and O(n²) with examples that run in the simulator, no stopwatch.

By Prof. Rodrigo Faustino · Updated on · 21 min read

Metrics for this article

Words
4,150
Characters
22,795
Reading time
~21 min

In the previous articles, two questions were left hanging. Why is binary search so much faster than linear search? Why does merge sort handle large lists and bubble sort does not? In each case, there was a count of comparisons and a sentence of the type "the work grows with the square of n". This article gives a name and a method to that idea: algorithm complexity, written with Big-O notation.

The question it answers is simple: when the input grows, how much does the work grow? A program that takes an instant with 10 readings may take an eternity with 10 thousand, or may stay instantaneous. Knowing which of the two cases applies before running is one of the most useful skills for anyone who programs, and on the ESP32 it weighs more than on the computer, because the board has far less memory and speed. All the examples below ran in the simulator, and the numbers quoted are the ones it produces. What the computer's Python shows differently is said in the text.

Counting steps instead of seconds

The first idea that comes to mind is to use a stopwatch: run the program and measure how long it took. The problem is that time depends on everything around it. The same function takes one time on the computer, another on the board, another if the computer is busy, and yet another if the simulator is running in a crowded tab. A number in seconds says nothing about the algorithm, only about that run.

The way out is to count steps. You choose the operation that best represents the work, such as a comparison or a turn of a loop, and count how many times it happens as a function of the input size n. The count is the same on any machine. See four functions with very different costs, each returning how many steps it took:

def constante(lista):
    return lista[0], 1

def linear(lista):
    passos = 0
    total = 0
    for v in lista:
        total += v
        passos += 1
    return total, passos

def quadratico(lista):
    passos = 0
    for i in range(len(lista)):
        for j in range(len(lista)):
            passos += 1
    return passos

def logaritmico(n):
    passos = 0
    while n > 1:
        n = n // 2
        passos += 1
    return passos

for n in [10, 20, 40, 80]:
    lista = list(range(n))
    print(n, "O(1):", constante(lista)[1], "O(n):", linear(lista)[1], "O(n^2):", quadratico(lista), "O(log n):", logaritmico(n))
print(logaritmico(1000), logaritmico(1000000), logaritmico(1))
print("end")

The output has four lines, one per size, and then 9 19 0 and end. For n = 10, the first line shows O(1): 1, O(n): 10, O(n^2): 100 and O(log n): 3. For n = 80, they are 1, 80, 6400 and 6. Compare the columns as n doubles from 10 to 20, to 40 and to 80. The O(1) one stays still at 1. The O(log n) one goes up from 3 to 4, 5 and 6, just one step at each doubling. The O(n) one doubles along with it, and the O(n^2) one quadruples: 100, 400, 1600, 6400. (The function names mean constante = constant, quadratico = quadratic and logaritmico = logarithmic.)

That is what Big-O describes. It does not matter whether a step takes a microsecond or a millisecond. What matters is the shape of the curve that the work draws as n grows.

A table of counted steps for lists of 10, 20, 40 and 80 elements. The class O(1) makes 1 step at all sizes. The class O(log n) makes 3, 4, 5 and 6. The class O(n) makes 10, 20, 40 and 80. The class O(n squared) makes 100, 400, 1600 and 6400. In the last row, when n doubles, O(1) stays the same, O(log n) adds one step, O(n) doubles and O(n squared) quadruples.
Steps counted in the simulator. What matters is what happens to the work when the size doubles.
FastMindLocal AI assistant

The AI that remembers your life and lives on your computer

  • Long-term memory with the source of every answer
  • Works offline: your data stays on your PC
  • Reads PDFs, slides, photos and meeting recordings
Download free for Windows

Essential plan free forever

Measuring time also has its place

Counting steps does not replace the stopwatch, it just answers another question. When the doubt is "does this stretch fit in the time I have between two sensor readings?", the answer comes from measuring on the board itself. MicroPython offers time.ticks_ms() and time.ticks_us() for this, and time.ticks_diff() to subtract two instants safely, since the counter wraps around after some time:

import time
print(hasattr(time, "ticks_ms"), hasattr(time, "ticks_us"), hasattr(time, "ticks_diff"))
t0 = time.ticks_ms()
t1 = time.ticks_ms()
d = time.ticks_diff(t1, t0)
print(type(d), d >= 0)
print("end")

The output is True True True, then <class 'int'> True and end. These functions belong to MicroPython. In the computer's Python they do not exist, and there you use time.perf_counter(). The measured values do not appear in the examples of this article because they change on every run, and an output that changes is no use for checking what was learned. The practical rule is to use the count to choose the algorithm and the stopwatch to decide whether it works on the board.

What Big-O describes

Writing O(n) is read "order of n", and says that the work grows proportionally to n. Two rules turn an exact count into a class:

  1. Only the term that grows the most counts. If an algorithm makes 3n² + 5n + 2 steps, it is O(n²), because for large n the other terms vanish next to the first.
  2. Multiplicative constants do not count. 3n² and n² are in the same class, because tripling the time does not change the shape of the curve.

The table below shows why the first rule works. It calculates 3n² + 5n + 2 and the share that belongs to the 3n² term:

for n in [10, 100, 1000, 10000]:
    total = 3 * n * n + 5 * n + 2
    so_n2 = 3 * n * n
    print(n, total, so_n2, round(100 * so_n2 / total, 2))
print("end")

The output is 10 352 300 85.23, 100 30502 30000 98.35, 1000 3005002 3000000 99.83, 10000 300050002 300000000 99.98 and end. With 10 elements, the 3n² term accounts for 85% of the total. With 10 thousand, for almost 100%. The terms 5n and 2 become a detail, and that is why Big-O throws them away.

This also shows the limit of the notation. O(n²) does not say that an algorithm is slow, it says that it becomes slow when n grows. With few elements, a simple O(n²) can beat a complicated O(n log n). The notation speaks of the algorithm's future, not its present.

The most common classes

Almost everything you write falls into one of these classes, from the cheapest to the most expensive:

O(1), constant. The work does not depend on n. Reading lista[0], reading lista[i], getting len(lista), adding two numbers, looking up a key in a dictionary. In general, also lista.append(x).

O(log n), logarithmic. The work grows slightly, one step each time the size doubles. It is the case of whoever cuts the problem in half at each step, like binary search.

O(n), linear. A loop that goes through each element once. Adding up the list, finding the largest, linear search, x in lista, sum(lista), max(lista).

O(n log n). The typical cost of good sorting algorithms, like merge sort. A little above linear.

O(n²), quadratic. One loop inside another, both going through the list. Bubble, selection and insertion sort, comparing all the pairs.

O(2ⁿ) and similar, exponential. The work doubles, or more, with each extra element. It only serves for tiny inputs. The recursive Fibonacci, further on, is an example.

One way to recognize the class by looking at the code is to count nested loops: with no loop over the input the cost is usually O(1) or O(log n), one loop over n is O(n), two nested are O(n²). But beware of three cautions that counting loops does not catch.

def dois_lacos_seguidos(n):
    passos = 0
    for i in range(n):
        passos += 1
    for j in range(n):
        passos += 1
    return passos

def laco_triangular(n):
    passos = 0
    for i in range(n):
        for j in range(i):
            passos += 1
    return passos

def meio_com_barra(n):
    passos = 0
    while n > 1:
        n = n / 2
        passos += 1
    return passos

def meio_com_duas_barras(n):
    passos = 0
    while n > 1:
        n = n // 2
        passos += 1
    return passos

for n in [10, 20, 40]:
    print(n, dois_lacos_seguidos(n), laco_triangular(n))
print(meio_com_barra(1000), meio_com_duas_barras(1000))
print("end")

The output starts with 10 20 45, 20 40 190 and 40 80 780, and then 10 9 and end. The two consecutive loops make 2n steps, which is still O(n): the two vanishes by the second rule. The nested loops of the triangle, in which the inner one only goes up to i, make n(n-1)/2 steps. It is half the square, and even so it is O(n²): from 20 to 40 elements, the work goes from 190 to 780, about four times. And the last line shows a silent trap: with /, the n becomes a decimal and the loop only ends when the value falls below 1, which gives 10 steps. With //, the integer division stops at 1 and gives 9. The class is the same, but the exact count changes, and the program does not warn. (The names mean dois_lacos_seguidos = two consecutive loops, laco_triangular = triangular loop, meio_com_barra = half with one slash and meio_com_duas_barras = half with two slashes.)

FastTeacherFor teachers

From the syllabus to a planned semester

  • Teaching plan and calendar built automatically
  • Class diary ready to paste into SIGA
  • Slides and exercise lists with answer keys
Meet FastTeacher

Free Community edition · Windows

O(log n): cutting the problem in half

The logaritmico from the first example deserves a closer look, because the class is the one least seen in everyday life. It asks: how many times can I divide n in half until I reach 1? For 1000, the answer was 9. For a million, 19. For 1, none, 0.

Notice the size of the jump: from a thousand to a million, a thousand times more elements, the work went from 9 to 19 steps. That is the power of binary search. Searching among a million ordered elements takes about 20 comparisons. Linear search, in the worst case, would take a million. It is the reason sorting, with its cost of O(n log n), pays off when you are going to search the list many times: you pay once, and each search afterwards costs almost nothing.

The logarithm appears whenever a step discards a fraction of what is left, and not a fixed amount. The base of the logarithm, 2, 10 or another, does not matter for Big-O, because changing base only multiplies by a constant. In the examples, division by 2 is used because it is what the algorithms do.

When recursion explodes: Fibonacci

Recursion can hide an enormous cost under short, pretty code. The classic example is Fibonacci, in which each number is the sum of the two before it. The recursive version copies the definition almost word for word, and the loop version keeps only the last two values. Both give the same result, and the program counts the work of each one:

chamadas = 0
def fib(n):
    global chamadas
    chamadas += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

def fib_laco(n):
    a = 0
    b = 1
    passos = 0
    for i in range(n):
        a, b = b, a + b
        passos += 1
    return a, passos

for n in [5, 10, 15, 20]:
    chamadas = 0
    r = fib(n)
    print(n, r, "calls:", chamadas, "| loop:", fib_laco(n))
print("end")

The output has four lines and the end. For n = 5, 5 5 calls: 15 | loop: (5, 5). For n = 10, 10 55 calls: 177 | loop: (55, 10). For 15, 15 610 calls: 1973 | loop: (610, 15). For 20, 20 6765 calls: 21891 | loop: (6765, 20).

The result is the same in both versions, but the work is not. The loop takes n steps. The recursion makes 15, 177, 1973 and 21891 calls, and the ratio between one line and the next stays around 11: each 5 extra units in n multiply the work by about eleven. This is exponential growth. The cause is that fib(n) calls fib(n - 1) and fib(n - 2), and the two recalculate, each on its own, almost everything the other has already calculated. For fib(30), there would be more than 2.6 million calls, and for fib(40), more than 331 million. These two totals come from the formula calculation, not from a run, because the simulator is no place to run that much work.

Bars on a logarithmic scale comparing the calls of the recursive fibonacci with the steps of the loop fibonacci. For n equal to 5, 10, 15 and 20, the recursive version makes 15, 177, 1973 and 21891 calls, and the loop makes 5, 10, 15 and 20 steps. At n equal to 20, the recursive version makes more than a thousand times the work of the loop.
Calls measured in the simulator. The double recursion of fibonacci grows exponentially, and the loop grows in a straight line.

The lesson is not "avoid recursion". Merge sort is recursive and is O(n log n). The lesson is that the shape of the code does not tell the cost: you need to count how many times each piece runs. In Fibonacci, the waste comes from recalculating the same values, and the cure is to store what has already been calculated, either in a loop, or in a dictionary used as memory.

The same problem with three costs

See how the choice of algorithm changes the class of the same problem: finding out whether a list has repeated values. Three ways, each counting its steps:

def duplicados_quadratico(lista):
    comparacoes = 0
    for i in range(len(lista)):
        for j in range(i + 1, len(lista)):
            comparacoes += 1
            if lista[i] == lista[j]:
                return True, comparacoes
    return False, comparacoes

def duplicados_ordenando(lista):
    copia = sorted(lista)
    comparacoes = 0
    for i in range(1, len(copia)):
        comparacoes += 1
        if copia[i] == copia[i - 1]:
            return True, comparacoes
    return False, comparacoes

def duplicados_conjunto(lista):
    vistos = set()
    passos = 0
    for v in lista:
        passos += 1
        if v in vistos:
            return True, passos
        vistos.add(v)
    return False, passos

for n in [10, 100]:
    sem = list(range(n))
    print(n, "no repeats:", duplicados_quadratico(sem), duplicados_ordenando(sem), duplicados_conjunto(sem))
com = [3, 8, 1, 9, 4, 8, 2]
print(duplicados_quadratico(com), duplicados_ordenando(com), duplicados_conjunto(com))
print("end")

The output has three lines and the end. The first, for 10 elements with no repeats, is 10 no repeats: (False, 45) (False, 9) (False, 10). The second, for 100, is 100 no repeats: (False, 4950) (False, 99) (False, 100). The third, with a repeat, is (True, 10) (True, 5) (True, 6). (The names mean duplicados = duplicates, ordenando = sorting, conjunto = set, vistos = seen and sem / com = without / with.)

All three answer correctly, but with different costs. The first compares all the pairs: 45 comparisons with 10 elements and 4950 with 100. It is O(n²), like the triangular loop from the earlier example. The second sorts first, and then the repeats sit side by side, so it is enough to compare each element with the previous one, 9 and 99 comparisons. But these counts do not include the sorting, which costs O(n log n). The total cost of the method is that of the sorting, because it dominates the comparisons. The third uses a set: for each value, it asks whether it has been seen, and the question v in vistos costs O(1) on average. There are 10 and 100 steps, O(n).

The third looks like the winner, and in most cases it is, but it brings a cost that the step count does not show: the set keeps a copy of the values, and that uses memory. On the ESP32, with little memory, trading space for time is not always a good deal. The first method uses almost no memory beyond the list. This is common: there is rarely an algorithm that wins on everything, and choosing is balancing time, memory and simplicity.

Notice also the case of the com list, with the 8 repeated: the methods stopped early, after 10, 5 and 6 steps. That is the best case, when the answer appears before the end. Big-O usually speaks of the worst case, which is that of a list with no repeats, because it is the one that guarantees a limit on waiting. It is what is measured in the table of counts with 10 and 100 elements.

The cost hidden inside a command

Counting loops misleads when a one-line command hides a loop. The most common example is the in applied to a list: x in lista looks like a question, but underneath it is a linear search, O(n). Inside a loop, it becomes O(n²) without the code showing two loops:

def contem(lista, x):
    global comparacoes
    for v in lista:
        comparacoes += 1
        if v == x:
            return True
    return False
comparacoes = 0
def sem_repetidos_lista(dados):
    global comparacoes
    resultado = []
    for x in dados:
        if not contem(resultado, x):
            resultado.append(x)
    return resultado
for n in [10, 100]:
    comparacoes = 0
    r = sem_repetidos_lista(list(range(n)))
    print(n, len(r), comparacoes)
dados = [3, 1, 3, 2, 1]
comparacoes = 0
print(sem_repetidos_lista(dados), comparacoes, sorted(set(dados)))
print("end")

The output is 10 10 45, 100 100 4950, [3, 1, 2] 6 [1, 2, 3] and end. The contem function (contains) plays the role of in and counts the comparisons, so the hidden work is in view. With 10 elements with no repeats, there were 45 comparisons, and with 100, 4950: the same numbers as the quadratic method from the previous section, only now inside a function that, at first sight, has a single loop. The last line shows that the function keeps the order of first appearance ([3, 1, 2]), while sorted(set(dados)) returns the values ordered.

Other commands that hide a loop are sum, max, min, lista.index(x), lista.count(x), lista.remove(x) and the slicing lista[a:b], which copies the elements. All of them cost O(n), and inside a loop over the same list they weigh like a second loop. The in of a set or of a dictionary, on the other hand, costs O(1) on average, and that is why the third method of the previous section stayed linear.

The following example puts it all together in a real ESP32 situation: the moving average of a sensor's last readings. The naive version redoes the sum of the whole window on every reading. The smart one keeps the sum and only updates the ends: it adds the new value and removes the one that left the window.

leituras = [20, 22, 21, 25, 30, 28, 27, 26, 24, 23]
janela = 4

def media_movel_refazendo(dados, k):
    medias = []
    somas = 0
    for i in range(k - 1, len(dados)):
        s = 0
        for j in range(i - k + 1, i + 1):
            s += dados[j]
            somas += 1
        medias.append(s / k)
    return medias, somas

def media_movel_acumulando(dados, k):
    medias = []
    operacoes = 0
    s = 0
    for i in range(len(dados)):
        s += dados[i]
        operacoes += 1
        if i >= k:
            s -= dados[i - k]
            operacoes += 1
        if i >= k - 1:
            medias.append(s / k)
    return medias, operacoes

m1, c1 = media_movel_refazendo(leituras, janela)
m2, c2 = media_movel_acumulando(leituras, janela)
print(m1)
print(m2)
print(m1 == m2, c1, c2)
grandes = list(range(100))
print(media_movel_refazendo(grandes, 10)[1], media_movel_acumulando(grandes, 10)[1])
print(media_movel_refazendo(grandes, 50)[1], media_movel_acumulando(grandes, 50)[1])
print("end")

(The names mean leituras = readings, janela = window, media_movel_refazendo = moving average redoing, media_movel_acumulando = moving average accumulating and grandes = large.) The output starts with the two lists of averages, equal: [22.0, 24.5, 26.0, 27.5, 27.75, 26.25, 25.0]. Then comes True 28 16, the sums done by the two methods on the 10 readings. Then 910 190, for 100 readings and a window of 10, and 2550 150, for a window of 50, and the end.

The two versions give the same answer, and that is the charm: the same problem, the same result, and very different costs. The version that redoes the sum makes (n - k + 1) * k sums, which grows with the window size: 910 with a window of 10 and 2550 with a window of 50. The one that accumulates makes about 2n operations, almost always the same, 190 and 150, because the larger window does not weigh. The first is O(n * k), and the second is O(n). On a board that reads a sensor several times per second, the difference decides whether the program gets the job done.

The algorithms of this part

With the notation at hand, the algorithms of the previous articles are summed up in a table. The columns separate the best and the worst case, because several algorithms behave differently depending on the input:

A table with the complexity of the algorithms in this part of the series. Linear search: best case O(1), worst case O(n). Binary search: O(1) and O(log n), and requires an ordered list. Bubble sort: O(n) and O(n squared), with early stop. Selection sort: O(n squared) in both cases. Insertion sort: O(n) and O(n squared). Merge sort: O(n log n) in both cases. Quick sort: O(n log n) and O(n squared). The x in list test: O(1) and O(n). The x in set test: O(1) on average.
The algorithms that the series has already shown, with the cost in the best and worst case.

A few points for reading the table carefully:

  • Linear search finds it at once if the value is at the start, O(1), and goes through everything if it is at the end or does not exist, O(n).
  • Binary search is O(log n), but only works on an ordered list. Sorting costs O(n log n), so it only pays off when you search several times.
  • Bubble sort with early stop does O(n) on an already ordered list, a single pass. Selection sort has no such exit: it makes the same comparisons on any list, and what it saves are the swaps.
  • Insertion sort is also O(n) when the list is already almost in order. That is why the article about it recommends it for small, almost ordered lists.
  • Merge sort keeps O(n log n) on any input, but creates new lists. In the previous article, a reversed list of 1000 elements took 5044 comparisons in merge sort, against 499500 for insertion sort, that second value calculated by the formula.
  • Quick sort is O(n log n) on average, but falls to O(n²) in the worst case, when the chosen pivot is always the smallest or the largest element.

The table also recalls an uncomfortable truth: for small lists, everything is fast. The difference between the classes only shows up when n grows. If your list has 20 elements and will never go beyond that, the algorithm that is simplest to understand is often the best.

What Big-O does not say

The notation is a tool, not a verdict, and it is worth knowing its limits:

  • It does not measure real time. Two O(n) algorithms can differ by ten times, because Big-O throws away the constants. The stopwatch on the board itself gives the final answer.
  • It does not measure memory. The set method won in time and lost in space. There is a space complexity, written with the same notation, and on the ESP32 it matters as much as the time one.
  • It does not hold for small n. Constants and smaller terms, which the notation ignores, decide when n is small.
  • It depends on the input. That is why there are the best, the average and the worst case, and you need to know which one you are talking about.
  • It depends on what is counted. Counting comparisons, swaps or calls gives different numbers, and all are valid as long as you say what the choice was.

On the ESP32: where this weighs

In a sensor program, n is almost never large, but the main loop runs forever. An O(n²) function over 50 readings makes 2500 steps, and if it runs on every cycle, the program spends all its time there. Three situations in which the calculation pays off:

  • Average or filter of the last readings. As in the moving average above, redoing the sum on every reading costs O(n * k). Keeping the sum costs O(n).
  • Searching a calibration table. If the table is consulted all the time, keeping it ordered and using binary search, or in a dictionary, costs far less than going through it on every query.
  • Keeping a history. A list that only grows ends up using up the board's memory. Limiting the size, discarding the oldest, is a decision of space that time Big-O does not see.

In all these cases, the path is the one in the article: count the steps, look at how the count grows, and only then, if the doubt persists, measure on the board with time.ticks_ms().

Common errors

Confusing two loops with O(n²). Two consecutive loops add up, O(n + n), and are still O(n). Only nested ones multiply. Before writing O(n²), check that one loop is really inside the other and that both go through the input size.

Forgetting the hidden loop. x in lista, max, sum and slicing are loops. Counting only the for and while that appear in the code gives a smaller class than the true one.

Failing to count the sorting. When a method starts with sorted(lista), its cost is at least that of the sorting, and not that of the loop that comes afterwards. On the computer, sorted() is O(n log n).

Confusing the best case with the common case. An algorithm that stopped early on the test list may need the whole list on another. Always test the worst case, such as the list with no repeats or the value that does not exist.

Measuring only with small n. With 10 elements, O(n²) and O(n) look the same. Compare counts with n, 2n and 4n, as in the examples, and see what happens when it doubles.

Counting without saying what. A count of "steps" that mixes comparisons and assignments has no meaning. Choose one operation and count only it.

What tends to show up in class

These are patterns that repeat with people learning this subject. They are not the account of a specific class.

Thinking Big-O is a measure of seconds. The person asks "how many seconds is O(n)?". The notation does not answer that, it only says how the work grows. Counting steps, before the stopwatch, helps separate the two ideas.

Memorizing the table without knowing how to count. Whoever memorizes "binary search is log n" but cannot count the steps of a small loop gets stuck on the first different function. Counting by hand, for n = 4 and n = 8, and checking in the simulator teaches more than the table.

Thinking fewer lines is faster. A line with x in lista inside a loop looks leaner than two loops, and costs the same. The size of the code says nothing about the cost.

Ignoring the loop inside the command. The question that helps is: what does this command do underneath? If the answer is "goes through the list", it is a loop.

Wanting the lowest class in everything. Rewriting a 20-element program to gain a class only complicates the code. The right question is whether n will grow.

Exercise: counting steps with user input

Write a program that reads a size n, counts the steps of a simple loop and of two nested loops, and shows the two totals. Then, ask for another n and repeat only the two loops, to see the effect of doubling. Use entradas of 6 and 12 in the simulator, which is what the block below does:

n = int(input("How many elements? "))
passos_um = 0
for i in range(n):
    passos_um += 1
passos_dois = 0
for i in range(n):
    for j in range(n):
        passos_dois += 1
print("n =", n)
print("one loop:", passos_um)
print("two loops:", passos_dois)
n = int(input("And now? "))
passos_dois = 0
for i in range(n):
    for j in range(n):
        passos_dois += 1
print("n =", n, "two loops:", passos_dois)
print("end")

The simulator shows each question followed by the typed value, so the output starts with How many elements? 6. Then come n = 6, one loop: 6, two loops: 36, And now? 12, n = 12 two loops: 144 and end. Notice: n doubled from 6 to 12, and the steps of the double loop went from 36 to 144, four times more, as the table at the start predicted.

To practice, try: swapping the double loop for a triangular one and checking the formula n(n-1)/2; adding a third nested loop and seeing the class become O(n³); counting the steps of a while that divides n by 2; and writing a function that counts the comparisons of linear search in the worst case.

Frequently asked questions

What does the O in Big-O mean? It is the letter for "order": the order of magnitude of the work's growth. O(n²) is read "O of n squared".

What is the difference between O(n) and O(n log n)? The second grows a little faster. For a million elements, n log n is about 20 million, against the million of n. For small lists, it is hardly noticed.

Does O(1) mean a single step? It means that the number of steps does not depend on n. It can be one, two or ten, as long as it does not grow along with the input.

Does the base of the logarithm matter? For Big-O, no. Changing base multiplies by a constant, and constants do not count.

Why does Big-O talk about the worst case? Because it gives a guarantee: the algorithm will never take longer than that. The best case is too optimistic for planning the time of a program.

Is there a Big-O for memory? There is, and it is called space complexity. The set method, for example, uses O(n) of extra space to gain time.

Does MicroPython's sorted() have the same cost as the computer's Python? On the computer, sorted() is O(n log n). MicroPython's implementation is another, as already appeared with stability, and this article does not state its exact cost.

Do I need to know Big-O to program the ESP32? For the first projects, no. But it avoids the shock of a program that works fine with 10 readings and hangs with 1000.

Where to go next

The challenges List Search, Largest in the List and Average of the List are good for counting steps: solve each one and say what the class of your solution is. To review the basics, go through the articles on linear and binary search, bubble and selection sort, insertion sort, merge sort and quick sort, recursion and sets, and through the Logic with Python track.

The next article opens a new part of the series, on objects, errors and good practices, with classes and objects: how to join data and functions into a single thing, with attributes, methods and __init__, and how this organizes a program that grows.

To practice in PyPlaca

Tracks

Lessons

Component datasheet cards

FastTeacherFor teachers

From the syllabus to a planned semester

  • Teaching plan and calendar built automatically
  • Class diary ready to paste into SIGA
  • Slides and exercise lists with answer keys
Meet FastTeacher

Free Community edition · Windows

FastMindLocal AI assistant

The AI that remembers your life and lives on your computer

  • Long-term memory with the source of every answer
  • Works offline: your data stays on your PC
  • Reads PDFs, slides, photos and meeting recordings
Download free for Windows

Essential plan free forever

FastTeacherFor teachers

From the syllabus to a planned semester

  • Teaching plan and calendar built automatically
  • Class diary ready to paste into SIGA
  • Slides and exercise lists with answer keys
Meet FastTeacher

Free Community edition · Windows