再帰関数が呼び出された回数を追跡する


62

 function singleDigit(num) {
      let counter = 0
      let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

      if(number <= 9){
          console.log(number)
      }else{
          console.log(number)
          return singleDigit(number), counter += 1
      }
   }
singleDigit(39)

上記のコードは整数を受け取り、それをそれ自身の桁で乗算することにより1桁に減らします。

例は39です。

3 x 9 = 27.
2 x 7 = 14.
1 x 4 = 4.

コンソールはログに記録します:

27 
14 
4

再帰関数が3回呼び出されたことを追跡するにはどうすればよいですか?

カウンターを追加しようとしましたが、更新できません。どんな助けにも感謝します


4
.map(Number)*演算子はとにかく数値を強制するため、冗長です。;-)
RobG

4
いくつかの質問:1)負の数をどのように扱うつもりですか?たとえば、数値-57は実際にはa -50-7..です。このように見た場合、-5x -7を減らして正の数値を生成し35ます。それともあなたはそれが唯一のマイナスの符号と見たいと思ってやる5といない7にもカントー、7だけでなく、実際にはマイナスです。2)ゼロを含む数値をどのように扱うつもりですか?これは自動的に削減をゼロにするからです。したがって、渡す数値が大きいほど、ゼロになる可能性が高くなります。もう1つのオプションは、ゼロをスキップすることです
Pimp Trizkit

3
上記の質問は、再帰を数えることではなく、この質問で使用されているコンテンツのパズルを解く面に関するものであることに気づきました。私を許してください。
Pimp Trizkit

3
私の答えが気に入って光栄ですが、実際には、stackoverflow.com / a / 59570894/1346276が最もクリーンな一般的なバリアントだと思います。
phipsgabler

2
@phipsgabler時間をかけてインテリジェントで首尾一貫した答えを書く人なら誰でも、それに値するものです。ありがとう
chs242

回答:


25

これはほとんど純粋に学術的な変形ですが、この目的のために修正された固定小数点コンビネータを使用できます。

元の関数を少し短くして改善しましょう:

function singleDigit(n) {
    let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
    return digitProduct <= 9 ? digitProduct : singleDigit(digitProduct);
}

// singleDigit(123234234) == 0

このバリアントから、再帰呼び出しを分解してカリー化できます。

function singleDigitF(recur) {
    return function (n) {
        let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
        return digitProduct <= 9 ? digitProduct : recur()(digitProduct);
    };
}

この関数は現在、固定小数点コンビネーターで使用できます。具体的には、次のように(厳密な)JavaScriptに適合したYコンビネーターを実装しました。

function Ynormal(f, ...args) {
    let Y = (g) => g(() => Y(g));
    return Y(f)(...args);
}

私たちがいるところYnormal(singleDigitF, 123234234) == 0

トリックがやって来る。Yコンビネーターへの再帰を除外したので、その中の再帰の数を数えることができます。

function Ycount(f, ...args) {
    let count = 1;
    let Y = (g) => g(() => {count += 1; return Y(g);});
    return [Y(f)(...args), count];
}

Node REPLで簡単に確認すると、次のようになります。

> Ycount(singleDigitF, 123234234)
[ 0, 3 ]
> let digitProduct = (n) => [...(n + '')].reduce((x, y) => x * y, 1)
undefined
> digitProduct(123234234)
3456
> digitProduct(3456)
360
> digitProduct(360)
0
> Ycount(singleDigitF, 39)
[ 4, 3 ]

このコンビネータは、のスタイルで記述された再帰関数の呼び出し数をカウントするように機能しsingleDigitFます。

(非常に頻繁に回答としてゼロを取得する2つのソースがあることに注意してください:数値のオーバーフロー(に123345456999999999なる123345457000000000など)と、入力のサイズが大きくなると、どこかでほぼ確実に中間値としてゼロを取得します。)


6
反対投票者へ:これは最良の実用的な解決策ではないことに同意します-そのため、私は「純粋に学術的」という接頭辞を付けました。
phipsgabler

正直なところ、それは素晴らしいソリューションであり、元の質問の回帰/数学タイプに完全に適しています。
Sheraff

73

関数定義にカウンター引数を追加する必要があります。

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)

