階乗、フィボナッチ数などのない再帰


48

再帰について見つけることができるほぼすべての記事には、階乗またはフィボナッチ数の例が含まれています。

  1. 数学
  2. 実生活では役に立たない

再帰を教えるための興味深い数学以外のコード例はありますか?

私は分割統治アルゴリズムを考えていますが、通常は複雑なデータ構造を伴います。


26
あなたの質問は完全に有効ですが、実際の生活では役に立たないフィボナッチ数列を呼び出すことをheします。同じことが階乗にも当てはまります。
ザックL

2
The Little Schemerは、FactやFibを決して使用しない再帰に関する本です。junix-linux-config.googlecode.com/files/...
エリック・ウィルソン

5
@Zach:それでも、再帰はフィボナッチ数を実装する恐ろしい方法です。実行時間が指数関数的であるためです。
dan04

2
@ dan04:再帰は、ほとんどの言語でスタックオーバーフローが発生する可能性があるため、ほとんどすべてを実装する恐ろしい方法です。
R ...

5
@ dan04あなたの言語はそれだけで正常に動作している場合には、ほとんどの関数型言語のように末尾呼び出しの最適化を実装するためのスマート十分でない限り
ザカリーK

回答:


107

ディレクトリ/ファイル構造は再帰を使用する最良の例です。なぜなら、開始する前に誰もがそれらを理解しているからです。しかし、ツリーのような構造を含むものなら何でもできます。

void GetAllFilePaths(Directory dir, List<string> paths)
{
    foreach(File file in dir.Files)
    {
        paths.Add(file.Path);
    }

    foreach(Directory subdir in dir.Directories)
    {
        GetAllFilePaths(subdir, paths)
    }
}

List<string> GetAllFilePaths(Directory dir)
{
    List<string> paths = new List<string>();
    GetAllFilePaths(dir, paths);
    return paths;
}

1
おかげで、私はファイルシステムで行くと思います。これは具体的なものであり、多くの実世界の例に使用できます。
シナプス

9
注:unixコマンドでは、多くの場合-rオプション(例:cpまたはrm)が含まれません。-r再帰の略です。
deadalnix

7
あなたが参加するとのサイクルなどのハードリンクを作成することができ、ファイルシステムでサポートされている場合、ファイル・システムは、必ずしも実際に木の有向グラフある現実の世界のように、ここで少し注意する必要があります
JK。

1
@jk:ディレクトリはハードリンクできないため、複数の場所に表示される可能性のあるいくつかのリーフを法として、シンボリックリンクを除外すると仮定すると、実世界のファイルシステムはツリーです。
R ..

1
ディレクトリのファイルシステムには、NTFS再解析ポイントなど、他の特性もあります。私のポイントは、これを特に認識していないコードは、実世界のファイルシステム
jk)で

51

ツリー構造に関係するものを探します。ツリーは比較的把握しやすく、再帰的なソリューションの美しさは、リストなどの線形データ構造よりもはるかに早く明らかになります。

考えるべきこと:

  • ファイルシステム-これらは基本的にツリーです。素敵なプログラミングタスクは、特定のディレクトリとそのすべてのサブディレクトリの下にあるすべての.jpgイメージを取得することです
  • 祖先-家系図が与えられた場合、共通の祖先を見つけるためにあなたが歩かなければならない世代の数を見つけます。または、ツリー内の2人が同じ世代に属しているかどうかを確認します。または、ツリー内の2人が合法的に結婚できるかどうかを確認します(管轄によって異なります:)
  • HTMLに似たドキュメント-ドキュメントのシリアル(テキスト)表現とDOMツリーとの間の変換。DOMのサブセットに対して操作を実行します(xpathのサブセットを実装することもできますか?)。...

これらはすべて実際の現実世界のシナリオに関連しており、現実世界で重要なアプリケーションですべて使用できます。


もちろん、独自のツリー構造を設計する自由がある場合は常に、親/次の兄弟/などへのポインタ/参照を保持する方が良いことに注意する必要があります。ノードで、再帰なしでツリーを反復処理できるようにします。
R ..

