关联列表(linkedList)的实现

178 阅读8分钟

与关联列表中的节点一起工作

在计算机科学中,你会遇到的另一个常见的数据结构是链表。链接列表是一个数据元素的线性集合,称为 "节点",每个节点都指向下一个节点。链接列表中的每个节点都包含两个关键信息:元素本身,以及对下一个节点的引用。

想象一下,你在一个队伍里。你的手放在队伍中的下一个人身上,而你后面的人的手也放在你身上。你可以看到你正前方的人,但他们挡住了前面其他排队的人的视线。一个节点就像队伍的人:他们知道自己是谁,他们只能看到排在后面的人,但他们不知道前面或后面的其他人。

//在代码编辑器中,已经创建了两个节点,小猫和小狗,并且我们已经手动将小猫节点与小狗节点连接起来。

//创建一个猫和狗的节点,并手动将它们添加到队伍中。
var Node = function(element) {
  this.element = element;
  this.next = null;
};
var Kitten = new Node('Kitten');
var Puppy = new Node('Puppy');

Kitten.next = Puppy;
const Cat = new Node('Cat');
const Dog = new Node('Dog');

Kitten.next = Puppy;
Puppy.next = Cat;
Cat.next = Dog;

创建一个链接列表类

让我们来创建一个链接列表类。每个链接列表开始时都应该有一些基本的属性:一个头(列表中的第一个项目)和一个长度(列表中的项目数)。有时你会看到链接列表的实现包含一个列表最后一个元素的尾部,但现在我们只坚持使用这两个。每当我们向链接列表添加一个元素时,我们的长度属性应该增加1。

我们要创建的第一个方法是 add 方法。

如果我们的列表是空的,向我们的链接列表添加一个元素是很简单的:我们只需将该元素包裹在一个 Node 类中,然后将该节点分配到我们的链接列表的头部。

但是如果我们的列表已经有一个或多个成员呢?我们如何向列表中添加一个元素呢?回顾一下,链表中的每个节点都有一个 next 属性。要向列表中添加一个节点,找到列表中的最后一个节点,并将该最后一个节点的下一个属性指向我们的新节点。(提示:当一个节点的下一个属性为空时,你就知道你已经到达了链接列表的末端)。

编写一个添加方法,将你推送到链表的第一个节点分配到头部;此后,每当添加一个节点时,每个节点都应该被前一个节点的下一个属性所引用。

注意: 每当一个元素被添加到链接列表中,你的列表的长度应该增加一个。

function LinkedList() {
  let length = 0;
  let head = null;

  function Node(element) {
    this.element = element;
    this.next = null;
  }

  this.head = () => head;

  this.size = () => length;

  this.add = element => {
    const node = new Node(element);
    if (head) {
      let current = head;
      while (current.next !== null) {
        current = current.next;
      }
      current.next = node;
    }
    else {
      head = node;
    }
    length++;
  };
}

从一个链接列表中删除元素

任何链接列表的实现都需要的下一个重要方法是移除方法。这个方法应该接受我们想要移除的元素作为参数,然后在列表中搜索,找到并移除包含该元素的节点。

每当我们从一个链接列表中删除一个节点时,重要的是我们在这样做时不会意外地使列表的其他部分成为孤儿。回想一下,每个节点的下一个属性都指向列表中紧跟它的节点。如果我们要删除中间的元素,比如说,我们要确保我们有一个从该元素的前一个节点的下一个属性到中间元素的下一个属性的连接(也就是列表中的下一个节点!)。

这听起来可能非常令人困惑,所以让我们回到队伍的例子,这样我们就有了一个好的概念模型。想象一下你在一个队中,你前面的人离开了队伍。刚刚离开队伍的人的手不再放在队伍中的任何人身上--而且你的手也不再放在离开的人身上。你向前走,把你的手放在你看到的下一个人身上。

如果我们想删除的元素是头部元素,我们就把头部重新分配到链表的第二个节点。

编写一个删除方法,该方法接收一个元素并将其从链表中删除。

注意:每次从链表中删除一个元素时,链表的长度应该减少一个。

