Previous Lecture | Lecture 14 | Next Lecture |
Lecture 14, Tue 11/14
Quicksort
Recorded Lecture: 11_14_23
Quicksort
- Another divide-and-conquer algorithm
- Can improve running times to O(n log n) in a typical case, but we’ll see how this can also lead to O(n2) in a worst-case scenario
- Idea:
- We can sort a list by subdividing the list based on a PIVOT value
- Place elements < pivot to the left-side of the list
- Place elements > pivot right-side of the list
- Recurse for each left / right portion of the list
- When sub list sizes == 1, then entire list is sorted
How do we partition the list into left / right sub lists?
- Choose pivot (typically first element in list)
- leftmark index is on left-most side of list, rightmark index is on right-most side of list, and both leftmark and rightmark work inwards
- Find an element in the left side (using leftmark index) that’s out-of-place (> pivot)
- Find an element in the right side (using rightmark index) that’s out-of-place (< pivot)
- Swap out-of-place elements with each other
- We’re putting them in the correct side of the list!
- Continue doing steps 1 - 5 until the rightmark index < leftmark index
- Swap the pivot with rightmark index
- We’re putting the pivot element in the correct place!
Quick Sort Hand Drawn Example
Quick Sort Implementation
def quickSort(alist):
quickSortHelper(alist, 0, len(alist) - 1)
# helper function so we can pass in the first / last index
# of lists
def quickSortHelper(alist, first, last):
if first < last:
# will define the indices of the left / right sub lists
splitpoint = partition(alist, first, last)
# recursively sort the left / right sub lists
quickSortHelper(alist, first, splitpoint-1) # left
quickSortHelper(alist, splitpoint+1, last) # right
# partition function will organize left sublist < pivot
# and right sub list > pivot
def partition(alist, first, last):
pivotvalue = alist[first] # choose first element as pivot
leftmark = first + 1
rightmark = last
done = False
while not done:
# move leftmark until we find a left element > pivot
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark = leftmark + 1
# move rightmark until we find a right element < pivot
while rightmark >= leftmark and alist[rightmark] >= pivotvalue:
rightmark = rightmark - 1
# check if we're done swapping left / right elements in
# correct side
if rightmark < leftmark:
done = True
else: # swap left and right elements into correct side of list
temp = alist[leftmark]
alist[leftmark] = alist[rightmark]
alist[rightmark] = temp
# put the pivot value into the correct place (swap pivot w/ rightmark)
temp = alist[first] # pivot
alist[first] = alist[rightmark]
alist[rightmark] = temp
return rightmark
Quick Sort Analysis
- Best-case running time is O(n log n)
- In the best case, there are log n levels. Each level is O(n) when performing the partition step
- However, the worst case is O(n2)
- Consider the case where the list is already sorted (or in reverse order)
- The sub lists aren’t evenly divided for every recursive call
- Quick Sort performance is dependent on the pivot value!
- Can try to improve the pivot choice by selecting random values and choosing the medium
- Textbook describes the median of three approach
- Choose first, middle, and last element. Choose the median of these values
- But even then, there is no guarantee that these values are good pivot values, but it does improve our chances that they are
- Note that Quicksort DOES NOT need extra space (unlike merge sort)