Python · Data Structures & Algorithms

Complete Guide to Data Structures & Algorithms (DSA)

This page is a full mini-course on DSA in Python. It covers core data structures, time complexity, sorting, searching, trees, graphs, and more – with diagrams and code examples.

Data Structures

Lists, stacks, queues, hash tables, trees, graphs.

Algorithms

Searching, sorting, BFS, DFS, recursion basics.

Complexity

Big-O time & space analysis for each operation.

1. What Are Data Structures & Algorithms?

1.1 Data Structures

A data structure is a way to organize and store data so it can be used efficiently.

Examples:

1.2 Algorithms

An algorithm is a step-by-step procedure to solve a problem.

Examples:

2. Time Complexity & Big-O Notation

Big-O describes how running time grows as input size (n) grows. It ignores constant factors and focuses on the growth trend.

O(1) – Constant

Example: accessing arr[i] or a key in a dictionary.

O(log n) – Logarithmic

Example: binary search in a sorted list.

O(n) – Linear

Example: scanning a list once in a loop.

O(n²) – Quadratic

Example: nested loops (basic bubble sort).

Show simple Big-O comparison example
# O(1): constant
def get_first(arr):
    return arr[0]

# O(n): linear
def print_all(arr):
    for x in arr:
        print(x)

# O(n^2): quadratic
def print_pairs(arr):
    for i in range(len(arr)):
        for j in range(len(arr)):
            print(arr[i], arr[j])

3. Core Python Data Structures

Python has several built-in data structures that are widely used: lists, tuples, sets, and dictionaries.

3.1 Lists (Dynamic Arrays)

Lists are ordered, mutable collections. Internally they behave like dynamic arrays. They automatically resize as you add or remove elements.

[ index ]: 0 1 2 3 [ value ]: [10] [20] [30] [40]
arr = [10, 20, 30]
arr.append(40)      # [10, 20, 30, 40]
arr.insert(1, 15)   # [10, 15, 20, 30, 40]
arr.remove(20)      # [10, 15, 30, 40]
x = arr[2]          # 30

Access: O(1), Append: O(1) amortized, Insert / Remove in middle: O(n). Are used for: dynamic collections, stacks, queues.

3.2 Tuples, Sets, Dictionaries

Tuples – like lists but immutable. They cannot be changed after creation.

point = (3, 5)
x, y = point

Sets – unordered collection of unique values. They automatically remove duplicates.

nums = {1, 2, 2, 3}
print(nums)  # {1, 2, 3}

Dictionaries – hash tables: key → value mapping. They provide fast access to values based on unique keys.

user = {"name": "Alex", "age": 25}
print(user["name"])       # "Alex"
user["country"] = "LT"

Dict / Set average lookup, insert, delete: O(1). Used for fast key-based access.

4. Stacks & Queues

4.1 Stack (LIFO)

Last In, First Out – like a stack of plates.

Top → [ 30 ] ← most recent [ 20 ] Bottom [ 10 ]
# Using list as a stack
stack = []
stack.append(10)
stack.append(20)
stack.append(30)

stack.pop()  # 30
stack.pop()  # 20

push / pop: O(1). Used for: undo history, function call stack.

4.2 Queue (FIFO)

First In, First Out – like a line in a store.

Front → [10] [20] [30] ← Back (enqueue at back, dequeue from front)
from collections import deque

q = deque()
q.append(10)   # enqueue
q.append(20)
q.append(30)

q.popleft()    # 10 (dequeue)
q.popleft()    # 20

enqueue / dequeue with deque: O(1). Used for: task scheduling, BFS traversal.(Breadth-First Search (BFS) traversal is a graph or tree traversal algorithm that explores nodes level by level, starting from a source node and visiting all its neighbors before moving to the next depth.)

5. Linked Lists

A linked list is a chain of nodes; each node stores data and a pointer to the next node. It allows efficient insertion and deletion from any position.

[10] → [20] → [30] → None
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def insert_front(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

Insert at head: O(1), search by value: O(n).

Why use linked lists instead of arrays?
  • Fast insertion/deletion when you already have the node reference.
  • No need to shift all elements when inserting in the middle.
  • Useful for queues, stacks, and custom memory-efficient structures.

6. Trees & Binary Search Trees (BST)

A tree is a hierarchical structure with nodes connected by edges. Each node can have multiple children, except in binary trees where each node has at most two children.

6.1 Binary Tree

A binary tree node has at most two children: left and right.

10 / \ 5 15 / \ 12 20
class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

6.2 Binary Search Tree (BST)

For each node: left < node < right. This makes searching efficient.

Average search / insert / delete: O(log n) Worst case (skewed): O(n).

Simple BST insert (recursive)
def insert(root, val):
    if root is None:
        return TreeNode(val)
    if val < root.val:
        root.left = insert(root.left, val)
    else:
        root.right = insert(root.right, val)
    return root

7. Graphs, BFS & DFS

A graph is a set of nodes (vertices) connected by edges. Graphs are used for: social networks, maps, recommendation systems, routing, and more.

7.1 Graph Representation (Adjacency List)

An adjacency list uses a dictionary where each key is a node and the value is a list of connected nodes.

1: [2, 3] 2: [1, 4] 3: [1] 4: [2]
graph = {
    1: [2, 3],
    2: [1, 4],
    3: [1],
    4: [2]
}

7.2 Breadth-First Search (BFS)

BFS explores a graph level by level (uses a queue).

from collections import deque

def bfs(graph, start):
    visited = set()
    q = deque([start])

    while q:
        node = q.popleft()
        if node not in visited:
            visited.add(node)
            for neighbor in graph[node]:
                q.append(neighbor)
    return visited

7.3 Depth-First Search (DFS)

DFS explores as far as possible along one branch (uses recursion or a stack).

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

8. Sorting Algorithms

Sorting is a classic algorithm topic. Here are the core ones:

8.1 Bubble Sort (O(n²))

Simple but slow for large inputs.

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

8.2 Merge Sort (O(n log n))

Efficient divide-and-conquer sort used in real systems.

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    result = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])
    return result

8.3 Quick Sort (Average O(n log n))

Fast in practice, uses partitioning.

Show simple quicksort implementation
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left  = [x for x in arr if x < pivot]
    mid   = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + mid + quick_sort(right)

9. Searching Algorithms

Common searching algorithms include linear and binary search:

9.1 Linear Search (O(n))

Scan each element until you find the target.

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

9.2 Binary Search (O(log n))

Binary search works only on sorted arrays.

Search 7 in [1, 3, 5, 7, 9] Step 1: mid = 5 → 7 > 5 → search right half Step 2: [7, 9] → mid = 7 → found
def binary_search(arr, target):
    low, high = 0, len(arr) - 1

    while low <= high:
        mid = (low + high) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

10. Space Complexity & Real-World Use

Space complexity measures how much extra memory an algorithm uses as input size grows. It is important for large datasets and memory-constrained environments.

10.1 Space Complexity Basics

Common space complexities:

def example(arr):
    x = 10          # O(1) extra space
    temp = arr[:]   # O(n) extra space (copy of array)
    return temp

10.2 Real-World Uses of DSA

11. Summary & Next Steps

In this DSA guide, you learned:

To deepen your understanding, practice implementing these structures and algorithms from scratch. Solve coding challenges on platforms like LeetCode, HackerRank, or CodeSignal. Study advanced topics like balanced trees, graph algorithms, and dynamic programming.