队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构,意味着队列中的元素按照它们进入队列的顺序依次处理。即,最先被添加到队列中的元素最先被移除。
队列在计算机中的实现可以使用多种数据结构,最常见的物理实现有以下几种:
优点:
缺点:
解决方案:
使用一个指针(或两个指针)来追踪队头和队尾,并且当队头移动时,可以实现一个循环队列(Circular Queue)。这样就可以避免空间浪费。
循环队列:队头和队尾在数组的两端循环,解决了数组中空间浪费的问题。具体做法是通过模运算来实现“首尾相接”:
front = (front + 1) % capacity; rear = (rear + 1) % capacity;
优点:
缺点:
优化:使用尾指针来直接指向队尾,这样入队操作就可以在 O(1) 时间内完成。
优点:
缺点:
数组实现(普通队列):
队列头 → [元素1, 元素2, 元素3, ..., 元素N] ← 队列尾
在数组队列中,通常使用两个指针:
如果采用循环数组实现,那么当 front
和 rear
都指向数组的末尾时,它们可以“绕回”到数组的开始部分,从而避免空间浪费。
链表实现:
队列头 → [元素1] → [元素2] → [元素3] → ... → [元素N] → 队列尾
链表队列的关键是使用两个指针:
在入队操作时,rear
移动到链表的末尾,新的节点连接到它上面;在出队操作时,front
移动到下一个节点。
队列在实际应用中广泛用于任务调度、数据缓冲、打印队列等场景。
栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。栈中的元素只有一个端口进行插入和删除操作,这个端口被称为栈顶(Top)。栈遵循的主要原则是:最新插入的元素最先被删除。常见的栈操作包括:
栈通常用于需要追踪"最近"或"递归"的场景,比如:浏览器的历史记录、表达式求值、函数调用的管理等。
栈的物理实现可以通过数组或链表两种方式。
在数组实现中,栈的元素存储在一个连续的内存块中(即数组)。栈顶由一个指针或索引(例如 top
)指向,指示当前栈顶的位置。
top+1
的位置,然后将 top
指针递增。top
指针递减。数组实现的优缺点:
代码示例:
#include
using namespace std;
class Stack {
private:
int* arr;
int top;
int capacity;
public:
Stack(int size) {
capacity = size;
arr = new int[size];
top = -1; // 栈为空时,top为-1
}
bool isEmpty() {
return top == -1;
}
bool isFull() {
return top == capacity - 1;
}
void push(int x) {
if (isFull()) {
cout << "Stack Overflow" << endl;
return;
}
arr[++top] = x;
}
void pop() {
if (isEmpty()) {
cout << "Stack Underflow" << endl;
return;
}
top--;
}
int peek() {
if (!isEmpty()) {
return arr[top];
}
cout << "Stack is Empty" << endl;
return -1;
}
~Stack() {
delete[] arr;
}
};
在链表实现中,栈的元素以链表节点的形式存在,栈顶由一个指针指向链表的头节点。每个新元素都被插入到链表的头部,形成栈顶。
链表实现的优缺点:
代码示例:
#include
using namespace std;
class Stack {
private:
struct Node {
int data;
Node* next;
};
Node* top;
public:
Stack() {
top = nullptr;
}
bool isEmpty() {
return top == nullptr;
}
void push(int x) {
Node* newNode = new Node;
newNode->data = x;
newNode->next = top;
top = newNode;
}
void pop() {
if (isEmpty()) {
cout << "Stack Underflow" << endl;
return;
}
Node* temp = top;
top = top->next;
delete temp;
}
int peek() {
if (!isEmpty()) {
return top->data;
}
cout << "Stack is Empty" << endl;
return -1;
}
~Stack() {
while (!isEmpty()) {
pop();
}
}
};
递归的模拟:
栈是递归的基础。每次递归调用都将函数的返回地址、局部变量等信息压入栈中,并在函数执行完毕后弹出栈顶的内容。栈模拟了递归函数的调用和返回过程。
表达式求值:
栈被广泛用于运算符的优先级处理和表达式的求值,尤其是在中缀表达式转换为后缀表达式(或前缀表达式)时。
深度优先搜索(DFS):
在图的遍历中,DFS 使用栈来追踪当前的路径,并回溯到上一层进行其他未探索的路径。
浏览器历史记录:
浏览器的前进和后退按钮通常依赖栈来存储用户的浏览历史。
撤销操作:
文本编辑器中的撤销功能通常会使用栈来记录操作历史,用户可以逐步回退到先前的状态。
栈是一种**后进先出(LIFO)**的线性数据结构,通过数组或链表实现,具有简单高效的操作(O(1)时间复杂度)。它广泛应用于递归、表达式求值、图遍历等问题中。
在 C++ 中,函数的参数传递可以是两种方式:
在你提供的代码中,tail
是一个指针类型的成员变量。当你在 insert()
函数内修改 tail
时,你实际上是通过直接修改这个指针指向的内存位置,而不是修改 tail
变量本身。关键点是:
tail
是类的成员变量,因此它是一个指针类型的成员。tail->next = p
更新 tail
指向的新节点时,你实际上是改变了指针 tail
的内容。tail
是类的成员变量,它的修改会直接反映在类的实例(对象)上,因此即使 tail
是在 insert()
函数中修改的,函数外的 tail
也会受到影响。tail
会影响函数外的 tail
?tail
是指向队列尾部的指针,它是类的成员变量,而不是局部变量。insert()
中使用 tail = p;
时,你并没有创建一个局部变量,而是直接修改了这个成员变量 tail
。tail
会直接影响到该对象的 tail
成员。假设有以下的队列类:
class Queue {
public:
Node* head;
Node* tail;
Queue() {
head = nullptr;
tail = nullptr;
}
void insert(int x) {
Node* p = new Node;
p->value = x;
p->next = nullptr;
if (tail == nullptr) { // 如果队列为空
head = tail = p;
} else {
tail->next = p; // 将原 tail 的 next 指向新节点
tail = p; // 更新 tail 为新节点
}
}
};
在调用 insert()
时,tail
的修改直接影响了该对象的成员变量 tail
,因为:
tail
是类的成员指针,指向队列尾部。tail = p;
,你并没有修改局部变量,而是修改了对象的成员变量。因此,无论函数结束与否,tail
在对象中的值都会发生变化。类成员变量可以在类的成员函数中直接使用,而无需作为参数传入。这是面向对象编程中的基本特性之一。类成员变量(通常称为“实例变量”)是类的一部分,它们属于对象的状态,所有的类成员函数都可以直接访问和修改这些变量。
类成员变量是对象的一部分:当你创建一个类的实例时,类的成员变量就成为该实例的一部分,每个对象都可以独立持有这些成员变量。
类成员函数内部可以访问类的成员变量:类的成员函数(方法)是与对象绑定的,并且它们在对象实例上执行,因此它们可以直接访问和修改该实例的成员变量,无需通过参数传递。
隐式访问机制:在类成员函数内部,你可以直接引用类成员变量,因为它们是当前对象的一部分,编译器会自动知道当前对象的上下文。
修改类的成员变量时,函数内对该成员变量的修改会影响函数外的对象,因为成员变量是与对象实例相关联的,而不是局部作用域内的临时变量。因此,在 insert()
函数中修改 tail
(以及 head
)会直接影响到队列实例的状态。
这种行为与指针传递和引用传递的原理密切相关。通过指针操作类的成员变量,函数内的修改会反映到对象的实际成员中。
class Queue
{
public:
Node* head = NULL;
Node* tail = head;
void insert(int x)
{
Node* p = new Node;
p->value = x;
if (head == NULL && tail == NULL)
{
head = p;
}
else
{
tail->next = p;
tail = tail->next;
}
}
int pop()
{
Node* temp;
int result;
temp = head;
result = head->value;
head = head->next;
delete temp;
return result;
}
};
1,原理:通过更新左右索引来实现插入弹出操作。
2,代码:
#include
using namespace std;
class Queue
{
public:
int l = 0, r = 0;
int* arr;
int size;
//create the an array
Queue(int n)
{
size = n;
arr = new int[n];
}
void insert(int n)
{
if (r < size)arr[r++] = n;
else printf("the queue is full !");
}
int pop()
{
if (l < r) return arr[l++];
else
printf("the queue is empty !");
}
int peak()
{
return arr[r];
}
};
int main()
{
Queue queue (5);
for (int i = 0; i < 6; i++)
{
queue.insert(i);
cout << queue.pop() << " ";
}
return 0;
}
代码展示:
#include
using namespace std;
class Stack
{
public:
int* arr;
int size = 0;
int index = 0;
Stack(int n)
{
arr = new int[n];
size = n;
}
void insert(int x)
{
if (index == size)
{
printf("the stack is full !\n");
return;
}
arr[index++] = x;
return;
}
int pop()
{
if (index < 0)
{
printf("the stack is empty !");
return -1;
}
int result = arr[index - 1];
index--;
return result;
}
};
int main()
{
Stack stack(2);
stack.insert(5);
stack.insert(6);
stack.insert(7);
cout << stack.pop();
return 0;
}
注意点:
1,对于有返回值的函数,要使它的所有情况都有返回值,即使是错误情况,也要,例如pop函数。