DOCODIVE
Beginner Free Learning Path

Algorithms Beginner Course

Learn to think in algorithms. Master Big O notation, search, sorting, recursion, data structures, graph traversal, and the problem-solving patterns that power every efficient program.

4–6 weeks 20 lessons 1 capstone Basic Python required
Start Learning
01

What Is an Algorithm?

12 min
What you'll learn
  • Define algorithm
  • Understand algorithms in daily life
  • Learn why they matter

An algorithm is a step-by-step procedure to solve a problem — like a recipe, but for computation. Sorting your contacts, finding the shortest route on a map, compressing a photo: all algorithms. The skill you're learning isn't memorizing code — it's breaking problems into clear, repeatable steps. This is the foundation every programmer and data scientist builds on.

intro.py
# A simple algorithm: find the largest number
def find_max(arr):
    max_so_far = arr[0]
    for num in arr:
        if num > max_so_far:
            max_so_far = num
    return max_so_far

print(find_max([3, 7, 2, 9, 5]))
Output
python
9
💡 Tip: An algorithm is a recipe — specific, ordered, and repeatable. If you can write instructions, you can write algorithms.
Try it yourself

Write an algorithm to find the minimum number in a list.

Like find_max, but check for smaller.
def find_min(arr):
    min_so_far = arr[0]
    for num in arr:
        if num < min_so_far:
            min_so_far = num
    return min_so_far
02

Big O Notation

16 min
What you'll learn
  • Understand complexity
  • Learn O(1), O(n), O(n²)
  • Compare algorithm efficiency

Big O notation describes how an algorithm's time or space grows as input grows. O(1) is constant — instant regardless of size. O(n) is linear — doubles with input. O(n²) is quadratic — 100 items means 10,000 operations. This is THE language for comparing algorithms: an O(n) algorithm beats an O(n²) one as data grows, no matter the constant factors.

bigo.py
def constant_time(arr): return arr[0]        # O(1)
def linear_time(arr):                       # O(n)
    for x in arr: print(x)
def quadratic_time(arr):                    # O(n²)
    for i in arr:
        for j in arr: print(i, j)
Live Preview
Big O Notation
O(1)
O(n)
O(n²)
🔎 Important: Big O is about the RATE OF GROWTH — focus on the fastest-growing term, drop constants.
Try it yourself

What's the Big O of a single loop over n elements?

One pass.
O(n) — linear.
03

Time vs Space Complexity

14 min
What you'll learn
  • Distinguish time and space
  • Trade speed for memory
  • Analyze both

Time complexity measures how long an algorithm takes; space complexity measures how much memory it uses. They're often a trade-off: caching results uses more memory but saves time. A hash table gives O(1) lookup but uses O(n) space. Understanding both helps you pick the right tool for your constraints — mobile apps care about memory, servers about speed.

complexity.py
# Time-efficient but space-heavy: hash lookup
def has_pair(arr, target):
    seen = set()  # O(n) space
    for x in arr:
        if target - x in seen:
            return True
        seen.add(x)
    return False
# Time: O(n), Space: O(n)
Live Preview
Time vs Space Complexity
O(1)
O(n)
O(n²)
💡 Tip: Always consider both — a 'fast' algorithm that eats memory may be wrong for constrained devices.
Try it yourself

What's the space complexity of creating a set of n items?

Storage.
O(n) — the set stores n items.
04

Linear Search

14 min
What you'll learn
  • Search unsorted data
  • Understand sequential scanning
  • Know when to use it

Linear search checks each element one by one until it finds the target. It's O(n) — slow for large data, but simple and works on ANY list, sorted or not. It's the baseline every faster search improves on. When data is small or unsorted, linear search is perfectly fine.

linear.py
def linear_search(arr, target):
    for i, value in enumerate(arr):
        if value == target:
            return i
    return -1

print(linear_search([4, 2, 9, 7], 9))
Live Preview
Linear Search
1
3
5
7
9
💡 Tip: Linear search is O(n) but needs no sorting — for small lists, simplicity beats speed.
Try it yourself

Write linear search that returns -1 when target not found.

Return -1 after the loop.
def search(arr, t):
    for i, v in enumerate(arr):
        if v == t: return i
    return -1
05

Binary Search

18 min
What you'll learn
  • Search sorted data
  • Understand divide-and-conquer
  • Achieve O(log n)

Binary search repeatedly halves the search space — check the middle, eliminate half, repeat. It requires SORTED data but runs in O(log n): a billion items needs only ~30 steps. This is the most famous efficient algorithm, and the 'divide and conquer' thinking it teaches powers merge sort, quicksort, and tree algorithms.

binary.py
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target: return mid
        if arr[mid] < target: left = mid + 1
        else: right = mid - 1
    return -1

print(binary_search([1, 3, 5, 7, 9], 7))
Live Preview
Binary Search
1
3
5
7
9
✓ Best Practice: O(log n) is the magic of binary search — log₂(1 billion) ≈ 30. Only 30 steps to search a billion items.
Try it yourself

Why does binary search need sorted data?

Eliminate half.
It relies on knowing which half the target is in based on comparison.
06

Sorting Basics: Bubble Sort

18 min
What you'll learn
  • Understand sorting
  • Learn bubble sort
  • Analyze its O(n²)

Bubble sort repeatedly compares adjacent elements and swaps them if out of order — the largest 'bubbles up' to the end each pass. It's O(n²), slow for real use, but the clearest introduction to how sorting works. Learning bubble sort first makes merge sort and quicksort's improvements obvious. It's a teaching tool, not a production algorithm.

bubble.py
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

print(bubble_sort([5, 2, 8, 1, 3]))
Live Preview
Sorting Basics: Bubble Sort
5
2
8
1
2
8
⚠️ Common Mistake: Bubble sort is O(n²) — never use it in production, but master it as your first sorting mental model.
Try it yourself

How many swaps does bubble sort do on [5,1]?

One pass.
1 swap.
07

Selection Sort

16 min
What you'll learn
  • Learn selection sort
  • Understand in-place sorting
  • Compare with bubble sort

Selection sort finds the smallest element and swaps it to the front, then repeats for the rest. It makes fewer swaps than bubble sort (O(n) swaps vs O(n²)), but still O(n²) comparisons. It's simple, in-place, and teaches the 'select the best remaining' pattern. Like bubble sort, it's for learning, not production.

selection.py
def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr

print(selection_sort([5, 2, 8, 1, 3]))
Live Preview
Selection Sort
5
2
8
1
2
8
💡 Tip: Selection sort does O(n) swaps — valuable when swaps are expensive (e.g., large records).
Try it yourself

Selection sort's time complexity is?

Nested loops.
O(n²).
08

Insertion Sort

16 min
What you'll learn
  • Learn insertion sort
  • Sort nearly-sorted data fast
  • Understand adaptive algorithms

Insertion sort builds the sorted array one element at a time, inserting each into its correct position — like sorting playing cards in your hand. It's O(n²) worst-case but O(n) on nearly-sorted data, making it adaptive. It's actually used in practice for small arrays inside faster algorithms (like Timsort, Python's built-in sort).

insertion.py
def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

print(insertion_sort([5, 2, 8, 1, 3]))
Live Preview
Insertion Sort
5
2
8
1
2
8
✓ Best Practice: Insertion sort shines on small or nearly-sorted data — it's O(n) when data is already sorted.
Try it yourself

Best-case time complexity of insertion sort?

Already sorted.
O(n).
09

Recursion Fundamentals

18 min
What you'll learn
  • Understand recursion
  • Learn base case
  • Trace recursive calls

Recursion is a function calling itself with a smaller version of the problem until it hits a base case. Every recursive solution needs: (1) a base case that stops, (2) a recursive step that shrinks toward it. Recursion is the natural way to express divide-and-conquer, tree traversal, and dynamic programming. It's tricky at first, but it's the gateway to advanced algorithms.

recursion.py
def factorial(n):
    if n <= 1: return 1        # base case
    return n * factorial(n - 1)  # recursive step

print(factorial(5))
Live Preview
Recursion Fundamentals
fact(3)
3 × fact(2)
2 × fact(1)
🔎 Important: Missing base case = infinite recursion = stack overflow. ALWAYS define the stopping condition first.
Try it yourself

Write a recursive function to sum numbers 1 to n.

sum(n) = n + sum(n-1).
def sum_to(n):
    if n <= 0: return 0
    return n + sum_to(n - 1)
10

Stacks & Queues

18 min
What you'll learn
  • Understand stack LIFO
  • Understand queue FIFO
  • Know real applications

Stacks are Last-In-First-Out (LIFO) — like a stack of plates; queues are First-In-First-Out (FIFO) — like a line of people. Stacks power undo/redo, function calls (the call stack), and depth-first search. Queues power BFS, task scheduling, and print spoolers. These two data structures are the simplest but most fundamental — they appear everywhere.

stack_queue.py
# Stack (LIFO)
stack = []
stack.append(1); stack.append(2); stack.append(3)
print(stack.pop())  # 3

# Queue (FIFO)
from collections import deque
queue = deque()
queue.append(1); queue.append(2); queue.append(3)
print(queue.popleft())  # 1
Live Preview
Stacks & Queues
3
2
1 ← top
💡 Tip: Stack = last in, first out. Queue = first in, first out. The entire web runs on these two.
Try it yourself

Which data structure powers the browser Back button?

Last page first.
A stack (LIFO).
11

Linked Lists

20 min
What you'll learn
  • Understand nodes and pointers
  • Build a linked list
  • Compare with arrays

A linked list is a chain of nodes, each storing data and a pointer to the next. Unlike arrays (contiguous memory), linked lists can grow and shrink freely, and insert/delete in O(1) at known positions. The trade-off: O(n) access (no indexing). They're the foundation for more complex structures like stacks, queues, and graphs.

linkedlist.py
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
    def append(self, data):
        new = Node(data)
        if not self.head:
            self.head = new
            return
        cur = self.head
        while cur.next: cur = cur.next
        cur.next = new
    def print_list(self):
        cur = self.head
        while cur:
            print(cur.data, end=' ')
            cur = cur.next

ll = LinkedList()
for x in [1, 2, 3]: ll.append(x)
ll.print_list()
Live Preview
Linked Lists
1
2
3
null
🔎 Important: Linked lists trade random access for flexible size — O(1) insert at head, O(n) access.
Try it yourself

What's the access time complexity of a linked list element?

No indexing.
O(n) — must traverse from the head.
12

Hash Tables / Dictionaries

20 min
What you'll learn
  • Understand hash functions
  • Achieve O(1) lookup
  • Use Python dicts and sets

A hash table maps keys to values by hashing the key to an index — giving O(1) average lookup, insert, and delete. Python's dict and set are hash tables. This is the single most important data structure for performance: converting O(n²) problems to O(n) with a 'seen' set. Collisions (two keys, same index) are handled by chaining or probing.

hash.py
# O(1) lookups
phonebook = {'Ali': '0300-111', 'Sara': '0301-222'}
print(phonebook['Ali'])  # instant, no looping

# Set for O(1) membership
seen = set()
for x in [1, 2, 3, 1, 2]:
    seen.add(x)
print(seen)
Live Preview
Hash Tables / Dictionaries
'Ali'
0300
'Sara'
0301
✓ Best Practice: Hash tables turn O(n) lookups into O(1) — they're the #1 performance trick in coding interviews.
Try it yourself

What's average lookup time in a Python dict?

Hash.
O(1).
13

Breadth-First Search (BFS)

22 min
What you'll learn
  • Traverse trees/graphs level by level
  • Use a queue
  • Find shortest paths

BFS explores level by level using a queue — first the root, then all neighbors, then their neighbors. It finds the SHORTEST path in unweighted graphs and is the backbone of many algorithms. The pattern: enqueue start, dequeue and visit, enqueue unvisited neighbors, repeat. BFS is how GPS finds routes, how social networks find connections, and how web crawlers explore.

bfs.py
from collections import deque

def bfs(graph, start):
    visited = set([start])
    queue = deque([start])
    while queue:
        node = queue.popleft()
        print(node, end=' ')
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

graph = {1:[2,3], 2:[4], 3:[5], 4:[], 5:[]}
bfs(graph, 1)
Live Preview
Breadth-First Search (BFS)
1
2
3
4
5
🔎 Important: BFS uses a QUEUE and finds shortest paths. DFS uses a stack and goes deep first.
Try it yourself

Which data structure does BFS use?

Level by level.
A queue (FIFO).
14

Depth-First Search (DFS)

22 min
What you'll learn
  • Traverse deep before wide
  • Use recursion or stack
  • Solve path and cycle problems

DFS explores as deep as possible before backtracking — using recursion (or an explicit stack). It's the natural traversal for trees, and solves problems like cycle detection, topological sort, and connected components. The pattern: visit a node, recursively visit each unvisited neighbor. DFS is elegant in code because recursion naturally implements the stack.

dfs.py
def dfs(graph, node, visited=None):
    if visited is None: visited = set()
    visited.add(node)
    print(node, end=' ')
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

graph = {1:[2,3], 2:[4], 3:[5], 4:[], 5:[]}
dfs(graph, 1)
Live Preview
Depth-First Search (DFS)
1
2
4
3
💡 Tip: DFS naturally matches recursion — the call stack IS the traversal stack.
Try it yourself

BFS vs DFS — which uses recursion naturally?

Go deep.
DFS.
15

Two Pointers Technique

18 min
What you'll learn
  • Solve array problems efficiently
  • Use left/right pointers
  • Achieve O(n) where naive is O(n²)

The two-pointers technique uses two indices moving toward each other (or same direction) to solve problems in one pass. Classic example: find a pair that sums to a target in a sorted array — left pointer at start, right at end, move them based on the sum. It turns naive O(n²) into O(n). This is one of the most common patterns in coding interviews.

twopointers.py
def two_sum_sorted(arr, target):
    left, right = 0, len(arr) - 1
    while left < right:
        current = arr[left] + arr[right]
        if current == target: return (left, right)
        if current < target: left += 1
        else: right -= 1
    return None

print(two_sum_sorted([1, 2, 4, 6, 9], 10))
Live Preview
Two Pointers Technique
1
2
4
6
9
✓ Best Practice: Two pointers is the most common interview pattern — master it early, it shows up everywhere.
Try it yourself

What's the time complexity of the two-pointer pair-sum on sorted data?

One pass.
O(n).
16

Sliding Window Technique

20 min
What you'll learn
  • Process subarrays efficiently
  • Avoid recomputation
  • Solve max-sum problems

The sliding window maintains a 'window' over an array and slides it, updating results incrementally instead of recomputing from scratch. Classic example: max sum of k consecutive elements — instead of summing k elements every time (O(n·k)), add the next and subtract the previous (O(n)). It's the key pattern for subarray and substring problems.

sliding.py
def max_subarray_sum(arr, k):
    window = sum(arr[:k])
    max_sum = window
    for i in range(k, len(arr)):
        window += arr[i] - arr[i - k]
        max_sum = max(max_sum, window)
    return max_sum

print(max_subarray_sum([1, 4, 2, 10, 3, 1], 3))
Live Preview
Sliding Window Technique
1
4
2
10
3
💡 Tip: Sliding window turns O(n·k) into O(n) by updating incrementally — add new, remove old.
Try it yourself

Sliding window's time complexity is?

Single pass.
O(n).
17

Dynamic Programming Intro

24 min
What you'll learn
  • Understand overlapping subproblems
  • Learn memoization
  • Solve the classic Fibonacci problem

Dynamic programming (DP) solves problems by breaking them into overlapping subproblems and caching results — so you compute each once, not repeatedly. Naive recursive Fibonacci is O(2ⁿ); memoized DP is O(n). The pattern: recursive solution + a cache (memo) for already-computed values. DP is the hardest beginner topic, but it's THE core of algorithmic problem-solving.

dp.py
def fib_memo(n, memo={}):
    if n in memo: return memo[n]
    if n <= 1: return n
    memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]

