Quickselect Median of Medians

i
local quickselect_builder = require("sorting.quickselect")

-- Quickselect, using median of medians as pivot strategy and thus guaranteeing linear time.
return quickselect_builder(function(arr, less_than, quickselect)
	-- Partition a slice of at most 5 elements, returning the index of the median after permuting it
	local function partition_5(from, to)
		-- Sort elements up to and including mid using a simple selectionsort
		local mid = math.floor((from + to) / 2)
		for i = from, mid do
			local min_idx, min_val = i, arr[i]
			for j = i + 1, to do
				if less_than(arr[j], min_val) then
					min_idx, min_val = j, arr[j]
				end
			end
			arr[i], arr[min_idx] = arr[min_idx], arr[i]
		end
		return mid -- index of the median
	end

	return function(from, to)
		if to - from < 5 then -- <= 5 elements
			return partition_5(from, to)
		end
		local medians_to = from -- build a slice of medians starting at `from`
		for i = from, to - 4, 5 do -- iterate over 5 element subslices
			local median_5 = partition_5(i, i + 4) -- find median
			-- Swap to the slice of medians
			arr[median_5], arr[medians_to] = arr[medians_to], arr[median_5]
			-- Extend the slice of medians
			medians_to = medians_to + 1
		end
		local rem = (from - to + 1) % 5 -- count of remaining elements
		if rem == 0 then -- no remaining elements
			medians_to = medians_to - 1 -- last index goes unused
		else -- deal with the remaining less than five elements
			local median_rem = partition_5(to - rem + 1, to) -- find median of remaining elements
			-- Swap to the slice of medians
			arr[median_rem], arr[medians_to] = arr[medians_to], arr[median_rem]
		end
		local medians_cnt = medians_to - from + 1
		local median_sorted_idx = math.ceil(medians_cnt / 2)
		return quickselect(from, medians_to, median_sorted_idx)
	end
end)