C(465 个字符)
#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 行到 100 万行不等,具体取决于我的格式化方式。
入口点是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
)。这种标记化是在for
循环中完成的E()
。它还处理任何一元+
和-
运算符,将它们合并到常量中。
令牌数组的“运算符”字段可以具有以下值之一:
0 : (
1 : )
2 : *
3 : +
4 :浮点常量值
5 : -
6 : ^
7 : /
8 :标记字符串的结尾
这个方案很大程度上是由Daniel Martin推导出来的,他注意到最后 3 位在这个挑战中每个运算符的 ASCII 表示中是唯一的。
的未压缩版本E()
看起来像这样:
void Evaluate(char *expression, char *result){
double tokenList[99];
char *parseEnd;
int i = 2, prevOperator = 0;
/* i must start at 2, because the EvalTokens will write before the
* beginning of the array. This is to allow overwriting an opening
* parenthesis with the value of the subexpression. */
for(; *expression != 0; i += 2){
/* try to parse a constant floating-point value */
tokenList[i] = strtod(expression, &parseEnd);
/* explanation below code */
if(parseEnd != expression && prevOperator != 4/*constant*/ &&
prevOperator != 1/*close paren*/){
expression = parseEnd;
prevOperator = tokenList[i + 1] = 4/*constant*/;
}else{
/* it's an operator */
prevOperator = tokenList[i + 1] = *expression & 7;
expression++;
}
}
/* done parsing, add end-of-token-string operator */
tokenList[i + 1] = 8/*end*/
/* Evaluate the expression in the token list */
EvalTokens(tokenList + 2); /* remember the offset by 2 above? */
sprintf(result, "%g", tokenList[0]/* result ends up in first value */);
}
由于我们保证输入有效,因此解析失败的唯一原因是因为下一个标记是运算符。如果发生这种情况,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/*end*/; i+= 2)
if(tokenList[i + 1] == 0/*open paren*/)
for(parenLevel = 1, parenStart = i; i += 2, parenLevel > 0){
if(tokenList[i + 1] == 0/*another open paren*/)
parenLevel++;
else if(tokenList[i + 1] == 1/*close paren*/)
if(--parenLevel == 0){
/* make this a temporary end of list */
tokenList[i + 1] = 8;
/* recursively handle the subexpression */
EvalTokens(tokenList + parenStart + 2);
/* fold the subexpression out */
FoldTokens(tokenList + parenStart, i - parenStart);
/* bring i back to where the folded value of the
* subexpression is now */
i = parenStart;
}
}
for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
if(tokenList[i + 1] == 6/*exponentiation operator (^)*/){
tokenList[i - 2] = pow(tokenList[i - 2], tokenList[i + 2]);
FoldTokens(tokenList + i - 2, 4);
i -= 2;
}
for(i = 0; tokenList[i + 1] != 8/*end*/; i+= 2)
if(tokenList[i + 1] == 2/*multiplication operator (*)*/ ||
tokenList[i + 1] == 7/*division operator (/)*/){
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/*end*/; i+= 2)
if(tokenList[i + 1] != 4/*constant*/){
tokenList[i - 2] +=
(tokenList[i + 1] == 3 ?
tokenList[i + 2] :
-tokenList[i + 2]);
FoldTokens(tokenList + i - 2, 4);
i -= 2;
}
tokenList[-2] = tokenList[0];
/* the compressed code does the above in a loop, equivalent to:
*
* for(i = 0; tokenList[i + 1] != 8; i+= 2)
* tokenList[i - 2] = tokenList[i];
*
* This loop will actually only iterate once, and thanks to the
* liberal use of macros, is shorter. */
}
一定程度的压缩可能会使这个函数更容易阅读。
一旦执行了操作,操作数和操作符就会被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; // force value to constant
while(tokenList[0] != 8/*end of token string*/){
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。另一方面,我也可以将我的函数转换为返回双精度而不是字符串,就像他一样,它会修剪 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 个没有 and 的前向声明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;
}
我会留给读者来决定哪个版本是合适的。