JavaScript 数据结构与算法 — 双向链表

双向链表(Doubly Linked List)是链表类型中的一种,顾名思义,它的数据链接是双向的,它的链表节点有三部分组成:数据部分(value)、链向前一个节点的指针(prev)和链向下一个节点的指针(next)。
双向链表的结构如下图所示:
双向链表

实现双向链表节点类

双向链表节点比单向链表节点多一个指针 prev,我们可以定义一个继承单向链表节点类的双向链表节点类:

/**
 * DoublyNode.js
 * @date: 2025-03-26
 * @description: 创建双向链表节点的类
 */

import Node from './Node.js'

export default class DoublyNode extends Node {
  constructor(value) {
    super(value)
    this.prev = null // 指向上一个节点的指针
  }
}

实现双向链表类

双向链表也可以继承自单向链表的类,因为双向链表不仅可以从头部开始迭代,还可以从尾部开始迭代,所以双向链表类中最好维护一个对链表最后一个节点的引用 tail。

/**
 * DoublyLinkedList.js
 * @date: 2025-03-26
 * @description: 双向链表
 */

import DoublyNode from '../../models/DoublyNode.js'
import LinkedList from './LinkedList.js'

export default class DoublyLinkedList extends LinkedList {
  constructor() {
    super()
    // 双向链表提供了两种迭代方式,从头到尾和从尾到头,因此需要两个指针,一个是指向第一个节点的指针head(继承自单向链表),另一个是指向最后一个节点的指针tail
    this.tail = null // 指向最后一个节点的指针
  }
}

实现链表方法

双向链表中的方法与单向链表是一样的,操作链表时要格外注意 prev 指针的链接与断开即可。

insert 方法:任意位置插入元素

在 DoublyLinkedList 类中,我们将重写 insert 方法,因为链表是双向的,所以我们要考虑头部插入、尾部插入和其他位置插入这三种情况。具体代码展示如下:

  // 在某个位置插入节点
  insert(index, val) {
    if (arguments.length < 2) return undefined
    if (typeof arguments[0] !== 'number') return undefined
    if (index < 0 || index > this.size) return undefined
    
    const node = new DoublyNode(val)
    let current = this.head
    // 头部插入
    if (index === 0) {
      // 链表为空时,首尾指针都指向新添加的元素节点
      if (!this.head) {
        this.head = node
        this.tail = node
      } else {
        node.next = this.head
        current.prev = node
        this.head = node
      }
    } else if (index === this.size) {
      // 尾部插入
      current = this.tail
      current.next = node
      node.prev = current
      this.tail = node
    } else {
      // 获取插入点前后元素节点
      const prev = this.getNodeAt(index - 1)
      current = prev.next
      // 插入新元素并与前后元素节点建立链接
      prev.next = node
      node.prev = prev
      node.next = current
      current.prev = node
    }
    
    this.size++
    return this.size
  }

上面代码中头部插入那部分代码我们可以通过 unshift 方法实现,尾部插入那部分代码我们可以通过 push 方法实现。这两部分提取出来的话为下面的代码:

  // 向链表尾部添加元素节点
  push(val) {
    if (arguments.length === 0) return this.size
    const node = new DoublyNode(val)
    if (!this.head) {
      this.head = node
      this.tail = node
    } else {
      this.tail.next = node
      node.prev = this.tail
      this.tail = node
    }
    this.size++
    return this.size
  }
  // 头部添加节点
  unshift(val) {
    if (arguments.length === 0) return this.size
    const node = new DoublyNode(val)
    if (!this.head) {
      this.head = node
      this.tail = node
    } else {
      this.head.prev = node
      node.next = this.head
      this.head = node
    }
    this.size++
    return this.size
  }

insert 最终代码可以表示为:

  insert(index, val) {
    if (arguments.length < 2) return undefined
    if (typeof arguments[0] !== 'number') return undefined
    if (index < 0 || index > this.size) return undefined

    if (index === 0) return this.unshift(val)
    if (index === this.size) return this.push(val)

    const node = new DoublyNode(val)
    const prev = this.getNodeAt(index - 1)
    const next = prev.next
    prev.next = node
    node.prev = prev
    node.next = next
    next.prev = node
    this.size++
    return this.size
  }

removeAt 方法:根据位置删除元素

跟 insert 插入方法一样,removeAt 方法也是分成三种情况:删除第一个节点、删除最后一个节点、删除其他位置节点。类似的,删除第一个节点的代码可以提取出来作为 shift方法,删除最后一个节点的代码可以提取出来作为 pop 方法。

  // 头部删除节点
  shift() {
    if (this.isEmpty()) return undefined
    const current = this.head
    if (this.size === 1) {
      this.head = null
      this.tail = null
    } else {
      this.head = current.next
      this.head.prev = null
    }
    this.size--
    return current.value
  }
  // 尾部删除节点
  pop() {
    if (this.isEmpty()) return undefined
    const current = this.tail
    if (this.size === 1) {
      this.head = null
      this.tail = null
    } else {
      this.tail = current.prev
      this.tail.next = null
    }
    this.size--
    return current.value
  }
  // 删除某个位置的节点
  removeAt(index) {
    if (arguments.length === 0) return undefined
    if (index < 0 || index >= this.size) return undefined
    if (index === 0) return this.shift()
    if (index === this.size - 1) return this.pop()
    const current = this.getNodeAt(index)
    current.prev.next = current.next
    current.next.prev = current.prev
    this.size--
    return current.value
  }
  // 获取任意位置的节点
  getNodeAt(index) {
    if (arguments.length === 0) return undefined
    if (index < 0 || index >= this.size) return undefined
    let current = this.head
    let i = 0
    while (current) {
      if (i === index) return current
      current = current.next
      i++
    }
    return undefined
  }

remove 方法:删除元素

迭代链表,找到匹配的第一个元素值的位置,然后通过 removeAt 方法进行删除即可。

  // 返回匹配值的第一个位置
  indexOf(val) {
    if (arguments.length === 0) return -1
    if (this.isEmpty()) return -1

    let current = this.head
    let index = 0
    while (current) {
      if (this.options.isEqual(current.value, val)) return index
      current = current.next
      index++
    }
    return -1
  }
  // 删除某个值的节点
  remove(val) {
    if (arguments.length === 0) return undefined
    const index = this.indexOf(val)
    return this.removeAt(index)
  }

其他方法便不一一展示,请查看gitee仓库或者gitHub仓库

posted @ 2025-04-03 17:40  老甄Home  阅读(37)  评论(0)    收藏  举报