我正在恢复Ascota 170古董机械可编程计算机。它已经在工作了。 现在,我正在寻找一种算法来证明其功能-例如计算三角表或对数表。或类似的东西。 不幸的是,从数学运算来看,计算机只能加和减整数(-1E12至1E12中的55个寄存器)。甚至没有移位到数字的运算,因此可以通过编程将其实现为仅乘以非常小的数字。 但是它的逻辑运算已经非常完善。
您能建议我任何合适的算法吗?
答案 0 :(得分:6)
所以您正在做的事情真的很棒。碰巧的是,我可以解释很多关于如何仅使用整数加减运算来实现分数对数的方法!这篇文章将很长,但是其中包含很多细节,最后还有一个可行的实现,对于您用怪异的机械计算机做一些有趣的事情应该足够了。
实施比较
您将需要能够比较数字。尽管您说可以执行== 0和> 0的比较,但是对于您要实现的大多数有趣算法来说,这还远远不够。您需要相对比较,可以通过减法确定:
isLessThan(a, b):
diff = b - a
if diff > 0 then return true
else return false
isGreaterThan(a, b):
diff = a - b
if diff > 0 then return true
else return false
isLessThanOrEqual(a, b):
diff = a - b
if diff > 0 then return false
else return true
isGreaterThanOrEqual(a, b):
diff = b - a
if diff > 0 then return false
else return true
在本文的其余部分中,我将只写一种更简单的a > b
形式,但是如果您不能直接这样做,则可以替代上面的一种操作。
实施班次
现在,由于您没有数字移位硬件,因此必须创建“例程”来实现它。左移很容易:向其本身添加一个数字,一次又一次地添加,然后添加原始数字,然后再添加一次。这相当于向左移一位。
因此向左移动一位数字,或者乘以十:
shiftLeft(value):
value2 = value + value
value4 = value2 + value2
value5 = value4 + value
return value5 + value5
多次移位只是重复调用shiftLeft()
:
shl(value, count):
repeat:
if count <= 0 then goto done
value = shiftLeft(value)
count = count - 1
done:
return value
向右移动一位数字会有点困难:我们需要反复进行减法和加法运算,如下面的伪代码所示:
shr(value, count):
if count == 0 then return value
index = 11
shifted = 0
repeat1:
if index < 0 then goto done
adder = shl(1, index - count)
subtractor = shl(adder, count)
repeat2:
if value <= subtractor then goto next
value = value - subtractor
shifted = shifted + adder
goto repeat2
next:
index = index - 1
goto repeat1
done:
return count
方便的是,由于一开始很难向右移位,因此该算法使我们可以直接选择要移位的位数。
乘法
看来您的硬件可能有乘法?但是,如果没有,您可以使用重复的加法和移位来实现乘法。二进制乘法是最有效的最简单实现形式,它要求我们首先使用实现multiplyByTwo()
和{{1的相同基本技术来实现divideByTwo()
和shiftLeft()
}}。
一旦实现了这些,乘法就涉及重复切出其中一个数字的最后一位,如果该位是shr()
,然后将另一个数字的递增版本添加到运行总计中:< / p>
1
如果需要的话,下面将提供完整的实现。
整数对数
我们可以使用向右移动一位数字的能力来计算数字的以10为底的对数的整数部分-实际上,这是您可以将数字右移多少次您得到的数字太小而无法移动。
multiply(a, b):
product = 0
repeat:
if b <= 0 then goto done
nextB = divideByTwo(b)
bit = b - multiplyByTwo(nextB)
if bit == 0 then goto skip
product = product + a
skip:
a = a + a
b = nextB
goto repeat
done:
return product
因此对于0-9,这将返回0;对于10-99,返回1;对于100-999,则返回2,依此类推。
整数指数
上述算法的相反之处是微不足道的:要计算10的整数次幂,我们只需将数字左移幂即可。
integerLogarithm(value):
count = 0
repeat:
if value <= 9 then goto done
value = shiftRight(value)
count = count + 1
goto repeat
done:
return count
因此,对于0,它将返回1;对于0,将返回1。为1,则返回10;对于2,返回100;对于3,返回1000;等等。
分解整数和分数
现在,我们可以处理整数幂和对数,几乎可以处理小数部分了。但是,在我们真正讨论如何计算对数的小数部分之前,我们必须讨论如何对问题进行除法,以便我们可以与整数部分分开计算小数部分。理想情况下,我们只希望处理固定范围内数字的计算对数-例如,从1到10,而不是从1到无穷大。
我们可以使用整数对数和指数例程对完整的对数问题进行切分,以便无论输入数字是多少,我们总是在处理[1,10)范围内的值。
首先,我们计算整数对数,然后计算整数指数,然后从原始数字中减去该对数。剩下的就是我们需要计算的小数部分:然后剩下的唯一工作就是移动该小数部分,以使其始终处于一致的范围内。
integerExponent(count):
value = shl(1, count)
return value
您可以花费很少的精力说服自己,无论原始值是多少,其最高非零数字都将移至第7列:因此,“ 12345”将变为“ 000000123450”(即“ 0000001.23450”)。这使我们可以假装总有一个不可见的小数点,比数字的一半还小,所以现在我们只需要解决计算[1,10)范围内的对数的问题。
(为什么“超过一半”?我们将需要值的上半部分始终为零,一会儿您就会明白为什么。)
分数对数
Knuth在计算机编程的艺术第1.2.2节中说明了如何执行此操作。我们的目标是计算log10(x),以便对于normalize(value):
intLog = integerLogarithm(value) // From 0 to 12 (meaningful digits)
if intLog <= 5 then goto lessThan
value = shr(value, intLog - 5)
goto done
lessThan:
value = shl(value, 5 - intLog)
done:
return value
,b1
,b2
...的某些值,其中b3
已经是n
(因为我们拆分了上面的整数部分):
log10(x)= n + b1 / 2 + b2 / 4 + b3 / 8 + b4 / 16 + ...
Knuth说我们可以像这样获得0
,b1
,b2
...
要获得b1,b2,...,我们现在设置x0 = x / 10 ^ n,并且对于k> = 1,
b [k] = 0,x [k] = x [k-1] ^ 2,如果x [k-1] ^ 2 <10;
b [k] = 1,如果x [k-1] ^ 2> = 10,则x [k] = x [k-1] ^ 2/10。
也就是说,每个步骤都使用伪代码循环,如下所示:
b3
为了使用上面的定点数字来实现此目的,我们必须使用移位实现fractionalLogarithm(x):
for i = 1 to numberOfBinaryDigitsOfPrecision:
nextX = x * x
if nextX < 10 then:
b[i] = 0
else:
b[i] = 1
nextX = nextX / 10
,以将小数点移回原位,这将丢失一些数字。正如Knuth所说,这将导致错误传播,但是它将提供足够的准确性,足以用于演示目的。
因此,给定x * x
生成的分数值,我们可以像这样计算其分数二进制对数:
normalize(value)
但是 binary 分数对数-单个位! —并不是特别有用,特别是因为我们在前面的步骤中计算了对数整数部分的十进制版本。因此,我们将再次修改此时间,以计算一个 decimal 分数对数到5个位,而不是计算一个位数组。为此,我们需要一个包含20个值的表,这些值代表每个位到十进制的转换,并且还将它们存储为定点数:
fractionalLogarithm(value):
for i = 1 to 20:
value = shr(value * value, 6)
if value < 1000000 then:
b[i] = 0
else:
b[i] = 1
value = shr(value, 1)
因此,现在有了该表,我们可以使用纯整数数学生成整个分数对数:
table[1] = 1/(2^1) = 1/2 = 500000
table[2] = 1/(2^2) = 1/4 = 250000
table[3] = 1/(2^3) = 1/8 = 125000
table[4] = 1/(2^4) = 1/16 = 062500
table[5] = 1/(2^5) = 1/32 = 031250
table[6] = 1/(2^6) = 1/64 = 015625
...
table[17] = 1/(2^17) = 1/131072 = 000008
table[18] = 1/(2^18) = 1/262144 = 000004
table[19] = 1/(2^19) = 1/514288 = 000002
table[20] = 1/(2^20) = 1/1048576 = 000001
全部组合
最后,对于您的机器可以代表的任何整数的完整对数,这就是全部,它将以六位数的精度计算对数,格式为“ 0000XX.XXXXXX”:
fractionalLogarithm(value):
log = 0
for i = 1 to 20:
value = shr(value * value, 6)
if value >= 1000000 then:
log = log + table[i]
value = shr(value, 1)
return log
演示
表明数学有效,并且效果很好! —以下是上述算法的JavaScript实现。它使用纯整数数学:仅加法,减法和相对比较。函数用于组织代码,但它们的行为类似于子例程:它们不是递归的,并且嵌套的也不深。
您可以现场试用(单击“运行”按钮,然后在输入字段中键入log(value):
intPart = integerLogarithm(value)
value = normalize(value)
fracPart = fractionalLogarithm(value)
result = shl(intPart, 6) + fracPart
return result
)。将结果与标准12345
函数进行比较,您将看到纯整数版本的接近程度:
Math.log()
function shiftLeft(value) {
var value2 = value + value;
var value4 = value2 + value2;
var value5 = value4 + value;
return value5 + value5;
}
function shl(value, count) {
while (count > 0) {
value = shiftLeft(value);
count = count - 1;
}
return value;
}
function shr(value, count) {
if (count == 0) return value;
var index = 11;
var shifted = 0;
while (index >= 0) {
var adder = shl(1, index - count);
var subtractor = shl(adder, count);
while (value > subtractor) {
value = value - subtractor;
shifted = shifted + adder;
}
index = index - 1;
}
return shifted;
}
//-----------------------------------
function multiplyByTwo(value) {
return value + value;
}
function multiplyByPowerOfTwo(value, count) {
while (count > 0) {
value = value + value;
count = count - 1;
}
return value;
}
function divideByPowerOfTwo(value, count) {
if (count == 0) return value;
var index = 39; // lg(floor(pow(10, 12)))
var shifted = 0;
while (index >= 0) {
var adder = multiplyByPowerOfTwo(1, index - count);
var subtractor = multiplyByPowerOfTwo(adder, count);
while (value >= subtractor) {
value = value - subtractor;
shifted = shifted + adder;
}
index = index - 1;
}
return shifted;
}
function divideByTwo(value) {
return divideByPowerOfTwo(value, 1);
}
function multiply(a, b) {
var product = 0;
while (b > 0) {
nextB = divideByTwo(b);
bit = b - multiplyByTwo(nextB);
if (bit != 0) {
product += a;
}
a = a + a;
b = nextB;
}
return product;
}
//-----------------------------------
var logTable = {
"1": 500000,
"2": 250000,
"3": 125000,
"4": 62500,
"5": 31250,
"6": 15625,
"7": 7813,
"8": 3906,
"9": 1953,
"10": 977,
"11": 488,
"12": 244,
"13": 122,
"14": 61,
"15": 31,
"16": 15,
"17": 8,
"18": 4,
"19": 2,
"20": 1,
};
//-----------------------------------
function integerLogarithm(value) {
var count = 0;
while (value > 9) {
value = shr(value, 1);
count = count + 1;
}
return count;
}
function normalize(value) {
var intLog = integerLogarithm(value);
if (intLog > 5)
value = shr(value, intLog - 5);
else
value = shl(value, 5 - intLog);
return value;
}
function fractionalLogarithm(value) {
var log = 0;
for (i = 1; i < 20; i++) {
var squaredValue = multiply(value, value);
value = shr(squaredValue, 5);
if (value >= 1000000) {
log = log + logTable[i];
value = shr(value, 1);
}
}
return log;
}
function log(value) {
var intPart = integerLogarithm(value);
value = normalize(value);
var fracPart = fractionalLogarithm(value);
var result = shl(intPart, 6) + fracPart;
return result;
}
//-----------------------------------
// Just a little jQuery event handling to wrap a UI around the above functions.
$("#InputValue").on("keydown keyup keypress focus blur", function(e) {
var inputValue = Number(this.value.replace(/[^0-9]+/g, ''));
var outputValue = log(inputValue);
$("#OutputValue").text(outputValue / 1000000);
var trueResult = Math.floor((Math.log(inputValue) / Math.log(10)) * 1000000 + 0.5) / 1000000
$("#TrueResult").text(trueResult);
});
答案 1 :(得分:2)
正如我在您的Original question on SE/RC中为pow,sqrt,n-root,log,exp
所提到的,请参阅:
以及其中的所有子链接。
一旦您开始使用*,/,<<,>>
(其他答案都很好)并且可以固定点而不是浮动,您也可以开始计算测角学。为此,最好使用Chebyshev系列,但是由于我缺乏数学原理,所以只能使用已经预先计算的... Taylor是一个常识,因此在这里进行计算应该很容易,因为我为算术模板编写的代码涵盖任意数学数据类型(bignums)的数学:
// Taylor goniometric https://en.wikipedia.org/wiki/Taylor_series
friend T sin (const T &x) // = sin(x)
{
int i; T z,dz,x2,a,b;
x2=x/(pi+pi); x2-=::integer(x2); x2*=pi+pi;
for (z=x2,a=x2,b=1,x2*=x2,i=2;;)
{
a*=x2; b*=i; i++; b*=i; i++; dz=a/b; z-=dz;
a*=x2; b*=i; i++; b*=i; i++; dz=a/b; z+=dz;
if (::abs(dz)<zero) break;
}
return z;
}
friend T cos (const T &x) // = cos(x)
{
int i; T z,dz,x2,a,b;
x2=x/(pi+pi); x2-=::integer(x2); x2*=pi+pi;
for (z=1,a=1,b=1,x2*=x2,i=1;;)
{
a*=x2; b*=i; i++; b*=i; i++; dz=a/b; z-=dz;
a*=x2; b*=i; i++; b*=i; i++; dz=a/b; z+=dz;
if (::abs(dz)<zero) break;
}
return z;
}
friend T tan (const T &x) // = tan(x)
{
int i; T z0,z1,dz,x1,x2,a,b;
x1=x/pi; x1-=::integer(x1); x1*=pi; x2=x1*x1;
for (z0=1,z1=1,a=1,b=1,i=2;;)
{
a*=x2; b*=i; i++; dz=a/b; z0-=dz; // z0=cos(x)
b*=i; i++; dz=a/b; z1-=dz; // z1=sin(x)/x
a*=x2; b*=i; i++; dz=a/b; z0+=dz;
b*=i; i++; dz=a/b; z1+=dz;
if (::abs(dz)<zero) break;
}
return (x1*z1)/z0;
}
friend T ctg (const T &x) // = cotan(x)
{
int i; T z0,z1,dz,x1,x2,a,b;
x1=x/pi; x1-=::integer(x1); x1*=pi; x2=x1*x1;
for (z0=1,z1=1,a=1,b=1,i=2;;)
{
a*=x2; b*=i; i++; dz=a/b; z0-=dz; // z0=cos(x)
b*=i; i++; dz=a/b; z1-=dz; // z1=sin(x)/x
a*=x2; b*=i; i++; dz=a/b; z0+=dz;
b*=i; i++; dz=a/b; z1+=dz;
if (::abs(dz)<zero) break;
}
return z0/(x1*z1);
}
friend T asin (const T &x) // = asin(x)
{
if (x<=-1.0) return -0.5*pi;
if (x>=+1.0) return +0.5*pi;
return ::atan(x/::sqrt(1.0-(x*x)));
}
friend T acos (const T &x){ T z; z=0.5*pi-::asin(x); return z; } // = acos(x)
friend T atan (const T &x) // = atan(x)
{
bool _shift=false;
bool _invert=false;
bool _negative=false;
T z,dz,x1,x2,a,b; x1=x;
if (x1<0.0) { _negative=true; x1=-x1; }
if (x1>1.0) { _invert=true; x1=1.0/x1; }
if (x1>0.7) { _shift=true; b=::sqrt(3.0)/3.0; x1=(x1-b)/(1.0+(x1*b)); }
for (x2=x1*x1,z=x1,a=x1,b=1;;) // if x1>0.8 convergence is slow
{
a*=x2; b+=2; dz=a/b; z-=dz;
a*=x2; b+=2; dz=a/b; z+=dz;
if (::abs(dz)<zero) break;
}
if (_shift) z+=pi/6.0;
if (_invert) z=0.5*pi-z;
if (_negative) z=-z;
return z;
}
friend T actg (const T &x){ T z; z=::atan(1.0/x); return z; } // = acotan(x)
friend T atan2 (const T &y,const T &x){ return atanxy(x,y); } // = atan(y/x)
friend T atanxy (const T &x,const T &y) // = atan(y/x)
{
int sx,sy; T a;
T _zero=1.0e-30;
sx=0; if (x<-_zero) sx=-1; if (x>+_zero) sx=+1;
sy=0; if (y<-_zero) sy=-1; if (y>+_zero) sy=+1;
if ((sy==0)&&(sx==0)) return 0.0;
if ((sx==0)&&(sy> 0)) return 0.5*x.pi;
if ((sx==0)&&(sy< 0)) return 1.5*x.pi;
if ((sy==0)&&(sx> 0)) return 0.0;
if ((sy==0)&&(sx< 0)) return x.pi;
a=y/x; if (a<0) a=-a;
a=::atan(a);
if ((sx>0)&&(sy>0)) a=a;
if ((sx<0)&&(sy>0)) a=x.pi-a;
if ((sx<0)&&(sy<0)) a=x.pi+a;
if ((sx>0)&&(sy<0)) a=x.pi+x.pi-a;
return a;
}
正如我提到的,您需要为此使用浮点数或定点数,因为结果不是整数!!!
但是正如我之前提到的,CORDIC更适合于整数计算(如果您在此处使用C ++代码在SE / SO上的此处QA中搜索某些QA)。
IIRC利用某些(弧形)正切角求和标识,可以很好地计算整数三角角,例如sqrt(1+x*x)
,可以很容易地计算整数。使用二进制搜索或近似/迭代,您可以计算任何角度的tan
,并且使用测角标识可以计算任何cotan
sin
和cos
... 但是我可能不对,因为我很久以前不使用CORDIC并阅读过有关内容
无论如何,一旦获得某些函数,通常可以通过二进制搜索来计算其逆。