(close)

Use this link to get a discount on the Automate the Boring Stuff online video course.

Support me on Patreon

Use this link to get a discount on the Automate the Boring Stuff online video course.

Support me on Patreon

Prev: Chapter 4 - Backtracking and Tree Traversal Algorithms | Next: Chapter 6 - Permutations and Combinations

Divide-and-Conquer Algorithms

*Divide-and-conquer algorithms** *are those that split large problems into smaller subproblems, then divide those subproblems into ones that are smaller yet, until they become trivial to conquer. This approach makes recursion an ideal technique to use: the recursive case divides the problem into self-similar subproblems, and the base case occurs when the subproblem has been reduced to a trivial size. One benefit of this approach is that these problems can be worked on in parallel, allowing multiple central processing unit (CPU) cores or computers to work on them.

In this chapter, we’ll look at some common algorithms that use recursion to divide and conquer, such as binary search, quicksort, and merge sort. We’ll also reexamine summing an array of integers, this time with a divide-and-conquer approach. Finally, we’ll take a look at the more esoteric Karatsuba multiplication algorithm, developed in 1960, that laid the basis for computer hardware’s fast integer multiplication.

Let’s say you have a bookshelf of 100 books. You can’t remember which books you have or their exact locations on the shelf, but you do know that they are sorted alphabetically by title. To find your book *Zebras: The Complete Guide*, you wouldn’t start at the beginning of the bookshelf, where *Aaron Burr Biography* is, but rather toward the end of the bookshelf. Your zebra book wouldn’t be the very last book on the shelf if you also had books on zephyrs, zoos, and zygotes, but it would be close. Thus, you can use the facts that the books are in alphabetical order and that *Z* is the last letter of the alphabet as *heuristics*, or approximate clues, to look toward the end of the shelf rather than the beginning.

*Binary search* is a technique for locating a target item in a sorted list by repeatedly determining which half of the list the item is in. The most impartial way to search the bookshelf is to start with a book in the middle, and then ascertain if the target book you’re looking for is in the left half or the right half.

You can then repeat this process, as shown in Figure 5-1: look at the book in the middle of your chosen half and then determine whether your target book is in the left-side quarter or the right-side quarter. You can do this until you either find the book, or find the place where the book should be but isn’t and declare that the book doesn’t exist on the shelf.

This process scales efficiently; doubling the number of books to search adds only one step to the search process. A linear search of a shelf with 50 books takes 50 steps, and a linear search of a shelf with 100 books takes 100 steps. But a binary search of a shelf with 50 books takes only 6 steps, and a shelf with 100 books takes only 7 steps.

Let’s ask the three recursion questions about our binary search implementation:

- What is the base case? Searching a range of items that is only one item in length.
- What argument is passed to the recursive function call? The indices of the left and right ends of the range in the list we are searching.
- How does this argument become closer to the base case? The range halves in size for each recursive call, so it eventually becomes one item long.

Examine the following `binarySearch()`

function in our *binarySearch.py* program, which locates a value, `needle`

, in a sorted list of values, `haystack`

:

**Python**

```
def binarySearch(needle, haystack, left=None, right=None):
# By default, `left` and `right` are all of `haystack`:
if left is None:
left = 0 # `left` defaults to the 0 index.
if right is None:
right = len(haystack) - 1 # `right` defaults to the last index.
print('Searching:', haystack[left:right + 1])
if left > right: # BASE CASE
return None # The `needle` is not in `haystack`.
mid = (left + right) // 2
if needle == haystack[mid]: # BASE CASE
return mid # The `needle` has been found in `haystack`
elif needle < haystack[mid]: # RECURSIVE CASE
return binarySearch(needle, haystack, left, mid - 1)
elif needle > haystack[mid]: # RECURSIVE CASE
return binarySearch(needle, haystack, mid + 1, right)
print(binarySearch(13, [1, 4, 8, 11, 13, 16, 19, 19]))
```

The *binarySearch.html* program has this JavaScript equivalent:

**JavaScript**

```
<script type="text/javascript">
function binarySearch(needle, haystack, left, right) {
// By default, `left` and `right` are all of `haystack`:
if (left === undefined) {
left = 0; // `left` defaults to the 0 index.
}
if (right === undefined) {
right = haystack.length - 1; // `right` defaults to the last index.
}
document.write("Searching: [" +
haystack.slice(left, right + 1).join(", ") + "]<br />");
if (left > right) { // BASE CASE
return null; // The `needle` is not in `haystack`.
}
let mid = Math.floor((left + right) / 2);
if (needle == haystack[mid]) { // BASE CASE
return mid; // The `needle` has been found in `haystack`.
} else if (needle < haystack[mid]) { // RECURSIVE CASE
return binarySearch(needle, haystack, left, mid - 1);
} else if (needle > haystack[mid]) { // RECURSIVE CASE
return binarySearch(needle, haystack, mid + 1, right);
}
}
document.write(binarySearch(13, [1, 4, 8, 11, 13, 16, 19, 19]));
</script>
```

When you run these programs, the list `[1, 4, 8, 11, 13, 16, 19, 19]`

is searched for `13`

, and the output looks like this:

```
Searching: [1, 4, 8, 11, 13, 16, 19, 19]
Searching: [13, 16, 19, 19]
Searching: [13]
4
```

The target value `13`

is indeed at index `4`

in that list.

The code calculates the middle index (stored in `mid`

) of the range defined by the `left`

and `right`

indices. At first, this range is the entire length of the items list. If the value at the `mid`

index is the same as `needle`

, then `mid`

is returned. Otherwise, we need to figure out whether our target value is in the left half of the range (in which case, the new range to search is `left`

to `mid - 1`

) or in the right half (in which case, the new range to search is `mid + 1`

to `end`

).

We already have a function that can search this new range: `binarySearch()`

itself! A recursive call is made on the new range. If we ever get to the point where the right end of the search range comes before the left, we know that our search range has shrunk down to zero and our target value isn’t to be found.

Notice that the code performs no actions after the recursive call returns; it immediately returns the return value of the recursive function call. This feature means that we could implement tail call optimization for this recursive algorithm, a practice we explain in Chapter 8. But also, it means that binary search can easily be implemented as an iterative algorithm that doesn’t use recursive function calls. This book’s downloadable resources at https://nostarch.com/recursive-book-recursion include the source code for an iterative binary search for you to compare with the recursive binary search.

Remember that `binarySearch()`

’s speed advantage comes from the fact that the values in items are sorted. If the values are out of order, the algorithm won’t work. Enter *quicksort*, a recursive sorting algorithm developed by computer scientist Tony Hoare in 1959.

Quicksort uses a divide-and-conquer technique called *partitioning*. Think of partitioning this way: imagine you have a large pile of unalphabetized books. Grabbing one book and placing it in the right spot on the shelf means you’ll spend a lot of time rearranging the bookshelf as it gets full. It would help if you first turned the pile of books into two piles: an *A* to *M* pile and an *N* to *Z* pile. (In this example, *M* would be our *pivot*.)

