阿克曼函数实现(Java代码)

此文记录阿克曼函数的递归和非递归的实现,以及我对阿卡曼函数的认识。

阿克曼函数定义

Ackermann(m,n)函数定义如下:
Ackermann(0,n) = n+1;
Ackermann(m,0) = Ackermann(m-1,1);
Ackermann(m,n) = Ackermann(m-1,Ackermann(m,n-1)),m>0,n>0。

递归实现

思路:递归实现就很简单了,因为给出了表达式,确定边界m==0后,直接往上套。
代码:

    public static long solve_rec(long m,long n){
        if(m == 0){
            return n+1;
        }else if(m > 0 && n == 0){
            return solve_rec(m-1,1);
        }else{
            return solve_rec(m-1,solve_rec(m,n-1));
        }
    }

非递归实现

思路:用栈来模拟递归实现阿克曼函数求解。先往栈中放入需要求解的ack(m,n),判断m,n的状态来进行pop和push。说的很模糊,详情见代码。(思路来自该博客:https://blog.csdn.net/xiaofei_it/article/details/51524754)
举例:以计算Ackermann(2,1)为例,结合代码食用更加,ack为一个类,见后面详细代码。(关于图中的res可以没有描述清楚,以栈顶的res为每次操作的结果)
计算Ackermann(2,1)过程
代码:

    //思路:使用栈来模拟递归函数
    public static long solve(long m,long n){
        Stack<Ack> stack = new Stack<>();
        stack.push(new Ack(m,n));   //放入要求解的ack
        //res用来记录ack(0,n)的值
        //如果res大于0,则说明当前的函数层层调用(push)已经到底了,开始pop了。
        long res = -1;
        while(!stack.empty()){
            Ack ack = stack.peek(); //对栈顶的ack进行分析
            if(ack.m == 0){         //m为0,则求解出ack(0,n)的结果赋给res,并移出stack
                res = ack.n+1;
                stack.pop();
            }else if(ack.n == 0 && ack.m > 0){
                if(res < 0) {
                    stack.push(new Ack(ack.m-1,1)); //res小于0,Ackermann(m,0) = Ackermann(m-1,1);
                }
                else {
                    stack.pop();    //res大于0,则说明已经计算过了,可以pop
                }
            }else{
                if(ack.data < 0){   //还没有赋值,只有维
                    if(res < 0){
                        stack.push(new Ack(ack.m,ack.n-1)); //计算Ackermann(m,n)所需要的Ackermann(m,n-1)
                    }else {
                        ack.data = res; //设置data是为了判断是否被计算
                        res = -1;       //重置为1
                        stack.push(new Ack(ack.m-1,ack.data));//Ackermann(m,n) = Ackermann(m-1,Ackermann(m,n-1)),这里的Ackermann(m,n-1)为data
                    }
                }else{
                    stack.pop();    //已经计算出来的ack值被用到了,所以就pop出来
                }
            }
        }
        return res;
    }

详细代码

import java.util.Stack;

public class Ackermann{
    public static void main(String[] args) {
        System.out.println(solve(2,1));
        System.out.println(solve_rec(2,1));
    }
    
	//思路:递归方法
    public static long solve_rec(long m,long n){
        if(m == 0){
            return n+1;
        }else if(m > 0 && n == 0){
            return solve_rec(m-1,1);
        }else{
            return solve_rec(m-1,solve_rec(m,n-1));
        }
    }
    
    //思路:使用栈来模拟递归函数。有点难理解
    public static long solve(long m,long n){
        Stack<Ack> stack = new Stack<>();
        stack.push(new Ack(m,n));   //放入要求解的ack
        //res用来记录ack(0,n)的值
        //如果res大于0,则说明当前的函数层层调用(push)已经到底了,开始pop了。
        long res = -1;
        while(!stack.empty()){
            Ack ack = stack.peek(); //对栈顶的ack进行分析
            if(ack.m == 0){         //m为0,则求解出ack(0,n)的结果赋给res,并移出stack
                res = ack.n+1;
                stack.pop();
            }else if(ack.n == 0 && ack.m > 0){
                if(res < 0) {
                    stack.push(new Ack(ack.m-1,1)); //res小于0,Ackermann(m,0) = Ackermann(m-1,1);
                }
                else {
                    stack.pop();    //res大于0,则说明已经计算过了,可以pop
                }
            }else{
                if(ack.data < 0){   //还没有赋值,只有维
                    if(res < 0){
                        stack.push(new Ack(ack.m,ack.n-1)); //计算Ackermann(m,n)所需要的Ackermann(m,n-1)
                    }else {
                        ack.data = res; //设置data是为了判断是否被计算
                        res = -1;       //重置为1
                        stack.push(new Ack(ack.m-1,ack.data));//Ackermann(m,n) = Ackermann(m-1,Ackermann(m,n-1)),这里的Ackermann(m,n-1)为data
                    }
                }else{
                    stack.pop();    //已经计算出来的ack值被用到了,所以就pop出来
                }
            }
        }
        return res;
    }

}

class Ack{
    public long m;
    public long n;
    public long data;

    public Ack(long m, long n) {
        this.m = m;
        this.n = n;
        this.data = -1;
    }
}

扯淡

做这道算法题时,题目很简陋(就一个ack函数,且没有给出范围!!!),并不知道这是阿克曼函数,以前也没有听闻过(比较孤陋寡闻。。。)。有趣的就是,当我写好这个递归代码时,我随便用了(5,3)作为例子来跑一下。等了一下说是栈溢出,我就感觉可能是代码哪个地方写错了。我就测试了一下(2,1)和(2,5)两个例子发现没有问题。然后我就随便在一个地方加上了一个print,发现一直在循环,这就让我怀疑是不是代码写错了(因为我不是print中间的结果),我又检查了一遍逻辑发现没有错误啊。
然后我就百度了一下,才发现这是阿克曼函数,百度百科给出的一句话“阿克曼函数它的输出值增长速度非常快,仅是对于(4,3)的输出已大得不能准确计算。”当时我就震惊了(内心os:啥叫大的不能计算。我也没看出它有这么大),然后看到关于阿卡曼函数的科普,给我的感觉就是,真tm大。大概有这么大:
Ackermann( 0 , n ) = n + 1
Ackermann ( 1 , n ) = n + 2
Ackermann ( 2 , n ) = 2n + 3
Ackermann ( 3 , n ) = 2^(3+n) - 3
Ackermann ( 4 , 0 ) = 2^4 - 3
Ackermann ( 4 , 1 ) = 2 ^ (2 ^ 4) - 3 = 2^16 -3
Ackermann ( 4 , 2 ) = 2^ (2^ (2^ 4)) - 3 = 2^65536 - 3

要知道java中的long类型最大值也就2^64-1。
我不禁回想起斐波拉契数列的第5000项也不过1000多位数(计算Fibonacci的第5000项),而Ackermann(4,2)就有19729位数。两者完全不是一个数量级的。我还企图计算Ackermann(5,3),算它几个世纪未必算出来。还是要多读书多看报才能避免自己的无知。¬_¬

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章