阅读 970

JavaScript实现一个链表结构

​前言:

很感谢每一位关注二哥的朋友,在各位朋友的帮助下,二哥也学到了非常多的东西。也希望通过自己的学习到的知识,分享给每个需要的朋友。如果在文章中写的有错误的地方,还希望各位朋友多多指出。有更好的思路,想法也可以留言或者私聊我。不甚感激。

简介:

链表在数据结构中,是一个非常基础也是比较好理解的一个数据结构。每个不同的语言实现起来有细微差距,但是总体思路还是一样的。今天给大家介绍的是单向链表,单向链表和双向链表的区别就是少了一个父节点的引用。

链表的结构是由一个根节点包含两个字段,一个value当前节点的值,next下一个节点的引用。数组与链表非常相似,但是数组的结构是在内存中开辟出一整块空间,根据内容长度的不同来对这块空间进行扩容和减少空间。数组是紧密型的数据结构。通过索引对数据的存储,是一个有序的排列。相比链表,数组对了很多方法,也可以对数据进行不同的操作。

链表是对内存的堆中的一些引用进行串联成一个链子一样。不是有序的。

// 链表数据结构 [1, 2, 3 ]
let linkedList = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3,
      next: null
    }
  }
}
复制代码

在JavaScript中是不提供原生链表结构的,我们可以用代码自己模拟一个链表,链表需要有一个头部节点 head, 默认是null。我们要对这个链表添加一个节点,那就需要一个添加节点的方法,同时我们想知道这个链表是否包含某个节点,删除一个节点等等的一些功能,我列举一些常见的方法。

  1. addLast // 尾部添加一个节点
  2. addFirst // 添加一个头部节点
  3. isEmpty // 判断当前列表是否为空
  4. find // 查找指定元素
  5. findLast // 获取最后一个节点
  6. removeLast // 删除最后一个节点
  7. removeFirst // 删除第一个节点
  8. remove // 删除一个指定节点
  9. insert // 指定位置插入
  10. dir // 展开这个链表

现在我们知道了我们要实现的这个链表包含了那些功能,我们一个一个的来实现,首先我们需要一个链表的构造函数,成为一个容器来承载我们的链表也承载对应的方法。

class LinkedList{
  constructor() {
    // 添加一个长度属性
    this.size = 0;
    // 声明一个头部节点
    this.head = null;
    // 维护一个尾部节点(添加的时候非常便利)
    this.tail = null;
  }
}
复制代码

我们的链表基本结构就有了,我们还需要一个生成节点的构造函数,来辅助我们每次生成一个节点。

class Node{
  constructor( data ) {
    // 节点的value
     this.value = data;
     // 节点的下一个引用指向
     this.next = null;
  }
}
复制代码

我们现在准备工作已经差不多了。我们的链表也已经有了。但是就好像设计好了一个机器人,还没有给它设计动作指令,还只能看不能使用。那么我们从添加一个节点开始。

尾部添加节点,我们刚刚在设计linkedList的时候就预留了一个尾部节点的引用,这样在链表数据庞大的时候,我们不用遍历整个列表到最后一个节点去添加,这就会显得非常快捷,但是它初始值是null。证明还没有尾部节点。也就是我们根节点都还不存在,所以在添加节点的这个方法里。我们要做的事情是判断是否有根节点,有就用尾部节点去添加,没有就直接添加在根节点上,并且维护一下尾部节点,更新一下链表长度。

addLast( item ) {
  // 对参数做一个非空验证
  if ( !item ) { throw new Error('请输入一个正确的节点!') }
  // 创建一个节点
  let node = new Node(item);
  // 判断链表是否为空
  if ( this.size === 0 ) {
    // 更新尾部节点,更新头部节点
    this.tail = this.head = node;
  } else {
    // 设置尾部节点的引用
    this.tail.next = node;
    // 将最新的尾部节点更新到tail
    this.tail = node
  }
  // 更新一下长度
  this.size++;
}
复制代码

刚刚实现了在链表的尾部添加一个节点,对应我们还可以在头部添加一个节点。思路和刚刚相差不大,因为我们有头部节点,能够直接获取到。

