Unit 4 notes · 17 topics, read straight through

Unit 4: Data Collections

Every topic in one read: the big idea, the notes, the Java examples and the exam traps. Practice comes after, on each topic's checkpoint.

4.1 Ethical and Social Issues Around Data Collection

Big ideaGood code with bad data still gives bad answers, and personal data always needs protecting.

1. Personal data is a privacy risk

Every time someone uses a computer, an app or a website, personal data can be collected: names, locations, contacts, search history, health or school records. Once this data is stored on a computer system, the person’s privacy is at risk 4.1.A.1. Stored data can be stolen in a data breach, shared with other companies, or combined with other data to reveal more than the user ever agreed to share.

Programmers make choices that decide how big that risk is. When you develop a new program, you should try to safeguard the personal privacy of the user 4.1.A.1:

  • Collect only what you need. A fitness app that counts steps does not need the user’s contact list.
  • Protect what you store. Keep personal data away from places where other users or programs can read it.
  • Keep it only as long as needed. Old data that is deleted cannot leak.
  • Be honest with users. Tell them what is collected and why.

Watch out: “The data is only stored, not shared” does not make it safe. Storage itself is the risk.

2. Algorithmic bias

Algorithmic bias describes systemic and repeated errors in a program that create unfair outcomes for a specific group of users 4.1.B.1. “Systemic” means the error comes from how the program or its data is built, so it happens again and again to the same group.

Example: a face-unlock feature is trained mostly on photos of adults. It works well for adults but often fails for children. The program has no single “bug” line, yet it keeps treating one group of users unfairly. The cause is the data used to build it.

Feature One random error Algorithmic bias
How often? Once, by chance Repeated
Who is affected? Anyone A specific group of users
Where it comes from A slip in one run The design or the data (systemic)

3. How was the data collected?

Before you use a data set to extrapolate new information or draw conclusions, you should know how the data was collected and whether that method could bring bias 4.1.B.2.

A school wants to know how students travel to school. Compare two collection methods:

Collection method Possible bias
An online form shared only in the cycling club group chat Cyclists are over-represented, so “most students cycle” would be a biased conclusion.
A short question asked to every student at the start of first lesson Everyone has the same chance to answer, so the result is much more representative.

Every answer in the first data set may be true, but the method still produces a biased picture.

4. Incomplete and inaccurate data

Some data sets are incomplete (values are missing) or inaccurate (values are wrong). If a program is developed or used with such data, it can work incorrectly or inefficiently 4.1.B.3.

In this example, a weather station failed on one day and stored -999.0 as a “missing” marker. The program averages the readings without knowing that.

double day1 = 21.5;
double day2 = 23.0;
double day3 = -999.0;   // sensor failed; this is not a real temperature
double day4 = 22.5;

double average = (day1 + day2 + day3 + day4) / 4;
System.out.println("Average temperature: " + average);

Output:

Average temperature: -233.0

The algorithm is correct, but the inaccurate data makes the answer useless. A program can also become inefficient with bad data, for example by processing thousands of duplicate records.

Watch out: when a question describes missing values, typing errors or duplicate records, the expected answer is usually that the program may produce incorrect results.

5. Choosing the right data set

The contents of a data set are usually related to a specific question or topic. A data set collected for one question might not give correct answers, or support extrapolation, for a different question 4.1.C.1.

Question: “Which hour of the day is busiest at a city library?”

Data set Appropriate? Why
Library entrance gate counts, by hour, for the last year Yes It measures visits per hour at that library.
Number of books borrowed per month No Monthly totals cannot show hours, and borrowing is not the same as visiting.
Gate counts, by hour, from a different city’s library No It is about a different library, so it cannot answer the question for this one.
A survey asking residents their favorite book No It is about a different topic.

To choose, check that the data set matches the question’s topic, place (population) and time period, and that it measures the thing the question asks about.

Exam traps

  • Algorithmic bias is about unfair outcomes for a specific group, not about a single random bug.
  • A survey answered only by volunteers, or only by one group, has a biased collection method even if every answer is accurate.
  • A question asks for the most appropriate data set: pick the one that matches the exact question, population and time period, not the biggest one.

Simulator and checkpoint for 4.1 →

4.2 Introduction to Using Data Sets

Big ideaEvery data set question is answered the same way: visit each value once and update a running result.

1. What a data set is

A data set is a collection of specific pieces of information, or data 4.2.A.1. You meet data sets everywhere:

Everyday data set The pieces of data
A week of temperatures 18.5, 21.0, 19.5, …
A class’s quiz scores 7, 9, 10, 6, …
A playlist song titles
A step counter steps per day

2. Processing values one at a time

You can manipulate and analyze a data set to solve a problem or answer a question 4.2.A.2. The key pattern: the values are accessed and used one at a time, and each one is processed according to the outcome you want 4.2.A.2.

Problem: “On how many days this week did a runner run more than 5 km?” Written in plain language, the algorithm is:

  1. Set a counter to 0.
  2. Look at the next day’s distance.
  3. If that distance is greater than 5, add 1 to the counter.
  4. Repeat steps 2 and 3 until every day has been looked at.
  5. The counter is the answer.

This “visit each value and update a running result” pattern is called a traversal in code, and it is the heart of the rest of Unit 4.

3. Plan with a table or chart

Data can be represented in a diagram such as a chart or table, and that visual can be used to plan the algorithm 4.2.A.3. Here is the runner’s week as a table, with an extra row that traces the counter after each value is processed:

Day Mon Tue Wed Thu Fri Sat Sun
Distance (km) 4.0 6.5 5.0 8.0 3.5 10.0 5.5
Greater than 5? no yes no yes no yes yes
Counter after this day 0 1 1 2 2 3 4

The trace shows the answer is 4, and it also catches a common trap: 5.0 is not greater than 5, so Wednesday does not count.

A chart works too. A bar chart of the distances with a line drawn at 5 km lets you count the bars above the line, which is exactly what the algorithm does one bar at a time.

4. From plan to code (preview)

Once the table works on paper, each written step becomes a line of code. Topic 4.3 shows how to store the data in an array; this preview shows how closely the code follows the plan.

double[] distances = {4.0, 6.5, 5.0, 8.0, 3.5, 10.0, 5.5};
int count = 0;                          // step 1
for (double d : distances)              // steps 2 and 4: each value, one at a time
{
    if (d > 5)                          // step 3
    {
        count++;
    }
}
System.out.println("Days over 5 km: " + count);   // step 5

Output:

Days over 5 km: 4

Watch out: the trace table is also your test. If the code prints something different from the last cell of your trace, either the plan or the code has a mistake.

Exam traps

  • When you plan a running result, choose the starting value carefully: 0 for a sum or count, the first value (not 0) for a maximum or minimum.
  • Read what the question wants returned: a count, a total, a single value or a true or false answer.

Simulator and checkpoint for 4.2 →

4.3 Array Creation and Access

Big ideaAn array is a fixed row of numbered boxes: same type in every box, numbered from 0 to length - 1.

1. What an array is

An array stores multiple values of the same type 4.3.A.1. Instead of six separate variables for six scores, you use one array variable with six elements. The values can be primitive values (such as int, double or boolean) or object references (such as String objects) 4.3.A.1.

The type of an array is written with square brackets after the element type:

int[] goals;          // an array of int values
double[] prices;      // an array of double values
String[] players;     // an array of String references

Declaring the variable does not create the array yet. An array is an object, so it is created with new or with an initializer list (sections 2 and 3). This page is about one-dimensional (1D) arrays: a single row of elements.

Watch out: every element must match the declared type.

int[] scores = {90, 85.5, 77};

Compiler error:

error: incompatible types: possible lossy conversion from double to int

2. Creating an array with new

When you create an array with the keyword new, you give its length, and every element is set to the default value for its type 4.3.A.3:

Element type Default value
int 0
double 0.0
boolean false
any reference type (String, a class) null
int[] goals = new int[4];
double[] prices = new double[3];
boolean[] present = new boolean[2];
String[] players = new String[3];

System.out.println(goals[0] + " " + prices[0] + " " + present[0] + " " + players[0]);
System.out.println("goals has " + goals.length + " elements");

Output:

0 0.0 false null
goals has 4 elements

The length of an array is established when it is created and cannot be changed 4.3.A.2. You read it with the length attribute 4.3.A.2. Notice: no parentheses. length is an attribute of the array, not a method.

int[] goals = new int[4];
int n = goals.length();

Compiler error:

error: cannot find symbol

And because the length cannot change, you cannot assign to it:

int[] goals = new int[4];
goals.length = 10;

Compiler error:

error: cannot assign a value to final variable length

3. Initializer lists

An initializer list creates an array and fills it with the values you list, all in one step 4.3.A.4. The length is the number of values in the list.

String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri"};
int[] laps = {62, 58, 60};

System.out.println(days.length);
System.out.println(laps.length);

Output:

5
3

Here is how days looks in memory. The index row is what you use to reach each element.

index 0 1 2 3 4
days “Mon” “Tue” “Wed” “Thu” “Fri”

4. Accessing and modifying elements

Square brackets [] with an index access an element, and the same brackets on the left of = modify it 4.3.A.5.

int[] scores = {7, 4, 9, 6};

System.out.println(scores[2]);     // read index 2

scores[1] = 10;                    // change index 1
scores[3] = scores[3] + 2;         // read index 3, add 2, store it back
scores[0]++;                       // works like any int variable

System.out.println(scores[0] + " " + scores[1] + " " + scores[2] + " " + scores[3]);

Output:

9
8 10 9 8
index 0 1 2 3
before 7 4 9 6
after 8 10 9 8

The index can be any int expression, such as a variable or scores.length - 1:

int[] scores = {7, 4, 9, 6};
int i = 1;
System.out.println(scores[i + 1]);
System.out.println(scores[scores.length - 1]);   // always the last element

Output:

9
6

5. Valid indexes and ArrayIndexOutOfBoundsException

The valid index values for an array are 0 through one less than the length of the array, inclusive 4.3.A.6. For an array of length 4, that means 0, 1, 2 and 3. Using any index outside that range results in an ArrayIndexOutOfBoundsException 4.3.A.6.

The code compiles, because the compiler does not check index values. The error happens at run time, and the program stops.

int[] scores = {7, 4, 9, 6};
System.out.println(scores[3]);
System.out.println(scores[4]);
System.out.println("done");

Output:

6
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

Real view: this is the first line the console shows. “done” is never printed, because the exception stops the program. A negative index such as scores[-1] causes the same exception.

Watch out: scores[scores.length] is always out of bounds. The last element is scores[scores.length - 1].

6. Arrays of object references

An array of a reference type stores references, not the objects themselves 4.3.A.1. After new, every element is null, so you must assign objects before calling methods on them.

String[] team = new String[3];
team[0] = "Ana";
team[2] = "Leo";

System.out.println(team[0].length());
System.out.println(team[1]);
System.out.println(team[1].length());

Output:

3
null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "team[1]" is null
index 0 1 2
team “Ana” null “Leo”

Printing a null element is fine; it prints null. Calling a method on it, such as team[1].length(), throws a NullPointerException. (The exact wording after the colon depends on your Java version and settings; on the AP® exam you only need the exception name.)

Exam traps

  • arr[arr.length] is always out of bounds; the last element is arr[arr.length - 1].
  • arr.length() does not compile for an array; length has no parentheses.
  • new double[3] holds 0.0 values and new boolean[3] holds false values, not 0 or null.
  • An out-of-bounds index compiles fine; the error only appears when the code runs.

Simulator and checkpoint for 4.3 →

4.4 Array Traversals

Big ideaTo change what is stored in an array, you need the index; the enhanced for loop only lends you copies.

1. What a traversal is

Traversing an array means using repetition statements (loops) to access all of the elements in an array, or an ordered sequence of them 4.4.A.1. “Ordered sequence” matters: a traversal can visit every element front to back, but also backward, every other element, or just part of the array.

2. Indexed for loops and while loops

A traversal with an indexed for loop or a while loop accesses the elements through their indexes 4.4.A.2. The loop variable is the index, and arr[i] is the element.

int[] temps = {12, 15, 9, 20};

for (int i = 0; i < temps.length; i++)
{
    System.out.println("Day " + i + ": " + temps[i]);
}

Output:

Day 0: 12
Day 1: 15
Day 2: 9
Day 3: 20

The same traversal written as a while loop: the three parts of the for header move to three places 4.4.A.2.

int[] temps = {12, 15, 9, 20};

int i = 0;                      // start
while (i < temps.length)        // condition
{
    System.out.println("Day " + i + ": " + temps[i]);
    i++;                        // update
}

Output:

Day 0: 12
Day 1: 15
Day 2: 9
Day 3: 20

Because you control the index, you can visit an ordered sequence instead of everything:

int[] temps = {12, 15, 9, 20, 17};

for (int i = temps.length - 1; i >= 0; i--)     // backward
{
    System.out.print(temps[i] + " ");
}
System.out.println();

for (int i = 0; i < temps.length; i += 2)       // every other element
{
    System.out.print(temps[i] + " ");
}
System.out.println();

Output:

