JavaScript【链表】

一、单向链表

1.链表和数组的缺点

2.链表的优势

3.单向链表简介

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有的语言称为指针或连接)组成。类似于火车头,一节车厢载着乘客(数据),通过节点连接另一节车厢。

 

 

  • head属性指向链表的第一个节点;
  • 链表中的最后一个节点指向null;
  • 当链表中一个节点也没有的时候,head直接指向null;

数组存在的缺点:

  • 数组的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的。所以当原数组不能满足容量需求时,需要扩容(一般情况下是申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)。
  • 在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

链表的优势:

  • 链表中的元素在内存中不必是连续的空间,可以充分利用计算机的内存,实现灵活的内存动态管理

  • 链表不必在创建时就确定大小,并且大小可以无限地延伸下去。

  • 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多。

链表的缺点:

  • 链表访问任何一个位置的元素时,都需要从头开始访问(无法跳过第一个元素访问任何一个元素)。
  • 无法通过下标值直接访问元素,需要从头开始一个个访问,直到找到对应的元素。
  • 虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。

链表中的常见操作:

  • append(element):向链表尾部添加一个新的项;
  • insert(position,element):向链表的特定位置插入一个新的项;
  • get(position):获取对应位置的元素;
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
  • update(position,element):修改某个位置的元素;
  • removeAt(position):从链表的特定位置移除一项;
  • remove(element):从链表中移除一项;
  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
  • size():返回链表包含的元素个数,与数组的length属性类似;
  • toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;

首先需要弄清楚:下文中的position指的是两个节点之间,并且与index的关系如下图所示:

 position的值一般表示position所指位置的下一个节点。当position的值与index的值相等时,比如position = index = 1,那么它们都表示Node2。

二、封装单向链表类

1.创建单向链表类

先创建单向链表类Linklist,并添加基本属性,再实现单向链表的常用方法:

    // 封装单向链表类
    function LinkList(){
      // 封装一个内部类:节点类
      function Node(data){
        this.data = data;
        this.next = null;
      }

      // 属性
      // 属性head指向链表的第一个节点
      this.head = null;
      this.length = 0;
      }

1.append(element)

    // 封装单向链表类
    function LinkList() {
      // 封装一个内部类:节点类
      function Node(data) {
        this.data = data;
        this.next = null;
      }

      // 属性
      // 属性head指向链表的第一个节点
      this.head = null;
      this.length = 0;

      // 1.追加方法
      LinkList.prototype.append=function(data){
        // 1.创建新的节点
        var newNode=new Node(data)

        // 2.判断是否添加的是第一个节点
        if(this.length===0){  //2.1 是第一个节点
          // 将head指向newNode
          this.head=newNode
        }else{
          // 先设置一个变量指向当前链表的最后一个节点
          var current=this.head
          while(current.next){//当不是最后一个节点,则不断向下指下去
            current=current.next
          }

          // 最后节点的next指向新的节点
          current.next=newNode
        }

        // 3.长度加1
        this.length+=1
      }
    }

过程详解:

  • 首先让current指向第一个节点:

  • 通过while循环使current指向最后一个节点,最后通过current.next = newNode,让最后一个节点指向新节点newNode:

测试代码: 

    //测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.测试append方法
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
    console.log(list);  

2.toString()

代码实现:

      // 实现toString方法
      LinkList.prototype.toString = () => {
        // 1.定义变量
        let current = this.head
        let listString = ""

        // 2.循环获取一个个的节点
        while(current){ 
          listString += current.data + " "
          current = current.next//千万不要忘了拼接完一个节点数据之后,让current指向下一个节点
        }
        return  listString
      }

测试代码:

    //测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
    
    //3.测试toString方法
    console.log(list.toString());

3.insert(position,element)

      // 实现insert方法
      LinkList.prototype.insert = (position, data) => {
      //理解positon的含义:position=0表示新界点插入后要成为第1个节点,position=2表示新界点插入后要成为第3个节点
        //1.对position进行越界判断:要求传入的position不能是负数且不能超过LinkList的length
        if(position < 0 || position > this.length){
          return false
        }
        //2.根据data创建newNode
        let newNode = new Node(data)

        //3.插入新节点
        //情况1:插入位置position=0
        if(position == 0){
          // 让新节点指向第一个节点
          newNode.next = this.head
          // 让head指向新节点
          this.head = newNode
        //情况2:插入位置position>0(该情况包含position=length)
        } else{
          let index = 0
          let previous = null
          let current = this.head
          //步骤1:通过while循环使变量current指向position位置的后一个节点(注意while循环的写法)
          while(index++ < position){
          //步骤2:在current指向下一个节点之前,让previous指向current当前指向的节点
            previous = current
            current = current.next
          }
          // 步骤3:通过变量current(此时current已经指向position位置的后一个节点),使newNode指向position位置的后一个节点
          newNode.next = current
          //步骤4:通过变量previous,使position位置的前一个节点指向newNode
          previous.next = newNode
          /*
            启示:
            1.我们无法直接操作链表中的节点,但是可以通过变量指向这些节点,以此间接地操作节点(替身使者);
            比如current指向节点3,想要节点3指向节点4只需要:current.next = 4即可。
            2.两个节点间是双向的,想要节点2的前一个节点为节点1,可以通过:1.next=2,来实现;
          */
        }
        //4.新节点插入后要length+1
        this.length += 1;

        return true
      }