addFirst( item ) {
  // 对参数做一个非空验证
  if ( !item ) { throw new Error('请输入一个正确的节点!') }
  // 创建一个node节点
  let node = new Node(item);
  // 判断当前链表是否为空
  if ( this.size === 0 ) {
     this.head = this.tail = node;
  } else {
    // 将整个链表作为新节点的next引用 
    node.next = this.head;
    // 将node赋值给head
    this.head = node;
  }
  // 更新一下长度
  this.size++
}
复制代码

头部添加一个节点也实现了。尾部添加一个节点也实现了。这个时候我们可以实现一下查找的方法。查找方法的主要作用是查看当前链表中是否包含某个节点,如果需要实现一个没有重复节点的链表,也可也在每次添加之前调用查找方法,看看添加的节点是否存在。我们这里就没有做类似的限制。

find( item ) {
  // 对参数做一个非空验证
  if ( !item ) { throw new Error('请输入一个正确的节点!') }
  // 获取到整个节点
  let linkedList = this.head;
  // 循环判断
  while ( linkedList && linkedList.next && linkedList.value !== item ){
    linkedList = linkedList.next;
  }
  return linkedList
}
复制代码

有了添加,查找。可以实现一下删除的功能首先实现一下头部删除,头部删除是比较简单的,找到第一个头部节点的引用返回,然后将头部指向头部节点的next引用就可以删除头部节点了。

removeFirst (){
  // 定义一个返回值
  let res = null;
  // 判断当前链表不为空
  if ( this.size !== 0 ) {
    res = this.head;
    // 跳过head节点,将head指向head的next
    this.head = this.head.next;
    res.next = null;
    // 维护一下长度
    this.size--;
    // 判断删除完,链表为空,清空一下尾部节点的引用
    this.tail = this.size ? this.tail : null;
  } else {
    // 如果链表为空,调用删除操作,就报一个警告
    throw new Error('链表为空,不能执行删除操作!')
  }
  return res
}
复制代码

删除头部节点实现了以后,可以把删除尾部节点也实现一下,删除尾部节点,需要先遍历到尾部节点的父级,然后更新尾部节点的引用。

removerLast() {
   // 定义一个返回值
  let res = null;
  // 拿到所有节点
  let linkedList = this.head;
  // 判断当前链表是否只有一个节点
  if ( this.size === 1 ) {
    res = this.head;
    this.head = this.tail = null;
    res.next = null;
  }
  // 判断是链表为为空
  if ( this.size === 0 ) {
   // 如果链表为空,调用删除操作,就报一个警告
    throw new Error('链表为空,不能执行删除操作!')
  } else {
    // 循环找倒数第二个节点
    while ( linkedList && linkedList.next && linkedList.next.next ){
      linkedList = linkedList.next;
    }
    res = linkedList.next;
    // 将最后一个引用置为空
    linkedList.next = null;
    // 维护一下尾部节点
    this.tail = linkedList;
    // 维护一下长度
    this.size--;
  }
  return res
}
复制代码

有了前两个删除的方法的经验,我们来实现一下指定节点的删除。删除指定节点也是需要遍历链表,获取到指定元素的父级,跟删除最后一个节点有点相似。只是不判断下下级,而是判断下下级是否等于指定删除的元素。由于我们今天分享的这个版本,并没有对唯一性限制,所以删除的时候,默认删除在找到的第一个元素删除。

remover( item ){
  // 定义一个返回值
  let res = null;
  // 对参数做一个非空验证
  if ( !item ) { throw new Error('请输入一个正确的节点!') }
  // 拿到整个链表
  let linkedList = this.head;
  // 判断链表只有一个节点
  if ( this.size === 1 && linkedList.value === item ) {
    res = linkedList;
    // 维护一下头部节点和尾部节点
    this.head = this.tail =  null;
    
  } else {
    // 循环找到指定节点的父级
    while ( linkedList && linkedList.next && linkedList.next.value !== item ) {
      linkedList = linkedList.next;
    }
    // 判断是否存在
    if ( linkedList && linkedList.next ) {
      // 存下删除节点
      res = linkedList.next;
      // 删除指定节点
      linkedList.next = linkedList.next.next;    
     
    } else {
      throw new Error('链表中没有该节点,不能执行删除操作!')
    }
  }
  
  // 判断一下删除节点是否为最后一个节点
  this.tail = linkedList.next ? this.tail : null;
  // 删除节点的next指向清空
  res.next = null;
   // 维护一下
   this.size--;
   return res
}
复制代码

