Uma leve introdução às listas vinculadas com o MongoDB
Avalie esse Tutorial
Você é novo em estruturas de dados e algoritmos? Neste post, você conhecerá uma das estruturas de dados mais importantes da Ciência da Computação, a Lista Vinculada, implementada com um toque do MongoDB. Esta postagem cobrirá os fundamentos da estrutura de dados da lista vinculada. Ele também responderá a perguntas como: "Como as listas vinculadas diferem das matrizes?" e "Quais são os prós e os contras de usar uma lista vinculada?"
Você sabia que as listas vinculadas são uma das estruturas de dados fundamentais da ciência da computação? Se você é como muitos desenvolvedores autodidatas ou se formou em um campo de treinamento de desenvolvedores, talvez precise de uma pequena lição sobre como essa estrutura de dados funciona. Ou, se você for como eu, pode precisar de uma atualização se já se passaram alguns anos desde sua última palestra de Ciência da Computação sobre estruturas de dados e algoritmos. Neste post, mostrarei como implementar uma lista vinculada do zero usando o Node.js e o MongoDB. Este também é um ótimo lugar para começar a lidar com os fundamentos das operações CRUD do MongoDB e essa estrutura de dados histórica. Vamos começar com o básico.
Uma lista vinculada é uma estrutura de dados que contém uma lista de nós conectados por meio de referências ou ponteiros. Um nó é um objeto na memória. Geralmente contém no máximo duas informações, um valor de dados e um ponteiro para o próximo nó na lista vinculada. As listas vinculadas também possuem referências de ponteiro separadas para o início e o fim da lista vinculada. A cabeça é o primeiro nó da lista, enquanto a cauda é o último objeto da lista.
Um nó que NÃO se vincula a outro nó
1 { 2 "data": "Cat", 3 "next": null 4 }
Um nó que faz um link para outro nó
1 { 2 "data": "Cat", 3 "next": { 4 "data": "Dog", 5 "next": { 6 "data": "Bird", 7 "next": null 8 } 9 } // these are really a reference to an object in memory 10 }
Há vários motivos pelos quais as listas vinculadas são usadas, em vez de outras estruturas de dados, como matrizes (mais sobre isso adiante). No entanto, usamos listas vinculadas em situações em que não sabemos o tamanho exato da estrutura de dados, mas prevemos que a lista possa crescer para tamanhos grandes. Muitas vezes, as listas vinculadas são usadas quando pensamos que a estrutura de dados pode ficar maior do que a memória disponível do computador com o qual estamos trabalhando. As listas vinculadas também são úteis se ainda precisarmos preservar a ordem e prever que a ordem mudará com o tempo.
As listas vinculadas são apenas objetos na memória. Um objeto contém uma referência a outro objeto ou um nó contém um ponteiro para o próximo nó. Na memória, uma lista vinculada tem a seguinte aparência:
- As listas vinculadas são dinâmicas por natureza, o que aloca a memória quando necessário.
- As operações de inserção e exclusão podem ser facilmente implementadas.
- Pilhas e filas podem ser facilmente executadas usando uma lista vinculada.
- A memória é desperdiçada, pois os ponteiros exigem memória extra para armazenamento.
- Nenhum elemento pode ser acessado aleatoriamente; ele tem que acessar cada nó sequencialmente a partir da cabeça.
- A travessia reversa é difícil em uma lista vinculada apenas.
Agora, você pode estar achando que as listas vinculadas se parecem muito com arrays, e você está correto! Ambos mantêm o controle de uma sequência de dados e podem ser iterados e repetidos em loop. Além disso, ambas as estruturas de dados preservam a ordem da sequência. No entanto, existem algumas diferenças importantes.
- As matrizes são simples e fáceis de usar.
- Oferecem acesso mais rápido aos elementos (O(1) ou tempo constante).
- Eles podem acessar elementos por qualquer índice sem precisar iterar por todo o conjunto de dados desde o início.
- Você estava ciente de que as arrays podem desperdiçar memória? Isso ocorre porque normalmente os compiladores pré-alocam um bloco sequencial de memória quando uma nova array é criada para fazer queries super rápidas. Portanto, muitos desses blocos de memória pré-alocados podem estar vazios.
- As arrays têm um tamanho fixo. Se o bloco de memória pré-alocado estiver cheio até a capacidade máxima, o compilador de código alocará um bloco de memória ainda maior e precisará copiar a matriz antiga para o novo bloco de memória da matriz antes que novas operações de matriz possam ser executadas. Isso pode ser caro com tempo e espaço.
- Para inserir um elemento em uma determinada posição, a operação é complexa. Talvez seja necessário mudar os elementos existentes para criar uma vaga e inserir o novo elemento na posição desejada.
Uma lista duplamente vinculada é o mesmo que uma lista vinculada simples, com a exceção de que cada nó também aponta para o nó anterior, bem como para o próximo nó.
Uma lista circular vinculada é igual a uma lista vinculada individualmente, com a exceção de que não há conceito de cabeça ou cauda. Todos os nós apontam para o próximo nó circularmente. Não há um início verdadeiro para a lista vinculada circular.
A primeira coisa que precisaremos configurar é uma contaMongoDB Atlas. E não se preocupar, você pode criar um cluster M0 MongoDB Atlas gratuitamente. Não é necessário nenhum cartão de crédito para começar! Para entrar em operação com um cluster M0 gratuito, siga o guia de Começando.
Depois de nos inscrevermos no Atlas, precisaremos implantar um cluster MongoDB gratuito. Observe que você também precisará adicionar uma regra para permitir o endereço IP do computador ao qual estamos conectando o MongoDB Atlas Custer e precisará criar um usuário do banco de dados antes de poder se conectar ao novo cluster. Esses são recursos de segurança implementados para garantir que atores mal-intencionados não possam acessar seu banco de dados.
Se você tiver algum problema ao conectar ou configurar seu MongoDB Atlas cluster gratuito, não deixe de conferir os MongoDB Community para obter ajuda.
Em seguida, vamos nos conectar ao nosso novo cluster de banco de dados MongoDB Atlas usando o plug-in MongoDB do Visual Studio Code. A extensão MongoDB nos permite:
- Conecte-se a um cluster do MongoDB ou Atlas, navegue pelos seus bancos de dados e coleções, obtenha uma visão geral rápida do seu esquema e veja os documentos em suas coleções.
- Crie o MongoDB Playgrounds, a maneira mais rápida de criar protótipos de operações CRUD e comandos do MongoDB.
- Acesse rapidamente o MongoDB Shell para iniciar o MongoDB Shell a partir da paleta de comandos e conecte-se rapidamente ao cluster ativo.
Para instalar MongoDB for VS Code, basta procurar por ele na lista de extensões diretamente dentro VS Code ou ir para a página inicial "MongoDB for VS Code" no VS Code Marketplace.
MongoDB for VS Code pode se conectar a instâncias ou clusters autônomos do MongoDB no MongoDB Atlas ou auto-hospedado. Uma vez conectado, você pode procurar bancos de dados, collectionse visualizações somente leitura diretamente da visualização em árvore.
Para cada collection, você verá uma lista de documentos de amostra e uma visão geral rápida do esquema. Isso é muito útil como referência ao escrever consultas e agregações.
Uma vez instalado, haverá uma nova guia MongoDB que podemos usar para adicionar nossas conexões clicando em "Adicionar conexão". Se você já usou o MongoDB Compass antes, o formulário deve ser familiar. Você pode inserir seus detalhes de conexão no formulário ou usar uma string de conexão. Escolhi o último, pois meu banco de dados está hospedado no MongoDB Atlas.
Para obter sua connection string, navegue até a página "Clusters" e selecione "Connect".
Escolha a opção "Conectar-se usando o MongoDB Compass" e copie a connection string. Adicione seu nome de usuário e senha em seus respectivos locais antes de inserir a string no VS Code.
Depois de se conectar com sucesso, você deverá ver um alerta. Neste ponto, você pode explorar os dados em seu cluster, bem como seus esquemas.
Tudo bem, agora que conseguimos nos conectar ao nosso banco de dados MongoDB Atlas, vamos escrever um código para permitir que nossa lista vinculada se conecte ao nosso banco de dados e faça algumas limpezas enquanto desenvolvemos nossa lista vinculada.
A estratégia geral para criar nossas listas vinculadas com o MongoDB será a seguinte. Usaremos um documento do MongoDB para manter o controle das metainformações, como a localização da cabeça e da cauda. Também usaremos um documento exclusivo do MongoDB para cada nó em nossa lista vinculada. Usaremos os IDs exclusivos que são gerados automaticamente pelo MongoDB para simular um ponteiro. Portanto, o próximo valor de cada nó da lista vinculada armazenará o ID do próximo nó da lista vinculada. Dessa forma, poderemos iterar em nossa lista vinculada.
Então, para conseguir isso, a primeira coisa que vamos fazer é configurar nossa classe de lista vinculada.
1 const MongoClient = require("mongodb").MongoClient; 2 3 // Define a new Linked List class 4 class LinkedList { 5 6 constructor() {} 7 8 // Since the constructor cannot be an asynchronous function, 9 // we are going to create an async `init` function that connects to our MongoDB 10 // database. 11 // Note: You will need to replace the URI here with the one 12 // you get from your MongoDB Cluster. This is the same URI 13 // that you used to connect the MongoDB VS Code plugin to our cluster. 14 async init() { 15 const uri = "PASTE YOUR ATLAS CLUSTER URL HERE"; 16 this.client = new MongoClient(uri, { 17 useNewUrlParser: true, 18 useUnifiedTopology: true, 19 }); 20 21 try { 22 await this.client.connect(); 23 console.log("Connected correctly to server"); 24 this.col = this.client 25 .db("YOUR DATABASE NAME HERE") 26 .collection("YOUR COLLECTION NAME HERE"); 27 } catch (err) { 28 console.log(err.stack); 29 } 30 } 31 } 32 33 // We are going to create an immediately invoked function expression (IFEE) 34 // in order for us to immediately test and run the linked list class defined above. 35 (async function () { 36 try { 37 const linkedList = new LinkedList(); 38 await linkedList.init(); 39 linkedList.resetMeta(); 40 linkedList.resetData(); 41 } catch (err) { 42 // Good programmers always handle their errors 43 console.log(err.stack); 44 } 45 })();
Em seguida, vamos criar algumas funções auxiliares para redefinir nosso banco de dados toda vez que executarmos o código, de modo que nossos dados não fiquem cheios de dados antigos.
1 // This function will be responsible for cleaning up our metadata 2 // function everytime we reinitialize our app. 3 async resetMeta() { 4 await this.col.updateOne( 5 { meta: true }, 6 { $set: { head: null, tail: null } }, 7 { upsert: true } 8 ); 9 } 10 11 // Function to clean up all our Linked List data 12 async resetData() { 13 await this.col.deleteMany({ value: { $exists: true } }); 14 }
Agora, vamos escrever algumas funções auxiliares para nos ajudar a consultar e atualizar nosso documento meta.
1 // This function will query our collection for our single 2 // meta data document. This document will be responsible 3 // for tracking the location of the head and tail documents 4 // in our Linked List. 5 async getMeta() { 6 const meta = await this.col.find({ meta: true }).next(); 7 return meta; 8 } 9 10 // points to our head 11 async getHeadID() { 12 const meta = await this.getMeta(); 13 return meta.head; 14 } 15 16 // Function allows us to update our head in the 17 // event that the head is changed 18 async setHead(id) { 19 const result = await this.col.updateOne( 20 { meta: true }, 21 { $set: { head: id } } 22 ); 23 return result; 24 } 25 26 // points to our tail 27 async getTail(data) { 28 const meta = await this.getMeta(); 29 return meta.tail; 30 } 31 32 // Function allows us to update our tail in the 33 // event that the tail is changed 34 async setTail(id) { 35 const result = await this.col.updateOne( 36 { meta: true }, 37 { $set: { tail: id } } 38 ); 39 return result; 40 } 41 42 // Create a brand new linked list node 43 async newNode(value) { 44 const newNode = await this.col.insertOne({ value, next: null }); 45 return newNode; 46 }
As etapas para adicionar um novo nó a uma lista vinculada são:
- Adicionar um novo nó à cauda atual.
- Atualize as caudas atuais ao lado do novo nó.
- Atualize sua lista vinculada para apontar para o novo nó.
1 // Takes a new node and adds it to our linked lis 2 async add(value) { 3 const result = await this.newNode(value); 4 const insertedId = result.insertedId; 5 6 // If the linked list is empty, we need to initialize an empty linked list 7 const head = await this.getHeadID(); 8 if (head === null) { 9 this.setHead(insertedId); 10 } else { 11 // if it's not empty, update the current tail's next to the new node 12 const tailID = await this.getTail(); 13 await this.col.updateOne({ _id: tailID }, { $set: { next: insertedId } }); 14 } 15 // Update your linked list to point tail to the new node 16 this.setTail(insertedId); 17 return result; 18 }
Para percorrer uma lista vinculada, devemos começar no início da lista vinculada, também conhecida como cabeça. Em seguida, seguimos cada referência de ponteiroseguinte até chegarmos ao final da lista vinculada, ou ao nó que estamos procurando. Isso pode ser implementado usando as seguintes etapas:
- Comece pelo nó principal da sua lista vinculada.
- Verifique se o valor corresponde ao que você está procurando. Se encontrado, retorna esse nó.
- Se não for encontrado, passe para o próximo nó por meio da propriedade next do nó atual.
- Repita até que o próximo seja nulo (final/fim da lista).
1 // Reads through our list and returns the node we are looking for 2 async get(index) { 3 // If index is less than 0, return false 4 if (index <= -1) { 5 return false; 6 } 7 let headID = await this.getHeadID(); 8 let postion = 0; 9 let currNode = await this.col.find({ _id: headID }).next(); 10 11 // Loop through the nodes starting from the head 12 while (postion < index) { 13 // Check if we hit the end of the linked list 14 if (currNode.next === null) { 15 return false; 16 } 17 18 // If another node exists go to next node 19 currNode = await this.col.find({ _id: currNode.next }).next(); 20 postion++; 21 } 22 return currNode; 23 }
Agora, digamos que queremos remover um nó em nossa lista encadeada. Para fazer isso, devemos acompanhar novamente o nó anterior para que possamos atualizar a próxima referência de ponteiro do nó anterior para o nó que está sendo excluído para o qual o próximo valor está apontando. Ou, dito de outra forma:
- Encontre o nó que você está procurando e acompanhe o nó anterior.
- Quando encontrado, atualize os nós anteriores a seguir para apontar para o próximo nó referenciado pelo nó a ser excluído.
- Exclui da memória o nó encontrado.
1 // reads through our list and removes desired node in the linked list 2 async remove(index) { 3 const currNode = await this.get(index); 4 const prevNode = await this.get(index - 1); 5 6 // If index not in linked list, return false 7 if (currNode === false) { 8 return false; 9 } 10 11 // If removing the head, reassign the head to the next node 12 if (index === 0) { 13 await this.setHead(currNode.next); 14 15 // If removing the tail, reassign the tail to the prevNode 16 } else if (currNode.next === null) { 17 await this.setTail(prevNode._id); 18 await this.col.updateOne( 19 { _id: prevNode._id }, 20 { $set: { next: currNode.next } } 21 ); 22 23 // update previous node's next to point to the next node referenced by node to be deleted 24 } else { 25 await this.col.updateOne( 26 { _id: prevNode._id }, 27 { $set: { next: currNode.next } } 28 ); 29 } 30 31 // Delete found node from memory 32 await this.col.deleteOne({ 33 _id: currNode._id, 34 }); 35 36 return true; 37 }
O código a seguir insere um nó após um nó existente em uma lista vinculada individualmente. A inserção de um novo nó antes de um existente não pode ser feita diretamente; em vez disso, deve-se acompanhar o nó anterior e inserir um novo nó depois dele. Podemos fazer isso seguindo estas etapas:
- Encontre a posição/nó em sua lista vinculada onde você deseja inserir seu novo nó depois.
- Atualize a próxima propriedade do novo nó para apontar para o nó para o qual o nó de destino aponta atualmente.
- Atualize a próxima propriedade do nó que você deseja inserir depois para apontar para o novo nó.
1 // Inserts a new node at the deisred index in the linked list 2 async insert(value, index) { 3 const currNode = await this.get(index); 4 const prevNode = await this.get(index - 1); 5 const result = await this.newNode(value); 6 const node = result.ops[0]; 7 8 // If the index is not in the linked list, return false 9 if (currNode === false) { 10 return false; 11 } 12 13 // If inserting at the head, reassign the head to the new node 14 if (index === 0) { 15 await this.setHead(node._id); 16 await this.col.updateOne( 17 { _id: node._id }, 18 { $set: { next: currNode.next } } 19 ); 20 } else { 21 // If inserting at the tail, reassign the tail 22 if (currNode.next === null) { 23 await this.setTail(node._id); 24 } 25 26 // Update the next property of the new node 27 // to point to the node that the target node currently points to 28 await this.col.updateOne( 29 { _id: prevNode._id }, 30 { $set: { next: node._id } } 31 ); 32 33 // Update the next property of the node you 34 // want to insert after to point to the new node 35 await this.col.updateOne( 36 { _id: node._id }, 37 { $set: { next: currNode.next } } 38 ); 39 } 40 return node; 41 }
Muitos desenvolvedores querem aprender as estruturas de dados e os algoritmos fundamentais da ciência da computação ou se atualizar sobre eles. Na humilde opinião deste autor, a melhor maneira de aprender estruturas de dados é implementando-as por conta própria. Este exercício é uma ótima maneira de aprender estruturas de dados, bem como aprender os fundamentos das operações CRUD do MongoDB.
Quando estiver pronto para implementar sua própria lista vinculada no MongoDB, confira o MongoDB Atlas, o banco de dados como serviço totalmente gerenciado do MongoDB. O Atlas é a maneira mais fácil de começar a usar o MongoDB e tem uma camada rica e gratuita para sempre.
Se você quiser saber mais sobre listas vinculadas e MongoDB, não deixe de conferir esses recursos.
Confira os seguintes recursos para obter mais informações: