最近高等代數正好講到這里,此篇文章正好對所學知識做一個具體程序實踐。
設計算法時使用遞歸的思想是一個程序員的基本素質,遞歸可以把一個很龐大的問題轉化為規模縮小了的同類問題的子問題,通過這一思想,我們編程時運用遞歸可以使用很少的代碼來處理很大的問題。這篇文章將會講到遞歸算法的運用。
在數學中,很多數學函數都是由一個公式來表達的,比如 f(x) = 100x; 這個公式可以讓我們把長度米轉換成長度厘米。 有了這個公式,在程序中敲出一行代碼,寫出一個函數(function)來計算實在是太簡單方便了,就像這樣。
int convert(int m){
return 100*m;
}
我們就寫好了一個函數來進行米到厘米的單位換算。
但是有的時候,數學函數會以不太標準的形式來定義,比如這個函數,他滿足 f(0)=0而且 f(x) = 2f(x-1)+x; 從這個函數定義我們可以得出 f(1)=1;f(2)=3;等等。當一個函數用他自己來定義時就稱為這個函數是遞歸的。
通俗地講,就是從前有個山,山里有個廟,廟里有個老和尚再給小和尚講故事,講的是:從前有個山,山里有個廟,廟里有個老和尚再給小和尚講故事。。。。這就是遞歸。
好了說了這么多你們肯定還是一頭霧水,現在來實踐一下。
遞歸求階乘
剛開始學編程的同學一定會寫求階乘的函數,使用for循環或者while循環都可以,但是遞歸卻完全用不上這兩個循環。
public static int factorial(int a){
if (a==0 || a==1){
return 1;
}
return a*factorial(a-1);
}
上面的代碼就是遞歸求階乘的方法,a是需要傳入的參數,比如我們要求5的階乘就傳入5這樣factorial函數最終的返回值為120;
分析這段代碼,他的第3行到第五行處理了 基準情況(Base Case),在這個情況下,函數的值可以直接算出而不用求出遞歸。就像上文提到的函數f(x) = 2f(x-1)+x;如果沒有f(0)=0這個事實在數學上沒有意義一樣。 再編程中,如果沒有基準情況也是無意義的。第7行執行的是遞歸調用。
所以所,設計遞歸算法,需要包含以下兩個基本法則:
1、基準情形(Base Case),必須總要有某些基準的清醒,在這個情形中,不執行遞歸就能求解。
2、不斷推進(Making Progress),對于需要遞歸求解的情形,遞歸調用必須總能夠朝著一個基準情形推進。這樣的話,不斷推進到基準情形,到達基準情形的時候遞歸才會推出,得到返回值。
n階矩陣行列式的求解
有了剛在知識的鋪墊,現在我們可以動手寫一個程序來用遞歸計算n階矩陣的行列式了。
首先來看下二階矩陣的求法:
也就是說2×2矩陣的元素交叉相乘再想減即可求出行列式。
接下來是3階矩陣:
3×3矩陣求解中,選擇任意行或者列,在那一行/列中,移除那個元素所在的行和列比如選擇a11,則移除第一行和第一列,這樣矩陣就變成了2×2的,再按照剛才的方法求2×2矩陣的行列式即可。之后整個行或列的3個元素進行此類運算后相加就是3×3的行列式。
n x n矩陣:
n階矩陣就和3階矩陣求解的方法一樣了,使用3×3求解的方法,比如4階矩陣,將4階消除成3階,然后再變成2階來算。但是矩陣每上升一個維度,計算量就會擴大很多。
知道了n階矩陣行列式的計算思路后,就可以開始編寫算法了。
首先是數據結構設計,我們需要設計一個矩陣類來提供便利,這個類有兩個成員,一個二維數組,用來儲存矩陣,一個整數,來儲存矩陣的行數或列數(行列式必須是方矩陣才可以求解所以行列無所謂)。
以下是整個Matrix類的設計:
static class Matrix{
private int rowNum=0;
private int[][] array;
//Constructor
public Matrix(int rowNum){
this.rowNum = rowNum;
array = new int[rowNum][rowNum];
}
public Matrix(int[][] array){
this.array = array;
this.rowNum = array.length;
}
int counter = 0; //For add Element
public void addElement(int a){
array[(counter)/rowNum][counter%rowNum] = a;
counter++;
}
//Print the instance itself
public void printMat(){
for (int i=0;i < rowNum ;i++){
for (int j=0;j < rowNum ;j++){
System.out.print(array[i][j]+\t;);
}
System.out.println();
}
}
//Setter and Getter
public int getRowNum() {
return rowNum;
}
public int[][] getArray() {
return array;
}
public void setArray(int[][] array) {
this.array = array;
}
}
Matrix類中有兩個構造方法:傳入整數a會初始化一個axa大小的空矩陣,傳入一個二維數組的話即可根據二維初始化一個Matrix對象。
Matrix類中有一個方法比較特殊:addElement方法,通過不斷調用這個函數即可向一個Matrix實例進行有順序的負值,第一次調用則會更改第第一行第一列位置上的值,第二次調用則會更改第一行第二列上的值,以此類推。
接下來就是設計一個MatrixCalculator類的,這個類中的一個成員方法可以求出行列式,我命名他為getDet(); 在計算行列式的時候需要移除元素所在的行和列,生成一個減小了一個維度的矩陣,我們需要編寫一個方法來完成這個操作,我命名他為removeRowAndCol();還有一個方法,由于相加的時候會產生符號的改變,所以需要寫一個方法來計算矩陣中一個元素的cofactor,命名為getCofactor。
以下就是removeRowAndCol方法的代碼:傳入需要移除的行和列和一個Matrix對象,函數會返回消除了指定行和列的Matrix對象。
public static Matrix removeRowAndCol(int row,int col,Matrix mat){
int matRowNum = mat.getRowNum();
int[][] arr = mat.getArray();
Matrix matrix = new Matrix(matRowNum-1);
for (int i = 0;i < matRowNum; i++){
for (int j = 0 < matRowNum; j++){
if (i!=row && j!=col) {
matrix.addElement(arr[i][j]);
}
}
}
matrix.printMat();
return matrix;
}
以下是getCofactor方法:由于我的算法只會去遍歷矩陣第一列來進行求解,所以得到Cofactor的代碼就變得簡單很多。
public static int getCofactor(int colNum){
if (colNum%2 == 0){
return 1;
}else {
return -1;
}
}
接下來就是核心的遞歸求解行列式的算法了,先理一下思路,遞歸算法的兩個要素:基準情形,不斷推進。
對于n階矩陣什么是基準情形呢?就是矩陣被降為2×2維度的時候,直接返回交叉相乘的差即可,不斷推進,如果是一個4階矩陣,算法會先把4將為3×3矩陣,然后3×3再拆成3個2×2矩陣來達到基準情形來算出答案,就和我們手算行列式時用到的方法一樣,手算時候也遵循這一算法。
public static int getDet(Matrix targetMatrix){
//Base (Finally reduced to 2 x 2 Matrix)
if (targetMatrix.rowNum == 2){
int[][] arr = targetMatrix.getArray();
// a*d - b*c
return arr[0][0]*arr[1][1] - arr[0][1]*arr[1][0];
}
//MARK- Recursion: to reduce dimension
int det = 0;
int colNum = targetMatrix.rowNum;
for (int i = 0; < colNum;i++){
det+= (targetMatrix.getArray()[i][0])*getCofactor(i)*getDet(removeRowAndCol(i,0,targetMatrix));
}
return det;
}
只有不到20行代碼,但是卻可以解決nxn的矩陣,是不是很神奇,這就是遞歸的優勢,把一個很龐大的問題轉化為規模縮小了的同類問題的子問題來求解。n階矩陣最后被降為若干個2×2矩陣。
加上適當的輸出語句,來求解一個3階矩陣行列式試一下:
2 1 2
-1 5 21
13 -1 -17
Element: 2
Cofactor: 1
Removing Row 0 Col 0
A 2 x 2 Matrix is initialized
5 21
-1 -17
END State Reached
Det: -64
Element: -1
Cofactor: -1
Removing Row 1 Col 0
A 2 x 2 Matrix is initialized
1 2
-1 -17
END State Reached
Det: -15
Element: 13
Cofactor: 1
Removing Row 2 Col 0
A 2 x 2 Matrix is initialized
1 2
5 21
END State Reached
Det: 11
Result: 0
但是有一個問題需要注意,就是這個算法的復雜度。
算法復雜度
這個算法的復雜度為O(n!), 這意味著他的運行速度很慢,隨著問題規模的增長,時間會大幅度增長。在我的電腦上,計算3×3到7×7內規模的矩陣,電腦都可以秒算出來,但是如果是一個10×10的矩陣,電腦需要54秒鐘,到了11×11時間將會變得更長,下圖是這個算法隨著問題規模增長對運行時產生影響的曲線。
可以看出7×7矩陣需要遞歸517次,到了10×10需要大約260萬次遞歸運算才能得到結果。可見問題規模增長后時間的開銷是十分巨大的。
原文:http://miketech.it/recursion_algorithm/ 感謝此篇文章的作者!