Introduction to Data Structures
Learn the five foundational data structures every programmer uses: stacks, queues, linked lists, binary search trees and hash tables. Each lesson pairs the theory (how it works and what it costs) with practice: the real Python code, an interactive visualizer you can drive yourself, and quizzes. Go at your own pace, then pass the final quiz to earn your certificate.
A data structure is a way of arranging data in a program so that some jobs, such as adding, finding or removing items, are fast. Each structure makes different jobs fast, and this course shows you which.
if, loops, functions and classes. If you are new to Python, take Introduction to Python first.Theory
The idea behind each structure, how its operations work step by step, Big-O costs, and when to choose it.
Practice
Real Python code with each line highlighted as it runs, interactive visualizers, and a quiz per lesson.
Certificate
Score 80% or more on the final quiz to get a certificate with your name, ready to print or save as PDF.
- 5 lessons
- 15 lesson quiz questions
- 10-question final quiz
- Self-paced Β· works offline
How the course works
- Start with the Big-O primera short introduction to Big-O, a way to describe how the work grows as the data grows.
- Work through the 5 lessons in orderread the idea, run the visualizer, watch the code panel, then take the lesson quiz.
- Practisecompare structures in "Choosing a structure" and the complexity cheat sheet.
- Take the final quizOne list of 10 questions covering every lesson. Score 8 of 10 or more to earn your certificate.
Colour key for the animations
When you press a button in any section, the boxes change colour to show what the code is doing at that moment:
- NormalAn item sitting in the structure, not involved in the current step.
- Being examinedThe item the code is looking at right now, e.g. comparing it during a search.
- Visited / markerAlready checked, or the item that a named marker (such as
head, the start of a linked list) is on. Lesson 3 explains these markers, called pointers. - Found / doneThe result: the item a search found, or a step that finished.
A quick primer on Big-O
Big-O describes how an operation's running time grows as the amount of data (n) grows. It ignores fixed multipliers and smaller extra terms, and only looks at the overall shape of the curve.
An operation is one action on the data, such as adding an item or searching for one. Its running time is counted in steps. For example, an operation that takes 5n + 20 steps is still O(n): when n is large, doubling n roughly doubles the work, and the 5 and the 20 don't change that shape.
| Notation | Name | Intuition | Example | Steps for n = 1,000,000 |
|---|---|---|---|---|
| O(1) | Constant | Same speed no matter how much data | Taking the top plate off a pile (a stack, Lesson 1) | 1 |
| O(log n) | Logarithmic | Each step cuts the problem in half | Finding a word in a paper dictionary by opening it in the middle each time | β 20 |
| O(n) | Linear | Touches every element once | Reading every name on a list once | 1,000,000 |
| O(n log n) | Linearithmic | n times log n: typical of fast sorting methods | Sorting a list with a fast sorting method (not covered in this course) | β 20,000,000 |
| O(nΒ²) | Quadratic | Compares every element with every other | Comparing every pair of people in a room to find two with the same birthday | 1,000,000,000,000 |
Where the numbers come from: log n (here log base 2) is how many times you can halve n before you reach 1. 1,000,000 can be halved about 20 times (220 = 1,048,576), so O(log n) is about 20 steps, and O(n log n) is 1,000,000 Γ 20 = 20,000,000. O(nΒ²) is 1,000,000 Γ 1,000,000.
The Log scale button squeezes big numbers so each grid line is 10 times the one below it, letting all five curves fit on one chart.
Suggested learning order
Start with the linear structures, because the tree builds on the same ideas. The hash table stands alone and can be read at any time.
1. Stack (LIFO)
A stack only lets you add or remove at the top: the last item in is the first item out.
Think of a pile of plates. You put a new plate on top, and you take a plate off the top. You never pull one from the middle. That one rule, Last In, First Out, makes a stack simple and fast.
How it works
The stack is a Python list where the end of the list is the top. append() and pop() at the end of a list are both O(1): nothing else has to move. Using the front of the list as the top would be a mistake, because pop(0) shifts every other element, which costs O(n).
([)] and ((().| Operation | Cost | Why |
|---|---|---|
push | O(1) | amortized; list.append at the end |
pop | O(1) | list.pop() at the end |
peek | O(1) | read _items[-1] |
| search for a value | O(n) | a stack isn't built for this |
Where it shows up
- The call stack: every function call pushes a frame, and every return pops one.
- Undo in editors.
- Matching brackets and parsing expressions.
- Depth-first search of trees and graphs.
2. Queue (FIFO)
A queue adds at the back and removes from the front: the first item in is the first item out.
Picture a line at a shop. People join at the back and leave from the front, in the order they arrived. This version hides a neat trick inside: it is built from two stacks, _in and _out.
_out is empty
How it works: the two-stack trick
A plain list would make dequeue O(n), because list.pop(0) shifts every element. Instead:
_in: new items are pushed here._out: items are popped from here.
When _out is empty, every item in _in is moved over to _out. Moving them reverses their order, so the oldest item ends up on top of _out.
_in to _out at most once. One dequeue can cost O(n), but averaged over many operations each one is O(1). Watch the βmoves per opβ counter in the demo stay at or below 1.collections.deque. The two-stack queue is shown here because it teaches amortized analysis well.| Operation | Cost | Why |
|---|---|---|
enqueue | O(1) | append to _in |
dequeue | O(1) amortized, O(n) worst case | the occasional transfer moves everything, but each item only once |
peek | O(1) | _out[-1] or _in[0] |
Where it shows up
- Print queues and task schedulers.
- Message brokers.
- Breadth-first search, e.g. the BST's level-order walk.
3. Singly Linked List (chain of nodes)
A linked list is a chain of nodes: each node holds a value and a pointer to the next node.
A pointer (also called a reference) tells you where another item is stored in the computer's memory, like a home address. Following a node's pointer takes you to the next node.
An array keeps its items side by side in one block of memory, numbered 0, 1, 2, β¦; a Python list works this way. A linked list is different: its items can live anywhere in memory. The list only remembers where the chain starts (head); the last node's next is None, Python's value for "nothing here". Think of a treasure hunt: each clue tells you where the next one is, so to reach clue 5 you must follow clues 1 to 4.
Reading the boxes: β means next points at the neighbouring box (follow the arrow), β
means next is None, and β20 (orange) means it points at the node holding 20, which is not drawn next to it. Pointer variables like cur and prev are shown as labels above the nodes.
Arrays vs linked lists
| Array (Python list) | Linked list | |
|---|---|---|
| Access by index | O(1) | O(n), you have to walk the chain |
| Insert/remove at the front | O(n), everything shifts | O(1) |
| Insert after a node you already have | O(n) | O(1), just change pointers |
| Memory | Compact | Extra pointer per node |
How it works: key techniques
insert(index, value): walk to the node before the position, then change pointers:cur.next = Node(value, cur.next). The new node grabs the oldnextfirst, so nothing is lost.remove(value): keep track ofprevso you can bypass the removed node:prev.next = cur.next. If there is noprev, the node is the head, soheadmoves instead.reverse(): the classic interview question. Walk the list once and flip each pointer backwards withcur.next, prev, cur = prev, cur, cur.next.
(prev, cur, cur.next) before assigning anything. So the old cur.next is saved even though cur.next is overwritten in the same line. Press reverse and watch the three labels move.head, so append must walk the whole chain to find the last node. Keeping a tail reference would make it O(1).| Operation | Cost | Why |
|---|---|---|
prepend | O(1) | new node points at the old head; head moves |
append | O(n) | no tail pointer, so walk to the end |
insert, remove, find | O(n) | walking to the position or value; the relink itself is O(1) |
reverse | O(n) time, O(1) extra space | one pass, three pointer variables |
Where it shows up
- The building block for stacks and queues when you want guaranteed O(1) at the front.
- Chains of entries in hash table buckets (separate chaining, see Hash Table).
- Immutable lists in functional languages (Lisp, Haskell): prepending shares the rest of the list.
- Interview questions: reverse a list, detect a cycle, find the middle node.
4. Binary Search Tree (BST)
A binary tree where everything in a node's left subtree is smaller and everything in its right subtree is larger, so, when the tree is well balanced, each comparison discards about half of what's left.
- A tree is a set of nodes joined like a family tree drawn upside down. The top node is the root.
- Each node can point to nodes below it, called its children. In a binary tree each node has at most two: a left child and a right child.
- A node with no children is a leaf.
- A node together with everything below it is a subtree. A node's left subtree is its left child and everything below that child.
- A node's depth is how many steps it is from the root (the root has depth 0).
- The tree's height is the number of nodes on its longest path from the root down to a leaf.
- A tree is balanced when it is short and bushy rather than long and thin, so no path is much longer than the others.
- In a BST, the items we store and compare are called keys.
Each node has at most two children, and the BST property holds at every node. That makes searching like looking up a word in a paper dictionary: open it in the middle, see whether the word comes before or after, and ignore the other half. At each node you compare once and continue in only one direction. Think of a "guess the number" game where each answer is "higher" or "lower".
How it works
contains(key): start at the root. Go left if the key is smaller, right if it's larger. Stop when you find it or fall off the tree (None).insert(key): search for the key, then attach a new node exactly where the search fell off. Duplicates are ignored.delete(key)has three cases:- Leaf: just remove it.
- One child: replace the node with its only child (the whole subtree moves up).
- Two children: find the in-order successor (the smallest key in the right subtree: go right once, then left as far as possible), copy its key into this node, then delete the successor from the right subtree. The successor never has a left child, so that second delete is always case 1 or 2.
Traversals
| Traversal | Order | For 50,30,70,20,40,60,80 | Use |
|---|---|---|---|
| In-order | left, node, right | 20 30 40 50 60 70 80 | Visits keys in sorted order |
| Pre-order | node, left, right | 50 30 20 40 70 60 80 | Copying or serializing a tree |
| Level-order | level by level | 50 30 70 20 40 60 80 | Uses a Queue (BFS on a tree) |
4,2,6,1,3,5,7 gives height 3; inserting the same keys sorted (1..7) gives height 7, which is just a linked list.
Try both buttons above. This BST does not rebalance itself; self-balancing trees such as AVL and red-black trees keep the height at O(log n) no matter the insert order.Complexity
| Operation | Balanced | Worst case (degenerate) |
|---|---|---|
insert, contains, delete | O(log n) | O(n) |
| Traversals (in-order, pre-order, level-order) | O(n) | O(n) |
height | O(n) | O(n) |
Where it shows up
- Ordered maps and sets (e.g. C++
std::map, JavaTreeMap, which are red-black trees). - Database indexes (B-trees are a wide, balanced generalization of the BST).
- Range queries and "next larger key" lookups, which a hash table can't do efficiently.
5. Hash Table (key β value, separate chaining)
A hash table stores pairs: a key (the name you look something up by, such as a word) and a value (what you want to find, such as how often the word appears). It keeps a row of numbered slots called buckets and turns each key into a bucket number, so a lookup takes O(1) on average.
Python's dict (key β value pairs, like {'apple': 3}) and set (a collection with no repeats) are both hash tables.
The idea
- A hash function is a fixed recipe that turns any key into a whole number:
hash("apple"). The same key always gives the same number. - Divide that number by the number of buckets and keep the remainder. This is called modulo, written
%in Python: for example13 % 8 = 5. With 8 buckets the remainder is always 0 to 7, so it always picks a valid bucket:hash(key) % len(buckets). - The
(key, value)pair is stored in that bucket. To find it again, recompute the same bucket and look only there.
Think of a coat check with numbered hooks: the attendant computes the hook number from your ticket, so they go straight to it instead of searching every coat.
hash() gives different numbers for words each time Python starts, so there's no single "real" value to show. This demo uses a simple fixed recipe called djb2 instead: start with h = 5381, then for each letter multiply by 33 and add the letter's code number (ord(ch) gives it): h = h * 33 + ord(ch). Whole numbers are used as their own hash, as in Python. Your bucket numbers in real Python will differ, but the steps are the same.Two small details: the demo keeps only the last 32 binary digits of
h so it doesn't grow forever, and, like Python, it treats hash(-1) as -2 because Python uses -1 internally to mean an error.Collisions and separate chaining
Two different keys can land in the same bucket. This is called a collision. In the demo above, 'date' and 'fig' both land in bucket 3, and the default key 'mango' lands in bucket 7 next to 'apple'. This implementation uses separate chaining: each bucket is a small Python list of pairs, and get / put / delete scan that list comparing k == key. Rows with a collision get an orange left border and a "collision ΓN" badge.
Load factor and resizing
The load factor is items / buckets. As it grows, chains get longer and lookups slow down. When it goes above 0.75, _resize() doubles the number of buckets and re-inserts every pair, because each key's bucket index (hash % capacity) changes when the capacity changes. Try it: starting from 5 items in 8 buckets, the 7th item pushes the load to 7/8 = 0.875 and triggers a resize to 16.
list.Rules for keys
Keys must be hashable, and equal keys must have equal hashes. That's why you can't use a list as a dict key: it can change after insertion, which would change its hash and strand it in the wrong bucket. Note that 1 and '1' are different keys here (an int and a string).
Complexity
| Operation | Average | Worst case (all keys collide) |
|---|---|---|
put | O(1) amortized | O(n) |
get | O(1) | O(n) |
delete | O(1) | O(n) |
key in table | O(1) | O(n) |
_resize (one call) | O(n) | O(n) |
Where it shows up
- Python's
dictandset, and object attribute lookup - Caches and memoization (an LRU cache is a hash table plus a doubly linked list)
- Counting and de-duplicating things (word frequencies, "have I seen this before?")
- Database hash indexes, compiler symbol tables, routing tables
Choosing the right structure
The real skill is matching a problem to a structure. For each scenario, pick the structure you'd reach for, then reveal the answer.
Quick reference
| You need to⦠| Use |
|---|---|
| Undo the most recent action, or match nested pairs | Stack |
| Process items in arrival order | Queue |
| Insert or remove often in the middle, given a reference to a node | Linked list |
| Keep data sorted while inserting and deleting | Balanced BST |
| Look up values by key as fast as possible | Hash table |
Complexity cheat sheet
Click a column header to sort, or filter to one operation to compare structures side by side.
* amortized β average case Β· dot color = growth class (same colors as the Big-O chart)
Final quiz & certificate
This is the last step of the course: one list of 10 questions covering every lesson. After each answer you'll see the correct option and why it's right. Score 80% or more (8 of 10) to earn a certificate with your name.