-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy path19_insertion_sort.html
514 lines (434 loc) · 19.3 KB
/
19_insertion_sort.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>19. Linear insertion sort</title>
<link rel="stylesheet" href="template/style.css">
</head>
<body>
<span style="float: right">
[
<a href="18_binary_insertion_sort.html">previous</a>,
<a href="index.html">contents</a>,
<a href="">next</a>
]
</span>
<a name="L19.-Linear-insertion-sort"></a>
<h1>19. Linear insertion sort</h1>
<a name="Thank-you-2c--noble-art."></a>
<h2>Thank you, noble art.</h2>
<p>Last time we started with <a href="https://www.youtube.com/watch?v=sIIS-UgixGE">“The Organ Grinder”</a> (Der Leiermann)
which explains how it feels to stand outside in the cold with an empty tray
being barked at by stray dogs.
That’s how it feels when I start here (joke).</p>
<p>Today, I decided to play another short song which addresses
another aspect of what we do.
This is a song by <a href="https://en.wikipedia.org/wiki/Franz_Schubert">Schubert</a> again, called <a href="https://en.wikipedia.org/wiki/An_die_Musik">“To Music”</a>
(An die Musik).
It’s a very short song which gives thanks to this great art which lifts us in
the grayest times of our lives.
That’s basically the words.
In this video the words will be first introduced by a great English pianist <a href="https://en.wikipedia.org/wiki/Gerald_Moore">Gerald Moore</a>, who accompanied many great singers in Schubert songs.
<a href="https://en.wikipedia.org/wiki/Elisabeth_Schwarzkopf">Elisabeth Schwarzkopf</a> was probably the greatest soprano of the last century.
She is also a beautiful woman.
Let us proceed… (<a href="https://www.youtube.com/watch?v=Bm_AKMV0ME0">Video here</a>).</p>
<p>Schubert was 18 years old when he composed it.
<em>If I had a choice between founding Facebook or writing this song, as my lifetime
accomplishment, I would not hesitate one second, and it’s not going to be the
Facebook</em>.
What was the point?
Why did I want to play this song?
Because I actually believe, against all odds, and in spite of the many terrible evidence to
the contrary, that the words which I used there for music; the noble art, actually
applies to programming.
In spite of all the terrible things which I
had to witness over my life, I am grateful.
I share the same attitude.
The song concludes with the words, “thank you, noble art.”
The only thing I could say is thank you.
It was wonderful that I was able to write code.
I think we need to learn to share this attitude.</p>
<a name="Linear-insertion-sort"></a>
<h2>Linear insertion sort</h2>
<p>It’s not clear whether binary insertion sort is the right algorithm to use at all.
If we have <code>BidirectionalIterator</code>, we could use regular insertion sort which is a
very good algorithm and we’ll need it later in the course.
It’s a very important component to be used in multiple places.
For example, when implementing quicksort.
Plus, we will be able to investigate
several interesting techniques and discover one deep problem on this very
simple sorting algorithm.
Binary search is not always a good thing to do.</p>
<p>Remember insertion sort is good when things are almost sorted.
They are pretty close to where they should be.
Binary search is going to poke far away.
It’s going to do a little poking, logarithmic poking,
but it’s going to poke potentially far away.
Since in many cases we are going to be using
insertion sort in the context where things are pretty close,
doing linear insertion might be a good thing to do.
Linear search is of course best on arrays that are already sorted.
You just look to the next guy, and do nothing.
It’s one comparison per element.</p>
<a name="Linear-insert"></a>
<h3>Linear insert</h3>
<p>How do you make code beautiful?
We find the main loop invariant.
In order to be able to do the right thing, we need to see what we need to accomplish.</p>
<p>Our goal, like binary insert, is to
insert an element into the portion
at the front of the range.
We first copy the element <code>value</code> out to insert,
and create a hole where it was left.
We then move the hole from the right,
as far left as possible (requiring <code>BidirectionalIterator</code>).</p>
<pre><code>[ | ]
^ ^
first hole
</code></pre>
<p>When are we allowed to move the hole?
What are the conditions?</p>
<ol>
<li><code>hole != first</code>. If this happens, we cannot move any further.</li>
<li><code>value < prev(hold)</code>.</li>
</ol>
<p>If both hold, we continue to move left.
Eventually, one of the conditions will not hold.
We can even prove it:
There are only finitely many things in the range,
so after so many iterations it will be exhausted (these termination proofs are not usually very profound).</p>
<p>In our code we will call <code>hole</code>, <code>current</code>:</p>
<pre><code>template <typename I, typename R>
// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
I linear_insert(I first, I current, R r) {
// precondition: is_sorted(first, current, r) && current is a valid iterator
typedef typename std::iterator_traits<I>::value_type T;
T value = *current;
while (first != current && r(value, *predecessor(current))) {
*current = *predecessor(current);
--current;
}
*current = value;
return current;
}
</code></pre>
<p>When <code>first == current</code> at the start, it will copy <code>*current</code> to a temporary
variable and put it right back.
Would a check be better to avoid this?
As we have talked about before, this is a case that seldom happens
whereas adding an explicit check would slow down every other case.</p>
<p>Of course, we need to define predecessor:</p>
<pre><code>template <typename I>
// requires I is BidirectionalIterator
inline
I predecessor(I x) { return --x; }
</code></pre>
<a name="Traditional-insertion-sort"></a>
<h3>Traditional insertion sort</h3>
<p>Now linear insertion sort is about identical to binary insertion sort,
we just use <code>linear_insert</code>, instead of <code>binary_insert</code>.</p>
<pre><code>template <typename I, typename N, typename R>
// I is BidirectionalIterator
// N is Integral
// R is WeakStrictOrdering on the value type of I
I linear_insertion_sort_n(I first, N n, R r) {
if (n == N(0)) return first;
I current = first;
++current;
N i(1);
while (i < n) {
// invariant: is_sorted_n(first, i, r) && std::distance(first, current) == i
linear_insert(first, current++, r);
++i;
}
return current;
}
</code></pre>
<p>Let’s write the version for bounded ranges.
It’s very easy.
As a base case in our induction, an empty range, and a one
element range, are both sorted.</p>
<pre><code>// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
void linear_insertion_sort(I first, I last, R r) {
if (first == last) return;
I current = first;
++current;
while (current != last) {
linear_insert(first, current, r);
++current;
}
}
</code></pre>
<a name="Sentinel-insertion-sort"></a>
<h2>Sentinel insertion sort</h2>
<p>I think we can optimize it further.
You might argue we don’t need to.
But, let me tell you one of the most humiliating times in my life.
I was giving a talk at <a href="https://en.wikipedia.org/wiki/Stanford_University">Stanford</a>.
A certain professor walked into the talk
and when I showed my code, roughly like what we just wrote (it was a little different, but same idea), I said, “it’s obviously optimal”.
This professor interrupted
and said, “no it’s not”.
The trouble is his name was Donald Knuth.
When somebody like Don Knuth says that your code is not optimal,
you are in a difficult situation.
His argument was that we do this conditional check:</p>
<pre><code>first != current
</code></pre>
<p><code>n</code> times, when we don’t need to.
If you’re really into performance you have to put a <a href="https://en.wikipedia.org/wiki/Sentinel_value">sentinel</a> in the back.
I was using sentinels before, but from that point on I decided
to make an effort to use them more.</p>
<p>This is a valid argument.
We are not here to impose some theoretical conditions on algorithms.
We are here to take existing efficient algorithms and find how to express them.
We have to write whatever we write to enable this code to work.
What this means is that we sometimes have to reject or ignore
other notions of “good software engineering” in order to get
the efficient algorithms.
This is a very simple loop, so literally every instruction counts.
Could we eliminate that condition?
Can we assure that we don’t need to perform it?
We cheat by changing the precondition.
What condition would allow us not to check?</p>
<p>Somehow we assure that the first element is always
smaller than the one we want to insert.
So copy paste our previous <code>linear_insert</code>
and let’s rewrite the precondition.</p>
<pre><code> // precondition: is_sorted(first, current, r) &&
// current is a valid iterator &&
// first != current &&
// !r(*current, *first)
</code></pre>
<p>Now we can remove the condition:</p>
<pre><code>template <typename I, typename R>
// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
I linear_insert_with_sentinel(I first, I current, R r) {
// precondition: is_sorted(first, current, r) &&
// current is a valid iterator &&
// first != current &&
// !r(*current, *first)
typedef typename std::iterator_traits<I>::value_type T;
T value = *current;
while (r(value, *predecessor(current))) {
*current = *predecessor(current);
--current;
}
*current = value;
return current;
}
</code></pre>
<a name="Application-to-quicksort"></a>
<h3>Application to quicksort</h3>
<p>Now we need to write a new insertion sort
which guarantees this condition.
Before we get there, let’s see if we can
make 1 or 2 additional components out of it that will help us sort.</p>
<p>Eventually we hope to study a very important algorithm called quicksort.
A long time ago the person who invented it, <a href="https://en.wikipedia.org/wiki/Tony_Hoare">Tony Hoare</a>
observed that quicksort becomes inefficient towards the end
of recursion, when you start doing partitions of very small things.
He suggested that we run insertion sort down there, at the lower
levels of recursion.
Originally people thought they would go down recursively and call insertion
sort every time we reach a small subrange.
But, Hoare observed you really don’t need to call insertion sort many times.
You can just stop the recursion when quicksort sorts things up to a certain size,
and then, at the end, run one pass of insertion sort, over the whole thing.
Because when things are almost where they should be, insertion sort is effective.
Quicksort can guarantee that eventually everyone will be within some threshold of their final destination.</p>
<p>Let’s assume we are sorting a million elements.
After we are done with quicksort we have a threshold,
say <code>k</code>, and all the elements are partitioned.
The first partition will be somewhere within the first k elements,</p>
<pre><code>[ | ... ]
^ ^
first first + k
</code></pre>
<p>What I realized is that the elements in that first sub range <code>[first, first + k)</code>
are all natural sentinels for all the others.
What we can do is use non-sentinel insertion sort for the first sub range.
But, then we can take the smallest and use it as a sentinel for all past it.</p>
<p>For example, when we stop quicksort early we might have:</p>
<pre><code>[ 1, 3, 5 | 2, 11, 17 ... ]
</code></pre>
<p>5 is not a sentinel for the right side, because 2 is smaller.
1 is the sentinel.
The line drawn in that range is not necessarily a quicksort partition.
If it were, we couldn’t have 2 on the right side.
We have absolutely no idea where the real quicksort partitions are.
But, we know that there is a partition boundary within some threshold
(say left of the line).</p>
<p>Now that is quicksort, but we will design our components in a way to support it.
Let’s write a function that assumes we have a prefix that is sorted
and contains a sentinel.</p>
<pre><code>template <typename I, typename R>
// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
void insertion_sort_suffix(I first, I last, R r) {
// precondition: one of -
// (1) a valid range before first is sorted with respect to r and contains a sentinel
// (2) *first is a sentinel
if (first == last) return;
I current = first;
++current;
while (current != last) {
linear_insert_with_sentinel(first, current, r);
++current;
}
}
</code></pre>
<a name="Optimized-insertion-sort"></a>
<h3>Optimized insertion sort</h3>
<p>So let us copy <code>linear_insertion_sort</code> and make our definitive <code>insertion_sort</code>.</p>
<pre><code>template <typename I, typename R>
// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
void insertion_sort(I first, I last, R r) {
if (first == last) return;
I current = first;
++current;
if (current == last) return;
// create a sentinel
I min = std::min_element(first, last, r);
rotate_right_by_one(first, ++min);
insertion_sort_suffix(current, last, r);
}
</code></pre>
<p>Copy paste, and make the unstable version
by replacing rotate with swap.</p>
<pre><code>template <typename I, typename R>
// I is BidirectionalIterator
// R is WeakStrictOrdering on the value type of I
void insertion_sort_unstable(I first, I last, R r) {
if (first == last) return;
I current = first;
++current;
if (current == last) return;
// create a sentinel
I min = std::min_element(first, last, r);
std::swap(*first, *min);
insertion_sort_suffix(current, last, r);
}
</code></pre>
<a name="Selection-sort"></a>
<h2>Selection sort</h2>
<p>This leads us to another classical sort,
it’s very slow, but since it’s classical and takes only a few lines we will discover it.</p>
<p>What’s the idea of selection sort?
You find the min, put him in the first place.
You find the min of the remaining range, put him in the next place, and so on.
Could we write it?</p>
<pre><code>template <typename I, typename R>
// I is ForwardIterator
// R is WeakStrictOrdering on the value type of I
void selection_sort(I first, I last, R r) {
while (first != last) {
I min = std::min_element(first, last, r);
std::swap(*first, *min);
++first;
}
}
</code></pre>
<p>It’s not stable, but it’s not hard to fix.
The problem is <code>std::swap</code> might skip over lots of equal guys.</p>
<pre><code>template <typename I, typename R>
// I is ForwardIterator
// R is WeakStrictOrdering on the value type of I
void stable_selection_sort(I first, I last, R r) {
while (first != last) {
I min = std::min_element(first, last, r);
rotate_right_by_one(first, ++min);
++first;
}
}
</code></pre>
<p>Comparison is typically fast, but <code>swap</code> we tend to think of as slow.
Imagine the elements are buildings that need to be lifted up and carried to swap places.
The unstable <code>selection_sort</code> is actually amazing in that it just does <code>n - 1</code> swaps, always.
Merge sort, quicksort, they do like <code>n log(n)</code> swaps.
Is it practically important? No.
Not once have I needed selection sort.
So why do I talk about it?
It shows us how to create a sentinel.</p>
<a name="Preconditions-are-essential"></a>
<h2>Preconditions are essential</h2>
<p>There’s this advertising company you might have heard in Mountain view called Google.
They have bright young things, and one of them decided that he wants to randomly shuffle a vector.
It’s a reasonable thing.
He decided that the correct way of doing that is the following:
you implement a comparison function which throws a coin.
It randomly generates true or false.
Then take <code>std::sort</code> and pass this function to it,
because it will obviously create a randomly shuffled thing.
Lo and behold to everybody’s amazement it caused a segmentation fault.
There were messages throughout Google saying, “STL is totally broken”.
Obviously, because it brings Google down.
Let’s argue why he shouldn’t do what he did.</p>
<ol>
<li><p>There is an algorithm in STL called <a href="https://en.cppreference.com/w/cpp/algorithm/random_shuffle"><code>std::random_shuffle</code></a>.
Why not use that?</p></li>
<li><p>Somebody more advanced, would say, even if it worked, it wouldn’t be a uniform random shuffle<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>.
What he did is screwed up.
But knowing that requires probability theory or Knuth.
These people at Google just don’t read.
The brightest people do not <em>need</em> to read (joke).</p></li>
<li><p>My dear friend Dave Musser who was on sabbatical at Google ventured to post that he did not satisfy the preconditions.
Randomly returning true or false is not a weak strict ordering, or any ordering whatsoever.
He tried to explain, but they said no.
It should work with whatever I pass to it.</p></li>
</ol>
<p>As you can imagine, we cannot rely on any properties, like sentinel with this going on.
For a while there were a bunch of postings on the internet saying,
do not use <code>std::sort</code> because it requires <code>WeakStrictOrdering</code>.
It’s provably the weakest possible requirement.
I thought it was good, but they turned it around and said no.
Use <code>std::stable_sort</code>.
I still see this in code, people use it when they don’t need stability because they read these discussions.</p>
<p>Apparently it is an expectation of a modern programmer that you don’t have to satisfy any precondition.
Things should do something and never cause a segmentation fault.
It is a tricky thing.
Nowadays I wonder.
What should we do when we build components?
Should we assume that we build them the fastest way and carefully specify preconditions?
Or should we build idiot-proof (Google quality) components?
This is a difficult question.
I do not know the answer.</p>
<a name="Final-project"></a>
<h2>Final project</h2>
<p>Write the fastest version of stable sort that you can, utilizing the ideas in these tools.
We have all the algorithmic ideas we need.
But, I invite you to read books.
I invite you to test and measure.
If you want you can even go read old STL code I wrote.
It’s a competition! Consider teaming up and sharing ideas.</p>
<a name="Code"></a>
<h2>Code</h2>
<ul>
<li><a href="code/insertion_sort.h">insertion_sort.h</a></li>
<li><a href="code/test_insertion_sort.cpp">test_insertion_sort.cpp</a></li>
</ul>
<div class="footnotes">
<hr/>
<ol>
<li id="fn:1">
A uniform shuffle of a range of elements <code>x1 ... xn</code>
is an algorithm which produces a random permutation of the elements,
in a manner such that all possible permutations are equally likely.
Since there are <code>n!</code> permutations, each permutation should occur
with probability <code>1/n!</code> (See “The Art of Computer Programming” 3.4.2).<a href="#fnref:1" rev="footnote">↩</a></li>
</ol>
</div>
<span style="float: right">
[
<a href="18_binary_insertion_sort.html">previous</a>,
<a href="index.html">contents</a>,
<a href="">next</a>
]
</span>
</body>
</html>