过程详解:

inset方法实现的过程:根据插入节点位置的不同可分为多种情况:

  • 情况1:position = 0

通过: newNode.next = this.head,建立连接1;

通过: this.head = newNode,建立连接2;(不能先建立连接2,否则this.head不再指向Node1)

 

  • 情况2:position > 0

首先定义两个变量previous和curent分别指向需要插入位置pos = X的前一个节点和后一个节点;

然后,通过:newNode.next = current,改变指向3;

最后,通过:previous.next = newNode,改变指向4;

  • 情况2的特殊情形:position = length

情况2也包含了pos = length的情况,该情况下current和newNode.next都指向null;建立连接3和连接4的方式与情况2相同。

 测试代码:

    //测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
    
    //3.测试insert方法
    list.insert(0, '在链表最前面插入节点');
    list.insert(2, '在链表中第二个节点后插入节点');
    list.insert(5, '在链表最后插入节点');
    alert(list);
    console.log(list);

4.get(position)

      //实现get方法
      LinkList.prototype.get = (position) => {
        //1.越界判断
        // 当position = length时,取到的是null所以0 =< position < length
        if(position < 0 || position >= this.length){
          return null
        }
        //2.获取指定的positon位置的后一个节点的data
        //同样使用一个变量间接操作节点
        let current = this.head
        let index = 0
        while(index++ < position){
          current = current.next
        }
        return current.data
      }

过程详解:

get方法的实现过程:以获取position = 2为例,如下图所示:

  • 首先使current指向第一个节点,此时index=0;

  • 通过while循环使current循环指向下一个节点,注意循环终止的条件index++ < position,即当index=position时停止循环,此时循环了1次,current指向第二个节点(Node2),最后通过current.data返回Node2节点的数据;

 测试代码:

	//测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')	

	//3.测试get方法
    console.log(list.get(0));
    console.log(list.get(1));

5.indexOf(element)

代码实现:

      //实现indexOf方法
      LinkList.prototype.indexOf = data => {
        //1.定义变量
        let current = this.head
        let index = 0

        //2.开始查找:只要current不指向null就一直循环
        while(current){
          if(current.data == data){
            return index
          }
          current = current.next
          index += 1
        } 

        //3.遍历完链表没有找到,返回-1
        return -1
      }

过程详解:

indexOf方法的实现过程:

  • 使用变量current记录当前指向的节点,使用变量index记录当前节点的索引值(注意index = node数-1):

 测试代码:

	//测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')	
    
    //3.测试indexOf方法
    console.log(list.indexOf('aaa'));
    console.log(list.indexOf('ccc'));

6.update(position,element)

代码实现:

      //实现update方法
      LinkList.prototype.update = (position, newData) => {
        //1.越界判断
        //因为被修改的节点不能为null,所以position不能等于length
        if(position < 0 || position >= this.length){
          return false
        }
        //2.查找正确的节点
        let current = this.head
        let index = 0
        while(index++ < position){
          current = current.next
        }
        //3.将position位置的后一个节点的data修改成newData
        current.data = newData
        //返回true表示修改成功
        return true
      }

测试代码:

	//测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')	
    
    //3.测试update方法
    list.update(0, '修改第一个节点')
    list.update(1, '修改第二个节点')
    console.log(list);
    console.log(list.update(3, '能修改么'));

7.removeAt(position)

代码实现:

      //实现removeAt方法
      LinkList.prototype.removeAt = position => {
        //1.越界判断
        if (position < 0 || position >= this.length) {//position不能为length
          return null
        }
        //2.删除元素
        //情况1:position = 0时(删除第一个节点)
        let current = this.head
        if (position ==0 ) {
        //情况2:position > 0时
          this.head = this.head.next
        }else{
          let index = 0
          let previous = null
          while (index++ < position) {
            previous = current
            current = current.next
          }
          //循环结束后,current指向position后一个节点,previous指向current前一个节点
          //再使前一个节点的next指向current的next即可
          previous.next = current.next
        }
        //3,length-1
        this.length -= 1

        //返回被删除节点的data,为此current定义在最上面
        return current.data
      }

