要实现队列,链表和数组如何选择?

418 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

1、分析问题?

链表,数组 => 队列?这里只实现最基本的队列:先进先出,删除、添件和记录长度
这里需要我们先了解一下数组、链表和队列的区别是什么。

  • 队列一种逻辑结构,不受语言的限制,在具体的语言中也需要具体的实现。它最主要的特点是先进先出
  • 数组和链表都是物理结构,它受编程语言的限制,有真实的功能,具体的操作方法。
    • 在上一篇文章中,我比较通俗地记录了链表和数组的区别,感兴趣的可以看一下

在之前曾写过“用两个栈实现一个队列”,那里是用数组模拟的栈,最终实现了队列,在最后的进行了复杂度的分析。那篇文章用的是两个数组或者说两个栈实现了一个队列,那么你能否用现有的办法利用一个数组实现队列呢?又如何使用链表来实现一个队列呢?其实更简单的说,这里就是让我们利用数组或者链表来实现一种先进先出的结构,提供插入元素(入队),弹出元素(出队)以及记录队列长度的方法,并通过性能分析来判断其复杂度。

2、解题

1、利用数组实现队列

数组本身就有插入和弹出元素的方法,所以实现一种先进先出的结构还是非常方便的。那就是利用push和shift来进行。

  • 代码
//插入元素
array.push()
//弹出元素
array.shift()
array.length
//记录长度

2、利用链表实现队列

  1. 链表和数组最大的区别是:链表是无序的,链表的每个节点都有next属性。双向链表的实现要比单向链表的实现要复杂很多,并且在这里单向链表已经能够满足需求。

    • 首先使用单向链表来实现队列
    • 其次要记录链表的head、tail和length。
    • 最后要注意元素的插入要从tail端,元素的弹出要从head端,并且要实时记录长度length,而不是遍历队列来进行记录长度length。
      1. 入队其实在head和tail端都是可以实现的;
      2. 实现队列的先进先出的特点,在1的前提下,重点考虑出队问题;
      3. 出队必须在head端进行:因为这里使用的是单向链表,链表中的节点都会存在next属性,如果在head端进行出队,只需要将head的链接动最开始的curNode转移到curNode.next上就可以了,但是如果从tail端进行出队,无法将tail的链接转移,因为单向链表没有prev属性。
      4. 实时记录或者说单独记录length,为了保证其性能,此时的时间复杂度会是O(1),如果便利队列,肯定可以实现,但是时间复杂度就变成了O(n)。
  2. 代码实现

/首先定义节点类型
interface IListNode {
    value: number
    next: IListNode | null
}
//定义一个队列的类
class queueClass {
  private head: IListNode | null = null
  private tail: IListNode | null = null
  private len = 0

  //在 tail 位置入队
  add(n: number) {
      const newNode: IListNode = {
          value: n,
          next: null,
      }

      // 处理 head,当head不存在的时候,说明队列是空的,那么head就指向新添加
      if (this.head == null) {
          this.head = newNode
      }

      // 处理 tail
      const tailNode = this.tail
      //如果tail存在,建立当前tail元素和新添件元素之间next的关系,就是改变或者增加当前最后一位元素next属性
      if (tailNode) {
          tailNode.next = newNode
      }
      //添加先元素后,tail将指向新的元素
      this.tail = newNode

      // 记录长度,实时改变
      this.len++
  }

  /**
   * 出队,在 head 位置
   */
  delete(): number | null {
      const headNode = this.head
      if (headNode == null) return null
      if (this.len <= 0) return null

      // 取值,弹出元素的value值
      const value = headNode.value

      // 处理 head,下方写法就是删除掉了当前的首元素
      this.head = headNode.next

      // 记录长度,实时改变
      this.len--

      return value
  }

  get length(): number {
      // length 要单独存储,不能遍历链表来获取(否则时间复杂度太高 O(n))
      return this.len
  }
}
  • 功能测试。
// 功能测试
const myQ = new queueClass()
myQ.add(100)
myQ.add(200)
myQ.add(300)
console.info('length1', myQ.length)//length1 3
console.log(myQ.delete())//100
console.info('length2', myQ.length)//length2 2
console.log(myQ.delete())//200
console.info('length3', myQ.length)//length3 1
console.log(myQ.delete())//300
console.info('length4', myQ.length)//length4 0
console.log(myQ.delete())//null
console.info('length5', myQ.length)//length5 0
//符合预期

3、 性能分析

  1. 链表
const myQ1 = new queueClass()
console.time('myQ1')
for (let i = 0; i < 10 * 10000; i++) {
  myQ1.add(i)// 入队
}
for (let i = 0; i < 10 * 10000; i++) {
  myQ1.delete()// 出队
}
console.timeEnd('myQ1') // myQ1: 16.968017578125 ms

耗时约17ms

2.数组

const myQ2 = []
console.time('myQ2')
for (let i = 0; i < 10 * 10000; i++) {
  myQ2.push(i) // 入队
}
for (let i = 0; i < 10 * 10000; i++) {
  myQ2.shift() // 出队
}
console.timeEnd('myQ2') // queue with array: 441.0078125 ms

耗时约442ms

4、复杂度分析

  1. 空间复杂度 使用数组和使用链表都是O(n),因为无论使用哪种方式,其所需要的空间和输入的数据都是正相关的,所以都是O(n)

  2. 时间复杂度

    • 数组:O(n) 使用数组的添加方法的时间复杂度是O(1),但是其删除的方法的时间复杂度是O(n),因为使用了shift,其本身就是一个时间复杂度为O(n)的方法,总的来说其时间复杂度是O(n)
    • 链表:O(1) 使用链表的添加方法的时间复杂度是O(1),其删除的方法的时间复杂度也是是O(1),包括其获得长度方法的时间复杂度,因为是实时更新,所以也是O(1),总的来说其时间复杂度是O(1)

5、总结

使用链表来实现简单的队列结构,使用链表来实现性能更好。
这里需要我记录的是:

  1. 了解链表、数组、队列的相关知识
  2. 选择合适的数据结构优于算法的优化,就像这里,选择链表实现就比你用数组实现要好,甚至使用链表都不需要再进行优化