JavaScript 实现经典数据结构之链表

131 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

链表(单向链表和双向链表)

上一节我们使用数组实现了堆栈和队列,链表和数组一样,也实现了按照一定的顺序存储元素,不同的地方在于链表不能像数组一样通过下标访问,而是每一个元素都能够通过“指针”指向下一个元素。我们可以直观地得出结论:链表不需要一段连续的存储空间,“指向下一个元素”的方式能够更大限度地利用内存。

根据上述内容,我们可以总结出链表的优点在于:

  • 链表的插入和删除操作的时间复杂度是常数级的,我们只需要改变相关节点的指针指向即可;
  • 链表可以像数组一样顺序访问,查找元素的时间复杂度是线性的。

要想实现链表,我们需要先对链表进行分类,常见的有单链表和双向链表。

链表类别特点
单链表:单链表是维护一系列节点的数据结构每个节点包含了数据,同时包含指向链表中下一个节点的指针
双向链表:不同于单链表每个节点分支除了包含其数据以外,还包含了分别指向其前驱和后继节点的指针

首先,根据双向链表的特点,我们实现一个节点构造函数(节点类),如下代码:

class Node {
    constructor(data) {
        // data 为当前节点存储的数据
        this.data = data
        // next 指向下一个节点
        this.next = null
        // prev 指向上一个节点
        this.prev = null
    }
}

来初步实现双向链表类:

class DoubleLinkedList() {
    constructor() {
        // 双向链表开头
        this.head = null
        // 双向链接结尾
        this.tail = null
    }
}

实现双向链表原型上的一些方法:

add 链表尾部增加一个节点

add(item) {
    let node = new Node(item)
    // 如果当前链表还没有头
    if (!this.head) {
        this.head = item
        this.tail = item
    } else { // 如果当前链表已经有头
        // 当前的尾作为新节点的prev
        node.prev = this.tail
        // 当前尾的next为新节点
        this.tail.next = node
        // 新的尾为新节点
        this.tail = node
    }    
}

addAt 指定位置增加一个节点

addAt(index, item) {
    let current = this.head
    
    // 维护查找时当前节点的索引
    let counter = 1
    let node = new Node(item)
    // 如果在头部插入
    if (index === 0) {
        this.head.prev = node
        node.next = this.head
        this.head = node
    } 

    // 非头部插入,需要从头开始,找寻插入位置
    else {
        while(current) {
            current = current.next
            if( counter === index) {
                node.prev = current.prev
                current.prev.next = node
                node.next = current
                current.prev = node
            }
            counter++
      }
    }
}

remove 删除链表指定数据项

remove(item) {
  let current = this.head
  while (current) {
       // 找到了目标节点
    if (current.data === item ) {
      // 目标链表只有当前目标项,即目标节点即是链表头又是链表尾
      if (current == this.head && current == this.tail) {
        this.head = null
        this.tail = null
      } 
      // 目标节点为链表头
      else if (current == this.head ) {
        this.head = this.head.next
        this.head.prev = null
      } 
      // 目标节点为链表尾部
      else if (current == this.tail ) {
        this.tail = this.tail.prev;
        this.tail.next = null;
      } 
      // 目标节点在链表首尾之间,中部
      else {
        current.prev.next = current.next;
        current.next.prev = current.prev;
      }
   }
   current = current.next
  }
}

reverse 翻转链表

reverse() {
  let current = this.head
  let prev = null
  while (current) {
   let next = current.next
   // 前后倒置
   current.next = prev
   current.prev = next
   prev = current
   current = next
  }
  this.tail = this.head
  this.head = prev
}

traverse 遍历链表

traverse(fn) {
  let current = this.head

  while(current !== null) {
    // 执行遍历时回调 
    fn(current)
    current = current.next
  }
  return true
}

find 查找某个节点的索引

find(item) {
  let current = this.head
  let counter = 0
  while( current ) {
    if( current.data == item ) {
      return counter
    }
    current = current.next
    counter++
  }
  return false
}

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。