我们知道计算机内存管理在处理数组是时需要分配连续的内存空间的,如果一个数组长度很大则在维护数组时就会性能很低,根据牺牲空间换时间的原理,链表这种数据结构就比较合适用来处理数据量表较大的线性结构数据。vue3框架中就有大量的链表数据结构应用,学习链表对阅读源码有很大帮助。
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
认识链表结构
链表用来存储有序的元素集合,与数组不同,链表中的元素并非保存在连续的存储空间内,每个元素由一个存储元素本身的节点和一个指向下一个元素的指针构成。当要移动或删除元素时,只需要修改相应元素上的指针就可以了。对链表元素的操作要比对数组元素的操作效率更高。下面是链表数据结构的示意图:
单链表的js实现
要实现链表数据结构,关键在于保存head元素(即链表的头元素)以及每一个元素的next指针,有这两部分我们就可以很方便地遍历链表从而操作所有的元素。可以把链表想象成一条锁链,锁链中的每一个节点都是相互连接的,我们只要找到锁链的头,整条锁链就都可以找到了。
let Node = function (element) {
this.element = element;
this.next = null;
};
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
append (element) {
let node = new Node(element);
// 如果当前链表为空,则将head指向node
if (this.head === null) this.head = node;
else {
// 否则,找到链表尾部的元素,然后添加新元素
let current = this.getElementAt(this.length - 1);
current.next = node;
}
this.length++;
}
insert (position, element) {
// position不能超出边界值
if (position < 0 || position > this.length) return false;
let node = new Node(element);
if (position === 0) {
node.next = this.head;
this.head = node;
}
else {
let previous = this.getElementAt(position - 1);
node.next = previous.next;
previous.next = node;
}
this.length++;
return true;
}
removeAt (position) {
// position不能超出边界值
if (position < 0 || position >= this.length) return null;
let current = this.head;
if (position === 0) this.head = current.next;
else {
let previous = this.getElementAt(position - 1);
current = previous.next;
previous.next = current.next;
}
this.length--;
return current.element;
}
remove (element) {
let index = this.indexOf(element);
return this.removeAt(index);
}
indexOf (element) {
let current = this.head;
for (let i = 0; i < this.length; i++) {
if (current.element === element) return i;
current = current.next;
}
return -1;
}
getElementAt (position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
for (let i = 0; i < position; i++) {
current = current.next;
}
return current;
}
isEmpty () {
// return this.head === null;
return this.length === 0;
}
size () {
return this.length;
}
getHead () {
return this.head;
}
clear () {
this.head = null;
this.length = 0;
}
toString () {
let current = this.head;
let s = '';
while (current) {
let next = current.next;
next = next ? next.element : 'null';
s += `[element: ${current.element}, next: ${next}] `;
current = current.next;
}
return s;
}
}
测试:
let linkedList = new LinkedList();
linkedList.append(0);
linkedList.append(1);
linkedList.append(2);
console.log("当前链表内容:")
console.log(linkedList.toString());
linkedList.insert(0, 10);
linkedList.insert(2, 9);
linkedList.insert(5, 8);
console.log("当前链表内容:")
console.log(linkedList.toString());
console.log("删除链表第0,1,3数据:")
console.log(linkedList.removeAt(0));
console.log(linkedList.removeAt(1));
console.log(linkedList.removeAt(3));
console.log("当前链表内容:")
console.log(linkedList.toString());
console.log("查询链表值为0的节点:")
console.log(linkedList.indexOf(0));
console.log("删除链表值为1的节点:")
linkedList.remove(1);
console.log("当前链表内容:")
console.log(linkedList.toString());
console.log("清除链表内容:")
linkedList.clear();
console.log(linkedList.size());
结果:
双链表js实现
双向链表中的每一个元素拥有两个指针,一个用来指向下一个节点,一个用来指向上一个节点。在双向链表中,除了可以像单向链表一样从头部开始遍历之外,还可以从尾部进行遍历。下面是双向链表的数据结构示意图:
继承单链表的类
/**
* 双链表节点生成器
* @class DoubleLinkNode
* @constructor
* @param {String} node 初始化节点
*/
let DoubleLinkNode = function(element){
this.element = element;
this.per = null;
this.next = null;
}
/**
* 双链表集合类
* @class DoubleLinkedList
* @constructor
*/
class DoubleLinkedList extends LinkedList {
constructor(){
super();
this.tail = null;
}
/**
* 双链表追加元素方法
* @method 方法名
* @for DoubleLinkedList
* @param {DoubleLinkNode} 节点
* @return {boolean} 添加成功返回true
*/
append(e){
let node = new DoubleLinkNode(e);
//当前列表如果为空,则双链表的头和尾都指向自身;
if(this.head == null){
this.head = node;
this.tail = node;
}else{
this.tail.next = node;
node.per = this.tail;
this.tail = node;
}
this.length ++;
return true;
}
/***
* 双向链表按索引查询
* @method getElement
* @for DoubleLinkedList
* @param {int} position 索引位置
* @return DoubleLinkNode 双向链表节点
*
* */
getElementAt(position){
//判断如果position值不合法则直接返回null
if(position<0 || position>this.length){
return null;
}
//如果position值大于双链表的中间值则从尾部开始遍历链表
if(position > this.length/2){
let current = this.tail;
for(let i = this.length-1;i > position; i--){
current = current.per;
}
return current;
}else{
//直接用父类遍历方法
return super.getElementAt(position);
}
}
/***
* 双向链表插入方法
* @method insert
* @for DoubleLinkedList
* @param {int,elemnt} position 索引位置 elemrnt 插入的节点元素
* @return {boolean} 插入成功则返回true
*
* */
insert(position,element){
//判断不合法的插入
if(position < 0 || position >this.length || element instanceof DoubleLinkNode){
return false
}
//如果position位置刚好在链表尾部则直接调用append方法
if(position == this.length-1){
this.append(element);
}else{
let node = new DoubleLinkNode(element);
//插入到头部
if(position == 0){
//如果为空链表
if(this.head === null){
this.head = node;
this.tail = node;
}else{
node.next = this.head;
this.head.per = node;
this.head = node;
}
}else{
let current = this.getElementAt(position);
let previous = current.per;
current.per = node;
node.per = previous;
node.next = current;
previous.next = node;
}
}
this.length++;
return true;
}
/***
* 双向链表定点删除方法
* @method removeAt
* @for DoubleLinkedList
* @param {int} position 索引位置
* @return {DoubellinkedNode} 插入成功则返回当前被删除的节点元素
*
* */
removeAt(position){
//非法位置判断
if(position < 0 || position >= this.length){
return null;
}
let current = this.head;
let previous = null;
//删除头部
if(position === 0){
this.head = current.next;
this.head.per = null;
if(this.length === 1){
this.tail = null;
}
}else if(position === this.length - 1){
current = this.tail;
this.tail = current.per;
this.tail.next = null;
}else{
current = this.getElementAt(position);
previous = current.per;
previous.next = current.next;
current.next.per = previous;
}
this.length --;
return current.element;
}
/***
* 双向链表获取尾部元素方法
* @method gettail
* @for DoubleLinkedList
* @param 无
* @return {DoubellinkedNode} 链表尾部节点元素
*
* */
getTail(){
return this.tail;
}
/***
* 双向链表清空元素方法
* @method clear
* @for DoubleLinkedList
* @param 无
* @return {boolean} 操作成功返回true
*
* */
clear(){
super.clear();
this.tail = null;
}
/***
* 双向链表打印的方法
* @method toString
* @for DoubleLinkedList
* @param 无
* @return {string}
*
* */
toString(){
let current = this.head;
let str = "";
while(current){
let next = current.next;
let previous = current.per;
next = next ? next.element : 'null';
previous = previous ? previous.element : 'null';
str += `[element: ${current.element}, prev: ${previous}, next: ${next}] `;
current = current.next;
}
return str;
}
}
测试:
//测试
let doubleLinkedList = new DoubleLinkedList();
console.log("链表追加值 张,王,李,赵")
doubleLinkedList.append("张");
doubleLinkedList.append("王");
doubleLinkedList.append("李");
doubleLinkedList.append("赵");
console.log("当前链表内容:")
console.log(doubleLinkedList.toString());
console.log("获取链表1,2,3位置的元素值")
console.log(doubleLinkedList.getElementAt(1).element);
console.log(doubleLinkedList.getElementAt(2).element);
console.log(doubleLinkedList.getElementAt(3).element);
console.log("向链表1,3位置添加孙,刘")
doubleLinkedList.insert(1, "孙");
doubleLinkedList.insert(3, "刘");
console.log("当前链表内容:")
console.log(doubleLinkedList.toString());
console.log("删除链表0,3位置内容:")
console.log(doubleLinkedList.removeAt(0));
console.log(doubleLinkedList.removeAt(3));
console.log("当前链表内容:")
console.log(doubleLinkedList.toString());
console.log("删除链表内容:孙")
doubleLinkedList.remove("孙");
console.log("当前链表内容:")
console.log(doubleLinkedList.toString());
结果:
关于循环链表除了面试,基本不怎么常用,就不实现了啊。