Quicksort in Excruciating Detail

13 Nov 2017

Let's take this slice of ints:

6, 63, 58, 24, 54, 28, 33, 80, 100

and try to qsort them.

func QSort(v []int, left int, right int) {
  last := 0

  if left >= right {
    return
  }
  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]
  last = left
  for i := left + 1; i <= right; i++ {
    if v[i] < v[left] {
      last++
      v[last], v[i] = v[i], v[last]
    }
  }
  v[left], v[last] = v[last], v[left]
  QSort(v, left, last-1)
  QSort(v, last+1, right)
}

It's interesting to note that the function needs to be called with the first and last index of our slice in addition to the slice itself! You'd think you could just call QSort(mySlice) and be done with it.

When we think about it, that makes sense, because for QSort to be able to work recursively, we need to be able to tell it to work on smaller and smaller ranges of the underlying slice, and to do that, we need to be able to call it with the starting and ending indices.

So if our above slice of ints is assigned to a varable v, we call QSort like so:

QSort(v, 0, len(v)-1)

QSort first call:

v: [6, 63, 58, 24, 54, 28, 33, 80, 100]
left: 0
right: 8
last: 0

So we definitely get past this:

  if left >= right {
    return
  }

Now we chose the pivot from the middle and swap it to the start of the slice:

  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]

As near as I can tell, this is in case the list is sorted; we then take the pivot that would be the best pivot to use for a sorted slice. It's interesting that we pick the pivot and then put it at the start of the array. I *think* this might make other implementation details easier, because then you have the pivot out of the way while you walk the remaining array looking for (and swapping) values that are less than the pivot. But I'm not sure.

So now our slice looks like this:

v: [54, 63, 58, 24, 6, 28, 33, 80, 100]

6 and 54 have swapped places.

Now we assign the left index to last:

left: 0
right: 8
last: 0

last will be the last index of the slice on our left. It will increment every time we swap a value smaller than the pivot into a new slice index, and we will feed it to a recursive call to QSort later.

Note that our for loop does not start at left, it starts at left + 1, so that we don't disturb the pivot that's all the way left.

Now we read the slice from left +1 to right, and whenever an item is larger than our pivot (located in v[left] we put it in the next available slot in the left part of the slice (which is why we increment last when the swap needs to happen):

    if v[i] < v[left] {
      last++
      v[last], v[i] = v[i], v[last]
    }

So the iteration looks like this:

left: 0
right: 8
last: 0
i: 1
v: [54, 63, 58, 24, 6, 28, 33, 80, 100]
         i                       right

63 is not less than 54, so next loop, increment i.

left: 0
right: 8
last: 0
i: 2
v: [54, 63, 58, 24, 6, 28, 33, 80, 100]
             i                   right

58 is not less than 54, so next loop, increment i.

left: 0
right: 8
last: 0
i: 3
v: [54, 63, 58, 24, 6, 28, 33, 80, 100]
                 i               right

24 is less than 54, so increment last and swap v[i] with v[last] (swap v[3] with v[1])

left: 0
right: 8
last: 1
i: 3
v: [54, 24, 58, 63, 6, 28, 33, 80, 100]
      last       i               right

next loop, increment i.

left: 0
right: 8
last: 1
i: 4
v: [54, 24, 58, 63, 6, 28, 33, 80, 100]
      last          i            right

6 is less than 54, so increment last and swap v[i] with v[last] (swap v[4] with v[2])

left: 0
right: 8
last: 2
i: 4
v: [54, 24, 6, 63, 58, 28, 33, 80, 100]
         last       i            right

next loop, increment i.

left: 0
right: 8
last: 2
i: 5
v: [54, 24, 6, 63, 58, 28, 33, 80, 100]
         last           i        right

28 is less than 54, so increment last and swap v[i] with v[last] (swap v[5] with v[3])

left: 0
right: 8
last: 3
i: 5
v: [54, 24, 6, 28, 58, 63, 33, 80, 100]
             last       i        right

next loop, increment i.

left: 0
right: 8
last: 3
i: 6
v: [54, 24, 6, 28, 58, 63, 33, 80, 100]
             last           i    right

33 is less than 54, so increment last and swap v[i] with v[last] (swap v[5] with v[3])

left: 0
right: 8
last: 4
i: 6
v: [54, 24, 6, 28, 33, 63, 58, 80, 100]
                 last       i    right

next loop, increment i.

left: 0
right: 8
last: 4
i: 7
v: [54, 24, 6, 28, 33, 63, 58, 80, 100]
                 last           i
                                 right

80 is not less than 54, so next loop, increment i.

left: 0
right: 8
last: 4
i: 8
v: [54, 24, 6, 28, 33, 63, 58, 80, 100]
                 last                i
                                 right

100 is not less than 54, so next loop, increment i.

left: 0
right: 8
last: 4
i: 9
v: [54, 24, 6, 28, 33, 63, 58, 80, 100]
                 last                    i
                                 right

Break out of loop because i, 9, is greater than right, 8.

Now, put the pivot in its correct location at last, and put last back at left, which is fine, because the value at last is definitely less than the pivot.

  v[left], v[last] = v[last], v[left]
  v[0], v[4] = v[4], v[0]
left: 0
right: 8
last: 4
v: [33, 24, 6, 28, 54, 63, 58, 80, 100]
  left           last            right

With the pivot, 54, at its correct final index in the slice, the correctly sorted slice would look like

qsorted[33, 24, 6, 28] + [54] + qsorted[63, 58, 80, 100]

So that looks like

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Notice last-1 and last+1 to leave last out of it, because last is pointing to our pivot at its correct place in the sorted slice.

In other words:

  QSort(v, 0, 3)  // let's call this QSort L1
  QSort(v, 5, 8)  // Let's call this QSort R1

Continuing on with the call, L1, to QSort(v, 0, 3):

left: 0
right: 3
last: 0
v: [33, 24, 6, 28, 54, 63, 58, 80, 100]

left is not greater than or equal to right, so we pass this:

  if left >= right {
    return
  }

We swap the middle index, our pivot, with the left-most index:

  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]
  v[0], v[1] = v[1], v[0]
