재귀 또는 while 루프


123

몇 가지 개발 인터뷰 관행, 특히 인터뷰에서 요청 된 기술 질문 및 테스트에 대해 읽었으며 장르의 말에 대해 몇 번이나 우연히 마쳤습니다. "Ok 루프로 문제를 해결했습니다. 이제 재귀 "또는"모두 100 줄 while 루프로이 문제를 해결할 수 있지만 5 줄 재귀 함수로 해결할 수 있습니까? " 기타

내 질문은 재귀가 if / while / for 구문보다 일반적으로 더 낫습니까?

솔직히 말해서 재귀는 힙보다 훨씬 작은 스택 메모리로 제한되기 때문에 많은 수의 함수 / 메소드 호출을 수행하는 것이 성능 관점에서 차선책이기 때문에 항상 바람직하지 않다고 생각했습니다. 틀리다 ...



73
재귀의 주제에서 이것은 매우 흥미로운 것처럼 보입니다.
dan_waterworth

4
@dan_waterworth도 도움이 될 것입니다 : google.fr/… 그러나 나는 항상 철자 가 틀린 것 같습니다 : P
Shivan Dragon

@ShivanDragon 나는 그렇게 생각했습니다 ^ _ ^ 어제 그것을 게시하는 것이 매우 적절했습니다 :-)
Neal

2
임베디드 환경에서 재귀에서 일한 것은 기껏해야 눈살을 찌푸리고 최악의 경우 공개적으로 막히게합니다. 제한된 스택 공간은 사실상 불법입니다.
Fred Thomsen

답변:


192

재귀는 루프보다 본질적으로 낫거나 나쁘지 않습니다. 각각은 장단점이 있으며 프로그래밍 언어 (및 구현)에 따라 다릅니다.

기술적으로 반복 루프는 하드웨어 수준에서 일반적인 컴퓨터 시스템에 더 잘 맞습니다. 머신 코드 수준에서 루프는 테스트와 조건부 점프에 불과하지만 재귀 (순진하게 구현)는 스택 프레임 푸시, 점프, 리턴 및 팝핑을 포함합니다. 스택에서. OTOH는 스택 푸시 / 팝을 피할 수 있도록 많은 재귀 사례 (특히 반복 루프와 매우 유사한 사례)를 작성할 수 있습니다. 재귀 함수 호출이 리턴하기 전에 함수 본문에서 마지막으로 발생하는 경우 가능하며 일반적으로 테일 호출 최적화 (또는 테일 재귀 최적화 )라고합니다. 적절하게 테일 콜에 최적화 된 재귀 함수는 대부분 머신 코드 레벨의 반복 루프와 동일합니다.

또 다른 고려 사항은 반복 루프에는 파괴적인 상태 업데이트가 필요하므로 순수한 (부작용이없는) 언어 시맨틱과 호환되지 않습니다. 이것이 Haskell과 같은 순수한 언어에 루프 구성이 전혀없는 이유이며, 다른 많은 함수형 프로그래밍 언어에는 완전히 부족하거나 가능한 한 피하는 것이 좋습니다.

인터뷰에서 이러한 질문이 너무 많이 나타나는 이유는 답변하기 위해서는 변수, 함수 호출, 범위 및 물론 루프 및 재귀와 같은 많은 중요한 프로그래밍 개념에 대한 철저한 이해가 필요하기 때문입니다. 두 가지 근본적으로 다른 각도에서 문제에 접근하고 동일한 개념의 다른 표현 사이를 이동할 수있는 정신적 유연성을 테이블에 가져옵니다.

경험과 연구에 따르면 변수, 포인터 및 재귀를 이해할 수있는 능력이있는 사람과 그렇지 않은 사람 사이에는 경계가 있다고합니다. 학습, 경험을 통해 프레임 워크, API, 프로그래밍 언어 및 관련 사례를 포함한 프로그래밍의 거의 모든 것을 얻을 수 있지만이 세 가지 핵심 개념에 대한 직관을 개발할 수없는 경우 프로그래머가되기에 부적합합니다. 간단한 반복 루프를 재귀 버전으로 변환하는 것은 비 프로그래머를 필터링하는 가장 빠른 방법에 관한 것입니다. 숙련되지 않은 프로그래머도 일반적으로 15 분 안에 처리 할 수 ​​있으며 언어에 구애받지 않는 문제이므로 후보자가 선택할 수 있습니다. 특유의 걸림돌 대신 자신이 선택한 언어.

