线性表
数组
STL中的可变数组vector
vector是一个不定长的数组,它还在内部封装了一些常用操作,例如:
1》 vector< int>a :声明一个vector,实际上它完整的写法是vector< int>a(n,i),表示该可变数组最开始有n个元素,全部初始化为 i。如果将其省略,则会默认为 0。尖括号内表示数据类型,也可换成其他类型。
2》 a.push_back(x) : 将元素x插入到数组尾部。
3》 a.pop_back() : 删除尾部最后一个元素。
4》 a.size() : 返回数组的长度。
5》 a.resize(n,m) : 将数组调整为 n ,多出的部分删除,新增的部分初始化为 m,m省略时默认为 0。
访问该数组时像访问普通数组一样用方括号,使用时需要头文件#include< vector>,此处不再赘述。
二维可变数组
与普通数组类似,可变数组也可以进行嵌套,例如vector< int>a[N]就是由N个可变数组组成的二维数组,只不过第一维大小固定,第二维不固定,当然也会有vector<vector< int> >这样的二维都不定长的数组,道理我们都懂,那它们到底适用于那些题目呢?下面我们通过一道题来看看它的作用:
例题:寄包柜
题目描述
超市里有 n(n≤10^5 ) 个寄包柜。每个寄包柜格子数量不一,第 i个寄包柜有ai(ai≤10^5) 个格子,不过我们并不知道各个ai的值。对于每个寄包柜,格子编号从 1 开始,一直到 ai 。现在有 q(q≤10^5 ) 次操作: 1.若输入 1 i j k:在第 i 个柜子的第 j 个格子存入物品 k(0≤k≤10^9)。当 k=0 时说明清空该格子。
2.若输入 2 i j:查询第 i个柜子的第 j 个格子中的物品是什么,保证查询的柜子有存过东西。
已知超市里共计不会超过10^7个寄包格子,ai是确定然而未知的,但是保证一定不小于该柜子存物品请求的格子编号的最大值。当然也有可能某些寄包柜中一个格子都没有。
输入格式:
第一行 2 个整数 n 和 q,寄包柜个数和询问次数。 接下来 q 个整数,表示一次操作。
输出格式:
对于查询操作时,输出答案。
分析:显然,需要建立一个二维数组分别记录柜子号和格子号,但根据本题的数据,建立10^5 ✖10^5 的int 数组肯定会超出内存限制,这时候就要用上vector了,我们先定义一个二维的vector<vector< int> >a(n+1),此时这个数组第二维有n+1个元素,代表1到n个柜子,而柜子里面的格子数,我们可以视情况而定,当数量不够时用resize调整,这样就避免了内存超出限制。参考代码:
#include<iostream>
#include<vector>
using namespace std;
int n,q;
int main()
{
cin>>n>>q;
vector<vector<int> >a(n+1);
while(q--)
{
int x,i,j,k;
cin>>x;
if(x==1)
{
cin>>i>>j>>k;
if(a[i].size()<j+1)
{
a[i].resize(j+1);
}
a[i][j]=k;
}
else
{
cin>>i>>j;
cout<<a[i][j]<<endl;
}
}
return 0;
}
栈
STL中的栈stack
所谓栈,就是符合“后进先出”规则的数据结构,头文件< stack>,主要有以下操作:
1》 stack< int>a : 建立一个栈a,其内部元素是int。
2》 a.push(x) : 将元素x压进栈。
3》 a.pop() : 将栈顶元素弹出。
4》 a.top() : 查询栈顶元素。
5》 a.size() : 查询元素个数。
6》 a.empty() : 查询栈是否为空。
例题:括号匹配
题目描述
给定若干字符串,每个字符串由(、)、[、 ] 、{ 、}组成,如果所有的括号都能匹配得上,说明这个字符串合法,否则为非法。
例如:字符串“ [()] ” 合法 ,字符串“[ { ]”非法。
若合法输出Yes 非法输出No。
分析:
将字符串遍历,遇到匹配的就消去,我们用栈来实现这个操作,从左向右遍历,若匹配不但不压入还要弹出栈顶,若不匹配就压入,最后判断栈是否为空即可。
#include<iostream>
#include<string>
#include<stack>
using namespace std;
string str;
stack<char>a;
int n;
char find(char x)
{
if(x==')') return '(';
if(x==']') return '[';
if(x=='}') return '{';
return '\0';
}
int main()
{
cin>>n;
getline(cin,str);
while(n--)
{
while(!a.empty()) a.pop();
getline(cin,str);
for(int i=0;i<str.size();i++)
{
if(a.empty())
{
a.push(str[i]);
continue;
}
if(a.top()==find(str[i]) )
{
a.pop();
}
else a.push(str[i]);
}
if(a.empty()) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}
例题:后缀表达式
题目描述
所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行(不用考虑运算符的优先级)。如:3*(5–2)+7对应的后缀表达式为:3.5.2.-*7.+@。’@’为表达式的结束符号。‘.’为操作数的结束符号。
分析:
首先我们读入字符串然后遍历,对于数字我们需要将它存起来,对于运算符,我们需要取出刚放入的两个数字进行相应运算,显而易见,这里我们用栈会更好,本题的坑点在于:题目中只给出了个位数的运算,实际上数据中存在多位数。如果能意识到这点的话应该能顺利做出来。代码如下:
#include<iostream>
#include<stack>
#include<string>
using namespace std;
stack<int>a;
int res;
int check(int x,int y,char z)
{
if(z=='*') return x*y;
if(z=='+') return x+y;
if(z=='-') return x-y;
if(z=='/') return x/y;
}
int main()
{
string str;
cin>>str;
for(int i=0;i<str.size();i++)
{
if(str[i]>='0'&&str[i]<='9')
{
res=res*10+str[i]-'0';
}
if(str[i]=='.')
{
a.push(res);
res=0;
}
if(str[i]=='@') break;
if(str[i]=='+'||str[i]=='*'||str[i]=='/'||str[i]=='-')
{
int num1,num2;
num1=a.top();a.pop();
num2=a.top();a.pop();
a.push(check(num2,num1,str[i]));
}
}
cout<<a.top();
return 0;
}
用数组实现栈
用 tt 表示栈顶元素。将tt 初始化为 0,表示没有元素。
1》 push操作 :将栈顶所在位置往后移动一格,放入x。即stk[++tt] = x
。
2》 pop 操作: 将tt 往前移动一格。即 tt--
。
3》empty 操作:如果tt 大于 0 则栈非空,若等于 0 则栈空。
4》query : 返回栈顶元素。stk[tt]
#include<iostream>
using namespace std;
const int N=10000;
int stk[N];
int tt,res,x;
void push(int x)
{
cin>>x;
stk[++tt]=x;
}
void pop(int x)
{
tt--;
}
void empty()
{
cout<<(tt==0?"YES":"NO")<<endl;
}
void query()
{
res=stk[tt];
cout<<res<<endl;
}
int main()
{
string t;
while(cin>>t)
{
//push x,pop,empty,query
if(t=="push") push(x);
else if(t=="pop") pop(x);
else if(t=="empty") empty();
else if(t=="query") query();
}
return 0;
}
队列
STL中的队列queue
与栈不同,队列是一种“先进先出”的线性表,限制在一端删除,另一端插入,头文件为< queue>,主要操作有:
1. queue< int>a : 建立一个队列。
2. a.push(x) : 将x插入队尾。
3. a.pop() : 删除队首。
4. a.front() : 查询队首。
5. a.back() : 查询队尾。
6. a.size() : 查询元素个数。
7. a.empty() : 查询队列是否为空。
例题:约瑟夫问题
题目描述
n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新从 1 开始报数,数到 m的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。输入格式 输入两个整数 n,m。
输出格式 输出一行 n 个整数,按顺序输出每个出圈人的编号。
当数据有先进先出的性质时,就可以考虑使用队列,仔细考虑本题,我们会发现出列的人都站到了队尾,这样一直循环,直到数到m的人出圈,又重新开始操作,完全符合队列的性质,利用队列模拟题意即可:
#include<iostream>
#include<queue>
using namespace std;
queue<int>a;
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
a.push(i);
}
while(!a.empty())
{
for(int i=1;i<=m;i++)
{
if(i==m)
{
cout<<a.front()<<' ';
a.pop();
continue;
}
a.push(a.front());
a.pop();
}
}
return 0;
}
例题:机器翻译
题目描述:
小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章。
这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换。对于每个英文单词,软件会先在内存中查找这个单词的中文含义,如果内存中有,软件就会用它进行翻译;如果内存中没有,软件就会在外存中的词典内查找,查出单词的中文含义然后翻译,并将这个单词和译义放入内存,以备后续的查找和翻译。假设内存中有 M 个单元,每单元能存放一个单词和译义。每当软件将一个新单词存入内存前,如果当前内存中已存入的单词数不超过
M−1,软件会将新单词存入一个未使用的内存单元;若内存中已存入 M 个单词,软件会清空最早进入内存的那个单词,腾出单元来,存放新单词。假设一篇英语文章的长度为 N 个单词。给定这篇待译文章,翻译软件需要去外存查找多少次词典?假设在翻译开始前,内存中没有任何单词。
分析:
分析题意,当内存占满时,存入新单词、清空旧单词,可不就是先进先出的队列嘛,只需要额外设置一个标记数组记录队列中的单词(本题中单词用数字代替),队内的设置为1,队外的时设置为0即可。代码如下:
#include<iostream>
#include<queue>
using namespace std;
const int N=1e3+10;
int check[N],a[N];
int n,m,res,x;
queue<int>q;
int main()
{
cin>>m>>n;
for(int i=0;i<n;i++)
{
cin>>x;
if(!check[x])
{
if(q.size()>=m)
{
check[q.front()]=0;
q.pop();
}
check[x]=1;
q.push(x);
res++;
}
}
cout<<res;
return 0;
}
数组模拟队列
用 head 表示队首元素,tail 表示队尾元素,将 tail 初始化为 -1,表示没有元素。
1》 push操作 :将队尾所在位置往后移动一格,放入x。即que[++tail] = x
。
2》 pop 操作: 将 head 往后移动一格。即head++
。
3》empty 操作:如果 head大于 tail 则队列非空,否则队列为空。
4》query :返回队首元素。que[head]
#include<iostream>
using namespace std;
const int N=100001;
int que[N];
int head,x,res;
int tail=-1;
void push(int x)
{
cin>>x;
que[++tail]=x;
}
void pop()
{
head++;
}
void empty()
{
cout<<(head>tail?"YES":"NO")<<endl;
}
void query()
{
res=que[head];
cout<<res<<endl;
}
int main()
{
string t;
while(cin>>t)
{
//push x,pop,empty,query
if(t=="push") push(x);
else if(t=="pop") pop();
else if(t=="empty") empty();
else if(t=="query") query();
}
return 0;
}
链表
在存储一大波数据时,我们常常会用到数组,但是数组在某些情况下往往不够灵活,例如在一串排好顺序的数中再插入一个数,那么我们就需要把这个数之后的所有数依次往后移动一位,这样的操作是很耗费时间的,这个时候就体现出了链表的重要性。
STL中的list链表
链表同样可以用 list 简化操作,需要使用< list>头文件,支持以下常用方法:
1. list< int>a : 定义 int 类型的链表 a
2. list< int>a(arr,arr+3) : 从数组 arr 中的前 3个元素作为链表 a 的初始值。
3. a.size() : 返回链表节点数量。
4. list< int>::iterator it : 定义名为 it 的迭代器。
5. a.begin() ,a.end() : 链表开始和末尾的迭代器。
6. it++ , it-- : 迭代器指向前一个和后一个元素 。
7. a.push_front( x ) , a,push_back( x ) : 在链表开始和末尾的迭代器指针 。
8. a.insert( it, x ) : 在迭代器 it 的前面插入元素 x 。
9. a.pop_front( ) , a.pop_back( ) : 删除链表开头或者末尾 。
10. a.erase( it ) : 删除迭代器所在元素。
11. for( it=a.begin(); it!=a.end() ; it++ ) : 遍历链表。
PS:end()
返回的不是指向最后一个元素的迭代器,而是指向最后一个元素后面的位置的迭代器,所以在遍历迭代器的过程中也不会遍历到end()
。
数组模拟链表
初始化操作:
const int N=1e5+10;
int n[N],ne[N];
int idx,k,x; // idx 为当前用到的点
void init()
{
ne[0]=N-1; // head为 0,tail为 N-1
idx=1;
}
删除操作:
void remove(int k)
{
ne[k]=ne[ne[k]]; // ne[k] 指向下一个 ne[] 指向的数
}
在第k个插入的数后插入一个数x:
void add(int k,int x)
{
n[idx]=x;
ne[idx]=ne[k]; // ne[idx] 指向原来 ne[k] 指向的数
ne[k]=idx;
idx++;
}
向头结点添加为:add(0,x)
输出整条链表:
for(int i=ne[0];i!=N-1;i=ne[i]) cout<<n[i]<<' ';
数组模拟双链表
初始化操作:
const int N = 1e5+10;
int e[N], l[N], r[N], idx;
void init()
{
l[N-1]=0;
r[0]=N-1;
idx=1;
}
其中数组 e 存放的是该结点的值,l 和 r 分别表示该节点左右相邻的节点
idx 为现在用到的节点
向第k个插入的数右边插入一个数x:
void add_r(int k,int x)
{
e[idx]=x;
l[idx]=k;
r[idx]=r[k];
l[r[k]]=idx;
r[k]=idx;
idx++;
}
那么如何向左边插入一个数呢?
向第k个数的左边插入一个数等价于向第k个数左边的数的右边插入一个数
只需将 k 改为 l[k] 即可
同理向头结点和尾结点添加分别为:add_r(0,x) 和 add_r(l[N-1],x)
删除第k个插入的数:
void remove(int k)
{
l[r[k]]=l[k];
r[l[k]]=r[k];
}
输出整条链表:
for(int i=r[0];i!=N-1;i=r[i]) cout<<e[i]<<" ";