数据结构是组织数据的结构,用来方便我们的操作。
数组是一种简单的数据结构(顺序存储结构的线性表)。
大清洗期间,小明每天都要洗盘子,盘子总是堆成一堆,已知:
小明总是取出最顶端的盘子,拿去洗。
其他人总是把需要清洗的盘子放在最顶端。
问:“放进盘子$1$,放进盘子$2$,清洗,放进盘子$3$,放进盘子$4$,清洗,清洗,放进盘子$5$,清洗,清洗”这一连串动作中,盘子清洗的顺序是什么?
答案是:$2->4->3->5->1$
可以注意到,我们永远是在顶端操作。在顶端加入,在顶端拿出。他带来了一个特性:后进先出。对于所有的$a,b$都有:若$a$比$b$晚进入,那么$a$比$b$先出去。
生活中有大量遵循“后进先出”规则的结构,我们将之抽象为栈。
栈$(stack)$支持如下操作:
查询栈顶
弹出栈顶(去掉)
向顶部压入元素(添加)
我们可以用一个图像(代码)来表示栈内的变化:
#include<bits/stdc++.h>
using namespace std;
int s[100005],tot;
int top(){//查询栈顶
return s[tot];
}
void push(int x){//向顶部压入元素(添加)
tot++;
s[tot]=x;
}
void pop(){//弹出栈顶(去掉)
tot--;
}
void debug(){//输出每一步栈内的值,便于检查
cout<<"["<<tot<<"] ";
for(int i=0;i<tot;i++){
cout<<s[i]<<" ";
}cout<<endl;
}
int main(){
debug();
push(1);//放进盘子1
debug();
push(2);//放进盘子2
debug();
pop();//清洗
debug();
push(3);//放进盘子3
debug();
push(4);//放进盘子4
debug();
pop();//清洗
debug();
pop();//清洗
debug();
push(5);//放进盘子5
debug();
pop();//清洗
debug();
pop();//清洗
debug();
return 0;
}
输出为:
[0]
[1] 0
[2] 0 1
[1] 0
[2] 0 1
[3] 0 1 3
[2] 0 1
[1] 0
[2] 0 1
[1] 0
[0]
如果不理解的话,也可以观察一下代码,找找规律。
当然,我们用$STL$也能做,但是$STL$相当于一个有进无出的黑盒子,它只能输出$top$(栈顶)的值。
代码:
#include<bits/stdc++.h>
using namespace std;
int tot;
stack<int>s;//STL的神奇做法,stack
int main(){
s.push(1);cout<<"top="<<s.top()<<endl;
s.push(2);cout<<"top="<<s.top()<<endl;
s.pop();cout<<"top="<<s.top()<<endl;
s.push(3);cout<<"top="<<s.top()<<endl;
s.push(4);cout<<"top="<<s.top()<<endl;
s.pop();cout<<"top="<<s.top()<<endl;
s.pop();cout<<"top="<<s.top()<<endl;
s.push(5);cout<<"top="<<s.top()<<endl;
s.pop();cout<<"top="<<s.top()<<endl;
s.pop();cout<<"top="<<s.top()<<endl;//每一次输出它的栈顶值
return 0;
}
输出为:
top=1
tpo=2
top=1
top=3
top=4
top=3
top=1
top=5
top=1
但是发现,在程序输出后,会出现问题,例如:
Dev-C++:xxx.exe已停止工作
VS:Segmentation fault
这是因为当我们执行了九次之后,第十次时,$s.top$是空的,系统因为这个而报错,所以,可以把最后一次输出去掉。
栈里面的元素越多,栈的空间就越大。
#include<bits/stdc++.h>
using namespace std;
int a[3000005],ans[3000005],n;//a是需要判断的数组(即输入的数组),ans是存储答案的数组
inline int read(){//快读
int ret=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
void inp(){//输入
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
}
}
stack<int>s;
void work(){
for(int i=n;i>=1;i--){
while(!s.empty()&&a[s.top()]<=a[i]){
s.pop();//弹出栈顶比当前数小的
}
if(s.empty()){
ans[i]=0;
}else{
ans[i]=s.top();
}
s.push(i);//压入当前元素
}
for(int i=1;i<=n;i++){
printf("%d ",ans[i]);
}
}
int main(){
inp();
work();
return 0;
}
这道题如果用$STL$写的话,运气好的可以得$90$分,运气差的只能得$60$分,但如果加上了快读,就没有什么问题啦。当然,还可以手写单调栈。
#include<bits/stdc++.h>
using namespace std;
int n,a[3000005],q[3000005],r,f[3000005];
//定义变量,其中a数组表示输入的数,q数组表示存下标的单调栈,f数组是存结果。
inline int read(){
int ret=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
int main() {
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
}
//上面是读入。
for (int i=n;i>=1;i--){
while (a[i]>=a[q[r]]&&r>0){
r--;
}
f[i]=q[r];
q[++r]=i;//入栈。
}
for(int i=1;i<=n;i++){
printf("%d ",f[i]);//最后正向输出
}
return 0;
}
经测试得出,好像不论$STL$还是手打, 都会$TLE$。所以,还是加快读保险一点。
现在有一群人排队打饭,每次加入的人都是在队尾,每次取出的人都是在队首。我们把这种队伍抽象为数据结构的队列。
先入先出:队列符合先入先出的结构,越早进入队列的,越早排完队出去。对于所有的$a,b$均有:若$a$比$b$早进入队列,那么$a$比$b$早出去。
队列提供以下操作:
可以定义一个数组来保存元素,用$fnt,end$分别记录头指针,尾指针。
int q[10005];
int fnt=1,end;
#define front (q[fnt])
#define pop (fnt++)
#define push(x) (q[++end]=(x))
但是,这样的话,q[fnt]之前所有的空间都被浪费了。
循环队列:在$end$超出数组大小之后,把数据溢出到数组的头部。
#define SIZE 100000//SIZE大小要估准!
int q[SIZE+5];
int fnt=1,end;
#define front (q[fnt%SIZE])
#define pop (fnt++)//fnt不取模
#define push(x) (q[(++end)%SIZE]=(x))
//队列大小:end-fnt+1,或int fnt=0;......++fnt
$STL$提供了队列,需要包含$queue$库。用法与$stack$相似,但队首用$front()$访问,$queue$也是动态空间,不会有空间被浪费掉。
#include<queue>
#include<iostream>
using namespace std;
void calc(){
queue<int>s;//发现一个队列,存储int类型
for(int i=1;i<=5;i++){
s.push(i);//压入队列
}
while(!s.empty()){//当队列非空时
cout<<s.front()<<endl;//队首
s.pop();//弹出
}
}
int main(){
calc();
}
$front,push,pop$时间复杂度为$O(1)$,所以建议使用$STL$ $queue$。
代码:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
queue<int>q;
cin>>n>>m;
for(int i=1;i<=n;i++){
q.push(i);
}
for(int i=1;i<=n;i++){
for(int j=1;j<m;j++){
q.push(q.front()),q.pop();
}cout<<q.front()<<" ";q.pop();
}
}
数组是一个顺序表。相邻元素在内存上的地址是连续的,这提供了:
虽然有上述好处,但也有明显不足:
为了解决数组的这两个缺陷,我们发明了链表。
链表是一种崭新的数据结构。它不以下标来寻址,而是通过相邻元素之间的联系。
通俗的讲,数组是一群人在内存上排了个队;链表不需要每个人都相邻,但是需要每个人记住自己旁边的人。
$3->1->2->4->5$
可见,每个元素都有一个指针,指向右端元素。
链表的存储
//指针
struct node{
int key;
node* rt;
};
//数组
struct node{
int key;//关键字
int rt;//右边元素的下标
}s[100005];
以上两种方法都可以存储链表,无优劣之分。
链表(以及很多其他的数据结构)的每个元素称为“结点”。链表分为两种:
单向链表只能单向查找,双向链表支持双向查找。
简单的链表只需要支持以下几个任务:
一般的,每一个链表都有一个伪结点。
还有一种叫循环链表,可以“走回头路”。
这一次,我们用链表来解决这道题。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m,rt[105],p=0;
cin>>n>>m;
for(int i=0;i<n;i++){
rt[i]=i+1;
}rt[n]=1;//循环单链表
for(int i=1;i<=n;i++) {
for(int j=1;j<m;j++){//走m-1个
p=rt[p];
}cout<<rt[p]<<" ";rt[p]=rt[rt[p]];
}cout<<endl;
return 0;
}
最基本的一个问题:家族问题,给你两个人,判断他们是不是同一个家族的人。
只需要判断他们的祖先是否是同一个人就可以了。
#include<bits/stdc++.h>
using namespace std;
int dad[100005];
int anc(int x){
if(dad[x])return anc(dad[x]);
return 0;
}
void uni(int x,int y){
x=anc(x);
y=anc(y);
if(x!=y){
dad[x]=y;
}
}
bool ask(int x,int y){
return anc(x)==anc(y);
}
int main(){
//......
return 0;
}
刚刚实现的数据结构叫做并查集,并查集是用来维护不相交集合的数据结构,支持两个操作:
带路径压缩的并查集,单次操作平均复杂度接近$O(1)$。
#include<bits/stdc++.h>
using namespace std;
int dad[100005];
int anc(int x){
if(dad[x]) return dad[x]=anc(dad[x]);
return x;
}
void uni(int x,int y){
x=anc(x);
y=anc(y);
if(x!=y){
dad[x]=y;
}
}
bool ask(int x,int y){
return anc(x)==anc(y);
}
int n,m,p;
void work(){
int x,y;
cin>>n>>m>>p;
for(int i=1;i<=m;i++){
cin>>x>>y;
uni(x,y);
}
for(int i=1;i<=p;i++){
cin>>x>>y;
puts(ask(x,y)?"Yes":"No");
}
}
int main(){
work();
return 0;
}
#include<bits/stdc++.h>
using namespace std;
int dad[100005];
int anc(int x){
if(dad[x]) return dad[x]=anc(dad[x]);
return x;
}
void uni(int x,int y){
x=anc(x);
y=anc(y);
if(x!=y){
dad[x]=y;
}
}
bool ask(int x,int y){
return anc(x)==anc(y);
}
int n,m;
void work(){
int cmd,x,y;
cin>>n>>m;
while(m--){
cin>>cmd>>x>>y;
if(cmd==1){
uni(x,y);
}else{
printf("%c\n","NY"[ask(x,y)]);
}
}
}
int main(){
work();
return 0;
}