一、栈和队列的定义与特点
栈(Stack)
栈是一种后进先出(LIFO, Last In First Out)的数据结构。它只允许在一端(称为栈顶)进行插入和删除操作。
主要操作
- 入栈(Push):将元素添加到栈顶。
- 出栈(Pop):移除并返回栈顶元素。
- 查看栈顶(Peek 或 Top):返回栈顶元素但不移除它。
- 检查栈是否为空(IsEmpty):判断栈是否为空。
- 获取栈的大小(Size):返回栈中元素的数量。
队列(Queue)
队列是一种先进先出(FIFO, First In First Out)的数据结构。它允许在一端(称为队尾)进行插入操作,在另一端(称为队头)进行删除操作。
主要操作
- 入队(Enqueue):将元素添加到队尾。
- 出队(Dequeue):移除并返回队头元素。
- 查看队头(Front):返回队头元素但不移除它。
- 查看队尾(Rear):返回队尾元素但不移除它(在某些实现中可能不支持)。
- 检查队列是否为空(IsEmpty):判断队列是否为空。
- 获取队列的大小(Size):返回队列中元素的数量。
二、常用案例引入
栈(Stack)的常见使用场景
- 函数调用和递归 :
- 在编程中,函数调用栈用于保存函数调用时的状态(如局部变量、返回地址等),以便在函数返回时能够恢复到之前的状态。
- 递归算法也常使用栈来保存中间结果或递归调用的状态。
- 表达式求值 :
- 在编译器和解释器中,栈用于解析和计算数学表达式、逻辑表达式等。
- 例如,逆波兰表示法(RPN)的求值就利用了栈的后进先出特性。
- 括号匹配 :
- 在编译器和文本编辑器中,栈用于检查括号(包括圆括号、方括号、花括号等)是否正确匹配。
- 路径导航 :
- 在文件系统和网页浏览器中,栈用于保存导航历史,以便用户能够回退到之前的页面或目录。
- 语法分析 :
- 在编译器中,栈用于语法分析,特别是用于处理上下文无关文法(CFG)的解析。
队列(Queue)的常见使用场景
- 任务调度 :
- 在操作系统中,队列用于管理等待CPU执行的进程或线程。
- 在多线程编程中,生产者-消费者模型常使用队列来传递任务或数据。
- 打印队列 :
- 在打印机和其他输出设备中,队列用于管理等待打印的文档。
- 广度优先搜索(BFS) :
- 在图论算法中,队列用于实现广度优先搜索,以逐层遍历图的节点。
- 消息传递 :
- 在分布式系统和网络编程中,队列用于在不同进程或线程之间传递消息。
- 事件处理 :
- 在图形用户界面(GUI)编程中,队列用于管理用户输入事件(如鼠标点击、键盘按键等)的处理顺序。
- 网络请求队列 :
- 在Web开发中,服务器通常使用队列来管理来自客户端的网络请求,以确保请求能够按照一定顺序被处理。
- 数据流处理 :
- 在实时数据处理系统中,队列用于缓存和调度数据流中的元素,以便进行进一步的处理和分析。
三、栈的表示和操作的实现
顺序栈
顺序栈是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设一个指针(通常称为栈顶指针)来指示当前栈顶元素的位置。顺序栈的底层实现通常是一个数组,栈顶指针则指向数组中的某个位置,表示栈顶元素。
-
特点:
- 栈底位置固定,栈顶位置动态变化。
- 栈满时,无法再进行入栈操作,除非进行栈的扩容(这通常涉及数组的重新分配和元素的复制,可能带来一定的性能开销)。
- 栈空时,栈顶指针通常指向一个特定的位置(如-1或数组的起始位置),表示栈为空。
-
操作:
- 入栈:将新元素放入栈顶位置,栈顶指针加1。
- 出栈:将栈顶元素弹出,栈顶指针减1。
- 取栈顶元素:获取栈顶元素的值,但不弹出栈顶元素。
顺序栈的表示和实现
顺序栈通常使用一个数组来存储栈中的元素,并使用一个整数变量(称为栈顶指针或top)来记录栈顶元素在数组中的位置。栈顶指针的初始值通常设为-1,表示栈为空。
java
public class SeqStack {
private int[] stackArray; // 存储栈元素的数组
private int top; // 栈顶指针,指向栈顶元素的索引(当栈为空时,top为-1)
private int maxSize; // 栈的最大容量
// 构造函数,初始化栈
public SeqStack(int size) {
maxSize = size;
stackArray = new int[maxSize];
top = -1;
}
// 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
// 判断栈是否已满
public boolean isFull() {
return top == maxSize - 1;
}
// 入栈操作
public boolean push(int elem) {
if (isFull()) {
System.out.println("Stack is full. Cannot push element.");
return false;
} else {
stackArray[++top] = elem;
return true;
}
}
// 出栈操作
public int pop() {
if (isEmpty()) {
System.out.println("Stack is empty. Cannot pop element.");
throw new RuntimeException("Stack Underflow");
} else {
return stackArray[top--];
}
}
// 获取栈顶元素但不移除
public int peek() {
if (isEmpty()) {
System.out.println("Stack is empty. Cannot peek element.");
throw new RuntimeException("Stack is empty");
} else {
return stackArray[top];
}
}
// 获取栈的大小(当前元素个数)
public int size() {
return top + 1;
}
public static void main(String[] args) {
SeqStack stack = new SeqStack(5); // 创建一个容量为5的栈
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top element is: " + stack.peek()); // 输出栈顶元素
System.out.println("Stack size is: " + stack.size()); // 输出栈的大小
System.out.println("Popped element is: " + stack.pop()); // 弹出栈顶元素
System.out.println("Stack size after pop is: " + stack.size()); // 输出弹出后的栈大小
}
}
链栈
链栈则是利用链表来实现栈的存储结构。链栈的栈顶通常设在链表的头部,这样入栈和出栈操作都可以在链表的头部进行,从而保持了栈的后进先出特性。
-
特点:
- 栈顶位置动态变化,链表头部始终指向栈顶元素。
- 不存在栈满的情况,因为链表可以动态增长。
- 栈空时,链表头部指针为NULL,表示栈为空。
-
操作:
- 入栈:创建一个新节点,将新节点的数据域设置为要入栈的元素值,新节点的指针域指向当前栈顶节点(链表头部节点),然后更新链表头部指针指向新节点。
- 出栈:将链表头部节点弹出(即删除链表头部节点),并返回该节点的数据域值作为出栈元素。同时更新链表头部指针指向下一个节点。
- 取栈顶元素:获取链表头部节点的数据域值作为栈顶元素的值,但不删除链表头部节点。
链栈的基本结构
链栈通常由以下部分组成:
- 节点结构:每个节点包含两部分,一部分是存储数据的数据域,另一部分是指向下一个节点的指针域。
- 栈顶指针:指向链栈中最后一个节点(即栈顶元素)的指针,用于快速访问栈顶元素和进行插入、删除操作。
- 空栈标识:通常通过栈顶指针是否为空来判断链栈是否为空。
链栈的基本操作
链栈的基本操作包括初始化、入栈、出栈、获取栈顶元素和判断栈是否为空等。
java
// 定义链栈的节点类
class StackNode {
int data; // 数据域
StackNode next; // 指针域,指向下一个节点
// 构造方法
StackNode(int data) {
this.data = data;
this.next = null;
}
}
// 定义链栈类
public class LinkedStack {
private StackNode top; // 栈顶指针
// 初始化栈
public LinkedStack() {
this.top = null;
}
// 入栈操作
public void push(int value) {
StackNode newNode = new StackNode(value); // 创建新节点
newNode.next = top; // 新节点的next指向当前栈顶
top = newNode; // 栈顶指针指向新节点
}
// 出栈操作
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty. Cannot pop."); // 栈空时抛出异常
}
int value = top.data; // 获取栈顶节点的数据
top = top.next; // 栈顶指针指向下一个节点
return value; // 返回出栈元素的值
}
// 获取栈顶元素但不移除
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty. Cannot peek."); // 栈空时抛出异常
}
return top.data; // 返回栈顶节点的数据
}
// 检查栈是否为空
public boolean isEmpty() {
return top == null;
}
// 主方法,用于测试链栈的基本操作
public static void main(String[] args) {
LinkedStack stack = new LinkedStack();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top element is: " + stack.peek()); // 输出栈顶元素
System.out.println("Popped element is: " + stack.pop()); // 输出并移除栈顶元素
System.out.println("Popped element is: " + stack.pop()); // 输出并移除栈顶元素
System.out.println("Is stack empty? " + stack.isEmpty()); // 检查栈是否为空
System.out.println("Top element is: " + stack.peek()); // 输出栈顶元素
System.out.println("Popped element is: " + stack.pop()); // 输出并移除栈顶元素
System.out.println("Is stack empty? " + stack.isEmpty()); // 检查栈是否为空
}
}
四、队列的表示和操作的实现
基于数组(循环数组):
- 使用固定大小的数组存储队列元素。
- 通过两个指针(front 和 rear)来追踪队列的头部和尾部。
- 当 rear 指针到达数组末尾时,可以回绕到数组的开头。
java
public class CircularArrayQueue {
private int[] queue;
private int front;
private int rear;
private int size;
private int capacity;
// 构造函数,初始化队列
public CircularArrayQueue(int capacity) {
this.capacity = capacity;
this.queue = new int[capacity];
this.front = 0;
this.rear = -1;
this.size = 0;
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 检查队列是否已满
public boolean isFull() {
return size == capacity;
}
// 入队操作
public void enqueue(int item) {
if (isFull()) {
throw new RuntimeException("Queue is full");
}
rear = (rear + 1) % capacity; // 循环到数组开头
queue[rear] = item;
size++;
}
// 出队操作
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int item = queue[front];
if (front == rear) { // 队列中只有一个元素
front = 0;
rear = -1;
} else {
front = (front + 1) % capacity; // 循环到数组开头
}
size--;
return item;
}
// 查看队头元素
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue[front];
}
// 获取队列大小
public int getSize() {
return size;
}
// 测试主方法
public static void main(String[] args) {
CircularArrayQueue queue = new CircularArrayQueue(5);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println(queue.dequeue()); // 输出 1
System.out.println(queue.peek()); // 输出 2
queue.enqueue(4);
queue.enqueue(5);
queue.enqueue(6); // 队列已满,但会覆盖前面的元素(如果实现允许)或抛出异常(如当前实现)
// 注意:上面的enqueue(6)在实际使用中应该是禁止的,因为队列已满,这里只是为了演示可能的覆盖情况(如果逻辑不严格)
// 正确的做法是在enqueue方法中检查是否已满,并抛出异常或采取其他措施
}
}
基于链表:
- 使用链表节点来存储队列元素。
- 每个节点包含数据部分和指向下一个节点的指针。
- front 指针指向队列的头部节点,rear 指针指向队列的尾部节点。
java
// 定义链表节点类
class Node<T> {
T data;
Node<T> next;
Node(T data) {
this.data = data;
this.next = null;
}
}
// 定义链表队列类
public class LinkedListQueue<T> {
private Node<T> front; // 指向队列头部
private Node<T> rear; // 指向队列尾部
private int size; // 队列中的元素数量
// 构造函数,初始化空队列
public LinkedListQueue() {
this.front = null;
this.rear = null;
this.size = 0;
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 入队操作
public void enqueue(T item) {
Node<T> newNode = new Node<>(item);
if (isEmpty()) {
front = rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
size++;
}
// 出队操作
public T dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
T item = front.data;
front = front.next;
if (front == null) { // 如果队列变为空,则重置rear指针
rear = null;
}
size--;
return item;
}
// 查看队头元素
public T peek() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return front.data;
}
// 获取队列大小
public int getSize() {
return size;
}
// 测试主方法
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println(queue.dequeue()); // 输出 1
System.out.println(queue.peek()); // 输出 2
queue.enqueue(4);
System.out.println(queue.getSize()); // 输出 3
}
}
五、栈和队列的比较
|------|------------------|-------------------|
| | 栈(Stack) | 队列(Queue) |
| 基本概念 | 后进先出(LIFO) | 先进先出(FIFO) |
| 操作 | 入栈(Push) | 入队(Enqueue) |
| | 出栈(Pop) | 出队(Dequeue) |
| | 查看栈顶(Peek/Top) | 查看队头(Peek/Front) |
| | 检查栈是否为空(IsEmpty) | 检查队列是否为空(IsEmpty) |
| 使用场景 | 表达式求值 | 任务调度 |
| | 函数调用管理 | 缓冲和排队 |
| | 路径导航(浏览器前进后退) | 广度优先搜索(BFS) |
| | 括号匹配和语法检查 | 消息传递(操作系统消息队列) |
| 实现方式 | 数组或链表 | 数组或链表 |
| 性能 | 时间复杂度:O(1)(基本操作) | 时间复杂度:O(1)(基本操作) |
| | 空间复杂度:取决于实现和元素数量 | 空间复杂度:取决于实现和元素数量 |