前端必备数据结构——链表

Posted MaNqo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端必备数据结构——链表相关的知识,希望对你有一定的参考价值。

一、单向链表

链表的优势

链表相对于数组的优点在于:

  • 内存空间不是必须连续的,可以充分利用计算机的内存,实现灵活的内存动态管理。
  • 链表不需要再创建的时候就确定大小,并且它的大小可以无限的延伸下去。
  • 链表在插入和删除数据时,时间复杂度(即执行算法所需要的计算工作量)可以达到O(1),相对数组效率高许多。

链表相对于数组的缺点在于:

  • 链表访问任何一个位置的元素时,都需要从头开始访问
  • 无法像数组一样通过下标值访问元素,需要从头开始访问,直到找到对应的元素

封装链表

链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成。也就是说每一个节点自己有一个data,并且有一个指向下一个节点的指针next,next的指向默认为null。

function LinkList() 
    function Node(data) 
        this.data = data;
        this.next = null;
    
    this.head = null;
    this.length = 0;    // 记录链表节点的个数

链表的常见操作

  • append(element):向列表尾部添加一个新的元素
  • insert(position, element):向列表的某个位置插入一个新的元素
  • get(position):获取对应位置的元素
  • indexOf(element):返回元素在列表中的索引。如果没有该元素则返回-1
  • update(position, data):修改某个位置的元素的data值
  • removeAt(position):从列表的某个位置移除一个元素
  • remove(data):从列表中移除一个元素
  • isEmpty():如果链表中没有元素,返回true。否则返回false
  • size():返回链表中包含的元素个数,和数组的length属性类似
  • toString():链表中元素是Node类,需要重写toString方法,方便输出打印元素的值。

1. append(data)方法

如果添加的是第一个节点的话,需要多做一个步骤——把head指针指向第一个节点。如果添加的不是第一个节点的话,就要把最后一个节点的指针指向新创建的节点。

查找方法:从this.head开始查找,如果当前指针指向的下一个current.next不为空,将下一个节点的指针current.next赋值给当前指针current;直到current.next为空,那么这个指针所在的节点就是链表中的最后一个节点,将这个指向空的指针指向新创建的节点即可。

LinkList.prototype.append = function (data) 
    var newNode = new Node(data);
    if (this.length == 0) 
        this.head = newNode;
     else 
        var current = this.head;
        while (current.next) 
            current = current.next;
        
        current.next = newNode;
    
    this.length += 1;

2. insert(position, data)方法

在某个位置中插入一个元素。我在这里的设置,传入的position不能小于0,因此小于0时return false,而且插入的位置也不能够大于此时链表的长度,在这种情况下也return false。接下来,插入元素存在两种情况:

第一种:将元素插到第一个节点,即position的值为0,此时要修改创建节点的指针指向,应该指向原来this.head指向的元素,再让this.head指向这个新创建的元素,即可完成插入。

第二种,将元素插到中间或者末尾,即0 < position < this.length,先找到要插入的位置的上一个节点。然后跟第一种方法的思路是差不多的,将创建的节点指针指向当前节点指向的元素newNode.next = current.next,然后让当前节点指针指向新创建的元素current.next = newNode

LinkList.prototype.insert = function (position, data) 
    var newNode = new Node(data);
    if (position < 0 || position > this.length) return false;
    if (position === 0) 
        newNode.next = this.head;
        this.head = newNode;
     else 
        var current = this.head;
        for (var i = 0; i < position - 1; i++) 
            current = current.next;
        
        newNode.next = current.next;
        current.next = newNode;
    
    this.length += 1;

3. get(position)方法

获取某个位置的元素,查找到这个位置,让current指向当前位置,返回当前位置的data值即可。

LinkList.prototype.get = function (position) 
    if (position < 0 || position >= this.length) return null;
    var current = this.head;
    for (var i = 0; i < position; i++) 
        current = current.next;
    
    return current.data;

4. update(position, data)方法

修改某个位置的元素值。这个跟get方法其实是差不多的,只不过get只是拿到这个位置的data值,但是update是要我们将这个data值改成传进来的data。这个就不用代码呈现了。

5. indexOf(element)方法