6
驚くばかり。関数で宣言したため、カウンターが機能していなかったようです
chs242

7
@ chs242スコープルールでは、関数で宣言すると、呼び出しごとに新しいスコープルールが作成されることになります。 stackoverflow.com/questions/500431/...
Taplar

10
@ chs242関数内で宣言したわけではありません。技術的には、すべてのデフォルトパラメータも同様です-あなたの場合、関数が再帰的に呼び出されたときに値が引き継がれなかっただけです。ae は、Sheraffのように再帰呼び出しで明示的に引き継がない限り、関数が実行counterされるたびに破棄され0、に設定されます。AesingleDigit(number, ++counter)
zfrisch

2
右@zfrischわかりました。それを説明するために時間を割いてくれてありがとう
chs242

35
に変更++counterしてくださいcounter+1。これらは機能的には同等ですが、後者は意図をより適切に指定し、(不必要に)変化したりパラメーター化したりせず、誤ってポストインクリメントする可能性がありません。さらに良いことに、これは末尾呼び出しなので、代わりにループを使用します。
BlueRaja-Danny Pflughoeft

37

従来の解決策は、別の回答で提案されているように、カウントをパラメーターとして関数に渡すことです。

ただし、jsには別の解決策があります。他のいくつかの答えは、単純に再帰関数の外でカウントを宣言することを提案しました:

let counter = 0
function singleDigit(num) {
  counter++;
  // ..
}

これはもちろん機能します。ただし、これにより関数は再入不可能になります(正しく2回呼び出すことはできません)。場合によっては、この問題を無視して、singleDigit2回呼び出さないようにすることもできます(JavaScriptはシングルスレッドなので、実行するのはそれほど難しくありません)。これは、singleDigit後で更新して非同期にすると、発生するのを待っているバグであり、醜い。

解決策は、counter変数をグローバルではなく外部で宣言することです。これは、JavaScriptにクロージャーがあるために可能です。

function singleDigit(num) {
  let counter = 0; // outside but in a closure

  // use an inner function as the real recursive function:
  function recursion (num) {
    counter ++
    let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

    if(number <= 9){
      return counter            // return final count (terminate)
    }else{
      return recursion(number)  // recurse!
    }
  }

  return recursion(num); // start recursion
}

これは、グローバルなソリューションが、あなたが呼び出すたびに似ていますsingleDigit(今ではない、それはの新しいインスタンスを作成します再帰関数)counterの変数を。


1
カウンター変数はsingleDigit関数内でのみ使用でき、引数imoを渡さずにこれを実行する代替のクリーンな方法を提供します。+1
AndrewL64

1
recursionは完全に分離されているので、最後のパラメーターとしてカウンターを渡しても完全に安全です。内部関数を作成する必要はないと思います。再帰の唯一の利益のためにパラメーターを持つという考えが気に入らない場合(ユーザーがそれらを混乱させる可能性があることを私は理解しています)Function#bind、部分的に適用された関数でそれらをロックします。
カスタムコマンダー

@customcommanderはい、私は私の回答の最初の部分でこれについて要約して述べました- the traditional solution is to pass the count as a parameter。これは、クロージャーを持つ言語の代替ソリューションです。変数インスタンスが無限にある可能性があるのではなく、変数が1つしかないため、いくつかの点で従うのが簡単です。他の方法で、このソリューションを知ることは、追跡しているものが共有オブジェクト(一意のマップを作成することを想像してください)または非常に大きなオブジェクト(HTML文字列など)である場合に
役立ち

counter--「2回正しく
呼び出せ

1
@MonkeyZeusその違いは何ですか?また、カウンタを初期化して、それが検出したいカウントであることを確認するために、どのような数値を知ることができますか?
slebetman

22

すべての数値を生成する別のアプローチは、ジェネレーターを使用することです。

最後の要素はあなたの数nを1桁の数に減らしたものであり、反復した回数を数えるには、単に配列の長さを読み取ります。

const digits = [...to_single_digit(39)];
console.log(digits);
//=> [27, 14, 4]
<script>
function* to_single_digit(n) {
  do {
    n = [...String(n)].reduce((x, y) => x * y);
    yield n;
  } while (n > 9);
}
</script>


