多精度算術學習

參考圖書:
Knuth <<計算機程序設計藝術>> 4.3 節: 多精度算術.
BigNum Math:加密多精度算法的理論與實現:
http://www.box-z.com/books/360buy-10454965.shtml html


  多精度算術的目的是利用固定精度數據類型建立和操縱可以表示大數的多精度
整數. 當我學習 Kawa(一種 Lisp 方言 Scheme 在 Java 平臺上的實現) 源代碼
的時候, 裏面爲支持 Lisp 的各類數字有關於多精度數字的部分, 若是僅僅會用
而不去探索它的原理, 顯然不是咱們研究學習應用的態度. 所以, 爲了有效地
學明白多精度算術的原理, 有必要構造一個簡化的多精度算術的程序. java

  多精度的數字是用多個較小的字來表示, 例如咱們能夠用兩個 short16
來表示一個 int32, 用八個 byte8 來表示一個 long64. 咱們在試驗程序中用
byte8 作爲基本字, 緣由是 byte8 範圍比較小, 所以容易測試
和使用. 將其推廣到 short, int 在本質上是不會有區別的. 只要計算機支持
在該類型上執行必要的運算便可. Kawa 是使用 int32 來表示, 而且有優化的
處理部分, 但與原理無關的部分, 咱們如今不去研究. 算法


  在測試的 java 程序中, 咱們使用類 mp_int 來表示一個多精度整數,
mp 表示 multiple precise arithmatic, 其內部數據爲:
class mp_int {
  int bnum;      // 說明使用了 arr[] 數組的 bnum 個元素來表示一個數字.
  byte[] arr;    // 使用此數組的 arr[0:bnum-1] 表示一個數字.
                 // 數組自己可能長度比 bnum 長.
}
這裏至少 bnum >= 1. 類型 byte 在 Java 中是有符號的字節. 數組

將整個數字看作是 arr[bnum-1], arr[bnum-2], ... arr[0] 這些字節組合
成一個大的數字, 最高位爲 arr[bnum-1]. 按照算法書上的術語, 這個數
的基beta = 256. 採用2進制補碼的方式存放整個數字, 這樣, 最高位數字
最高位字 arr[bnum-1], 若是 > 0 則表示整數, < 0 則表示負數. 也即
最高比特位是一個符號位. 這種表示方法與現代計算機結構中表示正負數
的方法一致. 關於二進制補碼, 參見: ... 性能優化


下面分別研究在這種表示結構之上的基本運算:
(爲表示一個大整數, 還有別的描述結構, 這裏使用二進制補碼方式. 其它
表示方式及其運算就不研究了.) 函數

==
構造該數字, 構造函數 mp_int(int i):
設給出一個整數 int32 i (32 bit 的int型整數, int 後數字表示所用bit數),
其能夠表示爲 4個 byte8, 所以構造方法爲:
  bnum = 4; // 使用 4 個 byte8 表示該 int32 整數.
  arr = new byte[4];  // 分配 4 個 byte8 空間.
  arr[0] = (byte)(i & 0xff);   // arr[0] 是最低的 8 個比特
  arr[1] = (byte)((i >> 8) & 0xff);  // arr[1] 是次低位的 8 個比特.
  arr[2] = (byte)((i >> 16) & 0xff); // 類推
  arr[3] = (byte)((i >> 24) & 0xff); // int32 的最高 8 個比特. 性能

設給出一個 long64 數字 l, 則該過程相似, 須要 8 個 byte8 來存放. 學習


==
加法: public static mp_int add(mp_int x, mp_int y) 求 x + y. 測試

加法是基本運算, 爲清楚基於二進制補碼錶示法的加法, 有必要深刻分析.
  設一個有 n 個(二進制) bit 表示的數字, 爲方便, 下面以 N 來表示 2^n (2 的n次方),
則當表示一個無符號整數的時候, 能表示的數字範圍爲 0~N-1.
  例如使用 n=4 個 bit, 則 N=2^4=16, 表示的無符號數字範圍是 0b0000~0b1111,
即 0~15.
  當使用二進制補碼錶示有符號整數的時候, 最高位爲 0 表示正數, 可表示的