You haven’t sorted the pile, but you have *partitioned* it. And partitioning is easy: the book doesn’t have to go into the correct place in one of the two piles, it just has to go into the correct pile. Then you can further partition these two piles into four piles: *A* to *G*, *H* to *M*, *N* to *T*, and *U* to *Z*. This is shown in Figure 5-2. If you keep partitioning, you end up with piles that contain one book each (the base case), and the piles are now in sorted order. This means the books are now in sorted order as well. This repeated partitioning is how quicksort works.

For the first partitioning of *A* to *Z*, we select *M* as the pivot value because it’s the middle letter between *A* and *Z*. However, if our collection of books consisted of one book about Aaron Burr and 99 books about zebras, zephyrs, zoos, zygotes, and other *Z* topics, our two partitioned piles would be heavily unbalanced. We would have the single Aaron Burr book in the *A* to *M* pile and every other book in the *M* to *Z* pile. The quicksort algorithm works fastest when the partitions are evenly balanced, so selecting a good pivot value at each partition step is important.

However, if you don’t know anything about the data you’re sorting, it’s impossible to select an ideal pivot. This is why the generic quicksort algorithm simply uses the last value in the range for the pivot value.

In our implementation, each call to `quicksort()`

is given an array of items to sort. It is also given `left`

and `right`

arguments specifying the range of indices in that array to sort, similar to `binarySearch()`

’s left and right arguments. The algorithm selects a pivot value to compare with the other values in the range, then places the values to either the left side of the range (if they’re less than the pivot value) or the right side (if they’re greater than the pivot value). This is the partition step. Next, the `quicksort()`

function is recursively called on these two, smaller ranges until a range has been reduced to zero. The list becomes more and more sorted as the recursive calls are made, until finally the entire list is in the correct order.

Note that the algorithm modifies the array in place. See “Modifying a List or Array in Place” in Chapter 4 for details. Thus, the `quicksort()`

function doesn’t return a sorted array. The base case merely returns to stop producing more recursive calls.

Let’s ask the three recursion questions about our binary search implementation:

- What is the base case? Being given a range to sort that contains zero or one item and that is already in sorted order.
- What argument is passed to the recursive function call? The indices of the left and right ends of the range in the list we are sorting.
- How does this argument become closer to the base case? The range halves in size for each recursive call, so it eventually becomes empty.

The following `quicksort()`

function in the *quicksort.py* Python program sorts the values in the items list into ascending order:

```
def quicksort(items, left=None, right=None):
# By default, `left` and `right` span the entire range of `items`:
if left is None:
left = 0 # `left` defaults to the 0 index.
if right is None:
right = len(items) - 1 # `right` defaults to the last index.
print('\nquicksort() called on this range:', items[left:right + 1])
print('................The full list is:', items)
if right <= left: ❶
# With only zero or one item, `items` is already sorted.
return # BASE CASE
# START OF THE PARTITIONING
i = left # i starts at the left end of the range. ❷
pivotValue = items[right] # Select the last value for the pivot.
print('....................The pivot is:', pivotValue)
# Iterate up to, but not including, the pivot:
for j in range(left, right):
# If a value is less than the pivot, swap it so that it's on the
# left side of `items`:
if items[j] <= pivotValue:
# Swap these two values:
items[i], items[j] = items[j], items[i] ❸
i += 1
# Put the pivot on the left side of `items`:
items[i], items[right] = items[right], items[i]
# END OF THE PARTITIONING
print('....After swapping, the range is:', items[left:right + 1])
print('Recursively calling quicksort on:', items[left:i], 'and', items[i + 1:right + 1])
# Call quicksort() on the two partitions:
quicksort(items, left, i - 1) # RECURSIVE CASE
quicksort(items, i + 1, right) # RECURSIVE CASE
myList = [0, 7, 6, 3, 1, 2, 5, 4]
quicksort(myList)
print(myList)
```

The *quicksort.html* program contains the JavaScript equivalent:

```
<script type="text/javascript">
function quicksort(items, left, right) {
// By default, `left` and `right` span the entire range of `items`:
if (left === undefined) {
left = 0; // `left` defaults to the 0 index.
}
if (right === undefined) {
right = items.length - 1; // `right` defaults to the last index.
}
document.write("<br /><pre>quicksort() called on this range: [" +
items.slice(left, right + 1).join(", ") + "]</pre>");
document.write("<pre>................The full list is: [" + items.join(", ") + "]</pre>");
if (right <= left) { ❶
// With only zero or one item, `items` is already sorted.
return; // BASE CASE
}
// START OF THE PARTITIONING
let i = left; ❷ // i starts at the left end of the range.
let pivotValue = items[right]; // Select the last value for the pivot.
document.write("<pre>....................The pivot is: " + pivotValue.toString() +
"</pre>");
// Iterate up to, but not including, the pivot:
for (let j = left; j < right; j++) {
// If a value is less than the pivot, swap it so that it's on the
// left side of `items`:
if (items[j] <= pivotValue) {
// Swap these two values:
[items[i], items[j]] = [items[j], items[i]]; ❸
i++;
}
}
// Put the pivot on the left side of `items`:
[items[i], items[right]] = [items[right], items[i]];
// END OF THE PARTITIONING
document.write("<pre>....After swapping, the range is: [" + items.slice(left, right + 1).join(", ") + "]</pre>");
document.write("<pre>Recursively calling quicksort on: [" + items.slice(left, i).join(", ") + "] and [" + items.slice(i + 1, right + 1).join(", ") + "]</pre>");
// Call quicksort() on the two partitions:
quicksort(items, left, i - 1); // RECURSIVE CASE
quicksort(items, i + 1, right); // RECURSIVE CASE
}
let myList = [0, 7, 6, 3, 1, 2, 5, 4];
quicksort(myList);
document.write("<pre>[" + myList.join(", ") + "]</pre>");
</script>
```

This code is similar to the code in the binary search algorithm. As defaults, we set the `left`

and `right`

ends of the range within the `items`

array to the beginning and end of the entire array. If the algorithm reaches the base case of the `right`

end at or before the `left`

end (a range of one or zero items), the sorting is finished ❶.

In each call to `quicksort()`

, we partition the items in the current range (defined by the indices in `left`

and `right`

), and then swap them around so that the items less than the pivot value end up on the left side of the range and the items greater than the pivot value end up on the right side of the range. For example, if `42`

is the pivot value in the array `[81, 48, 94, 87, 83, 14, 6, 42]`

, a partitioned array would be `[14, 6, 42, 81, 48, 94, 87, 83]`

. Note that a partitioned array is not the same thing as a sorted one: although the two items to the left of `42`

are less than `42`

, and the five items to the right of `42`

are greater than `42`

, the items are not in sorted order.

The bulk of the `quicksort()`

function is the partitioning step. To get an idea of how partitioning works, imagine an index `j`

that begins at the left end of the range and moves to the right end ❷. We compare the item at index `j`

with the pivot value and then move right to compare the next item. The pivot value can be arbitrarily chosen from any value in the range, but we’ll always use the value at the right end of the range.

