Lista duplamente vinculada: C++, Python (Exemplo de código)

O que é uma lista duplamente vinculada?

Em uma lista duplamente vinculada, cada nó possui links para o nó anterior e para o próximo. Cada nó consiste em três elementos: um contém os dados e outros dois são os ponteiros do nó seguinte e do nó anterior. Esses dois ponteiros nos ajudam a avançar ou retroceder a partir de um nó específico.

Aqui está a estrutura básica da lista duplamente vinculada.

Estrutura de uma lista duplamente vinculada
Estrutura de uma lista duplamente vinculada

Cada lista vinculada possui um nó inicial e um nó final. O nó principal não possui um nó “anterior” (ponteiro anterior) e o nó final não possui um nó “próximo”.

Aqui estão alguns termos importantes para uma lista duplamente vinculada:

  • prev: Cada nó está vinculado ao seu nó anterior. É usado como um ponteiro ou link.
  • Seguinte: Cada nó está vinculado ao seu próximo nó. É usado como um ponteiro ou link.
  • Data: Isso é usado para armazenar dados em um nó. “Dados” podem conter outros Estruturas de dados dentro dele. Por exemplo, string, dicionário, conjunto, hashmap, etc. podem ser armazenados em “Dados”.

Aqui está a estrutura básica de um único nó na lista duplamente vinculada:

Estrutura de um nó em uma lista duplamente vinculada

Estrutura de um nó em uma lista duplamente vinculada

Operações de lista duplamente vinculada

As operações de uma lista duplamente vinculada incluem adicionar e excluir nós, inserir e remover nós e percorrer a lista vinculada de cima para baixo.

Aqui está a lista de operações que podemos implementar usando a lista duplamente vinculada:

  • Inserção na frente
  • Inserção na cauda ou último nó
  • Inserção após um nó
  • Inserção antes de um nó
  • Exclusão da frente
  • Exclusão da cauda
  • Pesquise e exclua um nó
  • Atravessar da cabeça à cauda
  • Atravesse a cauda até a cabeça

Veremos a implementação e o pseudocódigo de todas as operações acima.

Inserção na frente da lista duplamente vinculada

A inserção na frente significa que precisamos criar um nó na lista vinculada e colocá-lo no início da lista vinculada.

Por exemplo, existe um determinado nó “15”. Precisamos adicionar isso ao nó principal.

Aqui estão duas condições importantes ao fazer esta operação:

  1. O novo nó será o nó principal se não houver nenhum nó na lista duplamente vinculada.
  2. Se já existir um nó principal, o nó anterior será substituído pelo novo nó.

Aqui está o pseudocódigo para esta operação:

function insertAtFront (ListHead, value):
  newNode = Node()
  newNode.value = value
  ListHead.prev = NewNode
  NewNode.next = ListHead
  newNode.prev = NULL
  return ListHead
Inserção no Nó Frontal
Inserção no nó frontal

Inserção no final da lista duplamente vinculada

“Inserção no final” significa que criaremos um nó na lista vinculada e o colocaremos no final.

Para fazer isso, podemos usar dois métodos:

  • Método 1: Comece a percorrer a partir do início da lista duplamente vinculada até que o “próximo” se torne nulo. Em seguida, vincule o novo nó ao “próximo”.
  • Método 2: Pegue o último nó da lista duplamente vinculada. Então, o “próximo” nó do último nó será vinculado ao novo nó. Agora, o novo nó será o nó final.

Aqui está o pseudocódigo para inserção no nó final:

function insertAtTail(ListHead, value):
  newNode = Node()
  newNode.value = value
  newNode.next = NULL
  while ListHead.next is not NULL:
	then ListHead = ListHead.next
  newNode.prev = ListHead
  ListHead.next = newNode
  return ListHead
Inserção no final da Lista Vinculada

Inserção no final da lista vinculada

Inserção após um nó

Digamos que temos uma lista duplamente vinculada como a seguinte:

Inserção após um nó

Queremos inserir um determinado nó que será vinculado após o nó, que possui o valor “12”.

Aqui estão as etapas para isso:

Passo 1) Percorra da cabeça até o último nó. Verifique qual nó tem o valor “12”.

Passo 2) Crie um novo nó e atribua-o como o próximo ponteiro do nó “12”. O “próximo” nó do novo nó será 15.

Aqui está o pseudocódigo para inserir um nó após um nó em uma lista duplamente vinculada:

function insertAfter(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.value is not equal searchItem
	then List = ListHead.next
  List = List.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Inserção após um nó

Inserção após um nó

Inserção antes de um nó

Esta operação é mais parecida com a “inserção após um nó”. Precisamos procurar o valor de um nó específico para fazer isso. Em seguida, criaremos um novo nó e o inseriremos antes do nó pesquisado.

Digamos que queremos inserir um determinado nó “15” antes do nó “12”. Então aqui estão as etapas para fazer isso:

Passo 1) Percorra a lista vinculada do nó principal ao nó final.

