Module 4: Supplemental Material


Binary search

First, let's consider a simple search in an array:

public class SimpleSearch {

    public static void main (String[] argv)
    {
        // Fill an array with some random values - for testing.
        int[] testData = {51, 24, 63, 73, 42, 85, 71, 41, 87, 32};

        // Create a test value to search for.
        int searchTerm = 32;
        
        boolean found = search (testData, searchTerm);

        // Print result.
        System.out.println ("found = " + true);
    }


    static boolean search (int[] A, int value)
    {
        for (int i=0; i < A.length; i++) {
            if (A[i] == value) {
                return true;
            }
        }
        return false;
    }

}
Note:

Now suppose the array were sorted:

  [24, 32, 41, 42, 51, 63, 71, 73, 85, 87]

The key ideas in binary search:

Now let's examine the program:
public class BinarySearch {

    public static void main (String[] argv)
    {
        // Sorted array:
        int[] testData = {24, 32, 41, 42, 51, 63, 71, 73, 85, 87};

        // Create a test value to search for.
        int searchTerm = 32;
        
        // Note: initially, the "range" is 0,...,testData.length-1
        boolean found = binarySearch (testData, searchTerm, 0, testData.length-1);

        // Print result.
        System.out.println ("found = " + found);

        // Another search.
        searchTerm = 55;
        found = binarySearch (testData, searchTerm, 0, testData.length-1);
        System.out.println ("found = " + found);
    }


    static boolean binarySearch (int[] A, int value, int start, int end)
    {
        // Bottom-out cases:
        if (start == end) {
            // There's only one value to check: A[start]=value?
            if (A[start] == value) {
                return true;
            }
            else {
                return false;
            }
        }
        
        // If there are only two values in the range:
        if (end == start+1) {
            if (A[start] == value) {
                return true;
            }
            else if (A[end] == value) {
                return true;
            }
            else {
                return false;
            }
        }

        // Otherwise recurse. We know that A[start] <= value <= A[end].

        // Find the middle:
        int mid = (start + end) / 2;

        if (value <= A[mid]) {
            // Search the left half: A[start],...,A[mid]
            return binarySearch (A, value, start, mid);
        }
        else {
            // Search the right half: A[mid+1],...,A[end]
            return binarySearch (A, value, mid+1, end);
        }
    }

}
Note:

Exercise 1: Implement and modify the above the program to print out something before every return statement in binarySearch(). This allows you to see what's going on. Now consider the following problem:

Does the code break down if you present it with a value outside the range? Try it out and see what happens. Then explain what you see.

Here's an interesting variation:

public class BinarySearch2 {

    public static void main (String[] argv)
    {
        // Fill an array with some random values - for testing.
        int[] testData = {24, 32, 41, 42, 51, 63, 71, 73, 85, 87};

        // Create a test value to search for.
        int searchTerm = 32;
        
        // Note: initially, the "range" is 0,...,testData.length-1
        boolean found = binarySearch (testData, searchTerm, 0, testData.length-1);

        // Print result.
        System.out.println ("found = " + found);

        // Another search.
        searchTerm = 55;
        found = binarySearch (testData, searchTerm, 0, testData.length-1);
        System.out.println ("found = " + found);
    }


    static boolean binarySearch (int[] A, int value, int start, int end)
    {
        // Only need to check if the interval got inverted.
        if (start > end) {
            return false;
        }
        
        // Find the middle:
        int mid = (start + end) / 2;

        if (A[mid] == value) {
            return true;
        }
        else if (value <= A[mid]) {
            // Search the left half: A[start],...,A[mid-1]
            return binarySearch (A, value, start, mid-1);
        }
        else {
            // Search the right half: A[mid+1],...,A[end]
            return binarySearch (A, value, mid+1, end);
        }
    }

}
Note:

Exercise 2: Download SearchComparison.java and UniformRandom.java and compare Binary Search to simple search. The code you downloaded contains both search methods. What you need to do is to add code to count the number of comparisons made by each method. Try this for array sizes of 10, 100, 1000 and 10000.

Finally, let's examine a non-recursive (iterative) version of Binary Search:


public class BinarySearch3 {

    public static void main (String[] argv)
    {
        // Fill an array with some random values - for testing.
        int[] testData = {24, 32, 41, 42, 51, 63, 71, 73, 85, 87};

        // Create a test value to search for.
        int searchTerm = 32;
        
        // Note: initially, the "range" is 0,...,testData.length-1
        boolean found = binarySearch (testData, searchTerm);

        // Print result.
        System.out.println ("found = " + found);

        // Another search.
        searchTerm = 55;
        found = binarySearch (testData, searchTerm);
        System.out.println ("found = " + found);
    }


