Imports
import Mathlib

CLRS Section 7.1 - Description of quicksort

This file starts the Chapter 7 sorting track with a Lean-friendly functional model of quicksort and a scan-state proof spine for the CLRS partition loop. The theorem layer proves the same mathematical facts used by the textbook proof:

  • partition returns exactly the original tail elements;

  • the left partition contains only elements at most the pivot;

  • the right partition contains only elements greater than the pivot;

  • the scan-state partition loop preserves its exact invariant and computes the same regions as the specification partition;

  • an array-facing wrapper returns a pivot index whose prefix/suffix satisfy the CLRS partition postcondition;

  • the array-facing partition output is reachable from the input by an explicit finite adjacent-swap trace;

  • functional quicksort returns an ordered permutation of the input.

The remaining array-level strengthening target is to refine this proof to a concrete index-level mutable array-segment loop. Randomized/expected-time analysis is also separate.

namespace CLRSnamespace Chapter07

Ordered lists and pivot bounds

A compact sortedness predicate for lists of natural numbers.

def Ordered : List Nat Prop | [] => True | [_] => True | x :: y :: ys => x y Ordered (y :: ys)

Every element of xs is at least lower.

def AllLe (lower : Nat) (xs : List Nat) : Prop := x xs, lower x

Every element of xs is at most upper.

def AllLeUpper (xs : List Nat) (upper : Nat) : Prop := x xs, x upper

Every element of xs is strictly greater than lower.

def AllGt (lower : Nat) (xs : List Nat) : Prop := x xs, lower < x
theorem ordered_tail {x : Nat} {xs : List Nat} (h : Ordered (x :: xs)) : Ordered xs := cases xs with theorem ordered_allLe_tail {x : Nat} {xs : List Nat} (h : Ordered (x :: xs)) : AllLe x xs := induction xs generalizing x with theorem ordered_cons_of_allLe {x : Nat} {xs : List Nat} (hxs : Ordered xs) (hall : AllLe x xs) : Ordered (x :: xs) := cases xs with exact hall y ( ), hxs

Bounds survive permutation.

theorem allLeUpper_of_perm {xs ys : List Nat} {upper : Nat} (hperm : xs.Perm ys) (hys : AllLeUpper ys upper) : AllLeUpper xs upper :=

Strict lower bounds survive permutation.

theorem allGt_of_perm {xs ys : List Nat} {lower : Nat} (hperm : xs.Perm ys) (hys : AllGt lower ys) : AllGt lower xs :=

If the left list is sorted and bounded by the pivot, and the right list is sorted and strictly above the pivot, then the quicksort concatenation is sorted.

Partition around a pivot

Stable partition around a pivot. The first component contains elements ≤ p; the second component contains elements > p.

def partitionAround (p : Nat) : List Nat List Nat × List Nat | [] => ([], []) | x :: xs => let parts := partitionAround p xs if x p then (x :: parts.1, parts.2) else (parts.1, x :: parts.2)

The left partition contains only elements at most the pivot.

theorem partitionAround_left_allLeUpper (p : Nat) (xs : List Nat) : AllLeUpper (partitionAround p xs).1 p := induction xs with

The right partition contains only elements strictly greater than the pivot.

theorem partitionAround_right_allGt (p : Nat) (xs : List Nat) : AllGt p (partitionAround p xs).2 := induction xs with

The left partition is no longer than the input tail.

theorem partitionAround_left_length_le (p : Nat) (xs : List Nat) : (partitionAround p xs).1.length xs.length := induction xs with

The right partition is no longer than the input tail.

theorem partitionAround_right_length_le (p : Nat) (xs : List Nat) : (partitionAround p xs).2.length xs.length := induction xs with

Moving an element from the middle of an append to the front preserves elements.

theorem perm_append_cons (x : Nat) (left right : List Nat) : (left ++ x :: right).Perm (x :: left ++ right) := induction left with

Adjacent swap traces

An explicit finite trace of adjacent swaps from one list to another.

This is a lightweight array-facing refinement of List.Perm: every constructor corresponds either to keeping a common head, swapping two adjacent cells, or composing two traces.

Empty trace.

Preserve a common head while tracing the tails.

