PAT相关的基础知识
- 一.sort函数
- 二.set的用法
- 三.输出百分比
- 四.数的科学计数法
- 五.lower_bound和upper_bound函数用法
- 六.最大公约数
- 七.最小公倍数
- 八.分母的四则运算
- 九.素数
- 十.atoi和stoi函数 将数字字符串转换为int输出
- 十一.字符的大小写转换 tolower和toupper
- 十二.字符串的大小写转换 transform
- 十三.map的用法
- 十四.散列
- 十五.auto的用法
- 十六.通过前序遍历建立二叉线索树
- 十七.判断二叉树每个结点的树高
- 十八.整数转换为数组
- 十九.数组转换为整数
- 二十.将字符串翻转 reverse和reverse_copy函数
- 二十一.四舍五入
- 二十二.不大于(小于)自变量的最大整数
- 二十三.根据先序和中序遍历建树
- 二十四.给定先序和中序,返回后序的第一个结点
- 二十五.给定二叉树先序和中序,指出两个结点的最低公共祖先
- 二十六.数组复制 memcpy
- 二十七.专业名词中英文
- 二十八.树中的层序遍历
- 二十九.给定完全二叉树的层序遍历,要求从右子树开始输出所有叶结点的路径
- 三十.给定完全二叉树的层序遍历,要求从左子树开始输出所有叶结点的路径
- 三十一.cctype头文件的使用
- 三十二.并查集
- 三十三.C++保留小数
一.sort函数
下面记录最基础的sort
函数的用法:
#include <algorithm>//头文件
//最简单的比较函数
bool cmp(int a,int b)
{
return a<b; //注意大于小于
}
//复杂一点的比较函数
bool cmp2(Student a, Student b) {
if (a.score != b.score)
return a.score > b.score;
else if (a.solve != b.solve)
return a.solve > b.solve;
else return a.id < b.id;
}
int main()
{
int a[10];
for(int i = 0; i < 10; i++)
scanf("%d",&a[i]);
sort(a, a + 10, cmp);
return 0;
}
二.set的用法
set的特性是,所有元素都会根据元素的键值自动排序,set的元素不像map那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值。set不允许两个元素有相同的键值。
set的各成员函数列表如下:
-
begin()–返回指向第一个元素的迭代器
-
clear()–清除所有元素
-
count()–返回某个值元素的个数
-
empty()–如果集合为空,返回true
-
end()–返回指向最后一个元素的迭代器
-
equal_range()–返回集合中与给定值相等的上下限的两个迭代器
-
erase()–删除集合中的元素
-
find()–返回一个指向被查找到元素的迭代器
-
get_allocator()–返回集合的分配器
-
insert()–在集合中插入元素
-
lower_bound()–返回指向大于(或等于)某值的第一个元素的迭代器
-
key_comp()–返回一个用于元素间值比较的函数
-
max_size()–返回集合能容纳的元素的最大限值
-
rbegin()–返回指向集合中最后一个元素的反向迭代器
-
rend()–返回指向集合中第一个元素的反向迭代器
-
size()–集合中元素的数目
-
swap()–交换两个集合变量
-
upper_bound()–返回大于某个值元素的迭代器
-
value_comp()–返回一个用于比较元素间的值的函数
建立
#include <set>
set <int> list[51];//set的数组
set <int> list2;
遍历
set<int>::iterator it;
for(it = list2.begin(); it! = list2.end(); it++)
{
cout << *it;
}
反向遍历
set<int>::reverse_iterator it;
for(it = list.rbegin(); it != list.rend(); it++)
{
cout << *it;
}
注意反向遍历用的指针类型不同,所指向的也不同,虽然是反向遍历,但是指针还是++而非- -
查找
if( list2.find (findnum) != list2.end() )
{
cout<<" The number is in list2"<<endl;
}
三.输出百分比
printf("%.1f%% \n", 100 * (double)comnum);
四.数的科学计数法
数的情况有以下几种:
0
0.12345
0.0012345
123.45
12345
001234.5
0012345
首先判断数的指数
该判断分为小于1的数和大于等于1的数
小于1的数,小数点后面连续0的个数即为数的指数的负数
大于等于1的数,小数点前面的数的个数即为数的指数(正数)
但在此之前要去掉数最前面的0,避免它也被算入其中
接着判断底,底只是非0数开头的要求长度的字符串
例如上述数字中,要求0后面保留4位,则(以下皆是科学计数法0.
后面的数字)
0 -> 0000
0.12345 -> 1234
0.0012345 ->1234
123.45 ->1234
此时的底已经与小数点没有关系了
但要注意,当字符串的长度不足要求时,要在后面补上对应个数的0
接下来就是处理步骤:
数字是保存在字符串或者字符数组中的,这样才能保证对数的每一位直接处理
1.
把开头的0去掉
此时小于1的是就是小数点开头,大于等于1的数就是非0的数字开头
2.
通过上述判断数是小于1的数还是大于等于1
这要分开处理,因为小于1的数指数是负的,大于等于1的数指数是正的
3.
若该数是小于1的数
则将字符串开头的小数点和连续的0都删除并且记录0的个数
此时0的个数即为10^-k
中的k
例如:0.001234
-> 0.1234*10^-2
4.
若该数是大于等于1的数
则计算小数点之前的数的个数并记录
此时该个数即为10^k
中的k
例如:123.45
-> 0.12345*10^3
在这个部分还需要将小数点从字符串中去掉,方便后面底的提取
5.
判断了上述3和4,还需要判断输入的数是不是0,因为如果是0,则k是未知的,因为在第1步时已经把0去掉了
因此还需要判断是不是0,如果是0则指数k
为0
如何判断是不是0:判断字符串是不是空即长度是否为0即可
6.
最后就是底数的处理了
在剩下的字符串提取要求个数的字符,如果字符串的长度小于要求的个数则在后面补0
7.
最后处理得出的结果,底数和指数可以建立一个结构体保存
最后比较两个结构体的底和指数就可以了
代码:
struct science_num {
string di;
int zhi;
} na, nb;
science_num change(string a, int N) {
int k = 0;
science_num na;
na.zhi = 0;
na.di.clear();
//去除前导0
while (a.length() > 0 && a[0] == '0') {
a.erase(a.begin());
}
//若a是小于1的数
if (a[0] == '.') {
a.erase(a.begin());//将小数点删除
while (a.length() > 0 && a[0] == '0') {
a.erase(a.begin());//将小数点后面连续的0删除
na.zhi--;//记录0的个数
}
}
//若a不小于,找到可能存在的小数点并删除
else {
while (k < a.length() && a[k] != '.') {
k++;
na.zhi++;//记录小数点前数字的个数
}
if (k < a.length()) {
a.erase(a.begin() + k);
}
}
//若a为0,将指数置0
if (a.length() == 0) {
na.zhi = 0;
}
//预处理完毕,规格化
if (a.length() >= N) {
na.di.insert(na.di.begin(), a.begin(), a.begin() + N);//提取要求长度的字符串作为底
}
else {
na.di.insert(na.di.begin(), a.begin(), a.end());
for (int i = 0; i < N - a.length(); i++) {
na.di += '0';//当长度不满足要求的时候在后面补0
}
}
return na;//返回结果结构体
}
补充:字符串的处理
删除字符串指定位置的函数:
a.erase(a.begin() + k);
将字符串一部分添加到另一个字符串:
str1.insert(str1.begin(), str2.begin(), str2.begin() + N);
清空字符串:
str1.clear();
五.lower_bound和upper_bound函数用法
在从小到大的排序数组中
lower_bound(begin,end,num)
表示从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字
upper_bound(begin,end,num)
表示从数组的begin位置到end-1位置二分查找第一个大于num的数字
在从大到小的排序数组中
lower_bound(begin,end,num,greater<type>())
表示从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字
upper_bound(begin,end,num,greater<type>())
表示从数组的begin位置到end-1位置二分查找第一个小于num的数字
以上若找到返回该数字的地址,不存在则返回end
因此需要通过返回地址减去起始地址begin,得到该数字在数组中的下标
六.最大公约数
int gcd(int a,int b){
if(b == 0)
return a;
else
return gcd(b, a % b);
}
七.最小公倍数
辗转相除法求最小公倍数:
int lcm(int x,int y){
int a = 0, b = 0;
int temp = 0;
if(x < y)
{
temp = x;
x = y;
y = temp;
}
a = x * y;
while(y != 0)
{
b = x % y;
x = y;
y = b;
}
return a/x;
}
八.分母的四则运算
分数的表示和化简
分数的表示
对一个分数来说,最简洁的写法就是写成假分数的形式,无论分子比分母大或者小,都保留其原数
因此可以使用一个结构体来存储这种只有分子和分母的分数:
struct Fraction{
int up,down;
};
对这种表示制订三项原则:
1.使down为非负数,如果分数为负,那么令分子up负即可
2.如果该分数恰为0,那么规定其分子为0,分母为1
3.分子和分母没有除了1以外的公约数
分数的化简
分数的化简主要用来使Fraction变量满足分数表示的三项规定,因此此步骤也分为三步:
1.如果分母down为负数,那么令分子up和分母down都变为相反数
2.如果分子up为0,那么令分母down为1
3.约分:求出分子绝对值与分母绝对值的最大公约数d,然后令分子分母同时除以d
代码如下:
Fraction reduction(Fraction result){
if(result.down < 0)
{
result.up = -result.up;
result.down = -result.down;
}
if(result.up == 0)
{
result.down = 1;
}
else
{
int d = gcd(abs(result.up), abs(result.down));
result.up /= d;
result.down /= d;
}
return result;
}
分数的四则运算
分数的加法
对两个分数f1
和f2
,其加法计算公式为
result = ( f1.up * f2.down +f2.up * f1.down ) / (f1.down * f2.down)
代码如下:
Fraction add( Fraction f1, Fraction f2)
{
Fraction result;
result.up = f1.up * f2.down + f2.up * f1.down;
result.down = f1.down * f2.down;
return reduction(result); //返回结果分数,注意化简
}
分数的减法
对两个分数f1
和f2
,其加法计算公式为
result = ( f1.up * f2.down - f2.up * f1.down ) / (f1.down * f2.down)
代码如下:
Fraction minu( Fraction f1, Fraction f2)
{
Fraction result;
result.up = f1.up * f2.down - f2.up * f1.down;
result.down = f1.down * f2.down;
return reduction(result); //返回结果分数,注意化简
}
分数的乘法
对两个分数f1
和f2
,其乘法计算公式为:
result = (f1.up * f2.up) / ( f1.down * f2.down)
代码如下:
Fraction multi(Fraction f1, Fraction f2)
{
Fraction result;
result.up = f1.up * f2.up;
result.down = f1.down * f2.down;
return reduction(result);
}
分数的除法
对两个f1
和f2
,其除法计算公式为:
result = (f1.up * f2.down) / (f2.up * f1.down)
代码如下:
Fraction divide(Fraction f1, Fraction f2){
Fraction result;
result.up = f1.up * f2.down;
result.down = f1.down * f2.up;
return reduction(result);
}
除法有额外注意事项,如果读入的除数为0(只需判断f2.up
是否为0),那么应当直接特别输出题目要求的输出语句(例如输出Error、Inf之类)。只有当除数不为0时,才能用上面的函数进行计算。
分数的输出
分数的输出根据题目的要求进行,但是大体上有以下几个注意点:
1.输出分数前,需要对其进行化简
2.如果分数r的分母down为1,说明该分数是整数,一般来说题目会要求直接输出分子,而忽略分母的输出
3.如果分数r的分子up的绝对值大于分母down,说明该分数是假分数,此时应按带分数的形式输出,即整数部分为r.up/r.down
,分子部分为abs(r.up)%r.down
,分母部分为r.down
4.以上均不满足时说明分数r是真分数,按原样输出即可
以下是一个输出示例:
void showResult( Fraction r)
{
if(r.down == 1)
printf("%lld",r.up);
else if(abs(r.up) > r.down)
printf("%d %d/%d",r.up/r.down, abs(r.up)%r.down, r.down);
else
printf("%d/%d", r.up, r.down);
}
强调一点,由于分数的乘法和除法的过程中可能使分子或分母超过int型表示范围,因此一般情况下,分子和分母应当使用long long型来存储。
九.素数
素数又称为质数,是指除了1和本身之外,不能被其他数整除的一类数。
1既不是素数,也不是合数。
素数的判断
#include <math.h>
bool isPrime(int n)
{
if(n <= 1)
return false;
int sqr = (int)sqrt(1.0 * n);//因为sqrt的参数要求是浮点数,因此在n前面乘以1.0来使其成为浮点型
for(int i = 2; i <= sqr; i++)
if(n % i == 0)
return false;
return true;
}
素数表的获取
从1~n进行枚举,判断每个数是否是素数,如果是素数则加入素数表。
这种方法的枚举部分的复杂度是O(n)
,而判断素数的复杂度是O(n^1/2^)
,因此总复杂度是O(n^3/2^)
但是当n大于105时,枚举的方法可能会导致超时,因此使用素数筛选的方法,其时间复杂度为O(nloglogn)
素数筛选法的代码如下:
const int maxn = 101;
int prime[maxn], pNum = 0;
bool p[maxn] = { 0 };
void Find_Prime()
{
for (int i = 2; i < maxn; i++)
{
if (p[i] == false)
{
prime[pNum++] = i;
for (int j = i + i; j < maxn; j += i)
{
p[j] = true;
}
}
}
}
完整的求解100以内所有素数的代码:
#include <stdio.h>
const int maxn = 101;
int prime[maxn], pNum = 0;
bool p[maxn] = { 0 };
void Find_Prime()
{
for (int i = 2; i < maxn; i++)
{
if (p[i] == false)
{
prime[pNum++] = i;
for (int j = i + i; j < maxn; j += i)
{
p[j] = true;
}
}
}
}
int main()
{
Find_Prime();
for (int i = 0; i < pNum; i++)
printf("%d ", prime[i]);
return 0;
}
输出结果:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
十.atoi和stoi函数 将数字字符串转换为int输出
头文件都是#include <cstring>
两个函数的不同点:
1.atoi()的参数是const char*
,因此对于一个字符串str我们需要用c_str()
的方法把string转换为const char类型
而stoi()的参数是const string
,不需要转换为const char
2.stoi()会做范围检查,默认范围是在int的范围内的,如果超出了会报错runtime error!
而atoi()不会做范围检查,如果超出范围的话,超出上界则返回上界,超出下界则返回下界
举例代码如下:
#include <cstring>
#include <string>
string z;
cin >> z;
int a = stoi(string(z.begin(), z.begin() + z.size() / 2));//z的前一半转换为数字a
int b = stoi(string(z.begin() + z.size() / 2, z.end()));//z的后一半转换为数字b
int c = stoi(z);//z转换为数字c
十一.字符的大小写转换 tolower和toupper
tolower()函数:把字符转换为小写
toupper()函数:把字符转换为大写
头文件:#include <cctype>
但是仅有#include <iostream>
也是可以的
参数是字符,因此要将字符串整个改成大写或小写需要遍历
或者有下述的方法
十二.字符串的大小写转换 transform
利用transform函数
transform(str.begin(),str.end(),str.begin(),::tolower);
头文件#include <algorithm>
例如:
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
cout<<"请输入一个含大写的字符串:";
string str;
cin>>str;
transform(str.begin(),str.end(),str.begin(),::tolower);
cout<<"转化为小写后为:"<<str<<endl;
transform(str.begin(),str.end(),str.begin(),::toupper);
cout<<"转化为大写后为:"<<str<<endl;
return 0;
}
十三.map的用法
map翻译为映射,是常用的STL容器
map可以将任何基本类型(包括STL容器)映射到任何基本类型(包括STL容器),也就可以建立string型到int型的映射
头文件:#include <map>
还需要:using namespace std;
map的定义
单独定义一个map:
map<typename1, typename2> mp;
其中,第一个是键的类型,第二个是值的类型
注意:如果是字符串到整数的映射,必须使用string,而不能使用char数组。因为char数组作为数组,不能被作为键值
例如:将一个set容器映射到一个字符串:
map<set<int>,string> mp;
map容器内元素的访问
map一般有两种访问方式:通过下标访问或通过迭代器访问
1.通过下标访问
例如对一个定义为map<char,int> mp;
的map来说,就可以直接使用mp['c']
的方式来访问它对应的整数
赋值的话,就是mp['c'] = 20
注意:map中的键是唯一的
2.通过迭代器访问
map迭代器的定义和其他STL容器迭代器的定义方式相同:
map<typename1, typename2>::iterator it;
map迭代器的使用方式和其他STL容器的迭代器不同,因为map的每一对映射都有两个typename,这决定了必须通过一个it来同时访问键和值
事实上,map可以使用it->first
来访问键,使用it->second
来访问值
举例代码如下:
#include <stdio.h>
#include <map>
int main()
{
map<char, int> mp;
mp['m'] = 20;
mp['r'] = 30;
mp['a'] = 40;
for(map<char,int>::iterator it = mp.begin(); it != mp.end(); it++)
printf("%c %d\n",it->first,it->second);
return 0;
}
此外,map会以键从小到大的顺序自动排序(这是由于map内部是使用红黑树实现的,set也是,在建立映射的过程中会自动实现从小到大的排序功能)
map常用函数
1.find(key)
返回键为key的映射的迭代器,时间复杂度为O(lonN)
,N为map中映射的个数
mp['b'] = 20;
map<char,int>::iterator it = mp.find('b');
cout<< it->first << " " << it->second << endl;
2.erase()
2.1 删除单个元素:
mp.erase(it)
it为要删除的元素的迭代器,时间复杂度为O(1)
2.2 删除映射的键
mp.erase(key)
,时间复杂度为O(logN),N为map内元素的个数
2.3 删除一个区间内的所有元素
‘mp.erase(first,last)’,其中first为需要删除区间的起始迭代器,last为需要删除区间的末尾迭代器的下一个地址,即该区间为左闭右开,时间复杂度为O(last - first)
示例代码:
#include <stdio.h>
#include <map>
int main()
{
map<char, int> mp;
mp['a'] = 1;
mp['b'] = 2;
mp['c'] = 3;
map<char,int>::iterator it = mp.find('b');
mp.erase(it, mp.end());//删除it之后的所有映射,即 b 2 和 c 3
for(map<char,int>::iterator it = mp.begin(); it != mp.end(); it++)
printf("%c %d\n",it->first,it->second);
return 0;
}
输出结果:
a 1
3.size()
用来获得map中映射的对数,时间复杂度为O(1)
4.clear()
清空map中的所有元素,复杂度为O(N)
,其中N为map中元素的个数
map的常用用途
1.需要建立字符(或字符串)与整数之间映射的题目,使用map可以减少代码量
2.判断大整数或者其他类型数据是否存在的题目u,可以把map当bool数组使用
3.字符串和字符串的映射也有可能会遇到
扩展
map的键和值是唯一的,而如果一个键需要对应多个值,可以使用multimap
另外,c++11标准中还增加了unordered_map
,以散列代替map内部的红黑树实现,使其可以用来处理只映射而不按key排序的需求,速度比map快得多
十四.散列
散列:就是将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素
这个转换函数就称为散列函数
常用的散列方法有:直接定址法、平方取中法、除留余数法
直接定址法
恒等变换:H(key) = key
线性变换:H(key) = a * key + b
平方取中法
指取key的平方的中间若干位作为hash值(很少用)
除留余数法
比较实用的还是除留余数法
H(key) = key % mod
注意:表长TSize必须不小于mod,不然会产生越界
当mod是素数时,H(key)可以尽可能覆盖[0,mod)的区间
但是还会存在问题:两个数最后的H(key)可能是相同的,这种情况叫做“冲突”
因此有三种常用方法来解决冲突:
线性探索法(Linear Probing)
当指定的位置已经被占用时,检查后面一个位置是否被占用,以此类推,若超过了表长,则回到表的首位继续循环,直到找到一个可以使用的位置,或者是发现表中的所有位置都已经被占用
但是这样会导致扎堆现象,即表中连续若干个位置都被占用,一定程度上降低效率
平方探索法(Quadratic Probing)
当表中下标为H(key)的位置被占时,将按照下面的顺序检查表中的位置:
H(key)+12、H(key)-12、H(key)+22、H(key)-22、H(key)+32、H(key)-32……
如果检查过程中H(key)+k2超过了表长TSize,那么就把H(key)+k2对表长TSize取模
十五.auto的用法
1.用于代替冗长复杂、变量使用范围专一的变量声明
例如:
#include <string>
#include <vector>
using namespace std;
int main()
{
vector<string> vs;
for(auto i = vs.begin(); i != vs.end(); i++)
{
……
}
}
2.在定义模板函数时,用于声明依赖模板参数的变量类型
例如:
#include <iostream>
using namespace std;
template <typename _Tx, typename _Ty>
void Multiply(_Tx x, _Ty y)
{
auto v = x * y;//因为无法知道x和y真正的类型是什么,用auto就可以解决
cout<<v;
}
注意:
1.auto变量必须在定义时初始化,这类似于const关键字
2.定义在一个auto序列的变量必须始终推导成同一类型
例如:
auto a = 10, b = 11, c = 13;//正确
auto a = 10, b = 'c', c = true;//错误
3.如果初始化表达式是引用,则去除引用语义
int a = 10;
int &b = a;
auto c = b;//c的类型为int而非int&(去除引用)
auto &d = b;//d的类型才为int&
c = 100;//
d = 100;//
4.如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。
const int a1 = 10;
auto b1= a1; //b1的类型为int而非const int(去除const)
const auto c1 = a1;//此时c1的类型为const int
b1 = 100;//合法
c1 = 100;//非法
5.如果auto关键字带上&号,则不去除const语意。
const int a2 = 10;
auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int
b2 = 10; //非法
这是因为如何去掉了const,则b2为a2的非const引用,通过b2可以改变a2的值,则显然是不合理的。
6.初始化表达式为数组时,auto关键字推导类型为指针。
int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout << typeid(b3).name() << endl;
程序将输出
int *
7.若表达式为数组且auto带上&,则推导类型为数组类型。
int a7[3] = { 1, 2, 3 };
auto & b7 = a7;
cout << typeid(b7).name() << endl;
程序输出
int [3]
8.函数或者模板参数不能被声明为auto
void func(auto a) //错误
{
//...
}
时刻要注意auto并不是一个真正的类型。
auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
cout << sizeof(auto) << endl;//错误
cout << typeid(auto).name() << endl;//错误
十六.通过前序遍历建立二叉线索树
因为二叉线索树本身存在一个知识点:它的中序遍历是从小到大的排序
因此只给一个前序遍历就可以建立二叉线索树
代码:
struct node
{
int val;
struct node* left,* right;
};
node* build(node* root, int v)
{
if (root == NULL)
{
root = new node;
root->val = v;
root->left = root->right = NULL;
}
else if (abs(v) <= abs(root->val))//如果小于根结点就是根的左边
root->left = build(root->left, v);//递归部分
else
root->right = build(root->right, v);//递归部分
return root;
}
int main()
{
int n;
cin >> n;
node* root = NULL;//建立根结点为空
for (int j = 0; j < n; j++)
{
cin >> arr[j];
root = build(root, arr[j]);
}
return 0;
}
十七.判断二叉树每个结点的树高
int getNum(node* root)
{
if (root == NULL)
return 0;
int l = getNum(root->left);
int r = getNum(root->right);
return max(l, r) + 1;
}
十八.整数转换为数组
不足s位的数,在高位补0
比如不足4位的数,视为在高位补0,如189为0189
代码:
//如果某步得到了不足4位的数,则视为在高位补0,如189为0189
void to_array(int x, int a[])
{
int n = 3;
while (x)
{
a[n] = x % 10;
x = x / 10;
n--;
}
while (n >= 0)
{
a[n] = 0;
n--;
}
}
直接转换为数组
void to_array(int x, int a[],int &n)//将数字倒着存入数组
{
int i = 0;
while (x)
{
a[i] = x % 10;
x = x / 10;
i++;
}
n = i;//n保存数组的长度
}
如果要正着,
那么还需要将数组翻转一下,或者使用栈
void to_array2(int x, int a[], int& n)
{
stack <int> sta;
while (x)
{
sta.push(x % 10);
x = x / 10;
}
int i = 0;
while (!sta.empty())
{
a[i++] = sta.top();
sta.pop();
}
n = i;
}
十九.数组转换为整数
int to_int(int a[])
{
int n = 1;
int sum = 0;
for (int i = 3; i >= 0; i--)
{
sum += a[i] * n;
n = n * 10;
}
return sum;
}
或者使用前面说的atoi和stoi函数
二十.将字符串翻转 reverse和reverse_copy函数
reverse(beg,end)//将字符串倒序
reverse_copy(sourceBeg,sourceEnd,destBeg)//将字符串倒序存入数组b中
头文件:#include <algrithm>
例如:
string a, b;
cin >> a;
reverse_copy(a.begin(), a.end(), b);
reverse(a.begin(), a.end());
int a[10],b[10];
for(int i = 0; i < 10; i++)
a[i] = i;
reverse_copy(a, a + 10, b);
reverse(a, a + 10);
二十一.四舍五入
round()函数
头文件:#include <math.h>
例如:
#include <iostream>
#include <math.h>
using namespace std;
int main()
{
double a,b;
a = 2.3; b = 2;
cout<<round(a*b)<<endl;
}
输出结果:
5
二十二.不大于(小于)自变量的最大整数
不大于自变量的最大整数:floor()
不小于自变量的最大整数:ceil()
例如:
a = 2.1; floor(a) => 2; ceil(a) => 3
二十三.根据先序和中序遍历建树
假设给定的先序和中序遍历是以数组的形式,用一个函数建立树
#include<iostream>
using namespace std;
const int maxn = 50005;
//定义
struct node
{
int v;
node* lchild = NULL;
node* rchild = NULL;
}
int pre[maxn], in[maxn];
node* build(int preL,int preR, int inL, int inR)
{
if(preL > preR)
return NULL;
node* temp = new node;
temp->v = pre[preL];
int k;
for(k = inL; k <= inR; k++)
if(in[k] == pre[preL])
break;
int num = k - inL;
temp->lchild = build(preL + 1, preL + num, inL, k - 1);
temp->rchild = build(preL + num + 1, preR, k + 1, intR);
return temp;
}
int main()
{
int n;
cin>>n;
for(int i = 0; i < n; i++)
cin>>pre[i];
for(int i = 0; i < n;i++)
cin>>in[i];
node* tree = build(0, n - 1, 0, n - 1);
return 0;
}
二十四.给定先序和中序,返回后序的第一个结点
#include <iostream>
#include <vector>
using namespace std;
vector<int> pre, in;
bool flag = false;
void postOrder(int prel, int inl, int inr) {
if (inl > inr || flag == true) return;
int i = inl;
while (in[i] != pre[prel]) i++;
postOrder(prel + 1, inl, i - 1);
postOrder(prel + i - inl + 1, i + 1, inr);
if (flag == false) {
printf("%d", in[i]);
flag = true;
}
}
int main() {
int n;
cin >> n;
pre.resize(n);
in.resize(n);
for (int i = 0; i < n; i++) cin >> pre[i];
for (int i = 0; i < n; i++) cin >> in[i];
postOrder(0, 0, n - 1);
return 0;
}
解释:
先序遍历的第一个一定是根
所以先在中序遍历里找到根对应的下标,然后从0到下标的前一个范围内的所有值都是该根对应的左子树,从下标的后一个到最后都是该根对应的右子树
寻找左子树中的根,再区分它的左子树和右子树,再寻找它的左子树的根,以此类推直到找到叶子结点,如果没有左子树,则寻找它右子树的左子树
二十五.给定二叉树先序和中序,指出两个结点的最低公共祖先
求两个结点x和y的最早祖先
在二叉树中找两个结点的最早祖先,转化为知道先序、中序求最早祖先
问题的关键是中序遍历和先序遍历的特点
在中序遍历中,如果一个结点位于结点x和y之间,那么该结点就是结点x和y的祖先
所求的LCA(最低公共祖先)是同时拥有两个结点作为后代的最深结点,
所以先序遍历在最前面的满足同时是u和v的祖先的结点就是所求的LCA
在此之前需要先判断x和y在不在树中,还要判断LCA是x或者y
要求:
1.用map<int, bool> mp;
或者bool mp[100001]={flase};
记录存在在树里的结点对应地址的值为true
这样判断x和y在不在树中,直接调用x和y的地址上的值是否是true即可
2.中序遍历存储的数组
int inorder[500001];
和1一样,结点的值是数组的下标,结点在中序遍历的位置就是数组的值
所以直接用inorder[结点值]就可以获取结点在中序遍历中的位置,可以直接判断
代码:
int preorder[10001], inorder[500001];
map<int, bool> is_sn;
void search(int u, int v, int n)
{
if (is_sn[u] == false && is_sn[v] == false)
{
printf("ERROR: %d and %d are not found.\n", u, v);
return;//两个结点都不存在树中
}
else if (is_sn[u] == false && is_sn[v] == true)
{
printf("ERROR: %d is not found.\n", u);
return;//结点u不存在树中
}
else if (is_sn[u] == true && is_sn[v] == false)
{
printf("ERROR: %d is not found.\n", v);
return;//结点v不存在树中
}
int ui, vi, ans;
ui = inorder[u];
vi = inorder[v];
for (int i = 0; i < n; i++)
{
ans = preorder[i];
if ((inorder[ans] >= ui && inorder[ans] <= vi) || (inorder[ans] >= vi && inorder[ans] <= ui))
break;//等到满足条件的结点值
}
if (ans == u)
printf("%d is an ancestor of %d.\n", u, v);
else if (ans == v)
printf("%d is an ancestor of %d.\n", v, u);
else
printf("LCA of %d and %d is %d.\n", u, v, ans);
}
二十六.数组复制 memcpy
将数组vec复制给数组b
int b[maxn],vec[maxn];
memcpy(b, vec, sizeof(vec));
memcpy函数要求头文件#include <cstring>
二十七.专业名词中英文
堆:heap
先序遍历:preorder
中序遍历:inorder
后序遍历:postorder
程序遍历:level order
二叉树:a binary tree
二叉完全树:a complete binary tree
二叉搜索树:a binary search tree
二十八.树中的层序遍历
层序遍历有一个,可以直接得到根的两个孩子
如果是存储在下标从0开始的数组中:
index[i]
的孩子为index[2 * i + 1]
和index[2 * i + 2]
如果是存储在下标从1开始的数组中:
index[i]
的孩子为index[2 * i]
和index[2 * i + 1]
二十九.给定完全二叉树的层序遍历,要求从右子树开始输出所有叶结点的路径
vector<int> v;
void dfs(int index) {
//cout << "index = " << index << endl;
if (index * 2 > n && index * 2 + 1 > n) {//没有左右孩子
if (index <= n) {//防止超过范围
for (int i = 0; i < v.size(); i++)//将当前容器里的结点遍历一遍
printf("%d%s", v[i], i != v.size() - 1 ? " " : "\n");
}
}
else {//有左右孩子
v.push_back(a[index * 2 + 1]);//进入它的右子树
dfs(index * 2 + 1);
v.pop_back();
v.push_back(a[index * 2]);//进入它的左子树
dfs(index * 2);
v.pop_back();//要推出结点
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
v.push_back(a[1]);
dfs(1);
return 0;
}
运行结果:
8
98 72 86 60 65 12 23 50
98 86 23
98 86 12
98 72 65
98 72 60 50
三十.给定完全二叉树的层序遍历,要求从左子树开始输出所有叶结点的路径
vector<int> v;
void dfs(int index) {
//cout << "index = " << index << endl;
if (index * 2 > n && index * 2 + 1 > n) {//没有左右孩子
if (index <= n) {//防止超过范围
for (int i = 0; i < v.size(); i++)//将当前容器里的结点遍历一遍
printf("%d%s", v[i], i != v.size() - 1 ? " " : "\n");
}
}
else {//有左右孩子
v.push_back(a[index * 2]);//进入它的左子树
dfs(index * 2);
v.pop_back();//要推出结点
v.push_back(a[index * 2 + 1]);//进入它的右子树
dfs(index * 2 + 1);
v.pop_back();
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
v.push_back(a[1]);
dfs(1);
return 0;
}
运行结果:
8
98 72 86 60 65 12 23 50
98 72 60 50
98 72 65
98 86 12
98 86 23
三十一.cctype头文件的使用
1.判断是否是字母数字
isalnum() 如果参数是字母数字,即字母或者数字,函数返回true
2.判断是否是字母
isalpha() 如果参数是字母,函数返回true
3.判断是否是数字
isdigit() 如果参数是数字(0-9),函数返回true
4.判断是否是大写字母
isupper() 如果参数是大写字母,函数返回true
5.判断是否是十六进制数字
isxdigit() 如果参数是十六进制数字,即0-9、a-f、A-F,函数返回true
6.返回小写字母
tolower() 如果参数是大写字符,返回其小写,否则返回该参数
7.返回大写字母
toupper() 如果参数是小写字符,返回其大写,否则返回该参数
三十二.并查集
并查集是一种维护集合的数据结构
用一个数组int father[N]
实现
father[1] = 2 说明1的父节点是2,且1和2在一个集合里面
对同一个集合来说,只存在一个根结点,且将其作为所属集合的标识
并查集的基本操作
总体来说,并查集的使用需要先初始化father数组,然后再根据需要进行查找或合并的操作
1.初始化
一开始每个元素都是独立的一个集合,所以father[i]=i
for(int i=0; i<n;i++)
father[i] = i;//令father[i]=-1也可以
2.查找
由于规定同一个集合中只存在一个根结点,因此查找操作就是对给定的结点寻找其根结点的过程
实现的方式可以是递归或者递推,思路都是一样的,即反复寻找父亲节点,直到找到根结点(即 father[i] = i 的结点)
递推的代码:
int findFather(int x)
{
while(x != father[x])
x = father[x];
}
return x;
}
递归的代码:
int findFather(int x)
{
if(x == father[x]) return x;
else return findFather(father[x]);
}
3.合并
合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在的集合合并
具体就是把其中一个集合的根结点的父亲指向另一个集合的根结点
合并的代码:
void Union(int a, int b)
{
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB)
father[faA] = faB;
}
最后说明并查集的一个性质:在合并的过程中,只对两个不同的集合进行合并,如果两个元素在相同的集合中,那么就不会对它们进行操作,这就保证了在同一个集合中一定不会产生环,即并查集产生的每一个集合都是一棵树。
路径压缩
上面的并查集查找函数是没有经过优化的,在极端情况下效率较低。
由于findFather函数的目的是查找根结点,例如:
father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 3;
因此,如果只是为了查找根结点,那么完全可以想办法把操作等价变成:
father[1] = 1;
father[2] = 1;
father[3] = 1;
father[4] = 1;
这就相当于把当前查询结点的路径上的所有结点的父亲都指向根结点,查找的时候就不需要一直回溯去找父亲了,查询的复杂度可以降为o(1)
那么如何转换呢?可以概括为两个步骤:
- 按原先的写法获得x的根结点r
- 重新从x开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲全部改为根结点r
递推代码:
int findFather(int x)
{
int a = x;
while(x != father[x])
x = father[x];
//到这里,x存放的是根结点,下面把路径上的所有结点的father都改成根结点
while(a != father[a])
{
int z = a;//因为a要被father[a]覆盖,所以先保存a的值,以修改father[a]
a = father[a];//a回溯父亲结点
father[z] = x;//将原先的结点a的父亲改为根结点x
}
return x;
}
递归代码:
int findFather(int v)
{
if(v == father[v])//找到根结点
return v;
else
{
int F = findFather(father[v]);//递归寻找father[v]的根结点F
father[v] = F;//将根结点F赋给father[v]
return F;//返回根结点F
}
}
实践发现,最后还需要设置一个数组isroot来存储每一个集合的元素数目,再遍历得到集合数
三十三.C++保留小数
头文件#include <iomanip>
setprecision()
四舍五入,不保留最后的0
int a = 3.450
cout<< a<<endl;
//3.450
cout<<setprecision(1)<<a<<endl;
//3.5
cout<<setprecision(3)<<a<<endl;
//3.45
setiosflags(ios::fixed|ios::showpoint)
保留最后的0
int a = 3.450
cout << setiosflags(ios::fixed|ios::showpoint) << setprecision(3) << a << endl;
//3.450
注意它们的应用范围是后面输出的所有数