function LinkedList() {
  var length = 0;
  var head = null;

  var Node = function(element){
    this.element = element;
    this.next = null;
  };

  this.size = function(){
    return length;
  };

  this.head = function(){
    return head;
  };

  this.add = function(element){
    var node = new Node(element);
    if(head === null){
        head = node;
    } else {
        let current = head;

        while(current.next){
            current  = current.next;
        }
        current.next = node;
    }
    length++;
  };

  this.remove = function (element) {

if (head.element === element) {
      head = head.next;
      return length--;
    }
    let previous = head;
    while (previous) {
      let current = previous.next;
      if (current) {// 确保我们不在current.next为空的地方。
        if (current.element === element) {
          previous.next = current.next;
          return length--;
        }
      }
      previous = current;
    }
  };
}

在一个链接列表中搜索

让我们为我们的链接列表类添加一些更有用的方法。如果我们能够知道我们的列表是否为空,就像我们的 Stack 和 Queue 类一样,这不是很有用吗?

我们还应该能够在我们的链接列表中找到特定的元素。遍历数据结构是要大量练习的东西!让我们创建一个indexOf类。让我们创建一个indexOf方法,它以一个元素为参数,并返回该元素在链表中的索引。如果在链接列表中没有找到该元素,则返回-1。

让我们也实现一个相反的方法:一个 elementAt 方法,它接收一个索引作为参数并返回给定索引的元素。如果没有找到元素,则返回未定义。

  • 编写一个 isEmpty 方法来检查链接列表是否为空,
  • 一个 indexOf 方法来返回给定元素的索引,
  • 一个 elementAt 方法来返回给定索引处的元素。
function LinkedList() {
  var length = 0;
  var head = null;

  var Node = function(element){ // {1}
    this.element = element;
    this.next = null;
  };

  this.size = function() {
    return length;
  };

  this.head = function(){
    return head;
  };

  this.add = function(element){
    var node = new Node(element);
    if(head === null){
      head = node;
    } else {
      var currentNode = head;

      while(currentNode.next){
        currentNode  = currentNode.next;
      }

      currentNode.next = node;
    }

    length++;
  };

  this.remove = function(element){
    var currentNode = head;
    var previousNode;
    if(currentNode.element === element){
      head = currentNode.next;
    } else {
      while(currentNode.element !== element) {
        previousNode = currentNode;
        currentNode = currentNode.next;
      }

      previousNode.next = currentNode.next;
    }

    length --;
  };

  this.isEmpty = function() {
    return this.size() > 0 ? false : true;
  };

  this.indexOf = function(el) {
    let currentNode = head, index = -1, indexFound = false;

    while (!indexFound && currentNode) {
      index++;
      if(currentNode.element === el) {
        indexFound = true;
      }
      currentNode = currentNode.next;
    } 

    return indexFound ? index : -1;
  };

  this.elementAt = function(i) {
    let currentNode = head, currentElement, index = -1, indexReached = false;

    while (!indexReached && currentNode) {
      index++;
      currentElement = currentNode.element;
      if(index === i) {
        indexReached = true;
      }
      currentNode = currentNode.next;
    } 

    return indexReached ? currentElement : undefined;
  }
}

通过索引从链接列表中删除元素

在我们转到另一个数据结构之前,让我们对链接列表做一些最后的练习。

让我们写一个 removeAt 方法,删除指定索引的元素。这个方法应该被称为 removeAt(index)。为了删除某个索引上的元素,我们需要在沿着链表移动时保持每个节点的运行计数。

一个用于遍历链表元素的常见技术涉及到一个 "运行器",或称哨兵,它 "指向 "你的代码正在比较的节点。在我们的例子中,从列表的头部开始,我们用一个从 0 开始的 currentIndex 变量。

就像我们在上一节中讲到的remove(element)方法一样,当我们在removeAt(index)方法中删除节点时,我们需要注意不要让我们列表的其他部分成为孤儿。我们通过确保对被删除节点有引用的节点有对下一个节点的引用来保持我们的节点的连续性。

写一个removeAt(index)方法,删除并返回给定索引的节点。如果给定的索引是负数,或者大于或等于链表的长度,该方法应该返回null。

注意:记住要保持currentIndex的计数。

