Estrutura de dado: Lista Ligada
Uma Lista Ligada é uma coleção de nós que formam uma sequência linear. A diferença entre um array e uma Lista Ligada é que o array possui elementos indexados, então podemos obter um elemento por tempo constante apenas pesquisando pelo seu índice. Na Lista Ligada, precisamos percorrer os nós para obter o elemento pesquisado e isso leva um tempo linear.
A vantagem é que as listas vinculadas podem inserir e remover itens em tempo constante.
Uma Lista Ligada é uma sequência de nós e cada nó possui dois attributes: o valor que ele armazena e a referência ao próximo nó da sequência.
O primeiro e o último nós são chamados de head e tail da lista, respectivamente. Então, para chegar ao final do último, percorremos a Lista Ligada movendo de um nó para outro usando a próxima referência de cada nó.
A Lista ligaga tendo o head e o tail como atributos ajuda a adicionar novos nós ao início e ao final da lista. Mas podemos implementá-lo com ou sem o atributo tail. Vamos mergulhar nesta implementação.
Podemos separar a Lista Ligada de seus elementos. Cada elemento é um nó e podemos implementar essa representação com uma classe Node.
class Node {
  constructor(value, next = null) {
    this.value = value;
    this.next = next;
  }
}
Basicamente, tem um valor e a referência ao próximo nó. Adicionamos um valor padrão (null) ao parâmetro next para torná-lo mais flexível ao criar novos nós.
A maneira mais simples de usar é:
new_node = new Node(1);
new_node.value; // 1
new_node.next; // null
- Instanciar o novo nó.
 - Podemos acessar os atributos 
valueenext. 
Mas com a flexibilidade do parâmetro next, também podemos usá-lo passando a próxima referência de nó.
const nextNode = new Node(2);
const newNode = new Node(1);
newNode.next = nextNode;
newNode.value; // 1
newNode.next.value; // 2
- Tenha o próximo nó.
 - Instancie o novo nó passando o valor e então atribuindo a referência ao próximo nó (
nextNodeno nosso caso). - Podemos acessar o valor 
valuee o valornext. 
Para a lista ligada, o primeiro passo é criar uma classe que a represente. Por enquanto, queremos apenas um atributo head ao criar uma lista vazia.
class LinkedList {
  constructor() {
    this.head = null;
  }
}
Simples assim. Apenas uma classe e inicialize o atributo head com null para uma lista vazia.
Vamos implementar o método mais fácil: isEmpty. Como sabemos quando uma lista está vazia? Se o head for null, não adicionamos nenhum nó a esta lista. Esta é a lógica por trás do método isEmpty.
isEmpty() {
  return this.head === null;
}
Bem simples, hein?
Agora o método pushFront. Basicamente, precisamos criar um novo nó, apontar o atributo next deste novo nó para o head e atribuir este novo nó para ser a nova lista ligada head.
Lembra que temos o parâmetro next ao criar um novo nó? Podemos usá-lo para atribuir o head anterior ao criar o novo nó. Algo assim:
new Node(value, previousHead);
No contexto da lista ligada, teremos o this.head. Então:
new Node(value, this.head);
O último passo é atribuir este novo nó ao head e nós o precederemos.
this.head = new Node(value, this.head);
- Criar novo nó
 - Atribua o atributo 
nextaoheadanterior - E atribua o novo nó ao 
head 
O método completo será assim:
pushFront(value) {
  this.head = new Node(value, this.head);
}
Apenas uma linha. Muito bom!
Para o pushBack, é um pouco diferente, pois, em vez de adicionar um novo nó ao início da lista, precisamos adicionar ao final. Então, basicamente, precisamos percorrer a lista para estar no último nó e apontar o atributo next para o nó recém-criado.
A questão é: como iteramos a lista?
A diferença entre o nó de cauda e o resto é o atributo next. A cauda não tem 'próximo'. Ele aponta para null. O resto sempre aponta para um nó diferente.
Para percorrer a lista para obter o último nó, obtemos o próximo nó até que o nó não tenha o atributo next. Comece com o primeiro nó: a cabeça.
let currentNode = this.head;
E então iterar.
while (currentNode && currentNode.next) {
  currentNode = currentNode.next;
}
Dividimos este código em duas partes:
- loop enquanto o nó não é 
nulle o atributonextdo nó também não énull - atualizar o nó atual atribuindo o próximo nó
 
