最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JavaScript实现一个链表结构

    正文概述 掘金(诚信赢天下)   2021-05-28   416

    ​前言:

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

    简介:

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

    链表的结构是由一个根节点包含两个字段,一个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
    

    起源地下载网 » JavaScript实现一个链表结构

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元