Case Study 1: Benchmarking Sort Algorithms
The Question
Every textbook tells you that merge sort is O(n log n) and selection sort is O(n²). But what does that mean in practice? How much faster is one algorithm than another on real data, on real hardware? This case study answers that question by benchmarking four sorting algorithms on arrays of different sizes.
The Benchmarking Program
We measure wall-clock time and comparison counts for selection sort, insertion sort, merge sort, and quicksort on arrays ranging from 100 to 100,000 elements.
program SortBenchmark;
uses
SysUtils, DateUtils;
var
CompCount: Int64; { Global comparison counter }
{ --- Sorting Algorithms with Counting --- }
procedure SelectionSortCounted(var Arr: array of Integer; N: Integer);
var
I, J, MinIdx, Temp: Integer;
begin
for I := 0 to N - 2 do begin
MinIdx := I;
for J := I + 1 to N - 1 do begin
Inc(CompCount);
if Arr[J] < Arr[MinIdx] then
MinIdx := J;
end;
Temp := Arr[I];
Arr[I] := Arr[MinIdx];
Arr[MinIdx] := Temp;
end;
end;
procedure InsertionSortCounted(var Arr: array of Integer; N: Integer);
var
I, J, Key: Integer;
begin
for I := 1 to N - 1 do begin
Key := Arr[I];
J := I - 1;
while J >= 0 do begin
Inc(CompCount);
if Arr[J] > Key then begin
Arr[J + 1] := Arr[J];
Dec(J);
end
else
Break;
end;
Arr[J + 1] := Key;
end;
end;
procedure MergeCounted(var Arr: array of Integer; L, M, R: Integer);
var
Temp: array of Integer;
I, J, K, Size: Integer;
begin
Size := R - L + 1;
SetLength(Temp, Size);
I := L; J := M + 1; K := 0;
while (I <= M) and (J <= R) do begin
Inc(CompCount);
if Arr[I] <= Arr[J] then begin
Temp[K] := Arr[I]; Inc(I);
end else begin
Temp[K] := Arr[J]; Inc(J);
end;
Inc(K);
end;
while I <= M do begin Temp[K] := Arr[I]; Inc(I); Inc(K); end;
while J <= R do begin Temp[K] := Arr[J]; Inc(J); Inc(K); end;
for I := 0 to Size - 1 do
Arr[L + I] := Temp[I];
end;
procedure MergeSortCounted(var Arr: array of Integer; L, R: Integer);
var
M: Integer;
begin
if L >= R then Exit;
M := (L + R) div 2;
MergeSortCounted(Arr, L, M);
MergeSortCounted(Arr, M + 1, R);
MergeCounted(Arr, L, M, R);
end;
function PartitionCounted(var Arr: array of Integer; Lo, Hi: Integer): Integer;
var
Pivot, Temp, I, J: Integer;
begin
Pivot := Arr[Hi];
I := Lo - 1;
for J := Lo to Hi - 1 do begin
Inc(CompCount);
if Arr[J] <= Pivot then begin
Inc(I);
Temp := Arr[I]; Arr[I] := Arr[J]; Arr[J] := Temp;
end;
end;
Temp := Arr[I+1]; Arr[I+1] := Arr[Hi]; Arr[Hi] := Temp;
PartitionCounted := I + 1;
end;
procedure QuickSortCounted(var Arr: array of Integer; Lo, Hi: Integer);
var
P: Integer;
begin
if Lo < Hi then begin
P := PartitionCounted(Arr, Lo, Hi);
QuickSortCounted(Arr, Lo, P - 1);
QuickSortCounted(Arr, P + 1, Hi);
end;
end;
{ --- Utility --- }
procedure FillRandom(var Arr: array of Integer; N: Integer);
var
I: Integer;
begin
for I := 0 to N - 1 do
Arr[I] := Random(N * 10);
end;
procedure CopyArray(const Src: array of Integer;
var Dst: array of Integer; N: Integer);
var
I: Integer;
begin
for I := 0 to N - 1 do
Dst[I] := Src[I];
end;
{ --- Main Benchmark --- }
const
SIZES: array[1..5] of Integer = (100, 1000, 5000, 10000, 50000);
var
Master, Work: array of Integer;
Size, S: Integer;
StartTime: TDateTime;
ElapsedMS: Int64;
begin
Randomize;
WriteLn('=== Sorting Algorithm Benchmark ===');
WriteLn;
WriteLn('Size':8, 'SelSort ms':12, 'comps':14,
'InsSort ms':12, 'comps':14,
'Merge ms':12, 'comps':14,
'Quick ms':12, 'comps':14);
WriteLn(StringOfChar('-', 120));
for S := 1 to 5 do begin
Size := SIZES[S];
SetLength(Master, Size);
SetLength(Work, Size);
FillRandom(Master, Size);
Write(Size:8);
{ Selection Sort }
CopyArray(Master, Work, Size);
CompCount := 0;
StartTime := Now;
SelectionSortCounted(Work, Size);
ElapsedMS := MilliSecondsBetween(Now, StartTime);
Write(ElapsedMS:12, CompCount:14);
{ Insertion Sort }
CopyArray(Master, Work, Size);
CompCount := 0;
StartTime := Now;
InsertionSortCounted(Work, Size);
ElapsedMS := MilliSecondsBetween(Now, StartTime);
Write(ElapsedMS:12, CompCount:14);
{ Merge Sort }
CopyArray(Master, Work, Size);
CompCount := 0;
StartTime := Now;
MergeSortCounted(Work, 0, Size - 1);
ElapsedMS := MilliSecondsBetween(Now, StartTime);
Write(ElapsedMS:12, CompCount:14);
{ Quicksort }
CopyArray(Master, Work, Size);
CompCount := 0;
StartTime := Now;
QuickSortCounted(Work, 0, Size - 1);
ElapsedMS := MilliSecondsBetween(Now, StartTime);
Write(ElapsedMS:12, CompCount:14);
WriteLn;
end;
WriteLn;
WriteLn('Benchmark complete.');
end.
Expected Results
On a typical modern machine, you should see results roughly like this:
| Size | Selection (ms) | Insertion (ms) | Merge (ms) | Quick (ms) |
|---|---|---|---|---|
| 100 | <1 | <1 | <1 | <1 |
| 1,000 | 2 | 1 | <1 | <1 |
| 5,000 | 40 | 25 | 1 | 1 |
| 10,000 | 160 | 100 | 2 | 2 |
| 50,000 | 4,000 | 2,500 | 10 | 8 |
The O(n²) algorithms become visibly slow around n = 5,000 and painfully slow at n = 50,000. The O(n log n) algorithms remain nearly instantaneous.
Key Observations
-
The n² vs. n log n gap is enormous in practice. At n = 50,000, selection sort takes over 4 seconds while quicksort takes under 10 milliseconds — a 400x speedup.
-
Insertion sort beats selection sort because it is adaptive and can terminate early in the inner loop.
-
Quicksort is typically faster than merge sort despite both being O(n log n), because quicksort has better cache locality and lower constant factors.
-
Comparison counts confirm the theory: Selection sort always makes exactly n(n-1)/2 comparisons. Merge sort and quicksort make approximately n log₂(n) comparisons.
Discussion Questions
-
How would the results change if the input arrays were already sorted? Which algorithms would benefit, and which would suffer?
-
At what array size does it stop making sense to use O(n²) algorithms? Based on your benchmarks, suggest a crossover point.
-
Why does quicksort have better "cache locality" than merge sort? (Hint: think about which memory locations each algorithm accesses.)
-
If you had to sort a million records, how long would selection sort take based on your measurements? Is this acceptable for a user-facing application?