Imagine a second index `i`

that also begins at the left end. If the item at index `j`

is less than or equal to the pivot, the items at indices `i`

and `j`

are swapped ❸ and `i`

is increased to the next index. So while `j`

always increases (that is, moves right) after each comparison with the pivot value, `i`

increases only if the item at index `j`

is less than or equal to the pivot.

The names `i`

and `j`

are commonly used for variables that hold array indices. Someone else’s `quicksort()`

implementation may instead use `j`

and `i`

, or even completely different variables. The important thing to remember is that two variables store indices and behave as shown here.

As an example, let’s work through the first partitioning of the array `[0, 7, 6, 3, 1, 2, 5, 4]`

, and the range defined by `left`

of `0`

and `right`

of `7`

to cover the full size of the array. The `pivot`

will be the value at the `right`

end, `4`

. The `i`

and `j`

index begin at index `0`

, the left end of the range. At each step, index `j`

always moves to the right. Index `i`

moves only if the value at index `j`

is less than or equal to the pivot value. The `items`

array, the `i`

index, and the `j`

index begin as follows:

```
items: [0, 7, 6, 3, 1, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 0 i
j = 0 j
```

The value at index `j`

(which is `0`

) is less than or equal to the pivot value (which is `4`

), so swap the values at `i`

and `j`

. This results in no actual change since `i`

and `j`

are the same index. Also, increase `i`

so that it moves to the right. The `j`

index increases for every comparison with the pivot value. The state of the variables now looks like this:

```
items: [0, 7, 6, 3, 1, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 1 i
j = 1 j
```

The value at index `j`

(which is `7`

) is not less than or equal to the pivot value (which is `4`

), so don’t swap the values. Remember, `j`

always increases, but `i`

increases only after a swap is performed—so `i`

is always either at or to the left of `j`

. The state of the variables now looks like this:

```
items: [0, 7, 6, 3, 1, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 1 i ^
j = 2 j
```

The value at index `j`

(which is `6`

) is not less than or equal to the pivot value (which is `4`

), so don’t swap the values. The state of the variables now looks like this:

```
items: [0, 7, 6, 3, 1, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 1 i ^
j = 3 j
```

The value at index `j`

(which is `3`

) is less than or equal to the pivot value (which is `4`

), so swap the values at `i`

and `j`

. The `7`

and `3`

swap positions. Also, increase `i`

so that it moves to the right. The state of the variables now looks like this:

```
items: [0, 3, 6, 7, 1, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 2 i ^
j = 4 j
```

The value at index `j`

(which is `1`

) is less than or equal to the pivot value (which is `4`

), so swap the values at `i`

and `j`

. The `6`

and `1`

swap positions. Also, increase `i`

so that it moves to the right. The state of the variables now looks like this:

```
items: [0, 3, 1, 7, 6, 2, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 3 i ^
j = 5 j
```

The value at index `j`

(which is `2`

) is less than or equal to the pivot value (which is `4`

), so swap the values at `i`

and `j`

. The `7`

and `2`

swap positions. Also, increase `i`

so that it moves to the right. The state of the variables now looks like this:

```
items: [0, 3, 1, 2, 6, 7, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 4 i ^
j = 6 j
```

The value at index `j`

(which is `6`

) is not less than or equal to the pivot value (which is `4`

), so don’t swap the values. The state of the variables now looks like this:

```
items: [0, 3, 1, 2, 6, 7, 5, 4]
indices: 0 1 2 3 4 5 6 7
^
i = 4 i ^
j = 7 j
```

We’ve reached the end of the partitioning. The index `j`

is at the pivot value (which is always the rightmost value in the range), so let’s swap `i`

and `j`

one last time to make sure the pivot is not on the right half of the partition. The `6`

and `4`

swap positions. The state of the variables now looks like this:

```
items: [0, 3, 1, 2, 4, 7, 5, 6]
indices: 0 1 2 3 4 5 6 7
^
i = 4 i ^
j = 7 j
```

Notice what is happening with the `i`

index: this index will always receive the values smaller than the pivot value as a result of swapping; then the `i`

index moves right to receive future smaller-than-the-pivot values. As a result, everything to the left of the `i`

index is smaller than or equal to the pivot, and everything to the right of the `i`

index is greater than the pivot.

The entire process repeats as we recursively call `quicksort()`

on the left and right partitions. When we partition these two halves (and then partition the four halves of these two halves with more recursive `quicksort()`

calls, and so on), the entire array ends up sorted.

When we run these programs, the output shows the process of sorting the `[0, 7, 6, 3, 1, 2, 5, 4]`

list. The rows of periods are meant to help you line up the output when writing the code:

```
quicksort() called on this range: [0, 7, 6, 3, 1, 2, 5, 4]
................The full list is: [0, 7, 6, 3, 1, 2, 5, 4]
....................The pivot is: 4
....After swapping, the range is: [0, 3, 1, 2, 4, 7, 5, 6]
Recursively calling quicksort on: [0, 3, 1, 2] and [7, 5, 6]
quicksort() called on this range: [0, 3, 1, 2]
................The full list is: [0, 3, 1, 2, 4, 7, 5, 6]
....................The pivot is: 2
....After swapping, the range is: [0, 1, 2, 3]
Recursively calling quicksort on: [0, 1] and [3]
quicksort() called on this range: [0, 1]
................The full list is: [0, 1, 2, 3, 4, 7, 5, 6]
....................The pivot is: 1
....After swapping, the range is: [0, 1]
Recursively calling quicksort on: [0] and []
quicksort() called on this range: [0]
................The full list is: [0, 1, 2, 3, 4, 7, 5, 6]
quicksort() called on this range: []
................The full list is: [0, 1, 2, 3, 4, 7, 5, 6]
quicksort() called on this range: [3]
................The full list is: [0, 1, 2, 3, 4, 7, 5, 6]
quicksort() called on this range: [7, 5, 6]
................The full list is: [0, 1, 2, 3, 4, 7, 5, 6]
....................The pivot is: 6
....After swapping, the range is: [5, 6, 7]
Recursively calling quicksort on: [5] and [7]
quicksort() called on this range: [5]
................The full list is: [0, 1, 2, 3, 4, 5, 6, 7]
quicksort() called on this range: [7]
................The full list is: [0, 1, 2, 3, 4, 5, 6, 7]
Sorted: [0, 1, 2, 3, 4, 5, 6, 7]
```

Quicksort is a commonly used sorting algorithm because it is straightforward to implement and, well, quick. The other commonly used sorting algorithm, merge sort, is also fast and uses recursion. We cover it next.

Computer scientist John von Neumann developed *merge sort* in 1945. It uses a divide-merge approach: each recursive call to `mergeSort()`

divides the unsorted list into halves until they’ve been whittled down into lists of lengths of zero or one. Then, as the recursive calls return, these smaller lists are merged together into sorted order. When the last recursive call has returned, the entire list will have been sorted.

For example, the divide step takes a list, such as `[2, 9, 8, 5, 3, 4, 7, 6]`

