链表节点
链表中的节点类型
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是否等于item
while (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;
// 将链表长度+1
this.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赋值给head
head.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>
### 判断单链表中是否有环
![](https://cdn.nlark.com/yuque/0/2022/jpeg/737887/1650972636098-d8a29b1e-44ac-419b-af87-355ceb10a4d0.jpeg)
类似于上图这样的,是一个有环的单链表
```javascript
var 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.head
var q = list.head
while (q) {
p = p.next
if (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后添加newElement
insert(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->7
test.insert(7, 8);
test.display(); // head->1->2->3->4->5->6->7->8
test.insert(`head`, 0.5);
test.display(); // head->0.5->1->2->3->4->5->6->7->8
test.reverseDisplay(); // 8->7->6->5->4->3->2->1->0.5->head
test.remove(0.5); // head->1->2->3->4->5->6->7->8
test.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重新被赋值为newNode
lastNode.next = newNode;
newNode.prev = lastNode;
newNode.next = lastNode.next;
lastNode.next.prev = newNode;
this.size++;
}