Understanding Abstract Data Types (ADTs)
Understanding Abstract Data Types (ADTs)
Definition
An Abstract Data Type (ADT) is a logical description of how data is viewed and what operations can be
performed on it — without specifying how these operations are implemented.
Key Idea
ADTs separate the interface (what to do) from the implementation (how to do it).
Aspect Description
Logical (Abstract) View Focuses on what the data structure does.
Physical (Implementation) View Focuses on how it does it (using arrays, linked lists, etc.).
TV Remote (ADT):
o Interface: Buttons like Power, Volume+, Volume−, Channel+.
o Implementation: Could be infrared or Bluetooth — user doesn’t care.
o → You only need to know what each button does, not how it works internally.
Bank Account (ADT):
o Data: Account balance.
o Operations: Deposit, Withdraw, Check balance.
o Implementation: Doesn’t matter whether it’s stored in a database, blockchain, etc.
Programming Example:
ADT Stack {
push(item)
pop()
peek()
isEmpty()
}
The above defines what operations a Stack supports — not how they’re written in code.
2. Characteristics of ADTs
1. Encapsulation
o Hides implementation details from the user.
o User interacts through defined operations only.
Example:
When you use a List in Python (append(), remove()), you don’t see how it’s internally stored
(array or linked list).
Example:
A Stack only allows push() and pop() from the top; you can’t remove items from the middle.
3. Abstraction
o Focus on what operations do, not how they do it.
Example:
o In an ATM machine, you know “Withdraw” takes money out, not the exact steps inside the
machine.
4. Implementation Independence
o Different data structures can implement the same ADT.
Example:
A. List ADT
Definition:
A collection of elements arranged in a sequence (order matters).
Operations:
insert(position, item)
delete(position)
get(position)
length()
Examples:
B. Stack ADT
Definition:
A collection where the last inserted item is the first to be removed.
(LIFO – Last In, First Out)
Operations:
push(item) → Add to top
pop() → Remove from top
peek() → View top item
isEmpty() → Check if stack is empty
Examples:
Real-world:
o Stack of plates — take from the top first.
o Undo/Redo feature in software.
Programming Example:
stack = []
[Link]('A')
[Link]('B')
[Link]() # removes 'B'
C. Queue ADT
Definition:
A collection where the first inserted item is the first removed.
(FIFO – First In, First Out)
Operations:
Examples:
Real-world:
o Queue at a ticket counter.
o Print jobs in a printer queue.
Programming Example:
from collections import deque
q = deque()
[Link]('A')
[Link]('B')
[Link]() # removes 'A'
D. Set ADT
Definition:
A collection of unique items (no duplicates, order doesn’t matter).
Operations:
add(item)
remove(item)
union(setB)
intersection(setB)
isMember(item)
Examples:
Real-world:
o Group of students enrolled in a course (each student appears once).
o Tags on social media posts (unique categories).
Programming Example:
A = {1, 2, 3}
B = {3, 4, 5}
[Link](B) # {1,2,3,4,5}
E. Tree ADT
Definition:
A hierarchical structure consisting of nodes, where each node has:
A value, and
Links to child nodes.
Operations:
insert(node)
delete(node)
traverse()
find(value)
Examples:
Real-world:
o Company hierarchy (CEO → Managers → Employees).
o Family tree.
Programming Example (Binary Tree):
class Node:
def __init__(self, val):
[Link] = None
[Link] = None
[Link] = val
Analogy:
Example Connection
ADT Stack {
push(item)
pop()
peek()
isEmpty()
}
class Stack:
def __init__(self):
[Link] = []
def push(self, item):
[Link](item)
def pop(self):
return [Link]()
def peek(self):
return [Link][-1]
def isEmpty(self):
return len([Link]) == 0
Complexity analysis
What is Complexity Analysis?
Complexity Analysis helps us measure the efficiency of an algorithm — how much time and space
(memory) it needs to run.
It allows us to:
Why It Matters
In real-world systems (like Google Search or Banking Apps), efficiency matters more than just
correctness.
A slow algorithm can make a system unusable even if it gives the correct result.
Example Scenarios
Example
def print_items(n):
for i in range(n):
print(i)
Visual Understanding
O(1) ── constant
O(log n) ── grows slowly
O(n) ── grows linearly
O(n²) ── grows fast
O(2ⁿ) ── explodes exponentially
Common Mistake
4. Space Complexity
Space complexity measures the amount of extra memory required by an algorithm apart from the input
data.
5. Asymptotic Analysis
Asymptotic analysis studies the behavior of an algorithm as input size → ∞ (very large).
It ignores constant factors and focuses on growth rate.
Example
for i in range(n):
for j in range(n):
print(i, j)
Mixed Example
for i in range(n):
print(i)
for j in range(n*n):
print(j)
Recursive Example
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
Common Mistakes
8. Real-World Relevance
Scenario Algorithm Choice Reason
Searching small data Linear Search Simpler, small input
Searching large sorted data Binary Search Faster (O(log n))
Sorting emails Merge Sort / QuickSort Efficient O(n log n)
Pathfinding in maps Dijkstra’s Algorithm Balances time and space
Data compression Huffman Coding Optimizes efficiency
Big-O notation
What Is Big-O Notation?
Simple Explanation
Big-O notation describes how the running time or memory usage of an algorithm grows as the input size
(n) increases.
It tells us the worst-case performance of an algorithm — how much time it could take at most.
Why We Use It
Example
Real-Life Analogy
2. Formal Definition
Let T(n) represent the running time of an algorithm for input size n.
We say:
T(n) = O(f(n))
if there exist positive constants c and n₀ such that
T(n) ≤ c × f(n) for all n ≥ n₀.
✅ Meaning:
After a certain input size n₀, the growth of T(n) will never exceed f(n) multiplied by some constant.
Example:
If T(n) = 5n + 3 → O(n)
(because for large n, the term “5n” dominates, and constants 5 and 3 don’t change the growth pattern)
Sorting algorithms like Merge Sort or QuickSort split the array (log n) and sort each part (n).
1. Identify loops
o One loop → O(n)
o Nested loops → Multiply: O(n²)
2. Ignore constants
o 2n + 10 → O(n)
3. Add separate parts
o O(n) + O(n²) → keep the dominant one → O(n²)
4. Consider recursive calls
o e.g., Binary Search halves each time → O(log n)
Example
for i in range(n): # O(n)
for j in range(n): # O(n)
print(i, j)
for k in range(n): # O(n)
print(k)
7. Real-World Examples
Situation Algorithm Complexity Meaning
Finding a person in a phonebook Binary Search O(log n) Quick lookup
Situation Algorithm Complexity Meaning
Checking all students’ marks Linear Search O(n) Slower for large classes
Sorting an email inbox Merge Sort O(n log n) Efficient
Password cracking (brute force) Exponential O(2ⁿ) Extremely slow
Generating all possible routes Factorial O(n!) Practically impossible
A Stack is a linear data structure that follows the LIFO (Last In, First Out) principle.
👉 The last element inserted into the stack is the first one to be removed.
Key Operations
Operation Description
Real-World Examples
Visual Representation
Top → [E]
[D]
[C]
[B]
Bottom [A]
Example:
Example:
Example:
Stack: [A, B, C]
peek() → C
Stack remains [A, B, C]
4️⃣ isEmpty
1. Using Arrays
2. Using Linked Lists
A. Stack Implementation Using Array
Concept
Structure
Variable Description
Operations
Push Operation
Pop Operation
Peek
def pop(self):
if [Link] == -1:
print("Stack Underflow")
return None
item = [Link][[Link]]
[Link] -= 1
return item
def peek(self):
if [Link] == -1:
return None
return [Link][[Link]]
def isEmpty(self):
return [Link] == -1
Example Run
s = StackArray(3)
[Link]('A')
[Link]('B')
print([Link]()) # B
[Link]() # removes B
[Link]('C')
Advantages
✅ Simple to implement
✅ Fast access using index
Disadvantages
Structure of Node
class Node:
data
next
Stack Visualization
Top → [30 | next] → [20 | next] → [10 | None]
def pop(self):
if [Link] is None:
print("Stack Underflow")
return None
popped = [Link]
[Link] = [Link]
return popped
def peek(self):
if [Link] is None:
return None
return [Link]
def isEmpty(self):
return [Link] is None
Example Run
s = StackLinkedList()
[Link](10)
[Link](20)
[Link](30)
print([Link]()) # 30
[Link]() # removes 30
print([Link]()) # 20
Push 10 → Top = 10
Push 20 → Top = 20 → 10
Push 30 → Top = 30 → 20 → 10
Pop → Top = 20 → 10
Advantages
Disadvantages
Overflow Condition Possible (if array full) Not unless memory full
5. Stack Applications
Stacks are used everywhere in computer science:
Application Description
Expression: (3 + 5) * (2 - 1)
6. Complexity Analysis
Operation Array Stack Linked List Stack
Mixing stack order (FIFO vs LIFO) Remember: LIFO (Last In, First Out)
Forgetting None check in linked list Always check if top is None before accessing
Recursion is a programming technique where a function calls itself directly or indirectly to solve a
problem.
In simple words: a recursive function solves a smaller version of the same problem until it reaches a base
case.
Mathematically:
n! = n × (n − 1) × (n − 2) × … × 1
or recursively,
n! = n × (n − 1)! , with 0! = 1
Python Example:
def factorial(n):
if n == 0: # Base case
return 1
else:
return n * factorial(n - 1) # Recursive case
Flow:
factorial(3)
→ 3 * factorial(2)
→ 3 * (2 * factorial(1))
→ 3 * (2 * (1 * factorial(0)))
→ 3 * 2 * 1 * 1 = 6
Code:
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
Real-World Examples
2. Types of Recursion
Type Description Example
Direct Recursion Function calls itself directly factorial(n)
Indirect Recursion Function A calls B, and B calls A A() → B() → A()
Tail Recursion Recursive call is the last statement in the function factorial in tail form
Non-Tail Recursion Function performs more work after recursive call Fibonacci
Mutual Recursion Two or more functions calling each other in cycle even(), odd() pair
Non-Tail Recursion
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1) # More work (multiplication) after call
Tail Recursion
Tail recursion is memory efficient, because it can be optimized by the compiler (converted to iteration).
The current state (variables, position, return address) is pushed onto the call stack.
When a base case is reached, each function call returns in reverse order (LIFO).
Example: factorial(3)
❌ Disadvantages
def factorial_iterative(n):
result = 1
for i in range(1, n+1):
result *= i
return result
Example 1: Factorial
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
Recurrence Relation:
T(n) = T(n−1) + O(1)
(Single recursive call + constant work)
Solution:
T(n) = O(n)
Example 2: Fibonacci
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
Recurrence Relation:
T(n) = T(n−1) + T(n−2) + O(1)
Solution:
T(n) ≈ O(2ⁿ)
Recurrence Relation:
T(n) = T(n/2) + O(1)
Recurrence Relation:
T(n) = 2T(n/2) + O(n)
By Master Theorem:
T(n) = O(n log n)
9. Optimization Techniques
1. Tail Recursion Optimization – keeps constant stack size.
2. Memoization – store results of subproblems (Dynamic Programming).
3. Iterative Conversion – rewrite using loops if recursion is too deep.
Break a big problem into smaller pieces → solve each piece → combine results.
Real-World Analogy
Sorting a deck of cards: Split the deck into halves, sort each half, then merge them.
Teamwork: Divide a big project among team members, solve parts individually, then integrate.
Binary Search: Divide the list into halves repeatedly until you find the target.
3. Mathematical Representation
If a problem of size n is divided into a subproblems, each of size n/b, and the combine step takes O(f(n)),
then the recurrence relation is:
Concept:
Search an element in a sorted array by repeatedly dividing the search space in half.
Code Example:
Time Complexity:
T(n) = T(n/2) + O(1) → O(log n)
Real Example:
Looking for a word in a dictionary by opening the middle page and narrowing the range.
️ 2. Merge Sort
Concept:
Divide array into halves → sort each half → merge them.
Code Example:
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
L = arr[:mid]
R = arr[mid:]
merge_sort(L)
merge_sort(R)
i = j = k = 0
while i < len(L) and j < len(R):
if L[i] < R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
Real Example:
Sorting a large list of names by splitting and merging sorted sublists.
️ 3. Quick Sort
Concept:
Choose a pivot, partition array into smaller/larger elements, and sort recursively.
Code Example:
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
Recurrence Relation:
T(n) = T(k) + T(n−k−1) + O(n)
Time Complexity:
Real Example:
Organizing students by height: pick a “pivot” height, divide into taller/shorter groups, repeat.
Concept:
Improves matrix multiplication using fewer recursive multiplications.
Traditional: O(n³)
Strassen: O(n^2.81)
Recurrence:
T(n) = 7T(n/2) + O(n²)
❌ Disadvantages
Example Applications
9. Common Mistakes
❌ Mistake ✅ Correction
Forgetting to combine results Always implement the combine step properly
Ignoring base case Add a clear base condition to stop recursion
Incorrect recurrence relation Carefully derive recurrence from algorithm steps
Assuming divide always improves performance Some problems are better solved iteratively
Sorting is the process of arranging data in a specific order — typically ascending or descending.
Purpose
Real-World Example
A. Bubble Sort
Idea
Repeatedly compare adjacent elements, swapping if they are in the wrong order.
[5, 3, 4, 1]
→ [3, 5, 4, 1]
→ [3, 4, 5, 1]
→ [3, 4, 1, 5]
→ ...
Python Example
def bubble_sort(arr):
n = len(arr)
for i in range(n-1):
for j in range(n-1-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
Time Complexity
Case Complexity
Best O(n) (if already sorted)
Average O(n²)
Worst O(n²)
Space O(1)
Stability ✅ Stable
⏱ Analogy: Like bubbles rising to the top — largest elements “bubble up” each pass.
B. Selection Sort
Idea
Select the smallest element and swap it with the first unsorted element.
Example:
[5, 3, 4, 1]
→ Select 1 → swap with 5 → [1, 3, 4, 5]
Python Example
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]
Complexity
Case Complexity
Best O(n²)
Average O(n²)
Worst O(n²)
Space O(1)
Stability ❌ Unstable
⏱ Analogy: Like picking the smallest card in each round from a deck.
C. Insertion Sort
Idea
Builds a sorted portion one element at a time by inserting new elements in the correct position.
Example:
[5, 2, 4, 6]
→ [2, 5, 4, 6]
→ [2, 4, 5, 6]
Python Example
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
Complexity
Case Complexity
Best O(n)
Average O(n²)
Worst O(n²)
Space O(1)
Stability ✅ Stable
Python Example
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
L = arr[:mid]
R = arr[mid:]
merge_sort(L)
merge_sort(R)
i = j = k = 0
while i < len(L) and j < len(R):
if L[i] < R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < len(L):
arr[k] = L[i]
i += 1
k += 1
while j < len(R):
arr[k] = R[j]
j += 1
k += 1
Complexity
Case Complexity
Best O(n log n)
Average O(n log n)
Worst O(n log n)
Space O(n)
Stability ✅ Stable
⏱ Analogy: Sorting by dividing a big list into halves and merging sorted parts.
Python Example
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
Complexity
Case Complexity
Best O(n log n)
Average O(n log n)
Worst O(n²) (if pivot bad)
Space O(log n)
Stability ❌ Unstable
F. Heap Sort
Idea
Convert array into a heap, then repeatedly extract the maximum element.
Python Example
def heapify(arr, n, i):
largest = i
l, r = 2*i + 1, 2*i + 2
if l < n and arr[l] > arr[largest]:
largest = l
if r < n and arr[r] > arr[largest]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
for i in range(n//2 - 1, -1, -1):
heapify(arr, n, i)
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
Complexity
Case Complexity
Best O(n log n)
Average O(n log n)
Worst O(n log n)
Space O(1)
Stability ❌ Unstable
G. Shell Sort
Idea
Improves insertion sort by comparing elements far apart, gradually reducing the gap.
Python Example
def shell_sort(arr):
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
Complexity
Case Complexity
Best O(n log n)
Average O(n^(3/2))
Worst O(n²)
Space O(1)
Stability ❌ Unstable
Python Example
def counting_sort(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for i in range(n):
index = arr[i] // exp
count[index % 10] += 1
for i in range(1, 10):
count[i] += count[i - 1]
i = n - 1
while i >= 0:
index = arr[i] // exp
output[count[index % 10] - 1] = arr[i]
count[index % 10] -= 1
i -= 1
for i in range(n):
arr[i] = output[i]
def radix_sort(arr):
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort(arr, exp)
exp *= 10
Complexity
Case Complexity
Best O(nk)
Average O(nk)
Worst O(nk)
Space O(n + k)
Stability ✅ Stable
⏱ Analogy: Sorting numbers by digits like a postal worker sorting by zip codes.
I. Bucket Sort
Distribute elements into buckets, sort each bucket, then combine.
Example:
Python Example
def bucket_sort(arr):
buckets = [[] for _ in range(len(arr))]
for num in arr:
index = int(num * len(arr))
buckets[index].append(num)
for bucket in buckets:
[Link]()
return [num for bucket in buckets for num in bucket]
Complexity
Case Complexity
Best O(n + k)
Average O(n + k)
Worst O(n²)
Space O(n + k)
Stability ✅ Stable (depends on bucket sort used)
Computers cannot directly evaluate infix expressions like humans do — they need a structured way to
process them.
Example:
Human-readable expression:
A + B * C
Computers can’t directly handle operator precedence here, so we use stacks to reorder or evaluate
expressions correctly.
Parentheses ()
Operator precedence (* before +)
Reversing order of operations
Stacks help:
3. Expression Notations
Because infix notation requires precedence and parentheses handling — stacks simplify this by making
order explicit.
Example 1:
Convert:
A + B * C
Step Symbol Stack Output
1 A A
2 + + A
3 B + AB
4 * +* AB
5 C +* ABC
6 End → Pop all ABC*+
✅ Postfix: A B C * +
Example 2:
Convert:
(A + B) * (C - D)
Step Symbol Stack Output
1 ( (
2 A ( A
3 + (+ A
4 B (+ AB
5 ) — AB+
6 * * AB+
7 ( *( AB+
8 C *( AB+C
9 - *(- AB+C
10 D *(- AB+CD
11 ) * AB+CD-
Step Symbol Stack Output
12 End AB+CD-*
✅ Postfix: A B + C D - *
Python Example:
def infix_to_postfix(expression):
precedence = {'+':1, '-':1, '*':2, '/':2, '^':3}
stack = []
output = ''
Example:
Evaluate:
Postfix: 2 3 4 * +
Step Symbol Stack Action
1 2 2 Push
2 3 23 Push
3 4 234 Push
4 * 2 12 Pop 3 & 4 → 3*4=12 → Push
5 + 14 Pop 2 & 12 → 2+12=14 → Push
✅ Result = 14
Python Example
def evaluate_postfix(expr):
stack = []
for ch in [Link]():
if [Link]():
[Link](int(ch))
else:
b = [Link]()
a = [Link]()
if ch == '+': [Link](a + b)
elif ch == '-': [Link](a - b)
elif ch == '*': [Link](a * b)
elif ch == '/': [Link](a / b)
return stack[0]
Step Operation
1 Scan expression right to left
2 Push operands
3 When operator found, pop two operands, evaluate, push result back
4 Final result is in the stack
Example:
Prefix: + 2 * 3 4
Evaluation:
1. * 3 4 = 12
2. + 2 12 = 14
✅ Result = 14
8. Real-World Applications
✅ Compilers & Interpreters — Convert infix expressions into postfix for machine execution
✅ Calculators — Evaluate expressions in postfix form internally
✅ Expression Evaluators in Programming Languages — e.g., math libraries, spreadsheets
Queues and variants (dequeue, priority queues)
What is a Queue?
Definition
A Queue is a linear data structure that follows the FIFO (First-In, First-Out) principle —
➡⏱ the first element added is the first one removed.
Real-World Analogies
Operation Description
Enqueue(x) Add element x at the rear (end) of the queue
Dequeue() Remove element from the front of the queue
Front / Peek() View the front element without removing it
IsEmpty() Check if the queue has no elements
IsFull() (For fixed-size queues) check if it’s full
Queue Representation
Front → [10][20][30][40] ← Rear
Dequeue → 10
Enqueue(50) → [20][30][40][50]
2. Queue Implementation
A. Array-Based Implementation
class Queue:
def __init__(self, size):
[Link] = [None] * size
[Link] = size
[Link] = [Link] = -1
def dequeue(self):
if [Link] == -1 or [Link] > [Link]:
print("Queue is Empty")
else:
print("Dequeued:", [Link][[Link]])
[Link] += 1
B. Linked List Implementation
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
class Queue:
def __init__(self):
[Link] = [Link] = None
def dequeue(self):
if [Link] is None:
print("Queue is Empty")
return
temp = [Link]
[Link] = [Link]
if [Link] is None:
[Link] = None
print("Dequeued:", [Link])
3. Types of Queues
Type Description Example Use
Simple Queue Standard FIFO queue Ticket line
Connects rear to front (circular
Circular Queue Memory-efficient buffer
array)
Double-Ended Queue
Insert/delete from both ends Undo/Redo, Browser history
(Deque)
CPU Scheduling, Dijkstra’s
Priority Queue Elements have priorities
Algorithm
4. Circular Queue
Problem with Simple Queue
When front moves forward, empty spaces form at the start → wasted space.
Formulae
Example
[10, 20, 30, 40], size = 4
Dequeue 10 → Front moves
Enqueue 50 → Rear wraps → [50, 20, 30, 40]
A Deque (Double-Ended Queue) allows insertion and deletion from both ends.
Types of Deque
Type Description
Input-Restricted Deque Insertion only at rear, deletion both ends
Output-Restricted Deque Deletion only at front, insertion both ends
Operations
Operation Description
insertFront(x) Add element at front
insertRear(x) Add element at rear
deleteFront() Remove element from front
deleteRear() Remove element from rear
Example Flow
InsertRear(10) → [10]
InsertRear(20) → [10, 20]
InsertFront(5) → [5, 10, 20]
DeleteRear() → [5, 10]
Python Example
from collections import deque
dq = deque()
[Link](10) # InsertRear
[Link](5) # InsertFront
[Link]() # DeleteRear
[Link]() # DeleteFront
Real-World Uses
🚨 6. Priority Queue
Definition
A Priority Queue is a special type of queue where each element has a priority,
and elements with higher priority are served first, regardless of insertion order.
Example
Jobs with priorities:
Job A (priority 3)
Job B (priority 1)
Job C (priority 2)
Served in order: A → C → B
Type Description
Ascending Priority Queue Lower values have higher priority
Descending Priority Queue Higher values have higher priority
import heapq
pq = []
[Link](pq, (2, 'Write Code'))
[Link](pq, (1, 'Fix Bug'))
[Link](pq, (3, 'Review'))
while pq:
print([Link](pq))
Output:
️ 7. Comparison Table
Queue Type Insert From Delete From Order Special Feature
Simple Queue Rear Front FIFO Basic form
Circular Queue Rear Front FIFO Efficient memory usage
Deque Both ends Both ends Flexible Used in complex apps
Priority Queue Anywhere (based on priority) Based on priority Priority-based Used in scheduling
8. Time Complexities
Operation Simple Queue Deque Priority Queue (Heap)
Enqueue O(1) O(1) O(log n)
Dequeue O(1) O(1) O(log n)
Peek O(1) O(1) O(1)
Search O(n) O(n) O(n)
9. Common Mistakes
❌ Forgetting to handle queue overflow/underflow
❌ Confusing front and rear pointers
❌ Mixing up priority-based and FIFO-based removal
❌ Not resetting indices in circular queues
A Linked List is a linear data structure where elements (called nodes) are connected using pointers.
Unlike arrays, linked lists don’t store elements in contiguous memory.
Visual Representation
[Data|Next] → [Data|Next] → [Data|Next] → NULL
Example:
Key Idea
The last node always points to NULL (indicating the end of the list).
The first node is called the Head.
2. Why Linked Lists?
Feature Array Linked List
Memory Allocation Fixed (contiguous) Dynamic (non-contiguous)
Insertion/Deletion Expensive (shifting needed) Efficient (just change pointers)
Random Access O(1) O(n)
Memory Use May waste space Efficient use (grow/shrink easily)
Structure
Head → [10|*] → [20|*] → [30|NULL]
def display(self):
temp = [Link]
while temp:
print([Link], end=" → ")
temp = [Link]
print("NULL")
Example Run
ll = LinkedList()
ll.insert_end(10)
ll.insert_end(20)
ll.insert_front(5)
[Link]()
Output:
5 → 10 → 20 → NULL
Structure
NULL ← [10|*|*] ↔ [20|*|*] ↔ [30|*|NULL]
Advantages
✅ Bidirectional traversal
✅ Easier deletion/insertion before or after any node
Disadvantages
Uses
Round-robin scheduling
Continuous data buffering
Music/video playlists
A sorted linked list is a linked list where elements are kept in sorted order automatically after every
insertion.
Insert in order:
Insert 20 → [20]
Insert 10 → [10 → 20]
Insert 30 → [10 → 20 → 30]
Insert 25 → [10 → 20 → 25 → 30]
Implementation Example
class SortedLinkedList:
def __init__(self):
[Link] = None
current = [Link]
while [Link] and [Link] < data:
current = [Link]
new_node.next = [Link]
[Link] = new_node
def display(self):
temp = [Link]
while temp:
print([Link], end=" → ")
temp = [Link]
print("NULL")
Output Example:
Insert(20), Insert(10), Insert(30), Insert(25)
→ 10 → 20 → 25 → 30 → NULL
Advantages
Disadvantages
9. Common Mistakes
❌ Forgetting to set next = None for the last node
❌ Losing the rest of the list while inserting/deleting (not updating pointers correctly)
❌ Confusing prev and next in doubly linked lists
❌ Assuming random access like arrays — no indexing!
Searching is the process of finding the location of a specific element (key) in a collection of data (like an
array or list).
In programming, searching determines whether an element exists and, if yes, where it is located.
Example
Logic Flow
For each element in the list:
If element == target → FOUND
If end reached → NOT FOUND
Python Example
def linear_search(arr, key):
for i in range(len(arr)):
if arr[i] == key:
return i # Return index
return -1 # Not found
Output:
Found at index: 3
Visualization
Index 0 12 3 4
Array 14 3 7 19 23
Key = 19 → Check sequentially until found at index 3
Time Complexity
Case Explanation Time
Best Case Target at first position O(1)
Worst Case Target at last position or not present O(n)
Average Case Found halfway through O(n/2) ≈ O(n)
Real-World Example
✅ Advantages
❌ Disadvantages
Binary Search is a fast search algorithm that works only on sorted data (ascending or descending order).
It repeatedly divides the search space in half, eliminating one half each time.
Core Idea
Example
Array (sorted):
Python Example
def binary_search(arr, key):
low, high = 0, len(arr) - 1
Output:
Found at index: 4
Visualization
[5, 10, 15, 20, 25, 30, 35]
↑
mid=3 (20)
25 > 20 → search right → [25, 30, 35]
next mid=30 → 25 < 30 → left → [25] ✅ found
Time Complexity
Space Complexity
Real-World Examples
⚠️ 6. Common Mistakes
❌ Using binary search on unsorted arrays → incorrect results
❌ Forgetting to update low and high correctly → infinite loops
❌ Integer overflow when calculating mid = (low + high) / 2 in large data (fix: mid = low + (high -
low)//2)
❌ Assuming binary search works for all types of data (it doesn’t — only sorted comparable data)
Hashing is a technique used to store and retrieve data quickly in a structure called a hash table.
It uses a hash function to convert a data value (key) into an index (address) in the hash table.
Simple Idea
Index = hash(key)
Example
Real-World Analogy
Imagine a library:
2. Hash Function
Definition
A hash function maps a large set of possible keys into a smaller set of table indices.
123456 → 12 + 34 + 56 = 102 →
Folding Method Divide key into parts and add them 2
Example:
Keys: 42, 52
h(key) = key % 10 → both map to index 2
Since only one element can occupy an index, we must handle the collision.
5. Open Addressing
Definition
A. Linear Probing
Formula:
where i = 0, 1, 2, 3, ...
Example:
Table size = 10
Keys = 23, 33, 43
h(key) = key % 10
23 → 3
33 → 3 (collision) → try 4
43 → 3 (collision) → try 4 (taken) → try 5
Result: [3]=23, [4]=33, [5]=43
B. Quadratic Probing
Example:
h(23)=3
h(33)=3 → collision → 3+1²=4
h(43)=3 → collision → 3+2²=7
✅ Reduces clustering
❌ Still may not find a free slot if table is nearly full.
C. Double Hashing
Formula:
Example:
h1(key) = key % 10
h2(key) = 7 - (key % 7)
In chaining, each hash table slot contains a linked list (or dynamic list).
When a collision occurs, the new key is appended to the linked list at that index.
Example
h(key) = key % 5
Table:
[0]: 10 → 20 → 25 → 30
[1]: None
[2]: 12
Advantages
Disadvantages
def display(self):
for i in range([Link]):
print(f"{i}: {[Link][i]}")
7. Indexing
Definition
Indexing is a data structure technique that helps access data quickly — especially in databases and large
datasets.
An index acts like a lookup table that maps a key to the actual data location.
Example
In Databases
️ 8. Comparison Table
Open Addressing Array only Store elsewhere in table Compact Slightly slower if full
Chaining Array of linked lists Store in separate linked lists More memory Consistent
📊 9. Time Complexities
A Tree is a non-linear data structure that represents data in a hierarchical form — like a family tree or
an organization chart.
Each element of a tree is called a node, and nodes are connected by edges.
Real-Life Examples
Basic Terminology
Root → A
Parent of D and E → B
Leaf nodes → D, E, C
Height = 2
2. Properties of Trees
1. If a tree has n nodes, it has (n – 1) edges.
2. There is exactly one path between any two nodes.
3. The root node has no parent.
4. Leaf nodes have no children.
3. Binary Tree
A Binary Tree is a tree in which each node can have at most two children — usually referred to as:
Left child
Right child
Example
10
/ \
20 30
/ \
40 50
Type Description
Full Binary Tree Every node has 0 or 2 children
Complete Binary Tree All levels filled except possibly last
Perfect Binary Tree All internal nodes have 2 children and all leaves at same level
Degenerate Tree Every parent has only one child (acts like linked list)
Tree:
10
\ /
20 30
Array: [10, 20, 30]
data
left (pointer/reference)
right (pointer/reference)
class Node:
def __init__(self, value):
[Link] = value
[Link] = None
[Link] = None
5. Tree Traversals
Definition
A
/ \
B C
/ \
D E
Order: D, B, E, A, C
Pseudocode:
def inorder(node):
if node:
inorder([Link])
print([Link])
inorder([Link])
Example Use:
Used in Binary Search Trees (BST) to get elements in sorted order.
Order: A, B, D, E, C
Pseudocode:
def preorder(node):
if node:
print([Link])
preorder([Link])
preorder([Link])
Example Use:
Used to copy a tree or generate prefix expressions (used in expression trees).
Order: D, E, B, C, A
Pseudocode:
def postorder(node):
if node:
postorder([Link])
postorder([Link])
print([Link])
Example Use:
Used to delete a tree safely or generate postfix expressions.
Example
Tree:
A
/ \
B C
/ \
D E
Output: A, B, C, D, E
Implementation (Using Queue)
from collections import deque
def level_order(root):
if not root:
return
queue = deque([root])
while queue:
node = [Link]()
print([Link])
if [Link]:
[Link]([Link])
if [Link]:
[Link]([Link])
Example
50
/\
30 70
/ \ / \
20 40 60 80
9. Expression Trees
Definition
Example:
Expression: (A + B) * (C - D)
*
/ \
+ -
/ \ / \
A B C D
Preorder → * + A B - C D → Prefix
Inorder → (A + B) * (C - D) → Infix
Postorder → A B + C D - * → Postfix
Expression trees
What is an Expression Tree?
Definition
Example Expression
Expression:
(A + B) * (C - D)
Expression Tree:
*
/ \
+ -
/ \ / \
A B C D
Real-World Analogy
Think of a calculator:
2. Key Characteristics
Property Explanation
Binary Tree Every operator has at most 2 operands
Operands as Leaves Constants or variables are leaves
Operators as Internal Nodes Represent actions (e.g., +, *)
Recursion Evaluation naturally follows recursive logic
Structure Determines Order Parent nodes define operation order
Infix (human-readable)
Prefix (Polish)
Postfix (Reverse Polish)
Example Tree
*
/ \
+ -
/ \ / \
A B C D
Traversal Type Traversal Order Result Expression Type
Inorder Left → Root → Right (A + B) * (C - D) Infix
Preorder Root → Left → Right * + A B - C D Prefix
Postorder Left → Right → Root A B + C D - * Postfix
Postfix: A B + C D - *
Example:
Postfix: A B + C D - *
✅ Final Tree:
*
/ \
+ -
/ \ / \
A B C D
Prefix: * + A B - C D
Algorithm:
Algorithm (Recursive)
def evaluate(root):
if root is None:
return 0
# If leaf node → return its value
if [Link] is None and [Link] is None:
return int([Link])
# Apply operator
if [Link] == '+':
return left_val + right_val
elif [Link] == '-':
return left_val - right_val
elif [Link] == '*':
return left_val * right_val
elif [Link] == '/':
return left_val / right_val
Example
Expression Tree:
*
/ \
+ -
/ \ / \
3 2 4 1
Evaluation:
= (3 + 2) * (4 - 1)
= 5 * 3
= 15
# Traversal functions
def inorder(node):
if node:
inorder([Link])
print([Link], end=" ")
inorder([Link])
def preorder(node):
if node:
print([Link], end=" ")
preorder([Link])
preorder([Link])
def postorder(node):
if node:
postorder([Link])
postorder([Link])
print([Link], end=" ")
# Example
expr = "AB+CD-*"
root = buildTree(expr)
print("Inorder: "); inorder(root)
print("\nPreorder: "); preorder(root)
print("\nPostorder: "); postorder(root)
9. Real-World Applications
Application Explanation
Compilers Parsing arithmetic and logical expressions
Calculators Evaluate nested arithmetic
Query Processing SQL expression evaluation
Expression Evaluation in AI Used in symbolic math and logic systems
Code Generation Expression trees help compilers generate assembly code
A Binary Search Tree (BST) is a special kind of binary tree where each node follows this property:
That means:
Every node in the left subtree has a smaller value than the root.
Every node in the right subtree has a larger value than the root.
Example
50
/
\
30 70
/ \ / \
20 40 60 80
✅ Properties:
Real-World Analogy
Think of a dictionary:
2. BST Properties
Property Description
A. Search
Algorithm (Recursive)
def search(root, key):
if root is None or [Link] == key:
return root
50
/ \
30 70
/ \
60 80
Steps:
Time Complexity
Case Complexity
⚠⏱ Skewed tree = when all elements are in ascending or descending order (like a linked list).
B. Insertion
Algorithm
1. Start at root.
2. If tree is empty → new node becomes root.
3. If key < [Link] → insert in left subtree.
4. If key > [Link] → insert in right subtree.
Example
Insert 65 into:
50
/ \
30 70
/ \
60 80
Steps:
65 > 50 → right
65 < 70 → left
65 > 60 → insert right of 60
✅ New Tree:
50
/ \
30 70
/ \
60 80
\
65
Code
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
[Link] = None
C. Deletion
Example:
Delete 20
30
/
20 → just remove it
Case 2: Node has one child
30
/
20
Example:
Delete 50 from:
50
/ \
30 70
/ \
60 80
Inorder successor of 50 = 60
✅ Replace 50 with 60
✅ Delete 60 (from right subtree)
Result:
60
/ \
30 70
\
80
Code Example
def minValueNode(node):
current = node
while [Link]:
current = [Link]
return current
temp = minValueNode([Link])
[Link] = [Link]
[Link] = deleteNode([Link], [Link])
return root
Example
BST:
50
\/
30 70
/ \ / \
20 40 60 80
Traversal Order
Inorder 20 30 40 50 60 70 80
Preorder 50 30 20 40 70 60 80
Postorder 20 40 30 60 80 70 50
5. Validating a BST
Sometimes you must check if a binary tree is a BST.
Rule
Recursive Approach
def isBST(root, min_val=float('-inf'), max_val=float('inf')):
if root is None:
return True
if [Link] <= min_val or [Link] >= max_val:
return False
return (isBST([Link], min_val, [Link]) and
isBST([Link], [Link], max_val))
50
\
60
\
70
\
80
⚠️ 8. Common Mistakes
❌ Confusing BST with Binary Tree (BST has ordering rule!)
❌ Forgetting to return nodes during recursion in insert/delete
❌ Not handling duplicate keys properly
❌ Building skewed trees with sorted input
❌ Forgetting base case for recursive calls
Heaps
What is a Heap?
Definition
Max-Heap → Every parent node has a value greater than or equal to its children.
Min-Heap → Every parent node has a value less than or equal to its children.
Example
Max-Heap
50
/\
30 40
/ \ / \
10 20 35 25
✅ Parent ≥ Children
(50 > 30, 40; 30 > 10, 20; etc.)
Min-Heap
10
/\
20 15
/ \ / \
30 40 25 35
✅ Parent ≤ Children
Heap Characteristics
Property Explanation
Complete Binary Tree All levels filled except possibly the last (filled left to right).
Heap Property For Max-Heap or Min-Heap, parent-child relationship follows the rule.
Not a BST! The ordering is local (only between parent and child), not global like in BST.
Left child → 2i + 1
Right child → 2i + 2
Parent → (i - 1) // 2
Example
Heap Tree:
50
/\
30 40
/ \ / \
10 20 35 25
Array Representation:
3. Types of Heaps
Type Heap Property Use Case
Insertion
Deletion (Extract Root)
Heapify
Build Heap
Peek (Get Max/Min)
Steps
Example (Max-Heap)
Steps
Example (Max-Heap)
if largest != index:
heap[index], heap[largest] = heap[largest], heap[index]
heapify_down(heap, largest, size)
def extract_max(heap):
size = len(heap)
if size == 0:
return None
root = heap[0]
heap[0] = heap[-1]
[Link]()
heapify_down(heap, 0, len(heap))
return root
C. Heapify Operation
Algorithm
D. Build Heap
Steps
Example
✅ Sorted Output
🏗️ 6. Applications of Heaps
Application Description
Median Finding Two heaps (max and min) used to find median in streams
️ 7. Real-World Examples
Scenario Heap Type Used Explanation
Streaming Median Both Max-Heap for lower half, Min-Heap for upper half
⚠️ 8. Common Mistakes
❌ Confusing heap property with BST ordering
❌ Forgetting to heapify after insert/delete
❌ Using O(n log n) for Build Heap (it’s O(n))
❌ Mixing up parent/child index formulas
❌ Trying to perform traversal-based sorting (heap only guarantees root order)
M-way trees
What is an M-Way Tree?
Simple Definition
An M-way tree is a tree data structure in which each node can have up to M children.
It’s a generalization of a binary tree (which is a 2-way tree).
Formal Definition
Visual Example (M = 4)
[20 | 40 | 60]
/ | | \
<20 20–40 40–60 >60
✅ Here:
Analogy
Node Example (M = 4)
P0 | P1 | P2 | P3
Meaning:
P0 → values < 20
P1 → values between 20–40
P2 → values between 40–60
P3 → values > 60
4. Properties of M-Way Trees
Property Description
Degree (M) Max number of children a node can have
Keys per node Up to M – 1 keys
Minimum keys Varies by implementation (e.g., balanced trees may have rules)
Search property Keys in each node are sorted; subtrees follow range rules
Height Decreases as M increases (faster access)
Example
[20 | 40 | 60]
/ | | \
A B C D
Code-Like Pseudocode
def search(node, key):
if node is None:
return False
i = 0
while i < len([Link]) and key > [Link][i]:
i += 1
if i < len([Link]) and key == [Link][i]:
return True
return search([Link][i], key)
Insert 50 into:
[20 | 40 | 60]
Result:
[40 | 60]
/ | \
[20] [50] [>60]
for i = 0 to n-1:
traverse(child[i])
print(key[i])
traverse(child[n])
Example
📊 9. Time Complexity
Operation Average Time Explanation
Search O(logₘ n) Fewer levels than binary tree
Insert O(logₘ n) Depends on height
Delete O(logₘ n) Similar to insertion
Space O(n) Stores all elements
✅ As M increases, height decreases, improving performance.
Height Formula
Height ≈ logₘ(n)
Balanced trees
What is a Balanced Tree?
Definition
A balanced tree is a tree data structure in which the height difference between subtrees is kept small to
ensure that operations like search, insertion, and deletion remain efficient (O(log n)).
In simpler terms:
A balanced tree keeps its branches evenly distributed so no side of the tree grows too tall.
Example
✅ Balanced Tree
3
/ \
2 4
/
1
✅ Balanced trees guarantee performance stability — crucial for real-world systems like file systems and
databases.
Example
10
/ \
5 15
/
2
Node Left Height Right Height Balance Factor Balanced?
10 2 1 +1 ✅
5 1 0 +1 ✅
15 0 0 0 ✅
Types of Rotations
Before:
30
/
20
/
10
After:
20
/ \
10 30
✅ Balanced restored.
1. Start at root.
2. Compare key.
3. Go left or right depending on value.
4. Because the tree is balanced, height = O(log n), so search time = O(log n).
Multi-way (M-way)
B-Tree — Databases, file systems
balanced tree
Self-adjusting based on
Splay Tree — Caches, adaptive search
recent access
Unbalanced → Right-heavy.
✅ Now Balanced.
Height≈logm(n)\text{Height} ≈ \log_m(n)Height≈logm(n)
Height≈log2(n)\text{Height} ≈ \log_2(n)Height≈log2(n)
AVL Tree Height difference (balance factor) Very strict Fast lookups
👨💻 Invented by:
Definition
An AVL Tree is a binary search tree (BST) where the difference in height between the left and right
subtrees of any node (called the balance factor) is at most 1.
Left-Left (LL) Right Rotation Insertion in left subtree of left child Right Rotate
Right-Right (RR) Left Rotation Insertion in right subtree of right child Left Rotate
Left-Right (LR) Double Rotation (Left then Right) Left subtree’s right child Left + Right
Right-Left (RL) Double Rotation (Right then Left) Right subtree’s left child Right + Left
️ Example: LL Rotation
Before Rotation:
30
/
20
/
10
After Right Rotation:
20
/ \
10 30
️ Example: LR Rotation
Before:
30
/
10
\
20
After:
20
/ \
10 30
✅ Balanced
️ Time Complexities
Operation Time (Balanced)
Search O(log n)
Insertion O(log n)
Deletion O(log n)
💡 Applications
Databases requiring frequent lookups
Memory indexing systems
Compiler symbol tables
⚠️ Common Mistakes
Forgetting to update height/balance factor after rotations
Misidentifying rotation cases (LL vs LR, RR vs RL)
Thinking AVL = BST (AVL is a type of BST)
🌳 3. Red-Black Trees
👨💻 Invented by:
⚙️ Definition
A Red-Black Tree is a binary search tree that maintains balance using color properties (each node is
either red or black) rather than strict height balance.
📏 Height Property
If the tree has n nodes, the height is always ≤ 2 * log₂(n + 1)
➡⏱ Ensures O(log n) performance (not as strictly balanced as AVL).
🔁 Balancing Operations
When an insertion or deletion breaks the Red-Black rules, it’s fixed using:
After balancing:
20(B)
/ \
10(R) 30(R)
⚙️ Rotations Used
Left Rotation
Right Rotation
Left-Right
Right-Left
Same concept as AVL, but triggered by color violations instead of height imbalance.
️ Time Complexities
Operation Time
Search O(log n)
Insertion O(log n)
Deletion O(log n)
Like Strict teacher (perfect balance) Chill teacher (just enough balance)
Concept AVL Red-Black
📚 Applications
Red-Black Trees
o C++ STL (map, set)
o Java Collections (TreeMap, TreeSet)
o Linux kernel scheduler
o Symbol tables, compilers
AVL Trees
o Memory-intensive databases
o Real-time lookups where search speed is critical
Graphs
What is a Graph?
Definition
Example
Visual:
A --- B
| |
D --- C
🌿 2. Types of Graphs
Type Description Example Use Case
Directed Graph (Digraph) Edges have direction (A→B) Social media follower graph
Undirected Graph Edges have no direction Friendship network
Weighted Graph Edges have weights/costs Road maps, shortest path
Unweighted Graph Edges have no weights Basic connectivity
Cyclic Graph Contains a cycle Circuit networks
Type Description Example Use Case
Acyclic Graph No cycles Task scheduling (DAG)
Connected Graph There is a path between every pair of vertices Road network
Disconnected Graph Not all vertices are reachable Isolated sub-networks
3. Graph Representations
A. Adjacency Matrix
A B C
A0 1 0
B1 0 1
C0 1 0
B. Adjacency List
Example
A -> B
B -> A, C
C -> B
C. Edge List
4. Graph Terminology
Term Definition
Vertex (Node) Fundamental unit (point)
Edge Connection between two vertices
Degree Number of edges connected to a vertex
In-degree Number of incoming edges (directed graph)
Out-degree Number of outgoing edges (directed graph)
Path Sequence of vertices connected by edges
Cycle Path where first = last vertex
Connected Component Subgraph where any vertex is reachable from any other
Term Definition
Weighted Edge Edge with a numeric value (cost, distance)
Algorithm Steps
Algorithm Steps
Example
DFS starting at A: A → B → C → D (depending on neighbor order)
🌿 7. Special Graphs
Directed Acyclic Graph (DAG)
o No cycles
o Used in task scheduling, course prerequisites
o Topological sorting applies
Complete Graph
o Every vertex connected to every other vertex
o Number of edges = n(n-1)/2 (undirected)
Sparse Graph
o Few edges relative to vertices
o Use adjacency list
Dense Graph
o Many edges
o Use adjacency matrix
📊 9. Complexity Analysis
Operation Adjacency Matrix Adjacency List
Add Edge O(1) O(1)
Remove Edge O(1) O(degree(v))
Check Edge O(1) O(degree(v))
BFS/DFS O(V²) O(V + E)
BFS explores a graph level by level, visiting all vertices at distance k from the start node before visiting
vertices at distance k+1.
Pseudocode
def BFS(graph, start):
visited = set()
queue = [start]
[Link](start)
while queue:
v = [Link](0)
print(v, end=" ")
for neighbor in graph[v]:
if neighbor not in visited:
[Link](neighbor)
[Link](neighbor)
Example
Graph:
A
/ \
B C
/ \ \
D E F
BFS starting at A:
A → B → C → D → E → F
Complexity
Time: O(V + E) → Each vertex and edge visited once
Space: O(V) → Queue + visited list
Applications
Pseudocode
def DFS(graph, v, visited=set()):
[Link](v)
print(v, end=" ")
for neighbor in graph[v]:
if neighbor not in visited:
DFS(graph, neighbor, visited)
Example
Graph:
A
/ \
B C
/ \ \
D E F
while stack:
v = [Link]()
if v not in visited:
[Link](v)
print(v, end=" ")
for neighbor in reversed(graph[v]):
if neighbor not in visited:
[Link](neighbor)
Complexity
Applications
Pathfinding in mazes
Detect cycles in graphs
Topological sorting (DAGs)
Strongly connected components (Kosaraju’s/ Tarjan’s algorithm)
Visualization
Graph:
A
/ \
B C
/ \ \
D E F
BFS DFS
ABCDEFABDECF
BFS = explores neighbors first, DFS = explores one path to the end first
⚠️ 5. Common Mistakes
Forgetting to mark vertices as visited → infinite loop
Confusing BFS & DFS traversal order
Using DFS for shortest path in unweighted graph (BFS should be used instead)
Incorrect stack/queue implementation in iterative versions
Topological order
What is Topological Order?
Definition
A topological order of a Directed Acyclic Graph (DAG) is a linear ordering of vertices such that:
For every directed edge u→vu → vu→v, vertex u comes before vertex v in the ordering.
Real-World Example
Course Scheduling:
o Courses A → B → C
o A must be taken before B, B before C
o Topological order: A → B → C
Build Systems / Compilation Order:
o Modules with dependencies must be compiled in order.
2. Conditions
1. Graph must be Directed
2. Graph must be Acyclic (no cycles)
DFS Pseudocode
def topoDFS(graph):
visited = set()
stack = []
def dfs(v):
[Link](v)
for neighbor in graph[v]:
if neighbor not in visited:
dfs(neighbor)
[Link](v) # Add after exploring neighbors
Example
Graph:
5 → 0 ← 4
|
↓
2
|
↓
3 → 1
def topoKahn(graph):
in_degree = {v: 0 for v in graph}
for v in graph:
for neighbor in graph[v]:
in_degree[neighbor] += 1
while queue:
v = [Link]()
topo_order.append(v)
for neighbor in graph[v]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
[Link](neighbor)
if len(topo_order) == len(graph):
return topo_order
else:
return "Graph has a cycle!"
4. Complexity
Algorithm Time Complexity Space Complexity
DFS-based O(V + E) O(V)
Kahn’s BFS O(V + E) O(V)
6. Common Mistakes
Applying topological sort on graphs with cycles
Confusing DFS post-order with topological order
Not updating in-degrees correctly in Kahn’s algorithm
Assuming unique order — multiple valid topological orders exist
Shortest path
What is Shortest Path?
Definition:
The shortest path between two vertices in a graph is the path with minimum total weight or minimum
number of edges (if unweighted) connecting them.
Applications:
Steps:
1. Start from the source vertex
2. Mark vertices as visited and track distance from source
3. Enqueue neighbors
4. First time reaching a vertex → shortest path
Example:
Graph:
A - B - D
| |
C - E
BFS from A:
Distance to B = 1
Distance to C = 1
Distance to D = 2
Distance to E = 2
Complexity:
Time = O(V + E)
Space = O(V)
Definition:
Finds shortest paths from a single source to all vertices in a weighted graph with non-negative weights.
Steps:
Example:
A - B(4)
A - C(2)
B - C(5)
B - D(10)
C - D(3)
Dijkstra from A:
Vertex Distance
A 0
B 4
C 2
D 5 (via C → D)
Time Complexity:
Definition:
Finds single-source shortest path, works even if some edges have negative weights, but no negative
cycles.
Steps:
Use Case: Graphs with negative edge weights, e.g., financial networks
Steps:
⚠️ 5. Common Mistakes
Using Dijkstra with negative weights → incorrect results
Forgetting to relax all edges in Bellman-Ford
Not initializing distance[source] = 0
BFS only works for unweighted graphs
🌿 2. Adjacency Matrix
Definition
Example
Graph:
A → B
B → C
C → A
Vertices: {A, B, C}
Adjacency Matrix:
ABC
A0 1 0
ABC
B0 0 1
C1 0 0
Properties / Complexity
Operation Complexity
Check if edge exists (i → j) O(1)
Add edge O(1)
Remove edge O(1)
Iterate neighbors O(V)
Space O(V²) → Can be large for sparse graphs
✅ Efficient for dense graphs but memory-heavy for large sparse graphs.
Advantages
Disadvantages
🌿 3. Adjacency List
Definition
An Adjacency List is an array or list of lists, where each vertex stores a list of its neighbors.
Example
Graph:
A → B, C
B → C
C → A
Adjacency List:
A -> [B, C]
B -> [C]
C -> [A]
Each vertex has a list of connected vertices
For weighted graphs, store tuples: (neighbor, weight)
Properties / Complexity
Operation Complexity
Check if edge exists (i → j) O(degree(i))
Add edge O(1)
Remove edge O(degree(i))
Iterate neighbors O(degree(i))
Space O(V + E) → Efficient for sparse graphs
Advantages
Disadvantages
Dynamic programming
What is Dynamic Programming?
Definition:
Dynamic Programming is a method for solving complex problems by breaking them into simpler
subproblems and storing the results of subproblems to avoid redundant computations.
Works when the problem has:
1. Overlapping Subproblems → Same subproblem occurs multiple times
2. Optimal Substructure → Optimal solution can be built from optimal solutions of
subproblems
Real-World Analogy
🌿 2. Key Concepts
1. Memoization (Top-Down)
o Recursive approach
o Store results in a table (array/dictionary) to avoid recalculation
2. Tabulation (Bottom-Up)
o Iterative approach
o Solve small subproblems first, then build up solution for larger problem
3. State
o Variables defining subproblem (e.g., n in Fibonacci, i,j in grid)
4. Transition/Recurrence Relation
o Formula to compute subproblem from smaller subproblems
⚡ 3. Common DP Problems
A. Fibonacci Numbers
def fib(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]
Problem:
Items with weights and values
Maximize value in knapsack with capacity W
DP Approach:
Recurrence:
dp[i][w]=max(dp[i−1][w],dp[i−1][w−weight[i]]+value[i])
Complexity: O(n*W)
Example:
DP Table:
dp[i][j]={0i=0 or j=0dp[i−1][j−1]+1X[i−1]=Y[j−1]max(dp[i−1][j],dp[i][j−1])X[i−1]≠Y[j−1]dp[i][j] =
\begin{cases} 0 & i=0 \text{ or } j=0 \\ dp[i-1][j-1] + 1 & X[i-1] = Y[j-1] \\ \max(dp[i-1][j], dp[i][j-1]) &
X[i-1] \neq Y[j-1] \end{cases}dp[i][j]=⎩⎨⎧0dp[i−1][j−1]+1max(dp[i−1][j],dp[i][j−1])
i=0 or j−1]
⚡ 5. Complexity Analysis
Approach Time Complexity Space Complexity
Top-Down (Memoization) Number of subproblems Number of subproblems (table)
Bottom-Up (Tabulation) Same as top-down Table size (can be optimized)
💡 6. Common DP Mistakes
Forgetting base cases
Not identifying overlapping subproblems
Using recursion without memoization → exponential time
Confusing subset sum / knapsack / LCS recurrence formulas
Not optimizing space when possible (1D array instead of 2D)
Greedy algorithms
What Are Greedy Algorithms?
Simple Explanation
A greedy algorithm builds up a solution step by step, always choosing the best immediate (local) option
at each stage — without reconsidering previous choices.
It assumes that by choosing the local optimum, the overall (global) optimum will be reached.
Formal Definition
A greedy algorithm is one that selects the locally optimal choice at each step with the hope that these local
choices lead to a globally optimal solution.
Real-Life Analogy
Imagine you are trying to get change for ₹10 using the least number of coins.
You pick the largest coin possible each time (₹5 → ₹2 → ₹2 → ₹1).
This greedy choice works perfectly when the currency system allows it.
2. Key Characteristics
Feature Description
Greedy Choice Property Local choice leads to global solution
Optimal Substructure Solution to the problem can be built using optimal solutions to subproblems
No Backtracking Once a choice is made, it’s never changed
Efficiency Often runs faster than dynamic programming
When It Works
✅ If either condition fails → greedy may not give the correct answer.
Goal:
Select the maximum number of activities that don’t overlap.
Example:
Algorithm Steps:
def activity_selection(activities):
[Link](key=lambda x: x[1]) # sort by finish time
selected = [activities[0]]
last_finish = activities[0][1]
for i in range(1, len(activities)):
if activities[i][0] >= last_finish:
[Link](activities[i])
last_finish = activities[i][1]
return selected
Complexity:
Goal:
Maximize value of items in a knapsack that can hold fractional parts.
Given:
Algorithm Steps:
Code Example:
Goal:
Compress data using variable-length binary codes.
Characters with higher frequency get shorter codes.
Steps:
1. Create a min-heap of characters by frequency
2. Pick two smallest, merge into one node
3. Repeat until one tree remains
Example:
Char Freq
A 5
B 9
C 12
D 13
E 16
F 45
Goal:
Find MST (Minimum Spanning Tree) with minimum edge cost.
Steps:
Goal:
Build MST by expanding from a single vertex.
Steps:
Steps:
️ 6. Common Mistakes
Assuming greedy always gives optimal result
Forgetting to check for greedy choice property
Not sorting input correctly before applying greedy logic
Confusing Fractional Knapsack (greedy) with 0/1 Knapsack (DP)
Ignoring tie-breaking conditions
Backtracking
What is Backtracking?
Definition:
Backtracking is a systematic method of exploring all possible solutions by:
Key Idea
Navigating a maze:
o Move forward until you hit a wall → backtrack → try another path
2. Characteristics of Backtracking
Feature Description
Recursive / Tree-based Often implemented with recursion
Exhaustive Search Tries all possibilities (prunes invalid paths)
Solution Space Tree of all possible partial solutions
Constraint Checking Prune paths that violate constraints
Optimality Can find all solutions or first valid solution
Problem: Place N queens on an N×N chessboard such that no two queens attack each other.
Algorithm (Step-by-Step):
Solution 1:
. Q . .
. . . Q
Q . . .
. . Q .
Solution 2:
. . Q .
Q . . .
. . . Q
. Q . .
B. Sudoku Solver
Problem: Fill a 9×9 Sudoku grid satisfying row, column, and 3×3 box constraints.
Algorithm:
Algorithm:
Example:
Set = [2, 3, 5], target = 5
D. Maze Solving
Problem: Find path from start to end in a maze (grid with walls).
Algorithm:
⚡ 6. Complexity
Time complexity depends on the number of possible solutions:
o Worst-case = O(branch^depth)
o Example: N-Queens → O(N!)
Space complexity = O(depth of recursion)
Amortized analysis
What is Amortized Analysis?
Definition:
Amortized analysis calculates the average time per operation over a sequence of operations, ensuring that
the occasional expensive operation does not make the average cost too high.
Unlike average-case analysis (which assumes random inputs), amortized analysis guarantees the average
cost over worst-case sequences.
Operation Cost
Append 1 1
Append 2 1
Append 3 3 (array doubles)
Append 4 1
Append 5 5 (array doubles)
C. Potential Method
Formula:
Append operation:
o Most of the time O(1)
o Occasionally O(n) when array doubles
Amortized cost per append = O(1) using aggregate / accounting method
5. Key Insights
Amortized cost = total cost of operations / number of operations
Helps analyze dynamic arrays, stacks with multipop, binary counters, splay trees
Average-case analysis vs amortized analysis:
o Average-case = assumes input distribution
o Amortized = guarantees average cost for any input sequence
Key Terms
Spanning Tree: Connects all vertices without forming cycles
Edge Weight: Cost, distance, or value associated with each edge
Graph Type: Weighted, undirected, connected
Example:
Graph:
Vertices: {A, B, C, D}
Edges: A-B(1), B-C(4), A-C(3), C-D(2), B-D(5)
Applications
⚡ 2. Properties of MST
1. Number of edges: n−1 (where n = number of vertices)
2. Acyclic: No cycles
3. Connected: Every vertex is reachable
4. Greedy choice works: Local optimum edges lead to global MST
5. Cut Property: For any cut, minimum-weight edge across cut belongs to some MST
6. Cycle Property: Maximum-weight edge in a cycle cannot be in MST
Idea:
Add edges in increasing order of weight while avoiding cycles.
Steps:
Example:
Edge Weight
A-B 1
C-D 2
A-C 3
B-C 4
B-D 5
MST edges = A-B, C-D, A-C → total weight = 6
Complexity:
Idea:
Grow MST starting from any vertex, always adding smallest edge connecting MST to a new vertex.
Steps:
Example:
Start A → choose edge A-B(1) → add A-C(3) → add C-D(2) → MST complete
Complexity:
Vertices: A, B, C, D, E
Edges & weights:
A-B(2), A-C(3), B-C(1), B-D(4), C-D(5), C-E(6), D-E(7)
Kruskal’s MST:
1. MST = {A}
2. Minimum edge from MST: A-B(2) → add B
3. Minimum edge from MST: B-C(1) → add C
4. Minimum edge: B-D(4) → add D
5. Minimum edge: C-E(6) → add E
5. Complexity Summary
Algorithm Time Complexity Space Complexity Best For
Kruskal O(E log E) O(V) Sparse graphs
Prim (Matrix) O(V²) O(V²) Dense graphs
Prim (Heap + List) O(E log V) O(V+E) Sparse graphs
Types of Correctness
1. Partial Correctness
o If the algorithm terminates, the output is correct
o Does not guarantee termination
2. Total Correctness
o Algorithm terminates and produces correct output
Proving Correctness
A. Loop Invariants
Loop invariant: “Subarray arr[0…i-1] contains the i smallest elements in sorted order”
Holds before/after each iteration → proves algorithm correctness
B. Induction
⚡ 2. Complexity Classes
Definition:
Complexity classes group problems based on resources needed (time or space) to solve them.
Time complexity: How running time grows with input size (n)
Space complexity: How memory usage grows with input size
O(1) – Constant Time does not depend on input size Access array element, push to stack
1. Correctness:
o Loop invariant: “Target x, if exists, lies within subarray arr[low…high]”
o Maintained each iteration → guarantees partial correctness
o Loop terminates → total correctness
2. Complexity:
o Worst-case comparisons: O(log n)
o Space: O(1) (iterative), O(log n) (recursive)
Selection Sort
1. Correctness:
o Loop invariant: smallest elements placed correctly at each iteration
o Proves algorithm sorts entire array
2. Complexity:
o Time: O(n²) worst, best, average
o Space: O(1)
4. Key Insights
Correctness ensures reliability → algorithm produces intended results
Complexity measures efficiency → how resources grow with input
Proving correctness often uses loop invariants, induction, recursion proofs
Classifying complexity helps compare algorithms and select best approach