Algorithmic Explorations

Hands-on implementation of theoretical concepts through practical lab exercises

Laboratory Journey Through Algorithms

Welcome to the laboratory section of my Design and Analysis of Algorithms portfolio. This section documents my hands-on journey implementing the theoretical concepts covered in the course. Through these experiments, I've transformed abstract algorithmic ideas into working code, analyzed performance characteristics, and gained practical insights into computational efficiency.

Performance Analysis

Each lab exercise includes empirical analysis of algorithmic performance across different input sizes.

Implementation Challenges

Converting theoretical algorithms to efficient code requires careful handling of edge cases and optimizations.

Iterative Improvement

Many implementations underwent multiple refinements to enhance efficiency and readability.

Methodology

  1. Algorithm Design: Planning the implementation approach
  2. Core Implementation: Coding the algorithm with focus on correctness
  3. Testing: Validating with various test cases including edge cases
  4. Optimization: Refining code for better performance
  5. Analysis: Documenting time and space complexity findings

Interactive Lab Guide

Explore each algorithm implementation below. Click on an experiment to view details about the implementation approach, code structure, and performance analysis.

Filter by type:
01
Insertion Sort & Bubble Sort
O(n²)

Elementary Sorting Algorithms

These fundamental sorting algorithms serve as a foundation for understanding more complex sorting techniques.

Insertion Sort

Builds the sorted array one item at a time by repeatedly taking elements from the unsorted part and inserting them into their correct position in the sorted part.

  • Best Case: O(n) - when array is already sorted
  • Average Case: O(n²)
  • Worst Case: O(n²) - when array is reverse sorted
  • Space Complexity: O(1) - in-place algorithm
  • Stability: Stable

Implementation Insight

My implementation focuses on readability while maintaining optimal performance. I used a slightly different approach than the textbook algorithm by using a temporary variable to hold the current element, which simplified the inner loop logic.

Bubble Sort

Repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The process repeats until no swaps are needed.

  • Best Case: O(n) - with optimization to detect already sorted array
  • Average Case: O(n²)
  • Worst Case: O(n²)
  • Space Complexity: O(1)
  • Stability: Stable

Implementation Insight

I enhanced the standard bubble sort with an optimization flag that detects when no swaps occur in a pass, indicating the array is sorted. This improved performance significantly on partially sorted arrays.

Performance Analysis

Insertion sort has a time complexity of O(n²) in the worst case, as it compares and shifts elements one by one. Bubble sort also has O(n²) in the worst case, repeatedly swapping adjacent elements. Both are inefficient for large datasets, but insertion sort can perform better with nearly sorted data.

Key Findings
  • Insertion sort outperformed bubble sort in nearly all test cases
  • For small arrays (n < 20), both algorithms showed minimal performance differences
  • Insertion sort was significantly faster on partially sorted arrays
  • Neither algorithm scaled well for large inputs (n > 10,000)
02
Linear & Binary Search
O(n) / O(log n)

Search Algorithm Implementation

This experiment compares two fundamental search algorithms: Linear Search for unsorted arrays and Binary Search for sorted arrays.

Linear Search

Sequentially checks each element until the target is found or the end of array is reached.

  • Time Complexity: O(n)
  • Space Complexity: O(1)
  • Key Advantage: Works on unsorted arrays

Binary Search

Uses divide-and-conquer by repeatedly dividing the search interval in half.

  • Time Complexity: O(log n)
  • Space Complexity: O(1) for iterative, O(log n) for recursive
  • Key Advantage: Much faster for large sorted arrays

Implementation Note

I implemented both recursive and iterative versions of binary search. The iterative version avoided stack overhead and had slightly better performance. Special care was taken to prevent integer overflow in calculating mid-points using the formula mid = low + (high - low) / 2.

Performance Analysis

Linear search has O(n) time complexity, checking each element, while binary search is more efficient with O(log n), but requires sorted data. For large datasets, binary search outperforms linear search.

Results Summary
  • Binary search showed logarithmic growth as expected
  • With large arrays (n > 10,000), binary search was 10-15x faster
  • For small arrays (n < 100), performance difference was negligible
  • The sorting time must be considered when choosing binary search
03
Merge Sort
O(n log n)

Merge Sort Implementation

A divide-and-conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves.

  • Time Complexity: O(n log n) in all cases
  • Space Complexity: O(n) auxiliary space
  • Stability: Stable

Implementation Challenges

The main challenge was optimizing the merge operation to minimize memory allocations. I used a single auxiliary array for all merge operations rather than creating new arrays for each recursive call, which significantly improved performance.

Performance Analysis

Merge sort consistently outperformed O(n²) algorithms for medium to large arrays. For very small arrays (n < 10), the overhead of recursion made it slightly slower than insertion sort.

Optimization Note

I implemented a hybrid approach that uses insertion sort for small subarrays (n < 10), resulting in about 15% performance improvement for large arrays.

01
Insertion Sort & Bubble Sort
O(n²)

Elementary Sorting Algorithms

These fundamental sorting algorithms serve as a foundation for understanding more complex sorting techniques.

Insertion Sort

Builds the sorted array one item at a time by repeatedly taking elements from the unsorted part and inserting them into their correct position in the sorted part.

  • Best Case: O(n) - when array is already sorted
  • Average Case: O(n²)
  • Worst Case: O(n²) - when array is reverse sorted
  • Space Complexity: O(1) - in-place algorithm
  • Stability: Stable

Implementation Insight

My implementation focuses on readability while maintaining optimal performance. I used a slightly different approach than the textbook algorithm by using a temporary variable to hold the current element, which simplified the inner loop logic.

Bubble Sort

Repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The process repeats until no swaps are needed.

  • Best Case: O(n) - with optimization to detect already sorted array
  • Average Case: O(n²)
  • Worst Case: O(n²)
  • Space Complexity: O(1)
  • Stability: Stable

Implementation Insight

I enhanced the standard bubble sort with an optimization flag that detects when no swaps occur in a pass, indicating the array is sorted. This improved performance significantly on partially sorted arrays.

Performance Analysis

Insertion sort has a time complexity of O(n²) in the worst case, as it compares and shifts elements one by one. Bubble sort also has O(n²) in the worst case, repeatedly swapping adjacent elements. Both are inefficient for large datasets, but insertion sort can perform better with nearly sorted data.

Key Findings
  • Insertion sort outperformed bubble sort in nearly all test cases
  • For small arrays (n < 20), both algorithms showed minimal performance differences
  • Insertion sort was significantly faster on partially sorted arrays
  • Neither algorithm scaled well for large inputs (n > 10,000)
02
Linear & Binary Search
O(n) / O(log n)

Search Algorithm Implementation

This experiment compares two fundamental search algorithms: Linear Search for unsorted arrays and Binary Search for sorted arrays.

Linear Search

Sequentially checks each element until the target is found or the end of array is reached.

  • Time Complexity: O(n)
  • Space Complexity: O(1)
  • Key Advantage: Works on unsorted arrays

Binary Search

Uses divide-and-conquer by repeatedly dividing the search interval in half.

  • Time Complexity: O(log n)
  • Space Complexity: O(1) for iterative, O(log n) for recursive
  • Key Advantage: Much faster for large sorted arrays

Implementation Note

I implemented both recursive and iterative versions of binary search. The iterative version avoided stack overhead and had slightly better performance. Special care was taken to prevent integer overflow in calculating mid-points using the formula mid = low + (high - low) / 2.

Performance Analysis

Linear search has O(n) time complexity, checking each element, while binary search is more efficient with O(log n), but requires sorted data. For large datasets, binary search outperforms linear search.

Results Summary
  • Binary search showed logarithmic growth as expected
  • With large arrays (n > 10,000), binary search was 10-15x faster
  • For small arrays (n < 100), performance difference was negligible
  • The sorting time must be considered when choosing binary search
03
Merge Sort
O(n log n)

Merge Sort Implementation

A divide-and-conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves.

  • Time Complexity: O(n log n) in all cases
  • Space Complexity: O(n) auxiliary space
  • Stability: Stable

Implementation Challenges

The main challenge was optimizing the merge operation to minimize memory allocations. I used a single auxiliary array for all merge operations rather than creating new arrays for each recursive call, which significantly improved performance.

Performance Analysis

Merge sort consistently outperformed O(n²) algorithms for medium to large arrays. For very small arrays (n < 10), the overhead of recursion made it slightly slower than insertion sort.

Optimization Note

I implemented a hybrid approach that uses insertion sort for small subarrays (n < 10), resulting in about 15% performance improvement for large arrays.