, and splits it into two lists, like `[2, 9, 8, 5]`

and `[3, 4, 7, 6]`

, to pass to two recursive function calls. At the base case, the lists have been divided into lists of zero or one item. A list with nothing or one item is naturally sorted. After the recursive calls return, the code merges these small, sorted lists together into larger sorted lists until finally the entire list is sorted. Figure 5-3 shows an example using merge sort on playing cards.

For example, at the end of the division phase, we have eight separate lists of single numbers: `[2]`

, `[9]`

, `[8]`

, `[5]`

, `[3]`

, `[4]`

, `[7]`

, `[6]`

. A list of just one number is naturally in sorted order. Merging two sorted lists into a larger sorted list involves looking at the start of both smaller lists and appending the smaller value to the larger list. Figure 5-4 shows an example of merging `[2, 9]`

and `[5, 8]`

. This is repeatedly done in the merge phase until the end result is that the original `mergeSort()`

call returns the full list in sorted order.

Let’s ask our three recursive algorithm questions about the merge sort algorithm:

- What is the base case? Being given a list to sort that has zero or one item in it, which is already in sorted order.
- What argument is passed to the recursive function call? Lists made from the left and right halves of the original list to sort.
- How does this argument become closer to the base case? The lists passed to the recursive call are half the size of the original list, so they eventually become a list of zero or one item.

The following `mergeSort()`

function in the *mergeSort.py* Python program sorts the values in the items list into ascending order:

```
import math
def mergeSort(items):
print('.....mergeSort() called on:', items)
# BASE CASE - Zero or one item is naturally sorted:
if len(items) == 0 or len(items) == 1:
return items ❶
# RECURSIVE CASE - Pass the left and right halves to mergeSort():
# Round down if items doesn't divide in half evenly:
iMiddle = math.floor(len(items) / 2) ❷
print('................Split into:', items[:iMiddle], 'and', items[iMiddle:])
left = mergeSort(items[:iMiddle]) ❸
right = mergeSort(items[iMiddle:])
# BASE CASE - Returned merged, sorted data:
# At this point, left should be sorted and right should be
# sorted. We can merge them into a single sorted list.
sortedResult = []
iLeft = 0
iRight = 0
while (len(sortedResult) < len(items)):
# Append the smaller value to sortedResult.
if left[iLeft] < right[iRight]: ❹
sortedResult.append(left[iLeft])
iLeft += 1
else:
sortedResult.append(right[iRight])
iRight += 1
# If one of the pointers has reached the end of its list,
# put the rest of the other list into sortedResult.
if iLeft == len(left):
sortedResult.extend(right[iRight:])
break
elif iRight == len(right):
sortedResult.extend(left[iLeft:])
break
print('The two halves merged into:', sortedResult)
return sortedResult # Returns a sorted version of items.
myList = [2, 9, 8, 5, 3, 4, 7, 6]
myList = mergeSort(myList)
print(myList)
```

The *mergeSort.html* program contains the equivalent JavaScript program:

```
<script type="text/javascript">
function mergeSort(items) {
document.write("<pre>" + ".....mergeSort() called on: [" +
items.join(", ") + "]</pre>");
// BASE CASE - Zero or one item is naturally sorted:
if (items.length === 0 || items.length === 1) { // BASE CASE
return items; ❶
}
// RECURSIVE CASE - Pass the left and right halves to mergeSort():
// Round down if items doesn't divide in half evenly:
let iMiddle = Math.floor(items.length / 2); ❷
document.write("<pre>................Split into: [" + items.slice(0, iMiddle).join(", ") +
"] and [" + items.slice(iMiddle).join(", ") + "]</pre>");
let left = mergeSort(items.slice(0, iMiddle)); ❸
let right = mergeSort(items.slice(iMiddle));
// BASE CASE - Returned merged, sorted data:
// At this point, left should be sorted and right should be
// sorted. We can merge them into a single sorted list.
let sortedResult = [];
let iLeft = 0;
let iRight = 0;
while (sortedResult.length < items.length) {
// Append the smaller value to sortedResult.
if (left[iLeft] < right[iRight]) { ❹
sortedResult.push(left[iLeft]);
iLeft++;
} else {
sortedResult.push(right[iRight]);
iRight++;
}
// If one of the pointers has reached the end of its list,
// put the rest of the other list into sortedResult.
if (iLeft == left.length) {
Array.prototype.push.apply(sortedResult, right.slice(iRight));
break;
} else if (iRight == right.length) {
Array.prototype.push.apply(sortedResult, left.slice(iLeft));
break;
}
}
document.write("<pre>The two halves merged into: [" + sortedResult.join(", ") +
"]</pre>");
return sortedResult; // Returns a sorted version of items.
}
let myList = [2, 9, 8, 5, 3, 4, 7, 6];
myList = mergeSort(myList);
document.write("<pre>[" + myList.join(", ") + "]</pre>");
</script>
```

The `mergeSort()`

function (and all the recursive calls to the `mergeSort()`

function) takes an unsorted list and returns a sorted list. The first step in this function is to check for the base case of a list containing only zero or one item ❶. This list is already sorted, so the function returns the list as is.

Otherwise, the function determines the middle index of the list ❷ so that we know where to split it into the left- and right-half lists to pass to two recursive function calls ❸. The recursive function calls return sorted lists, which we store in the left and right variables.

The next step is to merge these two sorted half lists into one sorted full list named `sortedResult`

. We’ll maintain two indices for the `left`

and `right`

lists named `iLeft`

and `iRight`

. Inside a loop, the smaller of the two values ❹ is appended to `sortedResult`

, and its respective index variable (either `iLeft`

or `iRight`

) is incremented. If either `iLeft`

or `iRight`

reaches the end of its list, the remaining items in the other half’s list are appended to `sortedResult`

.

Let’s follow an example of the merging step if the recursive calls have returned `[2, 9]`

for `left`

and `[5, 8]`

for `right`

. Since these lists were returned from `mergeSort()`

calls, we can always assume they are sorted. We must merge them into a single sorted list in `sortedResult`

for the current `mergeSort()`

call to return to its caller.

The `iLeft`

and `iRight`

indices begin at `0`

. We compare the value at `left[iLeft]`

(which is `2`

) and `right[iRight]`

(which is `5`

) to find the smaller one:

```
sortedResult = []
left: [2, 9] right: [5, 8]
indices: 0 1 0 1
iLeft = 0 ^
iRight = 0 ^
```

Since `left[iLeft]`

’s value, `2`

, is the smaller of the values, we append it to `sortedResult`

and increase `iLeft`

from `0`

to `1`

. The state of the variables is now as follows:

```
sortedResult = [2]
left: [2, 9] right: [5, 8]
indices: 0 1 0 1
iLeft = 1 ^
iRight = 0 ^
```

Comparing `left[iLeft]`

and `right[iRight]`

again, we find that of `9`

and `5`

, `right[iRight]`

’s `5`

is smaller. The code appends the `5`

to `sortedResult`

