• 链表


    链表的基本概念

    链表是一种非常有趣的动态的数据结构,这意味这我们可以从中任意的添加或移除项,它会按需进行扩容。

    在JS中要储存多个元素,数组或者列表可能是最常用的数据结构。但是这些数据结构也是有缺点的,从数组的起点或者中间插入或者删除元素的成本很高,因为需要移动元素(尽管JS内部已经实现了对数组项移动的相关操作,但其背后的情况仍然是这样的)。

    链表储存有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续的。每个元素有一个储存本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。如下图:

    1.png-33.8kB

    相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其它元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的元素,而想要访问链表中间的一个元素,需要从起点开始迭代列表直到找到所需要的元素。

    拿到现实中最能体现链表的例子,那就是火车。一列火车是由一系列车厢组成的。每节车厢都是互相链接的。可以很容易分离一节车厢,改变它的位置,添加或者移除它。每一节车厢就好比链表中的每一个元素,中间的链接就好比指针。

    1.png-31.6kB

    接下来你将会学到:链表和双向链表。


    创建一个链表

    接下来会创建一个Link类,并实现以下功能:

    • append(ele): 向链表尾部添加一个新的项
    • removeAt(index): 从链表特定位置移除一项
    • get(index): 获取指定位置的值
    • set(index, value): 设置指定位置的值
    • indexOf(ele): 返回元素在链表中的索引,如果没找到返回-1
    • remove(ele): 从链表中移除一项
    • insert(index, ele): 向链表指定的位置插入一个新的项
    • isEmpyt(): 如果链表不包含任何元素返回true否则false
    • size(): 返回链表包含元素的个数
    • toString(): 由于链表使用了Node类,需要重写JS的toString方法,让其只输出元素的指
    • getHead(): 返回链表的头部节点信息

    构造函数如下:

    1
    2
    3
    4
    5
    function Link(){
    this.head = null;
    this.length = 0;
    return this;
    }

    创建Node类的私有方法

    为了让Node类与链表本身更加紧密关联,在这里先实现一个可以创建Node类的私有方法,代码如下:

    1
    2
    3
    4
    5
    6
    Link.prototype._node = function (element){
    var node = {};
    node.element = element;
    node.next = null;
    return node;
    };

    该方法每次调用都会返回一个新对象,里面包含要保存的值以及指向下一个节点的指针。

    向链表尾部追加元素

    在追加元素的时候分为两种情况:链表为空,添加的是第一个元素,或者不为空,向其追加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Link.prototype.append = function (ele){
    var node = this._node(ele);
    var current = null;
    if(this.head === null){
    this.head = node;
    }else{
    current = this.head;
    while(current.next){
    current = current.next;
    }
    current.next = node;
    }
    this.length++;
    return this;
    };

    让我们来分析以下上面的代码:先是把要添加的元素作为参数传入,创建Node的实例。

    场景一: 向空的链表添加一个元素。当我们创建Link对象时,this.head会指向null,这就意味着像链表中添加第一个元素。因此要做的事情就是让head元素指向当前这个Node对象。同时下一个this.next会自动指向null

    链表中最后一个节点的下一个始终指向的是null

    1.png-28.9kB

    场景二:链表不为空。要向链表尾部添加元素,始终要记住一点就是首先需要找到最后一个元素,我们始终只有第一个元素的引用,因此循环访问链表,直到找到最后一项。为此,我们创建current这个中间量。当current.next === null的时候循环就会终止,这样就可以知道已经到达尾部了。之后要做的便是让最后一个元素的this.next指向要添加的元素。下图展示了这个行为:

    1.png-26.2kB

    1
    2
    3
    4
    5
    var linked = new Link();
    linked.append(1).append(2).append(3);
    console.log(linked);

    1.png-19.6kB

    最后别忘了更新链表的长度。

    从链表中移除元素

    移除元素也有两种场景:第一种是移除第一个元素,第二种是移除第一个以外的任意元素。

    要实现的移除方法

    • 根据位置移除 : removeAt(index)
    • 根据元素的值移除 : remove(element)

    这一部分先来实现根据位置移除元素的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Link.prototype.removeAt = function (index){
    if(index < 0 || index > this.length -1){
    return;
    }
    var current = this.head, idx = 0, previous = null;
    if(index === 0){
    // 如果传入的是0那么就是移除第一个
    // 简单的将head的指针修改即可
    this.head = current.next;
    }else{
    // 如果不是0,那么想找到对应位置的元素,就要从头开始循环
    // 原理就是找到对应的元素后,将对应元素的上一个元素的next指向当前元素的下一个元素
    while(idx++ < index){
    previous = current;
    current = current.next;
    }
    // 这样循环结束之后 previous 储存的就是目标元素的上一个,而current储存的就是目标元素
    // 之后将目标元素的上一个和目标元素的下一个相连
    previous.next = current.next;
    }
    // 更新长度;
    this.length--;
    // 返回删除元素的值
    return current.element;
    };

    让我们来进一步分析上面的代码:该方法首先来验证要移除元素的位置是否有效,如果无效九返回null,第一种场景如果是移除链表的第一个元素,就是让this.head指向链表的第二个元素,于是巧妙的利用了current这个中间变量。下图展示了移除第一个元素的过程,请好好理解:

    1.png-23kB

    第二种情况就是移除除了第一个元素意外的任意一个元素,先来看如果移除的是最后一个元素的情况,如图:

    1.png-41.1kB

    从图上可以清楚的看到,当移除的是最后一个元素的时候,在找到最后一个元素的上一个元素之后,将上一个的this.next引用到当前元素的下一个也就是this.next = null;

    再来看看,对于链表中其它的元素是否可以遵循同样的逻辑,如图:

    1.png-42.4kB

    可以看出来除了第一项其它的元素都遵循上面的逻辑。这里还值得说的是,当元素不再被任何变量引用,那么它占用的内存就会被垃圾回收机制释放掉。

    获取指定位置的值

    接下来我们要来实现链表的get()方法,用来方便的通过索引来获取对应的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Link.prototype.get = function (index){
    if(index < 0 || index > this.length - 1){
    return null;
    }
    var current = null, idx = 0;
    if(index === 0){
    return this.head.element;
    }
    current = this.head;
    while(idx++ < index){
    current = current.next;
    }
    return current.element;
    };

    设置指定位置的值

    下面来实现可以设置指定位置数据的set()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Link.prototype.set = function (index, value){
    if(index < 0 || index > this.length - 1){
    return null;
    }
    var current = null, idx = 0;
    if(index === 0){
    this.head.element = value;
    }else{
    current = this.head;
    while(idx++ < index){
    current = current.next;
    }
    current.element = value;
    }
    return this;
    };

    测试代码:

    1
    2
    3
    4
    5
    var nodeList = new Link();
    nodeList.append(1).append(2).append(3);
    nodeList.set(0, 10);
    console.log(nodeList);

    结果如图:

    1.png-48.2kB

    在任意位置插入一个元素

    insert()方法可以实现在任意地方插入新元素。其原理就是找到要插入的位置的前后元素,然后将它们链接起来。

    1
    2
    大专栏  链表
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Link.prototype.insert = function (index, element){
    if(index < 0 || index >= this.length + 1){
    throw new Error('fuck!');
    }
    var node = this._node(element), current = this.head, previous = null, idx = 0;
    if(index === 0){
    this.head = node;
    this.head.next = current;
    }else{
    while(idx++ < index){
    previous = current;
    current = current.next;
    }
    previous.next = node;
    node.next = current;
    }
    this.length++;
    return this;
    };

    有了前面的基础,我相信再看上面的代码应该会很容易了。同样分为两种场景,第一种是像最前面添加,第二种是向指定位置添加。来看图:

    向最前面添加,只需要改变this.head的指向即可:

    1.png-36.6kB

    向任意位置添加,同样需要先找到对应位置的元素和这个元素的上一个元素,然后将新元素和它们链接起来即可:

    • 向最后一个位置插入元素:

    1.png-37.6kB

    • 向中间任意位置插入元素:

    1.png-44.3kB

    实现其他方法

    接下来将实现:toString() indexOf() isEmpty() size()等Link类的方法。

    toString()方法会把Link对象转换成一个字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Link.prototype.toString = function (){
    var current = this.head,
    string = '';
    while(current){
    string += ',' + current.element;
    current = current.next;
    }
    return string.slice(1);
    };

    indexOf()方法接收一个元素的值,如果在链表中找到它就返回它的位置,否则就返回-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Link.prototype.indexOf = function (element){
    var current = this.head,
    index = -1;
    while(current){
    index++;
    if(element === current.element){
    return index;
    }
    current = current.next;
    }
    return -1;
    };

    有了这个方法就可以很容易的实现上面还没完成的remove()方法了:

    1
    2
    3
    4
    Link.prototype.remove = function (element){
    var index = this.indexOf(element);
    return this.removeAt(index);
    };

    isEment() size() getHead()三个方法相对比较简单,这里一次性写完

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Link.prototype.isEmpty = function (){
    return this.length === 0;
    };
    Link.prototype.size = function (){
    return this.length;
    };
    Link.prototype.getHead = function (){
    return this.head;
    };

    双向链表

    双向链表和普通链表的区别在于,一个元素不仅仅只有链向下一个节点的链接,而在双向链表中,链接是双向的,一个链向下一个元素,另一个链向上一个元素,如下图所示:

    1.png-28.2kB

    只需要在单项链表构造函数的基础上稍加改动即可实现:

    1
    2
    3
    4
    5
    6
    function DoubleLink(){
    this.head = null;
    this.tail = null; // 用来存放最后一个节点
    this.length = 0;
    return this;
    }

    双向链表提供了两种迭代的方法:从头到尾或者反过来。同时还可以访问任意一个元素的上一个和下一个兄弟元素。这是双向链表的一个优点。

    双向链表的Node类

    按照需求,需要对Node类进行修改,添加一个指向上一个节点的指针,如下:

    1
    2
    3
    4
    5
    6
    DoubleLink.prototype._node = function (element){
    this.element = element;
    this.prev = null;
    this.next = null;
    return this;
    };

    在任意位置插入一个新元素

    在双向链表中插入一个新元素跟单向链表非常类似。但是双向链表需要同时控制this.nextthis.prev两个指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    DoubleLink.prototype.insert = function (index, element){
    if(index < 0 || index > this.length){
    throw new Error('fuck!');
    }
    var count = 0, current = this.head, previous = null, node = this._node(element);
    if(index === 0){
    if(this.head){
    node.next = current;
    current.prev = node;
    this.head = node;
    }else{
    this.head = this.tail = node;
    }
    }else if(index === this.length){
    current = this.tail;
    current.next = node;
    node.prev = current;
    this.tail = node;
    }else{
    while(count++ < index){
    previous = current;
    current = current.next;
    }
    previous.next = node;
    node.prev = previous;
    node.next = current;
    current.prev = node;
    }
    this.length++;
    return this;
    };

    通过示意图来分析上面的代码

    第一种场景,向最前面添加元素,分为第一次添加和已经有第一个元素的两种情况。

    1.png-57.3kB

    第二种场景,向链表最后添加新元素。

    1.png-44.8kB

    第三种场景,向链表中间的位置添加新元素。

    1.png-61.7kB

    从任意位置移除元素

    从双向链表中移除元素跟单向链表非常类似。唯一区别就是需要设置前一个位置的指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    DoubleLink.prototype.removeAt = function (index){
    if(index < 0 || index > this.length - 1){
    return;
    }
    var current = this.head, previous = null, count = 0;
    if(index === 0){
    this.head = current.next;
    if(this.length === 1){
    this.tail = null;
    }else{
    this.head.prev = null;
    }
    }else if(index === this.length - 1){
    current = this.tail;
    this.tail = current.prev;
    this.tail.next = null;
    }else{
    while(count++ < index){
    previous = current;
    current = current.next;
    }
    previous.next = current.next;
    current.next.prev = previous;
    }
    this.length--;
    return current.element;
    }

    移除元素同样分为三种场景:从头部,从中间,从尾部;依然使用示意图来说明。

    移除第一个元素的过程:

    1.png-45.4kB

    移除最后一个元素的过程:

    1.png-49kB

    移除中间元素的过程:

    1.png-50.4kB

    其它方法和单项链表非常类似,这里就不一一去实现了。


    循环链表

    学了上面的链表相关的知识,再来看循环链表会变得相当简单。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针不是null,而是指向第一个元素this.head,如下图所示:

    单项循环链表示意图:

    1.png-31.7kB

    双向链表循环示意图:

    1.png-34.4kB

    这里暂且不对循环列表再进行coding,我相信如果上面的你学会了,那么很容易就能实现循环链表。


    小结

    链表的优点在于无需移动所有元素就可以方便的删除和添加元素。但是就JavaScript开发本身而言,我个人并不会去刻意的使用,或许我还没意识到这些数据结构的重要性吧。

    路还长着,且学且珍惜!

  • 相关阅读:
    mysql 安装教程
    Centos 7和 Centos 6开放查看端口 防火墙关闭打开
    yum源中默认好像是没有mysql的。为了解决这个问题,我们要先下载mysql的repo源。
    CentOS更改yum源与更新系统
    Linux中文显示乱码?如何设置centos显示中文
    centos 7 升级/安装 git 2.7.3
    Maven实现项目构建直接部署Web项目到Tomcat
    ODAC (odp.net) 从开发到部署
    OGNL的使用
    DotNet Core 中使用 gRPC
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12288914.html
Copyright © 2020-2023  润新知