CS 261 Lab #8

March 21st

Goals:

This week you'll get a chance to play around with the searching code we wrote in class. You'll trace the operation of Binary Search to drive home how it works, and run some experiments to measure the number of computational steps required to solve search problems of varying size. You'll then work on some related methods that will come in handy on the current assignment.

Partners

In each lab this semester you will work with a randomly assigned partner. (I'll have Zoom randomly set up breakout rooms.) Please be kind in your interactions with your partners! Keep in mind that students in this class have a range of previous programming experience, and that some have been college students for longer than others. We're all in this together, and you have something to learn from your partner, no matter who they are or what their previous experiences have been. I expect that group members will collaborate and work together on each step of the lab.

Getting Started

  1. Take a moment to introduce yourself to your partner(s). After social pleasantries are complete, pick one member of the team to be the "typer". They'll share their screen while editing the lab code in BlueJ. (I think BlueJ works better for these interactions than Eclipse. It's easier to see when sharing screens, and makes it easier to quickly test individual methods than Eclipse does.) Group members should contribute equally while working through the problems below and discuss all code to be written, though only the "typer" will be able to edit code. Resist the temptation to have both members work simultaneously in BlueJ — you're much more likely to "drift apart" over the course of the lab if you do so. The goal here is to have a partner who's engaged on exactly the same step of the lab as you are.

    Consider letting the less experienced member of the group do the typing if it seems like there's a mismatch in experience or comfort levels. That will help make sure they don't get left behind. Worst case, flip a coin to see who does the typing. Or have Java generate a random boolean value. (Type "(new java.util.Random()).nextBoolean()" in the codepad without the double quotes.)

    At the conclusion of the lab, make arrangements for the typer to share a copy of the code with the other member(s) of the group if desired. (E.g. email it, or put it on a shared Google drive, etc.) My solutions to the lab will get posted as well.

  2. Download the SearchingLab project and extract its contents, then start BlueJ and open the project. In the project you'll find the Searches and SearchTester classes. Our recursive searching methods from class are in Searches, and there's some code to help test out those search methods in the SearchTester class.

Phase I: Comprehending Binary Search

  1. Open up the Searches class and find the binarySearch method. Take a moment to review the code and figure out how it works: Look at the parameters and think about how they're used. Find the base case and think about when it applies and what it's returning. Consider the recursive case(s). Why are there two recursive calls? Might they both execute, or just one or the other? If the latter, how are we deciding which to use?
  2. Now that the details of Binary Search are fresh in your head, trace through an execution as it searches for 22 in an array containing [10,12,14,16,18,20,22,24,26,28,30]. Do not actually run it yet. Instead, use a piece of scratch paper or an online tool to help keep track of the recursive calls as you simulate its execution. Make sure you write down how left and right are changing as we make each recursive call, and think about how we "unwind" all of those recursive calls after hitting the base case.
  3. To test your work from the previous problem, add a print statement at the top of binarySearch that displays the values of left and right. If you're feeling ambitious, you could also put print statements above each of the return statements, making it clear which return statement is about to be executed. (It might make sense to print out left and right for these as well, so you can more easily see which recursive call is about to return.) Once you've got print statements in your binarySearch method, run the main method in SearchTester. It will build that same array and search for 22 so you can see the output. Compare the actual behavior to your predictions, and ask for help if you have any questions before proceeding.

