第三章 栈与队列
参考文章:www.hello-algo.com/
3.1 栈(Stack)
3.1.1 栈的定义与特性
- 定义:栈(Stack)是一种遵循“后进先出”(LIFO,Last In First Out)原则的线性数据结构。
- 类比:可以将栈类比为桌面上的一摞盘子,如果想取出底部的盘子,必须先移走上面的盘子。
- 关键术语:
- 栈顶(Top):允许操作的一端,元素从这里入栈和出栈。
- 栈底(Bottom):固定的一端,通常不允许直接操作。
- 入栈(Push):将元素添加到栈顶的操作。
- 出栈(Pop):从栈顶移除元素的操作。
- 查看栈顶元素(Peek):访问栈顶元素而不移除它。
3.1.2 栈的常用操作
- 常用方法:
push(value):将一个元素压入栈顶,时间复杂度为 O(1)。pop():移除并返回栈顶元素,时间复杂度为 O(1)。peek():返回栈顶元素但不移除它,时间复杂度为 O(1)。size():返回栈中元素的数量,时间复杂度为 O(1)。isEmpty():判断栈是否为空,时间复杂度为 O(1)。
3.1.3 栈的实现方式
- 基于数组的实现:
- 使用数组的尾部作为栈顶,通过动态数组(如 Python 的
list或 Java 的ArrayList)实现自动扩容。 - 优点:操作简单,时间复杂度低(均为 O(1))。
- 缺点:如果栈的大小变化频繁,可能会导致多次扩容操作。
- 使用数组的尾部作为栈顶,通过动态数组(如 Python 的
- 基于链表的实现:
- 使用链表的头部作为栈顶,通过链表节点的插入和删除操作实现栈的功能。
- 优点:无需担心扩容问题,适合元素数量变化大的场景。
- 缺点:需要额外的指针操作,代码实现相对复杂。
3.1.4 栈的典型应用
- 括号匹配:检查代码中的括号是否正确匹配。
- 逆波兰表达式求值:用于计算后缀表达式的值。
- 函数调用栈:在程序执行过程中,用于存储函数调用的上下文信息。
- 回溯算法:在解决组合问题、排列问题等时,用于记录状态并回溯到上一步。
- 深度优先搜索(DFS):在图或树的遍历中,用于存储访问路径。
3.1.5 代码示例
以下是基于数组和链表实现栈的代码示例(以 Java 为例):
基于数组的栈实现:
package com.liucc.chapter_stack_and_queue;
import java.util.ArrayList;
import java.util.Arrays;
/** 基于动态数组实现的栈 */
class ArrayStack {
private ArrayList<Integer> stack;
public ArrayStack(){
stack = new ArrayList<>();
}
/* 获取栈的长度 */
public int size(){
return stack.size();
}
/* 判断栈是否为空 */
public boolean isEmpty(){
return size() == 0;
}
/* 查看栈顶元素 */
public int peek(){
if (size() == 0) {
throw new IndexOutOfBoundsException();
}
return stack.get(size()-1);
}
/* 入栈 */
public void push(int num){
stack.add(num);
}
/* 出栈 */
public int pop(){
if (size() == 0) {
throw new IndexOutOfBoundsException();
}
return stack.remove(size()-1);
}
/* 转换为数组 */
public Object[] toArray(){
return stack.toArray();
}
}
public class array_stack {
public static void main(String[] args) {
/* 初始化栈 */
ArrayStack stack = new ArrayStack();
/* 元素入栈 */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
System.out.println("栈 stack = " + Arrays.toString(stack.toArray()));
/* 访问栈顶元素 */
int peek = stack.peek();
System.out.println("栈顶元素 peek = " + peek);
/* 元素出栈 */
int pop = stack.pop();
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray()));
/* 获取栈的长度 */
int size = stack.size();
System.out.println("栈的长度 size = " + size);
/* 判断是否为空 */
boolean isEmpty = stack.isEmpty();
System.out.println("栈是否为空 = " + isEmpty);
}
}
基于链表的栈实现:
package com.liucc.chapter_stack_and_queue;
import java.util.Arrays;
/** 双向链表节点 */
public class ListNode {
public int val;
public ListNode next; // 后继结点
public ListNode prev; // 前驱节点
public ListNode(int val) {
this.val = val;
next = prev = null;
}
}
/** 基于链表实现的栈 */
class LinkedListStack {
private ListNode head; // 头节点作为栈顶元素
private int stackSize; // 栈大小
public LinkedListStack(){
this.head = null;
}
/** 获取栈顶元素 */
public int peek(){
if (size() == 0) {
throw new IndexOutOfBoundsException();
}
return head.val;
}
/** 获取栈的长度 */
public int size(){
return stackSize;
}
/** 栈是否为空 */
public boolean isEmpty(){
return size() == 0;
}
/** 入栈 */
public void push(int num){
ListNode node = new ListNode(num);
node.next = head;
head = node; // 头节点指向新节点
stackSize++;
}
/** 出栈 */
public int pop(){
if (size() == 0) {
throw new IndexOutOfBoundsException();
}
int val = head.val;
head = head.next; // 指针后移
stackSize--;
return val;
}
/** 栈转为数组 */
public int[] toArray(){
// 1->2->3->4
int[] array = new int[size()];
ListNode temp = head;
for (int i = array.length - 1; i >= 0; i--) {
array[i] = temp.val;
temp = temp.next;
}
return array;
}
}
public class linkedlist_stack {
public static void main(String[] args) {
/* 初始化栈 */
LinkedListStack stack = new LinkedListStack();
/* 元素入栈 */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
System.out.println("栈 stack = " + Arrays.toString(stack.toArray()));
/* 访问栈顶元素 */
int peek = stack.peek();
System.out.println("栈顶元素 peek = " + peek);
/* 元素出栈 */
int pop = stack.pop();
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray()));
/* 获取栈的长度 */
int size = stack.size();
System.out.println("栈的长度 size = " + size);
/* 判断是否为空 */
boolean isEmpty = stack.isEmpty();
System.out.println("栈是否为空 = " + isEmpty);
}
}
3.2 队列
3.2.1 队列的定义
- 队列是一种遵循先入先出(FIFO) 规则的线性数据结构。
- 队列模拟了排队现象:新元素加入队列尾部(入队),位于队列头部的元素逐个离开(出队)。
- 队列的头部称为队首,尾部称为队尾。
3.2.2 队列的基本操作
- 入队(push):将元素添加到队尾,时间复杂度为 O(1)。
- 出队(pop):删除队首元素,时间复杂度为 O(1)。
- 访问队首元素(peek):查看队首元素,但不删除,时间复杂度为 O(1)。
- 获取队列长度(size):返回队列中元素的数量。
- 判断队列是否为空(is_empty):检查队列是否为空。
3.2.3 队列的实现方式
队列可以通过以下两种主要方式实现:
- 基于链表的实现:
- 使用链表的头节点作为队首,尾节点作为队尾。
- 入队操作在尾节点后添加新节点,出队操作从头节点删除节点。
- 优点:动态扩展,适合元素数量不确定的场景。
- 基于数组的实现:
- 使用数组模拟队列,通过索引操作实现入队和出队。
- 需要处理数组满或空的情况,可能需要额外的逻辑来循环使用数组空间(循环队列)。
- 优点:访问速度快,适合元素数量相对固定的场景。
3.2.4 队列的典型应用
- 广度优先搜索(BFS):在图和树的遍历中,队列用于逐层访问节点。
- 任务调度:操作系统中,任务按照到达顺序排队等待处理。
- 缓冲区管理:如打印机队列、数据流的缓冲处理。
3.2.5 示例代码
基于链表实现队列的Java代码示例:
package com.liucc.chapter_stack_and_queue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
import com.liucc.linkedlist.ListNode;
/**
* 基于链表实现的队列
*/
class LinkedListQueue{
private ListNode front ,rear; // 队首队尾指针
private int queueSize; // 队列长度
// 构造器初始化
public LinkedListQueue(){
front = null;
rear = null;
}
// 队列是否为空
public boolean isEmpty(){
return queueSize == 0;
}
// 获取队列长度
public int size(){
return queueSize;
}
// 访问队首元素
public int peek(){
if (isEmpty()) {
throw new IndexOutOfBoundsException();
}
return front.val;
}
// 入队
public void push(int num){
ListNode node = new ListNode(num);
// 如果队列为空,队首队尾指针均指向新节点
if (isEmpty()) {
front = node;
rear = node;
}else{ // 队列不为空,新节点添加到尾节点之后
rear.next = node;
rear = node;
}
queueSize++;
}
// 出队
public int pop(){
int peek = peek();
front = front.next;
queueSize--;
return peek;
}
// 转化为数组
public int[] toArray(){
int[] arrays = new int[size()];
ListNode temp = front;
for (int i = 0; i < arrays.length-1; i++) {
arrays[i] = temp.val;
temp = temp.next; // 指针后羿
}
return arrays;
}
}
public class linkedlist_queue {
public static void main(String[] args) {
/* 初始化队列 */
LinkedListQueue queue = new LinkedListQueue();
/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
System.out.println("队列 queue = " + Arrays.toString(queue.toArray()));
/* 访问队首元素 */
int peek = queue.peek();
System.out.println("队首元素 peek = " + peek);
/* 元素出队 */
int pop = queue.pop();
System.out.println("出队元素 pop = " + pop + ",出队后 queue = " + Arrays.toString(queue.toArray()));
/* 获取队列的长度 */
int size = queue.size();
System.out.println("队列长度 size = " + size);
/* 判断队列是否为空 */
boolean isEmpty = queue.isEmpty();
System.out.println("队列是否为空 = " + isEmpty);
}
}
基于数组的实现
数组直接删除元素的时间复杂度为 O(n),这会导致出队操作效率变低。然而,可以采取以下技巧进行优化:
定义变量 front 指向队首元素的索引,并维护变量 queSize 来存储队列的实际长度。可以通过rear = front + queSize 来计算队尾元素的插入位置。
- 入队操作:将元素添加到 rear 位置处,size++即可
- 出队操作:front加 1,size 减 1 即可
这种方式下时间复杂度优化为 O(1)
仍存在的问题:在不断出队、入队的过程中,front和 rear 一直向数组尾部移动,到达数组尾部就无法继续移动。
解决:通过环形数组进行解决,front、rear 到达尾部时再继续跳转到数组头部空闲位置。
示例代码
package com.liucc.chapter_stack_and_queue;
import java.util.Arrays;
/**
* 基于环形数组实现的队列
*/
class ArrayQueue {
private int[] nums; // 存储队列队列元素的数组
private int front; // 队首指针
private int queSize; // 队列实际长度
// 构造器
public ArrayQueue(int capacity) {
nums = new int[capacity]; // 队列容量
}
// 队列容量
public int capacity() {
return nums.length;
}
// 队列实际长度
public int size() {
return queSize;
}
// 队列是否为空
public boolean isEmpty() {
return queSize == 0;
}
// 入队
public void push(int num) {
// 队满,不可再添加元素
if (queSize == capacity()) {
System.out.println("队列已满,不可再添加元素");
return;
}
// 计算新元素应该存放的位置(rear)
int rear = (front + queSize) % capacity();
nums[rear] = num;
queSize++;
}
// 出队
public int pop() {
int num = peek();
front = (front + 1) % capacity();
queSize--;
return num;
}
// 访问队首元素
public int peek() {
if (queSize == 0) {
throw new IndexOutOfBoundsException();
}
return nums[front];
}
// 转换为数组
public int[] toArray(){
int[] arr = new int[queSize];
int idx = front;
for (int i = 0; i < queSize; i++) {
arr[i] = nums[idx];
idx = (idx+1)%capacity();
}
return arr;
}
}
public class array_queue {
public static void main(String[] args) {
/* 初始化队列 */
ArrayQueue queue = new ArrayQueue(5);
/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
queue.push(6); // 队列满,无法正常添加
System.out.println("队列 queue = " + Arrays.toString(queue.toArray()));
/* 访问队首元素 */
int peek = queue.peek();
System.out.println("队首元素 peek = " + peek);
/* 元素出队 */
int pop = queue.pop();
System.out.println("出队元素 pop = " + pop + ",出队后 queue = " + Arrays.toString(queue.toArray()));
/* 获取队列的长度 */
int size = queue.size();
System.out.println("队列长度 size = " + size);
/* 判断队列是否为空 */
boolean isEmpty = queue.isEmpty();
System.out.println("队列是否为空 = " + isEmpty);
}
}
💡
可以发现,基于环形数组实现的队列仍存在局限,数据的长度的固定的,无法自动扩容,但这一点可以采取动态数组进行优化,从而引入扩容机制。
3.3 双向队列
3.3.1 概念
双向队列是一种灵活的数据结构,允许在队列的头部和尾部执行元素的添加或删除操作。它结合了队列和栈的特点,提供了更高的操作灵活性。
3.3.2 常用操作
双向队列支持以下操作,且大多数操作的时间复杂度为 O(1):
| 方法名 | 描述 | 时间复杂度 |
|---|---|---|
push_first() | 在队首添加元素 | O(1) |
push_last() | 在队尾添加元素 | O(1) |
pop_first() | 删除队首元素 | O(1) |
pop_last() | 删除队尾元素 | O(1) |
peek_first() | 访问队首元素 | O(1) |
peek_last() | 访问队尾元素 | O(1) |
size() | 获取队列长度 | O(1) |
is_empty() | 判断队列是否为空 | O(1) |
3.3.3 实现方式
双向队列可以通过以下两种方式实现:
- 基于双向链表的实现:使用双向链表作为底层数据结构,允许在头部和尾部快速插入和删除节点。
- 基于数组的实现:使用数组模拟双向队列,但需要注意数组扩容和收缩的复杂度。
3.3.4 代码实现
基于双向链表的实现
-
节点结构:
/** 双向链表节点 */ public class ListNode { public int val; public ListNode next; // 后继结点 public ListNode prev; // 前驱节点 public ListNode(int val) { this.val = val; next = prev = null; } } -
双向队列类:
package com.liucc.chapter_stack_and_queue; import java.util.Arrays; import com.liucc.linkedlist.ListNode; /* 基于链表实现的双向队列 */ class LinkedListDeque{ private ListNode front, rear; // 队列头节点,尾节点 private int queSize; // 队列长度 public LinkedListDeque(){ front = rear = null; } // 队列是否为空 public boolean isEmpty(){ return queSize == 0; } // 队列大小 public int size(){ return queSize; } // 获取队首元素 public int peekFirst(){ if (isEmpty()) { throw new IndexOutOfBoundsException(); } return front.val; } // 获取队尾元素 public int peekLast(){ if (isEmpty()) { throw new IndexOutOfBoundsException(); } return rear.val; } // 入队 public void push(int num, boolean isFront){ ListNode node = new ListNode(num); // 如果队列为空,front、rear 均指向新节点 if (isEmpty()) { front = node; rear = node; }else{ if (isFront) { // 头插法 front.prev = node; node.next = front; front = node; // 更新头节点 }else{ // 尾插法 rear.next = node; node.prev = rear; rear = node; } } queSize++; } // 队首入队 public void pushFirst(int num){ push(num, true); } // 队尾入队 public void pushLast(int num){ push(num, false); } // 出队 public int pop(boolean isFront){ if (isEmpty()) { throw new IndexOutOfBoundsException(); } int val = -1; if (isFront) { // 保存头节点的值 val = front.val; ListNode temp = front.next; front.next = null; temp.prev = null; front = temp; // 更新头节点 }else{ val = rear.val; ListNode temp = rear.prev; rear.prev = null; temp.next = null; rear = temp; // 更新尾节点 } queSize--; return val; } // 队首出队 public int popFirst(){ return pop(true); } // 队尾出队 public int popLast(){ return pop(false); } // 转为数组 public int[] toArray(){ int[] arr = new int[queSize]; ListNode pointer = front; for (int i = 0; i < queSize; i++) { arr[i] = pointer.val; pointer = pointer.next; // 指针后移 } return arr; } } public class linkedlist_deque { public static void main(String[] args) { /* 初始化双向队列 */ LinkedListDeque deque = new LinkedListDeque(); deque.pushLast(3); deque.pushLast(2); deque.pushLast(5); System.out.println("双向队列 deque = " + Arrays.toString(deque.toArray())); /* 访问元素 */ int peekFirst = deque.peekFirst(); System.out.println("队首元素 peekFirst = " + peekFirst); int peekLast = deque.peekLast(); System.out.println("队尾元素 peekLast = " + peekLast); /* 元素入队 */ deque.pushLast(4); System.out.println("元素 4 队尾入队后 deque = " + Arrays.toString(deque.toArray())); deque.pushFirst(1); System.out.println("元素 1 队首入队后 deque = " + Arrays.toString(deque.toArray())); /* 元素出队 */ int popLast = deque.popLast(); System.out.println("队尾出队元素 = " + popLast + ",队尾出队后 deque = " + Arrays.toString(deque.toArray())); int popFirst = deque.popFirst(); System.out.println("队首出队元素 = " + popFirst + ",队首出队后 deque = " + Arrays.toString(deque.toArray())); /* 获取双向队列的长度 */ int size = deque.size(); System.out.println("双向队列长度 size = " + size); /* 判断双向队列是否为空 */ boolean isEmpty = deque.isEmpty(); System.out.println("双向队列是否为空 = " + isEmpty); } }
基于环形数组的实现
package com.liucc.chapter_stack_and_queue;
import java.util.Arrays;
/* 基于环形数组实现的双向队列 */
class ArrayDeque {
private int[] nums; // 用于存储双向队列元素
private int front; // 计算队首元素索引
private int queSize; // 队列长度
// 构造器
public ArrayDeque(int capacity) {
nums = new int[capacity];
}
// 队列容量
public int capacity() {
return nums.length;
}
public int size(){
return queSize;
}
// 队列是否为空
public boolean isEmpty() {
return queSize == 0;
}
// 计算环形数组的索引
public int index(int i) {
return (i + capacity()) % capacity();
}
// 访问队首元素
public int peekFirst() {
if (isEmpty()) {
throw new IndexOutOfBoundsException();
}
return nums[front];
}
// 访问队尾元素
public int peekLast(){
if (isEmpty()) {
throw new IndexOutOfBoundsException();
}
// 计算队尾元素位置
int rearIdx = index(front+queSize-1);
return nums[rearIdx];
}
// 队首入队
public void pushFirst(int num){
if (queSize == capacity()) {
System.out.println("双向队列已满");
return;
}
// 计算新头指针索引位置
front = index(front-1);
nums[front] = num;
queSize++;
}
// 队尾入队
public void pushLast(int num){
if (queSize == capacity()) {
System.out.println("双向队列已满");
return;
}
// 计算尾指针索引位置
int idx = index(front+queSize);
nums[idx] = num;
queSize++;
}
// 队首出队
public int popFirst(){
if (isEmpty()) {
throw new IndexOutOfBoundsException();
}
int val = nums[front];
front = index(front+1);
queSize--;
return val;
}
// 队尾出队
public int popLast(){
if (isEmpty()) {
throw new IndexOutOfBoundsException();
}
int val = peekLast();
queSize--;
return val;
}
// 转换为数组
public int[] toArray(){
int[] arr = new int[queSize];
int idx = front;
for (int i = 0; i < queSize; i++) {
arr[i] = nums[idx];
idx = index(idx+1);
}
return arr;
}
}
public class array_deque {
public static void main(String[] args) {
/* 初始化双向队列 */
ArrayDeque deque = new ArrayDeque(10);
deque.pushLast(3);
deque.pushLast(2);
deque.pushLast(5);
System.out.println("双向队列 deque = " + Arrays.toString(deque.toArray()));
/* 访问元素 */
int peekFirst = deque.peekFirst();
System.out.println("队首元素 peekFirst = " + peekFirst);
int peekLast = deque.peekLast();
System.out.println("队尾元素 peekLast = " + peekLast);
/* 元素入队 */
deque.pushLast(4);
System.out.println("元素 4 队尾入队后 deque = " + Arrays.toString(deque.toArray()));
deque.pushFirst(1);
System.out.println("元素 1 队首入队后 deque = " + Arrays.toString(deque.toArray()));
/* 元素出队 */
int popLast = deque.popLast();
System.out.println("队尾出队元素 = " + popLast + ",队尾出队后 deque = " + Arrays.toString(deque.toArray()));
int popFirst = deque.popFirst();
System.out.println("队首出队元素 = " + popFirst + ",队首出队后 deque = " + Arrays.toString(deque.toArray()));
/* 获取双向队列的长度 */
int size = deque.size();
System.out.println("双向队列长度 size = " + size);
/* 判断双向队列是否为空 */
boolean isEmpty = deque.isEmpty();
System.out.println("双向队列是否为空 = " + isEmpty);
}
}
3.3.5 应用场景
双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 50 步)。当栈的长度超过 50 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。