前言
這篇是我在解 HackerRank A Very Big Sum 時的刷題筆記.
題目本身看似只是把陣列全部加總, 但它真正想測的是你有沒有意識到「數字可能大到超出一般整數型別的範圍」. 在 TypeScript 裡這題幾乎是一路平順, 但到了 Java, 若沒有先想清楚 int、long 和 Stream 的差異, 很容易在型別上先翻車.
題目要求與核心考點
題目要求對一個整數陣列進行加總, 但特別標註了數值可能非常大, 單一元素甚至可達 $10^{10}$.
這題真正的考點其實不是加法本身, 而是先確認「你現在用的型別, 能不能安全裝下結果」. 只要型別選錯, 算法再簡單也會在測資裡直接失敗.
跨語言數值型別對照
在處理大數運算時, 先理解語言底層的數值型別很重要. 以下是 Java 與 TypeScript 的對照:
| 語言 | 型別 | 說明與極限範圍 |
|---|---|---|
| Java | int | 32 位元整數, 最大值約 21 億 ($2^{31} - 1$). 遇到這題的高位測資時, 很容易直接溢位. |
| Java | long | 64 位元整數, 最大值約 $2^{63} - 1$. 這題應該使用這個型別. |
| Java | float | 32 位元單精度浮點數, 用於帶小數點的數值, 精度較低. |
| Java | double | 64 位元雙精度浮點數, 用於帶小數點的數值, 精度較高. |
| TypeScript | number | 底層是 64 位元雙精度浮點數, 安全整數範圍可到 $2^{53} - 1$. 這題的加總規模遠小於這個上限, 所以用 number 即可. |
| TypeScript | bigint | 任意精度整數. 若遇到超過安全整數範圍的天文數字, 可以用 n 後綴來保證精度. |
TypeScript 的歲月靜好
延用上一題的 reduce 寫法, 這題在 TypeScript 幾乎沒有壓力. 只要知道 number 的安全邊界遠大於本題測資規模, 一行就能穩穩過關.
function aVeryBigSum(ar: number[]): number {
return ar.reduce((a, b) => a + b, 0);
}
Java 的嚴格審查與三種手感測試
面對 $10^{10}$ 等級的數值, Java 必須老老實實使用 long. 我自己在整理時試了三種寫法, 其中有一種還踩到經典的 Lambda 限制.
寫法 A: 傳統 for loop
這是最直覺、也最穩妥的寫法. 先宣告 long sum = 0L, 再直接累加.
long sum = 0L;
for (Long num : ar) {
sum += num;
}
return sum;
寫法 B: forEach 與 Lambda 的碰撞
原本想用 ar.forEach(num -> sum += num) 直接累加外部變數, 但會遇到編譯錯誤:
local variables referenced from a lambda expression must be final or effectively final
踩坑原因在於 Java 的 Lambda 不允許任意修改外部的區域變數. 這些變數必須是 effectively final, 也就是說在 Lambda 內不能被重新賦值.
理論上可以改用 LongAdder 繞過限制, 但那是為了多執行緒高併發設計的工具, 用在單純陣列加總上其實有點殺雞用牛刀.
寫法 C: Stream API 的流水線
如果不想碰外部狀態, Java 8 的 Stream API 會是更乾淨的選擇. 它把整個過程拆成資料流、型別轉換、終端加總三段, 可讀性很高.
long sum = ar.stream()
.mapToLong(Long::longValue)
.sum();
流水線拆解
ar.stream(): 把List<Long>展開成資料流, 讓元素開始進入管線.mapToLong(Long::longValue): 把包裝型別Long拆成原生型別long, 並升級成LongStream.sum(): 終端操作, 將所有數值加總後輸出結果.
這種寫法的好處是語意清楚, 沒有副作用, 也不需要手動維護外部累加狀態.
複雜度分析
- 時間複雜度: $O(N)$, 因為不管是
reduce、for loop還是 Stream, 都需要把長度為 $N$ 的陣列走訪一次. - 空間複雜度: $O(1)$, 只使用固定大小的累加器, 沒有額外隨輸入成長的記憶體需求.
核心體悟
- 跨語言解題時, 先確認型別範圍再寫邏輯, 比單純把程式打出來更重要.
- Java 的 Lambda 有明確的狀態限制, 遇到需要累加的場景, 傳統
for迴圈通常最穩. - Stream API 不是只是語法糖, 它能把資料處理過程拆成更清楚的流水線, 也更符合函式式思維.
總結
這題表面上是加總, 實際上是在提醒你: 大數題目先看型別, 再看寫法.
- TypeScript 因為
number的安全範圍夠大, 可以直接用reduce. - Java 必須選對
long, 才不會在中途溢位. - 若想把程式寫得更乾淨, Stream API 會比手動維護外部變數更自然.
把這個習慣養起來, 後面遇到數值範圍更敏感的題目時, 會少很多不必要的坑.