На прошлой неделе я рассмотрел, что такое (односвязные) списки, теперь давайте вернемся и посмотрим, что мы можем с ними сделать.

Напоминаем, вот диаграмма, изображающая связанный список:

А вот код, который мы использовали для его реализации:

class Node {
  constructor(data, next = null) {
    this.data = data;
    this.next = next; 
  }
}
class LinkedList {
  constructor(){
    this.head = null
  }
  
  insertAtHead(node){
    this.head = node
  }
  
  clearList(){
    this.head = null
  }
}

Таким образом, предпосылка заключается в том, что мы продолжаем добавлять nodes к head, и они будут ссылаться на следующие node. Следующий node будет ссылаться на node рядом с ним, и эта цепочка продолжается, пока мы не достигнем tail (который был первым вставленным node). На данный момент мы знаем, что это tail, так как он не ссылается ни на какие другие nodes.

Пока что мы реализовали метод для вставки node в head, а также метод для очистки всего списка. Но как насчет промежуточных методов?

Допустим, вместо вставки в head мы хотели вставить узел в tail. Как мы могли это сделать? Давайте работать над этим.

Сначала давайте попробуем получить правильные узлы. Легче всего получить узел head или первый узел. Мы можем использовать следующее внутри нашего класса связанного списка:

getFirstNode(){
  return this.head
}

Простой не так ли? Что ж, давайте применим ту же логику захвата узлов, чтобы захватить наш последний узел. Помните, мы знаем, что у нас есть хвост, как только мы достигаем узла, который, когда мы вызываем node.next, будет nil. И в нашем списке есть только один узел, который делает это, поэтому нам просто нужно повторять, пока мы не найдем его! Давайте посмотрим, как это выглядит в виде кода:

getLastNode(){
  let currentNode = this.head 
  //this will serve as our temporary node that is keep track
  //of where we are in the list
  while(currentNode.next){ //while node.next is truthy we continue
    currentNode = currentNode.next //reassign the current node
  }
  return currentNode // our loop will break when it doesn't have a
                     // next node, so we always stop at the tail
}

Итак, на данный момент мы добрались от одного конца связанного списка до другого конца. Помните, как мы вставляем узел? Мы должны дать ссылку

Теперь, чтобы мы могли вставить в конце, давайте просто дадим tail ссылку на следующий узел, который мы хотим вставить!

insertAtLast(data){
  const last = this.getLastNode() // DRY! Just reuse your code from before!
  last.next = new Node(data)
} 

На данный момент мы создали связанный список и добавили в него nodes. Если бы мы хотели проверить, вызываем ли мы просто linkedListExample.getFirstNode() или linkedListExample.get LastNode(), просто чтобы еще раз проверить, действительно ли мы вставили nodes и в правильную позицию. Мы также могли бы попытаться реализовать проверку того, что все наши nodes все еще там, создав функцию, которая проверяет размер/длину связанного списка. Логика здесь состоит в том, чтобы просто пересчитать все node в списке, пока мы не наткнемся на последний. Это очень похоже на то, что мы сделали, чтобы получить последний элемент:

getSize(){
  const currentNode = this.head //we start at the head 
  let counter = 0 // this will keep track of how many there are 
  while(currentNode){ //this will break after we've counted our last node
    currentNode = currentNode.next
    counter++
  }
  return counter
}

Теперь мы знаем, какой длины список, как взять head и tail и как добавить в начало и конец. Давайте разберемся, как их удалить!

Начнем с того, что head. Теперь мы должны быть осторожны с крайним случаем, когда в связанном списке ничего нет. В противном случае мы могли бы просто переназначить head и двигаться дальше:

removeFirstNode(){
  if(!this.head){
    return //if there's no head, just break out of the function
  } else {
    this.head = this.head.next //our previous head doesn't even exist to 
                               //us anymore
  }
}

Теперь, когда мы удалили head. Давайте прыгнем в кроличью нору и удалим tail. Нам все равно нужно будет отслеживать крайний случай, когда в списке ничего нет. Но мы также должны помнить, что если есть только head, нам больше ничего не нужно делать. Что касается ядра этого метода, мы должны продолжать итерацию, пока не дойдем до точки, где node.next.next === nil. Это немного сбивает с толку, я знаю, но это просто означает, что мы достигли node, чье next существует, поскольку мы все еще можем связать с ним next, но node’s следующим будет nil; и поскольку мы знаем, что tail’s next равно nil, у нас есть предпоследний узел! Вот в виде схемы:

Теперь для кода:

removeLastNode(){
    if(!this.head){
      return //again, if it's empty let's just break out of the function
    } else if(!this.head.next){
      this.head = null
      return //if we only have the head, we have nothing else to do
    } else {
      let currentNode = this.head //this keeps track of our current node
      let nextNode = this.head.next //this is to track the next node
      while(nextNode.next){ //will break when we get to the 2nd to last node
        currentNode = currentNode.next
        nextNode = nextNode.next
      }
      currentNode.next = null // we remove the reference to the old tail
    }
  }

Не так уж плохо, верно? Это просто вопрос обдумывания и выяснения того, какая логика должна быть применена. Я думаю, что наличие ручки и бумаги и возможность начертить эти узлы также помогают. По крайней мере, если вы не можете придумать код на месте для собеседования, по крайней мере, вы можете пройтись по логике того, как подойти к проблеме.

Надеюсь, это поможет!

Ресурсы:

https://en.wikipedia.org/wiki/Linked_list