正數範圍爲 0~N/2-1; 最高位爲 1 表示負數, 可表示的負數範圍爲 -N/2 ~ -1.
其中負數 -x (其中 x > 0) 實際以二進制 N-x 來表示.
  例如 n=4 bit 時, -3 表示爲 0b1101 = 13 = N-3. 優化

下面先研究符號擴展. 符號擴展指將符號位複製到擴展出來的更高位. 當符號位
爲 0 (表示正數), 則擴展的新位也是 0; 當符號位爲 1(表示負數), 則擴展出
的新位也是 1.
設 n=4 擴展到 n=5 的狀況:
若是是正數, 則高位=0, 擴展後的最高位爲 0, 依然是一個正數, 且新的正數 =
原正數. (後面的低位沒有發生變化).
若是是負數, 則高位=1, 擴展後的最高位爲 1, 原數字爲按照無符號數看待時
爲 N-x (其中x是該負數的相反數). 添加最高位1以後, 至關於無符號數加上
了 N(最高位=2^n = N), 也即新的無符號數 = N-x+N = 2N-x. 當作(n+1) 位
的有符號數看待時, 剛好表示負數 -x. 所以擴展後, 仍表示相同的負數.

根據上面的論述, 擴展1位以後, 表示的有符號數值不變, 則根據數學概括法,
擴展任意位數此結論都成立.

下面再論證, 當對兩個有符號數x,y 執行加法的時候, 符號擴展1位以後將擴展
以後的數字當作無符號數相加, 結果當作有符號數看待, 則結果爲 x+y.
這句話本質意思是, 對兩個有符號數執行加法, 可使用無符號數的運算而獲得
正確的結果, 固然須要擴展1位.

設用 n 個 bit 表示有符號數, 有兩個正數 x, y (其範圍在下面各狀況下詳細說明),
對下面各類狀況分別探討(N = 2^n, 2N = 2*N = 2^(n+1) ):
1. 兩個正數加法 x+y: 其中 x,y 的範圍爲 0~N/2-1.
  對x,y 進行符號擴展, 由於都是正數, 高位擴展後都是 0.
  擴展以後的位數 = n+1, 可表示的正數範圍爲 0~N-1 (如n=4, 擴展爲n=5,
    用做有符號數時, 可表示的正數範圍爲 0~15)

  擴展後當作無符號數執行加法, 其結果範圍爲 0~N-2, 在可表示的正數範圍
  0~N-1 以內, 所以是一個正數, 表示的正數正好爲 x+y.
2. 一個正數加一個負數 x + (-y), 其中 x 範圍爲 0~N/2-1, y 範圍爲 1~N/2
  對x,y 進行符號擴展, x擴展的高位爲 0, -y 擴展的高位爲1.
 
  擴展以後表示的有符號數的值不變, 可是當作無符號數看待時, x不變, y變爲
  2N-y, 此時執行無符號數加法, 其結果爲 2N+x-y.
 
  下面再細分當 x>=y, 和 x<y 兩種狀況分析:
  當 x>=y, 結果 2N+x-y >= 0 表示一個正數(有符號看待時), 因爲只有n+1 個
  bit, 所以實際結果爲 x-y (mod 2N), 2N 被溢出而拋棄了. 所以結果爲 x-y,
  正是 x+(-y) 是正確的結果.
 
  當 x<y, 結果 2N-(y-x) 的最高位爲 1, 表示一個負數(有符號看待時), 沒有
  高位的進位被拋棄, 這個數當作負數看待時, 實際表示負數 -(y-x) = x-y
  也正是 x+(-y) 的結果.
 
3. 兩個負數相加 (-x)+(-y). 其中 x,y 的範圍都是 1~N/2.
  對x,y 進行符號擴展, 高位都是1.
 
  擴展以後, 當作無符號數看待時變成 2N-x, 2N-y, 對它們執行無符號數加法,
  結果爲 4N-(x+y), 對 2N 取模獲得 2N-(x+y). 也即高位的進位被拋棄了.
 
  這個結果當作有符號數看待時, 即表示負數 -(x+y), 也即兩個負數相加的
  結果.
 
綜上所述, 當擴展1位以後, 將數當作無符號數執行加法, 結果當作有符號數
看待, 其便是所需結果. 可本身實際以 n=4 作一些筆算來試驗的.

一樣根據數學概括法, 擴展1位是正確的, 則擴展任意n位也是正確的.


根據上面結論, 兩個 mp_int 數 x,y 的加法的 java 代碼可描述以下:
  int n = Math.max(x.bnum, y.bnum); // 獲得最長的位數.
  x = signed_extend(x, n+1); // 對x,y 執行符號擴展. 使x,y 具備n+1個 byte 長.
  y = signed_extend(y, n+1); // 這裏咱們只考慮算法實現, 不考慮性能優化等問題.
     // 實際實現中不用作符號擴展的... 具體見 Kawa IntNum, MPN 類的實現.

  byte[] result = new byte[n+1]; // 和的位數, 加上擴展出來的一個字長度.
  int carry = 0; // 記住每一個步驟的進位.
  for (int j = 0; j < n+1; ++j) {
    carry += ((int)x.arr[j] & 0xFF)
           + ((int)y.arr[j] & 0xFF);   // 當作無符號數相加, 並累加前一次的進位.
      result[j] = (byte)carry;
      carry >>>= 8;  // 進位部分, 要麼是0, 要麼是1.
  }

  z = new mp_int(result, n+1); // 使用 n+1 個字構造一個新的 mp_int 便是結果.

最後對結果 z 須要進行一次規範化 canonicalize() 處理.

若是用 short 存放則上面的部分代碼可改寫爲:
  short[] result = new short[n+1]; // 和的位數, 加上擴展出來的一個字長度.
  int carry = 0; // 記住每一個步驟的進位.
  for (int j = 0; j < n+1; ++j) {
    carry += ((int)x.arr[j] & 0xFFFF)
           + ((int)y.arr[j] & 0xFFFF);   // 當作無符號數相加, 並累加前一次的進位.
      result[j] = (short)carry;
      carry >>>= 16;  // 進位部分, 要麼是0, 要麼是1.
  }

若是用 int 存放, 則上面的代碼可改寫爲:
  int[] result = new int[n+1]; // 和的位數, 加上擴展出來的一個字長度.
  long carry = 0; // 記住每一個步驟的進位.
  for (int j = 0; j < n+1; ++j) {
    carry += ((long)x.arr[j] & 0xFFFFFFFFL)
           + ((long)y.arr[j] & 0xFFFFFFFFL);   // 當作無符號數相加, 並累加前一次的進位.
      result[j] = (int)carry;
      carry >>>= 32;  // 進位部分, 要麼是0, 要麼是1.
  }

算法本質上是沒有多少變化, 只是移動的位數, 屏蔽碼發生些變化.

 

==
已知一個 bytes[], 以及使用的字節數 len, 求最少須要多少個字節能表示此數字.
方法原型: public static int bytesNeeded(byte[] bytes, int len).
此方法返回一個數字 x, 知足 x > 0, bytes[0:x-1] 表示的整數 == bytes[0:len-1]

這個方法用於規範化表示一個數字, 使用盡量少的 byte8 的位數來表示一個數字.
先看例子, 數 0x001234 若是用 4 個字節表示爲 [34,12,00,00] (注意這裏使用十六進制),
實際上這個數字能夠只用 2 個字節就足以表示: [34,12].
一樣, 數字 0xFFFF1234 (-60876) 用4個字節表示爲 [34,12,ff,ff] 實際上能夠用 3
個字節表示爲 [34,12,ff].

具體算法邏輯爲
  若是是正數, 則高位若是是 0, 且次高位爲正, 則高位的 0 能夠去掉.
  若是是負數, 則高位若是是 -1(0xff), 且次高位爲負, 則高位的 -1 能夠去掉.
  重複上面的檢查, 直到沒有高位能夠去掉時.

創建在此基礎上的方法 canonicalize() 使用最小的有效字節數量來表示一個數字.

相關文章
相關標籤/搜索