04
Quick Sort
O(n log n)

Quick Sort Implementation

Another efficient divide-and-conquer sorting algorithm that works by selecting a 'pivot' element and partitioning the array around it.

  • Average Case: O(n log n)
  • Worst Case: O(n²) when poorly chosen pivots
  • Space Complexity: O(log n) for recursion stack
  • Stability: Not stable

Pivot Selection Strategies

I implemented and tested three pivot selection strategies:

  • First element as pivot
  • Middle element as pivot
  • Median-of-three (first, middle, last)

The median-of-three approach consistently showed the best overall performance and resistance to worst-case scenarios.

Performance Comparison

Quick sort generally outperformed merge sort in practice despite having a worse worst-case complexity. The in-place partitioning gives it better cache performance and lower memory overhead.

Interesting Findings
  • Quick sort was ~20% faster than merge sort on random data
  • With median-of-three pivot selection, worst-case scenarios were rarely encountered
  • For already sorted arrays, naive quick sort degraded to O(n²)
05
Strassen's Matrix Multiplication
O(n^2.81)

Matrix Multiplication Optimization

Implementation of Strassen's algorithm for matrix multiplication, which reduces the number of recursive multiplications from 8 to 7.

  • Classical Approach: O(n³)
  • Strassen's Algorithm: O(n^2.81)
  • Space Complexity: O(n²)

Implementation Notes

The implementation required careful handling of matrix dimensions and recursion base cases. For matrices smaller than a threshold size (n < 32), I reverted to the classic algorithm as the overhead of Strassen's approach wasn't justified for small matrices.

Performance Analysis

Strassen's algorithm only showed significant advantages for large matrices (n > 128). For practical applications, the crossover point needs to be carefully determined.

Key Insights
  • For n = 1024, Strassen's was about 20% faster than classical
  • Memory usage was significantly higher with Strassen's
  • Numeric stability was slightly worse in some cases
06
Convex Hull Problem
O(n log n)

Graham Scan Implementation

Finding the smallest convex polygon that encloses a set of points in a plane using the Graham Scan algorithm.

Algorithm Overview

Graham Scan works in three phases:

  1. Find the point with lowest y-coordinate (anchor point)
  2. Sort all points by polar angle relative to anchor point
  3. Build hull by processing points in order, using a stack to manage the hull vertices

Performance Analysis

The algorithm's performance was consistently O(n log n), dominated by the sorting step. My implementation handled degenerate cases such as collinear points correctly.

Observations
  • For highly clustered points, performance was better than random distributions
  • Numerical precision issues required careful handling of floating-point comparisons
  • The algorithm correctly identified convex hulls of various shapes and distributions
07
Knapsack & Huffman Coding
O(n log n)

Greedy Algorithm Implementations

This experiment explored two important applications of the greedy approach.

Fractional Knapsack

Optimizing value by allowing fractions of items to be taken. Items are sorted by value-to-weight ratio and taken greedily.

  • Time Complexity: O(n log n) dominated by sorting
  • Space Complexity: O(n)

Huffman Coding

Building optimal prefix codes for data compression based on character frequencies.

  • Time Complexity: O(n log n) using a priority queue
  • Space Complexity: O(n) for the Huffman tree

Implementation Challenge

Building and traversing the Huffman tree efficiently was challenging. I used a min-heap (priority queue) to always extract the two nodes with lowest frequencies.

Results Analysis

Knapsack Findings
  • The greedy approach always found optimal solutions for fractional knapsack problems
  • Compared with dynamic programming solutions for 0/1 knapsack, this was significantly faster
Huffman Coding Findings
  • Achieved compression ratios between 1.5:1 and 4:1 depending on input characteristics
  • Text with highly skewed frequency distributions saw better compression
  • Generated codes were provably optimal for symbol-by-symbol encoding
08
Longest Common Subsequence
O(n*m)

LCS Implementation

Finding the longest subsequence common to two sequences using dynamic programming.

Algorithm Approach

The solution uses a 2D table to build up the LCS length for all prefixes of the two strings. After filling the table, the actual subsequence is constructed by backtracking.

  • Time Complexity: O(n*m) where n and m are lengths of the two sequences
  • Space Complexity: O(n*m) for the DP table

Space Optimization

