日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

尾递归及快排尾递归优化

發布時間:2024/2/28 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 尾递归及快排尾递归优化 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

尾遞歸

概念

如果一個函數中所有遞歸形式的調用都出現在函數的末尾,我們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最后執行的語句且它的返回值不屬于表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸函數的特點是在回歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的代碼。

原理

編譯器檢測到一個函數調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去創建一個新的。編譯器可以做到這點,因為遞歸調用是當前活躍期內最后一條待執行的語句,于是當這個調用返回時棧幀中并沒有其他事情可做,因此也就沒有保存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新添加一個,這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。

?

內存中的棧

計算機系統中,棧是一個具有以上屬性的動態內存區域(雖然與數據結構中的棧有區別,但是它們的思想都是先進后出)。程序可以將數據壓入棧中,也可以將數據從棧頂彈出。在i386機器中,棧頂由稱為esp的寄存器進行定位。壓棧的操作使得棧頂的地址減小,彈出的操作使得棧頂的地址增大。棧在程序的運行中有著舉足輕重的作用。最重要的是棧保存了一個函數調用時所需要的維護信息,這常常稱之為堆棧幀或者活動記錄堆棧幀一般包含如下幾方面的信息:

(1)函數的返回地址和參數

(2)臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其他臨時變量。

?

棧在函數調用過程中的工作原理

int?main() {foo1();foo2();return 0; }

上面是一個簡單的示例代碼,現在簡單模擬一下這個 main 函數調用的整個過程,$ 字符用于表示占地:

(1)建立一個函數棧。 $

(2)main 函數調用,將 main 函數壓進函數棧里面。$ [main]

(3)做完了一些操作以后,調用 foo1 函數,foo1 函數入棧。$ [main]?[foo1]

(4)foo1 函數返回并出棧。$ [main]

(5)做完一些操作以后,調用 foo2 函數,foo2 函數入棧。$ [main]?[foo2]

(6)foo2 函數返回并出棧。$ [main]

(7)做完余下的操作以后,main函數返回并出棧。$

上面這個過程說明了棧的作用。就是第 4 和第 6 步,讓 foo1 和 foo2 函數執行完了以后能夠在回到 main 函數調用 foo1 和 foo2 原來的地方。這就是棧,這種"先進后出"的數據結構的意義所在。

?

尾遞歸實例

尾遞歸,要先從遞歸講起。最簡單的例子——階乘。

以下是一個用線性遞歸寫的計算 n 的階乘的函數:

int fact(int n) //線性遞歸 {if (n < 0)return 0;else if(n == 0 || n == 1)return 1;elsereturn n * fact(n - 1); }

普通遞歸的問題在于展開的時候會產生非常大的中間緩存,而每一層的中間緩存都會占用寶貴的棧的空間,所導致了當這個 n 很大的時候,棧上空間不足則會產生"爆棧"的情況。

當n=5時,線性遞歸的遞歸過程如下:

fact(5) {5*fact(4)} {5*{4*fact(3)}} {5*{4*{3*fact(2)}}} {5*{4*{3*{2*fact(1)}}}} {5*{4*{3*{2*1}}}} {5*{4*{3*2}}} {5*{4*6}} {5*24} 120

?

n 的階乘的尾遞歸函數:

int facttail(int n, int a) //尾遞歸 {if (n < 0)return 0;else if (n == 0)return 1;else if (n == 1)return a;elsereturn facttail(n - 1, n * a); }

當n=5時,尾遞歸的遞歸過程如下:

facttail(5,1) facttail(4,5) facttail(3,20) facttail(2,60) facttail(1,120) 120

?

誤區

跟上面的普通遞歸函數比起來,貌似尾遞歸函數因為在展開的過程中計算并且緩存了結果,使得并不會像普通遞歸函數那樣展開出非常龐大的中間結果,所以不會爆棧?答案:當然不是!尾遞歸函數依然還是遞歸函數,如果不優化依然跟普通遞歸函數一樣會爆棧,該展開多少層依舊是展開多少層。不會爆棧是因為語言的編譯器或者解釋器所做了"尾遞歸優化",才讓它不會爆棧的。

?

階乘函數及gdb調試

將上述2個階乘代碼進行編譯,并對兩種方法進行調試,觀察在程序運行過程中棧幀的使用情況以及程序的運行情況。以下會使用的gdb調試命令:

編譯:gcc/g++ test.c -g -o test 運行:gdb test list+行號 查看程序指定行附近的代碼 b +行號 在該行添加斷點 r ?????????? 運行程序 n ?????????? 逐步運行程序 bt?????????? 打印調用棧的使用情況 info frame?? 查看當前棧幀的情況

代碼:

#include <bits/stdc++.h> using namespace std; #define M 5int fact(int n) //線性遞歸 {if (n < 0)return 0;else if(n == 0 || n == 1)return 1;elsereturn n * fact(n - 1); }int facttail(int n, int a) //尾遞歸 {if (n < 0)return 0;else if (n == 0)return 1;else if (n == 1)return a;elsereturn facttail(n - 1, n * a); }int facttail1(int n, int a) //尾遞歸轉化為循環 {while(n > 0){a = n * a;n--;}return a; }int main() {//printf("%p", facttail);int a = fact(M);int b = facttail(M, 1);cout << "A:" << a <<endl;cout << "B:" << b <<endl; }

?

非尾遞歸階乘的調試情況:

(1)使用 b 設置斷點并運行

(2)使用 bt 命令查看棧的使用情況

(3)遞歸層層返回

?

尾遞歸階乘的調試情況:

上述的尾遞歸階乘函數并未優化,所以兩個階乘函數展開的層數還是一樣的。但是兩者還是有不一樣的地方,從上圖中可以看出,尾遞歸階乘函數在運行到最后時,它是直接返回相應的值。而非尾遞歸階乘函數是層層深入然后再一層層地返回,最后得到結果。在這一過程中可以使用info frame命令查看更為詳細的棧幀信息。

所有遞歸都能等效于循環+棧(例如:數據結構中的非遞歸前、中、后序遍歷),尾遞歸只是只是恰好是那種沒有找的最簡單的情況遞歸之所以能寫出比循環可讀性高的代碼是因為遞歸隱含了一個棧,而用循環實現的時候需要手動維護一個棧導致代碼長,但是尾遞歸恰好就是那個不需要這個棧的特殊情況,也就是說這個時候遞歸相對于循環完全沒有任何優勢了。對于無棧循環不能等效的遞歸函數,轉化成尾遞歸比轉化成有棧循環更難看并且還更慢。

?

快排尾遞歸優化及gdb調試

以下將使用兩種快排的方法,即尾遞歸優化的快排和普通快排。通過對兩種方法的調試,觀察程序運行過程中棧的使用情況。將尾遞歸優化成迭代的關鍵

1.代碼主體是根據基準值完成排序后再遞歸調用函數。

2.將參數 low 提取出來,使其成為迭代變量。

3.將原來函數的里面所代碼在一個 while (true) 里面。

4.遞歸終止的 return 不變,這里當low >= high時遞歸終止

代碼:

#include <stdio.h>int Partition(int a[], int low, int high) {int i,j,k,temp;i = low;j = high+1;k = a[low];while(1){while(a[++i] < k && i < j);while(a[--j] > k);if(i >= j) break;else{temp = a[i];a[i] = a[j];a[j] = temp;}}a[low] = a[j];a[j] = k;return j; }void QuickSort(int a[], int low, int high) {if(low < high){int q = Partition(a, low, high);QuickSort(a, low, q-1);QuickSort(a, q+1, high);} }void QuickSort1(int a[], int low, int high) {int pivotPos;while(low < high){pivotPos = Partition(a,low,high);QuickSort(a,low,pivotPos-1);low = pivotPos + 1;} }int main() {int i;int a[10] = {3,4,5,6,1,2,0,7,8,9};int b[10] = {3,4,5,6,1,2,0,7,8,9};QuickSort(a, 0, 9);QuickSort1(b, 0, 9);for(i = 0; i < 10; ++i){printf("[%d]", a[i]);}printf("\n");for(i = 0; i < 10; ++i){printf("[%d]", b[i]);}printf("\n");return 0; }

?

普通快排調試情況:

(1)使用 b 設置斷點并運行,在這一過程中注意參數 low 、high 和棧的變化

(2)運行過程中參數的變化以及棧最深的情況

?

尾遞歸優化的快排調試情況:

(1)使用 b 設置第42行代碼為斷點并運行,在這一過程中注意參數 low 、high 和棧的變化

(2)接下來都是逐步運行并觀察參數和棧的使用情況

(3)最后一步運行完,返回 main 函數

從上圖中可以明顯看出,尾遞歸優化的快排使用的棧空間很少,因為該方法使用迭代代替了遞歸操作。當數據量足夠大時,使用尾遞歸優化后,可以縮減堆棧的深度,由原來的O(n)縮減為O(logn)。

?

總結

關于尾遞歸的問題,網上有許多資料,但大多都是將問題敘述了一遍,也沒有提及優化的過程。百度百科中以階乘函數的尾遞歸為例向大家介紹了這個問題,但是結論中有一個表述是:可以減少棧的深度。(1)這個表述是有問題的,經過對代碼的調試(沒有進行優化),發現兩種階乘方法遞歸的深度是一樣的。(2)需要對代碼進行尾遞歸優化才能達到減少棧的深度的目的。如果發現類似的問題,建議大家調試相應的程序,查看棧的使用情況。

參考:https://zhuanlan.zhihu.com/p/36587160

總結

以上是生活随笔為你收集整理的尾递归及快排尾递归优化的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。