Python Tutorial
Python DSA
Data Structures and Algorithms: the toolkit for organizing data and solving problems efficiently.
What Are Data Structures and Algorithms?
A data structure is a way to store and organize data so it can be used efficiently. An algorithm is a step-by-step procedure to solve a problem or transform data. Choosing the right pair is what separates a program that runs in milliseconds from one that takes hours.
This section builds from linear structures (arrays, stacks, queues, linked lists) to non-linear ones (trees, graphs, hash tables), then covers the classic searching and sorting algorithms that operate on them.
Big-O Notation
Big-O describes how an algorithm's running time or memory grows as the input size n grows — ignoring constants and focusing on the dominant term. It lets you compare algorithms independent of hardware.
| Big-O | Name | Example |
|---|---|---|
| O(1) | Constant | Index a list, hash lookup |
| O(log n) | Logarithmic | Binary search |
| O(n) | Linear | Scan a list |
| O(n log n) | Linearithmic | Merge sort, quicksort (avg) |
| O(n²) | Quadratic | Bubble/selection/insertion sort |
| O(2ⁿ) | Exponential | Naive recursive subsets |
Measuring Growth in Practice
import time
def linear_sum(n):
total = 0
for i in range(n): # O(n)
total += i
return total
for n in [10_000, 100_000, 1_000_000]:
start = time.perf_counter()
linear_sum(n)
print(n, round(time.perf_counter() - start, 4), "s")Notice the time grows roughly in proportion to n — the signature of an O(n) algorithm.
Time vs Space
Algorithms trade time against memory. A hash table answers "have I seen this before?" in O(1) time but uses O(n) extra memory. Sorting in place saves memory but may cost readability. Always ask what your problem's real constraint is — speed, memory, or simplicity.
In interviews and real systems, first state the brute-force solution and its Big-O, then improve it. A working O(n²) beats a broken O(n).
Python-Specific Notes
- Python
listis a dynamic array: indexing is O(1), appending is amortized O(1), inserting at the front is O(n). dictandsetare hash tables: average O(1) membership and lookup.collections.dequegives O(1) appends and pops at both ends — ideal for stacks and queues.- The built-in
sorted()andlist.sort()use Timsort, an optimized O(n log n) hybrid.
How to Study This Section
- Understand each structure's operations and their Big-O.
- Implement it from scratch once to internalize it.
- Then use Python's built-in equivalent in real code.
- Practice choosing the right structure for a given problem.
Try It Yourself
Exercise 1: What is the Big-O of finding an item in an unsorted list of n elements?
Show solution
O(n) — in the worst case you must check every element.
Exercise 2: Replace a slow repeated x in my_list check with a faster structure.
Show solution
seen = set(my_list) # build once, O(n)
print(x in seen) # each check O(1) instead of O(n)📘 Real-World Deep Dive
Algorithms & data structures are the difference between an O(n²) script and an O(n log n) one — and the only way to know which is which is to recognise the canonical patterns: BFS/DFS, quicksort/mergesort, hash tables.
Real-Life Scenario
A small flow-of-execution tracer: walk an arbitrary graph, detect cycles, topologically sort DAG-shaped dependency graphs, and emit them in a safe load order.
Real-Life Example
from collections import defaultdict, deque
from typing import Iterable
def topo_sort(deps: dict[str, list[str]]) -> list[str]:
"""Return a topological ordering of deps or raise on cycles."""
indeg: dict[str, int] = defaultdict(int)
nodes: set[str] = set(deps)
for parent, children in deps.items():
nodes.add(parent)
for c in children:
indeg[c] += 1
nodes.add(c)
queue = deque(sorted(n for n in nodes if indeg[n] == 0))
order: list[str] = []
while queue:
n = queue.popleft()
order.append(n)
for child in deps.get(n, []):
indeg[child] -= 1
if indeg[child] == 0:
queue.append(child)
if len(order) != len(nodes):
raise ValueError("cycle detected")
return order
deps = {
"compile": ["lex", "parse"],
"lex": ["scan"],
"parse": ["scan", "ast"],
"ast": [],
"scan": [],
}
print(topo_sort(deps))Expected Output
['ast', 'scan', 'lex', 'parse', 'compile']Common mistakes
- Recursive DFS on graphs with 100 k nodes blows the C stack — convert to iterative with an explicit stack.
- Python
sorted(iterable)is stable and O(n log n); for big-N work, considernumpy.argsortinstead. - Hash tables give amortised O(1) — but bad key distributions can degrade cache performance by orders of magnitude.
🚀 Performance & Best Practices
- For sliding-window workloads, use
collections.dequeinstead of left-shifting a list. - For heap-based priority queues,
heapqin the stdlib is C-fast. - Profile with
cProfilebefore any micro-optimisation — biggest wins usually come from data-structure swaps.
🧪 Try It Yourself
- Replace
defaultdictwithCounterand benchmark. - Add cycle detection with a visited-set during the DFS walk.
- Implement Kahn's algorithm as an iterative alternative.