Files
hello-algo/ja/docs/chapter_data_structure/number_encoding.md
T
Yudong Jin 7257641e0e Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese

* Fit Japanese homepage labels and localize heading permalinks

* Point Japanese contribution instructions to the Japanese preview

* Replace locale conditionals with multilingual UI tables
2026-10-30 02:12:26 +08:00

9.6 KiB
Raw Blame History

数値の符号化 *

!!! tip

本書で見出しに * が付く節は、任意の学習内容です。時間が足りない場合や難しく感じる場合は、ひとまず飛ばし、必読の節を学んだ後で取り組んでかまいません。

符号絶対値表現、1 の補数表現、2 の補数表現

前の節の表では、どの整数型も、表せる負の数が正の数より一つ多くなっていました。たとえば byte の範囲は [-128, 127] です。直感に反するこの性質には、符号絶対値表現、1 の補数表現、2 の補数表現が関わります。

まず、数値はコンピュータ内で「2 の補数表現」により保存されることを押さえましょう。理由を考える前に、三つを定義します。

  • 符号絶対値表現:二進表現の最上位ビットを符号とし、0 を正、1 を負とします。残りのビットで数の値を表します。
  • 1 の補数表現:正の数は符号絶対値表現と同じです。負の数は、符号絶対値表現の符号ビット以外をすべて反転します。
  • 2 の補数表現:正の数は符号絶対値表現と同じです。負の数は、1 の補数表現に 1 を加えます。

下図に、三つの表現の変換方法を示します。

符号絶対値表現、1 の補数表現、2 の補数表現の相互変換

符号絶対値表現(sign-magnitude)は最も直感的ですが、制約があります。一つは、負の数をそのまま演算に使えないことです。たとえば 1 + (-2) をこの表現で計算すると、明らかに誤った結果 -3 になります。


\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
& = 1000 \; 0011 \newline
& \rightarrow -3
\end{aligned}

