DSA with Python: Whether you’re just beginning your journey into coding or preparing for your next technical interview, understanding Data Structures and Algorithms (DSA) is essential. Pythonโs readability and simplicity make it an ideal language for mastering DSA.
In this guide, weโll walk through every core DSA concept in Pythonโstarting from the basics and building toward advanced topics, with clear examples, use cases, and explanations.
Table of Contents
- ๐ฐ Why Learn DSA in Python?
- ๐ง First, Why Learn DSA At All?
- โ 1. Simple and Clean Syntax
- โ 2. Powerful Built-In Data Structures
- โ 3. Rapid Prototyping & Testing
- โ 4. Huge Community and Learning Resources
- โ 5. In-Demand for Interviews and Jobs
- โ 6. Great for Beginners and Experts
- โ 7. Strong Library Support for Advanced Algorithms
- โ 8. You Can Build Real Projects with It
- ๐ ๏ธ Quick Summary Table
- ๐ Table of Contents
- ๐ง Introduction to DSA
- ๐๏ธ Data Structures
- ๐ 1. Arrays / Lists
- ๐ง Key Features
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- ๐ 2. Stack (LIFO)
- ๐ง Key Features
- โฑ๏ธ Time Complexity
- ๐ Better Stack with collections.deque
- ๐ Use Cases
- โ ๏ธ Python Tips
- ๐ 3. Queue (FIFO)
- ๐ง Key Features
- โฑ๏ธ Time Complexity
- ๐ Alternative: Queue with queue.Queue
- ๐ Use Cases
- โ ๏ธ Python Tips
- ๐งฑ Custom Queue Class (Bonus)
- ๐ 4. Linked List
- ๐งฑ Node Structure in Python
- ๐งฑ Singly Linked List (Basic Implementation)
- ๐ง Key Features
- ๐ฆ Types of Linked Lists
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- ๐ Doubly Linked List (Quick Peek)
- โ ๏ธ Linked List vs Array
- ๐ณ 5. Trees (Binary Tree Example)
- ๐ฟ Binary Tree
- ๐งฑ Binary Tree Node in Python
- ๐ง Key Features
- ๐ Tree Traversal Techniques
- ๐ณ Types of Binary Trees
- โฑ๏ธ Time Complexity (for balanced binary trees)
- ๐ Use Cases
- โ ๏ธ Tree vs Other Structures
- ๐ 6. Graphs
- ๐ Types of Graphs
- ๐งฑ Graph Representations in Python
- ๐ Basic Graph Operations
- ๐ Graph Traversal Algorithms
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- โ ๏ธ Graph Tips
- ๐ข 7. Heaps
- ๐งฑ Heap Structure
- ๐งช Array Representation of a Heap
- ๐ง Key Operations
- ๐งช Example: Min Heap Using heapq in Python
- ๐ง How Insert Works (Heapify Up)
- ๐ง How Extract Works (Heapify Down)
- ๐ Use Cases
- โ ๏ธ Important Notes
- ๐งฎ 8. Hash Tables
- ๐ How Does a Hash Table Work?
- ๐ง Key Concepts
- ๐งช Python Example Using Dictionary (Built-in Hash Table)
- ๐งฑ Simple Hash Table with Chaining (Custom Implementation)
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- โ ๏ธ Important Notes
- โ๏ธ Algorithms
- ๐ 1. Searching
- ๐ Linear Search
- ๐งฑ How Linear Search Works
- ๐งช Python Implementation
- ๐ Example
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- โ ๏ธ Important Notes
- ๐ Binary Search
- ๐งฑ How Binary Search Works
- ๐งช Python Implementation (Iterative)
- ๐งช Python Implementation (Recursive)
- ๐ Example
- โฑ๏ธ Time Complexity
- ๐ Requirements & Use Cases
- โ ๏ธ Important Notes
- ๐งผ 2. Sorting Algorithms
- ๐ Bubble Sort โ O(nยฒ)
- ๐งฑ How Bubble Sort Works
- ๐งช Python Implementation
- ๐ Example
- โฑ๏ธ Time Complexity
- ๐ Use Cases
- โ ๏ธ Important Notes
- ๐งฌ Merge Sort โ O(n log n)
- ๐ฆ How Merge Sort Works
- ๐ Visualization
- ๐งช Python Implementation
- ๐ Example
- โฑ๏ธ Time & Space Complexity
- ๐ฏ Use Cases
- โ ๏ธ Important Notes
- โก Quick Sort โ O(n log n) Average
- ๐ง How It Works
- ๐ Visualization
- ๐งช Python Implementation (Lomuto Partition)
- ๐ Example
- โฑ๏ธ Time & Space Complexity
- ๐ In-Place Sorting
- ๐ฏ Use Cases
- โ ๏ธ Important Notes
- โป๏ธ 3. Recursion
- ๐ง Key Components of Recursion
- ๐ Recursion Flow
- ๐งช Example 1: Factorial (n!)
- ๐งช Example 2: Fibonacci Sequence
- โฑ๏ธ Time Complexity
- ๐ When to Use Recursion
- โ ๏ธ Recursion vs Iteration
- ๐ ๏ธ Recursion Optimization Techniques
- ๐ณ Recursion in Data Structures
- ๐ Base Case is Critical!
- ๐ง Summary
- โ๏ธ 4. Divide and Conquer โ A Powerful Problem-Solving Paradigm
- ๐ง Key Steps of Divide and Conquer
- ๐ General Template (Python)
- ๐งช Example 1: Merge Sort (Classic Example)
- ๐งช Example 2: Binary Search
- ๐งช Example 3: Maximum Subarray Sum (Kadaneโs Alternative)
- ๐งช Example 4: Matrix Multiplication (Strassenโs Algorithm โ Advanced)
- โฑ๏ธ Time Complexity Pattern
- ๐ Divide and Conquer vs Other Paradigms
- ๐ Summary
- ๐ง Pro Tips
- ๐ 5. Dynamic Programming
- ๐ก Key Idea
- ๐ Steps to Solve a DP Problem
- ๐งช Example 1: Fibonacci Sequence
- ๐งช Example 2: 0/1 Knapsack Problem
- ๐ข Common Problems Solved Using DP
- โฑ๏ธ Complexity Comparison
- ๐ง When to Use DP
- โ ๏ธ Common Mistakes
- ๐ Summary
- ๐ Pro Tip for Interviews & Practice
- ๐ก 6. Greedy Algorithms
- ๐ How Greedy Works
- ๐ก When Does It Work?
- ๐งช Example 1: Coin Change (Greedy Version)
- ๐งช Example 2: Activity Selection Problem
- ๐งช Example 3: Fractional Knapsack
- โฑ๏ธ Time Complexity
- ๐ฏ Common Greedy Problems
- โ ๏ธ Pitfalls of Greedy
- ๐ Summary
- ๐ Pro Tip
- ๐ 7. Backtracking
- ๐ง Key Concept
- ๐ก Use Cases
- ๐ General Backtracking Template (Python)
- ๐งช Example 1: Generate All Subsets (Power Set)
- ๐งช Example 2: N-Queens Problem
- ๐งช Example 3: Sudoku Solver (9×9 grid)
- โฑ๏ธ Time Complexity (Varies)
- โ๏ธ Backtracking vs Recursion vs DFS
- ๐ Summary
- ๐ง Pro Tips
- ๐ ๏ธ Python Built-in Modules for DSA
- ๐งช Common DSA Problems
- ๐ Tips for Mastery
- ๐ Resources
- ๐ Final Thoughts
- ๐ง FAQ: Data Structures & Algorithms (DSA) in Python
- โ What is DSA and why is it important?
- โ Why learn DSA using Python?
- โ Is Python fast enough for competitive programming?
- โ What are the most important data structures in Python?
- โ What are the key algorithms every Python dev should know?
- โ Is recursion better than iteration?
- โ How to practice DSA in Python?
- โ Should I memorize code or understand logic?
- โ Can I use built-in functions in interviews?
- โ Whatโs the difference between list, tuple, set, dict in Python?
- โ How do I handle large input sizes in Python?
- โ How is memory managed in Python for DSA?
- โ Is learning DSA enough to become a good developer?
- โ What’s a good roadmap for DSA in Python?
- โ Best resources for learning DSA in Python?
- โ Final Advice
๐ฐ Why Learn DSA in Python?
Let’s first understand why you should learn DSA in Python. Why DSA learning is important at all.
๐ง First, Why Learn DSA At All?
Data Structures and Algorithms (DSA) help you:
- ๐งฉ Solve problems efficiently
- ๐ง Think logically and analytically
- ๐ผ Crack coding interviews
๐ Now, Why Learn DSA Specifically in Python?
- Simple syntax lets you focus on logic, not boilerplate.
- Built-in structures like list, set, dict, and tuple provide high-level abstractions.
- Python’s extensive libraries (e.g., heapq, collections, bisect) offer efficient implementations of complex data structures.
Let’s dive in detail.
โ 1. Simple and Clean Syntax
Python code is often like pseudocode โ clean, concise, and readable.
๐ Example:
Compare adding elements to a list in Python vs C++
# Python
arr = []
arr.append(10)
// C++
vector<int> arr;
arr.push_back(10);
This simplicity lets you focus on DSA concepts, not on syntax headaches.
โ 2. Powerful Built-In Data Structures
Python provides high-level built-in structures:
| Structure | Python Type | Use Case |
| Array | list | Store sequential data |
| Stack | list, deque | LIFO operations |
| Queue | deque, queue.Queue | FIFO structures |
| Hash Map | dict | Key-value storage |
| Set | set | Unique elements |
| Heap | heapq | Priority queue (min-heap) |
These built-ins save you from writing low-level implementations from scratch.
โ 3. Rapid Prototyping & Testing
Python is an interpreted language, so you can:
- Test code quickly
- Use REPL (like Jupyter Notebook or IDLE)
- Write less code and get more done
Ideal for practicing DSA problems rapidly.
โ 4. Huge Community and Learning Resources
Python has:
- Tons of DSA tutorials and practice problems
- A vibrant community on platforms like:
- LeetCode
- GeeksforGeeks
- HackerRank
- YouTube (e.g., NeetCode, Tech With Tim)
- LeetCode
You’re never stuck โ answers and explanations are everywhere.
โ 5. In-Demand for Interviews and Jobs
Most FAANG companies and tech startups now accept Python solutions in interviews. Interviewers care more about your logic and optimization than which language you use.
โ 6. Great for Beginners and Experts
| Skill Level | Why Python Works |
| Beginners | Easy syntax, intuitive loops and conditions |
| Intermediate | Focus on problem-solving, not boilerplate |
| Advanced | Use custom classes, advanced libraries |
โ 7. Strong Library Support for Advanced Algorithms
Python includes libraries like:
| Library | Purpose |
| collections | Stacks, queues, counters |
| heapq | Priority queues, heaps |
| bisect | Binary search utilities |
| itertools | Combinatorics, permutations |
| math | Efficient math functions |
| networkx | Graph algorithms (advanced) |
So for large-scale problems, Python scales surprisingly well.
โ 8. You Can Build Real Projects with It
Once you learn DSA, Python helps you apply it directly in:
- Web apps (Flask, Django)
- Data science (Pandas, NumPy)
- Machine learning (scikit-learn, TensorFlow)
- Game development, automation, scripting, and more
Itโs not just academic โ Python lets you apply what you learn immediately.
๐ ๏ธ Quick Summary Table
| Reason | Benefit |
| Easy syntax | Focus on learning DSA logic |
| Built-in structures | Fast prototyping |
| Strong community | Tons of free support and resources |
| Widely accepted in interviews | Preferred at companies like Google, Meta, etc. |
| Real-world usage | Build apps, APIs, ML models using the same language |
๐ Table of Contents
- Introduction to DSA
- Data Structures
- Linear Structures
- Arrays / Lists
- Stacks
- Queues
- Linked Lists
- Arrays / Lists
- Non-Linear Structures
- Trees
- Graphs
- Heaps
- Hash Tables
- Trees
- Linear Structures
- Algorithms
- Sorting
- Searching
- Recursion
- Divide and Conquer
- Dynamic Programming
- Greedy Algorithms
- Backtracking
- Sorting
- Python Built-in Modules for DSA
- Common DSA Problems (with Solutions)
- Tips for Mastery
- Resources to Learn More
๐ง Introduction to DSA
What is DSA?
- Data Structures are ways to store and organize data.
- Algorithms are step-by-step procedures to perform operations on data.
Goal: Choose the right structure and algorithm for efficient computing.
๐๏ธ Data Structures
๐ 1. Arrays / Lists
โ What is an Array/List?
An array (or list in Python) is a linear data structure that stores a collection of elements in a sequential order. Each element is assigned a unique index, starting from 0, and can be accessed directly using that index.
In Python, the built-in list type functions like a dynamic array โ it can grow or shrink in size and hold elements of different data types.
Pythonโs list is a dynamic array.
arr = [1, 2, 3, 4]
arr.append(5)
print(arr[2]) # Output: 3
- Pros: Easy access via index.
- Cons: Insertion/deletion can be slow (O(n)).
Example
# Creating a list
numbers = [10, 20, 30, 40]
# Accessing elements
print(numbers[0]) # Output: 10
print(numbers[-1]) # Output: 40 (last element)
# Modifying elements
numbers[1] = 25 # [10, 25, 30, 40]
# Adding elements
numbers.append(50) # [10, 25, 30, 40, 50]
# Removing elements
numbers.pop() # [10, 25, 30, 40]
numbers.remove(25) # [10, 30, 40]
๐ง Key Features
| Feature | Description |
| Ordered | Elements maintain insertion order |
| Mutable | You can change, add, or remove elements |
| Indexed | Access elements using zero-based indexing |
| Heterogeneous | Can store different data types together |
โฑ๏ธ Time Complexity
| Operation | Time Complexity |
| Access by index | O(1) |
| Append | O(1) (amortized) |
| Insert/remove at end | O(1) |
| Insert/remove at middle | O(n) |
| Search (linear) | O(n) |
Note: Pythonโs list is backed by a dynamic array under the hood, so resizing is handled automatically (but not cost-free).
๐ Use Cases
- Storing a sequence of items (e.g., shopping list, scores, tasks)
- Iterating through elements
- Stacking or queuing (with limitations)
- Implementing other data structures (stack, queue, matrix)
๐ 2. Stack (LIFO)
โ What is a Stack?
A stack is a linear data structure that follows the LIFO (Last In, First Out) principle. This means that the last element added to the stack is the first one removed.
Think of a stack like a pile of platesโyou can only take the top plate off or add a new one on top.
stack = []
stack.append(10)
stack.append(20)
stack.pop() # 20
Use collections.deque for faster performance.
๐งช Example (Using Python List)
stack = []
# Push elements onto the stack
stack.append(1)
stack.append(2)
stack.append(3)
print(stack) # [1, 2, 3]
# Pop element from the stack
top = stack.pop()
print(top) # 3
print(stack) # [1, 2]
๐ง Key Features
| Feature | Description |
| LIFO Order | Last pushed element is the first to pop |
| Push | Add an item to the top of the stack |
| Pop | Remove the item from the top of the stack |
| Peek | View the top item without removing it |
| Size | Check how many items are in the stack |
โฑ๏ธ Time Complexity
| Operation | Time Complexity |
| Push | O(1) |
| Pop | O(1) |
| Peek | O(1) |
| isEmpty | O(1) |
Note: Python’s list is commonly used for stack operations, but collections.deque is more efficient for large-scale use.
๐ Better Stack with collections.deque
from collections import deque
stack = deque()
stack.append(10)
stack.append(20)
print(stack.pop()) # Output: 20
Why use deque?
- Constant time appends and pops from both ends.
- Thread-safe and optimized for performance.
๐ Use Cases
- Undo/Redo functionality in editors
- Backtracking algorithms (e.g., maze, puzzles)
- Function call stack in programming languages
- Expression evaluation (e.g., postfix notation)
- Balanced parentheses checking
โ ๏ธ Python Tips
To peek at the top element without popping:
top = stack[-1] if stack else None
To check if the stack is empty:
if not stack:
- ย ย ย ย print(“Stack is empty”)
๐ 3. Queue (FIFO)
โ What is a Queue?
A queue is a linear data structure that follows the FIFO (First In, First Out) principle. This means the first element added to the queue is the first one to be removedโjust like people waiting in line at a ticket counter.
from collections import deque
queue = deque()
queue.append(1)
queue.append(2)
queue.popleft() # 1
๐งช Example (Using collections.deque)
from collections import deque
queue = deque()
# Enqueue (add to rear)
queue.append(‘A’)
queue.append(‘B’)
queue.append(‘C’)
print(queue) # deque([‘A’, ‘B’, ‘C’])
# Dequeue (remove from front)
front = queue.popleft()
print(front) # ‘A’
print(queue) # deque([‘B’, ‘C’])
๐ง Key Features
| Feature | Description |
| FIFO Order | First added element is removed first |
| Enqueue | Add item to the rear of the queue |
| Dequeue | Remove item from the front of the queue |
| Peek | View front element without removing |
| Size | Check number of elements in the queue |
โฑ๏ธ Time Complexity
| Operation | Time Complexity |
| Enqueue | O(1) |
| Dequeue | O(1) |
| Peek | O(1) |
| isEmpty | O(1) |
Using collections.deque is preferred over list because list.pop(0) has O(n) time complexity due to shifting elements.
๐ Alternative: Queue with queue.Queue
For multi-threaded environments, Python provides a thread-safe Queue:
from queue import Queue
q = Queue()
q.put(1) # Enqueue
q.put(2)
print(q.get()) # Dequeue โ 1
This is useful for producer-consumer problems and multithreaded programs.
๐ Use Cases
- Task scheduling (OS processes, print queues)
- BFS (Breadth-First Search) in graph/tree traversal
- Message queues in distributed systems
- Buffer management (networking, streaming)
- Customer service systems (help desk ticketing)
โ ๏ธ Python Tips
Peek front item without dequeuing:
front = queue[0] if queue else None
Check if queue is empty:
if not queue:
print(“Queue is empty”)
๐งฑ Custom Queue Class (Bonus)
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
self.items.append(item)
def dequeue(self):
if self.is_empty():
raise IndexError(“Queue is empty”)
return self.items.popleft()
def peek(self):
return self.items[0] if not self.is_empty() else None
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
๐ 4. Linked List
โ What is a Linked List?
A linked list is a linear data structure where each element (called a node) contains two parts:
- Data: The value to store.
- Pointer: A reference to the next node in the sequence.
Unlike arrays, linked lists do not store elements in contiguous memory and allow efficient insertion/deletion without shifting elements.
Custom implementation:
class Node:
def __init__(self, data):
self.data = data
self.next = None
More control over memory and structure.
๐งฑ Node Structure in Python
class Node:
def __init__(self, data):
self.data = data
self.next = None
๐งฑ Singly Linked List (Basic Implementation)
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def display(self):
current = self.head
while current:
print(current.data, end=” โ “)
current = current.next
print(“None”)
Usage:
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
ll.display() # Output: 10 โ 20 โ 30 โ None
๐ง Key Features
| Feature | Description |
| Dynamic Size | Grows as needed, no predefined size |
| Efficient Insertions | No shifting like arrays |
| Pointer-based | Each node links to the next |
| Non-contiguous | Nodes can be scattered in memory |
๐ฆ Types of Linked Lists
| Type | Description |
| Singly Linked List | Each node points to the next node only |
| Doubly Linked List | Nodes have prev and next pointers |
| Circular Linked List | Last node points back to the first node |
โฑ๏ธ Time Complexity
| Operation | Time Complexity |
| Access (by index) | O(n) |
| Insert at head | O(1) |
| Insert at tail | O(n) (or O(1) with tail pointer) |
| Deletion at head | O(1) |
| Deletion at tail | O(n) |
| Search | O(n) |
๐ Use Cases
- Dynamic memory allocation
- Implementing stacks/queues
- Undo functionality (editor or browser)
- Hash map chaining (collision handling)
- Polynomial arithmetic
๐ Doubly Linked List (Quick Peek)
class DNode:
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
Doubly linked lists allow traversal in both directions, and deletion is more efficient since you can access the previous node directly.
โ ๏ธ Linked List vs Array
| Feature | Linked List | Array (Python list) |
| Memory Layout | Non-contiguous | Contiguous |
| Random Access | โ No (O(n)) | โ Yes (O(1)) |
| Insert/Delete (start/mid) | โ Efficient (O(1)/O(n)) | โ Costly (O(n)) |
| Memory Overhead | Higher (due to pointers) | Lower |
๐ณ 5. Trees (Binary Tree Example)
โ What is a Tree?
A tree is a non-linear hierarchical data structure made up of nodes connected by edges. It starts with a root node and branches out to child nodes, forming a tree-like structure.
Each node can have zero or more child nodes, and there is only one path between any two nodes (no cycles).
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
Traversals:
- Inorder: left โ root โ right
- Preorder: root โ left โ right
- Postorder: left โ right โ root
๐ฟ Binary Tree
A binary tree is a tree where each node has at most two children, commonly referred to as:
- Left child
- Right child
๐งฑ Binary Tree Node in Python
class TreeNode:
def __init__(self, value):
self.val = value
self.left = None
self.right = None
Example:
# Creating nodes
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
This forms:
1
/ \
2 3
๐ง Key Features
| Feature | Description |
| Root | Topmost node of the tree |
| Leaf | Node with no children |
| Parent/Child | Relationships between connected nodes |
| Depth | Distance from root to a node |
| Height | Longest path from root to any leaf |
๐ Tree Traversal Techniques
Traversing means visiting all nodes in a particular order.
โ 1. Inorder Traversal (Left โ Root โ Right)
def inorder(node):
if node:
inorder(node.left)
print(node.val, end=’ ‘)
inorder(node.right)
โ 2. Preorder Traversal (Root โ Left โ Right)
def preorder(node):
if node:
print(node.val, end=’ ‘)
preorder(node.left)
preorder(node.right)
โ 3. Postorder Traversal (Left โ Right โ Root)
def postorder(node):
if node:
postorder(node.left)
postorder(node.right)
print(node.val, end=’ ‘)
โ 4. Level-order Traversal (Breadth-First Search)
from collections import deque
def level_order(root):
if not root:
return
queue = deque([root])
while queue:
node = queue.popleft()
print(node.val, end=’ ‘)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
๐ณ Types of Binary Trees
| Type | Description |
| Full Binary Tree | Every node has 0 or 2 children |
| Perfect Binary Tree | All levels are fully filled |
| Complete Binary Tree | All levels filled except possibly the last, left to right |
| Balanced Binary Tree | Height difference between left and right subtrees is minimal |
| Binary Search Tree (BST) | Left < Root < Right |
โฑ๏ธ Time Complexity (for balanced binary trees)
| Operation | Time Complexity |
| Insertion | O(log n) |
| Deletion | O(log n) |
| Search | O(log n) |
| Traversal | O(n) |
Note: In unbalanced trees, insertion/search can degrade to O(n).
๐ Use Cases
- Hierarchical data representation (e.g., file systems)
- Binary Search Trees (BSTs) for quick lookup
- Heaps for priority queues
- Expression trees (used in compilers)
- Trie trees (for dictionaries, autocomplete)
- Decision trees (AI/ML)
โ ๏ธ Tree vs Other Structures
| Feature | Tree | List/Array |
| Structure | Hierarchical | Linear |
| Search Speed | O(log n) (BST) | O(n) (linear search) |
| Insertion | Faster with balanced tree | Slower if shifting needed |
๐ 6. Graphs
โ What is a Graph?
A graph is a non-linear data structure made up of:
- Vertices (nodes) โ representing entities.
- Edges (connections) โ representing relationships between vertices.
Graphs are used to model pairwise relationships between objects, such as social networks, maps, and network topology.
Represent with adjacency list (using dict):
graph = {
‘A’: [‘B’, ‘C’],
‘B’: [‘A’, ‘D’],
‘C’: [‘A’],
‘D’: [‘B’]
}
DFS, BFS can be implemented using stack/queue.
๐ Types of Graphs
| Type | Description |
| Directed Graph (Digraph) | Edges have a direction (from one vertex to another) |
| Undirected Graph | Edges are bidirectional |
| Weighted Graph | Edges have weights (e.g., distances, costs) |
| Unweighted Graph | Edges have no weights |
| Cyclic Graph | Contains at least one cycle |
| Acyclic Graph | Contains no cycles |
๐งฑ Graph Representations in Python
1. Adjacency List (Recommended)
Stores each vertex and a list of its neighbors.
graph = {
‘A’: [‘B’, ‘C’],
‘B’: [‘A’, ‘D’, ‘E’],
‘C’: [‘A’, ‘F’],
‘D’: [‘B’],
‘E’: [‘B’, ‘F’],
‘F’: [‘C’, ‘E’]
}
Advantages:
- Efficient for sparse graphs
- Easy to iterate neighbors
2. Adjacency Matrix
A 2D matrix where matrix[i][j] = 1 if there is an edge from vertex i to vertex j, else 0.
# For vertices A=0, B=1, C=2
matrix = [
[0, 1, 1], # A
[1, 0, 0], # B
[1, 0, 0] # C
]
Advantages:
- Fast edge lookup O(1)
- Uses more memory (O(Vยฒ))
๐ Basic Graph Operations
| Operation | Description |
| Add vertex | Add a new node |
| Add edge | Connect two vertices |
| Remove vertex | Delete a node and connected edges |
| Remove edge | Delete a connection between two nodes |
| Traverse | Visit all vertices systematically |
๐ Graph Traversal Algorithms
1. Depth-First Search (DFS)
Explores as far as possible along each branch before backtracking.
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start, end=’ ‘)
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
2. Breadth-First Search (BFS)
Explores neighbors level by level.
from collections import deque
def bfs(graph, start):
visited = set([start])
queue = deque([start])
while queue:
vertex = queue.popleft()
print(vertex, end=’ ‘)
for neighbor in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
โฑ๏ธ Time Complexity
| Operation/Algorithm | Time Complexity |
| Add Vertex | O(1) |
| Add Edge | O(1) |
| DFS / BFS | O(V + E) (Vertices + Edges) |
| Check Edge | O(1) (Adjacency Matrix), O(V) (Adjacency List) |
๐ Use Cases
- Social Networks (friends, followers)
- Route/Path Finding (maps, GPS)
- Network Broadcasting (routers, communication)
- Web Crawlers (links between websites)
- Dependency Resolution (build systems, task scheduling)
โ ๏ธ Graph Tips
- Use sets or dicts for neighbors to avoid duplicates.
- Watch out for cycles during traversal (use visited set).
- For weighted graphs, algorithms like Dijkstraโs and Primโs are used.
๐ข 7. Heaps
โ What is a Heap?
A heap is a specialized tree-based data structure that satisfies the heap property:
- In a max heap, every parent node is greater than or equal to its children.
- In a min heap, every parent node is less than or equal to its children.
Heaps are commonly used to implement priority queues where the highest (or lowest) priority element is always at the root.
Use Pythonโs heapq:
import heapq
heap = []
heapq.heappush(heap, 10)
heapq.heappush(heap, 5)
print(heapq.heappop(heap)) # 5
๐งฑ Heap Structure
- Heaps are usually implemented as complete binary trees โ all levels are fully filled except possibly the last, which is filled from left to right.
- They can be efficiently represented using arrays (no explicit pointers needed).
๐งช Array Representation of a Heap
For a node at index i:
- Left child is at index 2*i + 1
- Right child is at index 2*i + 2
- Parent is at index (i – 1) // 2
๐ง Key Operations
| Operation | Description | Time Complexity |
| Insert | Add a new element and maintain heap property | O(log n) |
| Extract Max/Min | Remove the root element (max/min) and reheapify | O(log n) |
| Peek | View the root element without removing | O(1) |
| Heapify | Convert an unsorted array into a heap | O(n) |
๐งช Example: Min Heap Using heapq in Python
import heapq
heap = []
# Insert elements
heapq.heappush(heap, 10)
heapq.heappush(heap, 5)
heapq.heappush(heap, 20)
print(heap) # Output: [5, 10, 20] (min element at root)
# Extract minimum
min_element = heapq.heappop(heap)
print(min_element) # 5
print(heap) # [10, 20]
Pythonโs heapq module implements a min heap by default.
๐ง How Insert Works (Heapify Up)
- Add the element at the end of the array.
- Compare the element with its parent; if heap property is violated, swap them.
- Repeat until the heap property is restored.
๐ง How Extract Works (Heapify Down)
- Replace root with the last element.
- Remove last element.
- Compare the new root with its children; swap with the smaller (min heap) or larger (max heap) child if heap property is violated.
- Repeat until heap property is restored.
๐ Use Cases
- Priority queues (task scheduling, CPU processes)
- Heap sort algorithm
- Finding the k smallest/largest elements efficiently
- Graph algorithms like Dijkstraโs shortest path
- Median finding in data streams
โ ๏ธ Important Notes
- Max heap can be implemented by inserting negative values in a min heap or by custom implementations.
- Heaps do not support fast search for arbitrary elements.
๐งฎ 8. Hash Tables
โ What is a Hash Table?
A hash table (also called a hash map) is a data structure that stores key-value pairs, allowing fast insertion, deletion, and lookup operations, ideally in O(1) average time.
It works by using a hash function to convert keys into indices of an underlying array, where the values are stored.
Use Pythonโs dict:
hash_table = {“name”: “Alice”, “age”: 25}
print(hash_table[“name”]) # Alice
๐ How Does a Hash Table Work?
- Hash Function: Converts a key into an integer index.
- Indexing: The integer is used to find the position in an array.
- Handling Collisions: When two keys map to the same index, strategies like chaining or open addressing are used.
๐ง Key Concepts
| Concept | Explanation |
| Hash Function | Maps keys to array indices |
| Collision | When two keys hash to the same index |
| Chaining | Store collided elements in a linked list at index |
| Open Addressing | Find next available slot (linear probing, quadratic probing, double hashing) |
| Load Factor | Ratio of number of elements to table size (affects performance) |
๐งช Python Example Using Dictionary (Built-in Hash Table)
# Creating a hash table (dict)
hash_table = {}
# Inserting key-value pairs
hash_table[“apple”] = 5
hash_table[“banana”] = 3
hash_table[“orange”] = 7
# Accessing value by key
print(hash_table[“banana”]) # Output: 3
# Checking if key exists
if “apple” in hash_table:
print(“Apple is present”)
# Removing a key
del hash_table[“orange”]
๐งฑ Simple Hash Table with Chaining (Custom Implementation)
class HashTable:
def __init__(self, size=10):
self.size = size
self.table = [[] for _ in range(size)]
def _hash(self, key):
return hash(key) % self.size
def insert(self, key, value):
index = self._hash(key)
# Check if key exists, update
for pair in self.table[index]:
if pair[0] == key:
pair[1] = value
return
# Otherwise insert new
self.table[index].append([key, value])
def get(self, key):
index = self._hash(key)
for pair in self.table[index]:
if pair[0] == key:
return pair[1]
return None
def remove(self, key):
index = self._hash(key)
for i, pair in enumerate(self.table[index]):
if pair[0] == key:
del self.table[index][i]
return True
return False
โฑ๏ธ Time Complexity
| Operation | Average Case | Worst Case (Poor Hashing) |
| Insert | O(1) | O(n) |
| Search | O(1) | O(n) |
| Delete | O(1) | O(n) |
๐ Use Cases
- Implementing dictionaries and sets
- Database indexing
- Caching/memoization
- Counting frequencies (e.g., word count)
- Symbol tables in compilers
โ ๏ธ Important Notes
- A good hash function minimizes collisions.
- When the load factor grows too high, resizing (rehashing) the table improves performance.
- Pythonโs built-in dictionary uses dynamic resizing and a very efficient hash function internally.
โ๏ธ Algorithms
๐ 1. Searching
- Linear Search โ O(n)
- Binary Search โ O(log n) (works on sorted arrays)
๐ Linear Search
โ What is Linear Search?
Linear search is the simplest search algorithm. It works by checking each element in a list, one by one, from the beginning to the end, until the target element is found or the list ends.
๐งฑ How Linear Search Works
- Start from the first element.
- Compare the current element with the target.
- If they match, return the index or element.
- If not, move to the next element.
- Repeat until found or the end of the list is reached.
๐งช Python Implementation
def linear_search(arr, target):
for index, element in enumerate(arr):
if element == target:
return index # Element found at index
return -1 # Element not found
๐ Example
numbers = [3, 5, 2, 9, 1]
target = 9
result = linear_search(numbers, target)
if result != -1:
print(f”Element found at index {result}”)
else:
print(“Element not found”)
Output:
Element found at index 3
โฑ๏ธ Time Complexity
| Case | Time Complexity |
| Best Case | O(1) (target at first element) |
| Worst Case | O(n) (target at last or not found) |
| Average Case | O(n) |
๐ Use Cases
- Searching unsorted or small datasets
- When simplicity is preferred over speed
- When data is in linked lists (no direct indexing)
- Useful as a building block for other algorithms
โ ๏ธ Important Notes
- Linear search does not require sorted data.
- Itโs not efficient for large datasets โ better to use binary search if data is sorted.
๐ Binary Search
โ What is Binary Search?
Binary Search is an efficient algorithm to find the position of a target value within a sorted array (or list). It works by dividing the search interval in half repeatedly, eliminating half of the remaining elements each step until the target is found or the search interval is empty.
๐งฑ How Binary Search Works
- Start with two pointers:
- low at the beginning of the array
- high at the end of the array
- low at the beginning of the array
- Find the middle element: mid = (low + high) // 2
- Compare the middle element with the target:
- If equal, return the index mid
- If target is smaller, search the left half (high = mid – 1)
- If target is larger, search the right half (low = mid + 1)
- If equal, return the index mid
- Repeat until low > high (target not found)
๐งช Python Implementation (Iterative)
def binary_search(arr, target):
low, high = 0, len(arr) – 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid # Found target
elif arr[mid] < target:
low = mid + 1
else:
high = mid – 1
return -1 # Target not found
๐งช Python Implementation (Recursive)
def binary_search_recursive(arr, target, low, high):
if low > high:
return -1 # Not found
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, high)
else:
return binary_search_recursive(arr, target, low, mid – 1)
๐ Example
numbers = [1, 3, 5, 7, 9, 11]
target = 7
result = binary_search(numbers, target)
if result != -1:
print(f”Element found at index {result}”)
else:
print(“Element not found”)
Output:
Element found at index 3
โฑ๏ธ Time Complexity
| Case | Time Complexity |
| Best Case | O(1) (target at mid) |
| Worst Case | O(log n) |
| Average Case | O(log n) |
๐ Requirements & Use Cases
- Requires sorted data to work correctly.
- Much faster than linear search for large datasets.
- Used in:
- Searching in large databases or arrays
- Finding boundaries (e.g., first/last occurrence)
- Efficiently solving many algorithmic problems
- Searching in large databases or arrays
โ ๏ธ Important Notes
- Works only on sorted collections.
- Recursive approach can cause stack overflow on very large arrays (iterative is safer).
- Always ensure data is sorted before applying binary search.
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
๐งผ 2. Sorting Algorithms
- Bubble Sort โ O(nยฒ)
- Merge Sort โ O(n log n)
- Quick Sort โ O(n log n) average
๐ Bubble Sort โ O(nยฒ)
โ What is Bubble Sort?
Bubble Sort is one of the simplest sorting algorithms. It works by repeatedly swapping adjacent elements if they are in the wrong order. This way, larger (or smaller) elements โbubble upโ to the end of the list after each pass.
๐งฑ How Bubble Sort Works
- Start at the beginning of the list.
- Compare each pair of adjacent elements.
- If the elements are in the wrong order (e.g., first is greater than second for ascending sort), swap them.
- Continue through the list; after the first pass, the largest element moves to the end.
- Repeat for the remaining unsorted elements.
- Stop when no swaps are needed (list is sorted).
๐งช Python Implementation
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
# Last i elements are already sorted
for j in range(0, n – i – 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j] # Swap
swapped = True
# If no two elements were swapped in the inner loop, list is sorted
if not swapped:
break
๐ Example
numbers = [64, 34, 25, 12, 22, 11, 90]
bubble_sort(numbers)
print(“Sorted array:”, numbers)
Output:
Sorted array: [11, 12, 22, 25, 34, 64, 90]
โฑ๏ธ Time Complexity
| Case | Time Complexity |
| Best Case | O(n) (when already sorted, with optimization) |
| Worst Case | O(nยฒ) |
| Average Case | O(nยฒ) |
๐ Use Cases
- Educational purposes to understand sorting basics.
- Small datasets where simplicity matters more than speed.
- When the list is almost sorted (best case optimization applies).
โ ๏ธ Important Notes
- Bubble Sort is inefficient for large datasets.
- Itโs a stable sort (preserves the relative order of equal elements).
- The optimization with the swapped flag helps to detect early completion.
๐งฌ Merge Sort โ O(n log n)
โ What is Merge Sort?
Merge Sort is an efficient, general-purpose, divide and conquer sorting algorithm.
It works by:
- Dividing the list into halves recursively.
- Sorting each half.
- Merging the sorted halves into one sorted list.
๐ฆ How Merge Sort Works
- If the list has 0 or 1 element, itโs already sorted.
- Divide the list into two halves.
- Recursively sort both halves.
- Merge the two sorted halves into one sorted list.
๐ Visualization
Before sorting:
[38, 27, 43, 3, 9, 82, 10]
After recursive splits:
[38, 27, 43], [3, 9, 82, 10]
โ [38] [27] [43], [3] [9] [82] [10]
After merging:
[27, 38, 43], [3, 9, 10, 82]
โ Final sorted list: [3, 9, 10, 27, 38, 43, 82]
๐งช Python Implementation
def merge_sort(arr):
if len(arr) <= 1:
return arr
# Divide
mid = len(arr) // 2
left_half = merge_sort(arr[:mid])
right_half = merge_sort(arr[mid:])
# Conquer (Merge)
return merge(left_half, right_half)
def merge(left, right):
result = []
i = j = 0
# Compare and merge
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
# Append remaining elements
result.extend(left[i:])
result.extend(right[j:])
return result
๐ Example
arr = [38, 27, 43, 3, 9, 82, 10]
sorted_arr = merge_sort(arr)
print(“Sorted array:”, sorted_arr)
Output:
Sorted array: [3, 9, 10, 27, 38, 43, 82]
โฑ๏ธ Time & Space Complexity
| Case | Time Complexity | Space Complexity |
| Best Case | O(n log n) | O(n) |
| Worst Case | O(n log n) | O(n) |
| Average Case | O(n log n) | O(n) |
Merge Sort is not in-place, as it uses extra space for merging.
๐ฏ Use Cases
- Large datasets that donโt fit in memory (external sorting)
- Stable sorting where the relative order of equal elements matters
- Linked list sorting (can be done in O(1) space)
โ ๏ธ Important Notes
- Always stable (preserves order of duplicates).
- Performs well consistently (O(n log n) in all cases).
- Slower than quicksort in practice for small datasets due to overhead.
โก Quick Sort โ O(n log n) Average
โ What is Quick Sort?
Quick Sort is a highly efficient divide and conquer sorting algorithm that works by:
- Choosing a pivot element.
- Partitioning the array: placing all smaller elements to the left of the pivot and larger elements to the right.
- Recursively applying the same process to the left and right sub-arrays.
๐ง How It Works
- Pick a pivot (can be the first, last, middle, or a random element).
- Reorder the array so that:
- All elements less than the pivot come before it.
- All elements greater than the pivot come after it.
- All elements less than the pivot come before it.
- Recursively apply Quick Sort to the left and right sub-arrays.
๐ Visualization
Given: [10, 7, 8, 9, 1, 5]
Choose pivot = 5
After partitioning:
[1] [5] [10, 7, 8, 9]
Recursively sort left and right sub-arrays.
Final result: [1, 5, 7, 8, 9, 10]
๐งช Python Implementation (Lomuto Partition)
def quick_sort(arr):
def partition(low, high):
pivot = arr[high]
i = low – 1
for j in range(low, high):
if arr[j] < pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def quick_sort_recursive(low, high):
if low < high:
pi = partition(low, high)
quick_sort_recursive(low, pi – 1)
quick_sort_recursive(pi + 1, high)
quick_sort_recursive(0, len(arr) – 1)
return arr
๐ Example
arr = [10, 7, 8, 9, 1, 5]
quick_sort(arr)
print(“Sorted array:”, arr)
Output:
Sorted array: [1, 5, 7, 8, 9, 10]
โฑ๏ธ Time & Space Complexity
| Case | Time Complexity | Space Complexity |
| Best | O(n log n) | O(log n) |
| Average | O(n log n) | O(log n) |
| Worst | O(nยฒ) | O(log n) |
Worst case occurs when the pivot is always the smallest/largest element (e.g., already sorted array). This can be improved using random pivot or median-of-three.
๐ In-Place Sorting
Unlike merge sort, quick sort does not require extra space for merging โ it’s an in-place algorithm.
๐ฏ Use Cases
- Built-in sorting in many libraries (e.g., C++’s std::sort)
- Efficient for large datasets in memory
- High-performance applications where speed matters
- One of the fastest sorting algorithms in practice
โ ๏ธ Important Notes
- Not stable by default (does not preserve order of equal elements).
- Best to randomize the pivot to avoid worst-case performance.
- Faster than merge sort in practice due to better cache performance and less memory usage.
โป๏ธ 3. Recursion
โ What is Recursion?
Recursion is a programming technique where a function calls itself to solve a smaller instance of the same problem.
It works by breaking a problem into smaller subproblems, solving each recursively, and combining the results.
๐ง Key Components of Recursion
- Base Case
- The condition under which the recursion stops.
- Prevents infinite recursion.
- The condition under which the recursion stops.
- Recursive Case
- The part where the function calls itself with modified input.
- The part where the function calls itself with modified input.
๐ Recursion Flow
def recursive_function():
if base_case_condition:
return result # Base case
else:
return recursive_function(smaller_input) # Recursive call
๐งช Example 1: Factorial (n!)
def factorial(n):
if n == 0 or n == 1:
return 1 # Base case
else:
return n * factorial(n – 1) # Recursive case
Call Stack Flow for factorial(4)
factorial(4)
โ 4 * factorial(3)
โ 4 * 3 * factorial(2)
โ 4 * 3 * 2 * factorial(1)
โ 4 * 3 * 2 * 1 = 24
๐งช Example 2: Fibonacci Sequence
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
return fibonacci(n – 1) + fibonacci(n – 2)
โ ๏ธ This is not efficient for large n. We’ll discuss how to improve it later (memoization, dynamic programming).
โฑ๏ธ Time Complexity
| Example | Time Complexity | Space (due to call stack) |
| Factorial | O(n) | O(n) |
| Fibonacci | O(2โฟ) (naive) | O(n) |
๐ When to Use Recursion
- When a problem can naturally be divided into subproblems (e.g., trees, graphs).
- When the depth of the problem is small (to avoid stack overflow).
- When writing elegant, concise solutions (e.g., backtracking, DFS).
โ ๏ธ Recursion vs Iteration
| Feature | Recursion | Iteration |
| Code Style | Short and expressive | Often longer but efficient |
| Performance | Slower (more function calls) | Faster (less overhead) |
| Space Usage | Uses call stack | Uses loop variables |
| Tail Call Opt? | Not supported in Python | Not applicable |
๐ ๏ธ Recursion Optimization Techniques
- Memoization
- Store results of previous calls to avoid recomputation.
- Store results of previous calls to avoid recomputation.
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
- Convert to Iteration
- In critical applications, convert recursion to a loop for better performance.
- In critical applications, convert recursion to a loop for better performance.
- Tail Recursion
- Python doesn’t optimize tail recursion, but itโs a common concept in other languages.
- Python doesn’t optimize tail recursion, but itโs a common concept in other languages.
๐ณ Recursion in Data Structures
Recursion is especially useful in:
- Tree Traversal (Preorder, Inorder, Postorder)
- Graph Traversal (DFS)
- Backtracking (e.g., N-Queens, Sudoku Solver)
- Divide & Conquer Algorithms (Merge Sort, Quick Sort)
๐ Base Case is Critical!
Without a base case, the function will keep calling itself forever, leading to a RecursionError in Python:
def infinite_recursion():
return infinite_recursion()
infinite_recursion() # โ RecursionError: maximum recursion depth exceeded
๐ง Summary
| Feature | Description |
| Elegant Logic | Good for recursive problems like trees/graphs |
| Control Structure | Needs base case to prevent infinite loops |
| Stack Usage | Each recursive call adds a new stack frame |
| Efficiency | May need memoization or conversion to iteration |
Function calls itself:
def factorial(n):
if n == 0:
return 1
return n * factorial(n – 1)
โ๏ธ 4. Divide and Conquer โ A Powerful Problem-Solving Paradigm
โ What is Divide and Conquer?
Divide and Conquer is an algorithmic paradigm that breaks a problem into smaller subproblems, solves them recursively, and combines their solutions to solve the original problem.
Itโs especially useful when the subproblems are similar to the original problem but smaller in size.
๐ง Key Steps of Divide and Conquer
- Divide โ Split the problem into smaller subproblems.
- Conquer โ Solve each subproblem recursively.
- Combine โ Merge the solutions of subproblems to get the final result.
Often used with recursion, but can also be implemented iteratively.
๐ General Template (Python)
def divide_and_conquer(problem):
if base_case(problem):
return trivial_solution(problem)
# Divide
subproblems = divide(problem)
# Conquer
sub_solutions = [divide_and_conquer(sub) for sub in subproblems]
# Combine
return combine(sub_solutions)
๐งช Example 1: Merge Sort (Classic Example)
def merge_sort(arr):
if len(arr) <= 1:
return arr
# Divide
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# Conquer (merge)
return merge(left, right)
def merge(left, right):
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
๐ Time Complexity: O(n log n)
๐ฆ Space Complexity: O(n)
๐งช Example 2: Binary Search
def binary_search(arr, target):
def helper(low, high):
if low > high:
return -1
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return helper(mid + 1, high)
else:
return helper(low, mid – 1)
return helper(0, len(arr) – 1)
๐ Time Complexity: O(log n)
๐ฆ Space Complexity: O(log n) (due to recursion)
๐งช Example 3: Maximum Subarray Sum (Kadaneโs Alternative)
Divide and conquer version of finding the max sum of a contiguous subarray.
def max_subarray(arr):
def helper(left, right):
if left == right:
return arr[left]
mid = (left + right) // 2
left_sum = helper(left, mid)
right_sum = helper(mid + 1, right)
cross_sum = max_crossing_sum(arr, left, mid, right)
return max(left_sum, right_sum, cross_sum)
def max_crossing_sum(arr, left, mid, right):
left_max = float(‘-inf’)
right_max = float(‘-inf’)
sum = 0
for i in range(mid, left – 1, -1):
sum += arr[i]
left_max = max(left_max, sum)
sum = 0
for i in range(mid + 1, right + 1):
sum += arr[i]
right_max = max(right_max, sum)
return left_max + right_max
return helper(0, len(arr) – 1)
๐ Time Complexity: O(n log n)
๐ฆ Space Complexity: O(log n)
๐งช Example 4: Matrix Multiplication (Strassenโs Algorithm โ Advanced)
Strassenโs algorithm multiplies two matrices in less than O(nยณ) time using divide and conquer (advanced topic โ just noting it here).
โฑ๏ธ Time Complexity Pattern
| Problem Type | Time Complexity |
| Merge Sort | O(n log n) |
| Binary Search | O(log n) |
| Quick Sort (avg) | O(n log n) |
| Matrix Multiplication | O(n^2.81) (Strassen) |
๐ Divide and Conquer vs Other Paradigms
| Paradigm | Use Case | Recursive? | Space Efficient? |
| Divide & Conquer | Solves independent subproblems | โ Yes | โ (needs merging) |
| Dynamic Programming | Solves overlapping subproblems | โ Often | โ (table/memo) |
| Greedy | Local optima lead to global optima | โ Usually | โ |
| Backtracking | Explore all choices with pruning | โ Yes | โ (stack-heavy) |
๐ Summary
| Feature | Description |
| Core Idea | Divide โ Conquer โ Combine |
| Speed | Often O(n log n) or better |
| Best For | Sorting, Searching, Aggregation, Geometry |
| Recursion | Fundamental to the approach |
| Parallelizable? | โ Often suitable for parallel processing |
๐ง Pro Tips
- Use divide and conquer when:
- You can break the problem into independent parts.
- You can merge the parts efficiently.
- You can break the problem into independent parts.
- Be careful with base cases and merge steps.
- Optimize space by avoiding unnecessary data copies.
๐ 5. Dynamic Programming
โ What is Dynamic Programming?
Dynamic Programming (DP) is an optimization technique used to solve complex problems by breaking them down into simpler overlapping subproblems, solving each subproblem only once, and storing their solutions to avoid redundant work.
It is typically used in optimization and counting problems.
๐ก Key Idea
“Don’t solve the same subproblem more than once.”
DP uses two main techniques to achieve this:
- Memoization (Top-Down) โ Recursion + caching
- Tabulation (Bottom-Up) โ Iterative + table building
๐ Steps to Solve a DP Problem
- Identify subproblems โ Break down the original problem.
- Decide state(s) โ What parameters define a subproblem?
- Choose recurrence relation โ How do subproblems relate?
- Choose memoization or tabulation
- Implement with storage (array/dictionary/etc.)
๐งช Example 1: Fibonacci Sequence
โค Naive Recursive Version (Inefficient)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
- โ Time Complexity: O(2โฟ)
- Recalculates the same subproblems multiple times.
โ Memoization (Top-Down)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
- โ
Time Complexity: O(n)
- โ
Space: O(n) (due to recursion stack)
โ Tabulation (Bottom-Up)
def fib(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
- โ
Time Complexity: O(n)
- โ
Space Complexity: O(n) โ Can be optimized to O(1)
๐งช Example 2: 0/1 Knapsack Problem
Given weights, values, and a capacity, find the max value that can be carried.
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w – weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
๐ข Common Problems Solved Using DP
| Problem Type | Example Problems |
| Fibonacci-style | Fibonacci, Climbing Stairs, Tribonacci |
| Subset Problems | 0/1 Knapsack, Subset Sum, Partition Equal Subset |
| String DP | Longest Common Subsequence, Edit Distance |
| Grid DP | Unique Paths, Minimum Path Sum, DP on Trees |
| DP on LIS | Longest Increasing Subsequence, Box Stacking |
โฑ๏ธ Complexity Comparison
| Approach | Time Complexity | Space Complexity |
| Naive Recursion | Exponential | Call Stack |
| Memoization | O(n) to O(nยฒ) | O(n) or O(nยฒ) |
| Tabulation | O(n) to O(nยฒ) | O(n) or O(nยฒ) |
๐ง When to Use DP
- Problem has optimal substructure:
- Solution can be built from solutions of subproblems.
- Solution can be built from solutions of subproblems.
- Problem has overlapping subproblems:
- Same subproblems repeat multiple times.
- Same subproblems repeat multiple times.
โ ๏ธ Common Mistakes
- Forgetting the base case.
- Not properly storing intermediate results.
- Incorrect order of computation in tabulation.
- Using recursion for deep subproblem chains without memoization โ leads to stack overflow.
๐ Summary
| Term | Meaning |
| Memoization | Recursion + caching |
| Tabulation | Iterative DP with table |
| State | Variables that define a subproblem |
| Transition | How one state leads to another |
๐ Pro Tip for Interviews & Practice
Always ask yourself:
- Can I break this problem into subproblems?
- Do I solve the same subproblem more than once?
- Can I store and reuse results?
Break problems into overlapping subproblems.
def fib(n, memo={}):
if n <= 1:
return n
if n in memo:
return memo[n]
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
๐ก 6. Greedy Algorithms
Make optimal choice at each step.
Example: Activity selection, coin change (limited denominations).
โ What is a Greedy Algorithm?
A Greedy Algorithm is an approach where you make the locally optimal choice at each step, hoping that these local choices lead to a globally optimal solution.
It doesn’t reconsider decisions once made โ “greedily” picks what’s best at the moment.
๐ How Greedy Works
- Sort / prioritize based on a specific condition.
- At each step, choose the best option available.
- Ensure that the choice is feasible (fits constraints).
- Repeat until the problem is solved.
๐ก When Does It Work?
Greedy works when a problem has the following properties:
| Property | Meaning |
| Greedy-choice property | A global optimum can be arrived at by choosing local optima. |
| Optimal substructure | An optimal solution can be constructed from optimal solutions to subproblems. |
If a problem doesn’t satisfy these, greedy might give wrong results.
๐งช Example 1: Coin Change (Greedy Version)
Given denominations and an amount, find the minimum number of coins to make that amount.
def coin_change_greedy(coins, amount):
coins.sort(reverse=True)
count = 0
for coin in coins:
while amount >= coin:
amount -= coin
count += 1
return count if amount == 0 else -1 # -1 means greedy failed
โ ๏ธ Note: Greedy only works for coin systems like U.S. currency, not all cases (e.g., [9, 6, 5, 1] and amount = 11 โ greedy fails here).
๐งช Example 2: Activity Selection Problem
Select the maximum number of non-overlapping activities from a list of start and end times.
def activity_selection(activities):
# Sort by ending time
activities.sort(key=lambda x: x[1])
count = 1
last_end = activities[0][1]
for i in range(1, len(activities)):
if activities[i][0] >= last_end:
count += 1
last_end = activities[i][1]
return count
๐งช Example 3: Fractional Knapsack
Maximize profit by selecting fractions of items based on weight limit.
def fractional_knapsack(items, capacity):
# items = [(value, weight)]
items.sort(key=lambda x: x[0]/x[1], reverse=True) # sort by value per weight
total_value = 0.0
for value, weight in items:
if capacity >= weight:
total_value += value
capacity -= weight
else:
total_value += value * (capacity / weight)
break
return total_value
โฑ๏ธ Time Complexity
Greedy algorithms are generally efficient:
| Step | Time Complexity |
| Sorting (optional) | O(n log n) |
| Main logic | O(n) |
| Overall | O(n log n) often |
๐ฏ Common Greedy Problems
| Problem | Greedy Strategy |
| Activity Selection | Pick earliest finishing activity |
| Fractional Knapsack | Pick by highest value/weight ratio |
| Huffman Coding | Merge lowest frequency nodes first |
| Dijkstraโs Algorithm (variant) | Greedily choose shortest distance node |
| Job Sequencing | Maximize profit within deadlines |
| Gas Station / Jump Game | Greedy choices at every station/index |
| Minimum Spanning Tree (Prim, Kruskal) | Greedily add minimum weight edge |
โ ๏ธ Pitfalls of Greedy
- Greedy doesn’t guarantee the optimal solution for all problems.
- Always prove its correctness or check if:
- The problem has greedy-choice property
- It has optimal substructure
- The problem has greedy-choice property
๐ Summary
| Feature | Description |
| Speed | Very fast, often O(n log n) |
| Simplicity | Easy to implement and reason about |
| Applicability | Works when greedy-choice property holds |
| Risk | Might fail to give the best solution in some problems |
๐ Pro Tip
If you’re not sure whether to use Greedy or Dynamic Programming:
- Try greedy first โ itโs faster.
- If greedy fails (try edge cases), use DP.
๐ 7. Backtracking
Try all possibilities and backtrack if needed.
Example: N-Queens, Sudoku solver.
โ What is Backtracking?
Backtracking is a general algorithmic technique for solving problems recursively by:
- Trying all possible choices, and
- Undoing (“backtracking”) when a choice leads to a dead-end (invalid solution).
Think of it like a smart brute-force โ it tries all options but abandons ones that canโt possibly lead to a valid or optimal solution.
๐ง Key Concept
Backtracking is used to build a solution step-by-step, and undo a step (backtrack) if it doesnโt lead to a valid solution.
๐ก Use Cases
Backtracking is commonly used in problems involving:
- Combinatorics (permutations, combinations)
- Constraint Satisfaction Problems
- Sudoku
- N-Queens
- Crossword puzzles
- Sudoku
- Pathfinding (e.g. Maze, Rat in a Maze)
- String and subset problems
- Subset sum
- Palindrome partitioning
- Subset sum
๐ General Backtracking Template (Python)
def backtrack(state, choices):
if goal_reached(state):
process_solution(state)
return
for choice in choices:
if is_valid(choice, state):
make_choice(choice, state)
backtrack(state, choices)
undo_choice(choice, state) # backtrack
๐งช Example 1: Generate All Subsets (Power Set)
def subsets(nums):
result = []
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop() # backtrack
backtrack(0, [])
return result
Input: [1, 2]
Output: [[], [1], [1, 2], [2]]
๐งช Example 2: N-Queens Problem
Place N queens on an NรN chessboard so that no two queens attack each other.
def solve_n_queens(n):
result = []
def is_valid(board, row, col):
for i in range(row):
if board[i] == col or \
abs(board[i] – col) == row – i:
return False
return True
def backtrack(row, board):
if row == n:
result.append([“.” * c + “Q” + “.” * (n – c – 1) for c in board])
return
for col in range(n):
if is_valid(board, row, col):
board.append(col)
backtrack(row + 1, board)
board.pop()
backtrack(0, [])
return result
๐งช Example 3: Sudoku Solver (9×9 grid)
Each empty cell is filled using a backtracking strategy by trying numbers 1 to 9 and checking if theyโre valid.
Too large to include here completely โ but want a full implementation? Just ask!
โฑ๏ธ Time Complexity (Varies)
Backtracking is often exponential in time because it explores many combinations.
| Problem | Time Complexity |
| Subsets | O(2โฟ) |
| N-Queens | O(N!) |
| Sudoku | Up to O(9โธยน) (pruned using constraints) |
But many problems are pruned (using constraints like is_valid) to avoid exploring bad paths early.
โ๏ธ Backtracking vs Recursion vs DFS
| Feature | Backtracking | Recursion | DFS |
| Core Idea | Explore, undo bad choices | Function calls itself | Explore all paths in a structure |
| Used For | Puzzles, constraint problems | Divide & conquer, simple logic | Trees, graphs, search |
| “Undo” Step | Yes | Sometimes | No (in basic DFS) |
๐ Summary
| Feature | Description |
| Type | Recursive + decision tree + undo when needed |
| Efficient? | Smart brute-force + pruning |
| Best for | Combinatorial, constraint, and search problems |
| Key Concept | Try โ Check โ Undo if invalid |
๐ง Pro Tips
- Always define your base case and backtrack step clearly.
- Use sets/flags to track visited/used elements efficiently.
- Add early stopping (pruning) to improve performance.
๐ ๏ธ Python Built-in Modules for DSA
| Module | Use Case |
| collections | deque, Counter, defaultdict |
| heapq | Min-heap |
| bisect | Binary search and insert |
| itertools | Combinatorics, permutations |
| functools | Memoization (lru_cache) |
๐งช Common DSA Problems
- Reverse a Linked List
- Detect Cycle in Graph
- Find Kth Largest Element (heap)
- Longest Substring Without Repeating Characters
- Dynamic Programming: 0/1 Knapsack
Would you like examples and solutions for these?
๐ Tips for Mastery
- Understand the time and space complexity.
- Practice on platforms like LeetCode, HackerRank, Codeforces.
- Visualize structures using tools like visualgo.net.
- Start annotating problems you solve โ Why this structure? Why this algorithm?
๐ Resources
- Book: “Grokking Algorithms” by Aditya Bhargava
- Course: MITโs Introduction to Algorithms (YouTube)
- Practice: LeetCode, GeeksforGeeks
๐ Final Thoughts
Learning DSA in Python not only improves your coding interviews but enhances your logical thinking and problem-solving skills in real-world software development.
๐ง FAQ: Data Structures & Algorithms (DSA) in Python
โ What is DSA and why is it important?
DSA (Data Structures and Algorithms) is the foundation of efficient programming.
- Data Structures: Organize and store data (e.g., lists, stacks, trees).
- Algorithms: Step-by-step procedures to solve problems (e.g., sorting, searching).
๐งฉ Mastering DSA helps in:
- Writing efficient code
- Cracking coding interviews
- Solving real-world problems at scale
โ Why learn DSA using Python?
Python is:
- Beginner-friendly (easy syntax)
- Powerful (built-in data types like list, dict, set)
- Readable (great for focusing on logic, not syntax)
- Supported by strong libraries (like heapq, collections, bisect)
โ Is Python fast enough for competitive programming?
Python is slower than C++/Java, but:
- Itโs good enough for learning, practice, and interviews.
- You can use PyPy (a faster Python interpreter).
- Just avoid unnecessary recursion and large input sizes in contests.
โ What are the most important data structures in Python?
| Category | Python Structure | Built-in or Custom |
| Linear | list, deque, array | Built-in + collections |
| Stack | list or deque | Built-in |
| Queue | deque, queue.Queue | Built-in / module |
| Hash Table | dict, set | Built-in |
| Heap | heapq (min-heap) | Built-in module |
| Tree | custom classes | Custom implementation |
| Graph | dict of lists, adjacency matrix | Custom |
โ What are the key algorithms every Python dev should know?
| Type | Examples |
| Searching | Linear Search, Binary Search |
| Sorting | Bubble, Merge, Quick, Insertion |
| Recursion | Factorial, Fibonacci |
| Divide & Conquer | Merge Sort, Quick Sort |
| Backtracking | N-Queens, Sudoku Solver |
| Dynamic Programming | Knapsack, LCS, LIS |
| Greedy | Activity Selection, Fractional Knapsack |
| Graph | DFS, BFS, Dijkstraโs, Topo Sort |
| Tree | Traversals, BST, AVL |
โ Is recursion better than iteration?
- Recursion is elegant and great for tree-like problems, but:
- Python has a recursion depth limit (~1000).
- It can lead to stack overflow if not used carefully.
- Python has a recursion depth limit (~1000).
Use iteration for large loops or simple patterns.
โ How to practice DSA in Python?
๐ ๏ธ Platforms:
- LeetCode
- HackerRank
- Codeforces
- GeeksforGeeks
- Exercism.io
Start with:
- Arrays & Strings
- Then move to Recursion & Sorting
- Then Trees, Graphs, and DP
โ Should I memorize code or understand logic?
โ
Always focus on understanding the logic.
You can:
- Write your own pseudocode
- Use cheat sheets for syntax reference
- Practice writing from scratch, without copy-paste
โ Can I use built-in functions in interviews?
- For basic structures (like sort(), len(), max()), yes.
- For problem-specific logic (like heapq, bisect), check with the interviewer.
It’s better to show understanding of the algorithm, even if you use a built-in function.
โ Whatโs the difference between list, tuple, set, dict in Python?
| Type | Mutable? | Ordered? | Duplicates? | Use case |
| List | โ | โ | โ | Dynamic arrays |
| Tuple | โ | โ | โ | Fixed data (coordinates) |
| Set | โ | โ | โ | Unique elements |
| Dict | โ | โ (Py 3.7+) | Keys must be unique | Key-value pairs |
โ How do I handle large input sizes in Python?
- Use sys.stdin.readline() for fast input
- Avoid recursion for deep calls
- Use PyPy interpreter for contests
- Optimize algorithms (prefer O(n log n) over O(nยฒ))
โ How is memory managed in Python for DSA?
- Python manages memory automatically (garbage collection).
- But you must avoid:
- Unnecessary list copies
- Large call stacks
- Mutable default arguments
- Unnecessary list copies
โ Is learning DSA enough to become a good developer?
DSA is essential for:
- Thinking logically
- Writing optimized code
- Cracking interviews
But real-world development also needs:
- System design
- Databases
- Networking
- Frameworks and Tools
โ What’s a good roadmap for DSA in Python?
๐ Beginner
- Arrays, Strings
- Searching, Sorting
- Stacks, Queues
- Recursion
๐ Intermediate
- Linked Lists
- Hash Tables
- Trees
- Binary Search
- Two Pointers, Sliding Window
๐ Advanced
- Heaps
- Graphs (DFS, BFS, Dijkstra)
- Dynamic Programming
- Backtracking
- Greedy Algorithms
- Trie, Segment Trees
โ Best resources for learning DSA in Python?
- Books:
- Grokking Algorithms by Aditya Bhargava
- Introduction to Algorithms by Cormen (CLRS)
- Grokking Algorithms by Aditya Bhargava
- Courses:
- CS50 (Harvard)
- AlgoExpert
- GeeksforGeeks Python DSA
- Leetcode Explore
- CS50 (Harvard)
- YouTube Channels:
- Abdul Bari (Concepts)
- NeetCode (Python)
- Tech With Tim
- Abdul Bari (Concepts)
โ Final Advice
- Practice every day
- Start small, grow steadily
- Write clean, testable code
- Learn to analyze time & space complexity
- Donโt just memorize โ understand