这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
介绍
当我们要存储多个元素,数组可能是最常用的数据结构。但数组有个缺点就是,一旦进行插入或移除某项,那么后面的元素的位置全部都要进行变动,成本非常高,这时候链表这种数据结构将发挥了优势,因为他用指针连接其所有元素,当某项发生变化时,那么他只要改变一下指向就可以了。
本期我们会带大家讲述如何用普通对象手写一个链表结构,案例打印就用数码宝贝进化去讲解吧。
概念
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
正文
1.Node类
export class Node {
constructor(item) {
this.item = item;
this.next = null;
}
}
我们做链表之前必须先写一个节点类,他会保存当前值与下一个节点的指向。
2.LinkedList类
export class LinkedList {
constructor() {
this.count = 0;
this.head = null;
return this;
}
size() {
return this.count;
}
toString() {}
push(item) {}
indexOf(item) {}
removeAt(index) {}
remove(item) {}
insert(item, index) {}
}
我们使用链表会记录头head和当前链表长度count,至于后面的几个方法为:
- size:返回当前链表的节点个数。
- toString:返回当前链表所有值的字符串。
- push:推送一个节点加入链表,最后返回当前链表长度。
- indexOf:返回某个元素是否存在于链表中,返回其下标,如果没有返回-1。
- removeAt:根据传入下标,移除某项的节点,并返回true则为删除成功,false则删除为失败。
- remove:根据传入元素,移除某项的节点,并返回true则为删除成功,false则删除为失败。
- insert:插入一个节点,返回当前链表长度。
3.push&toString方法
push(item) {
let node = new Node(item);
if (!this.head) this.head = node;
else {
let target = this.head;
while (target.next) {
target = target.next;
}
target.next = node;
}
return ++this.count;
}
toString() {
let target = this.head;
let str = this.head.item || "";
while (target.next) {
const { next } = target;
target = next;
str += "," + (typeof next.item == "object" ? JSON.stringify(next.item) : next.item);
}
return str;
}
push方法类似于数组的push,都是在最后一项注入节点,但是链表我们就要去查他最后一项,然后去指向这个新节点。而toString也是要遍历找到最后一项,但只为了拼接字符串。
我们可以看到完成了一项数码宝贝的进化路线,并且用toString打印出了结果。
4.indexOf方法
indexOf(item) {
let target = this.head
for (let i = 0, len = this.count; i < len; ++i) {
if (target.item == item) return i
else target = target.next;
}
return -1;
}
indexOf类似于我们字符串和数组的indexOf,他会循环遍历整个链表,如果发现匹配项立即终端返回其位置,而如果没有找到则返回-1。
我们可以看到这条进化路线,存在亚古兽再第二项上,而没有加鲁鲁兽。
3.removeAt&remove方法
removeAt(index) {
if (index < 0 || index >= this.count) return false;
if (index == 0) {
this.head = this.head.next;
}else{
let target = this.head;
for (let i = 1; i < index; i++) {
target = target.next;
}
target.next = target.next.next;
}
this.count--;
return true;
}
remove(item) {
let index = this.indexOf(item);
if (index !== -1) return this.removeAt(index);
return false;
}
我们先说removeAt,要做个做边界判断,如果超出长度或者小于0则返回false。然后我们还要判断如果是第0项,我们就直接替换head头,因为我们这是单项普通链表,head没有指向他的节点。其他的就要遍历查找他要中断的位置,让后将他下个指向直接指向到他下一个节点的指向,相当于跳过了他。而remove方法直接借用indeOf查到下标转给removeAt方法去处理了,自己本身不做遍历运算。
通过测试,我们发现移除加鲁鲁兽是不行的因为进化路线没这种类型,而我们移除暴龙兽就成功了,当然再用linkedList.removeAt(3)移除了第三项,也成功了,这条进化链只能到亚古兽这里了。
4.insert方法
insert(item, index) {
if (index < 0 || index >= this.count) return false;
let node = new Node(item);
let target = this.head;
for (let i = 1; i < index; i++) {
target = target.next;
}
node.next = target.next;
target.next = node;
return ++this.count
}
insert为在某位置插入某项。跟removeAt一样,都要做边界判断,遍历找到要插入的位置,对这个位置的指向节点进行修改替换成当前node,并且node的指向节点要指向之前之前这个位置的指向节点。
如图可以看到添加了两项,现在问题来了怎么让暴龙兽进化成机械邪龙兽呢,我们用linkedList.insert("丧失暴龙兽",4)在第四项插入了丧失暴龙兽,这样一来他的进化线路就完整了。
结语
本次我们学习实现了链表结构。本期也是基础课,本身也是重于理解思想而不重于案例本身的实现,不要过于纠结,案例是数码宝贝的进化,但其实他本身并不是一个普通链表,他是一个双向链表或者说是个多分支的树,毕竟还有退化么。。我们可以想想火车或者地铁车厢,一节连着一节去理解吧。链表本身变种又很多,在某项情况会极大程度弥补数组的不足,我们也是择情使用吧。