JavaScript实现单向链表

147 阅读2分钟

最近学习了链表的基本概念后,打算自己用JavaScript实现一个单向链表类及相关的方法。首先实现一个 Node 类,主要包括两个实例属性:data(该节点的数据)和 next(指向下一个节点)。单项链表成员可以分为:链表头部(最初指向null)、中间节点(next指向另一个Node)、链表尾部(next指向null)。

// 链表节点
function Node({ data, next = null }) {
  this.data = data
  this.next = next
}

function LinkedList() {
  this.head = null
  this.length = 0

  // 获取某个位置的节点
  LinkedList.prototype.get = function (position) {
    // 空链表
    let preNode = this.head
    if (!preNode) return null

    // 只有一个节点
    let pos = 0
    if (position === pos) {
      return preNode
    }
    // 一个以上节点
    while (preNode.next) {
      pos++
      if (position === pos) {
        return preNode.next
      }
      preNode = preNode.next
    }
    // 超过链表长度
    return null
  }
  // 同 get(position)
  LinkedList.prototype.last = function () {
    // 空链表
    let preNode = this.head
    if (!preNode) return null

    // 非空链表
    while (preNode.next) {
      preNode = preNode.next
    }
    return preNode
  }
  // 获取某个节点所在位置
  LinkedList.prototype.indexOf = function (node) {
    // 空链表
    let preNode = this.head
    if (!preNode) return null

    // 只有一个节点
    let pos = 0
    if (preNode === node) {
      return pos
    }
    // 一个以上节点
    while (preNode.next) {
      pos++
      if (preNode.next === node) {
        return pos
      }
      preNode = preNode.next
    }
    // 超过链表长度
    return null
  }
  // 向链表末尾添加节点
  LinkedList.prototype.append = function (node) {
    isNodeValid.call(this, node)

    if (!this.head) {
      this.head = node
      return ++this.length
    }
    const lastNode = this.last()
    lastNode.next = node
    return ++this.length
  }
  // 某个位置插入节点
  LinkedList.prototype.insert = function (position, node) {
    isPosValid.call(this, position)
    isNodeValid.call(this, node)

    // 首节点
    if (position === 0) {
      node.next = this.head
      this.head = node
      this.length++
    } else {
      // 除了首节点
      let preNode = this.get(position - 1)
      let curNode = this.get(position)

      node.next = curNode
      preNode.next = node
      this.length++
    }
    return this
  }
  // 移除某个位置的节点
  LinkedList.prototype.removeAt = function (position) {
    isPosValid.call(this, position)

    const curNode = this.get(position)

    // 首节点
    if (position === 0) {
      this.head = curNode.next
    } else {
      // 非首节点
      const preNode = this.get(position - 1)
      preNode.next = curNode.next
    }
    this.length--
    return curNode
  }
  // 替换某个位置的节点
  LinkedList.prototype.replace = function (position, node) {
    isPosValid.call(this, position)
    isNodeValid.call(this, node)

    const curNode = this.get(position)

    // 首节点
    if (position === 0) {
      node.next = curNode.next
      this.head = node
    } else {
      // 非首节点
      const preNode = this.get(position - 1)
      node.next = curNode.next
      preNode.next = node
    }

    return this
  }
  // 获取所有值
  LinkedList.prototype.values = function () {
    const arr = []
    let preNode = this.head
    while (preNode) {
      arr.push(preNode.data)
      preNode = preNode.next
    }
    return arr
  }

  // 判断节点位置是否有效
  function isPosValid(position) {
    if (position < 0 || !Number.isInteger(position)) {
      throw new TypeError('position 应该为非负整数.')
    }

    // 无节点时
    if (!this.head) {
      throw new RangeError('链表为空.')
    }

    // 超出范围
    if (position > this.length - 1) {
      throw new RangeError('超出链表范围.')
    }
  }

  // 判断节点是否有效
  function isNodeValid(node) {
    if (!(node instanceof Node)) {
      console.warn(node)
      throw new TypeError('传入节点为非 Node 节点.')
    }
    // 将链表中所有的节点放到 WeakSet 中, 便于判断添加节点的唯一性, 避免节点之间相互引用
    const allNodes = new WeakSet()

    let preNode = this.head
    while (preNode) {
      allNodes.add(preNode)
      preNode = preNode.next
    }

    if (allNodes.has(node) || allNodes.has(node.next)) {
      throw new Error('该对象已存在于链表中, 请勿重复添加.')
    }
  }
}

// 测试
l = new LinkedList()
o1 = new Node({ data: 1 })
o2 = new Node({ data: 2 })
o3 = new Node({ data: 3, next: new Node({ data: '指定 Node 的 next' }) })
o4 = new Node({ data: 4 })
l.append(o1)
l.append(o2)
l.append(o3)
l.append(o4)
o5 = new Node({ data: 5 })
l.insert(0, o5)
console.log('===> ', l)
console.log('===> ', l.get(3))
console.log('indexOf ===> ', l.indexOf(o2))
console.log('values ===> ', l.values())

至于双向链表,大概思路是给Node实例添加一个pre属性,作用是指向前一个节点。每次向指定位置添加新节点node的时候,就将 node.pre 指向 该位置的前一个节点preNode、node.next 指向 preNode.next、preNode.next.pre指向node,然后preNode.next指向新节点node;删除节点同理。