AP Computer Science A · 鼎睿学苑

Unit 4: Data Collections

单元 4:数据集合

Arrays, ArrayLists, 2D arrays, text files, wrapper classes, searching & sorting algorithms, and recursion.

数组、ArrayList、二维数组、文本文件、包装类、查找与排序算法,以及递归。

30–40% of AP Exam 占 AP 考试 30–40% ~50–52 Class Periods 约 50–52 课时 17 Topics 17 个小节

Ethical and Social Issues Around Data Collection

数据收集中的伦理与社会问题

Before diving into data structures, consider the ethical implications of collecting and storing data. Every program that handles personal information must prioritize user privacy.

在深入数据结构之前,先思考收集和存储数据所带来的伦理影响。任何处理个人信息的程序都必须把用户隐私放在首位。

Privacy Risks: When using a computer, personal privacy is at risk. Programmers should attempt to safeguard the personal privacy of users when developing new programs.

Algorithmic Bias: Systemic and repeated errors in a program that create unfair outcomes for a specific group of users. Programmers should be aware of the data set collection method and the potential for bias.

Data Quality: Some data sets are incomplete or contain inaccurate data. Using such data can cause programs to work incorrectly or inefficiently.

隐私风险:使用计算机时,个人隐私存在被侵犯的风险。开发新程序时,程序员应尽力保护用户的个人隐私。

算法偏见:程序中存在的系统性、重复性错误,会对特定用户群体产生不公平的结果。程序员应了解数据集的收集方式以及潜在的偏见。

数据质量:有些数据集不完整或包含不准确的数据。使用这类数据可能导致程序运行不正确或效率低下。

Choose the Right Data Set Contents of a data set might be related to a specific question or topic and might not be appropriate for a different question or topic. Always verify that the data you're using actually addresses the problem at hand.
选择合适的数据集 一个数据集的内容可能只针对某个特定问题或主题,未必适用于其他问题或主题。请始终核实你所使用的数据是否真正能回答当前要解决的问题。
What is algorithmic bias?
什么是算法偏见?
A type of sorting error in algorithms算法中的一类排序错误
Systemic and repeated errors that create unfair outcomes for a specific group对特定群体造成不公平结果的系统性、重复性错误
When a program runs slower than expected程序运行速度比预期慢
Using too many nested loops使用了过多嵌套循环
Correct! Algorithmic bias describes systemic errors in a program that produce unfair outcomes for certain groups of users.
正确!算法偏见指的是程序中导致特定用户群体获得不公平结果的系统性错误。
Algorithmic bias = systemic and repeated errors creating unfair outcomes for a specific group of users.
算法偏见 = 对特定用户群体造成不公平结果的系统性、重复性错误。

Introduction to Using Data Sets

使用数据集入门

A data set is a collection of specific pieces of information or data. Data sets can be manipulated and analyzed to solve a problem or answer a question.

数据集是一组具体的信息或数据。我们可以对数据集进行操作和分析,以解决问题或回答某个问题。

Processing Data: When analyzing data sets, values within the set are accessed and utilized one at a time, then processed according to the desired outcome.

Visual Planning: Data can be represented in a diagram by using a chart or table. This visual can be used to plan the algorithm that will be used to manipulate the data.

处理数据:分析数据集时,集合中的值会被逐个访问、逐个使用,然后按所需结果进行处理。

可视化规划:可以用图表或表格把数据画出来。这种可视化形式可以用来规划接下来操作数据所要使用的算法。

Worked Example: Plan from a data table

Suppose the question is: “How many school days reached at least 20°C?” Represent the everyday data first:

DayMonTueWed
Temperature (°C)182119

Written algorithm: set count to 0 → inspect one temperature at a time → if it is at least 20, add 1 to count → after the last value, report count. For this table the result is 1. The table identifies the values to visit; the arrows describe the traversal and decision before any Java collection is chosen.

例题:根据数据表规划算法

假设问题是:“一周中有多少个上课日的最高气温至少为 20°C?”先用表格表示日常数据:

日期周一周二周三
气温(°C)182119

文字算法:令 count 为 0 → 每次检查一个气温 → 如果气温至少为 20,就把 count 加 1 → 检查完最后一个值后报告 count。这张表的结果是 1。表格确定要访问的数据,箭头步骤在选择具体 Java 集合之前说明了遍历和判断过程。


Array Creation and Access

数组的创建与访问

An array stores multiple values of the same type. The values can be either primitive values or object references. An array's length is established at creation and cannot be changed.

数组(array)存储多个相同类型的值。这些值既可以是基本类型的值,也可以是对象引用。数组的长度(length)在创建时就确定,之后不能改变。

Creating Arrays

创建数组

// Method 1: Using the new keyword// 方法 1:使用 new 关键字
int[] numbers = new int[5];          // {0, 0, 0, 0, 0}
double[] grades = new double[3];     // {0.0, 0.0, 0.0}
boolean[] flags = new boolean[4];   // {false, false, false, false}
String[] names = new String[3];     // {null, null, null}

// Method 2: Using an initializer list// 方法 2:使用初始化列表
int[] primes = {2, 3, 5, 7, 11};
String[] days = {"Mon", "Tue", "Wed"};
Default Values When created with new, elements are initialized to defaults: int → 0, double → 0.0, boolean → false, reference types → null.
默认值 使用 new 创建时,元素会被初始化(initialization)为默认值:int → 0,double → 0.0,boolean → false,引用类型 → null。

Accessing & Modifying Elements

访问与修改元素

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

// Access using index (0-based)// 使用索引访问(从 0 开始)
int first = arr[0];     // 10
int last = arr[4];      // 50

// Modify an element// 修改一个元素
arr[2] = 99;             // arr is now {10, 20, 99, 40, 50}// arr 现在是 {10, 20, 99, 40, 50}

// Array length (attribute, not method!)// 数组长度(是字段,不是方法!)
int len = arr.length;   // 5
ArrayIndexOutOfBoundsException Valid indices are 0 through array.length - 1. Accessing an index outside this range throws an ArrayIndexOutOfBoundsException.
ArrayIndexOutOfBoundsException 合法的索引(array index)范围是 0 到 array.length - 1。访问该范围之外的索引会抛出越界(out-of-bounds)异常 ArrayIndexOutOfBoundsException。
What is the value of arr[2] after int[] arr = new int[5];?
执行 int[] arr = new int[5]; 之后,arr[2] 的值是多少?
null
2
0
An exception is thrown抛出异常
Correct! int arrays default to 0. Index 2 is valid (0–4), so the value is 0.
正确!int 数组的默认值为 0。索引 2 在合法范围内(0–4),因此值为 0。
When an int array is created with new, all elements default to 0.
用 new 创建 int 数组时,所有元素的默认值都是 0。
AP Trap: length on arrays vs length() on Strings vs size() on ArrayLists Arrays use arr.length, no parentheses, because length is a public field, not a method. Strings use s.length(); ArrayLists use list.size(). Writing arr.length() or s.length is a compile error and shows up on MCQs as a "which line fails to compile?" trap.
AP 陷阱:数组的 length、String 的 length() 与 ArrayList 的 size() 数组使用 arr.length,没有括号,因为 length 是公有字段而不是方法。String 使用 s.length();ArrayList 使用 list.size()。写成 arr.length() 或 s.length 都是编译错误,常以"下列哪一行无法编译?"的形式出现在选择题中。

Array Traversals

数组遍历

Traversing an array means using repetition statements to access all or an ordered sequence of elements.

遍历(traverse)数组是指用循环语句访问数组的所有元素或一段有序的元素。

Indexed for Loop

带索引的 for 循环

int[] arr = {3, 7, 1, 9, 5};
for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + " ");
}
// Output: 3 7 1 9 5// 输出:3 7 1 9 5

Enhanced for Loop (for-each)

增强 for 循环(enhanced for loop / for-each)

for (int val : arr) {
    System.out.print(val + " ");
}
// Output: 3 7 1 9 5// 输出:3 7 1 9 5
Enhanced for Loop = Copy Only The enhanced for loop variable is a copy of the element. Assigning a new value to it does not change the array. However, if the array stores objects, you can call methods on the loop variable to modify the object's attributes.
增强 for 循环 = 仅是副本 增强 for 循环的循环变量是元素的副本。给它赋新值不会改变数组本身。不过,如果数组存的是对象,你可以在循环变量上调用方法来修改对象的属性。
FeatureIndexed for LoopEnhanced for Loop
Access by index✅ Yes❌ No
Modify array elements✅ Yes❌ No (copies only)
Traverse in reverse✅ Yes❌ No
Traverse partial array✅ Yes❌ No (full traversal)
Simpler syntax❌ More verbose✅ Cleaner
特性带索引的 for 循环增强 for 循环
通过索引访问✅ 可以❌ 不可以
修改数组元素✅ 可以❌ 不可以(只是副本)
反向遍历✅ 可以❌ 不可以
遍历部分数组✅ 可以❌ 不可以(必须完整遍历)
语法更简洁❌ 较冗长✅ 更简洁
Worked Example: Enhanced for can't modify primitive arrays
int[] arr = {1, 2, 3};

// Doesn't work, val is a copy// 不起作用,val 是副本
for (int val : arr) {
    val *= 2;
}
// arr is still {1, 2, 3}// arr 仍是 {1, 2, 3}

// Works, indexed for assigns through the array reference// 有效,带索引的 for 通过数组引用赋值
for (int i = 0; i < arr.length; i++) {
    arr[i] *= 2;
}
// arr is now {2, 4, 6}// arr 现在是 {2, 4, 6}

Why this matters: the enhanced for loop binds a copy of each value (for primitives) or a copy of each reference (for objects) to the loop variable. Assigning to that variable replaces the local copy and nothing else. To mutate the array itself, you need the index so you can write through arr[i] = ....

Object arrays are different: if the array holds object references, the enhanced for variable holds a reference to the same object. Calling a mutator method on it (e.g., account.deposit(10)) does change the object the array still points to. Only reassigning the variable (account = new Account()) leaves the array unchanged.

例题:增强 for 无法修改基本类型数组
int[] arr = {1, 2, 3};

// Doesn't work, val is a copy// 不起作用,val 是副本
for (int val : arr) {
    val *= 2;
}
// arr is still {1, 2, 3}// arr 仍是 {1, 2, 3}

// Works, indexed for assigns through the array reference// 有效,带索引的 for 通过数组引用赋值
for (int i = 0; i < arr.length; i++) {
    arr[i] *= 2;
}
// arr is now {2, 4, 6}// arr 现在是 {2, 4, 6}

为什么这很重要:增强 for 循环把每个值的副本(基本类型)或每个引用的副本(对象类型)绑定到循环变量上。给该变量赋值只会替换本地的副本,对其他东西没有影响。要真正修改数组本身,必须通过索引写入 arr[i] = ...。

对象数组则不同:如果数组存的是对象引用,增强 for 的循环变量持有的是同一对象的引用。调用其上的修改方法(例如 account.deposit(10))会改变数组所指向的同一个对象。只有重新赋值变量(account = new Account())时,数组才不会受到影响。


Implementing Array Algorithms

实现数组算法

Standard algorithms that traverse arrays to accomplish common tasks are fundamental to AP CSA.

通过遍历数组完成常见任务的标准算法,是 AP CSA 的基础内容。

findMax and average below assume a nonempty array: findMax reads arr[0], and average divides by arr.length.

下面的 findMax 和 average 假定数组非空:findMax 会读取 arr[0],而 average 会除以 arr.length。

Find Minimum / Maximum求最小值 / 最大值
public static int findMax(int[] arr) {
    int max = arr[0];
    for (int val : arr) {
        if (val > max) max = val;
    }
    return max;
}
Compute Sum / Average计算总和 / 平均值
public static double average(int[] arr) {
    int sum = 0;
    for (int val : arr) {
        sum += val;
    }
    return (double) sum / arr.length;
}
Count / Check Elements with a Property统计 / 判断具有某种性质的元素
// Count elements greater than a threshold// 统计大于阈值的元素
public static int countAbove(int[] arr, int threshold) {
    int count = 0;
    for (int val : arr) {
        if (val > threshold) count++;
    }
    return count;
}

// Check if ALL elements are positive// 判断是否所有元素都为正
public static boolean allPositive(int[] arr) {
    for (int val : arr) {
        if (val <= 0) return false;
    }
    return true;
}

// Check if ANY element is negative// 判断是否存在负数元素
public static boolean hasNegative(int[] arr) {
    for (int val : arr) {
        if (val < 0) return true;
    }
    return false;
}
Shift / Rotate / Reverse Elements元素的平移 / 循环移位 / 反转

Shift discards an end value and opens a position for a replacement. Rotate saves that end value and moves it to the opposite end. The loop direction matters: when copying right, work from right to left so a source value is not overwritten before it is copied. These examples assume the array has at least one element.

平移会丢弃一端的值,并在另一端腾出位置放入替代值;循环移位会先保存这一端的值,再把它放到另一端。循环方向很重要:向右复制时必须从右向左处理,避免源值在复制前被覆盖。以下示例假定数组至少有一个元素。

// Shift left: {2, 4, 6, 8} becomes {4, 6, 8, 0}// 左移:{2, 4, 6, 8} 变为 {4, 6, 8, 0}
public static void shiftLeft(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++)
        arr[i] = arr[i + 1];
    arr[arr.length - 1] = 0;
}

// Shift right: {2, 4, 6, 8} becomes {0, 2, 4, 6}// 右移:{2, 4, 6, 8} 变为 {0, 2, 4, 6}
public static void shiftRight(int[] arr) {
    for (int i = arr.length - 1; i > 0; i--)
        arr[i] = arr[i - 1];
    arr[0] = 0;
}

// Rotate left: {2, 4, 6, 8} becomes {4, 6, 8, 2}// 左循环移位:{2, 4, 6, 8} 变为 {4, 6, 8, 2}
public static void rotateLeft(int[] arr) {
    int first = arr[0];
    for (int i = 0; i < arr.length - 1; i++)
        arr[i] = arr[i + 1];
    arr[arr.length - 1] = first;
}

// Rotate right: {2, 4, 6, 8} becomes {8, 2, 4, 6}// 右循环移位:{2, 4, 6, 8} 变为 {8, 2, 4, 6}
public static void rotateRight(int[] arr) {
    int last = arr[arr.length - 1];
    for (int i = arr.length - 1; i > 0; i--)
        arr[i] = arr[i - 1];
    arr[0] = last;
}

// Reverse in place: swap matching positions from the two ends// 原地反转:交换从两端向中间对应的位置
public static void reverse(int[] arr) {
    for (int i = 0; i < arr.length / 2; i++) {
        int temp = arr[i];
        arr[i] = arr[arr.length - 1 - i];
        arr[arr.length - 1 - i] = temp;
    }
}
Detect Duplicates / Consecutive Pairs检测重复元素 / 相邻元素对
// Check for duplicates// 检测重复元素
public static boolean hasDuplicates(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[i] == arr[j]) return true;
        }
    }
    return false;
}

// Access consecutive pairs// 访问相邻元素对
for (int i = 0; i < arr.length - 1; i++) {
    System.out.println(arr[i] + " and " + arr[i + 1]);
}
Exam Tip On the AP Exam, you may need to combine several of these patterns. Practice writing algorithms that find min/max, sum, count, and check properties, both with indexed and enhanced for loops.
考试提示 在 AP 考试中,你可能需要把上述几种套路组合起来使用。请用带索引的 for 循环和增强 for 循环分别练习编写求最值、求和、计数以及判断性质的算法。

Using Text Files

使用文本文件

A file provides persistent storage, data survives after the program ends. Java uses the File and Scanner classes to read from text files.

文件提供持久化存储,数据在程序结束后依然保留。Java 使用 File 和 Scanner 类来读取文本文件。

Reading from a File

从文件读取内容

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

public static int countTokens() throws IOException {
    File file = new File("data.txt");
    Scanner input = new Scanner(file);
    int count = 0;

    while (input.hasNext()) {
        String token = input.next();
        count++;
    }
    input.close();
    return count;
}

This assessed pattern uses hasNext() to test whether another item exists, next() to consume exactly one token, and close() when the file is finished. If the pathname cannot be opened, declaring throws IOException is one way to specify that the program terminates instead of continuing with missing input.

这个考试范围内的模式用 hasNext() 判断是否还有下一个项目,用 next() 恰好读取一个 token,并在文件使用完毕后调用 close()。如果路径无法打开,在方法头声明 throws IOException 是一种处理方式:程序终止,而不是在缺少输入的情况下继续运行。

Scanner Methods, Quick Reference
MethodReturnsDescription
nextInt()intReads the next int; if the next int does not exist or is out of range, it results in an InputMismatchException
nextDouble()doubleReads the next double; if the next double does not exist, it results in an InputMismatchException
nextBoolean()booleanReads the next boolean; if the next boolean does not exist, it results in an InputMismatchException
nextLine()StringReads the next line; can return "" immediately after another Scanner method
next()StringReads next token (word)
hasNext()booleanTrue if more data remains
close()voidCloses the scanner/file
Scanner 方法速查
方法返回值说明
nextInt()int读取下一个 int;如果下一个 int 不存在或超出范围,会产生 InputMismatchException
nextDouble()double读取下一个 double;如果下一个 double 不存在,会产生 InputMismatchException
nextBoolean()boolean读取下一个 boolean;如果下一个 boolean 不存在,会产生 InputMismatchException
nextLine()String读取下一整行;紧接在另一个 Scanner 方法之后调用时可能返回 ""
next()String读取下一个 token(单词)
hasNext()boolean是否还有更多数据
close()void关闭扫描器/文件

The split() Method

split() 方法

String line = "Alice,90,85,92";
String[] parts = line.split(",");
// parts = {"Alice", "90", "85", "92"}// parts = {"Alice", "90", "85", "92"}
File and Input Assumptions A method that opens a file must indicate what happens if the file cannot be opened. Adding throws IOException to its header is one supported approach. File and IOException are in java.io; Scanner is in java.util. The Java Quick Reference specifies when nextInt, nextDouble, and nextBoolean result in an InputMismatchException.
文件与输入假设 打开文件的方法必须说明文件无法打开时如何处理。在方法头加入 throws IOException 是一种受支持的做法。File 和 IOException 位于 java.io,Scanner 位于 java.util。Java Quick Reference 明确规定了 nextInt、nextDouble 和 nextBoolean 在何种情况下会产生 InputMismatchException。
Current AP Scope Boundaries Keyboard input is outside the AP CSA course and exam. Although nextLine() can behave differently from token-reading methods because of whitespace, you will not be asked to write or analyze code that mixes nextLine() with other Scanner methods on the same input source. The delimiter passed to split is technically a regular expression, but special regular-expression behavior such as \* or \. is also outside scope; use ordinary delimiters such as "," in assessed examples.
当前 AP 范围边界 键盘输入不在 AP CSA 课程与考试范围内。由于空白字符的处理方式不同,nextLine() 与读取 token 的方法混用时可能出现特殊行为;但考试不会要求你编写或分析在同一输入源上混用 nextLine() 和其他 Scanner 方法的代码。split 的分隔符从技术上说是正则表达式,但 \*、\. 等正则表达式特殊性质也不在范围内;考试示例应使用 "," 这类普通分隔符。

Wrapper Classes

包装类(wrapper class)

The Integer and Double classes wrap primitive types into objects. Both are immutable, once created, their values cannot change.

Integer 和 Double 类把基本类型包装成对象。它们都是不可变的——一旦创建,值就不能再改变。

Autoboxing & Unboxing

自动装箱(autoboxing)与拆箱(unboxing)

// Autoboxing: primitive → wrapper object// 自动装箱:基本类型 → 包装对象
Integer x = 5;          // int → Integer// int → Integer
Double y = 3.14;        // double → Double// double → Double

// Unboxing: wrapper object → primitive// 拆箱:包装对象 → 基本类型
int a = x;              // Integer → int// Integer → int
double b = y;           // Double → double// Double → double

The compiler also boxes and unboxes method arguments when the parameter type requires the corresponding conversion:

当方法形参需要对应类型时,编译器也会对实参自动装箱或拆箱:

public static void printInteger(Integer value) {
    System.out.println(value);
}
public static void printInt(int value) {
    System.out.println(value);
}

int primitive = 8;
Integer wrapped = 12;
printInteger(primitive);    // autoboxes int to Integer// 把 int 自动装箱为 Integer
printInt(wrapped);  // unboxes Integer to int// 把 Integer 自动拆箱为 int

Parsing Strings to Numbers

把字符串解析成数字

int num = Integer.parseInt("42");       // "42" → 42// "42" → 42
double d = Double.parseDouble("3.14"); // "3.14" → 3.14// "3.14" → 3.14
Why Wrapper Classes? ArrayLists can only store objects, not primitives. Wrapper classes let you store int and double values in an ArrayList via autoboxing: ArrayList<Integer>.
为什么需要包装类? ArrayList 只能存对象,不能存基本类型。包装类通过自动装箱让你能够把 int 和 double 值存进 ArrayList,例如 ArrayList<Integer>。

ArrayList Methods

ArrayList 的方法

An ArrayList is a mutable-size collection that stores object references. Unlike arrays, it can grow and shrink dynamically.

ArrayList 是一种大小可变的集合,用来存储对象引用。与数组不同,它可以动态地增大或缩小。

Creating an ArrayList

创建 ArrayList

import java.util.ArrayList;

ArrayList<String> names = new ArrayList<String>();
ArrayList<Integer> nums = new ArrayList<Integer>();
ArrayList Methods, Quick Reference
MethodReturnsDescription
size()intNumber of elements
add(E obj)booleanAppends obj to end; returns true
add(int i, E obj)voidInserts obj at index i; shifts others right
get(int i)EReturns element at index i
set(int i, E obj)EReplaces element at i; returns old element
remove(int i)ERemoves at i; shifts others left; returns removed
ArrayList 方法速查
方法返回值说明
size()int元素个数
add(E obj)boolean把 obj 追加到末尾;返回 true
add(int i, E obj)void在索引 i 处插入 obj;其后元素整体右移
get(int i)E返回索引 i 处的元素
set(int i, E obj)E替换索引 i 处的元素;返回原元素
remove(int i)E删除索引 i 处的元素;其后元素整体左移;返回被删元素
Index Ranges For a list of size n, get, set, and remove require an existing index from 0 through n - 1. Indexed add accepts 0 through n, because index n appends after the current last element. An index outside the method's valid range results in an IndexOutOfBoundsException.
索引范围 对大小为 n 的列表,get、set 和 remove 必须使用已有索引 0 到 n - 1。带索引的 add 可使用 0 到 n,因为索引 n 表示追加到当前最后一个元素之后。索引超出相应方法的有效范围会导致 IndexOutOfBoundsException。
ArrayList<String> list = new ArrayList<String>();
list.add("A");          // ["A"]
list.add("B");          // ["A", "B"]
list.add(1, "X");      // ["A", "X", "B"]
list.set(0, "Z");      // ["Z", "X", "B"]
list.remove(1);        // ["Z", "B"]
String s = list.get(0); // "Z"
int sz = list.size();   // 2
Array vs ArrayList, Key Differences
FeatureArrayArrayList
SizeFixed at creationDynamic (grows/shrinks)
StoresPrimitives or objectsObjects only (use wrappers)
Accessarr[i]list.get(i)
Lengtharr.lengthlist.size()
Modifyarr[i] = vallist.set(i, val)
Insert/DeleteManual shiftingBuilt-in methods
数组 vs ArrayList:关键差异
特性数组ArrayList
大小创建时固定动态(可增可缩)
存储内容基本类型或对象只能存对象(使用包装类)
访问方式arr[i]list.get(i)
长度arr.lengthlist.size()
修改arr[i] = vallist.set(i, val)
插入 / 删除手动移动元素内置方法
After list.add(1, "X") on list ["A", "B", "C"], what is the list?
对列表 ["A", "B", "C"] 执行 list.add(1, "X") 之后,列表变成什么?
["X", "A", "B", "C"]
["A", "X", "B", "C"]
["A", "B", "X", "C"]
["A", "B", "C", "X"]
Correct! add(1, "X") inserts "X" at index 1, shifting "B" and "C" to the right.
正确!add(1, "X") 在索引 1 处插入 "X","B" 和 "C" 整体右移。
add(1, "X") inserts at index 1: elements at index 1+ shift right.
add(1, "X") 在索引 1 处插入:索引 1 及之后的元素整体右移。

ArrayList Traversals

ArrayList 遍历

Traversing an ArrayList works similarly to arrays but with critical pitfalls when modifying the list during traversal.

遍历 ArrayList 的方式与遍历数组类似,但在遍历过程中修改列表时存在严重的坑。

// Indexed for loop// 带索引的 for 循环
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// Enhanced for loop// 增强 for 循环
for (String s : list) {
    System.out.println(s);
}
Critical Pitfalls ConcurrentModificationException: Do NOT add or remove elements while using an enhanced for loop. Use an indexed for loop instead.

Skipping elements on removal: When removing elements with an indexed for loop, either traverse backward or decrement the index after removal.
关键陷阱 ConcurrentModificationException:在增强 for 循环中千万不要添加或删除元素。请改用带索引的 for 循环。

删除时跳过元素:在带索引的 for 循环中删除元素时,要么倒序遍历,要么在删除后将索引减一。

Safe Removal During Traversal

遍历过程中的安全删除

// Method 1: Traverse backward// 方法 1:倒序遍历
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("remove me")) {
        list.remove(i);
    }
}

// Method 2: Decrement index after removal// 方法 2:删除后将索引减一
for (int i = 0; i < list.size(); i++) {
    if (list.get(i).equals("remove me")) {
        list.remove(i);
        i--;  // adjust to not skip next element// 调整以免跳过下一个元素
    }
}
Worked Example: Why forward removal skips elements

Start with list = ["a", "x", "x", "b"] and try to remove every "x" with a forward indexed loop and no i-- adjustment:

ilist at startget(i)actionlist at end
0["a", "x", "x", "b"]"a"keep["a", "x", "x", "b"]
1["a", "x", "x", "b"]"x"remove → shift left["a", "x", "b"]
2["a", "x", "b"]"b"keep, ❌ skipped the "x" that slid to index 1["a", "x", "b"]

Final list: ["a", "x", "b"]. One "x" was never removed because the remaining elements shifted left under the loop cursor. The two fixes shown above (i-- after removal, or traverse backward) both keep every element exactly under inspection once.

例题:为什么正向删除会跳过元素

从 list = ["a", "x", "x", "b"] 开始,用正向带索引循环且不加 i-- 调整地删除所有 "x":

i开始时的 listget(i)动作结束时的 list
0["a", "x", "x", "b"]"a"保留["a", "x", "x", "b"]
1["a", "x", "x", "b"]"x"删除 → 左移["a", "x", "b"]
2["a", "x", "b"]"b"保留,❌ 跳过了滑到索引 1 的那个 "x"["a", "x", "b"]

最终列表:["a", "x", "b"]。有一个 "x" 始终没被删除,因为后续元素在循环游标下方左移了。上面给出的两种修复方法(删除后执行 i--,或倒序遍历)都能保证每个元素正好被检查一次。

AP Scope: Changing Size During Enhanced for Adding or removing elements changes an ArrayList's size and, during an enhanced for traversal, can result in a ConcurrentModificationException. Therefore, do not add or remove elements in that loop form. Within the AP Java Quick Reference, use an indexed loop: traverse backward for deletion, or adjust the index after a forward deletion so the shifted element is not skipped. The Topic 4.8 index ranges still apply during traversal; an invalid indexed access results in an IndexOutOfBoundsException.
AP 范围:在增强 for 中改变大小 添加或删除元素会改变 ArrayList 的大小;在增强 for 遍历期间这样做可能导致 ConcurrentModificationException。因此,不要在这种循环中添加或删除元素。在 AP Java Quick Reference 范围内应使用带索引的循环:删除时倒序遍历,或在正向删除后调整索引,避免跳过左移后的元素。遍历时仍须遵守小节 4.8 的索引范围;无效的索引访问会导致 IndexOutOfBoundsException。

Implementing ArrayList Algorithms

实现 ArrayList 算法

The same standard algorithms from arrays apply to ArrayLists, plus the ability to insert and delete elements. Some algorithms require traversing multiple String, array, or ArrayList objects simultaneously.

数组上的那套标准算法同样适用于 ArrayList,并且还可以插入和删除元素。有些算法需要同时遍历多个 String、数组或 ArrayList 对象。

Required algorithm familyArrayList pattern
Minimum/maximum; sum/averageAccumulator updated from get(i)
At least one; all; countBoolean return or counter inside a traversal
Consecutive pairsCompare get(i) with get(i + 1); stop at size() - 1
DuplicatesCompare each element with elements at later indices
Shift/rotate; reverseUse get/set and choose a direction that does not overwrite a needed value
Insert/deleteUse indexed add/remove and account for shifted indices
Simultaneous traversalUse one index to access corresponding positions in multiple collections
必备算法类别ArrayList 模式
最小/最大值;总和/平均值用 get(i) 读取元素并更新累积变量
至少一个;全部;计数在遍历中返回布尔值或更新计数器
相邻元素对比较 get(i) 与 get(i + 1);循环在 size() - 1 前停止
重复元素把每个元素与它后面索引位置的元素比较
平移/循环移位;反转使用 get/set,并选择不会提前覆盖所需值的方向
插入/删除使用带索引的 add/remove,同时考虑索引移动
同步遍历用同一个索引访问多个集合中的对应位置
Aggregates, Any/All, and Count聚合、任意/全部与计数

findMax and average assume a nonempty list. Notice how each pattern differs only in its initial value, update, and final return.

findMax 和 average 假定列表非空。注意这些模式的差别主要在初始值、更新方式和最终返回值。

public static int findMax(ArrayList<Integer> nums) {
    int max = nums.get(0);
    for (int i = 1; i < nums.size(); i++)
        if (nums.get(i) > max) max = nums.get(i);
    return max;
}

public static double average(ArrayList<Integer> nums) {
    int sum = 0;
    for (int value : nums) sum += value;
    return (double) sum / nums.size();
}

public static boolean hasNegative(ArrayList<Integer> nums) {
    for (int value : nums)
        if (value < 0) return true;   // at least one// 至少一个
    return false;
}

public static boolean allPositive(ArrayList<Integer> nums) {
    for (int value : nums)
        if (value <= 0) return false;  // one counterexample// 一个反例即可
    return true;
}

public static int countEven(ArrayList<Integer> nums) {
    int count = 0;
    for (int value : nums)
        if (value % 2 == 0) count++;
    return count;
}
Consecutive Pairs and Duplicates相邻元素对与重复元素
public static boolean hasAdjacentRepeat(ArrayList<String> words) {
    for (int i = 0; i < words.size() - 1; i++)
        if (words.get(i).equals(words.get(i + 1)))
            return true;
    return false;
}

public static boolean hasDuplicate(ArrayList<String> words) {
    for (int i = 0; i < words.size(); i++)
        for (int j = i + 1; j < words.size(); j++)
            if (words.get(i).equals(words.get(j)))
                return true;
    return false;
}

The first method examines only consecutive pairs. The nested-loop method checks every later position, so it detects duplicates anywhere in the list.

第一个方法只检查相邻元素对;嵌套循环方法会检查每个元素之后的所有位置,因此能发现列表中任意位置的重复元素。

Shift, Rotate, and Reverse平移、循环移位与反转

These examples assume a nonempty list. As with arrays, a shift discards a value; a rotation preserves it at the opposite end.

以下示例假定列表非空。与数组相同,平移会丢弃一个值;循环移位会把它保留到另一端。

// Shift left, filling the final position with 0// 左移,并用 0 填充最后一个位置
public static void shiftLeft(ArrayList<Integer> nums) {
    for (int i = 0; i < nums.size() - 1; i++)
        nums.set(i, nums.get(i + 1));
    nums.set(nums.size() - 1, 0);
}

// Rotate right: save the last value before shifting right// 右循环移位:向右移动前先保存最后一个值
public static void rotateRight(ArrayList<Integer> nums) {
    int last = nums.get(nums.size() - 1);
    for (int i = nums.size() - 1; i > 0; i--)
        nums.set(i, nums.get(i - 1));
    nums.set(0, last);
}

public static void reverse(ArrayList<Integer> nums) {
    for (int i = 0; i < nums.size() / 2; i++) {
        int opposite = nums.size() - 1 - i;
        int temp = nums.get(i);
        nums.set(i, nums.get(opposite));
        nums.set(opposite, temp);
    }
}

A right shift uses the same backward-copy loop as rotateRight, but places a replacement value at index 0 instead of restoring last. A left rotation uses the same forward-copy loop as shiftLeft, but restores the saved first value at the last index.

右移与 rotateRight 使用同样的反向复制循环,但会在索引 0 放入替代值,而不是恢复 last。左循环移位与 shiftLeft 使用同样的正向复制循环,但会把预先保存的首元素放到最后一个索引。

Remove All Elements with a Property删除所有具有某种性质的元素
// Remove all even numbers (traverse backward)// 删除所有偶数(倒序遍历)
for (int i = nums.size() - 1; i >= 0; i--) {
    if (nums.get(i) % 2 == 0) {
        nums.remove(i);
    }
}
Insert Elements Conditionally按条件插入元素
// Insert separator between consecutive duplicates// 在相邻重复元素之间插入分隔符
for (int i = 0; i < list.size() - 1; i++) {
    if (list.get(i).equals(list.get(i + 1))) {
        list.add(i + 1, "---");
        i++;  // skip the inserted element// 跳过刚插入的元素
    }
}
Simultaneous Traversal Some algorithms require traversing multiple String, array, or ArrayList objects simultaneously. If two lists have the same size, one index can compare corresponding elements:
public static int countMatches(ArrayList<String> a,
                               ArrayList<String> b) {
    int count = 0;
    for (int i = 0; i < a.size(); i++)
        if (a.get(i).equals(b.get(i))) count++;
    return count;
}
并行遍历 有些算法需要同时遍历多个 String、数组或 ArrayList 对象。如果两个列表大小相同,可以用同一个索引比较对应元素:
public static int countMatches(ArrayList<String> a,
                               ArrayList<String> b) {
    int count = 0;
    for (int i = 0; i < a.size(); i++)
        if (a.get(i).equals(b.get(i))) count++;
    return count;
}

2D Array Creation and Access

二维数组的创建与访问

A 2D array is stored as an array of arrays, essentially a table with rows and columns. Its size is fixed at creation.

二维数组本质上是数组的数组,相当于一张带行列的表格。它的大小在创建时就已固定。

Creating 2D Arrays

创建二维数组

// Using new (3 rows, 4 columns, all zeros)// 使用 new(3 行 4 列,全为 0)
int[][] grid = new int[3][4];

// Using initializer list// 使用初始化列表
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6}
};  // 2 rows, 3 columns// 2 行 3 列

Accessing Elements

访问元素

int val = matrix[1][2];   // row 1, col 2 → 6// 第 1 行第 2 列 → 6
matrix[0][0] = 99;       // set top-left to 99// 将左上角设为 99

// Dimensions// 维度
int rows = matrix.length;       // 2
int cols = matrix[0].length;   // 3

// Access a single row (it's a 1D array)// 访问单独一行(它本身就是一维数组)
int[] firstRow = matrix[0];     // {99, 2, 3}
2D Array Dimensions arr.length = number of rows. arr[0].length = number of columns. First index is row, second is column: arr[row][col]. Current AP CSA questions use rectangular 2D arrays; nonrectangular (“ragged”) 2D arrays are explicitly outside scope.
二维数组的维度 arr.length = 行数。arr[0].length = 列数。第一个下标是行,第二个下标是列:arr[row][col]。当前 AP CSA 题目使用矩形二维数组;非矩形(“ragged”)二维数组被明确排除在范围之外。
Default Element Values With new, every element receives its type’s default: int → 0, double → 0.0, boolean → false, and any reference type → null. For example, every element of new String[2][3] is initially null.
元素默认值 使用 new 创建时,每个元素都会取得该类型的默认值:int → 0,double → 0.0,boolean → false,任意引用类型 → null。例如,new String[2][3] 的每个元素初始时都是 null。
Valid 2D Indices For a nonempty rectangular array, a valid row index is 0 through arr.length - 1, and a valid column index is 0 through arr[0].length - 1. Access outside either range results in an ArrayIndexOutOfBoundsException.
有效的二维索引 对非空矩形二维数组,有效行索引是 0 到 arr.length - 1,有效列索引是 0 到 arr[0].length - 1。访问任一范围之外的位置会导致 ArrayIndexOutOfBoundsException。

2D Array Traversals

二维数组的遍历

2D arrays require nested loops to traverse all elements. Two common orderings are row-major and column-major.

二维数组需要用嵌套循环才能遍历所有元素。常见的两种遍历顺序是行优先(row-major)和列优先(column-major)。

Row-Major Order

行优先顺序

for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[0].length; c++) {
        System.out.print(grid[r][c] + " ");
    }
    System.out.println();
}

Column-Major Order

列优先顺序

for (int c = 0; c < grid[0].length; c++) {
    for (int r = 0; r < grid.length; r++) {
        System.out.print(grid[r][c] + " ");
    }
}

Enhanced for Loop with 2D Arrays

二维数组中的增强 for 循环

for (int[] row : grid) {         // outer: each row (1D array)// 外层:每一行(一维数组)
    for (int val : row) {        // inner: each element// 内层:每一个元素
        System.out.print(val + " ");
    }
}
Enhanced for Loop Types The outer loop variable type is a 1D array (e.g., int[]) because it receives each row. The inner loop variable is the element type (e.g., int). Assigning to the enhanced for variable does not change the array.
增强 for 循环的类型 外层循环变量的类型是一维数组(例如 int[]),因为它接收每一行。内层循环变量的类型是元素类型(例如 int)。给增强 for 的循环变量赋值不会改变数组本身。
In a nested enhanced for loop traversing a 2D int array, the outer loop variable type should be:
在遍历二维 int 数组的嵌套增强 for 循环中,外层循环变量的类型应该是?
int[]
int
int[][]
Integer
Correct! The outer loop iterates over rows, and each row is a 1D array, so the variable type is int[].
正确!外层循环遍历各行,每一行都是一维数组,因此变量类型是 int[]。
Each row of a 2D array is a 1D array, so the outer loop variable is int[].
二维数组的每一行都是一维数组,所以外层循环变量是 int[]。
Worked Example: Row-major vs column-major output order
int[][] g = {
    {1, 2, 3},
    {4, 5, 6}
};

// Row-major prints: 1 2 3 4 5 6// 行优先输出:1 2 3 4 5 6
for (int r = 0; r < g.length; r++)
    for (int c = 0; c < g[0].length; c++)
        System.out.print(g[r][c] + " ");

// Column-major prints: 1 4 2 5 3 6// 列优先输出:1 4 2 5 3 6
for (int c = 0; c < g[0].length; c++)
    for (int r = 0; r < g.length; r++)
        System.out.print(g[r][c] + " ");

Quick way to tell which one a snippet is doing: look at which index is in the outer loop. If the row index r moves slowest (outer), the output goes row by row. If the column index moves slowest, the output goes column by column. Total iteration count is rows × cols either way.

例题:行优先与列优先的输出顺序
int[][] g = {
    {1, 2, 3},
    {4, 5, 6}
};

// Row-major prints: 1 2 3 4 5 6// 行优先输出:1 2 3 4 5 6
for (int r = 0; r < g.length; r++)
    for (int c = 0; c < g[0].length; c++)
        System.out.print(g[r][c] + " ");

// Column-major prints: 1 4 2 5 3 6// 列优先输出:1 4 2 5 3 6
for (int c = 0; c < g[0].length; c++)
    for (int r = 0; r < g.length; r++)
        System.out.print(g[r][c] + " ");

判断代码是哪种遍历的快速方法:看哪个下标在外层循环。如果行下标 r 变化最慢(在外层),输出就是按行进行。如果列下标变化最慢,输出就是按列进行。无论哪种方式,总迭代次数都是 rows × cols。

A Uniquely Defined Order: Serpentine

自定义顺序示例:蛇形遍历

A problem may define its own traversal order. In a serpentine traversal, rows with even indices move left to right and rows with odd indices move right to left. For the same grid, the output is 1 2 3 6 5 4.

题目也可能自行规定遍历顺序。在蛇形遍历中,索引为偶数的行从左向右,索引为奇数的行从右向左。对同一个网格,输出为 1 2 3 6 5 4。

for (int r = 0; r < g.length; r++) {
    if (r % 2 == 0) {
        for (int c = 0; c < g[0].length; c++)
            System.out.print(g[r][c] + " ");
    } else {
        for (int c = g[0].length - 1; c >= 0; c--)
            System.out.print(g[r][c] + " ");
    }
}
AP Scope: Rectangular 2D Arrays For the current AP CSA course and exam, 2D arrays are rectangular: every row has the same number of columns. Use g.length for the row count and g[0].length for the column count. Java can represent nonrectangular arrays, but writing or analyzing them is explicitly outside the tested scope, so do not treat ragged-array behavior as an AP trap.
AP 范围:矩形二维数组 在当前 AP CSA 课程与考试中,二维数组是矩形的:每一行的列数相同。用 g.length 表示行数,用 g[0].length 表示列数。Java 可以表示非矩形数组,但编写或分析这类数组被明确排除在考试范围之外,因此不要把 ragged-array 行为当作 AP 陷阱来学习。

Implementing 2D Array Algorithms

实现二维数组算法

Standard 2D array algorithms extend 1D algorithms to work on the entire grid or specific rows, columns, or subsections.

标准的二维数组算法是把一维算法扩展到整张网格,或者扩展到特定的行、列或子区域上。

Required family2D traversal decision
Minimum/maximum; sum/averageChoose the entire grid or designated row, column, or subsection
At least one; all; countReturn on a match/counterexample or update a counter
Consecutive pairs; duplicatesDefine whether neighbors are horizontal/vertical and avoid crossing a boundary
Shift/rotateMove within one row left/right or one column up/down in a safe copy direction
ReverseSwap opposite positions within a designated row or column
必备类别二维遍历决策
最小/最大值;总和/平均值确定处理整张网格,还是指定行、列或子区域
至少一个;全部;计数找到匹配/反例时返回,或更新计数器
相邻元素对;重复元素明确相邻是水平方向还是垂直方向,并避免越过边界
平移/循环移位在一行内向左/右移动,或在一列内向上/下移动,并选择安全的复制方向
反转交换指定行或列中位置相对的元素
Sum of a Specific Row or Column求某一行或某一列的和
// Sum of row r// 第 r 行的和
public static int rowSum(int[][] grid, int r) {
    int sum = 0;
    for (int c = 0; c < grid[r].length; c++)
        sum += grid[r][c];
    return sum;
}

// Sum of column c// 第 c 列的和
public static int colSum(int[][] grid, int c) {
    int sum = 0;
    for (int r = 0; r < grid.length; r++)
        sum += grid[r][c];
    return sum;
}
Find Max of Entire 2D Array求整张二维数组的最大值

This example assumes a rectangular grid with at least one row and one column, because its initial maximum is read from grid[0][0].

此示例假定网格至少包含一行一列,因为它的初始最大值从 grid[0][0] 读取。

public static int findMax2D(int[][] grid) {
    int max = grid[0][0];
    for (int[] row : grid) {
        for (int val : row) {
            if (val > max) max = val;
        }
    }
    return max;
}
Properties and Counts in a Subsection子区域中的性质判断与计数

The bounds define a rectangular subsection: rows firstRow through lastRow and columns firstCol through lastCol, inclusive.

边界定义一个矩形子区域:从 firstRow 到 lastRow 的行,以及从 firstCol 到 lastCol 的列,均包含端点。

public static int countNegative(int[][] grid,
                                int firstRow, int lastRow,
                                int firstCol, int lastCol) {
    int count = 0;
    for (int r = firstRow; r <= lastRow; r++)
        for (int c = firstCol; c <= lastCol; c++)
            if (grid[r][c] < 0) count++;
    return count;
}

The same bounds support a subsection minimum, sum, average, “at least one,” or “all” algorithm by changing the accumulator and update. For “all,” return false on the first counterexample; for “at least one,” return true on the first match.

只需改变累积变量和更新方式,同样的边界就能用于求子区域最小值、总和、平均值、“至少一个”或“全部”算法。判断“全部”时遇到第一个反例就返回 false;判断“至少一个”时遇到第一个匹配就返回 true。

Consecutive Pairs and Duplicates相邻元素对与重复元素
// Check horizontal consecutive pairs throughout the grid// 检查整张网格中的水平相邻元素对
public static boolean hasHorizontalRepeat(int[][] grid) {
    for (int r = 0; r < grid.length; r++)
        for (int c = 0; c < grid[0].length - 1; c++)
            if (grid[r][c] == grid[r][c + 1]) return true;
    return false;
}

