FINDING THE KTH LEAST ELEMENT OF A VECTOR

Problem: Given a vector, a position in that vector, and a predicate that expresses a total ordering on a domain that includes the elements of that vector, identify the element that would occupy the specified position if the vector were completely sorted.

One approach is to sort the vector and inspect the position, but that's unnecessarily inefficient. A faster way is to partition the vector, using the value currently occupying the specified position as the pivot, and then to re-partition just the sub-vector that contains the specified position, and so on recursively, narrowing the sub-vector at each step, until one of the pivots turns out to be correctly placed in the specified position.

The internally defined partition! procedure is a lightly edited version of the one developed elsewhere in this collection. The main change is that we are now partitioning not the entire vector but the sub-vector that runs from a specified starting position start up to, but not including, a specified ending position stop.

(define kth-least
  (lambda (vec k . opt)
    (let ((partition!
           (let ((precedes?
                  (if (null? opt) < (car opt))))
 
              (lambda (start stop pivot)
                (letrec ((rightwards
                           (lambda (current)
                             (if (and (< current stop)
                                      (precedes? (vector-ref vec current)
                                                 pivot))
                                 (rightwards (+ current 1))
                                 current)))

                         (leftwards
                           (lambda (current)
                             (if (or (< current start)
                                     (precedes? (vector-ref vec current)
                                                pivot))
                                 current
                                 (leftwards (- current 1))))))

                  (let loop ((left-pointer (rightwards start))
                             (right-pointer (leftwards (- stop 1))))
                    (if (< left-pointer right-pointer)
                        (begin
                        (vector-swap! vec left-pointer right-pointer)
                        (loop (rightwards (+ left-pointer 1))
                              (leftwards (- right-pointer 1))))
                      left-pointer)))))))

      (let loop ((start 0)
                 (stop (- (vector-length vec) 1)))
        (let* ((pivot (vector-ref vec stop))
               (break (partition! start stop pivot)))
          (vector-swap! vec break stop)
          (cond ((< k break)
                 (loop start (- break 1)))
                ((< break k)
                 (loop (+ break 1) stop))
                (else
                 pivot)))))))
The main program is simply a recursion in which start and stop, initially the leftmost and rightmost positions in the vector, differ by less on each recursive call. At each step, the last element of the sub-vector is chosen as the pivot, and the rest of the sub-vector is partitioned. The variable break is set to the leftmost position occupied by a value greater than or equal to the pivot, and then the pivot is swapped into that position, so that items in positions to the left of break are all strictly less than the pivot, and those to the right of break are all greater than or equal to the pivot. This means that the pivot is now in whatever position it would occupy in a completely sorted vector.

At this point there are three possibilities: If k is less than break, we need only look at the sub-vector to the left of the pivot, and a recursive call is issued to search that sub-vector. Similarly, if k is greater than break, we search the sub-vector to the right of the pivot. In either of these cases, the sub-vector to be searched has been reduced by at least one element (since it does not include the pivot). The third possibility is that k and break are equal; in this case, the problem is solved, since the pivot, now occupying position k, is in the correct position.

Since in each call we either find the element we're looking for or reduce the number of elements in the sub-vector, the recursion must ultimately terminate in a successful solution.


This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/events/scheme-workshop/kth-least.html


created July 12, 1995
last revised June 24, 1996

John David Stone (stone@math.grin.edu)