图解单向链表原理以及JS实现链表的方法

196 阅读6分钟

前言

链表是我们在编程中重要的数据结构,由于它本身的数据特性,我们在开发中会频繁大量的使用链表。那么什么是链表呢?今天我们以单向链表为例子用图片来介绍一下什么是链表。

1.png

根据图片我们可以看出来链表的形态就是一条链子的形状,单向链表是在每一个节点中记录它下一个节点的内存地址来进行数据索引的,所以链表是逻辑上的一条链路,在实际内存空间上存储状态可能是这样的。

2.png

我们可以将内存理解为一个矩阵阵列,在每一个格子可以存储一些数据,每一个格子有自己相应的地址来访问这个数据,所以我们使用链表的主要用途就是将这些物理内存结构上非连续的数据做一个连接关系,这样可以快速的对数据进行引用和拿取,这种结构由于不需要内存上的大量连续空间可以有效的提高内存空间空间的利用率,在这一点上它的优势是优于数组的。

所以我们的链表结构在编程的角度上可以抽象成链表对象和节点对象。节点对象用于记录一个数据块的值和它下一个数据块的地址,节点对象本身的内存地址用于引用节点本身。通过链表对象来管理其内部的节点关系和常用的节点操作。

对于JS这种若类型语言来说,虽然我们无法直接操作物理内存,但是我们可以通过面向对象的方式来将链表对象和节点对象抽象并模拟出来。

链表的优势

在模拟对象之前我们还是再次分析一下链表这种数据结构在开发中的一些优势和使用场景。我们依然结合图形来分析。由于链表本身是链条形式的数据关系集合,所以我们可以通过链表来记录很多非连续数据,并且可以对集合进行“插入,变更,删除,索引”等操作。那么根据链表的结构我们来逐一分析。

链表的操作

插入操作

3.png 结合图片分析,我们可以看出在链表结构中我们如果想对链表本身的数据集合中插入新数据只需要找到目标位置并且将它原始的next记录的地址切换成要插入的节点的地址,并且将插入的节点地址的next指向原始的下一个节点,这样不需要改变其他元素的位置便形成了新的链表关系。所以链表的插入性能是很高的。这里主要消耗的就是找到插入点的操作,需要从head头部开始一次找到要插入的位置,而数组在插入元素上找到指定位置很快,但是插入元素时需要将后面的元素一次向后移位并且开辟新的空间这个操作耗时比较长。

删除操作

4.png 结合图片分析,链表的删除只需要找到要删除的节点的上一个位置,然后将它的next地址改变指向要删除节点的next地址即可,这样丢失引用的2号节点就会被gc回收掉。这里主要的耗时体现在找到要删除的节点,这个动作需要从head头部开始逐一找到指定元素。而数组删除元素时找到指定元素的操作时间短,而删除元素之后需要将后面的所有元素依次向前移位所以整体操作的工序繁琐性能不比链表。

指定获取元素

5.png 从图可知我们在链表中获取指定元素位置的时候每次都需要从头开始进行查找,并且元素越靠后查找的次数越多,所以链表在查询数据方面的性能体现不是很高,这点数组由于占用的是连续的内存空间,所以找到指定元素是可以直接从内存块上获取的。

指定更改元素

6.png 链表的指定更改元素和查找元素步骤一致,也是找到指定更新位置的元素的上一个元素,并且将他的next地址指向新的元素5,再将5这个元素指向要替换元素的next地址上,这样便实现了更改指定位置的元素,不过这个过程的性能显然没有数组结构的数据更改要快。

总结

综合了以上的图解我们大概对单向链表的结构有了一个初步的认识并且了解了它的不同场景的性能优劣,接下来我们进入今天的正题就是在JS中来实现一个链表结构。并且实现集合常用的功能,比如数据的插入,删除,更改,获取,还有获取长度等功能。这里需要我们创建两个对象。一个是节点对象,一个是链表对象。

基于以上对链表的了解我们将代码做如下封装。

节点对象的封装

/**
  * 节点对象
*/
class Node {
  /**
  * @param {Object} value 节点的值
  */constructor(value) {
    // 初始化节点的值
    this.value = value
    // 初始化节点的下一个元素this.next = null
    this.next = null
  }
}

链表对象封装

/**
 * 链表对象
 */
class LinkedList {
  /**
   * @param {Array} nodes 节点数组对象
   */
  constructor(nodes) {
    //根据构造函数的参数进行节点的初始化
    if (nodes) {
      this.length = nodes.length
      this.head = new Node(nodes.shift())
      this.initNodes(nodes)
    } else {
      this.length = 0
      this.head = null
    }
  }
  /**
   * 初始化链表所有节点指向
   * @param {Array} nodes 节点数组
   */
  initNodes(nodes) {
    if (nodes.length == 0) return null
    let eachNode = this.head
    nodes.forEach(item => {
      eachNode.next = new Node(item)
      eachNode = eachNode.next
    })
  }
  /**
   * 对链表尾部插入节点
   * @param {Node} node 节点对象
   */
  push(node) {
    let item = this.head
    if (this.length == 0) {
      this.head = node
      this.length++
      return
    }
    while (item.next) {
      item = item.next
    }
    item.next = node
    this.length++
    return this
  }
  /**
   * 从链表尾部拿出指定节点并删除
   */
  pop() {
    let item = this.head
    if (this.length == 0) {
      return null
    }
    if (this.length == 1) {
      this.head = null
      this.length = 0
      return item
    }
    while (item.next && item.next.next) {
      item = item.next
    }
    let last = item.next
    item.next = null
    this.length--
    return last
  }
  /**
   * @param {number} index 获取指定位置的节点
   */
  get(index) {
    let i = 0
    let item = this.head
    if (index >= this.length || index < 0) {
      return null
    }
    while (item.next) {
      if (index == i) {
        break
      }
      item = item.next
      i++
    }
    return item
  }
  /**
   * 向链表指定位置前置追加节点
   * @param {number} index 序号0到(length-1)
   * @param {Node} node 节点对象
   */
  insertBefore(index, node) {
    let i = 0
    let item = this.head

    if (index >= this.length || index < 0) {
      throw ('outOfIndexError')
      return
    }
    if (index == 0) {
      node.next = this.head
      this.head = node
      this.length++
    }
    while (item.next) {
      if (i == index - 1) {
        let oldNextNode = item.next
        item.next = node
        node.next = oldNextNode
        this.length++
        break
      }
      item = item.next
      i++
    }
  }
  /**
   * 将指定位置的节点替换
   * @param {number} index 序号0到(length-1)
   * @param {Node} node 节点对象
   */
  set(index, node) {
    let i = 0
    let item = this.head
    if (index == 0) {
      node.next = item.next
      this.head = node
      return
    }
    while (item.next) {
      if (i == index - 1) {
        let oldNextNode = item.next
        item.next = node
        node.next = oldNextNode.next
      }
      item = item.next
      i++
    }
    return this
  }
  /**
   * 删除指定位置的节点
   * @param {number} index 序号0到(length-1)
   */
  remove(index) {
    let i = 0
    let item = this.head
    if (i == 0) {
      this.head = this.head.next
      this.length--
    }
    while (item.next) {
      if (i == index - 1) {
        let oldNextNode = item.next
        item.next = oldNextNode.next
        this.length--
        break
      }
      item = item.next
      i++
    }
  }
  /**
   * 将链表转换成字符串输出
   */
  toString() {
    let str = ''
    let item = this.head
    str = item.value
    while (item.next) {
      str += '->' + item.next.value
      item = item.next
    }
    return '{' + str + '}'
  }
  /**
   * 将链表转换成Node数组
   */
  toNodeArray() {
    let arr = []
    let item = this.head
    arr.push(item)
    while (item.next) {
      arr.push(item.next)
      item = item.next
    }
    return arr
  }
  /**
   * 将链表转换成值数组
   */
  toArray() {
    let arr = []
    let item = this.head
    arr.push(item.value)
    while (item.next) {
      arr.push(item.next.value)
      item = item.next
    }
    return arr
  }
  /**
   * 将指定的数组转换成链表对象
   * @param {Array} arr js原生数组对象
   */
  fromArray(arr) {
    this.head = new Node(arr[0])
    let item = this.head
    for (var i = 1; i < arr.length; i++) {
      item.next = new Node(arr[i])
      item = item.next
    }
    this.length = arr.length
  }
}

最后

以上便是使用JS封装的链表对象的一个小分享,希望可以在学习数据结构的前端同学领域给予一些帮助,这个对象的使用方式我就不多做介绍了相信拿到源代码的同学看到代码会很轻松的进行使用的。这里仅仅是根据单向链表的特性进行JS的模拟,并不是真正的链表对象,所以我们以学习思路为主。