// Check for a duplicate anywhere within one designated row// 检查指定行内任意位置是否存在重复元素
public static boolean hasDuplicateInRow(int[][] grid, int row) {
    for (int c1 = 0; c1 < grid[0].length; c1++)
        for (int c2 = c1 + 1; c2 < grid[0].length; c2++)
            if (grid[row][c1] == grid[row][c2]) return true;
    return false;
}

For vertical consecutive pairs, compare grid[r][c] with grid[r + 1][c] and stop the row loop at grid.length - 1. A whole-grid duplicate check extends the nested comparison so every position is compared only with positions that come later.

检查垂直相邻元素对时,比较 grid[r][c] 与 grid[r + 1][c],并让行循环在 grid.length - 1 之前停止。检查整张网格的重复元素时,可扩展嵌套比较,使每个位置只与它后面的位置比较。

Shift, Rotate, and Reverse a Row or Column平移、循环移动或反转一行/一列

These examples assume a rectangular grid with at least one row and one column.

以下示例假定网格为至少包含一行一列的矩形二维数组。

// Rotate one row right// 把指定行向右循环移动
public static void rotateRowRight(int[][] grid, int row) {
    int last = grid[row][grid[0].length - 1];
    for (int c = grid[0].length - 1; c > 0; c--)
        grid[row][c] = grid[row][c - 1];
    grid[row][0] = last;
}

// Rotate one column down// 把指定列向下循环移动
public static void rotateColumnDown(int[][] grid, int col) {
    int last = grid[grid.length - 1][col];
    for (int r = grid.length - 1; r > 0; r--)
        grid[r][col] = grid[r - 1][col];
    grid[0][col] = last;
}

// Reverse one row in place// 原地反转指定行
public static void reverseRow(int[][] grid, int row) {
    for (int c = 0; c < grid[0].length / 2; c++) {
        int opposite = grid[0].length - 1 - c;
        int temp = grid[row][c];
        grid[row][c] = grid[row][opposite];
        grid[row][opposite] = temp;
    }
}

A shift uses the same copy loops but writes a replacement value instead of restoring the saved end. Left/up versions reverse the copy direction. To reverse a column, swap grid[r][col] with grid[grid.length - 1 - r][col].

平移使用相同的复制循环,但会写入替代值,而不是恢复预先保存的末端值。向左/向上的版本会反转复制方向。若要反转一列,可交换 grid[r][col] 与 grid[grid.length - 1 - r][col]。

Exam Tip, FRQ #4 The AP Exam's fourth free-response question focuses on 2D arrays. Practice traversing in row-major, column-major, and non-standard orders. Always verify boundary conditions in your loops!
考试提示:FRQ 第 4 题 AP 考试中第四道自由作答题的重点就是二维数组。请练习行优先、列优先以及非标准顺序的遍历,并且永远要仔细核对循环的边界条件!

Searching Algorithms

查找(searching)算法

Linear Search

线性查找(linear search)

Linear search checks each element in order until the target is found or all elements have been checked. It works on sorted or unsorted arrays and ArrayList objects, and the search may begin at either end.

线性查找按顺序依次检查每一个元素,直到找到目标或检查完所有元素为止。它适用于有序或无序的数组和 ArrayList,并且可以从任意一端开始查找。

public static int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) return i;
    }
    return -1;  // not found// 未找到
}

Search from the Other End and Search an ArrayList

从另一端查找与查找 ArrayList

public static int linearSearchFromRight(int[] arr, int target) {
    for (int i = arr.length - 1; i >= 0; i--)
        if (arr[i] == target) return i;
    return -1;
}

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

If duplicates exist, direction affects which matching index is returned: the left-to-right version returns the first match from the left; the right-to-left version returns the first match from the right.

如果存在重复值,查找方向会影响返回哪个匹配索引:从左向右的版本返回左侧遇到的第一个匹配,从右向左的版本返回右侧遇到的第一个匹配。

Linear Search on a 2D Array

在二维数组上的线性查找

public static boolean search2D(int[][] grid, int target) {
    for (int[] row : grid) {
        for (int val : row) {
            if (val == target) return true;
        }
    }
    return false;
}

Sorting Algorithms

排序(sorting)算法

Selection Sort

选择排序

Repeatedly finds the smallest element in the unsorted portion and swaps it into its correct final position.

不断地在未排序部分中找出最小的元素,并将它交换到它最终正确的位置上。

public static void selectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int minIdx = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIdx]) minIdx = j;
        }
        int temp = arr[i];
        arr[i] = arr[minIdx];
        arr[minIdx] = temp;
    }
}

Insertion Sort

插入排序

Takes each element and inserts it into its correct (but not necessarily final) position in the sorted portion by shifting elements.

取出每个元素,通过移动其他元素,把它插入到已排序部分中正确的位置(但不一定是最终位置)。

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];
            j--;
        }
        arr[j + 1] = key;
    }
}
Selection Sort vs Insertion Sort
FeatureSelection SortInsertion Sort
MethodSelect min, swap into placeInsert into sorted portion
Position after passFinal positionCorrect but not necessarily final
Core comparisonSelects the minimum from the unsorted suffixShifts values within the growing sorted prefix
选择排序 vs 插入排序
特性选择排序插入排序
方法选出最小值,交换到位插入到已排序部分
每轮过后元素位置最终位置正确但不一定是最终位置
核心区别从未排序后缀中选出最小值在不断扩大的有序前缀中移动元素
Worked Example: Sorting state after pass k

After pass k, selection sort has placed the smallest k elements in final positions. Insertion sort has sorted the first k + 1 original elements relative to one another, but they need not be in final positions.

Start: [5, 2, 4, 6, 1, 3]

Selection sort, after pass 1: [1, 2, 4, 6, 5, 3]
  ↳ Position 0 holds the global minimum, FINAL position.

Insertion sort, after pass 1: [2, 5, 4, 6, 1, 3]
  ↳ The first two elements are sorted, but 5 is NOT in its final position.
    The 1 and 3 still to come will push it right.

How AP MCQs phrase this: "After pass 3, which of the following must be true?" The selection-sort answer is "the smallest 3 elements are in positions 0–2". The insertion-sort answer is "the first 4 original elements are in sorted order relative to each other" (not relative to the full array).

例题:第 k 轮过后的排序状态

第 k 轮过后,选择排序已经把最小的 k 个元素放到最终位置;插入排序则把原序列的前 k + 1 个元素按彼此关系排好序,但它们不一定处于最终位置。

起始: [5, 2, 4, 6, 1, 3]

选择排序,第 1 轮后: [1, 2, 4, 6, 5, 3]
  ↳ 位置 0 已放上全局最小值,处于最终位置。

插入排序,第 1 轮后: [2, 5, 4, 6, 1, 3]
  ↳ 前两个元素已经有序,但 5 还不在它的最终位置。
    后面还会出现的 1 和 3 会把它向右挤。

AP 选择题的常见问法:"第 3 轮过后,下列哪一项一定成立?"选择排序的答案是"最小的 3 个元素位于位置 0–2"。插入排序的答案是"原序列的前 4 个元素相对彼此有序"(而不是相对于整个数组有序)。

AP Trap: Exactly n - 1 Outer Passes For an array of n elements, selection sort runs n - 1 outer passes (the last element falls into place automatically). Insertion sort also runs n - 1 outer passes (it starts at index 1 and inserts each subsequent element). The classic wrong answer on MCQs is n passes.
AP 陷阱:外层循环恰好执行 n - 1 轮 对长度为 n 的数组,选择排序的外层循环跑 n - 1 轮(最后一个元素会自动落到正确位置)。插入排序的外层循环也跑 n - 1 轮(它从索引 1 开始,依次插入后续每一个元素)。选择题中经典的错误答案是 n 轮。

Recursion

递归(recursion)

A recursive method is a method that calls itself. Every recursive method needs at least one base case (stops the recursion) and at least one recursive call.

递归方法是会调用自身的方法。每个递归方法都至少需要一个基线条件(base case,用来终止递归)和至少一次递归调用(recursive case)。

The factorial example assumes n >= 0; a negative input would keep moving away from the base case.

下面的阶乘示例假定 n >= 0;负数输入会不断远离基线条件。

public static int factorial(int n) {
    if (n == 0)          // base case// 基线条件
        return 1;
    return n * factorial(n - 1);  // recursive call// 递归调用
}
// factorial(4) → 4 * 3 * 2 * 1 * 1 = 24// factorial(4) → 4 * 3 * 2 * 1 * 1 = 24

Key Insights:

• Each recursive call gets its own set of local variables and parameters.

• Parameter values capture progress just like loop control variables do.

• Any recursive solution can be replicated with iteration, and vice versa.

关键要点:

• 每次递归调用都有自己一套独立的局部变量和形参。

• 形参的值记录着进度,就像循环的控制变量那样。

• 任何递归解法都可以用迭代来实现,反之亦然。

