一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
1、分析问题?
链表,数组 => 队列?这里只实现最基本的队列:先进先出,删除、添件和记录长度。
这里需要我们先了解一下数组、链表和队列的区别是什么。
- 队列一种逻辑结构,不受语言的限制,在具体的语言中也需要具体的实现。它最主要的特点是先进先出。
- 数组和链表都是物理结构,它受编程语言的限制,有真实的功能,具体的操作方法。
- 在上一篇文章中,我比较通俗地记录了链表和数组的区别,感兴趣的可以看一下。
在之前曾写过“用两个栈实现一个队列”,那里是用数组模拟的栈,最终实现了队列,在最后的进行了复杂度的分析。那篇文章用的是两个数组或者说两个栈实现了一个队列,那么你能否用现有的办法利用一个数组实现队列呢?又如何使用链表来实现一个队列呢?其实更简单的说,这里就是让我们利用数组或者链表来实现一种先进先出的结构,提供插入元素(入队),弹出元素(出队)以及记录队列长度的方法,并通过性能分析来判断其复杂度。
2、解题
1、利用数组实现队列
数组本身就有插入和弹出元素的方法,所以实现一种先进先出的结构还是非常方便的。那就是利用push和shift来进行。
- 代码
//插入元素
array.push()
//弹出元素
array.shift()
array.length
//记录长度
2、利用链表实现队列
-
链表和数组最大的区别是:链表是无序的,链表的每个节点都有next属性。双向链表的实现要比单向链表的实现要复杂很多,并且在这里单向链表已经能够满足需求。
- 首先使用单向链表来实现队列
- 其次要记录链表的head、tail和length。
- 最后要注意元素的插入要从tail端,元素的弹出要从head端,并且要实时记录长度length,而不是遍历队列来进行记录长度length。
- 入队其实在head和tail端都是可以实现的;
- 实现队列的先进先出的特点,在1的前提下,重点考虑出队问题;
- 出队必须在head端进行:因为这里使用的是单向链表,链表中的节点都会存在next属性,如果在head端进行出队,只需要将head的链接动最开始的curNode转移到curNode.next上就可以了,但是如果从tail端进行出队,无法将tail的链接转移,因为单向链表没有prev属性。
- 实时记录或者说单独记录length,为了保证其性能,此时的时间复杂度会是O(1),如果便利队列,肯定可以实现,但是时间复杂度就变成了O(n)。
-
代码实现
/首先定义节点类型
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、 性能分析
- 链表
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、复杂度分析
-
空间复杂度 使用数组和使用链表都是O(n),因为无论使用哪种方式,其所需要的空间和输入的数据都是正相关的,所以都是O(n)
-
时间复杂度
- 数组:O(n) 使用数组的添加方法的时间复杂度是O(1),但是其删除的方法的时间复杂度是O(n),因为使用了shift,其本身就是一个时间复杂度为O(n)的方法,总的来说其时间复杂度是O(n)
- 链表:O(1) 使用链表的添加方法的时间复杂度是O(1),其删除的方法的时间复杂度也是是O(1),包括其获得长度方法的时间复杂度,因为是实时更新,所以也是O(1),总的来说其时间复杂度是O(1)
5、总结
使用链表来实现简单的队列结构,使用链表来实现性能更好。
这里需要我记录的是:
- 了解链表、数组、队列的相关知识
- 选择合适的数据结构优于算法的优化,就像这里,选择链表实现就比你用数组实现要好,甚至使用链表都不需要再进行优化