Path: blob/main/latex-templates/templates/computer-science/sorting_algorithms.tex
51 views
unlisted
\documentclass[a4paper, 11pt]{article}1\usepackage[utf8]{inputenc}2\usepackage[T1]{fontenc}3\usepackage{amsmath, amssymb}4\usepackage{graphicx}5\usepackage{siunitx}6\usepackage{booktabs}7\usepackage{float}8\usepackage[makestderr]{pythontex}910\title{Computer Science: Sorting Algorithm Analysis and Comparison}11\author{Computational Science Templates}12\date{\today}1314\begin{document}15\maketitle1617\begin{abstract}18This document presents a comprehensive analysis of sorting algorithms including comparison-based sorts (QuickSort, MergeSort, HeapSort, InsertionSort, BubbleSort) and non-comparison sorts (CountingSort, RadixSort). We implement each algorithm in Python, measure their empirical performance across different input sizes and distributions, analyze time and space complexity, and compare their stability and practical applicability. The analysis demonstrates the trade-offs between different sorting strategies and guides algorithm selection for specific use cases.19\end{abstract}2021\section{Introduction}22Sorting is one of the most fundamental operations in computer science, with applications ranging from database operations to computational geometry. Understanding the theoretical complexity and practical performance of different sorting algorithms is essential for efficient algorithm design and selection.2324\section{Mathematical Framework}2526\subsection{Comparison-Based Sorting Lower Bound}27Any comparison-based sorting algorithm requires at least $\Omega(n \log n)$ comparisons in the worst case, as proven by the decision tree model.2829\subsection{Time Complexity Summary}30\begin{align}31\text{QuickSort: } &O(n \log n) \text{ average}, O(n^2) \text{ worst} \\32\text{MergeSort: } &O(n \log n) \text{ all cases} \\33\text{HeapSort: } &O(n \log n) \text{ all cases} \\34\text{InsertionSort: } &O(n^2) \text{ worst}, O(n) \text{ best} \\35\text{BubbleSort: } &O(n^2) \text{ all cases} \\36\text{CountingSort: } &O(n + k) \text{ where } k \text{ is range} \\37\text{RadixSort: } &O(d(n + k)) \text{ where } d \text{ is digits}38\end{align}3940\section{Computational Analysis}4142\begin{pycode}43import numpy as np44import time45import matplotlib.pyplot as plt46import sys47sys.setrecursionlimit(10000)48plt.rc('text', usetex=True)49plt.rc('font', family='serif')5051np.random.seed(42)5253# Store results54results = {}5556# Sorting algorithm implementations57def quicksort(arr):58"""QuickSort with median-of-three pivot selection"""59if len(arr) <= 1:60return arr61if len(arr) <= 10:62return insertion_sort(arr)6364# Median-of-three pivot65mid = len(arr) // 266if arr[0] > arr[mid]:67arr[0], arr[mid] = arr[mid], arr[0]68if arr[0] > arr[-1]:69arr[0], arr[-1] = arr[-1], arr[0]70if arr[mid] > arr[-1]:71arr[mid], arr[-1] = arr[-1], arr[mid]72pivot = arr[mid]7374left = [x for x in arr if x < pivot]75middle = [x for x in arr if x == pivot]76right = [x for x in arr if x > pivot]77return quicksort(left) + middle + quicksort(right)7879def mergesort(arr):80"""MergeSort - stable, O(n log n)"""81if len(arr) <= 1:82return arr83mid = len(arr) // 284left = mergesort(arr[:mid])85right = mergesort(arr[mid:])86return merge(left, right)8788def merge(left, right):89result = []90i = j = 091while i < len(left) and j < len(right):92if left[i] <= right[j]:93result.append(left[i])94i += 195else:96result.append(right[j])97j += 198result.extend(left[i:])99result.extend(right[j:])100return result101102def heapsort(arr):103"""HeapSort - in-place, O(n log n)"""104arr = arr.copy()105n = len(arr)106107def heapify(arr, n, i):108largest = i109left = 2 * i + 1110right = 2 * i + 2111if left < n and arr[left] > arr[largest]:112largest = left113if right < n and arr[right] > arr[largest]:114largest = right115if largest != i:116arr[i], arr[largest] = arr[largest], arr[i]117heapify(arr, n, largest)118119# Build max heap120for i in range(n // 2 - 1, -1, -1):121heapify(arr, n, i)122123# Extract elements124for i in range(n - 1, 0, -1):125arr[0], arr[i] = arr[i], arr[0]126heapify(arr, i, 0)127128return arr129130def insertion_sort(arr):131"""InsertionSort - stable, adaptive"""132arr = arr.copy()133for i in range(1, len(arr)):134key = arr[i]135j = i - 1136while j >= 0 and arr[j] > key:137arr[j + 1] = arr[j]138j -= 1139arr[j + 1] = key140return arr141142def bubble_sort(arr):143"""BubbleSort - stable, simple"""144arr = arr.copy()145n = len(arr)146for i in range(n):147swapped = False148for j in range(0, n - i - 1):149if arr[j] > arr[j + 1]:150arr[j], arr[j + 1] = arr[j + 1], arr[j]151swapped = True152if not swapped:153break154return arr155156def counting_sort(arr):157"""CountingSort - stable, O(n+k)"""158if not arr:159return arr160min_val, max_val = min(arr), max(arr)161range_val = max_val - min_val + 1162count = [0] * range_val163output = [0] * len(arr)164165for num in arr:166count[num - min_val] += 1167168for i in range(1, len(count)):169count[i] += count[i - 1]170171for num in reversed(arr):172output[count[num - min_val] - 1] = num173count[num - min_val] -= 1174175return output176177def radix_sort(arr):178"""RadixSort using CountingSort as subroutine"""179if not arr:180return arr181max_val = max(arr)182exp = 1183arr = arr.copy()184185while max_val // exp > 0:186arr = counting_sort_radix(arr, exp)187exp *= 10188189return arr190191def counting_sort_radix(arr, exp):192n = len(arr)193output = [0] * n194count = [0] * 10195196for num in arr:197index = (num // exp) % 10198count[index] += 1199200for i in range(1, 10):201count[i] += count[i - 1]202203for i in range(n - 1, -1, -1):204index = (arr[i] // exp) % 10205output[count[index] - 1] = arr[i]206count[index] -= 1207208return output209\end{pycode}210211\subsection{Performance Measurement}212213\begin{pycode}214# Test configurations215sizes = [100, 500, 1000, 2000, 5000]216algorithms = {217'QuickSort': quicksort,218'MergeSort': mergesort,219'HeapSort': heapsort,220'InsertionSort': insertion_sort,221}222223# Performance measurement224performance = {name: [] for name in algorithms}225performance['BubbleSort'] = []226227for n in sizes:228arr = list(np.random.randint(0, 10000, n))229230for name, func in algorithms.items():231t0 = time.time()232func(arr.copy())233performance[name].append((time.time() - t0) * 1000)234235# BubbleSort only for smaller sizes236if n <= 2000:237t0 = time.time()238bubble_sort(arr.copy())239performance['BubbleSort'].append((time.time() - t0) * 1000)240else:241performance['BubbleSort'].append(np.nan)242243# Store results for reporting244results['quick_1000'] = performance['QuickSort'][2]245results['merge_1000'] = performance['MergeSort'][2]246results['heap_1000'] = performance['HeapSort'][2]247results['insertion_1000'] = performance['InsertionSort'][2]248results['bubble_1000'] = performance['BubbleSort'][2]249\end{pycode}250251\subsection{Algorithm Comparison Visualization}252253\begin{pycode}254fig, axes = plt.subplots(2, 3, figsize=(15, 10))255256# Linear scale comparison257ax1 = axes[0, 0]258for name in ['QuickSort', 'MergeSort', 'HeapSort']:259ax1.plot(sizes, performance[name], 'o-', linewidth=2, markersize=6, label=name)260ax1.set_xlabel('Array Size')261ax1.set_ylabel('Time (ms)')262ax1.set_title('$O(n \\log n)$ Algorithms')263ax1.legend()264ax1.grid(True, alpha=0.3)265266# O(n^2) algorithms267ax2 = axes[0, 1]268ax2.plot(sizes, performance['InsertionSort'], 'o-', linewidth=2, label='InsertionSort')269ax2.plot(sizes[:4], performance['BubbleSort'][:4], 's-', linewidth=2, label='BubbleSort')270ax2.set_xlabel('Array Size')271ax2.set_ylabel('Time (ms)')272ax2.set_title('$O(n^2)$ Algorithms')273ax2.legend()274ax2.grid(True, alpha=0.3)275276# Log-log scale277ax3 = axes[0, 2]278for name in ['QuickSort', 'MergeSort', 'InsertionSort']:279ax3.loglog(sizes, performance[name], 'o-', linewidth=2, label=name)280# Theoretical curves281n_theory = np.array(sizes)282nlogn = n_theory * np.log(n_theory)283nlogn = nlogn / nlogn[2] * performance['QuickSort'][2]284n2 = n_theory**2285n2 = n2 / n2[2] * performance['InsertionSort'][2]286ax3.loglog(n_theory, nlogn, 'k--', alpha=0.5, label='$O(n\\log n)$')287ax3.loglog(n_theory, n2, 'k:', alpha=0.5, label='$O(n^2)$')288ax3.set_xlabel('Array Size')289ax3.set_ylabel('Time (ms)')290ax3.set_title('Log-Log Scale Comparison')291ax3.legend()292ax3.grid(True, alpha=0.3)293294# Best/Average/Worst case for QuickSort295ax4 = axes[1, 0]296quick_random = []297quick_sorted = []298quick_reverse = []299300for n in sizes:301# Random302arr = list(np.random.randint(0, 10000, n))303t0 = time.time()304quicksort(arr)305quick_random.append((time.time() - t0) * 1000)306307# Nearly sorted308arr = list(range(n))309np.random.shuffle(arr[:n//10])310t0 = time.time()311quicksort(arr)312quick_sorted.append((time.time() - t0) * 1000)313314# Reverse sorted (worst for naive quicksort, but our version handles it)315arr = list(range(n, 0, -1))316t0 = time.time()317quicksort(arr)318quick_reverse.append((time.time() - t0) * 1000)319320ax4.plot(sizes, quick_random, 'b-o', linewidth=2, label='Random')321ax4.plot(sizes, quick_sorted, 'g-s', linewidth=2, label='Nearly Sorted')322ax4.plot(sizes, quick_reverse, 'r-^', linewidth=2, label='Reverse')323ax4.set_xlabel('Array Size')324ax4.set_ylabel('Time (ms)')325ax4.set_title('QuickSort: Input Distribution Effect')326ax4.legend()327ax4.grid(True, alpha=0.3)328329# InsertionSort on different distributions330ax5 = axes[1, 1]331ins_random = []332ins_sorted = []333ins_reverse = []334335for n in sizes[:4]: # Limit size for O(n^2)336# Random337arr = list(np.random.randint(0, 10000, n))338t0 = time.time()339insertion_sort(arr)340ins_random.append((time.time() - t0) * 1000)341342# Sorted343arr = list(range(n))344t0 = time.time()345insertion_sort(arr)346ins_sorted.append((time.time() - t0) * 1000)347348# Reverse349arr = list(range(n, 0, -1))350t0 = time.time()351insertion_sort(arr)352ins_reverse.append((time.time() - t0) * 1000)353354ax5.plot(sizes[:4], ins_random, 'b-o', linewidth=2, label='Random')355ax5.plot(sizes[:4], ins_sorted, 'g-s', linewidth=2, label='Sorted')356ax5.plot(sizes[:4], ins_reverse, 'r-^', linewidth=2, label='Reverse')357ax5.set_xlabel('Array Size')358ax5.set_ylabel('Time (ms)')359ax5.set_title('InsertionSort: Input Distribution Effect')360ax5.legend()361ax5.grid(True, alpha=0.3)362363# Speedup over BubbleSort364ax6 = axes[1, 2]365speedups = {}366for name in ['QuickSort', 'MergeSort', 'HeapSort']:367speedups[name] = [performance['BubbleSort'][i] / performance[name][i]368if not np.isnan(performance['BubbleSort'][i]) else np.nan369for i in range(len(sizes))]370ax6.plot(sizes[:4], speedups[name][:4], 'o-', linewidth=2, label=name)371372ax6.set_xlabel('Array Size')373ax6.set_ylabel('Speedup vs BubbleSort')374ax6.set_title('Algorithm Speedup')375ax6.legend()376ax6.grid(True, alpha=0.3)377378results['quicksort_speedup_2000'] = speedups['QuickSort'][3]379380plt.tight_layout()381plt.savefig('sorting_comparison.pdf', bbox_inches='tight', dpi=150)382print(r'\begin{figure}[H]')383print(r'\centering')384print(r'\includegraphics[width=\textwidth]{sorting_comparison.pdf}')385print(r'\caption{Sorting algorithm comparison: (a) $O(n\log n)$ algorithms, (b) $O(n^2)$ algorithms, (c) log-log scale, (d) QuickSort distributions, (e) InsertionSort distributions, (f) speedup analysis.}')386print(r'\label{fig:comparison}')387print(r'\end{figure}')388plt.close()389\end{pycode}390391\subsection{Non-Comparison Based Sorting}392393\begin{pycode}394# Test non-comparison sorts395sizes_nc = [1000, 5000, 10000, 50000, 100000]396counting_times = []397radix_times = []398quick_times_large = []399400for n in sizes_nc:401arr = list(np.random.randint(0, 10000, n))402403# CountingSort404t0 = time.time()405counting_sort(arr)406counting_times.append((time.time() - t0) * 1000)407408# RadixSort409t0 = time.time()410radix_sort(arr)411radix_times.append((time.time() - t0) * 1000)412413# QuickSort for comparison414t0 = time.time()415quicksort(arr)416quick_times_large.append((time.time() - t0) * 1000)417418fig, axes = plt.subplots(1, 2, figsize=(12, 5))419420# Linear time sorts421ax1 = axes[0]422ax1.plot(sizes_nc, counting_times, 'b-o', linewidth=2, label='CountingSort')423ax1.plot(sizes_nc, radix_times, 'g-s', linewidth=2, label='RadixSort')424ax1.plot(sizes_nc, quick_times_large, 'r-^', linewidth=2, label='QuickSort')425ax1.set_xlabel('Array Size')426ax1.set_ylabel('Time (ms)')427ax1.set_title('Non-Comparison vs Comparison Sorts')428ax1.legend()429ax1.grid(True, alpha=0.3)430431results['counting_100k'] = counting_times[-1]432results['radix_100k'] = radix_times[-1]433results['quick_100k'] = quick_times_large[-1]434435# Effect of value range on CountingSort436ax2 = axes[1]437ranges = [100, 1000, 10000, 100000, 1000000]438counting_range_times = []439n_fixed = 10000440441for r in ranges:442arr = list(np.random.randint(0, r, n_fixed))443t0 = time.time()444counting_sort(arr)445counting_range_times.append((time.time() - t0) * 1000)446447ax2.semilogx(ranges, counting_range_times, 'b-o', linewidth=2)448ax2.set_xlabel('Value Range (k)')449ax2.set_ylabel('Time (ms)')450ax2.set_title(f'CountingSort: Effect of Range (n={n_fixed})')451ax2.grid(True, alpha=0.3)452453plt.tight_layout()454plt.savefig('sorting_linear.pdf', bbox_inches='tight', dpi=150)455print(r'\begin{figure}[H]')456print(r'\centering')457print(r'\includegraphics[width=\textwidth]{sorting_linear.pdf}')458print(r'\caption{Non-comparison sorting: (a) performance comparison with QuickSort, (b) CountingSort dependence on value range.}')459print(r'\label{fig:linear}')460print(r'\end{figure}')461plt.close()462\end{pycode}463464\subsection{Stability and Memory Analysis}465466\begin{pycode}467# Demonstrate stability468def test_stability(sort_func, name):469"""Test if sorting is stable using (value, index) pairs"""470arr = [(3, 'a'), (1, 'b'), (3, 'c'), (2, 'd'), (1, 'e')]471# Sort by first element472if name in ['CountingSort', 'RadixSort']:473values = [x[0] for x in arr]474sorted_vals = sort_func(values)475return "N/A" # Can't easily test with current implementation476477try:478sorted_arr = sort_func([x[0] for x in arr])479return "Stable" if sorted_arr == sorted([x[0] for x in arr]) else "Unstable"480except:481return "N/A"482483# Memory usage comparison (theoretical)484memory_complexity = {485'QuickSort': '$O(\\log n)$',486'MergeSort': '$O(n)$',487'HeapSort': '$O(1)$',488'InsertionSort': '$O(1)$',489'BubbleSort': '$O(1)$',490'CountingSort': '$O(n + k)$',491'RadixSort': '$O(n + k)$'492}493494stability = {495'QuickSort': 'No',496'MergeSort': 'Yes',497'HeapSort': 'No',498'InsertionSort': 'Yes',499'BubbleSort': 'Yes',500'CountingSort': 'Yes',501'RadixSort': 'Yes'502}503504# Visualization of sorting process505fig, axes = plt.subplots(2, 3, figsize=(15, 10))506507# Sample array for visualization508sample = list(np.random.randint(1, 50, 20))509510# Before sorting511ax1 = axes[0, 0]512ax1.bar(range(len(sample)), sample, color='steelblue', edgecolor='black', alpha=0.7)513ax1.set_xlabel('Index')514ax1.set_ylabel('Value')515ax1.set_title('Unsorted Array')516ax1.grid(True, alpha=0.3)517518# After QuickSort519ax2 = axes[0, 1]520sorted_sample = quicksort(sample)521colors = plt.cm.viridis(np.linspace(0, 1, len(sorted_sample)))522ax2.bar(range(len(sorted_sample)), sorted_sample, color=colors, edgecolor='black')523ax2.set_xlabel('Index')524ax2.set_ylabel('Value')525ax2.set_title('After Sorting')526ax2.grid(True, alpha=0.3)527528# Comparison count estimation529ax3 = axes[0, 2]530n_vals = np.array([10, 50, 100, 500, 1000])531comparisons_nlogn = n_vals * np.log2(n_vals)532comparisons_n2 = n_vals * (n_vals - 1) / 2533ax3.semilogy(n_vals, comparisons_nlogn, 'b-o', linewidth=2, label='$O(n\\log n)$')534ax3.semilogy(n_vals, comparisons_n2, 'r-s', linewidth=2, label='$O(n^2)$')535ax3.set_xlabel('Array Size')536ax3.set_ylabel('Number of Comparisons')537ax3.set_title('Theoretical Comparison Count')538ax3.legend()539ax3.grid(True, alpha=0.3)540541# Small array performance (where O(n^2) can win)542ax4 = axes[1, 0]543small_sizes = [5, 10, 20, 30, 50, 100]544small_quick = []545small_insertion = []546small_merge = []547548for n in small_sizes:549arr = list(np.random.randint(0, 1000, n))550551# Multiple runs for small arrays552runs = 100553t0 = time.time()554for _ in range(runs):555quicksort(arr.copy())556small_quick.append((time.time() - t0) / runs * 1000)557558t0 = time.time()559for _ in range(runs):560insertion_sort(arr.copy())561small_insertion.append((time.time() - t0) / runs * 1000)562563t0 = time.time()564for _ in range(runs):565mergesort(arr.copy())566small_merge.append((time.time() - t0) / runs * 1000)567568ax4.plot(small_sizes, small_quick, 'b-o', linewidth=2, label='QuickSort')569ax4.plot(small_sizes, small_insertion, 'g-s', linewidth=2, label='InsertionSort')570ax4.plot(small_sizes, small_merge, 'r-^', linewidth=2, label='MergeSort')571ax4.set_xlabel('Array Size')572ax4.set_ylabel('Time (ms)')573ax4.set_title('Small Array Performance')574ax4.legend()575ax4.grid(True, alpha=0.3)576577# Nearly sorted arrays (best case for adaptive sorts)578ax5 = axes[1, 1]579nearly_sizes = [100, 500, 1000, 2000]580swap_percentages = []581582for n in nearly_sizes:583# Create nearly sorted array584arr = list(range(n))585# Swap 5% of elements586num_swaps = n // 20587for _ in range(num_swaps):588i, j = np.random.randint(0, n, 2)589arr[i], arr[j] = arr[j], arr[i]590591t_ins = time.time()592insertion_sort(arr.copy())593t_ins = (time.time() - t_ins) * 1000594595t_quick = time.time()596quicksort(arr.copy())597t_quick = (time.time() - t_quick) * 1000598599swap_percentages.append((n, t_ins, t_quick))600601ns, t_ins_list, t_quick_list = zip(*swap_percentages)602ax5.plot(ns, t_ins_list, 'g-o', linewidth=2, label='InsertionSort')603ax5.plot(ns, t_quick_list, 'b-s', linewidth=2, label='QuickSort')604ax5.set_xlabel('Array Size')605ax5.set_ylabel('Time (ms)')606ax5.set_title('Nearly Sorted Arrays (5\\% swapped)')607ax5.legend()608ax5.grid(True, alpha=0.3)609610# Algorithm selection guide611ax6 = axes[1, 2]612scenarios = ['Small\n(<50)', 'General\nPurpose', 'Stable\nNeeded', 'Memory\nLimited', 'Integer\nKeys']613recommendations = ['Insertion', 'Quick', 'Merge', 'Heap', 'Radix']614colors_rec = ['#FF9999', '#99FF99', '#9999FF', '#FFFF99', '#FF99FF']615bars = ax6.bar(scenarios, [1]*5, color=colors_rec, edgecolor='black')616ax6.set_ylabel('Recommendation')617ax6.set_title('Algorithm Selection Guide')618for bar, rec in zip(bars, recommendations):619height = bar.get_height()620ax6.text(bar.get_x() + bar.get_width()/2., height/2,621rec, ha='center', va='center', fontsize=10, fontweight='bold')622ax6.set_ylim(0, 1.2)623ax6.set_yticks([])624625plt.tight_layout()626plt.savefig('sorting_analysis.pdf', bbox_inches='tight', dpi=150)627print(r'\begin{figure}[H]')628print(r'\centering')629print(r'\includegraphics[width=\textwidth]{sorting_analysis.pdf}')630print(r'\caption{Sorting analysis: (a) unsorted array, (b) sorted array, (c) comparison counts, (d) small array performance, (e) nearly sorted arrays, (f) selection guide.}')631print(r'\label{fig:analysis}')632print(r'\end{figure}')633plt.close()634\end{pycode}635636\section{Results and Discussion}637638\subsection{Performance at n=1000}639640\begin{table}[H]641\centering642\caption{Sorting Algorithm Performance (n=1000)}643\label{tab:performance}644\begin{tabular}{lcccc}645\toprule646\textbf{Algorithm} & \textbf{Time (ms)} & \textbf{Complexity} & \textbf{Stable} & \textbf{Space} \\647\midrule648QuickSort & \py{f"{results['quick_1000']:.3f}"} & $O(n \log n)$ & No & $O(\log n)$ \\649MergeSort & \py{f"{results['merge_1000']:.3f}"} & $O(n \log n)$ & Yes & $O(n)$ \\650HeapSort & \py{f"{results['heap_1000']:.3f}"} & $O(n \log n)$ & No & $O(1)$ \\651InsertionSort & \py{f"{results['insertion_1000']:.3f}"} & $O(n^2)$ & Yes & $O(1)$ \\652BubbleSort & \py{f"{results['bubble_1000']:.3f}"} & $O(n^2)$ & Yes & $O(1)$ \\653\bottomrule654\end{tabular}655\end{table}656657\subsection{Large Scale Performance (n=100,000)}658659For large arrays with integer keys:660\begin{itemize}661\item CountingSort: \py{f"{results['counting_100k']:.2f}"} ms662\item RadixSort: \py{f"{results['radix_100k']:.2f}"} ms663\item QuickSort: \py{f"{results['quick_100k']:.2f}"} ms664\end{itemize}665666\subsection{Speedup Analysis}667668At n=2000, QuickSort achieves \py{f"{results['quicksort_speedup_2000']:.1f}"}x speedup over BubbleSort.669670\subsection{Algorithm Selection Guidelines}671672\begin{enumerate}673\item \textbf{General purpose}: QuickSort (fast average case, good cache performance)674\item \textbf{Guaranteed $O(n \log n)$}: MergeSort or HeapSort675\item \textbf{Stability required}: MergeSort676\item \textbf{Memory constrained}: HeapSort ($O(1)$ space)677\item \textbf{Small arrays}: InsertionSort (low overhead)678\item \textbf{Nearly sorted}: InsertionSort ($O(n)$ best case)679\item \textbf{Integer keys, limited range}: CountingSort or RadixSort680\end{enumerate}681682\section{Conclusion}683This analysis demonstrated the implementation and comparison of major sorting algorithms. Key findings include:684\begin{enumerate}685\item $O(n \log n)$ algorithms dramatically outperform $O(n^2)$ algorithms for large inputs686\item QuickSort typically provides the best practical performance due to cache efficiency687\item MergeSort guarantees $O(n \log n)$ and stability but requires $O(n)$ extra space688\item HeapSort provides $O(n \log n)$ with $O(1)$ space but poor cache performance689\item InsertionSort excels for small or nearly sorted arrays690\item Non-comparison sorts achieve $O(n)$ for integer keys with limited range691\item Algorithm selection depends on input characteristics, memory constraints, and stability requirements692\end{enumerate}693694Understanding these trade-offs enables optimal algorithm selection for specific use cases.695696\end{document}697698699