Swap two adjacent cells.

Compose traces.

inductive AdjacentSwapTrace : List Nat List Nat Prop where | refl (xs : List Nat) : AdjacentSwapTrace xs xs | cons (x : Nat) {xs ys : List Nat} : AdjacentSwapTrace xs ys AdjacentSwapTrace (x :: xs) (x :: ys) | swap (x y : Nat) (xs : List Nat) : AdjacentSwapTrace (x :: y :: xs) (y :: x :: xs) | trans {xs ys zs : List Nat} : AdjacentSwapTrace xs ys AdjacentSwapTrace ys zs AdjacentSwapTrace xs zs
namespace AdjacentSwapTrace

Every adjacent-swap trace preserves the multiset of list elements.

theorem to_perm {xs ys : List Nat} : AdjacentSwapTrace xs ys xs.Perm ys | .refl xs => List.Perm.refl xs | .cons x h => List.Perm.cons x (to_perm h) | .swap x y xs => (List.Perm.swap x y xs).symm | .trans hxy hyz => (to_perm hxy).trans (to_perm hyz)

Any list permutation can be represented as a finite adjacent-swap trace.

theorem of_perm {xs ys : List Nat} (h : xs.Perm ys) : AdjacentSwapTrace xs ys := induction h with
end AdjacentSwapTrace

Partition returns exactly the input elements, just split by the pivot test.

theorem partitionAround_perm (p : Nat) (xs : List Nat) : ((partitionAround p xs).1 ++ (partitionAround p xs).2).Perm xs := induction xs with exact

The left partition is exactly the stable filter of elements at most the pivot.

theorem partitionAround_left_eq_filter (p : Nat) (xs : List Nat) : (partitionAround p xs).1 = xs.filter (fun x => decide (x p)) := induction xs with

The right partition is exactly the stable filter of elements greater than the pivot.

theorem partitionAround_right_eq_filter (p : Nat) (xs : List Nat) : (partitionAround p xs).2 = xs.filter (fun x => decide (p < x)) := induction xs with

Membership characterization for the left partition.

Membership characterization for the right partition.

Reader-facing correctness theorem for stable partition around a pivot.

It packages the facts used by the quicksort proof: the left side contains exactly the input elements at most the pivot, the right side contains exactly the input elements greater than the pivot, and concatenating the two sides is a permutation of the original input tail.

theorem partitionAround_correct (p : Nat) (xs : List Nat) : AllLeUpper (partitionAround p xs).1 p AllGt p (partitionAround p xs).2 ((partitionAround p xs).1 ++ (partitionAround p xs).2).Perm xs ( x, x (partitionAround p xs).1 x xs x p) ( x, x (partitionAround p xs).2 x xs p < x) := partitionAround_left_allLeUpper p xs, partitionAround_right_allGt p xs, partitionAround_perm p xs, mem_partitionAround_left_iff p xs, mem_partitionAround_right_iff p xs

A CLRS partition-loop proof spine

State for a Lean-friendly model of the CLRS PARTITION loop.

The loop scans the tail from left to right. The low region contains processed elements known to be at most the pivot; the high region contains processed elements known to be greater than the pivot.

Processed elements that belong on the left of the pivot.

Processed elements that belong on the right of the pivot.

structure PartitionLoopState where low : List Nat high : List Nat

Exact loop invariant for the scan model: after processing seen, the two regions are exactly the stable filters of seen.

def PartitionLoopInvariant (p : Nat) (seen : List Nat) (state : PartitionLoopState) : Prop := state.low = seen.filter (fun x => decide (x p)) state.high = seen.filter (fun x => decide (p < x))

One CLRS-style partition-loop step for a newly scanned element.

def partitionLoopStep (p : Nat) (state : PartitionLoopState) (x : Nat) : PartitionLoopState := if x p then { low := state.low ++ [x], high := state.high } else { low := state.low, high := state.high ++ [x] }

Run the partition loop from an arbitrary processed-prefix state.

def partitionLoopFrom (p : Nat) : PartitionLoopState List Nat PartitionLoopState | state, [] => state | state, x :: xs => partitionLoopFrom p (partitionLoopStep p state x) xs

