链表
- 基于栈和队列解构,我们可以发现,这两种结构虽然读取数据时是非常优秀的。但是它在数组的起点或中间进行插入或删除数据时,成本很高。虽然在js中,我们(开发者)不像java那样,需要手动对数组进行扩容操作,但是这种操作在js底层还是要做的。那么,对于这种新增或删除的操作,有没有另一种高效率的存储结构呢?那肯定是有的,比如,本次分享的链表这种数据结构。链表的结构,有很多种,比如:基础链表(单向链表)、双向链表、循环列表、排序链表等。故名思意,不同结构的链表,可以更好的实现不同的功能。下图,是针对单向链表的示意图:
- 下面,我使用js依次将其进行实现(单向链表、双向、循环)。
单向链表
代码实现如下:
// 链表
function LikedList(cal = function (a,b) { return a == b }) {
this.count = 0;
this.head = null;
this.equalsFn = cal;
// 向列表末尾追加元素
LikedList.prototype.push = function (data) {
let node = new Node(data);
if (!this.head) {
this.head = node;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
this.count++;
}
// 通过索引插入元素
LikedList.prototype.insert = function (data, index) {
if (index >= 0 && index <= this.count) {
let node = new Node(data);
if (index == 0) {
node.next = this.head;
this.head = node;
} else {
let previous = this.getElementFromIndex(index - 1);
// 减少循环的次数
// let current = this.getElementFromIndex(index);
let current = previous.next;
previous.next = node;
node.next = current;
}
this.count++
}
return false;
}
// 通过索引移除元素
LikedList.prototype.removeFromIndex = function (index) {
if (this.isEmpty()) return undefined;
if (index >= 0 && index < this.count) {
let current = this.head;
if (index == 0) {
this.head = current.next;
} else {
let previous = this.getElementFromIndex(index - 1);
current = previous.next;
previous.next = current.next;
}
this.count--;
return current.data;
}
return undefined;
}
// 通过元素指定key(id),删除. 此时cal回调函数必填
LikedList.prototype.remove = function (ele) {
if (this.isEmpty() || !ele ) return undefined;
let current = this.head;
let currentIndex = this.indexOf(ele);
if(currentIndex < 0) return undefined;
while(current){
if(this.equalsFn(current.data, ele)){
let previous = this.getElementFromIndex(currentIndex - 1);
previous.next = current.next;
this.count--;
return current.data;
}else{
current = current.next
}
}
}
// 通过元素,返回对应的索引
LikedList.prototype.indexOf = function (ele) {
if (this.isEmpty()) return -1;
if (!ele) return -1;
let current = this.head;
for(let i=0;i<this.count && current;i++){
if(this.equalsFn(ele, current.data)){
return i
}else{
current = current.next;
}
}
return -1
}
// 通过指定的索引获取元素
LikedList.prototype.getElementFromIndex = function (index) {
if (index >= 0 && index <= this.count) {
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
}
return undefined;
}
// 判断是否尾空链表
LikedList.prototype.isEmpty = function () {
return !this.count;
}
// 获取链表的大小
LikedList.prototype.size = function () {
return this.count;
}
// 获取链表的第一个元素
LikedList.prototype.getHead = function () {
return this.head;
}
// 清空链表
LikedList.prototype.clear = function () {
this.head = null;
this.count = 0;
}
// 链表toString方法
LikedList.prototype.toString = function () {
if (!this.head) return "";
let str = "";
let current = this.head;
while (current) {
let objStr = JSON.stringify(current.data);
str += objStr += ",";
current = current.next;
}
return str.slice(0, str.length - 1);
}
function Node(data = null) {
this.data = data;
this.next = null;
}
}
let list = new LikedList(function (a, b) {
return a.id == b.id
});
list.push({ id: 1 }) // 0
list.push({ id: 2 }) // 2
list.insert({ id: 3 }, 1) // 1
let res = list.remove({ id: 3 });
console.log(res) // Node,
console.log(list)
对于LikedList类,实现的功能:*push、insert、removeFromIndex、remove、indexOf、getElementFromIndex、*isEmpty、size、getHead、clear、toString。另外,在创建单向链表实例是,可以传入一个回调(cal,虽然有默认值,但仅用于简单之判断),用于进行复杂数据的判断,因为在进行元素删除(remove)、索引查找(indexOf)时,如果data
是简单值,貌似并没有太大问题(cal = function(a,b)=>a==b),但如果data
是复杂值(Object),那么,在创建单向链表实例,就需要传入自定义回调,用于唯一值的判断(这里是id)。另外在LikedList类中,还封装了一个Node的类,它用于创建每一个传入的数据,并保存next(指向)。
双向链表
写完上面的单向
链表,再写双向链表,就容易了一些,所谓的双向链表,就在在单向链表的基础上,把每个node的节点上,添加一个pre属性,用来记录与它相关的上一个节点的数据,代码实现如下:
// 双向链表
function DoubleLikedList(cal = (a, b) => a == b) {
this.head = null;
this.tail = null;
this.isEqual = cal;
this.count = 0;
// 在索引index之前,插入数据
// index != undefined时,从指定索引处追加元素
// index == undefined时,默认从链表尾追加元素
DoubleLikedList.prototype.insert = function (data, index) {
if (index == undefined) {
this.insert(data, this.count);
}
if (index >= 0 && index <= this.count) {
let node = new Node(data);
let current = this.head;
if (index == 0) {
// 表头插入
if (this.count == 0) {
this.head = this.tail = node
} else {
this.head = node;
node.next = current;
current.pre = node;
}
} else if (index == this.count) {
// 表尾插入
node.pre = current = this.tail;
current.next = this.tail = node;
} else {
// 中间任一位置插入
let previousNode = this.getElementFromIndex(index - 1);
current = previousNode.next;
previousNode.next = node;
node.pre = previousNode;
node.next = current;
current.pre = node;
}
this.count++;
return true;
}
return false;
}
// 通过索引删除
DoubleLikedList.prototype.removeAt = function (index) {
if (index >= 0 && index < this.count) {
let current = this.head;
if (index == 0) {
this.head = current.next;
if (this.count == 1) {
this.tail = null
} else {
this.head.pre = null;
}
} else if (index == this.count - 1) {
current = this.tail;
this.tail = current.pre;
this.tail.next = null;
} else {
current = this.getElementFromIndex(index);
let previous = current.pre;
previous.next = current.next;
current.next.pre = previous;
}
this.count--;
return current.data;
}
return undefined
}
// 获取元素->索引
DoubleLikedList.prototype.indexOf = function (data) {
if (this.isEmpty() || !data) return -1;
// 这里不好做优化,
// 如果做优化的话,只能先从中间前端,如果没找到,
// 再从中间往后端找,最后返回值。
// 照着这个思路,此方法优化节约的时间,所在范围只能在
// (1/4, 1/2) * this.count 处,才有效果
// 类似于正常逻辑中的 (0, 1/2) * this.count 处的效果
// 所以,`优化逻辑的代码`并不是真正意义上的优化,
// `优化逻辑的代码`,只是我的一个思考,暂放于此
// 正常逻辑
let cnt = 0
let current = this.head;
let index = 0;
while (current) {
cnt++;
if (this.isEqual(data, current.data)) {
console.log(cnt)
return index;
}
index++;
current = current.next;
}
// `优化逻辑`
// let midIndex = Math.floor(this.count / 2);
// let current = this.getElementFromIndex(midIndex);
// while (current && midIndex > 0) {
// cnt++;
// if (this.isEqual(data, current.data)) {
// console.log(cnt)
// return midIndex;
// }
// midIndex--;
// current = current.pre;
// }
// midIndex = Math.floor(this.count / 2);
// current = this.getElementFromIndex(midIndex);
// while (current && midIndex < this.count) {
// cnt++;
// if (this.isEqual(data, current.data)) {
// console.log(cnt)
// return midIndex;
// }
// midIndex++;
// current = current.next;
// }
return -1;
}
// 获取索引->元素
DoubleLikedList.prototype.getElementFromIndex = function (index) {
if (index >= 0 && index <= this.count) {
// 方案一:从头一次索引查找,性能普通
// let current = this.head;
// for (let i = 0; i < index && current != null; i++) {
// cnt++;
// current = current.next;
// }
// 方案二:判断index是否为count的一半,性能提升(约提升一半,在大数据量下较为明显)
// 为何此处举节约一半的性能,而不是1/3、1/4或者其它的呢
// 因为默认情况下,双向链表可以立即取到的值,只有 this.head 和 this.tail
// 所以在循环过程中,只能(最容易)基于这两个值来进行取值(current)
let halfCount = Math.floor(this.count / 2);
let current;
if (index >= halfCount) {
current = this.tail;
for (let i = this.count - 1; i > index && current != null; i--) {
current = current.pre
}
} else {
current = this.head;
for (let i = 0; i < index && current != null; i++) {
current = current.next;
}
}
return current;
}
return null;
}
// 为空判断
DoubleLikedList.prototype.isEmpty = function () {
return !this.count
}
function Node(data) {
this.data = data;
this.next = null;
this.pre = null
}
}
let obj = new DoubleLikedList((a, b) => {
return a.id == b.id;
});
obj.insert({ id: 1 });
obj.insert({ id: 2 });
obj.insert({ id: 3 });
obj.insert({ id: 4 });
obj.insert({ id: 5 });
obj.insert({ id: 6 });
obj.insert({ id: 7 });
obj.insert({ id: 8 });
obj.insert({ id: 9 });
obj.insert({ id: 10 });
let index = obj.indexOf({ id: 5 })
console.log(obj)
以上就是双向链表的实现,代码可独立运行,没有做继承依赖。在双向链表的索引查找元素方法中,做了一个循环判断的优化,用来在略大数据量下的优化。具体功能说明及实现,可见注释。
循环链表
熟悉了链表的基本结构之后,那么到循环链表这,就不攻自破了,循环链表,指的就是,在单向链表或双向链表的head或tail的指向不为null,比如基于双向链表的循环链表,它的head的pre,指向了tail;而tail.next指向了head,下面用代码实现基于双向链表的循环链表:
// 循环链表(基于双向链表的循环)
function CircularLikedList(cal = (a, b) => a == b) {
this.count = 0;
this.head = null;
this.tail = null;
this.isEqual = cal;
// 插入元素
CircularLikedList.prototype.insert = function (data, index) {
if (index == undefined) {
this.insert(data, this.count);
}
if (index >= 0 && index <= this.count) {
let current = this.head;
let node = new Node(data);
if (index == 0) {
// 新头
this.head = node;
if (this.count == index) {
// 新头 && 新尾
this.tail = node;
node.pre = node.next = node;
} else {
// 换新头
node.next = current;
node.pre = this.tail;
current.pre = node;
this.tail.next = node;
}
} else if (index == this.count) {
// 新尾(不存在新头 && 新尾,此种情况在上面已经判断),this.count > 0
current = this.tail;
this.head.pre = node;
this.tail = node;
node.pre = current;
node.next = this.head;
current.next = node
} else {
// 不存在新头/新尾,且this.count > 0
let previous = this.getElementFromIndex(index - 1);
current = previous.next;
previous.next = node;
node.pre = previous;
node.next = current;
current.pre = node;
}
this.count++;
return true
}
return false;
}
// 索引->元素
CircularLikedList.prototype.getElementFromIndex = function (index) {
if (index == undefined) return -1;
if (index >= 0 && index < this.count) {
let midIndex = Math.floor(this.count / 2);
let current, curIndex;
if (index > midIndex) {
// 从后面索引
current = this.tail;
curIndex = this.count;
while (current) {
if (--curIndex == index) {
return current
}
current = current.pre;
}
return undefined
} else {
// 从前面索引
current = this.head;
curIndex = 0;
while (current) {
if (curIndex++ == index) {
return current
}
current = current.next;
}
return undefined
}
}
return undefined
}
// 删除
CircularLikedList.prototype.remove = function (data) {
if (data == undefined) return undefined;
if (this.isEmpty()) return undefined;
let current = this.tail;
while (current) {
if (this.isEqual(data, current.data)) {
current.pre.next = current.next;
current.next.pre = current.pre
this.count--;
return data
}
current = current.next;
}
return undefined;
}
// 为空判断
CircularLikedList.prototype.isEmpty = function () {
return !this.count
}
function Node(data) {
this.data = data;
this.pre = null;
this.next = null;
}
}
let circularList = new CircularLikedList((a, b) => a.id == b.id);
circularList.insert({ id: 1 })
circularList.insert({ id: 2 })
circularList.insert({ id: 3 })
circularList.insert({ id: 4 }, 1) // 1 4 2 3
let rm = circularList.remove({id:4})
console.log(rm)
console.log(circularList)
对于循环链表的实现,这里没有特殊的感悟,主要还是看具体的使用场景,个人任务,循环链表使用的场景并不多。
以上便是,基于javaScript来实现的链表接口(‘单向’、双向、循环)。另外还有一个链表结构,有序链表(可以模拟栈结构(tail),也可以模拟队列(head)),有序链表模拟栈结构或队列,有个优势,就是,在大数据量下,数组会有些乏力(需要开辟连续内存空间)。