内容简介:上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。欢喜之余,不由得思考背后的原因,前端er离数据结构与算法太遥远了,论坛里也少有人去专门为数据结构与算法撰文,才使得这看似平平的文章收获如此。不过,这样也更加坚定了我继续学习数据结构与算法的决心(虽然只是入门级的)
写在前面
说明:JS数据结构与算法 系列文章的代码和示例均可 在此 找到
上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。欢喜之余,不由得思考背后的原因,前端er离数据结构与算法太遥远了,论坛里也少有人去专门为数据结构与算法撰文,才使得这看似平平的文章收获如此。不过,这样也更加坚定了我继续学习数据结构与算法的决心(虽然只是入门级的)
一、链表数据结构
相较于之前学习的 栈/队列 只关心 栈顶/首尾 的模式,链表更加像是数组。 链表和数组都是用于存储有序元素的集合 ,但有几点大不相同
表头
下面是单链表的基本结构
- 长度为3的单链表
- 每个元素由一个存储元素本身
data
的节点和一个指向下一个元素的引用next
(也称指针或链接)组成 - 尾节点的引用
next
指向为null
类比:寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你顺着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表寻找
二、链表的实现
链表的实现不像之前介绍的栈和队列一般依赖于数组(至少我们目前是这样实现的),它必须自己构建类并组织逻辑实现。我们先创建一个 Node
类
// 节点基类 class Node { constructor(data) { this.data = data; this.next = null; } }
一般单链表有以下几种方法:
append insert removeAt getNode print indexOf getHead getTail size clear
// 初始化链表 class LinkedList { constructor() { this._head = null; this._tail = null; this._length = 0; } // 方法... }
下面我们来实现几个重要的方法
2.1 append
方法
在链表尾部添加一个新的元素可分为两种情况:
- 原链表中无元素,添加元素后,
head
和tail
均指向新元素 - 原链表中有元素,更新
tail
元素(如下)
代码实现
// 在链表尾端添加元素 append(data) { const newNode = new Node(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true; }
2.2 print
方法
为方便验证,我们先实现 print
方法。方法虽简单,这里却涉及到链表遍历精髓
// 打印链表 print() { let ret = []; // 遍历需从链表头部开始 let currNode = this._head; // 单链表最终指向null,作为结束标志 while (currNode) { ret.push(currNode.data); // 轮询至下一节点 currNode = currNode.next; } console.log(ret.join(' --> ')); } // 验证 const link = new LinkedList(); link.append(1); link.append(2); link.append(3); link.print(); // 1 --> 2 --> 3
2.3 getNode
方法
获取指定索引位置的节点,依次遍历链表,直到指定位置返回
// 获取指定位置元素 getNode(index) { let currNode = this._head; let currIndex = 0; while (currIndex < index) { currIndex += 1; currNode = currNode.next; } return currNode; } // 验证【衔接上面的链表实例】 console.log(link.getNode(0)); // Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } } console.log(link.getNode(3)); // null
2.4 insert
方法
插入元素,需要考虑三种情况
append _head
代码实现
// 在链表指定位置插入元素 insert(index, data) { // 不满足条件的索引值 if (index < 0 || index > this._length) return false; // 插入尾部 if (index === this._length) return this.append(data); const insertNode = new Node(data); if (index === 0) { // 插入首部 insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置节点 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑节点连接 prevTargetNode.next = insertNode; insertNode.next = targetNode; } this._length += 1; return true; } // 验证 link.insert(0, 0); link.insert(4, 4); link.insert(5, 5); link.print(); // 0 --> 1 --> 2 --> 3 --> 4 --> 5
2.5 removeAt
方法
在指定位置删除元素同添加元素类似
_head _tail
代码实现
// 在链表指定位置移除元素 removeAt(index) { if (index < 0 || index >= this._length) return false; if (index === 0) { this._head = this._head.next; } else { const prevNode = this.getNode(index - 1); const delNode = prevNode.next; const nextNode = delNode.next; // 若移除为最后一个元素 if (!nextNode) this._tail = prevNode; prevNode.next = nextNode; } this._length -= 1; return true; } // 验证 link.removeAt(3); link.print(); // 0 --> 1 --> 2 --> 4 --> 5
2.6 其它方法
完整的链表代码,可点此获取
// 判断数据是否存在于链表内,存在返回index,否则返回-1 indexOf(data) { let currNode = this._head; let index = 0; while (currNode) { if (currNode.data === data) return index; index += 1; currNode = currNode.next; } return -1; } getHead() { return this._head; } getTail() { return this._tail; } size() { return this._length; } isEmpty() { return !this._length; } clear() { this._head = null; this._tail = null; this._length = 0; }
三、链表的应用
3.1 基于链表实现的 Stack
和 Queue
基于链表实现栈
class Stack { constructor() { this._link = new LinkedList(); } push(item) { this._link.append(item); } pop() { const tailIndex = this._link - 1; return this._link.removeAt(tailIndex); } peek() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() } }
基于链表实现队列
class Queue { constructor() { this._link = new LinkedList(); } enqueue(item) { this._link.append(item); } dequeue() { return this._link.removeAt(0); } head() { if (this._link.size() === 0) return undefined; return this._link.getHead().data; } tail() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() } }
3.2 链表翻转【面试常考】
(1)迭代法
迭代法的核心就是 currNode.next = prevNode
,然后从头部一次向后轮询
代码实现
reverse() { if (!this._head) return false; let prevNode = null; let currNode = this._head; while (currNode) { // 记录下一节点并重塑连接 const nextNode = currNode.next; currNode.next = prevNode; // 轮询至下一节点 prevNode = currNode; currNode = nextNode; } // 交换首尾 let temp = this._tail; this._tail = this._head; this._head = temp; return true; }
(2)递归法
递归的本质就是执行到当前位置时,自己并不去解决,而是等下一阶段执行。直到递归终止条件,然后再依次向上执行
function _reverseByRecusive(node) { if (!node) return null; if (!node.next) return node; // 递归终止条件 var reversedHead = _reverseByRecusive(node.next); node.next.next = node; node.next = null; return reversedHead; }; _reverseByRecusive(this._head);
3.3 链表逆向输出
利用递归,反向输出
function _reversePrint(node){ if(!node) return;// 递归终止条件 _reversePrint(node.next); console.log(node.data); };
四、双向链表和循环链表
4.1 双向链表
双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素,如下图
正是因为这种变化,使得链表相邻节点之间不仅只有单向关系,可以通过 prev
来访问当前节点的上一节点。相应的,双向循环链表的基类也有变化
class Node { constructor(data) { this.data = data; this.next = null; this.prev = null; } }
继承单向链表后,最终的双向循环链表 DoublyLinkedList
如下【 prev
对应的更改为 NEW
】
class DoublyLinkedList extends LinkedList { constructor() { super(); } append(data) { const newNode = new DoublyNode(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { newNode.prev = this._tail; // NEW this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true; } insert(index, data) { if (index < 0 || index > this._length) return false; if (index === this._length) return this.append(data); const insertNode = new DoublyNode(data); if (index === 0) { insertNode.prev = null; // NEW this._head.prev = insertNode; // NEW insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置节点 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑节点连接 prevTargetNode.next = insertNode; insertNode.next = targetNode; insertNode.prev = prevTargetNode; // NEW targetNode.prev = insertNode; // NEW } this._length += 1; return true; } removeAt(index) { if (index < 0 || index > this._length) return false; let delNode; if (index === 0) { delNode = this._head; this._head = this._head.next; this._head.prev = null; // NEW } else { const prevNode = this.getNode(index - 1); delNode = prevNode.next; const nextNode = delNode.next; // 若移除为最后一个元素 if (!nextNode) { this._tail = prevNode; this._tail.next = null; // NEW } else { prevNode.next = nextNode; // NEW nextNode.prev = prevNode; // NEW } } this._length -= 1; return delNode.data; } }
4.2 循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链 表之间唯一的区别在于,单向循环链表最后一个节点指向下一个节点的指针 tail.next
不是引用 null
, 而是指向第一个节点 head
,双向循环链表的第一个节点指向上一节点的指针 head.prev
不是引用 null
,而是指向最后一个节点 tail
总结
链表的实现较于栈和队列的实现复杂许多,同样的,链表的功能更加强大
我们可以通过链表实现栈和队列,同样也可以通过链表来实现栈和队列的问题
链表更像是数组一样的基础数据结构,同时也避免了数组操作中删除或插入元素对其他元素的影响
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 算法与数据结构之递归算法
- Python 数据结构与算法 —— 初识算法
- js实现数据结构及算法之排序算法
- 数据结构和算法面试题系列—递归算法总结
- 数据结构和算法面试题系列—随机算法总结
- 数据结构与算法——常用排序算法及其Java实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人工智能产品经理——AI时代PM修炼手册
张竞宇 / 电子工业出版社 / 2018-6 / 59
随着人工智能热潮的兴起,企业对人工智能领域产品经理的人才需求也开始井喷,人工智能产品经理成为顺应时代潮流的重要人力资源。实际上,人工智能确实给现有的产品和服务带来了全方位的升级,这也给产品经理从业人员提出了更高的要求,是关注人工智能产品的产品经理们面临的一次关键转型考验。 《人工智能产品经理——AI时代PM修炼手册》从知识体系、能力模型、沟通技巧等方面帮助大家系统地梳理了人工智能产品经理所必......一起来看看 《人工智能产品经理——AI时代PM修炼手册》 这本书的介绍吧!