Run the partition loop on an input tail from the empty state.

def partitionLoop (p : Nat) (xs : List Nat) : PartitionLoopState := partitionLoopFrom p { low := [], high := [] } xs

The exact invariant is preserved by one partition-loop step.

theorem partitionLoopStep_invariant (p : Nat) (seen : List Nat) (state : PartitionLoopState) (x : Nat) (hinv : PartitionLoopInvariant p seen state) : PartitionLoopInvariant p (seen ++ [x]) (partitionLoopStep p state x) :=

Running the loop over a remaining suffix preserves the exact invariant for the whole processed prefix.

theorem partitionLoopFrom_invariant (p : Nat) : (xs seen : List Nat) (state : PartitionLoopState), PartitionLoopInvariant p seen state PartitionLoopInvariant p (seen ++ xs) (partitionLoopFrom p state xs)

The partition loop satisfies the exact invariant for the whole input.

The loop's low region is the stable filter of elements at most the pivot.

theorem partitionLoop_low_eq_filter (p : Nat) (xs : List Nat) : (partitionLoop p xs).low = xs.filter (fun x => decide (x p)) := (partitionLoop_invariant p xs).1

The loop's high region is the stable filter of elements greater than the pivot.

theorem partitionLoop_high_eq_filter (p : Nat) (xs : List Nat) : (partitionLoop p xs).high = xs.filter (fun x => decide (p < x)) := (partitionLoop_invariant p xs).2

The loop model computes the same two regions as the specification partition partitionAround.

The loop's low region contains only elements at most the pivot.

The loop's high region contains only elements greater than the pivot.

The two loop regions contain exactly the scanned input elements.

Membership characterization for the loop's low region.

Membership characterization for the loop's high region.

Reader-facing correctness theorem for the CLRS-style partition loop.

It exposes the loop invariant's final consequences: low-side bounds, high-side bounds, permutation preservation for the scanned tail, and membership classification for both regions.

theorem partitionLoop_correct (p : Nat) (xs : List Nat) : AllLeUpper (partitionLoop p xs).low p AllGt p (partitionLoop p xs).high ((partitionLoop p xs).low ++ (partitionLoop p xs).high).Perm xs ( x, x (partitionLoop p xs).low x xs x p) ( x, x (partitionLoop p xs).high x xs p < x) := partitionLoop_low_allLeUpper p xs, partitionLoop_high_allGt p xs, partitionLoop_perm p xs, mem_partitionLoop_low_iff p xs, mem_partitionLoop_high_iff p xs

Partition result obtained by placing the pivot between the final low and high regions.

def clrsPartition (p : Nat) (xs : List Nat) : List Nat := let state := partitionLoop p xs state.low ++ p :: state.high

Reader-facing correctness theorem for the CLRS-style partition result.

The returned list is a permutation of the pivot followed by the scanned tail, and the final low/high loop regions satisfy the usual partition bounds.

Array-facing partition result

Array-facing result of partitioning around a pivot.

The list out is the post-partition array segment, and pivotIndex is the index at which the pivot is placed.

Post-partition array segment.

Zero-based index of the pivot in out.

structure PartitionArrayResult where out : List Nat pivotIndex : Nat

Array-facing wrapper for the CLRS partition result.

This keeps the proof connected to the scan-state invariant while exposing the ordinary array postcondition shape: a returned pivot index plus an output segment.

def clrsPartitionArray (p : Nat) (xs : List Nat) : PartitionArrayResult := let state := partitionLoop p xs { out := state.low ++ p :: state.high, pivotIndex := state.low.length }

The array-facing wrapper has the same output as clrsPartition.

theorem clrsPartitionArray_out (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).out = clrsPartition p xs :=

The returned pivot index is in bounds.

theorem clrsPartitionArray_pivotIndex_lt (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).pivotIndex < (clrsPartitionArray p xs).out.length :=

The pivot is stored exactly at the returned index.

theorem clrsPartitionArray_pivot (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).out[(clrsPartitionArray p xs).pivotIndex]? = some p :=

The segment left of the returned index contains only values at most the pivot.

theorem clrsPartitionArray_left_bound (p : Nat) (xs : List Nat) : AllLeUpper ((clrsPartitionArray p xs).out.take (clrsPartitionArray p xs).pivotIndex) p :=

