Sorting is a problem for which we can prove a non-trivial lower bound.

Similar documents
Sorting: Given a list A with n elements possessing a total order, return a list with the same elements in non-decreasing order.

Lecture 2: Divide&Conquer Paradigm, Merge sort and Quicksort

IS 709/809: Computational Methods in IS Research. Algorithm Analysis (Sorting)

Cpt S 122 Data Structures. Sorting

Problem. Input: An array A = (A[1],..., A[n]) with length n. Output: a permutation A of A, that is sorted: A [i] A [j] for all. 1 i j n.

Sorting Algorithms. For special input, O(n) sorting is possible. Between O(n 2 ) and O(nlogn) E.g., input integer between O(n) and O(n)

CS 137 Part 8. Merge Sort, Quick Sort, Binary Search. November 20th, 2017

Comparison Sorts. Chapter 9.4, 12.1, 12.2

CS 112 Introduction to Computing II. Wayne Snyder Computer Science Department Boston University

CSE 143. Two important problems. Searching and Sorting. Review: Linear Search. Review: Binary Search. Example. How Efficient Is Linear Search?

COSC242 Lecture 7 Mergesort and Quicksort

The divide and conquer strategy has three basic parts. For a given problem of size n,

Lecture 7 Quicksort : Principles of Imperative Computation (Spring 2018) Frank Pfenning

Lecture 9: Sorting Algorithms

1 Probabilistic analysis and randomized algorithms

Sorting. Sorting. Stable Sorting. In-place Sort. Bubble Sort. Bubble Sort. Selection (Tournament) Heapsort (Smoothsort) Mergesort Quicksort Bogosort

Sorting Goodrich, Tamassia Sorting 1

SAMPLE OF THE STUDY MATERIAL PART OF CHAPTER 6. Sorting Algorithms

Lecture Notes 14 More sorting CSS Data Structures and Object-Oriented Programming Professor Clark F. Olson

CS125 : Introduction to Computer Science. Lecture Notes #38 and #39 Quicksort. c 2005, 2003, 2002, 2000 Jason Zych

COMP Data Structures

CS2223: Algorithms Sorting Algorithms, Heap Sort, Linear-time sort, Median and Order Statistics

Quicksort (CLRS 7) We saw the divide-and-conquer technique at work resulting in Mergesort. Mergesort summary:

Unit 6 Chapter 15 EXAMPLES OF COMPLEXITY CALCULATION

Sorting. Bubble Sort. Pseudo Code for Bubble Sorting: Sorting is ordering a list of elements.

CSE 332: Data Structures & Parallelism Lecture 12: Comparison Sorting. Ruth Anderson Winter 2019

17/05/2018. Outline. Outline. Divide and Conquer. Control Abstraction for Divide &Conquer. Outline. Module 2: Divide and Conquer

CS 310 Advanced Data Structures and Algorithms

Scan and Quicksort. 1 Scan. 1.1 Contraction CSE341T 09/20/2017. Lecture 7

Presentation for use with the textbook, Algorithm Design and Applications, by M. T. Goodrich and R. Tamassia, Wiley, Merge Sort & Quick Sort

Question 7.11 Show how heapsort processes the input:

STA141C: Big Data & High Performance Statistical Computing

Introduction to Algorithms February 24, 2004 Massachusetts Institute of Technology Professors Erik Demaine and Shafi Goldwasser Handout 8

Lecture Notes on Quicksort

Quick Sort. CSE Data Structures May 15, 2002

Lecture Notes on Quicksort

Lecture 19 Sorting Goodrich, Tamassia

9. The Disorganized Handyman

Lecture 5: Sorting Part A

Data Structures. Sorting. Haim Kaplan & Uri Zwick December 2013

Data Structures and Algorithms Chapter 4

Sorting. Bringing Order to the World

Introduction to Computers and Programming. Today

STA141C: Big Data & High Performance Statistical Computing

