androidstudio调用系统相机为什么resultcode一直返回0_函数递归调用?看这文就够了...
作者 | Cooper Song
責(zé)編 | Elle
出品 | 程序人生(ID:coder_life)
我猜,大多數(shù)程序員第一次接觸函數(shù)的遞歸調(diào)用都是在算斐波那契數(shù)列某項(xiàng)值的時候,這是函數(shù)遞歸調(diào)用最常見的應(yīng)用之一。規(guī)定第一項(xiàng)和第二項(xiàng)為1,后面的項(xiàng),每一項(xiàng)都是其前面兩項(xiàng)的和。
用公式表示就是f(n)=f(n-2)+f(n-1)。
而進(jìn)一步轉(zhuǎn)化,就是f(n)=[f(n-2-2)+f(n-2-1)]+[f(n-1-2)+f(n-1-1)]。
很明顯,這是一個遞歸的過程。
遞歸的優(yōu)點(diǎn)是算法簡單、容易理解,代碼行數(shù)少。
但遞歸也有缺點(diǎn),咱們將上面的f(n)再化簡一下就變成了f(n)=[f(n-4)+f(n-3)]+[f(n-3)+f(n-2)],可以看出,f(n-3)被計算了兩次,而f(n-4)+f(n-3)就是再計算f(n-2),又與最后一項(xiàng)f(n-2)是一樣的,f(n-2)也被重復(fù)計算了。因此,遞歸的一大缺點(diǎn)就是存在大量的重復(fù)計算,運(yùn)行起來浪費(fèi)時間也浪費(fèi)空間。
遞歸的另一個缺點(diǎn)是遞歸的層數(shù)不能太多(不能遞歸太深)。那遞歸得太深了會怎樣呢?答案是會爆棧。那么什么是爆棧呢?又是怎樣引發(fā)爆棧的呢?下面就要從最底層的角度講一講函數(shù)調(diào)用及函數(shù)遞歸調(diào)用的原理,相信讀完了就會找到答案。
這就要先從程序的鏈接和裝入說起了。
程序的鏈接(Link)
一個程序是由多個模塊構(gòu)成的,以C語言為例,有頭文件,只有引用了這個頭文件你才能使用scanf和printf;還有頭文件,只有引用了這個頭文件你才能直接調(diào)用strlen函數(shù)得到字符串的大小。所謂程序的鏈接,就是將整個程序的所有目標(biāo)模塊(比如程序員自己寫的頭文件和函數(shù))以及其他所需要的庫函數(shù)裝配成一個完整的裝入模塊。
原來每個模塊都有每個模塊的邏輯地址,經(jīng)過鏈接后,形成了統(tǒng)一的從0開始的邏輯地址,如下圖所示。
如何理解模塊?看上圖大概就有了概念,一個函數(shù)就是一個模塊。
程序的裝入(Load)
學(xué)過計算機(jī)組成原理的同學(xué)都知道,在計算機(jī)中有個部件叫程序計數(shù)器(Program Counter,簡稱PC),它存放的是程序要執(zhí)行的下一條指令的地址,CPU要到內(nèi)存當(dāng)中去取指令,取到CPU中進(jìn)行譯碼分析然后執(zhí)行。
程序原本存儲在磁盤上,因此只經(jīng)過鏈接還不能運(yùn)行,還需要裝入主存(內(nèi)存),CPU通過PC提供的線索到內(nèi)存中去取指令,如此循環(huán)往復(fù),程序才得以運(yùn)行下去。雖然程序的第一條指令的邏輯地址是0,但它裝入內(nèi)存時在內(nèi)存中的地址可不是0,因?yàn)閮?nèi)存中的低地址是留給系統(tǒng)使用的,也就是系統(tǒng)區(qū),比系統(tǒng)區(qū)的地址高的空間才是留給用戶使用的,也就是用戶區(qū)。雖然裝入內(nèi)存后其地址不再是從0開始,但其相對地址是不變的,將上面鏈接好的裝入模塊裝入內(nèi)存,內(nèi)存空間示意圖如下。
函數(shù)的調(diào)用
所謂函數(shù)的調(diào)用,就是程序原本在主模塊中順序執(zhí)行,遇到調(diào)用指令暫時到別的模塊執(zhí)行,在別的模塊執(zhí)行完后再返回主模塊的下一條指令繼續(xù)執(zhí)行,如下圖所示。
為什么可以執(zhí)行著執(zhí)行著就跳到別的模塊執(zhí)行了?又為什么在別的模塊執(zhí)行完了又回到原來的模塊執(zhí)行了呢?之所以能跳到別的模塊執(zhí)行,是因?yàn)楹瘮?shù)調(diào)用指令就指明了目標(biāo)模塊的首地址,將目標(biāo)模塊的首地址傳送給了程序計數(shù)器PC,就中斷了程序的順序執(zhí)行,然后進(jìn)入目標(biāo)模塊執(zhí)行。之所以執(zhí)行完子模塊還能回到主模塊中執(zhí)行,是因?yàn)閮?nèi)存中有一個專門實(shí)現(xiàn)函數(shù)調(diào)用的棧區(qū),在執(zhí)行調(diào)用指令的時候,就將主模塊調(diào)用指令之后的指令的地址入了棧,當(dāng)子模塊執(zhí)行到返回指令的時候,再出棧,將棧頂元素(也就是主模塊中要執(zhí)行的下一條指令的地址)傳給PC,程序的執(zhí)行就又回到了主模塊。
假設(shè)模塊A中的指令是:
add ax,bx ;本條指令的地址為10000
call B ;調(diào)用模塊B本條指令的地址為10001
mov dx,ax ;本條指令的地址為10002
假設(shè)模塊B中的指令是:
sub cx,dx ;本條指令的地址為15000
mov bx,cx ;本條指令的地址為15001
ret ;本條指令的地址為15002
模塊A為主模塊,模塊B為目標(biāo)模塊,在執(zhí)行call B指令的時候,函數(shù)調(diào)用棧區(qū)示意圖如下(左邊為調(diào)用前,右邊為調(diào)用后),SP為棧頂指針。
執(zhí)行完call B,就開始在模塊B中執(zhí)行,一直執(zhí)行到ret返回指令,此時函數(shù)調(diào)用棧區(qū)示意圖如下(左邊為返回后,右邊為返回前)。
執(zhí)行完ret返回指令,將棧頂元素出棧送給程序計數(shù)器PC以供CPU繼續(xù)執(zhí)行主模塊A中的剩余指令。
實(shí)際上,函數(shù)調(diào)用時入棧保護(hù)的不僅僅有主模塊中調(diào)用指令之后的指令的地址,還有一些變量或者說數(shù)據(jù),每個函數(shù)都有每個函數(shù)的局部變量,在主函數(shù)中調(diào)用子函數(shù),主函數(shù)中的局部變量必須入棧保護(hù),否則就會丟失。比如下面這個例子:
int add(int x,int y){
int a=x+1;
int b=y+1;
int c=a+b;
return c;
}
int main
{
int a=1,b=2;
int c=add(a,b);
printf(“%d+%d=%d ”,a,b,c);
return 0;
}
主函數(shù)和add函數(shù)里都有變量a和b,執(zhí)行完add函數(shù)再返回到主函數(shù)中a的值必須還為1,b的值必須還為2,因此可以在調(diào)用add函數(shù)前先將主函數(shù)的所有變量(a和b)入棧保護(hù),待執(zhí)行完返回主函數(shù)時再出棧送給變量a和變量b。
遞歸函數(shù)的調(diào)用
遞歸函數(shù)的調(diào)用本質(zhì)上也是函數(shù)的調(diào)用,只不過是自己在調(diào)用自己罷了。
以求斐波那契數(shù)列的項(xiàng)為例:
int fibonacci(int n){
if(n==1||n==2) //假設(shè)本條指令的地址為10000
return 1; //假設(shè)本條指令的地址為10001
int a=fibonacci(n-2); //假設(shè)本條指令的地址為10002
int b=fibonacci(n-1); //假設(shè)本條指令的地址為10003
int c=a+b; //假設(shè)本條指令的地址為10004
return c; //假設(shè)本條指令的地址為10005
}
如果進(jìn)入函數(shù)的n是1或者是2,那么就直接返回1;
否則,就繼續(xù)遞歸下去。
假設(shè)主函數(shù)調(diào)用斐波那契函數(shù)的指令的地址為15000,其下一條指令的地址為15001。
假設(shè)我們要求斐波那契數(shù)列的第5項(xiàng),公式為
f(5)=f(3)+f(4)=[f(1)+f(2)]+[f(2)+f(3)]
=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]
函數(shù)調(diào)用棧的示意圖如下。
第一步,從主函數(shù)中進(jìn)入斐波那契函數(shù),傳入的n為5。
第二步,斐波那契函數(shù)中執(zhí)行到int a=fibonacci(n-2),將下一條指令的地址壓入棧,也就是將10003入棧,此時的n=5,將n=5壓入數(shù)據(jù)棧,傳入的n=3。
第三步,斐波那契函數(shù)中執(zhí)行到int a=fibonacci(n-2),將下一條指令的地址壓入棧,也就是將10003入棧,此時的n=3,將n=3壓入數(shù)據(jù)棧,傳入的n=1。
第四步,此時n=1,可以直接返回1給上層的斐波那契函數(shù)的a,返回的同時出棧10003給程序計數(shù)器PC,出棧n=3給上一層斐波那契函數(shù)的n,回到上層的斐波那契函數(shù)。
第五步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10003的指令),也就是執(zhí)行int b=fibonacci(n-1),是一個函數(shù)調(diào)用,將下一條指令的地址壓入棧,也就是將10004入棧,此時n=3,將n=3壓入數(shù)據(jù)棧,此時a=1,將a=1壓入數(shù)據(jù)棧,傳入的n=2。
第六步,此時n=2,可以直接返回1給上層斐波那契函數(shù)的b,返回的同時出棧10004給程序計數(shù)器PC,出棧n=3給上一層斐波那契函數(shù)的n,出棧a=1給上一層斐波那契函數(shù)的a,回到了上層的斐波那契函數(shù)。
第七步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10004的指令),也就是執(zhí)行int c=a+b,然后順序執(zhí)行一直到返回,返回2給上一層斐波那契函數(shù)的a,返回的同時出棧10003給程序計數(shù)器PC,出棧n=5給上一層的斐波那契函數(shù)的n,回到上層的斐波那契函數(shù)。
f(5)=f(3)+f(4)=[f(1)+f(2)]+[f(2)+f(3)]
=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]
此時紅色部分已通過遞歸計算完成。
第八步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10003的指令),也就是執(zhí)行int b=fibonacci(n-1),是一個函數(shù)調(diào)用,將下一條指令的地址壓入棧,也就是將10004入棧,此時n=5,將n=5壓入數(shù)據(jù)棧,此時a=2,將a=2壓入數(shù)據(jù)棧,傳入的n=4。
第九步,斐波那契函數(shù)中執(zhí)行到int a=fibonacci(n-2),將下一條指令的地址壓入棧,也就是將10003入棧,此時的n=4,將n=4壓入數(shù)據(jù)棧,傳入的n=2。
第十步,此時n=2,可以直接返回1給上層的斐波那契函數(shù)的a,返回的同時出棧10003給程序計數(shù)器PC,出棧n=4給上一層斐波那契函數(shù)的n,回到上層的斐波那契函數(shù)。
第十一步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10003的指令),也就是執(zhí)行int b=fibonacci(n-1),是一個函數(shù)調(diào)用,將下一條指令的地址壓入棧,也就是將10004入棧,此時n=4,將n=4壓入數(shù)據(jù)棧,此時a=1,將a=1壓入數(shù)據(jù)棧,傳入的n=3。
第十一步,斐波那契函數(shù)中執(zhí)行到int a=fibonacci(n-2),將下一條指令的地址壓入棧,也就是將10003入棧,此時的n=3,將n=3壓入數(shù)據(jù)棧,傳入的n=1。
第十二步,此時n=1,可以直接返回1給上層的斐波那契函數(shù)的a,返回的同時出棧10003給程序計數(shù)器PC,出棧n=3給上一層斐波那契函數(shù)的n,回到上層的斐波那契函數(shù)。
第十三步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10003的指令),也就是執(zhí)行int b=fibonacci(n-1),是一個函數(shù)調(diào)用,將下一條指令的地址壓入棧,此時n=3,將n=3壓入數(shù)據(jù)棧,此時a=1,將a=1壓入數(shù)據(jù)棧,傳入的n=2。
第十四步,此時n=2,可以直接返回1給上層的斐波那契函數(shù)的b,返回的同時出棧10004給程序計數(shù)器PC,出棧n=3給上一層斐波那契函數(shù)的n,出棧a=1給上一層斐波那契函數(shù)的a,回到上層的斐波那契函數(shù)。
第十五步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10004的指令),也就是執(zhí)行int c=a+b,然后順序執(zhí)行一直到返回,返回2給上層斐波那契函數(shù)的b,返回的同時出棧10004給程序計數(shù)器PC,出棧n=4給上一層的斐波那契函數(shù)的n,回到上層的斐波那契函數(shù)。
第十六步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10004的指令),也就是執(zhí)行int c=a+b,然后順序執(zhí)行一直到返回,返回3給上層斐波那契函數(shù)的b,返回的同時出棧10004給程序計數(shù)器PC,出棧n=5給上一層的斐波那契函數(shù)的n,出棧a=2給上一層的斐波那契函數(shù)的a,回到上層的斐波那契函數(shù)。
f(5)=f(3)+f(4)=[f(1)+f(2)]+[f(2)+f(3)]
=[f(1)+f(2)]+[f(2)+[f(1)+f(2)]]
此時紅色部分已通過遞歸計算完成。
第十七步,執(zhí)行程序計數(shù)器PC指向的指令(內(nèi)存地址為10004的指令),也就是執(zhí)行int c=a+b,然后順序執(zhí)行一直到返回,返回5給上層斐波那契函數(shù)的接收者,返回的同時出棧15001給程序計數(shù)器PC,出棧主函數(shù)中的數(shù)據(jù)(未體現(xiàn)在圖中),回到主函數(shù)。
此時斐波那契第五項(xiàng)計算完成。
后記
到了揭曉為什么會爆棧的時刻了,內(nèi)存中實(shí)現(xiàn)函數(shù)調(diào)用的棧區(qū)的大小是有限的,如果遞歸層數(shù)太深,入棧的內(nèi)容越來越多,甚至出現(xiàn)只入棧不出棧的情況(還沒有符合返回條件執(zhí)行到返回指令棧就滿了),如此進(jìn)行下去,棧滿、棧溢出、爆棧只是時間問題,因此在實(shí)際項(xiàng)目應(yīng)用中,如果不能估算出遞歸的深度,函數(shù)遞歸就要慎用了。
本文雖以斐波那契數(shù)列為例介紹函數(shù)遞歸調(diào)用的底層原理,但在真正的面試中如果面試官問到了斐波那契數(shù)列相關(guān)的問題,還是不要給面試官回答一個遞歸的解法,原因之一就是當(dāng)n非常大的時候容易爆棧,原因之二就是文章開頭說的會產(chǎn)生大量的重復(fù)計算。在這里我給大家再提一種解法,就是動態(tài)規(guī)劃(DP)解法。不要一看到動態(tài)規(guī)劃就害怕,斐波那契數(shù)列的動態(tài)規(guī)劃解法還是很好理解的。先開一個大一些的數(shù)組f。
int fibonacci(int n){
f[1]=1,f[2]=1;
for(int i=3;i<=n;i++)
{
f[i]=f[i-2]+f[i-1];
}
return f[n];
}
這樣無非是把遞歸變成了循環(huán),但優(yōu)點(diǎn)是不會出現(xiàn)重復(fù)計算。
簡單的遞歸實(shí)現(xiàn)求斐波那契數(shù)列項(xiàng)的算法底層之復(fù)雜是我沒有想象到的,直到一張圖一張圖親手畫出來我才大吃一驚,在這里我要感謝底層硬件工程師的辛勤付出,沒有他們?yōu)槲覀儾季€鋪路,我們是無法使用高級語言輕松編程的。
本文的介紹本著一切從簡、方便理解的原則,可能有些地方與實(shí)際情況有出入,但是基本思想是一樣的。如有不當(dāng)之處,還請大家批評指正。
總結(jié)
以上是生活随笔為你收集整理的androidstudio调用系统相机为什么resultcode一直返回0_函数递归调用?看这文就够了...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux不显示无线网卡驱动安装失败,L
- 下一篇: [Deepin - Pycharm调试记