C (465 characters)
#define F for(i=0;P-8;i+=2)
#define V t[i
#define P V+1]
#define S V+2]),K(&L,4),i-=2)
#define L V-2]
K(double*t,int i){for(*++t=4;*t-8;*++t=V])*++t=V];}M(double*t){int i,p,b;
F if(!P)for(p=1,b=i;i+=2,p;)P?P-1||--p||(P=8,M(t+b+2),K(t+b,i-b),i=b):++p;
F P-6||(L=pow(L,S;F P-2&&P-7||(L*=(P-7?V+2]:1/S;F P-4&&(L+=(P-5?V+2]:-S;
F L=V];}E(char*s,char*r){double t[99];char*e,i=2,z=0;for(;*s;i+=2)V]=
strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;P=8;M(t+2);sprintf(r,"%g",*t);}
前五个换行符是必须的,其余的只是为了可读性。我将前五个换行符计算为一个字符。如果你想按行计算,那么在我删除所有空白字符之前,它有28行,但这是一个毫无意义的数字。它可能是从6行到一百万行,取决于我如何格式化它。
入口点是
E()
(表示“评估”)。第一个参数是输入字符串,第二个参数指向输出字符串,并且必须由调用者分配(根据通常的C标准)。它最多可以处理47个标记,其中标记是运算符之一("
+-*/^()
")或浮点数。一元符号运算符不算作单独的标记。
这段代码基于我多年前做的一个练习项目。我删除了所有的错误处理和跳过空格,并使用高尔夫技巧重新装备了它。下面是28行代码,有足够的格式化,我能够写出来,但可能不足以阅读。你需要
#include
<stdlib.h>
、
<stdio.h>
和
<math.h>
(或参见底部的说明)。
请看代码后面的解释。
#define F for(i=0;P-8;i+=2)
#define V t[i
#define P V+1]
#define S V+2]),K(&L,4),i-=2)
#define L V-2]
K(double*t,int i){
for(*++t=4;*t-8;*++t=V])
*++t=V];
}
M(double*t){
int i,p,b;
F if(!P)
for(p=1,b=i;i+=2,p;)
P?P-1||--p||(P=8,M(t+b+2),K(t+b,i-b),i=b):++p;
F P-6||(L=pow(L,S;
F P-2&&P-7||(L*=(P-7?V+2]:1/S;
F P-4&&(L+=(P-5?V+2]:-S;
F L=V];
}
E(char*s,char*r){
double t[99];
char*e,i=2,z=0;
for(;*s;i+=2)
V]=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
P=8;
M(t+2);
sprintf(r,"%g",*t);
}
第一步是进行标记化。每个标记的双倍数组都包含两个值,一个操作符(
P
,因为
O
看起来太像零)和一个值(
V
)。这种标记化是在
E()
中的
for
循环中完成的。它还处理任何一元的
+
和
-
运算符,并将它们合并到常量中。
标记数组的“操作符”字段可以具有以下值之一:
0:(
1:)
2:*
3:+
4:浮点常量值
5:-
6:^
7:/
8:标记字符串结束
这个方案在很大程度上是由
Daniel Martin提出的,他注意到这个挑战中每个操作符的ASCII表示中的最后3位是唯一的。
E()
的未压缩版本看起来像这样:
void Evaluate(char *expression, char *result){
double tokenList[99];
char *parseEnd;
int i = 2, prevOperator = 0;
for(; *expression != 0; i += 2){
tokenList[i] = strtod(expression, &parseEnd);
if(parseEnd != expression && prevOperator != 4 &&
prevOperator != 1){
expression = parseEnd;
prevOperator = tokenList[i + 1] = 4;
}else{
prevOperator = tokenList[i + 1] = *expression & 7;
expression++;
}
}
tokenList[i + 1] = 8
EvalTokens(tokenList + 2);
sprintf(result, "%g", tokenList[0]);
}
由于我们保证了输入的有效性,如果解析失败的原因只可能是下一个标记是运算符。 如果这种情况发生,则
parseEnd
指针将与
tokenStart
相同。 我们还必须处理解析成功的情况,但我们真正想要的是运算符。 这将发生在加法和减法运算符中,除非直接跟随符号运算符。 换句话说,给定表达式“
4-6
”,我们希望将其解析为
{4, -, 6}
,而不是
{4, -6}
。 另一方面,给定“
4+-6
”,我们应该将其解析为
{4, +, -6}
。 解决方案非常简单。 如果解析失败
或前面的标记是常量或关闭括号(实际上是将求值为常量的子表达式),则当前标记是运算符,否则则是常量。
完成标记化后,通过调用
M()
来完成计算和折叠,它首先寻找任何匹配的括号对,并处理其中包含的子表达式,方法是递归调用它本身。 然后处理运算符,首先是幂运算,然后是乘法和除法一起,最后是加法和减法一起。 由于期望输入格式正确(按照要求规定),因此它不会显式检查加法运算符,因为在处理完其他所有操作符之后,它是最后一个合法的操作符。
计算函数没有进行高尔夫压缩,可能长这个样子:
void EvalTokens(double *tokenList){
int i, parenLevel, parenStart;
for(i = 0; tokenList[i + 1] != 8; i+= 2)
if(tokenList[i + 1] == 0)
for(parenLevel = 1, parenStart = i; i += 2, parenLevel > 0){
if(tokenList[i + 1] == 0)
parenLevel++;
else if(tokenList[i + 1] == 1)
if(--parenLevel == 0){
tokenList[i + 1] = 8;
EvalTokens(tokenList + parenStart + 2);
FoldTokens(tokenList + parenStart, i - parenStart);
i = parenStart;
}
}
for(i = 0; tokenList[i + 1] != 8; i+= 2)
if(tokenList[i + 1] == 6){
tokenList[i - 2] = pow(tokenList[i - 2], tokenList[i + 2]);
FoldTokens(tokenList + i - 2, 4);
i -= 2;
}
for(i = 0; tokenList[i + 1] != 8; i+= 2)
if(tokenList[i + 1] == 2 ||
tokenList[i + 1] == 7){
tokenList[i - 2] *=
(tokenList[i + 1] == 2 ?
tokenList[i + 2] :
1 / tokenList[i + 2]);
FoldTokens(tokenList + i - 2, 4);
i -= 2;
}
for(i = 0; tokenList[i + 1] != 8; i+= 2)
if(tokenList[i + 1] != 4){
tokenList[i - 2] +=
(tokenList[i + 1] == 3 ?
tokenList[i + 2] :
-tokenList[i + 2]);
FoldTokens(tokenList + i - 2, 4);
i -= 2;
}
tokenList[-2] = tokenList[0];
}
一些压缩可能会使这个函数更易读。
一旦操作执行完毕,操作数和运算符就会通过K()
(通过宏S调用)从标记列表中折叠出来。操作的结果留在折叠表达式的常量位置。因此,最终结果留在标记数组的开头,所以当控制返回到E()
时,它只是将其打印到一个字符串中,利用了数组中第一个值是令牌的值字段的事实。
这次对FoldTokens()
的调用发生在操作(^
, *
, /
, +
或-
)执行后,或者在子表达式(括号括起来)处理后。 FoldTokens()
例程确保结果值具有正确的运算符类型(4),然后复制子表达式的剩余部分。例如,当处理表达式"2+6*4+1
"时,EvalTokens()
首先计算6*4
,将结果留在6
的位置(2+24*4+1
)。然后FoldTokens()
删除子表达式"24*4
"的其余部分,留下2+24+1
。
void FoldTokens(double *tokenList, int offset){
tokenList++;
tokenList[0] = 4;
while(tokenList[0] != 8){
tokenList[0] = tokenList[offset];
tokenList[1] = tokenList[offset + 1];
tokenList += 2;
}
}
那就是这样了。宏只是用来替换常见操作,其他的都是对以上内容进行高尔夫压缩。
Strager 坚持认为代码应包括 #include 语句,因为如果没有对 strtod 和 pow 函数进行适当的前向声明,代码将无法正常工作。由于挑战只要求一个函数而不是完整的程序,我认为这并不是必需的。但可以通过添加以下代码来以最小的代价添加前向声明:
#define D double
D strtod(),pow();
我会将代码中所有的"double
"替换为"D
"。这将增加19个字符,使总共达到484个字符。另一方面,我也可以像他那样将我的函数转换为返回一个double而不是一个字符串,这将减少15个字符,将E()
函数更改为:
D E(char*s){
D t[99];
char*e,i=2,z=0;
for(;*s;i+=2)
V]=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
P=8;
M(t+2);
return*t;
}
这将使得总代码大小为469个字符(或452个字符,如果不包括strtod
和pow
的前向声明,但包括D
宏)。甚至可以通过要求调用者传递一个指向double类型返回值的指针来再次减少1个字符:
E(char*s,D*r){
D t[99];
char*e,i=2,z=0;
for(;*s;i+=2)
V=strtod(s,&e),P=z=e-s&&z-4&&z-1?s=e,4:*s++&7;
P=8;
M(t+2);
*r=*t;
}
我将把决定留给读者,以确定哪个版本是合适的。