js实现单向链表

在js中存储多个数据的通常方式是使用数组,其实还可以使用链表这种数据格式来保存数据,但是原生js并没有提供链表这种特殊的数据格式
在一些场景下,利用链表的的特性来解决一些算法类的问题是具有一定优势的。

链表是什么

先说说什么是链表,通常链表是由多个节点构成,每个节点都保存一个值(value),另外还保存了指向下一个节点的引用(指针next)
在这里插入图片描述
上边的链表就由5个节点构成,其中第一个节点叫做链表的头部(head),最后一个节点叫做链表的尾部(tail)。
我们猜想链表应该还具有一个length属性来记录链表长度(节点的个数)

动手开始写代码

首先由两个类分别用来创建节点链表

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = this.tail = null;
    this.length = 0;
  }
}
  • Node初始化的时候,用value记录了自身的值。next表示下一个节点,默认指向null
  • LinkedList 初始化的时候,链表头尾都指向null,自身长度默认为0。其中head表示第一个节点,tail表示最后一个节点

末尾追加节点

class LinkedList {
  constructor() {
    this.head = this.tail = null;
    this.length = 0;
  }

  // 末尾增加节点
  append(value) {
    const node = new Node(value);
    if (!this.head) {
      this.head = this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
    this.length++;
  }
}
  • 首先新建一个节点,
  • 判断是否是空链表:
  • 如果是空链表,直接把节点赋值给head和tail,
  • 如果不是空链表,就让最后一个节点tail指向新节点node ,其实这时候新节点node变成了最后一个节点,所以node才是tail,于是改写tail为node,
  • 由于末尾新增了一个节点,于是length加1

头部增加节点