Quando o loop while é interrompido, temos o último nó, então só precisamos atualizar o atributo next do último nó.
currentNode.next = new Node(value);
O código completo:
pushBack(value) {
  let currentNode = this.head;
  while (currentNode && currentNode.next) {
    currentNode = currentNode.next;
  }
  currentNode.next = new Node(value);
}
A implementação do método size é direta. Basicamente, precisamos percorrer toda a lista e contar cada nó.
Para iterar é bem simples. Nós só precisamos fazer um loop enquanto o nó atual não for null.
while (currentNode) {
  currentNode = currentNode.next;
}
E para cada iteração, precisamos aumentar nosso contador.
size() {
  let count = 0;
  let currentNode = this.head;
  while (currentNode) {
    count += 1;
    currentNode = currentNode.next;
  }
  return count;
}
- Inicialize o 
countcom0. - Obter o nó atual: o 
head. - Iterar através da lista.
 - Para cada iteração, aumente o contador.
 - Retorna a 'contagem'.
 
Para o algoritmo search, precisamos receber um valor e retornar true ou false se este valor estiver na lista ligada.
Então, basicamente, precisamos percorrer a lista ligada procurando por esse valor.
A iteração é simples:
while (currentNode) {
  currentNode = currentNode.next;
}
Agora, para cada nó, vemos se o valor do nó atual é o mesmo que o valor pesquisado.
while (currentNode) {
  if (currentNode.value === value) {
    return true;
  }
  currentNode = currentNode.next;
}
Podemos fazer desta forma para retornar true se o valor pesquisado for encontrado. Ou podemos fazer essa verificação somente depois que o loop parar. Então, precisaríamos parar o loop se encontrarmos o valor.
while (currentNode && currentNode.value !== value) {
  currentNode = currentNode.next;
}
- Iremos iterar enquanto não encontramos o valor e não é o último nó
 - Basicamente, o loop irá parar ao encontrar o valor pesquisado ou finalizar toda a lista ligada
 
Para retornar o valor, podemos usar a função Boolean.
return Boolean(currentNode && currentNode.value === value);
Com isso, cobrimos todas as possibilidades:
- Quando 
currentNodeénull:Booleantransformanullemfalse - Quando 
currentNodenão énulle o valor é igual ao valor pesquisado 
Para simplificar, também poderíamos escrever a declaração assim:
return Boolean(currentNode);
Porque se temos o currentNode, é porque encontramos o valor pesquisado. Se não tiver o currentNode (o nó é null), é porque não encontramos o valor pesquisado.
search(value) {
  let currentNode = this.head;
  while (currentNode && currentNode.value !== value) {
    currentNode = currentNode.next;
  }
  return Boolean(currentNode);
}
O último método a ser implementado é o método remove. Podemos pensar neste método em casos separados:
- quando a lista está vazia.
 - quando queremos remover o nó principal.
 - quando queremos remover um nó do meio ou do último.
 
Para o caso vazio é bastante simples. Nós apenas verificamos a lista com nosso método isEmpty.
if (this.isEmpty()) {
  return;
}
Também podemos lançar uma exceção de erro ou apenas imprimir "A lista está vazia", por exemplo.
Para o caso em que queremos remover o nó principal, verificamos primeiro e depois o removemos.
if (this.head.value === value) {
  this.head = this.head.next;
  return;
}
Para removê-lo, basta apontar a cabeça para o próximo nó.
O último caso é quando queremos remover um nó do meio ou o último. Vamos desenhá-lo!

