Articles

재귀와 스택

함수로 돌아가서 더 심층적으로 연구합시다.

우리의 첫 번째 주제는 재귀가 될 것입니다.프로그래밍에 익숙하지 않다면 아마 익숙 할 것이며이 장을 건너 뛸 수 있습니다.

재귀는 작업이 자연스럽게 같은 종류의 여러 작업으로 분할 될 수 있지만 더 간단한 상황에서 유용한 프로그래밍 패턴입니다. 또는 작업을 쉬운 동작과 동일한 작업의 더 간단한 변형으로 단순화 할 수있는 경우. 또는 곧 볼 수 있듯이 특정 데이터 구조를 처리합니다.,함수가 작업을 해결할 때 프로세스에서 다른 많은 함수를 호출 할 수 있습니다. 이것의 부분적인 경우는 함수가 스스로를 호출 할 때입니다. 이를 재귀라고합니다.

두 가지 방법을 생각

간단한 뭔가를 시작하자는 함수를 작성pow(x, n)발생시키는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.
  2. 그렇지 않으면pow(x, n)x * pow(x, n - 1)로 나타낼 수 있습니다., 수학에서는xn = x * xn-1를 작성합니다. 이라는 재귀적 단계:관리는 작업으로 간단한 동작(곱하여x)그리고 간단한 호출의 같은 작업(pow낮은n). 다음 단계는n1에 도달 할 때까지 더 단순화합니다.

우리는 또한pow재귀 적으로n == 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 engine 에 의해 제한됩니다. 우리는 그것이 10000 인 것에 의존 할 수 있으며,일부 엔진은 더 많은 것을 허용하지만,100000 은 아마도 대다수의 한계에서 벗어날 것입니다. 있는 자동적인 최적화를 완화하는 데 도움이(“꼬리 호출을 최적화”)지만,그들은 아직 어디서나 작업에서만 간단한 경우.

즉 재귀의 적용을 제한하지만 여전히 매우 넓게 남아 있습니다., 재귀 적 사고 방식이 더 간단한 코드를 제공하고 유지 관리가 더 쉬운 작업이 많이 있습니다.

실행 컨텍스트와 스택

이제 재귀 호출이 어떻게 작동하는지 살펴 보겠습니다. 이를 위해 우리는 기능의 후드 아래에서 살펴볼 것입니다.

실행중인 함수의 실행 프로세스에 대한 정보는 해당 실행 컨텍스트에 저장됩니다.,

실행 컨텍스트 내부 데이터 구조에 대한 세부 정보를 포함하는 실행하는 기능을 제어 흐름은 지금,현재는 변수 값의this(우리는 그것을 사용하지 않는 여기에)그리고 몇 가지 다른 내부 상세정보.

하나의 함수 호출에는 정확히 하나의 실행 컨텍스트가 관련되어 있습니다.

함수가 중첩 된 호출을 수행하면 다음과 같은 일이 발생합니다.

  • 현재 함수가 일시 중지됩니다.
  • 그와 관련된 실행 컨텍스트는 실행 컨텍스트 스택이라는 특수 데이터 구조에서 기억됩니다.,
  • 중첩 호출이 실행됩니다.
  • 가 끝나면 이전 실행 컨텍스트가 스택에서 검색되고 외부 함수는 중지 된 위치에서 다시 시작됩니다.

pow(2, 3)호출 중에 어떤 일이 발생하는지 봅시다.

pow(2,3)

의 시작 부분에서 전화pow(2, 3)실행 컨텍스트를 저장할 변수를x = 2, n = 3실행 흐름에서 선1의 기능입니다.,

우리는 우리 스케치할 수 있으로.

  • Context:{x:2,n:3,라인 1 에서}pow(2,3)

는 경우 이 함수가 실행되기 시작합니다., 상태n == 1은 falsy,그래서 계속으로 두 번째연의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, 에서 선 5}pow(2,3)

을 계산하는x * pow(x, n - 1),우리가 필요하 subcall 의pow새로운 인수pow(2, 2).,

pow(2,2)

중첩 호출을 수행하려면 JavaScript 는 실행 컨텍스트 스택에서 현재 실행 컨텍스트를 기억합니다.

여기서 우리는 동일한 함수pow를 호출하지만 절대적으로 중요하지 않습니다. 프로세스는 모든 기능에 대해 동일합니다.

  1. 현재 컨텍스트는 스택 위에”기억”됩니다.
  2. 하위 콜에 대해 새 컨텍스트가 생성됩니다.
  3. 서브 콜이 완료되면-이전 컨텍스트가 스택에서 팝되고 실행이 계속됩니다.,

여기에서의 컨텍스트 스택할 때 우리는 입력 subcallpow(2, 2)

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

새로운 현재 실행 컨텍스트에서 상단(bold),이전 기억 상황은 다음과 같습니다.

때 우리는 마무리 subcall–그것은 쉽게 이력서 이전의 문맥을 유지하기 때문에 두 변수 모두고 정확한 장소의 코드 그것은 중지되었습니다.,

참고:

여기에 사진 우리가 사용하는 단어를”라인”,으로 우리의 예에서는 단 하나의 subcall 라인에서,그러나 일반적으로 단일 코드의 라인을 포함할 수 있습 subcalls,같은pow(…) + pow(…) + somethingElse(…).

따라서 실행이”서브 콜 직후”다시 시작된다고 말하는 것이 더 정확할 것입니다.

pow(2,1)

프로세스가 반복:새로운 subcall 에서 선5,이제는 인수x=2,n=1.,

새 실행 컨텍스트를 만들 이전에 밀려서 최고의 스택:

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

있는 2 개의 오래된 상황 지금 1 현재 실행 중인pow(2, 1).,

출구

실행하는 동안의pow(2, 1),과는 달리,상태n == 1은 truthy,그래서 첫 번째연의if작동:

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

있는 더 이상 중첩된 통화를,그래서 그의 기능을 마감,반2.함수가 완료되면 실행 컨텍스트가 더 이상 필요하지 않으므로 메모리에서 제거됩니다., 이전 복원되지의 스택:

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

이전하는 컨텍스트가 복원됩니다:

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

이 완료되었을 때,우리는 우리가 결과의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사용하여 하나의 컨텍스트의 변화iresult에서 처리합니다. 메모리 요구 사항은 작고 고정되어 있으며n에 의존하지 않습니다.,

모든 재귀는 루프로 다시 쓸 수 있습니다. 루프의 변형이 일반적으로 더 효과적으로 만들 수 있습니다..

…그러나 때때로 재작성한 때,특히 기능을 사용하여 다른 재귀 subcalls 조건에 따라과 합병이 그들의 결과는 경우 또는 분기는 더 복잡하다. 그리고 최적화는 불필요하고 완전히 노력할 가치가 없을 수 있습니다.

재귀는 더 짧은 코드를 제공하고 이해하고 지원하기가 더 쉽습니다. 최적화는 모든 곳에서 필요하지 않으며,대부분 좋은 코드가 필요합니다.,

재귀 순회

재귀의 또 다른 훌륭한 응용 프로그램은 재귀 순회입니다.회사가 있다고 상상해보십시오. 즉,회사에는 부서가 있습니다.부서에는 직원의 배열이있을 수 있습니다. 예를 들어,sales부서에는 John 과 Alice 의 2 명의 직원이 있습니다.

  • 또는 부서로 분할할 수 있습 subdepartments,같은development두 가지:sitesinternals. 그들 각각은 그들 자신의 직원이 있습니다.,

  • 하위 섹션이 커지면 하위 하위 아파트(또는 팀)로 나눌 수도 있습니다.

    예를 들어,sites부서에서 미래될 수 있습 팀으로 분할에 대한siteAsiteB. 그리고 그들은 잠재적으로 더 많이 나눌 수 있습니다. 즉,마음에 가지고 단지 뭔가,사진에 없습니다.

  • 이제 모든 급여의 합계를 얻는 함수를 원한다고 가정 해 봅시다. 우리는 어떻게 그렇게 할 수 있습니까?반복 접근법은 구조가 단순하지 않기 때문에 쉽지 않습니다., 먼저 생각할 수 있을 만드는for루프company중첩된 subloop 이상 1 단계 부서입니다. 그러나 우리는 더 필요한 중첩된 subloops 반복하는 직원을 통해서 제 2 수준의 부서에 다음과 같이sites…그리고 다른 subloop 내부에 사람들을 위한 3 단계로 부서에서는 나타날 수 있습니다. 단일 객체를 트래버스하기 위해 코드에 3-4 개의 중첩 된 하위 루프를 넣으면 다소 추악 해집니다.

    재귀를 해봅시다.,

    우리가 볼 수 있듯이,우리의 기능을 가져옵부하는 합계는 두 가지 가능한 경우:

    1. 중 하나 그것은”간단한”부의 배열과 함께 사람들–그럼 우리는 할 수 있습 합 급여에서 간단한다.
    2. 또는 그것은 개체와 함께Nsubdepartments–다음 우리가 만들 수 있습니다N재귀를 호출하여 얻을 합의 각 subdeps 및 결과를 결합합니다.

    첫 번째 경우는 배열을 얻을 때 사소한 경우 인 재귀의 기본입니다.객체를 얻는 두 번째 경우는 재귀 단계입니다., 복잡한 작업은 소규모 부서의 하위 작업으로 분할됩니다. 그들은 차례로 다시 분할 될 수 있지만 조만간 분할은(1)에서 완료됩니다.알고리즘은 아마도 코드에서 읽기가 더 쉬울 것입니다.

    코드는 짧고 이해하기 쉽습니다(잘하면?). 그것이 재귀의 힘입니다. 또한 하위 아파트 중첩의 모든 수준에서 작동합니다.,

    여기에 다이어그램 전화:

    이 코드를 사용는 스마트 기능이 다루었습기:

    • 방법을arr.reduce설명했 장에서 배열하는 방법 합의 배열입니다.
    • 루프for(val of Object.values(obj))통해 반복하는 개체의 값:Object.values의 배열을 반환합니다.,

    재귀 구조

    재귀(재귀 적으로 정의 된)데이터 구조는 부분에서 자신을 복제하는 구조입니다.위의 회사 구조의 예에서 방금 보았습니다.

    회사 부서는 다음과 같습니다.

    • 사람들의 배열 중 하나.
    • 또는 부서가있는 객체입니다.

    웹 개발자에게는 훨씬 더 잘 알려진 예제가 있습니다:HTML 및 XML 문서.

    HTML 문서에서 HTML 태그는 다음과 같은 목록을 포함 할 수 있습니다.

    • 텍스트 조각.
    • 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는 직접 참조입니다., 하지만 목록에서 우리가 시작해야에서 첫번째 항목과 갈nextN번 N 번째 요소입니다.그러나 우리는 항상 그러한 작업을 필요로하지 않습니다. 예를 들어,우리가 필요한 큐 또는 심지어야 하는 주문된 구조할 수 있도록 해야 합니다 매우 빠른 추가/제거에서 요소를 모두 종료,하지만 액세스 중 필요하지 않습니다.

    목 향상 될 수 있습니다:

    • 우리는 우리를 추가 할 수 있습 속성을prev외에도next을 참조하려면 이전의 요소로 다시 이동하기 쉽다.,
    • 우리는 또한 추가할 수 있는 변수라는tail참조하는 마지막 요소의 목록(및 업데이트할 때 추가/제거 요소에서 결국).
    • …데이터 구조는 우리의 필요에 따라 다를 수 있습니다.

    요약

    용어

    • 재귀한 프로그래밍기적인 의미하는 함수를 호출하면서 자체입니다. 재귀 함수는 우아한 방법으로 작업을 해결하는 데 사용될 수있다.함수가 자체적으로 호출되면 재귀 단계라고합니다., 재귀의 기본은 함수가 추가 호출을하지 않도록 작업을 너무 단순하게 만드는 함수 인수입니다.

    • 재귀 적으로 정의 된 데이터 구조는 자체를 사용하여 정의 할 수있는 데이터 구조입니다.

      예를 들어,연결 목록을 정의할 수 있으로 데이터 구조로 구성된 객체 참조 목록(또는 null).

      list = { value, next -> list }

      나무 같은 HTML 을 요소를 트리거나 부리에서 이 장에서도 자연스럽게 재귀적:그들은 지점 및 각 지점할 수 있 다른 가지입니다.,

      재귀 함수는sumSalary예제에서 보았 듯이 그들을 걷는 데 사용할 수 있습니다.

    모든 재귀 함수는 반복적 인 함수로 다시 쓸 수 있습니다. 그리고 그것은 때로는 물건을 최적화하는 데 필요합니다. 그러나 많은 작업에 대해 재귀 솔루션은 충분히 빠르며 작성 및 지원이 더 쉽습니다.피>