阿克曼函數實現(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),算它幾個世紀未必算出來。還是要多讀書多看報才能避免自己的無知。¬_¬

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