From 3f99d9a4aa83267e4622f6fa9808cdc7deb3b537 Mon Sep 17 00:00:00 2001 From: XPoet Date: Mon, 27 Jul 2020 14:26:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=8C=E5=90=91=E9=93=BE?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E5=B0=81=E8=A3=85=E5=8F=8A=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...14\345\220\221\351\223\276\350\241\250.md" | 553 ++++++++++++++++++ src/DoublyLinkedList/doublyLinkedList.js | 202 +++++++ src/DoublyLinkedList/index.js | 48 ++ src/index.js | 3 + 4 files changed, 806 insertions(+) create mode 100644 "assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" create mode 100644 src/DoublyLinkedList/doublyLinkedList.js create mode 100644 src/DoublyLinkedList/index.js diff --git "a/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" "b/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" new file mode 100644 index 0000000..4c83a68 --- /dev/null +++ "b/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" @@ -0,0 +1,553 @@ +## JavaScript 数据结构与算法(七)双向链表 + +### 单向链表和双向链表 + +#### 单向链表 + +- 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)。 +- 链表相连的过程是单向的,实现原理是上一个节点中有指向下一个节点的引用。 +- 单向链表有一个比较明显的缺点:可以轻松到达下一个节点,但回到前一个节点很难,在实际开发中, 经常会遇到需要回到上一个节点的情况。 + +#### 双向链表 + +- 既可以从头遍历到尾,也可以从尾遍历到头。 +- 链表相连的过程是双向的。实现原理是一个节点既有向前连接的引用,也有一个向后连接的引用。 +- 双向链表可以有效的解决单向链表存在的问题。 +- 双向链表缺点: + - 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些。 + - 相对于单向链表,所占内存空间更大一些。 + - 但是,相对于双向链表的便利性而言,这些缺点微不足道。 + + + + + + + +### 双向链表结构 + +![](https://user-images.githubusercontent.com/24516169/88497604-724ef080-cff3-11ea-969b-3496e3a64ca6.png) + +- 双向链表不仅有 head 指针指向第一个节点,而且有 tail 指针指向最后一个节点。 +- 每一个节点由三部分组成:item 储存数据、prev 指向前一个节点、next 指向后一个节点。 +- 双向链表的第一个节点的 prev 指向 null。 +- 双向链表的最后一个节点的 next 指向 null。 + +### 双向链表常见的操作 + +- `append(element)` 向链表尾部追加一个新元素。 +- `insert(position, element)` 向链表的指定位置插入一个新元素。 +- `getElement(position)` 获取指定位置的元素。 +- `indexOf(element)` 返回元素在链表中的索引。如果链表中没有该元素就返回 -1。 +- `update(position, element)` 修改指定位置上的元素。 +- `removeAt(position)` 从链表中的删除指定位置的元素。 +- `remove(element)` 从链表删除指定的元素。 +- `isEmpty()` 如果链表中不包含任何元素,返回 trun,如果链表长度大于 0 则返回 false。 +- `size()` 返回链表包含的元素个数,与数组的 length 属性类似。 +- `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。 +- `forwardString()` 返回正向遍历节点字符串形式。 +- `backwordString()` 返回反向遍历的节点的字符串形式。 + +### 双向链表的封装 + +#### 创建双向链表类 DoublyLinkedList + +- DoublyNode 类继承单向链表的 Node 类,新添加 this.prev 属性,该属性用于指向上一个节点。 +- DoublyLinkedList 类继承 LinkedList 类,新添加 this.tail 属性,该属性指向末尾的节点。 + +```js +// 双向链表的节点类(继承单向链表的节点类) +class DoublyNode extends Node { + constructor(element) { + super(element); + this.prev = null; + } +} + +// 双向链表类继承单向链表类 +class DoublyLinkedList extends LinkedList { + + constructor() { + super(); + this.tail = null; + } + +} +``` + +#### append(element) + +```js +// append(element) 往双向链表尾部追加一个新的元素 +// 重写 append() +append(element) { + +// 1、创建双向链表节点 +const newNode = new DoublyNode(element); + +// 2、追加元素 +if (this.head === null) { + this.head = newNode; + this.tail = newNode; +} else { + // !!跟单向链表不同,不用通过循环找到最后一个节点 + // 巧妙之处 + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; +} + +this.length++; +} +``` + +#### insert(position, element) + +```js +// insert(position, data) 插入元素 +// 重写 insert() +insert(position, element) { + // 1、position 越界判断 + if (position < 0 || position > this.length) return false; + + // 2、创建新的双向链表节点 + const newNode = new DoublyNode(element); + + // 3、判断多种插入情况 + if (position === 0) { // 在第 0 个位置插入 + + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// + newNode.next = this.head; + this.head.perv = newNode; + this.head = newNode; + } + + } else if (position === this.length) { // 在最后一个位置插入 + + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } else { // 在 0 ~ this.length 位置中间插入 + + let targetIndex = 0; + let currentNode = this.head; + let previousNode = null; + + // 找到要插入位置的节点 + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + // 交换节点信息 + previousNode.next = newNode; + newNode.prev = previousNode; + + newNode.next = currentNode; + currentNode.prev = newNode; + } + + this.length++; + + return true; + } +``` + +#### insert(position, element) + +```js +// insert(position, data) 插入元素 +// 重写 insert() + insert(position, element) { + // 1、position 越界判断 + if (position < 0 || position > this.length) return false; + + // 2、创建新的双向链表节点 + const newNode = new DoublyNode(element); + + // 3、判断多种插入情况 + if (position === 0) { // 在第 0 个位置插入 + + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// + newNode.next = this.head; + this.head.perv = newNode; + this.head = newNode; + } + + } else if (position === this.length) { // 在最后一个位置插入 + + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } else { // 在 0 ~ this.length 位置中间插入 + + let targetIndex = 0; + let currentNode = this.head; + let previousNode = null; + + // 找到要插入位置的节点 + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + // 交换节点信息 + previousNode.next = newNode; + newNode.prev = previousNode; + + newNode.next = currentNode; + currentNode.prev = newNode; + } + + this.length++; + + return true; + } +``` + +#### removeAt(position) + +```js + // removeAt() 删除指定位置的节点 + // 重写 removeAt() + removeAt(position) { + // 1、position 越界判断 + if (position < 0 || position > this.length - 1) return null; + + // 2、根据不同情况删除元素 + let currentNode = this.head; + if (position === 0) { // 删除第一个节点的情况 + + if (this.length === 1) { // 链表内只有一个节点的情况 + this.head = null; + this.tail = null; + } else { // 链表内有多个节点的情况 + this.head = this.head.next; + this.head.prev = null; + } + + } else if (position === this.length - 1) { // 删除最后一个节点的情况 + + currentNode = this.tail; + this.tail.prev.next = null; + this.tail = this.tail.prev; + + } else { // 删除 0 ~ this.length - 1 里面节点的情况 + + let targetIndex = 0; + let previousNode = null; + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + previousNode.next = currentNode.next; + currentNode.next.perv = previousNode; + + } + + this.length--; + return currentNode.data; + } +``` + +#### update(position, data) + +```js + // update(position, data) 修改指定位置的节点 + // 重写 update() + update(position, data) { + // 1、删除 position 位置的节点 + const result = this.removeAt(position); + + // 2、在 position 位置插入元素 + this.insert(position, data); + return result; + } +``` + +#### forwardToString() + +```js +// forwardToString() 链表数据从前往后以字符串形式返回 + forwardToString() { + let currentNode = this.head; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.next; + } + + return result; + } +``` + +#### backwardString() + +```js +// backwardString() 链表数据从后往前以字符串形式返回 + backwardString() { + let currentNode = this.tail; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.prev; + } + + return result; + } +``` + +#### 其他方法的实现 + +其他方法通过继承单向链表来实现。 + +#### 完整实现 + +```js +class DoublyLinkedList extends LinkedList { + + constructor() { + super(); + this.tail = null; + } + + // ------------ 链表的常见操作 ------------ // + // append(element) 往双向链表尾部追加一个新的元素 + // 重写 append() + append(element) { + + // 1、创建双向链表节点 + const newNode = new DoublyNode(element); + + // 2、追加元素 + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + // !!跟单向链表不同,不用通过循环找到最后一个节点 + // 巧妙之处 + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } + + this.length++; + } + + // insert(position, data) 插入元素 + // 重写 insert() + insert(position, element) { + // 1、position 越界判断 + if (position < 0 || position > this.length) return false; + + // 2、创建新的双向链表节点 + const newNode = new DoublyNode(element); + + // 3、判断多种插入情况 + if (position === 0) { // 在第 0 个位置插入 + + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// + newNode.next = this.head; + this.head.perv = newNode; + this.head = newNode; + } + + } else if (position === this.length) { // 在最后一个位置插入 + + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } else { // 在 0 ~ this.length 位置中间插入 + + let targetIndex = 0; + let currentNode = this.head; + let previousNode = null; + + // 找到要插入位置的节点 + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + // 交换节点信息 + previousNode.next = newNode; + newNode.prev = previousNode; + + newNode.next = currentNode; + currentNode.prev = newNode; + } + + this.length++; + + return true; + } + + // getData() 继承单向链表 + getData(position) { + return super.getData(position); + } + + // indexOf() 继承单向链表 + indexOf(data) { + return super.indexOf(data); + } + + // removeAt() 删除指定位置的节点 + // 重写 removeAt() + removeAt(position) { + // 1、position 越界判断 + if (position < 0 || position > this.length - 1) return null; + + // 2、根据不同情况删除元素 + let currentNode = this.head; + if (position === 0) { // 删除第一个节点的情况 + + if (this.length === 1) { // 链表内只有一个节点的情况 + this.head = null; + this.tail = null; + } else { // 链表内有多个节点的情况 + this.head = this.head.next; + this.head.prev = null; + } + + } else if (position === this.length - 1) { // 删除最后一个节点的情况 + + currentNode = this.tail; + this.tail.prev.next = null; + this.tail = this.tail.prev; + + } else { // 删除 0 ~ this.length - 1 里面节点的情况 + + let targetIndex = 0; + let previousNode = null; + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + previousNode.next = currentNode.next; + currentNode.next.perv = previousNode; + + } + + this.length--; + return currentNode.data; + } + + // update(position, data) 修改指定位置的节点 + // 重写 update() + update(position, data) { + // 1、删除 position 位置的节点 + const result = this.removeAt(position); + + // 2、在 position 位置插入元素 + this.insert(position, data); + return result; + } + + // remove(data) 删除指定 data 所在的节点(继承单向链表) + remove(data) { + return super.remove(data); + } + + // isEmpty() 判断链表是否为空 + isEmpty() { + return super.isEmpty(); + } + + // size() 获取链表的长度 + size() { + return super.size(); + } + + + // forwardToString() 链表数据从前往后以字符串形式返回 + forwardToString() { + let currentNode = this.head; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.next; + } + + return result; + } + + // backwardString() 链表数据从后往前以字符串形式返回 + backwardString() { + let currentNode = this.tail; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.prev; + } + + return result; + } +} +``` + +#### 代码测试 + +```js +const doublyLinkedList = new DoublyLinkedList(); + +// append() 测试 +doublyLinkedList.append('ZZ'); +doublyLinkedList.append('XX'); +doublyLinkedList.append('CC'); +console.log(doublyLinkedList); + +// insert() 测试 +doublyLinkedList.insert(0, '00'); +doublyLinkedList.insert(2, '22'); +console.log(doublyLinkedList); + +// getData() 测试 +console.log(doublyLinkedList.getData(1)); //--> ZZ + +// indexOf() 测试 +console.log(doublyLinkedList.indexOf('XX')); //--> 3 +console.log(doublyLinkedList); + +// removeAt() 测试 +doublyLinkedList.removeAt(0); +doublyLinkedList.removeAt(1); +console.log(doublyLinkedList); + +// update() 测试 +doublyLinkedList.update(0, '111111'); +console.log(doublyLinkedList); + +// remove() 测试 +console.log(doublyLinkedList.remove('111111')); +console.log(doublyLinkedList.remove('22222')); +console.log(doublyLinkedList); + +// forwardToString() 测试 +console.log(doublyLinkedList.forwardToString()); + +// backwardString() 测试 +console.log(doublyLinkedList.backwardString()); +``` diff --git a/src/DoublyLinkedList/doublyLinkedList.js b/src/DoublyLinkedList/doublyLinkedList.js new file mode 100644 index 0000000..81d2bd0 --- /dev/null +++ b/src/DoublyLinkedList/doublyLinkedList.js @@ -0,0 +1,202 @@ +import { LinkedList, Node } from '../LinkedList/linkedList'; + +// 双向链表结构的封装 + +// 双向链表的节点类(继承单向链表的节点类) +class DoublyNode extends Node { + constructor(element) { + super(element); + this.prev = null; + } +} + +// 双向链表类(继承单向链表类) +export class DoublyLinkedList extends LinkedList { + + constructor() { + super(); + this.tail = null; + } + + // ------------ 链表的常见操作 ------------ // + // append(element) 往双向链表尾部追加一个新的元素 + // 重写 append() + append(element) { + + // 1、创建双向链表节点 + const newNode = new DoublyNode(element); + + // 2、追加元素 + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + // !!跟单向链表不同,不用通过循环找到最后一个节点 + // 巧妙之处 + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } + + this.length++; + } + + // insert(position, data) 插入元素 + // 重写 insert() + insert(position, element) { + // 1、position 越界判断 + if (position < 0 || position > this.length) return false; + + // 2、创建新的双向链表节点 + const newNode = new DoublyNode(element); + + // 3、判断多种插入情况 + if (position === 0) { // 在第 0 个位置插入 + + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + } else { + //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// + newNode.next = this.head; + this.head.perv = newNode; + this.head = newNode; + } + + } else if (position === this.length) { // 在最后一个位置插入 + + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } else { // 在 0 ~ this.length 位置中间插入 + + let targetIndex = 0; + let currentNode = this.head; + let previousNode = null; + + // 找到要插入位置的节点 + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + // 交换节点信息 + previousNode.next = newNode; + newNode.prev = previousNode; + + newNode.next = currentNode; + currentNode.prev = newNode; + } + + this.length++; + + return true; + } + + // getData() 继承单向链表 + getData(position) { + return super.getData(position); + } + + // indexOf() 继承单向链表 + indexOf(data) { + return super.indexOf(data); + } + + // removeAt() 删除指定位置的节点 + // 重写 removeAt() + removeAt(position) { + // 1、position 越界判断 + if (position < 0 || position > this.length - 1) return null; + + // 2、根据不同情况删除元素 + let currentNode = this.head; + if (position === 0) { // 删除第一个节点的情况 + + if (this.length === 1) { // 链表内只有一个节点的情况 + this.head = null; + this.tail = null; + } else { // 链表内有多个节点的情况 + this.head = this.head.next; + this.head.prev = null; + } + + } else if (position === this.length - 1) { // 删除最后一个节点的情况 + + currentNode = this.tail; + this.tail.prev.next = null; + this.tail = this.tail.prev; + + } else { // 删除 0 ~ this.length - 1 里面节点的情况 + + let targetIndex = 0; + let previousNode = null; + while (targetIndex++ < position) { + previousNode = currentNode; + currentNode = currentNode.next; + } + + previousNode.next = currentNode.next; + currentNode.next.perv = previousNode; + + } + + this.length--; + return currentNode.data; + } + + // update(position, data) 修改指定位置的节点 + // 重写 update() + update(position, data) { + // 1、删除 position 位置的节点 + const result = this.removeAt(position); + + // 2、在 position 位置插入元素 + this.insert(position, data); + return result; + } + + // remove(data) 删除指定 data 所在的节点(继承单向链表) + remove(data) { + return super.remove(data); + } + + // isEmpty() 判断链表是否为空 + isEmpty() { + return super.isEmpty(); + } + + // size() 获取链表的长度 + size() { + return super.size(); + } + + + // forwardToString() 链表数据从前往后以字符串形式返回 + forwardToString() { + let currentNode = this.head; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.next; + } + + return result; + } + + // backwardString() 链表数据从后往前以字符串形式返回 + backwardString() { + let currentNode = this.tail; + let result = ''; + + // 遍历所有的节点,拼接为字符串,直到节点为 null + while (currentNode) { + result += currentNode.data + '--'; + currentNode = currentNode.prev; + } + + return result; + } +} diff --git a/src/DoublyLinkedList/index.js b/src/DoublyLinkedList/index.js new file mode 100644 index 0000000..9af84eb --- /dev/null +++ b/src/DoublyLinkedList/index.js @@ -0,0 +1,48 @@ +import { DoublyLinkedList } from './doublyLinkedList'; + +// ---------------- 封装的双向链表结构测试 ---------------- // +console.log('// ----- 双向链表结构测试 START -----//'); +const doublyLinkedList = new DoublyLinkedList(); + +// append() 测试 +doublyLinkedList.append('ZZ'); +doublyLinkedList.append('XX'); +doublyLinkedList.append('CC'); +console.log(doublyLinkedList); + +// insert() 测试 +doublyLinkedList.insert(0, '00'); +doublyLinkedList.insert(2, '22'); +console.log(doublyLinkedList); + +// getData() 测试 +console.log(doublyLinkedList.getData(1)); //--> ZZ + +// indexOf() 测试 +console.log(doublyLinkedList.indexOf('XX')); //--> 3 +console.log(doublyLinkedList); + +// removeAt() 测试 +doublyLinkedList.removeAt(0); +doublyLinkedList.removeAt(1); +console.log(doublyLinkedList); + +// update() 测试 +console.log('update() 测试'); +doublyLinkedList.update(0, '111111'); +console.log(doublyLinkedList); + +// remove() 测试 +console.log(doublyLinkedList.remove('111111')); +console.log(doublyLinkedList.remove('22222')); +console.log(doublyLinkedList); + +// forwardToString() 测试 +console.log(doublyLinkedList.forwardToString()); + +// backwardString() 测试 +console.log(doublyLinkedList.backwardString()); + +console.log('// ----- 双向链表结构测试 END -----//'); + + diff --git a/src/index.js b/src/index.js index 8fe103a..a390cc7 100644 --- a/src/index.js +++ b/src/index.js @@ -9,3 +9,6 @@ import './PriorityQueue' // 导入单向链表结构的封装及测试代码 import './LinkedList' + +// 导入双向链表结构的封装及测试代码 +import './DoublyLinkedList'