链表是数据结构之一,数据呈线性排列
- 在链表中,数据的添加和删除都比较方便,就是访问比较消耗时间,访问时需要顺序访问,其时间为O(n)。
在删除或者添加时只需要操作链表的
指针即可,当删除的时候只需要将指定删除元素的前一个元素指针指向下一个元素即可,因为无法再访问这条数据,所以没必要特意的再删除它了;
删除或添加数据时只需要更改两个指针的指向,其耗时与n无关,如果已经到达编辑数据的位置时只需要花费O(1)的时间。
链表是非连续的,从底层数据结构看,链表不需要一整块连续的存储空间,而是通过指针将一组零散的数据单元串起来形成一个整体;
- 链表数据的添加,只需要将前一个元素的指针指向添加元素,然后将添加元素的指针指向下一个元素即可;
- 链表数据的删除,只需要将删除元素的前一个元素指针指向删除元素的后一个元素即可,不需要再理会删除元素;
- 双向链表和环形链表
- 环形链表是指将链表尾部的数据的指针指向链表头部的数据,将链表变成环形,环形链表没有头和尾的概念,当需要保存数量固定的最新数据时通常使用环形链表;
- 双向链表是一种特殊的单向链表,是将普通链表的单指针变成双指针,分别指向其前后数据,使用这种数据不仅可以顺序访问,还可以从后向前访问数据,十分方便;但是双向链表会存在两个问题,分别是指针数量的增加导致存储空间需求的增加,二是添加和删除数据的时候需要改变更多的指针指向。
单向链表的特点
- 每个节点(node)都由数据本身和一个指向后续节点的指针组成;
- 整个链表的存取必须从头指针开始,头指针指向第一个节点;
- 最后一个节点的指针指向空(null)
链表相对于数组的缺点
- 链表访问任意一个元素时都需要从头开始,无法跳过任意一个元素;
- 无法像数组一样通过下标进行访问数据,需要从头访问,直到找到对应位置;
总结
- 链表的插入、删除数据效率非常高,只需要考虑相邻节点的指针变化,不需要移动其他节点;所以时间复杂度是O(1);
- 内存方面,链表的内存消耗还是很大的,因为每个节点除了要存储数据本身,还要存储节点的地址信息(可以动态分配内存),数组需要占用整块对、连续的存储空间,会存在存储量过大,导致内存不足需要扩容得得得操作(扩容操作是会重新申请连续的存储空间,并把上一次的数组数据全部copy到新的内存空间中)
- 在进行链表的查询是,都需要从链表的头部开始找起,一步一步的遍历到目标节点,这个过程效率是非常低的,时间复杂度是O(n);
- 链表查询慢O(n),新增和删除快O(1);
- 数组查询快O(1),新增和删除慢O(n);
当需要频繁的插入删除数据时,可以用链表结构存储数据,当涉及到频繁得到查找操作,可以用数组处理,常见的都是采用组合模式,即链表和数组组合设计;
链表的常见应用
- LRU 缓存淘汰、最近消息推送等;
- 记录用户的相关操作,回退撤销功能、回退到某一版本等;
简单封装一个链表
基类链表构造函数
// 封装链表的构造函数
function LinkedList() {
function Node(el) { // 保存每个节点信息
this.element = el
this.next = null
}
// 链表中的拓展属性
this.length = 0
this.head = null
}
.append 尾部追加元素
LinkedList.prototype.append = function(ele){
//构建新节点信息 - 自身包含element信息和指针为null的next属性
const element = new Node(ele);
if(this.head === null){
//节点为空时将新节点赋值给head
this.head = element
} else {
//遍历链表-找到尾部节点
let current = this.head;
while(current.next){
current = current.next;
}
//将新节点赋值给尾部节点
current.next = element
}
// 追加后需要将链表长度加一
this.length++
}
.toString 获取链表的值
LinkedList.prototype.toString = function(){
let current = this.head;
let target = "";
while(current){
target += "," + current.element;
current = current.next
}
return target
}
.insert 插入元素
LinkedList.prototype.insert = function(position,ele){
//首先判断插入的位置是否合规
if(position<0 || position>this.length) return false;
/**
* 思路:缓存链表循环过程中的当前节点和前一个节点,当循环的index与传入的position相等的时候进行next指针替换操作,
* 将当前指针赋值给新节点对的next、上一个节点的next赋值为新节点
* newNode 新节点
* 需要建立三个临时变量
* current 当前节点
* previous 上一个节点
* index 链表循环到的位置
*/
let newNode = new Node(ele),
current = this.head,
previous = null,
index = 0;
if(position === 0){
newNode.next = current;
this.head = newNode;
} else {
while(index++ < position){
previous = current;
current = current.next;
}
newNode.next = current;
previous.next = newNode;
}
this.length++
return true
}
.removeAt移除指定位置的数据
- 常见方式
- 根据位置移除对应的数据
- 根据数据,先查到对应的位置,再移除数据
LinkedList.prototype.removeAt = function(position){
if(position < 0 || position > this.length ) return false;
let current = this.head,
previous = null,
index = 0;
if(position === 0){
// 移除head
this.head = current.next;
} else {
//按照位置移除元素
while(index++ < position){
previous = current;
current = current.next;
}
previous.next = current.next;
}
this.length++
return current.element
}
根据元素数据查找对应位置
LinkedList.prototype.indexOf = function(el){
let current = this.head,
index = 0;
while(current){
if(current.element === el) {
return index;
};
index++;
current = current.next;
}
return -1
}
根据元素删除数据
LinkedList.prototype.remove = function(el){
let index = this.indexOf(el);
return this.removeAt(index)
}
查找指定节点的上一个节点
LinkedList.prototype.preNode = function(el){
let current = this.head;
while(current && current.next && current.next.element !== el){
if(current.next) {
current = current.next;
} else {
current = null;
}
}
return current
}
打印节点
LinkedList.prototype.Array = function(){
let current = head,
result = [];
while(current){
result.push(current.element)
current = current.next
}
return result
}
其他简单API
LinkedList.prototype.isEmpty = function(){
return this.length === 0
}
LinkedList.prototype.size = function(){
return this.size
}
LinkedList.prototype.getFirst = function(){
return this.head.element
}