Книга: Practical Programming, Fourth Edition
Назад: Chapter 12: Designing and Benchmarking Algorithms
Дальше: Timing the Functions

Searching for the Two Smallest Values

This section will explore how to find the index of the two smallest items in an unsorted list using three quite different algorithms. You’ll go through a top-down design using each approach. A natural assumption is that a list has at least two items.

To begin, suppose you have data showing the number of humpback whales sighted off the coast of British Columbia over the past decade:

809

834

477

478

307

122

96

102

324

476

The first value, 809, represents the number of sightings ten years ago; the last one, 476, represents the number of sightings last year.

Let’s start with a simpler problem: what is the smallest value during those years? This code tells you just that:

 >>>​​ ​​counts​​ ​​=​​ ​​[809,​​ ​​834,​​ ​​477,​​ ​​478,​​ ​​307,​​ ​​122,​​ ​​96,​​ ​​102,​​ ​​324,​​ ​​476]
 >>>​​ ​​min(counts)
 96

To determine the year when the population bottomed out, you can use list.index to find the index of the smallest value:

 >>>​​ ​​counts​​ ​​=​​ ​​[809,​​ ​​834,​​ ​​477,​​ ​​478,​​ ​​307,​​ ​​122,​​ ​​96,​​ ​​102,​​ ​​324,​​ ​​476]
 >>>​​ ​​low​​ ​​=​​ ​​min(counts)
 >>>​​ ​​counts.index(low)
 6

Or, more succinctly:

 >>>​​ ​​counts​​ ​​=​​ ​​[809,​​ ​​834,​​ ​​477,​​ ​​478,​​ ​​307,​​ ​​122,​​ ​​96,​​ ​​102,​​ ​​324,​​ ​​476]
 >>>​​ ​​counts.index(min(counts))
 6

