JavaScript数据结构之链表--设计

上一篇文章中介绍了几种常见链表的含义,今天介绍下如何写出正确的链表代码。node


如何表示链表

咱们通常设计的链表有两个类。Node 类用来表示节点,LinkedList 类提供了一些辅助方法,好比说结点的增删改查,以及显示列表元素等方法。 接下来看看如何用 js 代码表示一个链表。git

代码演示:github

{
  var Node = function(data) {
    this.data = data;
    this.next = null;
  };
  var node1 = new Node(1);
  var node2 = new Node(2);
  var node3 = new Node(3);

  node1.next = node2;
  node2.next = node3;
  console.log(node1.data);
  console.log(node1.next.data);
  console.log(node1.next.next.data);
}
复制代码

Node 类包含两个属性:data 用来保存节点上的数据,next 用来保存指向下一个节点的连接。算法

{
    var LList = function() {
        this.head = new Node('head');
        this.find = find;
        this.insert = insert;
        this.remove = remove;
        this.display = display;
    }
}
复制代码

LList 类提供了对链表进行操做的方法。该类中使用一个 Node 类来保存链表中的头结点。当有新元素插入时,next 就会指向新的元素。bash

怎么样,简单吧,你已经学会链表了数据结构

但,这只是基本的链表表示法,水还很深。app

注意事项

把上一篇文章中的单链表图搬过来,方便参考post

重点一:理解指针或引用的含义

这里的指针或者引用,他们的意思都是同样的,都是存储所指对象的内存地址。this

将某个变量赋值给指针,实际上就是将某个变量的地址赋值给指针,或者反过来讲,指针中存储了这个变量的内存地址,指向了这个变量,经过指针就能找到这个变量。spa

看下面的伪代码表示什么意思:

p -> next = q;
复制代码

这行代码就是说 p 结点中的 next 指针存储了 q 结点的内存地址。

再看下面的代码表示什么:

p -> next = p -> next -> next;
复制代码

这行代码表示,p 结点的 next 指针存储了 p 结点的下下一个结点的内存地址。

如今应该能有所体会指针或者引用的概念了吧。

重点二:警戒指针丢失和内存泄露

在写链表代码的时候,尤为是咱们的指针,会不断的改变,指来指去的。因此在写的时候,必定注意不要弄丢了指针。

以下图中单链表的插入操做

咱们但愿在结点 B 和相邻的结点 C 之间插入 D 结点。假设当前指针 p 指向 B 结点。若是你将代码实现成下面这个样子,就会发生指针丢失和内存泄露。

// 伪代码
p -> next = d; // 将 p 的 next 指针指向 D 结点;
d -> next = p -> next // 将 D 的结点的 next 指针指向 C? 结点。
复制代码

咱们来分析下:p -> next 指针在完成第一步操做后,已经再也不指向结点 C 了,而是指向新增长的结点 D。第二行的代码至关于将 D 赋值给 d->next,本身指向本身。所以,整个链表也就被截断了。

因此咱们添加结点时,必定要注意操做的顺序,要先将结点 D 的 next 指针指向结点 C,再把结点 B 的指针指向 D,这样才不会丢失指针。对于刚才的那段代码,你知道怎么修改才是正确的了吧。

重点三:重点留意边界条件处理

首先来回顾下刚才所说的单链表的插入操做。若是在 p 结点后面增长一个新的结点,只须要关注如下两步便可。

new_node -> next = p -> next;
p -> next = new_node;
复制代码

可是,当咱们向一个空链表中插入第一个结点时,就须要特殊处理了。当链表为空时,也就是链表的head为空,那直接赋值便可,以下:

if(head == null) {
    head = new_node;
}
复制代码

看一段完整的添加节点代码:

// 添加一个新结点 tail:表示尾结点
  append(data) {
    const node = new Node(data);
    if (!this.head) {
      this.head = node;
      this.tail = this.head;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
  }
复制代码

若是头结点不存在的话,头结点等于尾结点。若是头结点存在的话,利用尾结点来扩充链表的数据,别忘了再移动 tail 成为尾结点。

再来看单链表结点的删除操做。若是在p结点后删除一个结点,只须要关注一步便可:

p -> next = p -> next -> next;
复制代码

可是,当链表中只剩一个结点head时,也须要特殊处理才能够,以下:

if(head -> next == null){
    head = null;
}
复制代码

删除的代码逻辑请查看 github-链表-remove

我提供的方法比较繁琐,当阅读了《数据结构与算法JavaScript描述》的时候,发现书上写的方法特别简洁。在此分享一下。

如下是此书中的删除结点代码:

{
/* 首先根据要删除的元素,检查每个结点的下一个结点中是否存储着要删除的数据。 若是找到,则返回该结点,即前一个结点。 */
  function findPvevious(item) {
    var currNode = this.head;
    while(!(currNode.next == null) && (currNode.next.element != item)) {
      currNode = currNode.next;
    }
    return currNode;
  }
 // 而后找到前一个结点后,利用上文提到的单链表删除操做进行删除。
  function remove(item) {
    var prevNode = this.findPvevious(item);
    if(!(prevNode.next == null)) {
      prevNode.next = prevNode.next.next;
    }
  }
}
复制代码

因此写链表代码时,要常常注意边界条件是否考虑到了:

  • 若是链表为空时,代码是否能正常工做?
  • 若是链表只有一个结点时,代码是否能正常工做?
  • 若是在处理头结点和尾结点时,代码是否能正常工做?

重点四:要学会画图辅助思考

好比一些单链表的增删改查等,指针老是不断的改变。这个时候若是大脑思考不过来的话,能够简单的画个示意图辅助一下。好比说单链表的增长结点操做,能够画出增长先后的链表变化。

看图写代码,是否是会比较清楚指针接下来的指向呢。

代码演示

请前往 github 查看 链表常见的方法

参考书籍

《数据结构与算法JavaScript描述》

有你才完美

链表这种数据结构,确实比较容易懂,可是想写出好相关的操做代码,确实不易,指针或者引用也是个人薄弱环节,指来指去便记不清该怎么指啦!动物的关节对于动物来说很是的重要,指针感受就是链表的关节,链表像一辆火车,每一个车箱的链接全靠车箱间的链接轴。

传送门

  1. JavaScript数据结构之栈
  2. JavaScript数据结构之队列
  3. JavaScript 数据结构之队栈互搏
  4. JavaScript数据结构之链表--介绍
  5. JavaScript 算法之复杂度分析
  6. JavaScript 算法之最好、最坏时间复杂度分析
相关文章
相关标签/搜索