Exam Tip Current AP CSA requires you to determine the result of recursive calls; writing recursive code is explicitly outside scope. Practice tracing parameter values, pending operations, and returns with a call tree.
考试提示 当前 AP CSA 要求你判断递归调用的结果;编写递归代码被明确排除在范围之外。请用调用树练习追踪形参值、等待执行的运算和返回过程。
What does mystery(5) return if mystery(int n) returns 0 when n <= 0, else n + mystery(n - 2)?
若 mystery(int n) 在 n <= 0 时返回 0,否则返回 n + mystery(n - 2),则 mystery(5) 返回什么?
15
5
9
12
Correct! mystery(5) = 5 + mystery(3) = 5 + 3 + mystery(1) = 5 + 3 + 1 + mystery(-1) = 5 + 3 + 1 + 0 = 9.
正确!mystery(5) = 5 + mystery(3) = 5 + 3 + mystery(1) = 5 + 3 + 1 + mystery(-1) = 5 + 3 + 1 + 0 = 9。
Trace it: mystery(5) = 5 + mystery(3) = 5 + 3 + mystery(1) = 5 + 3 + 1 + mystery(-1) = 9.
追踪一下:mystery(5) = 5 + mystery(3) = 5 + 3 + mystery(1) = 5 + 3 + 1 + mystery(-1) = 9。
Worked Example: Tracing recursion with a call tree
public static String mystery(int n) {
    if (n <= 0) return "";
    return mystery(n - 1) + n + " ";
}
// mystery(3) returns what?// mystery(3) 返回什么?

Step through it:

mystery(3)
= mystery(2) + 3 + " "
= (mystery(1) + 2 + " ") + 3 + " "
= ((mystery(0) + 1 + " ") + 2 + " ") + 3 + " "
= (("" + 1 + " ") + 2 + " ") + 3 + " "
= ("1 " + 2 + " ") + 3 + " "
= "1 2 " + 3 + " "
= "1 2 3 "

The pattern to look for: when the recursive call comes before the rest of the expression, the work happens on the way back up the call stack, in reverse order of the calls. When the recursive call comes after, the work happens on the way down. Whichever side the work sits on determines whether you build the string forward or backward.

例题:用调用树追踪递归
public static String mystery(int n) {
    if (n <= 0) return "";
    return mystery(n - 1) + n + " ";
}
// mystery(3) returns what?// mystery(3) 返回什么?

一步步追踪:

mystery(3)
= mystery(2) + 3 + " "
= (mystery(1) + 2 + " ") + 3 + " "
= ((mystery(0) + 1 + " ") + 2 + " ") + 3 + " "
= (("" + 1 + " ") + 2 + " ") + 3 + " "
= ("1 " + 2 + " ") + 3 + " "
= "1 2 " + 3 + " "
= "1 2 3 "

需要观察的规律:当递归调用出现在表达式的前面时,真正的工作发生在调用栈返回的过程中,并且顺序与调用的顺序相反。当递归调用出现在后面时,工作则发生在向下递归的过程中。工作落在哪一边,决定了你是正着构造字符串还是反着构造。

AP Trap: Base case that returns the wrong empty value For a recursive method that returns a String, the base case usually returns "". For one that returns an int sum, it returns 0. For one that returns a count, it might return 0 or 1 depending on whether the base case "counts itself". The wrong empty value is the most common reason a recursive MCQ trace is off by one element.
AP 陷阱:基线条件返回了错误的"空值" 对于返回 String 的递归方法,基线条件通常返回 ""。对于返回 int 累加和的方法,则返回 0。对于返回计数的方法,可能返回 0 也可能返回 1,要看基线条件本身是否"算上自己"。错误的空值是递归选择题追踪结果差一位的最常见原因。

Recursive Searching and Sorting

递归查找与排序

Tracing Recursive Traversals

追踪递归遍历

Recursion can traverse String, array, and ArrayList objects. Current AP CSA asks you to determine the result of recursive code, not to write recursive methods. In each example, the index parameter records progress toward the base case.

递归可以遍历 String、数组和 ArrayList。当前 AP CSA 要求你判断递归代码的结果,而不是编写递归方法。在每个示例中,索引形参都记录着朝基线条件推进的过程。

// String traversal: everyOther("ABCDE", 0) returns "ACE"// String 遍历:everyOther("ABCDE", 0) 返回 "ACE"
public static String everyOther(String text, int index) {
    if (index >= text.length()) return "";
    return text.substring(index, index + 1)
           + everyOther(text, index + 2);
}

// Array traversal: sumFrom({3, 1, 4}, 0) returns 8// 数组遍历:sumFrom({3, 1, 4}, 0) 返回 8
public static int sumFrom(int[] nums, int index) {
    if (index == nums.length) return 0;
    return nums[index] + sumFrom(nums, index + 1);
}

// ArrayList traversal: for [-2, 5, 0, 7], countPositive(..., 0) returns 2// ArrayList 遍历:对 [-2, 5, 0, 7],countPositive(..., 0) 返回 2
public static int countPositive(ArrayList<Integer> nums,
                                int index) {
    if (index == nums.size()) return 0;
    int current = 0;
    if (nums.get(index) > 0) current = 1;
    return current + countPositive(nums, index + 1);
}
Trace, Do Not Author For sumFrom, the return expression expands to 3 + 1 + 4 + 0. Track the parameter values and the pending operations on the way down, then evaluate returns on the way back up. Writing recursive code is outside the current course and exam scope.
只需追踪,不要求编写 对 sumFrom,返回表达式展开为 3 + 1 + 4 + 0。向下调用时记录形参值和等待执行的运算,再在返回时从下往上求值。编写递归代码不在当前课程与考试范围内。

Binary Search

二分查找(binary search)

Binary search requires data to be sorted. It starts at the middle and eliminates half of the remaining elements each step.

二分查找要求数据必须有序。它从中间开始,每一步都把剩下的元素排除掉一半。

Binary search can be implemented iteratively or recursively on a sorted array or ArrayList. The recursive array version below is for tracing; students are not required to write recursive code.

二分查找可以用迭代或递归方式处理有序数组或 ArrayList。下面的递归数组版本用于追踪;学生不需要编写递归代码。

public static int binarySearch(int[] arr, int lo, int hi, int target) {
    if (lo > hi) return -1;  // base case: not found// 基线条件:未找到
    int mid = (lo + hi) / 2;
    if (arr[mid] == target) return mid;
    else if (arr[mid] < target)
        return binarySearch(arr, mid + 1, hi, target);
    else
        return binarySearch(arr, lo, mid - 1, target);
}
Linear vs Binary Search
FeatureLinear SearchBinary Search
Requires sorted data?NoYes
Worst-case comparisons (n items)nAt most ⌊log₂(n)⌋ + 1 for n > 0
Example: 1,000,000 elements1,000,000~20
线性查找 vs 二分查找
特性线性查找二分查找
是否要求数据有序?否是
最坏情况比较次数(n 个元素)n当 n > 0 时至多为 ⌊log₂(n)⌋ + 1
例:1,000,000 个元素1,000,000约 20

Merge Sort

归并排序

Merge sort is a recursive sorting algorithm for arrays or ArrayList objects. It divides the collection in half, sorts each half, then merges the sorted halves together.

归并排序是一种用于数组或 ArrayList 对象的递归排序算法:把集合对半分开,分别对两半进行排序,然后再把已排序的两半合并起来。

1. Divide: Split the array into two halves repeatedly until each subarray has one element.

2. Conquer: Each single-element subarray is trivially sorted.

3. Merge: Combine sorted subarrays back together, maintaining order.

1. 分解:反复把数组对半分开,直到每个子数组只剩一个元素。

2. 解决:单元素子数组本身就是有序的。

3. 合并:把有序的子数组重新合并起来,保持有序。

// Trace: merge sort on [38, 27, 43, 3]// 追踪:对 [38, 27, 43, 3] 进行归并排序
// Split: [38, 27] and [43, 3]// 分解:[38, 27] 和 [43, 3]
// Split: [38] [27]    [43] [3]// 分解:[38] [27]    [43] [3]
// Merge: [27, 38]     [3, 43]// 合并:[27, 38]     [3, 43]
// Merge: [3, 27, 38, 43]// 合并:[3, 27, 38, 43]
Current AP Algorithm Boundary For search, the assessed algorithms are linear search and binary search. For sorting, they are selection sort, insertion sort, and merge sort. Writing recursive code and writing merge-sort code are outside scope; you should trace recursive calls and merge-sort stages instead.
当前 AP 算法边界 查找部分考查线性查找和二分查找;排序部分考查选择排序、插入排序和归并排序。编写递归代码和编写归并排序代码不在范围内;你应当能够追踪递归调用与归并排序的各个阶段。

Common Unit 4 MCQ Patterns

单元 4 常见选择题模式

Unit 4 carries the most weight on the AP CSA MCQ section. The questions cluster around a handful of operational pitfalls. Knowing the pattern usually beats tracing the whole snippet.

单元 4 在 AP CSA 选择题中占比最大。题目集中考查几类操作上的陷阱。摸清套路往往比从头追踪整段代码更高效。

