一、数组
在 C++ 的 STL 中,给我们提供了一个可变长度数组,可变长度数组的头文件是 < vector >,其中vector的常用功能如下:
说明 | 功能 |
---|---|
vector v(N,i) | 建立一个可变长度的 int 数组 v,且初始有 N 个为 i 的元素。N,i 可以省略。 |
v.push_back(a) | 将元素 a 插入 v 的末尾。 |
v.size() | 返回元素个数。 |
v.resize(n,m) | 重新调整数组大小为 n。如果 n 比原来大,则新增的部分都初始化为 m。 |
v[a] | 访问下标为 a 的元素。 |
注:vector 的下标也是从 0 开始的,且使用方括号索引来访问数组元素时,数组的大小必须不小于索引,否则就和访问普通数组越界一样而访问无效内存。可通过数组初始化、push_back 或 resize 成员函数来增加数组长度。
数组的特点和局限性:
- 存储查询给定索引(下标)的数据:效率很高,复杂度 O(1)
- 将整个数组的一段数据进行插入或删除操作,或者搜索指定元素(如果没有排序):效率很低,时间复杂度 O(n)。
1、题目描述: 洛谷 P3156 【深基15.例1】询问学号
有 n( n ≤ 2×
1
0
6
10^{6}
106 ) 名同学进入教室。每名同学的学号在 1 到
1
0
9
10^{9}
109之间,按进教室的顺序给出。
老师想知道第 i 个进入教室的同学的学号是什么?最先进入教室的同学 i=1,询问次数不超过
1
0
5
10^{5}
105 次。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m,tmp;
vector<int> stu; //定义一个stu数组
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>tmp;
stu.push_back(tmp);
// 将元素 tmp 插入 stu 的末尾
}
for(int i = 0; i < m; i++) {
cin >> tmp;
cout << stu[tmp - 1] << endl; // 访问下标为tmp-1的数组元素 并输出且换行
}
return 0;
}
二、栈
栈是一种“后进先出”的线性表,其限制是仅允许在表的一端进行插入和删除运算,这一端被称为栈顶。
在 C++ 的 STL 中,给我们提供了栈,头文件是< stack >,有以下几种方法:
说明 | 功能 |
---|---|
stack < int > s | 建立一个栈 s,其内部元素类型是 int。 |
s.push(a) | 将元素 a 压进栈 s。 |
s.pop() | 将 s 的栈顶元素弹出。 |
s.top() | 查询 s 的栈顶元素。 |
s.empty() | 查询 s 是否为空。 |
s.size() | 查询 s 的元素个数。 |
在使用栈的时候,需要防止栈因存储内容过多而导致溢出,也需要防止对空栈弹出元素。C++ STL 已经做了一部分处理,但是查询空栈的栈头也是会导致Runtime Error 的。对于手写栈可以在操作之前进行判断。例如弹出操作:
void pop(){
if (p == 0)
printf("Stack is empty");
else
p -= 1;
}
注意 STL 虽然提供了许多方便的功能,但是如果使用 STL 时不打开 -O2 优化开关,就有一点慢(常数大)。在需要追求运行速度的情况下,往往需要自己手写栈。
2、题目描述:洛谷 P1449 后缀表达式
后缀表达式是不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行的表达式,且不必考虑运算符优先级。
例如输入2.4.*1.3.±@,其中.是每个数字的结束标志,@是整个表达式的结束标志。输出4。
/*每次读到一个运算符,就取出在它前面的次近和最近的数字进行
相应运算后再把它放入原来的序列。
也就是每次操作,不是获得并且弹出序列的一端的前两个数字,
就是往这一端放入一个数字,符合栈的功能,用栈完成这个问题
*/
#include<bits/stdc++.h>
using namespace std;
int main(){
string ch,temp;
stack<int> n;
int s=0,x=0,y=0;
cin>>ch;
for(int i=0;i<ch.length();i++){
if(ch[i]>='0' && ch[i]<='9')
temp += ch[i];
if (ch[i] == '.')
s = atoi(temp.c_str()),n.push(s),temp.erase();
/*
atoi():把字符串类型转为整型
erase():初始化/置空
*/
else if (ch[i] == '+' || ch[i] == '-' || ch[i] == '*' || ch[i] == '/' ) {
x = n.top(); // 读取栈顶元素
n.pop(); // 弹出栈顶元素
y = n.top(); // 读取栈顶元素
n.pop(); // 弹出栈顶元素
switch (ch[i]) {
case '+': n.push(x + y); break;
case '-': n.push(y - x); break;
case '*': n.push(x * y); break;
case '/': n.push(y / x); break;
}
}
}
cout<<n.top();
return 0;
}
括号匹配
3、题目描述:
给定若干个字符串,每个字符串由()[]{}这六个字符构成。如果所有的括弧都可以匹配,例如[([]){}],那么这个字符串合法,否则非法。试判断一个字符串是否合法。
举例 | 结果 |
---|---|
([]) | Yes |
(([()]))) | No |
([()])() | Yes |
思路 我们假设有一个字符串,对于每个右括号去找匹配的左括号,匹配则删去,类似消消乐。
如果处理字符串的所有字符,发现还有剩下括号,那么就说明有些括号没有被匹配到,说明是非法的括号序列。比如([)(])。
首先编写一个 trans 函数进行括号匹配。
char trans(char a){
if (a == ')')return '(';
if (a == ']')return '[';
if (a == '}')return '{';
return '\0';
}
当括号不匹配的时候,栈内可能还存有元素,这会对之后判断括号匹配产生干扰。因此在匹配之前需要清空栈。
while(!s.empty()) s.pop();
接下来依次对字符串用栈处理即可完成本题。如果栈为空,直接放入栈中;如果发现读到的字符和栈顶的字符可以匹配,那么就消去。
getline(cin, p); //读入一行
for (int i=0; i < p.size(); ++i) {
if (s.empty()) {
//如果栈为空,直接放入栈中
s.push(p[i]);
continue;
}
if (trans(p[i]) == s.top())
s.pop();
else s.push(p[i]);
}
if (s.empty())printf("Yes\n");
else printf("No\n");
处理字符串问题时:
-
使用 cin 读入一个独占的数字后,其读入指针在这一行的末尾。
-
使用 getline 读入一行字符串时,只会读到空串(第一行)。如果希望读到第二行,则必须要假装读入这一行,可以使用 getline,也可以使用 getchar 等。
三、队列
队列是一种先进先出的线性表,可以在队列的一端插入元素,在另一端删除元素。
在 C++ 的 STL 中,给我们提供了队列,头文件是 < queue >,有以下几种方法:
说明 | 功能 |
---|---|
queue < int > q | 建立一个队列q,其内部元素类型是 int。 |
q.push(a) | 将元素 a 插入队列 q 的末尾。 |
q.pop() | 将 q 的队首元素删除。 |
q.front() | 查询 q 的队首元素。 |
q.end() | 查询 q 的队尾元素。 |
q.size() | 查询 q 的元素个数。 |
q.empty() | 查询 q 是否为空。 |
队列是有头(head)尾(tail)的。需要记录队列的头和尾在哪里。
push 操作:直接把 x 赋给队尾,再让队尾 +1。
void push(int x){
queue[tail] = x; tail += 1;
}
pop 操作:不必让元素全部往前进一位,只需让头指针前进。
void pop(){。
head += 1;
}
front 操作:直接获取 q[head] 即可。
int front(){
return queue[head];
}
4、题目描述:洛谷 P1996 约瑟夫问题
n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新从 1 开始报数,数到 m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
/*
问题可以转化为这 n 个小朋友在队列中,每次操作使在队首的人跑到队尾。每 k 次操作删去队首。
用队列模拟这个过程。
*/
#include<bits/stdc++.h>
using namespace std;
int main(){
queue<int> q; // 建立一个q 队列
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
q.push(i); // 将元素 i 插入队列 q 的末尾
while(q.size()!=0){
for(int i=1;i<k;i++){
q.push(q.front()); // 队首元素插入到队列q的末尾
q.pop(); // 删除队首元素
}
cout<<q.front()<<' '; // 输出队首元素
q.pop(); // 删除队首元素
}
}
/*
可使用循环队列的方式映射指针,增加空间利用率,队内长度不超过 n 即可。
但如果使用 STL 队列,那么就可以不用考虑循环队列了,方便但慢。
*/
四、链表
链表有很多种,其中比较基础的如下:
种类 | 特点 |
---|---|
单向链表 | 只记录每个节点的后继。 |
双向链表 | 记录每个节点的前驱和后继。 |
循环单向链表 | 单链表,最后一个节点后继为第一个节点。 |
循环双向链表 | 双链表,形成环形。 |
- 链表插入和删除的复杂度是 O(1)。
- 链表搜索指定元素位置/定位第k个元素的复杂度是 O(n)。
- 相比于数组,链表插入删除快,但是定位(找到第k个)慢。
五、二叉树
⼆叉树是每个结点最多有两个⼦树的树结构。也就是说⼆叉树不允许存在度⼤于2的树。它有五种最基本的形态:⼆叉树可以是空集。根可以有空的左⼦树或者右⼦树;或者左右⼦树都是空。其中只有左⼦树或者右⼦树的叫做斜树。
- 性质1:二叉树的第i层上至多有2i-1(i≥1)个节点
- 性质2:深度为h的二叉树中至多含有2h-1个节点
- 性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1
- 性质4:具有n个节点的满二叉树深为log2n+1
- 性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点:
当i=1时,该节点为根,它无双亲节点
当i>1时,该节点的双亲节点的编号为i/2
若2i≤n,则有编号为2i的左节点,否则没有左节点
若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点
4、题目描述:洛谷 P1030 [NOIP2001 普及组] 求先序排列
给出一棵二叉树的中序与后序排列。求出它的先序排列。(约定树结点用不同的大写字母表示,且二叉树的节点个数 ≤ 8)
输入格式
共两行,均为大写字母组成的字符串,表示一棵二叉树的中序与后序排列。
输出格式
共一行一个字符串,表示一棵二叉树的先序。
#include<bits/stdc++.h>
using namespace std;
void tree(string z,string h){
if (z.length()>0){
cout << h[h.length()-1];
int root=z.find(h[h.length()-1]);
tree(z.substr(0,root),h.substr(0,root));
tree(z.substr(root+1,h.length()-root),h.substr(root,h.length()-root-1));
}
}
int main(){
string a,b;
cin >> a>>b;
tree(a,b);
return 0;
}
补充:断点调试
为什么需要断点调试???
在开发中,新手程序员在查找错误时,这时老程序员就会温馨提示,可以用断点调试,一步一步的看源码执行的过程,从而发现错误所在
1、断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行就会停住,然后你可以一步一步往下调试,调试过程中可以看各个变量当前的值,出错的话,调试到出错的代码行即显示错误,停下。进行分析从而找到这个Bug
2、断点调试是程序员必须掌握的技能
3、断点调试也能帮助我们查看底层源代码的执行过程,提高程序员的编程水平。