できない.dev

配列の最大値と最小値を求めるには

配列(リスト)の中から最大値と最小値を取り出す基本形を各言語で示す。
空の配列を渡したときに例外になるか特別な値が返るかの違い、JavaScript のスプレッド構文に大きな配列を渡すと例外になる点、文字列や浮動小数点数を比べるときの落とし穴までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(max(nums), min(nums))  # -> 9 1
 
words = ["banana", "fig", "apple"]
print(max(words))            # -> fig(文字列は辞書順で比べる)
print(max(words, key=len))   # -> banana(key で比べる基準を指定する)
 
empty = []
print(max(empty, default=None))  # -> None(default が無いと ValueError になる)

組み込みの max と min に配列を渡すと最大値と最小値が返る。
比べる基準を変えたいときは key に関数を渡す。
空の配列を渡すと ValueError になるため、空になりうる場合は default で戻り値を決めておく。

JavaScript 実行確認済み

const nums = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max(...nums), Math.min(...nums)); // -> 9 1
 
// 空配列はエラーにならず -Infinity / Infinity が返る
console.log(Math.max(...[]), Math.min(...[])); // -> -Infinity Infinity
 
// 要素数が非常に多いと、スプレッドで展開した引数が多すぎて例外になる
const big = Array.from({ length: 1_000_000 }, (_, i) => i);
try {
  Math.max(...big);
} catch (e) {
  console.log(e.name); // -> RangeError
}
 
// 大きな配列は reduce で 1 つずつ比べる
console.log(big.reduce((a, b) => (a > b ? a : b))); // -> 999999

Math.max と Math.min は配列ではなく引数を並べて受け取るので、スプレッド構文で展開して渡す。
空配列でも例外にならず -Infinity と Infinity が返るため、結果をそのまま使う前に空かどうかを確かめる。
展開した引数が多すぎると RangeError になるので、要素数が読めない配列は reduce で比べる方が安全である。

TypeScript 実行確認済み

type Item = { name: string; price: number };
 
const items: Item[] = [
  { name: "pen", price: 120 },
  { name: "note", price: 300 },
  { name: "eraser", price: 80 },
];
 
// 最大の「値」だけなら map してから Math.max に渡す
console.log(Math.max(...items.map((x) => x.price))); // -> 300
 
// 最大の「要素」が欲しいときは reduce で比べる。空配列は undefined を返すようにする
function maxBy<T>(arr: readonly T[], key: (x: T) => number): T | undefined {
  if (arr.length === 0) return undefined; // 初期値なしの reduce は空配列で TypeError になる
  return arr.reduce((a, b) => (key(b) > key(a) ? b : a));
}
 
console.log(maxBy(items, (x) => x.price)?.name); // -> note
console.log(maxBy([] as Item[], (x) => x.price)); // -> undefined

値の最大だけなら JavaScript と同じく Math.max で求まるが、オブジェクトの配列で「最大の要素そのもの」が欲しいときは reduce で比べる。
空配列に備えて戻り値を T | undefined にしておくと、呼び出し側で undefined を扱わない限り strict モードの型チェックが通らないので、空の場合の処理漏れを防げる。

うまくいかない時: TypeScript で「Object is possibly 'null'」が解消できない

Go 実行確認済み

package main
 
import (
	"fmt"
	"slices"
)
 
func main() {
	nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
	fmt.Println(slices.Max(nums), slices.Min(nums)) // -> 9 1
 
	// 空のスライスを渡すと panic するので、長さを先に確かめる
	var empty []int
	if len(empty) == 0 {
		fmt.Println("empty") // -> empty
	} else {
		fmt.Println(slices.Max(empty))
	}
 
	// 値を並べて比べるだけなら組み込みの max / min が使える
	fmt.Println(max(3, 7, 5), min(3, 7, 5)) // -> 7 3
}

slices パッケージの slices.Max と slices.Min がスライスの最大値と最小値を返す(Go 1.21 以降)。
空のスライスを渡すと panic になるため、長さを確かめてから呼ぶ。
値を引数に並べて比べるだけなら、同じく Go 1.21 で入った組み込み関数の max と min が使える。

Rust 実行確認済み

fn main() {
    let nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
    // iter() は借用なので、max / min のあとも nums を使える
    println!("{:?} {:?}", nums.iter().max(), nums.iter().min()); // -> Some(9) Some(1)
    println!("{}", nums.len()); // -> 8
 
    let empty: Vec<i32> = Vec::new();
    println!("{:?}", empty.iter().max()); // -> None
 
    // f64 は Ord を実装していないので max() は使えない。fold で比べる
    let temps = [21.5, 18.0, 25.25];
    let hottest = temps.iter().copied().fold(f64::NEG_INFINITY, f64::max);
    println!("{}", hottest); // -> 25.25
}

イテレータの max と min は Option を返し、空なら None になるので、空の場合の扱いを型で強制される。
iter() ではなく into_iter() で呼ぶと Vec の所有権が移り、そのあとで元の変数を使うとコンパイルエラーになる。
f64 は Ord を実装していないため max() が使えず、fold に f64::max を渡して比べる。

