Phase 2 ⢠EduArtha
Programming & Software Engineering
Python is the language of AI. You must also understand how to write efficient, scalable code. This book covers Python mastery, scientific computing, software engineering practices, and hardware fundamentals.
ā± 2ā4 months | 13 Chapters | 50+ Exercises
Python Mastery
Core language skills every AI engineer needs
Data Structures & Algorithms
Learning Objectives
- Choose the right data structure for each problem (lists, dicts, sets, tuples)
- Implement stacks, queues, and linked lists
- Understand Big-O notation and analyze algorithm complexity
- Implement binary search, merge sort, and quicksort
Built-in Data Structures
| Structure | Ordered | Mutable | Duplicates | Lookup | Best For |
|---|---|---|---|---|---|
| List | ā | ā | ā | O(n) | Ordered collections |
| Tuple | ā | ā | ā | O(n) | Immutable records |
| Set | ā | ā | ā | O(1) | Membership testing |
| Dict | ā* | ā | Keys: ā | O(1) | Key-value mapping |
Python
# Performance comparison ā why choosing right structure matters
import time
data_list = list(range(1_000_000))
data_set = set(data_list)
# Searching for an element
target = 999_999
start = time.time()
_ = target in data_list # O(n) ā scans every element
print(f"List: {time.time()-start:.6f}s")
start = time.time()
_ = target in data_set # O(1) ā hash lookup
print(f"Set: {time.time()-start:.6f}s")
# Set is ~1000x faster for membership testing!
Big-O Notation
| Notation | Name | Example | 1M items |
|---|---|---|---|
| O(1) | Constant | Dict lookup | 1 op |
| O(log n) | Logarithmic | Binary search | 20 ops |
| O(n) | Linear | List scan | 1M ops |
| O(n log n) | Linearithmic | Merge sort | 20M ops |
| O(n²) | Quadratic | Bubble sort | 1T ops |
Searching & Sorting
Python
# Binary Search ā O(log n)
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Quick Sort ā O(n log n) average
def quicksort(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 quicksort(left) + middle + quicksort(right)
print(quicksort([38, 27, 43, 3, 9, 82, 10]))
# Stack implementation
class Stack:
def __init__(self): self.items = []
def push(self, item): self.items.append(item)
def pop(self): return self.items.pop()
def peek(self): return self.items[-1]
def is_empty(self): return len(self.items) == 0
Project: Task Scheduler with Priority Queue
Python
import heapq
class TaskScheduler:
def __init__(self):
self.heap = []
self.counter = 0
def add_task(self, task, priority):
heapq.heappush(self.heap, (priority, self.counter, task))
self.counter += 1
def get_next(self):
if self.heap:
priority, _, task = heapq.heappop(self.heap)
return task
return None
scheduler = TaskScheduler()
scheduler.add_task("Fix critical bug", 1)
scheduler.add_task("Write docs", 5)
scheduler.add_task("Deploy to prod", 2)
scheduler.add_task("Code review", 3)
while (task := scheduler.get_next()):
print(f"Executing: {task}")
Exercises
Exercise 1.1: Implement merge sort and explain its time complexity
def merge_sort(arr):
if len(arr) <= 1: return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:]); result.extend(right[j:])
return result
Time: O(n log n) always. Space: O(n). Divides array in half each time (log n levels), merges n elements at each level.
Exercise 1.2: When would you use a dict over a list?
Use dict when you need fast O(1) key-based lookup, counting occurrences, or mapping relationships. Use list when you need ordered elements, indexed access, or iteration in sequence. Example: counting word frequencies ā dict. Storing sorted scores ā list.
Exercise 1.3: Implement a queue using two stacks
class QueueFromStacks:
def __init__(self):
self.in_stack = []
self.out_stack = []
def enqueue(self, item):
self.in_stack.append(item)
def dequeue(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
return self.out_stack.pop()
Exercise 1.4: What is the time complexity of checking if an element exists in a list vs a set?
List: O(n) ā must scan linearly. Set: O(1) amortized ā uses hash table. For 1M elements, list takes ~1M comparisons, set takes ~1. Always use sets for membership tests.
Chapter Summary
- Choose data structures by access pattern: O(1) lookup ā dict/set, ordered ā list
- Binary search (O(log n)) requires sorted data; quicksort/mergesort are O(n log n)
- Big-O describes worst-case growth rate ā crucial for scalable code
- Stacks (LIFO) and queues (FIFO) solve specific ordering problems