1
ポインターはそれと何の関係がありますか?最初の子、次の兄弟、親へのポインタがある場合でも、何らかの方法でツリーを歩く必要があり、場合によっては、再帰が最も実行可能な方法です。
-tdammers

41

https://stackoverflow.com/questions/105838/real-world-examples-of-recursion

  • 伝染性感染症のモデリング
  • ジオメトリを生成する
  • ディレクトリ管理
  • 仕分け

https://stackoverflow.com/questions/2085834/how-did-you-practically-use-recursion

  • レイトレーシング
  • チェス
  • ソースコードの解析(言語文法)

https://stackoverflow.com/questions/4945128/what-is-a-good-example-of-recursion-other-than-generation-a-fibonacci-sequence

  • BST検索
  • ハノイの塔
  • 回文検索

https://stackoverflow.com/questions/126756/examples-of-recursive-functions

  • 就寝時の物語による再帰を説明する素敵な英語の物語を提供します。

10
これは理論的には質問に回答するかもしれませんが、それらの質問と回答の重要な部分をここに含め、参照用のリンクを提供することが望ましいでしょう。質問がSOから削除されると、あなたの答えはまったく役に立ちません。
アダムリア

@Annaまあ、ユーザーは自分の質問を削除することはできません。
vemv

4
@vemv投票、モデレーター、トピックの変更に関するルールを削除します... いずれにせよ、訪問者をすぐに4つの異なるページに送信するよりも、ここでより完全な答えを持つ方が望ましいでしょう。
アダムリア

@Anna:この考え方に従って、「完全な複製」として閉じられたすべての質問には、貼り付けられた元の回答が必要です。この質問は(SOの質問の)正確な複製です。プログラマに関する質問の重複?
SF。

1
@SF重複として閉じることができた場合は閉じますが、クロスサイトの重複はサポートされていません。プログラマーは別のサイトですので、ここでの答えは、SOを他の参照として使用し、完全に委任するのではないことが理想的です。「あなたの答えはこの本に載っています」と言うのと違いはありません-技術的には正しいですが、参考文献を参照しないとすぐには使えません。
アダムリア

23

ここに私の頭に浮かぶいくつかのより実用的な問題があります:

  • ソートのマージ
  • バイナリ検索
  • ツリーのトラバーサル、挿入、削除(データベースアプリケーションで主に使用)
  • 順列ジェネレータ
  • 数独ソルバー(バックトラッキング付き)
  • スペルチェック(バックトラッキングを使用)
  • 構文分析(例、プレフィックスをポストフィックス表記に変換するプログラム)

11

QuickSortは最初に思い浮かぶものです。バイナリ検索も再帰的な問題です。それ以外にも、再帰を使って作業を開始すると、ソリューションがほぼ無料で抜け落ちてしまう問題のクラスがあります。


3
バイナリ検索は、多くの場合、再帰的な問題として定式化されますが、命令的な方法で実装するのは簡単です(多くの場合、望ましい)。
ふわふわ

コードを使用している言語に応じて、明示的に再帰的または命令的または再帰的である場合とそうでない場合があります。しかし、問題を解決するためにますます小さなチャンクに分割するという点で、依然として再帰的なアルゴリズムです。
ザカリーK

2
@Zachary:末尾再帰(バイナリ検索など)を使用して実装できるアルゴリズムは、実際の再帰(または同様に高価なスペース要件を持つ独自の状態構造)を必要とするものとは根本的に異なるスペースクラスにあります。彼らが同じであるかのように一緒に教えられることは有益ではないと思います。
R ...

8

Pythonで再帰的に定義された並べ替え。