Phase II: Measuring Binary Search's Performance

  1. We can estimate the number of computational steps required for a particular binary search problem by counting the number of comparisons it performs as it runs. I've added a counter at the top of the Searches class, and methods for accessing that count. Your next task is to modify binarySearch so it counts the number of comparisons performed during a search. Make sure you increment the counter for every comparison between data values! (Don't bother counting how many comparisons get done as part of the test at the top of the loop — we're only interested in how many times we inspect data in the array.) You can ignore the commented-out lines for now, but later it might be interesting to add them back and see how that changes things. Verify that your counter is getting updated as expected before proceeding.
  2. If you test binarySearch via the codepad, you'll see that there's a bit of variation in the number of comparisons for searches on a given array, depending upon which values we search for, but the variation is small enough we can ignore it for now. As an "extra" at the end of the lab, you could get a more precise estimate by running a handful of searches and averaging the number of comparisons required across the set of searches. Below, notice that sometimes it takes 3 comparisons and sometimes 4:

    > int[] nums = {2,5,6,8,9,11,15,17,18,20,21,23};
    > Searches s = new Searches();
    > s.getCount()
       0   (int)
    > s.binarySearch(nums, 23, 0, 12)
       true   (boolean)
    > s.getCount()
       4   (int)
    > s.clearCount();
    > s.binarySearch(nums, 8, 0, 12)
       true   (boolean)
    > s.getCount()
       3   (int)
    > s.clearCount();
    > s.binarySearch(nums, 200, 0, 12)
       false   (boolean)
    > s.getCount()
       4   (int)
    

  3. What we'd really like to see is how the number of comparisons scales with the size of the array. Being computer scientists, we'll write code to automate this data collection to speed things up. Your job is to run tests on arrays of various sizes, then graph the results (with #comparisons on the vertical axis and array size on the horizontal axis).

    Finish defining the measureScaling method in the SearchTester class. At the moment, it sets up a search over an array of size 100 and then prints the results. (It's printing the array size and the number of comparisons separated by a tab character, so that later it'll be easy to copy our results and paste them into a Google sheet.) Put a loop around that search code so that the problem size, n, is being increased each time through the loop. The number of comparisons changes pretty quickly at first, and then gets more stable. In my code I did searches up to size 1000 with a step size of 2, and then switched to a second loop that did searches from 2000 to 1,000,000 with a step size of 1000, but you're welcome to take other approaches. Try to measure up to some pretty large array sizes though.

  4. For the full impact, you should graph these results: Enter your data in Excel or a GoogleDocs spreadsheet, and create a chart from the data. (You can just paste the output from the terminal window and paste it into your sheet, and it should automatically put the size in one column the number of comparisons in another.) If you're new to Google Docs, you can follow these instructions to create a chart.

Phase III: Making Use of Binary Search

Your current assignment is asking you to use Binary Search to look for values in lists (words in a list of previously seen words), but also to use Binary Search to help add items into a list such that the list is kept in order. This final phase of the lab will help demonstrate how our search routine can also help us add items in the right place.
  1. Open the Searches class and find the findPosition method. Notice that the signature differs from binarySearch. For one thing, it takes a list of integers rather than an array. (This makes it more like your assignment, where you'll be working with lists instead of arrays.) It also returns an int instead of a boolean value. The plan here is that we'll return the index of the "final box" we end up on after doing a binary search, but that we won't actually check the contents of that box. This makes the method potentially more useful than binarySearch — we could still use it to narrow down the search to a single box and then do the final check outside of findPosition, but we could also use the information it returns to insert at that position.
  2. Copy the code from binarySearch into the body of findPosition as a starting point, then edit it so that all of the array references are instead using the appropriate list operations. Then think about how to modify the code so that it returns the index of the "final box" when the search is complete. Test your code before proceeding. In the interactions below, for example, I'm verifying that findPostion returns the proper index for each of the values in the array. But it returns useful information for values that aren't in the array as well. For example, when I asked where the 9 was in [10,12,14], it reported that the 9 would be at position 0 in the array if it were there.

    > import java.util.ArrayList;
    > ArrayList numsList = new ArrayList();
    > numsList.add(10);
    > numsList.add(12);
    > numsList.add(14);
    > numsList.toString()
       "[10, 12, 14]"   (String)
    > Searches s = new Searches();
    > s.findPosition(numsList, 10, 0, numsList.size())
       0   (int)
    > s.findPosition(numsList, 12, 0, numsList.size())
       1   (int)
    > s.findPosition(numsList, 14, 0, numsList.size())
       2   (int)
    > s.findPosition(numsList, 9, 0, numsList.size())
       0   (int)
    > s.findPosition(numsList, 13, 0, numsList.size())
       1   (int)
    > s.findPosition(numsList, 27, 0, numsList.size())
       2   (int)
    
  3. Finally, let's finish the definition of the insert method. It takes an ordered list of values, and a new value to be inserted, and inserts the value such that the list remains ordered. You can call findPosition to see where a value would be if it were present. If it's not already at that position, you can pretty much just do an add at that position to insert the item. It's not quite that simple though. Notice in the interactions above that findPosition returned a 0 for 9. If we inserted 9 at position 0 the other values would shift down and we'd have an ordered array. It returned a 1 for 13 though. What would the list look like if we inserted 13 at position 1? (Hint: It wouldn't be ordered.) The findPosition result gets us close to the right insertion point, but you still need to do a bit more work to figure out exactly where to insert. You can uncomment the block of code at the bottom of the main method to test your insert method once it's complete.

Extensions

Looking for an extra challenge?


Brad Richards, 2023