Articles

再帰とスタック

関数に戻り、それらをより詳細に研究しましょう。

最初のトピックは再帰です。

プログラミングに慣れていない場合は、おそらく慣れているので、この章をスキップすることができます。

再帰は、タスクが同じ種類のいくつかのタスクに自然に分割できるが、より単純な状況で便利なプログラミングパターンです。 または、タスクを簡単なアクションに加えて、同じタスクのより単純なバリアントに単純化できる場合。 あるいは、すぐにわかるように、特定のデータ構造に対処することもできます。,

関数がタスクを解決すると、そのプロセスで他の多くの関数を呼び出すことができます。 これの部分的なケースは、関数が自分自身を呼び出すときです。 これは再帰と呼ばれます。

二つの考え方

始めるのが簡単なものについては、pow(x, n)xnの自然な力に上げる関数pow(x, n)を書いてみましょう。 つまり、xだけでn回を乗算します。

pow(2, 2) = 4pow(2, 3) = 8pow(2, 4) = 16

それを実装するには二つの方法があります。,

再帰的なバリアントが根本的に異なる方法に注意してください。

pow(x, n)が呼び出されると、実行は二つのブランチに分割されます。

 if n==1 = x /pow(x, n) = \ else = x * pow(x, n - 1)
  1. n == 1ならば、すべてが自明です。 これは、すぐに明白な結果が生成されるため、再帰の基底と呼ばれます。pow(x, 1)equalsx。それ以外の場合は、pow(x, n)x * pow(x, n - 1)として表すことができます。, 数学では、xn = x * xn-1と書くでしょう。 タスクをより単純なアクション(xによる乗算)と同じタスクのより簡単な呼び出し(powをより低いn)に変換します。 次のステップでは、n1に達するまで、さらに単純化します。また、pown == 1まで再帰的に自分自身を呼び出すと言うこともできます。,

    たとえば、pow(2, 4)

    1. pow(2, 4) = 2 * pow(2, 3)
    2. pow(2, 3) = 2 * pow(2, 2)
    3. pow(2, 2) = 2 * pow(2, 1)
    4. pow(2, 1) = 2

    だから、再帰は、結果が明らかになるまで、関数呼び出しをより単純なものに減らし、その後、さらに単純なものになるなど、さらに簡単になります。,

    入れ子になった呼び出しの最大数(最初の呼び出しを含む)は再帰深度と呼ばれます。 私たちの場合、正確にはnになります。

    最大再帰の深さはJavaScriptエンジンによって制限されます。 ま頼で10000、エンジンにすることが100000はその限にします。 これを軽減するのに役立つ自動最optimがあります(”テールコールの最適化”)が、どこでもまだサポートされておらず、単純な場合にのみ機能します。

    これは再帰の適用を制限しますが、それでも非常に広いままです。, 再帰的な考え方がより単純なコードを提供し、維持しやすくする多くのタスクがあります。

    実行コンテキストとスタック

    再帰呼び出しがどのように機能するかを調べてみましょう。 そのために我々は機能のフードの下に見ていきます。

    実行中の関数の実行プロセスに関する情報は、その実行コンテキストに格納されます。,

    実行コンテキストは、関数の実行に関する詳細を含む内部データ構造です:制御フローが現在ある場合、現在の変数、this(ここでは使用しません)の値、およびその他のいくつかの内部詳細。

    一つの関数呼び出しは、それに関連付けられた正確に一つの実行コンテキストを持

    関数がネストされた呼び出しを行うと、次のようになります。

    • 現在の関数は一時停止されます。

      • 現在の関数は一時停止されます。
      • 関連付けられた実行コンテキストは、実行コンテキストスタックと呼ばれる特別なデータ構造に記憶されます。,
      • ネストされた呼び出しが実行されます。
      • 終了すると、古い実行コンテキストがスタックから取得され、外部関数が停止した場所から再開されます。

      pow(2, 3)呼び出し中に何が起こるか見てみましょう。

      pow(2,3)

      呼び出しの開始時にpow(2, 3)実行コンテキストには変数が格納されます。x = 2, n = 3、実行フローは関数の1行にあります。,

      次のようにスケッチできます。

      • Context:{x:2,n:3,at line1}pow(2,3)

      これが関数の実行を開始するときです。, 条件n == 1偽であるため、フローはifの第二の分岐に続きます。

      function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); }}alert( pow(2, 3) );

      変数は同じですが、行が変わります。したがって、コンテキストは次のようになります。

      • context:{x:2,n:3,at line5}pow(2,3)

      x * pow(x, n - 1)を計算するには、pow新しい引数pow(2, 2)のサブコールを作成する必要があります。,

      pow(2,2)

      ネストされた呼び出しを行うには、JavaScriptは現在の実行コンテキストを実行コンテキストスタックに記憶します。

      ここでは、同じ関数を呼び出しますpow、しかし、それは絶対に問題ではありません。 プロセスはすべての関数で同じです:

      1. 現在のコンテキストはスタックの上に”記憶”されます。
      2. サブコール用に新しいコンテキストが作成されます。
      3. サブコールが終了すると、前のコンテキストがスタックからポップされ、その実行が続行されます。,

      この文脈にスタックの時にも参入いたしましたsubcallpow(2, 2)

      • コンテクスト:{x:2,n:2、1}捕虜(2,2)
      • コンテクスト:{x:n,2:3,5}捕虜(2,3)

      の現在の実行コンテキストではトップかつ大胆な、前記憶の文脈については以下をご覧ください。

      サブコールを終了すると、変数と停止したコードの正確な場所の両方を保持するため、以前のコンテキストを再開するのは簡単です。,

      注意してください:

      ここでは、この例のように、”line”という単語を使用しますが、一般的には、pow(…) + pow(…) + somethingElse(…)のように、単一のコード行に複数のサブコールが含まれている場合があります。

      したがって、実行が”サブコールの直後”に再開されると言う方が正確です。

      pow(2,1)

      プロセスが繰り返されます。5行に新しいサブコールが作成され、引数x=2n=1。,

      新しい実行コンテキストが作成され、前の実行コンテキストがスタックの上にプッシュされます。

      • Context:{x:2,n:1,at1}pow(2,1)
      • Context:{x:2,n:2,at5}pow(2,2)
      • Context:{x:2,n:3,at5}pow(2,3)

      現在は2つの古いコンテキストがあり、現在は1つがpow(2, 1)実行されています。,

      出口

      pow(2, 1)の実行中に、以前とは異なり、条件n == 1は真実であるため、ifの最初の分岐は機能します。

      function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); }}

これ以上のネストされた呼び出しはないので、関数は2を返して終了します。

関数が終了すると、その実行コンテキストはもう必要なくなるため、メモリから削除されます。, 前のコンテキストはスタックの先頭から復元されます。

  • Context:{x:2,n:2,at line5}pow(2,2)
  • Context:{x:2,n:3,at line5}pow(2,2)

前のコンテキストが復元されます。

  • Context:{x:2,n:3,at line5}pow(2,3)li終了すると、pow(2, 3) = 8の結果が得られます。

    この場合の再帰の深さは3でした。

    上の図からわかるように、再帰の深さはスタック内のコンテキストの最大数に等しくなります。

    メモリ要件に注意してください。 文脈は記憶を取る。, 私たちの場合、nの累乗に上げるには、実際にはnコンテキストのメモリが必要です。nのすべての低い値

    ループベースのアルゴリズムは、より多くのメモリ節約です:

    function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result;}

反復pow単一のコンテキスト変更を使用しますiおよびresultその過程で。 そのメモリ要件は小さく、固定されており、nに依存しません。,

任意の再帰はループとして書き換えることができます。 ループの異常をより効果的です。

…しかし、時には書き換えは、特に関数が条件に応じて異なる再帰的なサブコールを使用し、その結果をマージする場合、または分岐がより複雑な場合 そして最適化は努力の価値がない不必要、全くないかもしれない。