17 20 9 15 12 
12 9 17 

An indexed loop can also change elements, because temps[i] on the left of = writes into the array:

int[] temps = {12, 15, 9, 20};
for (int i = 0; i < temps.length; i++)
{
    temps[i] = temps[i] * 2;
}
System.out.println(temps[0] + " " + temps[1] + " " + temps[2] + " " + temps[3]);

Output:

24 30 18 40

Watch out: the most common traversal bug is <= instead of <.

int[] temps = {12, 15, 9, 20};
for (int i = 0; i <= temps.length; i++)
{
    System.out.println(temps[i]);
}

Output:

12
15
9
20
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

3. The enhanced for loop

An enhanced for loop header declares a variable, called the enhanced for loop variable. On each iteration, that variable is assigned a copy of the next element, without using its index 4.4.A.3. Read the colon as “in”: “for each score in scores”.

int[] scores = {8, 6, 10};
int total = 0;

for (int score : scores)
{
    total += score;
}
System.out.println("Total: " + total);

Output:

Total: 24

The type of the loop variable must match the element type (int for an int[], String for a String[]).

4. Assigning to the loop variable changes nothing

Because the loop variable holds a copy, assigning a new value to it does not change the value stored in the array 4.4.A.4.

int[] scores = {8, 6, 10};

for (int score : scores)
{
    score = 0;                  // changes only the copy
}
System.out.println(scores[0] + " " + scores[1] + " " + scores[2]);

Output:

8 6 10
index 0 1 2
scores before 8 6 10
scores after 8 6 10

The code compiles and runs with no error, which is why this is a classic exam trap. To reset the array, use an indexed loop: scores[i] = 0;.

5. Enhanced for loops with object references

When an array stores object references, the loop variable gets a copy of each reference. The copy still points to the same object, so calling a method on the loop variable can modify that object’s attributes 4.4.A.5. What it does not change is the references stored in the array 4.4.A.5.

public class Team
{
    public static void main(String[] args)
    {
        Player[] squad = {new Player("Mia", 3), new Player("Sam", 5)};

        for (Player p : squad)
        {
            p.addPoints(10);            // changes the object p refers to
        }
        System.out.println(squad[0].getPoints() + " " + squad[1].getPoints());

        for (Player p : squad)
        {
            p = new Player("New", 0);   // changes only the copy of the reference
        }
        System.out.println(squad[0].getName() + " " + squad[1].getName());
    }
}

class Player
{
    private String name;
    private int points;

    public Player(String n, int pts)
    {
        name = n;
        points = pts;
    }

    public void addPoints(int amount)
    {
        points += amount;
    }

    public int getPoints()
    {
        return points;
    }

    public String getName()
    {
        return name;
    }
}

Output:

13 15
Mia Sam

The first loop changed the players’ points (an attribute of each object). The second loop tried to put new players in the array, but the array still refers to Mia and Sam.

6. Rewriting an enhanced for loop

Code that traverses an array with an enhanced for loop can always be rewritten with an indexed for loop or a while loop 4.4.A.6. The pattern: the loop variable x becomes arr[i].

String[] words = {"red", "blue", "green"};

for (String w : words)                        // enhanced for
{
    System.out.print(w.length() + " ");
}
System.out.println();

for (int i = 0; i < words.length; i++)        // indexed for
{
    System.out.print(words[i].length() + " ");
}
System.out.println();

int k = 0;                                    // while
while (k < words.length)
{
    System.out.print(words[k].length() + " ");
    k++;
}
System.out.println();

Output:

3 4 5 
3 4 5 
3 4 5 

The opposite is not always possible. Choose an indexed loop when you need any of these:

You need to… Enhanced for? Indexed for or while?
read every element, in order yes yes
know the position (index) of an element no yes
compare an element with its neighbor no yes
change the values stored in the array no yes
go backward or skip elements no yes

Exam traps

  • for (int i = 0; i <= arr.length; i++) throws an ArrayIndexOutOfBoundsException on its last pass.
  • Assigning to the enhanced for loop variable compiles and runs but changes nothing in the array.
  • A while loop that forgets i++ never ends (infinite loop).
  • Going backward starts at arr.length - 1, not arr.length, and stops after index 0 (i >= 0).

Simulator and checkpoint for 4.4 →

4.5 Implementing Array Algorithms

Big ideaEvery standard array algorithm is one traversal with a running result: pick the right start value and the right bounds.

1. Minimum and maximum

There are standard algorithms that use array traversals, and the first ones find a minimum or maximum value 4.5.A.1. The plan: start with the first element as the “best so far”, then compare every other element and replace the best when a new one beats it.

public static int findMax(int[] arr)
{
    int max = arr[0];
    for (int i = 1; i < arr.length; i++)
    {
        if (arr[i] > max)
        {
            max = arr[i];
        }
    }
    return max;
}
int[] depths = {-12, -4, -30, -7};
System.out.println(findMax(depths));

Output:

-4

Watch out: if max started at 0, this call would return 0, a value that is not even in the array. For a minimum, flip the comparison to <. To find the index of the max instead of the value, store maxIndex = 0 and compare arr[i] > arr[maxIndex].

2. Sum, average and count

To compute a sum, start at 0 and add every element. The average is the sum divided by the number of elements 4.5.A.1. To determine the number of elements having a particular property, start a counter at 0 and add 1 each time an element passes the test 4.5.A.1.

int[] goals = {2, 0, 3, 1, 4, 0, 1};

int sum = 0;
int count = 0;
for (int g : goals)
{
    sum += g;
    if (g >= 2)          // the property: 2 or more goals
    {
        count++;
    }
}
double average = (double) sum / goals.length;

System.out.println("Sum: " + sum);
System.out.println("Average: " + average);
System.out.println("Games with 2+ goals: " + count);
System.out.println("Integer division: " + sum / goals.length);

Output:

Sum: 11
Average: 1.5714285714285714
Games with 2+ goals: 3
Integer division: 1

Watch out: sum / goals.length with two int values is integer division. Cast one side to double first.

3. At least one, and all

To determine if at least one element has a property, return true as soon as you find one; only after checking every element can you return false 4.5.A.1. To determine if all elements have a property, return false as soon as one fails; only after the loop can you return true 4.5.A.1.

public static boolean anyFreezing(double[] temps)
{
    for (double t : temps)
    {
        if (t < 0)
        {
            return true;        // one is enough
        }
    }
    return false;               // checked them all, none found
}

public static boolean allPassing(int[] scores)
{
    for (int s : scores)
    {
        if (s < 60)
        {
            return false;       // one failure is enough
        }
    }
    return true;                // checked them all, none failed
}
double[] week = {3.5, 1.0, -2.0, 4.5};
int[] quiz = {75, 88, 59, 91};
System.out.println(anyFreezing(week));
System.out.println(allPassing(quiz));

Output:

true
false

Watch out: this version of allPassing is wrong, because it returns on the very first element:

public static boolean allPassingBroken(int[] scores)
{
    for (int s : scores)
    {
        if (s >= 60)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    return true;
}
int[] quiz = {75, 88, 59, 91};
System.out.println(allPassingBroken(quiz));

Output:

true

It looked only at 75 and stopped. The correct answer is false, because 59 fails.

4. Consecutive pairs and duplicates

To access all consecutive pairs of elements, look at arr[i] and arr[i + 1] 4.5.A.1. Because arr[i + 1] must be a valid index, the loop stops one early: i < arr.length - 1.

int[] steps = {4000, 6500, 6200, 9000, 12000};

for (int i = 0; i < steps.length - 1; i++)
{
    int change = steps[i + 1] - steps[i];
    System.out.println("Day " + i + " to day " + (i + 1) + ": " + change);
}

Output:

Day 0 to day 1: 2500
Day 1 to day 2: -300
Day 2 to day 3: 2800
Day 3 to day 4: 3000

With i < steps.length, the last pass would read steps[5] and throw an ArrayIndexOutOfBoundsException.

To determine the presence or absence of duplicate elements, compare each element with every element after it 4.5.A.1. The inner loop starts at i + 1, so no element is compared with itself and no pair is checked twice.

public static boolean hasDuplicate(String[] names)
{
    for (int i = 0; i < names.length; i++)
    {
        for (int j = i + 1; j < names.length; j++)
        {
            if (names[i].equals(names[j]))
            {
                return true;
            }
        }
    }
    return false;
}
String[] signUps = {"Ada", "Ben", "Cleo", "Ben"};
String[] team = {"Ada", "Ben", "Cleo"};
System.out.println(hasDuplicate(signUps));
System.out.println(hasDuplicate(team));

Output:

true
false

Watch out: if the inner loop started at j = 0, every name would be compared with itself and the method would always return true. Use equals, not ==, to compare String objects.

5. Shift and rotate

To shift elements left, every element moves to the index one lower and the first value is lost. To rotate left, you save that first value and put it in the empty last spot 4.5.A.1.

int[] queue = {10, 20, 30, 40, 50};

int first = queue[0];                       // save the value that falls off
for (int i = 0; i < queue.length - 1; i++)
{
    queue[i] = queue[i + 1];                // move each element one place left
}
queue[queue.length - 1] = first;            // rotate: wrap it around

for (int q : queue)
{
    System.out.print(q + " ");
}
System.out.println();

Output:

20 30 40 50 10 
index 0 1 2 3 4
before 10 20 30 40 50
after shift left (last spot not set yet) 20 30 40 50 50
after rotate left 20 30 40 50 10

A plain shift left stops after the loop; the last spot usually gets a filler value such as 0.

To shift or rotate right, the loop must run backward, from the end toward index 1. Otherwise each value overwrites the next one before it is moved.

int[] queue = {10, 20, 30, 40, 50};

int last = queue[queue.length - 1];
for (int i = queue.length - 1; i > 0; i--)
{
    queue[i] = queue[i - 1];
}
queue[0] = last;

for (int q : queue)
{
    System.out.print(q + " ");
}
System.out.println();

Output:

50 10 20 30 40 

Watch out: a forward loop for a right shift copies the first value everywhere:

int[] queue = {10, 20, 30, 40, 50};
for (int i = 1; i < queue.length; i++)
{
    queue[i] = queue[i - 1];
}
for (int q : queue)
{
    System.out.print(q + " ");
}
System.out.println();

Output:

10 10 10 10 10 

6. Reverse

To reverse the order of the elements in place, swap the first and last, then the second and second-to-last, and so on until the middle 4.5.A.1. The mirror of index i is arr.length - 1 - i. A swap needs a temporary variable.

String[] podium = {"gold", "silver", "bronze", "fourth", "fifth"};

for (int i = 0; i < podium.length / 2; i++)
{
    String temp = podium[i];
    podium[i] = podium[podium.length - 1 - i];
    podium[podium.length - 1 - i] = temp;
}
for (String p : podium)
{
    System.out.print(p + " ");
}
System.out.println();

Output:

fifth fourth bronze silver gold 
index 0 1 2 3 4
before “gold” “silver” “bronze” “fourth” “fifth”
after i = 0 (swap 0 and 4) “fifth” “silver” “bronze” “fourth” “gold”
after i = 1 (swap 1 and 3) “fifth” “fourth” “bronze” “silver” “gold”

The loop stops when i reaches 5 / 2, which is 2; the middle element stays put. If the loop ran to podium.length, it would swap every pair a second time and put the array back in its original order.

A second way is to build a new array, filling it from the back of the original:

int[] laps = {61, 59, 64, 58};
int[] reversed = new int[laps.length];
for (int i = 0; i < laps.length; i++)
{
    reversed[laps.length - 1 - i] = laps[i];
}
for (int lap : reversed)
{
    System.out.print(lap + " ");
}
System.out.println();

Output:

58 64 59 61 

Exam traps

  • A max that starts at 0 fails when all values are negative; a min that starts at 0 fails when all values are positive.
  • Integer division in an average: 7 / 2 is 3, not 3.5.
  • An if with return true and an else with return false inside the loop only checks the first element.
  • Shifting right must go from the end backward, or one value gets copied into every element.
  • Reverse with a loop to arr.length (not arr.length / 2) puts every element back where it started.

Simulator and checkpoint for 4.5 →

4.6 Using Text Files

Big ideaReading a file is a traversal: while there is a next item, read it and process it.

1. Files and the classes that open them

A file is storage for data that persists when the program is not running, and a program can retrieve that data while it runs 4.6.A.1. A variable disappears when the program ends; a file such as scores.txt stays on the disk.

A file is connected to a program with two classes: File and Scanner 4.6.A.2. You open a file by creating a File object, with the file name as the argument of the constructor File(String str), where str is the pathname of the file 4.6.A.3. Then you pass that File to the Scanner(File f) constructor, which creates a Scanner that reads from it 4.6.A.6.

Step Code What it does
1. Name the file File f = new File("scores.txt"); Connects a File object to the file name
2. Make a reader Scanner input = new Scanner(f); A Scanner that reads from that file
3. Read input.nextInt(), input.next(), … Returns the next piece of data
4. Close input.close(); Ends the connection

File and IOException are in the java.io package, so an import statement must make them available 4.6.A.5. Scanner is in java.util, so it needs an import too.

2. throws IOException

When you use the File class, Java requires you to say what to do if the file with that name cannot be opened. One way is to add throws IOException to the header of the method that uses the file 4.6.A.4. If the file name is invalid, the program then terminates 4.6.A.4.

The data file used in this section, scores.txt:

12 7 25
9 14
30

A complete program that reads the first two numbers:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class FirstScores
{
    public static void main(String[] args) throws IOException
    {
        File f = new File("scores.txt");
        Scanner input = new Scanner(f);

        int a = input.nextInt();
        int b = input.nextInt();
        System.out.println(a + b);

        input.close();
    }
}

Output:

19

Real view: forget throws IOException and the code does not compile.

import java.io.File;
import java.util.Scanner;

public class NoThrows
{
    public static void main(String[] args)
    {
        Scanner input = new Scanner(new File("scores.txt"));
        System.out.println(input.nextInt());
        input.close();
    }
}

Compiler error:

error: unreported exception FileNotFoundException; must be caught or declared to be thrown

A FileNotFoundException is a kind of IOException, which is why throws IOException covers it. Forget the import for File and you get a different compiler error:

import java.io.IOException;
import java.util.Scanner;

public class NoImport
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("scores.txt"));
        input.close();
    }
}

