web-dev-qa-db-ja.com

再帰プログラミングの習得

再帰の観点から問題を考えたり解決するのに苦労しています。私はコンセプトを本当に感謝しており、ベースケース、出口ケース、再帰呼び出しなどの作成のようにそれらを理解することができます。配列に整数の階乗や合計を書くような単純な問題を解決できます。それは私の思考が停止するところです。問題が複雑になると、概念を実際に適用したり、解決策を思い付くことができませんでした。たとえば、ハノイの塔では、問題と解決策を理解できますが、私は自分で解決策を見つけることができません。クイックソート/バイナリツリートラバースなどの他のアルゴリズムにも適用されます。だから私の質問は

  1. それをマスターする最良の方法は何ですか?
  2. 誰かが問題や質問のリストを提案できますか?それを練習するための練習として使用できますか?
  3. 関数型言語を学ぶことは私の理解に役立ちますか?

ご意見をお聞かせください。

39
Hunter

再帰は、反復がそうであるように、単なる考え方です。私たちが学校の子供だったとき、私たちは再帰的に考えるように教えられなかったので、本当の問題があります。その考え方を武器に組み込む必要があります。いったんそれを行うと、それは永遠にそこにとどまります。

マスターするための最良の方法:

最初にベースケースを常に把握しておくと便利です。最初はベースケースが最も単純なものではないかもしれませんが、ベースケースの上に再帰を構築し始めると、簡単にできることに気付くでしょう。基本ケースを特定することの重要性は、最初に、最も単純な形式(より単純なケース)で解決する必要があるものに焦点を当て、これが何らかの方法で将来のアルゴリズムのロードマップを描くこと、次に、アルゴリズムを確認することですstops。たぶん、期待した結果を返さないかもしれませんが、少なくとも停止します。

また、問題の小さなインスタンスが、問題の大きなインスタンスの解決策を見つけるのにどのように役立つかを常に把握するのに役立ちます。これは、たとえば、入力nのソリューションを構築する方法です。すでに入力n-1

再帰的に考えることができるすべての問題を解決する。はい、ハノイタワーズは非常に良い例ではありません。その再帰的な解決策は非常に賢い解決策です。簡単な問題、ほとんど要素的な問題を試してください。

問題のリスト

  1. 数学演算:べき乗と考えられるすべての数学演算。
  2. 文字列処理:パリンドロームは非常に良い練習です。グリッド内の単語を検索することも便利です。
  3. ツリーデータ構造について学習します:これは特にIMOの最良のトレーニングです。ツリーは再帰的なデータ構造です。それらのトラバースについて学習します(順序、後順、先行順、高さ、直径の計算など)。ツリーのようなデータ構造に対するほとんどすべての操作は、すばらしい練習です。
  4. 組み合わせの問題:非常に重要、組み合わせ、順列など.
  5. パス検索: Leeのアルゴリズム、Mazeアルゴリズムなど.

しかし、最も重要なのは、単純な問題から始まるです。ほとんどすべての問題には再帰的な解決策があります。数学の問題は、把握するのに最適です。 forループまたはwhileループが表示されるたびに、そのアルゴリズムを再帰に変換します。

プログラミング言語

関数型プログラミングは再帰に大きく依存しています。それらは本質的に再帰的であり、まだ再帰をあまり理解していないユーザーにとっては面倒になる可能性があるため、これはあまり役に立たないと思います。

慣れ親しんだシンプルなプログラミング言語を使用します。できれば、メモリの煩わしさやポインタで頭を忙しくしないものを使用してください。 Pythonは非常に良い出発点です。非常にシンプルで、入力や複雑なデータ構造に煩わされません。言語が再帰のみに集中し続ける限り、良くなります。

最後のアドバイスとして、問題の解決策が見つからない場合は、インターネットで検索するか、助けを求めてください。それが何をするかを完全に理解してくださいそしてもう一方に進みます。あなたがやろうとしているのはあなたの頭にその考え方を取り入れるであるからです。

マスター再帰には、最初にマスター再帰が必要です:)

お役に立てれば!

39
Paulo Bu

