JavaScript数据结构——链表的实现与应用
- 时间:
- 浏览:0
链表用来存储有序的元素集合,与数组不同,链表中的元素并不保处于连续的存储空间内,每个元素由另一一一十个 存储元素并全是的节点和另一一一十个 指向下另一一一十个 元素的指针构成。当要移动或删除元素时,只时需修改相应元素上的指针就能并能并能了。对链表元素的操作要比对数组元素的操作时延更高。下面是链表数据形态的示意图:
要实现链表数据形态,关键在于保存head元素(即链表的头元素)以及每另一一一十个 元素的next指针,有这两部分让让我门 就能并能并能很方便地遍历链表从而操作所有的元素。能并能并能把链表想象成二根锁链,锁链中的每另一一一十个 节点全是相互连接的,让让我门 只要找到锁链的头,整条锁链就都能并能并能找到了。让我 们来看一下具体的实现最好的土土办法。
首先让让我门 时需另一一一十个 辅助类,用来描述链表中的节点。你什儿 类很简单,只时需另一一一十个 属性,另一一一十个 用来保存节点的值,另一一一十个 用来保存指向下另一一一十个 节点的指针。
let Node = function (element) { this.element = element; this.next = null; };
下面是让让我门 链表类的基本骨架:
class LinkedList { constructor() { this.length = 0; this.head = null; } append (element) {} // 向链表中加带节点 insert (position, element) {} // 在链表的指定位置插入节点 removeAt (position) {} // 删除链表中指定位置的元素,并返回你什儿 元素的值 remove (element) {} // 删除链表中对应的元素 indexOf (element) {} // 在链表中查找给定元素的索引 getElementAt (position) {} // 返回链表中索引所对应的元素 isEmpty () {} // 判断链表不是为空 size () {} // 返回链表的长度 getHead () {} // 返回链表的头元素 clear () {} // 清空链表 toString () {} // 辅助最好的土土办法,按指定格式输出链表中的所有元素,方便测试验证结果 }
让我 们从查找链表元素的最好的土土办法getElementAt()开始英语 英语 ,刚刚里边让让我门 会多次用到它。
getElementAt (position) { if (position < 0 || position >= this.length) return null; let current = this.head; for (let i = 0; i < position; i++) { current = current.next; } return current; }
首先判断参数position的边界值,刚刚值超出了索引的范围(小于0刚刚大于length - 1),则返回null。让让我门 从链表的head开始英语 英语 ,遍历整个链表直到找到对应索引位置的节点,但会 返回你什儿 节点。是全是很简单?和所有有序数据集合一样,链表的索引默认从0开始英语 英语 ,只要找到了链表的头(很多很多让让我门 时需在LinkedList类中保存head值),但会 就能并能并能遍历找到索引所在位置的元素。
有了getElementAt()最好的土土办法,接下来让让我门 就能并能并能很方便地实现append()最好的土土办法,用来在链表的尾部加带新节点。
append (element) { let node = new Node(element); // 刚刚当前链表为空,则将head指向node if (this.head === null) this.head = node; else { // 但会 ,找到链表尾部的元素,但会 加带新元素 let current = this.getElementAt(this.length - 1); current.next = node; } this.length++; }
刚刚链表的head为null(你什儿 情况示链表为空),则直接将head指向新加带的元素。但会 ,通过getElementAt()最好的土土办法找到链表的最后另一一一十个 节点,将该节点的next指针指向新加带的元素。新加带的元素的next指针默认为null,链表最后另一一一十个 元素的next值为null。将节点挂到链表上刚刚,并不忘记将链表的长度加1,让让我门 时需通过length属性来记录链表的长度。
接下来让让我门 要实现insert()最好的土土办法,能并能并能在链表的任意位置加带节点。
insert (position, element) { // position并能并能超出边界值 if (position < 0 || position > this.length) return false; let node = new Node(element); if (position === 0) { node.next = this.head; this.head = node; } else { let previous = this.getElementAt(position - 1); node.next = previous.next; previous.next = node; } this.length++; return true; }
首先也是要判断参数position的边界值,并能并能越界。当position的值为0时,表示要在链表的头部插入新节点,对应的操作如下图所示。将新插入节点的next指针指向现在的head,但会 更新head的值为新插入的节点。
刚刚要插入的节点在链表的里边刚刚尾部,对应的操作如下图。假设链表长度为3,要在位置2插入新节点,让让我门 首先找到位置2的前另一一一十个 节点previous node,将新节点new node的next指针指向previous node的next所对应的节点,但会 再将previous node的next指针指向new node,但会 就把新节点挂到链表中了。考虑一下,当插入的节点在链表的尾部,你什儿 情况也是适用的。而刚刚链表为空,即链表的head为null,则参数position会超出边界条件,从而insert()最好的土土办法会直接返回false。
最后,别忘了更新length属性的值,将链表的长度加1。
按照相同的最好的土土办法,让让我门 能并能并能很容易地写出removeAt()最好的土土办法,用来删除链表中指定位置的节点。
removeAt (position) { // position并能并能超出边界值 if (position < 0 || position >= this.length) return null; let current = this.head; if (position === 0) this.head = current.next; else { let previous = this.getElementAt(position - 1); current = previous.next; previous.next = current.next; } this.length--; return current.element; }
下面两张示意图说明了从链表头部和其它位置删除节点的情况。
刚刚要删除的节点为链表的头部,只时需将head移到下另一一一十个 节点即可。刚刚当前链表只另一一一十个 节点,如此下另一一一十个 节点为null,此时将head指向下另一一一十个 节点等同于将head设置成null,删除刚刚链表为空。刚刚要删除的节点在链表的里边部分,让让我门 时需找出position所在位置的前另一一一十个 节点,将它的next指针指向position所在位置的下另一一一十个 节点。总之,删除节点只时需修改相应节点的指针,使断开位置左右相邻的节点重新连接上。被删除的节点刚刚再也如此其它部分的引用而被丢弃在内存中,等候垃圾回收器来清除。有关JavaScript垃圾回收器的工作原理,能并能并能查看这里。
最后,别忘了将链表的长度减1。
下面让让我门 来看看indexOf()最好的土土办法,该最好的土土办法返回给定元素在链表中的索引位置。
indexOf (element) { let current = this.head; for (let i = 0; i < this.length; i++) { if (current.element === element) return i; current = current.next; } return -1; }
让让我门 从链表的头部开始英语 英语 遍历,直到找到和给定元素相同的元素,但会 返回对应的索引号。刚刚如此找到对应的元素,则返回-1。
链表类中的其它最好的土土办法都比较简单,就不再分部讲解了,下面是删剪的链表类的代码:
在isEmpty()最好的土土办法中,让让我门 能并能并能根据length不是为0来判断链表不是为空,当然并能并能并能根据head不是为null来进行判断,前提是所有涉及到链表节点加带和移除的最好的土土办法全是正确地更新length和head。toString()最好的土土办法但会 为了方便测试而编写的,让让我门 来看看十几个 测试用例:
let linkedList = new LinkedList(); linkedList.append(10); linkedList.append(15); linkedList.append(20); console.log(linkedList.toString()); linkedList.insert(0, 9); linkedList.insert(2, 11); linkedList.insert(5, 25); console.log(linkedList.toString()); console.log(linkedList.removeAt(0)); console.log(linkedList.removeAt(1)); console.log(linkedList.removeAt(3)); console.log(linkedList.toString()); console.log(linkedList.indexOf(20)); linkedList.remove(20); console.log(linkedList.toString()); linkedList.clear(); console.log(linkedList.size());
下面是执行结果:
双向链表
里边链表中每另一一一十个 元素只另一一一十个 next指针,用来指向下另一一一十个 节点,但会 的链表称之为单向链表,让让我门 并能并能从链表的头部开始英语 英语 遍历整个链表,任何另一一一十个 节点并能并能找到它的下另一一一十个 节点,而并能并能找到它的上另一一一十个 节点。双向链表中的每另一一一十个 元素拥有另一一一十个 指针,另一一一十个 用来指向下另一一一十个 节点,另一一一十个 用来指向上另一一一十个 节点。在双向链表中,除了能并能并能像单向链表一样从头部开始英语 英语 遍历之外,还能并能并能从尾部进行遍历。下面是双向链表的数据形态示意图:
刚刚双向链表具有单向链表的所有形态,但会 让让我门 的双向链表类能并能并能继承自前面的单向链表类,不过辅助类Node时需加带另一一一十个 prev属性,用来指向前另一一一十个 节点。
let Node = function (element) { this.element = element; this.next = null; this.prev = null; };
下面是继承自LinkedList类的双向链表类的基本骨架:
class DoubleLinkedList extends LinkedList { constructor() { super(); this.tail = null; } }
先来看看append()最好的土土办法的实现。当链表为空时,除了要将head指向当前加带的节点外,时需将tail也指向当时需加带的节点。当链表不为空时,直接将tail的next指向当时需加带的节点node,但会 修改node的prev指向旧的tail,最后修改tail为新加带的节点。让让我门 不时需从头开始英语 英语 遍历整个链表,而通过tail能并能并能直接找到链表的尾部,你什儿 点比单向链表的操作要更方便。最后将length的值加1,修改链表的长度。
append (element) { let node = new Node(element); // 刚刚链表为空,则将head和tail都指向当前加带的节点 if (this.head === null) { this.head = node; this.tail = node; } else { // 但会 ,将当前节点加带到链表的尾部 this.tail.next = node; node.prev = this.tail; this.tail = node; } this.length++; }
刚刚双向链表能并能并能从链表的尾部往前遍历,很多很多让让我门 修改了getElementAt()最好的土土办法,对基类中单向链表的最好的土土办法进行了改写。当要查找的元素的索引号大于链表长度的一时节 ,从链表的尾部开始英语 英语 遍历。
getElementAt (position) { if (position < 0 || position >= this.length) return null; // 从后往前遍历 if (position > Math.floor(this.length / 2)) { let current = this.tail; for (let i = this.length - 1; i > position; i--) { current = current.prev; } return current; } // 但会 往后遍历 else { return super.getElementAt(position); } }
有并全是遍历最好的土土办法,但会 往后遍历调用的是基类单向链表里的最好的土土办法,从后往前遍历时时需到节点的prev指针,用来查找前另一一一十个 节点。
让让我门 一起去还时需修改insert()和removeAt()这另一一一十个 最好的土土办法。记住,与单向链表唯一的区别但会 要一起去维护head和tail,以及每另一一一十个 节点上的next和prev指针。
insert (position, element) { if (position < 0 || position > this.length) return false; // 插入到尾部 if (position === this.length) this.append(element); else { let node = new Node(element); // 插入到头部 if (position === 0) { if (this.head === null) { this.head = node; this.tail = node; } else { node.next = this.head; this.head.prev = node; this.head = node; } } // 插入到里边位置 else { let current = this.getElementAt(position); let previous = current.prev; node.next = current; node.prev = previous; previous.next = node; current.prev = node; } } this.length++; return true; } removeAt (position) { // position并能并能超出边界值 if (position < 0 || position >= this.length) return null; let current = this.head; let previous; // 移除头部元素 if (position === 0) { this.head = current.next; this.head.prev = null; if (this.length === 1) this.tail = null; } // 移除尾部元素 else if (position === this.length - 1) { current = this.tail; this.tail = current.prev; this.tail.next = null; } // 移除里边元素 else { current = this.getElementAt(position); previous = current.prev; previous.next = current.next; current.next.prev = previous; } this.length--; return current.element; }
操作过程中时需判断一些特殊情况,例如链表的头和尾,以及当前链表不是为空等等,但会 守护tcp连接刚刚会在一些特殊情况下愿因越界和报错。下面是另一一一十个 删剪的双向链表类的代码:
让让我门 重写了toString()最好的土土办法以方便更加清楚地查看测试结果。下面是一些测试用例:
let doubleLinkedList = new DoubleLinkedList(); doubleLinkedList.append(10); doubleLinkedList.append(15); doubleLinkedList.append(20); doubleLinkedList.append(25); doubleLinkedList.append(150); console.log(doubleLinkedList.toString()); console.log(doubleLinkedList.getElementAt(1).element); console.log(doubleLinkedList.getElementAt(2).element); console.log(doubleLinkedList.getElementAt(3).element); doubleLinkedList.insert(0, 9); doubleLinkedList.insert(4, 24); doubleLinkedList.insert(7, 35); console.log(doubleLinkedList.toString()); console.log(doubleLinkedList.removeAt(0)); console.log(doubleLinkedList.removeAt(1)); console.log(doubleLinkedList.removeAt(5)); console.log(doubleLinkedList.toString());
对应的结果如下:
[element: 10, prev: null, next: 15] [element: 15, prev: 10, next: 20] [element: 20, prev: 15, next: 25] [element: 25, prev: 20, next: 150] [element: 150, prev: 25, next: null] 15 20 25 [element: 9, prev: null, next: 10] [element: 10, prev: 9, next: 15] [element: 15, prev: 10, next: 20] [element: 20, prev: 15, next: 24] [element: 24, prev: 20, next: 25] [element: 25, prev: 24, next: 150] [element: 150, prev: 25, next: 35] [element: 35, prev: 150, next: null] 9 15 150 [element: 10, prev: null, next: 20] [element: 20, prev: 10, next: 24] [element: 24, prev: 20, next: 25] [element: 25, prev: 24, next: 35] [element: 35, prev: 25, next: null]
循环链表
顾名思义,循环链表的尾部指向它被委托人的头部。循环链表能并能并能有单向循环链表,并能并能并能有双向循环链表。下面是单向循环链表和双向循环链表的数据形态示意图:
在实现循环链表时,时需确保最后另一一一十个 元素的next指针指向head。下面是单向循环链表的删剪代码:
单向循环链表的测试用例:
let circularLinkedList = new CircularLinkedList(); circularLinkedList.append(10); circularLinkedList.append(15); circularLinkedList.append(20); console.log(circularLinkedList.toString()); circularLinkedList.insert(0, 9); circularLinkedList.insert(3, 25); console.log(circularLinkedList.toString()); console.log(circularLinkedList.removeAt(0)); console.log(circularLinkedList.toString());
对应的测试结果:
下一章让让我门 将介绍如保用JavaScript来实现集合你什儿 数据形态。