Compiler error:

error: cannot find symbol

With everything correct but a file name that does not exist (here score.txt, a typo), the program compiles and then terminates when it runs:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class Typo
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("score.txt"));
        System.out.println("opened");
        input.close();
    }
}

Output:

Exception in thread "main" java.io.FileNotFoundException: score.txt (No such file or directory)

“opened” is never printed. (On some operating systems the message in brackets is worded differently.)

3. Scanner methods

These Scanner methods are in the Java Quick Reference 4.6.A.6. A token is a piece of text separated from the next one by whitespace (spaces, tabs or line breaks).

Method Returns If it goes wrong
int nextInt() the next int InputMismatchException if the next token is not an int or is out of range
double nextDouble() the next double InputMismatchException if the next token is not a double
boolean nextBoolean() the next boolean (true or false) InputMismatchException if it is not a boolean
String next() the next token as a String
String nextLine() the rest of the current line as a String (can be the empty string right after another Scanner method)
boolean hasNext() true if there is another item to read, false otherwise
void close() nothing; closes the scanner

The data file teams.txt, one team per line (name, wins, average goals, qualified):

Lions 3 4.5 true
Hawks 1 2.0 false
import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class Teams
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("teams.txt"));

        String name = input.next();
        int wins = input.nextInt();
        double avg = input.nextDouble();
        boolean qualified = input.nextBoolean();

        System.out.println(name + " won " + wins + " games");
        System.out.println("Average goals: " + avg);
        System.out.println("Qualified: " + qualified);
        System.out.println("Next team: " + input.next());

        input.close();
    }
}

Output:

Lions won 3 games
Average goals: 4.5
Qualified: true
Next team: Hawks

Notice that the line break after true does not matter to next(): it is just whitespace between tokens.

When a token does not match the method, the program stops with an InputMismatchException 4.6.A.6. The file laps.txt contains 8 5 abc 4:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class Laps
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("laps.txt"));
        int total = 0;
        while (input.hasNext())
        {
            total += input.nextInt();
            System.out.println("Total so far: " + total);
        }
        input.close();
    }
}

Output:

Total so far: 8
Total so far: 13
Exception in thread "main" java.util.InputMismatchException

hasNext() was true (the token abc exists), but nextInt() cannot turn it into an int.

Background: keyboard input is outside the scope of the AP® Computer Science A course and exam 4.6.A.6. Also, nextLine handles whitespace differently from the other methods, so mixing nextLine with nextInt, next and the others on the same file sometimes needs extra code to adjust 4.6.A.7. Writing or analyzing code that mixes them is outside the scope of the exam 4.6.A.7. A safe rule: either read tokens (next, nextInt, …) or read whole lines (nextLine), not both.

4. Reading until the end with hasNext

A while loop can keep reading as long as the file still contains items, using hasNext as the loop condition 4.6.A.9. This is the standard file traversal:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class SumScores
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("scores.txt"));
        int sum = 0;
        int count = 0;

        while (input.hasNext())
        {
            int s = input.nextInt();
            sum += s;
            count++;
        }
        input.close();

        System.out.println(count + " scores, sum " + sum);
        System.out.println("Average: " + (double) sum / count);
    }
}

Output:

6 scores, sum 97
Average: 16.166666666666668

The numbers were on three lines in scores.txt, but the loop does not care: it reads token after token until hasNext() returns false. Calling nextInt() again after that would throw a NoSuchElementException.

When you are finished with the file, close it with the close method from Scanner 4.6.A.10. Closing releases the file for other programs and makes clear the reading is over.

5. Reading lines and splitting them

Many data files store one record per line, with the values separated by commas. Read each whole line with nextLine() and break it into parts with the String method split 4.6.A.8: String[] split(String del) returns a String array where each element is a substring of the original, split around matches of del 4.6.A.8.

The data file roster.txt:

Ana,12,true
Ben,9,false
Cleo,15,true
import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class Roster
{
    public static void main(String[] args) throws IOException
    {
        Scanner input = new Scanner(new File("roster.txt"));

        while (input.hasNext())
        {
            String line = input.nextLine();
            String[] parts = line.split(",");
            System.out.println(parts[0] + " has " + parts.length + " fields; points = " + parts[1]);
        }
        input.close();
    }
}

Output:

Ana has 3 fields; points = 12
Ben has 3 fields; points = 9
Cleo has 3 fields; points = 15

For the line "Ana,12,true", split(",") gives this array:

index 0 1 2
parts “Ana” “12” “true”

Every element is a String, even "12". To do arithmetic with it, convert it with Integer.parseInt (topic 4.7).

String date = "2026 11 12";
String[] pieces = date.split(" ");
System.out.println(pieces.length);
System.out.println(pieces[1] + pieces[2]);

Output:

3
1112

pieces[1] + pieces[2] joins two strings, so the result is 1112, not 23.

Background: the del argument is a regular expression, a pattern language. Some characters such as . and * have special meanings in it. Code that uses those special properties is outside the scope of the AP® exam 4.6.A.8, so on the exam expect simple separators like ",", " " or ":".

Exam traps

  • Without throws IOException (or another way to handle it), the code does not compile.
  • nextInt on a token like abc throws an InputMismatchException at run time.
  • next() stops at whitespace, so "New York" is read as two tokens.
  • Calling a next method when hasNext() is false throws a NoSuchElementException.

Simulator and checkpoint for 4.6 →

4.7 Wrapper Classes

Big ideaWrapper classes let a primitive value live inside an object, and the compiler boxes and unboxes for you.

1. Integer and Double objects

Each primitive type has a matching wrapper class whose objects hold one value of that type. For the AP® exam you need two: Integer (wraps an int) and Double (wraps a double). Both are part of the java.lang package 4.7.A.1, like String and Math, so no import is needed.

Integer objects are immutable: once an Integer object is created, its attributes cannot be changed 4.7.A.1. Double objects are immutable in the same way 4.7.A.1. There is no method like setValue.

Primitive type Wrapper class Example value
int Integer 42
double Double 3.75

Why bother? Some Java code only works with objects. The most important example is ArrayList (topic 4.8), which stores object references. A list of whole numbers must use Integer:

ArrayList<int> laps = new ArrayList<int>();

Compiler error:

error: unexpected type

2. Autoboxing

Autoboxing is the automatic conversion the Java compiler makes from a primitive type to its wrapper class: an int to an Integer, a double to a Double 4.7.A.2. The compiler applies it when a primitive value is assigned to a variable of the wrapper class, or passed as a parameter to a method that expects an object of that wrapper class 4.7.A.2.

public static void showBoxed(Integer box)
{
    System.out.println("Received the object " + box);
}
Integer goals = 3;          // assigned to an Integer variable: autoboxing
Double price = 4.99;        // assigned to a Double variable: autoboxing

int bonus = 7;
showBoxed(bonus);           // passed to an Integer parameter: autoboxing

System.out.println(goals + " " + price);

Output:

Received the object 7
3 4.99

The match must be exact in type: a double value cannot be boxed into an Integer.

Integer wrong = 4.5;

Compiler error:

error: incompatible types: double cannot be converted to Integer

3. Unboxing

Unboxing is the automatic conversion the Java compiler makes from a wrapper class to its primitive type: an Integer to an int, a Double to a double 4.7.A.3. The compiler applies it when a wrapper object is assigned to a variable of the primitive type, or passed as a parameter to a method that expects that primitive type 4.7.A.3.

public static double half(double value)
{
    return value / 2;
}
Integer stored = 25;
Double rate = 0.5;

int plain = stored;                   // assigned to an int variable: unboxing
System.out.println(plain + 5);

System.out.println(half(rate));       // passed to a double parameter: unboxing
System.out.println(stored * rate);    // arithmetic unboxes both

Output:

30
0.25
12.5

Watch out: a wrapper variable is a reference, so it can be null. Unboxing null fails at run time.

Integer missing = null;
int value = missing;
System.out.println(value);

Output:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "missing" is null

The program stops with a NullPointerException. (The exact wording after the colon depends on your Java version and settings; on the AP® exam you only need the exception name.)

4. Immutable in practice

Because Integer objects cannot change, code that looks like it changes one actually creates a new object. The variable gets a reference to the new object, and any other variable still refers to the old one.

Integer score = 10;
Integer saved = score;      // both refer to the same Integer object

score++;                    // unbox 10, add 1, box 11 into a NEW object

System.out.println(score);
System.out.println(saved);

Output:

11
10

Watch out: == on two wrapper objects compares references, not values. Compare with equals, or unbox to int first.

Integer a = 1000;
Integer b = 1000;
int x = a;                  // unboxing
int y = b;                  // unboxing
System.out.println(a.equals(b));
System.out.println(x == y);
System.out.println(a == b);

Output:

true
true
false

(The == result for small values such as 5 can be true, because Java reuses some small Integer objects. Never rely on it.)

5. parseInt and parseDouble

Data read from a file or split from a line arrives as String values. Two static methods in the Java Quick Reference convert them to numbers:

  • static int parseInt(String s) in Integer returns the String argument as an int 4.7.A.4.
  • static double parseDouble(String s) in Double returns the String argument as a double 4.7.A.5.
String line = "Ana,12,8.5";
String[] parts = line.split(",");

int points = Integer.parseInt(parts[1]);
double minutes = Double.parseDouble(parts[2]);

System.out.println(parts[1] + parts[1]);     // String + String: joins
System.out.println(points + points);         // int + int: adds
System.out.println(points / minutes);

Output:

1212
24
1.411764705882353

Watch out: the String must really look like a number of that type, or the program stops with a NumberFormatException.

System.out.println(Double.parseDouble("12"));
System.out.println(Integer.parseInt("12.0"));

Output:

12.0
Exception in thread "main" java.lang.NumberFormatException: For input string: "12.0"

parseDouble("12") works (a whole number is a valid double), but parseInt("12.0") fails.

Exam traps

  • parseInt and parseDouble are static: call them on the class, Integer.parseInt(s), not on a String.
  • Integer.parseInt("12.0") and Integer.parseInt("twelve") throw a NumberFormatException at run time.
  • Use equals or compare unboxed int values; == on two Integer objects compares references.
  • Integer x = 4.5; does not compile: a double cannot be boxed into an Integer.

Simulator and checkpoint for 4.7 →

4.8 ArrayList Methods

Big ideaAn ArrayList is an array that resizes itself and keeps its elements packed together by shifting them.

1. Creating an ArrayList

An ArrayList object is mutable in size (it can grow and shrink) and it contains object references 4.8.A.1. The class is part of the java.util package, so an import statement must make it available 4.8.A.4. The constructor ArrayList() constructs an empty list 4.8.A.2.

Java allows the generic type ArrayList<E>, where the type parameter E is the type of the elements 4.8.A.3. With ArrayList<E>, the parameters and return types of the ArrayList methods are type E. Using ArrayList<E> is preferred over plain ArrayList, because it lets the compiler find errors that would otherwise only be found at run time 4.8.A.3.

import java.util.ArrayList;

public class Playlist
{
    public static void main(String[] args)
    {
        ArrayList<String> songs = new ArrayList<String>();
        System.out.println(songs.size());
        System.out.println(songs);
    }
}

Output:

0
[]

Printing an ArrayList shows its elements in square brackets, separated by commas.

With a type parameter, a wrong type is caught by the compiler:

ArrayList<String> songs = new ArrayList<String>();
songs.add(42);

Compiler error:

error: incompatible types: int cannot be converted to String

Background: without a type parameter, the same kind of mistake compiles and only fails when the program runs. This is why ArrayList<E> is preferred.

ArrayList songs = new ArrayList();
songs.add(42);
String first = (String) songs.get(0);
System.out.println(first);

Output:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')

Because the elements are object references, a list of numbers uses a wrapper class: ArrayList<Integer> or ArrayList<Double>. Autoboxing and unboxing (topic 4.7) convert for you.

2. size, add and get

