### Sorting Cheatsheet

```======================================================================
The Sorting Cheat Sheet
by: J. Forbes
======================================================================

This handout is intended as a very rough guide. It's for those of you
who want the quick and dirty answers to what how does sorting algorithm
"x" work and how does it compare to the other ones we've learned. If you
want to really understand these algorithms, you should look at the
code and notes from lecture.

For any particular sorting algorithm, we want to do it fast. In
practice, constant factors and ease of implementation can be important
in picking an algorithm. We'll assume for all of these algorithms that
our input is presented in an array of length n.

AVERAGE CASE TIME - given an arbitrary input, what do we expect
the running time to be
WORST CASE TIME - for a particular degenerate case, how bad will
the algorithm perform

BEST CASE TIME - for a particularly benevolent input case, what is
the best case performance. This will always be at least O(n).
Why?

SITUATIONS WHERE USEFUL - for waht inputs and applications, is this
kind of sort useful

Basic sorts
======================================================================
The "basic" sorts generally make n passes through the input vector
An important measure for any of these sorts is:

SITUATION AFTER ITH PASS: what the vector looks like after the going
through the vector i times

Insertion sort
----------------------------------------------------------------------
insert an as-yet-unprocessed record into a (sorted) list of the records
processed so far

SITUATION AFTER ITH PASS: first i elements sorted
AVERAGE CASE TIME: O(n^2)
WORST CASE TIME: O(n^2) when in reverse sorted order takes about twice
as long as average case
BEST CASE TIME: O(n) when already in sorted order
SITUATIONS WHERE USEFUL: Simple sort, easy to implement. Good when the
number of inversions is small.

Shell's sort
----------------------------------------------------------------------
The problem with insertion sort is that elements can only be moved one
slot at a time. In the reverse order case, we had to move each element
all the way down the vector each time. Shell's sort makes things better
on average by sorting the vector with various "strides."
SITUATION AFTER ITH PASS:
AVERAGE CASE TIME: O(n^1.5) when we divide the stride by 2.2 each time
WORST CASE TIME: with stride of 1 it's just insertion sort which can
take. The earlier passes don't necessarily have to help in the sorting
the overall vector. Luckily on average, this doesn't happen.
BEST CASE TIME: O(n) if we set the stride to 1 and we have a sorted list
SITUATIONS WHERE USEFUL: good sort for general purpose sorting when
insertion sort has trouble. There's a bit more overhead involved.

Selection sort
----------------------------------------------------------------------
Just find the minimum of the remaining elements each time.
SITUATION AFTER ITH PASS: first i elements are sorted and in proper
position
AVERAGE CASE TIME: O(n^2) you've got to go through and find the minimum
each time, which is a linear time operation, so T(n) = n + n-1 + ... + 1
which is in O(n^2)
WORST CASE TIME: O(n^2) It's not sensitive to the input
BEST CASE TIME: O(n^2)
SITUATIONS WHERE USEFUL: Another relatively easy sort. Insensitive to
the data, so it's good if we wanted to have our sort always take the
same time.

Bubble sort
----------------------------------------------------------------------
On each pass, if two elements are out of order swap them. You're done
when you can go through the entire vector with no swaps.
SITUATION AFTER ITH PASS: last i sorted and in proper position.
AVERAGE CASE TIME: O(n^2) sweeping through the first n-i each time
regardless.
WORST CASE TIME: O(n^2) and it really is slow
BEST CASE TIME: O(n) when sorted or mostly sorted (possibly a few
elements a place or two away from their correct spots)
SITUATIONS WHERE USEFUL: probably none

Divide and Conquer Sorts
======================================================================
Well, n^2 or even n^1.25 is good, but we can do better. In fact, we can
prove that we can do better.
BASE CASE - the end of the recursion

DIVIDE STEP - splitting up the problem into more manageable pieces

Merge sort
----------------------------------------------------------------------
BASE CASE:
DIVIDE STEP: mergesort each half and then merge the two in O(n) time
AVERAGE CASE TIME: O(nlogn), we split the problem in two every time
regardless
WORST CASE TIME: O(nlogn)
BEST CASE TIME: O(nlogn)
SITUATIONS WHERE USEFUL: very good for working in parallel, since every
mergesort is working on a different portion of the vector. Not sensitive
to the data input.

Quick sort
----------------------------------------------------------------------
BASE CASE: Is our portion of the vector close to sorted? If so, just use
insertion sort
DIVIDE STEP: Choose a pivot and divide the vector into elements smaller
than the pivot and elements greater than it
AVERAGE CASE TIME: O(nlogn) - on average the pivot will evenly divide
the vector
WORST CASE TIME: O(n^2) if the pivot gives us very uneven partitions
each time
BEST CASE TIME: O(n) if we're given a pretty well sorted vector, we can
just call insertion sort on it immediately
SITUATIONS WHERE USEFUL: the fastest practical sort. There exist very
finely tuned versions for most systems.

An interesting result is that for any sort which relies on just
comparisons, you will always have to do O(nlogn) comparisons in the
worst case.  Why is this?

See Weiss, section 8.8.

"Restricted" Sorts
======================================================================
If we only rely on comparisons, our algorithm must take O(nlogn) worst
case time. When we know more about our input, we can possibly make our
bound O(n).

RESTRICTION - in what form do the input elements need to be.

K FACTOR - how does the form of the input elements affect running time

Distribution sort
----------------------------------------------------------------------
Distribution sort is useful when the records to be sorted are in small
range of integers or other cardinal values.

So let's say our numbers are between U and L. Thus, we store our range
(k) is U-L+1.
Set counting vector of size k to all zeroes
For each of n in original set (n of them)
increment C[i] every time we see the number L+i. (O(1))
// C[i] now contains elements equal to L+i
For each element in counting vector (k of them)
C[j] += C[j-1]
// C[i] now has contains the number of elements <= L+i

//  Copy elements from original vector A into new vector B into a place
//  spot designated by C[i]
For each of n in original set (n of them)
put element in new vector B from A[i] in the spot dictated by C[A[i]-L]
since C[A[i]-L] is the number of elements less than A[i], that will
be a proper spot for the sorted A[i]
increment count vector to correspond with new position for next example
of A[i]

RESTRICTION: elements in integral range L to U
K FACTOR: k = U-L
AVERAGE CASE TIME: O(k + n), not sensitive to input beyond values of L
and U
WORST CASE TIME: O(k + n), don't forget that k is generally quite
large. For integers, k is 2^32,
BEST CASE TIME: O(k + n)
SITUATIONS WHERE USEFUL: have restricted range of inputs, like in radix
sort

----------------------------------------------------------------------
For each digit going least significant to most significant:
For all n numbers
Place number in proper bin
Distribution sort each bin
Concatenate sorted results from each bin and repeat

RESTRICTION: The keys are sequences of fixed-sized "digits"
K FACTOR: d is the number of digits. k is the range of each digit. For
example, for decimal numbers under 1000, d = 3 and k = 10.

AVERAGE CASE TIME: O(dn+kd)
WORST CASE TIME: O(dn+kd)
BEST CASE TIME: O(dn+kd)
SITUATIONS WHERE USEFUL: things like bounded integers or bounded length
strings

Bucket sort
----------------------------------------------------------------------
Bucket sort assumes that the input is generated by some random process
which uniformly distributes the numbers over some interval. Divide
interval into n equally sized buckets.

for each element in original vector
insert sorted into proper bucket O(1 + length of list in bucket)
concatenate the lists from all of the buckets together

If we have an input of size n, we have n buckets. On average each bucket
will have 1 element, so the length of the list in each bucket is 1. We
can then do insertion sort for a particular bucket in constant time.

RESTRICTION: uniformly distributed floating point numbers in some
interval
K FACTOR: k = length of list in bucket, so general running time is
O(1 + k)
AVERAGE CASE TIME: O(n)
WORST CASE TIME: O(n^2) if everything ends up in the same bucket
BEST CASE TIME: O(n)
SITUATIONS WHERE USEFUL: when we're sorting random numbers or randomly
generated values of some kind```