The Worst Sorting Algorithm: Deconstructing Slowsort and the 'Multiply and Surrender' Paradigm

Arpit Bhayani

Arpit Bhayani

Jun 18, 2021 • 6 min read

Play

The Worst Sorting Algorithm: Deconstructing Slowsort and the ‘Multiply and Surrender’ Paradigm

When software engineers evaluate sorting algorithms, the discussion almost universally centers around efficiency: optimizing comparisons, caching considerations, and striving toward the theoretical lower bound of comparison-based sorting, O(nlogn)O(n \log n).

However, studying pathologically inefficient algorithms offers immense pedagogical value. By analyzing algorithms engineered intentionally to perform poorly, we gain profound intuition into algorithmic design paradigms, recurrence relations, and the dangers of unnecessary subproblem expansion.

One of the most fascinating examples is Slowsort, introduced by Andrei Broder and Jorge Stolfi in 1986. Unlike randomized joke algorithms like Bogosort (which relies on random permutations), Slowsort is fully deterministic, provably correct, and rooted in an anti-heuristic dubbed Multiply and Surrender.


1. The Paradigm: Divide and Conquer vs. Multiply and Surrender

Traditional recursive algorithms like Mergesort and Quicksort rely on the Divide and Conquer paradigm:

  1. Divide: Break down a problem of size nn into smaller, independent subproblems.
  2. Conquer: Solve the subproblems recursively.
  3. Combine: Merge the solutions efficiently to produce the final result.

Slowsort inverts this philosophy into Multiply and Surrender:

  1. Multiply: Deconstruct the problem into overlapping, highly redundant subproblems.
  2. Surrender: Defer doing actual useful work until forced, repeatedly re-sorting segments that were already processed.

Slowsort does not attempt to merge efficiently. Instead, it sorts the first half, sorts the second half, ensures the maximum of the entire array is placed at the end, and then completely discards the fact that the prefix was already sorted, recursively re-sorting the prefix from scratch.

+-------------------------------------------------------------+
|                       Slowsort(1, n)                        |
+------------------------------+------------------------------+
                               |
       +-----------------------+-----------------------+
       v                                               v
+---------------+                               +---------------+
| Sort 1 to n/2 |                               |Sort n/2+1 to n|
+-------+-------+                               +-------+-------+
        |                                               |
        +-----------------------+-----------------------+
                                v
        +-----------------------------------------------+
        | Swap max(A[n/2], A[n]) to position A[n]       |
        +-----------------------+-----------------------+
                                |
                                v
        +-----------------------------------------------+
        | Recursively run Slowsort(1, n-1)              |
        | (Completely re-evaluates previously sorted)   |
        +-----------------------------------------------+

2. Algorithmic Mechanics of Slowsort

Slowsort operates on an array segment defined by indices ii (left boundary) and jj (right boundary).

Step-by-Step Procedure

  1. Base Case: If iji \ge j, the segment contains one or zero elements and is trivially sorted. Return immediately.
  2. Divide into Two Halves: Compute the midpoint m=(i+j)/2m = \lfloor (i + j) / 2 \rfloor.
  3. Recursively Sort Subarrays:
    • Recursively call Slowsort(i, m) to sort the first half.
    • Recursively call Slowsort(m + 1, j) to sort the second half.
  4. Place the Largest Element:
    • Because both halves are sorted, the largest element of the first half is at A[m]A[m], and the largest element of the second half is at A[j]A[j].
    • Compare A[m]A[m] and A[j]A[j]. If A[m]>A[j]A[m] > A[j], swap them.
    • This guarantees that the absolute largest element in the range A[ij]A[i \dots j] is safely parked at the final position A[j]A[j].
  5. Recursive Elimination (Surrender):
    • Now that the global maximum of the current segment is anchored at A[j]A[j], recursively invoke Slowsort(i, j - 1) on all remaining elements.

3. Implementation in Python

def slowsort(arr: list[int], i: int, j: int) -> None:
    """
    Recursively sorts arr[i...j] in-place using Slowsort.
    """
    # Base case: a single element or invalid bounds
    if i >= j:
        return

    # Step 1: Find midpoint
    m = (i + j) // 2

    # Step 2: Recursively sort left and right halves
    slowsort(arr, i, m)
    slowsort(arr, m + 1, j)

    # Step 3: Ensure maximum of both halves is at the end
    if arr[m] > arr[j]:
        arr[m], arr[j] = arr[j], arr[m]

    # Step 4: Recursively sort remaining n-1 elements
    slowsort(arr, i, j - 1)