Passo 2) Verifique se o próximo ponteiro do nó atual tem o valor “12” ou não.

Passo 3) Insira o novo nó como o “próximo” nó do nó atual.

Aqui está o pseudocódigo para inserir um nó antes de um nó em uma lista duplamente vinculada:

function insertBefore(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.next.value is not equal searchItem
	then List = ListHead.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Inserindo um nó antes de um nó

Inserindo um nó antes de um nó

Exclua o topo da lista duplamente vinculada

O nó principal na lista duplamente vinculada que não possui nenhum nó anterior. Portanto, o próximo ponteiro será o novo nó principal se quisermos excluir o nó principal. Também precisamos liberar a memória ocupada por um nó ao excluir um nó.

Aqui estão as etapas para excluir o nó principal:

Passo 1) Atribua uma variável ao nó principal atual.

Passo 2) Visite o nó “próximo” do nó principal atual e torne o nó “anterior” (ponteiro anterior) como ''NULL”. Isso significa que estamos desconectando o segundo nó do primeiro nó.

Passo 3) Libere a memória ocupada pelo nó principal anterior.

Aqui está o pseudocódigo para excluir o cabeçalho de uma lista duplamente vinculada:

function deleteHead(ListHead):
  PrevHead = ListHead
  ListHead = ListHead.next
  ListHead.prev = NULL
  PrevHead.next = NULL
  free memory(PrevHead)
  return ListHead
Excluindo o nó principal

Excluindo o nó principal

É necessário deletar a memória alocada após realizar qualquer tipo de operação de deleção. Caso contrário, durante todo o tempo de execução do programa, a memória do bloco excluído permanecerá ocupada. Nenhum outro aplicativo pode usar esse segmento de memória.

Exclua o final da lista duplamente vinculada.

Esta operação é semelhante à exclusão da cabeça. Aqui, em vez da cabeça, precisamos deletar a cauda. Para identificar um nó como nó final, devemos verificar se o próximo ponteiro ou o próximo nó é nulo ou não. Depois de deletar a cauda, ​​precisamos liberar memória.

Esta operação também é conhecida como “exclusão por trás”.

Aqui estão as etapas para fazer isso:

Passo 1) Percorra até o nó final da lista duplamente vinculada.

Passo 2) Atribua uma variável ou ponteiro ao nó final.

Passo 3) Torne o “próximo” nó como NULL e libere a memória do nó final.

Aqui está o pseudocódigo para excluir o nó final:

function DeleteTail( ListHead ):
  head = ListHead
  while ListHead.next is not NULL:
	ListHead = ListHead.next
  Tail = ListHead.next
  ListHead.next = NULL
  free memory( Tail )
  return head

Exclua a cauda do duplamente vinculado

Pesquise e exclua um nó da lista duplamente vinculada

Esta operação nos permite pesquisar dados de um nó específico e excluí-lo. Precisamos realizar uma pesquisa linear, pois a lista vinculada é uma estrutura de dados linear. Após a exclusão, também precisamos liberar memória.

Aqui estão as etapas para pesquisar e excluir um nó na lista duplamente vinculada:

Passo 1) Percorra a lista encadeada desde o início até que o valor do nó seja igual ao item de pesquisa.

Passo 2) Atribua uma variável “deleteNode” ao nó correspondente.

Passo 3) Atribua o nó anterior de “deleteNode” ao próximo nó.

Passo 4) Libere a memória do “deleteNode”

Aqui está o pseudocódigo para pesquisar e excluir um nó de uma lista vinculada:

function SearchAndDelete(ListHead, searchItem):
  head = ListHead
  while ListHead.next.value not equals searchItem:
	head = head.next
  deleteNode = head.next
  head.next = head.next.next
  head.next.prev = head
  deleteNode.next, deleteNode.next = NULL
  free memory(deleteNode)
  return ListHead
Pesquisar e Excluir Operação

Operação de pesquisa e exclusão

Percorrer uma lista duplamente vinculada de frente

Para percorrer a partir do nó principal e iterar no próximo nó até encontrar “NULL”. Ao percorrer cada nó, podemos imprimir o valor do nó. Aqui estão as etapas para atravessar pela frente (direção para frente):

Passo 1) Atribua um ponteiro ou variável ao nó principal atual.

Passo 2) Itere para o próximo nó do cabeçalho até obter “NULL”

Passo 3) Imprima os dados do nó em cada iteração.

Passo 4) Retorne o nó principal.

Aqui está o pseudocódigo para percorrer uma lista duplamente vinculada pela frente:

function traverseFromFront(ListHead):
  head = ListHead
  while head not equals to NULL:
	print head.data
	head = head.next
  return ListHead

Aqui, a devolução não é obrigatória. Mas é uma boa prática retornar o nó principal após as operações.

Percorra uma lista duplamente vinculada de trás para frente

Esta operação é o inverso da poligonal pela frente. A abordagem é a mesma com uma pequena diferença. Devemos percorrer até o nó final e, em seguida, percorrer do nó final até o nó frontal.

Aqui estão as etapas para percorrer uma lista duplamente vinculada desde o final:

Passo 1) Atravesse até chegar ao nó final.

Passo 2) A partir do nó final, percorreremos até obtermos o nó anterior como “NULL”. O “prev” (ponteiro anterior) será nulo para o nó principal

Passo 3) A cada iteração, imprimiremos os dados do nó.

Aqui está o pseudocódigo para percorrer de trás:

function traverseFromBack(ListHead):
  head = ListHead
  while head not equals NULL:
	head = head.next
  tail = head
  while tail not equal to NULL:
	print tail.value
	tail = tail.prev
  return ListHead

Diferença entre lista vinculada simples e duplamente

A principal diferença entre a lista individual e duplamente vinculada é o número de links.

Diferença entre lista vinculada simples e duplamente

Aqui está a diferença entre os nós de uma lista vinculada individualmente e a estrutura de nós da lista duplamente vinculada:

Campo Lista encadeada individualmente Lista duplamente vinculada
Estrutura Lista encadeada individualmente tem um campo de dados e um link para o próximo nó. A lista duplamente vinculada possui um campo de dados e dois links. Um para o nó anterior e outro para o próximo nó.
Traversal Ele só pode percorrer da cabeça à cauda. Ele pode percorrer tanto para frente quanto para trás.
Memória Ocupa menos memória. Ocupa mais memória do que uma lista vinculada individualmente.
Acessibilidade Listas vinculadas individualmente são menos eficientes porque usam apenas um link para o próximo nó. Não há link para o nó anterior. As listas duplamente vinculadas são mais eficientes do que as listas vinculadas individualmente.

Lista Duplamente Vinculada em C++

#include<iostream>
using namespace std;
  struct node{
  int data;
  struct node *next;
  struct node *prev;
  };
void insertFront(node* &listHead, int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  if(listHead!=NULL)
  {
	listHead->prev = newNode;
  	newNode->next = listHead;
  }
  
  listHead = newNode;
  cout<<"Added "<<value<<" at the front"<<endl;
  }
void insertEnd(node * &listHead, int value){
  if(listHead==NULL)
  {
  	insertFront(listHead,value);
  }
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  node *head = listHead;
  while(head->next!=NULL){
  head = head->next;
  }
  head->next = newNode;
  newNode->prev = head;
  cout<<"Added "<<value<<" at the end"<<endl;
  }
