Bem-vindo de volta, com mais um problema para resolver! Hoje, estamos lidando com listas encadeadas e remoção de seus elementos. Discutiremos um pouco sobre o que são listas encadeadas, criaremos uma e veremos como remover elementos dela.
Antes de começar, as isenções de responsabilidade habituais:
Não sou um programador especialista: apenas um cara que gosta de compartilhar suas fotos! Se você tiver uma solução melhor ou mais rápida para um algoritmo, envie-a nos comentários! Eu adoraria aprender um pouco mais sobre isso!
Chega de barulho; vamos pular para o problema!
Este problema foi inicialmente solicitado pela Amazon
Dada uma lista encadeada e um inteiro
k
, remova o k-ésimo nó do final da lista e retorne o início da lista.
k
tem a garantia de ser menor que o comprimento da lista.Faça isso em uma passagem.
Tudo bem então, vamos explicar um pouco o que está acontecendo aqui: temos uma lista encadeada e um elemento de índice k
para remover da lista. Depois disso, devemos retornar o cabeçalho da lista encadeada.
Por fim, devemos fazer tudo isso em apenas uma passagem, o que significa que não podemos percorrer a lista mais de uma vez.
Também temos a garantia de que o índice k
está contido na lista, portanto não precisamos verificar se ele ultrapassa o tamanho real da lista.
Usaremos Go para construir o algoritmo hoje, pois sua sintaxe é incrível para esse tipo de trabalho e, ao mesmo tempo, permanece bastante simples de entender.
Vamos começar do começo: construindo uma lista encadeada simples.
O conceito por trás da lista encadeada é bem simples: é uma lista feita de objetos (geralmente chamados de nós ), e cada nó deve ter pelo menos duas propriedades: um valor (algo realmente contido pelo nó) e um link para o próximo nó.
Normalmente, um ponteiro é usado como um link para pular de um nó para outro.
Além disso, o primeiro nó da lista é geralmente chamado de cabeça da lista, que também é o nó que devemos retornar pelo problema.
Aqui está um exemplo gráfico:
O primeiro nó à esquerda é o cabeçalho da lista, que contém seu valor v₀ e um ponteiro de memória P(n₁) que redireciona a lista para o próximo nó n₁. O próximo nó n₁ conterá seu valor e um ponteiro P(n₂) para o próximo nó e assim por diante.
O nó final, é claro, não terá nada para apontar, então o valor de seu ponteiro será null .
Vamos ver o código para criar um!
O código é bem simples: criamos duas estruturas, uma para o nó interno e outra para a lista encadeada.
Value
e a propriedade Next
, que contém um tipo *Node
como um ponteiro para o próximo nó.
A lista encadeada, uma estrutura para “inicializar” a lista encadeada, que terá apenas uma propriedade, o Head
da lista.
Depois disso, simplesmente inicializamos a lista na função principal e damos à sua cabeça um nó com um valor aleatório de 10.
A chave por trás da compreensão da lista encadeada é que agora o nó Head
, já que é do tipo Node
, terá inerentemente uma propriedade Next
, que conterá o próximo nó. Este último nó então terá sua propriedade Next
para pular para o próximo nó, e assim sucessivamente.
Tudo ficará mais claro assim que adicionarmos alguns nós à lista, então vamos lá! Temos duas opções: ou fazemos manualmente, um trabalho realmente tedioso, ou escrevemos um método para a classe de lista encadeada para adicionar mais alguns nós. Vamos conferir!
Mesmo que você tenha pouca experiência em programação, em quase todas as linguagens de programação, um dos primeiros conceitos sobre os quais você deve ter ouvido falar é a diferença entre listas mutáveis e imutáveis .
Como seus nomes sugerem, listas mutáveis são listas que você pode manipular e adicionar elementos a . Os imutáveis, em vez disso, têm um tamanho fixo e não podem ser acrescentados a novos elementos. Mas por que é assim?
As listas imutáveis são contíguas na memória , o que significa que seus elementos são armazenados na memória sequencialmente : para uma lista de 3 elementos, se o primeiro elemento for armazenado em 0x00, o segundo estará em 0x01 e o último em 0x02.
É por isso que é tão rápido iterar sobre uma matriz fixa, uma lista imutável ou uma tupla em Python porque a CPU simplesmente recupera os índices de memória um por um.
Por outro lado, as listas mutáveis são contíguas na memória apenas quando inicializadas: nesse ponto, se elementos forem adicionados, eles não serão mais sequenciais. Então, como podemos passar por cima deles?
Bem, porque o último elemento da primeira iniciação terá um ponteiro que o elemento adicionado depois dele , que nos trará o elemento to anexado e assim por diante. Então, sim, as listas mutáveis são muitas vezes listas encadeadas disfarçadas!
Agora vamos ver o código:
Adicionamos o método AddNode
aos métodos de nossa lista encadeada. O processo para adicionar um nó é assim:
Head
em uma variável chamada current
current
com o próximo nó todas as vezes, até que o próximo nó seja nulo. Uma vez que o nó em que estamos atualmente tem um ponteiro nulo, sabemos que estamos no último nó da lista.
Por fim, atualizamos este último nó com um novo nó e um novo valor (o ponteiro Next
deste novo nó será definido como nulo se o deixarmos em branco)
Graficamente, esse processo é mais ou menos assim:
Podemos verificar o correto funcionamento deste método na função main, imprimindo os valores dos nós da lista encadeada.
Agora, para a parte final e solução do nosso problema: remover um nó com o índice correto. Em primeiro lugar, o que significa remover um nó de uma lista encadeada? Se pensarmos bem, um nó em uma lista encadeada só existe se estiver conectado ao anterior , certo?
Por exemplo, se dermos a n-1ᵗʰ um valor Next
nulo, podemos desconectar o valor nᵗʰ da lista.
E se o elemento que queremos remover não for o último? Bem, podemos desvincular o elemento conectando o anterior ao próximo dele !
Portanto, para remover o elemento kᵗʰ, devemos encontrar o elemento k-1ᵗʰ e vinculá-lo ao nó k+1ᵗʰ. Precisamos armazenar o nó anterior (k-1ᵗʰ) .
Obviamente, não podemos indexar diretamente a lista : tente algo como linkedlist[n]
apenas gerará um erro. Dado isso, porém, podemos considerar a cabeça da lista como índice 0, e seu próximo nó como índice 1, e assim por diante, e também podemos percorrer os nós, como fizemos antes.
O que podemos fazer então é implementar um contador para rastrear o nó em que estamos!
Vamos codificar então.
Vejamos a função RemoveNode
que aceita um atributo index
. Depois disso, inicializamos três variáveis:
previousNode
, que manterá o nó anterior
current
, que manterá o nó em que estamos durante o loop
counter
, inicialmente igual a 0, que manterá nossa posição na lista
Vamos pular o primeiro bloco if por enquanto e nos concentrar no loop for. Começamos o loop até que nossa variável counter
seja estritamente menor que nosso index
: cada loop atualizará o nó anterior com o atual e passará a atualizar o nó atual e o contador.
Quando o loop é interrompido, significa que estamos apenas no nó anterior ao nosso índice desejado: precisamos apenas atualizar o ponteiro do nó anterior, prev.Next
, com o próximo nó na lista a partir deste em que estamos, que será current.Next
. Por fim, retornamos o cabeçalho da lista encadeada.
Agora, o que acontece se o índice a ser removido for a cabeça, que tem índice 0? O loop nunca iniciaria porque iniciaria com counter = 0
e index = 0
, e a condição seria instantaneamente falsa. Podemos gerenciar este primeiro caso com o bloco if no topo.
Basicamente, se o índice for 0, podemos atualizar diretamente o cabeçalho da lista encadeada com o nó próximo a ele e retorná-lo. Gostaria de chamar sua atenção, principalmente para a linha 31: Go, como em muitas outras linguagens, passa os atributos para as funções por value , o que significa que a função retém uma cópia do objeto passado, não o objeto real que você está passando .
Este conceito é visto claramente neste exemplo:
package main import "fmt" func printMemoryAddress(value int) { fmt.Println(&value) } func main() { a := 10 fmt.Println(&a) printMemoryAddress(a) } // 0xc00010a000 // 0xc00010a008 // Program exited.
Criamos uma função para imprimir o endereço de memória do valor passado como atributo. Então, na função principal, criamos uma variável a e imprimimos seu endereço de memória. Então passamos a mesma variável para a função e imprimimos seu endereço de memória.
Como você pode ver, se você tentar, os resultados serão diferentes: isso porque os atributos são passados para as funções como valor, ou seja, uma cópia de a
é criada apenas com o objetivo de passá-lo para a função.
De volta à nossa lista vinculada, devemos usar ponteiros para obter a cabeça real para o mesmo problema acima. Portanto, para chegar ao ll.Node
“real” , devemos recuperar seu ponteiro, *ll.Node
e movê-lo ainda mais com *ll.Node = *ll.Node.Next
.
Na função principal, adicionamos as chamadas ao método, por exemplo, chamando ll.RemoveNode(0)
e ll.RemoveNode(2)
, e podemos verificar o resultado onde o nó 0 e o nó 2 estarão ausentes.
Chegamos ao fim!
Se você leu até aqui, toda a minha gratidão vai para você. Se você estiver disposto a deixar um ou dois likes e compartilhar o artigo, isso fará meu dia e me ajudará a continuar escrevendo esses artigos!
Até o próximo artigo e, como sempre, obrigado pela leitura.
Nicola