リストを使ってシンプルな無符号多倍長整数 (bignum) を実装します。基数を 65536 (#x10000) とし、リストの先頭が下位の桁、末尾を上位の桁とします。簡単な例を示します。
0 => (0) 1 => (1) 65535 => (65535) 65536 => (0 1) 4294967295 => (65535 65535) 4294967296 => (0 0 1)
整数を多倍長整数に変換する関数 integer->bignum n を定義してください。
gosh> (integer->bignum 0) (0) gosh> (integer->bignum 1) (1) gosh> (integer->bignum 65535) (65535) gosh> (integer->bignum 65536) (0 1) gosh> (integer->bignum #x100000000) (0 0 1)
多倍長整数を整数に変換する関数 bignum->integer xs を定義してください。
gosh> (bignum->integer '(0)) 0 gosh> (bignum->integer '(1)) 1 gosh> (bignum->integer '(0 1)) 65536 gosh> (bignum->integer '(65535 1)) 131071 gosh> (bignum->integer '(65535 65535)) 4294967295 gosh> (bignum->integer '(0 0 1)) 4294967296
2 つの多倍長整数 xs, ys の論理積を求める関数 bignum-and xs ysを定義してください。
gosh> (bignum-and '(#xffff #xffff #xffff) '(#xf0f0 #x0f0f #xffff)) (61680 3855 65535) gosh> (bignum-and '(#xffff #xffff #xffff) '(#xf0f0 #x0f0f)) (61680 3855) gosh> (bignum-and '(#xffff #xffff #xffff) '(#xf0f0)) (61680) gosh> (bignum-and '(#xffff #xffff #xffff) '(0)) (0)
2 つの多倍長整数 xs, ys の論理和を求める関数 bignum-or xs ys を定義してください。
gosh> (bignum-or '(#xff00 #xf0f0 #x00ff) '(#x00ff #x0f0f #xff00)) (65535 65535 65535) gosh> (bignum-or '(#xff00 #xf0f0 #x00ff) '(#x00ff #x0f0f)) (65535 65535 255) gosh> (bignum-or '(#xff00 #xf0f0 #x00ff) '(#x00ff)) (65535 61680 255) gosh> (bignum-or '(#xff00 #xf0f0 #x00ff) '(0)) (65280 61680 255)
2 つの多倍長整数 xs, ys の排他的論理和を求める関数 bignum-xor xs ys を定義してください。
gosh> (bignum-xor '(#xff00 #xf0f0 #x00ff) '(#xffff #xffff #xffff)) (255 3855 65280) gosh> (bignum-xor '(#xff00 #xf0f0 #x00ff) '(0)) (65280 61680 255) gosh> (bignum-xor '(#xff00 #xf0f0 #x00ff) '(#x00ff #x0f0f #xff00)) (65535 65535 65535) gosh> (bignum-xor '(#xff00 #xf0f0 #x00ff) '(#xff00 #xf0f0 #x00ff)) (0)
多倍長整数 xs を左へ n bit シフトする関数 bignum-shift-left xs n を定義してください。
gosh> (bignum-shift-left '(#xf0f0) 8) (61440 240) gosh> (bignum-shift-left '(#xf0f0) 16) (0 61680) gosh> (bignum-shift-left '(#xf0f0) 24) (0 61440 240) gosh> (bignum-shift-left '(#xf0f0) 32) (0 0 61680)
多倍長整数 xs を右へ n bit シフトする関数 bignum-shift-right xs n を定義してください。
gosh> (bignum-shift-right '(0 #xffff) 8) (65280 255) gosh> (bignum-shift-right '(0 #xffff) 16) (65535) gosh> (bignum-shift-right '(0 #xffff) 24) (255) gosh> (bignum-shift-right '(0 #xffff) 32) (0)
下表に示す多倍長整数を比較する述語を定義してください。
| 関数名 | 機能 |
|---|---|
| bignum=? xs ys | xs と ys が等しいとき #t を返す |
| bignum>? xs ys | xs が ys より大きいとき #t を返す |
| bignum<? xs ys | xs が ys より小さいとき #t を返す |
| bignum>=? xs ys | xs が ys 以上のとき #t を返す |
| bignum<=? xs ys | xs が ys 以下のとき #t を返す |
| bignum-zero? xs | xs が 0 のとき #t を返す |
gosh> (bignum=? '(1 1 1) '(1 1 1)) #t gosh> (bignum=? '(1 1 1) '(1 1)) #f gosh> (bignum<? '(1 0 1) '(1 1 1)) #t gosh> (bignum<? '(1 0 1) '(1 1)) #f gosh> (bignum>? '(2 1 1) '(1 1 1)) #t gosh> (bignum>? '(1 1) '(1 1 1)) #f gosh> (bignum<=? '(1 1 1) '(1 1 1)) #t gosh> (bignum<=? '(2 1 1) '(1 1 1)) #f gosh> (bignum>=? '(1 1 1) '(1 1 1)) #t gosh> (bignum>=? '(1 0 1) '(1 1 1)) #f gosh> (bignum-zero? '(0)) #t gosh> (bignum-zero? '(0 1)) #f
多倍長整数 xs と基数未満の整数 n を加算する関数 bignum-add-int xs n を定義してください。
gosh> (bignum-add-int '(#xffff #xffff) 0) (65535 65535) gosh> (bignum-add-int '(#xffff #xffff) 1) (0 0 1) gosh> (bignum-add-int '(#xffff #xffff) #xffff) (65534 0 1)
多倍長整数 xs. ys を加算する関数 bignum-add xs ys を定義してください。
gosh> (bignum-add '(#xffff #xffff) '(0 0 1)) (65535 65535 1) gosh> (bignum-add '(#xffff #xffff) '(1 0 1)) (0 0 2) gosh> (bignum-add '(#xffff #xffff) '(0 1 1)) (65535 0 2)
多倍長整数 xs と基数未満の整数 n を減算する関数 bignum-sub-int xs n を定義してください。
gosh> (bignum-sub-int '(1) 1) (0) gosh> (bignum-sub-int '(0 1) 1) (65535) gosh> (bignum-sub-int '(0 0 1) 1) (65535 65535) gosh> (bignum-sub-int '(0 0 1) 65535) (1 65535) gosh> (bignum-sub-int '(0) 1) *** ERROR: oops!, underflow
多倍長整数 xs. ys を減算する関数 bignum-sub xs ys を定義してください。
gosh> (bignum-sub '(0 0 0 1) '(0 0 0 1)) (0) gosh> (bignum-sub '(0 0 0 1) '(0 0 1)) (0 0 65535) gosh> (bignum-sub '(0 0 0 1) '(#xffff #xffff #xffff)) (1) gosh> (bignum-sub '(0 0 1) '(#xffff #xffff #xffff)) *** ERROR: oops!, underflow
多倍長整数 xs と基数未満の整数 n を乗算する関数 bignum-mul-int xs n を定義してください。
gosh> (bignum-mul-int '(1 2 3 4 5) 2) (2 4 6 8 10) gosh> (bignum-mul-int '(1 2 3 4 5) 1) (1 2 3 4 5) gosh> (bignum-mul-int '(1 2 3 4 5) 0) (0) gosh> (bignum-mul-int '(#xffff #xffff #xffff) 2) (65534 65535 65535 1) gosh> (bignum-mul-int '(#xffff #xffff #xffff) #xffff) (1 65535 65535 65534)
多倍長整数 xs. ys を乗算する関数 bignum-mul xs ys を定義してください。
gosh> (bignum-mul '(1 1 1) '(1 1 1)) (1 2 3 2 1) gosh> (bignum-mul '(#xffff #xffff #xffff) '(1 1 1)) (65535 65534 65534 0 1 1) gosh> (bignum-mul '(#xffff #xffff #xffff) '(1 0 1)) (65535 65535 65534 0 0 1) gosh> (bignum-mul '(#xffff #xffff #xffff) '(0)) (0)
多倍長整数 xs と基数未満の整数 n を除算する関数 bignum-div-int xs n を定義してください。返り値は商と剰余を多値で返すものとします。
gosh> (bignum-div-int '(2 4 6 8) 2) (1 2 3 4) (0) gosh> (bignum-div-int '(1 2 3 4) 2) (0 32769 1 2) (1) gosh> (bignum-div-int '(1 2 3 4) 0) *** ERROR: oops!, division by zero
多倍長整数 xs. ys を除算する関数 bignum-div xs ys を定義してください。返り値は商と剰余を多値で返すものとします。
gosh> (bignum-div '(0 0 0 1) '(0 0 0 1)) (1) (0) gosh> (bignum-div '(0 0 0 1) '(0 0 1)) (0 1) (0) gosh> (bignum-div '(0 0 0 1) '(0 1)) (0 0 1) (0) gosh> (bignum-div '(0 0 0 1) '(#xffff #xffff)) (0 1) (0 1) gosh> (bignum-div '(0 0 0 1) '(0)) *** ERROR: oops!, division by zero
多倍長整数 xs を文字列に変換する関数 bignum->string xs r を定義してください。r は基数を表す整数値 (r <= 16) です。
gosh> (bignum->string '(722 18838) 10) "1234567890" gosh> (bignum->string '(65535 65535) 16) "FFFFFFFF" gosh> (bignum->string '(0 0 0 1) 16) "1000000000000" gosh> (bignum->string '(0 0 0 1) 10) "281474976710656"
文字列 str を多倍長整数に変換する関数 string->bignum str r を定義してください。r は基数を表す整数値 (r <= 16) です。
gosh> (string->bignum "1234567890" 10) (722 18838) gosh> (string->bignum "ffffffff" 16) (65535 65535) gosh> (string->bignum "ffffffffg" 16) *** ERROR: oops!, illegal char
今まで作成した多倍長整数を使って階乗を求める関数 bignum-fact n を定義してください。ただし、引数 n は基数 (65536) 未満の整数とします。
gosh> (dotimes (x 20) (print (bignum->integer (bignum-fact x)))) 1 1 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600 6227020800 87178291200 1307674368000 20922789888000 355687428096000 6402373705728000 121645100408832000 #t
今まで作成した多倍長整数を使って累乗を求める関数 bignum-power xs n を定義してください。引数 xs は多倍長整数、n は整数とします。
gosh> (bignum->string (bignum-power (integer->bignum 2) 8) 10) "256" gosh> (bignum->string (bignum-power (integer->bignum 2) 32) 10) "4294967296" gosh> (bignum->string (bignum-power (integer->bignum 2) 64) 10) "18446744073709551616" gosh> (bignum->string (bignum-power (integer->bignum 2) 128) 10) "340282366920938463463374607431768211456"
リスト : 整数を多倍長整数に変換する
; SRFI 1 を使用する
(use srfi-1)
; 定数
(define *base* #x10000)
(define *zero* '(0))
; 整数を bignum に変換
(define (integer->bignum n)
(let loop ((n n) (a '()))
(if (zero? n)
(if (null? a) *zero* (reverse! a))
(loop (quotient n *base*) (cons (modulo n *base*) a)))))
; 別解
(define (integer->bignum1 n)
(do ((n n (quotient n *base*))
(a '() (cons (modulo n *base*) a)))
((zero? n)
(if (null? a) *zero* (reverse! a)))))
integer->bignum は簡単です。整数 n を基数 *base* で割り算していき、剰余を累積変数 a に格納します。あとは n が 0 になったら、a を reverse! で反転するだけです。このとき、a が空リストであれば、多倍長整数の 0 を表す *zero* を返します。別解は do ループで書き直したものです。
リスト : 多倍長整数を整数に変換する
(define (bignum->integer xs)
(let loop ((xs xs) (x 1) (a 0))
(if (null? xs)
a
(loop (cdr xs) (* x *base*) (+ (* (car xs) x) a)))))
; 別解
(define (bignum->integer1 xs)
(do ((xs xs (cdr xs))
(x 1 (* x *base*))
(a 0 (+ (* (car xs) x) a)))
((null? xs) a)))
bignum->integer も簡単です。named let で xs から要素を順番に取り出し、位を表す引数 x と掛け算して累積変数 a に加算します。別解は do ループで書き直したものです。
リスト : 多倍長整数の論理積
; 先頭から連続している 0 を取り除く
; 最後尾の 0 は取り除かない
(define (remove-zero xs)
(if (or (null? (cdr xs)) (not (zero? (car xs))))
xs
(remove-zero (cdr xs))))
; 論理積
(define (bignum-and xs ys)
(let loop ((xs xs) (ys ys) (a '()))
(if (or (null? xs) (null? ys))
(reverse! (remove-zero a))
(loop (cdr xs) (cdr ys) (cons (logand (car xs) (car ys)) a)))))
; 別解
(define (bignum-and1 xs ys)
(reverse! (remove-zero (fold (lambda (x y a) (cons (logand x y) a)) '() xs ys))))
bignum-and は xs と ys の要素を順番に取り出して論理積を計算し、その結果を累積変数 a のリストに追加します。xs または ys が空リストになった場合、残りの要素は 0 との論理積になり結果は 0 になるので、ここで計算を打ち切ります。このとき、a をそのまま反転すると、末尾に余分な 0 が連続する場合があります。関数 remove-zero で先頭から連続している余分な 0 を取り除いてから、a を reverse! で反転します。
別解は畳み込みを行う関数 fold を使って書き直したものです。
リスト : 多倍長整数の論理和
; xs を反転して ys と連結
(define (reverse-append xs ys)
(let loop ((xs xs) (ys ys))
(if (null? xs)
ys
(loop (cdr xs) (cons (car xs) ys)))))
; 論理和
(define (bignum-or xs ys)
(let loop ((xs xs) (ys ys) (a '()))
(cond ((null? xs)
(reverse-append a ys))
((null? ys)
(reverse-append a xs))
(else
(loop (cdr xs) (cdr ys) (cons (logior (car xs) (car ys)) a))))))
bignum-or は xs と ys の要素を順番に取り出して論理和を計算し、その結果を累積変数 a のリストに追加します。xs が空リストになった場合、ys の残りの要素は 0 との論理和になり結果は ys と同じになります。関数 reverse-append で a を反転して ys と連結します。(reverse-append a ys) は (append (reverse a) ys) と同じです。ys が空リストになった場合は、a を反転して xs と連結します。
畳み込みによる別解は次のようになります。
リスト : 多倍長整数の論理和 (別解)
; 畳み込み (畳み込みの値と残りのリストを多値で返す)
(define (fold-left2 fn a xs ys)
(if (or (null? xs) (null? ys))
(values a xs ys)
(fold-left2 fn (fn (car xs) (car ys) a) (cdr xs) (cdr ys))))
(define (bignum-or1 xs ys)
(receive (zs rxs rys)
(fold-left2 (lambda (x y a) (cons (logior x y) a)) '() xs ys)
(reverse-append zs (if (pair? rxs) rxs rys))))
fold-left2 は 2 つのリスト xs と ys を受け取って畳み込みを行い、その結果と残ったリストを多値で返します。別解 bignum-or1 は fold-left2 の返り値を receive で受け取り、それを reverse-append で連結するだけです。
リスト : 多倍長整数の排他的論理和
(define (bignum-xor xs ys)
(let loop ((xs xs) (ys ys) (a '()))
(cond ((and (null? xs) (null? ys))
(reverse! (remove-zero a)))
((null? xs)
(reverse-append a ys))
((null? ys)
(reverse-append a xs))
(else
(loop (cdr xs) (cdr ys) (cons (logxor (car xs) (car ys)) a))))))
; 別解
(define (bignum-xor1 xs ys)
(receive (zs rxs rys)
(fold-left2 (lambda (x y a) (cons (logxor x y) a)) '() xs ys)
(if (and (null? rxs) (null? rys))
(reverse! (remove-zero zs))
(reverse-append zs (if (pair? rxs) rxs rys)))))
bignum-or は xs と ys の要素を順番に取り出して排他的論理和を計算し、その結果を累積変数 a のリストに追加します。xs と ys が空リストになった場合、remove-zero で連続する 0 を取り除いてから、reverse! で a を反転します。xs だけが空リストになった場合、ys の残りの要素は 0 との排他的論理和になり、結果は ys と同じになります。関数 reverse-append で a を反転して ys と連結します。ys だけが空リストになった場合は、a を反転して xs と連結します。
別解は fold-left2 を使って書き直したものです。
リスト : 多倍長整数の左シフト
; 定数
(define *mask* #xffff)
(define *base-bit* 16)
; b (b < 16) ビット左シフトする
(define (bignum-shift-left-bit xs b)
(let loop ((xs xs) (c 0) (a '()))
(if (null? xs)
(reverse! (if (zero? c) a (cons c a)))
(loop (cdr xs)
(ash (car xs) (- b *base-bit*))
(cons (logand (logior (ash (car xs) b) c) *mask*) a)))))
; n ビット左シフトする
(define (bignum-shift-left xs n)
(cond ((zero? n) xs)
((negative? n)
(error "oops!, out of range"))
((< n *base-bit*)
(bignum-shift-left-bit xs n))
(else
(let ((a (quotient n *base-bit*))
(b (modulo n *base-bit*)))
(append (make-list a 0)
(bignum-shift-left-bit xs b))))))
; 別解
(define (bignum-shift-left-bit1 xs b)
(let ((ys (fold (lambda (x a)
(list* (ash x (- b *base-bit*))
(logand (logior (ash x b) (car a)) *mask*)
(cdr a)))
'(0)
xs)))
(reverse! (if (zero? (car ys)) (cdr ys) ys))))
bignum-shift-left は引数 n が 0 の場合は xs をそのまま返します。負の場合はエラーを送出します。*base-bit* 未満の場合は関数 bignum-shift-left-bit を呼び出します。そうでなければ、n を *base-bit* で除算し、商を a に、剰余を b にセットします。そして、xs を b ビットシフトした結果に a 個の 0 を先頭に追加します。
実際のビットシフトは関数 bignum-shift-left-bit で行います。named let で xs の要素を順番に取り出します。変数 c には左ビットシフトしたときに溢れるビットをセットします。実際には、*base-bit* - b ビット右シフトして、下位 b ビットにセットしておきます。あとは要素 (car x) と c の論理和を求め、それと *mask* の論理積を累積変数 a のリストに格納します。xs が空リストになったら、a を reverse! で反転します。このとき、c が 0 でなければ、a に c を追加します。
別解は bignum-shift-left-bit を fold で書き直したものです。
リスト : 多倍長整数の右シフト
; b (b < 16) ビット右シフトする
(define (bignum-shift-right-bit xs b)
(let loop ((xs xs) (a '()))
(if (null? xs)
(reverse! (remove-zero a))
(loop (cdr xs)
(cons (logior (ash (car xs) (- b))
(if (null? (cdr xs))
0
(logand (ash (cadr xs) (- *base-bit* b)) *mask*)))
a)))))
; n ビット右シフトする
(define (bignum-shift-right xs n)
(cond ((zero? n) xs)
((negative? n)
(error "oops!, out of range"))
((< n *base-bit*)
(bignum-shift-right-bit xs n))
(else
(let ((a (drop xs (quotient n *base-bit*)))
(b (modulo n *base-bit*)))
(if (null? a)
*zero*
(bignum-shift-right-bit a b))))))
bignum-shift-right は引数 n が 0 のときは xs をそのまま返し、負の場合はエラーを送出します。*base-bit* 未満の場合は関数 bignum-shift-right-bit を呼び出します。そうでなければ、n を *base-bit* で除算し、xs の先頭から商の数だけ要素を取り除き、それを a にセットします。剰余は b にセットします。もし a が空リストならば *zero* を返します。そうでなければ、bignum-shift-rigth-bit を呼び出して、a を b ビット右へシフトします。
実際のビットシフトは関数 bignum-shift-right-bit で行います。named let で xs の要素を順番に取り出し、ビットシフトした結果を累積変数 a のリストに格納します。xs が空リストになったら、remove-zero で a の先頭にある 0 を削除してから reverse! で反転します。ビットシフトは簡単です。(car xs) を b ビット右シフトし、次の要素を *base-bit* - b ビット左シフトして *mask* との論理積を求め、その 2 つの値の論理和を求めるだけです。
リスト : 多倍長整数の比較
(define (bignum-compare xs ys)
(let loop ((xs xs) (ys ys) (r 0))
(cond ((null? xs)
(if (null? ys) r -1))
((null? ys) 1)
(else
(loop (cdr xs)
(cdr ys)
(let ((n (- (car xs) (car ys))))
(if (not (zero? n)) n r)))))))
(define (bignum=? xs ys) (equal? xs ys))
(define (bignum<? xs ys) (negative? (bignum-compare xs ys)))
(define (bignum>? xs ys) (positive? (bignum-compare xs ys)))
(define (bignum<=? xs ys) (<= (bignum-compare xs ys) 0))
(define (bignum>=? xs ys) (>= (bignum-compare xs ys) 0))
(define (bignum-zero? xs) (equal? xs *zero*))
; 別解
(define (bignum-compare1 xs ys)
(receive (r rxs rys)
(fold-left2 (lambda (x y a)
(let ((n (- x y)))
(if (not (zero? n)) n a)))
0 xs ys)
(cond ((pair? rxs) 1)
((pair? rys) -1)
(else r))))
bignum=? は xs と ys を equal? で比較し、bignum-zero? は xs と *zero* を equal? で比較するだけです。あとの述語は関数 bignum-compare を呼び出して比較します。bignum-compare は xs と ys が等しい場合は 0 を、xs のほうが大きい場合は正の値を、xs のほうが小さい場合は負の値を返します。
bignum-compare は 2 つのリストを順番にたどっていき、xs が先に空リストになったら -1 を、ys が先に空リストになったら 1 を返します。両方とも空リストになった場合、リストの長さが等しいので要素の値を比較します。リストをたどるとき、(- (car xs) (car ys)) を計算し、その値が 0 でなければ変数 r の値を更新します。もし、r の値が 0 ならば xs と ys は同じ値であることがわかります。負の場合、ys には xs よりも大きい要素が上位の桁にあるので、ys のほうが大きいことがわかります。逆に正の場合は xs が大きいことになります。
別解は bignum-compare を fold-left2 で書き直したものです。
リスト : 多倍長整数と整数の加算
(define (integer-add x y c)
(let ((n (+ x y c)))
(if (< n *base*)
(values n 0)
(values (- n *base*) 1))))
(define (bignum-add-int xs c)
(let loop ((xs xs) (c c) (a '()))
(cond ((null? xs)
(reverse! (remove-zero (cons c a))))
((zero? c)
(reverse-append a xs))
(else
(receive (n m)
(integer-add (car xs) 0 c)
(loop (cdr xs) m (cons n a)))))))
bignum-add-int は最下位の桁と引数 c を加算し、桁上がりがあればそれを上位の桁に加算します。あとは、桁上げの処理を繰り返すだけです。整数同士の加算は関数 integer-add で行います。引数 x, y, c を加算し、その値 n が *base* 未満であれば n と 0 を values で返します。そうでなければ、(- n *base*) と 1 を values で返します。
リスト : 多倍長整数の加算
(define (bignum-add xs ys)
(let loop ((xs xs) (ys ys) (c 0) (a '()))
(cond ((null? xs)
(if (null? ys)
(reverse! (remove-zero (cons c a)))
(reverse-append a (bignum-add-int ys c))))
((null? ys)
(reverse-append a (bignum-add-int xs c)))
(else
(receive (n m)
(integer-add (car xs) (car ys) c)
(loop (cdr xs) (cdr ys) m (cons n a)))))))
; 別解
(define (bignum-add1 xs ys)
(receive (zs rxs rys)
(fold-left2 (lambda (x y a)
(receive (n m)
(integer-add x y (car a))
(list* m n (cdr a))))
'(0) xs ys)
(cond ((null? rxs)
(if (null? rys)
(reverse! (remove-zero zs))
(reverse-append (cdr zs) (bignum-add-int rys (car zs)))))
(else
(reverse-append (cdr zs) (bignum-add-int rxs (car zs)))))))
bignum-add は xs と ys の要素と桁上げを表す変数 c を integer-add で加算し、その結果を累積変数 a のリストに格納していきます。xs が空リストで、かつ ys も空リストの場合、a に c を追加して、remove-zero で 0 を取り除いてから reverse! で反転します。ys が空リストでない場合、bignum-add-int で ys に c を加算し、その結果に a を反転したリストを連結します。ys が空リストの場合、xs は空リストではないので、xs に c を加算して、その結果に a を反転したリストを連結します。
別解は fold-left2 で書き直したものです。
リスト : 多倍長整数と整数の減算
(define (integer-sub x y c)
(let ((n (- x y c)))
(if (negative? n)
(values (+ n *base*) 1)
(values n 0))))
(define (bignum-sub-int xs c)
(let loop ((xs xs) (c c) (a '()))
(cond ((null? xs)
(if (positive? c)
(error "oops!, underflow")
(reverse! (remove-zero a))))
((zero? c)
(reverse-append a xs))
(else
(receive (n m)
(integer-sub (car xs) 0 c)
(loop (cdr xs) m (cons n a)))))))
bignum-sub-int は最下位の桁と引数 c を減算し、桁借りがあればそれを上位の桁から減算します。あとは、桁借りの処理を繰り返すだけです。xs が空リストで、桁借りの値 c が正であれば、計算結果は負になるのでエラーを送出します。そうでなければ、remove-zero で 0 を取り除いてから a を反転します。
integer-sub は x から y と c を減算して変数 n にセットします。もし、n が負になったならば、n + *base* と桁借りを表す 1 を valuse で返します。そうでなければ、n と 0 を values で返します。
リスト : 多倍長整数の減算
(define (bignum-sub xs ys)
(let loop ((xs xs) (ys ys) (c 0) (a '()))
(cond ((null? ys)
(let ((zs (bignum-sub-int xs c)))
(if (bignum-zero? zs)
(reverse! (remove-zero a))
(reverse-append a zs))))
((null? xs)
(error "oops!, underflow"))
(else
(receive (n m)
(integer-sub (car xs) (car ys) c)
(loop (cdr xs) (cdr ys) m (cons n a)))))))
; 別解
(define (bignum-sub1 xs ys)
(receive (r rxs rys)
(fold-left2 (lambda (x y a)
(receive (n m)
(integer-sub x y (car a))
(list* m n (cdr a))))
'(0) xs ys)
(cond ((null? rys)
(let ((zs (bignum-sub-int rxs (car r))))
(if (bignum-zero? zs)
(reverse! (remove-zero (cdr r)))
(reverse-append (cdr r) zs))))
(else
(error "oops!, underflow")))))
bignum-sub は xs と ys の要素と桁借りを表す変数 c を integer-sub で減算し、その結果を累積変数 a のリストに格納していきます。ys が空リストで xs が空リストでない場合、bignum-sub-int で xs から c を減算し、その結果を変数 zs にセットします。zs が 0 の場合、remove-zero で 0 を取り除いてから reverse! で反転します。zs が 0 でなければ、reverse-append で zs に a を反転したリストを連結します。xs が空リストの場合、結果は負になるのでエラーを送出します。
別解は fold-left2 で書き直したものです。
リスト : 多倍長整数と整数の乗算
(define (integer-mul x y c)
(let ((n (+ (* x y) c)))
(if (< n *base*)
(values n 0)
(values (modulo n *base*) (quotient n *base*)))))
(define (bignum-mul-int xs x)
(cond ((zero? x) *zero*)
((= x 1) xs)
(else
(let loop ((xs xs) (c 0) (a '()))
(cond ((null? xs)
(reverse! (remove-zero (cons c a))))
(else
(receive (n m)
(integer-mul (car xs) x c)
(loop (cdr xs) m (cons n a)))))))))
bignum-mul-int は引数 x が 0 ならば *zero* を、1 ならば xs をそのまま返します。それ以外の場合、xs の最下位の桁から順番に x と掛け算して、値を累積変数 a のリストに格納します。桁上がりは変数 c に格納して、上位の桁に足し算します。整数の乗算は関数 integer-mul で行います。引数 x, y が乗算する整数、c が桁上がりで加算する値です。x * y + c を n にセットし、値が *base* 未満であれば n と 0 を valuse で返します。そうでなければ、n と *base* の剰余と商を values で返します。
リスト : 多倍長整数の乗算
; fixed integer か
(define (fixint? xs) (null? (cdr xs)))
; 多倍長整数の乗算
(define (bignum-mul xs ys)
(cond ((fixint? xs)
(bignum-mul-int ys (car xs)))
((fixint? ys)
(bignum-mul-int xs (car ys)))
(else
(let loop ((xs xs) (ys ys) (a *zero*))
(if (null? ys)
a
(loop
(cons 0 xs)
(cdr ys)
(bignum-add (bignum-mul-int xs (car ys)) a)))))))
多倍長整数同士の乗算は筆算と同じ方法で行います。簡単な例を示しましょう。
xs : (4 3 2 1)
ys : (7 6 5)
1 2 3 4
* 5 6 7
----------------------
7 14 21 28
6 12 18 24 0
5 10 15 20 0 0
----------------------
5 16 34 52 45 28
図 : 多倍長整数の乗算
上図のように、xs を 16 bit 左シフトしながら ys の要素を掛け算し、その値を加算していけばいいわけです。
bignum-mul は引数 xs, ys が *base* 未満の整数であれば、bignum-mul-int を呼び出して計算します。そうでなければ、xs と ys の要素の乗算を bignum-mul-int で求め、累積変数 a にその値を bignum-add で加算します。ys の次の要素を乗算するとき、xs の先頭に 0 を挿入して 16 bit 左シフトします。
なお、今回の方法は桁数が多くなると遅くなります。これよりも高速な方法として「Karatsuba 法」や「高速フーリエ変換」を使った方法があります。これらのアルゴリズムについては、Fussy さんの「乗算処理の高速化」, 「高速フーリエ変換」、M.Kamada さんの「離散フーリエ変換を用いた多倍長乗算の話」が参考になると思います。
リスト : 多倍長整数と整数の除算
(define (integer-div x y c)
(let ((n (+ (* c *base*) x)))
(values (quotient n y) (modulo n y))))
(define (bignum-div-int xs x)
(cond ((zero? x)
(error "oops!, division by zero"))
((= x 1) (values xs *zero*))
(else
(let loop ((xs (reverse xs)) (c 0) (a '()))
(if (null? xs)
(values (if (null? a) *zero* a) (list c))
(receive (n m)
(integer-div (car xs) x c)
(loop (cdr xs)
m
(if (or (positive? n) (pair? a)) (cons n a) a))))))))
bignum-div-int は引数 x が 0 の場合はエラーを送出し、1 の場合は xs と 0 を valuse で返します。それ以外の場合は、xs の上位の桁から順番に整数 x で除算していきます。このため、xs を reverse で反転しています。named let の変数 c には上位の桁の余りをセットします。あとは、関数 integer-sub で xs の要素と x の除算を行います。このとき、c * *base* を加えてから x で割ることに注意してください。あとは商と剰余を values で返します。
bignum-div-int は上位の桁から処理を行うため、リストの末尾に 0 が付加されないように工夫する必要があります。値が 0 でない場合、または累積変数 a が空リストでない場合、値を a に追加します。それ以外の場合、つまり、a が空リストで値が 0 の場合は追加しません。最後に、a が空リストであれば *zero* と (list c) を、そうでなければ a と (list c) を valuse で返します。
多倍長整数の除算は筆算と同じ方法で行いますが、かなり複雑な処理になります。ここではアルゴリズムの概略を説明するだけにとどめます。詳細は参考文献をお読みください。
リスト : 多倍長整数の除算 (擬似コード)
xs = (x1 x2 ... xn), ys = (y1 y2 ... ym) とし、xs / ys の商と剰余を求める
*base* / 2 <= ym * d < *base* を満たす d を求め、(xs * d) / (ys * d) を計算する
xs1 = xs * d とする
xs1 と同じ桁数になるよう (ys * d) の下位に 0 を追加たものを ys1 とする
このとき、追加した 0 の個数を s とする
qs = ()
while( s >= 0 ){
xs1 / ys1 の仮の商 q' を求める。
(1) xs1 が ys1 よりも少ない桁数の場合、q' は 0 である
(2) xs1 と ys1 の桁数 (n) が同じ場合、q' = xn / yn とする
(3) xs1 が n 桁, ys1 が n - 1 桁の場合、
q' = min( (xn * *base* + xn-1) / yn-1, *base* - 1 ) とする
if( q' > 0 ){
ys2 = ys1 * q'
while( xs1 < ys2 ){
q' = q' - 1
ys2 = ys2 - ys1
}
xs1 = xs1 - ys2
}
q' を qs に追加する
ys1 の最下位から 0 を取り除く
s = s - 1
}
商は qs, 剰余は xs1 / d となる。
ポイントは仮の商 q' を求める処理です。ys1 の最上位の桁 ym が条件 (A) *base* / 2 <= ym < *base* を満たしている場合、(2) であれば q' は 0 か 1 になります。(3) であれば xs1 の上位 2 桁と ys1 の上位 1 桁 (ym) から仮の商を求めます。このとき、真の商を q とすると、条件 (A) を満たしているならば次の式が成り立ちます。
q <= q' <= q + 2
したがって、q の値は q', q' - 1, q' - 2 のどれかになります。ys2 = ys1 * q' を計算し、xs1 < ys2 であれば q' から 1 を、ys2 から ys1 を引き算します。これを xs1 >= ys2 になるまで繰り返しますが、最悪でも 2 回の繰り返しで済むわけです。
商 q が q' - 1 と q' - 2 になる例を示します。
xs1 = (65535 65535 32767) ys1 = (65535 32768) q' = (32767 * *base* + 65535) / 32768 = 65535 ys2 = (65535 32768) * 65535 = (1 32766 32768) > xs1 q' = q' - 1 = 65534 ys2 = ys2 - ys1 = (2 65533 32767) < xs1 q' = 65534, xs1 = xs1 - ys2 = (65533 2) ----------------------------------------------------- xs1 = (65535 0 32767) ys1 = (65535 32768) q' = (32767 * *base* + 0) / 32768 = 65534 ys2 = (65535 32768) 65534 = (2 65533 32767) > xs1 q' = q' - 1 ys2 = ys2 - ys1 = (3 32764 32767) > xs1 q' = q' - 1 ys2 = ys2 - ys1 = (4 65531 32766) < xs1 q' = 65532, xs1 = xs1 - ys2 = (65531 5)
なお、(3) を満たしているとき、より高い精度で仮の商 q' を求める方法があります。有名なクヌース先生のアルゴリズムDはこの方法を使っています。除算のアルゴリズムについては、参考文献『大きな整数の除算アルゴリズム』がわかりやすくまとまっていると思います。また、乗算の処理が高速な場合、ニュートン法で ys の逆数 1 / ys を求め、xs * (1 / ys) を計算することで除算を高速に実行することができます。
擬似コードをそのままプログラムすると次のようになります。
リスト : 多倍長整数の除算
; 定数
(define *half-base* #x8000)
; シフトするビット数を求める
(define (get-shift-bit n)
(let loop ((n n) (c 0))
(if (<= *half-base* n)
c
(loop (ash n 1) (+ c 1)))))
; 仮の商を求める
(define (get-quot xs ys)
(cond ((null? xs) 0)
((null? (cdr ys))
(if (null? (cdr xs))
; 同じ桁だから商は 0 または 1
(quotient (car xs) (car ys))
; 2 桁と 1 桁
(quotient (+ (* (cadr xs) *base*) (car xs)) (car ys))))
(else
(get-quot (cdr xs) (cdr ys)))))
; 多倍長整数の除算
(define (bignum-div xs ys)
(cond ((fixint? ys)
(bignum-div-int xs (car ys)))
((bignum<? xs ys)
(values *zero* xs))
(else
(let* ((d (get-shift-bit (last ys)))
(xs1 (bignum-shift-left xs d))
(s (- (length xs1) (length ys)))
(ys1 (bignum-shift-left ys (+ (* *base-bit* s) d)))
(q '()))
(do ((s s (- s 1))
(ys1 ys1 (cdr ys1)))
((negative? s) (values q (bignum-shift-right xs1 d)))
(let ((quot (min (get-quot xs1 ys1) (- *base* 1))))
(if (positive? quot)
(do ((quot quot (- quot 1))
(ys2 (bignum-mul-int ys1 quot)
(bignum-sub ys2 ys1)))
((bignum>=? xs1 ys2)
(push! q quot)
(set! xs1 (bignum-sub xs1 ys2))))
(if (pair? q) (push! q 0)))))))))
関数 get-shift-bit は ys の最上位の値が *base* / 2 以上になるよう、左シフトするビット数を求めます。関数 get-quot は仮の商を求めます。xs が空リストならば、xs の桁は ys よりも少ないので 0 を返します。ys が末尾の要素で、かつ xs も末尾の要素であれば、同じ桁数なので (car xs) / (car ys) を返します。そうでなければ、xs の上位 2 桁を求め、それを (car ys) で割り算します。関数 bignum-div は説明をそのままプログラムしただけなので、とくに難しいところはないと思います。
リスト : 多倍長整数を文字列に変換する
(define *char-table*
'(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\A #\B #\C #\D #\E #\F))
(define (bignum->string xs r)
(let loop ((xs xs) (a '()))
(if (bignum-zero? xs)
(list->string a)
(receive (n m)
(bignum-div-int xs r)
(loop n (cons (list-ref *char-table* (car m)) a))))))
bignum->string は簡単です。bignum-div-int で xs を基数 r で割り算し、*char-table* から (car m) 番目の要素を求め、それを累積変数 a のリストに追加します。この処理を xs が 0 になるまで繰り返し、最後に関数 list->string で a を文字列に変換します。
リスト : 文字列を多倍長整数に変換する
(define (position x xs)
(let loop ((xs xs) (n 0))
(cond ((null? xs) #f)
((eqv? (car xs) x) n)
(else
(loop (cdr xs) (+ n 1))))))
(define (string->bignum str r)
(let loop ((xs (string->list str)) (a *zero*))
(if (null? xs)
a
(let ((n (position (char-upcase (car xs)) *char-table*)))
(if (or (not n) (<= r n))
(error "oops!, illegal char"))
(loop (cdr xs)
(bignum-add-int (bignum-mul-int a r) n))))))
string->bignum も簡単です。文字列 str を関数 string->list でリストに変換し、named let で 1 文字ずつ順番に取り出します。そして、関数 position で文字を数値 n に変換します。このとき、英小文字を char-upcase で英大文字に変換しています。文字が見つからない場合、または n が基数 r 以上の場合はエラーを送出します。あとは、bignum-mul-int で累積変数 a と基数 r を掛け算し、それに bignum-add-int で n を加算していくだけです。最後に a を返します。
リスト : 階乗
(define (bignum-fact n)
(if (zero? n)
(integer->bignum 1)
(bignum-mul-int (bignum-fact (- n 1)) n)))
bignum-fact は引数 n が *base* 未満の整数なので、bignum-fact の返り値と n を bignum-mul-int で掛け算していくだけです。
リスト : 累乗
(define (bignum-power xs n)
(if (zero? n)
(integer->bignum 1)
(let* ((ys (bignum-power xs (quotient n 2)))
(zs (bignum-mul ys ys)))
(if (even? n)
zs
(bignum-mul zs xs)))))
bignum-power を再起呼び出しして xsn/2 を求め ys にセットし、bignum-mul で ys * ys を求めて zs にセットします。n が偶数の場合は zs を返し、そうでなければ、bignum-mul で xs * zs を求めて返します。