3.3 程序变换
power0是有关算法的一个令人满意的实现,它适用于运算的代价高于函数递归调用开销的情况.本节要推导出一个迭代算法,它执行运算的次数和power0一样.这里将要做一系列程序变换,这些变换也可以用在其他许多情况中.5 在本书后面的部分,通常将只给出算法的最终版本或几乎最终版本.
power0包含两个相同的递归调用,它每次只执行其中一个.这使我们可能通过公共子表达式删除技术来缩小代码的规模:
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power 1(Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n>0
if (n == I(1)) return a;Domain(Op) r = power 1(op(a, a), n / I(2), op);if (n % I(2) != I(0)) r = op(r, a);return r;
}
现在的目标是删除递归调用,为此要做的第一步是把过程变换到尾递归形式(tail-recursiveform),其中在过程执行的最后一步是对自身的递归调用.完成该变换的一种技术是引入累积变量(accumulation-variableintroduction),用于在不同递归调用之间携带累积的结果:
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 0(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
if (n == I(0)) return r;
5.只有在运算的语义和复杂性已知的情况下,编译器才会对一些内部类型做类似变换.规范性概念是类型创建者的一个断言,它保证程序员和编译器可以安全地执行这些变换.
if (n % I(2) != I(0)) r = op(r, a);
return power accumulate 0(r, op(a, a), n / I(2), op); }
设r0,a0和n0是r,a和n的原值,下面不变式(recursioninvariant)在每次递归调用时都成立:ran=r0an0 0 .这个版本还有另一优点,它不仅计算幂,还能计算乘以一个系数的幂.它也处理了指数为0的情况.但是在指数从1变到0时poweraccumulate0将多做一次平方.增加一种情况就可以消除它:
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 1(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
if (n == I(0)) return r;
if (n == I(1)) return op(r, a);
if (n % I(2) != I(0)) r = op(r, a);
return power accumulate 1(r, op(a, a), n / I(2), op); }
增加额外情况导致重复出现的子表达式,也使三个检测不独立了.通过仔细分析检测之间的依赖性和顺序,考虑它们的出现频率,可以给出
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 2(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
if (n % I(2) != I(0)) {
r = op(r, a);
if (n == I(1)) return r;
} else if (n == I(0)) return r;
return power accumulate 2(r, op(a, a), n / I(2), op);
}
在一个尾递归过程里,如果所有递归调用中的过程形参都是对应的实参,它就是一个严格尾递归的(stricttail-recursive)过程:
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 3(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
if (n % I(2) != I(0)) {
r = op(r, a);
if (n == I(1)) return r;
} else if (n == I(0)) return r;
a = op(a, a);
n = n / I(2);
return power accumulate 3(r, a, n, op); }
严格尾递归过程可以变换为一个迭代过程,方法是把每个递归调用代换为一个到过程开始的goto,也可以用一个等价的迭代结构:
template<typename I, typename Op> requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 4(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
while (true) {
if (n % I(2) != I(0)) {
r = op(r, a);
if (n == I(1)) return r;
} else if (n == I(0)) return r;a = op(a, a);n = n / I(2);
} }
递归不变式变成了这里的循环不变式(loopinvariant).如果开始时n>0,在变成0前要先经过1.我们借用这种情况消去对0的检查并加强前条件(strengthening`
javascript
precondition):
template requires(Integer(I) && BinaryOperation(Op))
Domain(Op) power accumulate positive 0(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n>0
while (true) {
if (n % I(2) != I(0)) {r = op(r, a);if (n == I(1)) return r;
}
a = op(a, a);n = n / I(2);
} }
知道了n>0会很有用.在开发组件的过程中经常会发现新的接口情况.现在放松前条件(relaxingprecondition):
template requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power accumulate 5(Domain(Op) r, Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n.0
if (n == I(0)) return r;return power accumulate positive 0(r, a, n, op);
}
通过一个简单的等式,就可以用poweraccumulate实现power:
nn.1
a = aa
这一变换就是消去累积变量(accumulation-variableelimination):
template requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power 2(Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n>0
return power accumulate 5(a, a, n -I(1), op);
}
这个算法多做了一些不必要的运算.例如,当n是16时它要执行7次运算,其中只有4次是必要的.当n是奇数时这个算法很好.避免上述问题的方法是反复做a的平方,并不断将指数折半直至它变成奇数:
template requires(Integer(I) && BinaryOperation(Op)) Domain(Op) power 3(Domain(Op) a, I n, Op op)
{
//前条件:associative(op)∧n>0
while (n % I(2) == I(0)) {a = op(a, a);n = n / I(2);
}
n = n / I(2);if (n == I(0)) return a;
return power accumulate positive 0(a, op(a, a), n, op);
}
练习3.1 请自己确认最后三行代码是正确的.