我在“浅谈 BigInteger”的随笔中实现了一个 Skyiv.Numeric.BigInteger 类,那时乘法是使用常规的 O(N2) 的算法,所以比 .NET Framework 3.5 Base Class Library 中的 System.Numeric.BigInteger 类稍慢,后者的乘法是使用 Karatsuba 算法,其时间复杂度约为 O(N1.585)。
现在,我已经实现了一个提供任意精度的算术运算的静态类: BigArithmetic,该类使用快速傅里叶变换计算大整数乘法,其时间复杂度降低到 O(N logN loglogN)。所以,Skyiv.Numeric.BigInteger 类也重新改写调用 BigArithmetic 类的静态方法来进行算术运算。
下面就是改写后的 Skyiv.Numeric.BigInteger 类的源程序代码:
1
using
System;
2
using
System.Text;
3
4
namespace
Skyiv.Numeric
5
{
6
sealed
class
BigInteger : IEquatable
<
BigInteger
>
, IComparable
<
BigInteger
>
7
{
8
static
readonly
byte
Len
=
2
;
9
static
readonly
byte
Base
=
(
byte
)Math.Pow(
10
, Len);
10
11
sbyte
sign;
//
符号,取值:-1, 0, 1。
12
byte
[] data;
//
字节数组以 100 为基,字节数组中第一个元素存储的数字是最高有效位。
13
因为 BigArithmetic 类是对字节数组进行算术运算,字节数组以 100 为基,为了方便调用 BigArithmetic 类的静态方法进行算术运算,BigInteger 也改为使用以 100 为基的字节数组来存储。原来是使用 109 为基的 List<int> 来存储的。
我在改写过程中曾经使用过以 108 为基的 List<int> 来存储,在需要调用 BigArithmetic 类的静态方法进行算术运算时将参数转换为以 100 为基的字节数组,运算完成后再将运算结果转换回来。这样做的好处是加法和减法运算比较快(还使用原来的方法,不调用 BigArithmetic 类的静态方法),并且除了 operator *、DivRem 和 Sqrt 方法以外,其他所有的方法都不用改写。不过经过综合考虑,还是采用现在的方案。
14
BigInteger()
15
{
16
}
17
18
BigInteger(
long
x)
19
{
20
sign
=
(
sbyte
)((x
==
0
)
?
0
: ((x
>
0
)
?
1
:
-
1
));
21
data
=
new
byte
[
10
];
//
long.MinValue = -9,223,372,036,854,775,808
22
ulong
z
=
(x
<
0
)
?
(
ulong
)
-
x : (
ulong
)x;
23
for
(
int
i
=
data.Length
-
1
; z
!=
0
; i
--
, z
/=
Base) data[i]
=
(
byte
)(z
%
Base);
24
Shrink();
25
}
26
27
BigInteger(BigInteger x)
28
{
29
sign
=
x.sign;
//
x != null
30
data
=
new
byte
[x.data.Length];
31
Array.Copy(x.data, data, data.Length);
32
}
33
34
public
static
implicit
operator
BigInteger(
long
x)
35
{
36
return
new
BigInteger(x);
37
}
38
私有的构造函数,和原来的大同小异。
39
public
static
BigInteger Parse(
string
s)
40
{
41
if
(s
==
null
)
return
null
;
42
s
=
s.Trim().Replace(
"
,
"
,
null
);
43
if
(s.Length
==
0
)
return
0
;
44
BigInteger z
=
new
BigInteger();
45
z.sign
=
(
sbyte
)((s[
0
]
==
'
-
'
)
?
-
1
:
1
);
46
if
(s[
0
]
==
'
-
'
||
s[
0
]
==
'
+
'
) s
=
s.Substring(
1
);
47
int
r
=
s.Length
%
Len;
48
z.data
=
new
byte
[s.Length
/
Len
+
((r
!=
0
)
?
1
:
0
)];
49
int
i
=
0
;
50
if
(r
!=
0
) z.data[i
++
]
=
byte
.Parse(s.Substring(
0
, r));
51
for
(; i
<
z.data.Length; i
++
, r
+=
Len) z.data[i]
=
byte
.Parse(s.Substring(r, Len));
52
z.Shrink();
53
return
z;
54
}
55
静态的 Parse 方法,也和原来的大同小异。
程序的第 56 到 95 行以及 111 到 116 行是 Abs、Pow 方法以及单目 +、-、++ 和 -- 运算符,以及减法(-)运算符,和原来的相同。
96
public
static
BigInteger
operator
+
(BigInteger x, BigInteger y)
97
{
98
if
(x
==
null
||
y
==
null
)
return
null
;
99
if
(x.AbsCompareTo(y)
<
0
) Utility.Swap(
ref
x,
ref
y);
100
BigInteger z
=
new
BigInteger();
101
z.sign
=
x.sign;
102
byte
[] bs
=
Utility.Expand(y.data, x.data.Length);
103
bool
isAdd
=
x.sign
*
y.sign
==
1
;
104
z.data
=
new
byte
[x.data.Length
+
(isAdd
?
1
:
0
)];
105
if
(isAdd) BigArithmetic.Add(z.data, x.data, bs, bs.Length);
106
else
BigArithmetic.Subtract(z.data, x.data, bs, bs.Length);
107
z.Shrink();
108
return
z;
109
}
110
加法(+)运算符,调用 BigArithmetic 类的 Add 和 Subtract 方法。
117
public
static
BigInteger
operator
*
(BigInteger x, BigInteger y)
118
{
119
if
(x
==
null
||
y
==
null
)
return
null
;
120
if
(x.sign
*
y.sign
==
0
)
return
0
;
121
BigInteger z
=
new
BigInteger();
122
z.sign
=
(
sbyte
)(x.sign
*
y.sign);
123
z.data
=
new
byte
[x.data.Length
+
y.data.Length];
124
BigArithmetic.Multiply(z.data, x.data, x.data.Length, y.data, y.data.Length);
125
z.Shrink();
126
return
z;
127
}
128
乘法(*)运算,直接调用 BigArithmetic 类的 Multiply 方法。
程序的第 129 到 141 行是 / 和 % 运算符,和原来的相同。
142
public
static
BigInteger DivRem(BigInteger dividend, BigInteger divisor,
out
BigInteger remainder)
143
{
144
remainder
=
null
;
145
if
(dividend
==
null
||
divisor
==
null
)
return
null
;
146
if
(divisor.sign
==
0
)
throw
new
DivideByZeroException();
147
if
(dividend.AbsCompareTo(divisor)
<
0
)
148
{
149
remainder
=
new
BigInteger(dividend);
150
return
0
;
151
}
152
BigInteger quotient
=
new
BigInteger();
153
remainder
=
new
BigInteger();
154
quotient.data
=
new
byte
[dividend.data.Length
-
divisor.data.Length
+
1
];
155
remainder.data
=
new
byte
[divisor.data.Length];
156
BigArithmetic.DivRem(quotient.data, remainder.data, dividend.data,
157
dividend.data.Length, divisor.data, divisor.data.Length);
158
quotient.sign
=
(
sbyte
)(dividend.sign
*
divisor.sign);
159
remainder.sign
=
dividend.sign;
160
quotient.Shrink();
161
remainder.Shrink();
162
return
quotient;
163
}
164
静态的 DevRem 方法,也是调用 BigArithmetic 类的 DivRem 方法。
165
public
static
BigInteger Sqrt(BigInteger x)
166
{
167
if
(x
==
null
||
x.sign
<
0
)
return
null
;
168
if
(x.sign
==
0
)
return
0
;
169
if
(x.data.Length
==
1
)
return
new
BigInteger((
long
)Math.Sqrt(x.data[
0
]));
170
BigInteger z
=
new
BigInteger();
171
z.sign
=
1
;
172
z.data
=
new
byte
[x.data.Length
/
2
+
3
];
173
z.data
=
Adjust(BigArithmetic.Sqrt(z.data, z.data, z.data.Length, x.data, x.data.Length), x.data.Length);
174
z.Shrink();
175
BigInteger z1
=
z
+
1
;
//
平方根有可能比实际小 1。
176
return
(z1
*
z1
<=
x)
?
z1 : z;
177
}
178
179
static
byte
[] Adjust(
byte
[] bs,
int
digits)
180
{
181
if
(bs[
0
]
>=
10
)
throw
new
OverflowException(
"
sqrt adjust
"
);
182
byte
[] nbs
=
new
byte
[(digits
+
1
)
/
2
];
183
if
(digits
%
2
==
0
)
184
for
(
int
k
=
bs[
0
], i
=
0
; i
<
nbs.Length; i
++
, k
=
bs[i]
%
10
)
185
nbs[i]
=
(
byte
)(k
*
10
+
bs[i
+
1
]
/
10
);
186
else
Array.Copy(bs, nbs, nbs.Length);
187
return
nbs;
188
}
189
求平方根(Sqrt),调用 BigArithmetic 类的 Sqrt 方法。但是要注意,BigArithmetic 类的 Sqrt 方法求得的平方根是小数,需要使用 Adjust 方法调整为整数。并且平方根有可能比实际的小 1,也需要判断和调整。
190
void
Shrink()
191
{
192
int
i;
193
for
(i
=
0
; i
<
data.Length; i
++
)
if
(data[i]
!=
0
)
break
;
194
if
(i
!=
0
)
195
{
196
byte
[] bs
=
new
byte
[data.Length
-
i];
197
Array.Copy(data, i, bs,
0
, bs.Length);
198
data
=
bs;
199
}
200
if
(data.Length
==
0
) sign
=
0
;
201
}
202
私有的 Shrink 方法用于在需要时收缩存储数据的字节数组。
程序的第 203 到 238 行是各种逻辑运算符,和原来的完全一样。
239
public
override
string
ToString()
240
{
241
StringBuilder sb
=
new
StringBuilder();
242
if
(sign
<
0
) sb.Append(
'
-
'
);
243
sb.Append((data.Length
==
0
)
?
0
: (
int
)data[
0
]);
244
for
(
int
i
=
1
; i
<
data.Length; i
++
) sb.Append(data[i].ToString(
"
D
"
+
Len));
245
return
sb.ToString();
246
}
247
从基类继承的 ToString 方法,和原来的大同小异。
程序的第 248 到 274 行是从基类继承的 GetHashCode、Equals 方法,以及用于实现接口的 Equals 和 CompareTo 方法,和原来的相同。
275
int
AbsCompareTo(BigInteger other)
276
{
277
if
(data.Length
<
other.data.Length)
return
-
1
;
278
if
(data.Length
>
other.data.Length)
return
1
;
279
return
BigArithmetic.Compare(data, other.data, data.Length);
280
}
281
}
282
}
最后是私有的 AbsCompareTo 方法,调用 BigArithmetic 类的 Compare 方法。
下面是程序中用到的辅助类:
1
using
System;
2
3
namespace
Skyiv
4
{
5
static
class
Utility
6
{
7
public
static
T[] Expand
<
T
>
(T[] x,
int
n)
8
{
9
T[] z
=
new
T[n];
//
assume n >= x.Length
10
Array.Copy(x,
0
, z, n
-
x.Length, x.Length);
11
return
z;
12
}
13
14
public
static
void
Swap
<
T
>
(
ref
T x,
ref
T y)
15
{
16
T z
=
x;
17
x
=
y;
18
y
=
z;
19
}
20
}
21
}
经过运行测试程序,发现计算 256,142 位的乘积的运行时间从原来的 34.2497761 秒降低到 0.1749114 秒,速度提高了将近二百倍。如果计算的数字更大的话,速度将提高得更多。因为原来乘法的时间复杂度是 O(N2)。而现在乘法使用 FFT,时间复杂度是 O(N logN loglogN)。
我想,这个 BigInteger 类的运算速度已经是非常快的了。不知道有没有其他 C# 的 BigInteger 类的运算速度更快。
本文中的所有源代码可以到 https://bitbucket.org/ben.skyiv/biginteger 页面下载。
也可以使用 hg clone http://bitbucket.org/ben.skyiv/biginteger/ 命令下载。
关于 hg ,请参阅 Mercurial 备忘录。
2010-04-22 更新: 对 Skyiv.Numeric.BigInteger 的 GetHashCode、Parse 和 ToString 方法进行了优化。请参阅:再谈 BigInteger - 优化