and increases `iRight`

from `0`

to `1`

. The state of the variables is now the following:

```
sortedResult = [2, 5]
left: [2, 9] right: [5, 8]
indices: 0 1 0 1
iLeft = 1 ^
iRight = 1 ^
```

Comparing `left[iLeft]`

and `right[iRight]`

again, we find that, of `9`

and `8`

, `right[iRight]`

’s `8`

is smaller. The code appends the `8`

to `sortedResult`

and increases `iRight`

from `0`

to `1`

. Here’s the state of the variables now:

```
sortedResult = [2, 5, 8]
left: [2, 9] right: [5, 8]
indices: 0 1 0 1
iLeft = 1 ^
iRight = 2 ^
```

Because `iRight`

is now `2`

and equal to the length of the `right`

list, the remaining items in `left`

from the `iLeft`

index to the end are appended to `sortedResult`

, as no more items remain in `right`

to compare them to. This leaves `sortedResult`

as `[2, 5, 8, 9]`

, the sorted list it needs to return. This merging step is performed for every call to `mergeSort()`

to produce the final sorted list.

When we run the *mergeSort.py* and *mergeSort.html* programs, the output shows the process of sorting the `[2, 9, 8, 5, 3, 4, 7, 6]`

list:

```
.....mergeSort() called on: [2, 9, 8, 5, 3, 4, 7, 6]
................Split into: [2, 9, 8, 5] and [3, 4, 7, 6]
.....mergeSort() called on: [2, 9, 8, 5]
................Split into: [2, 9] and [8, 5]
.....mergeSort() called on: [2, 9]
................Split into: [2] and [9]
.....mergeSort() called on: [2]
.....mergeSort() called on: [9]
The two halves merged into: [2, 9]
.....mergeSort() called on: [8, 5]
................Split into: [8] and [5]
.....mergeSort() called on: [8]
.....mergeSort() called on: [5]
The two halves merged into: [5, 8]
The two halves merged into: [2, 5, 8, 9]
.....mergeSort() called on: [3, 4, 7, 6]
................Split into: [3, 4] and [7, 6]
.....mergeSort() called on: [3, 4]
................Split into: [3] and [4]
.....mergeSort() called on: [3]
.....mergeSort() called on: [4]
The two halves merged into: [3, 4]
.....mergeSort() called on: [7, 6]
................Split into: [7] and [6]
.....mergeSort() called on: [7]
.....mergeSort() called on: [6]
The two halves merged into: [6, 7]
The two halves merged into: [3, 4, 6, 7]
The two halves merged into: [2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 4, 5, 6, 7, 8, 9]
```

As you can see from the output, the function divides the `[2, 9, 8, 5, 3, 4, 7, 6]`

list into `[2, 9, 8, 5]`

and `[3, 4, 7, 6]`

and passes these to recursive `mergeSort()`

calls. The first list is further split into `[2, 9]`

and `[8, 5]`

. That `[2, 9]`

list is split into `[2]`

and `[9]`

. These single-value lists cannot be divided anymore, so we have reached our base case. These lists are merged back into sorted order as `[2, 9]`

. The function divides the `[8, 5]`

list into `[8]`

and `[5]`

, reaches the base case, and then merges back into `[5, 8]`

.

The `[2, 9]`

and `[5, 8]`

lists are individually in sorted order. Remember, `mergeSort()`

doesn’t simply *concatenate* the lists into `[2, 9, 5, 8]`

, which would not be in sorted order. Rather, the function *merges* them into the sorted list `[2, 5, 8, 9]`

. By the time the original `mergeSort()`

call returns, the full list it returns is completely sorted.

We already covered summing an array of integers in Chapter 3 with the head-tail technique. In this chapter, we’ll use a divide-and-conquer strategy. Since the associative property of addition means that adding 1 + 2 + 3 + 4 is the same as adding the sums of 1 + 2 and 3 + 4, we can divide a large array of numbers to sum into two smaller arrays of numbers to sum.

The benefit is that for larger sets of data to process, we could farm out the subproblems to different computers and have them all work together in parallel. There’s no need to wait for the first half of the array to be summed before another computer can start summing the second half. This is a large advantage of the divide-and-conquer technique, as CPUs aren’t getting much faster but we can have multiple CPUs work simultaneously.

Let’s ask the three questions about recursive algorithms for our summation function:

- What is the base case? Either an array containing zero numbers (where we return
`0`

) or an array containing one number (where we return the number). - What argument is passed to the recursive function call? Either the left half or the right half of the array of numbers.
- How does this argument become closer to the base case? The size of the array of numbers is halved each time, eventually becoming an array containing zero or one number.

The *sumDivConq.py* Python program implements the divide-and-conquer strategy for adding numbers in the `sumDivConq()`

function:

**Python**

```
def sumDivConq(numbers):
if len(numbers) == 0: # BASE CASE
❶ return 0
elif len(numbers) == 1: # BASE CASE
❷ return numbers[0]
else: # RECURSIVE CASE
❸ mid = len(numbers) // 2
leftHalfSum = sumDivConq(numbers[0:mid])
rightHalfSum = sumDivConq(numbers[mid:len(numbers) + 1])
❹ return leftHalfSum + rightHalfSum
nums = [1, 2, 3, 4, 5]
print('The sum of', nums, 'is', sumDivConq(nums))
nums = [5, 2, 4, 8]
print('The sum of', nums, 'is', sumDivConq(nums))
nums = [1, 10, 100, 1000]
print('The sum of', nums, 'is', sumDivConq(nums))
```

The *sumDivConq.html* program contains the JavaScript equivalent:

**JavaScript**

```
<script type="text/javascript">
function sumDivConq(numbers) {
if (numbers.length === 0) { // BASE CASE
❶ return 0;
} else if (numbers.length === 1) { // BASE CASE
❷ return numbers[0];
} else { // RECURSIVE CASE
❸ let mid = Math.floor(numbers.length / 2);
let leftHalfSum = sumDivConq(numbers.slice(0, mid));
let rightHalfSum = sumDivConq(numbers.slice(mid, numbers.length + 1));
❹ return leftHalfSum + rightHalfSum;
}
}
let nums = [1, 2, 3, 4, 5];
document.write('The sum of ' + nums + ' is ' + sumDivConq(nums) + "<br />");
nums = [5, 2, 4, 8];
document.write('The sum of ' + nums + ' is ' + sumDivConq(nums) + "<br />");
nums = [1, 10, 100, 1000];
document.write('The sum of ' + nums + ' is ' + sumDivConq(nums) + "<br />");
</script>
```

The output of this program is:

```
The sum of [1, 2, 3, 4, 5] is 15
The sum of [5, 2, 4, 8] is 19
The sum of [1, 10, 100, 1000] is 1111
```

The `sumDivConq()`

function first checks the `numbers`

array for having either zero or one number in it. These trivial base cases are easy to sum since they require no addition: return either `0`

❶ or the lone number in the array ❷. Everything else is a recursive case; the middle index of the array is calculated ❸ so that separate recursive calls with the left half and right half of the numbers array are made. The sum of these two return values becomes the return value for the current `sumDivConq()`

