前言

這篇是我在解 HackerRank Plus Minus 時的刷題筆記。

題目本身在演算法層面不複雜, 單次走訪就能完成。但真正值得記錄的點是「跨語言數值處理」: 在 TypeScript 幾乎一路平順, 在 Java 卻可能因為整數除法與型別選擇而踩到地雷。

1. 題目要求與核心考點

給定一個包含 $N$ 個整數的陣列, 要求計算「正數、負數、零」各自所占的比例, 並分三行輸出。

真正的核心考點在於:

  1. 你是否意識到 Java 的整數除法會截斷小數。
  2. 你是否知道題目對浮點誤差的寬容範圍, 以及何時應該主動做格式化輸出。

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)

雖然初版程式碼很直白, 但其實連續閃過了兩個跨語言常見地雷:

  1. 閃過 Java 的整數除法陷阱。 在 Java 中若寫 int / int(例如 2 / 5), 系統會直接捨去小數得到 0。但初版宣告 Double positive = 0D, 讓後續除法觸發型別提升, 把 int length 轉為浮點運算, 因此可以正確得到 0.4
  2. 看懂 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, 並進行四捨五入。
JavaSystem.out.printf("%.6f%n", val)%n 為跨作業系統安全的換行符號。
Java(轉字串情境)String.format("%.6f", val)題目要求函式 return String 時可直接使用。

6. 複雜度分析

  • 時間複雜度(Time Complexity): $O(N)$, 只需單次走訪陣列一次.
  • 空間複雜度(Space Complexity): $O(1)$, 只使用固定數量的計數變數.

7. 核心體悟

  1. 第一版寫得快, 第二版寫得精: 刷題時先求 AC, 再透過重構把可讀性與可維護性補齊。
  2. 對 JVM 型別保持敏感: 在 TypeScript number 走天下, 但在 Java 區分 primitive 與 wrapper 是基本功。
  3. 先看題目誤差條款再決定輸出策略: 能快速通關, 也能在嚴格模式下有標準答案。

總結

這題表面上是比例計算, 實際上是在練語言細節敏感度。

  • 演算法層面很簡單, 真正差距在型別與輸出格式。
  • TypeScript 與 Java 都能用 $O(N)$ 解決, 但最佳實作重點不同。
  • 通關後再做一次重構, 才能把題目價值完整榨出來。

參考資料