Ackerman 函数如下:
如果用递归算法求 A(m, n) 非常简单:
int Ackerman(m, n)
{
if(m==0)
return n+1;
else if(m>0 && n==0)
return Ackerman(m-1, 1);
else if(m>0 && n>0)
return Ackerman(m-1, Ackerman(m, n-1));
}
那用非递归算法如何求解呢?也有很多方法。我们知道,在一般情况下,将递归算法转化程等价地非递归算法应该设置的数据结构为栈。所以这次选用栈来非递归地求解 Ackerman 函数。
既然要用到栈,我们必须考虑一个很重要的问题,那就是要把什么入栈。Ackerman 函数中一共涉及三个元素:m 、n 、函数结果值,栈中需要记录其中的一个还是多个元素呢?换句话说,遇到这种函数求值型的题目,我们是将变量(即 m 和 n)入栈,还是将函数结果值(即每层运算的 Ackerman 值)入栈呢?
接下来我们通过一个比 Ackerman(m, n) 简单的 F(n) 来探讨这个问题。
F(n) 函数如下:
F(n) 比 Ackerman(m, n) 简单在于:1)它只有一个变量n;2)它只包含两种情况。
这道题的解法是将 n 入栈,n 的值会一直除以 2 直到 n 的值为 0 为止,而顺序栈将记录这个过程中 n 的所有取值。假设我们要求 F(8) 的值,求解过程可得到下表:
所有 n 值入栈后,对应在栈中的存储形态为:
将所有的 n 值入栈后,再依次出栈,出栈时计算 F(n) 的结果。具体代码如下:
#include "stdio.h"
#include <stdlib.h>
#define MAXSIZE 100;
typedef struct
{
int data[MAXSIZE];
int top;
} Stack;
int F(n)
{
Stack* S=(Stack*)malloc(sizeof(Stack));
S->top=-1;
int f=0;
//将n值入栈
for(int i=n; i>=0; i=i/2)
{
S->top++;
S->data[S->top]=i;
}
//将n值出栈并计算结果
while(S->top>0)
{
if(S->data[S->top]==0)
f=1;
else
f=f*(S->data[S->top];
}
return f;
}
为什么不将 F(n) 入栈呢,答案很简单,因为一开始我们并不知道 F(n) 的值是多少(如果一开始知道就不用求解了),但我们知道 n 值是多少,所以对函数求值型的题目,我们将其变量入栈。
再回到 Ackerman 函数的问题上来,由于 Ackerman(m, n) 有两个变量,需要将 m 和 n 的值入栈,所以我们不再采用顺序栈而改为采用链栈。与解 F(n) 的方法一样,我们先用一些实际的值代入 m 和 n 求解答案,以找到函数其中的规律。首先先试着人工解出 Ackerman(2, 1) 。下面的解题过程希望大家可以在纸上跟着做。
根据 Ackerman 函数的公式,我们可以列出下面的表格:
将上面表格里的 m 和 n 入栈后,对应在栈中的存储状态为:
上面的表格所列出来的计算过程还不完整,但我们可以看到表格最后一行,已经推算到了 m = 0 的情况,那么对应的这个 A(0, A(1, 0)) 怎么去计算呢,代入 Ackerman 函数,A(0, A(1, 0)) = A(1, 0) + 1,且 A(0, A(1, 0)) 并不是 A(2, 1) 的答案。于是接下去推算:
将上面表格新增的 m 和 n 入栈后,对应在栈中的存储状态为:
现在,我们知道表格倒数第 4 行的 A(0, A(1, 0)) = A(0, 2),那么将多余的数据元素出栈并修改 A(0, A(1, 0)) 对应的数据元素的 n 值,得到下表:
接着又从上面这张表格推算出表格倒数第 5 行的 A(1, A(2,0)) = A(1, 3),重复之前的步骤,将多余的数据元素出栈并修改 A(0, A(1, 0)) 对应的数据元素的 n 值,得到下表:
栈中的存储状态为:
到这里可以总结出一些重复步骤:在推算结果的过程中,一开始,将 m 和 n 的值入栈,然后根据公式不断换算 A(m, n) 结果,并将换算出的 A'(m', n') 中的 m' 和 n' 入栈,直到换算出一个具体数值才停止入栈。
假设 A(m, n) 的 m 和 n 已入栈,那么根据公式换算的 A'(m', n') 的 m' 和 n' 该如何入栈呢。若 m > 0 且 n > 0,则 m' = m - 1 可以正常入栈,而 n' = A''(m, n-1),此时 n' 是一个新的公式,无法入栈,我们可以用一个标志性数值来标识它(比如 -1),就像表格里的橙色星号 * 一样,表示这个 n' 等待被计算。除 m > 0 且 n > 0 以外的两种情况,m' 和 n' 都可以正常入栈。
这样的重复入栈操作什么时候才停止呢,表格中 A(m, n) 一列的数据推算到具体数值才停止入栈,即 Top 指针指向的数据结构中,m = 0 且 n 已知时停止入栈,开始出栈。对照第二张表格的推算过程,一旦推算出 A(0, 1) = 2 ,这个 2 对应的是表格中最后一个橙色星号 * 的 n 值,也就是最邻近且等待被计算的 n 值,所以我们得到 A(0, 1) = 2 后,又不断出栈,直到找到那个最邻近且等待被计算的 n,并将 2 重新赋值给这个 n 。
然后重复入栈步骤,重复出栈步骤,直至得到最终结果。
比求解 F(n) 更复杂的是,求解 F(n) 只需要一次性入栈,一次性出栈,而求解 Ackerman 函数的入栈过程中也需要出栈操作。接下来我们接着之前的表格继续推算。
好的,现在已经算出结果了,A(2, 1) = 5 。
既然栈中的每个数据元素需要记录 m 和 n 的值,就不采用顺序栈而采用链栈了。
typedef int DataType;
typedef struct node
{
DataType m, n;
struct node* next;
} LinkStack;
我们再来用代码捋一遍思路。给定一个 A(m, n),然后入栈 / 出栈进行计算,假设现在计算进行到中途,栈中已经有了一些数据元素,此时 Top 指针指向数据元素 [ m = M, n = N ] ,那么我们接下来怎么继续计算呢,是入栈还是出栈呢?
这取决于 Top 指针指向的数据元素。
如果 M > 0 && N = 0,则将数据元素 [ m = M-1, n = 1 ] 入栈。
//A(m,n)=A(m-1,1)
if(Top->m>0 && Top->n==0)
{
LinkStack* p=(LinkStack*)malloc(sizeof(LinkStack));
p->m=Top->m-1;
p->n=1;
p->next=Top;
Top=p;
}
如果 M > 0 && N > 0,则将数据元素 [ m = M-1, n = -1 ] 和数据元素 [ m = M, n = N-1 ] 入栈,其中 [ m = M-1, n = -1 ] 是当前 Top 栈顶的 A(M, N) 所换算的 A(M-1, A(M, N-1)),而 A(M, N-1) 有待计算,还不能将该元素的 n 赋值为确切数值,所以先用 -1 做标记;另一个入栈的 [ m = M, n = N-1 ] 就是需要计算的 A(M, N-1),即 [ m = M-1, n = -1 ] 中的 n 值。
//A(m,n)=A(m-1,A(m,n-1))
if(Top->m>0 && Top->n>0)
{
LinkStack* p=(LinkStack*)malloc(sizeof(LinkStack));
LinkStack* q=(LinkStack*)malloc(sizeof(LinkStack));
p->m=Top->m-1;
p->n=-1;
q->m=Top->m;
q->n=Top->n - 1;
p->next=Top;
Top=p;
q->next=Top;
Top=q;
}
如果 M = 0 && N != -1 ,就可以计算当前 A(M, N) = N + 1,这个值其实是栈中所有待计算的 n 值中最邻近 Top 栈顶的 n 值然后开始出栈,记录这个值,然后开始不断出栈,直到找到那个 n,并将该值赋给它。
//A(m,n)=n+1
if (Top->m==0 && Top->n>=0)
{
DataType x=Top->n+1;
while(Top!=NULL && Top->n>=0)
{
//出栈
LinkStack* p=Top;
Top=Top->next;
free(p);
}
if(Top==NULL)
{
result=x;
break;
}
else if(Top->n<0)
{
Top->n=x;
}
}
完整的代码如下(代码中将入栈出栈的过程模拟地显示出来):
#include "stdio.h"
#include <stdlib.h>
typedef int DataType;
typedef struct node
{
DataType m, n;
struct node* next;
} LinkStack;
void Show(LinkStack* p)
{
printf("m:%d n:%d\n", p->m, p->n);
}
DataType Akm(DataType m, DataType n)
{
printf("以下模拟进出栈过程:")
LinkStack* Top = (LinkStack*)malloc(sizeof(LinkStack));
Top->m = m;
Top->n = n;
Top->next = NULL;
Show(Top);
DataType result = 0;
while (Top != NULL)
{
//akm(m,n)=n+1
if (Top->m == 0 && Top->n >= 0)
{
DataType x = Top->n + 1;
while (Top != NULL && Top->n >= 0)
{
//出栈
LinkStack* p = Top;
Top = Top->next;
free(p);
printf("pop\n");
}
if (Top == NULL)
{
result = x;
break;
}
else if (Top->n < 0)
{
Top->n = x;
}
}
//akm(m,n)=akm(m-1,1)
else if (Top->m > 0 && Top->n == 0)
{
LinkStack* p = (LinkStack*)malloc(sizeof(LinkStack));
p->m = Top->m - 1;
p->n = 1;
p->next = Top;
Top = p;
Show(Top);
}
//akm(m,n)=akm(m-1,akm(m,n-1))
else if (Top->m > 0 && Top->n > 0)
{
LinkStack* p = (LinkStack*)malloc(sizeof(LinkStack));
LinkStack* q = (LinkStack*)malloc(sizeof(LinkStack));
p->m = Top->m - 1;
p->n = -1;
q->m = Top->m;
q->n = Top->n - 1;
p->next = Top;
Top = p;
Show(Top);
q->next = Top;
Top = q;
Show(Top);
}
}
return result;
}
void main()
{
DataType r = Akm(2, 1);
printf("结果为:%d\n", r);
}