为了账号安全,请及时绑定邮箱和手机立即绑定

为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

标签:
JavaScript

第五章 链表

链表数据结构

要存储多个元素,数组(或列表)可能是最常见的数据结构了。然后这种数据结构有一个缺点:数组的大小是固定的,从数组的起点或中间插入或移除项的成本有点高,因为需要移动元素。

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

https://img1.sycdn.imooc.com//5bddb10c0001546307300170.jpg

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

现实中也有一些链表的例子,第一个例子就是康加舞队,每个人就是一个元素,手就是链向下一个人的指针,可以向队列汇总增加人——只需要找到想加入的点,断开连接,插入一个人,再重新连接起来。

另外一个例子就是寻宝游戏,你有一条线索,这条线索是指向寻找下一个线索的地点的指针,你顺着这条链去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一方法,就是从起点(第一个线索)顺着列表寻找。

还有一个可能就是用来说明链表中的最流行的例子,那就是火车。一列火车是由一系列车厢(也称车皮)组成的。每节车厢或车皮都互相连接。可以很容易的分离开一节车皮,改变它的位置,添加或移除它。

创建链表

了解链表是什么之后,就要开始实现我们的数据结构了,一下是我们的 LinkedList 类的骨架:

function LinkedList(){    let Node = function(element){ // 需要一个Node辅助类,表示要加入列表的项,element 代表要添加到列表中的值, next d代表指向列表的下一个节点向的指针
        this.element = element;        this.next = null;
    }    let length = 0; // 存储列表项的数量 length 属性
    let head = null; // 存储第一个节点的引用在 head 变量

    this.append = function(element){};    this.insert = function(position,element){}    this.removeAt = function(position){}    this.remove = function(element){}    this.indexOf = function(position){}    this.isEmpty = function(){}    this.size = function(){}    this.getHead = function(){}    this.toString = function(){}    this.print = function(){}

}

LinkedList 类的方法的功能

  • append(element):向列表尾部添加一个新的项

  • insert(position,element):向列表的特定位置插入一个新的项

  • removeAt(position):从列表的特定位置移除一项

  • remove(element):从列表中移除一项

  • indexOf(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1

  • isEmpty():如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回 false

  • size():返回链表包含的元素个数,与数字的 length 属性类似

  • toString():由于列表项使用 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。

向链表尾部追加元素

向 LinkedList 对象尾部添加一个元素时,可能有两种场景,列表为空,添加的是第一个元素,或者列表不为空,向追加元素。

this.append = function(element){    let node = new Node(element),current;    if(head === null){
        head = node;
    }else{
        current = head;        // 循环列表,直到找到最后一项
        while(current.next){
            current = current.next;
        }        // 当current.next元素为null时,找到最后一项,将其 next 赋为 node,建立连接
        current.next = node;
    }
    length++;  // 更新列表的长度};

可以在 append 函数 加上return node ,通过下面的代码来使用和测试目前创建的数据结构

let list = new LinkedList();console.log(list.append(15)); // Node {element: 15, next: null}console.log(list.append(10)); // Node {element: 10, next: null}

从链表中移除元素

移除元素也有两种场景:第一种是移除第一个元素,第二种是移除第一个以外的任一元素。我们要实现两种 remove 方法:第一种是从特定位置移除第一个元素,第二种是根据元素的值移除元素。

首先先实现移除特定位置的元素

this.removeAt = function(position){    // 检查越界值
    if(position >-1 && position < length){        let current = head,previous,index = 0;        // 移除第一项
        if(position === 0){
            head = current.next;            console.log(current.element);
        }else{            while(index++ < position){                previous = current;
                current = current.next;
            }
            // 将 previous 与 current 的下一项连接起来,跳过 current,从移除它            previous.next = current.next;
        }        length --;        console.log(current.element);        return current.element;
    }else{              
        return null;
    }           
}

如果想要移除第一个元素(position=0),要做的就是让 head 指向列表的第二个元素,我们用 current 变量穿甲一个对列表中第一个元素的应用,这样 current 变量就是对列表中第一个元素的引用,如果吧 head 赋值为 current.next 就会移除第一个元素。

如果我们要移除列表的最后一项或者是中间的一项,为此,需要依靠一个细节来迭代列表,知道到达目标位置(index++ < position),使用一个用于内部控制和递增的 index 变量,current 变量总是对所循环列表的当前元素的引用(current = current.next),我们还需要一个对当前元素的前一个元素的引用(previous = current),它被命名为 previous。

因此,要从列表中移除当前元素,要做的就是将 previous.next 和 current.next 链接起来,这样当前元素就会被丢弃在计算机内存中,等着被垃圾回收站清除。

对于最后一个元素,在(while(index++ < position))跳出循环时, current 变量总是对列表中最后一个元素的引用(要移除的元素)。current.next 的值将是 null(因为它是最后一个元素)。由于还保留了对 previous 的引用(当前元素的前一个元素),previous 就指向了 current 。那么要移除 current,要做的就是把 previous.next 的值改变为 current.next 。

在任意位置插入元素

实现 insert 方法,使用这个方法可以在任意位置插入一个元素。

this.insert = function(position,element){    // 检查越界值
    if(position >=0 && position <= length){        let node = new Node(element),
        current = head,
        previous,
        index = 0;        if(position === 0 ){ // 在第一个位置添加
            node.next = current;
            head = node;
        }else{            while(index++ < position){                previous = current;
                current = current.next;
            }            node.next = current;
            previous.next = node;
        }        length++;        return true;
    }else{        return false;
    }
}

current 变量是对列表中第一个元素的引用,我们需要做的是把 node.next 的值设为 current (列表中的第一个元素),现在 head 和 node.next 都指向了 current ,接下来要做的就是把 head 的引用改为 node ,这样列表中就有了一个新元素。

现在来处理第二种场景,在列表中间或者末尾添加一个新的元素。首先,循环访问列表,找打目标为位置,当跳出循环的时候, current 变量将是对想要插入新元素的位置之后一个元素的引用,而 previous 将是对想要插入新元素的位置之前的一个元素的引用。在这种情况下,我门要在 previous 和 current 之间添加新项。因此,需要将新项(node)和当前链接起来(node.next = current),然后需要改变 previous 和 current z之间的链接,我们还需要让 previous.next 指向 node。

实现其他方法

toString方法

this.toString = function(){    let current = head,
    string = '';    while(current){
        string += current.element + (current.next ? '-':'');
        current = current.next;
    }    return string;
}

赋值current为 head, 循环访问 current,将 current 变量当做索引,初始化用于拼接元素的变量(string)。通过 current 来检查元素是否存在,如果列表为空或是到达列表中最后一个元素的下一位(null),while 循环中的代码就不会执行,就可以得到元素的内容,将其拼接到字符串中,最后,迭代下一个元素。最后,返回列表内容的字符串。

indexOf方法

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

this.indexOf = function(element){    let current = head,
    index = 0;    while(current){        if(element === current.element){            return index;
        }
        index++;
        current = current.next;
    }    return -1;
}

循环变量 current,它的初始值是 head ,利用index 来计算位置数。访问元素,检查当前元素是否是我们要找的,如果是,就返回它的位置,不是就继续计数,检查列表中的下一个节点。

如果列表为空,或是到达列表的尾部(current = current.next 将是 null),循环就不会执行。如果没有找到值就返回 -1 。

实现了上面的方法,就可以实现remove等其他方法了

remove方法

this.remove = function(element){    let index = this.indexOf(element);    return this.removeAt(index);
}

isEmpty、size 和 getHead方法

isEmpty和size 和之前章节实现一模一样

this.isEmpty = function(){    return length === 0;
}this.size = function(){    return length;
}

还有 getHead方法

this.getHead = function(){    return head;
}

head 变量是 LinkedList 类的私有变量,我们如果需要在类的实现外部循环访问列表,就需要提供一种获取类的第一个元素的方法。

整个 LinkedList函数

function LinkedList(){    let Node = function(element){ // 需要一个Node辅助类,表示要加入列表的项,element 代表要添加到列表中的值, next d代表指向列表的下一个节点向的指针
        this.element = element;        this.next = null;
    }    let length = 0; // 存储列表项的数量 length 属性
    let head = null; // 存储第一个节点的引用在 head 变量

    this.append = function(element){        let node = new Node(element),current;        if(head === null){
            head = node;
        }else{
            current = head;            // 循环列表,直到找到最后一项
            while(current.next){
                current = current.next;
            }            // 当current.next元素为null时,找到最后一项,将其 next 赋为 node,建立连接
            current.next = node;
        }
        length++;  // 更新列表的长度
    };    this.insert = function(position,element){        // 检查越界值
        if(position >=0 && position <= length){            let node = new Node(element),
            current = head,
            previous,
            index = 0;            if(position === 0 ){ // 在第一个位置添加
                node.next = current;
                head = node;
            }else{                while(index++ < position){                    previous = current;
                    current = current.next;
                }                node.next = current;
                previous.next = node;
            }            length++;            return true;
        }else{            return false;
        }
    }    this.removeAt = function(position){
        // 检查越界值        if(position >-1 && position < length){            let current = head,previous,index = 0;

            // 移除第一项            if(position === 0){
                head = current.next;
                console.log(current.element);
            }else{                while(index++ < position){                    previous = current;
                    current = current.next;
                }
                // 将 previous 与 current 的下一项连接起来,跳过 current,从移除它                previous.next = current.next;
            }            length --;            return current.element;
        }else{              
            return null;
        }           
    }    this.remove = function(element){
        let index = this.indexOf(element);
        return this.removeAt(index);
    }    this.indexOf = function(element){
        let current = head,
        index = 0;
        while(current){            if(element === current.element){
                return index;
            }            index++;            current = current.next;
        }        return -1;
    }    this.isEmpty = function(){
        return length === 0;
    }    this.size = function(){
        return length;
    }    this.getHead = function(){
        return head;
    }    this.toString = function(){
        let current = head,
        string = '';        while(current){            string += current.element + (current.next ? '-':'');            current = current.next;
        }        return string;
    }

}

双向链表

链表有多种不同的类型,这一节介绍 双向链表,双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。如下图所示

https://img1.sycdn.imooc.com//5bddb1340001553e07300170.jpg

先从实现 DoublyLinkedList 类所需要的变动开始

function DoublyLinkedList(){    let Node = function(elememt){        this.elememt = elememt;        this.next = null;        this.prev = null; // 新增的
    }    let length = 0;    let head = null;    let tail = null // 新增的

    // 这里是方法}

可以看出,Node 类中新增了 prev属性(一个新指针),在 DoublyLinkedList 类里也有用来保存对列表最后一项的引用的 tail 属性。

双向链表提供了两种迭代列表的方式:从头到尾,或者从尾到头。我们也可以方位一个特定节点的下一个或者是上一个元素。在单向链表中,如果迭代列表时错过了要找的元素,就需要回到列表起点,重新迭代。这是双向链表的一个优点。

在任意位置插入新元素

向双向链表中插入一个新项跟(单向)链表非常相类似。区别在于,链表只要控制一个 next 指针,而双向链表则要同时控制 next 和 prev (previous,前一个)这两个指针。

this.insert = function(position,elememt){    // 检查越界值
    if(position >= 0 && position <= length){        let node = new Node(elememt),
        current = head,
        previous,
        index = 0;        if(position === 0){ // 在第一个位置添加
            if(!head){
                head = node;
                tail = node;
            }else{
                node.next = current;
                current.prev = node;
                head = node;
            }
        }else if(position === length){ // 最后一项
            current = tail;
            current.next = node;
            node.prev = current;
            tail = node;
        }else{            while (index++ < position) {                previous = current;
                current = current.next;
            }            node.next = current;
            previous.next = node;

            current.prev = node;
            node.prev = previous;
        }        length++        return true;
    }else{        return false;
    }
}

在列表的第一个位置(列表的起点)插入一个新元素,如果列表为空(if(!head)),那只需将 head 和 tail 都指向这个新节点。如果不为空, current 变量将是对列表中的第一个元素的引用。就像我们在链表中所做的,把 node.next 设为 current ,而head 将指针指向 node (它被设为列表中的第一个元素)。不同之处,我们还需要为指向上一个元素的指针设一个值。current.prev 指针将由 指向 null 变成指向 新元素(current.prev = node)。node.prev 指针已经是 null,因此不需要在更新任何东西了。

假如我们要在列表最后添加一个新元素。这是一个特殊情况,因为我们还控制着指向最后一个元素的指针(tail)。current 变量将引用最后一个元素(current = tail)。然后分开建立第一个链接:node.prev 将引用current。current.next 指针(指向null)将指向 node (由于构造函数,node.next 已经指向了 null)。然后只剩下一件事,就是更新 tail ,它将由 指向 current 变成指向 node 。

第三种场景:在列表中插入一个新元素,就像我们在之前方法中所做,迭代列表,知道到达要找的位置(while (index++ < position))。我们将在 current 和 previous 元素之间插入新元素。首先,node.next 将指向 current ,而 previous.next 将指向 node,这样就不会跌势节点之间的链接。然后需要处理所有的链接:current.prev 将指向node ,而 node.prev 将指向 previous

从任意位置移除元素

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

this.removeAt = function(position){    // 检查越界值
    if(position > -1 && position < length){        let current = head,
        previous,
        index = 0;        // 移除第一项
        if(position === 0){
            head = current.next;            // 如果只有一项,更新 tail
            if(length === 1){
                tail = null;
            }else{
                head.prev = null;
            }
        }else if(position === length-1){ // 最后一项
            current = tail;
            tail = current.prev;
            tail.next = null;
        }else{            while (index++ < position) {                previous = current;
                current = current.next;
            }
            // 将 previous 与 current 的下一项连接起来——跳过 current
            previous.next = current.next;
            current.next.prev = previous;
        }        length --;        return current.elememt;
    }else{        return null;
    }
}

我们需要处理三种场景,从头部、从中间和从尾部移除一个元素。

移除第一个元素。current 变量是对列表中第一个元素的引用,也就是我们想要移除的远古三。需要做的就是改变 head 的引用,将其从 current 改为下一个元素(head = current.next;),但是我们还需要更新 current.next 指向上一个元素的指针(因为第一个元素的 prev 指针是 null)。因此,把 head.prev 的引用改为 null(因为 head 也指向了列表中的第一个元素,或者也可以用 current.next.prev )。由于还需要控制 tail 的引用,我们可以检测要移除的是否是第一个元素,如果是,只需要把 tail 也设为 null。

移除最后一个位置的元素。既然已经有了对最后一个元素的引用(tail),我们就不需要为找到它而迭代列表。我们可以把 tail 的引用赋给 current 变量。接下来,就需要吧 tail 的引用更新为列表中的倒数第二个元素(current.prev,或者 tail.prev也可以)。既然 tail 指向了倒数第二个元素,我们需要把 next 指针更新为 null (tail.next = null)。

最后一种场景,从列表中移除一个元素。首先需要迭代列表,直到到达要找的位置。current 变量所引用的就是要移除的远古三。那么要移除它,我们可以通过更新 previous.next 和 current.next.prev 的引用,在列表中跳过它。因此,previous.next 将 指向 current.next ,而 current.next.prev 将指向 previous。

完整代码

function DoublyLinkedList(){    let Node = function(elememt){        this.elememt = elememt;        this.next = null;        this.prev = null; // 新增的
    }    let length = 0;    let head = null;    let tail = null // 新增的


    this.append = function(elememt){        let node = new Node(elememt),
        current;        if(!head){
            head = node;
            tail = node;
        }else{
            current = tail;
            current.next = node;
            node.prev = current;
            tail = node;
        }
        length++;
    }    this.insert = function(position,elememt){        // 检查越界值
        if(position >= 0 && position <= length){            let node = new Node(elememt),
            current = head,
            previous,
            index = 0;            if(position === 0){ // 在第一个位置添加
                if(!head){
                    head = node;
                    tail = node;
                }else{
                    node.next = current;
                    current.prev = node;
                    head = node;
                }
            }else if(position === length){ // 最后一项
                current = tail;
                current.next = node;
                node.prev = current;
                tail = node;
            }else{                while (index++ < position) {                    previous = current;
                    current = current.next;
                }                node.next = current;
                previous.next = node;

                current.prev = node;
                node.prev = previous;
            }            length++            return true;
        }else{            return false;
        }
    }   

    this.removeAt = function(position){
        // 检查越界值        if(position > -1 && position < length){            let current = head,
            previous,            index = 0;
            // 移除第一项            if(position === 0){
                head = current.next;
                // 如果只有一项,更新 tail
                if(length === 1){
                    tail = null;
                }else{                    head.prev = null;
                }
            }else if(position === length-1){ // 最后一项                current = tail;
                tail = current.prev;
                tail.next = null;
            }else{                while (index++ < position) {                    previous = current;
                    current = current.next;
                }
                // 将 previous 与 current 的下一项连接起来——跳过 current
                previous.next = current.next;
                current.next.prev = previous;
            }            length --;            return current.elememt;
        }else{            return null;
        }
    }    this.remove = function(elememt){
        let index = this.indexOf(elememt);
        this.removeAt(index);
    }   

    this.toString = function(){
        let current = head,
        str = '';        while (current) {            str += current.elememt + (current.next?'-':'');            current = current.next;
        }        return str;
    }       

    this.indexOf = function(elememt){
        let current = head,
        index = 0;
        while (current) {            if(current.elememt === elememt){
                return index
            }            current = current.next;
            index++
        }        return -1;
    }    this.isEmpty = function(){
        return length === 0;
    }    this.size = function(){
        return length;
    }    this.getHead = function(){
        return head;
    }

}

循环链表

循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用 null,而是指向第一个元素(head),如下图所示


双向链表有指向 head 元素的 tail.next ,和指向 tail 元素的 head.prev

https://img1.sycdn.imooc.com//5bddb0b50001025e07300170.jpg

小结

这一章中,学习了链表这种数据结构,及其辩题双向链表和循环链表,知道了如何在任意位置添加和移除元素,已经如何循环访问两边,比数组重要的优点就是,无需移动链表中的元素,就能轻松添加和移除元素。当你需要添加和移除很多元素的时候,最好的选择就是链表,而非数组。下一章将学习集合,最后一种顺序数据结构。

原文出处:https://www.cnblogs.com/lbh2018/p/JavaScript_Data_Structures_and_Algorithms_Part5.html  

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消