过程详解:

removeAt方法的实现过程:删除节点时存在多种情况:

  • 情况1:position = 0,即移除第一个节点(Node1)。

通过:this.head = this.head.next,改变指向1即可;

虽然Node1的next仍指向Node2,但是没有引用指向Node1,则Node1会被垃圾回收器自动回收,所以不用处理Node1指向Node2的引用next。

  • 情况2:positon > 0,比如pos = 2即移除第三个节点(Node3)。

注意:position = length时position后一个节点为null不能删除,因此position != length;

首先,定义两个变量previous和curent分别指向需要删除位置pos = x的前一个节点和后一个节点;

然后,通过:previous.next = current.next,改变指向1即可;

随后,没有引用指向Node3,Node3就会被自动回收,至此成功删除Node3 。

 测试代码:

    //测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
  
  //3.测试removeAt方法
    console.log(list.removeAt(0));
    console.log(list.removeAt(0));
    console.log(list);

8.其他方法

其他方法包括:remove(element)、isEmpty()、size()

代码实现:

/*-------------其他方法的实现--------------*/
      //一.实现remove方法
      LinkList.prototype.remove = (data) => {
        //1.获取data在列表中的位置
        let position = this.indexOf(data)
        //2.根据位置信息,删除结点
        return this.removeAt(position)
      }

      //二.实现isEmpty方法
      LinkList.prototype.isEmpty = () => {
        return this.length == 0
      }

      //三.实现size方法
      LinkList.prototype.size = () => {
        return this.length
      }

测试代码:

    //测试代码
    //1.创建LinkList
    let list = new LinkList()
    
    //2.插入数据
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')

/*---------------其他方法测试----------------*/
  	//remove方法
  	console.log(list.remove('aaa'));
  	console.log(list);
  	//isEmpty方法
  	console.log(list.isEmpty());
  	//size方法
  	console.log(list.size());

9.完整实现

    // 封装链表类
    function LinkList(){
      // 封装一个内部类:节点类
      function Node(data){
        this.data = data;
        this.next = null;
      }

      // 属性
      // 属性head指向链表的第一个节点
      this.head = null;
      this.length = 0;

      // 一.实现append方法
      LinkList.prototype.append = data => {
        //1.创建新节点
        let newNode = new Node(data)

        //2.添加新节点
        //情况1:只有一个节点时候
        if(this.length == 0){
          this.head = newNode
        //情况2:节点数大于1,在链表的最后添加新节点  
        }else {              
          //让变量current指向第一个节点
          let current = this.head
          //当current.next(下一个节点不为空)不为空时,一直循环,直到current指向最后一个节点
          while (current.next){
            current = current.next
          }
          // 最后节点的next指向新的节点
          current.next = newNode
        }
        //3.添加完新结点之后length+1
        this.length += 1
      }

      // 二.实现toString方法
      LinkList.prototype.toString = () => {
        // 1.定义变量
        let current = this.head
        let listString = ""

        // 2.循环获取一个个的节点
        while(current){ 
          listString += current.data + " "
          current = current.next//千万不要忘了拼接完一个节点数据之后,让current指向下一个节点
        }
        return  listString
      }

      // 三.实现insert方法
      LinkList.prototype.insert = (position, data) => {
      //理解positon的含义:position=0表示新界点插入后要成为第1个节点,position=2表示新界点插入后要成为第3个节点
        //1.对position进行越界判断:要求传入的position不能是负数且不能超过LinkList的length
        if(position < 0 || position > this.length){
          return false
        }
        //2.根据data创建newNode
        let newNode = new Node(data)

        //3.插入新节点
        //情况1:插入位置position=0
        if(position == 0){
          // 让新节点指向第一个节点
          newNode.next = this.head
          // 让head指向新节点
          this.head = newNode
        //情况2:插入位置position>0(该情况包含position=length)
        } else{
          let index = 0
          let previous = null
          let current = this.head
          //步骤1:通过while循环使变量current指向position位置的后一个节点(注意while循环的写法)
          while(index++ < position){
          //步骤2:在current指向下一个节点之前,让previous指向current当前指向的节点
            previous = current
            current = current.next
          }
          // 步骤3:通过变量current(此时current已经指向position位置的后一个节点),使newNode指向position位置的后一个节点
          newNode.next = current
          //步骤4:通过变量previous,使position位置的前一个节点指向newNode
          previous.next = newNode
          
		//我们无法直接操作链表中的节点,但是可以通过变量指向这些节点,以此间接地操作节点;
        }
        //4.新节点插入后要length+1
        this.length += 1;

        return true
      }

      //四.实现get方法
      LinkList.prototype.get = (position) => {
        //1.越界判断
        // 当position = length时,取到的是null所以0 =< position < length
        if(position < 0 || position >= this.length){
          return null
        }
        //2.获取指定的positon位置的后一个节点的data
        //同样使用一个变量间接操作节点
        let current = this.head
        let index = 0
        while(index++ < position){
          current = current.next
        }
        return current.data
      }

      //五.实现indexOf方法
      LinkList.prototype.indexOf = data => {
        //1.定义变量
        let current = this.head
        let index = 0

        //2.开始查找:只要current不指向null就一直循环
        while(current){
          if(current.data == data){
            return index
          }
          current = current.next
          index += 1
        } 

        //3.遍历完链表没有找到,返回-1
        return -1
      }

      //六.实现update方法
      LinkList.prototype.update = (position, newData) => {
        //1.越界判断
        //因为被修改的节点不能为null,所以position不能等于length
        if(position < 0 || position >= this.length){
          return false
        }
        //2.查找正确的节点
        let current = this.head
        let index = 0
        while(index++ < position){
          current = current.next
        }
        //3.将position位置的后一个节点的data修改成newData
        current.data = newData
        //返回true表示修改成功
        return true
      }

      //七.实现removeAt方法
      LinkList.prototype.removeAt = position => {
        //1.越界判断
        if (position < 0 || position >= this.length) {
          return null
        }
        //2.删除元素
        //情况1:position = 0时(删除第一个节点)
        let current = this.head
        if (position ==0 ) {
        //情况2:position > 0时
          this.head = this.head.next
        }else{
          let index = 0
          let previous = null
          while (index++ < position) {
            previous = current
            current = current.next
          }
          //循环结束后,current指向position后一个节点,previous指向current前一个节点
          //再使前一个节点的next指向current的next即可
          previous.next = current.next
        }
        //3,length-1
        this.length -= 1

        //返回被删除节点的data,为此current定义在最上面
        return current.data
      }

/*-------------其他方法的实现--------------*/
      //八.实现remove方法
      LinkList.prototype.remove = (data) => {
        //1.获取data在列表中的位置
        let position = this.indexOf(data)
        //2.根据位置信息,删除结点
        return this.removeAt(position)
      }

      //九.实现isEmpty方法
      LinkList.prototype.isEmpty = () => {
        return this.length == 0
      }

      //十.实现size方法
      LinkList.prototype.size = () => {
        return this.length
      }
    }

三、双向链表

1.双向链表简介

双向链表:既可以从头遍历到尾,又可以从尾遍历到头。也就是说链表连接的过程是双向的,它的实现原理是:一个节点既有向前连接的引用,也有一个向后连接的引用

双向链表的缺点:

  • 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些;
  • 相对于单向链表,所占内存空间更大一些;
  • 但是,相对于双向链表的便利性而言,这些缺点微不足道。

双向链表的结构:

  • 双向链表不仅有head指针指向第一个节点,而且有tail指针指向最后一个节点;
  • 每一个节点由三部分组成:item储存数据、prev指向前一个节点、next指向后一个节点;
  • 双向链表的第一个节点的prev指向null
  • 双向链表的最后一个节点的next指向null

2.双向链表常见的操作(方法):

  • append(element):向链表尾部添加一个新的项;
  • inset(position,element):向链表的特定位置插入一个新的项;
  • get(element):获取对应位置的元素;
  • indexOf(element):返回元素在链表中的索引,如果链表中没有元素就返回-1;
  • update(position,element):修改某个位置的元素;
  • removeAt(position):从链表的特定位置移除一项;
  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
  • size():返回链表包含的元素个数,与数组的length属性类似;
  • toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;
  • forwardString():返回正向遍历节点字符串形式;
  • backwordString():返回反向遍历的节点的字符串形式;

四、双向链表的封装

0.创建双向链表类

先创建双向链表类DoubleLinklist,并添加基本属性,再实现双向链表的常用方法:

   //封装双向链表类
   function DoubleLinklist(){
    //封装内部类:节点类
    function Node(data){
      this.data = data
      this.prev = null
      this.next = null
    }

    //属性
    this.head = null
    this.tail ==null
    this.length = 0
    }

1.append(element)

代码实现:

      //append方法
      DoubleLinklist.prototype.append = data => {
        //1.根据data创建新节点
        let newNode = new Node(data)

        //2.添加节点
        //情况1:添加的是第一个节点
        if (this.length == 0) {
          this.tail = newNode
          this.head = newNode 
        //情况2:添加的不是第一个节点
        }else {
          newNode.prev = this.tail
          this.tail.next = newNode
          this.tail = newNode
        }

        //3.length+1
        this.length += 1
      }

