目录
一、栈
通俗来说栈的特点可以被描述为“先进后出,后进先出”,正是因为这样的顺序,导致了栈可以做到使得数据部分甚至整体逆序的特点。
因此,栈属于一种只能对栈顶进行操作的数据结构(也就是绪论中所说只能对一端进行操作的结构)
3.1 栈的简单应用
3.1.1 括号匹配问题
这类题目一般是通常是给定一串括号,需要你判定输入的括号是否合法(即每个左括号有右括号对应且括号类型合法)
利用栈“先进后出,后进先出”的特性,这类问题往往都可以先遍历字符串,读入左括号,当遇到右括号时判断栈顶的左括号是否和当前位置匹配,若匹配则出栈继续,否侧就不合法。使得该算法的时间复杂度为O(n)
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
const int N = 1e6 + 100;
const int mod = 1e9 + 7;
const int maxn = 0x3f3f3f3f3f3f3f3f;
typedef pair<int, int> PII;
void solve() {
stack<char>st;
string x;
char k;
cin>>x;
for (int i=0;x[i];i++) {
k=x[i];
if (k == '(' || k == '[' ) st.push(k);
else if (st.empty()) {
cout << "Wrong\n";
return;
} else if (k == ')') {
if (st.top() == '(') st.pop();
else {
cout << "Wrong\n";
return;
}
} else if (k == ']') {
if (st.top() == '[') st.pop();
else {
cout << "Wrong\n";
return;
}
}
}
if(st.empty()) cout<<"OK\n";
else cout<<"Wrong\n";
}
signed main() {
int t = 1;
// cin >> t;
while (t--) solve();
}
3.1.2 表达式求值问题
这类问题其实就是括号匹配问题的升级版,由于在求值的时候需要先判断是否有括号以及括号的作用区域(括号优先级最高),可以使用栈的方式来实现,实现代码由于较长就不在这里展示了,在我的另一篇文章中有类似的实现(73条消息) 离散数学实验三则(关系元算,集合运算与操作,最短路)_ablity_66的博客-CSDN博客https://blog.csdn.net/m0_61126523/article/details/124607604
3.2 栈的操作
栈的操作实现其实和顺序表没有什么区别,唯一要注意的一点在于由于一般栈的操作只能发生在栈顶,所以只能在顺序表的尾端进行操作。
3.3 栈与递归
递归:在一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用,则称其为递归。
3.3.1 递归的适用情况
书本上给出了三种用递归解题的情况:
1)定义是递归的
如斐波拉契数列,,这种递推定义的数列往往可以用递归的方式解决。
而有一种思想叫做分治,简单来说就是将复杂的问题分解成毫不相关的若干个小问题来解决,最典型的例子有求阶乘,归并排序和求逆序对
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int INF = 0x3f3f3f3f3f3f3f3f;
const int N = 1e6 + 10;
const int mod = 1e9 + 7;
typedef pair<int, int> PII;
int f(int n) {
if (n == 1) return 1;
else return n * f(n - 1);
}
void solve() {
int n;
cin >> n;
cout << f(n);
}
signed main() {
IOS;
int t = 1;
// cin >> t;
for (int i = 1; i <= t; i++) {
solve();
}
}
递归法求阶乘
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int a[N];
int tmp[N];
void solve(int l,int r)
{
if(l>=r) return;
int mid=l+r>>1;
solve(l,mid);
solve(mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r)
{
if(a[i]<=a[j]) tmp[k++]=a[i++];
else tmp[k++]=a[j++];
}
while(i<=mid) tmp[k++]=a[i++];
while(j<=r) tmp[k++]=a[j++];
for(int i=l,j=0;i<=r;i++,j++) a[i]=tmp[j];
}
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
solve(1,n);
for(int i=1;i<=n;i++) cout<<a[i]<<" ";
}
归并排序
2)数据结构是递归的
某些数据结构本身就具有递归的特性,如链表,树和图,它们的后继本身其实是和当前节点具有相同的定义方式,因此,在实现遍历等操作的时候可以用递归的方式实现
3)解法是递归的
诸如深度优先搜索(DFS)求最短路或者是汉诺塔问题,解法都是可以写成递归的方式。
感觉上述三种只是一定的判断依据,但是有些题目特点很模糊,不能明确的归为三类中的一类,需要注意。
说了那么多递归,那为什么一定要放在栈这一节来讲呢?
在函数的递归调用的过程中,系统需要先完成三件事:
1、将所有实参、返回地址等信息传递给被调用函数保存;
2、为被调用函数的局部变量分配存储区;
3、将控制转移到被调用函数入口。
而执行完被调用函数也需要完成三件事:
1、保存被调用函数计算结果;
2、释放被调用函数的数据区;
3、依照被调用函数保存的返回地址将控制转移到调用函数。
当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,这一系列的操作就和栈的操作方式类似,因此,系统需要靠栈来实现调用。
3.3.2 递归算法的效率分析
1)时间复杂度
使用递归后,只能说梦回高中,就是求一个递归数列的和,貌似没有啥好说的。。。
2)空间复杂度
由于每一层递归系统都需要设立一个“递归工作栈”存储每一层递归信息,因此,分析空间复杂度即是分析“递归工作栈”的大小。
二、队列
队列的定义就是和现实中的“队列”类似,遵循“先进先出,后进后出”的原则。
3.1 队列的应用
相较于栈,队列大多时候是以辅助工具的形式存在,如在用广度优先搜索(BFS)查找时,往往使用队列来实现循环。
3.2 队列的操作
与顺序表相似,不同点在于队列通常操作只能在两端实现:队头出队和队尾入队。
由于在使用顺序表模拟队列的过程中,有可能出现不断入队和出队,使得当队尾到达顺序表最大容量时队头到顺序表的起始点还有很长一段空间未被利用而造成假溢出,而浪费存储空间,于是便出现了循环队列
和循环链表类似,当队尾到了表的尾部时,我们可以通过(Q.rear+1)%n的方式,使得队尾重新利用上队头到顺序表的起始点的未利用空间,此时判断队空的条件为:Q.front==Q.rear,队满的条件为(Q.rear+1)%MAXQSIZE==Q.front
三、总结
栈 | 队列 | |
逻辑结构 | 和线性表一样,数据元素存在一对一关系 | |
存储结构 | 顺序存储: 存储空间预先分配,可能导致空间闲置或栈满溢出现象;数据元素个数不能自由扩充 | 顺序存储:(常设计成循环队列形式) 存储空间预先分配,可能导致空间闲置或栈满溢出现象;数据元素个数不能自由扩充 |
链式存储: 动态分配,不会出现闲置或栈满溢出现象;数据元素个数可自由扩充 | 链式存储: 动态分配,不会出现闲置或栈满溢出现象;数据元素个数可自由扩充 | |
运算规则 | 插入和删除在表(栈顶)完成,后进先出 | 插入运算在表的一端(队尾)进行,删除运算在表的另一端(队头),先进先出 |