인터뷰에서 이와 같은 질문을한다면 좋은 징조입니다. 예비 고용주는 프로그래밍 도구 매뉴얼을 암기 한 사람들이 아니라 프로그래밍 할 수있는 사람들을 찾고 있음을 의미합니다.


3
이 질문에 대한 답변이 가질 수있는 가장 중요한 측면을 다루기 때문에 주요 기술 부분을 설명하고 프로그래밍 영역에서이 문제가 어떻게 적용되는지에 대한 축소 된 뷰를 제공합니다.
Shivan Dragon

1
또한 동기화 프로그래밍에서 .next () 메서드가 포함 된 반복기에서 재귀 호출을 선호하여 루프를 피하는 패턴을 발견했습니다. 오래 실행되는 코드가 너무 CPU 욕심이되는 것을 유지한다고 가정합니다.
Evan Plaice

1
반복적 인 버전은 또한 값을 밀고 터지는 것을 포함합니다. 이 작업을 수행하려면 코드를 수동으로 작성해야합니다. 재귀 버전은 반복 알고리즘에서 스택으로 상태를 푸시하고 있습니다. 일반적으로 상태를 일부 구조로 푸시하여 수동으로 시뮬레이션해야합니다. 가장 사소한 알고리즘 만이 상태를 필요로하지 않으며,이 경우 컴파일러는 일반적으로 꼬리 재귀를 발견하고 반복 솔루션을 만들 수 있습니다.
Martin York

1
@tdammers 당신이 언급 한 연구를 읽을 수있는 곳을 알려주시겠습니까? "경험과 연구는 사람들 사이에 경계가 있다는 것을 제안합니다 ..."이것은 나에게 매우 흥미로운 것 같습니다.
Yoo Matsuo

2
언급 한 것을 잊어 버린 한 가지 반복 코드는 단일 실행 스레드를 처리 할 때 더 잘 수행되는 경향이 있지만 재귀 알고리즘은 여러 스레드에서 실행되는 경향이 있습니다.
GordonM

37

따라 다릅니다.

  • 퀵 정렬과 같은 일부 문제는 재귀 솔루션에 매우 적합합니다.
  • 일부 언어는 재귀를 실제로 지원하지 않습니다 (예 : 초기 FORTRAN)
  • 일부 언어는 재귀를 반복의 기본 수단으로 가정합니다 (예 : Haskell)

꼬리 재귀에 대한 지원 은 꼬리 재귀 및 반복 루프를 동등하게 만듭니다. 즉, 재귀가 항상 스택을 낭비 할 필요는 없습니다.

또한 재귀 알고리즘은 항상 명시 적 스택을 사용하여 반복적으로 구현할 수 있습니다 .

마지막으로 5 라인 솔루션은 아마도 100 라인 솔루션보다 항상 낫습니다 (실제로 동일하다고 가정).


5
좋은 답변입니다 (+1). "5 라인 솔루션은 아마도 100 라인 솔루션보다 항상 낫습니다."간결함이 재귀의 유일한 장점은 아니라고 생각합니다. 재귀 호출을 사용하면 다른 반복 값 사이의 기능적 종속성을 명시 적으로 만들 수 있습니다.
Giorgio

4
짧은 솔루션은 더 나은 경향이 있지만 지나치게 간결한 것이 있습니다.
dan_waterworth

5
@dan_waterworth는 "100 라인"에 비해 지나치게 간결 하기가 다소 어렵습니다
gnat

4
@Giorgio, 불필요한 코드를 제거하거나 명시적인 것을 암시함으로써 프로그램을 더 작게 만들 수 있습니다. 전자를 고수하는 한 품질이 향상됩니다.
dan_waterworth

