如何使用栈非递归地求解Ackerman函数

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);
}

 

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值