call ❹.

Because of the associative nature of addition, there’s no reason an array of numbers must be added sequentially by a single computer. Our program carries out all operations on the same computer, but for large arrays or calculations more complicated than addition, our program could send the halves to other computers to complete. The problem can be divided into similar subproblems, which is a huge hint that a recursive approach can be taken.

The `*`

operator makes multiplication easy to do in high-level programming languages such as Python and JavaScript. But low-level hardware needs a way to perform multiplication using more primitive operations. We could multiply two integers using only addition with a loop, such as in the following Python code to multiply `5678 * 1234`

:

`>>> `**x = 5678**
>>> **y = 1234**
>>> **product = 0**
>>> **for i in range(x):**
... **product += y**
...
>>> **product**
7006652

However, this code doesn’t scale efficiently for large integers. *Karatsuba multiplication* is a fast, recursive algorithm discovered in 1960 by Anatoly Karatsuba that can multiply integers using addition, subtraction, and a precomputed multiplication table of all products from single-digit numbers. This multiplication table, shown in Figure 5-5, is called a *lookup table*.

Our algorithm won’t need to multiply single-digit numbers because it can just look them up in the table. By using memory to store precomputed values, we increase memory usage to decrease CPU runtime.

We’ll implement Karatsuba multiplication in a high-level language like Python or JavaScript as though the `*`

operator didn’t already exist. Our `karatsuba()`

function accepts two integer arguments, `x`

and `y`

, to multiply. The Karatsuba algorithm has five steps, and the first three involve making recursive calls to `karatsuba()`

with arguments that are smaller, broken-down integers derived from `x`

and `y`

. The base case occurs when the `x`

and `y`

arguments are both single-digit numbers, in which case the product can be found in the precomputed lookup table.

We also define four more variables: `a`

and `b`

are each half of the digits of `x`

, and `c`

and `d`

are each half of the digits of `y`

, as shown in Figure 5-6. For example, if `x`

and `y`

are `5678`

and `1234`

, respectively, then `a`

is `56`

, `b`

is `78`

, `c`

is `12`

, and `d`

is `34`

.

Here are the five steps of the Karatsuba algorithm:

- Multiply
`a`

and`c`

either from the multiplication lookup table or from a recursive call to`karatsuba()`

. - Multiply
`b`

and`d`

either from the multiplication lookup table or from a recursive call to`karatsuba()`

. - Multiply
`a + c`

and`b + d`

either from the multiplication lookup table or from a recursive call to`karatsuba()`

. - Calculate step 3 – step 2 – step 1.
- Pad the step 1 and step 4 results with zeros; then add them to step 2.

The result of step 5 is the product of `x`

and `y`

. The specifics of how to pad the step 1 and step 4 results with zeros are explained later in this section.

Let’s ask our three recursive algorithm questions about the `karatsuba()`

function:

- What is the base case? Multiplying single-digit numbers, which can be done with a precomputed lookup table.
- What argument is passed to the recursive function call? The
`a`

,`b`

,`c`

, and`d`

values derived from the`x`

and`y`

arguments. - How does this argument become closer to the base case? Since
`a`

,`b`

,`c`

, and`d`

are each half of the digits of`x`

and`y`

and themselves are used for the next recursive call’s`x`

and`y`

arguments, the recursive call’s arguments become closer and closer to the single-digit numbers the base case requires.

Our Python implementation for Karatsuba multiplication is in the *karatsubaMultiplication.py* program:

```
import math
# Create a lookup table of all single-digit multiplication products:
MULT_TABLE = {} ❶
for i in range(10):
for j in range(10):
MULT_TABLE[(i, j)] = i * j
def padZeros(numberString, numZeros, insertSide):
"""Return a string padded with zeros on the left or right side."""
if insertSide == 'left':
return '0' * numZeros + numberString
elif insertSide == 'right':
return numberString + '0' * numZeros
def karatsuba(x, y):
"""Multiply two integers with the Karatsuba algorithm. Note that
the * operator isn't used anywhere in this function."""
assert isinstance(x, int), 'x must be an integer'
assert isinstance(y, int), 'y must be an integer'
x = str(x)
y = str(y)
# At single digits, look up the products in the multiplication table:
if len(x) == 1 and len(y) == 1: # BASE CASE
print('Lookup', x, '*', y, '=', MULT_TABLE[(int(x), int(y))])
return MULT_TABLE[(int(x), int(y))]
# RECURSIVE CASE
print('Multiplying', x, '*', y)
# Pad with prepended zeros so that x and y are the same length:
if len(x) < len(y): ❷
# If x is shorter than y, pad x with zeros:
x = padZeros(x, len(y) - len(x), 'left')
elif len(y) < len(x):
# If y is shorter than x, pad y with zeros:
y = padZeros(y, len(x) - len(y), 'left')
# At this point, x and y have the same length.
halfOfDigits = math.floor(len(x) / 2) ❸
# Split x into halves a & b, split y into halves c & d:
a = int(x[:halfOfDigits])
b = int(x[halfOfDigits:])
c = int(y[:halfOfDigits])
d = int(y[halfOfDigits:])
# Make the recursive calls with these halves:
step1Result = karatsuba(a, c) ❹ # Step 1: Multiply a & c.
step2Result = karatsuba(b, d) # Step 2: Multiply b & d.
step3Result = karatsuba(a + b, c + d) # Step 3: Multiply a + b & c + d.
# Step 4: Calculate Step 3 - Step 2 - Step 1:
step4Result = step3Result - step2Result - step1Result ❺
# Step 5: Pad these numbers, then add them for the return value:
step1Padding = (len(x) - halfOfDigits) + (len(x) - halfOfDigits)
step1PaddedNum = int(padZeros(str(step1Result), step1Padding, 'right'))
step4Padding = (len(x) - halfOfDigits)
step4PaddedNum = int(padZeros(str(step4Result), step4Padding, 'right'))
print('Solved', x, 'x', y, '=', step1PaddedNum + step2Result + step4PaddedNum)
return step1PaddedNum + step2Result + step4PaddedNum ❻
# Example: 1357 x 2468 = 3349076
print('1357 * 2468 =', karatsuba(1357, 2468))
```

The JavaScript equivalent is in *karatsubaMultiplication.html*:

```
<script type="text/javascript">
// Create a lookup table of all single-digit multiplication products:
let MULT_TABLE = {}; ❶
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
MULT_TABLE[[i, j]] = i * j;
}
}
function padZeros(numberString, numZeros, insertSide) {
// Return a string padded with zeros on the left or right side.
if (insertSide === "left") {
return "0".repeat(numZeros) + numberString;
} else if (insertSide === "right") {
return numberString + "0".repeat(numZeros);
}
}
function karatsuba(x, y) {
// Multiply two integers with the Karatsuba algorithm. Note that
// the * operator isn't used anywhere in this function.
console.assert(Number.isInteger(x), "x must be an integer");
console.assert(Number.isInteger(y), "y must be an integer");
x = x.toString();
y = y.toString();
// At single digits, look up the products in the multiplication table:
if ((x.length === 1) && (y.length === 1)) { // BASE CASE
document.write("Lookup " + x.toString() + " * " + y.toString() + " = " +
MULT_TABLE[[parseInt(x), parseInt(y)]] + "<br />");
return MULT_TABLE[[parseInt(x), parseInt(y)]];
}
// RECURSIVE CASE
document.write("Multiplying " + x.toString() + " * " + y.toString() +
"<br />");
// Pad with prepended zeros so that x and y are the same length:
if (x.length < y.length) { ❷
// If x is shorter than y, pad x with zeros:
x = padZeros(x, y.length - x.length, "left");
} else if (y.length < x.length) {
// If y is shorter than x, pad y with zeros:
y = padZeros(y, x.length - y.length, "left");
}
// At this point, x and y have the same length.
let halfOfDigits = Math.floor(x.length / 2); ❸
// Split x into halves a & b, split y into halves c & d:
let a = parseInt(x.substring(0, halfOfDigits));
let b = parseInt(x.substring(halfOfDigits));
let c = parseInt(y.substring(0, halfOfDigits));
let d = parseInt(y.substring(halfOfDigits));
// Make the recursive calls with these halves:
let step1Result = karatsuba(a, c); ❹ // Step 1: Multiply a & c.
let step2Result = karatsuba(b, d); // Step 2: Multiply b & d.
let step3Result = karatsuba(a + b, c + d); // Step 3: Multiply a + b & c + d.
// Step 4: Calculate Step 3 - Step 2 - Step 1:
let step4Result = step3Result - step2Result - step1Result; ❺
// Step 5: Pad these numbers, then add them for the return value:
let step1Padding = (x.length - halfOfDigits) + (x.length - halfOfDigits);
let step1PaddedNum = parseInt(padZeros(step1Result.toString(), step1Padding, "right"));
let step4Padding = (x.length - halfOfDigits);
let step4PaddedNum = parseInt(padZeros((step4Result).toString(), step4Padding, "right"));
document.write("Solved " + x + " x " + y + " = " +
(step1PaddedNum + step2Result + step4PaddedNum).toString() + "<br />");
return step1PaddedNum + step2Result + step4PaddedNum; ❻
}
// Example: 1357 x 2468 = 3349076
document.write("1357 * 2468 = " + karatsuba(1357, 2468).toString() + "<br />");
</script>
```

When you run this code, the output looks like this:

```
Multiplying 1357 * 2468
Multiplying 13 * 24
Lookup 1 * 2 = 2
Lookup 3 * 4 = 12
Lookup 4 * 6 = 24
Solved 13 * 24 = 312
Multiplying 57 * 68
Lookup 5 * 6 = 30
Lookup 7 * 8 = 56
Multiplying 12 * 14
Lookup 1 * 1 = 1
Lookup 2 * 4 = 8
Lookup 3 * 5 = 15
Solved 12 * 14 = 168
Solved 57 * 68 = 3876
Multiplying 70 * 92
Lookup 7 * 9 = 63
Lookup 0 * 2 = 0
Multiplying 7 * 11
Lookup 0 * 1 = 0
Lookup 7 * 1 = 7
Lookup 7 * 2 = 14
Solved 07 * 11 = 77
Solved 70 * 92 = 6440
Solved 1357 * 2468 = 3349076
1357 * 2468 = 3349076
```

The first part of this program happens before `karatsuba()`

is called. Our program needs to create the multiplication lookup table in the `MULT_TABLE`

variable ❶. Normally, lookup tables are hardcoded directly in the source code, from `MULT_TABLE[[0, 0]] = 0`

to `MULT_TABLE[[9, 9]] = 81`

. But to reduce the amount of typing, we’ll use nested `for`

loops to generate each product. Accessing `MULT_TABLE[[m, n]]`

gives us the product of integers `m`

and `n`

.

Our `karatsuba()`

function also relies on a helper function named `padZeros()`

, which pads a string of digits with additional zeros on the left or right side of the string. This padding is done in the fifth step of the Karatsuba algorithm. For example, `padZeros("42", 3, "left")`

returns the string `00042`

, while `padZeros("99", 1, "right")`

returns the string `990`

.

The `karatsuba()`

function itself first checks for the base case, where `x`

and `y`

are single-digit numbers. These can be multiplied using the lookup table, and their product is immediately returned. Everything else is a recursive case.

We need to convert the `x`

and `y`

integers into strings and adjust them so that they contain the same number of digits. If one of these numbers is shorter than the other, zeros are padded to the left side. For example, if `x`

is `13`

and `y`

is `2468`

, our function calls `padZeros()`

so that `x`

can be replaced with `0013`

. This is required because we then create the `a`

, `b`

, `c`

, and `d`

variables to each contain one-half of the digits of `x`

and `y`

❷. The `a`

and `c`

variables must have the same number of digits for the Karatsuba algorithm to work, as do the `b`

and `d`

variables.

Note that we use division and rounding down to calculate how much is half of the digits of `x`

❸. These mathematical operations are as complicated as multiplication and might not be available to the low-level hardware we are programming the Karatsuba algorithm for. In a real implementation, we could use another lookup table for these values: `HALF_TABLE = [0, 0, 1, 1, 2, 2, 3, 3...]`

, and so on. Looking up `HALF_TABLE[n]`

would evaluate to half of `n`

, rounded down. An array of a mere 100 items would be sufficient for all but the most astronomical numbers and save our program from division and rounding. But our programs are for demonstration, so we’ll just use the `/`

operator and built-in rounding functions.

Once these variables are set up correctly, we can begin making the recursive function calls ❹. The first three steps involve recursive calls with arguments `a`

and `b`

, `c`

and `d`

, and finally `a + b`

and `c + d`

. The fourth step subtracts the results of the first three steps from each other ❺. The fifth step pads the results of the first and fourth steps with zeros on the right side, then adds them to the results of the second step ❻.

These steps may seem like magic, so let’s dive into the algebra that shows why they work. Let’s use 1,357 for *x* and 2,468 for *y* as the integers we want to multiply. Let’s also consider a new variable, *n*, for the number of digits in *x* or *y*. Since *a* is 13 and *b* is 57, we can calculate the original *x* as 10^{n}^{/2} × *a* + *b*, which is 10^{2} × 13 + 57 or 1,300 + 57, or 1,357. Similarly, *y* is the same as 10^{n}^{/2} × *c* + *d*.

This means that the product of *x* × *y* = (10^{n}^{/2} × *a* + *b*) × (10^{n}^{/2} × *c* + *d*). Doing a bit of algebra, we can rewrite this equation as *x* × *y* = 10*n* × *ac* + 10^{n}^{/2} × (*ad* + *bc*) + *bd*. With our example numbers, this means 1,357 × 2,468 = 10,000 × (13 × 24) + 100 × (13 × 68 + 57 × 24) + (57 × 68). Both sides of this equation evaluate to 3,349,076.

