Solution (source code)

= Solution

Use <three-way quicksort>. Fix the pivot value before moving any entries, and group equal entries together so that none needs a recursive call. These helpers and the sorter belong inside an enclosing scope containing `a`.
``
def swap(i: Int, j: Int): Unit = {
  val t = a(i); a(i) = a(j); a(j) = t
}

def split(l: Int, r: Int): (Int, Int) = {
  // Pre: 0 <= l < r <= a.length.
  val p = a(l)
  var lt = l
  var i = l
  var gt = r
  while (i < gt) {
    if (a(i) < p) {
      swap(lt, i)
      lt += 1
      i += 1
    } else if (a(i) > p) {
      gt -= 1
      swap(i, gt)
    } else {
      i += 1
    }
  }
  (lt, gt)
}

def quicksort(l: Int, r: Int): Unit = {
  require(0 <= l && l <= r && r <= a.length)
  if (r - l > 1) {
    val (lt, gt) = split(l, r)
    quicksort(l, lt)
    quicksort(gt, r)
  }
}

// Sort the whole array with quicksort(0, a.length).
``
The <array partitioning> <loop invariant> is
$$
l\leq lt\leq i\leq gt\leq r,\qquad
a[l..lt)<p,\quad a[lt..i)=p,\quad a[gt..r)>p,
$$
with $a[i..gt)$ unclassified. Swaps preserve the segment's <array multiset> and leave all entries outside it unchanged. The <loop variant> $gt-i$ decreases in every branch. When swapping an entry from the right into `i`, do not increment `i`, since the incoming entry still needs classification. At termination,
$$
\boxed{a[l..lt)<p=a[lt..gt)<a[gt..r).}
$$
Here the middle segment is nonempty because the original pivot occurs somewhere in the preserved <array multiset>. Both recursive segments therefore have length strictly smaller than $r-l$. <Mathematical induction> on segment length proves that the two recursive calls sort their segments, and their ordering relative to the middle segment proves the complete <sorting> <postcondition>.

There is no additional <array>. The call stack and returned boundary pairs still use <auxiliary space>. An all-equal input takes one linear scan and no nontrivial recursive calls, so its <time complexity> is $\Theta(N)$ for $N\geq1$. Balanced partitions give $O(N\log N)$ <time complexity>; a fixed first pivot can still give $O(N^2)$ on adversarial distinct inputs. The worst-case call stack has $O(N)$ entries.