Now, what if you want to find the indices of the two smallest values? Lists don’t have a direct method to do this, so you’ll need to design an algorithm yourself and then translate it into a Python function. Here is the header for a function that performs this task:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """

As you may recall from , the next step in the function design recipe is to write the function body.

There are at least three distinct algorithms, each of which will be subjected to top-down design. Let’s start by giving a high-level description of each. Each of these descriptions is the first step in doing a top-down design for that approach.

The first two algorithms mutate the list, either by removing an item or by sorting the list. Your algorithms must restore the mutated lists back to their original state, or the people who call your functions will be annoyed with you. The last two lines of the docstring check that the list isn’t mutated.

While you are investigating these algorithms in the next few pages, consider this question: Which one is the fastest?

Find, Remove, Find

Here is the algorithm again, rewritten with one instruction per line and explicitly discussing the parameter L:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 # Find the index of the minimum item in L
 # Remove that item from the list
 # Find the index of the new minimum item in the list
 # Put the smallest item back in the list
 # If necessary, adjust the second index
 # Return the two indices

To address the first step, find the index of the minimum item in L, skim the output produced by calling help(list) and find that there are no methods that do exactly that. Let’s refine it:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """ (see above) """
 
 # Get the minimum item in L <-- This line is new
 # Find the index of that minimum item <-- This line is new
 # Remove that item from the list
 # Find the index of the new minimum item in the list
 # Put the smallest item back in the list
 # If necessary, adjust the second index
 # Return the two indices

Those first two statements match Python functions and methods: min does the first, and list.index does the second. (There are other ways; for example, you could have written a loop to perform the search.)

You can see that list.remove implements the third statement, and the refinement of “Find the index of the new minimum item in the list” is also straightforward.

Notice that some of the English statements are left in as comments, which makes it easier to understand the problem that each chunk of code solves:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """ (see above) """
 
 # Find the index of the minimum and remove that item
  smallest = min(L)
  min1 = L.index(smallest)
  L.remove(smallest)
 
 # Find the index of the new minimum
  next_smallest = min(L)
  min2 = L.index(next_smallest)
 
 # Put the smallest item back in the list
 # If necessary, adjust the second index
 # Return the two indices

Since you removed the smallest item, you need to put it back where it was. Because removing a value affects the indices of the following values, you might need to add 1 to min2 if the smallest item came before the second-smallest item:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """ (see above) """
 
 # Find the index of the minimum and remove that item
  smallest = min(L)
  min1 = L.index(smallest)
  L.remove(smallest)
 
 # Find the index of the new minimum
  next_smallest = min(L)
  min2 = L.index(next_smallest)
 
 # Put smallest back into L
 # Fix min2 in case it was affected by the removal and reinsertion:
 # If min1 comes before min2, add 1 to min2
 # Return the two indices

That’s enough refinement (finally!) to do it all in Python:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 
 # Find the index of the minimum and remove that item
  smallest = min(L)
  min1 = L.index(smallest)
  L.remove(smallest)
 
 # Find the index of the new minimum
  next_smallest = min(L)
  min2 = L.index(next_smallest)
 
 # Put smallest back into L
  L.insert(min1, smallest)
 
 # Fix min2 in case it was affected by the removal and reinsertion:
 if​ min1 <= min2:
  min2 += 1
 
 return​ (min1, min2)

That seems like a lot of thought and care, and it is. However, even if you go right to code, you’ll have to think through all those steps. By writing them down first, you have a better chance of getting it right with minimal effort.

Sort, Identify Minimums, Get Indices

Here is the second algorithm rewritten with one instruction per line:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 
 # Sort a copy of L
 # Get the two smallest numbers
 # Find their indices in the original list L
 # Return the two indices

That looks straightforward; you can use the built-in function sorted, which returns a copy of the list with the items in order from smallest to largest. You could have used the list.sort method to sort L, but that breaks a fundamental rule: never mutate the contents of parameters unless the docstring explicitly states to do so.

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """ (see above) """
 
 # Get a sorted copy of the list so that the two smallest items are at the
 # front
  temp_list = sorted(L)
  smallest = temp_list[0]
  next_smallest = temp_list[1]
 
 # Find their indices in the original list L
 # Return the two indices

Now, you can find the indices and return them the same way you did in find-remove-find:

 def​ ​find_two_smallest​(L: list[float]) -> tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 
 # Get a sorted copy of the list so that the two smallest items are at the
 # front
  temp_list = sorted(L)
  smallest = temp_list[0]
  next_smallest = temp_list[1]
 
 # Find the indices in the original list L
  min1 = L.index(smallest)
  min2 = L.index(next_smallest)
 
 return​ (min1, min2)

Walk Through the List

Your last algorithm begins in the same way as the first two:

 from​ ​typing​ ​import​ List, Tuple
 
 def​ ​find_two_smallest​(L: List[float]) -> Tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 
 # Examine each value in the list in order
 # Keep track of the indices of the two smallest values found so far
 # Update the indices when a new smaller value is found
 # Return the two indices

Let’s move the second line before the first one because it describes the whole process; it isn’t a single step. Also, when you see phrases like each value, think of iteration; the third line is part of that iteration, so let’s indent it:

 def​ ​find_two_smallest​(L):
 """ (see above) """
 
 # Keep track of the indices of the two smallest values found so far
 # Examine each value in the list in order
 # Update the indices when a new smaller value is found
 # Return the two indices

Every loop has three parts: an initialization section to set up the variables you’ll need, a loop condition, and a loop body. Here, the initialization will set up min1 and min2, which will be the indices of the smallest two items encountered so far. A natural choice is to set them to the first two items of the list:

 def​ ​find_two_smallest​(L):
 """ (see above) """
 
 # Set min1 and min2 to the indices of the smallest and next-smallest
 # values at the beginning of L
 # Examine each value in the list in order
 # Update the indices when a new smaller value is found
 # Return the two indices

You can turn that first line into a couple of lines of code; let’s leave the English version as a comment:

 def​ ​find_two_smallest​(L):
 """ (see above) """
 
 # Set min1 and min2 to the indices of the smallest and next-smallest
 # Values at the beginning of L
 if​ L[0] < L[1]:
  min1, min2 = 0, 1
 else​:
  min1, min2 = 1, 0
 
 # Examine each value in the list in order
 # Update the indices when a new smaller value is found
 # Return the two indices

You now have a couple of choices. You can iterate with a for loop over the values, a for loop over the indices, or a while loop over the indices. Since you’re trying to find indices and you want to look at all of the items in the list, let’s use a for loop over the indices—and you’ll start at index 2 because you’ve examined the first two values already. At the same time, let’s refine the statement in the body of the loop to mention min1 and min2.

 def​ ​find_two_smallest​(L):
 """ (see above) """
 
 # Set min1 and min2 to the indices of the smallest and next-smallest
 # values at the beginning of L
 if​ L[0] < L[1]:
  min1, min2 = 0, 1
 else​:
  min1, min2 = 1, 0
 
 # Examine each value in the list in order
 for​ i ​in​ range(2, len(values)):
 # Update min1 and/or min2 when a new smaller value is found
 # Return the two indices

Now for the body of the loop. Let’s pick apart “update min1 and/or min2 when a new smaller value is found.” Here are the possibilities:

 def​ ​find_two_smallest​(L):
 """ (see above) """
 
 # Set min1 and min2 to the indices of the smallest and next-smallest
 # values at the beginning of L
 if​ L[0] < L[1]:
  min1, min2 = 0, 1
 else​:
  min1, min2 = 1, 0
 
 # Examine each value in the list in order
 for​ i ​in​ range(2, len(L)):
 # L[i] is smaller than both min1 and min2, in between, or
 # larger than both:
 # If L[i] is smaller than min1 and min2, update them both
 # If L[i] is in between, update min2
 # If L[i] is larger than both min1 and min2, skip it
 return​ (min1, min2)

All of those are easily translated to Python; in fact, you don’t even need code for the “larger than both” case:

 from​ ​typing​ ​import​ List, Tuple
 
 def​ ​find_two_smallest​(L: List[float]) -> Tuple[int, int]:
 """Return a tuple of the indices of the two smallest values in list L.
 
  >>> items = [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  >>> find_two_smallest(items)
  (6, 7)
  >>> items == [809, 834, 477, 478, 307, 122, 96, 102, 324, 476]
  True
  """
 
 # Set min1 and min2 to the indices of the smallest and next-smallest
 # values at the beginning of L
 if​ L[0] < L[1]:
  min1, min2 = 0, 1
 else​:
  min1, min2 = 1, 0
 
 # Examine each value in the list in order
 for​ i ​in​ range(2, len(L)):
 # L[i] is smaller than both min1 and min2, in between, or
 # larger than both
 
 # New smallest?
 if​ L[i] < L[min1]:
  min2 = min1
  min1 = i
 # New second smallest?
 elif​ L[i] < L[min2]:
  min2 = i
 
 return​ (min1, min2)
Назад: Chapter 12: Designing and Benchmarking Algorithms
Дальше: Timing the Functions