5 线性数据结构
包括向量,队列和栈
1 向量
普通数组的限制:创建时需要确定大小,不适用于数据量不确定的问题,申请太大会浪费内存,而申请太小可能会访问越界。
向量vector,是可以改变大小的线性序列容器,可以理解为“变长数组”,适用于数据量未知的情况;其内部实现方法为动态分配数组,每次重新分配时会多分配额外的空间,以便适应未来可能的数据增长。
例题5.1 完数和盈数(清华大学复试上机)
题目
描述
一个数如果恰好等于它的各因子(该数本身除外)子和,如:6=3+2+1。则称其为“完数”;若因子之和大于该数,则称其为“盈数”。 求出2到60之间所有“完数”和“盈数”。
输入描述:
题目没有任何输入。
输出描述:
输出2到60之间所有“完数”和“盈数”,并以如下形式输出: E: e1 e2 e3 …(ei为完数) G: g1 g2 g3 …(gi为盈数) 其中两个数之间要有空格,行尾不加空格。
示例1
输入:
无
输出:
按题目要求进行输出即可。
题解
#include <iostream>
#include <vector>
#include <cstdio>
#include <string>
using namespace std;
int main() {
//由于要存储到最后用来输出的“数组”是动态增长的,所以用vector
vector<int> Evec; //完数
vector<int> Gvec; //盈数
for(int num = 2;num<=60;num++){
int sum = 0;
for(int i = 1;i<num;i++){ //注意这里因子1仍要算进去,题目只排除了该数本身
if(num%i == 0){
sum += i;
}
}
if(sum == num){
Evec.push_back(num); //push_back()的使用
}
else if(sum > num){
Gvec.push_back(num);
}
}
//注意输出格式
printf("E:");
//使用迭代器(可以理解为指针)访问,输出时要加*
//访问vector元素也可以使用下标vector[i]和vector.size()
for(vector<int>::iterator it1 = Evec.begin();it1 != Evec.end();it1++){
printf(" %d",*it1);
}
printf("\n");
printf("G:");
for(vector<int>::iterator it2 = Gvec.begin();it2 != Gvec.end();it2++){
printf(" %d",*it2);
}
printf("\n");
}
笔记
- 题目数据量未知,因此使用向量vector
- vector的初始化、插入删除、下标访问、迭代器访问
- 解题细节:1这个因子也算进去;输出细节:注意在哪空格、换行
2 队列
队列的特点是先进先出,适用场景为公平的等待,比如广度优先遍历
例题5.1 约瑟夫问题No.2
题目
n个小孩围一圈,按顺时针编号1,2,…,n,从编号为p开始顺时针报数,从1报到m时,报m的小孩出去,然后下一个小孩再从1开始报数,报到m时再出去,直到所有小孩都从圈中出去,请按出去的先后顺序输出小孩的编号
输入:
每一行:第1个是n,第二个是p,第三个是m。
最后1行:0 0 0。
输出:
按出圈的顺序输出编号,之间以逗号间隔
题解
#include <cstdio>
#include <queue>
using namespace std;
int main(){
int n,p,m; //n是小孩数量,p是报数起始位置,m是总共报多少个数截止
while(scanf("%d%d%d",&n,&p,&m)!=EOF){
if(n == 0 && p == 0 && m == 0){
break;
}
//队列记录的是报数顺序
queue<int> childBaoshu;
//把第一轮的报数顺序排好队
for(int i = 0,j = 0;j<n;j++){
//i用来遍历孩子的编号,j用来记录已经遍历的孩子的数量
childBaoshu.push(i);
++i; //p->p+1->p+2->...->n->1->...->(共n次后为止)
if(i>n){
i = 1; //用重置i=1的方法,实现循环遍历
}
}
//接下来开始报数的过程
int baoshu = 1;//将要报的数,区分学生编号
while(true){
int cur = childBaoshu.front();//cur是队首孩子的编号,区分报的数
childBaoshu.pop();//报完数,队首就出队
if(baoshu = m){//检查一下刚刚报的是不是m
baoshu = 1;//如果是,那么下一个孩子报的数重置为1,并且报数的孩子也不需要归队了
if(childBaoshu.empty()){//只有报数为m时才会减少一个孩子,才需要检查检查是不是最后一个孩子出队
printf("%d\n",cur);
break;
}
else{
//接下来还会有孩子继续报数
printf("%d ",cur);
}
}
//如果报的数不是m,则报数出队后还要归队
else{
baoshu += 1;
childBaoshu.push(cur);
}
}
}
}
笔记
- “公平”的等待,用队列queue
- 队列的初始化、增删、访问
- 本题用队列记录“报数情况”,而不是简单地按顺序记录小孩编号,并用先出队再入队实现了循环队列
例题5.2 猫狗收容所
题目
只收留猫和狗,收养方式有两种:
第一种是直接收养所有动物中最早进入收容所的。
第二种是选择收养类型,并收养该种动物中最早进入收容所的。
#include <cstdio>
#include <queue>
using namespace std;
struct Animal{
int num;
int seq; //记录动物次序
};
int main(){
queue<Animal> catQue;
queue<Animal> dogQue;
int seq = 0;
int n;
scanf("%d",&n);
for(int i = 0; i<n;i++){
int method,para; //para是动物编号
scanf("%d%d",&method,¶);
if(method == 1){
//入队
if(para > 0){
//处理狗队列
Animal dog; //新声明一个dog类,记录参数和序列后push到队列中保存
dog.num = para;
dog.seq = seq;
++seq;
dogQue.push(dog);
}
else{
//处理猫队列
Animal cat;
cat.num = para;
cat.seq = seq;
++seq;
catQue.push(cat);
}
}
else{
//method为2时收养,出队
if(para==0){
//采用第一种收养方式,不区分猫狗
//情况1,无猫无狗
if(dogQue.empty() && catQue.empty()){
continue;//跳过本次循环剩余代码
}
//情况2,收养狗
//2.a 狗非空 猫为空
//2.b 狗非空 猫非空 狗队首的序列比猫队首的序列小
else if(catQue.empty()||
!dogQue.empty() & !catQue.empty() && dogQue.front().seq < catQue.front().seq){
printf("%d ",dogQue.front().num);
dogQue.pop();
}
else{
//剩下的情况即为,收养猫
printf("%d ",catQue.front().num);
catQue.pop();
}
}
else if(para == 1){
//当para为1时,明确指定领养狗
if(dogQue.empty()){
continue;
}
printf("%d ",dogQue.front().num);
dogQue.pop();
}
else{
//最后剩的情况为:para==-1,明确指定领养猫
if(cat.empty()){
continue;
}
printf("%d ",catQue.front().num);
catQue.pop();
}
}
}
printf("\n");
}
笔记
- 根据题目描述的先进先出,想到队列,且只维护1个队列不易实现所有情况,所以维护两个队列
- 队列元素可以为struct,方便加一些需要的标志
- 多种情况分支,可以先写出选择分支的大框架,并用注释标上每一种情况,再分别实现
栈
队列适用公平的等待,而栈适用于优先级差别的等待
例题5.4 zero-complexity transposition(上海交大复试上机)
题目
简而言之就是用栈实现逆序输出
题解
#include <cstdio>
#include <stack>
using namespace std;
int main(){
int n;
while(scanf("%d",&n)!=EOF){
stack<long long> myStack;
long long num; //根据题中数的范围,读取longlong类型的十进制数
for(int i = 0;i<n;i++){
scanf("%lld",&num);
myStack.push(num);
}
while(!myStack.empty()){
printf("%lld ",myStack.top());
myStack.pop();
}
printf("\n");
}
}
笔记
- 栈的应用:逆序输出;栈的使用:初始化、增删、访问,其中只用访问top()与队列不同
- 不过本题还要注意,数的范围超过了int,要用64位整型longlong来表示,%lld或%I64d,或用c++的cin进行输入可以无视系统的差异
例题5.5 括号匹配问题
题目
基本思想:左括号压栈,右括号弹栈;
步骤在数据结构里学过,读代码熟悉思路和细节
题解
#include <cstdio>
#include <stack>
#include <string>
#include <cstring>
using namespace std;
int main(){
char buf[200];
while(fgets(buf,200,stdin)!=NULL){
//fgets配合while实现不确定数量的多行读取,cin可能超时
//用string将输入的一行中最后的换行pop掉
string str = buf;
str.pop_back();
stack<int> indexStack;//记录了左括号的下标
string result;//保存输出的结果
for(int i = 0;i<str.size();i++){
if(str[i] == '('){
indexStack.push(i);//如果是左括号,则下标入栈
result.push_back('$');//暂且认为这个左括号是非法的,后面再改
}
else if(str[i] == ')'){
//如果是右括号,则检查左括号下标栈是否为空,若空则该右括号非法
if(indexStack.empty()){
result.push_back('?');
}
else{
//不空则匹配,将该右括号的判断位置赋为空格,并挽救一个左括号的清白
result.push_back(' ');
result[indexStack.top()] = ' ';
indexStack.pop();//匹配并挽救完1个左括号,将其弹出
}
}
//如果不是括号,则正常赋值空格
result.push_back(' ');
}
}
printf("%s\n%s\n",str.c_str(),result.c_str());
}
笔记
- fgets()的使用,配合while实现不确定数量的多行读取,用getline/ cin可能超时
- 左括号压栈,指的是左括号在原字符串中的下标压栈
- “暂且认为左括号非法”之后再挽救的思想,先赋值$之后再赋回空格
- 字符串string也可以用向量vector的函数push_back()来添加字符
习题5.1 堆栈的使用
题目
题解
#include <stack>
#include <cstdio>
#include <iostream>
using namespace std;
int main(){
int n;
while(cin>>n){
stack<int> myStack;
for(int i = 0;i<n;i++){
char input;
int inputnum;
cin>>input; //消停用cin吧,
if(input == 'P'){
cin>>inputnum; //cin会自动略过第一个分隔符
myStack.push(inputnum);
}
else if(input == 'O'){
if(!myStack.empty()){
myStack.pop();
}
else{
continue;
}
}
else if(input == 'A'){
if(myStack.empty()){
printf("E\n");
}
else{
printf("%d\n",myStack.top());
}
}
}
}
return 0;
}
笔记
- cin的使用,忽略开头的分隔符,并且不对结尾的换行做处理,因此在本题中能保证当一行输入为“P 3”时,能略过3前面的空格和3后面的换行,也就避免了scanf向字符input输入时要同时处理前后的分隔符的麻烦
- 其他,栈的使用、分支判断倒是不难