JS中的算法与数据结构——链表(Linked-list)

要存储多个元素,数组(或列表)可能是最常用的数据结构。但这种数据结构有一个缺点:(在大多数语言中)数据的大小是固定的,从数组的起点或中间插入或移除项的成本很高。

链表存储有序的集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。

相对于传统的数组,链表的一个好处是,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而想要访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到所需的元素。

举个例子,我们玩寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你沿着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表去寻找。

链表其实有许多的种类:单向链表、双向链表、单向循环链表和双向循环链表。

链表的定义

链表是一组节点组成的集合,每个节点都使用一个对象的引用来指向它的后一个节点。指向另一节点的引用讲做链。
在这里插入图片描述
其中,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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值