This workshop is important because:
Merge sort is the first fast, powerful sorting algorithm that you will encounter in the wilds of the real world (it's baked into Safari and Firefox!). It uses an extremely efficient application of the 'Divide and Conquer' concept to sort lists of elements. It's also a great chance to practice recursion.
After this workshop, developers will be able to:
- explain the three parts of a recursive algorithm.
- describe the merge algorithm and a merge sort algorithm.
- write a pseudocode version of merge sort and a draft version of merge sort in Ruby.
- determine the runtime (in big-
O()
notation) for merge sort.
Before this workshop, developers should already be able to:
- iterate through an array using a
for
loop,forEach
, or other iterators. - describe the purpose of big-
O()
notation.
A recursive function is a function that calls itself. Recursive functions can have results that feel magical compared to the amount of code that's been written. For example look at this version of a factorial function in JavaScript:
function factorial(n):
if (n === 0 || n === 1){
return 1;
}
else {
return n * factorial(n-1);
}
Or in Ruby:
def factorial(n)
if n == 0 || n == 1
1
else
n * factorial(n-1)
end
end
On the board, walk through this function with input of 1, 2, and 3 to find out what it returns.
Factorial uses the notation
n!
, pronounced "n
factorial" (like3!
or9001!
). It is defined for whole numbers greater than or equal to0
-- it means the result of multiplyingn * (n-1) * (n-2) * ... * 3 * 2 * 1
. You can probably think of an iterative way to write this with afor
loop, but a recursive version can give us a little insight into recursion.
Warning: there's a whole lot of insight involved in generating the code above. On your first encounters with recursion, it might be pretty hard to imagine writing the code yourself. That's OKAY! Look at and closely study many examples and you'll get closer and closer to generating this stuff yourself.
Let's use factorial
as an example to illustrate the three steps of a recursive algorithm:
function factorial(n):
if (n === 0 || n === 1){
return 1;
}
else {
return n * factorial(n-1);
}
- Define base case(s) - make special case or cases to handle the simplest possible inputs without recursion. The base cases above are where
n
is0
or1
. By definition,0!
and1!
are both just1
. - Make recursive call(s) - if you are not looking at a base case, recursively solve smaller subproblems to help find the answer to the main problem. In the example above,
factorial(n-1)
is the recursive call. It is a call to solve the subproblem of(n-1)!
. We pick that as our subproblem becausen!
is equal ton*(n-1)!
. For example3! = 3 * 2!
. - Phrase the returned value in terms of subproblem answer(s) - combine subproblem answers (the result of recursive calls) with any extra processing needed to pull out the final answer. In the example, multiplying the subproblem answer
factorial(n-1)
byn
gives the final result. We want to make sure we return that result.
What's the Fibonnaci sequence?
1
1
2
3
5
8
13
...
Here's pseudocode for calculating the Fibonacci sequence:
fibonacci(n) {
if (n === 1 or n === 0)
return 1
else
return fibonacci(n-1) + fibonacci(n-2)
}
- What base case(s) are covered?
- What recursive subproblem(s) are solved?
- What processing turns the answers from the subproblem(s) into an answer for the overall problem?
Now a more challenging example. Think about the same three questions for this binary search pseudocode:
binarySearch(array, target, low, high) {
if (low > high)
return -1;
int mid = (low + high)/2;
if (array[mid] == target)
return mid;
else if (array[mid] < target)
return binarySearch(array, target, mid+1, high);
else // last possibility: a[mid] > target
return binarySearch(array, target, low, mid-1);
}
Take a look at this video. Can you see how merge sort works?
Merge sort works on the basic principal of divide and conquer - dividing your list into sub-lists (recursively) until your sub-lists are of length one or zero. Once your sub-lists are at that size, you merge with a neighboring sub-list. When you merge them, you merge them in sorted order.
Divide Into Sub-lists
Merge Sort Visualization from University of Alberta
Merge To Build Sorted List
Merge Sort Visualization from University of Alberta
There are usually TWO algorithms that work together to accomplish a merge sort:
-
A merging algorithm that takes two sorted arrays and combined them into one large sorted array pushing the lowest to highest valued elements. The merge algorithm is not recursive.
-
A merge sort algorithm that takes an array, splits it into two halves, recursively merge sorts both halves, and finally uses the merge algorithm to put them back together into one sorted array.
Note: iterative merge sort is possible, but it's much harder. Please work on a recursive version!
To try to figure this out, let's try a few methods.
- On the graph below what are the realistic possibilities for the runtime of this algorithm?
-
How "tall" is the diagram below? That is, how many times do we divide and how many times do we "conquer"?
-
How "wide" is it? That is, how many steps are, roughly, there in each divide/conquer step?
Goal: Write a merge
function and a mergeSort
function to implement recursive merge sort as described above.
- Start with the recursive
mergeSort
-- assume you have a workingmerge
function. Consider:
- What base case(s) do you need for
mergeSort
? - What smaller subproblems can you solve recursively?
- How will you go from the solutions for subproblems, to a solution for the overall problem?
- Next, tackle
merge
. This isn't recursive, but edge cases can make it tricky!