print(fib_memo(50))
Live Preview
Dynamic Programming Intro
fib(n) = fib(n-1) + fib(n-2)
memo[n] cached ✓
🔎 Important: DP = recursion + cache. The 'memo' dictionary transforms exponential time into linear.
Try it yourself

Why is naive Fibonacci O(2ⁿ)?

Recompute.
It recomputes the same subproblems exponentially many times.
18

Greedy Algorithms

18 min
What you'll learn
  • Make locally optimal choices
  • Solve coin-change style problems
  • Understand when greedy works

Greedy algorithms make the best LOCAL choice at each step, hoping it leads to a global optimum. They're fast and simple — but only correct when the problem has 'greedy property' (local best = global best). Classic example: coin change with certain denominations, activity selection, Huffman coding. The skill is knowing when greedy applies and when you need DP instead.

greedy.py
def coin_change(coins, amount):
    coins.sort(reverse=True)
    count = 0
    for coin in coins:
        while amount >= coin:
            amount -= coin
            count += 1
    return count

print(coin_change([1, 5, 10, 25], 63))
Live Preview
Greedy Algorithms
25
25
10
1
💡 Tip: Greedy works when local best = global best. It fails on arbitrary coin systems — that's when you need DP.
Try it yourself

What's the risk of greedy algorithms?

