第四章 栈和队列
4.1栈
4.1.1栈的定义
- 栈(后进先出的线性表)是限定仅在表尾进行插入和删除操作的线性表
- 我们把允许插入和删除的一端称为 栈顶,另一端称为 栈底,不含任何数据元素的栈称为 空栈
- 插入元素到栈顶的操作,称为入栈;从栈顶删除最后一个元素的操作,称为出栈
4.1.1.1栈的顺序存储结构
🦊初始化操作
- 由于是顺序存储结构,那么底层存储数据的是数组
private Object[] arr;//存储数据的数组
private int top;//指向栈顶元素的指针
private int length;//存储数据长度
public ArrayStack(int capacity) {
this.arr = new Object[capacity];
this.top = -1;
this.length = 0;
}
🦉入栈操作
- 首先判断栈是否已满
- 如果未满,将top指针加1,并将元素添加到数组中
//入栈
public void push(T element){
if (top == arr.length - 1) {
throw new StackOverflowError();
}
top++;
arr[top]=element;
length++;
}
🦈出栈操作
- 首先需要判断当前栈是否还含有数据
- 当栈中存在数据,取出下标为top的元素并返回,随后将top栈顶指针减一,长度减一
//出栈
public T pop(){
if(length==0){
throw new EmptyStackException();
}
T a= (T) arr[top];
top--;
length--;
return a;
}
🦌获取所有数据
- 首先将top指针赋值给index,随后通过while循环遍历栈内数据,直到index=-1截止循环
//获取所有数据
public void getAllData(){
StringBuilder sb=new StringBuilder();
int index=top;
while(index!=-1){
sb.append(arr[index]).append(",");
index--;
}
String str=sb.toString();
if (str.endsWith(",")){
str=str.substring(0,str.length()-1);
}
System.out.println(str);
}
完整代码
package Stack;
import java.util.EmptyStackException;
public class ArrayStack<T> {
private Object[] arr;//存储数据的数组
private int top;//指向栈顶元素的指针
private int length;//存储数据长度
public ArrayStack() {
}
public int getLength() {
return length;
}
public ArrayStack(int capacity) {
this.arr = new Object[capacity];
this.top = -1;
this.length = 0;
}
//入栈
public void push(T element){
if (top == arr.length - 1) {
throw new StackOverflowError();
}
top++;
arr[top]=element;
length++;
}
//出栈
public T pop(){
if(length==0){
throw new EmptyStackException();
}
T a= (T) arr[top];
top--;
length--;
return a;
}
//获取栈顶元素
public T peek(){
if (length==0){
throw new EmptyStackException();
}
return (T) arr[top];
}
//获取所有数据
public void getAllData(){
StringBuilder sb=new StringBuilder();
int index=top;
while(index!=-1){
sb.append(arr[index]).append(",");
index--;
}
String str=sb.toString();
if (str.endsWith(",")){
str=str.substring(0,str.length()-1);
}
System.out.println(str);
}
}
测试
package Stack;
public class test {
public static void main(String[] args) {
ArrayStack<String> stack=new ArrayStack<>(20);
stack.push("1");
stack.push("2");
stack.push("3");
stack.push("4");
//获取所有数据
stack.getAllData();//4,3,2,1
//获取栈顶元素
System.out.println(stack.peek());//4
//出栈
System.out.println(stack.pop());//4
stack.getAllData();//321
//获取栈的数据长度
System.out.println(stack.getLength());//3
}
}
4.1.1.2两栈共享空间
-
两栈共享空间是一种数据结构,它允许两个栈
共享一个数组的空间
。这意味着这个数组被分成两个部分,每个部分都可以被一个栈使用。 -
在两栈共享空间中,两个栈可以在数组的
两端开始,向中间移动
。 -
对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数组空间,这就可以最大化的利用数组的空间
代码实现
🌾实现思路
- 定义一个整型数组作为两个栈的存储空间,同时定义两个整型变量分别表示两个栈的栈顶位置;
- 对于栈1的入栈操作,检查数组是否已满,如果未满则将元素加入栈1中,同时将栈1的栈顶位置加1;
- 对于栈2的入栈操作,同样检查数组是否已满,如果未满则将元素加入栈2中,同时将栈2的栈顶位置减1;
- 对于栈1的出栈操作,检查栈是否为空,如果非空则将栈1的栈顶元素弹出并返回,同时将栈1的栈顶位置减1;
- 对于栈2的出栈操作,同样检查栈是否为空,如果非空则将栈2的栈顶元素弹出并返回,同时将栈2的栈顶位置加1;
- 在进行栈操作时,需要注意栈的溢出和下溢问题,以保证栈的正确性。
🌿完整代码
package Stack;
public class ShareStack {
private int[] arr;
private int top1;
private int top2;
public ShareStack() {
this.arr = new int[10];
this.top1 = -1;
this.top2 = arr.length;
}
public void push1(int value) {
if (top1 < top2 - 1) {
arr[++top1] = value;
} else {
System.out.println("Stack Overflow");
}
}
public void push2(int value) {
if (top1 < top2 - 1) {
arr[--top2] = value;
} else {
System.out.println("Stack Overflow");
}
}
public int pop1() {
if (top1 >= 0) {
return arr[top1--];
} else {
System.out.println("Stack Underflow");
return -1;
}
}
public int pop2() {
if (top2 < arr.length) {
return arr[top2++];
} else {
System.out.println("Stack Underflow");
return -1;
}
}
}
4.1.2栈的链式存储结构
对于链式存储结构的栈,栈顶是指向最新入栈的元素的指针。由于链表是从头结点开始,一直延伸到尾结点的,因此在链式存储结构中,将栈顶指针指向链表的头结点,可以使得入栈和出栈操作都在链表的头部进行,这样可以方便的进行插入和删除操作。
代码实现
🍉完整代码
package Stack;
public class LinkedStack<T> {
private Node<T> top;
private static class Node<T> {
private T data;
private Node<T> next;
public Node(T data) {
this.data = data;
}
}
public boolean isEmpty() {
return top == null;
}
public void push(T data) {
Node<T> newNode = new Node<>(data);
newNode.next = top;
top = newNode;
}
public T pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
T data = top.data;
top = top.next;
return data;
}
}
🍊测试结果
package Stack;
public class test2 {
public static void main(String[] args) {
LinkedStack<String> stack = new LinkedStack<>();
stack.push("Hello");
stack.push("World");
System.out.println(stack.pop()); // 输出 "World"
System.out.println(stack.pop()); // 输出 "Hello"
}
}
4.1.3栈的应用——四则运算表达式求值
4.1.3.1后缀表达式的定义
后缀表达式,也叫做逆波兰表达式,是一种不需要括号的表达式表示方法。在后缀表达式中,运算符位于操作数之后。例如,中缀表达式3+4的后缀表达式表示为3 4 +
后缀表达式的优点是计算过程中不需要考虑运算符的优先级,直接按顺序进行操作即可
4.1.3.2中缀表达式转后缀表达式
🙈转换规则
1. 初始化一个空栈和一个空字符串来存储后缀表达式。
2. 从左到右扫描中缀表达式的每一个元素。
3. 如果当前元素是操作数(数字或变量),则将其添加到后缀表达式字符串的末尾。
3. 如果当前元素是左括号“(”,则将其压入栈中。
4. 如果当前元素是右括号“)”,则将栈中的元素弹出并添加到后缀表达式直到遇到左括号。左右括号不需要添加到后缀表达式中。
5. 如果当前元素是操作符(加、减、乘、除等),则将其与栈顶操作符进行比较。
6. 如果栈顶操作符的优先级大于或等于当前操作符,则弹出栈顶操作符并添加到后缀表达式字符串中,然后继续比较当前操作符与新的栈顶操作符。
7. 如果当前操作符的优先级大于栈顶操作符,则将当前操作符压入栈中。
8. 当中缀表达式扫描结束后,将栈中剩余的操作符弹出并添加到后缀表达式中,直到栈为空。
9. 后缀表达式即为转换后的结果。
🙉例子
假设有一个中缀表达式:
- 初始化一个空栈和一个空字符串:stack = [], postfix = “”
- 从左到右扫描中缀表达式的每一个元素。
- 是操作数,将其添加到后缀表达式字符串的末尾:postfix = “2”
- 是操作符,将其压入栈中:stack = [“+”]
- 是操作数,将其添加到后缀表达式字符串的末尾:postfix = “2 3”
- 是操作符,将其压入栈中:stack = [“+”, “*”]
- 是左括号,将其压入栈中:stack = [“+”, “*”, “(”]
- 是操作数,将其添加到后缀表达式字符串的末尾:postfix = “2 3 4”
- 是操作符,将其压入栈中:stack = [“+”, “*”, “(”, “-”]
- 是操作数,将其添加到后缀表达式字符串的末尾:postfix = “2 3 4 1”
- 是右括号,将栈中的元素弹出并添加到后缀表达式直到遇到左括号:postfix = “2 3 4 1 -”, stack = [“+”, “*”]
- 当中缀表达式扫描结束后,将栈中剩余的操作符弹出并添加到后缀表达式中,直到栈为空:postfix = “2 3 4 1 - * +”
- 后缀表达式即为转换后的结果:2 3 4 1 - * +
4.1.3.3 后缀表达式计算结果
🙈计算规则
1. 初始化一个空栈。
2. 从左到右扫描后缀表达式的每一个元素。
3. 如果当前元素是操作数(数字或变量),则将其压入栈中。
4. 如果当前元素是操作符(加、减、乘、除等),则从栈中弹出两个操作数进行运算,并将结果压入栈中。
5. 当扫描结束后,栈中只剩下一个元素,即为后缀表达式的计算结果。
🙉例子
假设有一个后缀表达式:
-
初始化一个空栈:stack = []
-
从左到右扫描后缀表达式的每一个元素。
- 2 是操作数,将其压入栈中:stack = [2]
- 3 是操作数,将其压入栈中:stack = [2, 3]
- 4 是操作数,将其压入栈中:stack = [2, 3, 4]
- 1 是操作数,将其压入栈中:stack = [2, 3, 4, 1]
- 是操作符,从栈中弹出两个操作数进行运算,并将结果压入栈中:stack = [2, 3, 3]
- 是操作符,从栈中弹出两个操作数进行运算,并将结果压入栈中:stack = [2, 9]
- 是操作符,从栈中弹出两个操作数进行运算,并将结果压入栈中:stack = [11]
-
当扫描结束后,栈中只剩下一个元素,即为后缀表达式的计算结果。
-
因此,后缀表达式 2 3 4 1 - * + 的计算结果为 11。
4.2队列
4.2.1队列的定义
- 队列是只允许在
一端进行插入操作,而在另一端进行删除操作
的线性表- 队列是一种先进先出的线性表,允许插入的一端称为队尾,允许删除的一端称为队头
4.2.2循环队列
4.2.2.1队列顺序存储的不足
-
存储空间的浪费:在队列顺序存储中,需要预分配一定的存储空间,但实际使用时可能
会出现存储空间的浪费
。例如,当队列中的元素个数小于数组长度时,数组中的一部分空间就会被浪费。 -
队列长度固定:由于队列顺序存储需要预先分配一定的存储空间,因此队列的长度是固定的,无法动态扩展。如果
队列中的元素个数超过了数组长度,就会出现队列溢出的问题
。 -
插入和删除操作低效:在队列顺序存储中,插入和删除操作需要移动元素,效率较低。例如,当队列中的元素出队时,
需要将队列中剩余的元素向前移动一个位置,以保证队列的连续性
。 -
队头出队困难:在队列顺序存储中,出队操作通常都是从队头开始的。但是
如果队列中有大量元素时,出队操作的效率会较低
,因为需要将队列中剩余的元素向前移动一个位置。
4.2.2.2循环队列
我们把队列的这种首尾相接的顺序存储结构称为循环队列
🎅队列满条件
针对循环队列,引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置
当持续向循环队列中插入元素,出现以下情况
这个时候front和rear指针指向同一个位置,这跟队列为空时的情况是相同的,那怎么来判断队列是否为空,又或是队满呢?
可采取两种方法进行区分:
方法一
设置一个计数器来记录队列中的元素个数,当队列中的元素个数等于队列的容量时,队列就被认为是已满的。
方法二
使用front指针和rear指针的相对位置。在循环队列中,当rear指针指向的位置的下一个位置等于front指针的位置时
,说明队列已满。
我们假设循环队列的容量为n,则队列已满的条件:
(rear+1)%n==front
需要注意的是,在循环队列的实现中,为了避免队列满时rear指针和front指针指向同一个位置,我们通常会浪费一个数组元素的空间
,及当数组中有n个元素时,实际只存储了n-1个元素,用来区分队列是满还是空
🎄通用的计算循环队列长度
假设循环队列的容量为n,则计算循环队列的长度:
(rear - front + n) % n
4.2.2.3循环队列的代码实现
经过上面的学习,定义一个循环队列是非常简单的啦,快来尝试一番吧
🍉完整代码
package Queue;
public class CircularQueue {
private int[] arr;
private int front;
private int rear;
private int capacity;
public CircularQueue(int capacity) {
arr = new int[capacity];
front = 0;
rear = 0;
this.capacity = capacity;
}
public CircularQueue() {
}
public boolean isEmpty(){
return front==rear;//判断队列是否为空
}
public boolean isFull(){
return (rear + 1) % capacity == front; // 判断队列是否已满
}
public int size() {
return (rear - front + capacity) % capacity; // 计算队列长度
}
public void enqueue(int data) {
if (isFull()) {
throw new RuntimeException("Queue is full.");
}
arr[rear] = data; // 插入元素到队尾
rear = (rear + 1) % capacity; // 移动队尾指针
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty.");
}
int data = arr[front]; // 取出队头元素
front = (front + 1) % capacity; // 移动队头指针
return data;
}
}
🍊测试
package Queue;
public class test {
public static void main(String[] args) {
CircularQueue queue = new CircularQueue(5);
System.out.println("Is queue empty? " + queue.isEmpty()); // true
System.out.println("Is queue full? " + queue.isFull()); // false
System.out.println("Queue size: " + queue.size()); // 0
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println("Is queue empty? " + queue.isEmpty()); // false
System.out.println("Is queue full? " + queue.isFull()); // false
System.out.println("Queue size: " + queue.size()); // 3
System.out.println("Dequeue: " + queue.dequeue()); // 1
System.out.println("Queue size: " + queue.size()); // 2
queue.enqueue(4);
queue.enqueue(5);
System.out.println("Is queue full? " + queue.isFull()); // true
try {
queue.enqueue(6); // throws exception
} catch (Exception e) {
System.out.println("Enqueue exception: " + e.getMessage()); // Queue is full.
}
while (!queue.isEmpty()) {
System.out.println("Dequeue: " + queue.dequeue());
}
System.out.println("Queue size: " + queue.size()); // 0
}
}
🍇结果
Is queue empty? true
Is queue full? false
Queue size: 0
Is queue empty? false
Is queue full? false
Queue size: 3
Dequeue: 1
Queue size: 2
Is queue full? true
Enqueue exception: Queue is full.
Dequeue: 2
Dequeue: 3
Dequeue: 4
Dequeue: 5
Queue size: 0
4.2.3队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已。
为了操作上的方便,我们将队头指针指向队列的头结点,而队尾指针指向终端结点,空队列时,front和rear都指向头结点。
🍉完整代码
package LinkedQue;
import java.util.NoSuchElementException;
public class LinkedQueue<T> {
private Node<T> front; // 队头指针
private Node<T> rear; // 队尾指针
// 节点类
private static class Node<T> {
T data; // 数据域
Node<T> next; // 指针域
Node(T data) {
this.data = data;
this.next = null;
}
}
public LinkedQueue() {
this.front = null;
this.rear = null;
}
// 判断队列是否为空
public boolean isEmpty() {
return front == null;
}
// 元素入队
public void enqueue(T data) {
Node<T> newNode = new Node<>(data);
if (isEmpty()) {
front = newNode;
rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
}
// 元素出队
public T dequeue() {
if (isEmpty()) {
throw new NoSuchElementException("Queue is empty");
} else {
T data = front.data;
front = front.next;
if (front == null) {
rear = null;
}
return data;
}
}
// 获取队头元素
public T peek() {
if (isEmpty()) {
throw new NoSuchElementException("Queue is empty");
} else {
return front.data;
}
}
// 获取队列长度
public int size() {
int count = 0;
Node<T> current = front;
while (current != null) {
count++;
current = current.next;
}
return count;
}
}
🍊测试
package LinkedQue;
import Stack.LinkedStack;
public class test {
public static void main(String[] args) {
LinkedQueue<Integer> queue = new LinkedQueue<>();
System.out.println("Queue is empty: " + queue.isEmpty()); // true
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println("Queue size: " + queue.size()); // 3
int front = queue.peek();
System.out.println("Front element: " + front); // 1
int element = queue.dequeue();
System.out.println("Dequeue element: " + element); // 1
System.out.println("Queue size: " + queue.size()); // 2
element = queue.dequeue();
System.out.println("Dequeue element: " + element); // 2
System.out.println("Queue size: " + queue.size()); // 1
element = queue.dequeue();
System.out.println("Dequeue element: " + element); // 3
System.out.println("Queue is empty: " + queue.isEmpty()); // true
// queue.dequeue(); // throws NoSuchElementException
// queue.peek(); // throws NoSuchElementException
}
}
🍇结果
Queue is empty: true
Queue size: 3
Front element: 1
Dequeue element: 1
Queue size: 2
Dequeue element: 2
Queue size: 1
Dequeue element: 3
Queue is empty: true
Exception in thread "main" java.util.NoSuchElementException: Queue is empty
at Queue.peek(Queue.java:37)
at QueueTest.main(QueueTest.java:30)