🤔 Para Refletir
:
"Quer ouvir um paradoxo? Desenvolvedores que fazem jogos envolventes."
-
InterPlay
Javascript está desativado. Para uma melhor experiência, por favor, habilite o javascript no seu navegador antes de prosseguir.
You are using an out of date browser. It may not display this or other websites correctly.
You should upgrade or use an
alternative browser .
Estruturas de dados 101 - Introdução a ponteiros & Listas Ligadas
[anchor=ClaNKOwlKFYyhzKY][/anchor]
Estruturas de dados 101
condadobraveheart.com/forum/index.php?action=dlattach;attach=2753;type=avatar[/imgleft]
Oi \o
Faz um tempo que não posto nada, então pra matar a saudade, decidi montar um aulinha pra falar sobre um assunto que eu acho bem relevante pra área de computação e que raramente se ouve falar nas comunidades de RPG Maker:
estruturas de dados.
Inicialmente, eu ia criar um tópico só com todo o conteúdo, mas notei que estava crescendo demais (tipo, muito mesmo), então decidi por dividir as coisas.
[/colorbox]
[anchor=jpkqtkwrxNJuYhWl][/anchor][b][SIZE=29px]Sumário[/SIZE][/b]
[list][*][URL=#ClaNKOwlKFYyhzKY]Estruturas de dados 101[/URL][list][*][URL=#jpkqtkwrxNJuYhWl]Sumário[/URL]
[*][URL=#zofFMrTvQNJFrbJt]1 Introdução[/URL][list][*][URL=#ySlIWWGmzHCAUGhR]1.1 Então, o que são estruturas de dados?[/URL]
[*][URL=#sFJxkhkBxTMSpcMT]1.2 E o que é a memória?[/URL][list][*][URL=#fluCZTZlTMJccdqm]1.2.1 Brincando com a memória[/URL]
[*][URL=#iEFODauPQoNXBWeB]1.2.2 Vetores[/URL]
[*][URL=#OLaxGiWmpayXhIPt]1.2.3 Ponteiros[/URL]
[/list]
[/list]
[*][URL=#wyNtpxEpAAcRoZcz]2 Eis a lista ligada[/URL][list][*][URL=#YyvikVPaSwWrmNGR]2.1 Operações[/URL][list][*][URL=#dhsqMEaVAwriWrbn]2.1.1 Travessia[/URL]
[*][URL=#uACieyqHHWKFGbVO]2.1.2 Inserção[/URL][list][*][URL=#VEQykQRaajWkwZZR]2.1.2.1 Inserção no começo[/URL]
[*][URL=#MwvrAENdfHRyNlDR]2.1.2.2 Inserção no meio[/URL]
[*][URL=#dnLjMUtBRuqlqqPT]2.1.2.3 Inserção no fim[/URL]
[*][URL=#biMcmOLqhQQDBVHR]2.1.2.4 Inserção em ordem[/URL]
[*][URL=#rQEjamwyuykzaDpS]2.1.2.5 Inserção de lista[/URL]
[/list]
[*][URL=#nfmaRyNximwmnpUQ]2.1.3 Remoção[/URL][list][*][URL=#bmvBRnnovQyHIKLh]2.1.3.1 Remoção no começo[/URL]
[*][URL=#CcnYFwUGBWpNnGri]2.1.3.2 Remoção no meio[/URL]
[*][URL=#KNcbELOQKVYYbxoU]2.1.3.3 Remoção no fim[/URL]
[/list]
[/list]
[*][URL=#FeiSRiOzYgSTGwQa]2.2 Propriedades recursivas da lista ligada[/URL][list][*][URL=#lDGyqDmEVkuoRdkb]2.2.1 (Bônus) Ordenação de listas ligadas com recursão[/URL]
[*][URL=#ZwJEGQMdXErqxHWn]2.2.2 (Bônus) Inversão de listas ligadas com recursão[/URL]
[/list]
[/list]
[*][URL=#JhsTrVOhgWzUHVxG]3 Conclusão[/URL]
[/list]
[/list]
[anchor=zofFMrTvQNJFrbJt][/anchor][b][SIZE=29px]1 Introdução[/SIZE][/b]
[redalert][color=white][imgright width=96]imgur.com/J8KKa5Y.png[/imgright][SIZE=11px] [i]Disclaimer[/i]: Eu não sou especialista em nada do que falo aqui.
Estudei computação e aplico esse conhecimento no dia a dia no trabalho, logo sinto-me relativamente à vontade para expor o que eu sei, mas é bem possível que eu cometa erros por aqui.
Se acontecer, podem dar um toque! (e por favor, deem!)[/SIZE][/color][/redalert]
[anchor=ySlIWWGmzHCAUGhR][/anchor][b][SIZE=24px]1.1 Então, o que são estruturas de dados?[/SIZE][/b]
Quando falamos em programas de computador, temos duas unidades fundamentais: [url=https://pt.wikipedia.org/wiki/Algoritmo]algoritmos[/url] e [url=https://pt.wikipedia.org/wiki/Dados#Uso_de_dados_em_ci%C3%AAncia_e_inform%C3%A1tica]dados[/url]. Os algoritmos são procedimentos desenvolvidos para resolver certo problema, e os dados são a forma que usamos para representar as variáveis desse problema no
computador.
Podemos adotar a analogia de que, enquanto os algoritmos são o "como?" e os dados são o "o quê?".
Dessa forma, podemos pensar em dados como "coisas" que temos na memória. As estruturas de dados são formas de colocar dados complexos na memória, e também geralmente fazê-lo de forma que seu uso traga vantagens do ponto de vista
algorítmico (tornando uma busca em um conjunto mais rápida, por exemplo).
Fica mais fácil ver isso quando você entende de fato esse negócio de "memória" e vê as estruturas de fato, e porque usamos cada uma delas, então não vou me estender muito aqui.
[anchor=sFJxkhkBxTMSpcMT][/anchor][b][SIZE=24px]1.2 E o que é a memória?[/SIZE][/b]
[yellowalert][imgleft width=96]imgur.com/PJTv26r.png[/imgleft][SIZE=11px] [i]Nota[/i]: Essa parte trata principalmente de como as coisas ficam na memória do computador, como funciona o famoso "binário" que o computador entende e, frente a tudo isso, o que são ponteiros. Se você já tem noções
dessas coisas, pule para [URL=#wyNtpxEpAAcRoZcz]a lista ligada[/URL][/SIZE][/yellowalert]
Para fins práticos, nós vamos entender a memória como um [b]vetor de [font=Courier]bits[/font][/b], ou uma sequência arbitrariamente longa de [font=Courier]0[/font]s e [font=Courier]1[/font]s. Claramente, em um computador de verdade existe muito mais que isso, e temos algumas limitações (de espaço, por exemplo), mas essa aproximação serve pro que precisamos aqui.
Veja que é fácil quebrar a memória em blocos de 8 para formar números de 8 [font=Courier]bits[/font]. Chamaremos esse número (que vai de 0 a 255) de "[font=Courier]byte[/font]".
É relativamente fácil, usando bytes, representar números e até mesmo texto:
[list][*]Suponha que queiramos o número 1729, podemos montá-lo com dois bytes: [font=Courier]00000110 11000001[/font]
[*]Agora queremos o texto "ABC". Usando a tabela ASCII, criamos: [font=Courier]01000001 01000010 01000011[/font]
[*]Podemos até criar listas na memória, se definirmos o tamanho de cada elemento: [font=Courier]00000110 11000001 01000001 01000010 01000011[/font] pode ser uma lista de bytes [font=Courier][6, 193, 65, 66, 67][/font]
[/list]
[anchor=fluCZTZlTMJccdqm][/anchor][b][SIZE=19px]1.2.1 Brincando com a memória[/SIZE][/b]
Beleza, vimos que conseguimos criar números e texto na memória separadamente. Mas e se quisermos juntar um número e um texto numa mesma "coisa"?
Suponhamos que queremos uma "Pessoa", que é dada por uma idade e um nome.
[i]Notação:[/i]
[code]
Pessoa = { idade: byte, nome: string }
[/code]
Podemos criar então uma sequência assim na memória:
[code]
00010010 01001101 01100001 01110011 01101011 01100101 01100100 00000000
[/code]
... Pera, quê?
Não é óbvio o que acontece aí, mas se olharmos mais de perto, dá pra ver o seguinte:
[list][*][font=Courier]00010010[/font] = 18. A idade da nossa pessoa.
[*][font=Courier]01001101 01100001 01110011 01101011 01100101 01100100[/font] = [font=Courier]77 97 115 107 101 100[/font], que com a tabela ASCII transformamos em [font=Courier]Masked[/font]. O nome da nossa pessoa.
[/list]
O [font=Courier]00000000[/font] no final é usado para determinar onde a string do nome da pessoa termina, já que nós não sabemos de antemão seu tamanho, e a memória não faz mágica.
Olha que louco, já conseguimos criar pessoas na memória do computador! ([i]kind of[/i])
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Como ficaria uma estrutura de pessoa com [font=Courier]nome[/font] e [font=Courier]sobrenome[/font] na memória? [/color] [/bluealert]
[anchor=iEFODauPQoNXBWeB][/anchor][b][SIZE=19px]1.2.2 Vetores[/SIZE][/b]
Mais uma ideia: e se quisermos uma pessoa que possa ter um irmão?
Com o que já sabemos, podemos criar mais um pedaço de dados dentro da pessoa para fazer isso:
[code]
Pessoa = {
idade: byte,
nome: string,
irmão: { idade: byte, nome: string }
}
[/code]
Aí o nosso carinha de exemplo fica assim:
[code]
00010010 01001101 01100001 01110011 01101011 01100101 01100100 00000000 // { idade: 18, nome: "Masked"
00001001 01001110 01101001 01101110 01100101 01001011 00000000 // irmão: { idade: 9, nome: "NineK" } }
[/code]
Maneiro, e se quisermos colocar mais e mais irmãos na história, podemos sempre botar mais uma pessoa logo ao lado:
[code]
00010010 01001101 01100001 01110011 01101011 01100101 01100100 00000000 // { idade: 18, nome: "Masked"
00001001 01001110 01101001 01101110 01100101 01001011 00000000 // irmão: { idade: 9, nome: "NineK" },
00001001 01001011 01100001 01110111 01110100 01101000 01100001 01110010 00000000 // irmão2: { idade: 9, nome: "Kawthar" } }
[/code]
Podemos encarar isso também como um vetor em memória de pessoas. Porém, temos um problema: para vetores, é interessante sabermos o tamanho de cada elemento na memória, de forma que conseguimos achar qualquer elemento nele só pela posição.
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Tente desenvolver um algoritmo que encontre o n-ésimo elemento desse vetor. [/color]
[/bluealert]
Mas nossa [font=Courier]Pessoa[/font] não tem um tamanho fixo: o nome pode ter quantas letras quisermos, e seu fim é indicado apenas pelo byte nulo ([font=Courier]00000000[/font])
Uma solução seria fixar o tamanho do nome da pessoa (limitar a 30, por exemplo, pode servir), mas nem sempre queremos/podemos fazer isso.
E agora?
[anchor=OLaxGiWmpayXhIPt][/anchor][b][SIZE=19px]1.2.3 Ponteiros[/SIZE][/b]
Aí entram os ponteiros!
Lembre que a memória é uma lista de bits. Se ela é uma lista, podemos acessar os elementos dela pela posição. Quando guardamos uma posição da memória como um número, chamamos ela de [i]ponteiro[/i].
O legal pro nosso caso é que o ponteiro tem tamanho fixo, então podemos criar uma estrutura assim:
[code]
Pessoa = {
idade: byte,
nome: string* // Ponteiro para uma string
}
[/code]
E nosso vetor de [font=Courier]Pessoa[/font]s então vai ter elementos que tem sempre 1 [font=Courier]byte[/font] + o tamanho do ponteiro. Geralmente, ponteiros tem 4 ou 8 [font=Courier]bytes[/font], variando com a implementação do sistema (32-bit ou 64-bit). Aqui, nosso ponteiro terá 1 byte, porque já dá pro gasto.
O vetor de antes ficaria assim:
[code]
// [
// { idade: 18, nome: ponteiro[00101010] },
// { idade: 9, nome: ponteiro[01101010] },
// { idade: 9, nome: ponteiro[10101010] }
// ]
00010010 00101010 00001001 01101010 00001001 10101010
[/code]
E aí nos endereços [font=Courier]00101010[/font], [font=Courier]01101010[/font] e [font=Courier]10101010[/font]:
[code]
// [00101010]
01001101 01100001 01110011 01101011 01100101 01100100 00000000 // Masked
[...]
// [01101010]
01001110 01101001 01101110 01100101 01001011 00000000 // NineK
[...]
// [10101010]
00001001 01001011 01100001 01110111 01110100 01101000 01100001 01110010 00000000 // Kawthar
[/code]
De maneira mais abstrata, podemos ver esse vetor como algo assim: [img width=800]http://imgur.com/M6IAxAK.png [bluealert] imgur.com/QmObjQC.png[/imgleft] [color=white] [b]Exercício![/b]
Se tivermos o ponteiro do vetor que criamos, como podemos, sem percorrer nada, pegar o ponteiro para o segundo elemento nele? E o terceiro? [/color] [/bluealert]
Que tal agora o seguinte: pra toda pessoa, queremos que ela possa ter um irmão, mas esse irmão possa ter um outro irmão também, que possa ter outro irmão, e assim por diante. Ou seja, cada irmão é uma pessoa também:
[code]
Pessoa = {
idade: byte,
nome: string,
irmão: Pessoa
}
[/code]
Mas é meio complicado definirmos uma Pessoa assim, porque toda pessoa teria que ter outra pessoa dentro, que teria outra, que teria outra...
O jeito de resolver isso é usando ponteiros!
[i]Notação[/i]:
[code]
Pessoa = {
idade: byte,
nome: string,
irmão: Pessoa* // Ponteiro para uma pessoa
}
[/code]
E aí podemos criar o seguinte em um lugar qualquer na memória:
[code]
// { idade: 18, nome: "Masked", irmão: ponteiro[00101010] }
00010010 01001101 01100001 01110011 01101011 01100101 01100100 00000000 00101010
[/code]
E então na posição [font=Courier]00101010[/font] (42) da memória:
[code]
// { idade: 9, nome: "NineK", irmão: ponteiro[01101010] }
00001001 01001110 01101001 01101110 01100101 01001011 00000000 01101010
[/code]
E depois na posição [font=Courier]01101010[/font] (106) da memória:
[code]
// { idade: 9, nome: "Kawthar", irmão: null }
00001001 01001011 01100001 01110111 01110100 01101000 01100001 01110010 00000000 00000000
[/code]
Onde [font=Courier]null[/font] é um ponteiro para a posição 0, indicando a ausência de um irmão.
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Crie um vetor de pessoas com irmãos, usando o que vimos até agora. Veja que você pode representar uma família só com a primeira pessoa, já que ela tem ponteiros para os outros.
[/color] [/bluealert]
[anchor=wyNtpxEpAAcRoZcz][/anchor][b][SIZE=29px]2 Eis a lista ligada[/SIZE][/b]
Tadaaa \o/
Aprendemos nossa primeira (sem contar a [font=Courier]Pessoa[/font] e o vetor) estrutura de dados.
Listas ligadas são uma alternativa flexível para vetores em memória: em uma lista ligada, inserir e remover elementos no meio da lista e adicionar quantos elementos for necessário é bem simples, enquanto que em vetores na memória pode ser
necessário deslocar e realocar memória para fazer isso.
Podemos definir uma lista ligada, de forma genérica, por:
[code]
ListaLigada = {
primeiroNó: Nó*
}
Nó = {
dado: ???, // Qualquer coisa, pra gente tanto faz.
próximo: Nó*
}
[/code]
Uma lista ligada pode ser vista, de forma abstrata, da seguinte forma:
[img]https://imgur.com/xe1sLd6.png Onde as caixas representam os "nós" (elementos) da lista, e as setas representam os ponteiros entre eles. [greenalert]i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
Os ponteiros são para uma direção apenas: o primeiro nó sabe onde está o próximo, mas este não sabe onde está o que veio antes dele.
É possível criar uma lista onde todo elemento sabe quem vem depois e quem vem antes (uma lista [i]duplamente ligada[/i]), basta adicionar um ponteiro para o anterior em cada nó.
Infelizmente, isso adiciona complexidade à lista, uma vez que temos que garantir a estabilidade também do ponteiro do nó anterior.
Não implementaremos listas duplamente ligadas aqui, mas é um exercício interessante de se fazer uma vez que você dominar a lista ligada! [/greenalert]
[anchor=YyvikVPaSwWrmNGR][/anchor][b][SIZE=24px]2.1 Operações[/SIZE][/b]
[yellowalert][imgright width=96]imgur.com/ookG1eI.png[/imgright][SIZE=11px] [i]Nota[/i]: Por motivos de [i]cansei de escrever 0 e 1[/i] e praticidade, daqui pra frente não estaremos usando a memória diretamente como estávamos antes. Mesmo assim, todos os conceitos ainda se aplicam.
Seguiremos com pseudocódigo para demonstrar os algoritmos para as operações das estruturas de dados.[/SIZE][/yellowalert]
[anchor=dhsqMEaVAwriWrbn][/anchor][b][SIZE=19px]2.1.1 Travessia[/SIZE][/b]
Fazer uma travessia (percorrer) em uma lista ligada é a operação mais simples, e envolve simplesmente seguir os ponteiros da lista a partir do primeiro nó.
Um algoritmo para isso, tendo em vista nossa definição, seria:
[code]
def percorre(lista: ListaLigada):
atual = lista.primeiroNó
while atual != null:
// Fazemos alguma coisa usando o nó aqui...
atual = atual.próximo
[/code]
Percorrer a lista, por si só, não traz nenhum ganho. Mas podemos usar a travessia para trabalhar em cima dos nós da lista.
Um exemplo de algoritmo que usa a travessia seria a soma dos números de uma lista:
[code]
def soma(lista: ListaLigada):
total = 0
atual = lista.primeiroNó
while atual != null:
total += atual.dado
atual = atual.próximo
return total
[/code]
[greenalert][imgleft width=96]i.imgur.com/XhsOFdO.png[/imgleft] [b]Veja só[/b]
Por conta da natureza dos ponteiros, a travessia da lista ligada acontece do primeiro nó para o último, a não ser que a lista seja duplamente ligada. [/greenalert]
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Usando a travessia, crie um algoritmo que calcula a média aritmética dos números em uma lista ligada. [/color] [/bluealert]
[anchor=uACieyqHHWKFGbVO][/anchor][b][SIZE=19px]2.1.2 Inserção[/SIZE][/b]
Em linhas gerais, a inserção da lista ligada consiste apenas em manipular os ponteiros dos nós da lista e do que está sendo inserido.
A forma como isso acontece é um pouco diferente para inserção de nós no começo, no fim e no meio da lista, então veremos esses casos separadamente.
[anchor=VEQykQRaajWkwZZR][/anchor][b]2.1.2.1 Inserção no começo[/b]
A inserção no começo da lista também é bem simples: precisamos colocar um novo [font=Courier]primeiroNó[/font]. Só temos que nos preocupar em colocar o primeiro nó antigo logo depois do novo!
O algoritmo:
[code]
def insereNoComeço(lista: ListaLigada*, novoNó: Nó):
novoNó.proximo = lista.primeiroNó
lista.primeiroNó = novoNó
[/code]
[anchor=MwvrAENdfHRyNlDR][/anchor][b]2.1.2.2 Inserção no meio[/b]
Para inserir um nó no meio da lista, só precisamos de outro nó que esteja na lista. Esse nó não necessariamente precisa ser o primeiro, qualquer nó serve.
Isso acontece porque, para esse caso, precisamos apenas poder modificar o nó e saber qual nó vem após ele.
Um algoritmo simples para isso seria:
[code]
def insereDepois(noDaLista: Nó*, novoNó: Nó*):
novoNó.proximo = noDaLista.proximo
noDaLista.proximo = novoNó
[/code]
Não existem muitos casos especiais aqui, é só isso mesmo.
[greenalert][imgright width=96]i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
Esse algoritmo insere o novo nó [i]após[/i] o nó que passamos (e, inclusive, serve para inserir ao fim! Você vê porquê?). Para inserir [i]antes[/i] de dado nó, precisaríamos que nossa lista fosse duplamente ligada, e teríamos que tratar o caso especial em que inserimos o nó no começo da lista.
[/greenalert]
[anchor=dnLjMUtBRuqlqqPT][/anchor][b]2.1.2.3 Inserção no fim[/b]
Para inserir um elemento ao fim de um lista ligada, intuitivamente o que precisamos fazer é colocar o ponteiro do nó que queremos inserir, no ponteiro do próximo do último nó da lista.
Usando nossa definição da lista ligada, podemos criar uma função assim:
[code]
def insereNoFim(lista: ListaLigada, novoNó: Nó*):
nóAtual = lista.primeiroNó
while nóAtual.próximo != null:
nóAtual = nóAtual.próximo
nóAtual.próximo = novoNó
[/code]
Olha só! Essa função faz o que precisamos, certo?
Fica mais fácil de ver se você pensar que o primeiro nó sem próximo deve, com certeza, ser o último nó da lista. Assim, nosso algoritmo procura esse nó e define o ponteiro do próximo para o ponteiro do nó que queremos inserir.
Pronto, certo? Ainda não. Falta um caso especial: e se a lista estiver vazia?
Se a lista estiver vazia, o [font=Courier]primeiroNó[/font] é nulo, certo? E nesse caso, o que queremos na verdade é colocar o nosso nó como o primeiro mesmo (o fim de uma lista vazia é o começo, certo?).
Nosso algoritmo fica então:
[code]
def insereNoFim(lista: ListaLigada, novoNó: Nó*):
if lista.primeiroNó == null:
lista.primeiroNó = novoNó
else:
nóAtual = lista.primeiroNó
while nóAtual.próximo != null:
nóAtual = nóAtual.próximo
nóAtual.próximo = novoNó
[/code]
Esse algoritmo funciona para todos os casos da nossa lista! (Exceto o caso em que não existe último nó, mas veremos isso mais pra frente)
[greenalert][imgleft width=96]i.imgur.com/XhsOFdO.png[/imgleft] [b]Veja só[/b]
Existe ainda mais uma melhoria que podemos fazer em relação à performance desse código: veja que esse algoritmo precisa percorrer toda a lista antes de inserir um nó no fim.
Isso fica bem ruim quando nossa lista fica muito grande.
Mas nós podemos fazer todo esse processo em tempo constante, ou seja, se nossa lista tiver dez ou dez milhões de elementos, o tempo será o mesmo. Isso pode ser feito com um ponteiro na lista para o último nó! [/greenalert]
Uma lista com ponteiro para o último nó é praticamente igual à original, mas com mais um ponteiro:
[code]
ListaLigada = {
primeiroNó: Nó*,
últimoNó: Nó*
}
[/code]
[yellowalert][imgleft width=96]imgur.com/PJTv26r.png[/imgleft][SIZE=11px] [i]Nota[/i]: O esforço de colocar esse nó a mais é quase nulo, e ele simplifica bastante a inserção no fim. Mas ele pode tornar as outras inserções mais complicadas, porque temos sempre que atualizar o ponteiro quando a lista
estava vazia.
Por isso, vamos seguir usando nossa definição de lista com apenas um ponteiro. A implementação das inserções com essa nova definição fica por conta do leitor.
Aliás...[/SIZE] [/yellowalert]
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Crie um algoritmo para cada inserção que vimos usando a nossa lista com ponteiro para o último nó. Veja o que muda em cada um. [/color] [/bluealert]
[anchor=biMcmOLqhQQDBVHR][/anchor][b]2.1.2.4 Inserção em ordem[/b]
Em diversos casos, é interessante que nossa lista esteja ordenada.
Para essa inserção em específico, vamos assumir que lista [i]já está ordenada[/i], especificamente em ordem crescente, embora as mudanças necessárias para contar com ordem decrescente sejam desprezíveis.
Mas veja que uma lista vazia, ou com apenas um elemento, é claramente ordenada. Assim, podemos criar listas ligadas ordenadas facilmente, se inserirmos todos os elementos na lista dessa forma.
De todo modo, eu gostaria de dizer que existe alternativa melhor, mas o melhor algoritmo para inserção em ordem em listas ligadas é o mais besta mesmo: procura-se o primeiro elemento maior que o que estamos inserindo na lista,
e insere-se o novo valor logo atrás dele.
Basicamente, nosso algoritmo é o seguinte:
[code]
def insereEmOrdem(lista: ListaOrdenada, novoNó: Nó*):
if lista.primeiroNó.dado > novoNó.dado:
novoNó.próximo = lista.primeiroNó
lista.primeiroNó = novoNó
else:
anterior = lista.primeiroNó
atual = anterior.próximo
while atual != null && atual.dado <= novoNó.dado:
anterior = atual
atual = atual.próximo
insereDepois(anterior, novoNó)
[/code]
Claramente, se nosso primeiro elemento é maior que o que queremos inserir, inserimos ele no começo da lista. Caso contrário, avançamos os ponteiros de [font=Courier]anterior[/font] e [font=Courier]atual[/font] até encontrarmos um [font=Courier]atual[/font] cujo [font=Courier]dado[/font] é maior que o do [font=Courier]novoNó[/font].
Assim, inserimos o [font=Courier]novoNó[/font] logo após o [font=Courier]anterior[/font], e temos uma inserção em ordem.
[bluealert] [imgright width=128]http://imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Adapte essa inserção para manter a lista em ordem [i]decrescente[/i]. [/color] [/bluealert]
[anchor=rQEjamwyuykzaDpS][/anchor][b]2.1.2.5 Inserção de lista[/b]
As listas ligadas tem uma propriedade muito interessante, que inclusive é uma de suas grandes vantagens em relação a vetores em memória, por exemplo: podemos inserir listas ao começo, no meio e ao fim de outras listas da mesma forma que
inserimos nós!
Louco né? Deixa eu explicar.
Nos três primeiros algoritmos de inserção que fizemos (a inserção em ordem não funciona aqui, infelizmente), contamos apenas com uma propriedade dos nós da lista: eles têm um ponteiro para o próximo (que pode ser nulo).
Mas se essa é a única propriedade de que dependemos, então veja que uma lista ligada pode ser vista como um "nó" também: podemos inserir seu [font=Courier]primeiroNó[/font] como o [font=Courier]próximo[/font] de outro nó, e podemos definir o ponteiro [font=Courier]próximo[/font] de seu último nó para outro nó também.
Fica mais claro com imagens. Imagine as seguintes listas ligadas:
[img]https://imgur.com/LcDKwRg.png Podemos facilmente inserir a lista B no começo, no meio ou no fim da lista A, mudando apenas os ponteiros de primeiroNó ou próximo de algum nó em A, e próximo do último nó de B. Veja:No começo
Inserção no começo: O primeiroNó de A vira Pet (primeiroNó de B), o próximo do último nó de B vira Masked (primeiroNó de A).
No meio Inserção no meio: Para inserir a lista B entre Masked e NineK , mudamos o próximo de Masked para o primeiro de B, e o próximo do último de B para NineK .
No fim Inserção no fim: o processo é quase idêntico à inserção de B no começo de A, mas neste caso inserimos A no começo de B.
Às operações de inserção de lista no começo e no fim, damos o nome especial de concatenação . A concatenação de listas ligadas é bem simples, e o algoritmo é trivial uma vez que definimos a inserção de nós no fim de listas:
Código:
def concatena(listaA: ListaLigada, listaB: listaLigada):
insereNoFim(listaA, listaB.primeiroNó)
Veja que esse algoritmo já cobre a inserção no começo e no fim, pois podemos inverter a ordem dos parâmetros:
Código:
concatena(A, B) // Insere B ao fim de A
concatena(B, A) // Insere B ao começo de A, ou A ao fim de B. Dá no mesmo.
A inserção de lista no meio de outra lista pode parecer mais complexa, mas também é fácil de implementar usando as funções que já temos:
Código:
def insereListaDepois(nó: Nó*, lista: ListaLigada):
insereNoFim(lista, nó.próximo)
nó.próximo = lista.primeiroNó
Observe que a inserção de listas sempre usa inserções ao fim. Isso indica que a concatenação de listas é muito mais eficiente em listas ligadas com ponteiro para o último nó. [bluealert] imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Implemente uma inserção de lista ligada para listas com ponteiro para o fim.
Lembre de tratar com cuidado o caso em que mudamos o último nó da lista. [/color] [/bluealert]
[anchor=nfmaRyNximwmnpUQ][/anchor][b][SIZE=19px]2.1.3 Remoção[/SIZE][/b]
A remoção na lista ligada é um processo bem simples na maioria dos casos, e é bem fácil vê-la como um processo inverso à inserção mesmo.
O que faremos nos algoritmos de remoção é, basicamente, fazer o ponteiro do nó anterior apontar direto para o próximo. Assim, "pulamos" o nó que está sendo removido, efetivamente tirando ele da lista.
Assim como a inserção, a remoção pode ser dividida em casos:
[anchor=bmvBRnnovQyHIKLh][/anchor][b]2.1.3.1 Remoção no começo[/b]
Para a remoção no começo, nosso primeiro nó sai da lista. Esse é um caso bem simples, já que podemos resolver isso simplesmente mudando nosso ponteiro do primeiro nó para o próximo:
[code]
def removeDoComeço(lista: ListaLigada):
lista.primeiroNó = lista.primeiroNó.próximo
[/code]
[greenalert][imgright width=96]i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
Para a lista com ponteiro para o último, essa remoção tem um caso especial: quando o primeiro nó é também o último.
Nesse caso, temos que mudar o ponteiro do último nó também. Isso é bem simples, visto que se o primeiro nó é o último, ao removê-lo temos uma lista vazia, e então [font=Courier]primeiroNó[/font] = [font=Courier]últimoNó[/font] = [font=Courier]null[/font]. [/greenalert]
[anchor=CcnYFwUGBWpNnGri][/anchor][b]2.1.3.2 Remoção no meio[/b]
Vamos definir nosso algoritmo de remoção de forma parecida com a inserção no meio:
[code]
def removeDepois(nó: Nó*):
nó.próximo = nó.próximo.próximo
[/code]
De forma análoga à inserção no meio, nosso algoritmo remove um nó que vem após outro.
Veja que é necessário saber o nó anterior ao que queremos remover para fazer esse "pulo" com os ponteiros.
[greenalert][imgright width=96]i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
A depender da implementação, você pode querer ter certeza que existe nó após o nó que usamos de base para a remoção. Isso porque se esse [font=Courier]próximo[/font] for [font=Courier]null[/font], teremos problemas para acessar [font=Courier]próximo.próximo[/font]. [/greenalert]
[anchor=KNcbELOQKVYYbxoU][/anchor][b]2.1.3.3 Remoção no fim[/b]
Um algoritmo que remove ao fim consiste em dois passos: encontrar o nó anterior ao último, e definir seu [font=Courier]próximo[/font] como nulo.
A implementação disso é a seguinte:
[code]
def removeDoFim(lista: ListaLigada):
anterior = null
nóAtual = lista.primeiroNó
while nóAtual.próximo != null:
anterior = nóAtual
nóAtual = nóAtual.próximo
anterior.próximo = null
[/code]
Esse é um pouco mais complicado que os outros, mas também não deve ser um problema muito grande: veja que, no começo, usamos a mesma repetição que usamos para achar o último nó na inserção no fim, com um adicional: o ponteiro
[font=Courier]anterior[/font].
Esse ponteiro [font=Courier]anterior[/font] é essencial para realizarmos a remoção, como vimos no caso da remoção no meio da lista.
Esse algoritmo, porém, não trata do caso em que a lista tem apenas um nó (i.e. o primeiro nó é o último). Podemos corrigir isso:
[code]
def removeDoFim(lista: ListaLigada):
if lista.primeiroNó.próximo == null:
lista.primeiroNó = null
else:
anterior = null
nóAtual = lista.primeiroNó
while nóAtual.próximo != null:
anterior = nóAtual
nóAtual = nóAtual.próximo
anterior.próximo = null
[/code]
O algoritmo é o mesmo, mas no caso em que o [font=Courier]próximo[/font] do [font=Courier]primeiroNó[/font] é nulo, ele define que o [font=Courier]primeiroNó[/font] seja nulo.
Se pensarmos na própria lista como um "nó" anterior a todos os outros, cujo [font=Courier]próximo[/font] é o [font=Courier]primeiroNó[/font], esse caso é basicamente igual ao outro.
[greenalert][imgright width=96]i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
Esse algoritmo é trivial e pode ser executado em tempo constante se nossa lista for duplamente ligada e com ponteiro para o final. [/greenalert]
[bluealert] [imgleft width=128]imgur.com/QmObjQC.png[/imgleft] [color=white] [b]Exercício![/b]
Tente implementar os algoritmos de remoção com listas duplamente ligadas e com ponteiro para o último nó. Cuidado com os casos especiais. [/color] [/bluealert]
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Usando a travessia e as remoções que aprendemos, crie um algoritmo que, dado um valor, procura ele na lista e remove o nó correspondente. [/color] [/bluealert]
[anchor=FeiSRiOzYgSTGwQa][/anchor][b][SIZE=24px]2.2 Propriedades recursivas da lista ligada[/SIZE][/b]
Uma lista ligada pode ser definida sem uso de uma classe especial para nó. Isso é feito da seguinte forma:
[code]
ListaLigada = {
dado: ???,
sublista: ListaLigada*
}
[/code]
Veja que, nessa definição, o [font=Courier]dado[/font] está diretamente na lista, e o que seria o [font=Courier]próximoNó[/font] é uma [font=Courier]sublista[/font].
Essa definição é possível porque o conjunto de todos os nós após o primeiro em uma lista ligada ainda é uma lista ligada. Leva um tempinho pra digerir, mas quando você entende fica bem intuitivo.
Os algoritmos para a lista ligada podem também ser definidos de forma recursiva. Nosso algoritmo para inserção ao fim com essa definição, por exemplo, seria:
[code]
def insereNoFim(lista: ListaLigada, sublista: ListaLigada*):
if listaLigada.sublista == null:
listaLigada.sublista = sublista
else:
insereNoFim(listaLigada.sublista, sublista)
[/code]
[bluealert] [imgright width=128]imgur.com/BFxxtmx.png[/imgright] [color=white] [b]Exercício![/b]
Tente implementar os algoritmos que já vimos de forma recursiva! Você pode usar tanto a definição recursiva de lista que criamos quando a original, já que as duas são equivalentes.
[/color] [/bluealert]
[yellowalert][imgleft width=96]imgur.com/PJTv26r.png[/imgleft][SIZE=11px]
[i]Nota[/i]: Daqui pra baixo até a [URL=#nbRSSqMPriuQZkHa]fila[/URL], temos conteúdo extra com alguns algoritmos recursivos para listas ligadas.
Esses algoritmos não são essenciais para o seu entendimento de listas ligadas (existem alternativas não-recursivas praticamente triviais para eles), mas são um exercício bem interessante pra quem quer aprender a lidar com recursão!
Mas só pra não dizerem que não avisei, talvez eu me empolgue (risos)
[/SIZE][/yellowalert]
[anchor=lDGyqDmEVkuoRdkb][/anchor][b][SIZE=19px]2.2.1 (Bônus) Ordenação de listas ligadas com recursão[/SIZE][/b]
Quando tratamos de listas, é comum querermos ordenar uma lista não ordenada.
Para isso, com listas ligadas, temos basicamente duas opções: ordenar por inserção, percorrendo a lista e inserindo (usando inserção em ordem) cada elemento em outra lista, ou ordenar de forma recursiva.
A ordenação por inserção é simples, e fica como exercício implementá-la. Já a ordenação recursiva é um pouco mais complicada, mas trás um entendimento maior sobre o conceito de recursão como um todo.
A ordenação recursiva de uma lista ligada é também interessante porque pode ser feita [i]in-place[/i], isto é, alterando a lista que já temos, sem criar uma nova lista.
Então, vamos ao algoritmo. Primeiro, quando trabalhamos com recursão, é essencial definir um [i]caso base[/i]. O caso base é um caso específico do problema que queremos resolver para o qual conseguimos resolver [b]sem precisar fazer recursão[/b].
Isso é essencial, pois se não tivermos um caso base, nossa recursão nunca chega ao fim. Outra coisa que precisamos garantir é que todo problema pode ser derivado do(s) caso(s) base.
Para esse algoritmo em específico, vou fazer as coisas fora de ordem: primeiro veremos o algritmo correto, e depois vamos entender por quê ele funciona. Aí vai:
[code]
def ordenaRecursivo(primeiroNó: Nó*):
if primeiroNó.próximo == null:
return primeiroNó
else:
sublistaOrdenada = ordenaRecursivo(primeiroNó.próximo)
return insereEmOrdem(sublistaOrdenada, primeiroNó)
[/code]
[center][SIZE=11px] Veja funcionando no [url=https://repl.it/repls/UnnaturalClearcutDesigner]repl.it[/url] [/SIZE][/center]
[greenalert][imgright width=96]http://i.imgur.com/CqkKDSg.png[/imgright] [b]Veja só[/b]
A função [font=Courier]insereEmOrdem[/font] aqui é levemente diferente da que definimos antes, recebendo um [font=Courier]primeiroNó[/font] ao invés de uma [font=Courier]ListaLigada[/font]. Apesar disso, a lógica é exatamente a mesma.
Esse ajuste foi feito para facilitar a recursão. [/greenalert]
Vamos entender o algoritmo então. Primeiro, temos claramente um caso base: [font=Courier]primeiroNó.próximo == null[/font], ou "quando a lista tem apenas um nó". Claro que esse é um caso muito bom para ser base da ordenação, já que uma lista com um nó só está sempre ordenada, sem que tenhamos que fazer nenhuma alteração nela.
Depois do caso base, temos o passo recursivo: para ordenar uma lista com N elementos, primeiro ordenamos a sublista nela que tem N - 1 elementos (ou seja, a lista que começa no [font=Courier]próximo[/font] de seu [font=Courier]primeiroNó[/font]); depois disso, inserimos nosso [font=Courier]primeiroNó[/font] em ordem nessa lista ordenada, e assim ela continua ordenada.
E é isso. Temos nosso algoritmo.
Pode parecer confuso a princípio porque isso funciona, mas somente porque na programação imperativa estamos acostumados a controlar os algoritmos através de repetições, e customamos declarar todos os passos a serem tomados.
Em algoritmos recursivos, não fica claro a princípio que realmente declaramos tudo que deve ser feito. Claro, definimos o que acontece com uma lista com tamanho 1. Mas e listas maiores? Por que é que elas funcionam?
[redalert][color=white][imgright width=96]https://i.imgur.com/J8KKa5Y.png[/imgright][SIZE=11px]
[i]Disclaimer[/i]: Gente, provas e demonstrações não são meu forte, então tomem cuidado com o que vem aqui em baixo. Acho que consegui passar a ideia, mas tá tudo meio louco rs.
[/SIZE][/color][/redalert]
Para esclarecer isso, vale a pena darmos uma olhada na prova de que o algoritmo termina para todo caso a partir do caso base. Podemos usar [url=https://pt.wikipedia.org/wiki/Indu%C3%A7%C3%A3o_matem%C3%A1tica]indução matemática[/url] para isso:
[box=Prova por indução]
Seja N o número de elementos na lista ligada. Veja que, quando N = 1, é garantido que o algoritmo termina, já que ele apenas retorna a lista, sem nenhuma alteração. Esse é nossa [b]base de indução[/b].
Vejamos então que o algoritmo também termina ? n > N, n ? ?.
[b]Hipótese de Indução:[/b] O algoritmo termina para algum n ? ?.
[b]Tese de Indução:[/b] O algoritmo termina também para n + 1.
Mas pela definição do nosso algoritmo, a solução para uma lista de tamanho n é, justamente, resolver para uma lista de tamanho n - 1, seguido de uma inserção em ordem (que sempre termina) na mesma lista, ainda com tamanho n - 1.
Podemos então definir o tempo que nosso algoritmo (que apelidaremos de S, de [i]sort[/i]) leva para completar por:
[center][img]imgur.com/YAGxd00.png[/center] Onde T(f(x)) é o tempo que a função f leva para terminar para a entrada x , S é nosso algoritmo de ordenação para uma lista de tamanho n e I é nosso algoritmo de inserção em ordem em uma lista de tamanho n . Veja então que, para a ordenação em n + 1 , o tempo é dado pelo seguinte: Mas, por hipótese, o algoritmo termina para S(n) e para I(n). Temos então: Assim, sabemos que existe um r0 = r1 + r2 que satisfaz r0 ? ? e r0 > T(S(n)) + T(I(n)), e segue que: Com isso temos que, a partir de N = 1, o algoritmo de ordenação recursiva termina para toda lista de tamanho finito N, como queríamos demonstrar. [/box] [yellowalert]imgur.com/PJTv26r.png[/imgleft][SIZE=11px] [i]Nota[/i]: Essa prova só funciona baseado na premissa de que a lista que começa a partir do [font=Courier]próximo[/font] do [font=Courier]primeiroNó[/font] tem tamanho N - 1 (o que, por sinal, pode não ser verdade!).
Porém, o único caso em que nossa prova não funciona é quando a lista é circular (ou seja, o último nó é ligado no primeiro), mas nesse caso nem é interessante ordenar a lista.
Aliás, é possível provar que nenhuma lista circular com mais de um valor único pode ser ordenada, mas isso está meio fora do escopo aqui, rs. [/SIZE][/yellowalert]
[anchor=ZwJEGQMdXErqxHWn][/anchor][b][SIZE=19px]2.2.2 (Bônus) Inversão de listas ligadas com recursão[/SIZE][/b]
Outra operação relativamente comum em listas (embora nem de longe tão comum quanto a ordenação) é a inversão de listas.
Assim como na ordenação, temos duas maneiras de fazer essa operação com listas ligadas: criando uma nova lista e inserindo os elementos de trás para frente, ou recursivamente.
[yellowalert][imgright width=96]imgur.com/ookG1eI.png[/imgright][SIZE=11px] [i]Nota[/i]: OK, existe um algoritmo iterativo que inverte a lista [i]in-place[/i], mas vou ser sincero: eu nunca entendi aquela desgraça. É uma sopa de ponteiros pra lá e pra cá que só por deus.
De toda forma, nosso objetivo nessa seção é falar de recursão, então vamos ignorar esse cara :^) [/SIZE][/yellowalert]
Um ponto curioso desse algoritmo é que ele é praticamente idêntico ao de ordenação, usando apenas um método de inserção diferente! Legal né?
A inversão recursiva pode ser definida assim:
[code]
def inverteRecursivo(primeiroNó: Nó):
if primeiroNó.próximo == null:
return primeiroNó
else:
sublista = inverteRecursivo(primeiroNó.próximo)
primeiroNó.próximo = null
return insereNoFim(sublista, primeiroNó)
[/code]
[center][SIZE=11px] Veja funcionando no [url=https://repl.it/repls/CompetitiveFrequentPriority]repl.it[/url] [/SIZE][/center]
Novamente, temos um caso base bem definido ([font=Courier]primeiroNó.próximo == null[/font], assim como antes) e subproblemas que têm a mesma cara que o original, só que para um subconjunto.
Na ordenação, nós ordenávamos todos os elementos após o primeiro e então inseríamos o primeiro em ordem nessa sublista ordenada.
Aqui, nossa abordagem é parecida: para uma lista de tamanho N, invertemos a sublista de N - 1 elementos (que começa a partir do [font=Courier]primeiroNó.próximo[/font]), e então inserimos o [font=Courier]primeiroNó[/font] [i]ao fim da lista[/i].
Note que temos um passo a mais, no entanto, que apesar de dispensável na ordenação é essencial aqui: definimos o [font=Courier]próximo[/font] do [font=Courier]primeiroNó[/font] para nulo antes de inserirmos ele no fim. Se não fizessemos isso, criaríamos uma lista circular, gerando um loop.
Fora isso, podemos provar que esse algoritmo sempre termina usando o mesmo método que usamos para a ordenação. Como a lógica é exatamente a mesma, não farei a prova de novo.
"Mas será que esse algoritmo inverte a lista mesmo?", você pode se perguntar. Podemos provar isso por indução também!
[box=Definição]
Antes de provar que o algoritmo inverte a lista, precisamos definir o que significa inverter a lista.
Pois bem, seja L uma lista ligada, denotaremos por L-1 a lista L invertida.
Denotaremos também todo nó de L por n, e definiremos uma função Ix(n, n0) (índice de n), dada por:
[center][img]http://i.imgur.com/RnazA7G.png[/center] Onde:
L x L é o produto cartesiano dos elementos da lista
n é o nó que estamos procurando e n0 é o primeiro nó da lista
Prox é o próximo do nó n .
Notação : Assim, podemos definir que uma lista L? é L invertida da seguinte forma:
Calma, parece difícil, mas isso só diz que as relações de ordem devem estar invertidas. Faz sentido, não?
[/box] Legal! Sabemos como verificar se uma lista é inversa de outra, vamos agora mostrar que o que nosso algoritmo faz é, justamente, inverter uma lista ligada.Prova por indução Para nossa base de indução , usaremos o caso em que a lista contém apenas um elemento (n0), para o qual nosso algoritmo simplesmente retorna a própria lista, sem alterações. Veja que, nesse caso, L é a própria inversa, pois L × L = {(n0, n0)}, mas n0 ? n0 e n0 ? n0, e isso satisfaz os dois lados da nossa condição. Prossigamos então para o passo indutivo. Seja k o tamanho da nossa lista:Hipótese de Indução: O algoritmo inverte a lista para algum k.Tese de Indução: O algoritmo inverte a lista também para k + 1. Observe que, para uma lista de tamanho k + 1, o procedimento do algoritmo diz o seguinte:
Inverter a lista a partir do nó n1 (que tem k elementos)
Inserir o nó n0 ao fim dessa nova lista
Por hipótese, sabemos que o passo 1 funciona. Vejamos que o passo 2 garante que a nova lista obedece a condição de inversa de L. Denotemos L? a sublista invertida, criada no passo 2, e por n-1 o primeiro nó dessa sublista. Mas veja que, para toda lista, vale que n0 < ni ? i > 0. Quando inserimos n0 ao fim de L?, garantimos que Ix(n0, n-1) > Ix(n, n-1) ? n ? L?, pela definição da inserção ao fim. Assim, claramente L? obedece à condição de inversa, pois:
Claro, para todo nj ? L, vale que n0 < nj, mas eu deixei a condição ali pra dar pra ver que os dois casos se completam. O j > 0 na primeira linha também é redundante, pois se j = 0, como i > 0, não existe nenhum par (ni, j) onde nj < nj em L, mas deixei porque a H.I. fala apenas sobre a lista onde i e j são maiores que 0.
Com isso, temos que para toda lista de tamanho k ? 1, nosso algoritmo inverte a lista conforme nossa definição de inversa, como queríamos demonstrar. [redalert][imgright width=96]https://i.imgur.com/J8KKa5Y.png[/imgright] Disclaimer : Essa prova ficou especialmente complicada. Checando ela, não parece que tem nada errado de fato, mas se alguém puder dar uma olhada pra mim eu agradeço, rs.
De todo modo, isso é mais a critério de curiosidade do que qualquer coisa, então estou satisfeito. Qualquer dúvida podem me consultar \o [/redalert] [anchor=JhsTrVOhgWzUHVxG][/anchor]3 Conclusão Oba, fim \o/ Essa primeira aula foi só uma introdução à ideia por trás das estruturas de dados e à nossa primeira estrutura de fato: a lista ligada. Enquanto ia escrevendo eu já deixei vários exercícios, então imagino que não precisamos de uma seção só pra isso. Acho que nessa conclusão só me resta dizer que espero que a aula tenha sido de alguma ajuda a alguém, nem que a critério de curiosidade (as provas dos algoritmos recursivos são bem legais, vai kk). Tenho um pouco de material sobre filas e pilhas guardado aqui, mas vou esperar ter mais maturidade no que escrevi para publicar aqui. E talvez nem publique, porque o tempo é escasso e, sinceramente, duvido um pouco que esse tipo de aula seja muito útil aqui. De todo modo, me diverti bastante escrevendo e adoraria dar uma mão para quem quiser fazer os exercícios e encontrar alguma dificuldade.See ya \o
Que BooB ardeio de informações...
Filho, se você criar um índice com esses tutoriais eu fixo e divulgo no portal. Topa?
Conteúdos assim são muito valiosos, parabéns por disponibilizar.
Fala [member=1]Yoshi[/member] \o
Véio, eu topo sim, mas acho que é melhor esperar eu ter mais coisa pronta aqui. Essas aulas dão um trabalhão de montar, e apesar de eu ter um cronograma na cabeça aqui me sinto melhor botando as coisas num índice quando tiver coisas de fato pra botar xd
Mas assim que eu tiver um eu te cobro, rs.
Aliás, as próximas aulas vão ter mais BooB, espero ?( ?° ?? ?° )????*
?
Um trabalho deveras admirável, está muito bem organizado e fácil de entender. Gostei mesmo!
Já estou marcando aqui nos favoritos. Sei que montar dá um trabalhão, mas valeu por todo o esforço, [member=78]Brandt[/member] !
Don't compare yourself to others, do what only you can do.
TÓPICO BONITO, TÓPICO BEM FEITO, TÓPICO FORMOSO! :bjomeliga: :bjomeliga: :bjomeliga:
Parabéns demaaaais, Timoteee~i! Ficou sensacional! Por mais BooBs :Hm: