Ranks are relative to the current segment. The pivot's rank is p-l, not its absolute index:
def select(k: Int, l: Int, r: Int): Int = {
  require(0 <= l && l < r && r <= a.length)
  require(0 <= k && k < r - l)
  if (r - l == 1) a(l)
  else {
    val p = partition(l, r)
    val rank = p - l
    if (k == rank) a(p)
    else if (k < rank) select(k, l, p)
    else select(k - rank - 1, p + 1, r)
  }
}
The array partitioning places exactly rank strictly smaller entries before the pivot, with all later entries at least the pivot. Thus the pivot is the element of sorted rank rank, even when equal values occur later. A requested earlier rank is unchanged in the left segment; a later rank loses the low entries and the pivot, accounting for the subtraction rank+1. The recursive segment is nonempty in either branch that takes it, and its length strictly decreases. Induction on segment length therefore proves the returned value is the requested order statistic, with at most one recursive call. Only the selected segment is permuted; all outside entries remain unchanged. This is Quickselect.
Test singleton and two-element segments, first/last/interior requested ranks, nonzero segment offsets, already sorted and reversed inputs, random inputs, all-equal inputs and several patterns of duplicates. Include Int.MinValue and Int.MaxValue to expose overflow-prone comparisons. An independent sorted copy supplies the value oracle; also check preservation of the segment's multiset and of all outside entries. For speed tests, increase segment size and include deliberately unfavorable pivot arrangements, rather than timing random inputs alone.
Distinct entries do not guarantee linear worst-case time: the worst case is . A median of three can repeatedly be the second-smallest remaining value, leaving a segment of size when seeking its maximum. The quadratic median-of-three selection family gives explicit distinct inputs realizing this behavior for the implementation above; one six-entry member is . Its recurrence is , yielding a quadratic sum. Balanced reductions instead give . Randomized or suitable average-case analyses give expected linear selection, but this deterministic heuristic alone supplies no such worst-case guarantee. Unlike Quicksort, Quickselect explores only one side, so a balanced run is linear rather than .
A distinct-input counterexample for Quickselect that chooses the first/middle/last median, reserves it at the last slot, scans lower values to the front, and finally swaps the pivot into place. Let be the following permutation of . For , take increasing order. For , let componentwise and , then set
and put at every other index . These assignments use every entry of once and add zero and one, so they really define a permutation.
The sampled values are , with the last at least two, so the pivot is one. After the initial pivot swap, the only lower value is the zero at index . Moving it to the front places the displaced at its intended tail position. The final pivot swap moves from index one to the last position. Every other tail entry already has its intended position. Thus the complete partitioned result is exactly followed by .
Selecting the maximum consequently repeats the same construction on entries. If counts only pivot comparisons made by the partition scan, then , with and . Hence and total runtime is . For example, . This disproves a linear worst-case claim even when every value is distinct.