The segment right of the returned index contains only values greater than the pivot.

theorem clrsPartitionArray_right_bound (p : Nat) (xs : List Nat) : AllGt p ((clrsPartitionArray p xs).out.drop ((clrsPartitionArray p xs).pivotIndex + 1)) :=

The array-facing partition output preserves exactly the input elements plus the pivot.

theorem clrsPartitionArray_perm (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).out.Perm (p :: xs) :=

The array-facing partition output is reachable by adjacent swaps.

theorem clrsPartitionArray_swapTrace (p : Nat) (xs : List Nat) : AdjacentSwapTrace (p :: xs) (clrsPartitionArray p xs).out := AdjacentSwapTrace.of_perm (clrsPartitionArray_perm p xs).symm

Reader-facing correctness theorem for the array-facing partition wrapper.

It packages the returned-index postcondition: the pivot is in bounds and stored at the returned index; the prefix before it is at most the pivot; the suffix after it is greater than the pivot; and the output is a permutation of the pivot followed by the scanned tail.

theorem clrsPartitionArray_correct (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).pivotIndex < (clrsPartitionArray p xs).out.length (clrsPartitionArray p xs).out[(clrsPartitionArray p xs).pivotIndex]? = some p AllLeUpper ((clrsPartitionArray p xs).out.take (clrsPartitionArray p xs).pivotIndex) p AllGt p ((clrsPartitionArray p xs).out.drop ((clrsPartitionArray p xs).pivotIndex + 1)) (clrsPartitionArray p xs).out.Perm (p :: xs) := clrsPartitionArray_pivotIndex_lt p xs, clrsPartitionArray_pivot p xs, clrsPartitionArray_left_bound p xs, clrsPartitionArray_right_bound p xs, clrsPartitionArray_perm p xs

Array-facing partition correctness with an explicit adjacent-swap trace.

This strengthens clrsPartitionArray_correct by recording that the output segment is not merely a permutation of the input segment, but is reachable by a finite sequence of adjacent swaps.

theorem clrsPartitionArray_correct_with_trace (p : Nat) (xs : List Nat) : (clrsPartitionArray p xs).pivotIndex < (clrsPartitionArray p xs).out.length (clrsPartitionArray p xs).out[(clrsPartitionArray p xs).pivotIndex]? = some p AllLeUpper ((clrsPartitionArray p xs).out.take (clrsPartitionArray p xs).pivotIndex) p AllGt p ((clrsPartitionArray p xs).out.drop ((clrsPartitionArray p xs).pivotIndex + 1)) AdjacentSwapTrace (p :: xs) (clrsPartitionArray p xs).out := clrsPartitionArray_pivotIndex_lt p xs, clrsPartitionArray_pivot p xs, clrsPartitionArray_left_bound p xs, clrsPartitionArray_right_bound p xs, clrsPartitionArray_swapTrace p xs

Functional quicksort

Fuelled functional quicksort. With fuel at least xs.length, the recursive calls have enough fuel for the partition tails. The public quickSort below uses exactly that amount of fuel.

def quickSortFuel : Nat List Nat List Nat | 0, xs => xs | _ + 1, [] => [] | fuel + 1, pivot :: xs => let parts := partitionAround pivot xs quickSortFuel fuel parts.1 ++ pivot :: quickSortFuel fuel parts.2

Functional quicksort over lists of natural numbers.

def quickSort (xs : List Nat) : List Nat := quickSortFuel xs.length xs

With enough fuel, quicksort preserves the input elements up to permutation.

With enough fuel, quicksort returns an ordered list.

Quicksort preserves the input elements up to permutation.

theorem quickSort_perm (xs : List Nat) : (quickSort xs).Perm xs :=

Quicksort returns an ordered list.

theorem quickSort_ordered (xs : List Nat) : Ordered (quickSort xs) :=

The reader-facing correctness theorem for the functional quicksort model.

theorem quickSort_correct (xs : List Nat) : Ordered (quickSort xs) (quickSort xs).Perm xs := quickSort_ordered xs, quickSort_perm xs
end Chapter07end CLRS