These methods are in the Java Quick Reference 4.8.A.5:

  • int size() returns the number of elements in the list.
  • boolean add(E obj) appends obj to the end of the list and returns true.
  • E get(int index) returns the element at position index.
ArrayList<Integer> laps = new ArrayList<Integer>();
laps.add(62);                          // autoboxing: int 62 becomes an Integer
laps.add(58);
boolean ok = laps.add(60);

System.out.println(ok);
System.out.println(laps.size());
System.out.println(laps.get(1));
int best = laps.get(1);                // unboxing
System.out.println(best + 1);
System.out.println(laps);

Output:

true
3
58
59
[62, 58, 60]
index 0 1 2
laps 62 58 60

3. add at an index: shifting right

void add(int index, E obj) inserts obj at position index, where 0 <= index <= size. The elements at position index and higher move to the right (1 is added to their indices), and 1 is added to the size 4.8.A.5.

ArrayList<String> queue = new ArrayList<String>();
queue.add("Ana");
queue.add("Ben");
queue.add("Cleo");

queue.add(1, "Dev");          // insert at index 1
System.out.println(queue);

queue.add(4, "Eli");          // index == size: same as appending
System.out.println(queue);
System.out.println(queue.size());

Output:

[Ana, Dev, Ben, Cleo]
[Ana, Dev, Ben, Cleo, Eli]
5
index 0 1 2 3 4
before “Ana” “Ben” “Cleo”
after add(1, "Dev") “Ana” “Dev” “Ben” “Cleo”
after add(4, "Eli") “Ana” “Dev” “Ben” “Cleo” “Eli”

“Ben” was at index 1 and is now at index 2: inserting changed its index.

4. set and remove

  • E set(int index, E obj) replaces the element at position index with obj and returns the element formerly at that position 4.8.A.5. The size does not change.
  • E remove(int index) removes the element at position index, moves the elements at index + 1 and higher to the left (1 is subtracted from their indices), subtracts 1 from the size, and returns the element formerly at that position 4.8.A.5.
ArrayList<String> queue = new ArrayList<String>();
queue.add("Ana");
queue.add("Dev");
queue.add("Ben");
queue.add("Cleo");

String old = queue.set(2, "Bea");
System.out.println(old + " was replaced: " + queue);

String gone = queue.remove(0);
System.out.println(gone + " left: " + queue);
System.out.println("Now first: " + queue.get(0) + ", size " + queue.size());

Output:

Ben was replaced: [Ana, Dev, Bea, Cleo]
Ana left: [Dev, Bea, Cleo]
Now first: Dev, size 3
index 0 1 2 3
before “Ana” “Dev” “Ben” “Cleo”
after set(2, "Bea") “Ana” “Dev” “Bea” “Cleo”
after remove(0) “Dev” “Bea” “Cleo”

Watch out: with an ArrayList<Integer>, remove(1) uses the Quick Reference method remove(int index): it removes the element at index 1, not the value 1.

ArrayList<Integer> nums = new ArrayList<Integer>();
nums.add(1);
nums.add(5);
nums.add(9);
nums.remove(1);
System.out.println(nums);

Output:

[1, 9]

5. Valid indexes and IndexOutOfBoundsException

The indices of an ArrayList start at 0 and end at the number of elements minus 1 4.8.A.6, so get, set and remove accept 0 to size() - 1. (Only add(index, obj) also accepts size().) Any other index throws an IndexOutOfBoundsException when the code runs.

ArrayList<Double> times = new ArrayList<Double>();
times.add(9.8);
times.add(10.1);
times.add(9.9);

System.out.println(times.get(times.size() - 1));
System.out.println(times.get(times.size()));

Output:

9.9
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3

set cannot grow a list either. On an empty list, set(0, ...) fails because index 0 does not exist yet; use add.

6. Array or ArrayList?

Task Array (int[] a) ArrayList<Integer> list
Create new int[5] or {3, 1, 4} new ArrayList<Integer>() (starts empty)
Number of elements a.length list.size()
Read element i a[i] list.get(i)
Change element i a[i] = 7; list.set(i, 7);
Add at the end not possible (fixed length) list.add(7);
Insert or remove in the middle write your own shift list.add(i, 7); and list.remove(i);
Element types primitives or references references only
Bad index ArrayIndexOutOfBoundsException IndexOutOfBoundsException

Exam traps

  • After remove(1), the element that was at index 2 is now at index 1.
  • add(size(), obj) is legal (it appends), but get(size()) throws an IndexOutOfBoundsException.
  • On an ArrayList<Integer>, remove(1) removes the element at index 1, not the value 1.
  • set(i, obj) on an empty list throws an exception: set never grows the list.

Simulator and checkpoint for 4.8 →

4.9 ArrayList Traversals

Big ideaWhen you remove or add while you loop, the elements move under your feet: control the index, or walk backward.

1. Traversing an ArrayList

Traversing an ArrayList means using iteration (loops) or recursive statements to access all of the elements, or an ordered sequence of them 4.9.A.1. The loops look like array loops with two changes: size() instead of length, and get(i) instead of [i].

ArrayList<Integer> steps = new ArrayList<Integer>();
steps.add(5200);
steps.add(8100);
steps.add(7400);

for (int i = 0; i < steps.size(); i++)          // indexed for
{
    System.out.print(steps.get(i) + " ");
}
System.out.println();

int total = 0;
for (int s : steps)                             // enhanced for (unboxing to int)
{
    total += s;
}
System.out.println("Total: " + total);

int k = steps.size() - 1;                       // while, backward
while (k >= 0)
{
    System.out.print(steps.get(k) + " ");
    k--;
}
System.out.println();

Output:

5200 8100 7400 
Total: 20700
7400 8100 5200 

To change elements, use set in an indexed loop (assigning to the enhanced for loop variable changes nothing, as with arrays):

ArrayList<String> tags = new ArrayList<String>();
tags.add("java");
tags.add("loop");

for (int i = 0; i < tags.size(); i++)
{
    tags.set(i, "#" + tags.get(i));
}
System.out.println(tags);

Output:

[#java, #loop]

Preview: a traversal can also be recursive: a method handles one element and then calls itself for the rest. Recursion is covered in topic 4.16; here is a small example to trace.

public static void printFrom(ArrayList<String> list, int index)
{
    if (index < list.size())
    {
        System.out.print(list.get(index) + " ");
        printFrom(list, index + 1);
    }
}
ArrayList<String> stops = new ArrayList<String>();
stops.add("Park");
stops.add("Museum");
stops.add("Harbor");
printFrom(stops, 0);
System.out.println();

Output:

Park Museum Harbor 

2. Removing during a traversal: the skipping bug

Deleting elements during a traversal of an ArrayList requires special techniques to avoid skipping elements 4.9.A.2. The reason: remove(i) shifts every later element one place left, so the next element moves into index i. If the loop then does i++, that element is never checked.

Goal: remove every score below 50.

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(72);
scores.add(40);
scores.add(35);
scores.add(90);

for (int i = 0; i < scores.size(); i++)
{
    if (scores.get(i) < 50)
    {
        scores.remove(i);
    }
}
System.out.println(scores);

Output:

[72, 35, 90]

35 survived. Trace it:

i list at the start of the pass get(i) action
0 [72, 40, 35, 90] 72 keep
1 [72, 40, 35, 90] 40 remove(1); 35 shifts to index 1
2 [72, 35, 90] 90 keep (35, now at index 1, was skipped)

3. Two safe ways to remove

Technique 1: only move forward when nothing was removed.

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(72);
scores.add(40);
scores.add(35);
scores.add(90);

int i = 0;
while (i < scores.size())
{
    if (scores.get(i) < 50)
    {
        scores.remove(i);       // the next element is now at index i: check it next
    }
    else
    {
        i++;
    }
}
System.out.println(scores);

Output:

[72, 90]

(The same idea in a for loop is to write i--; right after remove(i), so the i++ in the header cancels it out.)

Technique 2: traverse backward. Removing at index i only shifts elements after i, and a backward loop has already checked those.

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(72);
scores.add(40);
scores.add(35);
scores.add(90);

for (int i = scores.size() - 1; i >= 0; i--)
{
    if (scores.get(i) < 50)
    {
        scores.remove(i);
    }
}
System.out.println(scores);

Output:

[72, 90]
i list at the start of the pass get(i) action
3 [72, 40, 35, 90] 90 keep
2 [72, 40, 35, 90] 35 remove(2)
1 [72, 40, 90] 40 remove(1)
0 [72, 90] 72 keep

4. Adding during a traversal

Adding has the opposite shift. add(i, obj) pushes the current element to index i + 1, so a forward loop meets it again on the next pass. Goal: put a “warm up” before every “run”.

ArrayList<String> plan = new ArrayList<String>();
plan.add("run");
plan.add("rest");
plan.add("run");

for (int i = 0; i < plan.size(); i++)
{
    if (plan.get(i).equals("run"))
    {
        plan.add(i, "warm up");
        i++;                    // skip past the "run" that just moved to i + 1
    }
}
System.out.println(plan);

Output:

[warm up, run, rest, warm up, run]

Without the extra i++, the loop would find the same “run” at index i + 1, insert again, and keep going until the program runs out of memory.

5. IndexOutOfBoundsException

Attempting to access an index value outside the range 0 to size() - 1 results in an IndexOutOfBoundsException 4.9.A.3. Two ways it happens in traversals:

ArrayList<String> names = new ArrayList<String>();
names.add("Ada");
names.add("Ben");

for (int i = 0; i <= names.size(); i++)        // <= is one pass too many
{
    System.out.println(names.get(i));
}

Output:

Ada
Ben
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 2

Saving the size in a variable before a loop that removes elements also fails, because the list gets shorter but the saved number does not:

ArrayList<Integer> nums = new ArrayList<Integer>();
nums.add(4);
nums.add(-1);
nums.add(6);

int n = nums.size();                            // 3, and it stays 3
for (int i = 0; i < n; i++)
{
    if (nums.get(i) < 0)
    {
        nums.remove(i);
    }
}
System.out.println(nums);

Output:

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 2
i list at the start of the pass get(i) action
0 [4, -1, 6] 4 keep
1 [4, -1, 6] -1 remove(1); the size is now 2
2 [4, 6] get(2) exception: n is still 3, but index 2 no longer exists

Write i < nums.size() in the condition so the loop sees the current size (and combine it with a technique from section 3).

6. ConcurrentModificationException

Changing the size of an ArrayList while traversing it with an enhanced for loop can result in a ConcurrentModificationException 4.9.A.4. So when you use an enhanced for loop on an ArrayList, you should not add or remove elements 4.9.A.4.

ArrayList<String> cart = new ArrayList<String>();
cart.add("apple");
cart.add("soda");
cart.add("bread");
cart.add("soda");

for (String item : cart)
{
    if (item.equals("soda"))
    {
        cart.remove(1);
    }
}
System.out.println(cart);

Output:

Exception in thread "main" java.util.ConcurrentModificationException

The code compiles; the exception happens when the loop tries to move to the next element after the list has changed size. Adding inside an enhanced for loop fails the same way. Use one of the indexed techniques from section 3 instead.

Loop type Read elements Replace with set Remove or add
indexed for / while yes yes yes, with a special technique
enhanced for yes no (no index) no: ConcurrentModificationException

Exam traps

  • Forward loop with remove(i) and always i++: consecutive matches are skipped.
  • for (int i = 0; i <= list.size(); i++) with get(i) throws an IndexOutOfBoundsException on the last pass.
  • list.remove(x) inside for (String x : list) compiles fine and fails at run time.
  • Setting the enhanced for loop variable to a new object does not change the list; use set(i, obj) in an indexed loop.

Simulator and checkpoint for 4.9 →

4.10 Implementing ArrayList Algorithms

Big ideaEvery standard ArrayList algorithm is a traversal plus one variable that remembers what you have seen.

1. Minimum, maximum, sum and average

An ArrayList algorithm is a traversal with a purpose. For a minimum or maximum, keep the best value seen so far and compare every element with it 4.10.A.1. For a sum, add each element to a running total; for an average, divide that total by size() using double division 4.10.A.1.

ArrayList<Integer> temps = new ArrayList<Integer>();
temps.add(-4);
temps.add(-9);
temps.add(-1);
temps.add(-6);

int max = temps.get(0);
int min = temps.get(0);
int sum = 0;
for (int t : temps)
{
    if (t > max)
    {
        max = t;
    }
    if (t < min)
    {
        min = t;
    }
    sum += t;
}
double avg = (double) sum / temps.size();
System.out.println("max " + max + ", min " + min);
System.out.println("average " + avg);

Output:

max -1, min -9
average -5.0

Watch out: if max started at 0, it would stay 0 for this all-negative list. Seed with the first element. Also, sum / temps.size() with two int values drops the decimal part.

2. Property checks: at least one, all, and how many

Three algorithms ask a yes/no question about each element 4.10.A.1:

Question Variable When to stop early
Does at least one element have the property? boolean Return true at the first match
Do all elements have the property? boolean Return false at the first failure
How many elements have the property? int counter Never: visit every element
public static boolean anyOver(ArrayList<Integer> scores, int limit)
{
    for (int s : scores)
    {
        if (s > limit)
        {
            return true;
        }
    }
    return false;
}

public static boolean allPassing(ArrayList<Integer> scores)
{
    for (int s : scores)
    {
        if (s < 60)
        {
            return false;
        }
    }
    return true;
}

public static int countEven(ArrayList<Integer> scores)
{
    int count = 0;
    for (int s : scores)
    {
        if (s % 2 == 0)
        {
            count++;
        }
    }
    return count;
}

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(72);
scores.add(95);
scores.add(58);
scores.add(80);
System.out.println(anyOver(scores, 90));
System.out.println(allPassing(scores));
System.out.println(countEven(scores));

Output:

true
false
3

Watch out: putting else return true; inside the loop of allPassing makes the method decide after looking at only one element.

3. Consecutive pairs and duplicates

To compare each element with its neighbor, visit all consecutive pairs: get(i) and get(i + 1). The loop must stop at size() - 1 so that i + 1 is still a valid index 4.10.A.1.

To find duplicates, compare every element with every element after it, using nested loops 4.10.A.1. Use equals for objects such as String.

ArrayList<String> playlist = new ArrayList<String>();
playlist.add("Rain");
playlist.add("Echo");
playlist.add("Rain");
playlist.add("Glow");

int changes = 0;
for (int i = 0; i < playlist.size() - 1; i++)
{
    if (playlist.get(i).compareTo(playlist.get(i + 1)) > 0)
    {
        changes++;
    }
}
System.out.println("Out-of-order pairs: " + changes);

boolean hasDuplicate = false;
for (int i = 0; i < playlist.size(); i++)
{
    for (int j = i + 1; j < playlist.size(); j++)
    {
        if (playlist.get(i).equals(playlist.get(j)))
        {
            hasDuplicate = true;
        }
    }
}
System.out.println("Duplicate: " + hasDuplicate);

Output:

Out-of-order pairs: 2
Duplicate: true

The pairs checked are (“Rain”, “Echo”), (“Echo”, “Rain”) and (“Rain”, “Glow”). The first and the last pair are out of alphabetical order. Watch out: with i < playlist.size() the last pass calls get(4) and the program stops with an IndexOutOfBoundsException.

4. Shift, rotate and reverse

A shift moves every element one place left or right. A rotate is a shift where the element that falls off one end comes back at the other end 4.10.A.1. With an ArrayList you can rotate by moving one element: remove(0) returns the first element, and add puts it at the end. You can also do it the array way, with set and a saved value.

To reverse, swap the first and last elements, then the second and second to last, and stop at the middle 4.10.A.1.

ArrayList<Integer> lane = new ArrayList<Integer>();
for (int k = 1; k <= 5; k++)
{
    lane.add(k * 10);
}

// rotate left by one, using set and a saved value
int first = lane.get(0);
for (int i = 0; i < lane.size() - 1; i++)
{
    lane.set(i, lane.get(i + 1));
}
lane.set(lane.size() - 1, first);
System.out.println(lane);

// rotate right by one, using remove and add
lane.add(0, lane.remove(lane.size() - 1));
System.out.println(lane);

// reverse: swap pairs from the outside in
for (int i = 0; i < lane.size() / 2; i++)
{
    int j = lane.size() - 1 - i;
    int temp = lane.get(i);
    lane.set(i, lane.get(j));
    lane.set(j, temp);
}
System.out.println(lane);

Output:

[20, 30, 40, 50, 10]
[10, 20, 30, 40, 50]
[50, 40, 30, 20, 10]

Watch out: if the reverse loop ran to i < lane.size(), it would swap every pair twice and the list would end up back in its original order.

5. Inserting and deleting elements

add(index, obj) inserts and moves later elements right; remove(index) deletes and moves later elements left 4.10.A.1. Both change size(), so a loop that inserts or deletes must control its index carefully.

Deleting. The safest pattern runs backward. The elements that shift are the ones already visited, so nothing is skipped.

ArrayList<Integer> stock = new ArrayList<Integer>();
stock.add(0);
stock.add(0);
stock.add(7);
stock.add(0);
stock.add(3);

// buggy: forward loop always does i++
ArrayList<Integer> copy = new ArrayList<Integer>(stock);
for (int i = 0; i < copy.size(); i++)
{
    if (copy.get(i) == 0)
    {
        copy.remove(i);
    }
}
System.out.println("forward: " + copy);

// correct: backward loop
for (int i = stock.size() - 1; i >= 0; i--)
{
    if (stock.get(i) == 0)
    {
        stock.remove(i);
    }
}
System.out.println("backward: " + stock);

Output:

forward: [0, 7, 3]
backward: [7, 3]

Trace of the buggy loop: at i = 0 the first 0 is removed and the second 0 slides into index 0, but i becomes 1, so that 0 is never checked.

Inserting. After inserting at index i, the element you were looking at is now at i + 1. Skip past the new element so the loop does not process it again (or insert forever).

ArrayList<String> line = new ArrayList<String>();
line.add("Ana");
line.add("VIP");
line.add("Ben");
line.add("VIP");

for (int i = 0; i < line.size(); i++)
{
    if (line.get(i).equals("VIP"))
    {
        line.add(i, "Guard");
        i++; // jump over the element we just inserted
    }
}
System.out.println(line);

Output:

[Ana, Guard, VIP, Ben, Guard, VIP]

Watch out: calling add or remove on a list inside an enhanced for loop over that same list throws a ConcurrentModificationException.

6. Traversing more than one collection at a time

Some algorithms need two String, array or ArrayList objects at once 4.10.A.2. Two common shapes:

  • Parallel traversal: one index walks through both collections, so element i of one belongs with element i of the other.
  • Nested traversal: for each element of the first collection, walk through the whole second collection.
String[] players = {"Kai", "Lena", "Omar"};
ArrayList<Integer> goals = new ArrayList<Integer>();
goals.add(3);
goals.add(5);
goals.add(2);

String best = players[0];
int bestGoals = goals.get(0);
for (int i = 1; i < players.length; i++)
{
    if (goals.get(i) > bestGoals)
    {
        bestGoals = goals.get(i);
        best = players[i];
    }
}
System.out.println(best + " scored " + bestGoals);

// count letters of a word that also appear in a second word
String a = "plane";
String b = "apple";
int shared = 0;
for (int i = 0; i < a.length(); i++)
{
    if (b.indexOf(a.substring(i, i + 1)) >= 0)
    {
        shared++;
    }
}
System.out.println("shared letters: " + shared);

Output:

Lena scored 5
shared letters: 4

In “plane”, the letters p, l, a and e appear in “apple”; n does not. Watch out: parallel traversal only works if both collections have the same length, or if the loop stops at the shorter one.

Exam traps

  • Forward loop with remove(i) and i++ skips the element right after each removal.
  • Pair loops that run to i < list.size() and read list.get(i + 1) throw an IndexOutOfBoundsException on the last pass.
  • sum / list.size() with an int sum is integer division; cast to double first.
  • An enhanced for loop that calls add or remove on the same list throws a ConcurrentModificationException.
  • Rotating left: save the first element before the shift loop overwrites it.

Simulator and checkpoint for 4.10 →

4.11 2D Array Creation and Access

Big ideaA 2D array is an array whose elements are rows, and each row is an ordinary 1D array.

1. A 2D array is an array of arrays

A two-dimensional (2D) array stores related data in rows and columns, such as seats in a cinema or scores per player per round. Java stores it as an array of arrays: the outer array holds the rows, and each row is a 1D array 4.11.A.1. So creating and indexing a 2D array works much like a 1D array, with one extra pair of brackets 4.11.A.1.

Like a 1D array, a 2D array’s size is set when it is created and cannot change 4.11.A.1. It can store primitive data (int, double, boolean) or object references (String, or any class) 4.11.A.1.

int[][] seats = new int[2][3];        // 2 rows, 3 columns of int
String[][] board = new String[3][3];  // 3 rows, 3 columns of references
System.out.println(seats.length + " rows, " + seats[0].length + " columns");
System.out.println(board.length * board[0].length + " squares");

Output:

2 rows, 3 columns
9 squares

Picture seats like this:

column 0 column 1 column 2
row 0 seats[0][0] seats[0][1] seats[0][2]
row 1 seats[1][0] seats[1][1] seats[1][2]

Scope note: every 2D array on the AP® exam is rectangular (every row has the same length). Arrays with rows of different lengths are outside the course 4.11.A.1.

2. Creating with new: default values

When you create a 2D array with the keyword new, every element starts at the default value for its type 4.11.A.2:

Element type Default value
int 0
double 0.0
boolean false
any reference type (String, a class) null
int[][] counts = new int[2][2];
double[][] prices = new double[2][2];
boolean[][] booked = new boolean[2][2];
String[][] names = new String[2][2];
System.out.println(counts[1][1] + " " + prices[0][1] + " " + booked[1][0] + " " + names[0][0]);

Output:

0 0.0 false null

Watch out: names[0][0].length() would throw a NullPointerException, because the element holds null and not a String object.

3. Creating with an initializer list

An initializer list for a 2D array is a list of initializer lists: each inner list is one row, written like a 1D array 4.11.A.3. Java counts the rows and columns for you.

int[][] scores = { {12, 15, 9, 20},
                   {18, 11, 14, 16},
                   {10, 19, 13, 17} };
String[][] menu = { {"soup", "salad"},
                    {"pasta", "rice"} };
System.out.println(scores.length + " x " + scores[0].length);
System.out.println(menu[1][0]);

Output:

3 x 4
pasta

4. Reading and changing one element: [row][col]

Two indexes in square brackets reach one element: arr[row][col] 4.11.A.4. For the AP® exam, the first index is the row and the second index is the column 4.11.A.4. The same expression reads a value or, on the left of =, changes it.

int[][] scores = { {12, 15, 9, 20},
                   {18, 11, 14, 16},
                   {10, 19, 13, 17} };
System.out.println(scores[1][2]);   // row 1, column 2
scores[2][0] = scores[0][3] + 5;    // change row 2, column 0
scores[0][1]++;
System.out.println(scores[2][0] + " " + scores[0][1]);

Output:

14
25 16

The grid after the two changes:

col 0 col 1 col 2 col 3
row 0 12 16 9 20
row 1 18 11 14 16
row 2 25 19 13 17

5. One row as a 1D array

Use the array name with a single pair of brackets, arr[r], to get row r as a 1D array 4.11.A.5. This is a reference to the real row, not a copy, so changes through it change the 2D array.

double[][] rain = { {2.5, 0.0, 1.0},
                    {4.0, 3.5, 0.5} };
double[] week2 = rain[1];          // the whole second row
System.out.println(week2.length);
week2[0] = 9.9;                    // changes rain[1][0] too
System.out.println(rain[1][0]);

Output:

3
9.9

Watch out: there is no single-bracket way to get a column. A column must be built with a loop over the rows.

6. Size, valid indexes and ArrayIndexOutOfBoundsException

For a 2D array named grid 4.11.A.6:

What you want Expression Valid index values
Number of rows grid.length rows 0 to grid.length - 1
Number of columns grid[0].length (the length of any row) columns 0 to grid[0].length - 1

Any index outside these ranges throws an ArrayIndexOutOfBoundsException when the program runs 4.11.A.6.

String[][] maze = new String[4][6];
System.out.println("rows: " + maze.length);
System.out.println("columns: " + maze[0].length);
System.out.println("last row index: " + (maze.length - 1));
maze[3][6] = "#";   // column 6 does not exist

Output:

rows: 4
columns: 6
last row index: 3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 6

The message says “length 6” because the bad index was the column: each row has length 6, so the last column is 5.

Exam traps

  • arr.length is rows, not columns, and not the total number of elements.
  • The last valid element is arr[arr.length - 1][arr[0].length - 1], not arr[arr.length][arr[0].length].
  • A new 2D array of String or another class holds null in every element, so calling a method on one throws a NullPointerException.
  • arr[r] is a reference to the row, not a copy: changing it changes the 2D array.

Simulator and checkpoint for 4.11 →

4.12 2D Array Traversals

Big ideaThe loop on the outside decides the order; arr[row][col] never changes its shape.

1. Row-major order: across each row

Nested iteration statements visit every element of a 2D array 4.12.A.1. In row-major order the traversal moves across each row: the outer loop picks a row and the inner loop visits every column of that row 4.12.A.1.

int[][] grid = { {1, 2, 3},
                 {4, 5, 6} };
for (int r = 0; r < grid.length; r++)          // rows: slow
{
    for (int c = 0; c < grid[0].length; c++)   // columns: fast
    {
        System.out.print(grid[r][c] + " ");
    }
    System.out.println();
}

Output:

1 2 3 
4 5 6 

Visit order (each cell shows when it is visited):

col 0 col 1 col 2
row 0 1st 2nd 3rd
row 1 4th 5th 6th

2. Column-major order: down each column

In column-major order the traversal moves down each column 4.12.A.1. Put the column loop outside and the row loop inside. The bounds swap too, but the element is still grid[r][c].

int[][] grid = { {1, 2, 3},
                 {4, 5, 6} };
for (int c = 0; c < grid[0].length; c++)       // columns: slow
{
    for (int r = 0; r < grid.length; r++)      // rows: fast
    {
        System.out.print(grid[r][c] + " ");
    }
}
System.out.println();

Output:

1 4 2 5 3 6 
col 0 col 1 col 2
row 0 1st 3rd 5th
row 1 2nd 4th 6th

Watch out: if you keep r < grid[0].length as the inner bound, then on this 2 by 3 array r reaches 2 and grid[2][0] throws an ArrayIndexOutOfBoundsException.

3. Uniquely defined orders and parts of the array

Nested loops can also visit elements in an order you define, or only an ordered sequence of them 4.12.A.1. Change where a loop starts, where it stops and how it steps.

int[][] grid = { {1, 2, 3},
                 {4, 5, 6},
                 {7, 8, 9} };

// bottom row to top row, right to left
for (int r = grid.length - 1; r >= 0; r--)
{
    for (int c = grid[0].length - 1; c >= 0; c--)
    {
        System.out.print(grid[r][c] + " ");
    }
}
System.out.println();

// main diagonal only: one loop is enough
for (int i = 0; i < grid.length; i++)
{
    System.out.print(grid[i][i] + " ");
}
System.out.println();

// zigzag: even rows left to right, odd rows right to left
for (int r = 0; r < grid.length; r++)
{
    if (r % 2 == 0)
    {
        for (int c = 0; c < grid[r].length; c++)
        {
            System.out.print(grid[r][c] + " ");
        }
    }
    else
    {
        for (int c = grid[r].length - 1; c >= 0; c--)
        {
            System.out.print(grid[r][c] + " ");
        }
    }
}
System.out.println();

Output:

9 8 7 6 5 4 3 2 1 
1 5 9 
1 2 3 6 5 4 7 8 9 

Tip for tracing: write the values of r and c in the order the loops produce them, then read the grid at each pair.

4. Nested enhanced for loops

Because a 2D array is an array of arrays, an enhanced for loop works on it the same way as on a 1D array 4.12.A.1. The outer loop visits the rows, so its variable must have the row type, a 1D array. The inner loop visits one row, so its variable has the element type 4.12.A.2.

2D array type Outer variable (a row) Inner variable (an element)
int[][] int[] row int value
double[][] double[] row double value
String[][] String[] row String value
String[][] teams = { {"Ada", "Bo"},
                     {"Cy", "Dee"},
                     {"Eli", "Fay"} };
int letters = 0;
for (String[] row : teams)
{
    for (String name : row)
    {
        letters += name.length();
    }
}
System.out.println(letters + " letters");

Output:

16 letters

A nested enhanced for loop always visits elements in row-major order, and it gives no indexes. Use indexed loops when you need column-major order or the position of an element.

5. Assigning to the loop variable does not change the array

The enhanced for loop variable gets a copy of each element value. Assigning a new value to it does not change the value stored in the array 4.12.A.2.

int[][] marks = { {3, 7},
                  {5, 9} };
for (int[] row : marks)
{
    for (int m : row)
    {
        m = 0;               // changes only the copy
    }
}
System.out.println(marks[0][0] + " " + marks[1][1]);

for (int r = 0; r < marks.length; r++)
{
    for (int c = 0; c < marks[0].length; c++)
    {
        marks[r][c] = 0;     // changes the array
    }
}
System.out.println(marks[0][0] + " " + marks[1][1]);

Output:

3 9
0 0

Background: the outer variable row refers to the real row, so row[0] = 0; inside the outer loop would change the array. Assigning to the inner variable m never does.

Exam traps

  • In column-major order the access is still arr[r][c], not arr[c][r].
  • Column-major loops bound the outer loop by arr[0].length and the inner loop by arr.length.
  • for (int[] row : arr) { for (int x : row) { x = 0; } } leaves the array unchanged.
  • The outer enhanced for variable of a String[][] is a String[], not a String.

Simulator and checkpoint for 4.12 →

4.13 Implementing 2D Array Algorithms

Big ideaA 2D algorithm is a 1D algorithm plus a decision about which index stays still.

All examples on this page use the same data: points scored by 3 players (rows) in 4 games (columns).

game 0 game 1 game 2 game 3
player 0 12 7 15 9
player 1 4 18 11 18
player 2 20 6 13 10

1. Minimum, maximum, sum and average

The 1D algorithms work on the whole 2D array, on one designated row, on one designated column or on another subsection 4.13.A.1. For the whole array use nested loops. For one row, fix the row index. For one column, fix the column index.

int[][] pts = { {12, 7, 15, 9},
                {4, 18, 11, 18},
                {20, 6, 13, 10} };

// minimum of the whole 2D array
int min = pts[0][0];
for (int[] row : pts)
{
    for (int p : row)
    {
        if (p < min)
        {
            min = p;
        }
    }
}
System.out.println("lowest score: " + min);

// sum and average of designated row 1 (one player)
int rowSum = 0;
for (int c = 0; c < pts[1].length; c++)
{
    rowSum += pts[1][c];
}
System.out.println("player 1 average: " + (double) rowSum / pts[1].length);

// maximum of designated column 2 (one game)
int best = pts[0][2];
for (int r = 1; r < pts.length; r++)
{
    if (pts[r][2] > best)
    {
        best = pts[r][2];
    }
}
System.out.println("best in game 2: " + best);

// sum of a subsection: rows 1 to 2, columns 0 to 1
int corner = 0;
for (int r = 1; r <= 2; r++)
{
    for (int c = 0; c <= 1; c++)
    {
        corner += pts[r][c];
    }
}
System.out.println("lower-left corner total: " + corner);

Output:

lowest score: 4
player 1 average: 12.75
best in game 2: 15
lower-left corner total: 48

Watch out: the average of the whole array divides by pts.length * pts[0].length (12 elements), not by pts.length (3 rows).

2. Property checks: at least one, all, and how many

The same three yes/no algorithms from 1D arrays apply to the whole 2D array or to one part of it 4.13.A.1. “At least one” can return true at the first match, “all” can return false at the first failure, and a count must visit every element in the part.

public static boolean anyGameOver(int[][] pts, int limit)
{
    for (int[] row : pts)
    {
        for (int p : row)
        {
            if (p > limit)
            {
                return true;
            }
        }
    }
    return false;
}

public static boolean allColumnAtLeast(int[][] pts, int col, int min)
{
    for (int r = 0; r < pts.length; r++)
    {
        if (pts[r][col] < min)
        {
            return false;
        }
    }
    return true;
}

public static int countInRow(int[][] pts, int row, int limit)
{
    int count = 0;
    for (int c = 0; c < pts[row].length; c++)
    {
        if (pts[row][c] >= limit)
        {
            count++;
        }
    }
    return count;
}

int[][] pts = { {12, 7, 15, 9},
                {4, 18, 11, 18},
                {20, 6, 13, 10} };
System.out.println(anyGameOver(pts, 19));
System.out.println(allColumnAtLeast(pts, 2, 10));
System.out.println(countInRow(pts, 0, 10));

Output:

true
true
2

Every value in column 2 (15, 11, 13) is at least 10, and row 0 has two values (12 and 15) that are at least 10.

3. Consecutive pairs and duplicates

Consecutive pairs in a row are arr[r][c] and arr[r][c + 1]; in a column they are arr[r][c] and arr[r + 1][c] 4.13.A.1. Stop one index early in the direction you compare.

To detect duplicates in a row or column, compare each element with every later element in that part. For the whole 2D array, it is easiest to number the elements 0 to rows times columns minus 1 and turn each number back into a row and column with / and % 4.13.A.1.

int[][] pts = { {12, 7, 15, 9},
                {4, 18, 11, 18},
                {20, 6, 13, 10} };

// consecutive pairs in each row: count games where a player improved
int improved = 0;
for (int r = 0; r < pts.length; r++)
{
    for (int c = 0; c < pts[0].length - 1; c++)
    {
        if (pts[r][c + 1] > pts[r][c])
        {
            improved++;
        }
    }
}
System.out.println("improvements: " + improved);

// duplicates anywhere in the 2D array
int cols = pts[0].length;
int total = pts.length * cols;
boolean dup = false;
for (int i = 0; i < total; i++)
{
    for (int j = i + 1; j < total; j++)
    {
        if (pts[i / cols][i % cols] == pts[j / cols][j % cols])
        {
            dup = true;
        }
    }
}
System.out.println("duplicate score: " + dup);

Output:

improvements: 4
duplicate score: true

Row 0 improves once (7 to 15), row 1 twice (4 to 18 and 11 to 18) and row 2 once (6 to 13), so the count is 4. The value 18 appears twice, so there is a duplicate.

4. Shift, rotate and reverse a row or a column

You can shift or rotate the elements of a row left or right, or the elements of a column up or down, and reverse a row or a column 4.13.A.1. Save the element that falls off the end, move the others toward the gap, then place the saved element at the other end.

int[][] pts = { {12, 7, 15, 9},
                {4, 18, 11, 18},
                {20, 6, 13, 10} };

// rotate row 0 right by one
int last = pts[0][pts[0].length - 1];
for (int c = pts[0].length - 1; c > 0; c--)
{
    pts[0][c] = pts[0][c - 1];
}
pts[0][0] = last;

// rotate column 1 down by one
int bottom = pts[pts.length - 1][1];
for (int r = pts.length - 1; r > 0; r--)
{
    pts[r][1] = pts[r - 1][1];
}
pts[0][1] = bottom;

// reverse row 2
for (int c = 0; c < pts[2].length / 2; c++)
{
    int temp = pts[2][c];
    pts[2][c] = pts[2][pts[2].length - 1 - c];
    pts[2][pts[2].length - 1 - c] = temp;
}

for (int[] row : pts)
{
    for (int p : row)
    {
        System.out.print(p + " ");
    }
    System.out.println();
}

Output:

9 6 7 15 
4 12 11 18 
10 13 18 20 

Trace it step by step:

Step Row 0 Row 1 Row 2
Start 12 7 15 9 4 18 11 18 20 6 13 10
Rotate row 0 right 9 12 7 15 4 18 11 18 20 6 13 10
Rotate column 1 down 9 6 7 15 4 12 11 18 20 18 13 10
Reverse row 2 9 6 7 15 4 12 11 18 10 13 18 20

Watch out: rotating right with a forward loop (c = 1; c < length; c++) copies the first value into every position. Move values in the direction away from the gap: start at the end that receives a value.

Exam traps

  • Seeding a column maximum with arr[0][0] instead of arr[0][col] can give a value from the wrong column.
  • The number of elements is arr.length * arr[0].length, not arr.length.
  • Shifting a row right with a forward loop copies the first value into every position.
  • A duplicate check across the whole 2D array needs to compare each element with every later element, not just neighbors.

Simulator and checkpoint for 4.13 →

4.14 Searching Algorithms

Big ideaLinear search is a traversal that stops as soon as it finds what it is looking for.

1. Linear search on an array

A linear search is a standard algorithm that checks each element in order until it finds the desired value or has checked all elements 4.14.A.1. A search method usually returns the index of the target, or -1 when the target is not there. -1 works as a signal because it is never a valid index.

public static int findBib(int[] bibs, int target)
{
    for (int i = 0; i < bibs.length; i++)
    {
        if (bibs[i] == target)
        {
            return i;          // found: stop right away
        }
    }
    return -1;                 // checked every element: not found
}

int[] bibs = {305, 118, 742, 118, 560};
System.out.println(findBib(bibs, 742));
System.out.println(findBib(bibs, 118));
System.out.println(findBib(bibs, 999));

Output:

2
1
-1

Trace of findBib(bibs, 742):

i bibs[i] bibs[i] == 742?
0 305 false
1 118 false
2 742 true: return 2

Watch out: the version below compiles but is wrong. It returns -1 as soon as the first element does not match.

public static int badFind(int[] bibs, int target)
{
    for (int i = 0; i < bibs.length; i++)
    {
        if (bibs[i] == target)
        {
            return i;
        }
        else
        {
            return -1;
        }
    }
    return -1;
}

int[] bibs = {305, 118, 742, 118, 560};
System.out.println(badFind(bibs, 742));

Output:

-1

2. Linear search on an ArrayList, from either end

The same algorithm works on an ArrayList: use size() and get(i) 4.14.A.1. Use equals to compare objects such as String. A linear search can begin from either end 4.14.A.1: start at the last index and move backward to find the last occurrence.

public static int lastIndexOfTitle(ArrayList<String> titles, String target)
{
    for (int i = titles.size() - 1; i >= 0; i--)
    {
        if (titles.get(i).equals(target))
        {
            return i;
        }
    }
    return -1;
}

ArrayList<String> loans = new ArrayList<String>();
loans.add("Dune");
loans.add("Holes");
loans.add("Dune");
loans.add("Wonder");
System.out.println("last Dune at " + lastIndexOfTitle(loans, "Dune"));
System.out.println("Matilda at " + lastIndexOfTitle(loans, "Matilda"));

Output:

last Dune at 2
Matilda at -1

A forward search for “Dune” would return 0, the first occurrence. Watch out: starting a backward loop at titles.size() throws an IndexOutOfBoundsException on the first get.

3. Linear search on a 2D array

To search a 2D array, access each row, then apply a linear search to that row 4.14.A.2. Return (or store) both the row and the column when you find the target.

public static String findSeat(String[][] seats, String name)
{
    for (int r = 0; r < seats.length; r++)
    {
        String[] row = seats[r];               // access one row
        for (int c = 0; c < row.length; c++)   // linear search on that row
        {
            if (row[c].equals(name))
            {
                return "row " + r + ", seat " + c;
            }
        }
    }
    return "not seated";
}

String[][] seats = { {"Ivy", "Max", "Noor"},
                     {"Leo", "Sam", "Tia"} };
System.out.println(findSeat(seats, "Sam"));
System.out.println(findSeat(seats, "Zed"));

Output:

row 1, seat 1
not seated

4. How many comparisons? (informal run time)

The number of comparisons a linear search makes depends on where the target is 4.14.A.1. Counting them is a useful way to compare algorithms informally, and it prepares you for binary search in topic 4.17.

public static int countChecks(int[] data, int target)
{
    int checks = 0;
    for (int i = 0; i < data.length; i++)
    {
        checks++;
        if (data[i] == target)
        {
            return checks;
        }
    }
    return checks;
}

int[] data = {8, 3, 9, 1, 6, 4, 7, 2};
System.out.println("target 8: " + countChecks(data, 8));
System.out.println("target 6: " + countChecks(data, 6));
System.out.println("target 5: " + countChecks(data, 5));

Output:

target 8: 1
target 6: 5
target 5: 8
Case Where the target is Comparisons for n elements
Best case first element checked 1
Worst case last element checked, or absent n

Double the number of elements and the worst case doubles too.

Exam traps

  • Compare String and other objects with equals, not ==.
  • An else branch that returns -1 inside the loop ends the search after one element.
  • A backward search must start at length - 1 (or size() - 1), not at length.
  • In the worst case (target absent or in the last position checked) linear search makes one comparison per element.

Simulator and checkpoint for 4.14 →

4.15 Sorting Algorithms

Big ideaSelection sort fixes one final position per pass; insertion sort keeps a growing front section sorted.

1. Two iterative sorts

Selection sort and insertion sort are iterative sorting algorithms: they use loops (not recursion) and can sort the elements of an array or an ArrayList 4.15.A.1. Both split the list into a sorted portion at the front and an unsorted portion after it. Each pass of the outer loop makes the sorted portion one element longer, so a list of n elements needs n - 1 passes.

Every example on this page sorts the same six race times (in seconds) into increasing order:

index 0 1 2 3 4 5
value 29 12 41 7 18 33

2. Selection sort

Selection sort repeatedly selects the smallest element from the unsorted portion and swaps it into its correct and final position at the end of the sorted portion 4.15.A.2. (A version that selects the largest element and swaps it to the back works the same way.)

public static void selectionSort(int[] arr)
{
    for (int i = 0; i < arr.length - 1; i++)
    {
        int minIndex = i;
        for (int j = i + 1; j < arr.length; j++)
        {
            if (arr[j] < arr[minIndex])
            {
                minIndex = j;
            }
        }
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
        System.out.println("pass " + (i + 1) + ": " + Arrays.toString(arr));
    }
}

int[] times = {29, 12, 41, 7, 18, 33};
selectionSort(times);

Output:

pass 1: [7, 12, 41, 29, 18, 33]
pass 2: [7, 12, 41, 29, 18, 33]
pass 3: [7, 12, 18, 29, 41, 33]
pass 4: [7, 12, 18, 29, 41, 33]
pass 5: [7, 12, 18, 29, 33, 41]

(Arrays.toString is only used here to show the array; it is not part of the AP® Java Quick Reference.)

Pass by pass (bold = sorted portion, which is final):

Pass Minimum found Swap Array after the pass Comparisons
start 29 12 41 7 18 33
1 7 (index 3) index 0 and 3 7 12 41 29 18 33 5
2 12 (index 1) with itself 7 12 41 29 18 33 4
3 18 (index 4) index 2 and 4 7 12 18 29 41 33 3
4 29 (index 3) with itself 7 12 18 29 41 33 2
5 33 (index 5) index 4 and 5 7 12 18 29 33 41 1

Total: 5 + 4 + 3 + 2 + 1 = 15 comparisons. For n elements selection sort always makes n(n - 1) / 2 comparisons, even when the list starts sorted, because it must check every unsorted element to be sure it has the minimum.

Watch out: on passes 2 and 4 the minimum was already in place, so the array did not change. A question asking for “the array after pass 2” expects the same array as after pass 1.

3. Insertion sort

Insertion sort takes the first element of the unsorted portion and inserts it into its correct, but not necessarily final, position in the sorted portion. It makes room by shifting the larger elements of the sorted portion one place to the right 4.15.A.3.

public static void insertionSort(int[] arr)
{
    for (int i = 1; i < arr.length; i++)
    {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key)
        {
            arr[j + 1] = arr[j];   // shift right
            j--;
        }
        arr[j + 1] = key;          // insert into the gap
        System.out.println("pass " + i + ": " + Arrays.toString(arr));
    }
}

