Week 10. Sorting Algorithms

 

Sorting

Sorting is a fundamental application for computers. It is one of the most intensively studied and important operations. An initial sort of data can significantly enhance the performance of algorithms.

A sorting algorithm is stable if the relative order of any two items with the same key in an input sequence is preserved after the execution of the algorithm.

Summary of Sorting Algorithms

AlgorithmTimeNotes
selection-sortin-place / slow
insertion-sortin-place / slow
quick-sort
expected
in-place / fastest
heap-sortin-place / fast
merge-sortsequential data access / fast

Merge Sort

Divide-and-Conquer

Divide-and-Conquer is a general algorithm design paradigm:

  • Divide: divide the input data into two disjoint subsets and
  • Recur: solve the subproblems associated with and
  • Conquer: combine the solutions for and into a solution for
Link to original

Merge Sort

Merge Sort on an input sequence with elements consists of three steps:

  • Divide: if has zero or one element, return . Otherwise, remove all elements from and put them in two sequences, and , each containing about half of the elements of . contains the first elements of and contains remaining items.
  • Recur: recursively sort and
  • Conquer: merge and into a sorted sequence

The base case for the recursion are subproblems of size or .

Link to original

Tree Representation

Merge sort execution can be represented by the use of a Binary Tree.

  • Each node represents a recursive call of merge-sort and stores:
    • Unsorted sequence before the execution + its partition
    • Sorted sequence at the end of the execution
  • The root is the initial call
  • The leaves are calls on sub-sequences of size and

For example, sorting the sequence :

Sorting the sequence :

Performance

The height of the merge-sort tree is . At each recursive call, we divide the sequence is half.

  • Amount of work that needs to be done at the nodes of each depth is .
  • The tree has nodes at depth .
  • We partition and merge sequences of size .
  • Overall time spent at all the nodes at depth is equivalent to .

Hence the total running time of merge-sort is .

Quick Sort

Quick Sort is a randomised sorting algorithm based on Divide-and-Conquer but which favours a difficult division and easy conquer. We take the steps:

  • Divide: If has at least two elements (otherwise stop):
    • Pick a random element (we call this the pivot)
    • Remove all the elements from and put into three sequences:
      • : elements less than
      • : elements equal to
      • : elements greater than
  • Recur: sort and
  • Conquer: put the elements back in order into by inserting , , then

It is common practice to choose the pivot to be the last element in .

Partition

We partition an input sequence as follows:

  • remove each element from
  • insert into , , or depending on the result of the comparison with pivot

Each insertion and removal is at the beginning or at the end of a sequence, and hence takes time. Thus, the partition step of quick-sort takes time.

def partition(S, p):
	L, E, G = empty sequences
	x = S.remove(p)
	E.addLast(x)
	while not S.isEmpty():
		y = S.remove(S.first())
		if y < x:
			L.addLast(y)
		else if y = x:
			E.addLast(y)
		else:
			G.addLast(y)
 
	return L, E, G

Tree Representation

The execution of quick-sort can be represented by a Binary Tree.

  • Each node represents a recursive call of quick-sort and stores:
    • Unsorted sequence before execution + pivot
    • Sorted sequence at the end of execution
  • The root is the initial call
  • Leaves are calls on sub-sequences of size or

For example, sorting the sequence , with pivot = :

Sorting the sequence :

Worst-case run time

The worst-case for quick-sort occurs when the selected pivot is the unique minimum or maximum element of the list.

  • One of and has size and the other has size .
  • The running time is proportional to sum .

Hence, worst-case running time of quick-sort is .

Expected run time

Consider a recursive call of quick-sort on a sequence of size :

  • Good call: the sizes of and are each less than
  • Bad call: one of the sizes is greater than

A call is good with probability .

We use a probabilistic fact: the expected number of coin tosses required in order to get heads is . For a node of depth , we expect:

  • ancestors are good calls
  • the size of the input sequence for the current call is at most

Therefore, we have:

  • for a node of depth , the expected input size is one
  • the expected height of the quick-sort tree is

The amount or work done at the nodes of the same depth is , Thus, the expected running time of quick-sort is .

In-Place Quick Sort

Quick-sort can be implemented to run in-place. The divide step can be done without using any additional array:

  • assume we want to divide with respect to
  • maintain two indices, (left cursor) and (right cursor) with initial values set to and respectively
  • elements which have not been considered yet are in

Now, iterate until :

  • keep increasing by until an element is larger than the pivot and
  • keep decreasing by until an element is smaller than pivot and
  • if , swap and and proceed to the next iteration

Finally, swap and the pivot.

Bucket Sort

Bucket Sort

A bucket-sort does not use comparison. Let be a sequence of entries with keys in the range .

Bucket-sort uses the keys as indices into an auxiliary array of sequences.

  • Phase 1: empty sequence by moving each entry into its bucket
  • Phase 2: for , move the entries of bucket to the end of sequence

Between the two phases, bucket sort takes time:

  • Phase 1 takes time
  • Phase 2 takes time
Link to original

Sorting Lower Bound

Many sorting algorithms are comparison based. We can therefore derive a lower bound on the running time of any algorithm that uses comparisons to sort elements.

We can count all of the comparisons. Each possible run of the algorithm corresponds to a root-to-leaf path in a decision tree like so:

The height of the decision tree is a lower bound on the running time. Every input permutation must lead to a separate leaf output. Since there are leaves, the height is at least .

Any comparison-based sorting algorithm takes at least time. Therefore, any such algorithm takes at least:

That is, any comparison based sorting algorithm must run in time.