私のアドバイス:再帰関数が「仕事をする」ことを信頼する、つまりその仕様を満たします。そして、それを知っていれば、仕様を満たしながら、より大きな問題を解決する関数を作成できます。

ハノイの塔の問題をどのように解決しますか?ルールを侵害することなく、N個のディスクの山を移動できるHanoi(N)関数があると仮定します。この機能を使用すると、Hanoi '(N + 1)を簡単に実装できます。Hanoi(N)を実行し、次のディスクを移動して、Hanoi(N)を再度実行します。

Hanoi(N)が機能する場合、Hanoi '(N + 1)もルールに違反することなく機能します。引数を完了するには、再帰呼び出しが終了することを確認する必要があります。この場合、Hanoi(1)を非再帰的に(簡単に)解くことができれば、完了です。

このアプローチを使用すると、実際にどのように発生するかを心配する必要がなく、機能することが保証されます。 (問題のますます小さなインスタンスに移動した場合)

別の例:バイナリツリーの再帰的な走査。関数Visit(root)が仕事をすると仮定します。次に、if left -> Visit(left); if right -> Visit(right); print rootが仕事をします!最初の呼び出しは左のサブツリー(方法は心配しないでください)を、2番目の呼び出しは右のサブツリー(方法を心配しないでください)を出力し、ルートも印刷するためです。

後者の場合、より小さなサブツリーを葉まで処理することにより、終了が保証されます。

その他の例:クイックソート。配列をインプレースでソートする関数があると仮定して、Quicksortを使用します。次のように使用します。小さな要素を適切な「ピボット」値と比較することで、大きな要素の前にインプレースで移動します(実際には、配列の任意の値が実行できます)。次に、クイックソート機能を使用して小さな要素をソートし、同じ方法で大きな要素をソートします。発生するパーティションの正確なシーケンスを気にする必要はありません。 voidサブアレイを避けると、終了が保証されます。

最後の例、パスカルの三角形。要素はその上の2つの合計であり、1が両側にあることを知っています。目を閉じて、C(K, N)= 1 if K=0 or K=N, else C(K, N) = C(K-1, N-1) + C(K, N-1)それだけです!

9
Yves Daoust

関数型言語を学ぶことは、確かに再帰を考えるのに役立ちます。 HaskellまたはLISP(またはClojure)をお勧めします。良いことは、再帰に到達する前にこれらの言語の「ハードビット」に到達する必要がないことです。再帰について学ぶために、これらの言語のいずれかを「実際の」プログラミングを行うために十分に学ぶ必要はありません /

Haskellのパターンマッチング構文は、基本ケースが見やすいことを意味します。 Haskellでは、Factionalは次のようになります。

_factorial 0 = 1
factorial n = n * factorial (n - 1)
_

...手続き型言語とまったく同じです:

_int factorial(n) {
    if(n==0) {
         return 1;
    } else {
         return n * factorial(n-1)
    }
}
_

...しかし、概念を曖昧にするための構文が少ない。

完全を期すため、LISPの同じアルゴリズムを次に示します。

_(defun factorial (n)
   (if (== n 0)
       1
       (* n (factorial (- n 1)))))
_

あなたが見ることができるはずのものは同等ですが、最初はすべての括弧が、何が起こっているのかという人々の見方を曖昧にする傾向があります。それでも、LISPの本は多くの再帰的なテクニックをカバーしています。

さらに、関数型言語に関する本は、再帰の例をたくさん提供します。リストで機能するアルゴリズムから始めます。

_ addone [] = []
 addone (head:tail) = head + 1 : addone tail
_

..関数ごとに1つの再帰呼び出しを行う非常に一般的なパターンを使用します。 (実際、ほとんどすべての言語がmapと呼ばれるライブラリ関数に抽象化するほど一般的なパターン)

次に、ノードからブランチごとに1つの再帰呼び出しを行うことにより、ツリーをトラバースする関数に進みます。

より一般的には、次のような問題を考えてください。

「この問題の小さな部分を解決し、同じ問題を自分自身に残して、もっと小さくすることができますか?」.

...または...