Local vs global.
Local optimal choices may not lead to the global optimum.
19

Divide & Conquer

20 min
What you'll learn
  • Break problems in half
  • Understand the recursion pattern
  • Connect to merge sort and binary search

Divide and conquer splits a problem into smaller subproblems, solves them recursively, then combines the results. Binary search and merge sort are the canonical examples. The pattern: divide → conquer → combine. This is the thinking behind most efficient algorithms — breaking big problems into manageable halves.

dc.py
def find_max_dc(arr):
    if len(arr) == 1: return arr[0]  # base
    mid = len(arr) // 2
    left_max = find_max_dc(arr[:mid])    # divide
    right_max = find_max_dc(arr[mid:])   # conquer
    return max(left_max, right_max)       # combine

print(find_max_dc([3, 7, 2, 9, 5]))
Live Preview
Divide & Conquer
Full
Half
Half
✓ Best Practice: Divide and conquer is the parent of binary search, merge sort, quicksort — the efficient algorithm family.
Try it yourself

Which famous sort uses divide and conquer?

Merge.
Merge sort.
20

Capstone: Problem-Solving Toolkit

40 min
What you'll learn
  • Apply all learned patterns
  • Solve a real problem
  • Analyze complexity

Your capstone: solve a real problem using your full toolkit. Count word frequencies (hash table), find the most common word (greedy/iteration), sort results (sorting), and verify with binary search. You'll choose the right data structures and algorithms, analyze complexity, and document your choices. This is what real algorithm work looks like — pattern recognition + complexity analysis.

capstone.py
from collections import Counter

def top_words(text, k):
    counts = Counter(text.lower().split())  # hash table
    return counts.most_common(k)            # sort + select

result = top_words('the cat and the dog and the bird', 2)
print(result)
Live Preview
Capstone: Problem-Solving Toolkit
Count
Sort
Search
✓ Best Practice: Real algorithm work is CHOOSING the right tool — hash table, sort, search — and knowing the complexity.
Try it yourself

What's the time complexity of counting words with a Counter?

One pass.
O(n) — one pass with O(1) hash lookups.
You've completed all 20 lessons. Ready for more?

Continue to Algorithms Intermediate for advanced sorting, graphs, and dynamic programming.

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.