Medium Level - Complete Solutions#
This notebook contains solutions to all exercises from the Medium level lessons.
How to Use This Notebook#
Attempt the exercises first - Try solving them in the original notebooks
Study multiple approaches - Many problems have several valid solutions
Understand the concepts - Donโt just copy; understand why each solution works
Optimize your code - Compare time and space complexity
Experiment - Modify these solutions to deepen your understanding
Contents#
01 - Functions and Modules#
Solutions for exercises from 01_functions_and_modules.ipynb
Exercise Solutions#
The Functions and Modules notebook includes exercises on:
Creating functions with various parameter types
Using *args and **kwargs
Lambda functions and functional programming
Decorators
Module organization
# Example Solution 1: Function with default parameters
def greet_user(name, greeting="Hello", punctuation="!"):
"""
Greet a user with customizable greeting and punctuation.
Args:
name (str): User's name
greeting (str): Greeting word (default: "Hello")
punctuation (str): Ending punctuation (default: "!")
Returns:
str: Formatted greeting
"""
return f"{greeting}, {name}{punctuation}"
# Test
print(greet_user("Alice"))
print(greet_user("Bob", greeting="Hi"))
print(greet_user("Charlie", greeting="Hey", punctuation="."))
# Example Solution 2: Using *args and **kwargs
def analyze_scores(*scores, **settings):
"""
Analyze test scores with optional settings.
Args:
*scores: Variable number of numeric scores
**settings: Optional settings (show_details, round_to, etc.)
Returns:
dict: Analysis results
"""
if not scores:
return {"error": "No scores provided"}
average = sum(scores) / len(scores)
minimum = min(scores)
maximum = max(scores)
# Apply settings
if settings.get("round_to"):
average = round(average, settings["round_to"])
results = {
"count": len(scores),
"average": average,
"min": minimum,
"max": maximum,
"range": maximum - minimum
}
if settings.get("show_details"):
results["all_scores"] = scores
return results
# Test
print(analyze_scores(85, 90, 78, 92, 88))
print(analyze_scores(85, 90, 78, 92, 88, round_to=1))
print(analyze_scores(85, 90, 78, 92, 88, show_details=True, round_to=2))
# Example Solution 3: Higher-order functions
def apply_operation(numbers, operation):
"""
Apply an operation to all numbers.
Args:
numbers (list): List of numbers
operation (callable): Function to apply
Returns:
list: Transformed numbers
"""
return [operation(num) for num in numbers]
# Example operations
def square(x):
return x ** 2
def double(x):
return x * 2
# Test
data = [1, 2, 3, 4, 5]
print(f"Original: {data}")
print(f"Squared: {apply_operation(data, square)}")
print(f"Doubled: {apply_operation(data, double)}")
print(f"Custom (x+10): {apply_operation(data, lambda x: x + 10)}")
# Example Solution 4: Decorator for timing functions
import time
from functools import wraps
def timer(func):
"""Decorator to time function execution."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.6f} seconds")
return result
return wrapper
@timer
def slow_calculation(n):
"""Simulate slow calculation."""
total = 0
for i in range(n):
total += i ** 2
return total
# Test
result = slow_calculation(100000)
print(f"Result: {result}")
# Example Solution 5: Closure example
def create_multiplier(factor):
"""
Create a function that multiplies by a factor.
Demonstrates closures.
"""
def multiplier(x):
return x * factor
return multiplier
# Create specialized multipliers
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# Test
value = 5
print(f"Original: {value}")
print(f"Doubled: {double(value)}")
print(f"Tripled: {triple(value)}")
print(f"Times 10: {times_ten(value)}")
02 - Data Structures#
Solutions for exercises from 02_data_structures.ipynb
# Example Solution 1: List comprehensions
def analyze_numbers(numbers):
"""
Use comprehensions to analyze a list of numbers.
"""
# Filter evens
evens = [n for n in numbers if n % 2 == 0]
# Filter odds
odds = [n for n in numbers if n % 2 != 0]
# Square all numbers
squared = [n ** 2 for n in numbers]
# Get numbers divisible by 3
div_by_3 = [n for n in numbers if n % 3 == 0]
return {
"evens": evens,
"odds": odds,
"squared": squared,
"divisible_by_3": div_by_3
}
# Test
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
results = analyze_numbers(data)
for key, value in results.items():
print(f"{key}: {value}")
# Example Solution 2: Dictionary operations
def merge_student_data(student_info, grades, attendance):
"""
Merge student data from multiple dictionaries.
Args:
student_info: Dict with student names and IDs
grades: Dict with student grades
attendance: Dict with attendance percentages
Returns:
dict: Merged student records
"""
merged = {}
for student_id, name in student_info.items():
merged[student_id] = {
"name": name,
"grade": grades.get(student_id, "N/A"),
"attendance": attendance.get(student_id, 0)
}
return merged
# Test
students = {"001": "Alice", "002": "Bob", "003": "Charlie"}
grades = {"001": 95, "002": 87, "003": 92}
attendance = {"001": 98, "002": 85, "003": 100}
merged_data = merge_student_data(students, grades, attendance)
for student_id, data in merged_data.items():
print(f"{student_id}: {data}")
# Example Solution 3: Set operations
def analyze_course_enrollment(course_a, course_b):
"""
Analyze enrollment in two courses using sets.
Args:
course_a: Set of student IDs in course A
course_b: Set of student IDs in course B
Returns:
dict: Analysis of enrollment patterns
"""
return {
"both_courses": course_a & course_b, # Intersection
"either_course": course_a | course_b, # Union
"only_a": course_a - course_b, # Difference
"only_b": course_b - course_a, # Difference
"unique_students": course_a ^ course_b, # Symmetric difference
"total_a": len(course_a),
"total_b": len(course_b)
}
# Test
python_course = {"S001", "S002", "S003", "S004", "S005"}
ml_course = {"S003", "S004", "S006", "S007"}
analysis = analyze_course_enrollment(python_course, ml_course)
print("Enrollment Analysis:")
for key, value in analysis.items():
print(f" {key}: {value}")
# Example Solution 4: Using collections module
from collections import Counter, defaultdict, deque
def word_frequency_analysis(text):
"""
Analyze word frequency in text using Counter.
"""
words = text.lower().split()
word_counts = Counter(words)
return {
"most_common": word_counts.most_common(5),
"total_words": sum(word_counts.values()),
"unique_words": len(word_counts),
"all_counts": dict(word_counts)
}
# Test
sample_text = """Python is great and Python is powerful Python makes
programming fun and Python is easy to learn"""
analysis = word_frequency_analysis(sample_text)
print("Word Frequency Analysis:")
print(f"Total words: {analysis['total_words']}")
print(f"Unique words: {analysis['unique_words']}")
print(f"\nTop 5 words: {analysis['most_common']}")
# Example Solution 5: Nested data structures
def process_student_records(records):
"""
Process nested student records.
Args:
records: List of student dictionaries with nested grade data
Returns:
dict: Processed analytics
"""
total_students = len(records)
all_averages = []
for student in records:
grades = student.get("grades", {})
if grades:
avg = sum(grades.values()) / len(grades)
student["average"] = round(avg, 2)
all_averages.append(avg)
class_average = sum(all_averages) / len(all_averages) if all_averages else 0
return {
"total_students": total_students,
"class_average": round(class_average, 2),
"highest_average": round(max(all_averages), 2) if all_averages else 0,
"lowest_average": round(min(all_averages), 2) if all_averages else 0
}
# Test
students = [
{"name": "Alice", "grades": {"math": 95, "science": 92, "english": 88}},
{"name": "Bob", "grades": {"math": 87, "science": 90, "english": 85}},
{"name": "Charlie", "grades": {"math": 92, "science": 88, "english": 94}}
]
analytics = process_student_records(students)
print("Class Analytics:")
for key, value in analytics.items():
print(f" {key}: {value}")
03 - Classes and OOP#
Solutions for exercises from 03_classes_and_oop.ipynb
# Example Solution 1: Basic class with inheritance
class BankAccount:
"""Base class for bank accounts."""
def __init__(self, account_number, owner, balance=0):
self.account_number = account_number
self.owner = owner
self._balance = balance # Protected attribute
def deposit(self, amount):
"""Deposit money into account."""
if amount > 0:
self._balance += amount
return f"Deposited ${amount}. New balance: ${self._balance}"
return "Invalid deposit amount"
def withdraw(self, amount):
"""Withdraw money from account."""
if amount > 0 and amount <= self._balance:
self._balance -= amount
return f"Withdrew ${amount}. New balance: ${self._balance}"
return "Insufficient funds or invalid amount"
def get_balance(self):
"""Get current balance."""
return self._balance
def __str__(self):
return f"Account({self.account_number}): {self.owner} - ${self._balance}"
class SavingsAccount(BankAccount):
"""Savings account with interest."""
def __init__(self, account_number, owner, balance=0, interest_rate=0.02):
super().__init__(account_number, owner, balance)
self.interest_rate = interest_rate
def add_interest(self):
"""Add interest to balance."""
interest = self._balance * self.interest_rate
self._balance += interest
return f"Added interest: ${interest:.2f}. New balance: ${self._balance:.2f}"
# Test
account = SavingsAccount("SA001", "Alice", 1000, 0.05)
print(account)
print(account.deposit(500))
print(account.add_interest())
print(account.withdraw(200))
print(account)
# Example Solution 2: Class with properties and validation
class Student:
"""Student class with property validation."""
def __init__(self, name, age, student_id):
self.name = name
self._age = None
self.age = age # Use property setter
self.student_id = student_id
self._grades = []
@property
def age(self):
"""Get age."""
return self._age
@age.setter
def age(self, value):
"""Set age with validation."""
if value < 0 or value > 150:
raise ValueError("Age must be between 0 and 150")
self._age = value
def add_grade(self, grade):
"""Add a grade."""
if 0 <= grade <= 100:
self._grades.append(grade)
else:
raise ValueError("Grade must be between 0 and 100")
@property
def average_grade(self):
"""Calculate average grade."""
if not self._grades:
return 0
return sum(self._grades) / len(self._grades)
def __repr__(self):
return f"Student('{self.name}', {self.age}, '{self.student_id}')"
# Test
student = Student("Bob", 20, "S12345")
student.add_grade(85)
student.add_grade(90)
student.add_grade(88)
print(student)
print(f"Average grade: {student.average_grade:.2f}")
# Example Solution 3: Using dataclasses
from dataclasses import dataclass, field
from typing import List
@dataclass
class Book:
"""Book dataclass with automatic __init__, __repr__, etc."""
title: str
author: str
isbn: str
price: float
genres: List[str] = field(default_factory=list)
def apply_discount(self, percent):
"""Apply discount to price."""
self.price = self.price * (1 - percent / 100)
return self.price
def add_genre(self, genre):
"""Add a genre."""
if genre not in self.genres:
self.genres.append(genre)
# Test
book = Book(
title="Python Programming",
author="John Doe",
isbn="978-1234567890",
price=49.99
)
book.add_genre("Programming")
book.add_genre("Education")
print(book)
print(f"Price after 20% discount: ${book.apply_discount(20):.2f}")
# Example Solution 4: Abstract base class
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class for shapes."""
@abstractmethod
def area(self):
"""Calculate area."""
pass
@abstractmethod
def perimeter(self):
"""Calculate perimeter."""
pass
class Rectangle(Shape):
"""Rectangle implementation."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def __str__(self):
return f"Rectangle({self.width}x{self.height})"
class Circle(Shape):
"""Circle implementation."""
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
def __str__(self):
return f"Circle(r={self.radius})"
# Test
shapes = [
Rectangle(5, 10),
Circle(7)
]
for shape in shapes:
print(f"{shape}:")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
print()
04 - Machine Learning Basics#
Solutions for exercises from 04_machine_learning_basics.ipynb
# Example Solution 1: Complete ML workflow
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
# Load data
iris = load_iris()
X, y = iris.data, iris.target
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Scale features
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Train model
model = LogisticRegression(random_state=42, max_iter=200)
model.fit(X_train_scaled, y_train)
# Predict
y_pred = model.predict(X_test_scaled)
# Evaluate
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))
# Example Solution 2: Cross-validation and hyperparameter tuning
from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
# Perform cross-validation
rf_model = RandomForestClassifier(random_state=42)
cv_scores = cross_val_score(rf_model, X_train_scaled, y_train, cv=5)
print("Cross-Validation Scores:")
print(f"Scores: {cv_scores}")
print(f"Mean: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})")
# Hyperparameter tuning
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [None, 5, 10],
'min_samples_split': [2, 5]
}
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy'
)
grid_search.fit(X_train_scaled, y_train)
print("\nBest Parameters:", grid_search.best_params_)
print(f"Best CV Score: {grid_search.best_score_:.4f}")
# Test best model
best_model = grid_search.best_estimator_
test_accuracy = best_model.score(X_test_scaled, y_test)
print(f"Test Accuracy: {test_accuracy:.4f}")
# Example Solution 3: Feature importance analysis
import numpy as np
import matplotlib.pyplot as plt
# Train Random Forest to get feature importances
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train_scaled, y_train)
# Get feature importances
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
print("Feature Importances:")
for i, idx in enumerate(indices):
print(f"{i+1}. {iris.feature_names[idx]}: {importances[idx]:.4f}")
# Visualization would go here in a real notebook
# plt.figure(figsize=(10, 6))
# plt.bar(range(len(importances)), importances[indices])
# plt.xticks(range(len(importances)), [iris.feature_names[i] for i in indices], rotation=45)
# plt.title('Feature Importances')
# plt.show()
05 - Data Analysis with Pandas#
Solutions for exercises from 05_data_analysis_with_pandas.ipynb
# Example Solution 1: Data cleaning and analysis
import pandas as pd
import numpy as np
# Create sample data
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'Age': [25, np.nan, 35, 28, 32],
'Salary': [50000, 60000, np.nan, 55000, 65000],
'Department': ['HR', 'IT', 'IT', 'HR', 'Finance']
}
df = pd.DataFrame(data)
print("Original Data:")
print(df)
print(f"\nMissing values:\n{df.isnull().sum()}")
# Fill missing values
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Salary'].fillna(df['Salary'].mean(), inplace=True)
print("\nAfter filling missing values:")
print(df)
# Group by analysis
dept_stats = df.groupby('Department').agg({
'Age': ['mean', 'min', 'max'],
'Salary': ['mean', 'sum']
})
print("\nDepartment Statistics:")
print(dept_stats)
# Example Solution 2: Data merging and transformation
# Create two dataframes
employees = pd.DataFrame({
'EmployeeID': [1, 2, 3, 4],
'Name': ['Alice', 'Bob', 'Charlie', 'David'],
'DepartmentID': [10, 20, 20, 10]
})
departments = pd.DataFrame({
'DepartmentID': [10, 20, 30],
'DepartmentName': ['HR', 'IT', 'Finance'],
'Location': ['Building A', 'Building B', 'Building C']
})
print("Employees:")
print(employees)
print("\nDepartments:")
print(departments)
# Merge dataframes
merged = pd.merge(employees, departments, on='DepartmentID', how='left')
print("\nMerged Data:")
print(merged)
# Create pivot table
pivot = merged.pivot_table(
values='EmployeeID',
index='DepartmentName',
aggfunc='count'
)
print("\nEmployees per Department:")
print(pivot)
# Example Solution 3: Time series analysis
# Create time series data
dates = pd.date_range('2024-01-01', periods=30, freq='D')
sales_data = pd.DataFrame({
'Date': dates,
'Sales': np.random.randint(100, 500, size=30)
})
sales_data.set_index('Date', inplace=True)
print("Sales Data:")
print(sales_data.head())
# Calculate rolling average
sales_data['Rolling_7day'] = sales_data['Sales'].rolling(window=7).mean()
# Resample to weekly
weekly_sales = sales_data['Sales'].resample('W').sum()
print("\nWeekly Sales:")
print(weekly_sales)
# Summary statistics
print("\nSummary Statistics:")
print(sales_data.describe())
06 - Algorithms and Problem Solving#
Solutions for exercises from 06_algorithms_and_problem_solving.ipynb
# Exercise 1: Array Manipulation Solutions
def find_duplicates(arr):
"""
Find all duplicate elements in array.
Time: O(n), Space: O(n)
"""
seen = set()
duplicates = set()
for num in arr:
if num in seen:
duplicates.add(num)
seen.add(num)
return list(duplicates)
def reverse_array(arr):
"""
Reverse array in-place.
Time: O(n), Space: O(1)
"""
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr
def rotate_array(arr, k):
"""
Rotate array k positions to the right.
Time: O(n), Space: O(1)
"""
n = len(arr)
k = k % n # Handle k > n
# Reverse entire array
reverse_array(arr)
# Reverse first k elements
arr[:k] = reversed(arr[:k])
# Reverse remaining elements
arr[k:] = reversed(arr[k:])
return arr
def find_missing_number(arr, n):
"""
Find missing number from 1 to n.
Time: O(n), Space: O(1)
"""
# Sum of 1 to n
expected_sum = n * (n + 1) // 2
actual_sum = sum(arr)
return expected_sum - actual_sum
# Test
print("Duplicates:", find_duplicates([1, 2, 3, 2, 4, 5, 3]))
print("Reversed:", reverse_array([1, 2, 3, 4, 5]))
print("Rotated:", rotate_array([1, 2, 3, 4, 5], 2))
print("Missing:", find_missing_number([1, 2, 4, 5, 6], 6))
# Exercise 2: String Algorithms Solutions
def is_anagram(s1, s2):
"""
Check if two strings are anagrams.
Time: O(n log n), Space: O(1)
"""
return sorted(s1.lower()) == sorted(s2.lower())
def compress_string(s):
"""
Compress string using counts.
Time: O(n), Space: O(n)
"""
if not s:
return ""
result = []
count = 1
for i in range(1, len(s)):
if s[i] == s[i-1]:
count += 1
else:
result.append(s[i-1] + str(count))
count = 1
# Add last group
result.append(s[-1] + str(count))
return ''.join(result)
def longest_palindrome_substring(s):
"""
Find longest palindromic substring.
Time: O(nยฒ), Space: O(1)
"""
def expand_around_center(left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return s[left+1:right]
if len(s) < 2:
return s
longest = ""
for i in range(len(s)):
# Odd length palindrome
pal1 = expand_around_center(i, i)
# Even length palindrome
pal2 = expand_around_center(i, i+1)
longest = max([longest, pal1, pal2], key=len)
return longest
# Test
print("Anagram:", is_anagram("listen", "silent"))
print("Compressed:", compress_string("aaabbc"))
print("Longest palindrome:", longest_palindrome_substring("babad"))
# Exercise 3: Advanced Algorithms Solutions
def merge_sorted_arrays(arr1, arr2):
"""
Merge two sorted arrays.
Time: O(n + m), Space: O(n + m)
"""
result = []
i, j = 0, 0
while i < len(arr1) and j < len(arr2):
if arr1[i] <= arr2[j]:
result.append(arr1[i])
i += 1
else:
result.append(arr2[j])
j += 1
# Add remaining elements
result.extend(arr1[i:])
result.extend(arr2[j:])
return result
def kth_largest_element(arr, k):
"""
Find kth largest element.
Time: O(n log n), Space: O(1)
"""
# Sort in descending order
arr_sorted = sorted(arr, reverse=True)
if k <= len(arr_sorted):
return arr_sorted[k-1]
return None
def two_sum(arr, target):
"""
Find two numbers that sum to target.
Time: O(n), Space: O(n)
"""
seen = {}
for i, num in enumerate(arr):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return None
# Test
print("Merged:", merge_sorted_arrays([1, 3, 5], [2, 4, 6]))
print("Kth largest:", kth_largest_element([3, 2, 1, 5, 6, 4], 2))
print("Two sum:", two_sum([2, 7, 11, 15], 9))
Summary#
This notebook provides complete solutions to all Medium level exercises.
Key Concepts Covered#
Functions: Advanced parameter handling, decorators, closures
Data Structures: Comprehensions, collections module, nested structures
OOP: Inheritance, properties, dataclasses, abstract classes
Machine Learning: Complete ML workflow, cross-validation, tuning
Pandas: Data cleaning, merging, time series analysis
Algorithms: Sorting, searching, string manipulation, optimization
Learning Tips#
Practice Complexity Analysis: Always consider time and space complexity
Multiple Approaches: Many problems have several valid solutions
Test Thoroughly: Use edge cases and different input sizes
Optimize Gradually: Start with brute force, then optimize
Understand Patterns: Recognize common algorithm patterns
Next Steps#
Implement your own variations of these solutions
Combine concepts from different exercises
Move on to Hard level challenges
Build real-world projects using these techniques
Remember: Understanding the concepts behind these solutions is more important than memorizing the code!