うまくいかない時: Rust で「borrow of moved value」(E0382) が解決できない

Java 実行確認済み

import java.util.*;
 
public class Main {
    public static void main(String[] args) {
        List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);
        System.out.println(Collections.max(nums) + " " + Collections.min(nums)); // -> 9 1
 
        // プリミティブの配列は IntStream で求める。空に備えて OptionalInt が返る
        int[] arr = {3, 1, 4, 1, 5, 9, 2, 6};
        System.out.println(Arrays.stream(arr).max().getAsInt()); // -> 9
 
        int[] empty = {};
        System.out.println(Arrays.stream(empty).max().orElse(-1)); // -> -1
 
        // 文字列は辞書順で比べるので、数値として比べたいなら Comparator を渡す
        List<String> codes = List.of("10", "9", "100");
        System.out.println(Collections.max(codes)); // -> 9
        System.out.println(Collections.max(codes, Comparator.comparingInt(Integer::parseInt))); // -> 100
    }
}

List なら Collections.max と Collections.min、int[] のようなプリミティブの配列なら Arrays.stream で IntStream にしてから max と min を呼ぶ。
IntStream の max は空の可能性を表す OptionalInt を返すので、orElse で空のときの値を決めておく。
Collections.max は空のリストを渡すと NoSuchElementException を投げる。

C# 実行確認済み

int[] nums = { 3, 1, 4, 1, 5, 9, 2, 6 };
Console.WriteLine($"{nums.Max()} {nums.Min()}"); // -> 9 1
 
var items = new[] { (Name: "pen", Price: 120), (Name: "note", Price: 300), (Name: "eraser", Price: 80) };
Console.WriteLine(items.Max(x => x.Price));        // -> 300(キーの最大値)
Console.WriteLine(items.MaxBy(x => x.Price).Name); // -> note(最大の要素そのもの)
 
// int の Max() は空だと InvalidOperationException を投げるので、既定値を入れてから求める
int[] empty = { };
Console.WriteLine(empty.DefaultIfEmpty(-1).Max()); // -> -1

LINQ の Max と Min で最大値と最小値が求まり、ラムダを渡せばキーの最大値になる。
最大のキーを持つ要素そのものが欲しいときは .NET 6 で追加された MaxBy を使う。
int のような値型の空の配列に Max を呼ぶと InvalidOperationException になるため、DefaultIfEmpty で既定値を補ってから呼ぶ。

つまずき

空の配列を渡したときの振る舞いは言語で大きく分かれる。
Python の max は ValueError、Go の slices.Max は panic、Java の Collections.max は NoSuchElementException、C# の int 配列に対する Max は InvalidOperationException を投げる。
これに対して JavaScript の Math.max(...[]) は例外にならず -Infinity を返すため、画面に -Infinity が表示されるまで気づきにくい。
Rust の max は None を、Java の IntStream の max は空の OptionalInt を返し、空の場合の扱いを呼び出し側に委ねる。
データが空になりうる場面では、最大値を求める前に空の場合に何を返すかを決めておく。

大きな配列を Math.max に展開すると例外になる

Math.max(...arr) は配列の要素をすべて関数の引数として渡すため、要素数には実行環境の上限がある。
Node.js 22 で試すと 12 万要素では通り、15 万要素では RangeError: Maximum call stack size exceeded になった。
上限はスタックの大きさで変わるので、件数が決まっていない配列には reduce や for ループで 1 つずつ比べる書き方を使う。
ただし初期値を渡さない reduce は空配列で TypeError: Reduce of empty array with no initial value になるため、空の確認と組み合わせる。

文字列のまま比べると数値の大小にならない

CSV や入力フォームから読んだ値は文字列のままのことが多い。
Python の max(["10", "9"]) は辞書順で比べるので "9" を返し、Java の Collections.max も同じく "9" を選ぶ。
数値として比べたいときは、Python なら key=int を渡し、Java なら Comparator.comparingInt(Integer::parseInt) を渡す。
JavaScript の Math.max は引数を数値に変換してから比べるので Math.max("10", "9") は 10 になるが、数値に変換できない値が 1 つでも混ざると結果は NaN になる。

浮動小数点数と NaN

浮動小数点数の NaN はどの値と比べても大小が決まらないため、最大値の計算を狂わせる。
Python では max([float("nan"), 1.0]) が nan を、max([1.0, float("nan")]) が 1.0 を返し、並び順で結果が変わる。
JavaScript の Math.max は NaN が 1 つでも含まれると NaN を返す。
Rust は f64 に Ord を実装しておらず、iter().max() を呼ぶと the trait bound `f64: Ord` is not satisfied というコンパイルエラーになる。
fold に f64::max を渡す方法は NaN を無視し、max_by に total_cmp を渡す方法は f64::NAN を最大の値として選ぶので、欠損値を含みうるデータではどちらの挙動が必要かを決めてから選ぶ。

この記事は役立ちましたか?