栈和队列是操作受限的线性表,其基本操作是线性表的子集。
因此,栈和队列,可称之为限定性的数据结构。
0. 什么是栈
栈【stack】是限定仅在表尾进行插入或删除操作的线性表。
栈又称为“后进先出”的线性表。
- 栈顶【top】——表尾端
- 栈底【bottom】——表头端
- 空栈——不含元素的空表
- 后进先出【Last In First Out】——栈的修改原则
- 应用场景——按照保存数据时相反的顺序表来使用数据
1. 栈的实现
一种数据结构的逻辑结构根据需要可表示成多种存储结构
这里栈的逻辑结构分别用 动态数组 和 动态链表 的存储结构进行实现
-
动态数组
#include<iostream> using namespace std; class Stack { private: int top; // 栈顶指针 int capacity; // 栈的容量 int *data; // 存储元素的动态数组 public: Stack(int capacity) { this->capacity = capacity; top = -1; // 栈顶指针初始化为-1,表示栈为空 data = new int[capacity]; // 动态分配数组空间 } ~Stack() { delete[] data; // 释放数组空间 } bool isEmpty() { return top == -1; // 栈顶指针为-1表示栈为空 } bool isFull() { return top == capacity - 1; // 栈顶指针为容量减1表示栈已满 } void push(int element) { if (isFull()) { cout << "Stack overflow" << endl; return; } top++; // 栈顶指针加1 data[top] = element; // 将元素添加到栈顶 } int pop() { if (isEmpty()) { cout << "Stack underflow" << endl; return -1; } int element = data[top]; // 取出栈顶元素 top--; // 栈顶指针减1 return element; // 返回取出的元素 } int peek() { if (isEmpty()) { cout << "Stack is empty" << endl; return -1; } return data[top]; // 返回栈顶元素,但不删除 } }; /* 这个类中,私有变量top表示栈顶指针,初始值为-1表示栈为空, 私有变量capacity表示栈的容量,私有指针变量data为存储元素的动态数组。 类中的公有方法包括: 构造函数:根据传入的容量创建一个动态数组,并将栈顶指针初始化为-1 析构函数:释放动态数组占用的内存空间 isEmpty()方法:判断栈是否为空,当栈顶指针为-1时为空 isFull()方法:判断栈是否已满,当栈顶指针等于容量减1时已满 push(int element)方法:向栈中添加元素,如果栈已满,则输出“Stack overflow” pop()方法:从栈中弹出元素,并返回该元素的值,如果栈为空,则输出“Stack underflow” peek()方法:返回栈顶元素的值,但不删除该元素,如果栈为空,则输出“Stack is empty” */
-
动态链表
#include<iostream> using namespace std; class StackNode { public: int data; // 存储元素的值 StackNode *next; // 指向下一个节点的指针 StackNode(int data) { this->data = data; next = nullptr; // 初始化指针为空 } }; class Stack { private: StackNode *top; // 栈顶节点指针 public: Stack() { top = nullptr; // 栈顶指针初始化为空 } ~Stack() { StackNode *temp; while (top != nullptr) { temp = top; top = top->next; delete temp; // 释放节点空间 } } bool isEmpty() { return top == nullptr; // 栈顶指针为空表示栈为空 } void push(int element) { StackNode *node = new StackNode(element); // 创建一个新的节点 node->next = top; // 将新节点插入到栈顶 top = node; } int pop() { if (isEmpty()) { cout << "Stack underflow" << endl; return -1; } int element = top->data; // 取出栈顶元素 StackNode *temp = top; // 保存栈顶节点指针 top = top->next; // 栈顶指针指向下一个节点 delete temp; // 释放取出的节点空间 return element; // 返回取出的元素 } int peek() { if (isEmpty()) { cout << "Stack is empty" << endl; return -1; } return top->data; // 返回栈顶元素的值,但不删除该元素 } }; /* 这个类中,私有变量top为栈顶节点的指针,初始值为空指针, 每个节点包含一个整数值和一个指向下一个节点的指针。 类中的公有方法包括: 构造函数:创建一个空栈,栈顶指针初始化为空指针 析构函数:释放所有节点占用的内存空间 isEmpty()方法:判断栈是否为空,当栈顶指针为空指针时为空 push(int element)方法:向栈中添加元素,创建一个新的节点,将该节点插入到栈顶 pop()方法:从栈中弹出元素,并返回该元素的值,如果栈为空,则输出“Stack underflow” peek()方法:返回栈顶元素的值,但不删除该元素,如果栈为空,则输出“Stack is empty” */
2. 栈的可视化
Last-In-First-Out,LIFO
_____
| | <- 栈顶(top)
| |
| |
|_____| <- 栈底(bottom)
在上面的可视化图中,栈的数据结构可以看做是一列横向排列的盒子,
栈底在最下方,栈顶在最上方。
向栈中添加元素的操作称为“入栈”,从栈中删除元素的操作称为“出栈”。
当一个元素被入栈时,它会被放置在栈顶位置,
而原来在栈顶的元素则会被顶出栈。
当一个元素被出栈时,它会被从栈顶弹出,
栈顶位置下移,而原来在它下面的元素则成为了新的栈顶元素。
3. 栈的应用案例
- 函数调用
函数调用本质上也是一个栈的应用。
每次函数调用时,将函数的参数、局部变量和返回地址等信息压入栈中,
当函数返回时,从栈中弹出这些信息,返回到调用该函数的地方继续执行。
- 栈与递归
递归就是函数自己调用自己,
栈和递归之间有着紧密的关系,递归函数本质上也是在调用一个栈结构,
每个函数调用都会在栈中创建一个新的帧(frame),
该帧包含了函数的参数、局部变量和返回地址等信息。
每个函数执行完毕后,都会从栈中弹出该帧,返回到调用它的函数中。
在递归过程中,每个函数调用都会将当前的状态保存在栈中,
因此递归可以看作是一种栈的操作,每次递归调用都会将当前状态保存到栈中,
然后在函数返回时从栈中弹出上一个状态,继续执行下一次调用。
-
经典的递归函数
-
用于计算斐波那契数列的第n项:
int fibonacci(int n) { if (n <= 1) { return n; } return fibonacci(n-1) + fibonacci(n-2); } /* 该函数通过递归的方式计算斐波那契数列,如果n等于0或1,则直接返回n, 否则递归调用该函数计算n-1和n-2的值,并将它们相加得到结果。 在计算fibonacci(5)的过程中,该函数将被递归调用6次, 每次调用都会将当前状态保存在栈中,然后在函数返回时从栈中弹出上一个状态, 继续执行下一次调用,直到计算完成。 */
-
阶乘函数
int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } } /* 这个函数首先检查n是否为0。如果是,它返回1,因为0的阶乘是1。如果n不为0, 则使用递归调用计算(n-1)的阶乘,然后将其乘以n,以得到n的阶乘。 */
-
0. 什么是队列
队列【queue】是只允许在表的一段进行插入,而在另一端删除元素的线性表。
队列又称为“先进先出”的线性表。
- 队尾【rear】——允许插入的一端
- 队头【front】——允许删除的一端
- 先进先出【First In First Out】——队列的修改原则
- 应用场景——按照输入(保存数据)的先后次序排队来使用数据
1. 队列的实现
一种数据结构的逻辑结构根据需要可表示成多种存储结构
这里队列的逻辑结构分别用 动态数组 和 动态链表 的存储结构进行实现
-
动态数组
#include <iostream> using namespace std; class Queue { private: int *arr; // 数组指针 int front; // 队头指针 int rear; // 队尾指针 int size; // 队列的大小 int count; // 队列中的元素个数 public: // 构造函数,初始化队列 Queue(int queueSize) { arr = new int[queueSize]; size = queueSize; front = 0; rear = -1; count = 0; } // 判断队列是否为空 bool isEmpty() { return (count == 0); } // 判断队列是否已满 bool isFull() { return (count == size); } // 返回队列的大小 int getSize() { return count; } // 入队 void enqueue(int value) { if (isFull()) { cout << "Queue is full, cannot enqueue element." << endl; return; } rear = (rear + 1) % size; arr[rear] = value; count++; } // 出队 int dequeue() { if (isEmpty()) { cout << "Queue is empty, cannot dequeue element." << endl; return -1; } int temp = arr[front]; front = (front + 1) % size; count--; return temp; } // 获取队头元素 int getFront() { if (isEmpty()) { cout << "Queue is empty, cannot get front element." << endl; return -1; } return arr[front]; } // 获取队尾元素 int getRear() { if (isEmpty()) { cout << "Queue is empty, cannot get rear element." << endl; return -1; } return arr[rear]; } // 打印队列 void printQueue() { if (isEmpty()) { cout << "Queue is empty." << endl; return; } cout << "Queue: "; for (int i = front; i != rear; i = (i + 1) % size) { cout << arr[i] << " "; } cout << arr[rear] << endl; } // 析构函数,释放动态内存 ~Queue() { delete [] arr; } };
-
动态链表
#include <iostream> using namespace std; class Queue { private: struct Node { int data;// 数据 Node* next;// 指向下一个节点的指针 Node(int val) {// 构造函数 data = val; next = nullptr; } }; Node* front; // 队头指针 Node* rear; // 队尾指针 int count; // 队列中的元素个数 public: // 构造函数,初始化队列 Queue() { front = nullptr; rear = nullptr; count = 0; } // 判断队列是否为空 bool isEmpty() { return (count == 0); } // 返回队列的大小 int getSize() { return count; } // 入队 void enqueue(int value) { Node* newNode = new Node(value); if (isEmpty()) { front = newNode; } else { rear->next = newNode; } rear = newNode; count++; } // 出队 int dequeue() { if (isEmpty()) { cout << "Queue is empty, cannot dequeue element." << endl; return -1; } Node* temp = front; int data = temp->data; front = front->next; delete temp; count--; if (isEmpty()) { rear = nullptr; } return data; } // 获取队头元素 int getFront() { if (isEmpty()) { cout << "Queue is empty, cannot get front element." << endl; return -1; } return front->data; } // 获取队尾元素 int getRear() { if (isEmpty()) { cout << "Queue is empty, cannot get rear element." << endl; return -1; } return rear->data; } // 打印队列 void printQueue() { if (isEmpty()) { cout << "Queue is empty." << endl; return; } cout << "Queue: "; Node* current = front; while (current != nullptr) { cout << current->data << " "; current = current->next; } cout << endl; } // 析构函数,释放动态内存 ~Queue() { while (front != nullptr) { Node* temp = front; front = front->next; delete temp; } rear = nullptr; count = 0; } };
2. 队列的可视化
First In First Out,FIFO
初始状态:
+---+---+---+---+---+
| 10| 20| 30| 40| 50|
+---+---+---+---+---+
head tail
插入元素60后的状态:
+---+---+---+---+---+---+
| 10| 20| 30| 40| 50| 60|
+---+---+---+---+---+---+
head tail
删除头部元素后的状态:
+---+---+---+---+---+---+
| 20| 30| 40| 50| 60 |
+---+---+---+---+---+---+
head tail
插入元素70和80后的状态:
+---+---+---+---+---+---+---+
| 20| 30| 40| 50| 60| 70| 80|
+---+---+---+---+---+---+---+
head tail
3. 队列的应用案例
队列是一种常用的数据结构,它具有先进先出(FIFO)的特点,
常常被用于需要按照一定顺序进行处理的情况。
- 计算机任务调度:操作系统中的进程调度器通常使用队列来管理等待执行的进程。
队列中的进程按照先进先出的顺序被调度执行,确保公平性和资源的合理利用。 - 消息队列:在分布式系统中,为了解耦和处理组件之间的通信,通常使用消息队列。
消息队列可以将发送者和接收者之间的直接耦合转化为对消息队列的操作,
确保系统的可扩展性和高可用性。 - 网络数据包传输:网络路由器和交换机通常使用队列来管理待发送或接收的数据包。
这些数据包按照先进先出的顺序进行处理,确保网络传输的稳定性和可靠性。 - 银行排队服务:在银行等场景中,客户到达后会加入一个队列,等待服务。
队列中的客户按照先来先服务的原则被服务,确保公平性和服务的高效性。 - 多线程编程:在多线程编程中,线程之间的通信和同步通常需要使用队列。
比如,一个线程生产数据并将其加入队列,
另一个线程则从队列中取出数据并进行处理,
确保线程之间的数据同步和通信。 - 数据缓存:队列也可以用于数据缓存,将数据缓存在队列中,等待被处理或者持久化。
比如,将用户请求加入队列,等待后台处理或者存储到数据库中。