    static boolean binarySearch (int[] A, int value)
    {
        // Instead of calling the method recursively, we can also
        // set start and end initially, and simply adjust the values
        // in a loop.
        int start = 0;
        int end = A.length - 1;

        while (start <= end) {

            int mid = (start + end) / 2;

            if (value == A[mid]) {
                return true;
            }
            else if (value < A[mid]) {
                end = mid-1;
            }
            else {
                start = mid+1;
            }

        } // end-while
        
        return false;
    }

}
Note:


The power example revisited

We will revisit the power examples from Module 4:

To handle arbitrarily large integers, Java provides the BigInteger class:

Let's look at an example:
import java.math.*;    // We need to import from this library.

public class BigIntegerExample {

    public static void main (String[] argv)
    {
        // Create two instances with 5 and 2 as actual values.
        BigInteger A = new BigInteger ("5");
        BigInteger B = new BigInteger ("2");

        // Examples of arithmetic operations:
        BigInteger C = A.add (B);               // 7
        BigInteger D = A.multiply (B);          // 10
        BigInteger E = A.divide (B);            // 2
        BigInteger F = A.mod (B);               // 1
        
        System.out.println ("A=" + A + " B=" + B + " C=" + C + " D=" + D + " E=" + E + " F=" + F);

        // Two constants.
        BigInteger zero = new BigInteger ("0");
        BigInteger one = new BigInteger ("1");

        BigInteger G = A.add (zero);            // 5
        BigInteger H = B.multiply (one);        // 2
        
        System.out.println ("G=" + G + " H=" + H);

        // Largest possible int.
        int x = 2147483647;
        int y = x*x;          // "Garbage" result: it won't work.
        System.out.println ("x=" + x + " y=" + y);
        
        // But this works fine:
        BigInteger X = new BigInteger ("2147483647");
        BigInteger Y = X.multiply (X);
        System.out.println ("X=" + X + " Y=" + Y);

        // And now for some really large integers ...

        // Some of the largest known primes in history, found without computers.
        // Largest known in 1588, by P.Cataldi:
        BigInteger P1 = new BigInteger ("524287");       
        // Next record in 1772, by Euler:
        BigInteger P2 = new BigInteger ("2147483647");   
        // Then in 1876 by Lucas:
        BigInteger P3 = new BigInteger ("170141183460469231731687303715884105727");
        // Next record in 1951 by Ferrier:
        BigInteger P4 = new BigInteger ("20988936657440586486151264256610222593863921");
    }
    
}

Next, let's re-do our power example using BigInteger's:

import java.math.*;

public class BigPower {

    public static void main (String[] argv)
    {
        // The first two examples, of course, can be done with int's.
        BigInteger X = new BigInteger ("3");
        BigInteger Y = new BigInteger ("2");
        BigInteger Z = power (X, Y);
        System.out.println (X + "^" + Y + " = " + Z);

        X = new BigInteger ("2");
        Y = new BigInteger ("8");
        Z = power (X, Y);
        System.out.println (X + "^" + Y + " = " + Z);

        // But this one cannot be computed using int's.
        X = new BigInteger ("2");
        Y = new BigInteger ("1000");
        Z = power (X, Y);
        System.out.println (X + "^" + Y + " = " + Z);
    }


    // Some constants that will be useful below.
    static BigInteger zero = new BigInteger ("0");
    static BigInteger one = new BigInteger ("1");


    static BigInteger power (BigInteger A, BigInteger B)
    {
        if ( B.equals(zero) ) {
            return new BigInteger ("1");
        }

        // Recurse.

        BigInteger BMinus1 = B.subtract (one);
        BigInteger temp = power (A, BMinus1);
        BigInteger P = A.multiply (temp);

        // Thus, P = A*power(A, B-1)

        return P;
    }
    
}

Exercise 3: Recall how compactly the power() method was written for the case where we had int's:

    static int power (int a, int b)
    {
        if (b == 0) {
            return 1;
        }
        return (a * power (a, b-1));
    }
Modify the BigInteger example above and do the same. The result is probably less readable, but it's a good exercise to see how it can be done.


Recursion and fractals

Next, let's look at an interesting use of recursion in drawing a fractal:

To implement:

  • We will simply recurse downwards, changing the length of the "fractal side" to be drawn

  • This sounds simpler than it actually is. There are some complexities:
    • We will keep track of which segment we are drawing relative to the previously drawn segment.
    • To do this, we will compute the change in angle going from one segment to another.
Here's the program:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

class SnowflakePanel extends JPanel {