4. Execution Trace Walkthrough

Consider an array A=[4,2,5,1]A = [4, 2, 5, 1] with i=0i = 0 and j=3j = 3:

  1. Divide: m=(0+3)//2=1m = (0 + 3) // 2 = 1.
  2. Sort First Half (010 \dots 1):
    • Elements: [4,2][4, 2]
    • Splitting yields [4][4] and [2][2].
    • Swap occurs to put maximum at index 1: [2,4][2, 4].
    • Recursive call on remaining 000 \dots 0 returns [2,4][2, 4].
  3. Sort Second Half (232 \dots 3):
    • Elements: [5,1][5, 1]
    • Splitting yields [5][5] and [1][1].
    • Swap occurs to put maximum at index 3: [1,5][1, 5].
    • Recursive call on remaining 222 \dots 2 returns [1,5][1, 5].
  4. Current Array State: [2,4,1,5][2, 4, 1, 5].
  5. Compare Maxima: Compare A[m]=A[1]=4A[m] = A[1] = 4 and A[j]=A[3]=5A[j] = A[3] = 5. Since 4<54 < 5, no swap is needed. The element 5 is in its final position.
  6. The Redundant Call: Invoke Slowsort(0, 2) on [2,4,1][2, 4, 1].
    • Even though [2,4][2, 4] was already sorted earlier, the algorithm re-divides it, re-sorts it, and re-executes all comparisons.

5. Mathematical Complexity Analysis

The Recurrence Relation

For an input of size nn, the work done at each non-base level consists of:

  • Two recursive calls of size n/2n/2.
  • One constant-time swap/comparison O(1)O(1).
  • One recursive call of size n1n - 1.

The runtime recurrence is expressed as:

T(n)=2T(n2)+T(n1)+O(1)T(n) = 2 T\left(\frac{n}{2}\right) + T(n - 1) + O(1)

Asymptotic Growth

To understand how badly this performs:

  • Mergesort: T(n)=2T(n/2)+O(n)    O(nlogn)T(n) = 2T(n/2) + O(n) \implies O(n \log n).
  • Bubble Sort: T(n)=T(n1)+O(n)    O(n2)T(n) = T(n - 1) + O(n) \implies O(n^2).
  • Slowsort: Merges both patterns into a single catastrophic recurrence.

Even in the best case (a fully pre-sorted array), Slowsort does not terminate early because it lacks an adaptive condition. It systematically computes:

T(n)=Ω(nlog2n2+ϵ)T(n) = \Omega\left(n^{\frac{\log_2 n}{2 + \epsilon}}\right)

This makes Slowsort super-polynomial; its asymptotic complexity grows faster than any polynomial nkn^k for any constant kk. However, it remains strictly sub-exponential compared to factorial/exponential algorithms (O(n!)O(n!) or O(2n)O(2^n) like Bogosort or naive permutations).

Comparison Table

AlgorithmParadigmBest CaseAverage CaseWorst CaseDeterministic?
QuicksortDivide and ConquerO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n2)O(n^2)Yes / Randomized
MergesortDivide and ConquerO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)Yes
Bubble SortBrute Force / ExchangeO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)Yes
Stooge SortRecursive 2/3 overlappingO(nlog1.53)O(n2.71)O(n^{\log_{1.5} 3}) \approx O(n^{2.71})O(n2.71)O(n^{2.71})O(n2.71)O(n^{2.71})Yes
SlowsortMultiply and SurrenderΩ(nlogn)\Omega(n^{\log n})Super-polynomialSuper-polynomialYes
BogosortRandomize and CheckO(n)O(n)O((n+1)!)O((n+1)!)Unbounded (\infty)No

6. Key Takeaways

  • Correctness Does Not Equal Efficiency: Slowsort is completely sound—it preserves invariants and is mathematically guaranteed to sort any array—yet its design deliberately sabotages computational efficiency.
  • Avoiding Redundant Subproblems: Slowsort’s fatal flaw is re-sorting subsets of the array without memoization or reuse, illustrating why paradigms like dynamic programming and balanced divide-and-conquer are essential.
  • Theoretical Benchmarking: Studying algorithms across the entire spectrum—from optimal O(nlogn)O(n \log n) to super-polynomial Slowsort—deepens a software engineer’s grasp of recurrence trees, branching factors, and complexity bounds.
Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses