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

Part I

Python Mastery

Core language skills every AI engineer needs

Chapter 1

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

StructureOrderedMutableDuplicatesLookupBest 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

NotationNameExample1M items
O(1)ConstantDict lookup1 op
O(log n)LogarithmicBinary search20 ops
O(n)LinearList scan1M ops
O(n log n)LinearithmicMerge sort20M ops
O(n²)QuadraticBubble sort1T 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