Sorting

We have already seen:

I Selection-

I Insertion-sort

I Heap-sort

We will see:

I Bubble-sort

I Merge-sort

I Quick-sort

We will show that:

I O(n · log n) is optimal for comparison based . Bubble-Sort

The basic idea of bubble-sort is as follows:

I exchange neighboring elements that are in wrong order

I stops when no elements were exchanged bubbleSort(A): n = length(A) swapped = true while swapped == true do swapped = false for i = 0 to n − 2 do if A[i] > A[i + 1] then swap(A[i], A[i + 1]) swapped = true done n = n − 1 done Bubble-Sort: Example

5 4 3 7 1 Unsorted Part 4 5 3 7 1 Sorted Part 4 3 5 7 1 4 3 5 7 1 4 3 5 1 7 3 4 5 1 7 4 3 5 1 7 4 3 1 5 7 3 4 1 5 7 3 1 4 5 7 1 3 4 5 7 Bubble-Sort: Properties

Time complexity:

I worst-case: (n − 1)2 + n − 1 (n − 1) + ... + 1 = ∈ O(n2) 2 (caused by sorting an inverse sorted list)

I best-case: O(n)

Bubble-sort is:

I slow

I in-place Divide-and-Conquer

Divide-and-Conquer is a general design paradigm:

I Divide: divide the input S into two disjoint subsets S1, S2

I Recur: recursively solve the subproblems S1, S2

I Conquer: combine solutions for S1, S2 to a solution for S (the base case of the recursion are problems of size 0 or 1)

Example: merge-sort

7 2 | 9 4 7 2 4 7 9

7 | 2 7 2 7 9 | 4 7 4 9 → 7 7 7 2 7 2 9 7 9 4 7 4 → →

I | indicates→ the splitting→ point → →

I 7 indicates merging of the sub-solutions

Merge-sort of a list S with n elements works as follows:

I Divide: divide S into two lists S1, S2 of ≈ n/2 elements

I Recur: recursively sort S1, S2

I Conquer: merge S1 and S2 into a sorting of S

Algorithm mergeSort(S, C): Input: a list S of n elements and a comparator C Output: the list S sorted according to C if size(S) > 1 then (S1, S2) = partition S into size bn/2c and dn/2e mergeSort(S1, C) mergeSort(S2, C) S = merge(S1, S2, C) Merging two Sorted Sequences

Algorithm merge(A, B, C): Input: sorted lists A, B Output: sorted lists containing the elements of A and B S = empty list while ¬A.isEmtpy() or ¬B.isEmtpy() do if A.first().element < B.first().element then S.insertLast(A.remove(A.first())) else S.insertLast(B.remove(B.first())) done while ¬A.isEmtpy() do S.insertLast(A.remove(A.first())) while ¬B.isEmtpy() do S.insertLast(B.remove(B.first()))

Performance:

I Merging two sorted lists of length about n/2 is O(n) time. (for singly linked lists, double linked lists, and arrays) Merge-sort: Example

a n e x | a m p l e Divide (split) a n | e x a m | p l e

a | n e | x a | m p | l e

a n e x a m p l | e

Conquer l e (merge) a n e x a m e l

a e n x e l p

a e l m p

a a e e l m n p x Merge-Sort Tree

An execution of merge-sort can be displayed in a binary tree:

I each node represents recursive call and stores:

I unsorted sequence before execution, its partition

I sorted sequence after execution

I leaves are calls on subsequences of size 0 or 1 a n e x | a m p l e 7 a a e e l m n p x

a n | e x 7 a e n x → a m | p l e 7 a e l m p

a | n 7 a n → e | x 7 e x a | m 7 a m → p | l e 7 e l p

a 7 a→n 7 n e 7 e→x 7 x a 7 a →m 7 m p 7 p →l | e 7 e l

→ → → → → → → l 7 l →e 7 e

→ → Merge-Sort: Example Execution

7 1 2 9 | 6 5 3 8 7 1 2 3 5 6 7 8 9

7 1 | 2 9 7 1 2 7 9 → 6 5 | 3 8 7 3 5 6 8

7 | 1 7 1 7 → 2 | 9 7 2 9 6 | 5 7 5 6 → 3 | 8 7 3 8

7 7 7→1 7 1 2 7 2→9 7 9 6 7 6→5 7 5 3 7 3→8 7 8

→ → → → → → → → Finished merge-sort tree. Merge-Sort: Running Time

The height h of the merge-sort tree is O(log2 n): I each recursive call splits the sequence in half The work all nodes together at depth i is O(n): i i I partitioning, and merging of 2 sequences of n/2 i+1 I 2 ≤ n recursive calls

depth nodes size 0 1 n 1 2 n/2 i 2i n/2i ......

Thus the worst-case running time is O(n · log2 n). Quick-Sort

Quick-sort of a list S with n elements works as follows:

I Divide: pick random element x (pivot) from S and split S in:

I L elements less than x

I E elements equal than x

I G elements greater than x

7 2 1 9 6 5 3 8

I Recur: recursively sort L, and G

2 1 3 5 7 9 6 8

L E G

I Conquer: join L, E, and G

1 2 3 5 6 7 8 9 Quick-Sort: The Partitioning

The partitioning runs in O(n) time:

I we traverse S and compare every element y with x

I depending on the comparison insert y in L, E or G

Algorithm partition(S, p): Input: a list S of n elements, and positon p of the pivot Output: list L, E, G of lists less, equal or greater than pivot L, E, G = empty lists x = S.elementAtRank(p) while ¬S.isEmpty() do y = S.remove(S.first()) if y < x then L.insertLast(y) if y == x then E.insertLast(y) if y > x then G.insertLast(y) done return L, E, G Quick-Sort Tree

An execution of quick-sort can be displayed in a binary tree:

I each node represents recursive call and stores:

I unsorted sequence before execution, and its pivot

I sorted sequence after execution

I leaves are calls on subsequences of size 0 or 1

1 6 2 940 7 0 1 2469

120 7 012 → 69 7 69

0 7 0 → 2 7 2 → 9 7 9

→ → → Quick-Sort: Example

8 2 9 3 1 5 764 7 1 2 3 4 56789

2 3154 7 12345 → 897 7 789

→2354 7 2345 7 7 7 → 9 7 9

2 7 2 → 54 7 45 → →

→ 4 7 4→

→ Quick-Sort: Worst-Case Running Time

The worst-case running time occurs when:

I the pivot is always the minimal or maximal element

I then one L and G has size n − 1, the other size 0 Then the running time is O(n2): n + (n − 1) + (n − 2) + ... + 1 ∈ O(n2)

n 0 n − 1 0 n − 2 . 0 . 1 0 0 Quick-Sort: Average Running Time

Consider a recursive call on a list of size m: 3 I Good call: if both L and G are each less then 4 · s size 3 I Bad call: one of L and G is greater than 4 · s size

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bad calls good calls bad calls

1 A good call has probability 2 : I half of the pivots give rise to good calls Quick-Sort: Average Running Time

For a node at depth i, we expect (average):

I i/2 ancestors are good calls i/2 I the size of the sequence is ≤ (3/4) · n As a consequence:

I for a node at depth 2 · log3/4 n the expected input size is 1

I the expected height of the quick-sort tree is O(log n)

sa O(n)

sb sc O(n) O(log n) sd se sf sg O(n) ......

The amount of work at depth i is O(n). Thus the expected (average) running time is O(n · log n). In-Place Quick-Sort