1
@ jk, 나는 그것이 명시 적 정보를 암시 적으로 만드는 또 다른 형태라고 생각합니다. 변수가 사용되는 것에 대한 정보는 명시 적 이름에서 제거되고 암시적인 사용법으로 푸시됩니다.
dan_waterworth

17

프로그래밍에있어 "더 나은"에 대한 정의는 보편적으로 합의 된 것은 아니지만 "유지 / 읽기 더 쉽다"는 의미로 사용하겠습니다.

재귀는 반복 반복 구조보다 표현력이 뛰어납니다. while 루프는 꼬리 재귀 함수와 동일하고 재귀 함수는 꼬리 재귀가 아니기 때문에 이것을 말합니다. 강력한 구문은 읽기 어려운 작업을 수행 할 수 있기 때문에 일반적으로 나쁜 것입니다. 그러나 재귀는 가변성을 사용하지 않고 루프를 작성할 수있는 기능을 제공하며 내 마음에는 가변성이 재귀보다 훨씬 강력합니다.

따라서 낮은 표현력에서 높은 표현력에 이르기까지 반복 구성은 다음과 같이 쌓입니다.

  • 불변 데이터를 사용하는 테일 재귀 함수,
  • 불변 데이터를 사용하는 재귀 함수
  • 가변 데이터를 사용하는 while 루프
  • 가변 데이터를 사용하는 테일 재귀 함수
  • 가변 데이터를 사용하는 재귀 함수

이상적으로는 표현할 수있는 표현이 가장 적습니다. 물론, 언어가 테일 콜 최적화를 지원하지 않는 경우 루프 구성의 선택에 영향을 줄 수도 있습니다.


1
"while 루프는 꼬리 재귀 함수와 동일하며 재귀 함수는 꼬리 재귀 일 필요는 없습니다": +1. while 루프 + 스택을 사용하여 재귀를 시뮬레이션 할 수 있습니다.
조르지오

1
나는 100 %에 동의하지는 않지만 확실히 흥미로운 관점이므로 +1입니다.
Konrad Rudolph

좋은 답변과 일부 언어 (또는 컴파일러)가 테일 콜 최적화를 수행하지 않는다고 언급하면 ​​+1됩니다.
Shivan Dragon

@Giorgio, "while 루프 + 스택을 사용하여 재귀를 시뮬레이션 할 수 있습니다", 이것이 표현력이라고 말했습니다. 계산적으로, 그들은 똑같이 강력합니다.
dan_waterworth

@dan_waterworth : 정확하게, 당신이 대답에서 말했듯이 재귀를 시뮬레이션하기 위해 while 루프에 스택을 추가해야하기 때문에 재귀만이 while 루프보다 표현력이 좋습니다.
Giorgio

7

재귀는 종종 덜 분명합니다. 덜 명백한 것은 유지하기가 더 어렵다.

당신이 쓰는 경우 for(i=0;i<ITER_LIMIT;i++){somefunction(i);}주요 흐름, 당신은 완벽하게 분명 당신은 루프를 작성합니다. 글을 쓰면 somefunction(ITER_LIMIT);어떤 일이 일어날 지 분명하게 밝히지 않습니다. 내용보기 만 : 그 somefunction(int x)호출 somefunction(x-1)은 실제로 반복을 사용하는 루프 임을 알려줍니다. 또한 break;반복의 어딘가에 이스케이프 조건을 쉽게 넣을 수 없으므로 다시 전달되는 조건을 추가하거나 예외를 throw해야합니다. (그리고 예외는 다시 복잡성을 추가합니다 ...)

본질적으로 반복과 재귀 사이의 명백한 선택이라면 직관적 인 일을하십시오. 반복이 작업을 쉽게 수행한다면, 2 줄을 절약하는 것이 장기적으로 발생할 수있는 두통의 가치는 거의 없습니다.

물론 98 줄을 절약한다면 완전히 다른 문제입니다.

