数组作为js常用的数据结构,存取元素都非常的方便,但是其内部的实现原理跟其他计算机语言对数组的实现都是一样的。
由于数组在内存中的存储是连续的,所以当在数组的开头或者中间插入元素时,内部都需要去移动其他元素的位置,这个移动元素的成本很高。
链表也是存储有序的元素集合,但不同于数组,链表中的元素在内存中的存放并不是连续的,每个元素有一个存储元素自身的节点和一个指向下一个元素引用(也称为指针)的节点组成。
优点:相对于传统的数组,向链表中插入和移除元素,都不要移动其他任何元素。
缺点:要想访问链表中间的一个元素,需要从 (表头)开始迭代列表直到找到 所需的元素。
链表的基本骨架:
function linkedList() {
var Node = function(ele) {
this.ele = ele;
this.next = null;
}
var length = 0;
var head = null; // 用来存储第一个元素的引用
// 对外暴露的方法
this.append = (ele) => {}; //添加元素
this.insert = (pos, ele) => {}; // 向固定位置插入元素
this.removeAt = (pos) => {}; // 移除固定位置的元素
this.remove = () => {}; // 移除结尾元素
this.indexOf = (ele) => {}; // 返回元素所在位置
this.isEmpty = () => {}; // 链表是否为空
this.size = () => {}; // 返回链表的长度
this.toString = () => {}; // 返回
this.print = () => {}; // 打印链表数据
}
链表的具体实现
向链表尾部追加元素
向LinkedList对象尾部添加一个元素时,可能有两种场景:
1. 列表为空,添加的是第一个元素
2. 或者列表不为空,向其追加元素
this.append = function(element){
var node = new Node(element), //{1}
current; //{2}
if (head === null){ // 列表中的第一个节点 //{3}
head = node;
} else {
current = head; //{4}
// 循环列表直到找到链表的最后一项,
while(current.next){
current = current.next;
}
// 找到链表的最后一项,将其next赋为node,建立连接
current.next = node; //{5}
}
length++; // 更新链表的长度 //{6}
};
从链表中移除元素
从链表中移除某个元素,也可能有两种场景:
1. 移除第一个元素
2. 移除第一个元素以外的其他元素
// 移除特定位置的元素
this.removeAt = (pos) => {
// 检查边界条件
if (pos > -1 && pos < length) {
let current = head; // head指向第一个元素
let previous = null; // 存储前一个节点
let index = 0; // 计数
// 移除第一项
if (pos === 0) {
head = current.next; // 第二个元素
} else {
while (index++ < pos) {
previous = current;
current = current.next;
}
previous.next = current.next; // 将目标删除元素的前一个元素的next指向目标删除元素的后一个元素
}
return current.ele; // 返回要删除节点的值
} else {
return null; // 输入不合法
}
}
要从列表中移除当前元素,要做的就是将previous.next和current.next连接起来,当前元素就会被丢弃在计算机内存中,等待着被垃圾回收机制回收
在任意位置插入元素
同样考虑两种情况插入元素
1. 链表首部插入节点
2. 链表手部除外的其他位置插入节点
this.insert = (pos, ele) => {
// 首先检测边界值
if (pos >=0 && pos <= length) {
const node = new Node(ele); // 创建节点
let previous = null; // 存储前一个节点
let current = head; // 目标位置节点
let index = 0;
// 链表的起点插入一个值
if (pos === 0) {
node.next = current;
head = node;
} else {
while (index++ < pos) {
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
length++; // 更新链表的长度
return true; // 插入成功
} else {
return false;
}
}
其他方法的实现
this.toString = () => {
let current = head;
let rets = '';
let index = 0;
while (current) {
rets += current.ele;
current = current.next;
}
return rets;
}
this.indexOf = (ele) => {
let current = head;
let index = -1;
while (current) {
if (current.ele === ele) {
return index++;
}
index++;
current = current.next;
}
return index;
}
this.remove = (ele) => {
var pos = this.indexOf(ele); // 返回元素所在的位置
this.removeAt(pos);
}
this.isEmpty = () => {
return length === 0;
}
this.size = () => {
return length;
}
双向链表和普通链表的区别在于,在普通链表中, 一个节点只有指向下一个节点的连接,而在双向链表中,链接是双向的:一个指向下一个元素, 另一个指向前一个元素
双向链表的实现
function DoublyLinkedList() {
var Node = function(ele) {
this.ele = ele;
this.prev = null; // 新增的
this.next = null;
}
var length = 0;
var head = null; // 用来存储第一个元素的引用
var tail = null; // 用来存储最后一个元素的引用 新增的
// 对外暴露的方法
this.append = (ele) => {}; //添加元素
this.insert = (pos, ele) => {}; // 向固定位置插入元素
this.removeAt = (pos) => {}; // 移除固定位置的元素
this.remove = () => {}; // 移除结尾元素
this.indexOf = (ele) => {}; // 返回元素所在位置
this.isEmpty = () => {}; // 链表是否为空
this.size = () => {}; // 返回链表的长度
this.toString = () => {}; // 返回
this.print = () => {}; // 打印链表数据
}
双向链表提供了两种迭代列表的方法:从头到尾,或者反过来。我们也可以访问一个特定节 的下一个或前一个元素。在单向链表中,如果迭代列表时错过了要找的元素,就需要回到列表起点,重新开始迭代。这是双向链表的一个优点。
插入方法实现
this.insert = (pos, ele) {
// 检查边界条件
if (pos > -1 && pos <= length) {
let node = new Node(ele);
let current = head; // 链表第一个节点
let previous = null;
let index = 0;
// 链表首部新增
if (pos === 0) {
if(!head) {
head = node;
tail = node;
} else {
node.next = current;
current.prev = node;
head = node;
}
} else if(pos === length) { // 最后一项
current = tail;
current.next = node;
node.pre = current;
tail = node; // 更新最后一项
} else {
// 中间位置
while (index++ < pos) {
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
current.prev = node; // 新增
node.prev = previous; // 新增
}
length++; // 更新链表长度
return true;
} else {
return false;
}
}
从任意位置移除元素算法实现
需要处理三种场景:
1. 删除链表第一个元素
2. 删除链表最后一个元素
3. 删除链表中间某个元素
this.removeAt = (pos) => {
// 检查边界值
if (pos > -1 && pos <= length) {
let current = head;
let previous = null;
let index = 0;
// 移除第一个元素
if (pos === 0) {
head = current.next;
if(!head){ // 只有一个元素
tail = null;
} else {
head.prev = null;
}
} else if (pos === length) {
// 移除最后一个元素
current = tail;
tail = current.prev;
tail.next = null;
} else {
// 中间位置
while (index++) {
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
length--; // 更新链表长度
return true;
} else {
return false;
}
}
循环链表可以向链表一样单向引用,也可以像双向链表一样有双向引用。循环链表于链表的唯一区别是:循环链表的最后一个元素指向下一个元素的指针不是null,而是指向第一个元素(head);
总结: 链表相比数组最大的优点是,无需移动链表中的元素,就可以轻松实现元素的添加和删除。因此,当需要添加和删除很多元素时,最好的选择是链表而非数组