I implemented both the standard O(n*m) space solution and an optimized O(min(n,m)) space version that only keeps two rows of the DP table in memory at any time.

Performance Results
  • Space-optimized version was crucial for very long sequences
  • For sequences with many matches, runtime was faster due to simpler backtracking
  • The algorithm correctly handled edge cases like empty strings and no common elements
09
N-Queens Problem
O(N!)

Backtracking Implementation

Solving the classic N-Queens puzzle of placing N queens on an N×N chess board so that no two queens attack each other.

Solution Approach

The algorithm places queens one row at a time, backtracking whenever a conflict is detected. Optimization includes efficient conflict checking by maintaining sets of occupied columns, diagonals, and anti-diagonals.

  • Time Complexity: O(N!) in worst case
  • Space Complexity: O(N) for board representation

Performance Analysis

The pruning strategy dramatically reduced the search space compared to a naive approach.

Results
  • Successfully found all solutions for N up to 12
  • For N = 8, found all 92 solutions in under 20ms
  • Optimized conflict checking reduced runtime by ~60%
10
Traveling Salesman Problem
O(n²·2ⁿ)

TSP Implementation

Finding the shortest possible route that visits each city exactly once and returns to the origin city.

Solution Approach

Implemented Held-Karp algorithm using dynamic programming with bitmasks to represent sets of visited cities.

  • Time Complexity: O(n²·2ⁿ)
  • Space Complexity: O(n·2ⁿ)

Performance Analysis

The exact DP solution was practical only for small instances (n ≤ 20). For larger instances, I implemented approximation algorithms.

Findings
  • DP solution found optimal routes for n ≤ 15 within 2 seconds
  • 2-approximation via minimum spanning tree + DFS provided good results for larger instances
  • Local search improvements (2-opt) refined approximate solutions effectively
11
Randomized Quick Sort
O(n log n)

Randomized Pivot Selection

Enhancing Quick Sort with randomized pivot selection to avoid worst-case scenarios.

Implementation Details

The algorithm randomly selects a pivot element before each partition operation, making worst-case scenarios extremely unlikely.

  • Expected Time Complexity: O(n log n)
  • Worst Case (rare): O(n²)
  • Space Complexity: O(log n) average case for recursion stack

Comparative Analysis

Compared standard Quick Sort with randomized variant across different input distributions.

Results
  • Randomized version performed consistently regardless of input order
  • Standard Quick Sort degraded severely on sorted or nearly-sorted data
  • The randomization overhead was negligible compared to benefits gained
12
String Matching Algorithms
O(n + m)

Pattern Matching Implementation

Implementing and comparing efficient string matching algorithms: Naive, KMP, and Rabin-Karp.

Pattern Matching Implementation

Implementing and comparing efficient string matching algorithms: Naive, KMP, and Rabin-Karp.

Algorithm Details

Three distinct approaches were implemented:

  • Naive Algorithm:
    • Time Complexity: O(n*m) where n is text length and m is pattern length
    • Space Complexity: O(1)
    • Approach: Simple sliding window with character-by-character comparison
  • Knuth-Morris-Pratt (KMP):
    • Time Complexity: O(n+m)
    • Space Complexity: O(m) for prefix table
    • Approach: Utilizes pattern's self-similarity to avoid redundant comparisons
  • Rabin-Karp:
    • Time Complexity: O(n+m) average, O(n*m) worst case
    • Space Complexity: O(1)
    • Approach: Uses rolling hash function to quickly compare pattern with text windows
Results Summary
  • KMP consistently outperformed the naive approach, especially for patterns with repeated characters
  • Rabin-Karp performed well for multiple pattern searching using the same hash function
  • For short patterns (m < 5), the naive algorithm was surprisingly competitive due to lower overhead
  • Implementation complexity: Naive < Rabin-Karp < KMP
Practical Applications

The implementation was tested on real-world use cases:

  • Keyword searching in large text documents
  • DNA sequence matching with biological data
  • Plagiarism detection in code samples

KMP algorithm provided the best balance of performance and reliability across these applications.

Lab Experience Reflection

Through these lab experiments, I've gained practical experience implementing a wide range of algorithms, from elementary sorting techniques to complex dynamic programming solutions. This hands-on journey has transformed abstract algorithmic concepts into tangible code, deepening my understanding of algorithm design principles and performance optimization techniques.

Key Learning Outcomes