Theoretical Bounds

Merge Sort

  • Worst-case: \(O(N \log N)\)
  • Average-case: \(O(N \log N)\)
  • Recurrence relation:

    \[T(n) = 2T\left(\frac{n}{2}\right) + O(n)\]

Quicksort

  • Worst-case: \(O(N^2)\)
  • Average-case: \(O(N \log N)\)
  • Recurrence relation:

    \[T(n) = T(k) + T(n-k-1) + O(n)\]

The Statistics Connection

  • Quicksort’s performance relies heavily on random variables

  • We must measure its variance to understand its reliability

  • To measure the fluctuation across multiple runs, we use the sample standard deviation formula:

\(s = \sqrt{\frac{1}{n-1} \sum_{i=1}^{n} (x_i - \bar{x})^2}\)
  • \(n\): The number of times the benchmark was executed.
  • \(x_i\): The exact execution time of a single specific Quicksort run.
  • \(\bar{x}\): The average execution time calculated across all \(n\) runs.
  • \(n-1\): Bessel’s Correction, applied because we are only testing a sample of all possible array permutations.

Statistical Certainty

Unlike Quicksort, Merge Sort’s execution is strictly deterministic.

  • Consistent Partitioning: It always divides the dataset exactly in half, regardless of the initial array disorder.
  • Minimal Algorithmic Variance: Because the number of operations is identical for any input of size \(N\), the algorithm’s internal variance is theoretically \(0\).
  • Hardware Noise: Any minor fluctuations observed in runtimes will be due to hardware interference.
  • The Trade-off: Statistical reliability comes at a hardware cost, requiring \(O(N)\) auxiliary memory for array allocations.

Benchmarking Methodology

To better represent real world data, which often isn’t fully unordered (where Quicksort performs best), we run 100 trials on arrays of size \(N = 2,000\), shifting the input structure on a sliding scale from nearly random to perfectly sorted.

N <- 2000
runtime_df <- data.frame()

#run 100 trials
for (i in 1:100) {
  
  # define the scale from 1% sorted (i=1) to 100% sorted (i=100)
  percent_sorted <- i / 100
  num_sorted <- floor(N * percent_sorted)
  
  # build the array: a sorted block followed by a randomized block
  test_array <- c(1:num_sorted, sample((num_sorted + 1):N))
  
  # record the execution times (milliseconds)
  t_merge <- system.time(merge_sort(test_array))["elapsed"] * 1000
  t_quick <- system.time(quick_sort(test_array))["elapsed"] * 1000
  
  # bind results to dataframe for plotting
  runtime_df <- rbind(runtime_df, data.frame(
    Algorithm = c("merge_sort", "quick_sort"), 
    Time = c(t_merge, t_quick)))
}

Distribution of Execution Times

Holding the input size constant at \(N = 2,000\) across 100 independent runs on structured arrays reveals the true statistical spread of each algorithm.

Benchmarking Methodology (varied N)

To analyze how the algorithms scale, we programmatically loop through varying input sizes (\(N\)) and capture their execution distributions.

library(microbenchmark)

# define input sizes
sizes <- c(1000, 2000, 3000, 4000, 5000)
growth_df <- data.frame()

for (n in sizes) {
  test_array <- sample(1:n)
  
  # benchmark merge sort vs quick sort
  mb <- microbenchmark(
    merge_sort = merge_sort(test_array),
    quick_sort = quick_sort(test_array),
    times = 20 ) # 20 times per N
  
  # store results for visualization
  growth_df <- rbind(growth_df, data.frame(
    N = n, 
    Algorithm = mb$expr, 
    Time = mb$time / 1e6))
}

Empirical Growth Rates

Merge Sort and Quicksort share an \(O(N \log N)\) average-case time complexity on randomized arrays, but Quicksort typically has a smaller constant factor, making it faster in practice.

library(plotly)
library(htmlwidgets)

sizes <- seq(1000, 3000, by = 500)
sorted_ratios <- seq(0, 1, by = 0.2)
plot_data <- data.frame()

for(n in sizes) {
  for(p in sorted_ratios) {
    num_sorted <- floor(n * p)
    random_part <- c()
    if (n - num_sorted > 0) {
      random_part <- (num_sorted + 1):n
      if (length(random_part) > 1) random_part <- sample(random_part)
    }
    test_arr <- c(if(num_sorted > 0) 1:num_sorted else c(), random_part)
    
    # run each algorithm 15 times and take the median to filter out OS noise
    t_merge <- median(replicate(15, system.time(merge_sort(test_arr))["elapsed"] * 1000))
    t_quick <- median(replicate(15, system.time(quick_sort(test_arr))["elapsed"] * 1000))
    
    plot_data <- rbind(plot_data, 
                       data.frame(N = n, Sortedness = p, Time = t_merge, Algorithm = "merge_sort"),
                       data.frame(N = n, Sortedness = p, Time = t_quick, Algorithm = "quick_sort"))
  }
}

p <- plot_ly(data = plot_data, x = ~N, y = ~Sortedness, z = ~Time, color = ~Algorithm, 
        colors = c("#8C1D40", "#FFC627"),
        type = "scatter3d", mode = "markers",
        marker = list(size = 6, opacity = 0.9),
        width = 700, height = 450) %>%
  layout(scene = list(
           xaxis = list(title = 'Input Size (N)'),
           yaxis = list(title = 'Ratio Sorted'),
           zaxis = list(title = 'Time (ms)')
         ),
         margin = list(l = 0, r = 0, b = 0, t = 0))

# save the plot independently (because it takes a while to generate)
saveWidget(p, file = "plotly_3d_isolated.html", selfcontained = TRUE)

Plotting the data together in 3D

As scale and sortedness increase, Merge Sort remains a predictable, flat plane, while Quicksort degrades sharply.

Conclusion: The Value of Statistical Benchmarking

Rather than relying purely on Big-O notation and its implications, our benchmarking reveals how applying statistical analysis can provide a much clearer picture of an algorithm’s strengths and weaknesses.

  • Exposing the Constant Factor: Abstract theory drops constant factors, but the statistical median of our randomized runs quantifies Quicksort’s baseline speed advantage in unstructured environments.
  • Mapping the Tipping Point: While Big-O broadly states Quicksort degrades to \(O(N^2)\), our 3D surface mapping visualizes the exact structural threshold where input sortedness causes that performance collapse.
  • Isolating True Behavior: By utilizing variance metrics and replicating runs to calculate the median, we successfully filtered out random operating system noise to reveal concrete, constructive execution trends rather than theoretical assumptions.