最終的な考え

関数でreturn-early状態にすることを検討してください。ゼロが含まれる数値はゼロ返します。

singleDigit(1024);       //=> 0
singleDigit(9876543210); //=> 0

// possible solution: String(n).includes('0')

同じことは、数字1だけで作られたものについても言えます。

singleDigit(11);    //=> 1
singleDigit(111);   //=> 1
singleDigit(11111); //=> 1

// possible solution: [...String(n)].every(n => n === '1')

最後に、正の整数のみを受け入れるかどうかを明確にしませんでした。負の整数を受け入れる場合、それらを文字列にキャストすることは危険な場合があります。

[...String(39)].reduce((x, y) => x * y)
//=> 27

[...String(-39)].reduce((x, y) => x * y)
//=> NaN

可能な解決策:

const mult = n =>
  [...String(Math.abs(n))].reduce((x, y) => x * y, n < 0 ? -1 : 1)

mult(39)
//=> 27

mult(-39)
//=> -27

すごい。@customcommanderはこれを非常に明確に説明してくれてありがとう
chs242

6

ここには多くの興味深い答えがあります。私のバージョンはさらに興味深い代替手段を提供すると思います。

必要な機能でいくつかのことを行います。再帰的に1桁に減らします。中間値をログに記録し、行われた再帰呼び出しの数を求めます。これらすべてを処理する1つの方法は、最終結果、実行されたステップ、および呼び出しカウントをすべて1つに含むデータ構造を返す純粋な関数を記述することです。

  {
    digit: 4,
    steps: [39, 27, 14, 4],
    calls: 3
  }

その後、必要に応じてステップをログに記録したり、保存してさらに処理したりできます。

これはそれを行うバージョンです:

const singleDigit = (n, steps = []) =>
  n <= 9
    ? {digit: n, steps: [... steps, n], calls: steps .length}
    : singleDigit ([... (n + '')] .reduce ((a, b) => a * b), [... steps, n])

console .log (singleDigit (39))

を追跡しますstepsが、calls。追加のパラメーターを使用してコール数を追跡することはできますが、何も得られないようです。また、map(Number)ステップをスキップします。これらはいずれの場合も乗算によって数値に強制変換されます。

そのデフォルトのstepsパラメーターがAPIの一部として公開されることについて懸念がある場合は、次のような内部関数を使用して非表示にするのは簡単です。

const singleDigit = (n) => {
  const recur = (n, steps) => 
    n <= 9
      ? {digit: n, steps: [... steps, n], calls: steps .length}
      : recur ([... (n + '')] .reduce ((a, b) => a * b), [... steps, n])
  return recur (n, [])
}

どちらの場合も、桁の乗算をヘルパー関数に抽出する方が少しわかりやすいかもしれません。

const digitProduct = (n) => [... (n + '')] .reduce ((a, b) => a * b)

const singleDigit = (n, steps = []) =>
  n <= 9
    ? {digit: n, steps: [... steps, n], calls: steps .length}
    : singleDigit (digitProduct(n), [... steps, n])

2
別の素晴らしい答え;)nが負の場合、digitProductNaN-39 ~> ('-' * '3') * '9')を返すことに注意してください。したがって、nの絶対値を使用し、reduceの初期値として-1または1を使用することができます。
カスタムコマンダー

@customcommander:実際には、それが返され{"digit":-39,"steps":[-39],"calls":0}ているので、-39 < 9。これはいくつかのエラーチェックに関係している可能性があることに同意しますが、パラメーターは数値ですか?- 正の整数ですか?-など更新するつもりはありません。これはアルゴリズムをキャプチャし、エラー処理は多くの場合、コードベースに固有です。
Scott Sauyet

6

それが何回減少したかを数えようとしているだけで、特に再帰を気にしていない場合は...再帰を削除できます。以下のコードは、num <= 9削減が必要であるとは見なされないため、元の投稿に忠実です。したがって、singleDigit(8)必要がありますcount = 0、とsingleDigit(39)ありますcount = 3OPと受け入れ答えが実証されているだけのように、:

