线性表数据结构-队列
一种线性表数据结构,是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。
一、介绍
我们把队列中允许插入的一端称为 「队尾(rear)」;把允许删除的另一端称为 「队头(front)」。当表中没有任何数据元素时,称之为 「空队」
队列有两种基本操作:「插入操作」 和 「删除操作」。
- 队列的插入操作又称为「入队」。
- 队列的删除操作又称为「出队」。
简单来说,队列是一种 「先进先出(First In First Out)」 的线性表,简称为 「FIFO 结构」。
我们可以从两个方面来解释一下队列的定义:
- 第一个方面是 「线性表」。
队列首先是一个线性表,队列中元素具有前驱后继的线性关系。队列中元素按照 a1,a2,...,ana_1, a_2, ... , a_na1,a2,...,an 的次序依次入队。队头元素为 a1a_1a1,队尾元素为 ana_nan。
- 第二个方面是 「先进先出原则」。
根据队列的定义,最先进入队列的元素在队头,最后进入队列的元素在队尾。每次从队列中删除的总是队头元素,即最先进入队列的元素。也就是说,元素进入队列或者退出队列是按照「先进先出(First In First Out)」的原则进行的。
二、队列的顺序存储与链式存储
和线性表类似,队列有两种存储表示方法:「顺序存储的队列」 和 「链式存储的队列」
1. 队列的顺序存储
1.1. 初始化空队列
创建一个空队列 self.queue,定义队列大小 self.size。令队头指针 self.front 和队尾指针 self.rear 都指向 -1。即 self.front = self.rear = -1。
1.2. 判断队列是否为空
根据 self.front 和 self.rear 的指向位置关系进行判断。如果队头指针 self.front 和队尾指针 self.rear 相等,则说明队列为空。否则,队列不为空。
1.3. 判断队列是否已满
如果 self.rear 指向队列最后一个位置,即 self.rear == self.size - 1,则说明队列已满。否则,队列未满。
1.4. 插入元素(入队)
先判断队列是否已满,已满直接抛出异常。如果队列不满,则将队尾指针 self.rear 向右移动一位,并进行赋值操作。此时 self.rear 指向队尾元素。
1.5. 删除元素(出队)
先判断队列是否为空,为空直接抛出异常。如果队列不为空,则将队头指针 self.front 指向元素赋值为 None,并将 self.front 向右移动一位。
1.6. 获取队头元素
先判断队列是否为空,为空直接抛出异常。如果队列不为空,因为 self.front 指向队头元素所在位置的前一个位置,所以队头元素在 self.front 后面一个位置上,返回 self.queue[self.front + 1]。
1.7. 获取队尾元素
先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.rear 指向队尾元素所在位置,所以直接返回 self.queue[self.rear]
1.8. 代码(顺序存储)
class Queue {constructor(size = 100) {this.size = size; // 队列的大小,默认为100this.queue = new Array(size); // 使用数组来存储队列元素this.front = -1; // 队头指针,初始为-1this.rear = -1; // 队尾指钫,初始为-1}// 判断队列是否为空isEmpty() {return this.front === this.rear; // 队头和队尾指针相等时为空}// 判断队列是否已满isFull() {return this.rear + 1 === this.size; // 队尾指针到达队列尾部时为已满}// 入队操作enqueue(value) {if (this.isFull()) {throw new Error('队列已满'); // 如果队列已满,抛出异常} else {this.rear++; // 队尾指针加1this.queue[this.rear] = value; // 在队尾插入元素}}// 出队操作dequeue() {if (this.isEmpty()) {throw new Error('队列为空'); // 如果队列为空,抛出异常} else {this.front++; // 队头指针加1,出队return this.queue[this.front]; // 返回出队的元素}}// 获取队头元素frontValue() {if (this.isEmpty()) {throw new Error('队列为空'); // 如果队列为空,抛出异常} else {return this.queue[this.front + 1]; // 获取队头元素,不删除}}// 获取队尾元素rearValue() {if (this.isEmpty()) {throw new Error('Queue is empty'); // 如果队列为空,抛出异常} else {return this.queue[this.rear]; // 获取队尾元素,不删除}}
}// 使用示例
const queue = new Queue(5); // 创建一个大小为5的队列
console.log(queue.isEmpty()); // true
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
console.log(queue.isFull()); // false
console.log(queue.frontValue()); // 1
console.log(queue.rearValue()); // 3
queue.dequeue();
console.log(queue.frontValue()); // 2
2. 循环队列的顺序存储实现
2.1. 出现的问题
在「队列的顺序存储实现」中,如果队列中第 0 ~ size - 1 位置均被队列元素占用时,此时队列已满(即 self.rear == self.size - 1),再进行入队操作就会抛出队列已满的异常。
而由于出队操作总是删除当前的队头元素,将 self.front 进行右移,而插入操作又总是在队尾进行。经过不断的出队、入队操作,队列的变化就像是使队列整体向右移动。
当队尾指针满足 self.rear == self.size - 1 条件时,此时再进行入队操作就会抛出队列已满的异常。而之前因为出队操作而产生空余位置也没有利用上,这就造成了「假溢出」问题
class Queue {constructor(size) {this.size = size;this.queue = new Array(size);this.front = -1;this.rear = -1;}isFull() {return this.rear === this.size - 1;}enqueue(value) {if (this.isFull()) {throw new Error('队列已满')}this.rear++;this.queue[this.rear] = value;}dequeue() {if (this.front === this.rear) {throw new Error('队列为空')}this.front++;}printQueue() {for (let i = this.front + 1; i <= this.rear; i++) {console.log('队内数据', this.queue[i]);}}}const queue = new Queue(5); // 创建一个大小为5的队列// 入队操作queue.enqueue(1);queue.enqueue(2);// 出队 1 出队queue.dequeue();queue.enqueue(3);queue.enqueue(4);queue.enqueue(5);queue.printQueue()// 再次尝试入队queue.enqueue(6); // 队尾指针已经到达队列的末尾,但队列中有空闲位置,会打印提示信息
2.2. 解决思路
将队列想象成为头尾相连的循环表,利用数学中的求模运算,使得空间得以重复利用,这样就解决了问题
3. 环形队列
在进行插入操作时,如果队列的第 self.size - 1 个位置被占用之后,只要队列前面还有可用空间,新的元素加入队列时就可以从第 0 个位置开始继续插入。
我们约定:self.size 为循环队列的最大元素个数。队头指针 self.front 指向队头元素所在位置的前一个位置,而队尾指针 self.rear 指向队尾元素所在位置。则:
- 插入元素(入队)时:队尾指针循环前进 1 个位置,即 self.rear = (self.rear + 1) % self.size。
- 删除元素(出队)时:队头指针循环前进 1 个位置,即 self.front = (self.front + 1) % self.size
3.1. 初始化空队列
创建一个空队列,定义队列大小为 self.size + 1。令队头指针 self.front 和队尾指针 self.rear 都指向 0。即 self.front = self.rear = 0。
3.2. 判断队列是否为空
根据 self.front 和 self.rear 的指向位置进行判断。根据约定,如果队头指针 self.front 和队尾指针 self.rear 相等,则说明队列为空。否则,队列不为空。
3.3. 判断队列是否已满
队头指针在队尾指针的下一位置,即 (self.rear + 1) % self.size == self.front,则说明队列已满。否则,队列未满。
3.4. 插入元素(入队)
先判断队列是否已满,已满直接抛出异常。如果不满,则将队尾指针 self.rear 向右循环移动一位,并进行赋值操作。此时 self.rear 指向队尾元素。
3.5. 删除元素(出队)
先判断队列是否为空,为空直接抛出异常。如果不为空,则将队头指针 self.front 指向元素赋值为 None,并将 self.front 向右循环移动一位。
3.6. 获取队头元素
先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.front 指向队头元素所在位置的前一个位置,所以队头元素在 self.front 后一个位置上,返回 self.queue[(self.front + 1) % self.size]。
3.7. 获取队尾元素
先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.rear 指向队尾元素所在位置,所以直接返回 self.queue[self.rear]
3.8. 代码
class CircularQueue {constructor(size = 100) {this.size = size + 1; // 队列的大小,多出一个位置用于区分队满和队空this.queue = new Array(this.size).fill(null); // 使用数组来存储队列元素this.front = 0; // 队头指针this.rear = 0; // 队尾指针}// 判断队列是否为空isEmpty() {return this.front === this.rear; // 队头和队尾指针相等时为空}// 判断队列是否已满isFull() {return (this.rear + 1) % this.size === this.front; // (队尾+1)%队列大小 等于队头时队满}// 入队操作enqueue(value) {if (this.isFull()) {throw new Error('队列已满');} else {this.rear = (this.rear + 1) % this.size; // 环形操作,确保 rear 在 0 到 size-1 之间循环this.queue[this.rear] = value;}}// 出队操作dequeue() {if (this.isEmpty()) {throw new Error('队列为空');} else {this.queue[this.front] = null;this.front = (this.front + 1) % this.size; // 环形操作,确保 front 在 0 到 size-1 之间循环return this.queue[this.front];}}// 获取队头元素frontValue() {if (this.isEmpty()) {throw new Error('队列为空');} else {const value = this.queue[(this.front + 1) % this.size]; // 获取队头元素,不删除return value;}}// 获取队尾元素rearValue() {if (this.isEmpty()) {throw new Error('队列为空');} else {const value = this.queue[this.rear]; // 获取队尾元素,不删除return value;}}}// 使用示例const queue = new CircularQueue(5); // 创建一个大小为5的环形队列console.log(queue.isEmpty()); // truequeue.enqueue(1);queue.enqueue(2);// 出队一个queue.dequeue();queue.enqueue(3);queue.enqueue(4);queue.enqueue(5);// 这个时候还没满console.log(queue.isFull())// 可以继续添加queue.enqueue(6);
4. 队列的链式存储实现
对于在使用过程中数据元素变动较大,或者说频繁进行插入和删除操作的数据结构来说,采用链式存储结构比顺序存储结构更加合适。
所以我们可以采用链式存储结构来实现队列。
- 我们用一个线性链表来表示队列,队列中的每一个元素对应链表中的一个链节点。
- 再把线性链表的第 1 个节点定义为队头指针 front,在链表最后的链节点建立指针 rear 作为队尾指针。
- 最后限定只能在链表队头进行删除操作,在链表队尾进行插入操作,这样整个线性链表就构成了一个队列
4.1. 初始化空队列
建立一个链表头节点 self.head,令队头指针 self.front 和队尾指针 self.rear 都指向 head。即 self.front = self.rear = head。
4.2. 判断队列是否为空
根据 self.front 和 self.rear 的指向位置进行判断。根据约定,如果队头指针 self.front 等于队尾指针 self.rear,则说明队列为空。否则,队列不为空。
4.3. 插入元素(入队)
创建值为 value 的链表节点,插入到链表末尾,并令队尾指针 self.rear 沿着链表移动 1 位到链表末尾。此时 self.rear 指向队尾元素。
4.4. 删除元素(出队)
先判断队列是否为空,为空直接抛出异常。如果不为空,则获取队头指针 self.front 下一个位置节点上的值,并将 self.front 沿着链表移动 1 位。如果 self.front 下一个位置是 self.rear,则说明队列为空,此时,将 self.rear 赋值为 self.front,令其相等。
4.5. 获取队头元素
先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.front 指向队头元素所在位置的前一个位置,所以队头元素在 self.front 后一个位置上,返回 self.front.next.value。
4.6. 获取队尾元素
先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.rear 指向队尾元素所在位置,所以直接返回 self.rear.value
class Node {constructor(value) {this.value = value;this.next = null;}
}class Queue {constructor() {const head = new Node(0);this.front = head;this.rear = head;}// 判断队列是否为空isEmpty() {return this.front === this.rear; // 队头和队尾指针相等时为空}// 入队操作enqueue(value) {const node = new Node(value);this.rear.next = node;this.rear = node;}// 出队操作dequeue() {if (this.isEmpty()) {throw new Error('队列为空'); // 队列为空时抛出错误} else {const node = this.front.next;this.front.next = node.next;if (this.rear === node) {this.rear = this.front;}const value = node.value;return value;}}// 获取队头元素frontValue() {if (this.isEmpty()) {throw new Error('队列为空'); // 队列为空时抛出错误} else {return this.front.next.value;}}// 获取队尾元素rearValue() {if (this.isEmpty()) {throw new Error('队列为空'); // 队列为空时抛出错误} else {return this.rear.value;}}
}// 使用示例
const queue = new Queue();console.log(queue.isEmpty()); // truequeue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);console.log(queue.frontValue()); // 1
console.log(queue.rearValue()); // 3queue.dequeue();
console.log(queue.frontValue()); // 2queue.enqueue(4);
console.log(queue.rearValue()); // 4// 队列为空时的测试
try {queue.dequeue();
} catch (error) {console.log(error.message); // 输出 "队列为空"
}
三、小练习
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false
1. 思路
根据题意,要用两个队列来实现栈,首先我们知道,队列是先进先出,栈是后进先出。
知道了以上要点,我们两个队列的用处也就一目了然了。
一个队列为主队列,一个为辅助队列,当入栈操作时,我们先将主队列内容导入辅助队列,然后将入栈元素放入主队列队头位置,再将辅助队列内容,依次添加进主队列即可。
2. 代码
class MyStack {constructor() {this.queue = []; // 主队列,用于模拟栈的操作this._queue = []; // 辅助队列,用于在pop操作中临时存储元素}// 入栈操作push(x) {this.queue.push(x);}// 出栈操作pop() {while (this.queue.length > 1) {this._queue.push(this.queue.shift()); // 将主队列中除最后一个元素外的元素转移到辅助队列}let ans = this.queue.shift(); // 弹出最后一个元素,即栈顶元素while (this._queue.length) {this.queue.push(this._queue.shift()); // 恢复主队列的顺序}return ans;}// 获取栈顶元素top() {return this.queue[this.queue.length - 1]; // 获取栈顶元素,即主队列的最后一个元素}// 判断栈是否为空empty() {return this.queue.length === 0;}
}// 使用示例
const stack = new MyStack();stack.push(1);
stack.push(2);
console.log(stack.top()); // 2
console.log(stack.pop()); // 2
console.log(stack.empty()); // false
——未完待续——