在链表中查找某一个元素的值(data),当找到这个元素的时候就这个元素的索引号(即位于哪个位置)。如果没有找到这个数据的话就会返回-1。遍历链表,将每一个元素的data和要查找的element进行比较,如果相同就返回它的索引值。

LinkList.prototype.indexOf = function (element) 
    var current = this.head;
    var index = 0;
    while (current) 
        if (current.data == element) 
            return index;
        
        current = current.next;
        index += 1;
    
    return -1;

6. removeAt(position)方法

从某个位置移除一个元素。有两种情况:

1)删除position = 0的元素;虽然此时删除的元素还是指向第二个元素,但是此时虽然它有指向别人,但是没有人指向它,浏览器会自动把这些没用的对象回收。

2)删除position > 0的元素;查找元素,直到要删除元素位置的上一个位置,即currentcurrent.next原本指向的就是这个要删除的元素,对它的指向重新赋值为current.next.next,即指向删除元素的下一个元素位置。最后再返回删除元素的data值。

LinkList.prototype.removeAt = function (position) 
    var current = this.head;
    if (position < 0 || position >= this.length) return false;
    if (position == 0) 
        this.head = this.head.next
     else 
        for (var i = 0; i < position - 1; i++) 
            current = current.next;
        
        current.next = current.next.next;
    
    this.length -= 1;
    return current.data;

7. remove(data)方法

移除data等于传入的data的元素。1)用indexOf获取data在链表中的位置;2)根据位置信息删除节点,返回删除的数据。

LinkList.prototype.remove = function (data) 
    var position = this.indexOf(data);
    return this.removeAt(position);

8. toString()方法

这个方法只是方便我们查看链表中的数据。实现方法:遍历链表,将链表中的每一个节点的值都取出来放到一个字符串中,再作为返回值返回,

LinkList.prototype.toString = function () 
    var current = this.head;
    var listString = '';
    while (current) 
        listString += current.data + ' ';
        current = current.next;
    
    return listString;

二、双向链表

之前学了单向链表,但是单向链表有一个致命的缺点:无法返回到前一个结点。接下来就来学学可以返回到前一个结点的双向链表吧~

介绍双向链表

双向链表的缺点:

  • 每次在插入或删除某个节点时,需要处理4个引用,而不是2个,实现起来相对困难
  • 相对于单向链表,占用内存空间更大

双向链表的优点:

  • 既可以从头遍历到尾,又可以从尾遍历到头
  • 一个结点既有指向下一个节点的指针,也有一个指向上一个节点的指针
  • 使用起来方便许多,常用双向链表

双向链表的特点:

  • 使用一个head和一个tail分别指向链表头部和尾部的节点
  • 每个节点都由三部分组成:前一个节点的指针pre、保存的数据data、后一个节点的指针next
  • 双向链表的第一个节点的pre是null
  • 双向链表最后一个节点的next是null

封装双向链表

在双向链表中,初始状态头指针head和尾指针tail都指向空,再多给双向链表添加一个length属性,以便记录该链表的长度。对于双向链表中的每一个节点,都是由2个指针(一个指向下一个节点,还有一个指向上一个节点)和一个元素组成的,初始状态指针均指向空,即下面封装的Node类。

function doubleLinkedList() 
    this.head = null;
    this.tail = null;
    this.length = 0;
    function Node(data) 
        this.pre = null;
        this.data = data;
        this.next = null;
    

双向链表常见操作

和单向链表的常见操作是差不多的,只是在封装的时候内部的实现机制会相对复杂一点。而且双向链表比单向链表多了一个可以反向遍历。

  • forwardString():返回正向遍历的节点字符串形式
  • backwordString():返回反向遍历的节点字符串形式

1. append(element)方法

插入操作。第一个节点创建之后要插入,直接让head指针和tail指针指向这个节点即可。后面如果继续有节点要插入的话,这里的操作不同于单向链表(tail即指向链表的最后一个元素,无需像单向链表一样去遍历到最后一个元素),然后让新元素的pre指针指向链表的最后一个元素(即this.tail指向的元素),再让最后一个元素指向这个要插入的元素,再将尾指针指向这个新元素。

