Time Complexity and Space Complexity of an Algorithm Explained

Time Complexity and Space Complexity of an Algorithm Explained

Picture two programs that do the exact same job. Feed each one a thousand records and you can't tell them apart. Feed them a billion, though, and one finishes before you've let go of the Enter key while the other is still grinding away next Tuesday.

Same laptop. Same data. What separates them is time complexity, which is just a name for how fast an algorithm's workload balloons as you hand it more input.

We'll go through what the term actually means, how Big O notation writes it down, how to work it out from a real piece of code, and where space complexity comes in. Then, at the end, a place where all of this stops being homework and starts costing actual money: smart contracts.

What Is Time Complexity of an Algorithm?

Forget seconds for a moment. Time complexity counts steps. Comparisons, assignments, a bit of arithmetic here and there, and you write the total as a function of n, which is the input size.

Here's about the most boring function imaginable:

python def print_all(items): for item in items: print(item)

Ten items? Ten trips through the loop. Ten thousand? Ten thousand trips.

The work climbs in a straight line next to the input, so we call it O(n).

Now, maybe each trip really costs three little operations instead of one. Fine, that's 3n. Nobody cares, honestly, because doubling the list still doubles the work, and that growth curve is the bit we want. The exact number gets thrown out on purpose.

That's also why every DSA course and half the coding interviews out there hammer on time complexity. If you can read off the complexity of an algorithm by looking at it, you can guess how it'll behave on input data you never got around to testing. Which, in production, is most of the data.

Why Execution Time Is a Poor Measure of an Algorithm

So why not just run the thing with a timer? Plenty of people do, and I get it. The catch is that execution time drags in a pile of stuff your algorithm has nothing to do with:

  • Hardware. My five-year-old laptop and a brand-new cloud box will give you wildly different numbers for the same script.
  • Programming language. Write the same loop in C and in Python and you might see a 10× gap, or more.
  • System load. Some background updater kicks in, the garbage collector wakes up, the cache is cold. Your result moves.
  • Whatever input you happened to grab. A sort that flies on almost-sorted data can crawl on shuffled data.

A stopwatch gives you one run on one box with one input. Ask it about 100× the data and it's got nothing.

Step counts don't wobble. Same algorithm, same input, and you get the same number of steps whether it's Rust on a rack server or JavaScript on somebody's phone. That's why time complexity is a fair fight between two solutions before you've benchmarked a thing. I'd still benchmark afterwards, for what it's worth. It's just a different question.

Big O and Other Asymptotic Notations Explained

Textbooks call this asymptotic notation, which sounds worse than it is. You let n get enormous and stop caring about the small stuff, so constant multipliers and tiny inputs drop out and only the growth curve survives. There are three of them worth knowing:

Notation What it describes Plain meaning Example
Big O, O(f(n)) Upper bound The algorithm grows no faster than f(n) Linear search is O(n)
Big-Omega, Ω(f(n)) Lower bound The algorithm needs at least f(n) work Any comparison sort is Ω(n log n)
Big-Theta, Θ(f(n)) Tight bound Upper and lower bounds match Merge sort is Θ(n log n)

In practice you'll hear Big O notation about 95% of the time. It sets an upper bound, meaning the running time won't grow faster than the rate inside the parentheses. My first code review at a real job had a senior dev write "O(n²), pls fix" next to a nested loop, and what they meant was that the worst-case time complexity was quadratic and the list was about to get a lot longer.

Omega and Theta mostly live in university courses on the analysis of algorithms, and I don't think I've typed either one at work in years. Big O wins out because it answers what people actually ask in a meeting, which is "how bad does this get if traffic triples?"

There's one technicality that confused me for a while. Since Big O is only a ceiling, an O(n) function is also O(n²), strictly speaking, the same way a 5-foot person is "under 7 feet." True and useless. Everybody quotes the tightest bound they can justify and I'll do the same.

Time Complexity and Space Complexity

Common Time Complexity Classes With Examples

There aren't that many classes you'll see in real code, maybe seven if you count the scary ones. This is roughly what each costs at n = 1,000:

Class Name Typical example Operations at n = 1,000
O(1) Constant Array index, hash table lookup 1
O(log n) Logarithmic Binary search ~10
O(n) Linear Scanning a list 1,000
O(n log n) Linearithmic Merge sort, quicksort (average) ~10,000
O(n²) Quadratic Nested loops, bubble sort 1,000,000
O(2ⁿ) Exponential Naive recursive Fibonacci More than atoms in the universe
O(n!) Factorial Brute-force traveling salesperson Even larger

O(1): Constant Time

Whether your Python dict has ten keys or ten million, pulling one out by key costs about the same, and so does reading items[5] from a list. That flatness is the whole point of constant time.

O(log n): Logarithmic Time

I think of these as the "guess the number between 1 and 100" algorithms, where every guess rules out half of what's left. Binary search does exactly that on a sorted list:

