在卷子里考察的会拿红色着重注明。
3 线性表、栈、队列
3.1 掌握线性表的逻辑结构以及基本操作
23-24学年数据结构与算法Ⅲ的期末卷考察点
- 选择题 考察了链表相对于数组的优点(一般不会队满)
- 填空题 考察了单链表中头节点的意义(在插入在表头或者删除第一个结点时不用考虑特殊情况,使空表和非空表的处理统一起来)
- 算法设计题 利用链表类(使用Node)设计程序。
线性表的逻辑结构:
线性表是由n个数据元素构成的有限序列,其中元素之间存在唯一的前驱和后继关系。线性表可以为空表,也可以包含一个或多个元素。线性表有两种常见的实现方式:顺序表和链表。
线性表的基本操作
-
插入(Insert): 在线性表的指定位置插入一个新的元素。
void insert(ElementType item, int position);
-
删除(Delete): 删除线性表中指定位置的元素。
void delete(int position);
-
查找(Search): 在线性表中查找指定元素的位置。
int search(ElementType item);
-
遍历(Traverse): 依次访问线性表中的每个元素。
void traverse();
以上的基本操作适用于顺序表和链表两种实现方式。在顺序表中,元素在内存中是连续存储的;而在链表中,元素通过 节点 和 指针 的方式连接在一起,可以是连续的,也可以是分散的。
在实际应用中,选择顺序表还是链表取决于具体的需求。顺序表适用于对元素的随机访问较多的情况,而链表适用于频繁插入和删除操作的情况。
下面是一个简单的链表实现的 Java 代码示例,包括插入、删除和遍历操作:
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
public class LinkedList {
Node head;
// 插入操作
void insert(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = newNode;
}
}
// 删除操作
void delete(int data) {
if (head == null) {
return;
}
if (head.data == data) {
head = head.next;
return;
}
Node temp = head;
while (temp.next != null && temp.next.data != data) {
temp = temp.next;
}
if (temp.next != null) {
temp.next = temp.next.next;
}
}
// 遍历操作
void traverse() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " ");
temp = temp.next;
}
System.out.println();
}
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.insert(1);
list.insert(2);
list.insert(3);
System.out.println("Original List:");
list.traverse();
list.delete(2);
System.out.println("List after deleting element 2:");
list.traverse();
}
}
3.2 掌握用顺序存储结构对线性表基本操作的实现
当使用顺序存储结构(数组)实现线性表时,我们可以通过数组来存储元素,并通过数组的索引来表示元素在线性表中的位置。
顺序存储结构下线性表的基本操作:
-
插入(Insert): 在指定位置插入一个新的元素。
void insert(int[] array, int size, int position, int element) { if (position < 0 || position > size) { System.out.println("Invalid position"); return; } // 将插入位置后的元素向后移动一位 for (int i = size - 1; i >= position; i--) { array[i + 1] = array[i]; } // 在插入位置插入新元素 array[position] = element; }
-
删除(Delete): 删除指定位置的元素。
void delete(int[] array, int size, int position) { if (position < 0 || position >= size) { System.out.println("Invalid position"); return; } // 将删除位置后的元素向前移动一位 for (int i = position; i < size - 1; i++) { array[i] = array[i + 1]; } }
-
查找(Search): 查找指定元素的位置。
int search(int[] array, int size, int element) { for (int i = 0; i < size; i++) { if (array[i] == element) { return i; // 返回元素的位置 } } return -1; // 元素不存在 }
-
遍历(Traverse): 遍历整个线性表。
void traverse(int[] array, int size) { for (int i = 0; i < size; i++) { System.out.print(array[i] + " "); } System.out.println(); }
3.3 掌握链式存储结构的实现技术,比如单向链表以及带头节点的链表
当使用链式存储结构(链表)实现线性表时,我们可以通过节点之间的指针关系来表示元素的逻辑顺序。链表的基本操作包括插入、删除、查找和遍历。
单向链表的基本操作:
单向链表中,每个节点包含一个数据域和一个指向下一个节点的指针。
-
插入(Insert): 在指定位置插入一个新的节点。
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } void insert(Node head, int position, int element) { Node newNode = new Node(element); // 找到插入位置的前一个节点 Node temp = head; for (int i = 0; i < position - 1 && temp != null; i++) { temp = temp.next; } if (temp == null) { System.out.println("Invalid position"); return; } // 插入新节点 newNode.next = temp.next; temp.next = newNode; }
-
删除(Delete): 删除指定位置的节点。
void delete(Node head, int position) { // 找到删除位置的前一个节点 Node temp = head; for (int i = 0; i < position - 1 && temp != null; i++) { temp = temp.next; } if (temp == null || temp.next == null) { System.out.println("Invalid position"); return; } // 删除节点 temp.next = temp.next.next; }
-
查找(Search): 查找指定元素的位置。
int search(Node head, int element) { Node temp = head; int position = 0; while (temp != null && temp.data != element) { temp = temp.next; position++; } if (temp == null) { return -1; // 元素不存在 } else { return position; // 返回元素的位置 } }
-
遍历(Traverse): 遍历整个链表。
void traverse(Node head) { Node temp = head; while (temp != null) { System.out.print(temp.data + " "); temp = temp.next; } System.out.println(); }
带头节点的链表的基本操作:
带头节点的链表中,头节点不存储实际的数据,仅用于标识链表的起始位置。初始化:
package linklist;
public class ListNode {
public int id;
//数据域
public String data;
//指针域
public ListNode next;
public ListNode(int id,String data){
this.id = id;
this.data = data;
}
@Override
public String toString() {
return "ListNode{" +
"id=" + id +
", data='" + data + '\'' +
'}';
}
}
3.4 掌握链式存储结构对线性表基本结构的实现
具体实现:
package linklist;
public class SingleLinkList {
//先初始化一个头节点,头节点不存放具体数据
private ListNode head;
public void initSingLinkList() {
head = new ListNode(0, "");
}
//添加节点
public void insertNode(ListNode listNode) {
//使用temp指针进行遍历
ListNode temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = listNode;
}
//打印链表
public void showSingleLinkList() {
if (isEmpty()) {
return;
}
ListNode temp = head.next;
while (true) {
if (temp == null) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
//判断链表是否为空
public boolean isEmpty() {
if (head.next == null) {
System.out.println("链表为空");
return true;
}
return false;
}
//按照id顺序添加节点
public void insertByOrder(ListNode listNode) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.id > listNode.id) {
break;
} else if (temp.next.id == listNode.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag == true) {
System.out.println("准备插入的数据编号已存在,无法添加");
} else {
listNode.next = temp.next;
temp.next = listNode;
System.out.println("插入成功");
}
}
//修改节点数据
public void updateNode(ListNode listNode) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.id == listNode.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.data = listNode.data;
} else {
System.out.println("没有找到要修改的节点");
}
}
//删除节点
public void delNode(int id) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.id == id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("要删除的节点不存在");
}
}
//获取头节点
public void getHeadNode() {
ListNode temp = head;
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法获取头节点");
} else {
temp = temp.next;
System.out.println("头节点为:" + temp);
}
}
//获取尾节点
public void getTailNode() {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法获取尾节点");
break;
}
if (temp.next == null) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("尾节点为:" + temp);
} else {
System.out.println("无法找到该节点");
}
}
//清空链表
public void clearLinkList() {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法清空");
break;
}
temp = temp.next;
temp.data = "";
if (temp.next == null) {
flag = true;
break;
}
}
if (flag) {
System.out.println("清空完成");
}
}
//获取长度
public void getLength() {
ListNode temp = head;
int length = 0;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空长度为"+length);
break;
}
temp = temp.next;
length++;
if (temp.next == null) {
System.out.println("该链表为空长度为"+length);
break;
}
}
}
}
3.5 具有在实际中选取不同存储结构的判断能力
一些在选择存储结构时需要考虑的因素:
-
访问方式:
- 如果需要频繁按照索引或位置进行随机访问,数组(顺序存储结构)可能更为合适,因为数组的元素在内存中是连续存储的,可以通过索引直接访问。
- 如果主要进行插入、删除等操作,链表(链式存储结构)可能更为适用,因为链表在插入和删除时不需要移动大量元素,操作相对灵活。
-
元素的动态性:
- 如果元素的数量变化不大且已知,使用数组可能更为简便,因为数组的大小是固定的。
- 如果元素的数量变化较大或者无法预知,使用链表更具弹性,可以根据需要动态分配内存。
-
内存空间效率:
- 数组通常对内存的利用更为高效,因为它们是连续存储的,不需要额外的指针信息。
- 链表可能会占用更多的内存,因为每个节点需要额外的指针空间。
-
插入和删除的复杂性:
- 插入和删除操作对于链表而言通常更为高效,因为它们不需要移动大量元素。
- 数组中的插入和删除可能需要移动大量元素,因此这些操作的复杂度可能较高。
-
存储元素类型的灵活性:
- 如果元素类型不固定或者元素类型的变化较大,使用链表更为灵活,因为它可以容纳不同类型的节点。
- 数组在创建时需要指定元素的类型,并且所有元素的类型必须相同。
-
对缓存的利用:
- 数组在内存中是连续存储的,这有助于更好地利用缓存,因为缓存通常以块的形式读取数据。
- 链表的节点在内存中可能不是连续存储的,这可能导致缓存效率降低。
-
对算法的要求:
- 不同算法对数据的存储结构有不同的要求。例如,某些排序算法可能对数组更加友好,而某些图算法可能对链表更加适用。
在实际选择存储结构时,需要综合考虑这些因素,并根据具体问题的性质权衡它们。在某些情况下,也可以考虑使用一些高级的数据结构,如哈希表、树等,以满足更复杂的需求。
3.6 掌握栈、队列的逻辑结构以及基本操作
栈(Stack)
- 定义: 栈是一种具有后进先出(Last In First Out,LIFO)特性的线性数据结构,即最后进入的元素最先被访问。
- 逻辑结构: 栈可以看作是一种受限的线性表,只能在表的一端(称为栈顶)进行插入和删除操作。
- 基本操作:
- 入栈(Push): 将元素添加到栈的顶部。
- 出栈(Pop): 从栈的顶部移除元素。
- 获取栈顶元素(Top): 查看栈顶元素,但不对栈进行修改。
- 判空(isEmpty): 判断栈是否为空。
- 获取栈的大小(Size): 统计栈中元素的个数。
队列(Queue)
- 定义: 队列是一种具有先进先出(First In First Out,FIFO)特性的线性数据结构,即最先进入的元素最先被访问。
- 逻辑结构: 队列可以看作是一种受限的线性表,只能在表的一端(称为队尾)进行插入操作,在另一端(称为队头)进行删除操作。
- 基本操作:
- 入队(Enqueue): 将元素添加到队列的尾部。
- 出队(Dequeue): 从队列的头部移除元素。
- 获取队头元素(Front): 查看队列头部的元素,但不对队列进行修改。
- 判空(isEmpty): 判断队列是否为空。
- 获取队列大小(Size): 统计队列中元素的个数。
栈和队列的比较
- 栈适合解决需要后进先出的问题,如递归、表达式求值等。
- 队列适合解决需要先进先出的问题,如任务调度、广度优先搜索等。
- 在某些场景中,还可以使用双端队列(Deque)来结合队列和栈的特性,提供更灵活的操作。
下面是基于Java的简单实现:
import java.util.LinkedList;
// 栈的实现
class Stack {
private LinkedList<Integer> stack;
public Stack() {
stack = new LinkedList<>();
}
public void push(int element) {
stack.addLast(element);
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.removeLast();
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.getLast();
}
public boolean isEmpty() {
return stack.isEmpty();
}
public int size() {
return stack.size();
}
}
// 队列的实现
class Queue {
private LinkedList<Integer> queue;
public Queue() {
queue = new LinkedList<>();
}
public void enqueue(int element) {
queue.addLast(element);
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue.removeFirst();
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue.getFirst();
}
public boolean isEmpty() {
return queue.isEmpty();
}
public int size() {
return queue.size();
}
}
3.7 掌握顺序存储结构对栈和队列基本操作的实现
顺序存储结构对栈的基本操作
class ArrayStack {
private int[] stack;
private int top; // 栈顶指针
public ArrayStack(int capacity) {
stack = new int[capacity];
top = -1;
}
public void push(int element) {
if (top == stack.length - 1) {
throw new RuntimeException("Stack is full");
}
stack[++top] = element;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack[top--];
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack[top];
}
public boolean isEmpty() {
return top == -1;
}
public int size() {
return top + 1;
}
}
顺序存储结构对队列的基本操作
class ArrayQueue {
private int[] queue;
private int front; // 队头指针
private int rear; // 队尾指针
public ArrayQueue(int capacity) {
queue = new int[capacity];
front = rear = -1;
}
public void enqueue(int element) {
if (isEmpty()) {
front = rear = 0;
} else if (rear == queue.length - 1) {
throw new RuntimeException("Queue is full");
} else {
rear++;
}
queue[rear] = element;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = queue[front];
if (front == rear) {
front = rear = -1;
} else {
front++;
}
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue[front];
}
public boolean isEmpty() {
return front == -1 && rear == -1;
}
public int size() {
if (isEmpty()) {
return 0;
}
return rear - front + 1;
}
}
这里的栈和队列基于数组实现,通过维护相应的指针来模拟栈和队列的特性。这种实现方式在一些场景中可能更为直观,但需要注意数组容量的限制。如果数组容量不足,需要进行扩容操作。
3.8 掌握链式存储结构对栈和队列基本操作的实现
链式存储结构对栈和队列的实现同样可以通过指针和节点的方式完成。下面分别是链式存储结构对栈和队列的基本操作实现:
链式存储结构对栈的基本操作
// 节点类
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
// 链式栈的实现
class LinkedStack {
private Node top;
public LinkedStack() {
top = null;
}
public void push(int element) {
Node newNode = new Node(element);
newNode.next = top;
top = newNode;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
int element = top.data;
top = top.next;
return element;
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return top.data;
}
public boolean isEmpty() {
return top == null;
}
public int size() {
int count = 0;
Node temp = top;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
}
链式存储结构对队列的基本操作
// 链式队列的实现
class LinkedQueue {
private Node front; // 队头指针
private Node rear; // 队尾指针
public LinkedQueue() {
front = rear = null;
}
public void enqueue(int element) {
Node newNode = new Node(element);
if (isEmpty()) {
front = rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = front.data;
front = front.next;
if (front == null) {
rear = null; // 如果队列为空,重置队尾指针
}
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return front.data;
}
public boolean isEmpty() {
return front == null;
}
public int size() {
int count = 0;
Node temp = front;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
}
这里的链式存储结构实现了栈和队列的基本操作,通过指针的方式实现了元素的入栈、出栈、入队、出队等操作。链式结构相对于数组结构在动态操作方面更为灵活。
3.9 掌握顺序存储结构中实现循环队列的具体要求
循环队列是一种特殊的队列实现,通过使用循环方式维护队列的前后指针,实现循环利用底层数组,避免了队列元素在出队时需要移动整个队列的情况。在循环队列里要约定一个空位置,这是为了更好地区分队列为空和队列为满的情况。在循环队列中,队头指针 front
和队尾指针 rear
在数组中移动时,可能会出现两种情况:
- 队列为空: 当
front
和rear
指针重合时,队列为空。 - 队列为满: 当
(rear + 1) % capacity == front
时,队列为满。
如果没有约定一个空位置,那么在队列为空和队列为满时,front
和 rear
指针都会重合,导致无法准确判断队列的状态。通过约定一个空位置,我们可以确保在队列为满时 rear
的下一个位置一定是 front
,而在队列为空时,front
和 rear
指针重合。这样就能够清晰地判断队列的状态。
具体来说,如果队列的容量是 7,我们初始化数组为 int[8]
,并约定第 8 个位置为空。这样,当 front
和 rear
指针都指向数组的第 0 个位置时,表示队列为空;当 (rear + 1) % capacity == front
时,表示队列为满。
这种约定使得循环队列的状态判断更加清晰,避免了状态混淆。如果队列的容量是 n
,那么实际数组的大小应该是 n + 1
,其中 n
用于存储队列元素,而另外 1 个位置用于约定为空。
-
初始化:
- 使用一个数组来存储队列元素。
- 使用两个指针,一个指向队头(front),一个指向队尾(rear)。
- 需要额外一个元素的空间作为约定,即队列中的一个空位置不存储元素。
-
入队(Enqueue):
- 在队尾插入元素,然后将 rear 指针向后移动一位。
- 如果 rear 指针超过数组边界,将其置为数组的起始位置。
-
出队(Dequeue):
- 返回队头元素,然后将 front 指针向后移动一位。
- 如果 front 指针超过数组边界,将其置为数组的起始位置。
-
获取队头元素(Front):
- 返回 front 指针指向的元素。
-
判空(isEmpty):
- 当 front 和 rear 指针相等时,队列为空。
-
判满(isFull):
- 当 (rear + 1) % 数组长度等于 front 时,队列为满。
-
获取队列大小(Size):
- 队列大小为 (rear - front + 数组长度) % 数组长度。
下面是基于数组的循环队列的简单实现:
class CircularQueue {
private int[] queue;
private int front; // 队头指针
private int rear; // 队尾指针
private int capacity; // 队列容量
public CircularQueue(int capacity) {
this.capacity = capacity + 1; // 约定一个空位置
queue = new int[this.capacity];
front = rear = 0;
}
public void enqueue(int element) {
if (isFull()) {
throw new RuntimeException("Queue is full");
}
queue[rear] = element;
rear = (rear + 1) % capacity;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = queue[front];
front = (front + 1) % capacity;
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue[front];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear + 1) % capacity == front;
}
public int size() {
return (rear - front + capacity) % capacity;
}
}
这里的实现采用了取模运算来实现循环的效果,确保队尾指针在数组边界处正确回绕。
3.10 理解递归调用和栈之间的关系
递归调用的基本概念
-
递归定义: 递归是指在函数的定义中使用函数自身的方法。递归可以将一个大型问题分解成一个或多个相似但规模较小的子问题。
-
递归调用的特点:
- 每次递归调用都会生成一个新的函数调用栈帧(包含局部变量、返回地址等信息)。
- 每个栈帧都代表一个特定的递归层次,这些栈帧形成了一个栈结构。
递归调用的示例
// 计算阶乘的递归函数示例
public class RecursiveExample {
public static int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
} else {
// 递归调用
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
int result = factorial(5);
System.out.println("Factorial of 5: " + result);
}
}
递归调用和栈的关系
期末卷综合题中考察了递归函数运行时其栈的调用情况(要画图),建议自己多写几个程序画出其栈的调用情况来练习。
-
调用栈(Call Stack): 在进行递归调用时,系统会为每次调用生成一个栈帧,这些栈帧按照调用的先后顺序形成一个调用栈。
-
栈帧的生成与销毁:
- 当函数被调用时,会生成一个新的栈帧并推入调用栈。
- 当函数执行完毕或遇到递归终止条件时,对应的栈帧会从调用栈中弹出,被销毁。
-
栈的特性:
- 栈是一种后进先出(LIFO)的数据结构,与递归的特性相吻合。
- 每个栈帧都包含了函数调用的上下文信息,包括局部变量、返回地址等。
-
堆栈的大小限制: 递归调用过深可能导致调用栈溢出。在实际应用中,可以通过调整堆栈大小或使用迭代的方式来避免栈溢出的问题。
理解递归调用和栈的关系有助于更好地理解递归的执行流程和调试递归函数。
3.11 掌握栈和队列的经典应用
期末卷在填空题中有考察中缀表达式和后缀表达式的转换。
栈的经典应用
-
函数调用栈: 编程语言中的函数调用过程通常使用栈来管理函数调用和返回。每次函数调用都会生成一个栈帧,将函数的局部变量、返回地址等信息保存在栈上。
-
表达式求值: 栈可以用于中缀表达式到后缀表达式的转换,以及后缀表达式的求值。这种应用在计算机编译器和计算器中常见。
以中缀表达式 `3 + 5 * (4 - 2)` 为例,演示中缀表达式转后缀表达式的顺序流程: 1. **中缀表达式:** `3 + 5 * (4 - 2)` 2. **初始化:** 创建一个空的后缀表达式字符串(用 `postfixExpression` 表示)和一个空的操作符栈(用 `operatorStack` 表示)。 3. **遍历中缀表达式:** - 当遇到数字或字母时,直接输出到后缀表达式。当前缀表达式:`3` - 当遇到操作符 `+` 时,由于栈为空,直接入操作符栈。当前操作符栈:`+`,当前后缀表达式:`3` - 当遇到数字 `5` 时,直接输出到后缀表达式。当前操作符栈:`+`,当前后缀表达式:`3 5` - 当遇到操作符 `*` 时,由于栈为空,直接入操作符栈。当前操作符栈:`+*`,当前后缀表达式:`3 5` - 当遇到左括号 `(` 时,直接入操作符栈。当前操作符栈:`+*(`,当前后缀表达式:`3 5` - 当遇到数字 `4` 时,直接输出到后缀表达式。当前操作符栈:`+*(`,当前后缀表达式:`3 5 4` - 当遇到操作符 `-` 时,由于栈顶的操作符优先级低于当前操作符,直接入操作符栈。当前操作符栈:`+*(-`,当前后缀表达式:`3 5 4` - 当遇到数字 `2` 时,直接输出到后缀表达式。当前操作符栈:`+*(-`,当前后缀表达式:`3 5 4 2` - 当遇到右括号 `)` 时,弹出操作符栈中的所有操作符,直到遇到左括号 `(`,并输出到后缀表达式。当前操作符栈:`+*`,当前后缀表达式:`3 5 4 2 -` - 当遇到右括号 `)` 时,弹出操作符栈中的所有操作符,直到遇到左括号 `(`,并输出到后缀表达式。当前操作符栈:`+`,当前后缀表达式:`3 5 4 2 - * +` 4. **遍历结束:** 遍历完中缀表达式后,弹出操作符栈中的所有操作符,并输出到后缀表达式。最终后缀表达式为:`3 5 4 2 - * +` 通过以上步骤,我们成功地将中缀表达式 `3 + 5 * (4 - 2)` 转换为后缀表达式 `3 5 4 2 - * +`。
-
括号匹配: 栈常用于检查表达式中的括号是否匹配。通过维护一个栈,可以在遍历表达式时判断括号的匹配情况。
-
浏览器前进和后退: 浏览器的历史记录可以使用两个栈来实现,一个用于记录前进的页面,一个用于记录后退的页面。
-
撤销机制: 在图形设计和文本编辑软件中,撤销操作可以使用栈来实现。每次操作都将状态压入栈中,撤销时弹出栈顶状态。
队列的经典应用
-
任务调度: 操作系统中的进程调度和任务调度通常使用队列来管理待执行的任务。先进先出的特性使得任务按照顺序执行。
-
打印队列: 打印任务通常会进入一个打印队列,按照先进先出的原则进行打印。
-
广度优先搜索(BFS): 图的广度优先搜索算法常常使用队列来管理待访问的节点,确保按层次遍历图。
-
消息传递: 在计算机通信中,消息传递的队列模型常用于实现异步通信。消息发送者将消息放入队列,接收者从队列中取出消息进行处理。
-
缓冲区: 缓冲区是队列的一种应用,用于平衡生产者和消费者之间的速度差异。例如,生产者产生数据,将其放入缓冲区,消费者从缓冲区取出数据进行处理。