JavaScript数据结构与算法基础学习笔记03----链表与双向链表

前言:相对于结构比较简单的栈和队列,链表的结构就复杂一些。链表中的节点元素在存储元素值的同时,还包括了前后元素的的指引,又可以称之为指针。本篇博客主要记录了个人学习链表数据结构过程中的一些笔记,包含了基本的单向链表、双向链表、循环链表,以及补充了基于双向链表封装的栈结构方法。链表作为存储有序元素的集合,内部的元素在内存中并不是连续放置的。在增加和删除元素操作比较频繁的时候,链表由于不需要移动移动其他元素的位置相对于数组而言更加高效。

一、链表的相关概念以及和数组的区别

很多人都玩过寻宝游戏,就是最初得到一条线索,此条线索就是指向寻找下一条线索的地点的指针,每个地点都有下一条线索的地址,直到找到最终宝物。这和链表的结构及其相似。下图是最简单的链表结构示意图,也称之为单向链表结构。

数组:

1、创建的时候需要申请一段连续的内存空间,大部分语言中数组在初始化的时候长度就是固定的,当数组的容量不满足新的要求时,往往需要进行扩容操作,数组扩容很消耗资源。

2、在数组中插入和删除元素需要对其他的元素进行移动,操作成本很高。

链表:

1、链表中的元素在内存空间的存储不是连续的,内存空间的利用率很高。

2、链表中的每一个节点都是由一个存储元素值本身和指向下一个元素的引用所组成的。

作为一种数据结构,我们通常需要进行增删改查的操作。链表的方法也无非就是增删改查四个方面,接下来我们就先使用构造函数的方式封装一个单向链表结构,包含其基本属性和操作方法。

二、利用构造函数封装一个链表结构

       利用构造函数封装一个链表结构如下,内部包含了一个创建子节点的构造函数。

​
function LinkList(){
    //1 内部节点类
    function Node(data){
           this.data=data;
           this.next=null;
    }
    //属性
    this.head=null;
    this.length=0;

    //方法
    //1、向列表的尾部添加一个新的项。
    this.append=(data)=>{
        let newNode=new Node(data);
        //链表里面如果为空,则直接将head指向插入的节点元素。
        if(this.length===0){
            this.head=newNode;    //将newNode赋值给this.head表示其指向。
        }else{
            //循环找到最后一个节点,while循环体可以实现查找到满足条件的元素
            let current=this.head;  //声明一个变量表示链表中的第一个节点。
           while(current.next!==null){
                current=current.next;
           }
           //并将其指向赋值给newNode完成在链表尾部添加元素
           current.next=newNode;
        }
        //插入元素后别忘了将链表的长度加1,也可以写成this.length++
        this.length+=1;
    }
    //2、insert方法,将元素插入到链表指定的位置
    this.insert=(position,data)=>{
        let newNode=new Node(data);
        //检查越界值,如果不在范围内,则返回false
        if(position<0||position>this.length) return false;
        //当链表内部为空时,直接插入到第一个位置,默认的索引位置从1开始
        // this.append(data);
        // this.length+=1;
        if(position==0){
            //断开head和当前第一个元素的关系,将插入的元素的next指向当前第一个元素
            newNode.next=this.head;
            //再将head的指向调整为newNode。
            this.head=newNode;
        }else{
            //插入的位置不是第一个
            //声明一个变量存储第一个元素,以及一个变量接收当前元素的前一个元素
            //声明一个变量index作为查找指定位置的循环条件
            let index=0;
            let current=this.head;
            let previous=null;
            while(index++!=position){
                previous=current;   //存储当前位置元素
                current=current.next; //将当前元素指向下一个循环
            }
            //查找到指定位置后,将需要插入的元素位置的next指向current表示其向后移动一位
            newNode.next=current;
            //前一个元素的next指向需要插入的元素。
            previous.next=newNode
        }
        //插入元素完成后,链表长度增加1
        this.length+=1;
        return true;
    }
    //3、返回指定位置的元素值
    this.getPosition=(position)=>{
        if(position<0||position>=this.length) return null; 
        let current=this.head; //存储第一个元素
        let index=0; //查找到满足条件的元素
        while(index++!==position){
            current=current.next;
        }
        return current.data;
    }
    //4、返回元素在列表中的索引。如果没有该元素,则返回-1
    //链表中查找元素要从第一个元素开始
    this.indexOf=(data)=>{
        let current=this.head;
        let index=0;
        //依旧使用当current为空作为循环退出条件
        while(current){
            if(current.data==data){
                return index;
            }
            current=current.next;
            index+=1;
        }
        //循环体结束还没有找到,表示元素不存在。
        return -1;
    }
    //5、修改某个元素的位置。即根据给定的位置信息将其替换成最新的
    this.update=(position,data)=>{
        //先做是否越界的判断
        if(position<0||position>=this.length) return false;
        let current=this.head;
        let index=0;
        while(index!==position){
            current=current.next;
            index+=1;
        }
        //找到了指定位置的元素,将其新传入的值重新赋值
        current.data=data;
        return true;
    }
    //6、将指定位置的移出一项
    this.removeAt=(position)=>{
       if(position<0||position>=this.length) return false;
       if(position==0){
           this.head=this.head.next;
       }
       let current=this.head;
       let previous=null;
       let index=0;
       while(index!==position){
           previous=current;
           current=current.next;
           index+=1;
       }
       previous.next=current.next; //断开指定元素的连接就是将指定元素的下一个元素和当前元素的前一个元素建立索引关系
       this.length-=1;
       return true;
    }
    //7、删除链表中的指定元素的一项。
    this.remove=(data)=>{
        //直接调用前面封装好的查找和删除方法
        let position=this.indexOf(data);
        //根据查找到的位置信息删除元素
        return this.removeAt(position);
    }
    //判断链表内容是否为空
    this.isEmpty=()=>{
        return this.length===0;
    }
    // 返回链表中元素的个数多少
    this.size=()=>{
        return this.length;
    }
    //toString方法
      this.toString=()=>{
          //1、获取链表中的第一个元素,实际上根据this.head表示的就是第一个元素。
          let current=this.head;
          //2、定义一个空的字符串来接收结果
          let resultString="";
          while(current){
              resultString+=current.data+" ";  //循环迭代字符串化。
              current=current.next;   //自身字符串化之后指向下一个节点,直到指向空退出循环
          }
          return resultString;
      }
}

