Solution (source code)

= Solution

A second <binary search> locates the first entry strictly greater than `x`:
``
def upperBound(a: Array[Int], x: Int): Int = {
  require(a != null) // a is nondecreasing
  var lo = 0
  var hi = a.length
  while (lo < hi) {
    val mid = lo + (hi - lo) / 2
    if (a(mid) <= x) lo = mid + 1
    else hi = mid
  }
  lo
}

def equalRange(a: Array[Int], x: Int): (Int, Int) =
  (lowerBound(a, x), upperBound(a, x))
``
For <upper-bound search>, the <loop invariant> is $0\leq lo\leq hi\leq n$, $a[0..lo)\leq x$, and $x<a[hi..n)$; its <loop variant> is again $hi-lo$. The same midpoint and sortedness arguments prove preservation, termination and $O(\log(n+1))$ <time complexity>. The first <binary search> in part (b) already has its own <loop invariant> and <loop variant>.

Let the two boundaries be $i,j$. There cannot be a value greater than $x$ before the first value at least $x$, so $i\leq j$. Every entry between them is simultaneously at least and at most $x$, hence equals $x$. Entries before $i$ are smaller and entries at or after $j$ are larger, so the interval cannot be extended at either end. This proves the <equal-range search> result is maximal. \b[If `x` is absent, `i == j`, both equal to its insertion position]; this can be zero, $n$, or an interior gap. Two logarithmic searches still have logarithmic total <time complexity>. Computing an upper boundary as `lowerBound(a, x+1)` would introduce an unnecessary <integer overflow> bug at the largest <integer>.