寒假训练营 第二节 数据结构基础(一)总结

一、数组

在 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 队列,那么就可以不用考虑循环队列了,方便但慢。
*/

四、链表

链表有很多种,其中比较基础的如下:

种类特点
单向链表只记录每个节点的后继。
双向链表记录每个节点的前驱和后继。
循环单向链表单链表,最后一个节点后继为第一个节点。
循环双向链表双链表,形成环形。

在这里插入图片描述

  1. 链表插入和删除的复杂度是 O(1)。
  2. 链表搜索指定元素位置/定位第k个元素的复杂度是 O(n)。
  3. 相比于数组,链表插入删除快,但是定位(找到第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、断点调试也能帮助我们查看底层源代码的执行过程,提高程序员的编程水平。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值