js 数据结构 - 链表

559 阅读6分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

链表是由一个个结点组成的数据结构,而结点一般是由数据和记录下一个结点地址的“指针”组成(双向链表还有一个记录上一个结点地址的“指针”)。

链表有很多种。

  1. 单链表
  2. 循环链表(或叫单向循环链表)
  3. 双向链表
  4. 双向循环链表
  5. 带头链表(带有“哨兵结点”的非循环链表)

注意:文章会用到几个术语,我们一般把结点的下一个结点称为“后继结点”,结点的记录下一个结点地址的“指针”称为“后继指针”;把结点的上一个结点称为“前驱结点”,结点的记录上一个结点地址的“指针”称为“前驱指针”。

单链表

单链表有两个特殊的结点:头结点和尾结点。

头结点记录初始地址,且没有被任何结点的“后继指针”指向。

尾结点记录最后一个地址,且“后继指针”指向 null。

image.png

注意:下面的链表代码记录了尾结点,实际上,你可以完全不用记录尾结点就可以实现链表结构。

js 代码实现

    // 结点
    class Node {
      constructor(data) {
        this.data = data
        this.next = null
      }
    }

    // 单链表
    class LinkedList {
      #size = 0 // 链表长度
      #head = null // 头结点
      #tail = null // 尾结点

      /* 增 */
      // 尾部添加结点
      append(data) {
        let node = new Node(data)
        if (this.#tail) {
          this.#tail.next = node
          this.#tail = node
        } else {
          this.#head = node
          this.#tail = node
        }
        this.#size++
      }
      // 根据位置添加结点(位置是从0开始)
      insert(index, data) {
        if (index > this.#size || index < 0) {
          return false
        }
        let node = new Node(data)
        if (this.#size === 0) {
          // 没长度,直接插入
          this.#head = node
          this.#tail = node
        } else if (index == this.#size) {
          // 尾结点,特别操作
          this.#tail.next = node
          this.#tail = node
        } else if (index == 0) {
          // 头结点,特别操作
          node.next = this.#head
          this.#head = node
        } else {
          // 中间结点,前驱结点肯定会有
          let target = this.#head
          let prev = null
          for (let i = 1; i <= index; i++) {
            prev = target
            target = target.next
          }
          node.next = target
          prev.next = node
        }
        this.#size++
        return true
      }

      /* 删 */
      // 删除相关值最近的结点
      delete(data) {
        let node = this.#head
        let prev = null
        while (node) {
          if (node.data === data) {
            if (node === this.#head) { // 删除的结点为头结点,需要更改头结点
              if (this.#head === this.#tail) {
                this.#head = null
                this.#tail = null
              } else {
                this.#head = this.#head.next
              }
            } else if (node === this.#tail) { // 删除的结点为尾结点,需要更改尾结点
              prev.next = null
              this.#tail = prev
            } else {
              prev.next = node.next
            }
            this.#size--
            return true
          } else {
            prev = node
            node = node.next
          }
        }
        return false
      }
      // 删除相关位置的结点
      deleteIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return false
        }
        if (index === 0) { // 删除头结点
          if (this.#head === this.#tail) {
            this.#head = null
            this.#tail = null
          } else {
            this.#head = this.#head.next
          }
        } else {
          let node = this.#head
          let prev = null
          for (let i = 1; i <= index; i++) {
            prev = node
            node = node.next
          }
          if (node === this.#tail) {
            prev.next = null
            this.#tail = prev
          } else {
            prev.next = node.next
          }
        }
        this.#size--
        return true
      }

      /* 查 */
      // 链表长度
      size() {
        return this.#size
      }
      // 获取头结点
      head() {
        return this.#head
      }
      // 获取尾结点
      tail() {
        return this.#tail
      }
      // 找到第一次出现的结点
      find(data) {
        let node = this.#head
        while (node) {
          if (node.data === data) {
            return node
          } else {
            node = node.next
          }
        }
      }
      // 根据位置找到结点(位置是从0开始)
      findIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return
        }
        if (index == 0) {
          return this.#head
        }
        if (index == this.#size - 1) {
          return this.#tail
        }
        let node = this.#head
        for (let i = 1; i <= index; i++) {
          node = node.next
        }
        return node
      }
      // 查找值第一次出现的位置(位置是从0开始)
      indexOf(data) {
        let node = this.#head
        let i = 0
        while (node) {
          if (node.data === data) {
            return i
          } else {
            node = node.next
            i++
          }
        }
        return -1
      }
      // 是否为空
      isEmpty() {
        return this.#size === 0 ? true : false
      }
      // 打印(从头到尾)
      print() {
        if (this.#size === 0) {
          console.log('空链表,头结点:', this.#head, '尾结点:', this.#tail)
          return
        }
        let node = this.#head
        while (node) {
          console.log(node.data)
          node = node.next
        }
        console.log('头结点:', this.#head)
        console.log('尾结点:', this.#tail)
        console.log('总长度:' + this.#size)
      }
    }

单向循环链表

单向循环链表是在单链表的基础下衍生出来,区别是单向循环链表的尾结点不再指向 null,而是指向头结点,形成环形结构。

这种结构用来解决“约瑟夫问题”是特别有用的。

image.png

js代码实现

    // 结点
    class Node {
      constructor(data) {
        this.data = data
        this.next = null
      }
    }

    // 循环链表
    class CircleLinkedList {
      #size = 0 // 链表长度
      #head = null // 头结点
      #tail = null // 尾结点

      /* 增 */
      // 尾部添加结点
      append(data) {
        let node = new Node(data)
        if (this.#tail) {
          this.#tail.next = node
          this.#tail = node
          // 将尾结点重新连接到头结点
          this.#tail.next = this.#head
        } else {
          this.#head = node
          this.#tail = node
          this.#tail.next = this.#head
        }
        this.#size++
      }
      // 根据位置添加结点(位置是从0开始)
      insert(index, data) {
        if (index > this.#size || index < 0) {
          return false
        }
        let node = new Node(data)
        if (this.#size === 0) {
          // 没长度,直接插入
          this.#head = node
          this.#tail = node
        } else if (index == this.#size) {
          // 尾结点,特别操作
          this.#tail.next = node
          this.#tail = node
          this.#tail.next = this.#head
        } else if (index == 0) {
          // 头结点,特别操作
          node.next = this.#head
          this.#head = node
          this.#tail.next = this.#head
        } else {
          // 中间结点,前驱结点肯定会有
          let target = this.#head
          let prev = null
          for (let i = 1; i <= index; i++) {
            prev = target
            target = target.next
          }
          node.next = target
          prev.next = node
        }
        this.#size++
      }

      /* 删 */
      // 删除相关值最近的结点
      delete(data) {
        let node = this.#head
        let prev = null
        while (node) {
          if (node.data === data) {
            if (node === this.#head) {
              // 删除的结点为头结点,需要更改头结点
              if (this.#head === this.#tail) {
                this.#head = null
                this.#tail = null
                node.next = null
              } else {
                this.#head = node.next
                this.#tail.next = this.#head
              }
            } else if (node === this.#tail) {
              // 删除的结点为尾结点,需要更改尾结点
              prev.next = this.#head
              this.#tail = prev
            } else {
              prev.next = node.next
            }
            node.next = null // 将删除的结点的 next 指向空
            this.#size--
            return true
          } else {
            if (node === this.#tail) {
              break
            }
            prev = node
            node = node.next
          }
        }
        return false
      }
      // 删除相关位置的结点
      deleteIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return false
        }
        if (index === 0) {
          if (this.#head === this.#tail) {
            this.#head = null
            this.#tail = null
          } else {
            this.#head = this.#head.next
            this.#tail.next = this.#head
          }
        } else {
          let node = this.#head
          let prev = null
          for (let i = 1; i <= index; i++) {
            prev = node
            node = node.next
          }
          if (node === this.#tail) {
            prev.next = this.#head
            this.#tail = prev
          } else {
            prev.next = node.next
          }
          node.next = null
        }
        this.#size--
        return true
      }

      /* 查 */
      // 链表长度
      size() {
        return this.#size
      }
      // 获取头结点
      head() {
        return this.#head
      }
      // 获取尾结点
      tail() {
        return this.#tail
      }
      // 找到第一次出现的结点
      find(data) {
        let node = this.#head
        while (node) {
          if (node.data === data) {
            return node
          } else if (node === this.#tail) {
            break
          } else {
            node = node.next
          }
        }
      }
      // 根据位置找到结点(位置是从0开始)
      findIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return
        }
        if (index == 0) {
          return this.#head
        }
        if (index == this.#size - 1) {
          return this.#tail
        }
        let node = this.#head
        for (let i = 1; i <= index; i++) {
          node = node.next
        }
        return node
      }
      // 查找值第一次出现的位置(位置是从0开始)
      indexOf(data) {
        let node = this.#head
        let i = 0
        while (node) {
          if (node.data === data) {
            return i
          } else if (node === this.#tail) {
            break
          } else {
            node = node.next
            i++
          }
        }
        return -1
      }
      // 是否为空
      isEmpty() {
        return this.#size === 0 ? true : false
      }
      // 打印(从头到尾)
      print() {
        if (this.#size === 0) {
          console.log('空链表,头结点:', this.#head, '尾结点:', this.#tail)
          return
        }
        let node = this.#head
        while (node) {
          console.log(node.data)
          if (node === this.#tail) {
            break
          } else {
            node = node.next
          }
        }
        console.log('头结点:', this.#head)
        console.log('尾结点:', this.#tail)
        console.log('尾结点的下一个结点是否是头结点:', this.#head === this.#tail.next)
        console.log('总长度:' + this.#size)
      }
    }

双向链表

对比单向链表,双向链表多了一个“前驱指针”,“前驱指针”用来记录上一个结点的地址。由于双向链表既有“前驱指针”,也有“后继指针”,双向链表相比单向链表占用的内存会高很多。但是即使双向链表占用的内存高,我们在开发时更愿意使用这种结构,因为这种结构可以向前找结点,查找数据的效率比单向链表高,而且在一些插入或删除结点的操作中,操控前一个结点去连接另一个结点就简单多了。双向链表是典型的“空间换时间”的结构。

image.png

这里留意以下的 findIndex 方法,不再像单链表盲目从头到尾遍历。由于双向链表可以从尾到头遍历的特点,这里先判断 index 是否小于 size/2,如果小于,则从头到尾遍历,如果大于,则从尾到头遍历。

js 代码实现

// 结点,多了一个“前驱指针”
    class Node {
      constructor(data) {
        this.data = data
        this.next = null
        this.prev = null
      }
    }

    // 双向链表
    class DoubleLinkedList {
      #size = 0 // 链表长度
      #head = null // 头结点
      #tail = null // 尾结点

      /* 增 */
      // 尾部添加结点
      append(data) {
        let node = new Node(data)
        if (this.#tail) {
          this.#tail.next = node
          node.prev = this.#tail
          this.#tail = node
        } else {
          this.#head = node
          this.#tail = node
        }
        this.#size++
      }
      // 根据位置添加结点(位置是从0开始)
      insert(index, data) {
        if (index > this.#size || index < 0) {
          return false
        }
        let node = new Node(data)
        if (this.#size === 0) {
          // 没长度,直接插入
          this.#head = node
          this.#tail = node
        } else if (index == this.#size) {
          console.log(123)
          // 尾结点,特别操作
          node.prev = this.#tail
          this.#tail.next = node
          this.#tail = node
        } else if (index == 0) {
          // 头结点,特别操作
          this.#head.prev = node
          node.next = this.#head
          this.#head = node
        } else {
          // 中间结点,前驱结点肯定会有
          let target = this.#head
          for (let i = 1; i <= index; i++) {
            target = target.next
          }
          node.next = target
          node.prev = target.prev
          target.prev.next = node // 注意这一步
          target.prev = node
        }
        this.#size++
        return true
      }

      /* 删 */
      // 删除相关值最近的结点
      delete(data) {
        let node = this.#head
        while (node) {
          if (node.data === data) {
            if (node === this.#head) { // 删除的结点为头结点,需要更改头结点
              if (this.#head === this.#tail) {
                this.#head = null
                this.#tail = null
              } else {
                this.#head = this.#head.next
                this.#head.prev = null
              }
            } else if (node === this.#tail) { // 删除的结点为尾结点,需要更改尾结点
              node.prev.next = null
              this.#tail = node.prev
            } else {
              node.next.prev = node.prev
              node.prev.next = node.next
            }
            this.#size--
            return true
          } else {
            node = node.next
          }
        }
        return false
      }
      // 删除相关位置的结点
      deleteIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return false
        }
        if (index === 0) { // 删除头结点
          if (this.#head === this.#tail) {
            this.#head = null
            this.#tail = null
          } else {
            this.#head = this.#head.next
            this.#head.prev = null
          }
        } else if (index === this.#size-1) { // 删除尾结点 
          this.#tail.prev.next = null
          this.#tail = this.#tail.prev
        } else {
          // let node = this.#head
          // for (let i = 1; i <= index; i++) {
          //   node = node.next
          // }
          let node = this.findIndex(index)
          if (node) {
            node.next.prev = node.prev
            node.prev.next = node.next
          } else {
            return false
          }
        }
        this.#size--
        return true
      }

      /* 查 */
      // 链表长度
      size() {
        return this.#size
      }
      // 获取头结点
      head() {
        return this.#head
      }
      // 获取尾结点
      tail() {
        return this.#tail
      }
      // 找到第一次出现的结点
      find(data) {
        let node = this.#head
        while (node) {
          if (node.data === data) {
            return node
          } else {
            node = node.next
          }
        }
      }
      // 根据位置找到结点(位置是从0开始)
      findIndex(index) {
        if (index > this.#size - 1 || index < 0) {
          return
        }
        if (index == 0) {
          return this.#head
        }
        if (index == this.#size - 1) {
          return this.#tail
        }
        // 这里判断 index 在 size/2 的左边还是右边,可以更快找到数据
        let node
        if (index < this.#size/2) {
          node = this.#head
          for (let i = 1; i <= index; i++) {
            node = node.next
          }
        } else {
          node = this.#tail
          for (let i = this.#size-1; i > index; i--) {
            node = node.prev
          }
        }
        return node
      }
      // 查找值第一次出现的位置(位置是从0开始)
      indexOf(data) {
        let node = this.#head
        let i = 0
        while (node) {
          if (node.data === data) {
            return i
          } else {
            node = node.next
            i++
          }
        }
        return -1
      }
      // 是否为空
      isEmpty() {
        return this.#size === 0 ? true : false
      }
      // 打印(isPrev: false,从头到尾;true,从尾到头)
      print(isPrev) {
        if (this.#size === 0) {
          console.log('空链表,头结点:', this.#head, '尾结点:', this.#tail)
          return
        }
        if (!isPrev) {
          console.log('从头到尾打印')
          let node = this.#head
          while (node) {
            console.log(node.data)
            node = node.next
          }
        } else {
          console.log('从尾到头打印')
          let node = this.#tail
          while(node) {
            console.log(node.data)
            node = node.prev
          }
        }
        console.log('头结点:', this.#head)
        console.log('尾结点:', this.#tail)
        console.log('总长度:', this.#size)
      }
    }

双向循环链表

和单向链表类似,双向循环链表是在双向链表的基础下,尾结点的“后继指针”指向头结点。

image.png

看了上面的单向循环链表和双向链表,相信你已经对双向循环链表有一定的实现想法了,这里就不进行实现了。

带头链表

不管是单向链表,还是双向链表,插入新的头结点和删除头结点都要进行一些特殊的操作(这里不考虑循环链表,因为循环链表是没有边界问题)。

以单向链表为例,我们正常的插入操作,都是将前一个结点的“后继指针”指向新结点,新结点的“后继指针”指向下一个结点,但如果插入的是新的头结点,因为前一个结点不存在,我们只需要将新的头结点的“后继指针”指向原来的头结点。

而我们正常的删除操作,都是将前一个结点的“后继指针”指向需要删除结点的下一个结点即可,但如果删除的是头结点,由于前一个结点不存在,我们只需要将头结点的引用指向原来的头结点的下一个结点。

有可能会有人有疑问,那插入或删除尾结点是不是也有特殊操作?

并没有。

插入尾结点 node 的操作,伪代码

node.next = tail.next // 先将新的尾结点的“后继指针”指向原来尾结点的下一个结点,也即是 null
tail.next = node // 再将原来的尾结点“后继指针”指向新的尾结点

这个操作和在链表中间插入一个新结点完全一致,不同的是,新插入的尾结点的“后继指针”会指向 null,而不是一个结点。不过如果你写的代码需要时刻记录尾结点,像我上面写的代码一样,LinkedList 既有记录头结点#head,也记录尾结点#tail,你确实需要一个小小的额外操作,把#tail指向新的尾结点,事实上你写的链表也可以不记录尾结点#tail

回到正题,由于存在这种边界问题,在写代码时就很容易遗漏出错,所以这里可以加入了一个特殊结点“哨兵结点”来解决这种边界问题,这个“哨兵结点”的“后继指针”指向头结点,因此“哨兵结点”代替了原来的头结点,作为新的头结点,“哨兵结点”不存储任何的数据,它的数据始终是 null。

我们再来看看插入操作,如果我要插入第一个带有数据的新结点,我们需要将哨兵结点的“后继指针”指向新结点,新结点指向原来的第一个带有数据的结点。这个操作是不是和在链表中间插入新结点的操作完全一致。

删除同理,这里不做详细描述。

image.png

代码实现也很简单,只要在上面的单向链表基础下稍做修改,将原来的头结点改为哨兵结点,再稍微修改一下即可,这里就不实现了。

实践总结

链表虽然是一个很基础的结构,并且我以前就使用过 java 语言实现过一遍,但我在写的过程中依然出现问题,这让我花了一定的时间去排错,这些错误包含“边界没处理好”、“指针没有指到正确的地方”、“忘记把上一个结点的指针指到新结点上”等等。

我做了一个小总结,希望对大家有用,在写链表的时候注意以下几点:

  • 单链表
  1. 插入或删除头结点
  2. 只有一个结点并进行删除这个结点时,需要将头结点和尾结点都赋值为 null
  • 循环链表
  1. 插入或删除头结点,注意头结点和尾结点的联系
  2. 插入或删除尾结点,注意头结点和尾结点的联系
  3. 遍历数据时,注意防止死循环,遇到尾结点就跳出循环
  • 双向链表
  1. 删除或插入中间一个结点,注意 next 和 prev 的指向,可以先在笔记本画图出来,用箭头将结点的关系表示出来,要切断哪些关系,要连接哪些关系
  2. 由于双向链表可以向前找结点,一些查询的操作可以优化(如上文的“双向链表”的 findIndex)