过程详解:

添加节点时分为多种情况:

  • 情况1:添加的是第一个节点:只需要让head和tail都指向新节点即可;

  • 情况2:添加的不是第一个节点,如下图所示:只需要改变相关引用的指向即可。

    • 通过:newNode.prev = this.tail:建立指向1;
    • 通过:this.tail.next = newNode:建立指向2;
    • 通过:this.tail = newNode:建立指向3

    要注意改变变量指向的顺序,最后修改tail指向,这样未修改前tail始终指向原链表的最后一个节点。

测试代码: 

 

   //测试代码
   //1.创建双向链表
   let list = new DoubleLinklist()

    //2.测试append方法
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
    console.log(list);

2.toString()汇总

代码实现:

      //将链表转变为字符串形式
      //一.toString方法
      DoubleLinklist.prototype.toString = () => {
        return this.backwardString()
      }

      //二.forwardString方法
      DoubleLinklist.prototype.forwardString = () => {
        //1.定义变量
        let current =this.tail
        let resultString = ""

        //2.依次向前遍历,获取每一个节点
        while (current) {
          resultString += current.data + "--"
          current = current.prev 
        }
        return resultString
      }

      //三.backwardString方法
      DoubleLinklist.prototype.backwardString = () => {
        //1.定义变量
        let current = this.head
        let resultString = ""

        //2.依次向后遍历,获取每一个节点
        while (current) {
          resultString += current.data + "--"
          current = current.next
        }
        return resultString
      }

过程详解:

三种获取字符串的方法:toString()forwardString()backwardString()实现原理相似,仅以backWardString方法为例:

  • 定义current变量记录当前指向的节点。首先让current指向第一个节点,然后通过 current = current.next 依次向后遍历。在while循环中以(current)作为条件遍历链表,只要current != null就一直遍历,由此可获取链表所有节点的数据。

 测试代码:

    //测试代码
    //1.创建双向链表
    let list = new DoubleLinklist()
    
    //2.测试字符串方法   
    list.append('aaa')
    list.append('bbb')
    list.append('ccc')
    console.log(list.toString());
    console.log(list.forwardString());
    console.log(list.backwardString());

3.insert(position,element)

代码实现:

      //insert方法
      DoubleLinklist.prototype.insert = (position, data) => {
        //1.越界判断
        if (position < 0 || position > this.length) return false

        //2.根据data创建新的节点
        let newNode = new Node(data)

        //3.插入新节点
        //原链表为空
          //情况1:插入的newNode是第一个节点
        if (this.length == 0) {
          this.head = newNode
          this.tail = newNode
        //原链表不为空
        }else {
          //情况2:position == 0
          if (position == 0) {
            this.head.prev = newNode
            newNode.next = this.head
            this.head = newNode
          //情况3:position == this.length 
          } else if(position == this.length){
            this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
            //情况4:0 < position < this.length
          }else{
            let current = this.head
            let index = 0
            while(index++ < position){
              current = current.next
            }
            //修改pos位置前后节点变量的指向
            newNode.next = current
            newNode.prev = current.prev
            current.prev.next = newNode
            current.prev = newNode
          }
        }
        //4.length+1
        this.length += 1
        return true//返回true表示插入成功
      }

过程详解:

插入节点可分为多种情况:

当原链表为空时

  • 情况1:插入的新节点是链表的第一个节点;只需要让head和tail都指向newNode即可。

 

当原链表不为空时

  • 情况2:当position == 0,即在链表的首部添加节点:如下图所示:

 

首先,通过:this.head.prev = newNode,改变指向1;

然后,通过:newNode.next = this.head,改变指向2;

最后,通过:this.head = newNode,改变指向3;

 

  • 情况3:position == this.length,即在链表的尾部添加节点,如下图所示:

 

首先,通过:this.tail.next = newNode,改变指向1;(注意这里使用this.tail指向原链表最后一个节点,而不是this.head。因为当length>1时,this.head != this.tail。)

然后,通过:newNode.prev = this.tail,改变指向2;

最后,通过:this.tail = newNode,改变指向3;

 

  • 情况4:0 < position < this.length,即在链表的中间插入新节点,假设在position = 1的位置插入,如下图所示:

 首先,需要定义变量current按照之前的思路,通过while循环找到position位置的后一个节点,循环结束后index = position

 如下图所示:当position = 1时,current就指向了Node2。这样操作current就等同于间接地操作Node2,还可以通过current.prev间接获取Node1。得到了newNode的前一个节点和后一个节点就可以通过改变它们的prev和next变量的指向来插入newNode了。

 