int[] times = {29, 12, 41, 7, 18, 33};
insertionSort(times);

Output:

pass 1: [12, 29, 41, 7, 18, 33]
pass 2: [12, 29, 41, 7, 18, 33]
pass 3: [7, 12, 29, 41, 18, 33]
pass 4: [7, 12, 18, 29, 41, 33]
pass 5: [7, 12, 18, 29, 33, 41]

Pass by pass (bold = sorted portion, which is sorted but not final):

Pass Element inserted (key) Elements shifted right Array after the pass Comparisons (arr[j] > key)
start 29 12 41 7 18 33
1 12 29 12 29 41 7 18 33 1
2 41 none 12 29 41 7 18 33 1
3 7 41, 29, 12 7 12 29 41 18 33 3
4 18 41, 29 7 12 18 29 41 33 3
5 33 41 7 12 18 29 33 41 2

Total: 1 + 1 + 3 + 3 + 2 = 10 comparisons. In pass 1, 12 and 29 are in order with each other, but neither is final: on pass 3, the value 7 pushes both of them right.

Watch out: on pass 3 the loop stops because j becomes -1, not because of a comparison. The && short-circuits, so arr[-1] is never read.

4. Sorting an ArrayList

Both algorithms work on an ArrayList too 4.15.A.1. Replace arr[i] with get(i), assignments with set(i, value) and length with size().

