Initial commit

This commit is contained in:
Carlos Gutierrez
2025-11-16 13:45:43 -05:00
commit e68377d6c8
17 changed files with 1660 additions and 0 deletions

13
src/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
"""
MSCS532 Assignment 5: Quicksort Implementation and Analysis
This package contains implementations of Quicksort algorithms including:
- Deterministic Quicksort
- Randomized Quicksort
- Performance comparison utilities
"""
__version__ = "1.0.0"
__author__ = "Carlos Gutierrez"
__email__ = "cgutierrez44833@ucumberlands.edu"

191
src/comparison.py Normal file
View File

@@ -0,0 +1,191 @@
"""
Performance Comparison Utilities
This module provides utilities for comparing different sorting algorithms
and analyzing their performance characteristics.
"""
import time
import random
from typing import List, Callable, Dict, Tuple, Any
from functools import wraps
import statistics
def time_function(func: Callable) -> Callable:
"""
Decorator to measure the execution time of a function.
Args:
func: The function to time
Returns:
Wrapped function that returns (result, execution_time)
"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
return result, execution_time
return wrapper
def generate_random_array(size: int, min_val: int = 0, max_val: int = 1000) -> List[int]:
"""Generate a random array of integers."""
return [random.randint(min_val, max_val) for _ in range(size)]
def generate_sorted_array(size: int, start: int = 0, step: int = 1) -> List[int]:
"""Generate a sorted array of integers."""
return list(range(start, start + size * step, step))
def generate_reverse_sorted_array(size: int, start: int = 0, step: int = 1) -> List[int]:
"""Generate a reverse-sorted array of integers."""
return list(range(start + (size - 1) * step, start - step, -step))
def generate_nearly_sorted_array(size: int, swap_count: int = 10) -> List[int]:
"""Generate a nearly sorted array with a few swaps."""
arr = list(range(size))
for _ in range(swap_count):
i = random.randint(0, size - 1)
j = random.randint(0, size - 1)
arr[i], arr[j] = arr[j], arr[i]
return arr
def generate_array_with_duplicates(size: int, unique_count: int = 10) -> List[int]:
"""Generate an array with many duplicate values."""
unique_values = list(range(unique_count))
return [random.choice(unique_values) for _ in range(size)]
def benchmark_sorting_algorithm(
sort_func: Callable[[List[Any]], Any],
array: List[Any],
iterations: int = 1
) -> Dict[str, float]:
"""
Benchmark a sorting algorithm on a given array.
Args:
sort_func: The sorting function to benchmark
array: The array to sort
iterations: Number of iterations to run (for averaging)
Returns:
Dictionary with timing statistics
"""
times = []
for _ in range(iterations):
# Create a fresh copy for each iteration
arr_copy = array.copy()
start_time = time.perf_counter()
result = sort_func(arr_copy)
end_time = time.perf_counter()
# Verify the result is sorted
sorted_arr = result if result is not None else arr_copy
if sorted_arr != sorted(array):
raise ValueError(f"Sorting function {sort_func.__name__} produced incorrect results")
times.append(end_time - start_time)
return {
'mean': statistics.mean(times),
'median': statistics.median(times),
'min': min(times),
'max': max(times),
'stdev': statistics.stdev(times) if len(times) > 1 else 0.0
}
def compare_algorithms(
algorithms: Dict[str, Callable[[List[Any]], Any]],
array_generators: Dict[str, Callable[[int], List[Any]]],
sizes: List[int],
iterations: int = 3
) -> Dict[str, Dict[str, Dict[str, float]]]:
"""
Compare multiple sorting algorithms on different input distributions and sizes.
Args:
algorithms: Dictionary mapping algorithm names to sorting functions
array_generators: Dictionary mapping distribution names to generator functions
sizes: List of array sizes to test
iterations: Number of iterations per test (for averaging)
Returns:
Nested dictionary: results[algorithm][distribution][size] = timing_stats
"""
results = {}
for algo_name, algo_func in algorithms.items():
results[algo_name] = {}
for dist_name, gen_func in array_generators.items():
results[algo_name][dist_name] = {}
for size in sizes:
print(f"Testing {algo_name} on {dist_name} array of size {size}...")
# Generate test array
test_array = gen_func(size)
# Benchmark
try:
stats = benchmark_sorting_algorithm(algo_func, test_array, iterations)
results[algo_name][dist_name][size] = stats
except Exception as e:
print(f"Error testing {algo_name} on {dist_name} size {size}: {e}")
results[algo_name][dist_name][size] = {
'mean': float('inf'),
'median': float('inf'),
'min': float('inf'),
'max': float('inf'),
'stdev': 0.0
}
return results
def format_results_table(results: Dict[str, Dict[str, Dict[str, float]]]) -> str:
"""
Format benchmark results as a readable table.
Args:
results: Results dictionary from compare_algorithms
Returns:
Formatted string table
"""
lines = []
lines.append("=" * 80)
lines.append("SORTING ALGORITHM PERFORMANCE COMPARISON")
lines.append("=" * 80)
lines.append("")
for algo_name in results:
lines.append(f"\n{algo_name.upper()}")
lines.append("-" * 80)
for dist_name in results[algo_name]:
lines.append(f"\n {dist_name}:")
lines.append(f" {'Size':<10} {'Mean (s)':<15} {'Median (s)':<15} {'Min (s)':<15} {'Max (s)':<15}")
lines.append(" " + "-" * 70)
for size in sorted(results[algo_name][dist_name].keys()):
stats = results[algo_name][dist_name][size]
lines.append(
f" {size:<10} {stats['mean']:<15.6f} {stats['median']:<15.6f} "
f"{stats['min']:<15.6f} {stats['max']:<15.6f}"
)
lines.append("\n" + "=" * 80)
return "\n".join(lines)

284
src/quicksort.py Normal file
View File

@@ -0,0 +1,284 @@
"""
Quicksort Implementation
This module provides both deterministic and randomized versions of the Quicksort algorithm.
"""
from typing import List, Callable, Optional, Any
import random
def partition(
arr: List[Any],
low: int,
high: int,
pivot_index: int,
key: Optional[Callable[[Any], Any]] = None
) -> int:
"""
Partition the array around a pivot element.
After partitioning, all elements less than the pivot are on the left,
and all elements greater than or equal to the pivot are on the right.
Args:
arr: The array to partition
low: Starting index of the subarray
high: Ending index of the subarray (inclusive)
pivot_index: Index of the pivot element
key: Optional function to extract comparison key from elements
Returns:
The final position of the pivot element after partitioning
Time Complexity: O(n) where n = high - low + 1
Space Complexity: O(1)
"""
# Move pivot to the end
arr[pivot_index], arr[high] = arr[high], arr[pivot_index]
# Get pivot value
pivot_value = key(arr[high]) if key else arr[high]
# Index of smaller element (indicates right position of pivot)
i = low - 1
for j in range(low, high):
# Compare current element with pivot
current_value = key(arr[j]) if key else arr[j]
if current_value < pivot_value:
i += 1
arr[i], arr[j] = arr[j], arr[i]
# Place pivot in its correct position
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def _quicksort_recursive(
arr: List[Any],
low: int,
high: int,
pivot_selector: Callable[[int, int], int],
key: Optional[Callable[[Any], Any]] = None
) -> None:
"""
Recursive helper function for Quicksort.
Args:
arr: The array to sort
low: Starting index
high: Ending index (inclusive)
pivot_selector: Function that takes (low, high) and returns pivot index
key: Optional function to extract comparison key from elements
"""
if low < high:
# Select pivot using the provided selector function
pivot_index = pivot_selector(low, high)
# Partition the array and get the pivot's final position
pivot_pos = partition(arr, low, high, pivot_index, key)
# Recursively sort elements before and after partition
_quicksort_recursive(arr, low, pivot_pos - 1, pivot_selector, key)
_quicksort_recursive(arr, pivot_pos + 1, high, pivot_selector, key)
def quicksort(
arr: List[Any],
in_place: bool = True,
key: Optional[Callable[[Any], Any]] = None
) -> Optional[List[Any]]:
"""
Deterministic Quicksort algorithm.
Uses the last element as the pivot (Lomuto partition scheme).
Args:
arr: The array to sort
in_place: If True, sorts the array in place and returns None.
If False, returns a new sorted array without modifying the original.
key: Optional function to extract comparison key from elements.
If provided, elements are compared using key(element).
Returns:
None if in_place=True, otherwise a new sorted list
Time Complexity:
- Best case: O(n log n) - balanced partitions
- Average case: O(n log n) - expected balanced partitions
- Worst case: O(n²) - highly unbalanced partitions (e.g., sorted array)
Space Complexity:
- Best case: O(log n) - balanced recursion stack
- Average case: O(log n) - expected balanced recursion stack
- Worst case: O(n) - highly unbalanced recursion stack
Example:
>>> arr = [3, 6, 8, 10, 1, 2, 1]
>>> quicksort(arr)
>>> arr
[1, 1, 2, 3, 6, 8, 10]
>>> arr = [3, 6, 8, 10, 1, 2, 1]
>>> sorted_arr = quicksort(arr, in_place=False)
>>> sorted_arr
[1, 1, 2, 3, 6, 8, 10]
>>> arr # Original unchanged
[3, 6, 8, 10, 1, 2, 1]
"""
if not arr:
return None if in_place else []
if in_place:
# Use last element as pivot (deterministic)
pivot_selector = lambda low, high: high
_quicksort_recursive(arr, 0, len(arr) - 1, pivot_selector, key)
return None
else:
# Create a copy to avoid modifying the original
arr_copy = arr.copy()
pivot_selector = lambda low, high: high
_quicksort_recursive(arr_copy, 0, len(arr_copy) - 1, pivot_selector, key)
return arr_copy
def randomized_quicksort(
arr: List[Any],
in_place: bool = True,
key: Optional[Callable[[Any], Any]] = None,
seed: Optional[int] = None
) -> Optional[List[Any]]:
"""
Randomized Quicksort algorithm.
Uses a randomly selected element as the pivot, which helps avoid worst-case
performance on sorted or nearly sorted inputs.
Args:
arr: The array to sort
in_place: If True, sorts the array in place and returns None.
If False, returns a new sorted array without modifying the original.
key: Optional function to extract comparison key from elements.
If provided, elements are compared using key(element).
seed: Optional random seed for reproducibility
Returns:
None if in_place=True, otherwise a new sorted list
Time Complexity:
- Best case: O(n log n) - balanced partitions
- Average case: O(n log n) - expected balanced partitions with high probability
- Worst case: O(n²) - still possible but extremely unlikely with randomization
Space Complexity:
- Best case: O(log n) - balanced recursion stack
- Average case: O(log n) - expected balanced recursion stack
- Worst case: O(n) - highly unbalanced recursion stack (very unlikely)
Example:
>>> arr = [3, 6, 8, 10, 1, 2, 1]
>>> randomized_quicksort(arr, seed=42)
>>> arr
[1, 1, 2, 3, 6, 8, 10]
>>> arr = [3, 6, 8, 10, 1, 2, 1]
>>> sorted_arr = randomized_quicksort(arr, in_place=False, seed=42)
>>> sorted_arr
[1, 1, 2, 3, 6, 8, 10]
"""
if not arr:
return None if in_place else []
if seed is not None:
random.seed(seed)
if in_place:
# Use random element as pivot
pivot_selector = lambda low, high: random.randint(low, high)
_quicksort_recursive(arr, 0, len(arr) - 1, pivot_selector, key)
return None
else:
# Create a copy to avoid modifying the original
arr_copy = arr.copy()
pivot_selector = lambda low, high: random.randint(low, high)
_quicksort_recursive(arr_copy, 0, len(arr_copy) - 1, pivot_selector, key)
return arr_copy
def quicksort_3way(
arr: List[Any],
in_place: bool = True,
key: Optional[Callable[[Any], Any]] = None
) -> Optional[List[Any]]:
"""
Three-way Quicksort (Dutch National Flag algorithm variant).
Efficiently handles arrays with many duplicate elements by partitioning
into three parts: elements less than, equal to, and greater than the pivot.
Args:
arr: The array to sort
in_place: If True, sorts the array in place and returns None.
If False, returns a new sorted array without modifying the original.
key: Optional function to extract comparison key from elements.
Returns:
None if in_place=True, otherwise a new sorted list
Time Complexity:
- Best case: O(n) - when all elements are equal
- Average case: O(n log n)
- Worst case: O(n²) - but rare with good pivot selection
Example:
>>> arr = [3, 2, 3, 1, 3, 2, 1]
>>> quicksort_3way(arr)
>>> arr
[1, 1, 2, 2, 3, 3, 3]
"""
if not arr:
return None if in_place else []
def _3way_partition(low: int, high: int) -> tuple[int, int]:
"""Three-way partition: returns (lt, gt) indices."""
if low >= high:
return low, high
pivot_value = key(arr[high]) if key else arr[high]
lt = low # arr[low..lt-1] < pivot
i = low # arr[lt..i-1] == pivot
gt = high # arr[gt+1..high] > pivot
while i <= gt:
current_value = key(arr[i]) if key else arr[i]
if current_value < pivot_value:
arr[lt], arr[i] = arr[i], arr[lt]
lt += 1
i += 1
elif current_value > pivot_value:
arr[i], arr[gt] = arr[gt], arr[i]
gt -= 1
else:
i += 1
return lt, gt
def _3way_quicksort_recursive(low: int, high: int) -> None:
if low < high:
lt, gt = _3way_partition(low, high)
_3way_quicksort_recursive(low, lt - 1)
_3way_quicksort_recursive(gt + 1, high)
if in_place:
_3way_quicksort_recursive(0, len(arr) - 1)
return None
else:
arr_copy = arr.copy()
# Temporarily replace arr to use in recursive function
original_arr = arr
arr = arr_copy
_3way_quicksort_recursive(0, len(arr) - 1)
arr = original_arr
return arr_copy