通过:newNode.next = current,改变指向1;

通过:newNode.prev = current.prev,改变指向2;

通过:current.prev.next = newNode,改变指向3;

注意必须最后才修改current.prev的指向,不然就无法通过current.prev获取需要操作的Node1了。

通过:current.prev = current,改变指向4;

 

测试代码:

    //测试代码
    //1.创建双向链表
    let list = new DoubleLinklist()

	//2.测试insert方法
    list.insert(0, '插入链表的第一个元素')
    list.insert(0, '在链表首部插入元素')
    list.insert(1, '在链表中间插入元素')
    list.insert(3, '在链表尾部插入元素')
    console.log(list);
    alert(list)

7.update(position,element)

代码实现:

     //update方法
      DoubleLinklist.prototype.update = (position, newData) => {
        //1.越界判断
        if (position < 0 || position >= this.length) {
          return false
        }

        //2.寻找正确的节点
        let current = this.head
        let index = 0
        //this.length / 2 > position:从头开始遍历
        if (this.length / 2 > position) {
          while(index++ < position){
          current = current.next
        }
        //this.length / 2 =< position:从尾开始遍历
        }else{
          current = this.tail
          index = this.length - 1
          while (index -- > position) {
            current = current.prev
          }
        }

        //3.修改找到节点的data
        current.data = newData
        return true//表示成功修改
      }

过程详解:

以(index++ < position)为条件,通过while循环遍历链表中的节点(停止条件为index = position)。循环结束后,current指向需要修改的节点。

测试代码:

 

    //测试代码
    //1.创建双向链表
    let list = new DoubleLinklist()
    
    //2.测试update方法
    list.append('a')
    list.append('b')
    console.log(list.update(1, 'c'));
    console.log(list);

8.removeAt(position)

代码实现:

     //removeAt方法
      DoubleLinklist.prototype.removeAt = position => {
        //1.越界判断
        if (position < 0 || position >= this.length) {
          return null
        }
        
        //2.删除节点
        //当链表中length == 1
        //情况1:链表只有一个节点
        let current = this.head//定义在最上面方便以下各种情况返回current.data
        if (this.length == 1) {
          this.head = null
          this.tail = null
        //当链表中length > 1
        } else{
          //情况2:删除第一个节点
          if (position == 0) {
            this.head.next.prev = null
            this.head = this.head.next
          //情况3:删除最后一个节点
          }else if(position == this.length - 1){
            current = this.tail//该情况下返回被删除的最后一个节点
            this.tail.prev.next = null
            this.tail = this.tail.prev
          }else{
          //情况4:删除链表中间的节点
            let index = 0
            while(index++ < position){
              current = current.next
            }
            current.next.prev = current.prev
            current.prev.next = current.next
          }
        }

        //3.length -= 1
        this.length -= 1
        return current.data//返回被删除节点的数据
      }

过程详解:

删除节点时有多种情况:

当链表的length = 1时

  • 情况1:删除链表中的所有节点:只需要让链表的head和tail指向null即可。

当链表的length > 1时

  • 情况2:删除链表中的第一个节点:

    通过:this.head.next.prev = null,改变指向1;

    通过:this.head = this.head.next,改变指向2;

    虽然Node1有引用指向其它节点,但是没有引用指向Node1,那么Node1会被自动回收。

 

 

  • 情况3:删除链表中的最后一个节点:

    通过:this.tail.prev.next = null,修改指向1;

    通过:this.tail = this.tail.prev,修改指向2;

  • 情况4:删除链表中间的节点:

通过while循环找到需要删除的节点,比如position = x,那么需要删除的节点就是Node(x+1),如下图所示:

 

 

通过:current.next.prev = current.prev,修改指向1;

通过:current.prev.next = current.next,修改指向2;

这样就没有引用指向Node(x+1)了(current虽指向Node(x+1),但current时临时变量,该方法执行完就会被销毁),随后Node(x+1)就会被自动删除。

 测试代码:

    //测试代码
    //1.创建双向链表
    let list = new DoubleLinklist()	
	
	//2.测试removeAt方法
    list.append('a')
    list.append('b')
    list.append('c')
    console.log(list.removeAt(1));
    console.log(list);

9.其他方法

其他方法包括:remove(element)、isEmpty()、size()、getHead()、getTail()

