87 comments

[ 4.9 ms ] story [ 220 ms ] thread
It's fantastic. I don't know CoffeeScript so all I did was compare the pre-written ones to each other and see if they did what I expected them to do, but it's a nice visual representation. The only problem I have with it is that the neon yellow on black with a gray/white background can be pain inducing, especially when moving quickly.
Changed to a darker purple. You're right, it was pretty jarring.
Speaking of pain inducing, I found watching the bubble sort very pain inducing! I've always known it was slow compared to other options, but this make you never want to see the thing mentioned again!
Looks like bubble sort is so slow because this implementation has a very high ratio between the costs of a compare and a swap. A swap involves redrawing large parts of the graphical area, while a compare is fast. Bubble sort makes N^2 swaps, as compared to insertion sort which makes N^2 comparisons but only N swaps.

When a swap costs the same as a compare, bubble sort is as fast as the other N^2 sorts.

Turn off "Quick Compare" to make comparisons take longer if you want a different ratio. By default, it takes about 1/15 as long to compare as swap (I thought the visualization looked better this way).
I know Bubble sort is "bad", but there's no reason to make it worse than it needs to be. The current algorithm continues comparing the items that have already "bubbled" (or in this case, "sunk") by iterating the whole length on each pass, rather than shorting the loop to only consider the unsorted portion.

A minor tweak is (changed lines marked with #):

  bubbleSort = ->
  VA.locals.swapped = true
    y = VA.length  # grab initial length
    while VA.locals.swapped
      y--          # shorten sorted portion on each pass
      VA.locals.swapped = false
      for x in [0...y] # only iterate to y
        VA.locals.x = x
        if VA.gt(x, x + 1)
          VA.swap(x, x + 1)
          VA.locals.swapped = true

  bubbleSort()
Neat. I came back 5 hours later, and the change was live. Thanks. (I was going to submit a change via Git when I got home, but you beat me to it!)
I don't like the insertion sort. Couldn't it do a binary search on the sorted part of the list, when it's looking for the place to put the next element?
I coded up the easiest version of each algorithm, rather than the most efficient. (Quicksort uses the first element as a pivot, rather than something more intelligent) Feel free to email me an algorithm if you think it should be there/is better than what I wrote. The pre-written algorithms weren't exactly my focus.
Fair enough. I'll try to write one if I can learn coffeescript right now :)
This is fantastic for teaching sorting algorithms visually. I'm going to see if I can forward this to my former professors and teachers.
Needs a shuffle() function - can't bogosort without it.
You should be able to write one with swaps - go for it.
Bogo

  shuffle = ->
    for x in [0..VA.length]
      VA.swap x, Math.floor(Math.random()*VA.length)
  
  sort = ->
    VA.play()
    if !checkSorted()
      shuffle()
      setTimeout(sort, 10)
    
  checkSorted = ->
    for x in [0..VA.length-1]
      if VA.gte(x, x+1)
          return false
    return true
  
  sort()
I wanted to implement the sleep sort algorithm, but I can't use

  VA = [4, 2, 5, 7];
  for(x=0; x<VA.length; x++) {
      var y = VA[x];
      setTimeout('console.log(' + y + ')', y*1000);
  }
it tells me I can't use "var". And when I remove that I get some syntax error. Any nice alternative way to implement a sleep sort in coffescript?
This is the coffeescript equivalent, not sure how to get it working in the visualisation though as it doesn't involve swaps.

  VA = [4, 2, 5, 7]
  for y in VA
    setTimeout('console.log(' + y + ')', y*1000)
Try this:

    VA = [4, 2, 5, 7]
    for y in VA
      setTimeout "console.log(#{y})", y*1000
(comment deleted)
(comment deleted)
>Nothing will display if there is an infinite loop.

Did someone solve the halting problem while I wasn't looking?

I assume it just computes every step, and then displays them all.
This is exactly what's going on. You can't get a reflow on the page without giving up control, and I didn't want to require people to write asynchronous code to give up control after every swap. So it runs your code and queues up actions, to be played back when its done. This is also why you have to use the methods on VA instead of working directly on the array.
Hash sort is surprisingly fast:

  for x in [0 ... VA.length]
    ind = VA.get(x)-1
    VA.swap(x, ind + VA.length)
  for x in [0 ... VA.length]
    VA.swap(x, x + VA.length)
That's cheating somehow...
Trading a tremendous amount of memory for speed. Try replacing a list of integers with a list of Images. You might lock up the browser.
(comment deleted)
In 1/2 as many swaps:

  for x in [0 ... VA.length]
    ind = VA.get(x)-1
    while ind != x
      VA.swap(x, ind)
      ind = VA.get(x)-1
(comment deleted)
The quicksort implementation is incorrect. The code simply picking the left-most element as the pivot, and as a result it becomes pathologically slow for nearly-sorted array -- for example, run bubblesort then quicksort, or run quicksort twice -- not to mention potential stack overflow problem due to O(N) recursive call.
Poor choice of the pivot makes the implementation bad, but absolutely not incorrect.
That's still a quicksort - it always has n^2 as its worst case I thought? If you pick the wrong pivot each time.
There are ways to make the n^2 worst case extraordinarily unlikely, and there is a way to make it impossible (though it is highly technical).

Some others have posted methods (random pivot, median of 3). The gnu libc qsort implementation is a good learning tool (it uses median of 3). I think CLRS has the guaranteed n log n version.

> That's still a quicksort - it always has n^2 as its worst case I thought? If you pick the wrong pivot each time.

Pick median of three random elements as pivot; it's hard to pick wrong pivot every time unless your RNG always returns 7.

Why three random elements though? If you expect some structure in the array, then picking pivots based on that structure makes more sense (e.g. first, middle and last for sorted, reverse sorted for partially sorted arrays).

On the other hand, if you are sorting unsanitised input, then random is good, but it should be secure pseudo-random numbers if it's important to your security/performance whilst under attack.

(comment deleted)
Add the following right above the line "pivot = left" to fix it:

VA.swap(left, Math.floor(Math.random() * (right - left + 1)) + left)

Alternatively you can insert this code for the median of 3 version:

   centre = (left + right) / 2;
   if VA.lt(left, centre)
     if VA.lt(centre, right)
       VA.swap(left, centre)
     else if VA.lt(left, right)
       VA.swap(left, right)
   else
     if VA.lt(centre, right)
       VA.swap(left, centre)
     else if VA.lt(left, right)
       VA.swap(left, right)
I said this in another comment, but I coded up the simplest version of each algorithm - the quicksort is correct, just not optimal. The focus was on letting people write their own code, not the pre-written algorithms.
Radix-exchange sort:

  sort = (begin, end, bit) ->
    i = begin
    j = end
    mask = 1 << bit
    while i < j
      while i < j and !(VA.get(i) & mask)
        ++i
      while i < j and (VA.get(j - 1) & mask)
        --j
      if i < j
        VA.swap(i++, --j)

    if bit and i > begin
      sort(begin, i, bit - 1)
    if bit and i < end
      sort(i, end, bit - 1)

  sort(0, VA.length, 30)
Neat, I've never actually seen a radix sort before.

I thought it was a bit clearer to understand what it was doing when I stuck

  VA.locals.bit = bit
at the very top of your sort function.
Had fun writing a gnome-sort in a minute or two:

    pos = 1
    while pos < VA.length
      if (VA.gte(pos, pos-1))
        pos++
      else
        VA.swap(pos, pos-1)
        if (pos > 1)
          pos--
        else
          pos++
The input set is problematic because it contains every number from 0 to VA.length exactly once.

This allows "algorithms" like this:

  for x in [0...VA.length]
    while (VA.get(x) > x)
      VA.swap(VA.get(x), x)
Its not a game; its a learning tool. It doesn't bother me that people can do that.

Also, that's not going to work quite right - the values are [1..VA.length] (see the bottom of the page), while the indices are [0...VA.length].

This might be a dumb question, but can you add numbers about the size of the stack to the right-side statistics? I.e. How many variables and calls to functions are stored in memory at at a time.

I'm not sure if this possible or relevant to record based on how Javascript engines compile and work, but it would seem interesting to know.

The implementation here is doing tricksie things behind the scenes to make this work without instrumenting the javascript engine.

Instead of actually doing the operations, your sorting code outputs a list of actions to perform, which is then displayed on the webpage.

Yes; however, at each step I'm already storing the state of VA.locals. If you can show me how I would get this data, I'd be happy to display it.
How about adding the VA.equals and VA.notequals functions? You've got all the other comparators, but it seems a shame that you don't have these.
I...don't know how I missed them. They're in now.
(comment deleted)
Merge sort:

   s = (lo, hi) ->
      if lo==hi
         return
      if lo+1==hi
         if VA.gt(lo,hi)
            VA.swap(lo,hi)
         return
      mid=Math.floor(lo+(hi-lo)/2)
      s(lo,mid)
      s(mid+1,hi)
      mid++
      while lo<=mid && mid<=hi
         if VA.gt(lo,mid)
            VA.insert(mid, lo)
            mid++
         else
            lo++
   s(0,VA.length)
Merge sort w/o using insert (and probably with some bugs...):

    tmp = VA.length
    
    merge = (start1, start2, end2) ->
      out = start1
      end1 = start2 - 1
      for x in [tmp+out .. tmp+end2]
        if start1 <= end1 && start2 <= end2
          if VA.lt(start1, start2)
            VA.swap(start1, x)
            ++start1
          else
            VA.swap(start2, x)
            ++start2
        else if start1 <= end1
          VA.swap(start1, x)
          ++start1
        else
          VA.swap(start2, x)
          ++start2
      for x in [out..end2]
        VA.swap(x, tmp+x)
    
    mergesort = (left, right) ->
      if (right - left) == 1
        if VA.gt(left, right)
          VA.swap(left, right)
      else if (right - left) == 0
        /* do nothing */
      else
        split = Math.floor((right-left)/2) + left;
        mergesort(left, split - 1)
        mergesort(split, right)
        merge(left, split, right)
    
    mergesort(0, VA.length - 1)
Added as a prewritten sort. Thanks!
(comment deleted)
Very neat, but here's a couple of additions I'd like to see:

1. Allow for the array to contain values outside of the range of [1..length]. Ideally, allow for duplicate values as well. Since you control the swap and insert operations, you can maintain two separate sets, one of the actual numbers, and one of the "normalized" values that you display as your graph.

2. Give us an operation to highlight a set of lines and have the highlights persist. I wanted to take mayoff's radix-exchange sort and tweak it to highlight the "working set" that it's currently sorting, but there's no way to do that. I'm thinking here that we just need one function VA.persistHighlight() which takes start and end indices and highlights them, and persists those highlights until a new call to VA.persistHighlight(). The "transient" highlights would be layered on top of the persistent one.

1. Requires a bit more work than I have time for right now. I'll keep it in mind.

2. Fantastic idea. This is exactly what I needed when I was struggling to write a quicksort from memory. Its in there now.

I just sent you a pull request that implements 1.
Sleep sort:

  pos = [0 ... VA.length]
  rpos = [0 ... VA.length]

  swap = (i, j) ->
    VA.swap i, j
    tmp = rpos[j]; rpos[j] = rpos[i]; rpos[i] = tmp
    pos[rpos[i]] = i; pos[rpos[j]] = j

  j = 0

  for i in [0 ... VA.length]
    v = VA.get(i)
    do (v, i) ->
      setTimeout ->
        # Move the element that was originally in position i to position j
        swap pos[i], j++
        VA.play()
      , 100 * v
definitely have the page start with an example.
Makes sense. Starts with a bubble sort now.