const singleDigit = (num) => {
    let count = 0, ret, x;
    while (num > 9) {
        ret = 1;
        while (num > 9) {
            x = num % 10;
            num = (num - x) / 10;
            ret *= x;
        }
        num *= ret;
        count++;
        console.log(num);
    }
    console.log("Answer = " + num + ", count = " + count);
    return num;
}

9以下の数値(つまりnum <= 9)を処理する必要はありません。残念ながら、OPコードは、num <= 9カウントしない場合でも処理します。上記のコードは、まったく処理もカウントもしませんnum <= 9。そのまま通過します。

.reduce実際の計算を実行する方がはるかに高速だったため、私は使用しないことを選択しました。そして、私にとっては、理解しやすいです。


スピードについてさらに考える

良いコードも速い感じがします。このタイプのリダクションを使用している場合(これは数値計算でよく使用されます)、大量のデータで使用する必要がある場合があります。この場合、速度が最も重要になります。

両方を使用.map(Number)し、console.log(各還元工程において)は、両方の非常に非常に長い実行し、不要になっています。.map(Number)OPから削除するだけで、約4.38倍高速化されます。削除するconsole.logと、それをスピードアップし、適切にテストすることはほとんど不可能でした(私はそれを待ちたくありませんでした)。

したがって、customcommanderの回答と同様に、norを使用せ.map(Number)console.log、結果を配列にプッシュして.lengthfor を使用するcountと、はるかに高速になります。残念ながら、カスタムコマンダーの回答では、ジェネレーター関数の使用は本当に遅いです(その回答は.map(Number)、およびなしのOPより約2.68倍遅いですconsole.log)。

また、代わりに.reduce実際の数学を使用しました。この1回の変更だけで、関数のバージョンが3.59倍に高速化されました。

最後に、再帰は遅く、スタック領域を消費し、より多くのメモリを使用し、「再帰」できる回数に制限があります。または、この場合、完全な削減を完了するために使用できる削減のステップ数。反復を反復ループにロールアウトすると、すべてがスタックの同じ場所に保持され、終了に使用できる削減ステップの数に理論上の制限はありません。したがって、これらの関数は、ここではほとんどすべてのサイズの整数を「削減」でき、実行時間と配列の長さに制限されます。

これらすべてを念頭に置いて...

const singleDigit2 = (num) => {
    let red, x, arr = [];
    do {
        red = 1;
        while (num > 9) {
            x = num % 10;
            num = (num - x) / 10;
            red *= x;
        }
        num *= red;
        arr.push(num);
    } while (num > 9);
    return arr;
}

let ans = singleDigit2(39);
console.log("singleDigit2(39) = [" + ans + "],  count = " + ans.length );
 // Output: singleDigit2(39) = [27,14,4],  count = 3

上記の関数は非常に高速に実行されます。それはOPよりも(なし速い3.13xについてです.map(Number)console.logより速い)と8.4xについてcustomcommanderの答え。console.logOPから削除すると、削減の各ステップで数値が生成されなくなることに注意してください。したがって、これらの結果を配列にプッシュする必要があります。

PT


1
この回答には多くの教育的価値があるので、感謝します。I feel good code is also fast.コードの品質、事前定義された一連の要件に対して測定する必要あると思います。パフォーマンスがそれらのいずれでもない場合、誰でも理解できるコードを「高速」コードに置き換えても何も得られません。私が見たコードの量が、だれもそれを理解できないほどパフォーマンスが高くなるようにリファクタリングされているとは思わないでしょう(何らかの理由で、最適なコードも文書化されていない傾向があります;)。最後に、遅延生成されたリストを使用すると、オンデマンドでアイテムを消費できることに注意してください。
カスタムコマンダー

ありがとうございます。私見では、これを行う方法の実際の数学を読むことよりも...私のために理解することが簡単だった[...num+''].map(Number).reduce((x,y)=> {return x*y})かさえ[...String(num)].reduce((x,y)=>x*y)、私がここで最も答えに見ていますことを声明。したがって、私にとって、これには、各反復で何が起こっているかをよりよく理解しはるかに速くなるという追加の利点がありました。はい、縮小されたコード(その場所があります)は非常に読みにくいです。しかし、それらの場合、一般的に意識してその可読性を気にするのではなく、カットアンドペーストして次に進む最終結果だけを気にします。
Pimp Trizkit

