栈
简述
栈(Stack)是一种特殊的线性数据结构,它的特殊之处在于其操作顺序是后进先出(Last In First Out,LIFO)的,即最后压入栈的元素最先弹出。
在程序设计中,栈是一种非常有用的数据结构,它可以帮助我们实现递归算法、表达式求值、括号匹配、回溯算法等等。本文将介绍栈的基本概念、操作和实现方式,并提供基于数组和链表两种方式的栈的代码实现。
栈的基本概念
栈是一种只允许在栈顶进行插入和删除操作的线性数据结构。我们可以将栈比作一堆盘子,每次往上面放盘子或者取盘子都只能从最上面进行。栈中的元素遵循后进先出的原则,也就是说最后一个压入栈的元素最先弹出。
栈有两个主要操作:压栈和弹栈。压栈就是往栈顶插入一个元素,而弹栈则是从栈顶删除一个元素并返回其值。除此之外,还有一些其他操作,例如获取栈顶元素和判断栈是否为空等.
基本操作:
- push(item):将元素压入栈顶。
- pop():从栈顶删除元素。
- top():获取栈顶元素,但不删除。
- empty():判断栈是否为空。
- size():获取栈中元素的数量。
栈的实现方式
栈的实现方式主要有两种:基于数组和基于链表。下面我们分别介绍这两种方式的实现方式。
基于数组的栈
数组实现栈的思路是创建一个数组来保存元素,并记录栈顶指针,栈顶指针指向栈顶元素的位置。栈顶指针初始值为-1,表示栈为空
下面是用C++实现的数组栈
#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class ArrayStack
{
private:
int top; // 栈顶指针
int arr[MAX_SIZE]; // 数组
public:
ArrayStack() // 构造函数
{
top = -1; // 初始化栈顶指针为-1
}
bool push(int x) // 入栈操作
{
if (top >= MAX_SIZE - 1) // 栈满,无法入栈
{
return false;
}
else
{
arr[++top] = x; // 栈顶指针加1,将元素入栈
return true;
}
}
bool pop() // 出栈操作
{
if (top < 0) // 栈空,无法出栈
{
return false;
}
else
{
top--; // 栈顶指针减1,表示出栈
return true;
}
}
int peek() // 获取栈顶元素
{
if (top < 0)// 栈空,无栈顶元素
{
return -1;
}
else
{
return arr[top];
}
}
bool empty() // 判断栈是否为空
{
return top < 0;
}
int size() // 获取栈中元素的数量
{
return top + 1;
}
};
int main()
{
// 创建一个栈
ArrayStack s;
// 入栈操作
s.push(1);
s.push(2);
s.push(3);
//获取栈中元素大小
printf("Stack size %d\n",s.size());
printf("Top value %d\n",s.peek()) ;
// 出栈操作
s.pop();
//获取栈中元素大小
printf("Stack size %d\n",s.size());
printf("Top value %d\n",s.peek()) ;
return 0;
}
输出:
Stack size 3
Top value 3
Stack size 2
Top value 2
基于链表的栈
#include <iostream>
using namespace std;
// 定义一个节点结构体
struct Node
{
int data;
Node* next;
};
// 定义一个链表栈结构体
struct LinkedStack
{
Node* top; // 栈顶指针
int size; // 栈的大小
// 初始化栈
LinkedStack()
{
top = NULL;
size = 0;
}
// 判断栈是否为空
bool isEmpty()
{
return top == NULL;
}
// 入栈操作
void push(int data)
{
Node* newNode = new Node;
newNode->data = data;
newNode->next = top;
top = newNode;
size++;
}
// 出栈操作
int pop()
{
if (isEmpty())
{
cout << "栈已空,无法出栈" << endl;
return -1;
}
int data = top->data;
Node* temp = top;
top = top->next;
delete temp;
size--;
return data;
}
// 获取栈顶元素
int getTop()
{
if (isEmpty())
{
cout << "栈已空,无法获取栈顶元素" << endl;
return -1;
}
return top->data;
}
// 获取栈的大小
int getSize()
{
return size;
}
// 清空栈
void clear()
{
while (!isEmpty())
{
pop();
}
}
};
int main()
{
LinkedStack stack;
stack.push(1);
stack.push(2);
stack.push(3);
cout << "栈顶元素为:" << stack.getTop() << endl;
cout << "栈的大小为:" << stack.getSize() << endl;
stack.pop();
cout << "栈顶元素为:" << stack.getTop() << endl;
cout << "栈的大小为:" << stack.getSize() << endl;
stack.clear();
cout << "栈的大小为:" << stack.getSize() << endl;
return 0;
}
输出
栈顶元素为:3
栈的大小为:3
栈顶元素为:2
栈的大小为:2
栈的大小为:0
上图是链表形式组成的栈,其中栈底节点的下一个指针指向空。我们定义了一个ListNode类表示链表的结点,以及一个Stack类表示栈。Stack类包含了push、pop、peek、isEmpty四个方法,分别用于向栈中添加元素、从栈中弹出元素、获取栈顶元素、以及判断栈是否为空。
我们使用top变量来表示栈顶元素,每次添加元素时将其添加到链表的头部,每次弹出元素时从链表头部删除元素。在pop和peek方法中,我们需要先判断栈是否为空。如果栈为空,则输出错误信息并返回一个默认值。
STL库实现栈
#include <iostream>
#include <stack>
using namespace std;
int main()
{
stack<int> myStack;
// 添加元素
myStack.push(1);
myStack.push(2);
myStack.push(3);
// 获取栈顶元素
cout << "栈顶元素是:" << myStack.top() << endl;
// 弹出栈顶元素
myStack.pop();
// 遍历栈中元素
while (!myStack.empty())
{
cout << myStack.top() << " ";
myStack.pop();
}
cout << endl;
return 0;
}
输出
栈顶元素是:3
2 1
栈应用
数据反转
STL库的方式
#include <iostream>
#include <stack>
using namespace std;
void reverseArray(int arr[], int n)
{
stack<int> s;
for(int i=0; i<n; i++)
{
s.push(arr[i]);
}
for(int i=0; i<n; i++)
{
arr[i] = s.top();
s.pop();
}
}
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr)/sizeof(arr[0]);
cout << "Original array: ";
for(int i=0; i<n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
reverseArray(arr, n);
cout << "Reversed array: ";
for(int i=0; i<n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
输出结果:
Original array: 1 2 3 4 5
Reversed array: 5 4 3 2 1
链表方式
以下是实现c++链表栈的数据反转的代码:
#include <iostream>
#include <stack>
using namespace std;
// 定义链表的节点结构体
struct ListNode
{
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
// 定义链表栈的数据结构
class LinkedListStack
{
private:
stack<ListNode*> stk; // 使用栈存储链表节点指针
public:
LinkedListStack() {}
void push(ListNode* node)
{
stk.push(node);
}
ListNode* pop()
{
ListNode* node = stk.top();
stk.pop();
return node;
}
bool empty()
{
return stk.empty();
}
};
// 实现链表的反转
ListNode* reverseList(ListNode* head)
{
if (head == NULL || head->next == NULL)
{
return head;
}
LinkedListStack stk;
while (head != NULL)
{
stk.push(head);
head = head->next;
}
ListNode* newHead = stk.pop();
ListNode* p = newHead;
while (!stk.empty())
{
p->next = stk.pop();
p = p->next;
}
p->next = NULL;
return newHead;
}
// 测试链表反转函数
void testReverseList()
{
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
cout << "原链表:" << endl;
ListNode* p = head;
while (p != NULL) {
cout << p->val << " ";
p = p->next;
}
cout << endl << "反转后的链表:" << endl;
ListNode* newHead = reverseList(head);
p = newHead;
while (p != NULL) {
cout << p->val << " ";
p = p->next;
}
}
int main()
{
testReverseList();
return 0;
}
运行结果:
原链表:
1 2 3 4 5
反转后的链表:
5 4 3 2 1
检查括号的匹配性
#include <iostream>
using namespace std;
int main()
{
for(int i=0;i<5;i++
{
i++;
return 0;
}
main.cpp: In function ‘int main()’:
main.cpp:6:24: error: expected ‘)’ before ‘{’ token
6 | for(int i=0;i<5;i++
| ~ ^
| )
7 | {
| ~
简述:如上编写代码的时候,会偶尔出现for循环的右括号)
或者}
、]
等漏写的情况出现,这种情况一般编译器都会指示我们某行某列出现漏写的情况.那么编译器是如何实现括号漏泄的检测?括号漏写的检测原理又是怎么样的?
这种检测原理利用的就是栈这种数据结构。假如,要检测的符号是{[()]}
从左到右先将左边每检测一个开符号{[(
就压入栈中。当检测到一个闭合的符号)]}
就将它最近的那一个符号惊醒弹栈操作,如果栈顶的符号与检测到的闭合符号不符合就提示某某符号出现问题。
利用数组实现
#include<iostream>
#include<stack>
#include<string>
using namespace std;
bool isMatched(string str) // 判断括号是否匹配函数
{
stack<char> s;
for(int i=0; i<str.length(); i++)
{
if(str[i] == '(' || str[i] == '[' || str[i] == '{') // 如果是左括号,入栈
s.push(str[i]);
else if(str[i] == ')') // 如果是右括号,判断栈顶元素是否匹配
{
if(s.empty() || s.top() != '(')
return false;
else
s.pop();
}
else if(str[i] == ']')
{
if(s.empty() || s.top() != '[')
return false;
else
s.pop();
}
else if(str[i] == '}')
{
if(s.empty() || s.top() != '{')
return false;
else
s.pop();
}
}
return s.empty(); // 最后判断栈是否为空,为空则匹配,不为空则不匹配
}
int main()
{
string str;
cout << "请输入一个字符串:" << endl;
getline(cin, str); // 读入一行字符串
if(isMatched(str))
cout << "括号匹配" << endl;
else
cout << "括号不匹配" << endl;
return 0;
}
链表实现
#include<iostream>
using namespace std;
struct node
{
char data;
node *next;
};
class Stack
{
private:
node *top;
public:
Stack()
{
top = NULL;
}
void push(char x)
{
node *temp = new node;
temp -> data = x;
temp -> next = top;
top = temp;
}
void pop()
{
if(top == NULL)
{
cout << "Stack Underflow" << endl;
return;
}
node *temp = top;
top = top -> next;
delete(temp);
}
bool isEmpty()
{
return (top == NULL);
}
char Top()
{
if(top == NULL)
{
return NULL;
}
return top -> data;
}
};
bool isBalanced(string exp)
{
Stack s;
for(int i=0; i<exp.length(); i++){
if(exp[i] == '(' || exp[i] == '[' || exp[i] == '{')
{
s.push(exp[i]);
}
else if(exp[i] == ')' || exp[i] == ']' || exp[i] == '}')
{
if(s.isEmpty() || (exp[i] == ')' && s.Top() != '(') || (exp[i] == ']' && s.Top() != '[') || (exp[i] == '}' && s.Top() != '{')){
return false;
}
else
{
s.pop();
}
}
}
return s.isEmpty();
}
int main()
{
string exp;
cout << "Enter expression: ";
cin >> exp;
if(isBalanced(exp)){
cout << "Balanced" << endl;
}
else
{
cout << "Not Balanced" << endl;
}
return 0;
}
前中后缀的应用
在计算机科学中,前缀、中缀和后缀表达式都是表示数学表达式的方式,它们之间的差别在于运算符的位置不同。
中缀表达式
所谓的中缀表达式就是运算符夹在操作符之间。在计算机中,中缀表达式是我们最常见的数学表达式形式,例如 3 + 4 * 5 - 6,然而中缀表达式存在一些问题。
运算符优先级问题:在中缀表达式中,不同运算符有不同的优先级,例如乘除法的优先级高于加减法,括号中的运算符优先级更高。因此,在计算中缀表达式时,需要遵循运算符优先级规则,进行正确的计算。这就增加了表达式的复杂度和计算的难度。
举例:考虑中缀表达式 2 + 3 * 4 - 5,按照运算符优先级规则,需要先计算 3 * 4,然后再加上 2,最后减去 5,得到的结果为 9。但如果不按照优先级规则计算,例如先计算 2 + 3,得到 5,再乘以 4,得到 20,最后减去 5,得到 15,这样计算结果就是错误的。
括号匹配问题:在中缀表达式中,括号的使用可以改变运算符优先级,但是如果括号不匹配,就会导致表达式无法正确解析。因此,在编写中缀表达式时,需要仔细检查括号的使用。
举例:考虑中缀表达式 (2 + 3) * (4 - 5),这个表达式中,括号是匹配的,但如果将其改成 (2 + 3 * (4 - 5)),就存在括号不匹配的问题,这样的表达式就无法正确解析。
数字位数限制问题:在中缀表达式中,数字的位数是有限制的,如果数字的位数太长,就会导致表达式无法正确解析。因此,在处理大数运算时,需要使用其他的表达式形式。
举例:考虑中缀表达式 12345678901234567890 + 1,这个表达式中的数字已经超过了计算机可以表示的位数,因此无法正确计算。
为了解决中缀表达式存在的问题,人们提出了前缀和后缀表达式等其他表达式形式,可以简化计算过程和减少计算错误。
后缀表达式
后缀表达式也被称为逆波兰式(Reverse Polish notation),它与中缀表达式相比,运算符位于操作数的后面。
例如,“1 2 3 * +”表示的就是中缀表达式“1+2*3”。后缀表达式也有类似于前缀表达式的计算过程,只不过是从左到右扫描表达式,遇到操作数就压入栈中,遇到运算符就弹出栈中的两个元素进行计算,最后得到表达式的结果。
前缀表达式
前缀表达式也被称为波兰式(Polish notation),它与中缀表达式相比,运算符位于操作数的前面。
例如,“+ 1 * 2 3”表示的就是中缀表达式“1+2*3”。前缀表达式在计算机领域中得到了广泛应用,因为它的计算过程非常简单,只需要从右到左依次扫描表达式,遇到操作数就压入栈中,遇到运算符就弹出栈中的两个元素进行计算,最后得到表达式的结果。由于前缀表达式的计算过程非常直观,因此在计算机科学中被广泛使用。
前后缀的优点
- 前缀和后缀表达式的计算过程非常直观,不需要进行语法分析和优先级计算,因此计算速度更快。
- 由于前缀和后缀表达式不需要使用括号,因此可以减少因括号使用不当而引起的歧义和混淆。
- 前缀和后缀表达式可以使用栈来实现,而栈是一种非常高效的数据结构。
注意事项
- 在前缀和后缀表达式中,每个操作数和运算符之间必须用空格隔开,否则会引起解析错误。
- 在使用栈实现前缀和后缀表达式计算时,需要注意栈的数据类型和顺序,以免出现数据类型不匹配或顺序错误等问题。在使用前缀和后缀表达式时,需要注意表达式中运算符的优先级和结合性,以免出现计算错误。
- 在将中缀表达式转换成前缀或后缀表达式时,需要遵循一定的规则和算法,以确保转换正确无误。常用的算法有中缀表达式转后缀表达式的逆波兰算法和中缀表达式转前缀表达式的波兰算法。
- 在使用前缀和后缀表达式计算时,需要考虑运算数的位数和精度问题,以免出现精度丢失或计算溢出等问题。
前后缀实现原理是:由中缀->前缀->后缀。前后缀只需将操作数放进栈中,遇到操作符就弹出。
// 栈的实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXSIZE 100
typedef struct
{
char data[MAXSIZE];
int top;
} Stack;
void InitStack(Stack *s)
{
s->top = -1;
}
bool IsEmpty(Stack *s)
{
return s->top == -1;
}
bool IsFull(Stack *s)
{
return s->top == MAXSIZE - 1;
}
bool Push(Stack *s, char x)
{
if (IsFull(s))
{
return false;
}
else
{
s->data[++(s->top)] = x;
return true;
}
}
bool Pop(Stack *s, char *x)
{
if (IsEmpty(s))
{
return false;
}
else
{
*x = s->data[(s->top)--];
return true;
}
}
bool GetTop(Stack *s, char *x)
{
if (IsEmpty(s))
{
return false;
}
else
{
*x = s->data[s->top];
return true;
}
}
void ClearStack(Stack *s)
{
s->top = -1;
}
// 中缀表达式转换为后缀表达式
void InfixToPostfix(char infix[], char postfix[])
{
int i = 0, j = 0;
char ch, x;
Stack s;
InitStack(&s);
while ((ch = infix[i++]) != '\\0')
{
switch (ch)
{
case '(':
Push(&s, ch);
break;
case ')':
while (GetTop(&s, &x) && x != '(')
{
Pop(&s, &x);
postfix[j++] = x;
}
Pop(&s, &x);
break;
case '+':
case '-':
while (!IsEmpty(&s) && GetTop(&s, &x) && x != '(')
{
Pop(&s, &x);
postfix[j++] = x;
}
Push(&s, ch);
break;
case '*':
case '/':
while (!IsEmpty(&s) && GetTop(&s, &x) && (x == '*' || x == '/')) {
Pop(&s, &x);
postfix[j++] = x;
}
Push(&s, ch);
break;
default:
postfix[j++] = ch;
break;
}
}
while (!IsEmpty(&s))
{
Pop(&s, &x);
postfix[j++] = x;
}
postfix[j] = '\\0';
}
// 中缀表达式转换为前缀表达式
void InfixToPrefix(char infix[], char prefix[])
{
int i = 0, j = 0;
char ch, x;
Stack s;
InitStack(&s);
while ((ch = infix[i++]) != '\\0')
{
switch (ch)
{
case '(':
Push(&s, ch);
break;
case ')':
while (GetTop(&s, &x) && x != '(')
{
Pop(&s, &x);
prefix[j++] = x;
}
Pop(&s, &x);
break;
case '+':
case '-':
while (!IsEmpty(&s) && GetTop(&s, &x) && x != '(')
{
Pop(&s, &x);
prefix[j++] = x;
}
Push(&s, ch);
break;
case '*':
case '/':
while (!IsEmpty(&s) && GetTop(&s, &x) && (x == '*' || x == '/')) {
Pop(&s, &x);
prefix[j++] = x;
}
Push(&s, ch);
break;
default:
prefix[j++] = ch;
break;
}
}
while (!IsEmpty(&s))
{
Pop(&s, &x);
prefix[j++] = x;
}
prefix[j] = '\\0';
}
// 后缀表达式求值
int EvaluatePostfix(char postfix[])
{
int i = 0;
char ch, x;
int a, b;
Stack s;
InitStack(&s);
while ((ch = postfix[i++]) != '\\0')
{
switch (ch)
{
case '+':
Pop(&s, &x);
b = x - '0';
Pop(&s, &x);
a = x - '0';
Push(&s, a + b + '0');
break;
case '-':
Pop(&s, &x);
b = x - '0';
Pop(&s, &x);
a = x - '0';
Push(&s, a - b + '0');
break;
case '*':
Pop(&s, &x);
b = x - '0';
Pop(&s, &x);
a = x - '0';
Push(&s, a * b + '0');
break;
case '/':
Pop(&s, &x);
b = x - '0';
Pop(&s, &x);
a = x - '0';
Push(&s, a / b + '0');
break;
default:
Push(&s, ch);
break;
}
}
Pop(&s, &x);
return x - '0';
}
int main()
{
char infix[MAXSIZE], postfix[MAXSIZE], prefix[MAXSIZE];
int result;
printf("请输入中缀表达式:");
scanf("%s", infix);
InfixToPostfix(infix, postfix);
printf("后缀表达式为:%s\", postfix);
InfixToPrefix(infix, prefix);
printf("前缀表达式为:%s\", prefix);
result = EvaluatePostfix(postfix);
printf("表达式的值为:%d\", result);
return 0;
}
输入
a+b*c
输出
后缀表达式为:abc*+
前缀表达式为:+a*bc
链表方式
#include <iostream>
using namespace std;
struct Node
{
char data;
Node* next;
Node(char d) : data(d), next(nullptr) {}
};
class Stack
{
private:
Node* top;
public:
Stack() : top(nullptr) {}
~Stack()
{
while (top)
{
Node* temp = top;
top = top->next;
delete temp;
}
}
bool empty()
{
return top == nullptr;
}
void push(char c)
{
Node* newNode = new Node(c);
newNode->next = top;
top = newNode;
}
char pop()
{
if (empty())
{
cout << "Stack is empty!" << endl;
return '\\0';
}
char data = top->data;
Node* temp = top;
top = top->next;
delete temp;
return data;
}
char peek()
{
if (empty())
{
cout << "Stack is empty!" << endl;
return '\\0';
}
return top->data;
}
};
bool isOperator(char c)
{
return (c == '+' || c == '-' || c == '*' || c == '/');
}
bool isOperand(char c)
{
return (c >= '0' && c <= '9');
}
string infixToPostfix(string infix)
{
Stack s;
string postfix = "";
for (int i = 0; i < infix.length(); i++)
{
char c = infix[i];
if (isOperand(c))
{
postfix += c;
}
else if (c == '(')
{
s.push(c);
}
else if (c == ')') {
while (!s.empty() && s.peek() != '(')
{
postfix += s.pop();
}
if (!s.empty() && s.peek() == '(')
{
s.pop();
}
else
{
cout << "Invalid infix expression!" << endl;
return "";
}
}
else if (isOperator(c))
{
while (!s.empty() && s.peek() != '(' && ((c == '+' || c == '-') && (s.peek() == '*' || s.peek() == '/')))
{
postfix += s.pop();
}
s.push(c);
}
else
{
cout << "Invalid infix expression!" << endl;
return "";
}
}
while (!s.empty())
{
postfix += s.pop();
}
return postfix;
}
int evaluatePostfix(string postfix)
{
Stack s;
for (int i = 0; i < postfix.length(); i++)
{
char c = postfix[i];
if (isOperand(c))
{
s.push(c - '0');
}
else if (isOperator(c))
{
int op2 = s.pop();
int op1 = s.pop();
switch (c) {
case '+': s.push(op1 + op2); break;
case '-': s.push(op1 - op2); break;
case '*': s.push(op1 * op2); break;
case '/': s.push(op1 / op2); break;
}
}
else
{
cout << "Invalid postfix expression!" << endl;
return 0;
}
}
return s.pop();
}
int main()
{
string infix = "5+((1+2)*4)-3";
string postfix = infixToPostfix(infix);
cout << "Postfix expression: " << postfix << endl;
int result = evaluatePostfix(postfix);
cout << "Result: " << result << endl;
return 0;
}
输出
Postfix expression: 512+4*3-+
Result: 14
时间与空间复杂度
栈是一种后进先出(LIFO)的数据结构,其时间复杂度与空间复杂度如下:
入栈
(push)操作的时间复杂度是 O(1),因为只需要在栈顶添加元素即可,不需要对整个栈进行操作。
出栈
(pop)操作的时间复杂度也是 O(1),因为只需要从栈顶删除一个元素即可,不需要对整个栈进行操作。
获取栈顶元素
(top)操作的时间复杂度也是 O(1),因为栈会记录栈顶元素的位置,只需要返回这个位置上的元素即可。
判断栈是否为空
(empty)操作的时间复杂度也是 O(1),因为只需要判断栈中是否有元素即可。
对于空间复杂度
,栈的空间复杂度取决于栈中的元素数量。假设栈中最多存储 n 个元素,那么栈的空间复杂度就是 O(n)。
需要注意的是,如果使用数组实现的栈,当栈已满时需要进行扩容操作,此时入栈操作的时间复杂度就变为了 O(n),因为需要重新分配空间并将元素复制到新空间中。因此,在使用数组实现栈时,需要预估好栈的最大容量,以免出现频繁的扩容操作影响效率。而链表实现的栈则没有这个问题。
队列
简述
队列(Queue)是一种常用的数据结构,其特点是遵循“先进先出”(First In First Out,FIFO)的原则,即最先进入队列的元素最先被取出。类比现实生活中排队的场景,先到先得,后到只能等待。
在计算机科学中,队列也是一种基本的数据结构,常用于算法设计、操作系统、计算机网络等领域。本篇文章将介绍C++中队列的实现方式和基本操作。特别在Freertos中经常会使用到队列这种数据结构
队列的实现
队列的实现方式有多种,最常见的有数组和链表两种。数组实现的队列在内存中连续存储,支持随机访问,而链表实现的队列则通过指针进行连接,支持动态扩容。
(1)数组实现队列
数组实现的队列需要定义两个指针 front 和 rear,分别表示队头和队尾。其中,front 指针指向队列的第一个元素,而 rear 指针指向队列的最后一个元素的下一个位置,即 rear = (last_index + 1) % capacity。这样,当队列为空时,front 和 rear 的值相等。
入队操作时,我们将元素添加到 rear 指针所指向的位置,并将 rear 指针向后移动一位。如果队列已满,入队操作将无法执行。
出队操作时,我们将 front 指针指向的元素移除,并将 front 指针向后移动一位。如果队列为空,出队操作将无法执行。
下面是数组实现队列的示意图:
+---+---+---+---+---+---+---+---+
| | | | | | | | |
+---+---+---+---+---+---+---+---+
^ ^
front rear
(2)链表实现队列
链表实现的队列需要定义一个队列节点结构体,其中包含元素值和下一个节点的指针。我们还需要定义两个指针 front 和 rear,分别指向队头和队尾的节点。
入队操作时,我们创建一个新的节点,并将其添加到链表的尾部,然后将 rear 指针指向新节点。如果队列为空,我们同时将 front 指针指向新节点。
出队操作时,我们将 front 指针指向的节点移除,并将 front 指针指向下一个节点。如果队列为空,出队操作将无法执行。
下面是链表实现队列的示意图:
+---+ +---+ +---+ +---+
| 1 | -> | 2 | -> | 3 | -> | 4 |
+---+ +---+ +---+ +---+
^ ^
front rear
队列的运行原理比较简单,它遵循先进先出的原则,即先入队的元素先出队,后入队的元素后出队。这个特点使得队列在很多场景中非常有用,例如在操作系统中,进程被添加到就绪队列中等待执行;在网络中,数据包被添加到发送队列中等待发送等等。
下面我们以数组实现的队列为例,来分析一下队列的运行过程:
队列的初始化
在使用队列之前,需要先对队列进行初始化。对于数组实现的队列,我们需要定义队列的容量,并分别初始化 front 和 rear 指针。此时,队列为空,front 和 rear 的值相等。
入队操作
入队操作可以将元素添加到队列的末尾,并将 rear 指针向后移动一位。如果队列已满,入队操作将无法执行。下面是入队操作的示意图:
+---+---+---+---+---+---+---+---+
| A | B | C | | | | | |
+---+---+---+---+---+---+---+---+
^ ^
front rear
出队操作
出队操作可以将队列的第一个元素移除,并将 front 指针向后移动一位。如果队列为空,出队操作将无法执行。下面是出队操作的示意图:
+---+---+---+---+---+---+---+---+
| | B | C | | | | | |
+---+---+---+---+---+---+---+---+
^ ^
front rear
在使用队列时,需要注意以下几点:
(1)当队列为空时,front 和 rear 的值应该相等。当队列已满时,rear 的值应该是 last_index + 1,其中 last_index 表示队列的最后一个元素的索引,即 rear = (last_index + 1) % capacity。
(2)在使用数组实现的队列时,由于队列的容量是固定的,因此需要注意队列满的情况。一种解决方法是使用循环队列,即将数组视为一个环形结构,这样当 rear 指针到达数组末尾时,可以将其重新指向数组的开头。
(3)在使用链表实现的队列时,需要注意处理队列为空的情况,否则在进行出队操作时可能会导致程序出错。一种解决方法是将 front 和 rear 初始值都设置为 NULL,表示队列为空。
(4)在使用队列时,需要考虑队列的并发性。如果多个线程同时访问同一个队列,可能会出现并发问题。一种解决方法是使用线程安全的队列,例如使用互斥锁或信号量来保护队列的访问。
(5)在使用队列时,需要考虑队列的性能。如果队列的入队和出队操作比较频繁,那么队列的性能可能会成为程序的瓶颈。一种解决方法是使用双端队列,即既可以在队列的前端插入元素,也可以在队列的后端插入元素。这样可以减少队列的移动操作,从而提高队列的性能。
数组实现队列
数组实现队列需要两个指针,一个指向队首(front),一个指向队尾(rear)。当有新元素入队时,将其插入到队尾并将rear指针后移;当有元素出队时,将队首元素删除并将front指针后移。由于队列是先进先出的,因此我们需要保证队首指针始终指向队列中最先入队的元素。
#include <iostream>
using namespace std;
class ArrayQueue {
private:
int *arr; // 用于存储队列元素的数组
int capacity; // 队列容量
int front, rear; // 队头和队尾下标
public:
// 构造函数,初始化队列容量和数组
ArrayQueue(int cap)
{
capacity = cap;
arr = new int[capacity];
front = rear = -1;
}
// 入队操作
void enqueue(int data)
{
if (isFull())
{
cout << "Queue is full" << endl;
return;
}
if (isEmpty())
{
front = rear = 0;
}
else
{
rear = (rear + 1) % capacity;
}
arr[rear] = data;
}
// 出队操作
int dequeue()
{
if (isEmpty())
{
cout << "Queue is empty" << endl;
return -1;
}
int data = arr[front];
if (front == rear)
{
front = rear = -1;
}
else
{
front = (front + 1) % capacity;
}
return data;
}
// 获取队首元素
int getFront()
{
if (isEmpty())
{
cout << "Queue is empty" << endl;
return -1;
}
return arr[front];
}
// 获取队列大小
int size()
{
if (isEmpty())
{
return 0;
}
if (front <= rear)
{
return rear - front + 1;
}
else
{
return capacity - front + rear + 1;
}
}
// 判断队列是否为空
bool isEmpty()
{
return front == -1 && rear == -1;
}
// 判断队列是否已满
bool isFull()
{
return (rear + 1) % capacity == front;
}
// 析构函数,释放数组空间
~ArrayQueue()
{
delete[] arr;
}
};
// 测试
int main()
{
ArrayQueue queue(5);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
cout << queue.dequeue() << endl;
cout << queue.getFront() << endl;
cout << queue.size() << endl;
cout << queue.isEmpty() << endl;
cout << queue.isFull() << endl;
return 0;
}
输出
1
2
2
0
0
链表实现队列
#include <iostream>
using namespace std;
class Node
{
public:
int data;
Node* next;
Node(int val)
{
data = val;
next = nullptr;
}
};
class LinkedListQueue
{
private:
Node* front; // 队头指针
Node* rear; // 队尾指针
int size; // 队列大小
public:
// 构造函数,初始化队头、队尾指针和队列大小
LinkedListQueue()
{
front = rear = nullptr;
size = 0;
}
// 入队操作
void enqueue(int data)
{
Node* newNode = new Node(data);
if (isEmpty())
{
front = rear = newNode;
}
else
{
rear->next = newNode;
rear = newNode;
}
size++;
}
// 出队操作
int dequeue()
{
if (isEmpty())
{
cout << "Queue is empty" << endl;
return -1;
}
Node* temp = front;
int data = temp->data;
front = front->next;
delete temp;
size--;
if (isEmpty())
{
rear = nullptr;
}
return data;
}
// 获取队首元素
int getFront()
{
if (isEmpty())
{
cout << "Queue is empty" << endl;
return -1;
}
return front->data;
}
// 获取队列大小
int getSize()
{
return size;
}
// 判断队列是否为空
bool isEmpty()
{
return size == 0;
}
// 析构函数,释放链表空间
~LinkedListQueue()
{
Node* temp = front;
while (temp != nullptr)
{
Node* next = temp->next;
delete temp;
temp = next;
}
}
};
// 测试
int main()
{
LinkedListQueue queue;
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
cout << queue.dequeue() << endl;
cout << queue.getFront() << endl;
cout << queue.getSize() << endl;
cout << queue.isEmpty() << endl;
return 0;
}
输出
1
2
2
0
0
STL库实现队列
#include <iostream>
#include <queue> // 包含队列头文件
using namespace std;
int main()
{
queue<int> q; // 定义一个空队列
q.push(1); // 入队操作
q.push(2);
q.push(3);
cout << "队列大小为:" << q.size() << endl; // 获取队列大小
cout << "队首元素为:" << q.front() << endl; // 获取队首元素
q.pop(); // 出队操作
cout << "队首元素为:" << q.front() << endl;
cout << "队列是否为空:" << q.empty() << endl; // 判断队列是否为空
return 0;
}
输出
队列大小为:3
队首元素为:1
队首元素为:2
队列是否为空:0
这个例子中,我们首先创建了一个空的queue队列对象。然后,我们使用push()方法将元素1、2、3入队。size()方法返回队列的大小,front()方法返回队列的队首元素。我们使用pop()方法从队列中删除队首元素,empty()方法返回一个布尔值,表示队列是否为空。
需要注意的是,STL队列默认是以双端队列的形式实现的,可以在队列前端和后端进行插入和删除操作。如果我们想要实现一个纯粹的队列,只能使用队列的部分接口来进行操作。
时间与空间复杂度
入队
(enqueue)操作的时间复杂度是 O(1),因为只需要在队列的末尾添加元素即可,不需要对整个队列进行操作。
出队
(dequeue)操作的时间复杂度也是 O(1),因为只需要从队列的头部删除一个元素即可,不需要对整个队列进行操作。
获取队列大小
(size)操作的时间复杂度也是 O(1),因为队列会记录当前队列中元素的个数,只需要返回这个记录的值即可。
获取队首元素
(front)操作的时间复杂度也是 O(1),因为队列会记录队首元素的位置,只需要返回这个位置上的元素即可。
判断队列是否为空
(empty)操作的时间复杂度也是 O(1),因为只需要判断队列中是否有元素即可。
对于空间复杂度,队列的空间复杂度取决于队列中的元素数量。假设队列中最多存储 n 个元素,那么队列的空间复杂度就是 O(n)。
需要注意的是
,如果使用数组实现的队列,当队列已满时需要进行扩容操作,此时入队操作的时间复杂度就变为了 O(n),因为需要重新分配空间并将元素复制到新空间中。因此,在使用数组实现队列时,需要预估好队列的最大容量,以免出现频繁的扩容操作影响效率。而链表实现的队列则没有这个问题。