재귀 함수가 몇 번이나 호출되었는지 추적


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)

위의 코드는 정수를 사용하여 자체 숫자로 곱하여 한 자리수로 줄입니다.

예는 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와 a입니다 -7. 이런 식으로 보면 -5x -7를 줄여 양수를 산출합니다 35. 또는가 5아닌 마이너스 부호 만 보길 7원합니까, 심지어 7는 실제로 마이너스이기도합니다. 2) 0을 포함하는 숫자를 어떻게 처리 할 계획입니까? 이것은 자동으로 축소를 제로화하기 때문입니다. 따라서 숫자가 많을수록 제로화 될 가능성이 높습니다. 다른 옵션은 0을 건너 뛰는 것입니다
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.

(주이 매우 빈번한 답변으로 제로를 가져 오는 소스가 있음 : 숫자 오버 플로우 ( 123345456999999999지고 123345457000000000. 등), 입력의 크기가 증가 할 때 당신은 거의 확실하게, 중간 값 곳으로 제로를 얻을 것이라는 사실을)


6
downvoters에게 : 나는 이것이 당신에게 이것이 가장 실용적인 해결책이 아니라는 것에 동의합니다. 그래서 나는 그것이 "순수하게 학문적"이라고 접두사로 붙인 이유입니다.
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 기능 실행이 모든 시간 counter에 폐기 및 세트를 얻을 것 0, 하지 않는 한 Sheraff가하는 당신이 명시 적으로 재귀 호출에 이월. AesingleDigit(number, ++counter)
zfrisch

2
바로 @zfrisch 나는 그것을 이해합니다. 시간을내어 설명해 주셔서 감사합니다
chs242

35
변경하시기 바랍니다 ++countercounter+1. 그것들은 기능적으로 동일하지만 후자는 의도를 더 잘 지정하고 (필수적으로) 돌연변이 및 매개 변수를 사용하지 않으며 실수로 후 증가 할 가능성이 없습니다. 또는 꼬리 호출이므로 루프를 대신 사용하십시오.
BlueRaja-대니 Pflughoeft

37

기존 솔루션은 다른 답변에서 제안한대로 카운트를 매개 변수로 함수에 전달하는 것입니다.

그러나 js에는 또 다른 해결책이 있습니다. 다른 답변은 재귀 함수 외부에서 카운트를 선언하는 것만 제안했습니다.

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

이것은 물론 작동합니다. 그러나 이로 인해 함수가 재진입되지 않습니다 (두 번 올바르게 호출 할 수 없음). 어떤 경우에는이 문제를 무시하고 단순히 singleDigit두 번 호출하지 않아야합니다 (자바 스크립트는 단일 스레드이므로 너무 어렵지 않습니다). 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

@customcommander 예, 나는 대답의 첫 부분에서 요약하여 이것을 언급했습니다- the traditional solution is to pass the count as a parameter. 이것은 클로저가있는 언어의 대체 솔루션입니다. 어떤면에서는 무한한 수의 변수 인스턴스 대신 하나의 변수이기 때문에 따라하기가 더 쉽습니다. 다른 방법으로이 솔루션을 아는 것은 추적중인 대상이 공유 객체 (독특한 맵 구성을 상상) 또는 매우 큰 객체 (예 : HTML 문자열) 일 때 도움이됩니다.
slebetman

counter--"두 번 올바르게 호출 할 수 없습니다"라는 주장을 해결하는 전통적인 방법 일 것입니다.
MonkeyZeus

1
@MonkeyZeus 그 차이는 무엇입니까? 또한, 우리가 찾고자하는 카운트임을 확인하기 위해 카운터를 초기화 할 숫자를 어떻게 알 수 있습니까?
slebetman

22

다른 방법은 모든 숫자를 생성하므로 생성기를 사용하는 것입니다.

마지막 요소는 당신의 숫자입니다 n 를 한 자리 줄이고 반복 횟수를 세려면 배열의 길이를 읽으십시오.

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>


마지막 생각들

당신은 고려하는 것이 좋습니다 함수에서 초기 상태를 . 거기에 제로로 모든 숫자는 것이다 0을 반환.

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

여기에는 많은 흥미로운 답변이 있습니다. 내 버전은 다른 흥미로운 대안을 제공한다고 생각합니다.

