一,队列
队列的逻辑含义:
队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构,意味着队列中的元素按照它们进入队列的顺序依次处理。即,最先被添加到队列中的元素最先被移除。
队列的基本操作:
- 入队(Enqueue):将一个元素添加到队列的末尾。
- 出队(Dequeue):从队列的头部移除一个元素。
- 查看队头元素(Front/Peek):获取队列头部的元素,但不移除它。
- 判断队列是否为空(IsEmpty):检查队列中是否有元素。
- 获取队列的大小(Size):返回队列中元素的个数。
队列的物理实现:
队列在计算机中的实现可以使用多种数据结构,最常见的物理实现有以下几种:
1. 数组实现:
- 队头和队尾都通过数组索引来表示。
- 入队操作将元素放到队尾,出队操作将队头元素移除。
优点:
- 操作简单,直接使用数组即可。
缺点:
- 空间浪费:当出队时,队头元素被移除,但数组的空间并没有被回收,可能导致空间浪费。
- 队列长度限制:如果数组大小固定,队列的最大长度也是固定的,可能会遇到溢出问题。
解决方案:
使用一个指针(或两个指针)来追踪队头和队尾,并且当队头移动时,可以实现一个循环队列(Circular Queue)。这样就可以避免空间浪费。
循环队列:队头和队尾在数组的两端循环,解决了数组中空间浪费的问题。具体做法是通过模运算来实现“首尾相接”:
front = (front + 1) % capacity; rear = (rear + 1) % capacity;
2. 链表实现:
- 链表队列通常使用单链表来实现。
- 队头指向链表的第一个节点,队尾指向链表的最后一个节点。
- 入队:通过将新元素添加到链表的尾部来实现。
- 出队:通过移除链表的头部元素来实现。
优点:
- 动态分配内存,大小不固定。
- 不存在空间浪费的问题。
缺点:
- 每次入队时,需要找到链表的尾部(如果没有尾指针的话),这可能导致较高的时间复杂度(O(n))。
- 实现稍复杂,涉及指针操作。
优化:使用尾指针来直接指向队尾,这样入队操作就可以在 O(1) 时间内完成。
3. 双端队列(Deque):
- 双端队列是允许从两端进行插入和删除的队列。
- 它既支持队列的先进先出(FIFO)特性,也支持从队头和队尾都能进行操作。
优点:
- 既能实现队列的操作,也能实现栈的操作。
缺点:
- 实现更为复杂。
队列的物理实现示意:
-
数组实现(普通队列):
队列头 → [元素1, 元素2, 元素3, ..., 元素N] ← 队列尾在数组队列中,通常使用两个指针:
- front:指向队列的头部,出队时移动。
- rear:指向队列的尾部,入队时移动。
如果采用循环数组实现,那么当
front和rear都指向数组的末尾时,它们可以“绕回”到数组的开始部分,从而避免空间浪费。 -
链表实现:
队列头 → [元素1] → [元素2] → [元素3] → ... → [元素N] → 队列尾链表队列的关键是使用两个指针:
- front:指向链表的第一个元素。
- rear:指向链表的最后一个元素。
在入队操作时,
rear移动到链表的末尾,新的节点连接到它上面;在出队操作时,front移动到下一个节点。
总结:
- 逻辑:队列是一种先进先出(FIFO)的数据结构,具有入队、出队、查看队头、检查队列空否等基本操作。
- 物理实现:
- 使用数组实现时,队列的大小固定,可能存在空间浪费问题。可以通过循环数组来优化。
- 使用链表实现时,队列的大小动态,且不容易产生空间浪费,但操作时需要涉及指针管理。
- **双端队列(Deque)**支持从两端插入和删除。
队列在实际应用中广泛用于任务调度、数据缓冲、打印队列等场景。
二,栈
栈的逻辑含义
栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。栈中的元素只有一个端口进行插入和删除操作,这个端口被称为栈顶(Top)。栈遵循的主要原则是:最新插入的元素最先被删除。常见的栈操作包括:
- 压栈(Push):将一个元素放入栈顶。
- 弹栈(Pop):从栈顶移除一个元素。
- 查看栈顶元素(Top/Peek):查看栈顶元素,但不移除它。
- 检查栈是否为空(IsEmpty):检查栈中是否有元素。
- 栈的大小(Size):返回栈中元素的数量。
栈通常用于需要追踪"最近"或"递归"的场景,比如:浏览器的历史记录、表达式求值、函数调用的管理等。
栈的物理实现
栈的物理实现可以通过数组或链表两种方式。
1. 数组实现
在数组实现中,栈的元素存储在一个连续的内存块中(即数组)。栈顶由一个指针或索引(例如 top)指向,指示当前栈顶的位置。
- 入栈(Push):将一个新元素放置到数组中索引为
top+1的位置,然后将top指针递增。 - 出栈(Pop):将栈顶的元素移除,并将
top指针递减。
数组实现的优缺点:
- 优点:
- 操作简单,直接通过数组索引即可访问栈元素。
- 入栈和出栈的时间复杂度都是 O(1)。
- 缺点:
- 栈的大小是固定的,如果栈满了就无法继续入栈。
- 需要预先分配足够的内存空间来存储栈中的元素,如果栈空间用尽,需要重新分配内存(动态扩展)。
代码示例:
#include <iostream>
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;
}
};
2. 链表实现
在链表实现中,栈的元素以链表节点的形式存在,栈顶由一个指针指向链表的头节点。每个新元素都被插入到链表的头部,形成栈顶。
- 入栈(Push):创建一个新节点,将其指向当前的栈顶,然后更新栈顶指针指向新节点。
- 出栈(Pop):移除栈顶节点,并将栈顶指针指向下一个节点。
链表实现的优缺点:
- 优点:
- 栈的大小是动态的,可以根据需要增加或减少内存使用。
- 不会发生栈溢出问题,除非系统内存不足。
- 缺点:
- 每次操作都需要通过指针管理,稍微复杂。
- 每次出栈时需要销毁节点,可能涉及内存管理问题(如内存泄漏)。
代码示例:
#include <iostream>
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)时间复杂度)。它广泛应用于递归、表达式求值、图遍历等问题中。
三,队列的实现
一,链表
1,误区:指针与引用,类的成员变量。
1. 基本概念:
在 C++ 中,函数的参数传递可以是两种方式:
- 值传递(Pass by value):将变量的副本传递给函数,函数内对副本的修改不会影响函数外的原始变量。
- 引用传递(Pass by reference):将变量的地址(即指针)传递给函数,函数内对这个引用的修改会直接影响外部的原始变量。
2. 指针与引用的区别:
在你提供的代码中,tail 是一个指针类型的成员变量。当你在 insert() 函数内修改 tail 时,你实际上是通过直接修改这个指针指向的内存位置,而不是修改 tail 变量本身。关键点是:
tail是类的成员变量,因此它是一个指针类型的成员。- 当你通过
tail->next = p更新tail指向的新节点时,你实际上是改变了指针tail的内容。 - 因为
tail是类的成员变量,它的修改会直接反映在类的实例(对象)上,因此即使tail是在insert()函数中修改的,函数外的tail也会受到影响。
3. 为什么修改 tail 会影响函数外的 tail?
tail是指向队列尾部的指针,它是类的成员变量,而不是局部变量。- 当你在
insert()中使用tail = p;时,你并没有创建一个局部变量,而是直接修改了这个成员变量tail。 - 类的成员变量是与类的实例(对象)相关联的,而不是局部作用域内的临时变量。因此,在类方法内部修改
tail会直接影响到该对象的tail成员。
4. 示意说明:
假设有以下的队列类:
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在对象中的值都会发生变化。
5,类成员变量
类成员变量可以在类的成员函数中直接使用,而无需作为参数传入。这是面向对象编程中的基本特性之一。类成员变量(通常称为“实例变量”)是类的一部分,它们属于对象的状态,所有的类成员函数都可以直接访问和修改这些变量。
原因:
-
类成员变量是对象的一部分:当你创建一个类的实例时,类的成员变量就成为该实例的一部分,每个对象都可以独立持有这些成员变量。
-
类成员函数内部可以访问类的成员变量:类的成员函数(方法)是与对象绑定的,并且它们在对象实例上执行,因此它们可以直接访问和修改该实例的成员变量,无需通过参数传递。
-
隐式访问机制:在类成员函数内部,你可以直接引用类成员变量,因为它们是当前对象的一部分,编译器会自动知道当前对象的上下文。
6. 总结:
修改类的成员变量时,函数内对该成员变量的修改会影响函数外的对象,因为成员变量是与对象实例相关联的,而不是局部作用域内的临时变量。因此,在 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<iostream>
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<iostream>
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函数。

2712

被折叠的 条评论
为什么被折叠?



