您现在的位置是:首页 > 文章详情

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

日期:2018-11-11点击:273

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

本章内容

  • 链表数据结构
  • 向链表添加元素
  • 从链表移除元素
  • 使用 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数据结构与算法

原文链接:https://yq.aliyun.com/articles/667998
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章