We’ve broken the multiplication of *xy* into the multiplications of *ac*, *ad*, *bc*, and *bd*. This forms the basis of our recursive function: we’ve defined the multiplication of *x* and *y* by using multiplication of smaller numbers (remember, *a*, *b*, *c*, and *d* are half the digits of *x* or *y*) that approach the base case of multiplying single-digit numbers. And we can perform single-digit multiplication with a lookup table rather than multiplying.

So we need to recursively compute *ac* (the first step of the Karatsuba algorithm) and *bd* (the second step). We also need to calculate (*a* + *b*)(*c* + *d*) for the third step, which we can rewrite as *ac* + *ad* + *bc* + *bd*. We already have *ac* and *bd* from the first two steps, so subtracting those gives us *ad* + *bc*. This means we need only one multiplication (and one recursive call) to calculate (*a* + *b*)(*c* + *d*) instead of two to calculate *ad* + *bc*. And *ad* + *bc* is needed for the 10*n*^{/2} × (*ad* + *bc*) part of our original equation.

Multiplying by the 10*n* and 10^{n}^{/2} powers of 10 can be done by padding zero digits: for example, 10,000 × 123 is 1,230,000. So, there’s no need to make recursive calls for those multiplications. In the end, multiplying *x* × *y* can be broken into multiplying three smaller products with three recursive calls: `karatsuba(a, c)`

, `karatsuba(b, d)`

, and `karatsuba((a + b), (c + d))`

.

With some careful study of this section, you can understand the algebra behind the Karatsuba algorithm. What I can’t understand is how Anatoly Karatsuba was clever enough to devise this algorithm in less than a week as a 23-year-old student in the first place.

Dividing problems into smaller, self-similar problems is at the heart of recursion, making these divide-and-conquer algorithms especially suited for recursive techniques. In this chapter, we created a divide-and-conquer version of Chapter 3’s program for summing numbers in an array. One benefit of this version is that upon dividing a problem into multiple subproblems, the subproblems can be farmed out to other computers to work on in parallel.

A binary search algorithm searches a sorted array by repeatedly narrowing the range to search in half. While a linear search starts searching at the beginning and searches the entire array, a binary search takes advantage of the array’s sorted order to home in on the item it is looking for. The performance improvement is so great that it may be worthwhile to sort an unsorted array in order to enable a binary search on its items.

We covered two popular sorting algorithms in this chapter: quicksort and merge sort. Quicksort divides an array into two partitions based on a pivot value. The algorithm then recursively partitions these two partitions, repeating the process until the partitions are the size of a single item. At this point, the partitions, and the items in them, are in sorted order. Merge sort takes an opposite approach. The algorithm splits the array into smaller arrays first, and then merges the smaller arrays into sorted order afterward.

Finally, we covered Karatsuba multiplication, a recursive algorithm for performing integer multiplication when the `*`

multiplication operator isn’t available. This comes up in low-level hardware programming that doesn’t offer a built-in multiplication instruction. The Karatsuba algorithm breaks down multiplying two integers into three multiplications of smaller integers. To multiply single-digit numbers for the base case, the algorithm stores every product from 0 × 0 to 9 × 9 in a lookup table.

The algorithms in this chapter are part of many data structure and algorithm courses that freshman computer science students take. In the next chapter, we’ll continue to look at other algorithms at the heart of computing with algorithms that calculate permutations and combinations.

The Computerphile channel on YouTube has videos on quicksort at https://youtu.be/XE4VP_8Y0BU and merge sort at https://youtu.be/kgBjXUE_Nwc. If you want a more comprehensive tutorial, the free “Algorithmic Toolbox” online course covers many of the same topics that a freshman data structures and algorithms course would cover, including binary search, quicksort, and merge sort. You can sign up for this Coursera course at https://www.coursera.org/learn/algorithmic-toolbox.

Sorting algorithms are often compared to each other in lessons on big O algorithm analysis, which you can read about in Chapter 13 of my book *Beyond the Basic Stuff with Python* (No Starch Press, 2020). You can read this chapter online at https://inventwithpython.com/beyond. Python developer Ned Batchelder describes big O and “how code slows as your data grows” in his 2018 PyCon talk of the same name at https://youtu.be/duvZ-2UK0fc.

Divide-and-conquer algorithms are useful because they often can be run on multiple computers in parallel. Guy Steele Jr. gives a Google TechTalk titled “Four Solutions to a Trivial Problem” on this topic at https://youtu.be/ftcIcn8AmSY.

Professor Tim Roughgarden produced a video lecture for Stanford University on Karatsuba multiplication at https://youtu.be/JCbZayFr9RE.

To help your understanding of quicksort and merge sort, obtain a pack of playing cards or simply write numbers on index cards and practice sorting them by hand according to the rules of these two algorithms. This offline approach can help you remember the pivot-and-partition of quicksort and the divide-merge of merge sort.

Test your comprehension by answering the following questions:

- What is a benefit of the divide-and-conquer summing algorithm in this chapter compared to the head-tail summing algorithm in Chapter 3?
- If a binary search of 50 books on a shelf takes six steps, how many steps would it take to search twice as many books?
- Can a binary search algorithm search an unsorted array?
- Is partitioning the same thing as sorting?
- What happens in quicksort’s partition step?
- What is the pivot value in quicksort?
- What is the base case of quicksort?
- How many recursive calls does the
`quicksort()`

function have? - How is the array
`[0, 3, 1, 2, 5, 4, 7, 6]`

not properly partitioned with a pivot value of`4`

? - What is the base case of merge sort?
- How many recursive calls does the
`mergeSort()`

function have? - What is the resultant array when the merge sort algorithm sorts the arrays
`[12, 37, 38, 41, 99]`

and`[2, 4, 14, 42]`

? - What is a lookup table?
- In the Karatsuba algorithm that multiplies integers
*x*and*y*, what do the*a*,*b*,*c*, and*d*variables store? - Answer the three questions about recursive solutions for each of the recursive algorithms presented in this chapter:
- What is the base case?
- What argument is passed to the recursive function call?
- How does this argument become closer to the base case?

Then re-create the recursive algorithms from this chapter without looking at the original code.

For practice, write a function for each of the following tasks:

- Create a version of the
`karatsuba()`

function that has a multiplication lookup table of products from 0 × 0 to 999 × 999 rather than 0 × 0 to 9 × 9. Get a rough estimate of how long it takes to calculate`karatsuba(12345678, 87654321)`

10,000 times in a loop with this larger lookup table compared to the original lookup table. If this still runs too quickly to measure, increase the number of iterations to 100,000 or 1,000,000 or more. (Hint: you should delete or comment out the`print()`

and`document.write()`

calls inside the`karatsuba()`

function for this timing test.) - Create a function that performs a linear search on a large array of integers 10,000 times. Get a rough estimate of how long this takes, increasing the number of iterations to 100,000 or 1,000,000 if the program executes too quickly. Compare this with how long a second function takes to sort the array once before performing the same number of binary searches.