前言
這篇是我在解 HackerRank Plus Minus 時的刷題筆記。
題目本身在演算法層面不複雜, 單次走訪就能完成。但真正值得記錄的點是「跨語言數值處理」: 在 TypeScript 幾乎一路平順, 在 Java 卻可能因為整數除法與型別選擇而踩到地雷。
1. 題目要求與核心考點
給定一個包含 $N$ 個整數的陣列, 要求計算「正數、負數、零」各自所占的比例, 並分三行輸出。
真正的核心考點在於:
- 你是否意識到 Java 的整數除法會截斷小數。
- 你是否知道題目對浮點誤差的寬容範圍, 以及何時應該主動做格式化輸出。
2. 我的第一版實作 (Version 1: 功能通關版)
解題當下的直覺, 是採用標準的指令式寫法: 宣告三個計數器, 走訪陣列時透過 if-else 歸類, 最後相除印出。
TypeScript 初版程式碼
function plusMinus(arr: number[]): void {
let positive: number = 0;
let negative: number = 0;
let zero: number = 0;
const { length } = arr;
arr.forEach((num) => {
if (num < 0) {
negative += 1;
} else if (num > 0) {
positive += 1;
} else {
zero += 1;
}
});
console.log(positive / length);
console.log(negative / length);
console.log(zero / length);
}
Java 初版程式碼
public static void plusMinus(List<Integer> arr) {
Double positive = 0D;
Double negative = 0D;
Double zero = 0D;
int length = arr.size();
for (int i = 0; i < length; i++) {
int num = arr.get(i);
if (num < 0) {
negative += 1;
} else if (num > 0) {
positive += 1;
} else {
zero += 1;
}
}
System.out.println(positive / length);
System.out.println(negative / length);
System.out.println(zero / length);
}
3. 初版立大功的兩個神迴避 (Aha Moments)
雖然初版程式碼很直白, 但其實連續閃過了兩個跨語言常見地雷:
- 閃過 Java 的整數除法陷阱。
在 Java 中若寫
int / int(例如2 / 5), 系統會直接捨去小數得到0。但初版宣告Double positive = 0D, 讓後續除法觸發型別提升, 把int length轉為浮點運算, 因此可以正確得到0.4。 - 看懂 HackerRank 的寬容條款。 題目雖然寫到小數點後 6 位, 但 Note 也明確說明可接受誤差(error up to $10^{-4}$)。因此初版直接印原生浮點數也能 AC, 不必一開始就卡在格式化語法。
4. 進階 Code Review 與重構 (Version 2: Senior 優化版)
在演算法邏輯達標後, 我從可擴展性與 JVM 記憶體開銷角度再打磨一次。
TypeScript 進化版: 查表歸類法
function plusMinus(arr: number[]): void {
const { length } = arr;
const counts = { positive: 0, negative: 0, zero: 0 };
arr.forEach((num) => {
const key = num > 0 ? "positive" : num < 0 ? "negative" : "zero";
counts[key]++;
});
Object.values(counts).forEach((count) => {
console.log((count / length).toFixed(6));
});
}
V1 到 V2 的差異:
- 消滅重複分支輸出: 透過物件查表(Object Lookup)集中管理分類結果。
- 嚴謹度補強: 主動加上
.toFixed(6), 在嚴格比對字串的評測環境更穩。
Java 進化版: primitive 小寫與語意化迴圈
public static void plusMinus(List<Integer> arr) {
double positive = 0;
double negative = 0;
double zero = 0;
int length = arr.size();
for (int num : arr) {
if (num > 0) positive++;
else if (num < 0) negative++;
else zero++;
}
System.out.printf("%.6f%n", positive / length);
System.out.printf("%.6f%n", negative / length);
System.out.printf("%.6f%n", zero / length);
}
V1 到 V2 的差異:
- 記憶體與效能更精省:
Double改為double, 減少裝箱與拆箱成本。 - 語意更清楚: 改用 enhanced for-loop, 直接傳達「只關心元素值, 不關心索引」。
5. 跨語言嚴格格式化小數 (Cheat Sheet)
若未來遇到嚴格比對字串輸出的機考題, 可以直接套用以下寫法:
| 語言 | 語法寫法 | 說明 |
|---|---|---|
| TypeScript / JS | (val).toFixed(6) | 回傳型別為 string, 並進行四捨五入。 |
| Java | System.out.printf("%.6f%n", val) | %n 為跨作業系統安全的換行符號。 |
| Java(轉字串情境) | String.format("%.6f", val) | 題目要求函式 return String 時可直接使用。 |
6. 複雜度分析
- 時間複雜度(Time Complexity): $O(N)$, 只需單次走訪陣列一次.
- 空間複雜度(Space Complexity): $O(1)$, 只使用固定數量的計數變數.
7. 核心體悟
- 第一版寫得快, 第二版寫得精: 刷題時先求 AC, 再透過重構把可讀性與可維護性補齊。
- 對 JVM 型別保持敏感: 在 TypeScript
number走天下, 但在 Java 區分 primitive 與 wrapper 是基本功。 - 先看題目誤差條款再決定輸出策略: 能快速通關, 也能在嚴格模式下有標準答案。
總結
這題表面上是比例計算, 實際上是在練語言細節敏感度。
- 演算法層面很簡單, 真正差距在型別與輸出格式。
- TypeScript 與 Java 都能用 $O(N)$ 解決, 但最佳實作重點不同。
- 通關後再做一次重構, 才能把題目價值完整榨出來。