python def binary_search(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid if arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1

With a billion sorted items it needs about 30 comparisons, while plodding through them one by one could take the full billion, and I've never quite stopped finding that funny.

O(n) and O(n log n)

A linear algorithm visits each element a fixed number of times, the way you would if you were hunting for the biggest number in a spreadsheet column by eye. O(n log n) is the home of decent sorting. Merge sort splits the list down to single items (log n levels of splitting) and then does one linear pass per level while merging, so its running time stays put whether the data arrives sorted or scrambled.

O(n²) and Worse

Quadratic code nearly always has a loop tucked inside another loop, like this duplicate check that compares every pair:

python def has_duplicate(items): for i in range(len(items)): for j in range(i + 1, len(items)): if items[i] == items[j]: return True return False

Exponential and factorial growth are worse by a mile. The textbook recursive Fibonacci keeps solving the same subproblems over and over, so every +1 on n roughly doubles the work. At n = 20 you won't notice; at n = 60 you can go cook dinner, eat it, wash up, and it still won't be done.

How to Calculate Time Complexity Step by Step

You almost never need a formal proof to calculate time complexity. I run through this list in my head and it covers most code I see:

  1. Define n. Decide what the input size is: the length of a list, the number of nodes in a graph, the number of digits in a number.
  2. Count the work in each line. Simple statements (assignments, comparisons, arithmetic) cost O(1).
  3. Add sequential blocks. Two loops one after another, each over n, give n + n = 2n.
  4. Multiply nested blocks. A loop over n inside another loop over n gives n × n = n².
  5. Drop constants. 2n becomes n, and 500 becomes 1.
  6. Keep only the dominant term. In n² + n, the n² part dwarfs the rest as n grows.

Quick worked one. Your count comes out to T(n) = 3n² + 5n + 2. Stick in a million: 3n² is three trillion, 5n is five million. Rounding error. Call it O(n²), done.

Loops that skip around instead of stepping by one are sneakier:

python def count_halvings(n): steps = 0 while n > 1: n //= 2 steps += 1 return steps

Every pass cuts n in half, so this while loop gives up after roughly log₂(n) rounds. Which is basically the whole answer to "when is time complexity O(log n)?" If what's left shrinks by a constant factor each step, you're there.

Recursion wants one more tool, a recurrence. Merge sort does two half-size jobs and then a merge, so T(n) = 2T(n/2) + n. The master theorem is a cheat sheet for recurrences of that shape, and for this one it spits out O(n log n).

And a rule of thumb from competitive programming I lean on a lot: a normal machine gets through something like 10⁸ simple operations a second. Very rough, but it's enough to sanity-check the time complexity of a solution before you hit submit. With n at 10⁵, O(n²) is about 10¹⁰ operations, so you time out. O(n log n) is under 2 million. Sails through.

Worst Case, Best Case and Average Case Analysis

Feed the same algorithm different input and the amount of work can swing wildly. Linear search shows it nicely:

  • Best case: what you want is sitting right at the front. One comparison and you're out.
  • Worst case: it's at the very end, or not there at all. That's n comparisons.
  • Average case: if it could be anywhere with equal odds, you'll check around n/2 on average. Still O(n), mind you.

Ask a developer about "the complexity" of something and they'll nearly always give you worst-case time complexity, because that number is a promise you can plan around.

Say your API handles a request in O(n) at worst. Great, now you know how the slowest request behaves, and you're also covered when somebody sends you garbage on purpose. People absolutely do this: if an attacker works out which keys make your hash table collide, those are the only keys you'll be seeing for a while.

The average case is worth a look when the bad scenario hardly ever turns up. Quicksort is the poster child here, averaging O(n log n) but sliding to O(n²) if you pick pivots naively on data that's already sorted. Standard libraries work around that with random pivots, or by switching to a different sorting algorithm once the recursion goes suspiciously deep.

What Is Space Complexity and Auxiliary Space?

Memory is the other bill an algorithm runs up. Space complexity tracks how much of it you need as the input size grows, and you write it with the same Big O notation as time.

Part of that memory is the input itself, which you're paying for no matter what you do with it. The part you actually control is called auxiliary space, meaning the extra stuff your code allocates while it works, like temp variables and new arrays, plus the call stack that nobody remembers until it bites them.

Some quick cases:

  • Swapping two variables needs a couple of slots regardless of anything else, so it's O(1) auxiliary space.
  • Copying a list of n items into a fresh list costs you O(n) extra memory.
  • Recursing n levels deep leaves n frames sitting on the stack, which is O(n) space even though you never built an array.

I watched a junior dev lose an afternoon to that last one. Their recursive function looked like it used no memory at all, but every unfinished call was parked on the stack waiting for the deeper ones to return, and on a big input the whole thing died with a stack overflow.

A lot of optimization is just trading memory for time or the other way round. Memoization caches results you've already computed, which takes naive Fibonacci from O(2ⁿ) down to O(n) time in exchange for O(n) memory. Hash tables buy you O(1) average lookups by spending space on buckets. On a server with 64 GB of RAM that trade is usually a no-brainer, but on a microcontroller or inside a smart contract you'd think twice.

Time Complexity and Space Complexity

Time and Space Complexity of Common Data Structures

Picking the right data structure usually beats micro-tuning whatever's inside your loop. Average-case costs for the ones you'll reach for most:

Data structure Access Search Insert Delete Space
Array O(1) O(n) O(n) O(n) O(n)
Linked list O(n) O(n) O(1) O(1) O(n)
Hash table n/a O(1) O(1) O(1) O(n)
Balanced binary search tree O(log n) O(log n) O(log n) O(log n) O(n)
Binary heap O(1) for min/max O(n) O(log n) O(log n) O(n)

Two footnotes. Linked list inserts and deletes are only O(1) if you're already holding the node. And a hash table can sink to O(n) when lots of keys collide.

Sorting algorithms vary just as much:

Sorting algorithm Best Average Worst Auxiliary space
Bubble sort O(n) O(n²) O(n²) O(1)
Merge sort O(n log n) O(n log n) O(n log n) O(n)
Quicksort O(n log n) O(n log n) O(n²) O(log n)
Heap sort O(n log n) O(n log n) O(n log n) O(1)

Merge sort is predictable but wants extra memory. Heap sort sorts in place and keeps the same worst case. Quicksort, despite the uglier ceiling, tends to win in practice.

Time Complexity in Blockchain and Smart Contracts

Here's where it gets fun. On your own server, a sloppy algorithm wastes CPU and maybe annoys a user. On Ethereum or any EVM chain, it wastes money. Every opcode a smart contract executes costs gas, paid in the chain's native coin, so your operation count shows up as a line on somebody's bill. Bad time complexity, in other words, comes with a price tag.

Say you've got a contract that loops over every token holder to pay out rewards. O(n), and gas creeps up with each new holder. On launch day nobody notices.

Then the holder list keeps growing. One day the payout needs more gas than the block gas limit, which is the cap on how much gas a single block can fit. After that the transaction can't go through. Not "it's slow." It just never succeeds, and whatever funds were waiting behind that function are basically stuck. Auditors file unbounded loops like this under denial-of-service, and fair enough.

The fixes are complexity thinking, pure and simple:

  • Split the work into fixed-size batches over several transactions.
  • Flip it around so each user claims their own reward. Every call is O(1).
  • Swap the loop for a Merkle proof.

I love Merkle proofs as an example of why log n matters so much on-chain. A Merkle tree hashes a big pile of data up into one root hash.

To prove one entry belongs to a set of n, you need about log₂(n) hashes. For a million entries that's roughly 20. Not a million. Airdrops lean on this, and light Bitcoin wallets use the same trick to confirm a transaction without downloading the entire block.

Off-chain, same deal. Picture a payment backend that matches each incoming blockchain transaction to an invoice by scanning every open one. O(n) per payment, and it gets slower as the shop grows. Put the invoices in a hash map keyed by address or ID and each match is O(1). And if you'd rather not build and babysit that plumbing yourself, a crypto payment gateway such as Plisio does it for you.

Any questions?

No, because it counts operations relative to input size and the hardware has no say in that count. What you’ll see change from machine to machine is execution time. A fast CPU finishes an O(n²) job sooner than a slow one, but double the input on either and the work still goes up about four times.

Constant time, O(1), since the work never grows. Logarithmic O(log n) is a close second and handles a billion items in roughly 30 steps. Linear comes next, then O(n log n), then quadratic, and the exponential classes bring up the rear.

Once inputs get large, yes, but with small ones it can go either way. Big O ignores constant factors, so an O(1) call that costs 10,000 steps can lose to an O(n) loop over 50 items, which is why scanning a short array sometimes beats a hash table lookup.

Time complexity describes how the number of operations grows with the input, and space complexity describes how memory grows. Merge sort is quick but needs an O(n) extra array, while bubble sort is slow and gets by with O(1) space, so the two don’t have to move together.

It comes down to how the loop variable moves. Stepping by 1 up to n gives O(n), doubling or halving on each pass gives O(log n), and if there’s another loop over n inside the body you multiply the two together.

It gives you a way to compare algorithms by how they scale without dragging a specific machine into it. You drop the constants and small terms, keep the upper bound, and get a clear picture of how an algorithm behaves as the input grows, which is usually what you need when picking between two approaches.

Ready to Get Started?

Create an account and start accepting payments – no contracts or KYC required. Or, contact us to design a custom package for your business.

Make first step

Always know what you pay

Integrated per-transaction pricing with no hidden fees

Start your integration

Set up Plisio swiftly in just 10 minutes.