“遞歸”在C++中主要解決具有樹型特徵的算法或數據結構,遞歸的利用可以使算法或數據結構大大簡化,代碼簡潔明瞭,相同一個具有該特性的課題採用遞歸或其他算法,所要求的預定義及相應的結果都將不一樣,用了遞歸可能使用減少部份定義,代碼實現部份大大減少,一看便知。下面是一個從數據庫中取數的例子對比:
實現中所使用的數據結構(表結構)
序號 |
英文名 |
中文名 |
類型 |
說明 |
1 |
Id |
權限ID |
Int |
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
2 |
ParentId |
父權限ID |
Int |
用於指定父結點 |
3 |
Name |
權限名稱 |
Varchar(32) |
|
4 |
IdCode |
菜單項ID |
int |
權限與菜單項關聯 |
由數據結構可以看出,通過ParentId,實現權限的樹狀結構,來描述權限的層次關係,這是一個典型的樹型特徵的數據結構,採用遞歸可以簡化程序的實現過程,但通過實驗證明簡單的採用遞歸將導致性能上的不足,運行效果無法滿足用戶的基本操作,在實現遞歸算法的後面將描述本程序在實現遞歸中作了相應的處理。
1、通過對樹結點的記憶來實現假遞歸
DWORD dwFunction = 0; //功能ID HTREEITEM hItemLayer[5][2]; //用於保存當前正在操作的結點,用於回溯 int nIdCollection[5][2]; //保留父層結點的ID,用於識別下一個結點的父層所屬 // 設置樹根 hItemLayer[0][0] = m_treeOperatorPermission.InsertItem(_T("權限設置"),3,3); m_treeOperatorPermission.SetItemData (hItemLayer[0][0] , dwFunction); hItemLayer[0][1] = hItemLayer[0][0]; nIdCollection[0][0] = 0; //父層ID nIdCollection[0][1] = 0; //當前層ID
int nCurParentLay = 0; CADORecordset collection(&m_conn); //ADO對象,用於從數據庫取出記錄集 CString strSQLString("select id ,ParentId , Name , IdCode from tbl_function order by id , parentid"); if(collection.Open (strSQLString)) { int nCount = collection.GetRecordCount (); CString strFunctionName; for(int i = 0;i <nCount;i ++) { //從數據庫中取出結點數據 collection.GetFieldValue ("Name" , strFunctionName); int nId; int nParentId; collection.GetFieldValue ("Id" , nId); collection.GetFieldValue ("ParentId" , nParentId); do { //判斷其保留的父結點是否一致,用於判斷是否從當前插入子結點,還是從父結點插入子結點 if(nParentId == nIdCollection[nCurParentLay][0]) { //向父層插入子結點,並保留當前結點數據,用於回溯 hItemLayer[nCurParentLay][1] = m_treeOperatorPermission.InsertItem ((LPCTSTR)strFunctionName , 0 , 1 , hItemLayer[nCurParentLay][0]); nIdCollection[nCurParentLay][1] = nId; m_treeOperatorPermission.SetHalfChecked (hItemLayer[nCurParentLay][1]); dwFunction = nId; m_treeOperatorPermission.SetItemData (hItemLayer[nCurParentLay][1] , dwFunction); } else if(nParentId == nIdCollection[nCurParentLay][1]) { //在當前層建立子層 hItemLayer[nCurParentLay + 1][1] = m_treeOperatorPermission.InsertItem ((LPCTSTR)strFunctionName , 0 , 1 , hItemLayer[nCurParentLay][1]); hItemLayer[nCurParentLay + 1][0] = hItemLayer[nCurParentLay][1]; nIdCollection[nCurParentLay + 1][0] = nParentId; nIdCollection[nCurParentLay + 1][1] = nId; m_treeOperatorPermission.SetChecked (hItemLayer[nCurParentLay + 1][1] , FALSE); dwFunction = nId; m_treeOperatorPermission.SetItemData (hItemLayer[nCurParentLay + 1][1] , dwFunction); nCurParentLay ++; } else { //回溯,用於找到相匹配的父結點,便於插入結點 nCurParentLay --; continue; } break; }while(true); collection.MoveNext (); } m_treeOperatorPermission.Expand (hItemLayer[0][0] , TVE_EXPAND); } collection.Close (); m_treeOperatorPermission.ClearALLCheck (); return 0; |
點評:這種方法是通過狀態的方法來實現遞歸的變相方法,可以看出在代碼實現方面相當複雜,程序員必須詳細註明其實現過程,才能夠使其他程序員讀懂(當然註釋本來就是應該的,這裏所說的是如何讓其他程序更容易看懂代碼)。
本程序中採用保留從父結點到當前結點的路徑,用於回溯找到下一個結點的父結點,程序員是費盡心機,在他走過的足上做個標籤,便於他回去是可以認得路,也便於摸索下一條路時不會重複走同樣的一條分支(形成死循環)。
優點:該程序只用到一條檢索語句即實現權限樹的初始化,減少數據庫連接數,從而在性能上將會是最優,即實現最其本的數據操作。
缺點:在點評中已經說到,代碼的複雜性,給代碼隱患的存在帶來了很大的可能性,另外對數據也有一定的要求,必須符合一不的順序才能夠被正確執行。
2、遞歸算法的應用
long InitDefaultPermissionTree(int nParentId ,HTREEITEM hItem) { CString strSQLString; strSQLString.Format ("select id , name from tbl_function where parentid = %d" , nParentId); CADORecordset collection(&m_conn); if(collection.Open (strSQLString)) { //將所有數據取出 CArray <int , int > nIdArray; CArray <CString , CString> strNameArray; int nCount = collection.GetRecordCount (); for(int i = 0;i < nCount ;i ++) { int nId; CString strName; collection.GetFieldValue ("id" , nId); collection.GetFieldValue ("name" , strName); collection.MoveNext (); nIdArray.Add (nId); strNameArray.Add (strName); } collection.Close (); //將從數據庫中取出的數據插入到樹圖上 for(i = 0;i < nCount;i ++) { int nId = nIdArray.GetAt (i); HTREEITEM hSonItem = m_treeOperatorPermission.InsertItem (strNameArray.GetAt (i) , 0 , 0 , hItem); m_treeOperatorPermission.SetItemData (hSonItem , nId); //後面講述採用m_TreeDataMap(CMap<int , int & ,HTREEITEM, HTREEITEM&>)的目的 m_TreeDataMap.SetAt(nId , hSonItem); //對當前結點進行遞歸插入子結點數據 InitDefaultPermissionTree(nIdArray.GetAt (i) , hSonItem); } } return 0; } |
點評:在本程序中簡單地看去,只用了一個循環即完成數據的讀取與顯示(本程序採用兩個循環只是想減少由於遞歸而增加數據庫連接數),顯而易見,代碼清晰易懂。不需要太多的註釋便可明白其中的實現過程。
在實現過程中沒有象第一個例子的那樣具有相當多的輔助變量來幫助記憶樹的結構,這個實例由遞歸的特性來完成。
優點:簡潔明瞭,通俗易懂,最大的特點就是執行遞歸時對其實現的默認,這也是在編寫遞歸程序時應該具備的基本思想認識,不然程序員絕對想不到該算法是可以用遞歸來實現的。
缺點:第一例中已經說到的優點,其實也就是本例的缺點,遞歸所產生相應的出入棧操作及相當的其他數據(如數據庫連接數等)都將對程序的性能產生負面影響,特別對於層次較多的情況則更爲嚴重,當然對於非樹型特徵的不提倡採用遞歸的實現算法,如求1~100的累加時,雖然可以用遞歸算法可以實現,但它仍然可以用常規算法來實現,這裏就不提倡遞歸算法。
正常算法 Int Sum(int nMax) { int nSum = 0; for(int I = 1;I <= nMax;I ++) { nSum += I; } return nSum; } 遞歸算法 Int Sum(int nMax) { if(nMax > 0) { return Sum(nMax – 1) + nMax; } else { return 0; } } |
綜上所述,遞歸算法應該用於某些採用常規算法實現較爲困難、並且具有遞歸特徵的算法纔會採用遞歸算法,否則不建議變相應用遞歸算法,如後面所述的計算1~100的累加,這裏就是堅決否定遞歸算法的應用。
編寫代碼應該考慮多方面因素,包括代碼的可讀性、可理解性、簡單性(這個特性有一定的侷限性)、執行性能等因素決定。
對於一個性能要求不高但採用遞歸可以提高代碼的可讀性與可理解性並且可以大大簡化代碼的實現過程,遞歸將是首選。
對於執行性能要求較高時,可能要求程序員採用其他類似的算法來替代,確保性能優先,但部份情況,採用其他算法來替代遞歸未必能夠提高算法的性能,反而遞歸是最佳算法(一般指需要的遞歸層次較少)。
總之,使用遞歸有其利,也有其弊,程序員在實現過程中是否應該採用遞歸算法,應考慮採用遞歸算法是否會影響相關模塊或系統的整體要求。