用三角數字問題說明遞歸
Int triangle(int n)
{
if(n ==1) //基值條件
return 1;
else
return(n + triangle(n-1)); //遞歸調用自身
}
說明:導致遞歸的方法返回而沒有再一次進行遞歸調用,這稱爲基值情況。
從上面可以看出遞歸方法的特徵:
1, 調用自身
2, 當它調用自身的時候,它這樣做是爲了解決更小的問題。
3, 存在某個足夠簡單的問題的層次,在這一層算法不需要調用自己就可以直接解答,且返回結果。
遞歸的效率問題:調用一個方法總要有一定的額外開銷。控制必須從這個調用的位置轉移到這個方法的開始處。而且,傳給這個方法的參數以及這個方法的返回的地址都要被壓入一個內部的棧裏。正因此,遞歸降低了執行效率。另外,中間參數以及返回值要佔內存,如果數據量大的話,可能會造成棧溢出。
遞歸的好處就在於它從概念上簡化了問題。
計算階乘也是一個遞歸的經典例子,另外找兩個數的最大公約數、求一個數的乘方等很多數學問題都可以用遞歸的思想來解決。
用遞歸方法解決變位字問題
假設要列出一個指定單詞的所有變位字,也就是列出該詞的全排列,它們都是由原來單詞的字母組成。我們稱這個工作是變位一個單詞或稱全排列一個單詞。
解決思想:假設這個詞有n個字母。
1, 全排列最右邊的n-1個字母。
2, 輪換所有n個字母。
3, 重複以上步驟n次。
“輪換”這個詞意味着所有的字母向左移一位,最左邊的字母輪換至最右邊字母的後邊。輪換單詞n次以給每個字母排在開頭的機會。當選定字母佔據第一個位置時,所有其他字母被全排列。如何來全排列最右邊的n-1個字母呢?通過遞歸,即調用方法自身。每調用一次自身,全排列的字母數減少1,當詞的大小隻剩一個字母時即出現基值條件,方法返回。
public static void doAnagram(int newSize)
{
if(newSize == 1) //if too small,go no further
return;
for (int j=0; j<newSize; j++) //for each position
{
doAnagram(newSize - 1); //anagram remaining
if (newSize == 2) //if innermost,
{
displayWord(); //display it
}
rotate(newSize); //rotate word
}
}
遞歸的二分查找
用最少的比較次數在一個有序的數組中找到給定的一個數據項。二分查找的方法是把數組從中間分爲兩半,然後看要查找的數據項在數組的哪一半,再次地折半,如此進行下去。下面是此方法的遞歸實現代碼:
private int recFind(long searchKey,int lowerBound,int upperBound)
{
int curIn;
curIn = (lowerBound + upperBound) / 2;
if(a[curIn]==searchKey)
return curIn; //find it
else //can't find it
{
if (a[curIn] < searchKey) //it's in upper half
return recFind(searchKey,curIn+1,upperBound);
else //it's in lower half
return recFind(searchKey,lowerBound,curIn-1);
} //end else divide range
} //end recFind
二分查找是分治算法的一個例子。把一個大問題分成兩個相對來說更小的問題,並且分別解決每一個小問題。對於每個小問題的解決方法也是一樣的,每個小問題又分成兩個更小的問題。一直持續下去直到達到易於求解的基值情況,就不用再分了。
分治算法通常是一個方法,在這個方法中含有兩個對自身的遞歸調用,分別對應於問題的兩個部分。在二分查找中,有兩個這樣的調用,不過只有一個真的執行。後面的歸併排序是真正執行了兩個遞歸調用。
歸併排序
歸併算法的中心是歸併兩個已經有序的數組。歸併兩個有序的數組A和B,就生成了第三個數組C,數組C包含數組A和B的所有數據項,並且使它們有序的排列在數組C中。做法就是用三個while循環,第一個循環沿A和B走,比較它們的數據項,並且複製它們中較小的數據項到數組C。後面兩個循環分別對應B的數據項已經全部移出,而C中還有剩餘元素的情況,和B中還有剩餘,C已經全部移出的情況。循環就是把數組中剩餘的數據項複製到C中。
歸併排序的思想是把一個數組分成兩半,排序每一半,然後用歸併方法把數組的兩半歸併成一個有序的數組。而爲每一半進行排序,就用遞歸。即把每個一半都分成兩個四分之一,對每個四分之一部分排序,然後把它們歸併成一個有序的一半。
歸併排序的基值條件,就是當發現mergeSort()方法發現只有一個數據項的數組時,它就返回。把這兩個數據項歸併到一個有兩個數據項的數組中。還要建一個工作空間數組,,它和初始數組一樣大小。歸併得到的數組存儲到工作空間數組中,每一次歸併完成之後,工作數組的內容被複制回原來的數組中。
歸併排序的運行時間是O(N*logN)
下面是歸併排序的完整代碼:
{
private static long[] theArray;
private static int nElems;
public static void display()
{
for(int j=0;j<nElems;j++)
System.out.print(theArray[j] + " ");
System.out.println("");
}
public static void mergeSort()
{
long[] workSpace = new long[nElems]; //provides workspace
recMergeSort(workSpace,0,nElems-1);
}
private static void recMergeSort(long[] workSpace,int lowerBound,int upperBound)
{
if(lowerBound == upperBound) //if range is 1,no use sorting
return;
else
{
int mid = (lowerBound+upperBound)/2; //find midpoint
recMergeSort(workSpace,lowerBound,mid); //sort low half
recMergeSort(workSpace,mid+1,upperBound); //sort high half
merge(workSpace,lowerBound,mid+1,upperBound); //merge them
}
}
private static void merge(long[] workSpace,int lowPtr,int highPtr,int upperBound)
{
int j=0; //workspace index
int lowerBound = lowPtr; //因爲隨着歸併的進行,lowPtr會變化,lowerBound變量用於記住該變量,
//以備將歸併後的數據拷貝回原數組時使用
int mid = highPtr-1;
int n = upperBound-lowerBound+1; //# of items
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++];
}
for (j=0; j<n; j++)
{
theArray[lowerBound+j] = workSpace[j];
}
}
public static void main(String[] args)
{
theArray = new long[]{64,21,33,70,12,85,44,3,99,0,108,36};
nElems=12;
display(); //display items
mergeSort(); //merge sort the array
display(); //display again
}
}
消除遞歸:遞歸和棧有緊密的聯繫,大部分編譯器都是使用棧來實現遞歸的。可以用棧實現把遞歸算法轉換成非遞歸的算法。
遞歸應用
求一個數的乘方:基於x的y次方等於x*x的y/2次方這個原理
揹包問題:從選擇第一個數據項開始,剩餘的數據項的加和必須符合揹包的目標重量減去第一個數據項的重量;這是一個新的目標重量。逐個試每種數據項組合的可能性,如果沒有組合合適的話,放棄第一個數據項,並且從第二個數據項開始再重複整個過程。依次進行下去。
組合問題:從n個人中選出k個組隊,有多少種組合?(n,k)= (n-1, k-1) + (n, k-1)