Yes. I too think that's reasonable. It isn't how I would
usually write an insertion sort, but presenting it as one
way of writing insertion sort is appropriate IMO.
> and some people would not call your example bubble sort because it
> has no "almost sorted" advantage.
Yes, I acknowledge that. It kind of surprised me when I first
heard it but it is obviously true (that is, that some people
would have that reaction). As a double check on my own usage I
consulted two acquaintances, with different backgrounds, and
phrasing the question as neutrally as I could. Both of them
answered the way I would (i.e., that early termination is not
essential to be termed bubble sort), but all that proves is only
that different people mean somewhat different things when they
talk about it.
> These discussions often end up a bit ungrounded because the
> terms are not exact.
Certainly they often do end up a bit ungrounded. I might say
that's largely because different people define the terms slightly
differently and also vary in how much latitude they are willing
to accept. I myself tend to fall in the more tolerant admission
in this area. But the main thing is I think we agree on the key
point, that it's hard to reach a shared conclusion because people
bring with them (and sometimes unconsciously) different working
assumptions.
> And if you don't want to do the insert using swaps, why not write
> an insert function? In what way would that not be an easy-to
> understand abstraction? Although insert is more complex that
> swap, it has the effect of cutting the complexity of the sort
> function in half: one loops inserts each element into a growing
> sorted array; inserting each element needs a simple loop.
Let me try to respond to this paragraph more all-at-once than a
sentence at a time.
First I think all your statements and questions are reasonable.
Writing a separate function to perform insertions clearly can
help simplify the overall algorithm. I suppose the same sort of
thing might be doable with bubble sort, but it's less obvious
how much benefit that would bring.
I suspect a large part of the difficulty we have in reconciling
our different points of view comes from how we characterize the
different sorting algorithms and how we measure "simpleness". My
view of things like sorting algorithms tends to be conceptual: I
remember (what I think are) the essential elements, generally the
fewer the better, and forget the details because for me it's
usually easier to reconstruct them than remember them. The
central component of bubble sort (as I conceptualize it, in case
that needs saying) is comparing and possibly swapping adjacent
elements. The central component of insertion sort is finding and
moving an element into the appropriate place of an already sorted
subarray. (Sorry for the poor phrasing there.) Insertion isn't
a very complicated abstraction but it's a "larger" abstraction
than swapping - it involves a larger part of the element array,
and in a qsort-like setting needs the comparison function, which
swap does not.
Summarizing by example: for me a cocktail sort (which I learned as
a "brick and bubble" sort) is really just a bubble sort; that the
"bubbles" are going two different directions doesn't change that.
For deciding which is simpler, I think bubble sort is simpler
because I think the "central operation" (swap versus insert) is
easier to explain to an eight year old. I will grant you that both
of these views are both personal and subjective. Perhaps part of
my point is to get the personalness and subjectiveness out into the
open rather than hidden behind seemingly absolute statements.
Let me run now through several revisions of a function that shows
a way of coding a sorting algorithm that I might call bubble
sort. All of these writings have the property that early
termination (that is, in time closer to O(N) than to O(N log N)
will occur for some set of inputs that are close to being sorted.
I wrote all these myself, in the order shown, but not exactly
with a clear destination in mind. So some of my comments below
have the benefit of hindsight, although the algorithms themselves
mostly don't.
(Incidental note: I use functions 'backwards()' and 'exchange()'
for purposes of comparing and swapping, respectively. This was
done to allow instrumentation and measurement.)
First, the most simple-minded version:
void
naive_bubble_sort( int a[], int n ){
int i;
start:
for( i = 1; i < n; i++ ){
if( backwards( a, i-1, i ) ){
exchange( a, i-1, i );
goto start;
}
}
}
In descriptive English: look for the first out-of-order pair.
If one is found, put them in order, and simply start again from
the beginning.
(Historical note: this version of bubble sort was actually used
for a common command in a popular OS of the late 1960's and early
1970's. Fortunately it was sorting a very limited number of
items.)
I think this is great place to start an explanation of bubble
sort, because the code is both easy to understand and obviously
works correctly. IMO the 'goto' actually helps the presentation,
at least for the first version, but let's bow to structured
tradition and eliminate it. Doing this is straightforward:
void
poor_mans_bubble_sort( int a[], int n ){
int i;
for( i = 1; i < n; i++ ){
if( backwards( a, i-1, i ) ){
exchange( a, i-1, i );
i = 0;
}
}
}
Now suitably goto-free, but both of these have an awful property:
the usual running time is not even O(N*N) but O(N*N*N). Terrible!
To improve that, we might observe that when the out-of-order pair
is reached, all the ones before it haven't changed and so don't
need to be looked at again. What has changed is a[i-1], so we
should go back and look at a[i-2] and a[i-1] -- assuming, that is,
that there /is/ an a[i-2]; if i is less than 2 we can simply go
forward. This ideas gives the next revision:
void
better_bubble_sort( int a[], int n ){
int i;
for( i = 1; i < n; i++ ){
if( backwards( a, i-1, i ) ){
exchange( a, i-1, i );
if( i > 1 ) i -= 2;
}
}
}
This "minor" change is surprisingly effective. Not only has the
running time improved from O(N*N*N) to O(N*N), but it does early
termination quite well. On scrambled inputs it's competitive with
the Knuth version of bubble sort (sometimes better, sometimes
worse), and on "nearly sorted" inputs it does well on a larger set
of inputs than the Knuth bubble sort. It needs more explanation
to show that it's correct, which of course is usually the cost
of a fancier algorithm.
Besides being faster on more nearly sorted inputs, this version
behaves much like other bubble sorts. In particular it uses about
twice as many compares as (linear) insertion sort. The "bubbles"
are mostly "sinking" rather than "rising", but for me that doesn't
change things enough to make a difference.
(Please also see the note at the bottom regarding this function.)
We can make a further improvement by noting a simlar property in
the other direction. After finding an out-of-order pair, and
dragging the smaller element down to its appropriate depth, there
is no need check the elements between there and where we first
found it. We add a variable 'next_i' to remember the high water
mark, and use 'next_i++' whenever we switch directions from
"downwards" to "upwards". Here is the code:
void
even_better_bubble_sort( int a[], int n ){
int i = 1, next_i = 2;
while( i < n ){
if( backwards( a, i-1, i ) ){
exchange( a, i-1, i );
i = i > 1 ? i-1 : next_i++;
} else {
i = next_i++;
}
}
}
Probably because of how I arrived at this refinement, I thought of
it as an enhanced bubble sort. Then I ran my little test harness,
and was surprised to see that it does exactly the same number of
compares as (linear) insertion sort. In retrospect, naturally, I
can see that what is being done here is isomorphic to a swapping
insertion sort. But it wasn't what I was expecting when I first
coded it. I hope at least that the journey was interesting.
Two further comments. First is that I coded up all these version
myself without consulting any references. It was only after I had
written all of them (and run the measurements) that I learned from
a friend that what I call here "better_bubble_sort()" is known
more widely as "Gnome sort". So that was serendipitous.
My closing comment gets back to comparing the two sorting methods.
If we contrast the last two functions, what I think stands out is
the extra work done to allow the skipping of comparisons between
the sink point and the high water mark. To me this difference both
shows why insertion sort runs faster than bubble sort and also
demonstrates that insertion sort is more elaborate than bubble
sort: more mechanism is needed, which requires more explanation,
but that additional mechanism is what gives insertion sort its
speed advantage over bubble sort. I admit the conclusion is both
personal and subjective. To me it still seems right because it
isn't what I set out to do - my motivation was only to present a
series of algorithms that I would all call bubble sort, to show
what I mean when I use the term. I was actually very surprised
when I saw that the last version had the same behavior as does
insertion sort.
Sorry this has ended up being so long, and again I hope it was
at least interesting.