Initial commit
This commit is contained in:
13
src/__init__.py
Normal file
13
src/__init__.py
Normal 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
191
src/comparison.py
Normal 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
284
src/quicksort.py
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user