遞迴在C++應用中的利與弊 (轉)

amyz發表於2007-10-31
遞迴在C++應用中的利與弊 (轉)[@more@]

“遞迴”在C++中主要解決具有樹型特徵的演算法或資料結構,遞迴的利用可以使演算法或資料結構大大簡化,程式碼簡潔明瞭,相同一個具有該特性的課題採用遞迴或其他演算法,所要求的預定義及相應的結果都將不一樣,用了遞迴可能使用減少部份定義,程式碼實現部份大大減少,一看便知。下面是一個從中取數的例子對比:

實現中所使用的資料結構(表結構)

序號

英文名

中文名

型別

說明

1

Id

ID

Int

 :namespace prefix = o ns = "urn:schemas--com::office" />

2

ParentId

父許可權ID

Int

用於指定父結點

3

Name

許可權名稱

Varchar(32)

 

4

IdCode

選單項ID

int

許可權與選單項關聯

由資料結構可以看出,透過ParentId,實現許可權的樹狀結構,來描述許可權的層次關係,這是一個典型的樹型特徵的資料結構,採用遞迴可以簡化的實現過程,但透過實驗證明簡單的採用遞迴將導致上的不足,執行效果無法滿足的基本操作,在實現遞迴演算法的後面將描述本程式在實現遞迴中作了相應的處理。

 

1、透過對樹結點的記憶來實現假遞迴

  D dwFunction = 0;  //功能ID

  HTREEITEM hItemLayer[5][2]; //用於儲存當前正在操作的結點,用於回溯

  int nIdCollection[5][2];  //保留父層結點的ID,用於識別下一個結點的父層所屬

  // 設定樹根

  hItemLayer[0][0] = m_treeOperatorPession.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 strString(" 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

  {

  //從資料庫中取出結點資料

  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   nIdArray;

  CArray   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)的目的

  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的累加,這裡就是堅決否定遞迴演算法的應用。

編寫程式碼應該考慮多方面因素,包括程式碼的可讀性、可理解性、簡單性(這個特性有一定的侷限性)、執行效能等因素決定。

對於一個效能要求不高但採用遞迴可以提高程式碼的可讀性與可理解性並且可以大大簡化程式碼的實現過程,遞迴將是首選。

對於執行效能要求較高時,可能要求程式設計師採用其他類似的演算法來替代,確保效能優先,但部份情況,採用其他演算法來替代遞迴未必能夠提高演算法的效能,反而遞迴是最佳演算法(一般指需要的遞迴層次較少)。

總之,使用遞迴有其利,也有其弊,程式設計師在實現過程中是否應該採用遞迴演算法,應考慮採用遞迴演算法是否會影響相關模組或的整體要求。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-980009/,如需轉載,請註明出處,否則將追究法律責任。

相關文章