Algorithms Introduction
What an algorithm is, why the same problem can have wildly different-speed solutions, and a Big-O refresher.
What is an algorithm?
An algorithm is simply a precise, step-by-step procedure for solving a problem — a recipe that a computer can follow exactly, every time, with no ambiguity about what to do next. "Sort this list," "find the shortest route between two cities," "check whether this word is a palindrome" — each of these is a problem; the specific sequence of steps you write to solve it is the algorithm.
Here's the idea this entire course is built around: the same problem can have multiple correct algorithms that solve it, and those algorithms can differ enormously in how fast they run — not because one is "better code," but because they fundamentally do a different amount of work. Learning algorithms means building a library of these strategies, and — just as importantly — building the judgment to recognize which one a given problem actually calls for.
If you've already gone through the data structures course, this idea should sound familiar: it's the exact same lesson as "the same data, organized differently, changes how fast an operation runs" — just applied to procedures instead of storage. Data structures and algorithms are really two sides of one coin: how you organize data, and what steps you perform on it.
A concrete example: checking for duplicates
Suppose you're given a list of numbers and need to answer: does any value appear more than once?
The naive approach compares every pair of elements directly:
def has_duplicate_naive(nums):
for i in range(len(nums)):
for j in range(len(nums)):
if i != j and nums[i] == nums[j]:
return True
return False
For every one of the n elements, this scans up to the rest of the list again — a nested loop over the same data, which does roughly n * n comparisons in the worst case. Written in Big-O terms (covered fully in the data structures introduction, if you haven't seen it yet): this is O(n²).
A much better approach uses a set to remember what's already been seen:
def has_duplicate_fast(nums):
seen = set()
for num in nums:
if num in seen: # O(1) average membership check
return True
seen.add(num)
return False
This makes exactly one pass over the list, doing O(1) average work per element (a hash set membership check), for O(n) total — a full order of magnitude better than the naive version.
Does the difference actually matter? Let's make it concrete. For a list of 10,000 numbers, the naive approach does up to 100,000,000 comparisons; the fast approach does about 10,000. That's not a minor tweak — it's the difference between a function that returns instantly and one that might take noticeably longer, purely because of which steps it chose to take, not because of anything about the hardware it's running on. Both functions are 100% correct. Only one of them scales.
Big-O's role in comparing algorithms
Big-O notation — O(1), O(log n), O(n), O(n log n), O(n²), and so on — is the language used to describe how the number of steps an algorithm takes grows as the input size grows. It intentionally ignores exact timings (which depend on your CPU, your language, background load) in favor of describing the shape of the growth curve, so that two algorithms can be compared fairly on paper, without running either one.
If this notation is new to you, the data structures introduction page walks through it in full, with an analogy and tiny code example for each growth rate — it's genuinely worth reading before continuing, since every algorithm covered from here on will be described in exactly this vocabulary ("this sort runs in O(n log n)," "this search is O(log n) but only works on sorted data"). The short version, as a refresher table:
| Notation | Name | Feels like |
|---|---|---|
| O(1) | Constant | Same speed no matter the input size |
| O(log n) | Logarithmic | Barely slows down as input grows — think binary search |
| O(n) | Linear | Slows down proportionally — one pass over the data |
| O(n log n) | Linearithmic | The typical cost of an efficient sort |
| O(n²) | Quadratic | Nested loops over the same data — gets painfully slow, fast |
Why this course is organized the way it is
Each page from here covers one strategy or family of algorithms — recursion, sorting, searching, and progressively more advanced patterns like divide and conquer, dynamic programming, and greedy algorithms — always grounded in complete, traceable Python code and a real problem the strategy actually solves well. By the end, the goal isn't memorizing a list of named algorithms; it's recognizing, when you face a new problem, which family of thinking it belongs to, the same way the data structures course aimed to make "which structure does this problem need?" an instinct rather than a guess.
Common mistakes
- Judging an algorithm's quality only by whether it produces the right answer. Correctness is the minimum bar — the entire discipline of algorithm design is about correctness and efficiency together.
- Assuming Big-O differences don't matter "in practice." They don't matter for small inputs — but real systems rarely stay small, and an O(n²) approach that felt instant during testing can become the exact reason a production system times out once real data arrives.
- Reaching for brute force out of habit rather than pausing to ask "have I seen a similar shape of problem before, and what technique solved it?" — building that pattern-recognition is the actual point of everything that follows.