재귀가 완벽하게 맞는 상황이 있으며 실제로 드문 일이 아닙니다. 트리 구조, 다중 연결된 네트워크, 자체 유형의 다차원 들쭉날쭉 한 배열을 포함 할 수있는 구조 (기본적으로 간단한 벡터 나 고정 차원의 배열이 아닌 모든 것)의 순회. 알려진 직선 경로를지나 가면 반복하십시오. 알 수없는 상태로 뛰어들었다면 재귀하십시오.

기본적으로 somefunction(x-1)레벨 당 두 번 이상 자체에서 호출해야하는 경우 반복을 잊어 버리십시오.

... 재귀로 가장 잘 수행되는 작업에 대해 반복적으로 기능을 작성하는 것은 가능하지만 즐겁지 않습니다. 사용하는 곳마다 int다음과 같은 것이 필요합니다 stack<int>. 나는 실제적인 목적보다 운동으로 한 번 더했다. 일단 당신이 그런 일에 직면하면 당신이 말한 것과 같은 의심은 없을 것입니다.


10
분명한 것과 덜 분명한 것은 익숙한 것에 부분적으로 달려 있습니다. 반복은 CPU의 작동 방식에 더 가깝기 때문에 프로그래밍 언어에서 더 자주 사용되었습니다 (즉, 적은 메모리를 사용하고 더 빠르게 실행 함). 그러나 귀납적으로 생각하는 데 익숙하다면 재귀는 직관적 일 수 있습니다.
Giorgio

5
"루프 키워드를 보면 루프라는 것을 알 수 있습니다. 그러나 재귀 키워드는 없습니다. f (x) 내부에서 f (x-1)을보고 만 인식 할 수 있습니다.": 재귀 함수를 호출하면 재귀적임을 알고 싶지 않습니다. 마찬가지로 루프가 포함 된 함수를 호출 할 때 루프가 포함되어 있는지 알고 싶지 않습니다.
Giorgio

3
@SF : 예. 그러나 함수의 본문을 볼 때만 볼 수 있습니다. 루프의 경우 루프가 표시되고 재귀의 경우 함수가 자신을 호출하는 것을 볼 수 있습니다.
Giorgio

5
@SF : 나에게 순환 추론과 비슷한 것 같습니다. "직관에서 루프라면 루프입니다." 목록을 순회하고 목록의 각 멤버에 기능을 적용한다는 것이 직관적으로 명확하더라도 map재귀 함수 (예 : haskell.org/tutorial/functions.html 참조)로 정의 할 수 있습니다 .
Giorgio

5
@SF map는 키워드가 아니며 일반적인 기능이지만 약간 관련이 없습니다. 함수형 프로그래머가 재귀를 사용하는 경우 일반적으로 일련의 동작을 수행하려는 것이 아니기 때문에 해결중인 문제를 함수 및 인수 목록으로 표현할 수 있기 때문입니다. 그런 다음 문제는 다른 함수와 다른 인수 목록으로 축소 될 수 있습니다. 결국, 사소한 문제를 해결할 수 있습니다.
dan_waterworth

6

평상시와 같이, 이것은 실제로 사례 사이에 광범위하지 않고 유스 케이스 내에서 서로 다른 추가 요소가 있기 때문에 일반적으로 대답 할 수 없습니다. 다음은 몇 가지 압력입니다.

  • 짧고 우아한 코드는 일반적으로 길고 복잡한 코드보다 우수합니다.
  • 그러나 개발자 기반이 재귀에 익숙하지 않고 배우고 싶지 않거나 배우지 못하는 경우 마지막 포인트는 다소 무효화됩니다. 심지어 긍정적이 아닌 약간 부정적 이 될 수도 있습니다 .
  • 실제로 통화가 많이 필요 하고 테일 재귀를 사용할 수 없거나 환경에서 테일 재귀를 최적화 할 수없는 경우 재귀는 효율성이 떨어질 수 있습니다 .
  • 중간 결과를 제대로 캐시 할 수없는 경우 많은 경우 재귀도 좋지 않습니다. 예를 들어, 트리 재귀를 사용하여 피보나치 수를 계산하는 일반적인 예는 캐시하지 않으면 끔찍한 성능을 나타 냅니다. 캐시를 사용하면 간단하고 빠르며 우아하고 훌륭합니다.
  • 재귀는 어떤 경우에는 적용되지 않으며 다른 경우의 반복만큼 우수하고 다른 경우에는 절대적으로 필요합니다. 긴 비즈니스 규칙 체인을 통한 플로팅은 일반적으로 재귀로 전혀 도움이되지 않습니다. 데이터 스트림을 통한 반복은 재귀를 통해 유용하게 수행 할 수 있습니다. 다차원 동적 데이터 구조 (예 : 미로, 객체 트리 등)를 반복하는 것은 재귀, 명시 적 또는 암시 없이는 거의 불가능합니다. 이 경우 명시 적 재귀는 암시 적보다 훨씬 낫습니다. 누군가가 무서운 R- 단어를 피하기 위해 언어 내에서 임의의 불완전하고 버그가있는 스택을 구현 한 코드를 읽는 것보다 더 고통스러운 것은 없습니다.

재귀와 관련하여 캐싱한다는 것은 무엇을 의미합니까?
Giorgio

@Giorgio 아마도 memoization
jk.

페. 환경이 테일 콜을 최적화하지 않으면 더 나은 환경을 찾고 개발자가 재귀에 익숙하지 않은 경우 더 나은 개발자를 찾아야합니다. 몇 가지 표준이 있습니다.
CA McCann

1

재귀는 함수 호출을 반복하는 것에 관한 것이고, 루프는 메모리에 배치하기 위해 점프를 반복하는 것에 관한 것입니다.

스택 오버플로에 대해서도 언급해야합니다-http: //en.wikipedia.org/wiki/Stack_overflow


1
재귀는 자체 호출을 정의하는 함수를 의미합니다.
hardmath

1
간단한 정의가 정확히 100 % 정확하지는 않지만 스택 오버플로를 언급 한 유일한 사람입니다.
Qix

1

실제로 편의성 또는 요구 사항에 따라 다릅니다.

프로그래밍 언어 Python 을 사용하면 재귀를 지원하지만 기본적으로 재귀 깊이 (1000)에는 제한이 있습니다. 한도를 초과하면 오류 또는 예외가 발생합니다. 이 제한은 변경 될 수 있지만 그렇게하면 언어에 비정상적인 상황이 발생할 수 있습니다.

현재 (재귀 수준보다 많은 호출 수) 루프 구성을 선호해야합니다. 스택 크기가 충분하지 않으면 루프 구조를 선호해야합니다.


3
여기에 그가 (다른 언어는 별개의 전술적 접근 방법을 취하는 것이 개념을 지원) 파이썬에서 꼬리 재귀 최적화를하지 않는 이유에 귀도 반 로섬 (Guido van Rossum)에 의한 블로그입니다.
hardmath

-1

전략 디자인 패턴을 사용하십시오.

  • 재귀가 깨끗합니다
  • 루프는 (논쟁 적으로) 효율적입니다

하중 (및 / 또는 기타 조건)에 따라 하나를 선택하십시오.


5
무엇을 기다립니다? 전략 패턴은 여기에 어떻게 맞습니까? 그리고 두 번째 문장은 빈 문구처럼 들립니다.
Konrad Rudolph

@KonradRudolph 나는 재귀를 갈 것입니다. 매우 큰 데이터 집합의 경우 루프로 전환합니다. 그게 내 뜻이야 명확하지 않은 경우 사과드립니다.
Pravin Sonawane

3
아 글쎄, 나는 이것이 여전히 "전략 디자인 패턴"이라고 불릴 수 있는지 잘 모르겠다. 이것은 매우 고정 된 의미를 가지고 있으며 은유 적이다. 하지만 이제는 적어도 어디로 가는지 봅니다.
Konrad Rudolph

@KonradRudolph는 중요한 교훈을 배웠습니다. 말하고 싶은 것을 깊이 설명하십시오 .. 감사합니다 .. 도움이되었습니다 .. :)
Pravin Sonawane

2
@Pravin Sonawane : 꼬리 재귀 최적화를 사용할 수 있다면 거대한 데이터 세트에서도 재귀를 사용할 수 있습니다.
Giorgio
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.