栈和队列
简而言之,就是顺序表和链表在满足栈和队列出入数据的特性的基础上衍生出的新的数据结构,也就是栈和队列,他们都有两种实现方式,顺序表实现和链表实现。
1. 栈stack
1.1 栈的概念及结构
栈是只允许在栈顶进行插入删除操作的线性表。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出***LIFO(Last In First Out)***的原则。
- 入栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶Top。 (栈顶对应表尾)
- 出栈:栈的删除操作叫做出栈。出数据也在栈顶Base。(栈底对应表头)
按存储结构,栈分为顺序栈和链栈。栈的顺序存储–顺序栈。栈的链式存储–链栈
栈用来解决先进后出的问题,常见的有进制转换、括号匹配问题、行编辑程序、迷宫求解、表达式求值、八皇后问题、函数递归调用。这些都在本文末尾一一讲解
LIFO是栈的特点,栈接口函数数据插入和删除操作都是在栈顶实现:
- 进栈
- 出栈
1.2 顺序栈实现
逻辑结构如下:
顺序栈实现算法概述:利用顺序表,通过改变出入数据的方式,达到后进先出的效果。
静态顺序表就是实现静态栈,动态顺序表实现的则是动态栈。我们一般用动态栈。
下面是动态栈实现的代码:
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<stdio.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}Stack;
void StackInit(Stack* ps);
void StackDestory(Stack* ps);
void StackPush(Stack* ps, STDataType x);
void StackPop(Stack* ps);
STDataType StackTop(Stack* ps);
int StackSize(Stack* ps);
bool StackEmpty(Stack * ps);
#include"Stack.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(4 * sizeof(STDataType));
if (ps->a == NULL)
{
printf("malloc failed\n");
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
}
void StackDestory(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, 2 * ps->capacity * sizeof(STDataType));
if (tmp == NULL)
{
printf("realloc failed\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top);
ps->top--;
}
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
#include"Stack.h"
void Stacktest()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
while (!StackEmpty(&st))
{
printf("%d\n", StackTop(&st));
StackPop(&st);
}
StackDestory(&st);
}
int main()
{
Stacktest();
}
入栈顺序:12345,出栈顺序:54321
1.3 链栈实现
逻辑结构如下:
用一个头指针指向栈顶的链表,对应只提供头插和头删两个方法实现。模拟实现栈的先进后出。很简单就不在此赘述。
1.4 栈和递归
-
若一个对象部分地包含自己,或用自己给自己定义,则称这个对象是递归的。 -
若一个过程直接的或间接的调用自己,则称这个过程是递归过程。如函数的递归调用、数的遍历、斐波那契数列等 -
著名的迷宫求解问题和Hanoi塔问题都能用递归求解
-
递归问题-用分治法求解
分治法,对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解。
分治求解递归问题算法的一般形式
1.4.1 函数调用过程
调用前,将实参、返回地址等传递给被调用函数–>为被调用函数的局部变量分配存储区–>将控制转移到被调用函数的入口(每次调用都会记录实参,局部变量,返回地址)
调用后,保存被调用函数的计算结果–>释放被调用函数的数据区–>依照被调用函数保存的返回地址将控制转移到调用函数
递归阶乘n!的过程
int Fact(int x)
{
if(x==0)return 1;
else return x*Fact(x-1);
}
1.5 括号匹配问题
20. 有效的括号 - 力扣(Leetcode)这是相应的括号匹配问题力扣题
括号匹配就是“( )”“{ }”“[ ]”这些括号满足一一对应关系。可以嵌套定义,如:({()})。而
括号匹配问题就是:
给定一个只包括 '(' ,')' ,'{' ,'}' ,'[' ,']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号
设计思路:
- 如果碰到的是左圆括号或者左大括号,直接压栈;
- 如果碰到的是右圆括号或者右大括号,就直接和栈顶元素配对:如果匹配,栈顶元素弹栈;反之,括号不匹配;
这里使用C++来实现算法
class Solution {
public:
bool isValid(string s) {
stack<char>storage;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '(' || s[i] == '[' || s[i] == '{')
storage.push(s[i]);
else
{
if (s[i] == ')')
{
if (!storage.empty() && storage.top() == '(')storage.pop();
else return false;
}
else if (s[i] == '}')
{
if (!storage.empty() && storage.top() == '{')storage.pop();
else return false;
}
else if (s[i] == ']')
{
if (!storage.empty() && storage.top() == '[')storage.pop();
else return false;
}
}
}
return storage.empty();
}
};
1.6 表达式求值问题
所谓表达式,就是由变量、常量以及运算符组合而成的式子。其中,常用的运算符无非 !(阶乘运算符)、^(指数 运算符)、+、-、*、/ 、( ) 这几种,比如 3!+4*2/(1-5)^2 就是一个表达式。 那么,如何用栈结构求一个表达式的值呢。用后缀表达式法。
什么是后缀表达式?就是将表达式中所有运算符放在它的运算项后面
这里以 3!+4*2/(1-5)^2 为例:6+8/16
! 运算符对应的运算项为 3 ,转换后得到3! + 运算符对应的运算项是 3! 和 4*2/(1-5)^2 ,转换之后得到:3! 4*2/(1-5)^2 + * 运算符对应的运算项是 4 和 2,转换之后得到 4 2 * / 运算符对应的运算项是4 2 * 和(1-5)^2 ,转换后得到 4 2 * (1-5)^2 / - 运算符对应的运算项是 1 和 5,转换后得到1 5 - ^ 运算符对应的运算项是1 5 - 和2 ,转换后得到 1 5 - 2 ^ 。
整合之后,整个普通表达式就转换成了 3 ! 4 2 * 1 5 - 2 ^ / + 这就是其对应的后缀表达式。
得到的后缀表达式,如何计算?首先创建一个栈。接着按照从左到右的顺序扫描后缀表达式,遇到运算项就入栈。遇到n元运算符,就出栈顶元素n个计算并将计算结果压回栈中。代码实现应注意的是:从栈出来的先后顺序,对应原来运算的哪一个运算项!
如:遇到阶乘(一元运算符),出栈顶计算。遇到乘法(二元运算符),出栈顶两元素计算并压回栈中。循环上述操作,最后栈中最后一个元素,即为此运算项即为整个表达式的值。
double calculate(char* out)
{
int index = 0;
stack<double>result;
while (out[index] != '\0')
{
char c = out[index];
switch (c)
{
case '!':
{
double i = result.top();
result.pop();
double end = 1;
while (i != 1) end *= i-- ;
result.push(end);
break;
}
case '*':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
result.push(left * right);
break;
}
case '/':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
if (!right)
{
cout << "分母为零,错误" << endl;
exit(-1);
}
else
{
result.push(left / right);
break;
}
}
case '+':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
result.push(left + right);
break;
}
case '-':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
result.push(left - right);
break;
}
case '^':
{
double exp = result.top();
result.pop();
double base = result.top();
result.pop();
if (!base)
{
cout << "底数为零" << endl;
exit(-1);
}
else
{
double end = 1;
for (int i = 0; i < exp; i++)
{
end *= base;
}
result.push(end);
break;
}
}
default:
{
result.push(c - 48);
}
}
index++;
}
return result.top();
}
1.6.1 根据表达式求后缀表达式
上面讲过了如何求解表达式:表达式–后缀表达式–利用栈运算,那么如何获得后缀表达式?
首先看规则:
普通表达式转换为后缀表达式需要用到一个空栈(假设名为Optr)和一个空数组(数组名)
- 如果为 ‘0’~‘9’ 的字符,将其添加到 postexp 数组的末尾;
- 如果该字符为除 ‘(’ 和 ‘)’ 以外的运算符,将其与 Optr 栈顶的运算符进行优先级比较(如乘法高于加法),如果该运算符优先级高于或等于栈顶运算符,则将其入栈;反之,如果该运算符优先级小于栈顶运算符,则将栈顶运算符出栈并添加到 postexp 数组的尾部,然后继续拿当前运算符同新的栈顶运算符做大小比较,以此类推。
- 如果该字符为 ‘(’ 运算符,直接入栈;如果为 ‘)’ 运算符,依次取 Optr 栈顶运算符并将其添加到 postexp 数组末尾,直到遇到 ‘(’ 字符为止(注意,‘(’ 字符也从栈顶取出,但不将其添加 postexp 数组中)。
依照以上处理过程,直到将普通表达式遍历完毕,如果 Optr 栈中仍有运算符,依次将它们出栈并添加到 postex数组尾部。最终,postexp 数组内存储的表达式就是转换后的后缀表达式。
总结一句:运算项直接放数组中,运算符压入栈中,只有遇到比栈顶运算符优先级低的或栈空才出栈放入数组中。括号单独考虑。
如此一来运算符优先级高的就紧随运算项,先运算。运算符优先级低的往往在后缀表达式最右边。
下面是表达式3 ! 4 2 * 1 5 - 2 ^ / + 转换为后缀表达式的过程:(按序号看)
代码实现
void transform(char* expr, char* out)
{
stack<char>temp;
int index = 0;
int i = 0;
while(expr[i]!='\0')
{
char c = expr[i];
switch (c)
{
case '!':
{
while (!temp.empty())
{
if (temp.top() == '!')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('!');
i++;
break;
}case '*':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*' || temp.top() == '/')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('*');
i++;
break;
}case '/':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*' || temp.top() == '/')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('/');
i++;
break;
}case '+':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*'
|| temp.top() == '/' || temp.top() == '+' || temp.top() == '-')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('+');
i++;
break;
}case '-':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*'
|| temp.top() == '/' || temp.top() == '+' || temp.top() == '-')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('-');
i++;
break;
}case '(':
{
temp.push('(');
i++;
break;
}case ')':
{
while (temp.top() != '(')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
temp.pop();
i++;
break;
}case '^':
{
while (!temp.empty())
{
if (temp.top() == '!'||temp.top()=='^')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else
{
break;
}
}
temp.push('^');
i++;
break;
}default :
{
out[index++] = c;
i++;
break;
}
}
}
while (!temp.empty())
{
out[index++] = temp.top();
temp.pop();
}
out[index] = '\0';
}
int main() {
char* s = (char*)malloc(15 * sizeof(char));
char* out = (char*)malloc(13 * sizeof(char));
char temp[] = "3!+4*2/(1-5)^2";
for (int i = 0; i < 15; i++)
{
s[i] = temp[i];
}
transform(s,out);
cout << "后缀表达式为:"<< out << endl;
cout <<"表达式运算结果为:"<< calculate(out);
}
2. 队列
2.1 队列的概念及结构
队列:只允许一端插入数据,另一端删除数据作的线性表。队列中数据遵守先进先出FIFO(First In First Out)原则。
- 入队:进行插入操作的一端称为队尾
- 出队:进行删除操作的一端称为队头
按存储结构,队列分为顺序队和链队。队列的顺序存储–顺序队,队列的链式存储–链队。
队列用于解决先进先出的问题,如脱机打印输出、多用户系统中多个用户排成队,分时地循环使用CPU和主存、网络电文传输,按到达时间先后顺序依次进行。
2.2 链队的实现
顺序队相比于链队,顺序队每次出数据都需要向前移动整个队列数据,相对浪费时间。相比之下链队更加优秀。用两个指针分别指向队列的队头和队尾(初始状态如图a)入队出队的过程其实就是无头单向链表的实现(头插和尾删),具体请看:(99条消息) C/C++【数据结构笔记】(顺序表&链表)_Answer-2296的博客-CSDN博客)
-
入队 -
出队 -
队列头文件
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<stdio.h>
#include<assert.h>
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QueueNode;
typedef struct Queue
{
QueueNode* head;
QueueNode* tail;
}Queue;
void QueueInit(Queue* qp);
void QueueDestory(Queue* qp);
void QueuePush(Queue* qp, QDataType x);
void QueuePop(Queue* qp);
QDataType QueueFront(Queue* qp);
QDataType QueueBack(Queue* qp);
int QueueSize(Queue* qp);
bool QueueEmpty(Queue* qp);
#pragma once
#include"Queue.h"
void QueueInit(Queue* qp)
{
assert(qp);
qp->head = qp->tail = NULL;
}
void QueueDestory(Queue* qp)
{
assert(qp);
QueueNode* cur = qp->head;
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
qp->head = qp->tail = NULL;
}
void QueuePush(Queue* qp, QDataType x)
{
assert(qp);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (!newnode)
{
printf("malloc failed\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (qp->tail == NULL)
{
qp->head = qp->tail = newnode;
}
else
{
qp->tail->next = newnode;
qp->tail = newnode;
}
}
void QueuePop(Queue* qp)
{
assert(qp);
assert(qp->head);
if (qp->head->next == NULL)
{
free(qp->head);
qp->head = qp->tail = NULL;
}
else
{
QueueNode* next = qp->head->next;
free(qp->head);
qp->head = next;
}
}
QDataType QueueFront(Queue* qp)
{
assert(qp);
assert(qp->head);
return qp->head->data;
}
QDataType QueueBack(Queue* qp)
{
assert(qp);
assert(qp->head);
return qp->tail->data;
}
int QueueSize(Queue* qp)
{
assert(qp);
int size = 0;
QueueNode* cur = qp->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
bool QueueEmpty(Queue* qp)
{
assert(qp);
return qp->head == NULL;
}
#include"Queue.h"
void Queuetest()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q))
{
printf("%d\n",QueueFront(&q));
QueuePop(&q);
}
}
int main()
{
Queuetest();
}
队列入队顺序12345,出队顺序12345
2.3 顺序队实现
用顺序表头删和尾插分别模拟顺序队的出队和入队,相比于链队,顺序队每次出队都需要将顺序队中所有的元素前移,浪费时间。
而队列的顺序存储也有静态顺序存储和动态顺序存储之分
2.4 环形队列
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组,实现,也可以使用链表实现。
- 内存上并没有环形的结构,因此环形队列实际上是数组的线性空间来实现的。
- 当数据到了尾部该如何处理呢?它将转回到原来位置进行处理,通过取模操作来实现
这里用数组实现的环形队列为例。
Q.front表示队头,初始值为0。
Q.rear表示队列最后一个元素的后一个位置(预留一个空间,作为约定,方便判断队列是否为空)。
maxSize: 数组的最大容量
我们用求模运算来实现循环。一般来说队头位置固定为0,每次出队需要将队列中所有的元素前移,十分浪费空间。对此我们不固定队头,出队,只需要前移队头!
Q.base[Q.rear]=x;
Q.rear=(Q.rear+1)%maxSize;
x=Q.base[s.front];
Q.front=(Q.front+1)%maxSize;
#include<iostream>
using namespace std;
#define MAXSIZE 11
typedef int QueueDataType;
struct Queue{
QueueDataType* base;
int front;
int rear;
};
void QueueInit(Queue& Q)
{
Q.base = new QueueDataType[MAXSIZE];
if (!Q.base)exit(-1);
Q.front = Q.rear = 0;
}
void QueuePush(Queue& Q, QueueDataType data)
{
if ((Q.rear + 1) % MAXSIZE == Q.front)
{
cout << "入队失败,队列已满" << endl;
exit(-1);
}
Q.base[Q.rear] = data;
Q.rear = (Q.rear + 1) % MAXSIZE;
}
void QueuePop(Queue& Q)
{
if (Q.rear == Q.front)
{
cout << "对空,出队失败" << endl;
exit(-1);
}
Q.front = (Q.front + 1) % MAXSIZE;
}
QueueDataType QueueTop(Queue Q)
{
if (Q.rear == Q.front)
{
cout << "队空,取队头失败" << endl;
exit(-1);
}
else
{
return Q.base[Q.front];
}
}
int main()
{
Queue q;
QueueInit(q);
for (int i = 0; i < 10; i++)
{
QueuePush(q, i);
}
for (int i = 0; i < 10; i++)
{
int temp = QueueTop(q);
QueuePop(q);
cout << temp<<" ";
}
}
3. 栈和队列区别与联系
栈中的数据元素遵守后进先出***LIFO(Last In First Out)***的原则。并且是相对而言,多为后进先出是指,栈现有的数据是后进先出,所以可以出现先进栈的数据比后进栈的数据先出(比如数据进栈立马出栈,这时它就比后面数据先出来,所以说栈的后进先出是相对而言的)
队列中数据遵守先进先出FIFO(First In First Out)原则。并且是绝对而言,只要是先进队列的一定是先出来的。
- 静态顺序表实现静态栈,动态顺序表实现动态栈。而顺序表的实现方式是数组,通过数组是否定长即可相应类型的栈。而数据结构的实现方式和顺序表如出一辙,具体不同体现在栈的先进后出的特点,所以相应的函数有所不同。
- 队列的实现则和链表的实现即为相似,都定义了结点这个结构体作为单元,有些不同的是队列还定义了Queue结构体包装有两个指针,分别指向队列头和队列尾。而链表直接用第一个结点(或第一个结点的地址)作为链表的标识。此外具体实现上队列遵循先进先出,所以函数实现上也有所不同。
3.1 栈编码总结
分清数据在哪里发生变化!Stack中的指针,top,capacity在哪里变化,怎么变?对于指针申请空间,我们用函数包装好,内部做判断,capacity和和空间绑定在一块,所以capacity也是在函数内部来变化!top作为待插入数据的位置坐标,是在入栈出栈的时候做出改变。
3.2 队列编码总结
在出队函数实现时尤为需注意野指针的诞生,因为一般队列足够长时,出队只需要操作对头,并释放对应数据。但如果队列只有一个数据,这时释放完空间,却没有对队尾指针修改,此时尾指针指向的是一个未知空间!
4.栈和队列的相互转换
(224条消息) 【数据结构拓展笔记】_栈和队列的相互转换_Answer-2296的博客-CSDN博客
由于篇幅过长,转战。
|