Python: Quicksort with median of three

Question:

I’m trying to change this quicksort code to work with a pivot that takes a “median of three” instead.

def quickSort(L, ascending = True): 
    quicksorthelp(L, 0, len(L), ascending)


def quicksorthelp(L, low, high, ascending = True): 
    result = 0
    if low < high: 
        pivot_location, result = Partition(L, low, high, ascending)  
        result += quicksorthelp(L, low, pivot_location, ascending)  
        result += quicksorthelp(L, pivot_location + 1, high, ascending)
    return result


def Partition(L, low, high, ascending = True):
    print('Quicksort, Parameter L:')
    print(L)
    result = 0 
    pivot, pidx = median_of_three(L, low, high)
    L[low], L[pidx] = L[pidx], L[low]
    i = low + 1
    for j in range(low+1, high, 1):
        result += 1
        if (ascending and L[j] < pivot) or (not ascending and L[j] > pivot):
            L[i], L[j] = L[j], L[i]  
            i += 1
    L[low], L[i-1] = L[i-1], L[low] 
    return i - 1, result

liste1 = list([3.14159, 1./127, 2.718, 1.618, -23., 3.14159])

quickSort(liste1, False)  # descending order
print('sorted:')
print(liste1)

But I’m not really sure how to do that. The median has to be the median of the first, middle and last element of a list. If the list has an even number of elements, middle becomes the last element of the first half.

Here’s my median function:

def median_of_three(L, low, high):
    mid = (low+high-1)//2
    a = L[low]
    b = L[mid]
    c = L[high-1]
    if a <= b <= c:
        return b, mid
    if c <= b <= a:
        return b, mid
    if a <= c <= b:
        return c, high-1
    if b <= c <= a:
        return c, high-1
    return a, low
Asked By: Dennis

||

Answers:

Let us first implement the median-of-three for three numbers, so an independent function. We can do that by sorting the list of three elements, and then return the second element, like:

def median_of_three(a, b, c):
    return sorted([a, b, c])[1]

Now for a range low .. high (with low included, and high excluded), we should determine what the elements are for which we should construct the median of three:

  1. the first element: L[low],
  2. the last element L[high-1], and
  3. the middle element (in case there are two such, take the first) L[(low+high-1)//2].

So now we only need to patch the partitioning function to:

def Partition(L, low, high, ascending = True):
    print('Quicksort, Parameter L:')
    print(L)
    result = 0 
    pivot = median_of_three(L[low], L[(low+high-1)//2], L[high-1])
    i = low + 1  
    for j in range(low + 1, high, 1): 
        result += 1
        if (ascending and L[j] < pivot) or (not ascending and L[j] > pivot):
            L[i], L[j] = L[j], L[i]  
            i += 1  
    L[low], L[i-1] = L[i-1], L[low] 
    return i - 1, result

EDIT: determining the median of three elements.

The median of three elements is the element that is in the middle of the two other values. So in case a <= b <= c, then b is the median.

So we need to determine in what order the elements are, such that we can determine the element in the middle. Like:

def median_of_three(a, b, c):
    if a <= b and b <= c:
        return b
    if c <= b and b <= a:
        return b
    if a <= c and c <= b:
        return c
    if b <= c and c <= a:
        return c
    return a

So now we have defined the median of three with four if cases.

EDIT2: There is still a problem with this. After you perform a pivot, you swap the element L[i-1] with L[low] in your original code (the location of the pivot). But this of course does not work anymore: since the pivot now can be located at any of the three dimensions. Therfore we need to make the median_of_three(..) smarter: not only should it return the pivot element, but the location of that pivot as well:

def median_of_three(L, low, high):
    mid = (low+high-1)//2
    a = L[low]
    b = L[mid]
    c = L[high-1]
    if a <= b <= c:
        return b, mid
    if c <= b <= a:
        return b, mid
    if a <= c <= b:
        return c, high-1
    if b <= c <= a:
        return c, high-1
    return a, low

Now we can solve this problem with:

def Partition(L, low, high, ascending = True):
    print('Quicksort, Parameter L:')
    print(L)
    result = 0 
    pivot, pidx = median_of_three(L, low, high)
    i = low + (low == pidx)
    for j in range(low, high, 1):
        if j == pidx:
            continue
        result += 1
        if (ascending and L[j] < pivot) or (not ascending and L[j] > pivot):
            L[i], L[j] = L[j], L[i]  
            i += 1 + (i+1 == pidx)
    L[pidx], L[i-1] = L[i-1], L[pidx] 
    return i - 1, result

EDIT3: cleaning it up.

Although the above seems to work, it is quite complicated: we need to let i and j “skip” the location of the pivot.

It is probably simpler if we first move the pivot to the front of the sublist (so to the low index):

def Partition(L, low, high, ascending = True):
    print('Quicksort, Parameter L:')
    print(L)
    result = 0 
    pivot, pidx = median_of_three(L, low, high)
    L[low], L[pidx] = L[pidx], L[low]
    i = low + 1
    for j in range(low+1, high, 1):
        result += 1
        if (ascending and L[j] < pivot) or (not ascending and L[j] > pivot):
            L[i], L[j] = L[j], L[i]  
            i += 1
    L[low], L[i-1] = L[i-1], L[low] 
    return i - 1, result
Answered By: Willem Van Onsem

In a “median of three” version of quicksort, you do not only want to find the median to use it as the pivot, you also want to place the maximum and the minimum values in their places so some of the pivoting is already done. In other words, you want to sort those three items in those three places. (Some variations do not want them sorted in the usual way, but I’ll stick to a simpler-to-understand version for you here.)

You probably don’t want to do this in a function, since function calls are fairly expensive in Python and this particular capability is not broadly useful. So you can do some code like this. Let’s say the three values you want to sort are in indices i, j, and k, with i < j < k. In practice you probably would use low, low + 1, and high, but you can make those changes as you like.

if L(i) > L(j):
    L(i), L(j) = L(j), L(i)
if L(i) > L(k):
    L(i), L(k) = L(k), L(i)
if L(j) > L(k):
    L(j), L(k) = L(k), L(j)

There are some optimizations that can be done. For example, you probably will want to use the median value in the pivot process, so you can change the code to have stored the final value of L(j) in a simple variable, which reduces array lookups. Note that you cannot do this in less than three comparisons in general–you cannot reduce it to two comparisons, though in some special cases you could do that.

Answered By: Rory Daulton

one possible way can be selecting medians randomly from left and right positions.

def median_of_three(left, right):
    """
    Function to choose pivot point
    :param left: Left index of sub-list
    :param right: right-index of sub-list
    """

    # Pick 3 random numbers within the range of the list
    i1 = left + random.randint(0, right - left)
    i2 = left + random.randint(0, right - left)
    i3 = left + random.randint(0, right - left)

    # Return their median
    return max(min(i1, i2), min(max(i1, i2), i3))
Answered By: Sid