🧱 Intro to Data Structures
Free self-paced course Β· Certificate on completion

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.

Before you start: this course assumes you can read basic Python: variables, lists, 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

  1. Start with the Big-O primera short introduction to Big-O, a way to describe how the work grows as the data grows.
  2. Work through the 5 lessons in orderread the idea, run the visualizer, watch the code panel, then take the lesson quiz.
  3. Practisecompare structures in "Choosing a structure" and the complexity cheat sheet.
  4. Take the final quizOne list of 10 questions covering every lesson. Score 8 of 10 or more to earn your certificate.
Tip for each lesson: before each animation step finishes, guess what will happen next. Use the Animation speed slider at the top to slow down tricky steps. There's no time limit, and you can retake any quiz.

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.

NotationNameIntuitionExampleSteps for n = 1,000,000
O(1)ConstantSame speed no matter how much dataTaking the top plate off a pile (a stack, Lesson 1)1
O(log n)LogarithmicEach step cuts the problem in halfFinding a word in a paper dictionary by opening it in the middle each timeβ‰ˆ 20
O(n)LinearTouches every element onceReading every name on a list once1,000,000
O(n log n)Linearithmicn times log n: typical of fast sorting methodsSorting a list with a fast sorting method (not covered in this course)β‰ˆ 20,000,000
O(nΒ²)QuadraticCompares every element with every otherComparing every pair of people in a room to find two with the same birthday1,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.

Worst case is the cost on the unluckiest possible input. Average case is the typical cost over many inputs. The lessons give both when they differ.
Amortized cost means "averaged over a long sequence of operations." An occasional operation may be slow, but if it's rare enough, the average stays low. You'll see this first in the Stack (push), then in the Queue and the Hash Table.

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.

Stack s Β· _items (end of list = top)
balanced(expr) β€” scanning characters
Press β€œCheck brackets” to run the demo.
st

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).

Bracket check: push every opening bracket. For each closing bracket, pop and check that the two match. If a pop finds the wrong bracket (or nothing), or the stack is not empty at the end, the brackets are unbalanced. Try ([)] and ((().
OperationCostWhy
pushO(1)amortized; list.append at the end
popO(1)list.pop() at the end
peekO(1)read _items[-1]
search for a valueO(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.

Logical queue (front β†’ back)
dequeue β—„
β—„ enqueue
Inside: two Python lists used as stacks
_out (pop here)
⟸
move when
_out is empty
_in (push here)

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.

Amortized O(1): each item is moved from _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.
In real code, use collections.deque. The two-stack queue is shown here because it teaches amortized analysis well.
OperationCostWhy
enqueueO(1)append to _in
dequeueO(1) amortized, O(n) worst casethe occasional transfer moves everything, but each item only once
peekO(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.

SinglyLinkedList ll Β· each box is [value | next]

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 indexO(1)O(n), you have to walk the chain
Insert/remove at the frontO(n), everything shiftsO(1)
Insert after a node you already haveO(n)O(1), just change pointers
MemoryCompactExtra 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 old next first, so nothing is lost.
  • remove(value): keep track of prev so you can bypass the removed node: prev.next = cur.next. If there is no prev, the node is the head, so head moves instead.
  • reverse(): the classic interview question. Walk the list once and flip each pointer backwards with cur.next, prev, cur = prev, cur, cur.next.
Why the one-line swap works: Python evaluates the whole right side (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.
No tail pointer: this class only stores head, so append must walk the whole chain to find the last node. Keeping a tail reference would make it O(1).
OperationCostWhy
prependO(1)new node points at the old head; head moves
appendO(n)no tail pointer, so walk to the end
insert, remove, findO(n)walking to the position or value; the relink itself is O(1)
reverseO(n) time, O(1) extra spaceone 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.

Tree words used in this lesson:
  • 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".

tree Β· x = in-order position, y = depth
output
run a traversal to fill this list

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:
    1. Leaf: just remove it.
    2. One child: replace the node with its only child (the whole subtree moves up).
    3. 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

TraversalOrderFor 50,30,70,20,40,60,80Use
In-orderleft, node, right20 30 40 50 60 70 80Visits keys in sorted order
Pre-ordernode, left, right50 30 20 40 70 60 80Copying or serializing a tree
Level-orderlevel by level50 30 70 20 40 60 80Uses a Queue (BFS on a tree)
Balance matters. The cost of every operation depends on the tree's height, not on the number of nodes. Inserting 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

OperationBalancedWorst case (degenerate)
insert, contains, deleteO(log n)O(n)
Traversals (in-order, pre-order, level-order)O(n)O(n)
heightO(n)O(n)

Where it shows up

  • Ordered maps and sets (e.g. C++ std::map, Java TreeMap, 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

  1. 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.
  2. Divide that number by the number of buckets and keep the remainder. This is called modulo, written % in Python: for example 13 % 8 = 5. With 8 buckets the remainder is always 0 to 7, so it always picks a valid bucket: hash(key) % len(buckets).
  3. 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.

About the hash numbers in this demo: Python's own 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.
buckets: each one is a list of (key, value) pairs
0.75

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.

Why resizing is still O(1) on average: a resize costs O(n), but it only happens after the table has doubled in size. Spread over all the puts that led up to it, each put pays a constant amount (amortized O(1)), just like appending to a Python 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

OperationAverageWorst case (all keys collide)
putO(1) amortizedO(n)
getO(1)O(n)
deleteO(1)O(n)
key in tableO(1)O(n)
_resize (one call)O(n)O(n)

Where it shows up

  • Python's dict and set, 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 pairsStack
Process items in arrival orderQueue
Insert or remove often in the middle, given a reference to a nodeLinked list
Keep data sorted while inserting and deletingBalanced BST
Look up values by key as fast as possibleHash 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.