Para este algoritmo, o que queremos é obter o nó anterior do nó a ser removido e apontar para o próximo nó do nó a ser removido. Portanto, precisamos ter o nó anterior em cada iteração. Esta é a parte fundamental do nosso algoritmo.
let currentNode = this.head;
while (currentNode.next) {
  if (currentNode.next.value === value) {
    currentNode.next = currentNode.next.next;
  }
  currentNode = currentNode.next;
}
Este é o algoritmo.
Iremos percorrer a lista enquanto o próximo nó atual não for um valor null. Por quê? Porque queremos comparar o próximo valor do nó. Não o atual.
currentNode.next.value === value;
Esta é a lógica que procuramos. O próximo valor do nó atual é o valor que queremos remover?
Se for true, basicamente removemos o próximo nó do nó atual apontando o next para o next.next e retornando a função.
Se for false, continuamos iterando até encontrarmos o valor que queremos ou quando terminarmos a lista inteira.
Juntando todas as partes, temos:
remove(value) {
  if (this.isEmpty()) {
    return;
  }
  if (this.head.value === value) {
    this.head = this.head.next;
    return;
  }
  let currentNode = this.head;
  while (currentNode.next) {
    if (currentNode.next.value === value) {
      currentNode.next = currentNode.next.next;
    }
    currentNode = currentNode.next;
  }
}
A classe Linked List
Juntando todas as partes que falamos e implementamos, temos:
class Node {
  constructor(value, next = null) {
    this.value = value;
    this.next = next;
  }
}
class LinkedList {
  constructor() {
    this.head = null;
  }
  pushFront(value) {
    this.head = new Node(value, this.head);
  }
  pushBack(value) {
    let currentNode = this.head;
    while (currentNode && currentNode.next) {
      currentNode = currentNode.next;
    }
    currentNode.next = new Node(value);
  }
  size() {
    let count = 0;
    let currentNode = this.head;
    while (currentNode) {
      count += 1;
      currentNode = currentNode.next;
    }
    return count;
  }
  search(value) {
    let currentNode = this.head;
    while (currentNode && currentNode.value !== value) {
      currentNode = currentNode.next;
    }
    return Boolean(currentNode);
  }
  remove(value) {
    if (this.isEmpty()) {
      return;
    }
    if (this.head.value === value) {
      this.head = this.head.next;
      return;
    }
    let currentNode = this.head;
    while (currentNode.next) {
      if (currentNode.next.value === value) {
        currentNode.next = currentNode.next.next;
        return;
      }
      currentNode = currentNode.next;
    }
  }
  isEmpty() {
    return this.head === null;
  }
}
Vamos testar!
const linkedList = new LinkedList();
linkedList.isEmpty(); // true
linkedList.size(); // 0
linkedList.pushFront(1);
linkedList.isEmpty(); // false
linkedList.size(); // 1
linkedList.head; // new Node(1)
linkedList.pushBack(2);
linkedList.pushBack(3);
linkedList.pushBack(4);
linkedList.size(); // 4
linkedList.pushFront(0);
linkedList.size(); // 5
linkedList.search(0); // true
linkedList.search(1); // true
linkedList.search(2); // true
linkedList.search(3); // true
linkedList.search(4); // true
linkedList.search(5); // false
linkedList.remove(5);
linkedList.size(); // 5
linkedList.remove(0);
linkedList.size(); // 4
linkedList.remove(4);
linkedList.size(); // 3
linkedList.remove(2);
linkedList.size(); // 2
linkedList.remove(1);
linkedList.size(); // 1
linkedList.remove(3);
linkedList.size(); // 0
linkedList.isEmpty(); // true
O que fazemos aqui?
- Crie a lista ligada
 - Verifique se está vazio
 - Verifique o tamanho da lista
 - Empurre um novo item para a frente
 - Agora não está mais vazio, tem tamanho 1, e a cabeça é o nó com valor 1
 - Empurre novos valores para o final da lista: 2, 3, 4. E agora o tamanho da lista é 4
 - Empurre um novo valor para o início da lista: 0. Tamanho: 5
 - Procure por 0 a 4: todos retornam 
true, encontramos o valor - Procure por 5: retorna 
falsepois não temos esse valor na lista - Remova 5 e a lista mantém o tamanho de 5
 - Remova os valores de 4 a 0, a lista está vazia e com tamanho 0