Yesterday’s post from Programming Praxis asks us to solve a problem known as the Dutch National Flag problem (attributed to Edsgar Dijkstra): sort an array of red, white and blue symbols so that all reds come together, followed by all whites, followed finally by all blues.
For once, the first algorithm to come to mind was actually the correct one. This doesn’t always happen quite so nicely, but it is pleasant when it does. The basic idea is similar to the in-place partition often used with quicksorts1: Start with labels at the beginning and end of the array; advance the labels towards each other, swapping elements on the wrong side of the pivot.
In this case, there are three labels (one for each color), but that doesn’t actually change the algorithm much at all. Here are the basic steps:
- Start with three labels Lr = 0, Lw = 0, Lb = |data| – 1
- While Lw < Lb, check the element at Lw:
- Red: Swap Lr and Lr and increment Lr and Lw
- White: Increment Lw
- Blue: Swap Lw and Lb and decrement Lb
That’s all there is to it. Here’s how you might translate that into Racket:
(define (vector-swap! v i j) (define temp (vector-ref v i)) (vector-set! v i (vector-ref v j)) (vector-set! v j temp)) (define (partition-flag! flag) (let loop ([r 0] [w 0] [b (- (vector-length flag) 1)]) (cond [(> w b) flag] [(eq? (vector-ref flag w) 'red) (vector-swap! flag r w) (loop (+ r 1) (+ w 1) b)] [(eq? (vector-ref flag w) 'white) (loop r (+ w 1) b)] [(eq? (vector-ref flag w) 'blue) (vector-swap! flag w b) (loop r w (- b 1))]))) |
Since the code is relative straight forward, I went ahead off the deep end and worked out a visualization that will run in your web browser. If everything works as it should, clicking ‘Run Demo’ below should show you exactly what happens when partitioning a 50-element vector.
Demo
jQuery(function($) { // Context to draw with var canvas = $('#canvas')[0]; var context = canvas.getContext('2d'); var progress = $('#progress'); // Various constants var demo_size = 50; var demo_step = 250; var left_offset = canvas.width / 10; var top_offset = canvas.height / 6; var block_width = 4/5 * canvas.width / demo_size; var block_height = 1/3 * canvas.height; var label_width = 3; var label_height = block_height / 3; // Data for the simulation var styles = new Array('red', 'white', 'blue'); var labels = new Array(0, 0, 0); var data = new Array(); var tick = 0; var comparisons = 0; var swaps = 0; // Logging function function log(msg) { progress.html( 'Step ' + tick + ':<br />' + msg + '<br /><br />' + 'So far:<br />' + comparisons + ' comparisons<br />' + swaps + ' swaps<br />' + demo_size + ' elements' ); } // Run the entire demo function showDemo() { // Generate a new set of data data = new Array(demo_size); for (var i = 0; i < data.length; i++) { data[i] = Math.floor(Math.random() * 3); } // Reset the arrays to left, left, and right labels = new Array(0, 0, data.length - 1); // Clear progress and raw everything the first time progress.text(''); draw(); // Run the update functions comparisons = 0; swaps = 0; tick = 0; function simStep() { tick += 1; if (step()) setTimeout(simStep, demo_step); else log("finished"); draw(); } setTimeout(simStep, demo_step); } // Swap two data points function swap(i, j) { swaps += 1; var temp = data[i]; data[i] = data[j]; data[j] = temp; } // Take a single step of the sorting function function step() { // Stop when the white and blue pointers cross if (labels[1] > labels[2]) { return false; } // On red: swap to red and increment red and white else if (data[labels[1]] == 0) { log('red, swap to red and increment red and white'); swap(labels[0], labels[1]); labels[0] += 1; labels[1] += 1; } // On white: increment white else if (data[labels[1]] == 1) { log('white, increment white'); labels[1] += 1; } // On blue: swap to blue and decrement blue else if (data[labels[1]] == 2) { log('blue, swap to blue and decrement blue'); swap(labels[1], labels[2]); labels[2] -= 1; } // Done, signify that we took a step comparisons += 1; return true; } // Draw the blocks and labels function draw() { // Clear the screen context.clearRect(0, 0, canvas.width, canvas.height); // Draw blocks context.lineWidth = 1; context.strokeStyle = 'black'; for (var i = 0; i < data.length; i++) { context.beginPath(); context.rect( left_offset + i * block_width, top_offset, block_width, block_height ); context.fillStyle = styles[data[i]]; context.fill(); context.stroke(); } // Draw labels var center_x = 0, center_y = 0; for (var i = 0; i < labels.length; i++) { center_x = left_offset + labels[i] * block_width + block_width / 3; center_y = 2 * top_offset + block_height + i * label_height; context.beginPath(); context.rect( center_x - label_width / 2, center_y - label_height / 2, label_width, label_height ); context.fillStyle = styles[i]; context.fill(); context.stroke(); } } $('#run').click(showDemo); }); |
- This is actually more of a partitioning problem than a sorting one [↩]
| ← Cheesy chicken a la slow cooker | All | My personal (NaNoWriMo) March Madness → |
| ← February #1GAM post-mortem | By category | Knight moves → |