三个步骤:1)新创建的元素指向最后一个元素;2)让最后一个元素指回新创建的元素(形成双向);3)将链表的尾指针指向新创建的元素(新的最后一个元素)

doubleLinkedList.prototype.append = function (data) 
    var newNode = new Node(data);
    if (this.length === 0) 
        this.head = newNode;
        this.tail = newNode;
     else 
        newNode.pre = this.tail;
        this.tail.next = newNode;
        this.tail = newNode;
    
    this.length += 1;

2. insert(position, data)方法

相对于单向链表,双向链表insert方法的处理方式要麻烦一丢丢。

一共有三种情况:

  1. 插入位置position = 0

    让第一个元素this.headpre指针指向新元素,新元素的next指针指向第一个元素,建立双向关系,再让头指针指向这个新元素。

    this.head.pre = newNode;
    newNode.next = this.head;
    this.head = newNode;
    
  2. 插入位置position = this.length

    让新元素的pre指针指向最后一个元素,最后一个元素的next指针指向新元素,建立双向关系,最后让尾指针指向这个新元素。

    newNode.pre = this.tail;
    this.tail.next = newNode;
    this.tail = newNode;
    
  3. 插入位置位于链表的中间,也就是插入后,该元素的前后都有元素

    首先看一下这张图(记录第一次画图)

    从图中我们可以看出,当我们要在中间插入一个新的元素的时候,需要对4个指针的指向进行修改。一般我的思路是先让新元素的2个指针指向前后两个元素,再让前面的元素的next指针指向新元素current.pre.next = newNode、后面的元素的pre指针指向新元素current.pre = current

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jpk7hm8g-1628425935164)(E:\\算法\\note\\imgnote\\双向链表.png)]

    newNode.pre = current.pre;
    newNode.next = current;
    current.pre.next = newNode; 
    current.pre = newNode; 
    

其他封装代码:

doubleLinkedList.prototype.insert = function (position, data) 
    if (position < 0 || position > this.length) return false;
    var newNode = new Node(data);
    var current = this.head;
    for (var i = 0; i < position; i++) 
        current = current.next;
    
    // ...上述不同情况下的处理

3. removeAt(position)方法

在实现这个方法之前,要判断这个链表的长度是否为空,是否为一(为1的时候直接让头指针head和尾指针tail指向null即可)接着和inset方法相同,有三种情况:

  1. 删除元素的position = 0,原来头指针head指向的元素即要删除元素,要先让下一个元素的pre指针指向空,然后再让head指向下一个元素,建立新的双向关系

    this.head.next.pre = null;
    this.head = this.head.next;
    
  2. 删除元素position = length,尾指针tail指向的元素即要删除元素,要先让删除元素的上一个元素this.tail.prenext指针指向null,再让tail指向这个元素,建立新的双向关系

    this.tail.pre.next = null;
    this.tail = this.tail.pre;
    
  3. 删除的元素位于中间,要先遍历到这个要删除元素的位置,让要删除元素两边的元素建立关系即可,这里没有插入那么麻烦,处理2个指针的指向就好了。

    var current = this.head;for (var i = 0; i < position; i++)     current = current.next;current.pre.next = current.next;current.next.pre = current.pre;
    

4. 链表转成字符串的方法

正向遍历和反向遍历链表,将每一个元素的data值转成字符串后存储在一个字符串中,便于查看。与单向链表不同的是,双向链表能够反向遍历。这里只演示反向遍历。

doubleLinkedList.prototype.forwardString = function ()     var current = this.tail;    var resultString = '';    while (current)         resultString += current.data + ' ';        current = current.pre;        return resultString;

总结

实现了insetremoveAt方法之后,其它方法的处理方式相对都是比较容易实现的,其中要注意的是,双向链表可以反向遍历,在查找的position > length/2的时候,我们可以选择反向遍历(提高效率)。

以上是关于前端必备数据结构——链表的主要内容,如果未能解决你的问题,请参考以下文章

前端必备数据结构——链表

前端必备数据结构——链表

一文带你拿下前端必备数据结构 -- 链表 !!

一文带你拿下前端必备数据结构 -- 链表 !!

Java高级工程师必备知识!mysql格式化日期函数

面试必备的「反转链表」