字符串
字符串是计算机与人类沟通的重要手段。——闫学灿
字符与整数的联系——ASCII码
每个常用字符都对应一个-128 ~ 127
的数字,二者之间可以相互转化:
#include <iostream>
using namespace std;
int main()
{
char c = 'a';
cout << (int)c << endl;
int a = 66;
cout << (char)a << endl;
return 0;
}
常用ASCII值:'A'- 'Z'
是65 ~ 90
,'a' - 'z
’是97 - 122
,0 - 9
是 48 - 57
。
字符可以参与运算,运算时会将其当做整数:
#include <iostream>
using namespace std;
int main()
{
int a = 'B' - 'A';
int b = 'A' * 'B';
char c = 'A' + 2;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
练习:输入一行字符,统计出其中数字字符的个数,以及字母字符的个数。
字符数组
字符串就是字符数组加上结束符'\0'
。
可以使用字符串来初始化字符数组,但此时要注意,每个字符串结尾会暗含一个’\0’字符,因此字符数组的长度至少要比字符串的长度多 11 !
#include <iostream>
using namespace std;
int main()
{
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显示的空字符
char a3[] = "C++"; // 自动添加表示字符串结尾的空字符
char a4[6] = "Daniel"; // 错误:没有空间可以存放空字符
return 0;
}
2.1 字符数组的输入输出:
#include <iostream>
using namespace std;
int main()
{
char str[100];
cin >> str; // 输入字符串时,遇到空格或者回车就会停止
cout << str << endl; // 输出字符串时,遇到空格或者回车不会停止,遇到'\0'时停止
printf("%s\n", str);
return 0;
}
读入一行字符串,包括空格:
#include <iostream>
using namespace std;
int main()
{
char str[100];
fgets(str, 100, stdin); // gets函数在新版C++中被移除了,因为不安全。
// 可以用fgets代替,但注意fgets不会删除行末的回车字符
cout << str << endl;
return 0;
}
2.2 字符数组的常用操作
下面几个函数需要引入头文件:
#include <string.h>
(1) strlen(str)
,求字符串的长度
(2) strcmp(a, b)
,比较两个字符串的大小,a < b返回-1,a == b返回0,a > b返回1。这里的比较方式是字典序!
(3) strcpy(a, b)
,将字符串b复制给从a开始的字符数组。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char a[100] = "hello world!", b[100];
cout << strlen(a) << endl;
strcpy(b, a);
cout << strcmp(a, b) << endl;
return 0;
}
2.3 遍历字符数组中的字符:
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char a[100] = "hello world!";
// 注意:下述for循环每次均会执行strlen(a),运行效率较低,最好将strlen(a)用一个变量存下来
for (int i = 0; i < strlen(a); i ++ )
cout << a[i] << endl;
return 0;
}
练习:给定一个只包含小写字母的字符串,请你找到第一个仅出现一次的字符。如果没有,输出no。
练习:把一个字符串中特定的字符全部用给定的字符替换,得到一个新的字符串。
标准库类型string
可变长的字符序列,比字符数组更加好用。需要引入头文件:
#include <string>
3.1 定义和初始化
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1; // 默认初始化,s1是一个空字符串
string s2 = s1; // s2是s1的副本,注意s2只是与s1的值相同,并不指向同一段地址
string s3 = "hiya"; // s3是该字符串字面值的副本
string s4(10, 'c'); // s4的内容是 "cccccccccc"
return 0;
}
3.2 string上的操作
(1) string的读写:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1, s2;
cin >> s1 >> s2;
cout << s1 << s2 << endl;
return 0;
}
注意:不能用printf直接输出string,需要写成:printf(“%s”, s.c_str());
(2) 使用getline
读取一整行
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
getline(cin, s);
cout << s << endl;
return 0;
}
(3) string
的empty
和size
操作(注意size
是无符号整数,因此 s.size() <= -1
一定成立):
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1, s2 = "abc";
cout << s1.empty() << endl;
cout << s2.empty() << endl;
cout << s2.size() << endl;
return 0;
}
(4) string的比较:
支持 >, <, >=, <=, ==, !=
等所有比较操作,按字典序进行比较。
(5) 为string对象赋值:
string s1(10, 'c'), s2; // s1的内容是 cccccccccc;s2是一个空字符串
s1 = s2; // 赋值:用s2的副本替换s1的副本
// 此时s1和s2都是空字符串
(6) 两个string对象相加:
string s1 = "hello, "", s2 = "world\n";
string s3 = s1 + s2; // s3的内容是 hello, world\n
s1 += s2; // s1 = s1 + s2
(7) 字面值和string对象相加:
做加法运算时,字面值和字符都会被转化成string
对象,因此直接相加就是将这些字面值串联起来:
string s1 = "hello", s2 = "world"; // 在s1和s2中都没有标点符号
string s3 = s1 + ", " + s2 + '\n';
当把string
对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string
:
string s4 = s1 + ", "; // 正确:把一个string对象和有一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算对象都不是string
string s6 = s1 + ", " + "world"; // 正确,每个加法运算都有一个运算符是string
string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加,运算是从左到右进行的
3.3 处理string对象中的字符
可以将string
对象当成字符数组来处理:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "hello world";
for (int i = 0; i < s.size(); i ++ )
cout << s[i] << endl;
return 0;
}
或者使用基于范围的for
语句:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "hello world";
for (char c: s) cout << c << endl;
for (char& c: s) c = 'a';
cout << s << endl;
return 0;
}
练习:密码翻译,输入一个只包含小写字母的字符串,将其中的每个字母替换成它的后继字母,如果原字母是'z'
,则替换成'a'
。
练习:输入两个字符串,验证其中一个串是否为另一个串的子串。
STL容器、位运算与常用库函数
STL是提高C++编写效率的一个利器。—— 闫学灿
#include <vector>
vector是变长数组,支持随机访问,不支持在任意位置 O(1)O(1) 插入。为了保证效率,元素的增删一般应该在末尾进行。
1.1 声明
#include <vector> // 头文件
vector<int> a; // 相当于一个长度动态变化的int数组
vector<int> b[233]; // 相当于第一维长233,第二位长度动态变化的int数组
struct rec{…};
vector<rec> c; // 自定义的结构体类型也可以保存在vector中
1.2 size/empty
size
函数返回vector
的实际长度(包含的元素个数),empty
函数返回一个bool
类型,表明vector
是否为空。二者的时间复杂度都是 O(1)O(1)。
所有的STL容器都支持这两个方法,含义也相同,之后我们就不再重复给出。
1.3 clear
clear
函数把vector
清空。
1.4 迭代器
迭代器就像STL容器的“指针”,可以用星号*操作符解除引用。
一个保存int
的vector
的迭代器声明方法为:
vector<int>::iterator it;
vector的迭代器是“随机访问迭代器”,可以把vector的迭代器与一个整数相加减,其行为和指针的移动类似。可以把vector的两个迭代器相减,其结果也和指针相减类似,得到两个迭代器对应下标之间的距离。
1.5 begin/end
begin
函数返回指向vector
中第一个元素的迭代器。例如a
是一个非空的vector
,则*a.begin()
与a[0]
的作用相同。
所有的容器都可以视作一个“前闭后开”的结构,end函数返回vector的尾部,即第n 个元素再往后的“边界”。*a.end()
与a[n]
都是越界访问,其中n = a.size()
。
下面两份代码都遍历了vector
for (int i = 0; i < a.size(); i ++)
cout << a[i] << endl;
for (vector<int>::iterator it = a.begin(); it != a.end(); it ++)
cout << *it << endl;
1.6 front/back
front
函数返回vector
的第一个元素,等价于*a.begin()
和a[0]
。
back
函数返回vector
的最后一个元素,等价于*--a.end()
和a[a.size() – 1]
。
1.7 push_back()和pop_back()
a.push_back(x)
把元素x
插入到vector a
的尾部。
b.pop_back()
删除vector a
的最后一个元素。
#include <queue>
头文件queue
主要包括循环队列queue
和优先队列priority_queue
两个容器。
2.1 声明
queue<int> q;
struct rec{…}; queue<rec> q; //结构体rec中必须定义小于号
priority_queue<int> q; // 大根堆
priority_queue<int, vector<int>, greater<int>> q; // 小根堆
priority_queue<pair<int, int>>q;
2.2 循环队列queue
push // 从队尾插入
pop // 从队头弹出
front // 返回队头元素
back // 返回队尾元素
2.3 优先队列priority_queue
push // 把元素插入堆
pop // 删除堆顶元素
top // 查询堆顶元素(最大值)
#include <stack>
头文件stack
包含栈。声明和前面的容器类似。
push // 向栈顶插入
pop // 弹出栈顶元素s
#include <deque>
双端队列deque
是一个支持在两端高效插入或删除元素的连续线性存储空间。它就像是vector
和queue
的结合。与vector
相比,deque
在头部增删元素仅需要 O(1)O(1) 的时间;与queue
相比,deque
像数组一样支持随机访问。
[] // 随机访问
begin/end // 返回deque的头/尾迭代器
front/back // 队头/队尾元素
push_back // 从队尾入队
push_front // 从队头入队
pop_back // 从队尾出队
pop_front // 从队头出队
clear // 清空队列
#include <set>
头文件set
主要包括set
和multiset
两个容器,分别是“有序集合”和“有序多重集合”,即前者的元素不能重复,而后者可以包含若干个相等的元素。set
和multiset
的内部实现是一棵红黑树,它们支持的函数基本相同。
5.1 声明
set<int> s;
struct rec{…}; set<rec> s; // 结构体rec中必须定义小于号
multiset<double> s;
5.2 size/empty/clear
与vector
类似
5.3 迭代器
set
和multiset
的迭代器称为“双向访问迭代器”,不支持“随机访问”,支持星号*
解除引用,仅支持++
和--
两个与算术相关的操作。
设it
是一个迭代器,例如set<int>::iterator it
;
若把it ++
,则it
会指向“下一个”元素。这里的“下一个”元素是指在元素从小到大排序的结果中,排在it
下一名的元素。同理,若把it --
,则it
将会指向排在“上一个”的元素。
5.4 begin/end
返回集合的首、尾迭代器,时间复杂度均为 O(1)O(1)。
s.begin()
是指向集合中最小元素的迭代器。
s.end()
是指向集合中最大元素的下一个位置的迭代器。换言之,就像vector
一样,是一个“前闭后开”的形式。因此-- s.end()
是指向集合中最大元素的迭代器。
5.5 insert
s.insert(x)
把一个元素x插入到集合s中,时间复杂度为 O(logn)O(logn)
。
在set
中,若元素已存在,则不会重复插入该元素,对集合的状态无影响。
5.6 find
s.find(x)
在集合s
中查找等于x
的元素,并返回指向该元素的迭代器。若不存在,则返回s.end()
。时间复杂度为 O(logn)O(logn)
。
5.7 lower_bound/upper_bound
这两个函数的用法与find
类似,但查找的条件略有不同,时间复杂度为O(logn)O(logn)
。
s.lower_bound(x)
查找大于等于x
的元素中最小的一个,并返回指向该元素的迭代器。
s.upper_bound(x)
查找大于x
的元素中最小的一个,并返回指向该元素的迭代器。
5.8 erase
设it
是一个迭代器,s.erase(it)
从s
中删除迭代器it
指向的元素,时间复杂度为 O(logn)O(logn)
。
设x
是一个元素,s.erase(x)
从s
中删除所有等于x
的元素,时间复杂度为 O(k+logn)O(k+logn)
,其中` k 是被删除的元素个数。
5.9 count
s.count(x)
返回集合s
中等于x
的元素个数,时间复杂度为 O(k+logn)O(k+logn)
,其中 k 为元素x
的个数。
#include <map>
map容器是一个键值对key-value的映射,其内部实现是一棵以key为关键码的红黑树。Map的key和value可以是任意类型,其中key必须定义小于号运算符。
6.1 声明
map<key_type, value_type> name;
//例如:
map<long, long, bool> vis;
map<string, int> hash;
map<pair<int, int>, vector<int>> test;
6.2 size/empty/clear/begin/end
均与set
类似。
6.3 insert/erase
与set
类似,但其参数均是pair<key_type, value_type>
。
6.4 find
h.find(x)
在变量名为h
的map
中查找key
为x
的二元组。
6.5 []操作符
h[key]
返回key
映射的value
的引用,时间复杂度为 O(logn)O(logn)
。
[]
操作符是map
最吸引人的地方。我们可以很方便地通过h[key]
来得到key
对应的value
,还可以对h[key]
进行赋值操作,改变key
对应的value
。
位运算与常用库函数
C++帮我们实现好了很多有用的函数,我们要避免重复造轮子。 —— 闫学灿
位运算
常用操作:
- 求x的第k位数字
x >> k & 1
lowbit(x) = x & -x
,返回x的最后一位1
常用库函数
2.1 reverse翻转
翻转一个vector:
reverse(a.begin(), a.end());
翻转一个数组,元素存放在下标1 ~ n:
reverse(a + 1, a + n + 1);
2.2 unique去重
返回去重(只去掉相邻的相同元素)之后的尾迭代器(或指针),仍然为前闭后开,即这个迭代器是去重之后末尾元素的下一个位置。该函数常用于离散化,利用迭代器(或指针)的减法,可计算出去重后的元素个数。
把一个vector去重:
int m = unique(a.begin(), a.end()) – a.begin();
把一个数组去重,元素存放在下标1 ~ n:
int m = unique(a + 1, a + n + 1) – (a + 1);
2.3 random_shuffle随机打乱
用法与reverse
相同。
2.4 sort
对两个迭代器(或指针)指定的部分进行快速排序。可以在第三个参数传入定义大小比较的函数,或者重载“小于号”运算符。
把一个int数组(元素存放在下标1 ~ n)从大到小排序,传入比较函数:
int a[MAX_SIZE];
bool cmp(int a, int b)
{
return a > b;
}
sort(a + 1, a + n + 1, cmp);
把自定义的结构体vector排序,重载“小于号”运算符:
struct Rec
{
int id, x, y;
};
vector<Rec> a;
bool operator <(const Rec &a, const Rec &b)
{
return a.x < b.x || a.x == b.x && a.y < b.y;
}
sort(a.begin(), a.end());
2.5 lower_bound/upper_bound 二分
lower_bound
的第三个参数传入一个元素x
,在两个迭代器(指针)指定的部分上执行二分查找,返回指向第一个大于等于x
的元素的位置的迭代器(指针)。
upper_bound
的用法和lower_bound
大致相同,唯一的区别是查找第一个大于x的元素。当然,两个迭代器(指针)指定的部分应该是提前排好序的。
在有序int
数组(元素存放在下标1 ~ n
)中查找大于等于x
的最小整数的下标:
int i = lower_bound(a + 1, a + 1 + n, x) - a;
在有序vector
int y = *--upper_bound(a.begin(), a.end(), x);
评论区