Quick-Sort can be sorted in-place (but then non-stable): Algorithm inPlaceQuickSort(A, l, r): Input: list A, indices l and r Output: list A where elements from index l to r are sorted if l ≥ r then return p = A[r] (take rightmost element as pivot) l0 = l and r 0 = r while l0 ≤ r 0 do while l0 ≤ r 0 and A[l0] ≤ p do l0 = l0 + 1 (find > p) while l0 ≤ r 0 and A[r 0] ≥ p do r 0 = r 0 − 1 (find < p) if l0 < r 0 then swap(A[l0], A[r 0]) (swap < p with > p) done swap(A[r], A[l0]) (put pivot into the right place) inPlaceQuickSort(A, l, l0 − 1) (sort left part) inPlaceQuickSort(A, l0 + 1, r) (sort right part) Considered in-place although recursion needs O(log n) space. In-Place Quick-Sort: Example

5 8 3 7 1 6 Unsorted Part 5 1 3 7 8 6 Sorted Part 5 1 3 6 8 7 Pivot 1 5 3 6 8 7 1 3 5 6 8 7 1 3 5 6 8 7 1 3 5 6 8 7 1 3 5 6 7 8 1 3 5 6 7 8 Sorting: Lower Bound

Many sorting algorithms are comparison based:

I sort by comparing pairs of objects

I Examples: selection-sort, insertion-sort, bubble-sort, heap-sort, merge-sort, quick-sort,. . .

xi < xj ? yes no

No comparison based can be faster than Ω(n · log n) time (worst-case).

We will prove this lower bound on the next slides. . . Sorting: Lower Bound (Decision Tree)

We will only count comparisons (sufficient for lower bound):

I Assume input is a of the numbers 1, 2, . . . , n.

I Every execution corresponds to a path in the decision tree:

xa < xb?

xc < xd ? xe < xf ?

xg < xh? xi < xj ? xk < xl ? xm < xo? ......

I Algorithm itself maybe does not have this tree structure, but this is the maximal information gained by the algorithm. Sorting: Lower Bound (Leaves)

Every leaf corresponds to exactly one input permutation:

I application of the same swapping steps on two different input , e.g. . . . ,6,. . . ,7,. . . and . . . ,7,. . . ,6,. . . , yields different results (not both results can be sorted)

xa < xb?

xc < xd ? xe < xf ?

xg < xh? xi < xj ? xk < xl ? xm < xo? ...... Sorting: Lower Bound (Height of the Decision Tree)

The height of the tree is a lower bound on the running time:

I There are n! = n · (n − 1) ··· 1 permutations of 1, 2, . . . , n.

I Thus the height of the tree is at least: log2(n!).

xa < xb?

xc < xd ? xe < xf ? log2(n!) xg < xh? xi < xj ? xk < xl ? xm < xo? ......

n! Sorting: Lower Bound

Hence any comparison-based sorting algorithm takes at least n/2 log2(n!) ≥ log2(n/2) n n = log 2 2 2 ∈ Ω(n · log n) time in worst-case. Summary of Comparison-Based Sorting Algorithms

Algorithm Time Notes 2 selection-sort O(n ) I slow (but good for small lists) insertion-sort O(n2) I in-place, stable I insertion-sort good for online bubble-sort O(n2) sorting and nearly sorted lists

I in-place, not stable, fast heap-sort O(n · log2 n) I good for large inputs (1K − 1M)

I fast, stable, usually not in-place

merge-sort O(n · log2 n) I sequential data access I good for large inputs (> 1M)

O(n · log n) I in-place, randomized, not stable quick-sort 2 (expected) I fastest, good for huge inputs

Quick-sort usually performs fastest, although worst-case O(n2). Sorting: Comparison of Runtime

Algorithm 25.000 100.000 25.000 100.000 sorted sorted not sorted not sorted selection-sort 1.1 19.4 1.1 19.5 insertion-sort 0 0 1.1 19.6 bubble-sort 0 0 5.5 89.8

Algorithm 5 million 20 million 5 million 20 million sorted sorted not sorted not sorted insertion-sort 0.03 0.13 timeout timeout heap-sort 3.6 15.6 8.3 42.2 merge-sort 2.5 10.5 3.7 16.1 quick-sort 0.5 2.2 2.0 8.7

I Source: Gumm, Sommer Einführung in die Informatik.