链表节点
链表中的节点类型
class Node {constructor(data) {this.data = data; // 节点的数据this.prev = null; // 节点的前指针this.next = null; // 节点的下一个指针}}
单向链表

单链表的基本操作方法
class SingleList {constructor() {this.size = 0; // 单链表的长度this.head = new Node('head'); // 表头节点this.currNode = ''; // 当前节点的指向}find(item) // 在单链表中寻找item元素insert(item, element); // 向单链表中item节点后插入element元素remove(item); // 在单链表中删除一个节点append(element); // 在单链表的尾部添加元素findLast(); // 获取单链表的最后一个节点isEmpty(); // 判断单链表是否为空show(); // 显示当前节点getLength(); // 获取单链表的长度advance(n, currNode); // 从当前节点向前移动n个位置display(); // 单链表的遍历显示clear(); // 清空单链表reverseList(); // 翻转链表}
单链表中寻找item元素
find(item) {let currNode = this.head;// 判断currNode的element是否等于itemwhile (currNode && currNode.element !== item) {currNode = currNode.next;}return currNode;}
查询最后节点
当前节点的next指针不为空就一直向下遍历,直到当前节点的next为空时即是最后一个节点
findLast() {let currNode = this.head;while (currNode.next) {currNode = currNode.next;}return currNode;}
向前移动n位
设置初始节点为head。
当向前移动的位数超过单链表的长度时,总是返回单链表的最后一个节点。
advance(n, currNode = this.head) {this.currNode = currNode;while (n-- && this.currNode.next) {this.currNode = this.currNode.next;}return this.currNode;}
向尾部添加元素
在尾部添加元素时使用到了findLast()方法,findLast()方法返回单链表的最后一个元素,因此在单链表的尾部添加元素时,只要将新元素赋值给单链表的最后一个元素的next指针即可。
append(element) {// 新建要插入的节点let newNode = new Node(element);// 查处最后节点let currNode = this.findLast();// 最后节点的next指向创建的节点currNode.next = newNode;// 将链表长度+1this.size++;}
向单链表插入数据
insert(item, element) {// 先查找插入时的参考元素是否存在let itemNode = this.find(item);if (!itemNode) {// 如果不存在,则不处理return;}let newNode = new Node(element);newNode.next = itemNode.next;itemNode.next = newNode;this.size++;}
单链表中删除元素
- 要删除head节点
- 删除不存在的节点
- 删除存在的节点,先查询出item的位置currNode,将currNode.next设置为其后面一个元素即可
remove(item) {// 元素在链表中不存在「2」let itemNode = this.find(item);if (!itemNode) {return;}// 删除head节点「1」if (item === "head") {if (!this.isEmpty()) {// this.head = this.head.next;return;} else {this.head.next = null;return;}}let currNode = this.head; //删除存在的节点「3」// 当前节点的下一个节点不是要查询的元素,向下继续查询while (currNode.next.element !== item) {if (!currNode.next) {// 所有节点下一个节点都不存在,说明删除的节点不存在return;}currNode = currNode.next;}// 此时当前节点的下一个节点的element 等于要查询的item,则将currNode的next更新currNode.next = currNode.next.next;this.size--;}
翻转单链表
单链表完整代码reverseList() {const reverse = (head) => {// 如果是空或者只有一个数据时if (head === null || head.next === null) return head;// 对head的下一个元素进行递归操作let newHead = reverse(head.next);// 重新设置head元素,将head.next【第二个元素】的next赋值给headhead.next.next = head;// 将head的next设置为空,这样就进行了两个元素的翻转head.next = null;return newHead;};this.head = reverse(this.head);return this.head;}// 使用while循环,非递归reverseList1() {// 获取原链表的头部数据let oldHead = this.head;if (oldHead === null || oldHead.next === null) return oldHead;let newHead = null;while (oldHead !== null) {// 先把原来链的head下一个数据暂存起来let temp = oldHead.next;oldHead.next = newHead;newHead = oldHead;oldHead = temp;}this.head = newHead;return this.head;}
单链表测试
```javascript let myList = new SingleList(); let arr = [3, 4, 5, 6, 7, 8, 9];
for (let i = 0; i < arr.length; i++) { myList.append(arr[i]); } myList.display(); // head->3->4->5->6->7->8->9
myList.reverseList(); myList.display(); // 9->8->7->6->5->4->3->head myList.reverseList1(); myList.display(); // head->3->4->5->6->7->8->9
console.log(myList.find(4)); // Node {data: 4, prev: null, next: Node}
myList.insert(9, 9.1); myList.insert(3, 3.1); myList.display(); // head->3->3.1->4->5->6->7->8->9->9.1
myList.remove(9.1); myList.remove(3); myList.display(); // head->3.1->4->5->6->7->8->9
console.log(myList.findLast()); // Node {data: 9, prev: null, next: null}
console.log(myList.advance(4)); // Node {data: 6, prev: null, next: Node}
console.log(myList.getLength()); // 7
myList.clear(); myList.display(); // head
<a name="m9iZy"></a>### 判断单链表中是否有环类似于上图这样的,是一个有环的单链表```javascriptvar myList = new SingleList()var arr = ['A', 'B', 'C', 'D', 'E']arr.forEach(item => myList.append(item))var B = myList.find('B')var E = myList.findLast()E.next = B
用函数判断链表是否有环
使用快慢指针,如果快指针走到最后为null,说明链表没有环,如果两个指针在某个时刻相等,则说明链表有环。
function isLoop (list) {// 使用快慢指针var p = list.headvar q = list.headwhile (q) {p = p.nextif (p.next) {q = q.next.next}if (p === q) {console.log('链表是环状链表')return}}console.log('不是环状链表')}isLoop(myList)
单向循环链表
单向循环链表结构
单向循环链表继承于单向链表;
class CirSingleList extends SingleList {constructor() {super();}// 在单循环链表中寻找最后一个节点findLast() {}// 在单循环链表中寻找数据find(item) {}// 在数据为item的节点后面插入数据为element元素的节点insert(item, element) {}remove(item) {}display() {}//在尾部添加数据append(element) {}}
寻找单向循环链表的最后一个节点
使用count来标记已经寻找过的节点数目,如果count与单向循环链表长度相等时,说明找到了最后一个节点
findLast() {let currNode = this.head;let count = 0;// 判断链表长度while(count++ !== this.size){currNode = currNode.next;}return currNode;}
查询单向循环链表的一个元素
find()方法,和单链表中的find()方法不同。
由于是环状链表,如果查询不出元素,并且已经到了链表末尾,再继续查找就会无限循环。所以需要加判断查询的是不是最后一个节点。如果是最后一个节点,还未找到,则跳出循环。
find(item) {let currNode = this.head;let lastNode = this.findLast();while (currNode.element !== item) {// 判断查找的节点是不是最后一个节点,如果是最后一个节点,则停止循环。// 不添加判断 会进入无限循环if (currNode === lastNode) {currNode = null;return;}currNode = currNode.next;}return currNode;}
插入元素
- 如果单向循环链表是空,直接将新的节点插入到head节点后面,再让新的节点指向自身
- 要插入的位置处于单向循环链表中间的位置,n节点是新的要插入的节点,只需要将E节点的next指针指向C节点,再将B节点的next指针指向新的E节点就可以

- 要插入的位置处于单向循环链表表头结点后面,新插入D

- 将D节点的next指针指向头结点后面的第一个节点A。
- 第二步将头结点的next指针指向D节点。
最后将这个单向循环链表的最后一个节点C的next指针指向D节点。
insert(item, element) {let itemNode = this.find(item);let newNode = new Node(element);if (!itemNode) {// 如果item在单循环链表中不存在return;}// 插入的位置处于头结点之后,第一个节点之前if (item === "head") {if (this.size === 0) {// 当单循环链表为空时this.head.next = newNode;newNode.next = this.head.next;} else {// 当单循环链表不空时let lastNode = this.findLast();newNode.next = this.head.next;this.head.next = newNode;lastNode.next = newNode;}this.size++;return;}// 处于链表中间位置时newNode.next = itemNode.next;itemNode.next = newNode;this.size++;}
节点的删除操作
remove()方法,先用写好的find()方法找到要删除的节点,再用写好的findLast()方法找到最后一个节点。找到要删除的节点后,再次从头结点开始遍历,直到找到要删除节点的前一个节点。
- 当待删除的节点是第一个节点时,如果此时单向循环链表只有一个节点,直接将此单向循环链表置空即可。
- 当待删除的节点是第一个节点时,且此时单向循环链表不仅只有一个节点时,此时将头结点的next指针指向待删除节点的下一个节点,并将最后一个节点指向待删除节点的下一个节点。

除了前面的两种情况之外,只要将待删除节点的前一个节点next指针指向待删除节点的后一个节点即可
remove(item) {let curNode = this.find(item); // 找到待删除节点let lastNode = this.findLast(); // 找到最后一个节点let preCurNode = this.head;// 找到待删除节点的前一个节点while (preCurNode.next !== curNode) {preCurNode = preCurNode.next;}if (curNode == this.head.next) {// 如果当前节点是第一个节点//头节点的后一个节点if (this.size == 1) {//只剩最后一个节点this.head.next = null;} else {//还有其他节点this.head.next = curNode.next;lastNode.next = curNode.next;}} else {// 其他情况preCurNode.next = curNode.next;}this.size--;}
显示所有节点
遍历展示此链表,到达链表末尾,停止遍历
display() {let description = "head";let currNode = this.head;let lastNode = this.findLast();// 判断节点是否到了尾节点while (currNode !== lastNode) {currNode = currNode.next;description += `->${currNode.element}`;}console.log(description);}
在链表最后追加元素
单向循环链表的尾部添加数据。
append(element) {let lastNode = this.findLast();let newNode = new Node(element);lastNode.next = newNode;newNode.next = this.head.next;this.size++;}
单向循环链表解决的实际问题
魔术师牌的排序
魔术师手中有A、2、3……J、Q、K十三张黑桃扑克牌。在表演魔术前,魔术师已经将他们依照一定的顺序叠放好(有花色的一面朝下)。 魔术表演过程为:一开始,魔术师数1,然后把最上面的那张牌翻过来,是黑桃A;然后将其放到桌面上; 第二次,魔术师数1、2;将第一张牌放到这些牌的最以下,将第二张牌翻转过来,正好是黑桃2; 第三次,魔术师数1、2、3;将第1、2张牌依次放到这些牌的最以下,将第三张牌翻过来正好是黑桃3; 以此类推,直到将全部的牌都翻出来为止。问原来牌的顺序是怎样
// 魔术师发牌问题const { CirSingleList } = require("../CirSingleList");let magicList = new CirSingleList();function magician() {let arr = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];for (let i = 0; i < 13; i++) {magicList.append(""); // 单向循环链表的每项节点值为空}let n = 1;let toColor = undefined;while (n <= 13) {let forward = n; // 记录此次循环需要的次数while (forward != 0) {toColor = magicList.advance(1, toColor); // 前进一个节点if (!toColor.element) {forward--; // 在节点值为空的时候forward减1,就可以继续前进一步}}toColor.element = arr[n - 1];n++;}magicList.display();}magician();
约瑟夫环,避免被杀死
在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓。 于是决定了自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀。然后接着下一个重新报数,直到所有人都自杀身亡为止。 然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
// n个人围成一圈,杀死第m个人,直到剩下s个人为止// 输出存活的人的序号const { CirSingleList } = require("../CirSingleList");let myList = new CirSingleList();function killPerson(n, m, s) {for (let i = 1; i <= n; i++) {myList.append(i);}let currNode = undefined;let toKill = null;while (myList.size > s) {// 直到剩下s个节点为止toKill = myList.advance(m, currNode); // 从currNode开始,前进m个节点currNode = toKill; // 保存要删除的节点作为下一次循环的参数myList.remove(toKill.element); // 删除第m个节点}myList.display();}killPerson(41, 3, 2); // head->16->31 16和31是避免被杀死的人// killPerson(5, 4, 1); // head->1
双向链表
双向链表结构
双向链表继承单向循环链表,双向链表多了一个向前的指针。
//双向链表class DbList extends CirSingleList {constructor() {super();}// 在item后添加newElementinsert(item, newElement) {}// 从双向链表中移除item节点remove(item) {}// 反向遍历reverseDisplay() {}// 在尾部添加数据append(element) {}}
双向链表的插入方法
插入的位置在中间和结尾,可以通过当前节点的next指针是否为空来区分
- 插入节点的位置在中间时:如下图所示,第一步将n节点的next指针指向B节点,再将B节点的prev指针指向n节点。第二步将A节点的next指针指向n节点,再将n节点的prev指针指向A节点就可以

- 插入节点的位置在末尾时比较简单,只要将最后一个节点的next指针指向新的节点,再将新节点的prev指针指向之前的最后一个节点

insert(item, newElement) {let newNode = new Node(newElement);let itemNode = this.find(item);// 如果itemNode.next有值,则说明是往中间插入值if (itemNode.next) {newNode.next = itemNode.next;itemNode.next.prev = newNode;itemNode.next = newNode;newNode.prev = itemNode;} else {itemNode.next = newNode;newNode.prev = itemNode;}this.size++;}
双向链表移除元素
删除节点时,也可以分为几种情况
- 当传入的参数item为head时,约定将此链表清空
- 当节点值为item的节点存在于该链表中时,如果此时要删除的节点恰好是最后一个节点,只要直接将最后一个节点删除
- 当节点值为item的节点存在,且处于链表中间位置。B表示待删除的节点。此时只需要将A的next指针指向C节点。然后再将C的prev指针指向A节点即可。在代码的实现过程中,待删除节点B起到了“承前启后”的作用,通过B向后可以找到C,向前可以找到A

remove(item) {let curNode = this.find(item);// 删除头节点if (item === "head") {this.head.next = null;this.head.prev = null;this.size = 0;return;}if (curNode) {// 没有next节点,说明是尾节点if (!curNode.next) {curNode.prev.next = null;} else {curNode.next.prev = curNode.prev;curNode.prev.next = curNode.next;}curNode = null;this.size--;}}
向链表尾部添加元素
append(element) {let lastNode = this.findLast();let newNode = new Node(element);lastNode.next = newNode;newNode.prev = lastNode;this.size++;}
反向遍历展示
reverseDisplay() {let description = "";let currNode = this.findLast();while (currNode.element !== "head") {description += `${currNode.element}<->`;currNode = currNode.prev;}description += 'head';console.log(description);}
双向链表操作测试
let test = new DbList();let arr = [1, 2, 3, 4, 5, 6, 7];for(let i=0; i<arr.length; i++){test.append(arr[i]);}test.display(); // head->1->2->3->4->5->6->7test.insert(7, 8);test.display(); // head->1->2->3->4->5->6->7->8test.insert(`head`, 0.5);test.display(); // head->0.5->1->2->3->4->5->6->7->8test.reverseDisplay(); // 8->7->6->5->4->3->2->1->0.5->headtest.remove(0.5); // head->1->2->3->4->5->6->7->8test.display();test.remove(8);test.display(); // head->1->2->3->4->5->6->7
双向循环链表
代码结构
//双向循环链表class CirDbList extends DbList {constructor() {super();this.head.next = this.head;this.head.prev = this.head;}// 向双向循环链表中插入数据insert(element, item) {}// 从双向循环链表中删除数据remove(item) {}// 在尾部添加数据append(element) {}}
继承于双向链表的方法
// 在链表中寻找最后一个节点findLast() {let currNode = this.head;let count = 0;while(count++ !== this.size){currNode = currNode.next;}return currNode;}// 遍历链表display() {let result = 'head';let currNode = this.head;let lastNode = this.findLast();while(currNode !== lastNode) {currNode = currNode.next;result += `->${currNode.data}`;}console.log(result);}// 在链表中寻找数据find(item) {let currNode = this.head;let lastNode = this.findLast();while(currNode.data !== item) {// 判断当前节点是不是最后一个节点if(currNode === lastNode) {currNode = null;break;}currNode = currNode.next;}return currNode;}// 反向遍历reverseDisplay() {let result = '';let currNode = this.findLast();while (currNode.data !== 'head') {result += `${currNode.data}->`;currNode = currNode.prev;}result += `head`;console.log(result);}
双向循环链表的插入方法
insert(item, element) {let itemNode = this.find(item);let newNode = new Node(element);// 双向循环链表中插入是双向链表中间插入值的实现newNode.next = itemNode.next;itemNode.next.prev = newNode;itemNode.next = newNode;newNode.prev = itemNode;this.size++;}
双向循环链表的删除方法
remove(item) {// 删除头节点if (item === "head") {this.head.next = this.head;this.head.prev = this.head;this.size = 0;return;}let currNode = this.find(item);if (currNode) {currNode.next.prev = currNode.prev;currNode.prev.next = currNode.next;this.size--;}}
双向循环链表从尾部添加节点
- 首先找到当前链表的最后一个节点
- 将新节点new的next指针指向head节点
- 将head节点的prev节点指向新节点new
- 将last节点的next指针指向新节点new,新节点new的prev指针指向last节点

append(element) {let lastNode = this.findLast();let newNode = new Node(element);// lastNode.next指向head,head.prev重新被赋值为newNodelastNode.next = newNode;newNode.prev = lastNode;newNode.next = lastNode.next;lastNode.next.prev = newNode;this.size++;}