再帰は、より短いコードを提供し、理解しやすくサポートすることができます。 最適化はあらゆる場所で必要とされるわけではなく、ほとんどが良いコードが必要なので、それが使用されています。,

再帰トラバーサル

再帰のもう一つの優れたアプリケーションは、再帰トラバーサルです。

想像してみてください、私たちは会社を持っています。 スタッフの構造は、オブジェクトとして提示することができます。

言い換えれば、会社には部門があります。

  • 部門にはスタッフの配列がある場合があります。 たとえば、sales部門には、JohnとAliceの2人の従業員がいます。

  • または、developmentにはsitesinternalsのように、部門がサブデパートメントに分割されることがあります。 それぞれに独自のスタッフがいます。,

  • サブデパートメントが成長すると、サブデパートメント(またはチーム)に分割されることもあります。

    たとえば、将来のsites部門は、siteAsiteBのチームに分割される可能性があります。 いるので分割です。 それは写真にはなく、心に留めておくべきことだけです。

ここで、すべての給与の合計を取得する関数が欲しいとしましょう。 どうすればそれができますか?

反復的なアプローチは、構造が単純ではないため、容易ではありません。, 最初のアイデアは、forループをcompanyネストされたサブループを1レベルの部門にわたって作成することです。 しかし、次に、sitesのような2レベルの部門のスタッフを反復するために、より多くのネストされたサブループが必要です…そして、将来現れる3レベル に置かれていま3-4入れ子subloopsのコードをトラバース、単一のオブジェクトではなく醜い.

再帰を試してみましょう。,

わかるように、私たちの関数が部門を合計すると、二つの可能なケースがあります:

  1. それは人々の配列を持つ”単純な”部門です–その後、単純なループで給与を合計することができます。li>
  2. またはそれはNsubdepartmentsを持つオブジェクトです-その後、N再帰呼び出しを行って、各subdepsの合計を取得し、結果を組み合わせます。

1番目のケースは、配列を取得するときの再帰の基本、些細なケースです。

オブジェクトを取得する2番目のケースは再帰的なステップです。, 複雑なタスクは、小さな部門のサブタスクに分割されます。 彼らは順番に再び分割することができますが、遅かれ早かれ分割は(1)で終了します。

アルゴリズムはおそらくコードから読みやすいでしょう:

コードは短くて理解しやすいです(うまくいけば?). それが再帰の力です。 それはまたsubdepartmentの入れ子のあらゆるレベルのために働く。,

コールのダイアグラムは次のとおりです。

コードはスマートな機能を使用していることに注意してください。前にカバーしました:

  • メソッドarr.reduce配列の合計を取得するための章配列メソッドで説明しました。
  • ループfor(val of Object.values(obj))オブジェクト値を反復処理するには:Object.valuesそれらの配列を返します。,

再帰構造

再帰的な(再帰的に定義された)データ構造は、それ自体を部分的に複製する構造です。

上記の会社構造の例で見たことがあります。

会社の部門は次のとおりです。

  • どちらかの人々の配列です。
  • または部門を持つオブジェクト。