有时候我们可能需要判断一下这个链表是否为空,我们实现一下 isEmpty 方法,这个方法比较讨巧。因为只需要判断一下我们维护的size属性,就能知道是否为空了。虽然说我们这里构建的链表其实可以在整个链表的属性中找到size属性。这个验证是否为空的函数是可以不用出的。但是在真实开发过程中用到的时候,其实是不希望size属性被修改的。也不会被暴露出来。JavaScript中比较灵活,很多限制没有像java那么好做。我们这里就没有去对其做限制。

isEmpty() {
  return this.size === 0
}
复制代码

现在我们的链表其实基本功能都能够满足了。一般对一个数据的操作无非增删改查,目前我们都已经实现了。还可以增加一个插入的功能,插入是指定位置插入,如果找不到该位置,抛错,如果位置正确,插入的节点不正确也抛错。

insert ( item, ele ) {
  // 验证两个参数不能为空
  if ( !item || !ele ) { throw new Error('请输入一个正确的节点或者查找元素!') }
  // 拿到整个链表
  let linkedList = this.head;
  // 循环查找匹配
  while ( linkedList  && linkedList.value !== item ) {
    linkedList = linkedList.next;
  }
  // 验证结果,结果只能是两个,找到,没找到
  if ( linkedList ) {
    // 生成一个节点
    let node = new Node(ele);
    // 将匹配到的节点的next引用指向新节点的next引用
    node.next = linkedList.next;
    // 将新节点赋值给 匹配到的节点
    linkedList.next = node;
    // 判断一下是否是插入到了最后一个元素
    this.tail = node.next ? this.tail : node;
    // 维护一下长度
    this.size++;
  } else {
    throw new Error('没有匹配到需要插入位置的元素!')
  }
}
复制代码

在平时也会关注到链表的第一项和链表的最后一项。在双向链表中,可以从头往后遍历,也可以从尾部朝前遍历。就需要直接拿到尾部节点,我们这个示例中没有涉及到双向链表。但是我们维护了一个尾部节点,获取链表最后一项还是比较方便的。

findLast() {
  return this.tail
}
复制代码

由于整个链表的结构比较复杂。查看起来并不是特别的方便,在文章开头的时候简单讲解了一下3个节点的展开面结构。已经不太利于阅读了。那如果我们链表长度是30,可能展开面就非常大了。这个时候我们就需要有一个方法来查看一下整个链表的数据了。

dir(){
  // 定义一个返回值
  let res = '';
  // 获取整个链表
  let linkedList = this.head;
  res = linkedList ? linkedList.value : '';
  // 循环获取整个链表的数据
  while ( linkedList.next ) {
      res += '->' + linkedList.next.value;
      linkedList = linkedList.next;
  }
  // 返回数据
  return res
}
复制代码

链表可以实现的东西很多。是个非常不错的数据结构。比如可以用链表来实现一个微型的JavaScript中的数组,实现一个栈,实现一个队列。后期如果有空我再更新一章关于链表实现一个微型数组的文章。

关于链表的介绍和一些功能的实现就已经结束了,讲的不是很详细,如果有讲的不对的地方。代码规范,代码逻辑错误的地方,希望您可以多多指教。

关于上面介绍的链表的源代码我放到了gitee上,本来想放github上,不知道为啥今天上不去。我在上面也分享了一些在力扣上刷题的题解,今天分享了链表,我也对应找了一道 力扣上关于链表的中等题来实现。有兴趣的可以去看看,顺便点个小星星呀。谢谢

刚到掘金来。其他历史文章在公众号分享的,感兴趣的朋友可以关注一波公众号。以后也会同步更新公众号和掘金 公众号【 二哥前端学习之路 】

    gitee地址:https://gitee.com/chengxinyingtianxia/leetcode
复制代码
文章分类
前端
文章标签