left: 0
right: 3
last: 0
v: [24, 33, 6, 28, 54, 63, 58, 80, 100]
            right

We make last equal to left:

left: 0
right: 3
last: 0
v: [24, 33, 6, 28, 54, 63, 58, 80, 100]
            right

Now we enter our loop, with i starting at left + 1:

left: 0
right: 3
last: 0
i: 1
v: [24, 33, 6, 28, 54, 63, 58, 80, 100]
         i  right

33 is not less than 24, so next loop, increment i.

left: 0
right: 3
last: 0
i: 2
v: [24, 33, 6, 28, 54, 63, 58, 80, 100]
            right
            i

6 is less than 24, so increment last and swap v[i] with v[last] (swap v[2] with v[1])

left: 0
right: 3
last: 1
i: 2
v: [24, 6, 33, 28, 54, 63, 58, 80, 100]
            right
            i
     last

next loop, increment i.

left: 0
right: 3
last: 1
i: 3
v: [24, 6, 33, 28, 54, 63, 58, 80, 100]
            right
                i
     last

28 is not less than 24, so next loop, increment i.

left: 0
right: 3
last: 1
i: 4
v: [24, 6, 33, 28, 54, 63, 58, 80, 100]
            right
                    i
     last

Break out of loop because i, 4, is greater than right, 3.

Now, put the pivot in its correct location at last, and put last back at left, which is fine, because the value at last is definitely less than the pivot.

  v[left], v[last] = v[last], v[left]
  v[0], v[1] = v[1], v[0]
left: 0
right: 3
last: 1
v: [6, 24, 33, 28, 54, 63, 58, 80, 100]
 left       right
     last

With the pivot, 24, at its correct final index in the slice, the correctly sorted sub slice would look like

qsorted[6] + [24] + qsorted[33, 28]

So that looks like

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Notice last-1 and last+1 to leave last out of it, because last is pointing to our pivot at its correct place in the sorted slice.

In other words:

  QSort(v, 0, 0)  // let's call this QSort L2
  QSort(v, 2, 3)  // Let's call this QSort R2

Continuing on with the call, L2, to QSort(v, 0, 0):

left is equal to right, so we return from this:

  if left >= right {
    return
  }

Continuing on with the call, R2, to QSort(v, 2, 3):

left is not greater than or equal to right, so we pass this:

  if left >= right {
    return
  }

We swap the middle index, our pivot, with the left-most index:

  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]
  v[2], v[2] = v[2], v[2]

The interesting thing being our numbers are now so close together that we just swapped the first value with itself!

left: 2
right: 3
last: 0
v: [6, 24, 33, 28, 54, 63, 58, 80, 100]
         left
            right

We make last equal to left:

left: 2
right: 3
last: 2
v: [6, 24, 33, 28, 54, 63, 58, 80, 100]
         left
            right
         last

Now we enter our loop, with i starting at left + 1:

left: 2
right: 3
last: 2
i: 3
v: [6, 24, 33, 28, 54, 63, 58, 80, 100]
         left
            right
         last
                i

28 is less than 33, so increment last and swap v[i] with v[last] (swap v[3] with v[2])

left: 2
right: 3
last: 2
i: 3
v: [6, 24, 28, 33, 54, 63, 58, 80, 100]
         left
            right
         last
                i

next loop, increment i.

left: 2
right: 3
last: 2
i: 4
v: [6, 24, 28, 33, 54, 63, 58, 80, 100]
         left
            right
         last
                    i

Break out of loop because i, 4, is greater than right, 3.

Now, put the pivot in its correct location at last, and put last back at left, which is fine, because the value at last is definitely less than the pivot.

  v[left], v[last] = v[last], v[left]
  v[3], v[3] = v[3], v[3]

Again, because our numbers are so close together, we swap the pivot with itself!

left: 2
right: 3
last: 2
v: [6, 24, 28, 33, 54, 63, 58, 80, 100]
         left
            right
         last

With the pivot, 24, at its correct final index in the slice, the correctly sorted sub sub slice would look like

qsorted[] + [24] + qsorted[28]

So that looks like

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Notice last-1 and last+1 to leave last out of it, because last is pointing to our pivot at its correct place in the sorted slice.

In other words:

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Continuing on with the call, L3, to QSort(v, 2, 1):

left is greater than right, so we return from this:

  if left >= right {
    return
  }

Continuing on with the call, R3, to QSort(v, 3, 3):

left is equal to right, so we return from this:

  if left >= right {
    return
  }

Continuing on with the call, R1, to QSort(v, 5, 8):

left: 5
right: 8
last: 0
v: [6, 24, 28, 33, 54, 63, 58, 80, 100]

left is not greater than or equal to right, so we pass this:

  if left >= right {
    return
  }

We swap the middle index, our pivot, with the left-most index:

  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]
  v[5], v[6] = v[6], v[5]
left: 5
right: 8
last: 0
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right

We make last equal to left:

left: 5
right: 8
last: 5
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last

Now we enter our loop, with i starting at left + 1:

left: 5
right: 8
last: 5
i: 6
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last
                            i

63 is not less than 58, so next loop, increment i.

left: 5
right: 8
last: 5
i: 7
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last
                                i

80 is not less than 58, so next loop, increment i.

left: 5
right: 8
last: 5
i: 8
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last
                                     i

100 is not less than 58, so next loop, increment i.

left: 5
right: 8
last: 5
i: 9
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last
                                          i

Break out of loop because i, 9, is greater than right, 8.

Now, put the pivot in its correct location at last, and put last back at left, which is fine, because the value at last is definitely less than the pivot.

  v[left], v[last] = v[last], v[left]
  v[5], v[5] = v[5], v[5]

Here, because everything was larger than the pivot, we end up swapping the pivot with itself, leaving it where it was!

left: 5
right: 8
last: 5
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                     left
                                 right
                     last

With the pivot, 58, at its correct final index in the slice, the correctly sorted sub slice would look like

qsorted[] + [58] + qsorted[63, 80, 100]

So that looks like

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Notice last-1 and last+1 to leave last out of it, because last is pointing to our pivot at its correct place in the sorted slice.

In other words:

  QSort(v, 5, 4)  // let's call this QSort L4
  QSort(v, 6, 8)  // Let's call this QSort R4

Continuing on with the call, L4, to QSort(v, 5, 4):

left is greater than right, so we return from this:

  if left >= right {
    return
  }

Continuing on with the call, R1, to QSort(v, 6, 8):

left: 6
right: 8
last: 0
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                         left
                                 right

left is not greater than or equal to right, so we pass this:

  if left >= right {
    return
  }

We swap the middle index, our pivot, with the left-most index:

  v[left], v[(left+right)/2] = v[(left+right)/2], v[left]
  v[6], v[7] = v[7], v[6]
left: 6
right: 8
last: 0
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right

We make last equal to left:

left: 6
right: 8
last: 6
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right
                         last

Now we enter our loop, with i starting at left + 1:

left: 6
right: 8
last: 6
i: 7
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right
                         last
                                i

63 is less than 80, so increment last and swap v[i] with v[last] (swap v[7] with v[7])

left: 6
right: 8
last: 6
i: 7
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right
                             last
                                i

next loop, increment i.

left: 6
right: 8
last: 6
i: 8
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right
                             last
                                     i

100 is not less than 80, so next loop, increment i.

left: 6
right: 8
last: 6
i: 9
v: [6, 24, 28, 33, 54, 58, 80, 63, 100]
                         left
                                 right
                             last
                                         i

Break out of loop because i, 9, is greater than right, 8.

Now, put the pivot in its correct location at last, and put last back at left, which is fine, because the value at last is definitely less than the pivot.

  v[left], v[last] = v[last], v[left]
  v[6], v[7] = v[7], v[8]
left: 6
right: 8
last: 6
v: [6, 24, 28, 33, 54, 58, 63, 80, 100]
                         left
                                 right
                             last

With the pivot, 80, at its correct final index in the slice, the correctly sorted sub slice would look like

qsorted[63] + [80] + qsorted[100]

So that looks like

  QSort(v, left, last-1)
  QSort(v, last+1, right)

Notice last-1 and last+1 to leave last out of it, because last is pointing to our pivot at its correct place in the sorted slice.

In other words:

  QSort(v, 6, 6)  // let's call this QSort L5
  QSort(v, 8, 8)  // Let's call this QSort R5

Continuing on with the call, L5, to QSort(v, 6, 6):

left is equal to right, so we return from this:

  if left >= right {
    return
  }

Continuing on with the call, R5, to QSort(v, 8, 8):

left is equal to right, so we return from this:

  if left >= right {
    return
  }

DONE!