阿克曼函数(ackerman)的递归与非递归实现
1.阿克曼函数的定义
若m=0,返回n+1
若m≠0且n=0,返回Ackermann(m-1,1)
若m≠0且n≠0,返回Ackermann(m-1,Ackermann(m,n-1))
2.递归实现
对于给出了分段形式的函数,使用递归仅仅只是将公式翻译成代码,不多讲,直接上代码:
long Ackerman(long m, long n) {
if (m == 0) {
return n + 1;
}
else if (m != 0 && n == 0) {
return Ackerman(m - 1, 1);
}
else {
return Ackerman(m - 1, Ackerman(m, n - 1));
}
}
3.非递归实现
其实当我们用纸笔简单模拟(2,1)的计算过程,就会知道这是一个不断进栈出栈的过程,只不过是两个数字同时进出,而且其中还会有一层等待计算的小递归藏在里面(如:要解决A(1,3),就得先解决A(0,A(1,2)))。这种后进先出的形式既可以用栈来实现,也可以用循环单链表来模拟。
3.1利用栈
观察表达式,显然只有 m = 0 的情况才能直接求得具体值,因此递归的出口一定是 m = 0 。
先分析不那么特殊的两种情况:
①n = 0
则( m − 1 , 1 )入栈。
②m ≠ 0 , n ≠ 0
由于n 的值也需要通过 Ack 函数求出,先标记为− 1,即待求值,将( m − 1 , − 1 )入栈;
然后将求 n的值的 Ack 函数入栈,即( m , n − 1 ) 入栈。
然后考虑m = 0 的情况:
由于m = 0 时,函数求得了具体值,先更新返回值ans为n + 1。
通过栈是否为空判断是中间过程还是递归出口,栈空则说明达到了递归出口。
①中间过程
返回值为之前讨论的“用==-1标记的待求的n==”,因此更新栈顶的n。
②递归出口
栈空,说明没有待更新、待求值的了,该返回值即最终结果。
代码如下:
//利用栈的非递归算法
#include<stack>
long Ackerman(long m, long n) {
struct Node {
int m;
int n;
Node(int m, int n): m(m), n(n) {};//构造函数
}node(m, n);//用m,n的值初始化node
stack<Node> s;//STL栈
s.push(node);//m,n压入栈中
int ans = -1;//返回值
while (!s.empty()) {
node = s.top();
s.pop();
if (node.m == 0) {
ans = node.n + 1;
if (s.empty()) { break; }//如果已经空了,就说明计算结束了
node = s.top(); //取出来
s.pop(); //取出来
node.n = ans; //修改n的值,使其变成-1,即标志为未计算
s.push(node);//更新栈顶
}
else if(node.n == 0) {
s.push(Node(node.m - 1, 1));
}
else {
s.push(Node(node.m - 1, -1));
s.push(Node(node.m, node.n - 1));
}
}
return ans;
}
3.2利用循环单链表
与栈思路相同,以链表的头指针作为栈顶, 每次入栈就是向链表头部插入一个元素,出栈就是从链表头部删除一个元素
具体实现如下:
//不利用栈的非递归算法
struct LNode {
int data; // 数据域
LNode* next; // 指针域
};
typedef LNode LNode; // LNode是一个循环单链表的结点
typedef LNode* LinkList; // LinkList是一个循环单链表
// 定义两个List 分别存储 m 和 n 的值.
LinkList m_ListHead;
LinkList n_ListHead;
// 初始化循环单链表
void InitList(LinkList& L) {
L = new LNode;
L->next = L;
}
// 判断循环单链表是否为空
bool Empty(LinkList& L) {
if (L->next == L) {
return true;
}
else {
return false;
}
}
// 使用头插法插入元素
void push(LinkList ListHead, long num) {
LinkList List_tmp;
List_tmp = new LNode;
List_tmp->data = num;
List_tmp->next = ListHead->next;
ListHead->next = List_tmp;
}
// 从头部删除元素
void pop(LinkList ListHead) {
LinkList List_tmp;
if (!Empty(ListHead)) {
List_tmp = ListHead->next;
ListHead->next = List_tmp->next;
free(List_tmp); // 释放元素
}
}
long Ackerman(long m, long n) {
InitList(m_ListHead);//初始化循环链表
InitList(n_ListHead);
push(m_ListHead, m); // 将 m 入栈
push(n_ListHead, n); // 将 n 入栈
while (!Empty(m_ListHead)) // 判断 m 链表是否为空
{
while (m != 0) {
if (n == 0) {
m = m - 1;
n = 1;
push(m_ListHead, m);
push(n_ListHead, n);
}
else {
n = n - 1;
push(m_ListHead, m - 1);
push(n_ListHead, -1);
}
}
n = n + 1;
while ((!Empty(m_ListHead)) && (n_ListHead->next->data != -1)) {
pop(m_ListHead);
pop(n_ListHead);
}
if ((!Empty(m_ListHead))) {
m = m_ListHead->next->data;
pop(n_ListHead);
push(n_ListHead, n);
}
}
return n;
}
4.总结
由于阿克曼函数中,每计算一个数,其都高度依赖前一个数计算的结果,
因此像是此类将递归转化为非递归实现的题目的思路,基本上是利用栈来实现。
参考文档:
Ackermann函数(阿克曼函数)的递归、非递归(手动栈模拟)
关于阿克曼函数(akermann)非递归算法的一点见解 c++
结构体struct node的构造函数
经典算法实现 15_阿克曼函数(非递归实现)