ArrayList<String> names = new ArrayList<String>();
names.add("Mia");
names.add("Ali");
names.add("Zoe");
names.add("Ben");

// selection sort, alphabetical order
for (int i = 0; i < names.size() - 1; i++)
{
    int minIndex = i;
    for (int j = i + 1; j < names.size(); j++)
    {
        if (names.get(j).compareTo(names.get(minIndex)) < 0)
        {
            minIndex = j;
        }
    }
    String temp = names.get(i);
    names.set(i, names.get(minIndex));
    names.set(minIndex, temp);
}
System.out.println(names);

Output:

[Ali, Ben, Mia, Zoe]

5. Comparing the two sorts informally

Count comparisons to compare the algorithms informally. The starting order matters for insertion sort but not for selection sort.

public static int selectionComparisons(int[] arr)
{
    int count = 0;
    for (int i = 0; i < arr.length - 1; i++)
    {
        int minIndex = i;
        for (int j = i + 1; j < arr.length; j++)
        {
            count++;
            if (arr[j] < arr[minIndex])
            {
                minIndex = j;
            }
        }
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return count;
}

public static int insertionComparisons(int[] arr)
{
    int count = 0;
    for (int i = 1; i < arr.length; i++)
    {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0)
        {
            count++;
            if (arr[j] > key)
            {
                arr[j + 1] = arr[j];
                j--;
            }
            else
            {
                break;
            }
        }
        arr[j + 1] = key;
    }
    return count;
}

int[] sorted = {1, 2, 3, 4, 5, 6};
int[] reversed = {6, 5, 4, 3, 2, 1};
System.out.println("selection, sorted:   " + selectionComparisons(new int[] {1, 2, 3, 4, 5, 6}));
System.out.println("selection, reversed: " + selectionComparisons(new int[] {6, 5, 4, 3, 2, 1}));
System.out.println("insertion, sorted:   " + insertionComparisons(sorted));
System.out.println("insertion, reversed: " + insertionComparisons(reversed));

Output:

selection, sorted:   15
selection, reversed: 15
insertion, sorted:   5
insertion, reversed: 15

(The break statement here only makes counting easy; it is not needed for the AP® exam.)

Selection sort Insertion sort
Each pass select the minimum of the unsorted portion, swap it in insert the next element, shifting larger ones right
Is the position final after the pass? yes 4.15.A.2 not necessarily 4.15.A.3
Comparisons, 6 elements already sorted 15 5
Comparisons, 6 elements in reverse order 15 15
Comparisons, n elements always n(n - 1) / 2 from n - 1 up to n(n - 1) / 2

Topic 4.17 adds a third sort, merge sort, which is recursive.

Exam traps

  • Exam questions often ask for the array after a given pass: count passes carefully (pass 1 is the first iteration of the outer loop).
  • Selection sort may swap an element with itself when the minimum is already in place; the array does not change on that pass.
  • Insertion sort's outer loop starts at index 1, because a list of one element is already sorted.
  • A list of n elements needs n - 1 passes in both sorts.

Simulator and checkpoint for 4.15 →

4.16 Recursion

Big ideaRecursion repeats by calling itself, and every waiting call keeps its own copy of the parameters.

1. What makes a method recursive

A recursive method is a method that calls itself 4.16.A.1. Every recursive method contains at least one base case, which halts the recursion, and at least one recursive call 4.16.A.1. Recursion is another form of repetition, like a loop 4.16.A.1.

public static void countdown(int n)
{
    if (n == 0)                  // base case: stop
    {
        System.out.println("Liftoff!");
    }
    else
    {
        System.out.println(n);
        countdown(n - 1);        // recursive call
    }
}

countdown(3);

Output:

3
2
1
Liftoff!

Each call moves n one step closer to the base case. Scope note: writing recursive code is outside the scope of the AP® Computer Science A exam. You must be able to read recursive methods and determine the result of calling them 4.16.A.1.

2. Tracing a method that returns a value

Each recursive call has its own set of local variables, including its parameters 4.16.A.2. A call that is waiting for a deeper call to return stays “paused” with its own values. Java keeps these paused calls on the call stack.

public static int sumDigits(int n)
{
    if (n < 10)
    {
        return n;
    }
    return n % 10 + sumDigits(n / 10);
}

System.out.println(sumDigits(4071));

Output:

12

Trace it in two directions. Going down, write each call. Coming back up, fill in each return value.

Call n Base case? Returns
sumDigits(4071) 4071 no 1 + sumDigits(407) = 1 + 11 = 12
sumDigits(407) 407 no 7 + sumDigits(40) = 7 + 4 = 11
sumDigits(40) 40 no 0 + sumDigits(4) = 0 + 4 = 4
sumDigits(4) 4 yes 4

There are four calls and four different values of n alive at the same time. When sumDigits(4) returns, sumDigits(40) continues with its own n, which is still 40.

3. Before or after the recursive call

Where a statement sits relative to the recursive call decides when it runs. A statement before the call runs on the way down. A statement after the call waits until every deeper call has returned, so it runs on the way back up, in reverse order 4.16.A.2.

public static void echo(String word)
{
    if (word.length() > 0)
    {
        System.out.print(word.substring(0, 1));       // before the call
        echo(word.substring(1));
        System.out.print(word.substring(0, 1));       // after the call
    }
}

echo("abc");
System.out.println();

Output:

abccba
Step What happens Printed so far
1 echo("abc") prints a, calls echo("bc") a
2 echo("bc") prints b, calls echo("c") ab
3 echo("c") prints c, calls echo("") abc
4 echo("") is the base case: does nothing, returns abc
5 back in echo("c"): prints c abcc
6 back in echo("bc"): prints b abccb
7 back in echo("abc"): prints a abccba

4. Parameters track progress, like a loop control variable

Parameter values capture the progress of a recursive process, much like the values of a loop control variable capture the progress of a loop 4.16.A.2. Some methods make more than one recursive call; draw a tree of calls and add the results from the bottom up.

public static int paths(int steps)
{
    if (steps <= 1)
    {
        return 1;
    }
    return paths(steps - 1) + paths(steps - 2);
}

System.out.println(paths(4));

Output:

5

Call tree for paths(4):

Call Makes the calls Returns
paths(4) paths(3) and paths(2) 3 + 2 = 5
paths(3) paths(2) and paths(1) 2 + 1 = 3
paths(2) paths(1) and paths(0) 1 + 1 = 2
paths(1), paths(0) base cases 1 each

In total paths(4) causes 9 calls, because paths(2) is computed twice and paths(1) three times.

5. Recursion and iteration are interchangeable