Pattern 1: length vs length() vs size() Arrays: arr.length (field, no parens). Strings: s.length() (method). ArrayLists: list.size() (method). MCQs frequently include a "won't compile" trap that swaps one for another. (See Topic 4.3.)
套路 1:length 与 length() 与 size() 数组:arr.length(字段,不带括号)。String:s.length()(方法)。ArrayList:list.size()(方法)。选择题经常埋"无法编译"的陷阱,把它们互相调换。(见小节 4.3。)
Pattern 2: Enhanced for can't mutate primitives Reassigning the loop variable in for (int x : arr) doesn't change arr. Calling a mutator on an object loop variable does change the object. The answer choice that says "the array is unchanged" is right for primitives. (See Topic 4.4.)
套路 2:增强 for 无法修改基本类型 在 for (int x : arr) 中给循环变量重新赋值不会改变 arr。如果循环变量是对象,调用它的修改方法会改变对象本身。对于基本类型,正确答案是"数组保持不变"。(见小节 4.4。)
Pattern 3: Remove during forward traversal Calling list.remove(i) in a forward indexed loop without a compensating i-- skips the element that slid into position i. Either traverse backward or adjust the index. Adding or removing during an enhanced for traversal can result in ConcurrentModificationException, so do not change the list's size in that loop form. (See Topic 4.9.)
套路 3:正向遍历过程中的删除 在正向带索引循环中调用 list.remove(i) 而没有相应的 i--,会跳过滑到位置 i 的元素。要么倒序遍历,要么调整索引。在增强 for 遍历中添加或删除元素可能导致 ConcurrentModificationException,因此不要在这种循环中改变列表大小。(见小节 4.9。)
Pattern 4: Row-major vs column-major output Which loop is outer tells you the traversal direction. For the rectangular 2D arrays assessed by current AP CSA, a complete traversal visits rows × cols elements; use g.length and g[0].length as the dimensions. Nonrectangular arrays are outside scope. (See Topic 4.12.)
套路 4:行优先 vs 列优先的输出 哪个循环在外层,决定了遍历方向。对于当前 AP CSA 考查的矩形二维数组,完整遍历会访问 rows × cols 个元素;维度使用 g.length 与 g[0].length。非矩形数组不在范围内。(见小节 4.12。)
Pattern 5: Sort state after pass k Selection sort: after pass k, the first k elements are the k smallest, in final position. Insertion sort: after pass k, the first k + 1 original elements are sorted relative to each other, but not necessarily in their final spots. (See Topic 4.15.)
套路 5:第 k 轮过后的排序状态 选择排序:第 k 轮后,前 k 个元素就是最小的 k 个,且处于最终位置。插入排序:第 k 轮后,原序列的前 k + 1 个元素相对彼此有序,但不一定在最终位置上。(见小节 4.15。)
Pattern 6: Recursion call-tree trace Recursive call before the rest of the expression means work happens on the way back up. After means work happens on the way down. Trace the base case first and unfold from there. (See Topic 4.16.)
套路 6:用调用树追踪递归 递归调用出现在表达式前面,意味着工作发生在递归返回的过程中。出现在后面,则发生在向下递归的过程中。先追踪基线条件,再由此展开。(见小节 4.16。)

Flashcards

闪卡

Click each card to flip and reveal the answer.

点击每张卡片翻面查看答案。

Default value of an int array element?int 数组元素的默认值?
0
Array: .length
ArrayList: ???
数组:.length
ArrayList:???
.size(), it's a method, not a field.size(),是方法,不是字段
Invalid array index exception?数组索引非法时抛出的异常?
ArrayIndexOutOfBoundsException
Add/remove from ArrayList in enhanced for loop?在增强 for 循环中对 ArrayList 增/删?
Do not do it; changing size can result in
ConcurrentModificationException
不要这样做;改变大小可能导致
ConcurrentModificationException
Autoboxing: int → ???自动装箱:int → ???
Integer
Automatic primitive → wrapper conversion
Integer
基本类型 → 包装类的自动转换
Number of rows in 2D array grid?二维数组 grid 的行数?
grid.length
Binary search requires data to be ___二分查找要求数据必须 ___
Sorted, eliminates half the data each step有序,每一步排除一半数据
3 sorting algorithms on the AP exam?AP 考试涉及的 3 种排序算法?
Selection sort, Insertion sort, Merge sort选择排序、插入排序、归并排序
Every recursive method needs…每个递归方法都需要……
1. A base case (stops recursion)
2. A recursive call (moves toward base)
1. 一个基线条件(终止递归)
2. 一次递归调用(向基线条件靠近)
Selection sort: final position?选择排序:是否处于最终位置?
Yes, each pass places one element in its correct final position.是,每一轮都会把一个元素放到它正确的最终位置。
What does split(",") return?split(",") 返回什么?
A String[] array split by the delimiter按分隔符切分后得到的 String[] 数组
Import for ArrayList?ArrayList 的 import 语句?
java.util.ArrayList

Unit 4, Practice Quiz

单元 4 · 练习小测

1. What is the value of x after int[] arr = {5, 10, 15, 20}; int x = arr[arr.length - 1];?
1. 执行 int[] arr = {5, 10, 15, 20}; int x = arr[arr.length - 1]; 之后,x 的值是多少?
5
15
20
ArrayIndexOutOfBoundsException
Correct! arr.length is 4, so arr[3] is 20 (the last element).
正确!arr.length 为 4,因此 arr[3] 是 20(最后一个元素)。
arr.length - 1 = 3, so arr[3] = 20.
arr.length - 1 = 3,所以 arr[3] = 20。
2. Which operation can result in a ConcurrentModificationException?
2. 下列哪种操作可能导致 ConcurrentModificationException?
Modifying an element via indexed for loop on an ArrayList在 ArrayList 上用带索引的 for 循环修改某个元素
Removing an element during an enhanced for loop on an ArrayList在 ArrayList 的增强 for 循环中删除某个元素
Using get() in an enhanced for loop在增强 for 循环中使用 get()
Adding to an ArrayList outside of any loop在所有循环之外向 ArrayList 添加元素
Correct! Changing an ArrayList's size during an enhanced for traversal can result in ConcurrentModificationException, so additions and removals are not safe in that loop form.
正确!在增强 for 遍历中改变 ArrayList 的大小可能导致 ConcurrentModificationException,因此不应在这种循环中添加或删除元素。
Adding or removing elements changes the ArrayList's size and can result in ConcurrentModificationException during an enhanced for traversal.
添加或删除元素会改变 ArrayList 的大小,并可能在增强 for 遍历中导致 ConcurrentModificationException。
3. Given int[][] m = new int[3][4];, what is m[0].length?
3. 给定 int[][] m = new int[3][4];,m[0].length 的值是多少?
3
7
12
4
Correct! m.length is 3 (rows). m[0].length is 4 (columns in each row).
正确!m.length 为 3(行数)。m[0].length 为 4(每行的列数)。
m.length = rows (3). m[0].length = columns (4).
m.length = 行数(3)。m[0].length = 列数(4)。
4. What must be true about data before applying binary search?
4. 应用二分查找之前,数据必须满足什么条件?
The data must be sorted数据必须有序
The data must contain no duplicates数据中不能有重复元素
The data must be stored in an ArrayList数据必须存放在 ArrayList 中
The data must have at least 10 elements数据至少要有 10 个元素
Correct! Binary search eliminates half the data each step by comparing to the middle, which only works on sorted data.
正确!二分查找每一步通过与中间元素比较来排除一半数据,这只在有序数据上才行得通。
Binary search requires data to be sorted. It compares the target to the middle element to decide which half to eliminate.
二分查找要求数据有序。它通过将目标与中间元素比较,来决定排除哪一半。
5. What does mystery(4) return?
public static int mystery(int n) { if (n == 1) return 1; return n + mystery(n - 1); }
5. mystery(4) 返回什么?
public static int mystery(int n) { if (n == 1) return 1; return n + mystery(n - 1); }
4
7
10
1
Correct! mystery(4) = 4 + mystery(3) = 4 + 3 + mystery(2) = 4 + 3 + 2 + mystery(1) = 4 + 3 + 2 + 1 = 10.
正确!mystery(4) = 4 + mystery(3) = 4 + 3 + mystery(2) = 4 + 3 + 2 + mystery(1) = 4 + 3 + 2 + 1 = 10。
Trace: mystery(4) = 4 + 3 + 2 + 1 = 10.
追踪:mystery(4) = 4 + 3 + 2 + 1 = 10。

AP-Style Practice Problems

AP 风格练习题

15 exam-level multiple-choice questions (3 medium · 12 hard), code-trace heavy with realistic distractors and fully worked solutions. Built for top-score prep — go here after you've worked through the notes and the in-page quiz above.

15 道考试难度的选择题(3 道中等 · 12 道困难),以代码追踪为主,配有逼真的干扰项和完整的解析。专为冲高分而设计——读完笔记并完成上面的小测之后再来做这部分。

Practice Problems →练习题 →

鼎睿学苑 · Dingrui Scholars

AP Computer Science A, Unit 4: Data Collections · 2026 Edition

AP 计算机科学 A · 单元 4:数据集合 · 2026 版