有限小数、循環小数、分数、無理数

2進数の循環小数に注意
数の概念
複素数実数有理数整数自然数\( 1, \ 2, \ 3 \)
ゼロ\( 0 \)
負の整数\( -1, \ -2, \ -3 \)
有限小数分数\( \displaystyle 0.5, \ \frac{3}{4} \)
循環小数\( \displaystyle \frac{1}{3} \)
無理数無限小数\( \displaystyle \sqrt{2}, \ \pi, \ \log2 \)
虚数\( \displaystyle 3i, \ -5i \)
長さや重さといった計測値は、小数で表すことが多い。小数を表すデータ属性には、固定小数と浮動小数がある。あらわせる数の範囲と誤差、とくに2進数で循環小数になる場合に注意が必要だ。

サンプル・プログラム

固定小数

たとえば8ビットのデータを2つに分割し、5ビットを整数部、3ビットを小数部に充てるとする。ビット数が固定されているところから、固定小数と呼ぶ。
固定小数で 01101.011 は
\( \displaystyle 01101.011 = 2^3 + 2^2 + 2^0 + 2^{-2} + 2^{-3} = 13.375 \)
となる。
5+3の固定小数では、整数部の範囲は +15~-16 で、小数部は 2-3 = 0.125 未満は扱うことができない(誤差になってしまう)。
データ幅を64ビットに増やしたとしても、固定小数の位置によって、その数量を計算するのにふさわしくない可能性がある。
このため、固定小数は、黎明期を除いてコンピュータで利用されることは無かった。

浮動小数

実数を、符号、仮数、指数の3つ組データで表現するのが浮動小数である。
10進数の浮動小数は、たとえば -123.4 = -1.23×102 とあらわし、1.23が仮数、2が指数となる。
浮動小数はIEEE754という国際規格で定められており、符号は1ビット、仮数は23ビット、指数は8ビットの合計32ビットである。
コンピュータ内部では2進数となるから、表現可能な最小値は ±2-126 ≒ ±1.2×10-38、最大値は ±2127 ≒ ±3.4×1038 となる。32ビット整数で扱える最大値が 2.1×109 であることを考えると、それよりはるかに大きな数を扱うことができる。
また、浮動小数は3つのデータから成ることから、簡単なデータ構造であるといってもいい。

名数の計算では単位を揃える必要があることは、すでに述べたとおりだ。たとえば長さの単位を SI単位系であるメートルに統一すると、水素原子核の直径から宇宙の果てまでの距離まで、浮動小数の範囲で扱うことができる。
対象メートル
水素原子核の直径1.75×10-15
最新CPUの配線の幅1.4×10-8
インフルエンザウイルスの大きさ1.0×10-7
大腸菌の大きさ1.0~3.0×10-6
ヒトの毛髪の直径1.0×10-4
日本人成人男子の平均身長1.67×100
東京駅~新大阪駅5.526×106
成田空港~ヒースロー空港9.585×107
地球~月3.844×1010
地球~太陽1.496×1012
太陽~冥王星(平均距離)5.954×1013
最も近い恒星(プロキシマケンタウリ)までの距離4.02×1016
天の川銀河中心部までの距離2.65×1020
ソンブレロ星雲(M104)までの距離2.65×1023
宇宙の果てまでの距離1.31×1026
システムで扱おうとしている数が、浮動小数の範囲内に収まるかどうか注意が必要だ。SI単位系は、SI接頭辞(マイクロ、ミリ、キロ、メガ‥‥)を付けることで10±3ずつ指数部の桁数を変えることができるから、範囲内に収まるような単位と接頭辞を選ぼう。

また、データ幅を64ビットに拡張した倍精度浮動小数も定義されている。
CやC#では、単精度を float、倍精度を double で型宣言する。
 符号仮数指数範囲
単精度1ビット23ビット8ビット±1.2×10-38~±3.4×1038
倍精度1ビット52ビット11ビット±2.2×10-308~±1.8×10308

数の精度

単精度浮動小数では 1.2×10-38 より小さな数が、倍精度浮動小数では 2.2×10-308 より小さな数を扱うことができない。つまり、これより小さい数は誤差となる。
測定値の場合は有効数字として桁数を使うことが多いが、浮動小数の場合は指数のビット数が精度を左右する。

循環小数

1 ÷ 3 = 0.333‥‥循環小数である。
このため、データ属性が浮動小数の場合、1 ÷ 3 × 3= 0.999‥‥≠1 ということが起こりうる。システム開発する際、処理系の制約を確認しておこう。

10進数では有限小数だが、2進数で循環小数になる場合もある。たとえば 0.4 がそうである。
\( \displaystyle 0.4 = 2^{-2} + 2^{-3} + 2^{-6} + 2^{-7}... \)

   2: //演算誤差
   3: $a = 6 - 5.6;
   4: $b = 0.4;
   5: echo  $a - $b;

"dt02-04-01.php" はPHPによる簡単な引き算だが、処理系によってはゼロにならない。
#演算誤差
a = 6 - 5.6
b = 0.4
print(a - b)
Pythonプログラム "dt02-04-02.py" でも同様に誤差が発生する。
//演算誤差
int main() {
	float a, b;
	a = 6 - 4.6;
	b = 0.4;
	cout << setprecision(8) << setiosflags(ios::fixed) << a - b << endl;
	return(0);
}
一方、C++(g++でコンパイル)プログラム "dt02-04-21.cpp" では誤差が発生しない。
このように循環小数になると、数の精度の範囲内であっても演算誤差が発生する場合がある。とくにループの終了条件で循環小数が発生したりすると、システム設計は正しくてもループ回数が狂うといったバグの温床になりかねない。

分数

RubyやPythonは分数を扱うクラス Fraction が備わっている。
#分数クラス
from fractions import Fraction
a = 6 - Fraction(56, 10)
b = Fraction(4, 10)
print(a - b)
先ほどの循環小数の計算を分数で行う "dt02-04-03.py" では、正しい答えが求まる。

PHPによる分数クラスの作成

PHPには分数を扱う機能が備わっていない。そこで、分数の四則計算を行うことができるユーザー・クラス pahooFrac を作成し、"pahooDataStructure.php" を  require_once  することで再利用できるようにした。次のメソッドを備えている。
メソッド機能
pahooFrac($n, $d)インスタンス生成。$nが分子、$mが分母。$n, $dは負数でも小数でもよい。
dec()分数を小数に変換する。
isFiniteDecimal()分数が有限小数かどうかが判定する。
printFrac($dec)分数表記を返す。$decがTRUEなら。有限小数の時は小数表記に変換する。
add($x, $reduce)分数クラス $x を加算する。$reduceがFALSEなら約分しない。
sub($x, $reduce)分数クラス $x を減算する。$reduceがFALSEなら約分しない。
mul($x, $reduce)分数クラス $x を乗算する。$reduceがFALSEなら約分しない。
div($x, $reduce)分数クラス $x を除算する。$reduceがFALSEなら約分しない。

 

  17: class pahooFrac {
  18:     var $error, $errmsg;            //エラーフラグ,エラーメッセージ
  19:     var $numerator;             //分子
  20:     var $denominator;               //分母
  21: 
  22: /**
  23:  * コンストラクタ
  24:  * @param   double $numerator   分子(小数可;初期値=0)
  25:  * @param   double $denominator 分母(小数可;初期値=1)
  26:  * @return  なし
  27: */
  28: function __construct($numerator=0, $denominator=1) {
  29:     $this->error  = FALSE;
  30:     $this->errmsg = '';
  31:     $this->numerator   = $numerator;
  32:     $this->denominator = $denominator;
  33:     $this->dec2frac();
  34: }
  35: 
  36: /**
  37:  * デストラクタ
  38:  * @param   なし
  39:  * @return  なし
  40: */
  41: function __destruct() {
  42: }
  43: 
  44: /**
  45:  * 符号は分子に寄せる
  46:  * @param   なし
  47:  * @return  なし
  48: */
  49: function __sign() {
  50:     if ($this->denominator < 0) {
  51:         $this->numerator *= (-1);
  52:         $this->denominator = abs($this->denominator);
  53:     }
  54: }
  55: 
  56: /**
  57:  * 小数を整数部と小数部に分解する
  58:  * @param   double $a 小数
  59:  * @return  array(整数部, 小数部)
  60: */
  61: function split_dec($a) {
  62:     $b = $a > 0 ? floor($a: ceil($a);
  63:     $c = $a - $b;
  64:     return array($b, $c);
  65: }
  66: 
  67: /**
  68:  * 分子・分母を整数に正規化する
  69:  * @param   なし
  70:  * @return  なし
  71: */
  72: function dec2frac() {
  73:     if (is_float($this->numerator)) {
  74:         list($a, $b) = $this->split_dec($this->numerator);
  75:         $n = 0;
  76:         while (TRUE) {
  77:             list($c, $d) = $this->split_dec($b);
  78:             if ($d == 0)    break;
  79:             $b *10;
  80:             $n++;
  81:         }
  82:         $this->numerator = $a * pow(10, $n+ $b;
  83:         $this->denominator *pow(10, $n);
  84:     }
  85:     if (is_float($this->denominator)) {
  86:         list($a, $b) = $this->split_dec($this->denominator);
  87:         $n = 0;
  88:         while (TRUE) {
  89:             list($c, $d) = $this->split_dec($b);
  90:             if ($d == 0)    break;
  91:             $b *10;
  92:             $n++;
  93:         }
  94:         $this->denominator = $a * pow(10, $n+ $b;
  95:         $this->numerator *pow(10, $n);
  96:     }
  97:     $this->reduce();
  98: }
  99: 
 100: /**
 101:  * 分数を小数に変換する
 102:  * @param   なし
 103:  * @return  double 小数
 104: */
 105: function dec() {
 106:     if ($this->denominator == 0) {
 107:         $this->error = TRUE;
 108:         $this->errmsg = 'pahooFrac: illegal denominator';
 109:         $res = NULL;
 110:     } else {
 111:         $res = $this->numerator / $this->denominator;
 112:     }
 113:     return $res;
 114: }
 115: 
 116: /**
 117:  * 最大公約数を求める(ユークリッドの互除法)
 118:  * @param   int $m, $n 整数
 119:  * @return  int 最大公約数
 120: */
 121: function gcdEuclidean($m, $n) {
 122:     if ($n == 0)    return $m;
 123:     return $this->gcdEuclidean($n, (int)$m % $n);
 124: }
 125: 
 126: /**
 127:  * 最小公倍数を求める(ユークリッドの互除法)
 128:  * @param   int $m, $n 整数
 129:  * @return  int 最小公倍数
 130: */
 131: function lcmEuclidean($m, $n) {
 132:     if ($n == 0)    return $m;
 133:     return $m * $n / $this->gcdEuclidean($m, $n);
 134: }
 135: 
 136: /**
 137:  * 素数を求める(エラトステネスのふるい)
 138:  * @param   int   $max     最大値
 139:  * @param   array $primes  素数を格納する配列
 140:  * @return  array $primes
 141: */
 142: function EratosthenesSieve($max, &$primes) {
 143:     for ($i = 2$i <$max$i++) {
 144:         $flag = TRUE;
 145:         for ($k = 2$k < $i$k++) {
 146:             if ($i % $k === 0) {
 147:                 $flag = FALSE;
 148:                 break;
 149:             }
 150:         }
 151:         if ($flag)  $primes[$i] = 0;
 152:     }
 153:     return $primes;
 154: }
 155: 
 156: /**
 157:  * 素因数分解
 158:  * @param   array $primes  素因数を格納する配列
 159:  * @param   int   $n       素因数分解する整数
 160:  * @return  array $primes
 161: */
 162: function primeDecomposition($n, &$primes) {
 163:     $max = floor(sqrt($n));
 164:     $this->EratosthenesSieve($max, $primes);
 165: 
 166:     //2から平方根までの素因数を求める
 167:     for ($i = 2$i <$max$i++) {
 168:         if (isset($primes[$i])) {
 169:             do {
 170:                 $b = $n % $i;
 171:                 if ($b == 0) {
 172:                     $primes[$i]++;
 173:                     $n = floor($n / $i);
 174:                 }
 175:             } while ($b == 0);
 176:         }
 177:     }
 178:     //残った素数
 179:     if ($n > $max)  $primes[$n] = 1;
 180: 
 181:     return $primes;
 182: }
 183: 
 184: /**
 185:  * 有限小数かどうか
 186:  * @param   なし
 187:  * @return  bool TRUE/FALSE
 188: */
 189: function isFiniteDecimal() {
 190:     $gcd = $this->gcdEuclidean($this->denominator, $this->numerator);
 191:     $max = $this->denominator / $gcd;
 192: 
 193:     //分母に2, 5以外の素因数が含まれているかどうか
 194:     $primes = array();
 195:     $this->primeDecomposition($max, $primes);
 196:     $res = TRUE;
 197:     foreach ($primes as $key=>$val) {
 198:         if ($key !2 && $key !5 && $val > 0) {
 199:             $res = FALSE;
 200:             break;
 201:         }
 202:     }
 203:     return $res;
 204: }
 205: 
 206: /**
 207:  * 約分する
 208:  * @param   なし
 209:  * @return  なし
 210: */
 211: function reduce() {
 212:     $m = $this->gcdEuclidean($this->numerator, $this->denominator);
 213:     $this->numerator /= $m;
 214:     $this->denominator /= $m;
 215:     $this->__sign();
 216: }
 217: 
 218: /**
 219:  * 分数表記を返す
 220:  * @param   bool $dec = TRUE:有限小数なら小数表記
 221:  * @return  string 
 222: */
 223: function printFrac($dec=FALSE) {
 224:     return ($dec && $this->isFiniteDecimal()) ? sprintf("%g", $this->dec()) :
 225:                 sprintf("%d/%d", $this->numerator, $this->denominator);
 226: }
 227: 
 228: /**
 229:  * 分数を加算する
 230:  * @param   pahooFrac $x 加える分数
 231:  * @param   bool TRUE:約分する/FALSE:約分しない
 232:  * @return  なし
 233: */
 234: function add($x, $reduce=TRUE) {
 235:     $denominator = $this->lcmEuclidean($this->denominator, $x->denominator);
 236:     $this->numerator = $this->numerator * ($denominator / $this->denominator)
 237:                         + $x->numerator * ($denominator / $x->denominator);
 238:     $this->denominator = $denominator;
 239:     $this->__sign();
 240:     if ($reduce)    $this->reduce();
 241: }
 242: 
 243: /**
 244:  * 分数を減算する
 245:  * @param   pahooFrac $x 減ずる分数
 246:  * @param   bool TRUE:約分する/FALSE:約分しない
 247:  * @return  なし
 248: */
 249: function sub($x, $reduce=TRUE) {
 250:     $denominator = $this->lcmEuclidean($this->denominator, $x->denominator);
 251:     $this->numerator = $this->numerator * ($denominator / $this->denominator)
 252:                         - $x->numerator * ($denominator / $x->denominator);
 253:     $this->denominator = $denominator;
 254:     $this->__sign();
 255:     if ($reduce)    $this->reduce();
 256: }
 257: 
 258: /**
 259:  * 分数を乗算する
 260:  * @param   pahooFrac $x 乗ずる分数
 261:  * @param   bool TRUE:約分する/FALSE:約分しない
 262:  * @return  なし
 263: */
 264: function mul($x, $reduce=TRUE) {
 265:     $this->numerator   *$x->numerator;
 266:     $this->denominator *$x->denominator;
 267:     $this->__sign();
 268:     if ($reduce)    $this->reduce();
 269: }
 270: 
 271: /**
 272:  * 分数を除算する
 273:  * @param   pahooFrac $x 除する分数
 274:  * @param   bool TRUE:約分する/FALSE:約分しない
 275:  * @return  なし
 276: */
 277: function div($x, $reduce=TRUE) {
 278:     $this->numerator   *$x->denominator;
 279:     $this->denominator *$x->numerator;
 280:     $this->__sign();
 281:     if ($reduce)    $this->reduce();
 282: }
 283: 
 284: // End of Class

通分に用いる分母同士の最小公倍数、約分に用いる分子・分母の最小公約数、また、メソッド isFiniteDecimal は、「PHPで分数が有限小数かどうか判定する」で紹介したプログラムを流用した。

  11: //データ構造クラス
  12: require_once('pahooDataStructure.php');
  13: 
  14: // メイン・プログラム ======================================================
  15: printf("\n");
  16: 
  17: $b = new pahooFrac(1, 3);       //分数
  18: 
  19: $a = new pahooFrac(3, 5);       //分数
  20: printf("%s", $a->printFrac(FALSE));
  21: $a->add($b);                    //加算
  22: printf(" + %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
  23: 
  24: $a = new pahooFrac(0.6, 1); //分数;分子が小数でもOK
  25: printf("%s", $a->printFrac(FALSE));
  26: $a->sub($b);                    //減算
  27: printf(" - %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
  28: 
  29: $a = new pahooFrac(1.5, 2.5);   //分数;分母が小数でもOK
  30: printf("%s", $a->printFrac(FALSE));
  31: $a->mul($b);                    //乗算
  32: printf(" × %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
  33: 
  34: $a = new pahooFrac(3, 5);       //分数
  35: printf("%s", $a->printFrac(FALSE));
  36: $a->div($b);                    //除算
  37: printf(" ÷ %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
  38: 
  39: /*

PHPによる分数計算
pahooFrac クラスを用いて分数の四則計算を行うサンプル・プログラムが "dt02-04-12.php" である。
実行結果は左図の通り。3分の1に関わる計算も循環小数とならない。

無理数

平方根や対数など、各処理系には無理数を扱う数学関数が用意されている。
ここでも誤差が問題になる。

   2: //無理数
   3: $a = sqrt(3);
   4: $b = $a * $a;
   5: echo  $b;

"dt02-04-11.php" をWindows用PHPバージョン7.2.6(64ビット)で実行したところ、誤差は発生しなかった。
#無理数
import math
a = math.sqrt(3)
b = a * a
print(b)
"dt02-04-12.py" をWindows用Pythonバージョン3.6.5(64ビット)で実行したところ、誤差が発生した。
//無理数
int main() {
	float a, b;
	a = sqrt(3);
	b = a * a;
	cout << setprecision(8) << setiosflags(ios::fixed) << b << endl;
	return(0);
}
"dt02-04-22.py" をC++(g++)で実行したところ、誤差は発生しなかった。

コラム:虚数

複素平面
複素平面
虚数と聞くと、数学のテストで出てくるもので実社会では役に立たない、SFやオカルトのネタ――と思っている方がいるかもしれない。だが、もし電気・電子回路や無線、音響、映像など、波形を扱う仕事では、十中八九、虚数のお世話になる。
上図は、波形(正弦波)をあらわしたものだが、横軸は実軸、縦軸が虚軸となっている。これ複素平面と呼ぶ。
なぜXY平面で描かないかというと、波形処理では、波形上にある点を微分で求めることが多い。このとき、XY平面では2つの式を処理するか、三角関数を使わなければならないが、複素平面だと \( a + bi \) という1つの式で表すことができ、計算量が減る。微分方程式は計算量が多いので、元の方程式の計算量が減ることで、プログラムを簡素化できたり、ハードウェア回路を小型化、高速化することができる。さらに、複素フーリエ級数からフーリエ変換を導出できる。

フーリエ変換は、少ない計算量で様々な波形処理ができることから、ノイズフィルタ、イコライザはもちろん、AIに学習させるデータを前処理する工程や、統計・検定でも活躍する。
フーリエ変換は理系のためのツールではない。景気変動やデリバティブにも適用されている。文系の方、とくに経済・金融関係の仕事をされている方は、虚数の存在を思い出してほしい。
詳しくは『今日から使えるフーリエ変換 普及版』(三谷政昭,2019年4月)をご覧いただきたい。
2次方程式の虚数解
2次方程式の虚数解
さて、2次方程式で虚数解となる場合、左図のように2次方程式のグラフがX軸と交わらないと習う。
だが、詳しくは専門書をご覧いただきたいのだが、このグラフに虚軸を加えると、グラフは曲面となり、X軸を含む平面と交わる部分が出てくる。
中学・高校の数学は、社会で必要になることの基礎(入口)を学んでいるに過ぎない。社会人になったら、自分が必要としている数学の分野をさらに深く学ぼう。
(この項おわり)
header