Any recursive solution can be replicated with an iterative approach (a loop), and any iterative solution can be written recursively 4.16.A.3. The parameter plays the role of the loop control variable, and the base case plays the role of the loop condition becoming false.

public static int powerRec(int base, int exp)
{
    if (exp == 0)
    {
        return 1;
    }
    return base * powerRec(base, exp - 1);
}

public static int powerLoop(int base, int exp)
{
    int result = 1;
    for (int i = exp; i > 0; i--)
    {
        result *= base;
    }
    return result;
}

System.out.println(powerRec(3, 4) + " " + powerLoop(3, 4));

Output:

81 81
Recursive version Iterative version
parameter exp loop control variable i
exp - 1 in the recursive call i--
base case exp == 0 loop stops when i > 0 is false

6. No base case reached: StackOverflowError

If the recursive calls never reach a base case, every call waits on the call stack until Java runs out of room 4.16.A.1. The program stops with an error.

public static int halve(int n)
{
    if (n == 1)
    {
        return 0;
    }
    return 1 + halve(n / 2);
}

System.out.println(halve(8));
System.out.println(halve(0));

Output:

3
Exception in thread "main" java.lang.StackOverflowError

halve(8) reaches the base case after three recursive calls (n is 8, then 4, 2 and 1). But halve(0) calls halve(0) again and again, because 0 / 2 is 0 and the base case n == 1 is never true.

Exam traps

  • Count the calls: mystery(4) that recurses on n - 1 down to a base case at 0 makes 5 calls, not 4.
  • Statements after the recursive call run in reverse order of the calls.
  • If the parameter never reaches the base case, the program stops with a StackOverflowError.
  • With two recursive calls in one return statement, the number of calls grows fast: draw a tree.

Simulator and checkpoint for 4.16 →

4.17 Recursive Searching and Sorting

Big ideaBinary search and merge sort both win by splitting the problem in half again and again.

1. Recursion over strings and collections

Recursion can traverse String objects, arrays and ArrayList objects 4.17.A.1. The parameter that changes is either an index that moves through the collection or a shorter version of the String. The base case is “nothing left to visit”.

public static int countVowels(String s)
{
    if (s.length() == 0)
    {
        return 0;
    }
    int first = 0;
    if ("aeiou".indexOf(s.substring(0, 1)) >= 0)
    {
        first = 1;
    }
    return first + countVowels(s.substring(1));
}

public static int maxFrom(int[] arr, int index)
{
    if (index == arr.length - 1)
    {
        return arr[index];
    }
    int restMax = maxFrom(arr, index + 1);
    if (arr[index] > restMax)
    {
        return arr[index];
    }
    return restMax;
}

public static String joinBackward(ArrayList<String> list, int index)
{
    if (index < 0)
    {
        return "";
    }
    return list.get(index) + " " + joinBackward(list, index - 1);
}

System.out.println(countVowels("banana"));
int[] heights = {152, 171, 166, 160};
System.out.println(maxFrom(heights, 0));
ArrayList<String> route = new ArrayList<String>();
route.add("home");
route.add("park");
route.add("school");
System.out.println(joinBackward(route, route.size() - 1));

Output:

3
171
school park home 

Trace of maxFrom(heights, 0): the calls go down with index 0, 1, 2, 3. The base case returns 160; index 2 compares 166 with 160 and returns 166; index 1 compares 171 with 166 and returns 171; index 0 compares 152 with 171 and returns 171.

2. Binary search: the idea and the precondition

Binary search only works on data in sorted order 4.17.B.1. It starts at the middle element of a sorted array or ArrayList. If the middle element is the target, it is done. If not, the target can only be in one half, so binary search eliminates the other half and repeats on what is left, until the desired value is found or all elements have been eliminated 4.17.B.1.

The algorithm keeps two indexes, low and high, around the part that is still possible, and checks mid = (low + high) / 2 (integer division). Binary search can be written either iteratively or recursively 4.17.B.3. Here is the iterative version, printing each step:

public static int binarySearch(int[] arr, int target)
{
    int low = 0;
    int high = arr.length - 1;
    while (low <= high)
    {
        int mid = (low + high) / 2;
        System.out.println("low " + low + ", high " + high + ", mid " + mid + ", arr[mid] " + arr[mid]);
        if (arr[mid] == target)
        {
            return mid;
        }
        else if (arr[mid] < target)
        {
            low = mid + 1;     // eliminate the left half
        }
        else
        {
            high = mid - 1;    // eliminate the right half
        }
    }
    return -1;
}

int[] ids = {3, 8, 14, 21, 27, 35, 42, 50, 61};
System.out.println("found at " + binarySearch(ids, 42));
System.out.println("found at " + binarySearch(ids, 10));

Output:

low 0, high 8, mid 4, arr[mid] 27
low 5, high 8, mid 6, arr[mid] 42
found at 6
low 0, high 8, mid 4, arr[mid] 27
low 0, high 3, mid 1, arr[mid] 8
low 2, high 3, mid 2, arr[mid] 14
found at -1

Trace of the search for 10:

Search for 10 low high mid arr[mid] Decision
step 1 0 8 4 27 10 < 27: high = 3
step 2 0 3 1 8 10 > 8: low = 2
step 3 2 3 2 14 10 < 14: high = 1
end 2 1 low > high: return -1

Watch out: on unsorted data the code still compiles and runs, but it can discard the half that holds the target and return -1 for a value that is in the array.

The recursive version passes low and high as parameters 4.17.B.3. Each recursive call eliminates half of the remaining elements 4.17.B.1. There are two base cases: the target is found, or low > high (nothing left).

public static int search(int[] arr, int target, int low, int high)
{
    if (low > high)
    {
        return -1;                                   // base case: nothing left
    }
    int mid = (low + high) / 2;
    System.out.println("search(" + low + ", " + high + ") checks index " + mid);
    if (arr[mid] == target)
    {
        return mid;                                  // base case: found
    }
    if (arr[mid] < target)
    {
        return search(arr, target, mid + 1, high);   // right half
    }
    return search(arr, target, low, mid - 1);        // left half
}

int[] ids = {3, 8, 14, 21, 27, 35, 42, 50, 61};
System.out.println("result " + search(ids, 61, 0, ids.length - 1));

Output:

search(0, 8) checks index 4
search(5, 8) checks index 6
search(7, 8) checks index 7
search(8, 8) checks index 8
result 8

The recursive version checks exactly the same middle elements as the iterative version would. The parameters low and high capture the progress, just like the two variables in the loop.

4. Why binary search is typically more efficient

Binary search is typically more efficient than linear search 4.17.B.2. Each check halves what is left, so the number of checks grows very slowly as the data grows.

public static int linearChecks(int[] arr, int target)
{
    int checks = 0;
    for (int i = 0; i < arr.length; i++)
    {
        checks++;
        if (arr[i] == target)
        {
            return checks;
        }
    }
    return checks;
}

public static int binaryChecks(int[] arr, int target)
{
    int checks = 0;
    int low = 0;
    int high = arr.length - 1;
    while (low <= high)
    {
        int mid = (low + high) / 2;
        checks++;
        if (arr[mid] == target)
        {
            return checks;
        }
        else if (arr[mid] < target)
        {
            low = mid + 1;
        }
        else
        {
            high = mid - 1;
        }
    }
    return checks;
}

int[] sizes = {8, 1000, 1000000};
for (int n : sizes)
{
    int[] data = new int[n];
    for (int i = 0; i < n; i++)
    {
        data[i] = i * 2;          // sorted even numbers
    }
    System.out.println(n + " elements, target absent: linear " + linearChecks(data, -1)
        + ", binary " + binaryChecks(data, -1));
}
int[] small = {2, 4, 6, 8, 10, 12, 14, 16};
System.out.println("target first: linear " + linearChecks(small, 2) + ", binary " + binaryChecks(small, 2));

Output:

8 elements, target absent: linear 8, binary 3
1000 elements, target absent: linear 1000, binary 9
1000000 elements, target absent: linear 1000000, binary 19
target first: linear 1, binary 3
Elements n Linear search, worst case Binary search, worst case
8 8 checks 4 checks
1,000 1,000 checks 10 checks
1,000,000 1,000,000 checks 20 checks

The worst case for binary search is about log2(n) + 1 checks (the run above used a target smaller than every element, which takes one check less than the worst case). The last line shows why binary search is only “typically” more efficient: when the target is the first element, linear search wins. Scope note: linear search and binary search are the only search algorithms on the AP® exam 4.17.B.2.

5. Merge sort

Merge sort is a recursive sorting algorithm for an array or ArrayList 4.17.C.1. It repeatedly divides the array into smaller subarrays until each subarray has one element, which is already sorted. Then it recursively merges the sorted subarrays back together, in sorted order, to form the final sorted array 4.17.C.2.

To merge two sorted subarrays, compare their front elements, copy the smaller one into a temporary array, and repeat; when one side runs out, copy the rest of the other side.

public static void mergeSort(int[] arr)
{
    int[] temp = new int[arr.length];
    sortRange(arr, temp, 0, arr.length - 1);
}

public static void sortRange(int[] arr, int[] temp, int from, int to)
{
    if (from >= to)
    {
        return;                                  // one element: already sorted
    }
    int middle = (from + to) / 2;
    sortRange(arr, temp, from, middle);          // sort the left half
    sortRange(arr, temp, middle + 1, to);        // sort the right half
    merge(arr, temp, from, middle, to);          // merge the two sorted halves
}

public static void merge(int[] arr, int[] temp, int from, int middle, int to)
{
    int i = from;
    int j = middle + 1;
    int k = from;
    while (i <= middle && j <= to)
    {
        if (arr[i] <= arr[j])
        {
            temp[k] = arr[i];
            i++;
        }
        else
        {
            temp[k] = arr[j];
            j++;
        }
        k++;
    }
    while (i <= middle)
    {
        temp[k] = arr[i];
        i++;
        k++;
    }
    while (j <= to)
    {
        temp[k] = arr[j];
        j++;
        k++;
    }
    String merged = "";
    for (k = from; k <= to; k++)
    {
        arr[k] = temp[k];
        merged += arr[k] + " ";
    }
    System.out.println("merge " + from + " to " + to + ": " + merged);
}

int[] scores = {38, 16, 27, 5, 49, 12, 30, 21};
mergeSort(scores);

Output:

merge 0 to 1: 16 38 
merge 2 to 3: 5 27 
merge 0 to 3: 5 16 27 38 
merge 4 to 5: 12 49 
merge 6 to 7: 21 30 
merge 4 to 7: 12 21 30 49 
merge 0 to 7: 5 12 16 21 27 30 38 49 

Divide, then merge. Read the divide phase top to bottom and the merge phase in the order the merges happen:

Phase Subarrays
start [38 16 27 5 49 12 30 21]
divide [38 16 27 5] [49 12 30 21]
divide [38 16] [27 5] [49 12] [30 21]
divide (one element each) [38] [16] [27] [5] [49] [12] [30] [21]
merge 1 [16 38] [27] [5] [49] [12] [30] [21]
merge 2 [16 38] [5 27] [49] [12] [30] [21]
merge 3 [5 16 27 38] [49] [12] [30] [21]
merge 4 [5 16 27 38] [12 49] [30] [21]
merge 5 [5 16 27 38] [12 49] [21 30]
merge 6 [5 16 27 38] [12 21 30 49]
merge 7 [5 12 16 21 27 30 38 49]

Watch out: because the left half is sorted completely before the right half starts, the whole left half [5 16 27 38] is finished (merge 3) before 49 and 12 are merged (merge 4).

Merging [5 16 27 38] with [12 21 30 49] (the last merge) step by step:

Compare Smaller goes to temp temp so far
5 vs 12 5 5
16 vs 12 12 5 12
16 vs 21 16 5 12 16
27 vs 21 21 5 12 16 21
27 vs 30 27 5 12 16 21 27
38 vs 30 30 5 12 16 21 27 30
38 vs 49 38 5 12 16 21 27 30 38
left side empty copy 49 5 12 16 21 27 30 38 49

Informally, merge sort does about n times log2(n) steps (log2(n) levels of merging, about n steps per level). For 8 elements that is about 24 steps, and for 1,000 elements about 10,000, compared with 499,500 comparisons for selection sort. Scope note: selection sort, insertion sort and merge sort are the only sorting algorithms on the AP® exam 4.17.C.1.

Exam traps

  • mid = (low + high) / 2 uses integer division: (0 + 7) / 2 is 3, not 3.5.
  • After checking mid, the next range is low to mid - 1 or mid + 1 to high; mid itself is never checked again.
  • Binary search on unsorted data compiles and runs, but can return a wrong result.
  • Merge sort questions ask for the subarrays after a given merge: trace the left half completely before the right half.
  • The only searches in scope are linear and binary search; the only sorts are selection, insertion and merge sort.

Simulator and checkpoint for 4.17 →

Notes done

Now check yourself

Close the notes and take each topic's checkpoint. Retrieval practice works better for memory than reading again.

Start with the 4.1 checkpoint →

Was this page helpful?

No names are ever shown, and every public comment is checked by us first. Please do not include personal details. To get a reply, email [email protected].

↑ Top

Enlarged view of the selected figure