​

链表封装完成后,我们应该对链表中的方法进行测试,通过测试结果查看其是否达到了想要的效果。测试代码如下:

let links=new LinkList();
//1、测试向链表尾部添加元素的方法
links.append('lsx');
links.append('lls');
links.append('zdy');
console.log(links.toString())
//2、测试向链表的指定位置添加元素的方法
links.insert(3,'hyw'); 
//3、测试将链表中的数据字符串输出的方法
console.log(links.toString());
console.log(links.length);
//4、测试链表的长度是否正确
console.log(links.isEmpty());
//5、测试获取指定位置元素的方法
console.log(links.getPosition(3));
//6、查找指定元素在链表中索引值的方法
console.log(links.indexOf('lls'));
console.log(links.indexOf('zzz'));
//7、将链表中指定位置的元素进行更新的方法
console.log(links.update(1,'sjj'));
console.log(links.toString());
//8、移出指定位置元素的方法
console.log(links.removeAt(3));
console.log(links.toString());
//9、移出指定元素的方法
console.log(links.remove("zdy"));  //测试remove其返回值结果为true,证明查找到了此元素进行删除。
console.log(links.toString());

测试结果如下,证明封装的链表属性和方法均有效。

 三、使用类封装一个双向链表结构

   链表相对于数组而言,在删除和修改元素的时候效率更高,但其也存在着很多的局限性。比如我们可以依次往下查找到元素,但是有时候想回到上一个元素时,我们需要从链表首部重新开始往下查找。双向链表中的元素节点不仅包含了当前元素值和下一个元素的引用地址,还包括了对前一个元素的引用地址,对于我们提高链表的使用效率大有帮助。但是在处理其元素的插入、删除时,我们需要处理好的引用关系也相对较多,需要仔细理清楚引用之间的关系。双向链表的尾部也多了一个引用tail,可以通过其获取到链表的最后一个元素。