代码实现:

  /*--------------------其他方法-------------------*/
  //八.remove方法
  DoubleLinklist.prototype.remove = data => {
    //1.根据data获取下标值
    let index = this.indexOf(data)
    
    //2.根据index删除对应位置的节点
    return this.removeAt(index)
  }

  //九.isEmpty方法
  DoubleLinklist.prototype.isEmpty = () => {
    return this.length == 0
  }

  //十.size方法
  DoubleLinklist.prototype.size = () => {
    return this.length
  }

  //十一.getHead方法:获取链表的第一个元素
  DoubleLinklist.prototype.getHead = () => {
    return this.head.data
  }

  //十二.getTail方法:获取链表的最后一个元素
  DoubleLinklist.prototype.getTail = () => {
    return this.tail.data
  }

测试代码:

    //测试代码
    //1.创建双向链表
    let list = new DoubleLinklist()	

/*------------其他方法的测试--------------*/
    list.append('a')
    list.append('b')
    list.append('c')
    list.append('d')
    //remove方法
    console.log(list.remove('a'));
    console.log(list);
    //isEmpty方法
    console.log(list.isEmpty());
    //size方法
    console.log(list.size());
    //getHead方法
    console.log(list.getHead());
    //getTead方法
    console.log(list.getTail());

10.完整实现

//封装双向链表
function DoubleLinklist(){
  //封装内部类:节点类
  function Node(data){
    this.data = data
    this.prev = null
    this.next = null
  }

  //属性
  this.head = null
  this.tail ==null
  this.length = 0

  //常见的操作:方法
  //一.append方法
  DoubleLinklist.prototype.append = data => {
    //1.根据data创建新节点
    let newNode = new Node(data)

    //2.添加节点
    //情况1:添加的是第一个节点
    if (this.length == 0) {
      this.tail = newNode
      this.head = newNode 
    //情况2:添加的不是第一个节点
    }else {
      newNode.prev = this.tail
      this.tail.next = newNode
      this.tail = newNode
    }

    //3.length+1
    this.length += 1
  }

  //二.将链表转变为字符串形式
  //2.1.toString方法
  DoubleLinklist.prototype.toString = () => {
    return this.backwardString()
  }

  //2.2.forwardString方法
  DoubleLinklist.prototype.forwardString = () => {
    //1.定义变量
    let current =this.tail
    let resultString = ""

    //2.依次向前遍历,获取每一个节点
    while (current) {
      resultString += current.data + "--"
      current = current.prev 
    }
    return resultString
  }

  //2.3.backwardString方法
  DoubleLinklist.prototype.backwardString = () => {
    //1.定义变量
    let current = this.head
    let resultString = ""

    //2.依次向后遍历,获取每一个节点
    while (current) {
      resultString += current.data + "--"
      current = current.next
    }
    return resultString
  }

  //三.insert方法
  DoubleLinklist.prototype.insert = (position, data) => {
    //1.越界判断
    if (position < 0 || position > this.length) return false

    //2.根据data创建新的节点
    let newNode = new Node(data)

    //3.插入新节点
    //原链表为空
      //情况1:插入的newNode是第一个节点
    if (this.length == 0) {
      this.head = newNode
      this.tail = newNode
    //原链表不为空
    }else {
      //情况2:position == 0
      if (position == 0) {
        this.head.prev = newNode
        newNode.next = this.head
        this.head = newNode
      //情况3:position == this.length 
      } else if(position == this.length){
        this.tail.next = newNode
        newNode.prev = this.tail
        this.tail = newNode
        //情况4:0 < position < this.length
      }else{
        let current = this.head
        let index = 0
        while(index++ < position){
          current = current.next
        }
        //修改pos位置前后节点变量的指向
        newNode.next = current
        newNode.prev = current.prev
        current.prev.next = newNode
        current.prev = newNode
      }
    }
    //4.length+1
    this.length += 1
    return true//返回true表示插入成功
  }

  //四.get方法
  DoubleLinklist.prototype.get = position => {
    //1.越界判断
    if (position < 0 || position >= this.length) {//获取元素时position不能等于length
      return null
    }

    //2.获取元素
    let current = null
    let index = 0
    //this.length / 2 > position:从头开始遍历
    if ((this.length / 2) > position) {
      current = this.head
      while(index++ < position){
      current = current.next
    }
    //this.length / 2 =< position:从尾开始遍历
    }else{
      current = this.tail
      index = this.length - 1
      while(index-- > position){
      current = current.prev
    }
    }
    return current.data
  }

  //五.indexOf方法
  DoubleLinklist.prototype.indexOf = data => {
    //1.定义变量
    let current = this.head
    let index = 0

    //2.遍历链表,查找与data相同的节点
    while(current){
      if (current.data == data) {
        return index
      }
      current = current.next
      index += 1
    }
    return -1
  } 

  //六.update方法
  DoubleLinklist.prototype.update = (position, newData) => {
    //1.越界判断
    if (position < 0 || position >= this.length) {
      return false
    }

    //2.寻找正确的节点
    let current = this.head
    let index = 0
    //this.length / 2 > position:从头开始遍历
    if (this.length / 2 > position) {
      while(index++ < position){
      current = current.next
    }
    //this.length / 2 =< position:从尾开始遍历
    }else{
      current = this.tail
      index = this.length - 1
      while (index -- > position) {
        current = current.prev
      }
    }

    //3.修改找到节点的data
    current.data = newData
    return true//表示成功修改
  }

  //七.removeAt方法
  DoubleLinklist.prototype.removeAt = position => {
    //1.越界判断
    if (position < 0 || position >= this.length) {
      return null
    }
    
    //2.删除节点
    //当链表中length == 1
    //情况1:链表只有一个节点
    let current = this.head//定义在最上面方便以下各种情况返回current.data
    if (this.length == 1) {
      this.head = null
      this.tail = null
    //当链表中length > 1
    } else{
      //情况2:删除第一个节点
      if (position == 0) {
        this.head.next.prev = null
        this.head = this.head.next
      //情况3:删除最后一个节点
      }else if(position == this.length - 1){
        current = this.tail//该情况下返回被删除的最后一个节点
        this.tail.prev.next = null
        this.tail = this.tail.prev
      }else{
      //情况4:删除链表中间的节点
        let index = 0
        while(index++ < position){
          current = current.next
        }
        current.next.prev = current.prev
        current.prev.next = current.next
      }
    }

    //3.length -= 1
    this.length -= 1
    return current.data//返回被删除节点的数据
  }
  /*--------------------其他方法-------------------*/
  //八.remove方法
  DoubleLinklist.prototype.remove = data => {
    //1.根据data获取下标值
    let index = this.indexOf(data)
    
    //2.根据index删除对应位置的节点
    return this.removeAt(index)
  }

  //九.isEmpty方法
  DoubleLinklist.prototype.isEmpty = () => {
    return this.length == 0
  }

  //十.size方法
  DoubleLinklist.prototype.size = () => {
    return this.length
  }

  //十一.getHead方法:获取链表的第一个元素
  DoubleLinklist.prototype.getHead = () => {
    return this.head.data
  }

  //十二.getTail方法:获取链表的最后一个元素
  DoubleLinklist.prototype.getTail = () => {
    return this.tail.data
  }

}