Algorithms and Data Structures (INF1) Lecture 7/15 Hua Lu

Analyze the obvious algorithm, 5 points Here is the most obvious algorithm for this problem: (LastLargerElement[A[1..n]:

Total Points: 60. Duration: 1hr

CSE373: Data Structure & Algorithms Lecture 18: Comparison Sorting. Dan Grossman Fall 2013

Data Structures and Algorithms Week 4

Sorting and Selection

CS 303 Design and Analysis of Algorithms

Quick Sort. Biostatistics 615 Lecture 9

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

7. Sorting I. 7.1 Simple Sorting. Problem. Algorithm: IsSorted(A) 1 i j n. Simple Sorting

Lecture 6 Sequences II. Parallel and Sequential Data Structures and Algorithms, (Fall 2013) Lectured by Danny Sleator 12 September 2013

Quick-Sort. Quick-sort is a randomized sorting algorithm based on the divide-and-conquer paradigm:

Divide and Conquer Sorting Algorithms and Noncomparison-based

CPSC 311 Lecture Notes. Sorting and Order Statistics (Chapters 6-9)

SEARCHING AND SORTING HINT AT ASYMPTOTIC COMPLEXITY

BM267 - Introduction to Data Structures

Chapter 7 Sorting. Terminology. Selection Sort

// walk through array stepping by step amount, moving elements down for (i = unsorted; i >= step && item < a[i-step]; i-=step) { a[i] = a[i-step];

SORTING. Insertion sort Selection sort Quicksort Mergesort And their asymptotic time complexity

(Refer Slide Time: 01.26)

Presentation for use with the textbook, Algorithm Design and Applications, by M. T. Goodrich and R. Tamassia, Wiley, 2015

EECS 2011M: Fundamentals of Data Structures

Sorting. Data structures and Algorithms

DIVIDE AND CONQUER ALGORITHMS ANALYSIS WITH RECURRENCE EQUATIONS

Sorting. Task Description. Selection Sort. Should we worry about speed?

We can use a max-heap to sort data.

Sorting. Riley Porter. CSE373: Data Structures & Algorithms 1

Lecture 2: Getting Started

Sorting algorithms Properties of sorting algorithm 1) Adaptive: speeds up to O(n) when data is nearly sorted 2) Stable: does not change the relative

UNIT-2. Problem of size n. Sub-problem 1 size n/2. Sub-problem 2 size n/2. Solution to the original problem

Sorting Shabsi Walfish NYU - Fundamental Algorithms Summer 2006

SORTING, SETS, AND SELECTION

Lecture 15 : Review DRAFT

DIVIDE & CONQUER. Problem of size n. Solution to sub problem 1

Computer Science 385 Analysis of Algorithms Siena College Spring Topic Notes: Divide and Conquer

Randomized Algorithms, Quicksort and Randomized Selection

SORTING AND SELECTION

e-pg PATHSHALA- Computer Science Design and Analysis of Algorithms Module 10

CSCI 2132 Software Development Lecture 18: Implementation of Recursive Algorithms

COMP2012H Spring 2014 Dekai Wu. Sorting. (more on sorting algorithms: mergesort, quicksort, heapsort)

Analysis of Algorithms - Quicksort -

Multiple-choice (35 pt.)

CS240 Fall Mike Lam, Professor. Quick Sort

CSE 373: Data Structures and Algorithms

CSE 373 MAY 24 TH ANALYSIS AND NON- COMPARISON SORTING

Lecture 11: In-Place Sorting and Loop Invariants 10:00 AM, Feb 16, 2018

CS Sorting Terms & Definitions. Comparing Sorting Algorithms. Bubble Sort. Bubble Sort: Graphical Trace

Algorithm Efficiency & Sorting. Algorithm efficiency Big-O notation Searching algorithms Sorting algorithms

Searching, Sorting. part 1