この問題を解決するため、1 の補数表現(1's complement)が導入されました。符号絶対値表現から変換して 1 + (-2) を計算し、結果を元の表現へ戻すと、正しい答え -1 が得られます。


\begin{aligned}
& 1 + (-2) \newline
& \rightarrow 0000 \; 0001 \; \text{(符号絶対値)} + 1000 \; 0010 \; \text{(符号絶対値)} \newline
& = 0000 \; 0001 \; \text{(1 の補数)} + 1111  \; 1101 \; \text{(1 の補数)} \newline
& = 1111 \; 1110 \; \text{(1 の補数)} \newline
& = 1000 \; 0001 \; \text{(符号絶対値)} \newline
& \rightarrow -1
\end{aligned}

もう一つは、ゼロの符号絶対値表現が +0 と -0 の二つあることです。ゼロに異なる二進符号が対応すると、曖昧さが生じる可能性があります。たとえば条件判定で正負のゼロを適切に区別しなければ、誤った結果になるかもしれません。この曖昧さに対応するには追加の判定が必要で、演算効率を下げる可能性があります。


\begin{aligned}
+0 & \rightarrow 0000 \; 0000 \newline
-0 & \rightarrow 1000 \; 0000
\end{aligned}

1 の補数表現にも正負のゼロの問題があるため、さらに2 の補数表現(2's complement)が導入されました。まず、負のゼロの変換を見てみましょう。


\begin{aligned}
-0 \rightarrow \; & 1000 \; 0000 \; \text{(符号絶対値)} \newline
= \; & 1111 \; 1111 \; \text{(1 の補数)} \newline
= 1 \; & 0000 \; 0000 \; \text{(2 の補数)} \newline
\end{aligned}

負のゼロの 1 の補数に 1 を加えると桁上がりしますが、byte は 8 ビットなので、9 ビット目にはみ出た 1 は捨てられます。つまり、負のゼロの 2 の補数は 0000 \; 0000 で、正のゼロと同じです。ゼロの表現が一つになり、曖昧さが解消されます。

最後に、byte の範囲 [-128, 127] で、一つ余分な負の数 -128 がどこから来るのかを考えます。[-127, +127] の整数には、符号絶対値表現、1 の補数表現、2 の補数表現があり、符号絶対値表現と 2 の補数表現は相互変換できます。

ただし、2 の補数 1000 \; 0000 は例外で、対応する符号絶対値表現がありません。変換手順に従うと 0000 \; 0000 になりますが、これは数値 0 で、その 2 の補数表現は自身であるはずなので矛盾します。この特殊な 1000 \; 0000 は、-128 を表すと定められています。実際、2 の補数で (-1) + (-127) を計算すると -128 になります。


\begin{aligned}
& (-127) + (-1) \newline
& \rightarrow 1111 \; 1111 \; \text{(符号絶対値)} + 1000 \; 0001 \; \text{(符号絶対値)} \newline
& = 1000 \; 0000 \; \text{(1 の補数)} + 1111  \; 1110 \; \text{(1 の補数)} \newline
& = 1000 \; 0001 \; \text{(2 の補数)} + 1111  \; 1111 \; \text{(2 の補数)} \newline
& = 1000 \; 0000 \; \text{(2 の補数)} \newline
& \rightarrow -128
\end{aligned}

ここまでの計算がすべて加算であることに気づいたかもしれません。これは重要な事実、コンピュータ内部の回路は主に加算を基礎に設計されていることを示しています。加算は乗算・除算・減算などに比べて回路を実装しやすく、並列化も容易で、演算速度が速いためです。

もちろん、加算しかできないという意味ではありません。加算と基本的な論理演算を組み合わせれば、さまざまな数学的演算を実現できます。たとえば減算 a - b は加算 a + (-b) に置き換えられ、乗算と除算も、複数回の加算や減算に変換できます。

2 の補数を使う理由は、正負の加算を同じ回路と操作で処理でき、減算専用の回路や正負のゼロへの特別な処理が不要になるためです。これにより、回路設計が大幅に簡単になり、演算効率が高まります。

2 の補数の設計は巧妙ですが、紙幅の都合上、ここまでにします。興味があれば、さらに詳しく調べてみてください。

浮動小数点数の符号化

int と float はどちらも 4 バイトなのに、なぜ float の方がはるかに広い範囲を表せるのでしょうか。小数も表すなら範囲が狭くなりそうなので、直感に反します。

実際には、float が別の表現方法を使っているためです。32 ビットの二進数を次のように表します。


b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0

IEEE 754 規格によれば、32-bit の float は三つの部分からなります。

  • 符号部 $\mathrm{S}$:1 ビットで、b_{31} に対応します。
  • 指数部 $\mathrm{E}$:8 ビットで、b_{30} b_{29} \ldots b_{23} に対応します。
  • 仮数部 $\mathrm{N}$:23 ビットで、b_{22} b_{21} \ldots b_0 に対応します。

二進数で表した float の値は、次の式で計算します。


\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2

十進数の式に直すと、次のようになります。


\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N})

各項の範囲は次のとおりです。


\begin{aligned}
\mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}]
\end{aligned}

IEEE 754 に従った float の計算例

上図の例では、$\mathrm{S} = 0$、$\mathrm{E} = 124$、\mathrm{N} = 2^{-2} + 2^{-3} = 0.375 なので、次の値になります。


\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875

これで最初の疑問に答えられます。float には指数部があるため、int よりはるかに広い範囲を表せるのです。上の計算から、最大の正の数は 2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38} となり、符号を変えると最小の負の数になります。

float は範囲を広げる代わりに、精度を犠牲にしています。int は 32 ビット全体で数を表し、値は等間隔に分布します。一方、float は指数部があるため、数値が大きいほど、隣り合う表現可能な数の間隔も大きくなる傾向があります。

下表のように、指数部 \mathrm{E} = 0 と \mathrm{E} = 255 は特殊な意味を持ち、ゼロ、無限大、\mathrm{NaN} などを表すために使います。

表   指数部の意味

指数部 E 仮数部 \mathrm{N} = 0 仮数部 \mathrm{N} \ne 0 計算式
0 \pm 0 非正規化数 (-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})
1, 2, \dots, 254 正規化数 正規化数 (-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})
255 \pm \infty \mathrm{NaN}

非正規化数は、浮動小数点数の精度を大きく改善します。最小の正の正規化数は $2^{-126}$、最小の正の非正規化数は 2^{-126} \times 2^{-23} です。

倍精度の double も float に似た方法で表現するため、ここでは説明を省きます。