五、链表总结

单向链表有head和next两个属性,双向链表有head、tail、next、prev四个属性。处理好它们的指向,相当于将它们正确地连接在一起,这样就组成了一条链,这就是简单链表的实现。

在实际开发中链表使用得非常多,比如Java中的LinkList就是双向链表。

1.注意点

  • 在链表中current = current.next 可以从左往右看,看成是current --> current.next,即current指向current的下一个节点。
  • 删除节点的原理:只要没有引用指向该对象,无论该对象是否有引用指向其他对象,该对象都会被回收(删除)。
  • 参数中凡是有position的都要进行越界判断。

2.链表的增删改查

以双向链表为例:链表的增删改查无非就是获取链表中相应的节点改变其中的prev和next两个变量的指向

  • 情况一:只需要headtail两个变量就可以获取需要操作的变量(这里指的是能够轻松获取,当然你想通过head.next.next...或tail.prev.prev...来获取想要的节点也可以),在这种情况下链表的长度length:0 <= length <=2

  • 情况二:不能靠tail和head来获取到需要操作的变量时,可采用while循环遍历的方式,找到需要操作的节点:

 在这种情况下,如果我们想要在链表的position = x的位置插入新节点,那么可以通过current获取position的后一个节点Node(x+1),通过current.prev获取position位置的前一个节点Node(x);之后修改Node(x+1)和Node(x)中的prev和next两个变量的指向即可在pos=x 的位置插入新节点。

 3.修改链表引用指向

应先修改newNode引用的指向,再修改其他引用

  • 情况1:通过head和tail引用就能获取需要操作的节点时,最后更改head或tail变量的指向(因为它们分别指向链表的第一个和最后一个节点,获取其他节点时可能需要用到它们)。
  • 情况2:使用current获取到需要操作的节点时,最后更改curren.next或current.prev的指向。因为current.next和current.prev表示的是Node(x+2)和Node(x)这两个节点,如下图所示,一旦变更它们的指向就无法获取Node(x)或Node(x+2)了,

4.遍历链表

积累两种遍历思路

  • 获取指定的position = x 位置的后一个节点和索引值:

 

循环结束后index = position = x,变量current就指向了Node(x+1),变量index的值为Node(x+1)的索引值x。

  • 遍历链表中的所有节点:

 

 当current.next = null时停止循环,此时current指向链表的最后一个节点。

​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值