class DoubleNode{
    constructor(element){
       this.element=element;
       this.next=null;
       this.prev=null;
    }
}
class DoubleLinkList{
    constructor(){
      this.head=null;
      this.tail=null;
      this.length=0;
    }
    //1、向链表的尾部添加一个元素。
    append(element){
      //如果链表为空,则将元素插入到第一个位置
      let newNode=new DoubleNode(element)
      if(this.length===0){
          //head指向第一个节点,this.head实际上就是第一个元素。
          this.head=newNode;
          //tail指向最后一个节点,this.tail实际上就是最后一个元素
          this.tail=newNode;
      }else{
          //链表中存在一个tail表示尾部的节点元素,可以实现从尾部开始查找
          //需要先获取到当前最后一个节点,将其next指向新插入的节点,再改变其tail的指向
          this.tail.next=newNode;
          newNode.prev=this.tail;  //newNode的prev指向当前最后一个节点,
          this.tail=newNode;  //再改变this.tail的指向。
      }
      //链表长度增加1
      this.length+=1;
    }
    //2、向链表的指定位置插入一个元素,这也是相对情况最多的一种方法,要考虑清楚。
    insert(position,element){
        let newNode=new DoubleNode(element);
    //先做越界判断
    if(position<0||position>this.length) return false;
    //其实和单向链表类似,首先是需要找到指定位置的元素,然会对指引进行多一步的处理即可
    //但是需要充分考虑在首部和尾部插入元素时两个tail和head的指向都需要移动变化
    if(position==0){
        if(this.length==0){
            this.head=newNode;
            this.tail=newNode;
        }else{
            //链表中的首部存在了一个元素
            newNode.next=this.head;
            this.head.prev=newNode;
            this.head=newNode;
        }
    }else if(position==this.length){
        //在链表的末尾插入元素
        this.tail.next=newNode;
        newNode.prev=this.tail;
        this.tail=newNode;
    }else{
        //插入到链表的中间部分位置,先要求找到对应的位置,再进行插入操作。
        let current=this.head;
        let previous=null;
        let index=0;
        //通过while循环查找到对应的元素
        while(index<position){
            //保存当前元素
            previous=current;
            //继续赋值下一个元素
            current=current.next;
            //索引值加1
            index++;
        }
        //通过循环查找到了对应的元素值。
        //先断开建立右侧元素的关系
        newNode.next=current;
        current.prev=newNode;
        //再断开建立左侧的联系
        previous.next=newNode;
        newNode.prev=previous;


    } 
    //完成插入后链表的长度加1
    this.length+=1;
    return true;
}
    //3、删除指定位置的元素,这个操作方法也要仔细考虑清楚。
    removeAt(position){
        if(position<0||position>=this.length) return null;
        //1 删除链表首部的元素?
        let current=this.head;
        if(position===0){
            //如果链表中仅有一个元素
            if(this.length===1){
                this.head=null;
                this.tail=null;
            }else{
                //链表中不只有一个元素
                this.head=this.head.next;
                this.head.prev=null;
            }  
        }else if(position===this.length-1){
            current=this.tail;
            current.prev.next=null;
            this.tail=this.tail.prev;
        }else{
            let previous=null;
            let index=0;
            while(index!==position){
                previous=current;
                current=current.next;
                index++;
            }
            //找到目标元素后,断开指向关系并建立新的指引关系
            //右侧写需要指向的值,左侧写相关节点元素的指引
            current.next.prev=previous;
            previous.next=current.next;
            //将链表的长度相应的减1
            this.length--;
        }
        return current.element;
    }
    //双向链表中的update方法其实和普通链表中的一样,找到元素后,进行替换掉
    update(position,newElement){
          if(position<0||position>=this.length) return false;
          //let newNode=new DoubleNode(position,newElement);
          let current=this.head;
          let index=0;
          while(index!==position){
             if(current.element==newElement){
                 return current;
             }
             current=current.next;
             index++;
          } 
           current.element=newElement;
          return true;
    }
    //返回指定元素值的下标索引值
    indexOf(newElement){
        let current=this.head;
        let index=0;  //作为定义的索引值数据返回
        //以current不为空作为循环判断条件
        while(current){
            if(current.element==newElement){
                return index;
            }
            current=current.next;
            index++;
        }
        return -1; //没有查找到,则返回空值。
    }

