此文記錄阿克曼函數的遞歸和非遞歸的實現,以及我對阿卡曼函數的認識。
阿克曼函數定義
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爲每次操作的結果)
代碼:
//思路:使用棧來模擬遞歸函數
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),算它幾個世紀未必算出來。還是要多讀書多看報才能避免自己的無知。¬_¬