void insertAfter(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;
  node *head = listHead;	
  while(head->next!=NULL &&  head->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" after node "<<searchValue<<endl;
  }
void insertBefore(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;	
  node *head = listHead;	
  while(head->next!=NULL &&  head->next->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" before node "<<searchValue<<endl;	
  }
void traverseFromFront(node *listHead){
  node* head = new node();
  head = listHead;
  cout<<"Traversal from head:\t";	
  while(head!=NULL){	
  cout<<head->data<<"\t ";	
  head = head->next;	
  }	
  cout<<endl;
  }	
void traverseFromEnd(node *listHead){
  node* head = new node();
  head = listHead;	
  cout<<"Traversal from head:\t";	
  while(head->next!=NULL){	
  head = head->next;	
  }	
  node *tail = head;	
  while(tail!=NULL){	
  cout<<tail->data<<"\t";	
  tail = tail->prev;	
  }	
  cout<<endl;	
  }
void searchAndDelete(node **listHead, int searchItem){
  node* head = new node();
  head = (*listHead);
  while(head->next!=NULL &&  head->data!=searchItem){
  head = head->next;
  }
  if(*listHead == NULL || head == NULL) return;
  if((*listHead)->data == head->data){
  	*listHead = head->next;
  }
  if(head->next != NULL){
  	head->next->prev = head->prev;
  }
  
  if(head->prev != NULL){
  	head->prev->next = head->next;
  }
  free(head);
  cout<<"Deleted Node\t"<<searchItem<<endl;
  return;
  }
int main(){
  node *head = NULL;
  insertFront(head,5);
  insertFront(head,6);
  insertFront(head,7);
  insertEnd(head,9);
  insertEnd(head,10);
  insertAfter(head,5,11);
  insertBefore(head,5,20);
  traverseFromFront(head);
  traverseFromEnd(head);
  searchAndDelete(&head,7);
  traverseFromFront(head);
  traverseFromEnd(head);
}

saída

Added 5 at the front
Added 6 at the front
Added 7 at the front
Aded 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversal from head:    7        6       20      5       11      9       10
Traversal from head:    10      9       11      5       20      6       7
Deleted Node    7
Traversal from head:    6        20      5       11      9       10
Traversal from head:    10      9       11      5       20      6

Lista Duplamente Vinculada em Python

class node:
  def __init__(self,data = None, prev=None, next = None):
    self.data = data
    self.next = next
    self.prev = prev
class DoublyLinkedList:
  def __init__(self):
    self.head = None

  def insertFront(self, val):
    newNode = node(data=val)
    newNode.next = self.head
    if self.head is not None:
      self.head.prev = newNode
    self.head = newNode
    print("Added {} at the front".format(val))

  def insertEnd(self,val):
    newNode = node(data=val)
    if self.head is None:
      self.insertFront(val)
    
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    newNode.prev = temp
    print("Added {} at the end".format(val))

  def traverseFromFront(self):
    temp = self.head
    print("Traversing from head:\t",end="")
    while temp:
      print("{}\t".format(temp.data),end="")
      temp = temp.next
    print()

  def traverseFromEnd(self):
    temp = self.head
    print("Traversing from Tail:\t",end="")
    while temp.next is not None:
      temp = temp.next
    tail = temp
    while tail is not None:
      print("{}\t".format(tail.data),end="")
      tail = tail.prev
    print()
  
  def insertAfter(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} after node {}".format(value,searchItem))

  def insertBefore(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.next.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} before node {}".format(value,searchItem))

  def searchAndDelete(self,searchItem):
    temp = self.head

    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    if self.head is None or temp is None:
      return

    if self.head.data is temp.data:
      self.head = temp.next

    if temp.next is not None:
      temp.next.prev = temp.prev
    
    if temp.prev is not None:
      temp.prev.next = temp.next
    
    print("Deleted Node\t{}".format(searchItem))
    return

doublyLinkedList = DoublyLinkedList()
doublyLinkedList.insertFront(5)
doublyLinkedList.insertFront(6)
doublyLinkedList.insertFront(7)
doublyLinkedList.insertEnd(9)
doublyLinkedList.insertEnd(10)
doublyLinkedList.insertAfter(5, 11)
doublyLinkedList.insertBefore(5, 20)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()
doublyLinkedList.searchAndDelete(7)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()

saída

Added 5 at the front
Added 6 at the front
Added 7 at the front
Added 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversing from head:   7       6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6       7
Deleted Node    7
Traversing from head:   6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6

Complexidade da lista duplamente vinculada

Geralmente, a complexidade do tempo é dividida em três tipos.

Eles são:

  1. Melhor Case
  2. Caso Médio
  3. Pior caso

Complexidade de tempo no melhor caso para lista duplamente vinculada:

  1. A inserção na cabeça ou na cauda custará O(1). Porque não precisamos percorrer a lista vinculada. O ponteiro inicial e final podem nos dar acesso ao nó principal e final com complexidade de tempo O (1).
  2. A exclusão no início ou no final custará O(1).
  3. A pesquisa de um nó terá a complexidade de tempo de O(1). Porque o nó de destino pode ser o nó principal.

Complexidade de tempo no caso médio para lista duplamente vinculada:

  1. A inserção na cabeça ou na cauda terá a complexidade de tempo do custo O(1).
  2. A exclusão no início ou no final terá a complexidade de tempo do custo O(1).
  3. A pesquisa de um nó terá a complexidade de tempo de O(n). Porque o item de pesquisa pode residir em qualquer lugar da lista vinculada. Aqui, “n” é o nó total presente na lista vinculada.

A complexidade de tempo do pior caso da lista duplamente vinculada será a mesma do caso médio.

Complexidade de memória da lista duplamente vinculada

A complexidade da memória é O(n), onde “n” é o número total de nós. Ao implementar a lista vinculada, devemos liberar a memória que usamos. Caso contrário, para uma lista vinculada maior, causará vazamento de memória.

Resumo

  • Uma lista vinculada é um tipo de estrutura de dados linear, uma coleção de dados representados de maneira linear.
  • Uma lista duplamente vinculada é um tipo de lista vinculada em que um nó possui links com o nó anterior e o próximo.
  • A lista duplamente vinculada contém todas as operações como adicionar um nó, excluir um nó, inserir um nó antes ou depois de outro nó e percorrer a lista vinculada do início ao fim.
  • A lista duplamente vinculada possui um campo de dados e dois links. Um para o nó anterior e outro para o próximo nó.
  • Complexidade da lista duplamente encadeada Melhor Caso, Caso médio
  • E o pior caso.