Recursion and stack
Let’s return to functions and study them more in-depth.o nosso primeiro tópico será a recursão.
Se você não é novo na programação, então ele provavelmente é familiar e você pode saltar este capítulo.
recursão é um padrão de programação que é útil em situações em que uma tarefa pode ser naturalmente dividida em várias tarefas do mesmo tipo, mas mais simples. Ou quando uma tarefa pode ser simplificada em uma ação fácil mais uma variante mais simples da mesma tarefa. Ou, como veremos em breve, para lidar com certas estruturas de dados.,
Quando uma função resolve uma tarefa, no processo ela pode chamar muitas outras funções. Um caso parcial disto é quando uma função se chama a si mesma. Chama-se recursão.
Two ways of thinking
For something simple to start with-let’s write a function pow(x, n)
that raisesx
to a natural power ofn
. Em outras palavras, multiplies x
por si són
vezes.
pow(2, 2) = 4pow(2, 3) = 8pow(2, 4) = 16
Há duas maneiras de implementá-lo.,
Por Favor note como a variante recursiva é fundamentalmente diferente.
Quando pow(x, n)
é chamada, a execução se divide em dois ramos:
if n==1 = x /pow(x, n) = \ else = x * pow(x, n - 1)
- Se
n == 1
e, em seguida, tudo o que é trivial. É chamada de base da recursão, porque imediatamente produz o resultado óbvio:pow(x, 1)
é igual ax
.caso contrário, podemos representarpow(x, n)
comox * pow(x, n - 1)
., Em matemática, se escreveriaxn = x * xn-1
. Isso é chamado recursivo passo: transformar a tarefa em uma simples ação (multiplicação porx
) e um mais simples, chamada de a mesma tarefa (pow
com menorn
). Os próximos passos simplificam-na cada vez mais até quen
atinge1
.
também podemos dizer quepow
recursivamente se chama tilln == 1
.,
Por exemplo, para calcular pow(2, 4)
recursiva variante faz estes passos:
pow(2, 4) = 2 * pow(2, 3)
pow(2, 3) = 2 * pow(2, 2)
pow(2, 2) = 2 * pow(2, 1)
pow(2, 1) = 2
Então, a recursividade reduz a uma chamada de função para um mais simples, e então – mesmo a mais simples, e assim por diante, até que o resultado se torna óbvia.,
O número máximo de chamadas aninhadas (incluindo a primeira) é chamado de profundidade de recursão. No nosso caso, será exatamente n
.
a profundidade de recursão máxima é limitada pelo motor JavaScript. Podemos contar com 10000, alguns motores permitem mais, mas 100000 está provavelmente fora do limite para a maioria deles. Existem otimizações automáticas que ajudam a aliviar isso (“tail calls optimizations”), mas elas ainda não são suportadas em todos os lugares e funcionam apenas em casos simples.
isso limita a aplicação da recursão, mas ainda permanece muito largo., Há muitas tarefas onde a forma recursiva de pensar dá um código mais simples, mais fácil de manter.
o contexto de execução e a pilha
Agora vamos examinar como as chamadas recursivas funcionam. Para isso vamos olhar sob o capô das funções.
a informação sobre o processo de execução de uma função em execução é armazenada no seu contexto de execução.,
O contexto de execução é uma estrutura de dados interna que contém detalhes sobre a execução de uma função: onde o fluxo de controle é agora, com as variáveis, o valor de this
(nós não usá-lo aqui) e alguns outros detalhes internos.
uma chamada de função tem exatamente um contexto de execução associado a ela.
Quando uma função faz uma chamada aninhada, acontece o seguinte:
- A função actual está em pausa.
- o contexto de execução associado a ele é lembrado em uma estrutura de dados especial chamada de pilha de contexto de execução.,
- a chamada aninhada executa.
- Depois que termina, o contexto de execução antigo é recuperado da pilha, e a função externa é retomada de onde parou.
Let’s see what happens during the pow(2, 3)
call.
pow(2, 3)
No início da chamada pow(2, 3)
o contexto de execução irá armazenar variáveis: x = 2, n = 3
, o fluxo de execução é a linha 1
da função.,
podemos esboçar como:
- o Contexto: { x: 2, n: 3, na linha 1 } pow(2, 3)
quando a função começa a executar., A condição n == 1
é falsy, de modo que o fluxo continua em segundo ramo de if
:
function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); }}alert( pow(2, 3) );
As variáveis são as mesmas, mas as mudanças de linha, para que o contexto agora é:
- o Contexto: { x: 2, n: 3, na linha 5 } pow(2, 3)
Para calcular x * pow(x, n - 1)
, precisamos fazer uma subcall de pow
com novos argumentos pow(2, 2)
.,
pow(2, 2)
para fazer uma chamada aninhada, JavaScript lembra o contexto de execução atual na pilha de contexto de execução.
aqui chamamos a mesma funçãopow
, mas isso absolutamente não importa. O processo é o mesmo para todas as funções:
- O contexto atual é “lembrado” no topo da pilha.
- o novo contexto é criado para a subcall.
- Quando a subcall está terminada – o contexto anterior é estourado da pilha, e sua execução continua.,
Aqui está o contexto da pilha, quando entrou na subcall pow(2, 2)
:
- o Contexto: { x: 2, n: 2, linha 1 } pow(2, 2)
- o Contexto: { x: 2, n: 3, na linha 5 } pow(2, 3)
O novo contexto de execução atual está no topo (e ousada), e anterior lembrado contextos estão abaixo.
quando terminarmos a subcall – é fácil retomar o contexto anterior, porque mantém ambas as variáveis e o local exato do Código onde parou.,
Aqui na foto usamos a palavra “linha”, como no nosso exemplo só há uma subcall em linha, mas, geralmente, uma única linha de código pode conter vários subcalls, como pow(…) + pow(…) + somethingElse(…)
.
assim, seria mais preciso dizer que a execução recomeça “imediatamente após a subcall”.
pow(2, 1)
O processo se repete: um novo subcall é feita na linha 5
, agora com argumentos x=2
n=1
.,
Um novo contexto de execução é criado, o anterior é empurrado para o topo da pilha:
- o Contexto: { x: 2, n: 1, na linha 1 } pow(2, 1)
- o Contexto: { x: 2, n: 2, na linha 5 } pow(2, 2)
- o Contexto: { x: 2, n: 3, na linha 5 } pow(2, 3)
Existem 2 idade contextos agora e 1 atualmente em execução para pow(2, 1)
.,
sair
Durante a execução de pow(2, 1)
, ao contrário de antes, a condição n == 1
é truthy, então, a primeira filial do if
funciona:
function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); }}
não Existem mais chamadas aninhadas, então, a função termina, retornando 2
.
à medida que a função termina, seu contexto de execução não é mais necessário, então ele é removido da memória., O anterior é restaurado para fora do topo da pilha:
- o Contexto: { x: 2, n: 2, na linha 5 } pow(2, 2)
- o Contexto: { x: 2, n: 3, na linha 5 } pow(2, 3)
em Seguida, o contexto anterior é restaurado:
- o Contexto: { x: 2, n: 3, na linha 5 } pow(2, 3)
Quando terminar, temos um resultado de pow(2, 3) = 8
.
A profundidade de recursão neste caso foi: 3.
Como podemos ver a partir das ilustrações acima, a profundidade de recursão é igual ao número máximo de contexto na pilha.Note os requisitos de memória. Contextos levam memória., No nosso caso, aumentar o poder de n
requer, na realidade, a memória de n
contextos, para todos os menores valores de n
.
Um loop baseado no algoritmo é mais memória-poupança:
function pow(x, n) { let result = 1; for (let i = 0; i < n; i++) { result *= x; } return result;}
iterativo pow
usa um único contexto de mudança i
e result
no processo. Suas necessidades de memória são pequenas, fixas e não dependem de n
.,
qualquer recursão pode ser reescrita como um loop. A variante do laço geralmente pode ser feita mais eficaz.
… mas às vezes a reescrita não é trivial, especialmente quando a função usa subcalls recursivos diferentes dependendo das condições e funde seus resultados ou quando a ramificação é mais complexa. E a otimização pode ser desnecessária e totalmente não vale os esforços.
recursão pode dar um código mais curto, mais fácil de entender e suportar. Otimizações não são necessárias em todos os lugares, principalmente precisamos de um bom código, é por isso que é usado.,
traversais recursivos
outra grande aplicação da recursão é uma traversal recursiva.Imagine, temos uma empresa. A estrutura da equipe pode ser apresentada como um objeto:
em outras palavras, uma empresa tem departamentos.um departamento pode ter uma série de funcionários. Por exemplo, sales
department has 2 employees: John and Alice.
Ou de um departamento pode dividir em subdepartments, como development
tem dois ramos: sites
e internals
. Cada um deles tem o seu próprio pessoal.,
também é possível que quando um subdepartamento cresce, ele se divide em subsubdepartamentos (ou equipes).
Por exemplo, sites
departamento, no futuro, poderão ser divididos em equipas de siteA
e siteB
. E eles, potencialmente, podem dividir-se ainda mais. Isso não está na foto, apenas algo para ter em mente.
Agora vamos supor que queremos uma função para obter a soma de todos os salários. Como podemos fazer isso?
uma abordagem iterativa não é fácil, porque a estrutura não é simples., A primeira ideia pode ser fazer umfor
loop sobrecompany
com subloop aninhado sobre departamentos de primeiro nível. Mas então precisamos de mais subloops aninhados para iterar sobre o pessoal em departamentos de 2º nível como sites
… e então outro subloop dentro daqueles para departamentos de 3º nível que podem aparecer no futuro? Se colocarmos 3-4 subloops aninhados no código para atravessar um único objeto, ele se torna bastante feio.
vamos tentar recursão.,
Como podemos ver, quando a nossa função é um departamento para somar, há dois casos possíveis:
- Seja um “simples” departamento com uma variedade de pessoas–, então podemos somar os salários em um simples ciclo.
- Ou é um objeto com
N
subdepartments – então podemos fazer oN
chamadas recursivas para obter a soma para cada um dos subdeps e combinar os resultados.
o primeiro caso é a base da recursão, o caso trivial, quando obtemos um array.
O segundo caso quando temos um objeto é o passo recursivo., Uma tarefa complexa é dividida em sub-tarefas para departamentos menores. Eles podem, por sua vez, dividir novamente, mas mais cedo ou mais tarde a divisão terminará em (1).
o algoritmo é provavelmente ainda mais fácil de ler a partir do Código:
o código é curto e fácil de entender (Esperemos?). É o poder da recursão. Ele também funciona para qualquer nível de nidificação subdepartada.,
Aqui está o diagrama de chamadas:
Note que o código utiliza recursos inteligentes que nós falamos antes:
- Método
arr.reduce
explicado no capítulo Matriz de métodos para obter a soma da matriz. - Loop
for(val of Object.values(obj))
para iterar os valores dos objectos:Object.values
devolve uma lista deles.,
estruturas recursivas
uma estrutura de dados recursiva (definida recursivamente) é uma estrutura que se replica em partes.
acabamos de vê-lo no exemplo de uma estrutura da empresa acima.
um departamento da empresa é:
- ou um conjunto de pessoas.ou um objecto com departamentos.
para os programadores da web existem exemplos muito mais conhecidos: documentos HTML e XML.
no documento HTML, uma tag HTML pode conter uma lista de:
- peças de texto.
- HTML-comments.,
- outras tags HTML (que por sua vez pode conter peças de texto/comentários ou outras tags, etc).
que é mais uma vez uma definição recursiva.
para uma melhor compreensão, vamos cobrir mais uma estrutura recursiva chamada “lista vinculada” que pode ser uma melhor alternativa para arrays em alguns casos.
lista ligada
Imagine, queremos armazenar uma lista ordenada de objectos.
A escolha natural seria uma matriz:
let arr = ;
…Mas há um problema com matrizes., As operações” apagar elemento “e” inserir elemento ” são dispendiosas. Por exemplo, a operação tem que renumerar todos os elementos para criar espaço para um novo obj
, e se o array é grande, leva tempo. O mesmo acontece com arr.shift()
.
As únicas modificações estruturais que não requerem renumeração de massa são aquelas que operam com o fim da matriz: arr.push/pop
. Assim, uma matriz pode ser bastante lenta para grandes filas, quando temos que trabalhar com o início.,
Alternativamente, se realmente precisamos de inserção/exclusão rápida, podemos escolher outra estrutura de dados chamada de lista vinculada.
o elemento da lista ligada é recursivamente definido como um objecto com:
-
value
. -
next
property referencing the next linked list element ornull
if that’s the end.,
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., A variável
é o primeiro objeto na cadeia, então seguindonext
ponteiros a partir dela podemos alcançar qualquer elemento.
A lista pode ser facilmente dividido em várias partes e, mais tarde, juntou-se novamente:
let secondList = list.next.next;list.next.next = null;
Para participar:
list.next.next = secondList;
E, certamente, podemos inserir ou remover itens em qualquer lugar.,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
., O valor 1
está agora excluído da cadeia. Se não for armazenado em qualquer outro lugar, será automaticamente removido da memória.
Ao contrário de matrizes, não há renumeração em massa, podemos facilmente reorganizar elementos.
naturalmente, as listas nem sempre são melhores que as matrizes. Caso contrário, todos usariam apenas listas.
A principal desvantagem é que não podemos facilmente acessar um elemento pelo seu número. In an array that’s easy: arr
is a direct reference., Mas na lista precisamos começar do primeiro item e ir next
N
vezes para obter o elemento Nth.
… mas nem sempre precisamos de tais operações. Por exemplo, quando precisamos de uma fila ou mesmo de um deque – a estrutura ordenada que deve permitir adicionar/remover elementos muito rapidamente de ambas as extremidades, mas o acesso ao seu meio não é necessário.
as Listas podem ser reforçadas:
- podemos adicionar propriedade
prev
além denext
para referenciar o elemento anterior, para mover-se para trás facilmente., - Também podemos adicionar uma variável chamada
tail
referenciando o último elemento da lista (e atualizá-lo ao adicionar/remover elementos do final).a estrutura de dados pode variar de acordo com as nossas necessidades.
resumo
Termos:
-
recursão é um termo de programação que significa chamar uma função de si mesma. Funções recursivas podem ser usadas para resolver tarefas de formas elegantes.
Quando uma função se chama a si mesma, isso é chamado de passo recursivo., A base da recursão é argumentos de função que tornam a tarefa tão simples que a função não faz mais chamadas.
-
uma estrutura de dados definida recursivamente é uma estrutura de dados que pode ser definida usando-se a si mesma.
Por exemplo, a lista vinculada pode ser definida como uma estrutura de dados que consiste de um objeto referenciando uma lista (ou nulo).
list = { value, next -> list }
árvores como a árvore de elementos HTML ou a árvore de departamentos deste capítulo também são naturalmente recursivas: elas se ramificam e cada ramo pode ter outros ramos.,
funções recursivas podem ser usadas para caminhá-las como vimos no exemplo de
sumSalary
.qualquer função recursiva pode ser reescrita em uma função iterativa. E isso às vezes é necessário para otimizar as coisas. Mas para muitas tarefas uma solução recursiva é rápida o suficiente e mais fácil de escrever e suportar.