Sorting. CSE 143 Java. Insert for a Sorted List. Insertion Sort. Insertion Sort As A Card Game Operation. CSE143 Au

Sorting. Sorting in Arrays. SelectionSort. SelectionSort. Binary search works great, but how do we create a sorted array in the first place?

Object-oriented programming. and data-structures CS/ENGRD 2110 SUMMER 2018

Divide & Conquer. 2. Conquer the sub-problems by solving them recursively. 1. Divide the problem into number of sub-problems

SORTING. Comparison of Quadratic Sorts

Component 02. Algorithms and programming. Sorting Algorithms and Searching Algorithms. Matthew Robinson

Sorting L7.2 Recall linear search for an element in an array, which is O(n). The divide-and-conquer technique of binary search divides the array in ha

Transcription:

Sorting The sorting problem is defined as follows: Sorting: Given a list a with n elements possessing a total order, return a list with the same elements in non-decreasing order. Remember that total order means that any two elements a and b can be compared and we either have a < b, or a = b, or a > b. We only discuss sorting algorithms that work by comparing pairs of elements, and so the same algorithm could be applied to strings, integers, floating point numbers, etc. Sorting is one of the most fundamental problems in computer science. Here are a few reasons: Often the need to sort information is inherent in an application. For instance, you want to report the files that use all the space on your hard disk in decreasing order of size. Algorithms often use sorting as a key subroutine. For example, consider the problem of checking whether a list contains duplicated data: The first of the following two algorithms takes O(n 2 ) time, while the second one uses sorting and then takes only linear time. def has_duplicates(a): for i in range(len(a)): for j in range(i+1, len(a)): if a[i] == a[j]: return True return False # This function assumes that a is sorted! def has_duplicates_sorted(a): for i in range(len(a)-1): if a[i] == a[i+1]: return True return False There are a wide variety of sorting algorithms, and they use a rich set of techniques, as you can see in the following sections. In fact, many important techniques used throughout algorithm design are represented in the body of sorting algorithms that have been developed over the years. Sorting is a problem for which we can prove a non-trivial lower bound. For simplicity, we will explain the algorithms by sorting integer lists, but they all work for arbitrary elements with a total order. Selection Sort We already know how to find the minimum of a list of values. And since we are also Masters of Recursion, that means we can sort: We find the smallest element, recursively sort the rest of the list, and concatenate the two pieces: def selection_sort(a): if len(a) <= 1: return a k = find_min_index(a) b = selection_sort(a[:k] + a[k+1:]) return [a[k]]+b 1

Note the base case: a list with zero or one elements is already sorted! What s the running time of selection sort? find_min_index takes O(n) time, so we get the following recursion formula for the running time T (n) of selection sort: { O(1) for n 1 T (n) = T (n 1) + O(n) else The solution is T (n) = O(n 2 ). In-place sorting Our definition of sorting above requires us to return the elements in a new, sorted list. Often this is not necessary, because we no longer need the original, unsorted data. When sorting a huge data set, we can save a lot of memory space by sorting the data in-place. This means that the sorting is performed without extra storage (or only little of it). Of course, this is only possible if the data is in a mutable data structure. We will use Python lists: InPlaceSorting: Given a Python list a with n elements possessing a total order, rearrange the elements inside the list into non-decreasing order. Selection sort can easily be rewritten as an in-place sorting algorithm: def selection_sort(a, i): if j - i <= 1: return k = find_min_index(a, i) # exchange a[i] and a[k] t = a[i] a[i] = a[k] a[k] = t # sort the rest selection_sort(a, i+1) The running time is still O(n 2 ), of course. In fact, this implementation hardly counts as in-place, since it needs n stack frames, which take quite a bit of memory. We should therefore switch to an iterative version (this is easy, because we already have tail recursion): def selection_sort(a): n = len(a) for i in range(0, n-1): # elements 0..i-1 are smallest elements in sorted order k = find_min_index(a, i) # exchange a[i] and a[k] t = a[i] a[i] = a[k] a[k] = t To argue that this algorithm is correct, we use the loop invariant that elements at index 0 to i-1 are the smallest elements of the list and already in sorted order. Insertion Sort Insertion sort uses recursion the other way round: we recursively sort n 1 elements, and finally insert the remaining element into the sorted list. It is based on the observation that it is easy to insert a new element into a sorted list. Here is a version that keeps the original data intact: 2

def sorted_linear_search(a, x): for i in range(len(a)): if a[i] >= x: return i return len(a) def insertion_sort(a): if len(a) <= 1: return a b = insertion_sort(a[:-1]) k = sorted_linear_search(b, a[-1]) b.insert(k, a[-1]) return b The running time of sorted_linear_search is clearly O(n), and therefore the running time of insertion_sort is again O(n 2 ). Let s make insertion sort in-place: # sort a[:j] def insertion_sort(a, j): if j <= 1: return insertion_sort(a, j-1) k = j-1 # remaining element index x = a[k] # value of remaining element while k > 0 and a[k-1] > x: a[k] = a[k-1] k -= 1 a[k] = x Note how we search for the correct position to place x and move the other elements to the side at the same time. This version is still recursive and will therefore cause a runtime stack overflow quickly. Even though it does not use tail recursion, it s still quite easy to switch to iteration instead of recursion: def insertion_sort(a): for j in range(2, len(a)+1): # a[:j-1] is already sorted k = j-1 # remaining element index x = a[k] # value of remaining element while k > 0 and a[k-1] > x: a[k] = a[k-1] k -= 1 a[k] = x Note the loop invariant that allows us to argue correctness of this function: At the beginning of the loop, the first j 1 elements of the list are already sorted. Bubble Sort A very simple in-place sorting algorithm is bubble sort. It s called bubble sort because large elements rise to the end of the array like bubbles in a carbonated drink. What makes it so simple is the fact that it only uses exchanges of adjacent elements: 3

def bubble_sort(a): for last in range(len(a), 1, -1): # bubble max in a[:last] to a[last-1] for j in range(last-1): if a[j] > a[j+1]: t = a[j] a[j] = a[j+1] a[j+1] = t To prove correctness, we can use the loop invariant that at the beginning of the outer loop the elements at index last to len(a)-1 are already the largest elements of the list in sorted order. The running time is clearly quadratic. Bubble sort with early termination One observation about bubble sort is that we can stop once a bubble phase has made no more change then we know that the array is already in sorted order. def bubble_sort(a): for last in range(len(a), 1, -1): # bubble max in a[:last] to a[last-1] flipped = False for j in range(last-1): if a[j] > a[j+1]: flipped = True t = a[j] a[j] = a[j+1] a[j+1] = t if not flipped: return Does this improve the time complexity of the algorithm? In the best case, when the input data is already sorted, the running time improves from O(n 2 ) to O(n). The case of sorted or nearly-sorted input is important, so this is an important improvement. Unfortunately, in the worst case early termination does not help. The reason is that in every bubble round, the smallest element in the list can only move one position down. So if we start with any list where the smallest element is in the last position, it must take n 1 bubble rounds to finish. And therefore bubble sort with early termination still takes quadratic time in the worst case. Merge-Sort All the sorting algorithms we have seen so far have a time complexity of O(n 2 ). To beat this quadratic bound, we need to go back to the idea of divide and conquer that we used for the Maximum Contiguous Subsequence Sum problem and for binary search. Recall that divide-and-conquer consists of the following three steps: Split the problem into smaller instances. Recursively solve the subproblems. Combine the solutions to solve the original problem. Merge-Sort is a sorting algorithms that uses divide-and-conquer as follows: We split the list into two halves, sort each sublist recursively, and then merge the two sorted lists. def merge_sort(a): if len(a) <= 1: return a 4

mid = len(a) // 2 return merge(merge_sort(a[:mid]), merge_sort(a[mid:])) How do we merge two lists b and b into a new list s? The first element of s must be either the first element of a, or the first element of b. So we compare these two elements, choose the smaller one for s, and continue: def merge(a, b): i = 0 j = 0 res = [] while i < len(a) and j < len(b): va = a[i] vb = b[j] if va <= vb: res.append(va) i += 1 else: res.append(vb) j += 1 # now just copy remaining elements # (only one of these can be non-empty) res.extend(a[i:]) res.extend(b[j:]) return res The running time of merge is O(n), where n is the total length of the two input lists. This is true because in every iteration of the while loop, the size of s increases by one, and in the end s has length n. Let now T (n) denote the number of primitive operations to sort n elements using Merge-Sort. We have the following recurrence relation: { c if n = 1, T (n) = 2T ( n 2 ) + cn otherwise. The recursion looks familiar from the Maximum Contiguous Subsequence Sum analysis. O(n log n). The solution is Analysis using a recursion tree. We can also analyze the time complexity of Merge-Sort using a recursion tree. A recursion tree represents the (recursive) function calls performed during the execution of some program. Each node represents the execution of a function. If the function makes recursive calls, the execution of those recursive calls become children of the node. The recursion tree for Merge-Sort looks as in Fig. 1. At the root, we work on a list of length n. This is split into two lists of length n/2, which are handled at the two children of the node. Each of them makes two calls for lists of length n/4, which become their children, and so on. We have marked in red the running time of each function execution, but not counting the recursive calls. At the root, we work on a list of length n and so spend time cn. The children of the root work on a list of length n/2, and so the time spent is cn/2. Going down each level, the time spent inside a function execution decreases to half. At the bottom level, we handle lists of length one. No recursive call is necessary, and the function returns after running for time c. Since the subproblem size decreases by a factor two from one level of the tree to the next, the height of the tree is log n. On each level of the tree, the total size of the input lists for all the subproblems on this level is n, and so the total running time of all the functions on this level is cn. It follows that the total running time of Merge-Sort on a list of length n is bounded by cn log n. We thus have a second proof that the time complexity of Merge-Sort is O(n log n). 5

cn n cn Height: log n cn/2 n/2 cn/2 n/2 n/4 n/4 n/4 n/4 cn/4 cn/4 cn/4 cn/4 cn cn 1 1 1 c c c cn Figure 1: The recursion tree of Merge-Sort. Quick-Sort In Merge-Sort, the divide step is trivial, and the combine step is where all the work is done. In Quick-Sort it is the other way round: the combine step is trivial, and all the work is done in the divide step: 1. If L has less than two elements, return. Otherwise, select a pivot p from L. Split L into three lists S, E, and G, where S stores the elements of L smaller than x, E stores the elements of L equal to x, and G stores the elements of L greater than x. 2. Recursively sort S and G. 3. Form result by concatenating S, E, and G in this order. Here is an implementation in Python: def quick_sort(a): if len(a) <= 1: return a pivot = a[len(a) // 2] small = [] equal = [] large = [] for x in a: if x < pivot: small.append(x) elif x == pivot: equal.append(x) else: large.append(x) return quick_sort(small) + equal + quick_sort(large) 6

Analysis. In the worst case, the running time of Quick-Sort is O(n 2 ). This happens whenever the partitioning routine produces one subproblem with n 1 elements and one with 1 element In other words, each time when we choose the smallest element or the largest element as a pivot, T (n) = O(1) + T (n 1), which gives us the same time complexity as selection sort or insertion sort: O(n 2 ). The best case happens when the pivot splits the list into two equal parts S and G. In that case the recurrence is T (n) = 2T (n/2) + O(n), which solves to O(n log n) as in Merge-Sort. In old textbooks, the pivot is often chosen as the first element in the list. This is a really bad choice, since in practice lists are often already sorted somewhat. Chosing the first element would then often give us the smallest element in the list. One good strategy is to chose a pivot randomly (generate a random index into the list and pick that element), as we have done in the code above. With probability 1/2, we will pick a pivot such that neither S nor G has more than 3n/4 elements. Imagine that this happens in every step: Then the depth of the recursion is still O(log n), and the recursion tree argument implies that the total running time is O(n log n). Of course we cannot expect this good case to happen all the time, but on average you should get a good split every second time, and this is enough to argue that the depth of the recursion tree will be O(log n) with high probability. The journal Computing in Science & Engineering did a poll of experts to make a list of the ten most important and influential algorithms of the twentieth century, and it published a separate article on each of the ten algorithms. Quicksort was one of the ten, and it was surely the simplest algorithm on the list. Quicksort s inventor, Sir C. A. R. Tony Hoare, received the ACM Turing Award in 1980 for his work on programming languages, and was conferred the title of Knight Bachelor in March 2000 by Queen Elizabeth II for his contributions to Computing Science. In-place Quick-Sort One advantage of Quick-Sort compared to Merge-Sort is that it can be implemented as an in-place algorithm, needing no extra space except the array storing the elements: # sort range a[lo:hi+1] def quick_sort(a, lo, hi): if (lo < hi): pivotindex = partition(a, lo, hi) quick_sort(a, lo, pivotindex - 1) quick_sort(a, pivotindex + 1, hi) The critical method is partition, which choses the pivot and partitions the list into two pieces. This is quite tricky, as we (a) want to do it in-place, (b) have to nicely handle the case when there are many equal elements. It s easy to write a buggy or quadratic version by mistake. Early editions of the Goodrich and Tamassia book did. We have an array a in which we want to sort the items starting at a[lo] and ending at a[hi]. We choose a pivot index p and move it out of the way by swapping it with the last item, a[hi]. We employ two array indices, i and j. i is initially lo - 1, and j is initially hi, so that i and j sandwich the items to be sorted (not including the pivot). We will enforce the following loop invariants (v is the value of the pivot): a[k] v for k i, a[k] v for k j. To partition the array, we advance the index i until it encounters an item whose key is greater than or equal to the pivot s key; then we decrement the index j until it encounters an item whose key is less than or equal to the pivot s key. Then, we swap the items at i and j. We repeat this sequence until the indices i and j meet in the middle. Then, we move the pivot back into the middle (by swapping it with the item at index i). 7

What about items having the same key as the pivot? Handling these is particularly tricky. We d like to put them on a separate list (as in the list version above), but doing that in-place is too complicated. If we put all these items into the first list, we ll have quadratic running time when all the keys in the array are equal, so we don t want to do that either. The solution is to make sure that each index, i and j, stops whenever it reaches a key equal to the pivot. Every key equal to the pivot (except perhaps one, if we end with i = j) takes part in one swap. Swapping an item equal to the pivot may seem unnecessary, but it has an excellent side effect: if all the items in the array have the same key, half of these items will go into the left part, and half into the right part, giving us a well-balanced recursion tree. (To see why, try running the function below on paper with an array of equal keys.) # partition range a[lo:hi+1] and return index of pivot def partition(a, lo, hi): p = (lo + hi)//2 pivot = a[p] a[p] = a[hi] # Swap pivot with last item a[hi] = pivot i = lo - 1 j = hi while i < j: i += 1 while a[i] < pivot: i += 1 j -= 1 while a[j] > pivot and j > lo: j -= 1 if i < j: t = a[i]; a[i] = a[j]; a[j] = t # swap a[i] and a[j] a[hi] = a[i] a[i] = pivot # Put pivot where it belongs return i # index of pivot Can the while a[i] < pivot loop walk off the end of the list and generate an exception? No, because a[hi] contains the pivot, so i will stop advancing when i == hi (if not sooner). There is no such assurance for j, though, so the while a[j] > pivot loop explicitly tests whether j > lo before decreasing j. 8