Companion to the AP-Style MC Practice SetAP 风格选择题练习的解析配套
Unit 4: Data Collections第 4 单元:数据集合CSA
Multiple Choice)—— 详细解析Each item restates the prompt and choices, marks the correct letter, and gives a brief justification. Trap distractors are called out where useful.每道题重述题干与选项、标出正确答案字母,并给出简明解析;对容易混淆的干扰项(trap distractor)单独提示。
arr.length vs list.size()?arr.length 对比 list.size()?
4 33 34 4arr.length (no parentheses). The literal {1,2,3,4} has length 4.ArrayList uses a method: list.size() (with parentheses). After three adds, list.size() == 3.length — but they do not have .length(). String, the other way around, uses s.length(). Mix these up and the compiler instantly complains. AP MCQ writers love this comparison.
arr.length(不带括号)。字面量 {1,2,3,4} 长度为 4。ArrayList 用方法:list.size()(带括号)。三次 add 之后,list.size() == 3。length——但没有 .length()。反过来 String 用的是 s.length()。混用编译器立刻报错。AP 出题人很爱这种对比。
int[] vals = {10,20,30}; println(vals[3]);int[] vals = {10,20,30}; println(vals[3]);
0 (default).打印 0(默认值)。30 (last).打印 30(最后一个)。ArrayIndexOutOfBoundsException.。n has valid indices 0 through n − 1. With vals.length == 3, valid indices are 0, 1, 2. Reading vals[3] is out of bounds and Java throws ArrayIndexOutOfBoundsException at run time.
Trap (D): the bounds check is dynamic, not static — the compiler cannot generally tell that an index will be out of range. Trap (B): Java does not "wrap around" or silently clamp to the last element — some languages do this, but Java does not.
n 的数组合法下标是 0 到 n − 1。vals.length == 3,合法下标是 0, 1, 2。读 vals[3] 越界(out-of-bounds),Java 在运行时抛出 ArrayIndexOutOfBoundsException。
陷阱 (D):边界检查是动态的,不是静态的——编译器通常无法判断某个下标是否越界。陷阱 (B):Java 不会"环绕"或者悄悄夹紧到最后一个元素——有些语言会,Java 不会。
for (int v : vals) v *= 10; — does it change vals?for (int v : vals) v *= 10; —— 会不会修改 vals?
10 401 41 40v is a copy of each array element (since int is a primitive). Reassigning v changes only the local copy — the array is untouched.
for (int i = 0; i < vals.length; i++) vals[i] *= 10;v wouldn't modify the array — you'd need to mutate the object's state via v.someMethod(...).for 循环(enhanced for-loop)的循环变量 v 是每个数组元素的副本(因为 int 是基本类型)。给 v 重新赋值只改变本地副本——数组本身不受影响。
for (int i = 0; i < vals.length; i++) vals[i] *= 10;v 也不会修改数组——需要通过 v.someMethod(...) 修改对象的状态。int[] a = {1,2,3}; b = a; b[1] = 99; c = {1,99,3};
Print a[1] + " " + (a == b) + " " + (a == c).int[] a = {1,2,3}; b = a; b[1] = 99; c = {1,99,3};
打印 a[1] + " " + (a == b) + " " + (a == c)。
1 true false99 false false99 true true99 true falseb = a does not clone the array — both names point to the same underlying object, so mutations are shared. (2) == on array references compares reference identity, not contents.
b = a: both a and b reference the same array {1, 2, 3}.b[1] = 99 reaches through that shared reference → the array becomes {1, 99, 3}. Reading a[1] sees 99.c = {1, 99, 3} allocates a brand-new array on the heap with matching contents.a == b → same reference → true.a == c → different references (even though contents are identical) → false."99 true false".
Trap (A) "1 true false" assumes b = a is a copy. (C) "99 true true" assumes == compares content — it doesn't (no .equals() override for plain int[] beyond Object's reference equality; use Arrays.equals for content). (B) mixes both mistakes.
b = a 不会复制数组——两个名字指向同一个底层对象,修改是共享的(别名 aliasing)。(2) 数组引用的 == 比较的是引用身份,不是内容。
b = a 之后:a 与 b 都引用同一个数组 {1, 2, 3}。b[1] = 99 透过共享引用修改 → 数组变为 {1, 99, 3}。读 a[1] 得到 99。c = {1, 99, 3} 在堆上分配一个全新的、内容相同的数组。a == b → 引用相同 → true。a == c → 引用不同(即便内容完全一致)→ false。"99 true false"。
陷阱 (A) "1 true false" 以为 b = a 是复制。(C) "99 true true" 以为 == 比较内容——并不(原生 int[] 没有重写 .equals() 超出 Object 的引用相等;要比较内容用 Arrays.equals)。(B) 把两种错误都犯了。
[20, 20, 30, 40]; remove every value divisible by 20 with a forward index loop.[20, 20, 30, 40];用向前递增的下标循环删除每个能被 20 整除的值。
[30][20, 30][][20, 20, 30]remove(i), every later element shifts left by one — but i still increments, so the loop skips the element that just moved into position i.
state i get(i) action
[20,20,30,40] 0 20 remove → [20,30,40], i = 1
[20,30,40] 1 30 keep, i = 2
[20,30,40] 2 40 remove → [20,30], i = 3
[20,30] 3 — 3 < 2 false → exit
The second 20 (which shifted into index 0) is never re-checked, so it survives in the list. Result: [20, 30].
Trap (A) is the intended result if the bug weren't there. Fix: either loop backwards (for i = list.size() − 1 down to 0), or do i-- after a successful remove so the same index is re-checked.
remove(i) 之后,后续每个元素左移一位——但 i 仍然自增,于是循环跳过了刚刚移动到位置 i 的那个元素。
state i get(i) 动作
[20,20,30,40] 0 20 remove → [20,30,40], i = 1
[20,30,40] 1 30 保留, i = 2
[20,30,40] 2 40 remove → [20,30], i = 3
[20,30] 3 — 3 < 2 false → 退出
第二个 20(已经移动到下标 0)从未被重新检查,所以它留在了列表里。结果:[20, 30]。
陷阱 (A) 是 bug 不存在时本应得到的结果。修复方法:要么逆序循环(for i = list.size() − 1 down to 0),要么在成功删除后执行 i--,让同一个下标被再次检查。
3×3 grid 1..9; nested loop sums g[r][c] when r + c == 2.3×3 网格 1..9;嵌套循环在 r + c == 2 时累加 g[r][c]。
9121518r + c == 2 picks out the anti-diagonal — the diagonal running from top-right to bottom-left.
row\col 0 1 2
0 | . . [3] ← r+c=2
1 | . [5] . ← r+c=2
2 | [7] . . ← r+c=2
Sum: 3 + 5 + 7 = 15.
Anti-diagonal trick: for an N×N grid, all elements on the anti-diagonal satisfy r + c == N − 1. Memorize the two diagonal forms — they appear constantly on FRQ-style 2D problems.
r + c == 2 选出反对角线(anti-diagonal)——从右上到左下的对角线。
row\col 0 1 2
0 | . . [3] ← r+c=2
1 | . [5] . ← r+c=2
2 | [7] . . ← r+c=2
求和:3 + 5 + 7 = 15。
反对角线小窍门:对 N×N 网格,反对角线上的所有元素满足 r + c == N − 1。把两条对角线的判定形式(r == c 主对角线、r + c == N − 1 反对角线)背熟——FRQ 风格的 2D 题里反复出现。
transpose(g) swaps m[r][c] with m[c][r] for all c > r. Print g[0][2] + " " + g[2][0] + " " + g[1][1] after.transpose(g) 对所有 c > r 交换 m[r][c] 与 m[c][r]。之后打印 g[0][2] + " " + g[2][0] + " " + g[1][1]。
3 7 57 3 53 3 56 8 5c = r + 1, so each pair (r, c) with r < c is swapped exactly once. (If the inner started at c = 0, each pair would swap twice and the array would end up unchanged.)
start: swap (0,1) ↔ (1,0): swap (0,2) ↔ (2,0): swap (1,2) ↔ (2,1):
1 2 3 1 4 3 1 4 7 1 4 7
4 5 6 2 5 6 2 5 6 2 5 8
7 8 9 7 8 9 3 8 9 3 6 9
Final grid is the transpose: rows are the original columns. Read out:
g[0][2] → 7g[2][0] → 3g[1][1] → 5 (center is invariant under transpose)"7 3 5".
2D-mutation-through-parameter pattern: the array is passed by reference, so changes inside transpose persist in the caller — no return needed.
c = r + 1 开始,所以每对 (r, c)(满足 r < c)只交换一次。(如果内层从 c = 0 开始,每对会交换两次,最终数组保持不变。)
start: swap (0,1) ↔ (1,0): swap (0,2) ↔ (2,0): swap (1,2) ↔ (2,1):
1 2 3 1 4 3 1 4 7 1 4 7
4 5 6 2 5 6 2 5 6 2 5 8
7 8 9 7 8 9 3 8 9 3 6 9
最终的网格是转置(transpose):行是原来的列。读出:
g[0][2] → 7g[2][0] → 3g[1][1] → 5(中心在转置下不变)"7 3 5"。
"通过参数修改 2D 数组"的模式:数组按引用传递,transpose 内的修改会持久到调用方——不需要返回值。
a = [1,2,3]; b = a; b.set(0, 99); b.add(4); Print a.size() + " " + a.get(0) + " " + a.get(3).a = [1,2,3];b = a; b.set(0, 99); b.add(4); 打印 a.size() + " " + a.get(0) + " " + a.get(3)。
3 1 ?3 99 44 1 44 99 4ArrayList is an object; b = a aliases the same underlying list. Every mutation through b is visible through a.
step | underlying list a.size()
a.add(1..3) | [1, 2, 3] 3
b = a | [1, 2, 3] 3 (same list, two names)
b.set(0, 99) | [99, 2, 3] 3 (no size change — set overwrites)
b.add(4) | [99, 2, 3, 4] 4 (append → size grows)
Then a.size() = 4, a.get(0) = 99, a.get(3) = 4 → print "4 99 4".
Lesson: assigning one ArrayList reference to another never clones. To copy, use new ArrayList<>(a).
ArrayList 是对象;b = a 让两者别名同一个底层列表。通过 b 的每次修改对 a 都可见。
step | 底层列表 a.size()
a.add(1..3) | [1, 2, 3] 3
b = a | [1, 2, 3] 3 (同一个列表,两个名字)
b.set(0, 99) | [99, 2, 3] 3 (size 不变——set 是覆盖)
b.add(4) | [99, 2, 3, 4] 4 (追加 → size 增长)
于是 a.size() = 4,a.get(0) = 99,a.get(3) = 4 → 打印 "4 99 4"。
规律:把一个 ArrayList 引用赋给另一个,永远不会复制底层列表。要复制,用 new ArrayList<>(a)。
{3,7,5,7,9,7}; loop without early exit, store every match's index in idx.{3,7,5,7,9,7};循环没有提前退出,idx 每次都被命中的下标覆盖。
-1135break, no return), so it visits every index 0–5. Each match overwrites idx:
i arr[i] action idx after
0 3 skip -1
1 7 idx = 1 1
2 5 skip 1
3 7 idx = 3 3
4 9 skip 3
5 7 idx = 5 5
Final idx = 5 — the index of the last occurrence. Trap (B) is what a correctly-written "find first" would produce (with a break after the first match). On the AP exam, watch for the missing break: it inverts the algorithm from "find first" to "find last."
break、没 return),所以会遍历 0–5 的所有下标。每次命中都会覆盖 idx:
i arr[i] 动作 idx 之后
0 3 跳过 -1
1 7 idx = 1 1
2 5 跳过 1
3 7 idx = 3 3
4 9 跳过 3
5 7 idx = 5 5
最终 idx = 5 —— 最后一次出现(last occurrence)的下标。陷阱 (B) 是"找第一次出现"的正确写法(要在第一次命中后 break)的结果。AP 考试时务必盯紧有没有 break:差这一句,算法就从"找第一次"变成了"找最后一次"。
{5, 3, 8, 1, 9, 2} — state after 2 complete passes of selection sort?{5, 3, 8, 1, 9, 2} —— 选择排序 2 轮之后的状态?
{1, 2, 3, 5, 8, 9}{1, 2, 8, 5, 9, 3}{1, 3, 8, 5, 9, 2}{3, 5, 8, 1, 9, 2}k (0-indexed), find the minimum of a[k..end] and swap it to position k.
start : {5, 3, 8, 1, 9, 2}
Pass 1 (k=0):
min of full array = 1 at idx 3
swap a[0] ↔ a[3] : {1, 3, 8, 5, 9, 2}
Pass 2 (k=1):
min of a[1..5] = 2 at idx 5
swap a[1] ↔ a[5] : {1, 2, 8, 5, 9, 3}
After 2 passes, indices 0 and 1 hold the two smallest values; everything from index 2 onward is whatever was left after the swaps. Trap (A) jumps to the fully-sorted state. (C) stops after one pass.
selection sort):第 k 轮(从 0 计)找出 a[k..end] 的最小值,与位置 k 交换。
start : {5, 3, 8, 1, 9, 2}
Pass 1 (k=0):
全数组最小 = 1,下标 3
swap a[0] ↔ a[3] : {1, 3, 8, 5, 9, 2}
Pass 2 (k=1):
a[1..5] 最小 = 2,下标 5
swap a[1] ↔ a[5] : {1, 2, 8, 5, 9, 3}
2 轮之后,下标 0、1 已经是最小的两个值;下标 2 起的部分就是几次交换之后留下的样子。陷阱 (A) 直接跳到"完全排好"的状态。(C) 只做了一轮就停。
{5, 2, 8, 1, 9, 3} — state after 2 complete passes of insertion sort?{5, 2, 8, 1, 9, 3} —— 插入排序 2 轮之后的状态?
{2, 5, 8, 1, 9, 3}{1, 2, 5, 8, 9, 3}{2, 5, 8, 9, 1, 3}{5, 2, 8, 1, 9, 3}a[i] into the already-sorted prefix a[0..i-1].
start : {5, 2, 8, 1, 9, 3}
Pass 1 (i=1):
insert 2 into [5] → prefix becomes [2, 5]
array : {2, 5, 8, 1, 9, 3}
Pass 2 (i=2):
insert 8 into [2, 5] → 8 > 5, no shift
array : {2, 5, 8, 1, 9, 3}
After 2 passes, indices 0–2 are a sorted prefix; the tail is untouched. Trap (D) assumes nothing has been inserted yet.
Selection vs Insertion (compare with Q10): selection sort fixes one element at a time from the front of the unsorted region; insertion sort grows a sorted prefix by slotting each new element. After k passes both have the prefix correct, but the trailing region looks different.
insertion sort):从下标 1 开始,把 a[i] 插入已经排好序的前缀 a[0..i-1]。
start : {5, 2, 8, 1, 9, 3}
Pass 1 (i=1):
把 2 插入 [5] → 前缀变为 [2, 5]
数组 : {2, 5, 8, 1, 9, 3}
Pass 2 (i=2):
把 8 插入 [2, 5] → 8 > 5,不用移动
数组 : {2, 5, 8, 1, 9, 3}
2 轮之后,下标 0–2 已经是排好的前缀;尾部不变。陷阱 (D) 以为还没开始插入。
选择 vs 插入(对比 Q10):选择排序从未排序区的最前面,每轮固定一个元素;插入排序把已排好的前缀慢慢扩大,把新元素插入合适位置。k 轮后两者前缀都正确,但尾部状态不同。
Binary-search {2,5,7,12,18,23,31,42,56} for 19 — last element examined?在 {2,5,7,12,18,23,31,42,56} 上二分查找 19 —— 最后检查的元素是?
182331560:2, 1:5, 2:7, 3:12, 4:18, 5:23, 6:31, 7:42, 8:56.
lo hi mid arr[mid] compare to 19 next
0 8 4 18 18 < 19 lo = 5
5 8 6 31 31 > 19 hi = 5
5 5 5 23 23 > 19 hi = 4
5 4 — — lo > hi, exit (not found)
Examined values in order: 18, 31, 23. The last one examined before the loop exits is 23.
Note the search runs even when the target is absent — the loop terminates when lo > hi. Trap (A) stops after the first comparison; (C) stops before the final mid.
0:2, 1:5, 2:7, 3:12, 4:18, 5:23, 6:31, 7:42, 8:56。
lo hi mid arr[mid] 与 19 比较 下一步
0 8 4 18 18 < 19 lo = 5
5 8 6 31 31 > 19 hi = 5
5 5 5 23 23 > 19 hi = 4
5 4 — — lo > hi,退出(未找到)
检查的值依次为:18, 31, 23。退出循环之前最后检查的是 23。
注意:即使目标不存在,二分查找(binary search)也会一直运行,直到 lo > hi 时退出。陷阱 (A) 只看到第一次比较就停;(C) 在最后一个 mid 之前就停。
mystery(n) = (n == 0) ? "X" : mystery(n - 1) + mystery(n - 1); mystery(4).length()?mystery(n) = (n == 0) ? "X" : mystery(n - 1) + mystery(n - 1);mystery(4).length() 是?
481632n | mystery(n) length
0 | "X" 1
1 | "X" + "X" = "XX" 2
2 | "XX" + "XX" = "XXXX" 4
3 | "XXXX"+"XXXX" 8
4 | … (8+8) 16
In general, mystery(n) has length 2ⁿ. For n = 4, that's 2⁴ = 16.
The deeper lesson: a recursive method with k recursive calls has a call tree with branching factor k. Total work is exponential in the depth: this method makes 2ⁿ + 2ⁿ⁻¹ + … + 2 + 1 = 2ⁿ⁺¹ − 1 total calls. Even at n = 30, you'd be making ~2 billion calls — the classic naive-Fibonacci trap.
n | mystery(n) 长度
0 | "X" 1
1 | "X" + "X" = "XX" 2
2 | "XX" + "XX" = "XXXX" 4
3 | "XXXX"+"XXXX" 8
4 | … (8+8) 16
一般地,mystery(n) 的长度是 2ⁿ。n = 4 时是 2⁴ = 16。
更深的教训:递归方法中每层做 k 次递归调用,调用树就是分支因子为 k。总工作量随深度呈指数增长(exponential branching):这道题总共会调用 2ⁿ + 2ⁿ⁻¹ + … + 2 + 1 = 2ⁿ⁺¹ − 1 次。就算 n = 30,已经要做大约 20 亿次调用——这就是经典的"朴素斐波那契陷阱"。
g(n) = (n < 2) ? 0 : 1 + g(n / 2); g(17)?g(n) = (n < 2) ? 0 : 1 + g(n / 2);g(17) 是?
34517g(17) = 1 + g(8)
= 1 + 1 + g(4)
= 1 + 1 + 1 + g(2)
= 1 + 1 + 1 + 1 + g(1)
= 1 + 1 + 1 + 1 + 0
= 4
Conceptually, this is ⌊log₂(n)⌋ for n ≥ 1 — exactly the running-time intuition behind binary search (Q12).
Trap (C) counts the base call. Trap (A) stops one halving early. Be careful with the integer-division step: 17 / 2 = 8, not 8.5.
n 整除以 2 多少次才能降到 2 以下。
g(17) = 1 + g(8)
= 1 + 1 + g(4)
= 1 + 1 + 1 + g(2)
= 1 + 1 + 1 + 1 + g(1)
= 1 + 1 + 1 + 1 + 0
= 4
概念上,对 n ≥ 1,这就是 ⌊log₂(n)⌋——正是二分查找(Q12)背后的运行时直觉。
陷阱 (C) 多算了基础调用。陷阱 (A) 提前一次停止。注意整除:17 / 2 = 8,不是 8.5。
modify(a): a[0] = 99; a = new int[]{100,200,300}; a[0] = 999; — after modify(vals) on {1,2,3,4,5}, print vals[0] + " " + vals[2] + " " + vals.length.modify(a):a[0] = 99; a = new int[]{100,200,300}; a[0] = 999; —— 对 {1,2,3,4,5} 执行 modify(vals) 之后,打印 vals[0] + " " + vals[2] + " " + vals.length。
1 3 599 3 5999 300 3100 300 3a is a local copy of the reference. The body does three things, in order — and only the first escapes the method.
step | parameter a points to caller's vals
on entry | (original) {1,2,3,4,5} {1,2,3,4,5}
a[0] = 99 | {99,2,3,4,5} {99,2,3,4,5} ← shared array mutated
a = new int[]{100,200,300} | (NEW) {100,200,300} {99,2,3,4,5} ← caller untouched
a[0] = 999 | (NEW) {999,200,300} {99,2,3,4,5} ← still untouched
After return: vals = {99, 2, 3, 4, 5}. Print vals[0] = 99, vals[2] = 3, vals.length = 5 → "99 3 5".
Trap (C) "999 300 3" is the canonical mistake — assumes the method's reassignment of a is visible to the caller. It isn't. Reassigning a parameter never reaches the caller; mutating through it does. (A) "1 3 5" assumes pass-by-value means nothing happens. (D) "100 300 3" sees the rebind but misses the second mutation.
This is the array twin of Unit 3 Q7's rebind-vs-mutate. The rule is uniform across int[], ArrayList, and any class instance.
a 是引用的一个本地副本。方法体按顺序做三件事——只有第一件会逃出方法。
步骤 | 参数 a 指向 调用方的 vals
进入方法时 | (原数组){1,2,3,4,5} {1,2,3,4,5}
a[0] = 99 | {99,2,3,4,5} {99,2,3,4,5} ← 共享数组被修改
a = new int[]{100,200,300} | (新对象){100,200,300} {99,2,3,4,5} ← 调用方未受影响
a[0] = 999 | (新对象){999,200,300} {99,2,3,4,5} ← 仍未受影响
返回后:vals = {99, 2, 3, 4, 5}。打印 vals[0] = 99、vals[2] = 3、vals.length = 5 → "99 3 5"。
陷阱 (C) "999 300 3" 是经典错误——以为方法内对 a 的重新赋值(rebind)对调用方可见。其实不会。"重新赋值参数"永远不会影响调用方;"透过参数修改对象"才会。(A) "1 3 5" 以为按值传递就什么都不发生。(D) "100 300 3" 看到了 rebind,却漏掉了之后的第二次修改。
这是 Unit 3 Q7 "rebind vs mutate" 的数组版。规则对 int[]、ArrayList 和任何类实例都一致。