def sort( a ):
    if len(a) == 1: return a
    part1= sort( a[:len(a)//2] )
    part2= sort( a[len(a)//2:] )
    return merge( part1, part2 )

再帰的に定義されたマージ。

def merge( a, b ):
    if len(b) == 0: return a
    if len(a) == 0: return b
    if a[0] < b[0]:
        return [ a[0] ] + merge(a[1:], b)
    else:
        return [ b[0] ] + merge(a, b[1:]) 

再帰的に定義された線形検索。

def find( element, sequence ):
    if len(sequence) == 0: return False
    if element == sequence[0]: return True
    return find( element, sequence[1:] )

再帰的に定義されたバイナリ検索。

def binsearch( element, sequence ):
    if len(sequence) == 0: return False
    mid = len(sequence)//2
    if element < mid: 
        return binsearch( element, sequence[:mid] )
    else:
        return binsearch( element, sequence[mid:] )

6

ある意味で、再帰とは解決策を分割して征服することです。つまり、問題空間をより小さなものに分割して単純な問題の解決策を見つけ、通常は元の問題を再構築して正しい答えを構成します。

再帰を教えるために数学を使わないいくつかの例(少なくとも大学時代から覚えている問題):

これらは、バックトラッキングを使用して問題を解決する例です。

他の問題は、人工知能ドメインの古典的なものです:深さ優先検索の使用、経路探索、計画。

これらの問題はすべて、ある種の「複雑な」データ構造に関係していますが、数学(数字)で教えたくない場合は、選択肢がより制限される可能性があります。Yoyは、リンクリストのような基本的なデータ構造でティーチングを開始する場合があります。たとえば、リストを使用して自然数を表す:

0 =空のリスト1 = 1つのノードを持つリスト。2 = 2ノードのリスト。...

次のように、このデータ構造に関して2つの数値の合計を定義します。空+ N = N Node(X)+ N = Node(X + N)


5

ハノイの塔は、再帰を学ぶのに役立ちます。

多くの異なる言語でウェブ上にそれに対する多くの解決策があります。


3
これは実際には別の悪い例です。まず、非現実的です。人々が実際に抱えている問題ではありません。第二に、簡単な非再帰的ソリューションがあります。(1つは、ディスクに番号を付けることです。同じパリティのディスクにディスクを移動したり、最後に行った移動を元に戻したりしないでください。 )
エリックリッパー

5

パリンドローム検出器:

文字列で始まる: "ABCDEEDCBA"開始文字と終了文字が等しい場合は、再帰して "BCDEEDCB"などを確認します。


6
それは再帰なしで解決するのも簡単であり、私見では、それなしで解決する方が良いです。
Blrfl

3
同意しましたが、OPはデータ構造の使用を最小限に抑えた教育の例を特に求めました。
NWS

5
生徒が非再帰的な解決策をすぐに考えることができる場合、これは良い教育例ではありません。あなたの例が「ここでループで行うのは簡単なことです。今、明確な理由なしに、より難しい方法を紹介します」と誰かが注意を払うのはなぜですか。
モニカ

5

バイナリ検索アルゴリズムは、あなたが望むもののように聞こえます。


4

関数型プログラミング言語では、高階関数が利用できない場合、可変状態を回避するために命令型ループの代わりに再帰が使用されます。

F#は両方のスタイルを可能にする不純な関数型言語なので、ここで両方を比較します。以下は、リスト内のすべての数値を合計します。

可変変数を使用した命令型ループ

let xlist = [1;2;3;4;5;6;7;8;9;10]
let mutable sum = 0
for x in xlist do
    sum <- sum + x

可変状態のない再帰ループ

let xlist = [1;2;3;4;5;6;7;8;9;10]
let rec loop sum xlist = 
    match xlist with
    | [] -> sum
    | x::xlist -> loop (sum + x) xlist
let sum = loop 0 xlist

この種の集約高次関数でキャプチャされ、単にとして便利な関数として、または実際にはさらに簡単にList.fold記述できることに注意してください。List.fold (+) 0 xlistList.sumList.sum xlist


あなたは私からの+1よりも多くのポイントに値するでしょう。再帰なしのF#は非常に退屈です。これは、ループ構造のみを持たないHaskellの場合にのみ当てはまります。
schoetbi

3

AIをプレイするゲームで再帰を多用しました。C ++で記述し、互いに順番に呼び出す一連の約7つの関数を使用しました(最初の関数には、それらすべてをバイパスし、代わりに2つの関数のチェーンを呼び出すオプションがあります)。いずれかのチェーンの最終関数は、検索したい残りの深さが0になるまで最初の関数を再度呼び出しました。その場合、最終関数は評価関数を呼び出して位置のスコアを返します。

複数の機能により、プレイヤーの決定またはゲーム内のランダムなイベントに基づいて簡単に分岐できました。非常に大きなデータ構造をやり取りしていたため、できる限り参照渡しを使用していましたが、ゲームの構造が原因で、検索で「移動を取り消す」ことは非常に困難でした。元のデータを変更しないために、一部の関数で値渡しを使用します。このため、再帰的なアプローチではなくループベースのアプローチに切り替えるのは非常に困難であることが判明しました。

この種のプログラムの非常に基本的な概要を見ることができます。https://secure.wikimedia.org/wikipedia/en/wiki/Minimax#Pseudocodeを参照してください


3

ビジネスでの実に良い実例は、「部品表」と呼ばれるものです。これは、完成品を構成するすべてのコンポーネントを表すデータです。たとえば、自転車を使用しましょう。自転車には、ハンドルバー、ホイール、フレームなどがあります。また、これらの各コンポーネントにはサブコンポーネントを含めることができます。たとえば、ホイールはスポーク、バルブステムなどを持つことができます。したがって、通常、これらはツリー構造で表されます。

ここで、BOMに関する集計情報を照会したり、BOMの要素を変更したりするために、再帰に頼ることがよくあります。

    class BomPart
    {
        public string PartNumber { get; set; }
        public string Desription { get; set; }
        public int Quantity { get; set; }
        public bool Plastic { get; set; }
        public List<BomPart> Components = new List<BomPart>();
    }

そして、サンプルの再帰呼び出し...

    static int ComponentCount(BomPart part)
    {
        int subCount = 0;
        foreach(BomPart p in part.Components)
            subCount += ComponentCount(p);
        return part.Quantity * Math.Max(1,subCount);

    }

明らかに、BomPartクラスにはさらに多くのフィールドがあります。あなたが持っているプラ​​スチック部品の数、完全な部品を作るのにどれだけの労力がかかるかなどを理解する必要があるかもしれません。これはすべて、ツリー構造での再帰の有用性に戻っています。


部品表は、ツリーではなく指向性のあるグラスである場合があります。たとえば、同じ仕様のネジを複数のコンポーネントで使用できます。
イアン

-1

家族関係は良い例になります。誰もが直感的に理解しているからです。

ancestor(joe, me) = (joe == me) 
                    OR ancestor(joe, me.father) 
                    OR ancestor(joe, me.mother);

このコードはどの言語で書かれていますか?
törzsmókus

@törzsmókus特定の言語はありません。構文はC、Obj-C、C ++、Java、および他の多くの言語に非常に近いですが、実際のコードが必要な場合||は、などの適切な演算子を置き換える必要がありますOR
カレブ

-1

かなり役に立たないが、再帰内部がうまく機能していることは再帰的strlen()です:

size_t strlen( const char* str )
{
    if( *str == 0 ) {
       return 0;
    }
    return 1 + strlen( str + 1 );
}

数学なし-非常にシンプルな機能。もちろん、実際には再帰的に実装するわけではありませんが、再帰の良いデモです。


-2

学生が関係する可能性がある別の現実世界の再帰の問題は、Webサイトから情報を引き出し、そのサイト内のすべてのリンク(およびそれらのリンクからのすべてのリンクなど)を追跡する独自のWebクローラーを構築することです。


2
一般的に、従来の意味での再帰とは対照的に、プロセスキューの方が適切です。
ふわふわ

-2

再帰プログラムを使用して、チェス盤上のナイトパターンに関する問題を解決しました。騎士を動かして、いくつかのマークされた正方形を除くすべての正方形に触れるようにする必要がありました。

あなたは単に:

mark your "Current" square
gather a list of free squares that are valid moves
are there no valid moves?
    are all squares marked?
        you win!
for each free square
    recurse!
clear the mark on your current square.
return.    

このようなツリーで将来の可能性をテストすることで、多くの種類の「先読み」シナリオを機能させることができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.