Ⅰ、三角數字
首先我們來看一組數字:1,3,6,10,15,21.....,在這個數列中第n項是由n-1項加n得到的,這個序列中的數字稱為三角數字因為他們可以形象化地表示成一個三角形排列。如下圖
通過上面的圖首先我們就可以想到使用循環來查找第n項的數值,下面代碼就是從高度為n的列加到高度為1的列
int triangle(int n){ int total = 0; while(n>0){ total = total + n; --n; } return total; }
上面方法循環了n次,第一次加n,第二次加n-1,一直加到1從而可以算出第n項的值。
使用遞歸的思想查找第n項的值,此時我們將三角數字看做是第一列和剩余所有列的和,如下圖所示,可以寫出triangle()方法
int triangle(int n){ //1. return (n + sumAllColums(n-1)) //這里我們可以發現sumAllColums方法做的事情和triangle做的事情一模一樣 return (n + triangle(n-1)) //這里我們可以將上面步驟換為此步驟,從而得到求三角數字的遞歸算法 }
此時上面的遞歸代碼是不會終止的,所以我們的每一個遞歸代碼要有一個我們稱之為基值(base case)以防止無限遞歸導致程序崩潰,所以上面求三角函數代碼的基值就是1,
int triangle(int n){ if(n==1){ return 1; //求三角數字的基值 }else{ return (n+triangle(n-1)) ; //進行遞歸邏輯 } }
下圖表示了遞歸triangle方法的執行過程,假設傳入n=5,從5開始每次減1,不斷進入方法自身,直到減到1時,方法進行返回,直到返回到最外層。注意,在方法返回1之前,實際上同時有5個不同的triangle()方法實例存在,最外層傳入的參數是5,最內層傳入的參數是1
綜上我們可以總結出遞歸方法的特征:
- 調用自身
- 調用自身為了解決更小的問題
- 存在足夠簡單的層次,即上面說的基值
遞歸的效率:遞歸的過程中控制必須從調用的位置轉移到方法的開始處,除此之外這個方法的參數以及返回值會壓入到一個內部棧中,從而知道訪問的參數值和返回到哪里。所以遞歸效率較低,我們常常采用遞歸,是因為他從概念上簡化了問題,而不是它更有效率。
II、歸並排序
接下來我們講講與遞歸相關的排序算法,歸並排序:
首先這種排序在時間上更有效,時間復雜度為O(N*logN),如果排序數據項N為10000,那么一般的簡單排序N2就是100000000,而N*logN只是40000,意思就是若使用歸並排序需要40s,那么使用插入排序需要近28小時。歸並排序的缺點是需要在存儲器中有另一個大小等於被排序的數據項數目的數組,所以排序對數組的大小有一定的限制。
歸並算法的核心是歸並兩個已經有序的數組,我們假設數組A有4個數據項,數組B有6個數據項,他們要被歸並到C中,開始的時候C有10個空的存儲空間。下表顯示了歸並進行的必要的比較,每一次比較將較小的數據項復制到數組C中,表中B數組在第八步之后是空的,所以不需要再進行比較,直接將A數組復制到C中去即可。
接下來我們給出歸並的java代碼:
public int[] merge(int[] arrayA,int sizeA,int[] arrayB,int sizeB,int[] arrayC){ int aDex=0,bDex=0,cDex=0; //記錄三個數組當前的腳標 while(aDex<sizeA&&bDex<sizeB){ //此時還需進行比較 if(arrayA[aDex] < arrayB[bdex]) arrayC[cDex++] = arrayA[aDex++]; else arrayC[cDex++] = arrayA[bDex++]; } while(aDex<sizeA) //說明數組a中還有剩余數據,拷貝到數組c arrayC[cDex++] = arrayA[aDex++]; while(bDex<sizeB) //說明數組b中還有剩余數據,拷貝到數組c arrayC[cDex++] = arrayB[bDex++]; return arrayC; }
歸並排序簡單來講就是反復地將數組進行分割,利用遞歸的思想,直到子數組只含有一個數據項(基值),在歸並排序方法中每一次調用自身的時候排列都會被分成兩部分,並且每一次返回時都會把兩個較小的排列合並成一個更大的排列,接下來我們給出歸並排序的代碼:
public class DArray { private long[] theArray; //存儲數據的數組 private int nElems; //填充數據個數索引 public DArray(int max) { theArray = new long[max]; nElems = 0; } /***插入數據*/ public void insert(long value){ theArray[nElems++] = value; } /***歸並排序的方法*/ public void mergeSort() { long[] workSpace = new long[nElems]; recMergeSort(workSpace, 0, nElems - 1); } /** * 利用遞歸進行歸並排序 * @param workSpace 工作區 * @param lowerBound 歸並區域的起始索引 * @param upperBound 歸並區域的結束索引 */ private void recMergeSort(long[] workSpace, int lowerBound, int upperBound) { if (lowerBound == upperBound) { return; } else { //找到中間分界點 int mid = (lowerBound + upperBound) / 2; //首先遞歸調用自己將前半部分的數據歸並為有序 recMergeSort(workSpace, lowerBound, mid); //然后遞歸調用自己將后半部分的數據歸並為有序 recMergeSort(workSpace, mid + 1, upperBound); //調用歸並算法將上面歸並有序后的數據進行歸並 merge(workSpace, lowerBound, mid + 1, upperBound); } } /** * 歸並算法 * @param workSpace 工作區 * @param lowPtr 首段歸並區域的初始索引 * @param highPtr 末端歸並區域的初始索引 * @param upperBound 末端歸並區域的結束索引 */ private void merge(long[] workSpace, int lowPtr, int highPtr, int upperBound) { int j = 0; //工作區的index int lowerBound = lowPtr; //首段歸並區域的初始索引(復制到theArray的初始索引) int mid = highPtr - 1; //對應索引較小區域的結束位置索引 int n = upperBound - lowerBound + 1; //歸並的此段區域所含有的數據項的個數 //將對應范圍lowPtr到upperBound的數據復制到工作區 while (lowPtr <= mid && highPtr <= upperBound) { if (theArray[lowPtr] < theArray[highPtr]) { workSpace[j++] = theArray[lowPtr++]; } else { workSpace[j++] = theArray[highPtr++]; } } //將對應還未復制完的數據復制到工作區中 while (lowPtr <= mid) { workSpace[j++] = theArray[lowPtr++]; } while (highPtr <= upperBound) { workSpace[j++] = theArray[highPtr++]; } //此步驟相當於是將對象內的theArray數組變成lowPtr到upperBound局部有序,將歸並到工作區的數據放入theArray的對應位置 for (j = 0; j < n; j++) { theArray[lowerBound + j] = workSpace[j]; } } }
歸並的效率:(假設復制和比較是最耗時的操作)
復制次數:
上表中可以看出來當N為2的乘方的時候的操作次數,我們可以這樣來理解需要的復制次數,log2N表示將N對半分解為我們歸並的基值1的時候需要的步數,然后每一步我們都需要將N個數據項復制到我們的工作區,所以復制到工作區的次數就應該是N*log2N,這些數據復制到工作區之后還需要復制到原數組中所以復制次數會增加一倍
比較次數:
上圖中我們可以前面表示進行歸並的時候進行的最多和最少的比較次數,后表列舉出了包含8個數據項進行歸並排序的比較次數,對於八個數據項需要七次歸並的操作,對於每一次歸並最大比較次數是數據項減1,最小比較次數是數據項的一半,加在一起可算出歸並排序需要的比較次數在12到17之間。
至此關於遞歸的思想,和關於遞歸的歸並排序就結束了。