function LinkedList() {
  let head   = null;
  let length = 0;

  function Node(element) {
    this.element = element;
    this.next    = null;
  };

  this.size = function() {
    return length;
  };

  this.head = function() {
    return head;
  };

  this.add = function(element) {
    let node = new Node(element);
    if (head === null) {
      head = node;
    } else {
      let currentNode = head;
      while (currentNode.next) {
        currentNode  = currentNode.next;
      }
      currentNode.next = node;
    }
    length++;
  };

  // 
  this.removeAt = function(index) {
    // Exit early on bad input
    if (index < 0 || index >= length) {
      return null;
    }

    // Find deleted node and remove
    let deletedNode = head;
    if (index == 0) {
      head = null;
    } else {
      let currentNode  = head;
      let currentIndex = 0;
      while (currentIndex < index - 1) {
        currentNode = currentNode.next;
        currentIndex++;
      }
      deletedNode      = currentNode.next;
      currentNode.next = deletedNode.next;
    }

    // Update and return
    length--;
    return deletedNode.element;
  }
}

在一个关联列表中的特定索引处添加元素

让我们创建一个 addAt(index,element) 方法,在给定的索引上添加一个元素。就像我们如何在给定的索引上删除元素一样,我们需要在遍历链接列表时跟踪currentIndex。当currentIndex与给定的索引相匹配时,我们需要重新分配前一个节点的next属性来引用新添加的节点。而新节点应该引用currentIndex中的下一个节点。回到队伍的例子,一个新人想加入队伍,但他想在中间加入。你在队伍的中间,所以你把你的手从你前面的人身上拿开。新人走过来,把他的手放在你曾经把手放在的人身上,而你现在把你的手放在新人身上。

创建一个addAt(index,element)方法,在一个给定的索引处添加一个元素。如果元素不能被添加,则返回false。

注意:记住要检查给定的索引是否为负数或长于链表的长度。

function LinkedList() {
  var length = 0;
  var head = null;

  var Node = function(element) {
    this.element = element;
    this.next = null;
  };

  this.size = function() {
    return length;
  };

  this.head = function() {
    return head;
  };

  this.add = function(element) {
    var node = new Node(element);
    if (head === null) {
      head = node;
    } else {
      var currentNode = head;

      while (currentNode.next) {
        currentNode = currentNode.next;
      }

      currentNode.next = node;
    }
    length++;
  };

  this.addAt = (index, element) => {
    if (index < 0 || index >= length) {
      return false;
    }

    let node = head;
    if (index > 0) {
      let i = 0;
      while (i + 1 !== index) {
        node = node.next;
        i++;
      }
    }

    const newNode = new Node(element);
    newNode.next = node.next;

    if (index === 0) {
      head = newNode;
    } else {
      node.next = newNode;
    }

    length++;
  };
}

创建一个双链表

到目前为止,我们所创建的所有的链表都是单链表。这里,我们将创建一个双链表。顾名思义,双链表中的节点有对列表中下一个和上一个节点的引用。

这允许我们在两个方向上遍历列表,但它也需要使用更多的内存,因为每个节点都必须包含对列表中前一个节点的额外引用。

我们已经提供了一个Node对象并开始了我们的DoublyLinkedList。让我们为我们的双链表添加两个方法,叫做add和remove。add方法应该将给定的元素添加到列表中,而remove方法应该删除列表中给定元素的所有出现。

在编写这些方法时要注意处理任何可能的边缘情况,如删除第一个或最后一个元素。另外,在一个空的列表上删除任何项目都应该返回空。

反转一个双链表

让我们为我们的双链表再创建一个名为 reverse 的方法,它可以将列表原地反转。一旦该方法被执行,头部应该指向前一个尾部,尾部应该指向前一个头部。现在,如果我们从头到尾遍历列表,与原始列表相比,我们应该以相反的顺序遇到节点。试图反转一个空列表应该返回空。

var Node = function(data, prev) {
  this.data = data;
  this.prev = prev;
  this.next = null;
};
var DoublyLinkedList = function() {
  this.head = null;
  this.tail = null;

this.add = function(element) {
    let node = new Node(element, this.tail);
    let currentNode = this.head;
    let previousNode;

    if (this.head === null) {
      this.head = node;
      this.tail = node;
    } else {
      while (currentNode.next) {
        previousNode = currentNode;
        currentNode = currentNode.next;
      }
      node.prev = currentNode;
      currentNode.next = node;
      this.tail = node;
    }
  };
  this.reverse = function() {
    let temp = null;
    let currentNode = this.head;

    if (this.head === null) {
      return null;
    }

    this.tail = currentNode;

    while (currentNode) {
      temp = currentNode.prev;
      currentNode.prev = currentNode.next;
      currentNode.next = temp;
      currentNode = currentNode.prev;
    }

    if (temp != null) {
      this.head = temp.prev;
    }
  };
};