要存储多个元素,数组(或列表)可能是最常用的数据结构。但这种数据结构有一个缺点:(在大多数语言中)数据的大小是固定的,从数组的起点或中间插入或移除项的成本很高。
链表存储有序的集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
相对于传统的数组,链表的一个好处是,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而想要访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到所需的元素。
举个例子,我们玩寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你沿着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表去寻找。
链表其实有许多的种类:单向链表、双向链表、单向循环链表和双向循环链表。
链表的定义
链表是一组节点组成的集合,每个节点都使用一个对象的引用来指向它的后一个节点。指向另一节点的引用讲做链。
其中,data中保存着数据,next保存着下一个链表的引用。上图中,我们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。上图,值得注意的是,我们将链表的尾元素指向了 null 节点,表示链接结束的位置。
由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。进过改造,链表就成了如下的样子:
向链表中插入一个节点的效率很高,需要修改它前面的节点(前驱),使其指向新加入的节点,而将新节点指向原来前驱节点指向的节点即可。下面我将用图片演示如何在 data2 节点 后面插入 data4 节点。
同样,从链表中删除一个节点,也很简单。只需将待删节点的前驱节点指向待删节点的,同时将待删节点指向null,那么节点就删除成功了。下面我们用图片演示如何从链表中删除 data4 节点。
链表的设计
我们设计链表包含两个类,一个是 Node 类用来表示节点,另一个事 LinkedList 类提供插入节点、删除节点等一些操作。
Node类
Node类包含连个属性: element 用来保存节点上的数据,next 用来保存指向下一个节点的链接。
LinkedList类
LinkedList类提供了对链表进行操作的方法,包括插入删除节点,查找给定的值等。
单向链表
function LinkedList(){
function Node(element){
this.element=element;
this.next=null;
}
this.head=null;
this.length=0;
}
// 向尾部加入一个节点
LinkedList.prototype.append=function(element){
let newNode=new Node(element);
if(this.length===0){
this.head=newNode;
}else{
let curr=this.head;
while(curr.next){
curr=curr.next;
}
curr.next=newNode;
}
this.length++;
}
// 读出字符串
LinkedList.prototype.toString=function(){
let curr=this.head;
let str='';
while(curr){
str+=curr.element+' ';
curr=curr.next;
}
return str;
}
// 获取某个位置的数据值
LinkedList.prototype.get=function(position){
if(position<0||position>=this.length) return null;
let curr=this.head;
let index=0;
while(index++<position){
curr=curr.next;
}
return curr.element;
}
// 是否存在值
LinkedList.prototype.indexOf=function(element){
let curr=head;
let index=0;
while(curr){
if(curr.element===element)return index;
curr=curr.next;
index++;
}
return -1;
}
// 更新值
LinkedList.prototype.update=function(position,newNode){
if(position<0||position>=this.length)return false;
let curr=this.head;
let index=0;
while(index++<position){
curr=curr.next;
}
curr.element=newNode;
return true;
}
// 查找给定值
LinkedList.prototype.find=function(element){
let curr=this.head;
while(curr.element!==element){
curr=curr.next;
}
return curr;
}
// 查找给定值前一节点
LinkedList.prototype.findPrev=function(element){
let curr=this.head;
while((curr.next!==null)&&(curr.next.element!==element)){
curr=curr.next;
}
return curr;
}
// 删除指定位置数据
LinkedList.prototype.removeAt=function(position){
if(position<0||position>=this.length)return null;
let curr=this.head;
let index=0;
if(position===0){
this.head=this.head.next;
}else{
let prev=null;
while(index++<position){
prev=curr;
curr=curr.next;
}
prev.next=curr.next;
}
this.length--;
return curr.element;
}
// 删除指定数据--方法一
LinkedList.prototype.remove=function(element){
let prev=this.findPrev(element);
if(prev.next.next!==null){
prev.next=prev.next.next;
}else{
prev.next=null;
}
this.length--;
}
// 删除指定数据--方法二
LinkedList.prototype.remove=function(element){
let pos=this.indexOf(element);
return this.removeAt(position);
}
// 在指定位置插入新元素
LinkedList.prototype.insertPos=function(position,element){
if(position>=0||position<this.length){
let curr=this.head;
let prev;
let index=0;
let newNode=new Node(element);
if(position===0){
newNode.next=curr;
this.head=newNode;
}else{
while(index++<position){
prev=curr;
curr=curr.next;
}
newNode.next=curr;
prev.next=newNode;
}
this.length++;
return true;
}else{
return false;
}
}
// 在指定节点后插入新节点
LinkedList.prototype.insert=function(newNode,item){
let curr=this.find(item);
newNode.next=curr.next;
curr.next=newNode;
}
// 是否为空链表
LinkedList.prototype.isEmpty=function(){
return this.length===0;
}
LinkedList.prototype.size=function(){
return this.length;
}
// 显示链表
LinkedList.prototype.display=function(){
let curr=this.head;
while(curr.next){
console.log(curr.next.element);
curr=curr.next;
}
}
双向链表
尽管从链表的头节点遍历链表很简单,但是反过来,从后向前遍历却不容易。我们可以通过给Node类增加一个previous属性,让其指向前驱节点的链接,这样就形成了双向链表,如下图:
此时,向链表插入一个节点就要更改节点的前驱和后继了,但是删除节点的效率提高了,不再需要寻找待删除节点的前驱节点了。
function LinkedList() {
function Node(element) {
this.element = element;
this.next = null;
this.prev = null;
}
this.head = null;
this.length = 0;
}
//查找元素
LinkedList.prototype.find = function (element) {
let curr = this.head;
while (curr.element !== element) {
curr = curr.next;
}
return curr;
}
// 查找最后一个元素
LinkedList.prototype.findLast=function(){
let curr=this.head;
while(curr.next!==null){
curr=curr.next;
}
return curr;
}
// 在指定节点后插入新节点
LinkedList.prototype.insert = function (newNode, item) {
let curr = this.find(item);
newNode.next = curr.next;
newNode.prev = curr;
curr.next = newNode;
this.length++;
}
// 删除指定数据
LinkedList.prototype.remove = function (element) {//不需要查找前驱节点,只要找出待删除节点,然后将该节点的前驱 next 属性指向待删除节点的后继,设置该节点后继 previous 属性,指向待删除节点的前驱即可
let curr = this.find(element);
curr.prev.next = curr.next;
if(curr.next)curr.next.prev = curr.next;// 删除的是最后一个节点
curr.next = null;
curr.prev = null;
this.length--;
}
// 显示链表
LinkedList.prototype.display = function () {
let curr = this.head;
while (curr!==null) {
console.log(curr.next.element);
curr = curr.next;
}
}
// 反向显示链表
LinkedList.prototype.displayReverse=function(){
let curr=this.findLast();
while(curr!==null){
console.log(curr.element);
curr=curr.prev;
}
}
循环链表
循环链表和单链表相似,节点类型都是一样,唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性执行它本身,即
head.next = head;
这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表,如下图所示:
参考:
https://www.jianshu.com/p/b4111327e843
https://blog.csdn.net/ab31ab/article/details/91472531
https://www.jianshu.com/p/f254ec665e57