 // 头部增加节点
  prepend(value) {
    const node = new Node(value);
    node.next = this.head;
    this.head = node;
    this.length++;
  }
  • 这里和上面类似,但是不用判断是否是空链表,因为在append的时候,tail有可能是null,所以不存在next属性,强行获取tail身上的next属性就会报错。
  • 而prepend的时候node本身就是一个对象,可以放心地设置next属性。

查找指定索引节点

在链表中获取指定索引的元素不像数组那么方便,需要一个遍历操作才能找到指定节点(这也是链表的缺点之一

 find(index) {
    const { length } = this;
    if (index < 0 || index > length - 1 || length === 0) return null; // 范围溢出或者空链表
    let currentNode = this.head;
    let i = 0;
    while (i < index) {
      currentNode = currentNode.next;
      i++;
    }
    return currentNode;
  }
  • 首先排除索引超出范围和链表为空的情况
  • 记录第一个节点,开始遍历,只要没到目标索引,就不断获取下一个节点,最后返回目标节点。
  • 这样看来,访问越后面的节点需要循环查询的次数越多。时间复杂度是O(n)

在指定索引插入节点

插入节点的操作分为3个步骤

  • 找到目标索引的节点和前一个节点
  • 把前一个节点指向新节点
  • 把新节点指向目标索引节点
    在这里插入图片描述
insert(value, index) {
    if (index <= 0) {
      this.prepend(value);
    } else if (index >= this.length) {
      this.append(value);
    } else {
      const node = new Node(value);
      const prevNode = this.find(index - 1);
      const nextNode = prevNode.next;
      prevNode.next = node;
      node.next = nextNode;
      this.length++;
    }
  }

先处理两种特殊情况,

  • 如果index等于0说明要在头部插入节点,可以直接使用prepend方法。
  • 如果index大于链表长度,说明是要在末尾插入节点,于是可以直接使用append方法。
  • 默认情况下新建一个节点,获取前一个节点prevNode 与目标节点nextNode ,改变前一个节点与新节点的指向,再把length加1。

移除节点

移除节点的思想和插入节点类似,找到相应节点,改变节点指向

remove(index) {
    const { length } = this;
    if (index < 0 || index > length - 1 || length === 0) return; // 范围溢出或者空链表

    // 只有一个节点
    if (length === 1) {
      this.head = this.tail = null;
    }
    // 移除头节点
    else if (index === 0) {
      this.head = this.head.next;
    }
    // 移除尾节点
    else if (index === length - 1) {
      const prevNode = this.find(index - 1);
      this.tail = prevNode;
      prevNode.next = null;
    } else {
      const prevNode = this.find(index - 1);
      const nextNode = prevNode.next.next;
      prevNode.next = nextNode;
    }
    this.length--;
  }

这里边考虑了几种特殊情况:

  • 范围溢出或者链表为空就啥也不用做
  • 只有一个节点的话,直接重写head和tail为null就相当于移除节点了,因为此时将没有任何一个指针指向该节点,节点会被js的垃圾回收机制清除
  • 当index为0的时候,表示要移除第一个节点,直接重写head就好了,原来的head由于没有人指向他,会被自动清除
  • 当index指向最后一个节点的时候,先找到倒数第二个节点,把他指向null,这就意味着原来的最后一个节点没有人指向他了,会被自动清除
  • 默认情况下,直接找到前一个节点与下一个节点,直接把前一个节点指向下一个节点,原来index位置的节点被跳过了,于是被自动清除了。

反转链表

反转链表稍微复杂一些,

  • 这里用2个指针分别记录前一个节点当前节点
  • 每次循环改变当前节点的指向,然后把2个指针往右边移动一格,
  • 直到当前节点为空的时候,整个链表指针就被翻转过来了
 reverse() {
    let prevNode = null;
    let currentNode = (this.tail = this.head);
    while (currentNode) {
      const nextNode = currentNode.next;
      currentNode.next = prevNode; // 反转
      prevNode = currentNode; // 指针右移
      currentNode = nextNode; // 指针右移
    }
    this.head = prevNode;
  }
  • 翻转开始之前,记得重写一下tail。
  • 在每次循环的时候。临时用nextNode 保存一下下一个节点,不然改变currentNode.next 的时候,下一个节点会丢失,这样循环就没办法连续进行下去了。
  • 循环结束后prevNode其实表示最后一个节点,也就是翻转以后的第一个节点,所以赋值给head。

完整代码贴上

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = this.tail = null;
    this.length = 0;
  }
  // 末尾增加节点
  append(value) {
    const node = new Node(value);
    if (!this.head) {
      this.head = this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
    this.length++;
  }
  // 头部增加节点
  prepend(value) {
    const node = new Node(value);
    node.next = this.head;
    this.head = node;
    this.length++;
  }
  // 指定索引插入节点
  insert(value, index) {
    if (index <= 0) {
      this.prepend(value);
    } else if (index >= this.length) {
      this.append(value);
    } else {
      const node = new Node(value);
      const prevNode = this.find(index - 1);
      const nextNode = prevNode.next;
      prevNode.next = node;
      node.next = nextNode;
      this.length++;
    }
  }
  // 移除节点
  remove(index) {
    const { length } = this;
    if (index < 0 || index > length - 1 || length === 0) return; // 范围溢出或者空链表

    // 只有一个节点
    if (length === 1) {
      this.head = this.tail = null;
    }
    // 移除头节点
    else if (index === 0) {
      this.head = this.head.next;
    }
    // 移除尾节点
    else if (index === length - 1) {
      const prevNode = this.find(index - 1);
      this.tail = prevNode;
      prevNode.next = null;
    } else {
      const prevNode = this.find(index - 1);
      const nextNode = prevNode.next.next;
      prevNode.next = nextNode;
    }
    this.length--;
  }
  // 反转链表
  reverse() {
    let prevNode = null;
    let currentNode = (this.tail = this.head);
    while (currentNode) {
      const nextNode = currentNode.next;
      currentNode.next = prevNode; // 反转
      prevNode = currentNode; // 指针右移
      currentNode = nextNode; // 指针右移
    }
    this.head = prevNode;
  }
  // 查找指定索引节点
  find(index) {
    const { length } = this;
    if (index < 0 || index > length - 1 || length === 0) return null; // 范围溢出或者空链表
    let currentNode = this.head;
    let i = 0;
    while (i++ < index) {
      currentNode = currentNode.next;
    }
    return currentNode;
  }
}

测试:

const list = new LinkedList();
      list.append("111");
      list.append("222");
      list.append("333");
      list.append("444");
      list.prepend("000");
      console.log(list, list.find(2));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值