Companion to the AP-Style MC & Free-Response 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 和任何类实例都一致。
Recycling depot. Write removeLightLoads(int minKg), which removes from the ArrayList<Deposit> deposits every deposit of fewer than minKg kilograms, leaves the survivors in their original relative order, and returns how many were removed.社区回收站。编写 removeLightLoads(int minKg):从 ArrayList<Deposit> deposits 中移除所有公斤数小于 minKg 的投放物,保留下来的元素维持其原有相对顺序,并返回被移除的数量。
0, removing by index and counting从最后一个下标倒序遍历至 0,按下标移除并计数Removing element i from an ArrayList slides every later element down by one position. A forward loop that then does i++ lands on the element after the one that just slid into position i — so the slid element is never examined. Two consecutive light deposits are exactly the case that exposes this, and only one of them gets removed.从 ArrayList 中移除下标为 i 的元素后,其后的每个元素都会向前移动一位。此时若循环仍执行 i++,就会落到「刚刚滑入下标 i 的那个元素」之后的位置 — 于是滑入的那个元素从未被检查。两个相邻的轻投放物正是暴露该问题的情形:其中只有一件会被移除。
Traversing backwards removes the problem instead of patching it. When index i is removed, only indices greater than i shift — and those have already been visited. Every index still waiting to be examined is smaller than i, and none of them moves.倒序遍历是从根本上消除该问题,而非事后打补丁。移除下标 i 时,只有大于 i 的下标会发生移动 — 而它们都已被访问过。所有尚待检查的下标都小于 i,因而全都不会移动。
public int removeLightLoads(int minKg)
{
int removed = 0;
for (int i = deposits.size() - 1; i >= 0; i--)
{
if (deposits.get(i).getKilograms() < minKg)
{
deposits.remove(i);
removed++;
}
}
return removed;
}
Removing elements in a different order than they appear does not disturb the relative order of the ones that stay: each removal closes a gap without reordering anything, so the survivors keep the sequence they started in. An empty list makes deposits.size() - 1 equal to -1, the loop body never runs, and the method correctly returns 0.按与元素出现顺序不同的次序移除,并不会打乱保留元素之间的相对顺序:每次移除只是合拢一个空位,不会对任何元素重新排序,因此留下来的元素仍保持原有次序。若列表为空,则 deposits.size() - 1 等于 -1,循环体一次也不执行,方法正确地返回 0。
| #序号 | Criterion — awarded for observable behavior评分点 — 依据可观察到的行为给分 | Pts分值 |
|---|---|---|
| 1 | Examines every element of deposits, correctly accounting for the index shift that removal causes检查 deposits 中的每一个元素,且正确处理了移除所引起的下标移动 | 1 |
| 2 | Obtains the kilograms of the deposit being examined by calling getKilograms() on an element of deposits通过对 deposits 的元素调用 getKilograms() 取得当前所检查投放物的公斤数 | 1 |
| 3 | Uses a strict comparison with minKg, so a deposit of exactly minKg kilograms is kept与 minKg 作严格比较,使公斤数恰为 minKg 的投放物得以保留 | 1 |
| 4 | Removes exactly those deposits that are too light, and no others, from deposits itself从 deposits 本身中恰好移除那些过轻的投放物,且不移除其他任何元素 | 1 |
| 5 | Returns the number of deposits removed, and returns 0 when none is removed返回被移除投放物的数量;未移除任何元素时返回 0 | 1 |
| Total合计 | 5 |
for, and equally by a forward loop that advances the index only when nothing was removed, or by a while loop with the same discipline. Criterion 4 may also be met by building a new ArrayList of the survivors in order and assigning it to deposits, provided the field itself ends up holding the survivors. Criterion 5 may be met with a counter or by recording deposits.size() before the loop and returning the difference. Criteria are judged independently — a response that traverses forwards without adjusting the index loses criterion 1 but can still earn 2, 3, 4, and 5.任何满足某评分点的实现均可得该分。评分点 1 可由倒序 for 循环满足,也可由「仅在未移除元素时才递增下标」的正序循环,或采用同样策略的 while 循环满足。评分点 4 也可这样满足:按顺序新建一个仅含保留元素的 ArrayList 并赋回给 deposits,只要字段本身最终持有的是这些保留元素即可。评分点 5 可用计数器实现,也可在循环前记录 deposits.size()、最后返回差值。各评分点独立评判 — 正序遍历而未调整下标的作答会失去评分点 1,但仍可得 2、3、4、5。
i++. On the example list, removeLightLoads(10) would return 2 instead of 3: after the 3-kilogram deposit at index 1 is removed, the 7-kilogram deposit slides into index 1 and i++ skips straight past it.正序循环中无条件执行 i++。对示例列表,removeLightLoads(10) 会返回 2 而非 3:下标 1 处的 3 公斤投放物被移除后,7 公斤那件滑入下标 1,而 i++ 恰好跨了过去。deposits.size() in the loop bound of a forward loop. for (int i = 0; i < n; i++) with n fixed before the loop walks past the end of a shrinking list and throws IndexOutOfBoundsException.在正序循环的边界中缓存 deposits.size()。若 n 在循环前就已固定,for (int i = 0; i < n; i++) 会越过不断缩短的列表的末端,抛出 IndexOutOfBoundsException。for loop. for (Deposit d : deposits) with a remove in the body throws ConcurrentModificationException. An enhanced for may read a list but must not structurally change it.在增强型 for 循环中移除元素。在 for (Deposit d : deposits) 的循环体内调用 remove 会抛出 ConcurrentModificationException。增强型 for 可以读取列表,但不得改变其结构。<= instead of <. That removes a deposit of exactly minKg kilograms, which the question keeps. On the example list removeLightLoads(9) would return 3 rather than 2.把 < 误写为 <=。这会移除公斤数恰为 minKg 的投放物,而题目要求保留它。对示例列表,removeLightLoads(9) 会返回 3 而非 2。deposits untouched. The question asks for both, and they are scored separately.只计数而不移除。若方法只统计过轻的投放物并返回其数量,返回值示例全部通过,但 deposits 丝毫未变。题目对两者都有要求,且它们分别评分。ArrayList situation where the obvious loop is wrong, and it is worth understanding rather than memorizing. The trouble is that an index is not a name for an element — it is a name for a position, and removal moves elements between positions. Going backwards fixes this not by cleverness but by arrangement: every position that removal disturbs is a position you are already finished with. That is the general move. When an operation invalidates part of your state, reorder the work so the invalidated part is always the part you no longer need. The same reasoning is why you delete files from the end of a list, and why the backward loop shows up again the first time you shift array elements in place.「边遍历边移除」是 ArrayList 中唯一一种「最直观的循环恰恰是错的」的情形,值得真正理解而非死记。问题的根源在于:下标并不是元素的名字 — 它是位置的名字,而移除会使元素在位置之间迁移。倒序遍历之所以奏效,靠的不是技巧而是安排:凡是会被移除操作扰动的位置,都是你已经处理完毕的位置。这才是可推广的思路 — 当某个操作会使你的一部分状态失效时,就重新安排工作顺序,使失效的那部分永远是你不再需要的那部分。同样的道理也解释了为何删除文件要从列表末尾开始,以及为何当你第一次就地移动数组元素时,倒序循环会再次出现。Solar farm. Write bestSlot(), which returns the index of the time slot (column) of the rectangular int[][] output whose total across all panel rows is greatest, returning the smallest index when slots tie.太阳能电站。编写 bestSlot():对矩形数组 int[][] output,返回沿所有光伏板行求和后总量最大的那个时段(列)的下标;若有并列,返回其中最小的下标。
The usual nesting visits a 2D array row by row. This question asks for a total down a column, so the nesting is reversed: the outer loop chooses the slot and the inner loop walks the rows for that one slot. Nothing about the array changes — only which index is held still while the other moves.常见的嵌套是逐行访问二维数组。本题要求的是沿某一列向下求和,因此嵌套顺序相反:外层循环选定时段,内层循环为该时段逐行向下遍历。数组本身没有任何变化 — 变的只是哪个下标保持不动、哪个下标在移动。
The number of slots is output[0].length, the length of one row, because a row holds one entry per slot. The number of rows is output.length. Getting these two the wrong way round is harmless on a square array and throws immediately on any other, which is why the question insists the array need not be square.时段的数量是 output[0].length,即一行的长度 — 因为每一行中每个时段各占一个元素。行的数量则是 output.length。把这两者弄反,在方阵上毫无影响,在任何非方阵上则会立即抛出异常;这正是题目特意强调数组不一定是方阵的原因。
public int bestSlot()
{
int bestIndex = 0;
int bestTotal = -1;
for (int c = 0; c < output[0].length; c++)
{
int total = 0;
for (int r = 0; r < output.length; r++)
{
total += output[r][c];
}
if (total > bestTotal)
{
bestTotal = total;
bestIndex = c;
}
}
return bestIndex;
}
total is declared inside the outer loop, so it starts at 0 for each slot. Declaring it outside would make every slot total include the slots before it. Seeding bestTotal at -1 guarantees that slot 0 wins the first comparison, since every entry is at least 0, so bestIndex is always set at least once — including for an array whose entries are all 0.total 声明在外层循环内部,因此每个时段都从 0 重新开始。若声明在外面,每个时段的总量都会把它之前各时段的总量一并累计进来。把 bestTotal 的初值设为 -1,可保证时段 0 必定赢得第一次比较(因为每个元素都不小于 0),从而 bestIndex 至少被赋值一次 — 即便数组的所有元素都是 0 也是如此。
The tie rule is carried entirely by the strict >. A later slot that merely equals the best total fails the test, so bestIndex keeps the earlier slot. Writing >= would silently return the largest tied index instead of the smallest.并列规则完全由严格的 > 承担。若后面某个时段的总量只是等于当前最大值,该判断为假,bestIndex 便保留更靠前的那个时段。写成 >= 则会悄无声息地返回并列中最大的下标,而非最小的。
| #序号 | Criterion — awarded for observable behavior评分点 — 依据可观察到的行为给分 | Pts分值 |
|---|---|---|
| 1 | Visits every time-slot index from 0 through output[0].length - 1遍历从 0 到 output[0].length - 1 的每一个时段下标 | 1 |
| 2 | For each slot, visits every panel row from 0 through output.length - 1对每个时段,遍历从 0 到 output.length - 1 的每一行光伏板 | 1 |
| 3 | Accesses entries as output[row][slot] — row index first, slot index second以 output[行][时段] 的形式访问元素 — 行下标在前,时段下标在后 | 1 |
| 4 | Accumulates each slot’s total across the rows, beginning again from 0 for every new slot沿各行累加出每个时段的总量,且每换一个时段都重新从 0 开始 | 1 |
| 5 | Compares each slot total to the best total seen so far in a way that keeps the earlier index when two totals are equal将每个时段的总量与当前最大总量作比较,且在两者相等时保留更靠前的下标 | 1 |
| 6 | Returns the index of the best slot, not its total返回最佳时段的下标,而非其总量 | 1 |
| Total合计 | 6 |
for or while, and the slot bound may be written output[0].length or as the length of any row. Criterion 4 may be met with a local total reset each time round the outer loop, or by an array of slot totals filled in a first pass. Criterion 5 may be met by a strict > against a running best, by seeding the best from slot 0 before the loop, or by scanning a completed array of totals for the first occurrence of its maximum. Criteria are judged independently: a response with the loops correct but the tie rule reversed earns 1, 2, 3, 4, and 6 but not 5.任何满足某评分点的实现均可得该分。循环可用 for 也可用 while;时段数的上界写作 output[0].length 或任意一行的长度均可。评分点 4 可通过「每轮外层循环重置一个局部总量」实现,也可先用一趟遍历填出一个各时段总量的数组。评分点 5 可通过「与当前最大值作严格 > 比较」实现,也可在循环前用时段 0 为最大值赋初值,或在算好的总量数组中查找其最大值首次出现的位置。各评分点独立评判:若循环全部正确而并列规则写反,可得 1、2、3、4、6,但不得 5。
output.length as the number of slots and output[0].length as the number of rows passes on a square array and throws ArrayIndexOutOfBoundsException on the 3×5 example. Test any 2D method on a non-square array.把两个长度弄反。用 output.length 作时段数、output[0].length 作行数,在方阵上能通过,在 3×5 的示例上则会抛出 ArrayIndexOutOfBoundsException。任何二维数组的方法都应在非方阵上测试。output[c][r]. The first index is always the row. Reversing the subscripts while keeping the loop bounds correct produces either an exception or, on a square array, the answer to a different question — the best row.写成 output[c][r]。第一个下标永远是行。在循环边界正确的前提下颠倒下标,要么抛出异常,要么在方阵上给出另一个问题的答案 — 即最佳的行。total outside the outer loop. The totals then accumulate across slots and never decrease, so the method returns the last slot index every time.把 total 声明在外层循环之外。各时段的总量会不断累加而永不下降,方法因而每次都返回最后一个时段的下标。>= in the comparison. Every tie then overwrites the earlier index, and the method returns the largest tied index instead of the smallest. This is invisible until two slots actually tie, which is why the tie case needs a test of its own.比较时使用 >=。每次并列都会覆盖此前的下标,方法便会返回并列中最大而非最小的下标。在真正出现并列之前,这个错误完全看不出来 — 这正是并列情形需要单独测试的原因。return bestTotal; gives 22 where 3 was wanted. The two happen to be small numbers of the same type, so the compiler says nothing.返回总量而非下标。return bestTotal; 会给出 22,而所求是 3。两者恰好都是同一类型的小整数,编译器不会给出任何提示。output[0][0]. That is one entry, not slot 0’s total. With three rows it is far too small to be a valid starting total, and although this particular method still returns the right index, the habit breaks as soon as entries may be negative.用 output[0][0] 为最大总量赋初值。那是单个元素,而非时段 0 的总量。在有三行的情况下,它作为初始总量远远偏小;尽管本方法仍能返回正确的下标,可一旦元素允许为负,这个习惯就会立刻出错。output.length and output[0].length mean such different things, and why only the first subscript can be asked for its length directly. Once you accept that the array is indifferent to your picture of it, every 2D question reduces to one decision: which subscript does the outer loop control? Summing along rows and summing down columns are the same six lines with the loop headers exchanged. Say out loud what the outer loop is choosing — here, “for each time slot” — and the nesting writes itself; guess at it, and a square test array will let the mistake through to the exam.二维数组本身既没有行也没有列 — 它是「数组的数组」,「行」与「列」不过是我们强加给两个下标的名字。这正是 output.length 与 output[0].length 含义如此不同的原因,也是只有第一个下标能直接被问及长度的原因。一旦接受「数组并不在意你脑中的那张图」,所有二维数组的题目都归结为一个决定:外层循环控制的是哪个下标?沿行求和与沿列求和,本是同样的六行代码,只把两个循环头对调而已。把外层循环所选定的东西念出声来 — 此处是「对每一个时段」 — 嵌套结构便会自然成形;若只是凭感觉猜,一个方阵测试用例就会把这个错误一路放行到考场上。