Algorithms
Algorithms
•Why it Matters in AI
• Algorithms define how models learn, optimize, and generalize
• Underpin efficiency, accuracy, and fairness
•Example:
Sorting a list, finding the shortest path, or performing calculations.
Real-Life Analogy
•Example: A recipe
• Ingredients = Input
• Instructions = Steps of the algorithm
• Final Dish = Output
Time and Space Complexity
•Time Complexity: How much time an algorithm takes based on the
input size.
•Space Complexity: How much memory an algorithm uses during
execution.
•Goal: Optimize for both, though sometimes there's a trade-off.
Practical Tips
•Always analyze the algorithm before implementation.
•Consider edge cases and scalability.
•Choose the right algorithm based on your problem and data size.
Conclusion
•Algorithms are the core of problem-solving in computer science.
•Time & Space Complexity help assess efficiency.
•Big O Notation is a key tool for comparing algorithms.
Why Algorithms Are Critical for
Understanding the Performance of AI
Models and Training Pipelines
Unlocking the Logic Behind Intelligent Systems
The Foundation of AI Models
•Algorithms define:
• How data is processed
• How models learn patterns (e.g., backpropagation in neural
networks)
•Without algorithms, there is no structured way to train models
Real-World Example
•Case Study: Image classification with CNNs
• Algorithm: Convolutional layers, ReLU, softmax
• Optimizer: SGD vs. Adam
• Impact: Faster convergence, better accuracy with Adam
Ethical and Responsible AI
•Algorithms impact:
• Fairness
• Bias
• Interpretability
•Transparent algorithmic choices are key for trust
Summary
•Algorithms are:
• Essential for model design, training, evaluation
• Crucial for debugging and improving pipelines
• Central to responsible and high-performance AI
Analyzing the Complexity of Simple
Search and Sort Algorithms
Understanding Time and Space Efficiency
What is Algorithm Complexity?
•Time Complexity: Measures how execution time grows with input size.
•Space Complexity: Measures how memory usage grows with input
size.
•Big-O Notation: Used to express upper bounds of an algorithm’s
growth.
Big-O Notation Examples
Binary Search
•Description: Searches sorted array by repeatedly dividing in half.
•Time Complexity: O(log n)
•Space Complexity: O(1) for iterative, O(log n) for recursive
•Limitation: Works only on sorted arrays.
Bubble Sort
•Description: Repeatedly swaps adjacent elements if they are in the
wrong order.
•Time Complexity:
• Best Case (sorted): O(n)
• Average/Worst Case: O(n²)
•Space Complexity: O(1)
Selection Sort
•Description: Selects the smallest element and places it in order.
•Time Complexity: O(n²) in all cases
•Space Complexity: O(1)
•Stability: Not stable
Insertion Sort
•Description: Builds sorted array one item at a time.
•Time Complexity:
• Best Case (nearly sorted): O(n)
• Average/Worst: O(n²)
•Space Complexity: O(1)
•Stability: Stable
Key Takeaways
•Choose wisely: Simpler algorithms may be fine for small data
sets.
•Understand trade-offs: Time vs. space, best vs. worst case.
•Binary search is faster but requires sorted input.
•Sorting complexity greatly impacts large datasets.
Comparison Table
Linear
O(1) O(n) O(n) O(1) Yes
Search
Binary
O(1) O(log n) O(log n) O(1) N/A
Search
Bubble
O(n) O(n²) O(n²) O(1) Yes
Sort
Selection
O(n²) O(n²) O(n²) O(1) No
Sort
Insertion
O(n) O(n²) O(n²) O(1) Yes
Sort
Understanding
Recursion
Basics, Divide-and-Conquer Approach, and Recursion vs Iteration
What is Recursion?
Definition:
Recursion is a programming technique where a function calls
itself to solve a problem.
Key Features:
•Problem broken into smaller subproblems
•Each recursive call has its own execution context
•Base case and recursive case required
def factorial(n):
if n == 0: # Base case
return 1
else:
return n * factorial(n - 1) # Recursive case
Components:
•Base Case: Condition to stop recursion
•Recursive Case: Function calls itself
Types of Recursion
•Direct Recursion – Function calls itself directly
•Indirect Recursion – Function A calls B, which calls A
•Tail Recursion – Recursive call is the last operation
•Non-Tail Recursion – Further computation after recursion call
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
return arr
Visual Flow:
Array → Divide → Sort Left & Right → Merge
Recursion vs Iteration – Conceptual Comparison
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
Iterative:
def factorial_iter(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
When to Use Recursion vs Iteration
Use Recursion When:
•Problem is naturally recursive (e.g., trees)
•Readability and maintainability matter
•Divide-and-conquer is suitable
Use Iteration When:
•Performance is critical
•Stack space is a concern
•Simple repetitive tasks
Summary
•Recursion simplifies problem-solving using self-calls
•Divide-and-conquer is a powerful recursive paradigm
•Iteration is generally faster and more memory efficient
•Choose based on problem structure and performance needs
The Key to Understanding
Recursive Model Structures
Exploring Recursive Logic in Decision Trees
What Are Recursive Model Structures?
Definition:
Recursive models break down problems into smaller parts by
applying the same logic repeatedly at each level of a structure.
Common Use Cases:
•Decision Trees
•Recursive Neural Networks
•Hierarchical Models
Core Idea:
The same model structure is applied at each level of the hierarchy.
Real-World Analogy
Example:
"20 Questions" game — Ask yes/no questions recursively to
narrow down the answer.
Key Characteristics:
•Start from a general question
•Each answer leads to a more specific question
•Process repeats until a conclusion is reached
Decision Trees as Recursive Models
What is a Decision Tree?
A tree-like model used for classification and regression.
Recursive Nature:
•Each node represents a decision rule
•Each branch represents the outcome
•Each leaf node represents a final output
•Tree construction is recursive: split → apply rule →
repeat
Example of a Decision Tree
Sample Problem: Classify whether someone will play tennis
Outlook?
/ \
Sunny Overcast
/ \ \
Humidity? Play
/ \
High Normal
No Yes
Recursive Flow:
•Check "Outlook"
•If "Sunny", check "Humidity"
•Each decision rule leads to another rule or final decision
Building a Decision Tree – Recursive Algorithm
Common Algorithm: ID3, CART, C4.5
Steps (Recursively Applied):
1.Check for a stopping condition (e.g., all data has same label)
2.Select the best feature to split on (e.g., based on Information
Gain)
3.Split dataset based on the selected feature
4.Recursively apply the above steps to each subset
def build_tree(data):
if stopping_condition(data):
return LeafNode()
best_feature = choose_best_feature(data)
branches = {}
for value in unique_values(data[best_feature]):
subset = filter_data(data, best_feature, value)
branches[value] = build_tree(subset)
return DecisionNode(feature=best_feature,
branches=branches)
Why Recursion Works Well for Trees
•Tree structures are inherently recursive
•Natural way to divide data at each node
•Each subtree is an independent problem
•Simplifies model building and prediction logic
Recursion in Prediction
Prediction Algorithm:
•Start at root node
•Test the input feature
•Follow the branch matching the input value
•Recurse down until reaching a leaf
Example:
def predict(node, input):
if node is LeafNode:
return node.label
value = input[node.feature]
return
predict(node.branches[value], input)
Limitations:
•Can overfit (if tree is too deep)
•Recursive depth can be computationally expensive
•Need pruning techniques
Implementing Fibonacci with
Recursion and Memoization
From Naive to Efficient Solutions
What is the Fibonacci Sequence?
Definition:
A series of numbers where each number is the sum of
the two preceding ones.
Formula:
F(n) = F(n - 1) + F(n - 2)
Base Cases:
F(0) = 0, F(1) = 1
Example:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
Recursive Implementation
(Naive)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
Pros:
•Simple and elegant
•Directly follows the mathematical definition
Cons:
•Exponential time complexity: O(2^n)
•Many redundant calculations
Problem with Naive Recursion
Example: fib(5)
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
...
Redundant Calls:
•fib(3) is calculated twice
•fib(2) is calculated three times
Introducing Memoization
Definition:
An optimization technique to store results of expensive
function calls and reuse them.
Approach:
•Store intermediate results in a dictionary or array
•Check before computing
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
Advantages:
•Cleaner code
•Automatic caching
•Ideal for Python 3.2+
Bottom-Up Dynamic Programming
(Tabulation)
def fib_dp(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]
Pros:
•No recursion
•Time: O(n), Space: O(n)
Optimized Bottom-Up (Constant
Space)
def fib_optimized(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
Space Complexity:
•Reduced to O(1)
•Ideal for large inputs
Performance Comparison
Space
Method Time Complexity Notes
Complexity
Recursion + Great
O(n) O(n)
Memo improvement
Pythonic,
LRU Cache O(n) O(n)
concise
Bottom-Up DP O(n) O(n) No recursion
Optimized Most efficient for
O(n) O(1)
Iterative large n
def quick_sort(arr):
if len(arr) <= 1:
return arr # Base case
pivot = arr[-1]
left = [x for x in arr[:-1] if x <= pivot]
right = [x for x in arr[:-1] if x > pivot]
return quick_sort(left) + [pivot] + quick_sort(right)
# Test
arr = [7, 2, 1, 6, 8, 5, 3, 4]
sorted_arr = quick_sort(arr)
print("Sorted array:", sorted_arr)
Output:
Key Properties:
•Stable sort (doesn’t change the relative order of
equal elements)
•Not in-place (requires extra space)
•Good for sorting linked lists or large datasets that
don't fit in memory
sort the array:[38, 27, 43, 3, 9, 82, 10]
Step 1: Divide Phase (Recursive
Splitting)
We keep dividing the array until we reach
subarrays of size 1.
[38, 27, 43, 3, 9, 82, 10]
↓
Left: [38, 27, 43] Right: [3, 9, 82, 10]
↓ ↓
[38] [27, 43] [3, 9] [82, 10]
↓ ↓ ↓ ↓
[27] [43] [3] [9] [82] [10]
Step 2: Merge Phase (Combine Back
Sorted)
Now, we start merging the sorted parts back
together.
Merge [27] and [43]:
[27] + [43] → [27, 43]
Merge [38] and [27, 43]:
Compare 38 with 27 → 27 is smaller
Compare 38 with 43 → 38 is smaller
Only 43 remains
Merge step-by-step:
1.Compare 27 with 3 → 3
2.Compare 27 with 9 → 9
3.Compare 27 with 10 → 10
4.Compare 27 with 82 → 27
5.Compare 38 with 82 → 38
6.Compare 43 with 82 → 43
7.Only 82 remains
[27, 38, 43] + [3, 9, 10, 82] → [3, 9, 10, 27, 38, 43,
82]
Final Sorted Array:
Comparison
Element at
Step Index with Target Result
Index
(3)
Keep
1 0 5 5≠3
searching
Keep
2 1 8 8≠3
searching
Keep
3 2 12 12 ≠ 3
searching
Found at
4 3 3 ✅3=3
index 3
Final Result:
The algorithm stops at index 3 because that’s
where the target (3) is located.
If Target Not Found:
Suppose the target was 7 instead. Here's what would happen:
Element at Comparison
Step Index Result
Index with 7
Keep
1 0 5 5≠7
searching
Keep
2 1 8 8≠7
searching
Keep
3 2 12 12 ≠ 7
searching
Keep
4 3 3 3≠7
searching
Keep
5 4 9 9≠7
searching
❌ Return -1
6 - End of list Not found or "Not
Found"
Python Code: Linear Search
def linear_search(arr, target):
# Step 1: Loop through the array
for index in range(len(arr)):
# Step 2: Compare current element with target
if arr[index] == target:
# Step 3: If match is found, return the index
return index
# Step 4: If not found, return -1
return -1
# Example usage:
numbers = [5, 8, 12, 3, 9]
target = 3
target = 7
Output:
if arr[mid] == target:
return mid # Found the target
elif arr[mid] < target:
low = mid + 1 # Search right half
else:
high = mid - 1 # Search left half
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 Usage
# Iterative
index_iter = binary_search_iterative(arr, target)
print(f"Iterative: Found at index {index_iter}" if index_iter != -1 else "Not
found")
# Recursive
index_rec = binary_search_recursive(arr, target, 0, len(arr) - 1)
print(f"Recursive: Found at index {index_rec}" if index_rec != -1 else "Not
found")
Iterative Binary Search with Tracing
def binary_search_iterative(arr, target):
low = 0
high = len(arr) - 1
if arr[mid] == target:
print(f"✅ Found {target} at index {mid}")
return mid
elif arr[mid] < target:
print(f"{target} > {arr[mid]} → Searching right half")
low = mid + 1
else:
print(f"{target} < {arr[mid]} → Searching left half")
high = mid - 1
if arr[mid] == target:
print(f"✅ Found {target} at index {mid}")
return mid
elif arr[mid] < target:
print(f"{target} > {arr[mid]} → Recursing into right half")
return binary_search_recursive(arr, target, mid + 1, high)
else:
print(f"{target} < {arr[mid]} → Recursing into left half")
return binary_search_recursive(arr, target, low, mid - 1)
Example Usage
Feature Engineering
•Interaction terms: Multiply or combine features (f1 * f2) to capture interactions.
•Polynomial features: Use PolynomialFeatures for non-linear relationships.
•Dimensionality reduction:
•PCA: Principal Component Analysis.
•UMAP / t-SNE: For visualization or unsupervised learning.
omation with Pipelines
sklearn.pipeline.Pipeline or sklearn.compose.ColumnTransformer to:
Automate and reproduce preprocessing steps.
Avoid data leakage.
Streamline training and cross-validation.
Task Tools/Libraries
pandas, NumPy, Polars, Dask,
General Processing
Modin
sklearn, Feature-engineering
Feature Engineering
packages
Scaling sklearn.preprocessing
Average
Algorithm Time Best Case Worst Case Stable?
Complexity
Bubble Sort O(n²) O(n) O(n²) Yes
Selection
O(n²) O(n²) O(n²) No
Sort
Insertion
O(n²) O(n) O(n²) Yes
Sort
Merge Sort O(n log n) O(n log n) O(n log n) Yes
Quick Sort O(n log n) O(n log n) O(n²) No
Heap Sort O(n log n) O(n log n) O(n log n) No
TimSort
O(n log n) O(n) O(n log n) Yes
(Python)
Dataset Considerations
Time performance depends heavily on:
•Size: e.g., 100, 1,000, 10,000, 1,000,000 elements
•Order: sorted, reverse, random, nearly sorted
•Type: integers, floats, strings, complex objects
import time
import random
import pandas as pd
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i-1
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
def selection_sort(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i+1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
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
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[0]
less = [x for x in arr[1:] if x <= pivot]
greater = [x for x in arr[1:] if x > pivot]
return quick_sort(less) + [pivot] + quick_sort(greater)
# Timing wrapper
def time_sorting_algorithms(data):
results = {}
algorithms = {
'Bubble Sort': bubble_sort,
'Insertion Sort': insertion_sort,
'Selection Sort': selection_sort,
'Merge Sort': merge_sort,
'Quick Sort': quick_sort,
'Python TimSort': sorted
}
for name, func in algorithms.items():
data_copy = data.copy()
start = time.time()
func(data_copy)
end = time.time()
results[name] = end - start
return results
# Create dataset
dataset_size = 1000
random_data = [random.randint(0, 10000) for _ in range(dataset_size)]
# Run benchmarks
timing_results = time_sorting_algorithms(random_data)
timing_df = pd.DataFrame(list(timing_results.items()),
columns=["Algorithm", "Time (s)"])
timing_df.sort_values(by="Time (s)", inplace=True)
print(timing_df)
Hash Functions
A hash function takes an input (or "key") and returns a fixed-
size string of bytes. The output is typically a number, which is
used as an index in a hash table.
Properties of a good hash function:
•Deterministic: Same input always gives the same output.
•Fast: Should compute quickly.
•Uniform distribution: Spreads keys evenly across the hash
table.
•Minimizes collisions: Different keys should ideally have
different hash values.
Example:
phone_book = {
"Alice": "123-4567",
"Bob": "987-6543"
}
Collision Handling
A collision occurs when two keys hash to the same
index.
Common collision resolution techniques:
1.Chaining
1.Each table index holds a list of entries.
2.Colliding items are stored in the same list.
table[3] = [("apple", 100), ("grape", 200)]
Open Addressing
•If a spot is taken, find the next open one using a probing
sequence.
•Types of probing:
• Linear probing: Try the next slot.
• Quadratic probing: Try i² slots away.
• Double hashing: Use a second hash function.
Summary Table
Concept Description
def fibonacci(n):
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fibonacci(n-1) + fibonacci(n-2)
return cache[n]
print(fibonacci(10)) # Output: 55
Databases / Indexing
•Use case: Quickly locate records.
•Real-world example: Hash indexes in databases like
MongoDB, PostgreSQL.
Password Hashing
•Use case: Store passwords securely.
•Real-world example: User authentication systems.
Code Example:
import hashlib
# Verify password
input_password = "my_secret_password"
if
hashlib.sha256(input_password.encode()).hexdiges
t() == hashed:
print("Password is correct.")
Data Deduplication
•Use case: Identify and remove duplicates quickly.
•Real-world example: File systems (like ZFS), backup
software.
Code Example:
file_hashes = set()
def is_duplicate(file_content):
file_hash =
hashlib.md5(file_content.encode()).hexdigest()
if file_hash in file_hashes:
return True
file_hashes.add(file_hash)
return False
embedding_vector =
embedding_matrix[word_index]
@lru_cache(maxsize=10000)
def get_embedding(word):
return load_embedding_from_disk(word)
b. Faiss for Fast Similarity Search
If you're doing similarity or nearest neighbor search (e.g., semantic
search):
•Use Faiss (Facebook AI Similarity Search).
•It indexes large embedding spaces efficiently.
c. Memory-Mapped Files
For large embeddings (e.g., GloVe), use memory-mapped files via
NumPy:
embeddings = np.memmap("glove.dat",
dtype='float32', mode='r', shape=(vocab_size,
emb_dim))
Summary Table
class EmbeddingModel(nn.Module):
def __init__(self, num_items, embedding_dim):
super(EmbeddingModel, self).__init__()
self.embeddings = nn.Embedding(num_items,
embedding_dim)
# Example usage
model = EmbeddingModel(num_items=1000,
embedding_dim=64)
item_ids = torch.LongTensor([1, 5, 42])
embeddings = model(item_ids)
print(embeddings.shape) # (3, 64)
TensorFlow: Basic Embedding Layer
Embedding layer using TensorFlow / Keras.
import tensorflow as tf
embedding_layer =
tf.keras.layers.Embedding(input_dim=1000,
output_dim=64)
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
@app.get("/embedding/{item_id}")
def get_embedding(item_id: str):
emb_json = r.get(item_id)
if emb_json is None:
raise HTTPException(status_code=404, detail="Embedding not found")
return {"item_id": item_id, "embedding": json.loads(emb_json)}
@app.post("/embedding/{item_id}")
def store_embedding(item_id: str, embedding: list):
r.set(item_id, json.dumps(embedding))
return {"status": "stored"}
Run it:
uvicorn embedding_service:app --reload
For a recommender system using PyTorch, you can implement a neural
collaborative filtering (NCF) model. The model can learn embeddings for
users and items and then predict the interaction score between them (which
could represent a user's preference for an item).
Additionally, we'll implement similarity search using the learned
embeddings (typically cosine similarity).
# Compute cosine similarity between the given item and all items
similarities = F.cosine_similarity(item_embedding,
all_item_embeddings)
return top_k_indices
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
2. DFS Traversals on Binary Tree
def bfs_tree(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)
Graph Representations + BFS & DFS
a) Graph Using Adjacency List
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
def bfs_graph(start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(node, end=' ')
visited.add(node)
for neighbor in graph[node]:
queue.append(neighbor)
d) Graph Using Adjacency Matrix
# A - 0, B - 1, C - 2, D - 3
adj_matrix = [
[0, 1, 1, 0], # A
[0, 0, 0, 1], # B
[0, 0, 0, 1], # C
[0, 0, 0, 0], # D
]
Example Usage
import torch
import torch.nn as nn
class SimpleNN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(4, 10) # 4 input features, 10
hidden units
self.relu = nn.ReLU()
self.fc2 = nn.Linear(10, 3) # 3 output classes
model = SimpleNN()
print(model)
2. Decision Trees
🔹 Core Idea:
Decision trees are supervised learning models that split
data into subsets based on feature values. They're
interpretable and good for classification/regression on
tabular data.
🔹 Structure:
•Nodes represent features/conditions.
•Edges are decision outcomes (e.g., True/False).
•Leaves are output labels or values.
Use Cases:
•Tabular classification problems
•Loan approval, fraud detection
•As base models for ensembles (Random
Forest, XGBoost)
Example (using scikit-learn):
X, y = load_iris(return_X_y=True)
clf = DecisionTreeClassifier(max_depth=3)
clf.fit(X, y)
plot_tree(clf, filled=True)
plt.show()
3. Knowledge Graphs
🔹 Core Idea:
A knowledge graph is a structured
representation of entities and relationships.
It's like a graph database (nodes and edges)
with semantics.
🔹 Structure:
•Nodes: Entities (e.g., "Einstein", "Physics")
•Edges: Relationships (e.g., "studied",
"invented")
•Often based on triples: (subject, predicate,
object)
Use Cases:
•Semantic search (e.g., Google Knowledge
Graph)
•Question answering systems
•Data integration in enterprise systems
Example (using RDFLib):
g = Graph()
EX = Namespace("http://example.org/")
for s, p, o in g:
print(s, p, o)
DFS Implementation for a Graph
We'll use an adjacency list to represent the graph and implement a
recursive DFS function.
🔹 Graph + DFS Code
# Define the graph using an adjacency list
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
# DFS implementation
def dfs(node, visited=None):
if visited is None:
visited = set()
if node not in visited:
print(node, end=' ')
visited.add(node)
for neighbor in graph[node]:
dfs(neighbor, visited)
(Note: Output order may vary depending on Python's dictionary ordering unless you use OrderedDict.)
When It Works
A greedy algorithm builds up a solution piece by piece, always choosing the
locally optimal option at each step, with the hope that this leads to a globally
optimal solution.
Key requirement: The problem must exhibit greedy choice property and
optimal substructure.
When It Works
Dynamic programming solves problems by breaking them into overlapping subproblems,
solving each just once, and storing the solutions.
Key requirement: The problem must have optimal substructure and overlapping
subproblems.
1. Feature Selection
Greedy algorithms are used to iteratively select features that contribute
the most to a model's predictive power.
•Forward Selection: Starts with no features and adds one feature at a
time that improves model performance the most.
•Backward Elimination: Starts with all features and removes the least
useful one at each step.
•Recursive Feature Elimination (RFE): A popular greedy approach
that fits a model and removes the weakest features recursively.
Why greedy works here: While not guaranteed to find the global
optimal subset, greedy methods offer a good trade-off between
performance and computational cost.
2. Clustering
Greedy methods can be used in clustering when building clusters
incrementally.
•Agglomerative Hierarchical Clustering: A bottom-up greedy
approach that merges the closest pair of clusters step-by-step.
•K-means++ Initialization: Greedily chooses initial cluster centers to
improve clustering performance over random initialization.
Why greedy works here: It builds solutions in a step-by-step fashion,
often leading to faster convergence and better initialization.
3. Resource Allocation
Greedy algorithms are frequently used in problems like:
•Knapsack Problem (Greedy version): Allocate resources to
maximize value without exceeding capacity, picking items with the best
value-to-weight ratio.
•Task Scheduling: Assign tasks to resources (e.g., CPUs) based on
shortest processing time or earliest deadline.
•Bandwidth Allocation: Assign bandwidth to users/applications
greedily based on priority or demand.
Why greedy works here: These problems often require fast
decisions, and greedy strategies can give near-optimal solutions
efficiently.
Graph Algorithms for AI
A --(1)--> B
A --(4)--> C
B --(2)--> C
B --(5)--> D
C --(1)--> D
while priority_queue:
curr_distance, curr_node = heapq.heappop(priority_queue)
return distances
Graph Input Format:
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('C', 2), ('D', 5)],
'C': [('D', 1)],
'D': []
}
Run:
print(dijkstra(graph, 'A'))
Key Notes
•Efficient with binary heap: O((V + E) log V)
•Works only for non-negative edge weights
•Great for GPS, network routing, and AI pathfinding
Problem: Predicting Customer Churn
We want to predict whether a customer will cancel their subscription
based on usage behavior and demographic data.
Algorithmic Concepts:
•Univariate feature selection (e.g., SelectKBest using mutual information)
•Recursive Feature Elimination (RFE)
•Tree-based feature importance (from Random Forest or XGBoost)
3. Model Building
Models to Consider:
•Logistic Regression (for interpretability)
•Random Forest (for non-linear interactions)
•Gradient Boosting (e.g., XGBoost or LightGBM for performance)
•Neural Networks (if you have high-dimensional data)
Algorithmic Concepts:
•Cross-validation (e.g., Stratified K-Fold)
•Hyperparameter tuning using:
• Grid Search
• Random Search
• Bayesian Optimization (e.g., with Optuna or Hyperopt)
4. Model Decision-Making (Explainability & Threshold Tuning)
Concepts Used:
•ROC-AUC curve to select the best classification threshold
•Precision-Recall trade-off analysis (important for imbalanced
datasets)
•SHAP (SHapley Additive exPlanations) or LIME for model
explainability
•Cost-sensitive learning (e.g., adjusting weights for false positives vs.
false negatives)
5. Post-Model Actions (Deployment & Feedback Loop)
Concepts:
•Model inference pipeline: Wrap preprocessing + model in a pipeline (e.g., with
sklearn.pipeline)
•Monitor model drift using statistical tests (e.g., Kolmogorov-Smirnov)
•Online learning or retraining schedule based on new data (e.g., weekly or
monthly retraining)
Combined Concepts in Action:
Example Pipeline:
python
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
categorical_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore"))
])
# Combine preprocessing
preprocessor = ColumnTransformer(
transformers=[
("num", numeric_transformer, numeric_features),
("cat", categorical_transformer, categorical_features)
]
)
Concept Applied To
Data Cleaning & Encoding Preprocessing
Feature Selection Dimensionality Reduction
ML Modeling Random Forest, XGBoost
Hyperparameter Tuning Optimization
Threshold Adjustment Decision-making
Explainability (SHAP) Trust & Audit
Pipelines Deployment
Building a simple spam filter using decision trees and hashing is a classic
example combining feature engineering and classification.
Goal:
•Use hashing trick to convert email text into fixed-length numerical
features.
•Train a decision tree classifier to distinguish spam from non-spam
emails.
Step-by-step:
X = vectorizer.transform(emails)
# 2. Split dataset
X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.3, random_state=42)
Possible improvements:
•Increase n_features to get finer text representation.
•Use more training data.
•Use tree ensembles like Random Forests or Gradient Boosting for better accuracy.
•Add preprocessing like removing stop words or stemming before hashing.