前面介绍了一种受限的线性结构:栈结构。它对于解决某些特定问题,会有特别的效果。
队列(Queue),也是一种受限的线性表,它的特性是:先进先出(FIFO First In First Out)。
- 受限之处在于,它只允许在队列的前端(front),进行删除操作;
- 而在队列的后端(rear)进行插入操作;
遍历二叉树的几种方式:
- 先序遍历;
- 中序遍历;
- 后序遍历;
- 层序遍历(要用到队列)。
电影院,商场,排队买票,结账,上厕所。
优先排队的人,优先处理。
打印队列:
- 有若干文档需要打印,打印机会按照文档放入队列的次序,依次从队列中取出文档打印,直到队列中不再有新的文档。
线程队列:
- 在开发中,为了让任务可以并行处理,通常会开启多个线程。
- 但是,不会让大量的线程同时运行处理任务,否则会占用过多的资源;
- 这时,会使用线程队列,依照次序来启动线程,并且处理对应的任务。
当然,队列还有很多应用,后续很多算法中,都会用到队列(比如二叉树的层序遍历)。
队列的实现,和栈一样,有两种方案:
- 基于数组实现;
- 基于链表实现。
队列结构,基于链表实现更好;因为:当队列中的元素需要出队时,意味着要在队列的头部删除元素:
- 在链表中,删除引用,就可以实现元素的删除。
- 数组是一段连续的内存,删除了前面的内容,后面的要向前移,浪费性能。
这里,仍然用数组来封装队列:
demo-project\02-队列\01-ArrayQueue.ts
import type { IQueue } from './iqueue'
class ArrayQueue<T> implements IQueue<T> {
private data: T[]
constructor() {
this.data = []
}
enqueue(item: T) {
this.data.push(item)
}
dequeue(): T | undefined {
return this.data.shift()
}
peek(): T | undefined {
return this.data[0]
}
isEmpty(): boolean {
return this.data.length === 0
}
size(): number {
return this.data.length
}
clear() {
this.data = []
}
}
// 示例用法
const queue = new ArrayQueue<number>()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
console.log(queue.dequeue()) // 输出: 1
console.log(queue.peek()) // 输出: 2
console.log(queue.isEmpty()) // 输出: false
console.log(queue.size()) // 输出: 2
queue.clear()
console.log(queue.isEmpty()) // 输出: true
代码解析:
- 创建了一个
ArrayQueue
类,它是一个泛型类。 - 在类中,定义了一个属性
data
,这个属性用于保存当前队列对象中所有的元素 (和创建栈非常相似)。 - 这个属性,是一个数组类型,之后在队列中,添加、删除元素,都是在这个数组中完成的。
- 队列和栈一样,有一些相关的操作方法,无论是什么语言,操作都是比较类似的。
enqueue(element)
:入队,向队列尾部添加一个(或多个)新的元素。
dequeue()
:出队,移除队列的第一(即排在队列最前面的)元素,并返回被移除的元素。
front/peek()
:返回队列中第一个元素,队列不做任何变动。
isEmpty()
:如果队列中不包含任何元素,返回 true
,否则返回 false
。
size()
:返回队列包含的元素个数,与数组的 length
属性类似。
demo-project\02-队列\type.d.ts
export interface IQueue<T> {
enqueue(item: T): void
dequeue(): T | undefined
peek(): T | undefined
isEmpty(): boolean
size(): number
clear(): void
}
抽取栈、队列接口中,相同的方法。
demo-project\type\ilist.ts
export interface IList<T> {
peek(): T | undefined // 返回栈顶元素但不移除
isEmpty(): boolean // 判断栈是否为空
size(): number // 获取栈的大小
}
demo-project\02-队列\iqueue.ts
import type IList from '../type/ilist'
export interface IQueue<T> extends IList<T> {
enqueue(item: T): void
dequeue(): T | undefined
clear(): void
}
击鼓传花,是一个常见的面试算法题:使用队列,可以非常方便的实现最终的结果。
现在,对游戏规则稍加修改:
- 几个玩家围成一圈,开始数数,数到某个数字的人自动淘汰。
- 以此类推,最后剩下的人,会获得胜利,
- 问:最后剩下的,是原来在哪一个位置上的人?
封装一个基于队列的函数:
- 参数:所有参与人的姓名组成的数组;
- 返回结果:最终剩下的一个人,在原数组中的索引;
代码实现:
demo-project\02-队列\02-面试题-击鼓传花.ts
import { ArrayQueue } from './01-ArrayQueue'
function hotPotato(players: string[], num: number): number {
if (players.length === 0) return -1
// 1.创建队列
const queue = new ArrayQueue<string>()
// 2.所有玩家入队
players.forEach(item => {
queue.enqueue(item)
})
while (queue.size() > 1) {
// 1、2 不淘汰
for (let i = 1; i < num; i++) {
queue.enqueue(queue.dequeue()!)
}
// 3 淘汰
queue.dequeue()
}
// 返回获胜者的索引
return players.indexOf(queue.dequeue()!)
}
console.log(hotPotato(['zzt', 'messi', 'ronaldo', 'krose', 'kevin'], 3))
阿桥问题(也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。
- 人们站在一个等待被处决的圈子里。
- 计数从圆圈中的指定点开始,并沿指定方向,围绕圆圈进行。
- 在跳过指定数量的人之后,处刑下一个人。
- 对剩下的人重复该过程;
- 直到只剩下一个人,并被释放。
在给定数量的情况下,站在哪个位置,可以避免被处决?
这个问题,是以弗拉维奥·约瑟夫命名的,他是 1 世纪的一名犹太历史学家。
- 他在自己的日记中写道,他和他的 40 个战友,被罗马军队包围在洞中。
- 他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定谁杀掉谁。
击鼓传花和约瑟夫环,其实是同一类问题,这种问题还会有其他解法(如动态规划)同样的题目在 Leetcode 上也有
- 0,1,···,n-1 这 n 个数字,排成一个圆圈;
- 从数字 0 开始,每次从这个圆圈里,删除第 m 个数字;
- 删除后从下一个数字开始计数;
- 求出这个圆圈里剩下的最后一个数字。
例如,
- 0、1、2、3、4 这 5 个数字组成一个圆圈;
- 从数字 0 开始每次删除第 3 个数字;
- 则删除的前 4 个数字依次是 2、0、4、1;
- 因此最后剩下的数字是 3。
代码实现:
demo-project\02-队列\03-面试题-约瑟夫环.ts
import { ArrayQueue } from './01-ArrayQueue'
/**
* @description: 此函数用于:解决与瑟夫环问题。
* @Author: ZeT1an
* @param {number} n 所有成员的编号总数,最小值为 1。
* @param {number} m 间隔的个数。
* @return {number} 被释放者的编号。
*/
function josephusProblem<T>(n: number, m: number): number {
const queue = new ArrayQueue<number>()
for (let i = 0; i < n; i++) {
queue.enqueue(i)
}
while (queue.size() > 1) {
for (let i = 1; i < m; i++) {
queue.enqueue(queue.dequeue()!)
}
const index = queue.dequeue()
console.log(`淘汰成员编号: ${index}`)
}
return queue.dequeue()! // 返回获胜者
}
// 示例用法
const winner = josephusProblem(5, 3)
console.log(`最后幸存者编号: ${winner}`)
也可用动态规划来做。
demo-project\02-队列\04-约瑟夫环(动态规划).ts
function lastRemaining(n: number, m: number): number {
let position = 0
for (let i = 2; i <= n; i++) {
position = (position + m) % i
}
return position
}
console.log(lastRemaining(5, 3)) // 3
console.log(lastRemaining(10, 17)) // 2
优先级队列,可用数组,链表,堆(基于二叉树)来实现。