「残りの部分だけがすでに解決されていれば、この問題は簡単に解決できるでしょうか?」.

したがって、たとえば、factorial(n)を知っていれば、factorial(n-1)は簡単に解決できます。これは、再帰的な解決策を示唆しています。

多くの問題がそのように考えられることがわかります:

「1000個のアイテムのリストを並べ替えるのは難しいように思えますが、ランダムな数字を選んだ場合、それよりも小さい数字をすべて並べ替え、それよりも大きい数字をすべて並べ替えれば完了です。」 (最終的には長さ1のリストのソートになります)

...

「ノードへの最短パスを計算するのは困難ですが、隣接する各ノードからそこまでの距離を知ることができれば、簡単です。」

...

「このディレクトリツリー内のすべてのファイルを表示するのは困難ですが、ベースディレクトリ内のファイルを見て、サブディレクトリも同じように見ることができます。」

同様にハノイの塔。次のように記述すれば、ソリューションは簡単です。

_ To move a stack from a to c:
  If the stack is of size 1
      just move it.
  otherwise
      Move the stack minus its largest ring, to b (n-1 problem)
      Move the largest ring to c (easy)
      Move the stack on b to c (n-1 problem)
_

明らかに困難な2つのステップをスケッチすることで、問題を簡単に見せることができました。しかし、これらの手順は再び同じ問題ですが、「1つ小さい」です。


この回答で説明されているように、紙を使用して再帰アルゴリズムを手動でステップスルーして呼び出しスタックを表すと便利です: Understanding stack unwinding in recursion(tree traversal)


再帰に慣れてきたら、後ろに戻り、特定のケースに適したソリューションかどうかを考えます。 factorial()は再帰の概念を示す良い方法ですが、ほとんどの言語では反復ソリューションの方が効率的です。 tail recursion optimisation 、それが特徴の言語、およびその理由を調べてください。

3
slim

再帰は、分割統治パラダイムを実装するための便利な方法です。特定の問題を解決する必要がある場合、強力なアプローチはそれを問題に分解することです同じ性質、しかし小さいサイズ。このプロセスを繰り返すことで、別の方法で簡単に解決できるほど小さな問題に取り組むことになります。

自問しなければならない質問は、「この問題を部分的に解決することで解決できますか?」です。答えが正の場合、次の有名なスキームを適用します。

  • サイズが小さくなるまで、問題を再帰的にサブ問題に分割し、

  • 直接的な方法で副問題を解決し、

  • 逆順にソリューションをマージします。

分割は2つ以上の部分で行うことができ、これらはバランスをとることができるかどうかに注意してください。

たとえば、部分的な並べ替えを実行して数値の配列を並べ替えることはできますか?

回答1:はい、最後の要素を除外して残りを並べ替える場合、最後の要素を適切な場所に挿入することで配列全体を並べ替えることができます。これは挿入ソートです。

回答2:はい、最大の要素を見つけて最後まで移動した場合、残りの要素を並べ替えることで配列全体を並べ替えることができます。これは選択ソートです。

回答3:はい、配列の半分を並べ替える場合、移動用の補助配列を使用して、2つのシーケンスをマージすることで配列全体を並べ替えることができます。これはマージソートです。

回答4:はい、ピボットを使用して配列を分割する場合、2つの部分を並べ替えることで配列全体を並べ替えることができます。これは簡単なソートです。

これらのすべての場合、同じ性質の部分問題を解決し、接着剤を追加することで問題を解決します。

3
Yves Daoust

再帰は難しい考え方だからです。若い頃には決して紹介されなかったものです。

あなたが言っていることから、あなたはすでにあなたが本当に必要とする概念を持っているだけで、それをもっと練習するだけです。関数型言語は間違いなく役立ちます。あなたは再帰的に問題について考えることを余儀なくされ、それを知る前に再帰は非常に自然に見えるでしょう

再帰に関連して実行できるエクササイズはたくさんありますが、ループで行われた操作はすべて再帰的にも実行できることに注意してください。

これを参照してください answer 参照と演習問題の詳細について

複雑な問題については、問題のサイズを小さくして問題を解決し、どのようなパターンを見つけるかを確認することをお勧めします。たとえば、ハノイの塔では、問題のサイズ1から始めて、2、3のようになります。ある時点で、おそらくパターンが見え始め、あなたが持っているもののいくつかに気付くでしょう。行うことは、小さなサイズの問題でやらなければならなかったことと同じであるか、以前と同じテクニックを使用できますが、ある程度のバリエーションはあります。

私はハノイの塔の問題を自分で経験し、自分の考えを勉強しました。私はサイズ1の問題から始めました。

ペグAに1つのディスクがあります。
 ***ペグCに移動します。
完了!

2つになりました。

ペグAに2つのディスクがあります。
最初のディスクを邪魔にならないようにするためにペグBを使用する必要があります。
 ***ペグAからペグBに移動します
残りの操作ができるようになりました
 ***ペグAからペグC 
に移動します***ペグBからペグC 
に移動しました!

さあ、3つ。

物事はもう少し面白くなり始めます。解決策はそれほど明白ではありません。ただし、2つのディスクをペグから別のペグに移動する方法を考え出したので、2つのディスクをペグAからペグBに移動し、次に1つのディスクをペグAからペグCに移動し、次にペグBから2つのディスクを移動しますCをペグするには、完了です! 2つのディスクの場合のロジックは機能しますが、ペグが異なります。ロジックを関数に入れて、ペグのパラメーターを作成すると、ロジックを再利用できます。

def move2(from_peg,to_peg,other_peg):
   # We have two disks on from_peg
   # We need to use other_peg to get the first disk out of the way
   print 'Move from peg '+from_peg+' to peg '+other_peg
   # Now I can do the rest
   print 'Move from peg '+from_peg+' to peg '+to_peg
   print 'Move from peg '+other_peg+' to peg '+to_peg

ロジックは次のとおりです。

 move2( 'A'、 'B'、 'C​​')
 print 'ペグAからペグCへ移動' 
 move2( 'B'、 'C​​'、 ' A ')

Move1関数も持つことで、これをより簡単にできます。

def move1(from_peg,to_peg):
    print 'Move from '+from_peg+' to '+to_peg

今、私のmove2関数は

def move2(from_peg,to_peg,other_peg):
   # We have two disks on from_peg
   # We need to use other_peg to get the first disk out of the way
   move1(from_peg,other_peg,to_peg)
   # Now I can do the rest
   move1(from_peg,to_peg)
   move1(other_peg,to_peg)

OK、4はどうですか?

同じロジックを適用できるようです。ペグAからペグB、次にAからC、そして3からBからCの3つのディスクを取得する必要があります。3つを移動することはすでに解決しましたが、ペグが間違っているため、一般化します。

def move3(from_peg,to_peg,other_peg):
   move2(from_peg,other_peg,to_peg)
   move1(from_peg,to_peg)
   move2(other_peg,to_peg,from_peg)

涼しい!待ってください。move3とmove2は今ではかなり似ており、それは理にかなっています。任意のサイズの問題について、1つのディスクを除くすべてをペグBに移動し、1つのディスクをAからCに移動し、次にペグBのすべてのディスクをペグCに移動できます。パラメーター:

def move(n,from_peg,to_peg,other_peg):
    move(n-1,from_peg,other_peg,to_peg)
    move1(from_peg,to_peg)
    move(n-1,other_peg,to_peg,from_peg)

これは本当に近いように見えますが、n == 1の場合は動作しません。これは、move(0、...)を呼び出すことになってしまうためです。したがって、それを処理する必要があります。

def move(n,from_peg,to_peg,other_peg):
    if n==1:
        move1(from_peg,to_peg)
    else:
        move(n-1,from_peg,other_peg,to_peg)
        move1(from_peg,to_peg)
        move(n-1,other_peg,to_peg,from_peg)

優秀な! 5の問題サイズはどうですか? move(5、 'A'、 'C​​'、 'B')を呼び出すだけです。問題のサイズは同じものであるように見えるため、主な機能は次のとおりです。

def towers(n):
    move(n,'A','C','B')

これで完了です!

1
Vaughn Cato