[PHP] PHPの再帰、効率、分析

再帰の定義

 

    再帰(http:/en.wikipedia.org/wiki/Recursive)は、関数がそれ自体を(直接的または間接的に)呼び出すためのメカニズムです。この強力なアイデアにより、いくつかの複雑な概念を非常に単純にすることができます。コンピュータサイエンス以外、特に数学では、再帰の概念は珍しいことではありません。例:再帰的説明に最も一般的に使用されるフィボナッチ数列は、非常に典型的な例です。クラス(n!)などの他のシーケンスも、再帰的定義(n!= n *(n-1)!)に変換できます。 )実生活でも再帰的思考はどこにでも見られます。たとえば、学問上の問題のために校長のスタンプが必要ですが、校長は「教育学部長がスタンプされた場合にのみスタンプをスタンプします」と言います。学部長、教える学部長はまた、「部門の学部長がスタンプを押した場合にのみスタンプを押します」と述べました...最終的にヘッドティーチャーを見つけるまで、ヘッドティーチャーの大胆なスタンプを取得した後、学部長、教育学部長、そして最後に校長シール、プロセスは次のとおりです。

 

  刻印された物語は面白くありませんが(その大学生活は悲しみとは何の関係もありませんか?私たちが悲しんでいない場合、どうすれば私たちが若いことを証明できますか)、それは再帰の基本的な考え方、つまり2つの基本的な条件を反映しています再帰の:

  1.再帰的終了条件は、再帰を正常に実行するための必要条件であり、再帰が正しく戻ることができるようにするための必要条件です。この条件がないと、システムによって指定されたリソースが使い果たされるまで(ほとんどの言語では、スタックスペースが使い果たされるまで)再帰が無期限に続きます。したがって、「スタックオーバーフロー」(C言語では、「 「スタックオーバーフロー」および「最大ネストレベル100に達しました」(PHPでは、再帰制限を超えています)は、ほとんどの場合、不適切な終了条件が原因であり、再帰の深さが過剰になるか、再帰が無限になります。2.再帰的プロセス。関数呼び出しから次の関数呼び出しへの再帰。例としてn!を取り上げます。n> 1の場合。N!= N *(N-1)!は再帰関数の再帰プロセスであり、単に「再帰式」と呼ぶこともできます。

これらの2つの基本的な条件を使用して、再帰の一般的なパターンを取得します。これは、コードで次のように記述できます。

function Recur(param){if(reach the baseCondition){Calu(); // calculate return;} //それ以外の場合は再帰的に実行しますparam = modify(param)/パラメーターを変更し、Recurを呼び出すために下位層に入る準備をします(パラメータ);}

再帰の一般的なパターンを使用すると、ほとんどの再帰関数を簡単に実装できます。例:よく言及されるフィボナッチ数列の再帰的実現、およびディレクトリへの再帰的アクセス:

function ScanDir($ path){if(is_dir($ path)){$ handler = opendir($ path); while($ dir = readdir($ handler)){if($ dir == '。' || $ dir == '..'){続行; } if(is_dir($ path。 "/"。$ dir)){ScanDir($ path。 "/"。$ dir。 "/"); } else {echo "file:"。$ path。 "/"。$ dir.PHP_EOL; }}}} ScanDir( "./");

注意深い学生は、表現の過程で「レイヤー」という用語を何度も使用していることに気付くかもしれません。主な理由は2つあります。

1.再帰を分析する過程で、人々はしばしば再帰ツリーの形式を使用して再帰関数の傾向を分析します。例としてフィボナッチ数列を取り上げます。最初に、フィボナッチ数列は次のように定義されます。

 

したがって、Fab(n)の値を取得するには、次の図に示すように、「再帰ツリー」の形式に展開する必要があることがよくあります。

 

再帰計算プロセスは、上から下、左から右で、再帰ツリーのリーフノードに到達すると(つまり、再帰終了条件)、レイヤーごとに返されます。次の図に示すように(参照URL:http:/www.csharpwin.com/csharpspace/12292r4006.shtml):

 

2.スタックの構造。

再帰に関連するもう1つの重要な概念はスタックです。Baidu百度百度のスタックの説明を借りると、「Windowsでは、スタックは、メモリの連続した領域である下位アドレス拡張されたデータ構造です。この文は、アドレスを意味します。スタックの最上位にあり、スタックの最大容量はシステムによって事前定義されています。WINDOWSでは、スタックのサイズは2Mです(1Mと言われることもあります。つまり、コンパイル時に決定される定数です)。 。要求されたスペースがスタックサイズを超える場合空きスペースがあるとオーバーフローが発生するため、スタックから取得できるスペースは小さくなります。「Linuxシステムでは、ulimit-sコマンドを使用してシステムの最大スタックサイズを表示します。スタックは「後入れ先出し」という特徴があります。つまり、最後にプッシュされた要素の優先度が最も高くなります。データがプッシュされるたびにスタックが積み重ねられ、データがフェッチされるとスタックがスタックされます。スタックの最上位から取得されます。データ。スタックを再帰に特に適したものにするのは、スタックのこの機能です。具体的には、再帰プログラムの実行時に、システムは定格サイズのスタックスペースを割り当て、各関数呼び出し(スタックフレームと呼ばれる)のパラメータ、ローカル変数、および関数リターンアドレスがスタックスペースにプッシュされます(必要に応じて「シーンに戻る」ように「シーンを保護する」と呼ばれます)、このレイヤーを再帰的に呼び出すたびに、無条件になります(無条件のスタックオーバーフロー攻撃が発生する可能性があるため、(http:/ wenku.baidu。com/view / 7fb00bc2d5bbfd0a7956737d.html  )以前に保存した戻りアドレスに戻って、コードの実行を続行します。このように、スタック構造は通常のプレートのスタックのようになります。

 

再帰の基本的な例として、以下を練習に使用できます。

 

1.ディレクトリの再帰的走査。

2.無制限の分類。

3.バイナリ検索とマージソート。

4.再帰的動作に関連するPHP組み込み関数(array_merge_recursive、array_walk_recursive、array_replace_recursiveなど、それらの実装を検討してください)

 

関数呼び出しの再帰スタックトレースを理解する

 

 

C言語では、GDBなどのデバッグツールを使用して関数呼び出しのスタックをトレースし、関数の実行プロセスを詳細に追跡できます(GDBの使用については、@左耳ゲームのブログをお勧めします:http: /blog.csdn.net/haoel/ article / details / 2879  )。

phpでは、使用できるデバッグ方法は次のとおりです。

1.ネイティブのprint、echo、var_dump、print_rなど、通常は単純なプログラムの場合、関数のキーポイントを出力するだけで済みます。

2. PHPの組み込みスタックトレース関数:debug_backtraceおよびdebug_print_backtrace。

3.xdebugやxhprofなどのデバッグツール。

理解を容易にするために、例としてフィボナッチ数列を取り上げます(ここでは、nは非負の数でなければならないと仮定します)。

function fab($ n){debug_print_backtrace(); if($ n == 1 || $ n == 0){return $ n;} return fab($ n-1)+ fab($ n-2);} fab(4);

 

印刷されたフィボナッチコールスタックは

[/search/nginx/html/test/Fab.php:10]で呼び出される#0 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出される#0 fab(3)

[/search/nginx/html/test/Fab.php:10]で呼び出される#1 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出される#0 fab(2)

[/search/nginx/html/test/Fab.php:8]で呼び出された#1 fab(3)

[/search/nginx/html/test/Fab.php:10]で呼び出される#2 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出された#0 fab(1)

[/search/nginx/html/test/Fab.php:8]で呼び出される#1 fab(2)

[/search/nginx/html/test/Fab.php:8]で呼び出される#2 fab(3)

[/search/nginx/html/test/Fab.php:10]で呼び出される#3 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出された#0 fab(0)

[/search/nginx/html/test/Fab.php:8]で呼び出される#1 fab(2)

[/search/nginx/html/test/Fab.php:8]で呼び出される#2 fab(3)

[/search/nginx/html/test/Fab.php:10]で呼び出される#3 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出された#0 fab(1)

[/search/nginx/html/test/Fab.php:8]で呼び出された#1 fab(3)

[/search/nginx/html/test/Fab.php:10]で呼び出される#2 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出される#0 fab(2)

[/search/nginx/html/test/Fab.php:10]で呼び出される#1 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出された#0 fab(1)

[/search/nginx/html/test/Fab.php:8]で呼び出される#1 fab(2)

[/search/nginx/html/test/Fab.php:10]で呼び出される#2 fab(4)

[/search/nginx/html/test/Fab.php:8]で呼び出された#0 fab(0)

[/search/nginx/html/test/Fab.php:8]で呼び出される#1 fab(2)

[/search/nginx/html/test/Fab.php:10]で呼び出される#2 fab(4)

 

この出力の混乱を一見すると、無知なようです。実際、上記の出力の各行には、次の項目が含まれています。

A.#0はスタックの最上位を意味し、#1はスタックフレームの第1層を意味し、#2はスタックフレームの第2層を意味するなど、スタックレベルは、数値が大きいほど深さが深くなります。スタックフレームの。

B.呼び出される関数とパラメーター。たとえば、fab(4)は、実際の実行関数がfab関数であることを示し、4は関数の実際のパラメーターを示します。

C.呼び出しの場所:ファイル名と実行された行数を含みます。

実際、出力情報を追加することで、関数の呼び出しスタックと計算プロセスをより明確に確認できます。たとえば、関数レベルの基本情報を追加します。

 

function fab($ n){echo“ -n = $ n ----------------------------”。PHP_EOL; debug_print_backtrace() ; if($ n == 1 || $ n == 0){return $ n;} return fab($ n-1)+ fab($ n-2);} fab(4);

次に、fab(4)を実行した後の呼び出しスタックは次のとおりです。

---- n = 4 ------------------------------------------- - 
#0 fab(4)が[/search/nginx/html/test/Fab.php:11]で呼び出されました
---- n = 3 ----------------- ---------------------------- 
#0 fab(3)が[/search/nginx/html/test/Fab.phpで呼び出されました: 9] 
[/search/nginx/html/test/Fab.php:11]で呼び出される#1 fab(4)
---- n = 2 ----------------- ---------------------------- 
#0 fab(2)が[/search/nginx/html/test/Fab.phpで呼び出されました: 9] 
[/search/nginx/html/test/Fab.php:9]で呼び出される#1 fab(3)[/ search / nginx / html / test / Fab.php:11]で呼び出される
#2 fab(4)
---- n = 1 ------------------------------------------- - 
#0 fab(1)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
#1 fab(2)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
[/search/nginx/html/test/Fab.php:9]で呼び出される#2 fab(3)[/ search / nginx / html / test / Fab.php:11]で呼び出される
#3 fab(4)
- --n = 0 --------------------------------------------- 
#0 fab(0)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
#1 fab(2)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
#2 [/search/nginx/html/test/Fab.php:9]で呼び出されるfab(3)
#3 [/search/nginx/html/test/Fab.php:11]で呼び出されるfab(4)
---- n = 1 --------------------------------------------- 
#0 [/search/nginx/html/test/Fab.php:9]で呼び出されるfab(1)
#1 [/search/nginx/html/test/Fab.php:9]で呼び出される#1 fab(3)
#2 fab( 4)[/ search / nginx / html / test / Fab.php:11]で呼び出されます
---- n = 2 ----------------------- ---------------------- 
#0 fab( 2)[/ search / nginx / html / test / Fab.php:9]で呼び出され
ます#1 fab(4)[/ search / nginx / html / test / Fab.php:11]で呼び出されます
---- n = 1 ------------------------------------------- - 
#0 fab(1)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
#1 fab(2)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
[/search/nginx/html/test/Fab.php:11]で呼び出される#2 fab(4)
---- n = 0 ------------------- -------------------------- 
#0 fab(0)が[/search/nginx/html/test/Fab.php:9]で呼び出されました
[/search/nginx/html/test/Fab.php:9]で呼び出される#1 fab(2)[/ search / nginx / html / test / Fab.php:11]で呼び出される
#2 fab(4)

 出力の説明(出力の最初の2列に注意してください):プログラムはfab(4)の値を計算する必要があるためです。fab(4)の値はfab(3)とfab(2)の値に依存するため、fab(4)の値を直接計算することはできません.1の1に対応するスタックにプッシュする必要があります下の図。fab(4)の左側のブランチはfab(3)であり、fab(3)の値を直接計算することはできません。したがって、下の図の2に対応するように、fab(3)もスタックにプッシュする必要があります。同じことがfab(2)にも当てはまります。再帰ツリーのリーフノードまでスタックにプッシュする必要もあります。次の図に示すように、リーフノードを計算した後、スタックが空になるまでスタックを順番に返します。

 パフォーマンス-再帰的効率分析

 

 

  昨日、Pu Lingの「Non-SimplicityNODE.js」を読んでいたときに、著者がさまざまな言語のパフォーマンスをテストしたときに与えられたテスト結果を見ました。大まかに:フィボナッチ数列の単純な再帰計算を通じて、さまざまな言語の計算パフォーマンスを大まかに評価するために、さまざまな言語の計算時間がテストされます。その中で、PHPの計算時間は私を驚かせました:n = 40の場合、フィボナッチ数列を計算するためにPHPが消費する時間は1分17秒728秒で、77.728秒であり、C言語の0.202秒よりはるかに悪いです。 。約380回!(テスト結果は下の図で見ることができます)

 

  ご存知のように、PHPコードの実行プロセスは、スキャンコード、字句解析、構文解析、およびその他のプロセスを通じて行われ、PHPプログラムは中間コード(Opcodeバイトコード)にコンパイルされてから、Zendコアエンジンによって実行されます。 PHPこれは、C言語に基づく高級言語の実装です。このように、PHPのコンパイルプロセスではコンパイルの最適化があまり行われず、Zend仮想マシンで実行する必要があるため、ネイティブC言語と比較して効率が大幅に低下することになります。大きなギャップ、それは避けられません。それは信じられないほどです。

PHPでの再帰の効率が非常に低いのはなぜですか?(PHPは末尾再帰の最適化をサポートしていないため、ツリー再帰の反復と計算が繰り返されるため、再帰の効率が大幅に低下し、許容できる再帰レベルも大幅に減少します。c/ c ++では、gcc -O2以上を使用すると、コンパイラはそれに応じて再帰を最適化します)?、この記事(実装原理およびPHP関数のパフォーマンス分析)、著者の説明の一つがある:「ファンクション再帰は、スタックを介して行われ、PHPで、それはまた、同様の方法で実装されているZendのは、各PHP関数アンアクティブシンボルを提供します。テーブル(active_sym_table)は、現在の関数のすべてのローカル変数の状態を記録するために割り当てられます。すべてのシンボルテーブルはスタックの形式で維持されます。関数呼び出しがあるたびに、新しいシンボルテーブルが割り当てられ、スタックに追加されます。
呼び出し後、現在のシンボルテーブルがスタックからポップアウトされます。これにより、状態の保存と再帰が実現されます。スタックを維持するために、ここでzendが最適化されます。長さNの静的配列を事前に割り当てて、スタック。動的データ構造をシミュレートする方法は、独自のプログラムでもよく使用されます。この方法は、各呼び出しによって引き起こされるメモリの割り当てと破棄を回避します。ZENDは、現在のスタックの最後にあるシンボルテーブルデータをクリーンアップするだけです。関数呼び出しはい。静的配列の長さがNであるため、関数呼び出しレベルがNを超えると、プログラムはスタックをオーバーフローしません。この場合、zendはシンボルテーブルを割り当てて破棄し、パフォーマンスが向上します。 zendでは、Nの現在の値は32です。したがって、phpプログラムを作成する場合、関数呼び出しレベルは32を超えてはなりません。

 

到外、phpバグ中也有説明:“ PHP 4.0(Zend)は、ヒープを使用するのではなく、集中的なデータにスタックを使用します。つまり、その許容度の再帰関数は大幅に

他の言語よりも低い 」

したがって、PHPでは、あまり必要がない場合は、特に再帰レベルが大きいか推定できない場合は、再帰をできるだけ少なくすることをお勧めします。

参照:

1.   http://www.csharpwin.com/csharpspace/12292r4006.shtml

2.  http:/devzone.zend.com/283/recursion-in-php-tapping-unharnessed-power/

3.  http://blog.csdn.net/heiyeshuwu/article/details/5840025

4.  http:/www.nowamagic.net/librarys/veda/detail/2336 

5.  http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html

6.  http://wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html

おすすめ

転載: blog.csdn.net/kexin178/article/details/112728323