web開発者には、HTMLやXML文書など、よく知られている例があります。HTML文書において、HTML-tagは、以下のリストを含むことができる:<ul><li>テキストピースを含むことができる。

  • HTML-コメント。,
  • その他のHTMLタグ(テキスト部分/コメントまたはその他のタグなどを含むことができます)。
  • これは再び再帰的な定義です。

    理解を深めるために、場合によっては配列のより良い代替手段となる可能性のある”リンクリスト”という名前の再帰構造をカバーします。

    リンクリスト

    オブジェクトの順序付けられたリストを格納したいと想像してみてください。

    自然な選択は配列になります:

    let arr = ;

    …しかし、配列に問題があります。, “要素の削除”および”要素の挿入”操作は高価です。 たとえば、arr.unshift(obj)操作は、新しいobjのスペースを確保するためにすべての要素の番号を変更する必要があり、配列が大きい場合 同じarr.shift()

    一括再番号付けを必要としない唯一の構造変更は、配列の末尾で動作するものです:arr.push/pop。 したがって、最初に作業する必要がある場合、大きなキューでは配列がかなり遅くなる可能性があります。,

    あるいは、本当に高速な挿入/削除が必要な場合は、リンクリストと呼ばれる別のデータ構造を選択することができます。

    リンクされたリスト要素は、

    • valueでオブジェクトとして再帰的に定義されます。
    • next次のリンクリスト要素を参照するプロパティまたはnullそれが終わりであれば。,

    For instance:

    let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } }};

    Graphical representation of the list:

    An alternative code for creation:

    let list = { value: 1 };list.next = { value: 2 };list.next.next = { value: 3 };list.next.next.next = { value: 4 };list.next.next.next.next = null;

    Here we can even more clearly see that there are multiple objects, each one has the value and next pointing to the neighbour., list変数はチェーン内の最初のオブジェクトなので、nextポインタから任意の要素に到達できます。

    リストは簡単に複数の部分に分割し、後で結合することができます。

    let secondList = list.next.next;list.next.next = null;

    参加するには:

    list.next.next = secondList;

    そして、確かに我々は任意の場所に項目を挿入または削除することができます。,head of the list:

    To remove a value from the middle, change next of the previous one:

    list.next = list.next.next;

    We made list.next jump over 1 to value 2., 値1がチェーンから除外されるようになりました。 他の場所に保存されていない場合は、自動的にメモリから削除されます。

    配列とは異なり、大量の番号を変更することはなく、要素を簡単に再配置することができます。

    当然のことながら、リストは配列よりも優れているとは限りません。 そう利用のみのリストが表示されます。

    主な欠点は、要素にその番号で簡単にアクセスできないことです。 簡単な配列では:arr直接参照です。, しかし、リストでは、最初の項目から開始し、nextNN番目の要素を取得するために回を移動する必要があります。

    …しかし、そのような操作は必ずしも必要ではありません。 たとえば、queueやdequeが必要な場合–両端から要素を非常に高速に追加/削除できるようにする必要がありますが、その中間へのアクセスは必要ありません。

    リストを強化することができます:

    • プロパティを追加することができますprevnext前の要素を参照して、簡単に戻ることができます。,li>
    • リストの最後の要素を参照するtailという名前の変数を追加することもできます(最後から要素を追加/削除するとき
    • …データ構造はニーズに応じて異なる場合があります。

    概要

    用語:

    • 再帰は、それ自体から関数を呼び出すことを意味するプログラミング用語です。 再帰関数は、洗練された方法でタスクを解決するために使用できます。

      関数がそれ自身を呼び出すとき、それは再帰ステップと呼ばれます。, 再帰の基礎は、関数がそれ以上の呼び出しを行わないようにタスクを非常に単純にする関数の引数です。

    • 再帰的に定義されたデータ構造は、それ自体を使用して定義できるデータ構造です。

      たとえば、リンクリストは、リストを参照するオブジェクト(またはnull)からなるデータ構造として定義することができます。

      list = { value, next -> list }

    HTML要素ツリーやこの章の部門ツリーのようなツリーも自然に再帰的です:それらは分岐し、すべてのブランチは他のブランチを持つこと,

    再帰関数を使用して、sumSalaryの例で見たように、再帰関数を使用してそれらを歩くことができます。

    任意の再帰関数を反復関数に書き換えることができます。 そして、それは時々物を最適化するために必要です。 しかし、多くのタスクでは、再帰的な解決策は十分に速く、書き込みとサポートが容易です。