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


    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张。

    本章内容

    • 链表数据结构
    • 向链表添加元素
    • 从链表移除元素
    • 使用 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){};
        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;
        }
    
    }
    

    双向链表

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

    双向链表

    先从实现 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

    双向循环链表

    小结

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

    书籍链接: 学习JavaScript数据结构与算法

  • 相关阅读:
    网络编程:心跳检测保证连接有效
    网络编程:端口复用
    网络编程:优雅的关闭
    网络编程:小包数据的处理
    网络编程:理解TCP中的“流”
    网络编程:TCP故障模式
    C语言实现高阶阶乘(1000的阶乘C语言实现)
    函数指针
    CCS(TI IDE)中英文切换
    命令行与C++
  • 原文地址:https://www.cnblogs.com/lbh2018/p/JavaScript_Data_Structures_and_Algorithms_Part5.html
Copyright © 2020-2023  润新知