    // These values will be used in the recursion.
    int depth = 0;
    double currentAngle;

    // The end points of the current "fractal edge" 
    Point currentPoint, nextPoint;
    

    // paintComponent is called whenever the screen needs to be rendered.
    // This is where we do all our drawing work.

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);
        
        if (depth == 0) {
            // We actually need this because there is nothing to draw
            // when the screen first comes up.
            return;
        }
        
        // Create a grey background and white pen color.
        Dimension D = this.getSize();
        g.setColor (Color.white);
        g.fillRect (0,0, D.width, D.height);
        g.setColor (Color.black);

        // Now figure out an appropriate size for the length of
        // the side of the initial triangle.
        int minDim = D.width;
        if (D.height < minDim) {
            minDim = D.height;
        }
        int leftInset = (int) (0.2 * minDim);
        double initLength = (int) (0.6 * minDim);

        // The first point is a little inside the origin.
        currentPoint = new Point (leftInset, leftInset);

        // Start with initial angle horizontal.
        currentAngle = 0;

        // Draw three "fractal sides" next.

        // The first one.
        drawSide (initLength, depth, g);
        changeAngleRight (120);              
        // After drawing the base, the next line comes at an angle of 120.

        // Now the second one, after which there's another 120 rotation.
        drawSide (initLength, depth, g);
        changeAngleRight (120);

        // Last one.
        drawSide (initLength, depth, g);
    }

    
    void drawSide (double length, int depth, Graphics g)
    {
        if (length <= 1) {
            // Too small: we can't draw something smaller than a pixel.
            System.out.println ("Length too small: depth too much");
            return;
        }
        
        // Base case:
        if (depth == 1) {
            // Compute next point by turning the angle and using the angle
            // to compute the (x,y) values of the end-point.
            nextPoint = new Point ();
            double radians = currentAngle*Math.PI/180.0;
            nextPoint.x = currentPoint.x + (int) (Math.cos(radians) * length);
            nextPoint.y = currentPoint.y + (int) (Math.sin(radians) * length);

            // Draw.
            Dimension D = this.getSize();
            g.drawLine (currentPoint.x, D.height-currentPoint.y, nextPoint.x, D.height-nextPoint.y);

            // Update.
            currentPoint = nextPoint;
            return;
        }

        // Recursive case: draw FOUR sides fractally. After each, the angle
        // needs to be changed to compute the new end-points.
        drawSide (length/3, depth-1, g);
        changeAngleLeft (60);

        drawSide (length/3, depth-1, g);
        changeAngleRight (120);

        drawSide (length/3, depth-1, g);
        changeAngleLeft (60);

        drawSide (length/3, depth-1, g);
    }


    // Methods to set the angle.

    void changeAngleRight (double increment)
    {
        currentAngle = currentAngle + increment;
    }

    void changeAngleLeft (double increment)
    {
        currentAngle = currentAngle - increment;
    }

} //end-SnowflakePanel



// The remainder of the code is entirely GUI-related.

class SnowflakeFrame extends JFrame {

    // A text field where the user enters the fractal depth.
    JTextField depthField;

    // The panel on which we draw.
    SnowflakePanel drawPanel;

    public SnowflakeFrame ()
    {
        this.setSize (500,500);
        this.setTitle ("Von Koch Snowflake");
        this.setResizable (true);

        // This is how stuff is put into a frame.
	Container cPane = this.getContentPane();
        drawPanel = new SnowflakePanel ();
        cPane.add (drawPanel, BorderLayout.CENTER);
        
        // Make the controls.
        JPanel panel = new JPanel ();
        JLabel label = new JLabel ("Enter depth: ");
        panel.add (label);
        depthField = new JTextField (5);
        panel.add (depthField);
        JButton button = new JButton ("Go");
	button.addActionListener (
	  new ActionListener () {
	      public void actionPerformed (ActionEvent a)
	      {
		  handleButtonClick();
	      }
	  }
        );
        panel.add (button);
        cPane.add (panel, BorderLayout.SOUTH);
        
        this.setVisible (true);
    }

    void handleButtonClick ()
    {
        // Extract the string from the textfield where the user typed the strings.
        String inputStr = depthField.getText ();
        try {
            int d = Integer.parseInt (inputStr.trim());
            drawPanel.depth = d;
            drawPanel.repaint();
        }
        catch (NumberFormatException e) {
            System.out.println (e);
        }
        
    }

}



public class Snowflake {

    public static void main (String[] argv)
    {
        SnowflakeFrame s = new SnowflakeFrame ();
    }

}

Exercise 4: Download and execute the above program to see how it works.


© 2006-2020, Rahul Simha & James Taylor (revised 2020)