    isEmpty(){
        return this.length===0;
    }
    size(){
        return this.length;
    }
    //将链表中的所有数据以字符串的形式输出
    toString(){
     let resultString='';
     let current=this.head;
     //以当前元素current不为空作为循环遍历的条件
     while(current){
         resultString+=current.element+'  ';
         current=current.next;
     }
     return resultString;
    }
}

​

测试双向链表的代码及其结果如下:

const dbl=new DoubleLinkList();
//1、测试向链表尾部添加元素的方法
dbl.append('lsx');
dbl.append('zdy');
dbl.append('hyw');
dbl.append('sx');
dbl.append('tz');
console.log(dbl.length);
console.log(dbl.toString())
//2、测试Insert方法
dbl.insert(3,'wsy');
dbl.insert(2,'sjj');
console.log(dbl.toString());
//3、测试指定位置删除元素,并返回所删除的元素的方法
console.log(dbl.removeAt(2));
//console.log(dbl.length);
console.log(dbl.toString());
//4、测试更新指定位置元素的方法
dbl.update(1,'dsd');
//5、测试返回指定元素下标索引的方法。
console.log(dbl.indexOf('dsd')); 
console.log(dbl.toString());
console.log(dbl.length);

 结果证明,上述使用类封装的双向链表属性以及方法封装完全符合条件。

双向链表和普通链表的区别主要在于增加和删除节点元素时,需要考虑的分类情况比较多,需要处理的指引值数量也较多,除了这两种常用到的链表,还有一些其他的链表结构。

四、循环链表和基于链表封装的栈

       循环链表并不是什么新的数据结构,其可以如链表一样只有单向的引用,也可以向双向链表一样存在双向引用关系。循环链表的唯一特点就是,最后一个元素的指向下一个元素的指针(tail.next)不是undefined,而是应该指向第一个元素head。双向的循环链表有指向元素head的tail.next,也有指向tail元素的head.prev。循环链表结构示意图如下。

 之前我们所封装的栈是基于数组的,在我们学习完链表结构之后,可以基于链表实现栈的的一些方法的封装,比较典型的方法就是出栈pop和入栈的方法push。

class StackLinkList{
    constructor(){
        //使用双向链表来存储数据
        this.items=new DoubleLinkList();
    }
    //1、将元素压入栈
    push(element){
        this.items.append(element)
    }
    //2、将元素出栈
    pop(){
        //先判断栈内是否为空,为空则返回Undefined;
        if(this.items.isEmpty()) return undefined;
        return this.items.removeAt(this.items.length-1);  //将栈顶的元素定向删除,也就是出栈
    }
}
//测试代码
let stack=new StackLinkList();
stack.push('111');
stack.push('222');
console.log(stack);
console.log(stack.pop('222'));

测试代码输出的结果如下:

五、总结归纳

    链表是目前为止学到的相对复杂一点的数据结构,相对于之前的栈和队列,我们需要在增删的同时处理其节点元素中的签后元素的索引值。学习链表结构我们要抓住以下几点,学起来会相对轻松一点。

1、紧扣链表中的属性,如this.length,存储指向头部元素的head,指向尾部元素的tail。在进行增删操作时,我们通常需要先通过this.head以及this.tail来获取链表中的首部节点元素和尾部节点元素,再根据具体情况修改其索引值的指向,右侧写元素节点,左侧写赋予元素节点的指引,结构清晰易理解。

2、链表的操作比较抽象,在写的时候可以配合着画图加强理解,根据图解理清楚指引的关系。

3、充分考虑所有的情况,以及一些特殊情况,比如在插入删除时需要考虑在首部,尾部,还是中间位置插入元素各种情况。

学习完链表之后,下面就是进入到集合Set和字典Map的学习了,预计花费2到3天时间,届时将会整理出相应的学习笔记和一些思考。未完待续..........

学习JavaScript数据结构与算法进度117/293  0531

再远的路只要不断出发总能走完!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值