필요한 기능으로 몇 가지 작업을 수행합니다. 한 자릿수로 재귀 적으로 줄입니다. 중간 값을 기록하고 재귀 호출 횟수를 원합니다. 이 모든 것을 처리하는 한 가지 방법은 최종 결과, 취한 단계 및 호출 횟수를 모두 포함하는 데이터 구조를 반환하는 순수한 함수를 작성하는 것입니다.

  {
    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 이 음수이면 ( ) digitProduct을 반환 합니다. 당신의 절대 값에 사용할 수 있도록 N 및 사용을 또는 의 초기 값로 줄일 수 있습니다. NaN-39 ~> ('-' * '3') * '9'-11
customcommander

@customcommander : 실제로, 그것은 반환 {"digit":-39,"steps":[-39],"calls":0}이후 -39 < 9. 나는 이것이 오류 검사와 관련이 있다는 것에 동의하지만 매개 변수는 숫자입니까? - 양의 정수입니까? -등등. 나는 그것을 포함하기 위해 업데이트 할 것이라고 생각하지 않습니다. 이것은 알고리즘을 포착하며, 오류 처리는 종종 자신의 코드 기반에 따라 다릅니다.
Scott Sauyet

6

당신은 그냥 사라지게과 재귀에 대해 걱정하지 않습니다 얼마나 많은 시간을 계산하려는 경우 특별히 ... 당신은 단지 재귀를 제거 할 수 있습니다. 아래 코드는 num <= 9축소가 필요 하지 않은 것으로 원래 게시물에 충실합니다 . 따라서 singleDigit(8)count = 0, 그리고 singleDigit(39)count = 3다만 영업 이익과 허용 대답 보여주는 것 같은 :

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 의 대답 과 유사하게 결과를 배열로 사용 .map(Number)하지 않고 console.log배열로 푸시하고 .lengthfor를 사용 하는 count것이 훨씬 빠릅니다. 불행히도에 대한 customcommander 의 대답은 사용하는 발전기 기능은 정말 정말 느린 (대답은하지 않고 영업 이익에 비해 2.68x에 대한 느린 것입니다 .map(Number)console.log)

또한 .reduce사용하는 대신 실제 수학을 사용했습니다. 이 단일 변경만으로도 내 함수 버전을 3.59x까지 올렸습니다.

마지막으로 재귀 속도가 느려지고 스택 공간을 차지하고 더 많은 메모리를 사용하며 "재귀"할 수있는 횟수에 제한이 있습니다. 또는이 경우 전체 축소를 완료하는 데 사용할 수있는 축소 단계 수입니다. 반복 루프로 재귀를 롤아웃하면 스택의 모든 동일한 위치에 유지되며 완료에 사용할 수있는 축소 단계 수에 대한 이론적 인 제한이 없습니다. 따라서이 함수들은 실행 시간과 배열의 길이에 의해서만 제한되는 거의 모든 크기의 정수를 "감소"할 수 있습니다.

이 모든 것을 염두에두고 ...

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

위의 기능은 매우 빠르게 실행됩니다. 이는 영업 이익에 비해 (없이 빠른 3.13x에 관한 것입니다 .map(Number)console.log보다 빠른)와 8.4 배에 대해 customcommander 의 대답. console.logOP에서 삭제 하면 각 축소 단계에서 숫자가 생성되지 않습니다. 따라서이 결과를 배열로 푸시해야합니다.

PT


1
이 답변에는 많은 교육 가치가 있으므로 감사합니다. I feel good code is also fast.코드 품질 사전 정의 된 요구 사항 집합에 대해 측정해야 한다고 말합니다 . 성능이 그 중 하나가 아닌 경우 누구나 "빠른"코드로 이해할 수있는 코드를 대체하여 아무것도 얻지 못합니다. 아무도 보지 못했던 지점까지 리팩토링 된 코드의 양은 더 이상 이해할 수 없다고 생각하지 않을 것입니다. 마지막으로 게으른 생성 된 목록을 사용하면 필요에 따라 항목을 소비 할 수 있습니다.
customcommander

고마워요 이럴는보다 나를 위해 이해하기 쉬웠다 수행하는 방법의 실제 수학 .. 읽기 [...num+''].map(Number).reduce((x,y)=> {return x*y})또는 [...String(num)].reduce((x,y)=>x*y)내가 여기에 대부분의 대답에보고하고 있음을 진술. 그래서, 나에게,이 각 반복에서 세드릭의 더 나은 이해의 추가 혜택했다 훨씬 더 빨리합니다. 예, 축소 된 코드 (그 자리가 있음)는 읽기가 매우 어렵습니다. 그러나 이러한 경우에는 일반적으로 의식적으로 가독성을 고려하지 않고 잘라내어 붙여 넣기 및 이동하는 최종 결과 만 고려합니다.
포주 Trizkit

JavaScript에는 정수 나누기가 없으므로 C와 동등한 작업을 수행 할 수 digit = num%10; num /= 10;있습니까? 할 필요 num - x분할 가능성이 나머지를 얻기 위해했던 일에서 별도의 부서를 수행 할 JIT 컴파일러를 강제하는 것입니다 전에 후행 숫자를 제거하는 첫 번째.
Peter Cordes

나는 그렇게 생각하지 않는다. 이들은 var(JS는 ints 가 없다 ). 따라서 필요한 경우 float n /= 10;로 변환 n합니다. num = num/10 - x/10부동 소수 점수로 변환 할 수 있습니다. 따라서 리팩토링 된 버전을 사용하여 num = (num-x)/10;정수로 유지해야합니다 .JavaScript에서 단일 분할 연산의 몫과 나머지를 모두 줄 수있는 방법은 없습니다. 또한 digit = num%10; num /= 10;두 개의 별도 진술과 따라서 두 개의 별도 나누기 작업이 있습니다 .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
그러나 이렇게하면 카운터가 다음 통화를 계속 계산하므로 초기 통화마다 재설정해야합니다. 어려운 질문으로 이어집니다. 재귀 함수가 다른 컨텍스트에서 호출되는 방법,이 경우 전역 대 함수입니다.
RobG

이것은 생각을하기위한 예일뿐입니다. 사용자에게 그의 요구를 요청하여 수정 될 수 있습니다.
Kholiavko

@RobG 귀하의 질문을 이해하지 못합니다. 재귀 함수는 내부 함수이므로 클로저 외부에서 호출 할 수 없습니다. 따라서 가능한 상황이 하나뿐이기 때문에 상황을 구별 할 가능성이 없거나 구분할 필요가 없습니다
slebetman

@slebetman 카운터는 재설정되지 않습니다. 에 의해 반환 된 함수 singleDigitDecorator()는 호출 될 때마다 동일한 카운터를 계속 증가시킵니다.
customcommander

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
파이썬에서는 다음과 같은 것을 선호 합니다 .
phipsgabler
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.