JavaScriptには整数除算がないので、Cと同等のことができますdigit = num%10; num /= 10;か?num - x除算する前に最初に後続の桁を削除する必要がある場合、JITコンパイラーは余りを得るために行った除算とは別の除算を強制する可能性があります。
Peter Cordes

私はそうは思いません。これらはvarsです(JSにはints はありません)。したがって、必要に応じてfloatにn /= 10;変換さnれます。num = num/10 - x/10これは、方程式の長い形式である浮動小数点に変換する場合があります。したがって、リファクタリングされたバージョンnum = (num-x)/10;を使用して整数を維持する必要があります。JavaScriptで、単一の除算演算の商と剰余の両方を提供できる方法を見つけることはできません。また、digit = num%10; num /= 10;2つの個別のステートメント、つまり2つの個別の除算演算です。Cを使用して久しぶりですが、Cもそうだと思いました。
Pimp Trizkit

6

console.countあなたの関数で呼び出しをしてみませんか?

編集:ブラウザーで試すスニペット:

function singleDigit(num) {
    console.count("singleDigit");

    let counter = 0
    let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

    if(number <= 9){
        console.log(number)
    }else{
        console.log(number)
        return singleDigit(number), counter += 1
    }
}
singleDigit(39)

Chrome 79とFirefox 72で動作しています


(上記の回答で説明したように)関数が呼び出されるたびにカウンターがリセットされるため、console.countは役に立ちません
chs242

2
ChromeとFirefoxで動作しているため、問題を理解できません。回答にスニペットを追加しました
Mistermatt

6

これにはクロージャーを使用できます。

counter関数のクロージャに格納するだけです。

次に例を示します。

function singleDigitDecorator() {
	let counter = 0;

	return function singleDigitWork(num, isCalledRecursively) {

		// Reset if called with new params 
		if (!isCalledRecursively) {
			counter = 0;
		}

		counter++; // *

		console.log(`called ${counter} times`);

		let number = [...(num + "")].map(Number).reduce((x, y) => {
			return x * y;
		});

		if (number <= 9) {
			console.log(number);
		} else {
			console.log(number);

			return singleDigitWork(number, true);
		}
	};
}

const singleDigit = singleDigitDecorator();

singleDigit(39);

console.log('`===========`');

singleDigit(44);


1
ただし、この方法では、カウンターは次の呼び出しでカウントし続けるため、最初の呼び出しごとにリセットする必要があります。これは難しい質問につながります。再帰関数が別のコンテキストから呼び出されたときの判別方法、この場合はグローバルvs関数です。
RobG

これは一例です。ユーザーのニーズを尋ねることで変更される可能性があります。
Kholiavko

@RobG質問が理解できません。再帰関数は内部関数であるため、クロージャーの外で呼び出すことはできません。唯一の可能な状況があるので、だから、何の可能性や分化に関連する必要はありません
slebetman

@slebetmanカウンタはリセットされません。によって返される関数は、singleDigitDecorator()呼び出されるたびに同じカウンタをインクリメントし続けます。
カスタムコマンダー

1
@slebetman —問題は、singleDigitDecoratorによって返された関数が再度呼び出されたときにカウンターをリセットしないことです。これは、カウンターをリセットするタイミングを知る必要がある関数です。それ以外の場合は、使用するたびに関数の新しいインスタンスが必要です。Function.callerの可能なユースケースは?;-)
RobG

1

slebetmanの回答で示唆されているように、ラッパー関数を使用してカウンターを簡略化するPythonバージョンは次のとおりです。この実装では、コアとなるアイデアが非常に明確であるため、これを記述しています。

from functools import reduce

def single_digit(n: int) -> tuple:
    """Take an integer >= 0 and return a tuple of the single-digit product reduction
    and the number of reductions performed."""

    def _single_digit(n, i):
        if n <= 9:
            return n, i
        else:
            digits = (int(d) for d in str(n))
            product = reduce(lambda x, y: x * y, digits)
            return _single_digit(product, i + 1)

    return _single_digit(n, 0)

>>> single_digit(39)
(4, 3)

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