信息学竞赛 CPP 进阶篇

image-20220219193138448

软件的发展历史,是不断追求重用的历史。

c++ 如何实现重用?

  • 函数:通过参数变化,暴露接口,隐藏实现
  • 宏: 模板代码,编译时替换展开
  • 类:更复杂的抽象,暴露接口,隐藏内部结构、状态、实现细节
  • 模板泛型:把算法和类型分离开

宏的常见用法

三种格式

  • define XXX

    只是定义了一个符号,常用于条件编译

    可用#undef XXX 取消该定义

  • define PI 3.14

    定义一个常量,避免魔法数字,新标准建议用 const

  • define ADD(x,y) x+y

    有参的宏定义

    表面看像个函数,实际上是代码的硬替换

    有建议,用内联函数代替有参宏定义

宏在编译前进行宏替换处理,也叫:宏展开

这种替换是硬性的替换,与类型、语法无关,使用不当,容易引发诡异的错误。

例如:#define PI 3.14;

其后的源码:a = PI r r;

在编译前,被替换为:a = 3.14; r r;

原因仅仅是:#define 语句后边多了个分号。

宏的常见应用场景

  • 避免魔法数字,增加程序可读性,可维护性。

    1. #define M 100
    2. ....
    3. char x[M];
    4. x[M-1]='\0';

    注意陷阱:

    1. #define M 100+5
    2. ...
    3. int x[2*M]; // 想开两倍空间,实际展开:int x[2*100+5];
  • 预定义宏

    1. __FILE__ // 两个下划线,当前文件
    2. __LINE__ // 当前行号
    3. __DATE__ // 当前日期
    4. __TIME__ // 当前时间
  • 防止头文件多次包含

    传统方案是用宏配合条件编译:

    1. #ifndef _MY_H_
    2. #define _MY_H_
    3. // 文件内容
    4. #endif

    另一个方案,文件开始处:#pragma once

    以此表明:此文件只编译一次

    但,有些编译器可能不支持

  • 管理调试信息

    开发中,经常会在程序的中间步骤上输出调试信息,开发结束后则不需要这些输出

    1. //#define LOG(x) cout << (x) << endl;
    2. #define LOG(x)
    3. int main()
    4. {
    5. cout << "normal" << endl;
    6. LOG("only for debug...")
    7. return 0;
    8. }
  • 不调函数,提高效率

    比如,经常交换两个变量,不想用函数开销,则可以用有参宏替换:

    1. #define SWAP(a,b) {int t=a; a=b; b=t; }
    2. int main()
    3. {
    4. int a=5,b=8;
    5. SWAP(a,b)
    6. cout << a << "," << b << endl;
    7. return 0;
    8. }

    这有个小缺点:当类型不是整数,岂不悲哀?

    我们心里希望,那个临时变量类型与 a, b 是一样的。不一定是整数!

    也有办法:

    1. #define SWAP(a,b) {typeof(a) t=a; a=b; b=t;}

    typeof 与 sizeof 类似,它在编译阶段来确定参数的类型,所以并非真正的“动态”。

另一个有参宏的例子:

求最大值

  1. #define MAX(a,b) \
  2. ({ typeof(a) _a = (a); \
  3. typeof(b) _b = (b); \
  4. _a > _b ? _a : _b; })
  5. int main()
  6. {
  7. cout << MAX(2+3,4*5) << endl;
  8. return 0;
  9. }

实时效果反馈

1. 关于c++宏, 说法正确的是:__

  • A 宏的展开在程序运行时进行

  • B 在宏定义的末尾不能出现分号

  • C 宏定义只能写在同一行

  • D 为了避免魔法数字,也可以使用const定义常量

2. 下列x平方的宏,哪个更恰当?__

  • A #define PF(x) x*x

  • B #define PF(x) (x*x)

  • C #define PF(x) ((x)*(x))

  • D #define PF (x) ((x)*(x))

答案

1=>D 2=>C

宏(2)

image-20220219193727138

宏在编译前执行,可以很简单,也可以超级复杂。

因为宏可以嵌套展开。

  1. #define V1 "27.3.6"
  2. #define V2 "1.4"
  3. #define VER(pre,v) pre "." #v
  4. #define PR(x) cout << (x) << endl;
  5. int main()
  6. {
  7. PR(VER(V1,25))
  8. PR(VER(V2,25))
  9. return 0;
  10. }

运行结果:

章节三:信息学竞赛 CPP 进阶篇 - 图3

更多的宏特性

宏的展开规则:类似于函数调用,有参宏展开时,先对其参数宏展开

有例外:如果宏定义中有#或##则其参数不展开

我们可以利用这个特性,观察宏展开的结果:

  1. #define TO_STR(x) TO_S(x)
  2. #define TO_S(x) #x
  3. ....
  4. PR(TO_STR(VER(V1,25)))

如果x是参数,则#x 是把x加双引号包围

## 则是拼接的意思,可以造出一个标识符来

  1. #define ODD(x,y) int y##x ()
  2. ODD(in,ma)
  3. {
  4. cout << "haha" << endl;
  5. return 0;
  6. }

这个功能太强大了,如果精心设计,我们可以改变c++的语法!

但,程序的可读性会严重下降。

宏的优势

宏的优势在于,可以在编译前对源码进行变换。

这种变换不占用运行时,使用得好,可以减少模板代码,可以提高效率。

c++还有很多关于宏的很细致的规定和约束,

并非一开始就设计成那样,这是长期实践,积累,修补的结果。

一般认为,适量使用宏可以获得效率、代码可读性、兼容性、可维护性等方面的益处。

但,过于复杂的宏很可能是隐患。

宏注意

  • 有参宏的括号重要性
  1. #define AREA(r) 3.14*r*r

有参宏,定义了圆面积的计算方法。

如果使用:

  1. double a = AREA(1+1) ;
  2. cout << a << endl;

不会得到期望的结果,以内,硬性替换后,相当于:

  1. double a = 3.14*1+1*1+1;

较好的方法是:

  1. #define AREA(r) (3.14*(r)*(r))
  • 宏替换的第一个位置必须为合法的标识符

    #define 0x ABC

  • 宏替换的后边内容也不能随意写,引号必须配对

    #define P1 at"

  • 双引号内的内容不会被替换

    #define S(a) "a"

  • 一个标识符整体被替换,它的一部分不会替换

宏:只负责文字上的替换,不管语法的事情,也不会计算表达式。

常量,可以用 const 代替

有参宏,一般可以用 inline 函数代替

很多情况也可以使用 函数模板、类模板来解决。

总结

就像 goto 语句一样,宏在有些场合用,很容易,很恰当;但,滥用,会导致灾难。

理论上,宏足以强大到用 c 构建出另一种高级语言来。

实践上,复杂的宏,常常让我们涌起砸碎显示屏的冲动。

实时效果反馈

1. 关于宏的弱点, 说法==不正确==的是:__

  • A 逻辑稍复杂后,可读性差

  • B 难于调试

  • C 没有名字空间,污染全局域

  • D 拖慢运行速度

2. c中的有参宏,c++中建议用什么代替?__

  • A const

  • B inline function

  • C function template

  • D function pointer

答案

1=>D 2=>B

内联函数

image-20220221102442300

为什么要定义如下宏?

  1. #define MIN(x,y) ((x)<(y)?(x):(y))

更好一点的版本:

  1. #define MIN(a,b) \
  2. ({ typeof(a) _a = (a); \
  3. typeof(b) _b = (b); \
  4. _a < _b ? _a : _b; })

既然,这么简单的功能,如此繁冗的代码,使用函数不好吗?

函数调用的弱点

因为,如果用一个函数,调用函数会耗费运行时。

函数调用过程:

  1. 保存当前的执行断点(执行上下文)
  2. 分配形参变量空间
  3. 用实参来初始化形参
  4. 跳转到被调函数代码执行
  5. 释放形参变量(可能有必要的清理工作,比如析构函数)
  6. 恢复原来的执行断点,继续主调函数的工作

如果频繁地切换小函数,只执行很短的代码,则切换环境会相对浪费时间。

如果,直接写 ?: 表达式,又太麻烦,可读性不好。

宏呢?虽然简单如此,也要一堆括号,防止参数中含有表达式。

使用宏的弱点

  • 比起函数来,宏的可读性差,宏不容易调试,不容易维护

  • 宏没有类型,编译器没办法做更多的介入和检查

  • 宏不能访问类的私有成员,但使用函数,可以声明友元函数

inline function

有没有什么办法,既有函数的形式,又不真的调用函数呢?

内联函数是个解决方案

  1. inline int min(int a, int b)
  2. {
  3. return a < b ? a : b;
  4. }
  5. int main()
  6. {
  7. cout << min(5, 6) << endl;
  8. cout << min(6+8, 5+4) << endl;
  9. return 0;
  10. }

这段代码,

编译时,会把对 min 的调用直接展开为代码,

这样,虽然最终生成的代码变长了,也包含了重复,但执行效率提高了。

注意事项

  • inline 关键字并非函数签名的一部分,它并非是接口,它是控制实现方式的。

inline 只有与函数的定义(而非声明)放在一起才有效。

  • 定义在 class 内的成员函数自动变为内联函数
  1. class MyA{
  2. public:
  3.   int add() { return i+j;}
  4.   inline int dec() { return i-j;}
  5.   int getNum();
  6. private:
  7. int i,j;
  8. }
  9. inline int MyA::getNum(){
  10. return i;
  11. }
  • 小的函数用内联(建议小于10行),大函数得不偿失

带来的问题

宏是不管类型的,仅仅是文字上的替换。

函数是有类型的,编译器可以介入检查,虽然安全,但带来了类型限制。

比如,min 函数怎么指定其参数是:字符串,或其它更复杂的类型呢?

这方面,宏的类型无关反而成了优点。

但,c++的函数模板,类模板可以解决这个问题。

实时效果反馈

1. 关于inline, 说法==不正确==的是:__

  • A inline 写在.h文件中,则该函数编译为内联函数

  • B inline 与实现写在一起才有效,以便于编译时展开。

  • C 直接定义在 class 中的函数,自动编译为内联函数

  • D 一般建议小函数用 inline,函数太庞大,用内联得不偿失

2. 内联函数与宏的关系是:__

  • A 内联函数可以完全代替宏的功能

  • B 使用内联比使用宏执行效率更高

  • C 一般情况下,与内联函数相比,使用宏的代码可读性更好一些

  • D 与宏相比,内联函数的可读性,可维护性更好些。

答案

1=>A 2=>D

函数模板

image-20220221145018622

c++中有泛型编程的概念。

即,编程时,类型是不确定的,在调用处,替换为真正的具体类型。

c++的指针泛化,在一定程度上解决了泛型编程的问题,但指针泛化是在运行时发生的,对泛型指针的调用,比普通的成员函数调用,更耗费处理器时间。

而这里所说的泛型编程,力求在编译阶段解决问题,不能降低执行效率。

c++使用函数模板类模板来解决这个问题。

函数模板定义

以 template 来开启泛型函数声明

告诉编译器, T是个待定的类型,在具体调用时编译为实际的类型。

  1. template <typename T>
  2. void swap1(T& a, T& b)
  3. {
  4. T t = a;
  5. a = b;
  6. b = t;
  7. }
  8. int main()
  9. {
  10. int a=5,b=8;
  11. swap1<int>(a,b);
  12. //swap1(a,b);
  13. cout << a << "," << b << endl;
  14. return 0;
  15. }

标准库中已经有了 std::swap,所以改了名字

调用的时候,用来指定 T 的具体类型,

这里,不指定也可以,因为,编译器能推断出 T 的类型是 int

可以把 T 换成其它类型,甚至是用户自己定义的类型来试试。

使用注意

  • 编译器会对函数模板进行两次编译,第一次编译模板函数本身,第二次替换类型后,当作普通函数编译
  • 函数模板不能隐式转换,类型需要严格匹配
  • 函数模板可以有多个模板类型参数
  • 无法自动推断返回值类型,需要自己指定
  1. template<typename T1, typename T2>
  2. T1 f(T2 x, T2, y) { ... }
  3. ....
  4. auto t1 = f<int>(a, b); // 指定了返回值类型

类型名需要从左到右依次指定(可以部分指定),所以返回值应该定义为 T1,

否则,比如在最后一个typename, 为了指定返回值,就需要把所有名字都指定了。

与函数重载关系

可以共存。

函数重载,在有限的类型种类中,分别定制如何处理,而模板类型可以没有限定。

当发生二义时,首先选普通函数,其次才考虑模板

如果模板可以更精确匹配,选择模板

我们可以使用空尖括号,限定只能使用模板

与 inline 联合使用

这样用可以代替一些有参宏,并且是类型安全的。

比如,经典的求最大值

  1. template <class T>
  2. inline T max1(const T& a, const T& b) // 标准库中已有max
  3. {
  4. return a > b? a : b;
  5. }

这样,兼具了宏的类型无关,和内联函数编译监督两个好处。

实时效果反馈

1. 关于函数模板, 说法正确的是:__

  • A 与宏一样,函数模板在编译前展开

  • B 函数模板比普通函数执行更有效率

  • C 函数模板在编译时,把泛定类型确定为具体类型

  • D 函数模板,在运行时把泛定类型替换为普通类型

2. 如果有 template<class T> T f(T a, Tb){....},以下错误的是:__

  • A f(1,2);

  • B f(1,2);

  • C f(1.2, 3.4);

  • D f(1.2, 3);

答案

1=>C 2=>D

函数模板特化

image-20220223085016293

语法固然重要,但更重要的是最佳实践。

宏可以带来效率,可以类型无关;函数可以有可预料的行为和安全的类型。

内联的函数模板可以兼具两者优势。

常见的用法举例:

  1. // 删除堆指针的时候,一般的行为
  2. template <typename T>
  3. inline void SafeDelete(T*& p)
  4. {
  5. if(p){delete p; p=NULL;}
  6. }
  7. // 返回数组变量的元素个数
  8. template <typename T>
  9. inline int CountOf(const T& array)
  10. {
  11. return sizeof(array) / sizeof(array[0]);
  12. }
  13. // 判断一个类型是否为 unsigned 类型
  14. template <typename T>
  15. inline bool IsUnsigned(T)
  16. {
  17. return (T)-1 > 0;
  18. }

对特殊情形的单独处理

  • 可以定义一个重载的普通函数
  • 可以定义一个特化的函数模板

特化的例子

  1. template <class T>
  2. bool eq(const T& a, const T& b)
  3. {
  4. return a==b;
  5. }
  6. template<>
  7. bool eq<char*>(char* const & p1, char* const & p2)
  8. {
  9. cout << "eq...<>" << endl;
  10. return strcmp(p1,p2)==0;
  11. }

调用处:

  1. int a = 10;
  2. cout << eq(a,5+5) << endl;
  3. char* p1 = "abc";
  4. char* p2 = "abc";
  5. char p3[] = "abc";
  6. cout << eq(p1, p2) << endl;
  7. //cout << eq(p1, p3) << endl;

注意:p1,p3 比较会报编译错误,因为:类型不一致。模板函数类型严格。

p1 类型:char*

p3 类型:char[4]

其实,我们也可以直接用重载,它会进行一些必要的类型转换,不会要求那么严格匹配。

  1. bool eq(char* p1, char* p2)
  2. {
  3. cout << "eq...normal" << endl;
  4. return strcmp(p1,p2)==0;
  5. }
  6. int main()
  7. {
  8. int a = 10;
  9. cout << eq(a,5+5) << endl;
  10. char* p1 = "abc";
  11. char* p2 = "abc";
  12. char p3[] = "abc";
  13. cout << eq<>(p1, p2) << endl; // 强制使用模板
  14. cout << eq(p1, p3) << endl;
  15. return 0;
  16. }

实时效果反馈

1. 关于函数模板与重载函数的关系, 说法正确的是:__

  • A 重载函数不可以和函数模板同名

  • B 模板与重载函数都匹配时,优先使用具体函数版本

  • C 函数模板匹配参数很宽松,可以自动进行必要的类型转换

  • D 函数模板不可以实现为内联函数

2. 为解决函数模板对有些类型不通用的问题,哪个解决方案最==不推荐==?:__

  • A 定义函数模板的特化版本

  • B 定义针对具体类型的普通函数重载版本

  • C 在函数模板实现中用 if 语句断定类型,分别处理

  • D 设计复杂的宏来取代模板

答案

1=>B 2=>D

函数模板与数组引用

image-20220223100937515

当数组作为参数传递时,会退化为指针。

也就是说,数组的元素个数信息会丢失。我们可以通过typeid来检查验证。

  1. void g(int x[3])
  2. {
  3. cout << typeid(x).name() << endl;
  4. // [3] 是徒劳,因为退化问题,与 int x[], 或者 int* x 是一样的
  5. }

注意,别忘了:#include

调用处:

  1. int* p;
  2. int q[] = {1,2,3};
  3. int a = 16;
  4. g(q);
  5. cout << typeid(p).name() << endl;
  6. cout << typeid(q).name() << endl;

但是,当数组作为引用传递时,就不一样:

  1. void g(int (&x)[3])
  2. {
  3. cout << typeid(x).name() << endl;
  4. }

注意,括号的使用,

若不然,表示x是数组,元素是 int&

如果希望元素个数==泛定==怎么办呢?

我们可以这样考虑,在编译的时候,肯定是知道元素个数的,可不可以先写N,在编译时,再替换为具体的数字呢?这不恰恰是模板的特征吗?

确实可以!

  1. template <int N>
  2. void f(int (&x)[N])
  3. {
  4. cout << typeid(x).name() << endl;
  5. }

在试试传入数组吧。

最佳实践

经常,我们希望 cout 在输出数组类型时,能输出有所得元素,并不想打印指针

能不能写一个通用得函数模板,对所有的数组都打印它的所有元素呢?

  1. template <class T, int N>
  2. void print_array(T (&x)[N])
  3. {
  4. for(int i=0; i<N; i++) cout << x[i] << " ";
  5. cout << endl;
  6. }
  7. int main()
  8. {
  9. int a[] = {1,2,3,4,5};
  10. char* b[] = {"abc", "hah..", "123"};
  11. print_array(a);
  12. print_array(b);
  13. return 0;
  14. }

实时效果反馈

1. 关于数组和指向它第一个元素的指针, 说法正确的是:__

  • A 该数组与指针是同一个类型

  • B 这是两个完全不同的类型,不能自动转换

  • C 指针类型可以自动转为数组类型

  • D 数组类型可以自动转为指针类型

2. template <类型列表>,语法==错误==的是:__

  • A template <>

  • B template <typename T>

  • C template <typename T, int 3>

  • D template <typename T1, int N>

答案

1=>D 2=>C

类模板

image-20220223114328582

我们在前边定义过 Point 类型,包含 x,y 坐标

  1. struct Point{
  2. int x;
  3. int y;
  4. };

但,x, y 类型有几种可能: int, double, long long 甚至 char 都是选择。

我们不希望对类似的代码定义许多次,仅仅是 x y 类型上有差异,其它都一样或类似。

类模板就是用来解决这个问题的。

语法上,与函数模板类似,以 template 来开启模板的定义:

  1. template <typename T>
  2. struct Point{
  3. Point(T& a, T& b):x(a),y(b){ }
  4. friend ostream& operator<< (ostream& out, Point<T>& obj){
  5. out << "(" << obj.x << "," << obj.y << ") ";
  6. return out;
  7. }
  8. T x;
  9. T y;
  10. };

调用方式:

  1. Point<int> a(1,2);
  2. Point<double> b(2.3, 4);
  3. cout << a << endl;
  4. cout << b << endl;

注意,这里的 operator<< 并非是成员函数,它是模板函数,

而且,它一般是定义在类外的。

但此处,如果挪到类外:

  1. template <typename T>
  2. ostream& operator<< (ostream& out, Point<T>& obj){
  3. out << "(" << obj.x << "," << obj.y << ") ";
  4. return out;
  5. }

会导致编译不通过。

这是由于模板的二次编译造成的。

解决的方案,是明确告知编译器, operator<< 是个函数模板,并在类内具化。

即,增加前置声明

  1. template <typename T> class Point;
  2. template <typename T>
  3. ostream& operator<< (ostream& out, Point<T>& obj);
  4. template <typename T>
  5. struct Point{
  6. Point(T a, T b):x(a),y(b){}
  7. friend ostream& operator<< <T> (ostream& out, Point<T>& obj);
  8. T x;
  9. T y;
  10. };
  11. template <typename T>
  12. ostream& operator<< (ostream& out, Point<T>& obj){
  13. out << "(" << obj.x << "," << obj.y << ") ";
  14. return out;
  15. }

一般工程规范情况下,要划分 .h 文件和 .cpp 文件。

划分职责如下:

头文件主体内容:

  1. ///////// .h
  2. template <typename T> class Point;
  3. template <typename T>
  4. ostream& operator<< (ostream& out, Point<T>& obj);
  5. template <typename T>
  6. struct Point{
  7. Point(T a, T b):x(a),y(b){}
  8. friend ostream& operator<< <T> (ostream& out, Point<T>& obj);
  9. double distance(Point<T>& that);
  10. T x;
  11. T y;
  12. };

.cpp 主体内容:

  1. ////////// .cpp
  2. template <typename T>
  3. ostream& operator<< (ostream& out, Point<T>& obj){
  4. out << "(" << obj.x << "," << obj.y << ") ";
  5. return out;
  6. }
  7. template <typename T>
  8. inline SQ(T x) { return x * x; }
  9. template <typename T>
  10. double Point<T>::distance(Point<T>& t)
  11. {
  12. return sqrt(SQ(t.x-x) + SQ(t.x-x));
  13. }

使用场景

  1. int main()
  2. {
  3. Point<int> a(1,2);
  4. Point<int> b(3,4);
  5. Point<double> c(2.3, 4);
  6. cout << a << b << c<< endl;
  7. cout << a.distance(b) << endl;
  8. return 0;
  9. }

实时效果反馈

1. 关于类模板, 说法正确的是:__

  • A 类模板在实例化时,可以根据参数自动推断泛定类型

  • B 类模板实例化时,需要用<>中指定具化类型

  • C 与宏类似,类模板在编译之前进行替换展开

  • D 类模板只能有1个模板参数

2. 当在类模板内声明友元函数,类外去定义,说法错误的是:__

  • A 类模板和友元函数模板都要前置声明

  • B 类内声明要对友元函数模板具化

  • C 该友元函数一定实现为函数模板形式

  • D 该友元函数不能重载

答案

1=>B 2=>D

栈的数组实现

前边的课程中,我们实现过简单栈,但元素的类型是定死的。

现在有了模板,我们可以类型灵活起来,顺带也让数组大小灵活起来

还记得我们之前实现的支持任意类型的栈吗?

那个任意是有代价的。

image-20220223162708264

  • 通过 IObj* 访问 User 对象的成员是多态访问,不是直接编译为确定函数的调用

  • MyStack 与 User 数据间是聚合关系,不是组合。

    栈的数组中只保存对象指针,而不是对象本身,内存多次 new,效率不高

  • 如果用户的数据,有些是局部的,有些在堆中,管理将十分麻烦。

有了类模板,我们可以在不损失性能的前提下,实现类型无关。

  1. template <typename T>
  2. struct IStack{
  3. virtual ~IStack() {}
  4. virtual IStack& push(const T&) = 0;
  5. virtual T pop() = 0;
  6. virtual bool empty() = 0;
  7. };

c++中,接口并不特殊,当然可以是模板类型

其它类可以继承这个模板,可以普通类,也可以是类模板。

但,无论如何,在继承时,都需要对 IStack进行具化处理。

  1. template <typename T>
  2. class MyStack:public IStack<T>{
  3. public:
  4. MyStack() { n = 0; }
  5. virtual MyStack& push(const T& x) override{
  6. if(n>=100) throw "stack overflow";
  7. data[n++] = x;
  8. return *this;
  9. }
  10. virtual T pop() override{
  11. if(n==0) throw "empty pop!";
  12. return data[--n];
  13. }
  14. virtual bool empty() override{
  15. return n == 0;
  16. }
  17. private:
  18. T data[100];
  19. int n;
  20. };

这个类模板还有点小缺陷,数组的大小顶死了,其实可以当模板参数的。

在用户创建实例的时候指定这个参数:

  1. template <typename T, int N>
  2. class MyStack:public IStack<T>{
  3. public:
  4. MyStack() { n = 0; }
  5. virtual MyStack& push(const T& x) override{
  6. if(n>=N) throw "stack overflow";
  7. data[n++] = x;
  8. return *this;
  9. }
  10. virtual T pop() override{
  11. if(n==0) throw "empty pop!";
  12. return data[--n];
  13. }
  14. virtual bool empty() override{
  15. return n == 0;
  16. }
  17. private:
  18. T data[N];
  19. int n;
  20. };

当然,调用处要多指定一个具化参数:

  1. MyStack<Point,200> a;

实时效果反馈

1. 关于类模板, 说法正确的是:__

  • A 含有虚函数的类不能做成类模板

  • B 接口不能是类模板

  • C 类模板无法被普通的类继承

  • D 类模板可以指定多个泛型参数

2. 关于类模板的继承,==错误==的说法是:__

  • A 继承的类不一定是类模板,也可以是普通类

  • B 使用类模板继承时,子类的模板参数个数必须与父类模板相同

  • C 继承时,需要对父类模板具化处理。

  • D 类模板的成员函数,如果定义在类外,一定是函数模板

答案

1=>D 2=>C

类模板特化

image-20220225091139589

与函数模板的情况类似,类模板有时也不能完全兼容所有泛定类型的处理。

这时,可以固定全部或部分的类型,来实现一个特殊的版本。

  1. template <typename T1, typename T2>
  2. class MyA{
  3. public:
  4. MyA(const T1& a, const T2& b){
  5. cout << "MyA(T1, T2)" << endl;
  6. }
  7. void f1() { cout << "f1" << endl; }
  8. void f2() { cout << "f2" << endl; }
  9. };
  10. // MyA 模板的特化版,如需要,必须重新定义所有函数,或添加新函数
  11. template<>
  12. class MyA<int, int>{
  13. public:
  14. MyA(const int& a, const int& b){
  15. cout << "MyA(int, int)" << endl;
  16. }
  17. ...
  18. };

调用:

  1. MyA<int, double> a(1, 1.0);
  2. MyA<int, int> b(1, 1);
  3. b.f1(); // 错误,特化版中没有这个函数

如果我们的特化版本仅仅是某个局部有不同处理,大部分都相同,那不如用继承:

  1. // 可以重用 MyA 模板的内容
  2. struct MyB: public MyA<double,double>{
  3. MyB(int x, int y):MyA(x,y){}
  4. };

调用:

  1. MyB c(2.0,2.0);
  2. c.f1();

特化的花样

特化能做出很多花样来,前边的例子是全特化

  • 全特化

    1. template <>
    2. class MyA<int, int> { // 这里所有参数都固定下来
    3. ....
    4. }
  • 部分特化

    1. template <typename T> // 列出剩余没有固定的类型
    2. class MyA<T, int> { // 只固定部分类型
    3. ...
    4. }
  • 偏特化

    对类型施加某些限制

    1. template <typename T1, typename T2>
    2. class MyA<T1*, T2*>{ // 限定了泛型必须为指针类型
    3. ...
    4. }

    再比如:

    1. template <typename T>
    2. class MyA<T, T>{ // 限定了两个泛型必须是相同的类型
    3. ...
    4. }

    如果某个调用同时匹配多个特化,就会产生二义性。

    可以针对特殊情况定义相应的全特化版。

与函数模板特化的比较

  • 函数模板只能全特化

  • 函数可以重载,类无法重载,同名的类会冲突

    1. namespace my{
    2. template <typename T1, typename T2>
    3. bool eq(const T1& a, const T2& b){
    4. return a == b;
    5. }
    6. template <typename T>
    7. bool eq(double a, const T& b){
    8. return fabs(a-b) < 0.0001;
    9. }
    10. template <typename T>
    11. bool eq(const T& a, double b){
    12. return fabs(a-b) < 0.0001;
    13. }
    14. }
    15. int main()
    16. {
    17. cout << my::eq(1.000001, 1) << endl;
    18. cout << my::eq(1, 0.999999) << endl;
    19. //cout << my::eq(1.000001, 1.0001) << endl;
    20. return 0;
    21. }
  • 工程实践中,建议尽量优先使用函数模板特化、类模板特化,

    无法满足要求时,才使用函数重载、定义新的类

实时效果反馈

1. 关于模板特化, 说法正确的是:__

  • A 函数模板支持全特化和部分特化

  • B 类模板支持全特化和部分特化

  • C 函数模板只能部分特化

  • D 类模板只能部分特化

2. 当类模板对某个泛型的参数需要轻微修正函数行为,一般怎么处理?:__

  • A 考虑继承类模板,重载其中不满足要求的函数

  • B 写一个普通类,重载类模板

  • C 写一个全新的类

  • D 使用条件编译,分别处理

答案

1=>B 2=>A

队列模板

image-20220225111932904

前边的学习中,实现过循环队列,是用数组实现的。

并且,也可以通过指针泛化来支持任意的用户定义类型。

但泛型版本更有效率上的优势。

本节利用模板知识,实现一个单链表的队列,其中的元素类型是泛定的。

队列容量没有限制。

原理

image-20220225100104302

Que 含有两个节点指针,front 指向队头,back 指向队尾。

节点 Node 是实现机制,可以对外隐藏,实现为内部类。

Node 包装了用户类型 T data

实现

  1. template <typename T>
  2. class Que{
  3. public:
  4. Que() { front = back = NULL; }
  5. ~Que() {
  6. while(!empty()){ cout << "debug: " << remove() << endl; }
  7. }
  8. Que& add(const T& x);
  9. T remove();
  10. bool empty() { return front==NULL; }
  11. private:
  12. class Node{
  13. public:
  14. Node(const T& x){ data = x; next=NULL; }
  15. T data;
  16. Node* next;
  17. };
  18. private:
  19. Node* front;
  20. Node* back;
  21. };

节点类是队列内部使用的,不需要暴露给用户,所以是private

需要注意的是, class Node 并没有定义为模板类,是因为Que在具化的时候,就会一并具化 class Node,因而就消除了不确定的类型。

类外的实现:

  1. template <typename T>
  2. Que<T>& Que<T>::add(const T& x)
  3. {
  4. Node* p = new Node(x);
  5. if(empty())
  6. front = p;
  7. else
  8. back->next = p;
  9. back = p;
  10. return *this;
  11. }
  12. template <typename T>
  13. T Que<T>::remove()
  14. {
  15. if(empty()) throw -1;
  16. Node* p = front;
  17. front = front->next;
  18. if(front==NULL) back = NULL;
  19. T rt = p->data;
  20. delete p;
  21. return rt;
  22. }

这些都需要实现为函数模板,并且其中引用类的时候,要具化。

注意:析构的重要性,否则会产生内存泄漏

调用处:

  1. Que<int> a;
  2. a.add(1).add(2).add(3);
  3. while(!a.empty()) cout << a.remove() << endl;
  4. a.add(4).add(5);

模板对宏的比较优势

模板泛型体系是c++较晚才引入的特性。

此前,通过有参宏替换来实现。

其问题是:

  • 宏展开的时候,无法语法追踪,写法太灵活。
  • 宏替换在编译时,已经确定为具体的类型,无法让两个泛型实例并存。

实时效果反馈

1. 关于队列, 说法正确的是:__

  • A 队列是后进先出的线性结构

  • B 队列的实现可以是链表,数组,动态数组,块链或者更复杂的结构

  • C 泛型队列模板与指针泛化的方案比起来,主要优势是适用更多类型

  • D 两个不同用户类型的 Que 无法并存

2. 对于本节Que的实现,当物理内存耗尽,表现是:__

  • A 程序行为不确定,与内存随机数据有关

  • B 栈溢出错误

  • C 会抛出异常,程序终止

  • D add新数据没有效果

答案

1=>B 2=>C

traits 技术

image-20220228094405719

我们在适用函数模板,类模板编程的时候,很快就会发现,一个模板代码很难尽善尽美,

经常会出现特殊情况。即,对某个类型,或某种类型要特殊处理。

  • 使用类模板的特化如何?

    只修改一点点,大部分都要抄下来,违背了 don’t repeat 原则

  • if ( T 是某某类型) 则: 。。。。

    这个如果是动态判断,消耗运行时处理器时间,有时,效率很敏感。

其实,我们可能也希望写 if( T 是某种类型 ) 。。。。 但,我们希望这个 if 能在编译时确定好,不用运行时现场断定类型,也就是所说的:静态反射机制

traits, 翻译为:特性,或特质,按c++之父的说法:

Think of a trait as a ==small object== whose main purpose is to ==carry information== used by another object or algorithm to determine “policy” or “implementation details”.

可以看到,关键字就是:携带类型信息,服务于其它类或函数。

而,这个类型信息是编译时的,不是运行时的。

示例

  1. template <typename T, bool isPointer>
  2. class Test{
  3. public:
  4. void f() {
  5. if(isPointer) cout<< "POINTER "; // 类型不同特殊处理
  6. cout << "do somthing..." << endl;
  7. }
  8. };
  9. int main()
  10. {
  11. Test<int,false>().f();
  12. Test<int*, true>().f();
  13. return 0;
  14. }

当类型 T 为指针时,处理逻辑上轻微调整。

如果,用 if 实现分支处理,则需要传入一个 bool信息

客户会奇怪,因为这本应该与他无关。

实际上,这个bool信息显然可以在编译过程中,从T提取,方法:

  1. template <typename T>
  2. struct TestHelper{
  3. static const bool isPointer = false;
  4. };
  5. template <typename T>
  6. struct TestHelper<T*>{
  7. static const bool isPointer = true;
  8. };
  9. template <typename T>
  10. class Test{
  11. public:
  12. void f() {
  13. if(TestHelper<T>::isPointer) cout<< "POINTER ";
  14. cout << "do somthing..." << endl;
  15. }
  16. };
  17. int main()
  18. {
  19. Test<int>().f();
  20. Test<int*>().f();
  21. return 0;
  22. }

经过这样处理,isPointer 这个逻辑就被封装在Test中

示例2

比如,我们开始有这样的类模板,

  1. template <typename T>
  2. class Test{
  3. public:
  4. T f(T x);
  5. private:
  6. T data;
  7. };

后来,需求变更了,要求:

  • 当 T 为指针时,传入为 int, 传出为 int*
  • 当 T 为 int 时,传入为 int, 传出为 double

这时,就可以用trait技术了,

我们可以用trait辅助类封装原来的输入值和输出值的类型:

  1. template <typename T>
  2. struct mytrait{
  3. typedef T ret_t;
  4. typedef T par_t;
  5. };
  6. template <typename T>
  7. struct mytrait<T*>{
  8. typedef int* ret_t;
  9. typedef int par_t;
  10. };
  11. template <>
  12. struct mytrait<int>{
  13. typedef double ret_t;
  14. typedef int par_t;
  15. };
  16. template <typename T>
  17. class Test{
  18. public:
  19. typename mytrait<T>::ret_t f(typename mytrait<T>::par_t x);
  20. private:
  21. T data;
  22. };

注意: mytrait::ret_t 之前的 typename,这是因为,c++编译器默认::限定的东西是个value,而非type。

为了避免冗长,可以把这个长长的类型再次typedef

  1. template <typename T>
  2. class Test{
  3. public:
  4. typedef typename mytrait<T>::ret_t ret_t;
  5. typedef typename mytrait<T>::par_t par_t;
  6. ret_t f(par_t x);
  7. private:
  8. T data;
  9. };

这种技巧很实用,因为 ret_t 很可能在程序中多处被引用。

实时效果反馈

1. 关于traits, 说法正确的是:__

  • A traits 是一个复杂的函数模板,负责分发请求

  • B traits 一般是一个很小的类模板,封装与泛定类型相关的类型信息

  • C traits 是一个个独立的接口,普通类可以继承多个trait

  • D traits 是一些重载函数

2. 使用traits封装后,间接地使用类型,其好处是:__

  • A 隔离不同类型间的差异,使得调用有统一的形式

  • B 对冗长的类型重定义,使得表达更短小

  • C 对特殊的类型更友好,使得其操作快速

  • D 对指针类型重定义,区别对待

答案

1=>B 2=>A

迭代器

image-20220228103919820

迭代器是重要的抽象,它提供遍历容器中元素的方法,却要求与容器的物理实现无关。

首先考虑一个简单的数组 int [N],我们如何遍历?

显然,最容易想到的方法,一个 int* 指针,指向第一个元素,

一直往后走,直到数组尾部。

  1. int a[N] = ....;
  2. int* p = a;
  3. int* end = a + N;
  4. while(p != end) cout << *p << endl;

那么,如果容器并非数组,而是链表呢?块链呢?

能统一地、无差别地对待吗?

这时,我们需要一个智能指针类型,表面上,感觉就是一个指针,支持:

p++, *p, p != end 这些操作。

我们只需要重载这些运算符就好。

示例

使用动态数组时:

  1. class MyArray{
  2. public:
  3. MyArray(){ size=10; data = new int[size]; n=0; }
  4. ~MyArray() { delete [] data; }
  5. void add(int x){
  6. if(n==size){
  7. int* old = data;
  8. data = new int[size * 2];
  9. memcpy(data, old, size * sizeof(int));
  10. delete [] old;
  11. size *= 2;
  12. }
  13. data[n++] = x;
  14. }
  15. private:
  16. int size; // 初始大小
  17. int n; // 已有元素个数
  18. int* data;
  19. };

使用块链结构时:

  1. class MyBlock{
  2. public:
  3. MyBlock() { tail = head = new Node(); }
  4. ~MyBlock() {
  5. while(head){
  6. Node* p = head;
  7. delete [] p;
  8. head = head->next;
  9. }
  10. }
  11. void add(int x){
  12. if(tail->n == BLOCK_SIZE) {
  13. Node* p = new Node();
  14. tail->next = p;
  15. tail = p;
  16. }
  17. tail->data[tail->n++] = x;
  18. }
  19. private:
  20. static const int BLOCK_SIZE = 10;
  21. struct Node{
  22. Node() { n=0; next=NULL; }
  23. int data[BLOCK_SIZE];
  24. int n;
  25. Node* next;
  26. };
  27. private:
  28. Node* head;
  29. Node* tail;
  30. };

统一的显示操作:

分别为这两个类增加一个内部 Iter 类型,可以理解为智能指针。

  1. class MyArray{
  2. public:
  3. MyArray(){ size=10; data = new int[size]; n=0; }
  4. ~MyArray() { delete [] data; }
  5. void add(int x){ .... }
  6. public:
  7. typedef int* Iter; // 迭代器就是 int* 本身
  8. Iter begin() { return data; }
  9. Iter end() { return data + n; }
  10. private:
  11. int size; // 初始大小
  12. int n; // 已有元素个数
  13. int* data;
  14. };

MyBlock 的Iter就要复杂了:

  1. class MyBlock{
  2. public:
  3. MyBlock() { tail = head = new Node(); }
  4. ~MyBlock() { .... }
  5. void add(int x){ .... }
  6. public:
  7. struct Node;
  8. class Iter{
  9. public:
  10. Iter(Node* it, int n){
  11. this->it = it;
  12. this->p = it->data + n;
  13. }
  14. int operator* () { return *p; }
  15. void operator++(int) {
  16. p++;
  17. if(p==it->data + it->n && it->next){
  18. it = it->next;
  19. p = it->data;
  20. }
  21. }
  22. bool operator!= (const Iter& that){
  23. if(this->it != that.it) return true;
  24. if(this->p != that.p) return true;
  25. return false;
  26. }
  27. private:
  28. Node* it; //当前块
  29. int* p; // 当前位置
  30. };
  31. Iter begin() { return Iter(head, 0); }
  32. Iter end() { return Iter(tail, tail->n); }
  33. public:
  34. static const int BLOCK_SIZE = 10;
  35. struct Node{
  36. Node() { n=0; next=NULL; }
  37. int data[BLOCK_SIZE];
  38. int n;
  39. Node* next;
  40. };
  41. private:
  42. Node* head;
  43. Node* tail;
  44. };

使用:

  1. MyArray a;
  2. MyBlock b;
  3. for(int i=0; i<50; i++){
  4. a.add(i);
  5. b.add(i);
  6. }
  7. for(MyArray::Iter i=a.begin(); i!=a.end(); i++)
  8. cout << "a: " << *i << endl;
  9. for(MyBlock::Iter i=b.begin(); i!=b.end(); i++)
  10. cout << "b: " << *i << endl;

使用函数模板统一:

  1. template <typename T>
  2. void display(T& x){
  3. for(auto i=x.begin(); i!=x.end(); i++)
  4. cout << *i << " ";
  5. cout << endl;
  6. }
  7. int main()
  8. {
  9. MyArray a;
  10. MyBlock b;
  11. for(int i=0; i<50; i++){
  12. a.add(i);
  13. b.add(i);
  14. }
  15. display(a);
  16. display(b);
  17. return 0;
  18. }

实时效果反馈

1. 关于迭代器, 说法正确的是:__

  • A 迭代器就是指向容器中元素的指针

  • B 迭代器的目的是能更方便地访问容器中的元素

  • C 迭代器的目的是能更高效地访问容器中的元素

  • D 迭代器的目的是能更统一地访问容器中的元素,与容器类型无关

2. 当迭代器p不是原生指针,要重载运算符,一般哪个==不==需要重载?__

  • A p++

  • B *p

  • C p != q

  • D p[5]

答案

1=>D 2=>D

STL的迭代器

image-20220228161750336

STL中提供了丰富的容器类型。

为了用统一的方式,访问这些容器中的元素,使得算法与类型隔离,与容器的物理结构隔离,广泛使用了迭代器的概念。

最常见的表现形式:

  1. for(auto i=容器.begin(); i!=容器.end(); ++i){
  2. // 对 *i 进行操作
  3. }

常见的迭代器有以下几种类型:

章节三:信息学竞赛 CPP 进阶篇 - 图16

这个图画的不是继承关系,只是表达越往后边的迭代器功能越多。

比如, p 是forward迭代器,则支持: ++p, 但不支持 —p,而双向迭代器就可以。

迭代器功能

p,p1 是迭代器对象,则:

  • 所有迭代器:

    | 运算 | 含义 | | —— | —————————— | | p++ | 自增迭代,返回增前值 | | ++p | 自增迭代,返回之后值 |

  • 输入迭代器

    | 运算 | 含义 | | ———- | ——————————— | | xx = *p | 读取迭代器位置的元素值 | | p = p1 | 迭代器赋值 | | p == p1 | 迭代器比较 | | p != p1 | 迭代器比较 |

  • 输出迭代器

    | 运算 | 含义 | | ———— | ———————— | | *p = xxx | 向迭代器位置写入 | | p = p1 | 迭代器赋值 |

  • 正向迭代器 = 提供所有输入、输出迭代器的功能

  • 双向迭代器(提供正向所有功能)

运算 含义
—p 前置递减,返回减前的值
p— 后置递减,返回减后的值
  • 随机迭代器

    | 运算符 | 含义 | | ———- | ————————————— | | p += i | 连续前进 i 步 | | p -= i | 连续后退 i 步 | | p + i | 相对 p 位置前进 i 位置 | | p - i | 相对 p 位置后退 i 位置 | | p[i] | 相对 p 偏移 i 位置的元素值 | | p < p1 | p 在 p1 之前 | | p <= p1 | p 在 p1 之前,或相等 | | p > p1 | p 在 p1 之后 | | p >= p1 | p 在 p1 之后,或相等 |

可以看出,迭代器是对指针的概念的升华。

与指针的区别

迭代器是“假”指针,表面上装作指针的样子,指针的行为,但毕竟是个对象。

比如:容器.end() 返回的是最后一个元素后边那个位置,称为:尾后迭代器,它并非一个真正的“位置”,而 *p 也是引用一个并不存在的元素。

还有,p++ 是广义的 + ,移动方向未必是指针增量方向。比如反向遍历时,用的也是++

我们对 string 使用迭代器概念:

  1. string s = "1234567";
  2. for(auto i = s.rbegin(); i != s.rend(); ++i){
  3. cout << *i << endl;
  4. }

看到,反向的输出效果,但 ++i, 不是 —i

iterator 的编译类型

从编译器的角度看,迭代器有 4 种:

  • 容器::iterator 正向迭代器

  • 容器::const_iterator 常量正向迭代器

  • 容器::reverse_iterator 反向迭代器
  • 容器::const_reverse_iterator 常量反向迭代器

我们应该使用尽量具体的类型,以提供更多的编译保护。

比如,上边的反向遍历串种元素,如果只是显示,不需要更改,则:

  1. string s = "1234567";
  2. for(string::const_reverse_iterator i = s.crbegin();
  3. i != s.crend(); ++i){
  4. cout << *i << endl;
  5. }

STL各种容器类型种的迭代器

这里列举主要的STL容器特点:

容器 容器结构 支持迭代器了类型
vector 动态数组,提供可以扩容的连续存储空间 random access
deque 数组类型,可以快速在两端进行插入删除,可按需扩容 random access
list 链表,不支持随机访问,插入、删除快速 bidirection
set 快速判定元素是否在容器种关键字=元素值 bidirection
multiset 与 set 类似,但关键字可以包含重复元素 bidirection
map 键值对儿的容器,根据键可以高效地查找值 bidirection
multimap 类似map,但不同的是,每个关键字可以关联多个值 bidirection

实时效果反馈

1. 如果我们定义单链表,则它的迭代器应该是:__

  • A 前向迭代器

  • B 双向迭代器

  • C 随机迭代器

  • D 输入迭代器

2. set类型容器为什么不提供随机迭代器?:__

  • A set 是哈希表实现的,随机没有线性的概念

  • B 为了快速判断元素是否存在,元素不是按某种顺序规律存储,随机访问会很慢

  • C set 是链表,不方便提供随机访问

  • D 为了安全性不提供

答案

1=>A 2=>B

函数对象

章节三:信息学竞赛 CPP 进阶篇 - 图17

一个对象可以伪装为函数。

  • 对象(参数,…) 表面上看好像函数调用
  • 一个需要函数指针的地方,可以传入与之类型匹配的函数对象
  • 可以通过重载 operator() 运算符来实现
  1. struct MyF{
  2. int operator() (int a, int b) { return a * 10 + b; }
  3. };

调用方法:

  1. MyF a;
  2. cout << a(2,3) << endl;

明明可以用普通的函数,现在搬出个对象来有什么用?

答曰:对象可以持有状态

比如,我们要多次调用一个 add(int x),最后获得累加值,该如何处理呢?

如果用普通函数,就必须借助 static 或全局变量:

  1. int g_a = 0;
  2. void add(int x)
  3. {
  4. g_a += x;
  5. }

调用:

  1. g_a = 0;
  2. for(int i=0; i<10; i++) add(i);
  3. cout << g_a << endl;

这个方案的弱点是:

  • 引入全局变量,终究是个隐患
  • 多个线程中执行add过程,将引发灾难

如果改用函数对象,对象可以持有自己的状态,就能避免全局变量与线程竞争问题。

函数对象实现add

  1. class MyF{
  2. public:
  3. MyF():sum(0){}
  4. void operator() (int x) { sum += x; }
  5. int get_sum() { return sum; }
  6. private:
  7. int sum;
  8. };

调用:

  1. MyF a;
  2. for(int i=0; i<10; i++) a(i);
  3. cout << a.get_sum() << endl;

让一个函数持有状态,计算过程与状态有关,这是现代高级语言中的函数闭包

c++的函数对象与函数闭包比较,有更强大的能力和可塑性。

与STL协作

STL的核心思想是算法与容器类型分离

有些算法,需要传入用户自己定义的处理过程,即函数指针,此时如果需要持有状态,就可以用函数对象来代替之。

比如:for_each,它需要传入2个迭代器,以及一个函数

  1. vector<int> v;
  2. v.push_back(1);
  3. v.push_back(2);
  4. v.push_back(3);
  5. MyF a = for_each(v.begin(), v.end(), MyF());
  6. cout << a.get_sum() << endl;

注意,需要 include

  1. #include <algorithm>
  2. #include <vector>

实时效果反馈

1. 怎样的类,才能生成函数对象:__

  • A 类中重载了 operator() 运算符

  • B 类中定义了函数指针

  • C 类中定义了函数模板

  • D 类中定义了特殊函数

2. 函数对象,与普通函数比,有何优势?__

  • A 能够支持任意多的参数

  • B 能够指定默认值

  • C 能够更好地重载

  • D 能使用局部变量来持有状态

答案

1=>A 2=>D

STL中的函数对象

image-20220302102250006

根据函数输入参数数目的不同,STL中常用到的函数对象:

  • 发生器

    没有任何参数,返回值为某个类型。比如:随机数产生器

  • 一元函数

    一个输入参数,返回值类型可能与输入参数不同

  • 二元函数

    两个输入参数,返回值可能与输入参数不同

  • 一元判定函数(一元谓词)

    返回值是 bool 的一元函数

  • 二元判定函数(二元谓词)

    返回值是 bool 的二元函数

STL算法的通用性

STL算法希望能够通用,它采用:

  • 模板定义,与具体的类型隔离开,同时又不损失运行期效率。
  • 函数对象,与具体的行为细节隔离开,可以接受函数对象或者函数指针
  1. int add(int a, int b){ return a + b; }
  2. struct MyF{
  3. int operator() (int a, int b){ return a*b; }
  4. };
  5. int main()
  6. {
  7. int a[] = {1,2,3,4,5,6,7,8,9};
  8. const int N = sizeof(a)/sizeof(int);
  9. cout << accumulate(a, a+N, 0, add) << endl;
  10. cout << accumulate(a, a+N, 1, MyF()) << endl;
  11. return 0;
  12. }

注意,需要 #include

accumulate 算法是通用的,它工作在迭代器上,与容器类型无关。

它把元素逐个==累积==起来,至于如何累积,需要用户自己传入一个函数来决定。

image-20220301151157005

这有些类似于,函数式编程语言中的:reduce

再举个一元谓词的例子:

  1. inline bool f(char x) { return x>='0' && x<='9'; }
  1. int main()
  2. {
  3. string s = "1234abcd5678xyz";
  4. cout << count_if(s.cbegin(), s.cend(), f) << endl;
  5. return 0;
  6. }

STL内建函数对象

STL提供了大量的内建函数对象,都是模板。

主要包含算数运算,关系运算,逻辑运算三大类型。

常用的列举如下:

名称 类型 行为描述
plus 二元 加法: arg1 + arg2
minus 二元 减法:arg1 - arg2
multiples 二元 乘法:arg1 * arg2
divides 二元 除法: arg1 / arg2
modules 二元 模取: arg1 % arg2
negate 一元 取负值:-arg1
equal_to 二元 等于:arg1 == arg2
not_equal_to 二元 不等于:arg1 != arg2
greater 二元 大于:arg1 > arg2
greater_equal 二元 大于等于:arg1 >= arg2
less 二元 小于: arg1 < arg2
less_equal 二元 小于等于:arg1 <= arg2
logical_and 二元 逻辑与:arg1 && arg2
logical_or 二元 逻辑或:arg1 \ \ arg2
logical_not 一元 逻辑非:!arg1

还是前边例子,如果用内置函数对象,就是:

  1. int a[] = {1,2,3,4,5,6,7,8,9};
  2. const int N = sizeof(a)/sizeof(int);
  3. cout << accumulate(a, a+N, 1, multiplies<int>()) << endl;

再举个排序的例子:

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <iterator> // ostream_iterator
  4. using namespace std;
  5. template <typename T, int N>
  6. void mysort(T(&a)[N])
  7. {
  8. sort(a, a+N, greater<T>());
  9. }
  10. template <typename T, int N>
  11. void disp(T (&a)[N])
  12. {
  13. copy(a, a+N, ostream_iterator<T>(cout, " "));
  14. cout << endl;
  15. }
  16. int main()
  17. {
  18. int a[] = { 15, 22, 13, 28, 3, 16, 4, 27, 11 };
  19. mysort(a);
  20. disp(a);
  21. return 0;
  22. }

这里,再次看到c++泛型编程的强大威力。

copy 把源迭代器的元素值,写入到目标迭代器的位置。它并不关心目标的类型,源的类型,可以适用于各种支持迭代器的的对象。

sort 也不清楚容器的物理结构,它只负责排序算法(比如快速排序),也不管怎样去比较元素的大小,元素的比较问题就是由外部传入的函数对象管理的。

而,这个函数对象可能又转而把这个责任交给 T 类型的元素自身。

实时效果反馈

1. 关于STL与函数对象, 说法正确的是:__

  • A STL的算法参数中,默认传递函数对象的引用

  • B STL的算法参数中,默认传递函数对象的指针

  • C STL的算法参数中,默认传递函数对象的拷贝

  • D STL的算法参数中,默认传递函数对象的成员函数

2. 一元谓词,指的是哪种函数?:__

  • A 有一个参数,返回 bool 类型

  • B 有一个参数,反追指针类型

  • C 有二个参数,返回 bool 类型

  • D 有二个参数,返回相同类型

答案

1=>C 2=>A

STL通用容器

image-20220303090458219

STL主要提供了通用容器、通用算法及其辅助设施。

容器是容纳一组某种特定类型元素的复合类型。

STL容器可分为:

  • 顺序容器

    按照线性排列来存储元素

    vector deque list

  • 关联容器

    与顺序容器比,关联容器更注重高效地检索数据的能力。把元素值或它的部分作为键,来检索元素的存储位置。

    set multiset map multimap

  • 适配器

    对已有的容器进行某种封装,不是真正的新容器。

    stack queue

所有容器的共性

  • 所有的容器都是值语义。赋值会引起元素的复制。
  • 默认的构造会建造一个空容器
  • 支持关系运算符:==,!=, <, <=, >, >=
  • begin(), end() 获得容器首、尾后迭代器
  • clear() 将容器清空
  • empty() 判断容器是否为空
  • size() 容器中元素的个数
  • s1.swap(s2) 互换两个容器的内容
  • S::iterator, S::const_iterator 迭代器类型

不同的结构,不同的性能

以顺序容器为例:

  • vector 内部是数组,与普通数组不同,是动态增长的

    可以快速随机访问

    尾巴插入、删除最容易

    中间插入、删除与元素个数和位置有关

  • deque 内部也是数组,但采用分块数组的策略,头尾端都可以增加新块。

    可以较快速随机访问(较vector慢)

    头部、尾部 增删都容易

    中间插入删除更困难些

  • list 双向链表

    可以双向遍历,不能随机访问

    任意位置插入、删除都很容易

“坑”也通用

这里介绍一个迭代器失效问题

  1. string s = "abcdefg";
  2. for(auto i=s.begin(); i!=s.end(); ++i){
  3. cout << *i << endl;
  4. if(*i=='g') s.erase(i);
  5. }
  6. cout << s << endl;

本意是删除串中所有的 ‘g’

但,执行完 erase 后,迭代器就失效了,再调用 ++i,行为不确定

我们可以试试其它容器, vector, deque, list ….

每个容器都有很不同的反应,有些时候十分诡异!

实时效果反馈

1. 关于STL容器, 说法正确的是:__

  • A list 是关联容器

  • B vector 不可以在头部插入数据

  • C list 在任何位置插入删除都是常数时间,与元素个数无关

  • D deque 不支持随机访问元素

2. 对于STL容器,容器.end(),含义是:__

  • A 取得反向迭代器

  • B 取得正向迭代的尾后迭代器

  • C 取得最后一个元素的指针

  • D 取得指向最尾元素的迭代器

答案

1=>C 2=>B

vector

image-20220303122047999

c 的原生数组创建后大小就不可改变。堆中开数组,容易忘记释放。

vector 可以理解为动态数组。它在需要的时候,可以自动扩容。

比较优势:尾部的添加、删除;随机访问。

注意,以下代码都需要头文件:#include

初始化

  1. int x[] = {10,20,30};
  2. vector<int> a; // size为 0
  3. vector<int> b(5); // 指定初始大小,元素随机值
  4. vector<int> c(5,9); // 5个元素,每个都是9
  5. vector<int> d(c); // 拷贝构造
  6. vector<int> e(d.begin(), d.end()); //重要,通过另一个容器构造自己
  7. vector<int> f(x,x+3); //真假指针通吃

读取元素的方法

  • v[n] 下标访问,可读可写

    v[2] = v[3] + 10;

  • front, back

    v[0] v[v.size()-1] 同样

  • 迭代器访问,读取时尽量用 const_iterator

    1. vector<int> a(5,9);
    2. for(auto i=a.cbegin(); i!=a.cend(); ++i){
    3. cout << *i << endl;
    4. }

常用操作

assign 类似于初始化,不过是对象已经存在后,重新赋值

  1. vector<int> a;
  2. a.assign(5,9);
  3. cout << a[4] << endl;

push_back, pop_back 在尾部可以高效地操作

注意,由于效率问题,没有对应的 push_front, pop_front

  1. vector<int> a;
  2. a.push_back(10);
  3. a.push_back(20);
  4. a.push_back(30);
  5. a.pop_back();
  6. cout << a.back() << endl;

empty, clear 判断是否为空,清空vector中元素(不释放空间)

erase 删除一个元素,或一个区间内的元素。参数是迭代器。

  1. vector<int> a;
  2. for(int i=0; i<10; i++) a.push_back(i*10);
  3. a.erase(a.begin());
  4. a.erase(a.begin()+3,a.begin()+5);
  5. for(int i=0; i<a.size(); i++) cout << a[i] << " ";
  6. cout << endl;

insert 任意位置插入一个元素,或者插入一个迭代区间

  1. vector<int> a(5,9);
  2. int b[] = {1,2,3,4,5};
  3. a.insert(a.begin(), 100);
  4. a.insert(a.begin()+1, b+2,b+5);
  5. for(int i=0; i<a.size(); i++) cout << a[i] << " ";
  6. cout << endl;

整体比较 ==,!=,>=,>,<=,<

与算法的配合

需要头文件:#include

sort 排序

  1. string s = "sothat";
  2. vector<char> a(s.begin(), s.end());
  3. sort(a.begin(), a.end());
  4. copy(a.begin(), a.end(), ostream_iterator<char>(cout," "));
  5. cout << endl;

copy 从一个迭代区间拷贝到另一个迭代器

find 在迭代区间查找

reverse 迭代区间元素反转

实时效果反馈

1. 关于vector, 说法正确的是:__

  • A vector 内部用数组实现,可以根据需要扩大

  • B vector 内部用多个数据块拼接

  • C vector 在头部和尾部添加比较高效

  • D vector 随机访问效率随着元素增加而降低

2. 如果我们想显示vector中所有元素,又不想写循环,怎样最简便?__

  • A 使用 printf

  • B 使用 copy 算法

  • C 重载 operator<<

  • D 使用递归函数

答案

1=>A 2=>B

vector(2)

image-20220303134851024

vector 内部是用数组实现的。

当空间不够时,会申请更大的空间。怎样去观察这个空间的大小?

size 与 capacity 区别

  1. vector<int> a(10);
  2. a.pop_back();
  3. cout << a.size() << endl; // 9
  4. cout << a.capacity() << endl; // 可能是10
  5. cout << a.max_size() << endl; // 很大的数

如果希望预留大空间,不希望多次“搬家”

可以用 v.reserve(999999); 预留一个大空间

vector的内存策略,只增大,删除或clear都不会自动释放空间,析构时自动释放

如何释放多余的空间?可以新建vector,拷贝老数据出去。

c++11 标准中新增加了 shrink_to_fit 方法来缩小多余的空间。

但此调用仅仅是对编译器发出请求,并没有强制性。

自定义类型

当心,使用自定义类型,有些编译器需要全局定义。

如果,希望容器比较,自己的类型要可以比较 ,重载 operator<

  1. struct Point{
  2. int x;
  3. int y;
  4. bool operator< (const Point& t) const { return x < t.x; }
  5. friend ostream& operator<< (ostream& os, const Point& t)
  6. {
  7. os << "(" << t.x << "," << t.y << ")";
  8. return os;
  9. }
  10. };

调用处:

  1. vector<Point> a;
  2. a.push_back(Point{2,5});
  3. a.push_back(Point{3,1});
  4. a.push_back(Point{1,8});
  5. sort(a.begin(), a.end()); // 需要用户类 operator<
  6. copy(a.begin(), a.end(), ostream_iterator<Point>(cout, " "));
  7. cout << endl;

at 与 [] 的区别

引用第 i 个元素,可以用:

v[i], 也可以用 v.at(i) 可以读,也可以写:

  1. v.at(5) = xxx; // 写入 v[5] = xxx;
  2. xxx = v.at(3);

at() 返回的是元素引用,所以可以赋值。

使用 at 的好处是,会检查数组是否越界,越界会抛出异常

删除大量元素的策略

在使用STL迭代器时,要牢记:删除操作后,迭代器可能失效

那么,如何处理遍历过程中的删除呢?

  1. vector<int> a;
  2. for(int i=1; i<=10; i++) a.push_back(i);
  3. for(auto i=a.begin(); i!=a.end();){
  4. if(*i % 2 == 0)
  5. a.erase(i);
  6. else
  7. ++i;
  8. }

可以看到,对vector而言,删除后,下一个元素换到当前的删除位置,因此,迭代器不需要进行 ++i 的动作了。

实时效果反馈

1. 关于vector预留空间的释放, 说法正确的是:__

  • A vector 会在clear() 后释放当前空间

  • B vector会在 size 远小于 capacity的时候,释放当前空间

  • C vector会在 size 为 0 时释放当前空间

  • D vector只会在析构时,释放当前空间

2. vector.at(i),说法正确的是:__

  • A at(i) 返回第 i 个元素的值

  • B at(i) 返回第 i 个元素的引用

  • C at(i) 当访问越界时,会返回 0

  • D at(i) 不会进行越界检查

答案

1=>D 2=>B

vector应用

image-20220304085404750

只是明白语法,了解原理,意义不大。

在实际问题中反复演练,在工程实践中不断打磨,才能真正掌握。

题目

给定一个正整数 n,拆分为多个正整数的和,有多少拆分方法? n = 6 5 + 1, 4 + 2, 3 + 3 4 + 1 + 1, 3 + 2 + 1 1 + 1 + 1 + 1 + 1 + 1 等都是合适的拆分。 因为加法的交换律,不同次序的拆分算同一种。 比如:3 + 2 + 1 与 2 + 1 + 3 是同一种 题目的要求是,根据 n,列出所有的拆分形式。

思路

对一个大问题,可以划分为多个子问题。 如果,每个子问题解决了,把结果收集整理,即是大问题的解。

这是我们人类解决复杂问题的最基本手段。

把所有的拆分按如下划分为等价类 1 + … 2 + … 3 + … 等 也就是说: 4 + 2, 4 + 1 + 1 就应该算做同一子类

接下来,怎么解决次序无关呢? 我们不妨约定: 后边的数字不可以大于前边的数字。 也就是对 n 进行分解时,同时还增加个限高来约束。

解决方案

  1. typedef vector<int> VI;
  2. typedef vector<VI> VVI;
  3. // n: 待分解, h: 限高
  4. VVI f(int n, int h)
  5. {
  6. VVI rt;
  7. if(n==0){
  8. rt.push_back(VI());
  9. return rt;
  10. }
  11. for(int i=1; i<=n; i++){
  12. if(i>h) break;
  13. VVI t = f(n-i, i);
  14. for(auto k=t.cbegin(); k!=t.cend(); ++k){
  15. VI t2;
  16. t2.push_back(i);
  17. t2.insert(t2.end(),(*k).begin(), (*k).end());
  18. rt.push_back(t2);
  19. }
  20. }
  21. return rt;
  22. }

实时效果反馈

1. v1, v2 是vector, 如何实现 v1,v2 的拼接?__

  • A copy(v2.begin(), v2.end(), v1.end())

  • B v1.insert(v1.begin(), v2.begin(), v2.end())

  • C v1.insert(v1.end(), v2.begin(), v2.end())

  • D v1 + v2

2. 如果一个vector 的元素也是vector,如何定义?:__

  • A vector<vector<int>> x;

  • B vector<<vector>int> x;

  • C typedef vector T; vector<T> x;

  • D vector<>vector<int> x;

答案

1=>C 2=>A

deque

image-20220304113752342

deque在逻辑上是一片连续的存储区,与array, vector一样

但,存储实现上,是许多独立的“块”,deque主要任务是管理这些块的指针,使得外部看起来,是一整片空间。

image-20220304080504966

因为随机访问效率低于 vector,

当数据较多的时候做sort,可以考虑先放入 vector, sort 后再copy 回 deque

特色

操作上,大部分与 vector 相同

push_front,pop_front 有很高的效率。

具有双端队列性质的应用,用deque很合适

  1. deque<int> a = {1,2,3};
  2. a.push_front(0);
  3. a.push_front(-1);
  4. a.push_back(4);
  5. copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));

约瑟夫环问题

n个小朋友排队站一个圆圈,从1号开始报数,报到3出列,下一个人重新从1报数

求最后剩下谁?

思路:

让小朋友站一队,报1,报2的小朋友跑到队尾

报3的小朋友直接出队

最后剩下一个…

  1. deque<int> a;
  2. int N = 10;
  3. for(int i=1; i<=N; i++) a.push_back(i);
  4. while(a.size()>1){
  5. a.push_back(a.front());
  6. a.pop_front();
  7. a.push_back(a.front());
  8. a.pop_front();
  9. a.pop_front();
  10. }
  11. cout << a[0] << endl;

实时效果反馈

1. 关于STL的deque存储结构, 说法正确的是:__

  • A 双向块链存储

  • B 分块数组存储

  • C 双向链表

  • D 二叉树

2. vector 比 deque 的优势在?:__

  • A 只扩容,不收缩

  • B 在头部、尾部都可以快速增删元素

  • C 扩容时,不需要大规模拷贝元素

  • D 随机访问速度快

答案

1=>B 2=>D

array

image-20220307084618928

有的时候我们需要的仅仅是一个定长数组,并没有动态需要,用vector有点大材小用。

array基本等同于原生数组,是c++11 才引入的容器。

优点:

  • 使用 at() 访问元素可以防止数组越界的尴尬
  • 当作参数传递时,同时传递了数组的大小,不用像c语言那样,传2个参数
  • 符合容器的接口,是值语义,两个数组见复制很容易。
  • 创建多维数组更方便,直观

注意:

array完全是对原生数组的封装,只包含数据元素本身,甚至没有保存大小(由编译器记录)

array把原生数组加了浅表封装,使之可以兼容普通容器的功能。

swap 的行为是交换所有元素,而不是互换指针控制权。

示例

  1. array<int,3> a = {10,20,30};
  2. array<array<int,3>,2> b = {1,2,3,4,5,6};
  3. b[0] = a; // 数组作为值语义整体赋值
  4. cout << b[0][1] << endl;
  5. cout << b[1][2] << endl;
  6. copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout," "));

示例2

螺旋填充一个矩阵,形如:

章节三:信息学竞赛 CPP 进阶篇 - 图27

可以看到,规则就是:先向右行进,遇到已经填充的数字或是边界,则右转弯。

直到填满整个数组。

所谓:行进方向,可以用行的增量与列的增量来联合描述。

所谓改变方向也不要想复杂,因为只有4种方向,是有规律循环的。

我们把问题焦点放在:“遇到数字或出界” 这个条件上,能不能很简练地表达呢?

此处,用 array 就可以简练,

因为,所谓的“出界”就是:当抛出异常时。。。

可以让“遇到数字”也抛出异常,这样就统一了。

  1. template <int M, int N>
  2. void fill(array<array<int,N>,M>& da)
  3. {
  4. struct{ // 定义了匿名类的对象
  5. void next() { p = (p+1) % 4; } // 下一个偏移模式
  6. int i(){ return da[p][0]; } // 当前行偏移
  7. int j(){ return da[p][1]; } // 当前列偏移
  8. int da[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
  9. int p = 0; // 当前偏移模式
  10. } dr;
  11. int i=0,j=0; // 当前位置。i=行,j=列
  12. for(int k=1; k<=M*N; k++){
  13. da[i][j] = k;
  14. try{
  15. if(da.at(i+dr.i()).at(j+dr.j())>0) throw 1;
  16. }catch(...){
  17. dr.next();
  18. }
  19. i += dr.i();
  20. j += dr.j();
  21. }
  22. }

调用处:

  1. const int M = 4;
  2. const int N = 5;
  3. array<array<int, N>,M> x;
  4. // 清零
  5. for(int i=0; i<x.size(); i++) x[i].fill(0);
  6. // 螺旋填充
  7. fill<M,N>(x);
  8. // 结果显示
  9. for(int i=0; i<x.size(); i++){
  10. for(int j=0; j<x[i].size(); j++)
  11. cout << setw(3) << x[i][j];
  12. cout << endl;
  13. }

实时效果反馈

1. 关于array比原生数组的优势, 说法==不正确==的是:__

  • A 提供了与其它STL容器一致的接口

  • B at() 访问时,会进行越界检查

  • C 无论传引用还是指针,都会携带size信息,不会产生“指针退化”现象

  • D 可以用字面常量为数组赋初值

2. array虽然是浅封装,仍然提供大量成员函数,下列哪个==没有==提供?:__

  • A size()

  • B begin()

  • C swap()

  • D capacity()

答案

1=>D 2=>D

韩信分油问题

image-20220307102509735

韩信走马分油问题,国外也称为:泊松分酒问题。

很古老的益智游戏。

话说韩信骑马街上遛弯儿,遇到两个人在路边为分油而发愁。

大葫芦中装满油,10斤。

另有 3 斤,7斤葫芦。

问,如何导出 5 斤油来。

韩信还没下马,就想出了答案。。。。

这个问题一般化:

有 N 个容器,容量 V1, V2, V3 …,当前装的油量:v1, v2, v3, …

求如何腾挪,出现 x 在任意容器中。

分析

我们可以把所有瓶子中的油量看作是一种状态,

从当前状态 s0 出发,所有可以的倒油法,会产生新的状态 s11, s12, s13…

由这些新状态出发,又能产生新状态 s21, s22, s23, s24, …

直到我们撞上目标,或者,无法产生出不重复的新状态。

其实,这就是: BFS 广度优先遍历。

只不过,这些状态并非是先就存在,而是计算中动态产生的而已。

章节三:信息学竞赛 CPP 进阶篇 - 图29

实现

  1. template <int N>
  2. struct Bottles{
  3. array<int,N> V; //容量
  4. array<int,N> v; //现量
  5. bool operator==(const Bottles& t) const {
  6. for(int i=0; i<N; i++) if(v[i] != t.v[i])
  7. return false;
  8. return true;
  9. }
  10. // 从第 i 个瓶子倒入 第 j 个瓶子
  11. bool flow(int i, int j){
  12. if(i==j) return false;
  13. if(v.at(i)==0) return false; // 源瓶空
  14. if(v.at(j)==V.at(j)) return false; // 目标瓶满
  15. if(v[i] + v[j] < V[j]){
  16. v[j] += v[i];
  17. v[i] = 0;
  18. }
  19. else{
  20. v[i] -= V[j] - v[j];
  21. v[j] = V[j];
  22. }
  23. return true;
  24. }
  25. friend ostream& operator<<(ostream& os, const Bottles<N> t){
  26. cout << "(";
  27. for(int i=0; i<N; i++) cout << t.v[i] << " ";
  28. cout << ")";
  29. return os;
  30. }
  31. };
  32. template <int N>
  33. bool ok(const Bottles<N>& bt, int goal)
  34. {
  35. auto it = find(bt.v.cbegin(), bt.v.cend(), goal);
  36. return it != bt.v.cend();
  37. }
  38. template <int N>
  39. bool fen(Bottles<N> bt, int goal)
  40. {
  41. deque<Bottles<N>> da; // 工作队列,执行BFS
  42. da.push_back(bt);
  43. vector<Bottles<N>> his; // 记录所有已知的状态
  44. his.push_back(bt);
  45. while(true){
  46. int n = da.size();
  47. if(n==0) return false;
  48. for(int i=0; i<n; i++){
  49. auto t = da.front();
  50. if(ok(t, goal)) return true;
  51. da.pop_front();
  52. for(int i=0; i<N; i++)
  53. for(int j=0; j<N; j++){
  54. auto t2 = t;
  55. if(!t2.flow(i,j)) continue;
  56. if(find(his.cbegin(), his.cend(), t2) != his.cend()) continue;
  57. cout << "?" << t2 << endl;
  58. his.push_back(t2);
  59. da.push_back(t2);
  60. }
  61. }
  62. cout << "?------------" << endl;
  63. }
  64. }

实时效果反馈

1. 下列哪种情况,更适合用array 而不是vector:__

  • A 数组大小在开始是知道的,并且不会在运行期间变化

  • B 数组只在尾巴添加,不会在其它位置插入数据

  • C 数组中的元素是用户定义的类型

  • D 数组中的元素是指针类型

2. 下列哪种情况,更适合用 deque,而不是 vector:__

  • A 数组在中间位置频繁添加和删除

  • B 数组的首尾两端都有频繁增删的情况

  • C 数组规模特别巨大

  • D 多维数组,各个维度的大小都可能动态变化

答案

1=>A 2=>B

list

image-20220307162505792

以双向循环链表实现。

优势在于:任何位置插入删除都是常数时间,与规模无关。

缺点:不能随机访问。

大量的通用容器方法很容易理解:

front, back, push_back,push_front, pop_back, pop_front 等

begin, cbegin, rbegin …

clear, erase, size, empty …

特有的函数

unique() 删除相邻的重复元素

splice() 从第2个链表剪切,拼接到第1个链表

merge() 归并法,把链表2,并入链表1

sort() 对链表排序,默认升序

remove() remove_if() 删除特有的元素

参考示例

排序

  1. list<int> a = {5,7,10,22,15,3,6,5,1,1,5};
  2. a.sort();
  3. a.unique();
  4. copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));

归并

  1. list<int> a = {1,3,5,7,9}; // 已经有序
  2. list<int> b = {2,2,6,10}; // 已经有序
  3. a.merge(b);
  4. copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));

剪切与拼接

  1. list<int> a = {1,2,3,4,5};
  2. list<int> b = {10,20,30,40};
  3. auto i = a.begin();
  4. advance(i,3);
  5. a.splice(i, b); // 也可以只剪切 b 的一部分
  6. copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));
  7. cout << endl;
  8. copy(b.cbegin(), b.cend(), ostream_iterator<int>(cout, " "));
  9. cout << endl;

注意与 insert 的区别,这里剪切-粘贴,insert是:拷贝-粘贴

很有意思的是,list 是双向循环链表,可以很高效地实现“循环移动”效果:

  1. list<int> a = {1,2,3,4,5};
  2. auto i = a.begin();
  3. advance(i,2);
  4. a.splice(a.begin(), a, i, a.end()); // 3,4,5,1,2

链表做过滤是很高效的:

  1. bool is_digit(const char& x)
  2. {
  3. return x >= '0' && x <= '9';
  4. }
  5. /// 调用处:
  6. string s = "a1b2c33de4f";
  7. list<char> a(s.cbegin(), s.cend());
  8. a.remove_if(is_digit);
  9. copy(a.begin(), a.end(), ostream_iterator<char>(cout, " ");

实时效果反馈

1. 关于list, 说法正确的是:__

  • A list内部实现用单链表

  • B list 可以快速随机访问元素

  • C list 可以快速在任何位置插入、删除

  • D list 比vector更节约存储空间

2. 下列哪个动作,list 比 vector 更高效?__

  • A 容器 a 的值复制到容器 b

  • B 删除容器中符合某种特征的元素

  • C 大量在尾部添加新元素

  • D 把容器的中间位置一分为二

答案

1=>C 2=>B

stack与queue

image-20220309084614934

stack与queue属于容器适配器

它们默认在底层封装了一个 deque容器。

stack 和 queue 都是弱化了 deque 的功能,把 deque的使用限定在特定的逻辑层上。

强限定可以在使用时更安全,在大型项目开发上,也便于追踪、排查错误。

工程上的实践是:要求程序员一定不要xxx,往往没有多少约束力;让编译器限定他能做哪些动作,不能做哪些,要有效得多。

stack

stack 实现后进先出(LIFO)的逻辑。

如果把 deque 比作透明的软管,两端可以方便操作,也可以看见任意元素,并且可以在中间吃力地操作元素;

那么,stack 就是一个不透明的带底的硬桶。只能在一端操作,

此外,没有办法看见或者操作中间的元素

注意,stack 不提供迭代器,没有办法遍历所有元素!

逆波兰表达式计算

(1 + 2)* (3 + 4) 是我们常见的表达式的形式,运算符在中间,很直观。

缺点是:有时,需要用括号控制计算次序。

一般地:

a + b 为中缀形式;

+ a b 为前缀形式,也成为波兰表达式;

a b + 则为后缀形式,也称为逆波兰表达式

后缀形式有 2 个优点: 1. 不需要括号, 2. 方便机器求值

比如,(1 + 2) * (3 + 4) 的后缀形式为:1 2 + 3 4 + *

求值的过程是:

  • 遇到操作数,则压入栈中
  • 遇到操作符,则从栈中弹出两个操作数,计算后再压入栈中
  1. stack<int> w;
  2. stringstream ss("10 20 + 1 2 + *");
  3. string s;
  4. while(ss >> s){
  5. if(s=="+" || s=="*"){
  6. int b = w.top(); w.pop();
  7. int a = w.top(); w.pop();
  8. if(s=="+")
  9. w.push(a + b);
  10. else
  11. w.push(a * b);
  12. }else{
  13. w.push(stoi(s));
  14. }
  15. }
  16. cout << w.top() << endl;

注意:与多数高级语言不同

stack.pop() 只是删除,并不返回栈顶的元素,只能通过stack.top() 返回引用

这样设计是为了高效率。

queue

与stack情况类似,只是,queue 实现先进先出的逻辑。

可以把queue比作一个不透明的硬管子,并且标明了流向

它的一端只能插入,另一端只能取出。

我们同样没有办法遍历其中的元素。

实时效果反馈

1. 关于STL的stack, 说法正确的是:__

  • A stack底层是数组

  • B stack底层是链表

  • C stack是对deque的再封装,底层是分块数组

  • D stack可以遍历所有元素

2. STL提供适配器类型容器的目的是?:__

  • A 提高编译速度

  • B 提高运行效率

  • C 增加安全性,方便调试和维护

  • D 兼容 c 库

答案

1=>C 2=>C

迷宫问题

看如下的迷宫问题:

章节三:信息学竞赛 CPP 进阶篇 - 图32

一种朴素的想法是:

把入口标为 0,

所有和它相连的通路标为 1

所有和1相连的通路标为 2 (已经标记过的略过)

….

重复这个动作,直到撞上出口,所标数字即是解

如果标记进行不下去,仍没有碰到出口,则为无解

章节三:信息学竞赛 CPP 进阶篇 - 图33

从文件读入数据

由于测试数据可能很大,我们可以从文件读入。

文件格式规定为:

行数 列数

第一行。。。。

第二行。。。。

。。。。

因为STL对流的统一抽象,从文件读入和从控制台读入几乎没什么差别。

读入的数据存入二维数组。

但这个数组是动态大小的,可以用 vector 来模拟。

具体地说,我们打算做一个函数 read, 其格式如下:

vector<vector<char>> read(const char* filename)

注意对迭代器和通用算法的应用:

  1. vector<vector<char>> read(const char* fname)
  2. {
  3. ifstream in(fname);
  4. int M, N;
  5. in >> M >> N;
  6. vector<vector<char>> t(M, vector<char>(N,'1'));
  7. in.ignore();
  8. string s;
  9. for(int i=0; i<=M; i++){
  10. getline(in, s);
  11. copy(s.cbegin(), s.cend(), t[i].begin());
  12. }
  13. return t;
  14. }

这里还有个小陷阱:

当读入了两个整数后,回车被剩在缓冲区中,导致下一个getline读到一个空行。

为解决这个十分普遍的问题,输入流提供了 ignore() 可以读取并丢弃一些内容。

默认就是读到第一个换行符的出现。

增加围墙

为了将来处理方便,我们把整个数据外围增加一圈墙,以解决访问越界问题

  1. vector<vector<char>> read(const char* fname)
  2. {
  3. ifstream in(fname);
  4. int M, N;
  5. in >> M >> N;
  6. vector<vector<char>> t(M+2, vector<char>(N+2,'1'));
  7. in.ignore();
  8. string s;
  9. for(int i=1; i<=M; i++){
  10. getline(in, s);
  11. copy(s.cbegin(), s.cend(), t[i].begin()+1);
  12. }
  13. return t;
  14. }

数据准备好后,就可以继续后边的工作了(待续)

实时效果反馈

1. 对于大小不能事先确定的整型二维数组, 用以下哪个结构比较方便?__

  • A array>

  • B vector>

  • C vector>

  • D 原生数组

2. 二维数组a, a[2][3]a.at(2).at(3)的区别是:__

  • A 用 at 访问进行越界检查,可能抛出异常

  • B at访问比 [ ] 访问更高效

  • C at 只能读取元素,不能改写

  • D at 是c++11 才有的标准

答案

1=>B 2=>A

迷宫问题(2)

image-20220309144045607

当前所处的位置可以做成对象:

  1. struct Point{
  2. int row; int col; // 当前的行号,列号
  3. array<Point, 4> go(){
  4. array<Point, 4> t;
  5. t[0] = Point{row-1, col};
  6. t[1] = Point{row+1, col};
  7. t[2] = Point{row, col-1};
  8. t[3] = Point{row, col+1};
  9. return t;
  10. }
  11. };

我们提供一个go函数,返回从当前位置能走到的4个位置。

这个返回值是固定大小的,用 array 合适,c语言无法直接返回原生数组。

解法:

  1. int solve(vector<vector<char>>& da)
  2. {
  3. struct Point{
  4. int row; int col;
  5. array<Point, 4> go(){
  6. array<Point, 4> t;
  7. t[0] = Point{row-1, col};
  8. t[1] = Point{row+1, col};
  9. t[2] = Point{row, col-1};
  10. t[3] = Point{row, col+1};
  11. return t;
  12. }
  13. };
  14. queue<Point> w;
  15. w.push(Point{1,1});
  16. int k = 0;
  17. while(w.size()>0){
  18. int n = w.size();
  19. for(int i=0; i<n; i++){
  20. auto t = w.front();
  21. if(t.row == da.size()-2 && t.col == da[0].size()-2)
  22. return k;
  23. auto tt = t.go();
  24. for(auto j=tt.cbegin(); j!=tt.cend(); ++j){
  25. if(da[(*j).row][(*j).col]=='.'){
  26. da[(*j).row][(*j).col] = '*';
  27. w.push(*j);
  28. }
  29. }
  30. w.pop();
  31. }
  32. k++;
  33. }
  34. return -1;
  35. }

可以看到,本题目的核心算法仍然是 BFS,

BFS一般并不需要访问中间的元素,只是从一端读取数,向另一端压入数,

此种情形,用queque就足够了。

实时效果反馈

1. 求最短路径问题,一般可以用:__

  • A 深度优先搜索算法

  • B 广度优先搜索算法

  • C 递归算法

  • D 二分搜索算法

2. 在queue上,==不可以==执行的操作是?:__

  • A front() 读取元素

  • B pop() 删除元素

  • C size() 求包含元素个数

  • D begin() 取得正向迭代器

答案

1=>B 2=>D

集合

image-20220309210623320

数学上的集合包含了若干元素,其中每个元素都是唯一的。

类似于数学上的集合概念。set可以高效率地判断一个元素是否在本集合中。

可以从集合中高效率地删除元素、向集合中插入元素。

但,不支持随机访问。

STL 的 set 是关联型容器。内部存储结构是非线性的。

其内部以二叉树形式实现。并且,为了增删的高效,使用了红黑树。

当频繁增删时,较AVL树有更高的效率。

红黑树是一种排序二叉树,元素间必须可比较大小。

如果元素是自己定义的类型,必须实现比较操作 operator<

简单示例

  1. set<int> a = {1,2,3,4};
  2. a.insert(4);
  3. a.insert(5);
  4. a.insert(6);
  5. for(auto i=a.begin(); i!=a.end(); ++i) cout << *i << " ";
  6. cout << endl;

判断元素是否存在

  1. set<int> a = {1,2,3,4};
  2. if(a.find(3) != a.end()) cout << "exist" << endl;
  3. cout << a.count(3) << endl;

有两种方法。

注意:find 的操作与其它容器是一致的,找到则返回其迭代器,找不到返回 a.end()

自定义的元素类型

  1. struct STU{
  2. string name;
  3. int age;
  4. bool operator<(const STU& t) const {
  5. if(age==t.age) return name < t.name;
  6. return age < t.age;
  7. }
  8. };

注意:

自定义类型一定要重载 operator< 来决定元素间的大小次序。

同时,也决定了元素间是否相等。

相等的定义是:

a > b, b > a 都是 false

测试:

  1. set<STU> a;
  2. a.insert(STU{"zhang", 15});
  3. a.insert(STU{"li", 16});
  4. a.insert(STU{"tao", 10});
  5. a.insert(STU{"mao", 10});
  6. for(auto i=a.begin(); i!=a.end(); ++i)
  7. cout << (*i).name << ": " << (*i).age << endl;

实时效果反馈

1. STL的set容器, 底层采用的数据结构是:__

  • A AVL平衡二叉树

  • B 红黑树

  • C B+ 树

  • D 线索树

2. STL 的 set,对用户定义的类型,有什么要求?:__

  • A 重载 operator ==

  • B 重载 operator <

  • C 重载 operator <<

  • D 重载 operator !=

答案

1=>B 2=>B

集合的运算

image-20220309211439660

数学上,集合有:交、差、并、补等运算。

这些功能并不在 set 中。

STL把此功能归为算法,适用于更多的数据类型,不仅仅是集合。

set_intersection : 交集

set_difference: 差集

set_union: 并集

set_symmetric_difference: 对称差

image-20220310085520127

基本示例

以set_intersection为例,其调用格式:

image-20220310091035474

需要第一个容器的迭代区间,第二个容器的迭代区间,

然后是一个输出迭代器,表示结果写到哪里去。

这是源代码:

  1. template <class InputIterator1, class InputIterator2,
  2. class OutputIterator>
  3. OutputIterator set_intersection (
  4. InputIterator1 first1, InputIterator1 last1,
  5. InputIterator2 first2, InputIterator2 last2,
  6. OutputIterator result)
  7. {
  8. while (first1!=last1 && first2!=last2)
  9. {
  10. if (*first1<*first2) ++first1;
  11. else if (*first2<*first1) ++first2;
  12. else {
  13. *result = *first1;
  14. ++result; ++first1; ++first2;
  15. }
  16. }
  17. return result;
  18. }

显而易见,这里并不要求特定的容器类型,

但要求元素已经排好了次序,容器支持遍历就可以了。

这里需要注意,输出迭代的容器要提供足够的存储空间,以 vector为例:

  1. set<int> a = {1,2,3,4};
  2. set<int> b = {3,4,5,6};
  3. vector<int> t(10); // 需要留出足够空间
  4. set_intersection(a.cbegin(),a.cend(),
  5. b.cbegin(),b.cend(),
  6. t.begin());
  7. copy(t.cbegin(),t.cend(), ostream_iterator<int>(cout," "));

需要注意几点:

  • set_intersection 是通用的算法,不仅仅支持set,还可以支持其它容器

    但要求:元素间需要能比较大小 operator<

  • a, b 都必须是已经有序的容器(事先排过序)

  • 输出位置必须有足够的空间供给写出操作

实际上,set_intersection返回了输出迭代器的结束位置,我们可以保存它。

  1. .....
  2. auto end = set_intersection(a.cbegin(),a.cend(),
  3. b.cbegin(),b.cend(), t.begin());
  4. copy(t.begin(), end, ostream_iterator<int>(cout," "));

注意,如果希望输出的容器是个集合,该如何处理?

vector -> set ?

直接放个集合不可以:1. 没法预留空位, 2. 集合的迭代器是无法修改值的

实际上,我们的需求是,在输出迭代器写入的时候,自动变成插入操作。

  1. set<int> a = {1,2,3,4};
  2. set<int> b = {3,4,5,6};
  3. set<int> c;
  4. set_intersection(a.begin(),a.end(),b.begin(),b.end(),
  5. insert_iterator<set<int>>(c,c.begin()));
  6. copy(c.begin(), c.end(), ostream_iterator<int>(cout," "));

注意:嵌入头文件:

很多高级语言,提供了集合的运算符重载:

a + b 求并集,不改变 a, b 返回新的集合

a += b 求并集,直接改变 a

a - b 求差集, 不改变 a, b 返回新的集合

a -= b 求差集,直接修改 a

STL 没有提供这些功能,如果需要直接修改集合,其实可以直接调用set成员函数

a.insert 实现并集

差集就要自己用erase一个一个删除了,效率稍差些

a += b 效果

  1. set<int> a = {1,2,3,4};
  2. set<int> b = {3,4,5,6};
  3. a.insert(b.begin(), b.end());
  4. copy(a.begin(), a.end(), ostream_iterator<int>(cout," "));

a -= b 效果:

  1. set<int> a = {1,2,3,4};
  2. set<int> b = {3,4,5,6};
  3. for(auto i=b.begin(); i!=b.end(); ++i) a.erase(*i);
  4. copy(a.begin(), a.end(), ostream_iterator<int>(cout," "));

实时效果反馈

1. 如果集合a={1,2,3,4}, b={3,4,5,6} , a, b 的对称差为:__

  • A {1,2}

  • B {5,6}

  • C {1,2,5,6}

  • D {3,4}

2. setdifference 算法对参数的要求,哪个是不必要的?:_

  • A 输入容器必须是已排序

  • B 输入容器的元素必须支持是可比较的

  • C 输出迭代器所在容器已经预留好足够的空间

  • D 容器的元素类型重载了 operator==

答案

1=>C 2=>D

更多的集合

image-20220310194413947

multiset

与 set 基本相同,只是它支持元素的重复。

当把重复的元素加入集合中,这些重复的元素同时存在。

可以用count(x) 返回重复元素的个数

  1. multiset<int> a = {1,2,3,4};
  2. a.insert(3);
  3. a.insert(4);
  4. a.insert(5);
  5. a.erase(4);
  6. for(auto i=a.begin(); i!=a.end(); ++i) cout << *i << " ";
  7. cout << endl;

运行结果:

章节三:信息学竞赛 CPP 进阶篇 - 图40

bitset

类似于一个元素值为0或1的 array

在逻辑上,等价于set,但是加上了范围的限定。

可以有效节约存储空间,并提高运行速度。

可以与string类型间相互转换

  1. bitset<8> a("11110000");
  2. cout << a[0] << "," << a[7] << endl;
  3. a[7] = 0;
  4. cout << a.to_string() << endl;

它的模板参数是含有的位数,可以非常大。

bitset 提供了丰富的与位运算有关的高效率的函数:

函数 功能
b.any() 整个set中是否存在1
b.none() 整个set都是0,与any()反义
b.count() 1 的个数
b.test(pos) 与 b[pos] 读取同义
b.set() 所有位都置为 1
b.reset() 所有位置都清 0
b.flip() 所有位取反
os << b b中的位输出到流

应用示例

求 n 以内的所有素数的个数

当 n 较大时,最有效的方法仍然是古老的筛法

  1. template <int N>
  2. int su()
  3. {
  4. bitset<N> da;
  5. da.set();
  6. da[0] = 0;
  7. da[1] = 0;
  8. for(int k=2; k<N; k++){
  9. if(da[k]==0) continue;
  10. int p = k + k;
  11. while(p<N){
  12. da[p] = 0;
  13. p += k;
  14. }
  15. }
  16. return da.count();
  17. }

调用处:

  1. cout << su<1000 * 1000>() << endl;

参数 N 必须是个编译期的常数,所以不能做成传入参数的形式。

实时效果反馈

1. 关于bitset, 说法正确的是:__

  • A bitset 可以支持用户自定义类型的元素

  • B bitset 可以支持元素为 string 类型

  • C 逻辑上,bitset元素固定为其size() 范围内的整数

  • D bitset 比用普通 set 省空间,但速度慢些

2. 求大范围内的所有素数,怎么计算较快?:__

  • A 逐一测试所有数字是否有因数

  • B 用高级数学公式,快速求出

  • C 用古老的筛法,配合按位运算

  • D 使用费尔马定理

答案

1=>C 2=>C

映射

image-20220311105048251

map是关联容器,提供对<键-值>结构的高效检索。

它的内部是通过红黑树实现,所以要求元素的要支持比较大小的操作。

注意,仅仅对 key 有要求, value 则没有任何限制。

如果使用迭代器来操作,能修改value,但不能修改key。

map是类模板

定义对象时,需要两个参数: key 的类型和 value 的类型

  1. map<string,int> a;
  2. a["zhang"] = 70;
  3. a["li"] = 58;
  4. a["wang"] = 99;
  5. a["zhang"] += 10;
  6. a["li"] = 85;
  7. cout << a["zhang"] << endl;

[ ] 操作有很多功能,可以

  • 读取某个key的值,
  • 也可以添加新的 key-value 键值对
  • 也可以修改某个 key 的 value 为新值

注意:一个常见的陷阱,

当访问的 key 不存在的时候,会添加一个 key-value 到map中, value取缺省值

  1. map<string,int> a;
  2. a["zhang"] = 70;
  3. a["li"] = 85;
  4. a["wang"] = 99;
  5. a["zhang"] += 10;
  6. cout << a.size() << endl;
  7. cout << a["zhang1"] << endl; // 会偷偷添加一个键值对
  8. cout << a.size() << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图42

判断某个 key 是不是存在,可以用 m.find(key) != m.end() 来实现

也可以用 m.count(key)

这方面与 set 相同。

可以用 m.erase() 删除某个键值对,有3种格式

  • m.erase(key) 删除某个值,返回的是删除的个数(0 或 1)
  • m.erase(position) 给定迭代器,删除该位置键值对,返回下一个位置迭代器
  • m.erase(first,last) 给定迭代区间,删除该区间元素,返回下一个位置迭代器

map 的遍历

虽然 map 底层是树形结构,仍然是可以遍历的。其遍历特性与 set 类似。

但,i 的类型是*键值对,在程序上表达为 pair

pair 是个模板类,仅仅就是把两个类型捆绑在一起而已。

访问的时候,用 p.first, p.second 即可。

  1. map<string,int> a;
  2. a["zhang"] = 70;
  3. a["li"] = 85;
  4. a["wang"] = 99;
  5. a["zhang"] += 10;
  6. for(auto i=a.begin(); i!=a.end(); ++i)
  7. cout << i->first << ":" << i->second << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图43

map的用途

map 的优势在于能快速地定位元素,并携带关联信息。

如果广义地看,数组在逻辑上也可以看成是以 int 为 key 的 map

有些高级语言中只提供一种数据结构,就是 map,可见其用途之大。

比如,循环链表以 map 来表示。

约瑟夫环问题,可以用 1->2, 2->3, …. n->1 来表示小朋友间的关系。

这个思路的解决方案:

  1. int N = 10; // 参加约瑟夫环的人数
  2. map<int,int> da;
  3. for(int i=1; i<=N; i++) da[i] = i + 1;
  4. da[N] = 1;
  5. int k = 1; // 当前持有令牌
  6. while(da[k]!=k){
  7. k = da[k];
  8. da[k] = da[da[k]];
  9. k = da[k];
  10. }
  11. cout << k << endl;

实时效果反馈

1. 关于STL中的map, 说法正确的是:__

  • A map 是序列型容器

  • B map 的底层实现是自平衡的排序二叉树结构

  • C x = map[key] 会修改 x 的值,不会修改map

  • D map 不支持元素的遍历

2. m是map类型的对象,对m.erase() 调用格式错误的是:__

  • A m.erase(某个键)

  • B m.erase(某个迭代器)

  • C m.erase(某个迭代区间)

  • D m.erase(某个键值对)

答案

1=>B 2=>D

map的应用

image-20220311140006721

map 在保存数据间的关系方面特别有用,几乎是万能的数据结构。

前边课程中的【韩信分油】问题,如果求具体的分油方案,就可以用 map

思路

章节三:信息学竞赛 CPP 进阶篇 - 图45

从初态 ${s_0}$到达某个终点态${s_k}$,我们希望通过 ${s_k}$来追溯它的历史:

${sk}$ => ${s{k-1}}$ => ${s_{k-2}}$ => … => ${s_0}$

解法

  1. #include <iostream>
  2. #include <vector>
  3. #include <array>
  4. #include <deque>
  5. #include <map>
  6. #include <algorithm>
  7. using namespace std;
  8. template <int N>
  9. struct Bottles{
  10. array<int,N> V;
  11. array<int,N> v;
  12. bool operator==(const Bottles& t) const {
  13. return v == t.v;
  14. }
  15. bool operator<(const Bottles& t) const {
  16. return v < t.v;
  17. }
  18. // 从第 i 个瓶子倒入 第 j 个瓶子
  19. bool flow(int i, int j){
  20. if(i==j) return false;
  21. if(v.at(i)==0) return false; // 源瓶空
  22. if(v.at(j)==V.at(j)) return false; // 目标瓶满
  23. if(v[i] + v[j] < V[j]){
  24. v[j] += v[i];
  25. v[i] = 0;
  26. }
  27. else{
  28. v[i] -= V[j] - v[j];
  29. v[j] = V[j];
  30. }
  31. return true;
  32. }
  33. friend ostream& operator<<(ostream& os,
  34. const Bottles<N> t){
  35. cout << "(";
  36. for(int i=0; i<N; i++) cout << t.v[i] << " ";
  37. cout << ")";
  38. return os;
  39. }
  40. };
  41. template <int N>
  42. bool ok(const Bottles<N>& bt, int goal)
  43. {
  44. auto it = find(bt.v.cbegin(), bt.v.cend(), goal);
  45. return it != bt.v.cend();
  46. }
  47. template <int N>
  48. vector<Bottles<N>> fen(Bottles<N> bt, int goal)
  49. {
  50. deque<Bottles<N>> da; // 工作队列,执行BFS
  51. da.push_back(bt);
  52. map<Bottles<N>, Bottles<N>> his; // 记录所有已知的状态
  53. his[bt] = bt;
  54. vector<Bottles<N>> rt;
  55. while(true){
  56. int n = da.size();
  57. if(n==0) return rt;
  58. for(int i=0; i<n; i++){
  59. auto t = da.front();
  60. if(ok(t, goal)) {
  61. while(!(his[t] == t)){
  62. rt.push_back(t);
  63. t = his[t];
  64. }
  65. return rt;
  66. }
  67. da.pop_front();
  68. for(int i=0; i<N; i++)
  69. for(int j=0; j<N; j++){
  70. auto t2 = t;
  71. if(!t2.flow(i,j)) continue;
  72. if(his.find(t2) != his.end()) continue;
  73. his[t2] = t;
  74. da.push_back(t2);
  75. }
  76. }
  77. }
  78. }
  79. int main()
  80. {
  81. Bottles<3> bt;
  82. bt.V = {10,7,3};
  83. bt.v = {10,0,0};
  84. vector<Bottles<3>> x = fen(bt,5);
  85. for(int i=0; i<x.size(); i++)
  86. cout << x[i] << endl;
  87. return 0;
  88. }

实时效果反馈

1. 关于通用find算法, 对容器中元素要求是:__

  • A 容器中元素要重载 operator<

  • B 容器中元素要重载 operator==

  • C 容器中元素要重载 operator()

  • D 容器中元素要重载 operator[ ]

2. 如果判断某个 key 是否在 map 中?合适的方案是:__

  • A 调用 map[key]

  • B 调用 STL 通用的 find 算法

  • C 调用 map 自身的 find

  • D 调用 map.test

答案

1=>B 2=>C

multimap

image-20220312212519746

与 multiset 的情况类似,multimap与map基本相同,

除了它可以支持多个重复key的并存

可以通过count 检索重复元素的个数

multimap 因为同样key可以存在多个,所以插入的位置就有很多变化。

其 insert 较普通的 map 要复杂一些。

insert 示例

假设我们有一个应用,需要:名次-->姓名 这样的键值关系。

同一名次可能有多个人,给定一个名次,要立即返回所有这些人,用什么结构描述好呢?

multimap 就是不错的选择。

  1. multimap<int, string> a;
  2. a.insert(pair<int,string>(1, "zhang"));
  3. auto it = a.insert(pair<int,string>(2,"wang"));
  4. // 返回迭代器,指向新元素
  5. a.insert(pair<int,string>(2,"li"));
  6. a.insert(it, pair<int,string>(2,"zhao")); // 尽量在 it 之前插入
  7. a.insert({{4,"zhou"},{4,"wu"},{4,"zheng"}}); // c++11 标准
  8. for(auto i=a.begin(); i!=a.end(); ++i)
  9. cout << i->first << ":" << i->second << endl;

多值查找

给定一个键,如何返回多个值?

通用的 find 可以找到第一个,再自己向后迭代。

equal_range 也可以更方便实现这个功能。

  1. multimap<int, string> a;
  2. a.insert(pair<int,string>(1, "zhang"));
  3. auto it = a.insert(pair<int,string>(2,"wang"));
  4. // 返回迭代器,指向新元素
  5. a.insert(pair<int,string>(2,"li"));
  6. a.insert(it, pair<int,string>(2,"zhao")); // 尽量在 it 之前插入
  7. a.insert({{4,"zhou"},{4,"wu"},{4,"zheng"}}); // c++11 标准
  8. auto pa = a.equal_range(2); // 返回一对迭代器 (起始,结束)
  9. for(auto i=pa.first; i!=pa.second; ++i)
  10. cout << i->first << ":" << i->second << endl;

应用示例

一个十分常见的应用场景:从 key —> value 的映射,获得它的逆映射:

即:value —> key

用原来的 value 作为key,原来的 key 为 value, 同时,又不能损失信息。

  1. map<string, string> a;
  2. a["xx1001"] = "zhang";
  3. a["xx1002"] = "wang";
  4. a["xx1003"] = "li";
  5. a["xx1004"] = "zhao";
  6. a["xx1005"] = "wang";
  7. multimap<string, string> b;
  8. for(auto i=a.begin(); i!=a.end(); ++i)
  9. b.insert(make_pair(i->second, i->first));
  10. cout << b.count("zhang") << endl;
  11. cout << b.count("wang") << endl;
  12. auto it = b.find("wang");
  13. while(it != b.end() && it->first == "wang"){
  14. cout << it->second << endl;
  15. ++it;
  16. }

从multiset 中查询,可以用 equal_range,也可以用原来的 find

find 返回了迭代器,所有相同key的记录,必然存在相邻的位置,

所以让迭代器向后遍历即可。

实时效果反馈

1. 关于multimap, 说法正确的是:__

  • A 其中元素按进行联合排序

  • B key相同的元素按插入的顺序排序

  • C key相同的元素默认按插入顺序排序,但可以通过程序控制插入位置

  • D 可以通过迭代器修改 key 和 value

2. multimap 与 vector 相比,哪个操作较慢?:__

  • A 查找某个元素

  • B 删除某个元素

  • C 求交集

  • D 取出第 n 个元素

答案

1=>C 2=>D

STL算法概览

image-20220315200759306

STL作为c++标准的一部分,提供了十分实用、高效、健壮的基础设施。

学好STL,是提高生产力的有效途径。

同时,STL以源码的形式提供,可作为我们分析、学习程序设计的范本。

STL算法以函数模板的形式呈现。

这是c++特有的类型泛化方式,能同时坚持了三样东西:

  • 类型泛化

    这是算法抽象的要求,高级程序语言的必由之路。

  • 静态类型

    编译期类型、语法检查,把大多数错误消灭在编译阶段。

  • 高执行效率

    c++的模板采用展开与二次编译方式,把抽象局限在编译阶段,能够以极小的代价获得类型的灵活性与算法的通用性。

STL 的算法尽量保持了通用性,与具体的容器特性相隔离

章节三:信息学竞赛 CPP 进阶篇 - 图48

STL 算法是以函数模板的形式存在的。

也就是说,提供的是源代码,需要经过二次编译。

即,当我们确定了模板参数后,编译器替我们展开为特定的具体函数,再进一步编译。

容器内是具体的操作目标,可以把容器看成:名词

仿函数,也叫函数对象,提供比函数更好的封装特性,是对可定制动作的抽象,看作:动词

函数适配器,把函数的接口规格进行适配、整合,看作:副词

可分类

  • 非变易算法(nomodifying)

    不修改所操作的内容(只是读取)

  • 可变易算法(modifying)

    修改操作对象

  • 排序算法(sorting)

    包括排序,合并,搜索,有序序列上的集合操作

  • 数值算法(numeric)

    数值计算,科学计算

  • 其它算法

    堆、关系运算、生成算法等

一般规律

  • 迭代区间的表示

    [first, last) 形成前闭后开的半开区间。

    包含 first,不包含 last

  • 迭代器的类型有 5 种,算法参数总是定义成它的最低要求

    高类型的迭代器可以胜任低类型迭代器的工作,反之不可。

    章节三:信息学竞赛 CPP 进阶篇 - 图49

    注意,迭代器的类型间并非继承关系,没有强制的类型约束

    编译通过了,仍然可能有运行问题

  • 加_if 后缀的,与无后缀的比较,需要额外的一个谓词函数(返回值为bool)

  • 变易算法通常有两个版本,一个是在原来的容器上修改,称为 in-place (就地执行)

    另一个是 copy 到另外的地点:一般为了区分,加 _copy 后缀

  • 数值算法需要包含 , 其他算法需要包含:

实时效果反馈

1. STL算法引入iterator的目的是:__

  • A 使得算法与容器的具体类型隔离开,算法更具有一般性

  • B 使得所有操作通过指针完成,提高效率

  • C 使得算法可以兼容传统的 c 指针和 c++ 的对象

  • D 是的算法可以用模板完成,增强泛型能力

2. c++泛型策略比较于其它高级语言,特点是:__

  • A 类型更具有一般性

  • B 能完全兼容非面向对象(C语言)的编程模式

  • C 具有更高的一致性和内在的自洽性

  • D 提供抽象的同时,尽量做到极小的运行成本

答案

1=>A 2=>D

STL算法概览(2)

image-20220316105623402

STL 算法强调效率,甚至无所不用其极。

来看用途十分广的 copy 算法

image-20220315112850045

copy 不会分配空间,我们自己要确保有足够的空间

  1. vector<int> a = {1,2,3,4,4,5,6,7};
  2. array<int,4> b;
  3. copy(a.end()-4, a.end(), b.begin());
  4. copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));

也可以自己拷贝给自己,如果重叠,自己要当心:

  1. vector<int> a = {1,2,3,4,5,6,7,8};
  2. copy(a.begin(), a.begin()+4, a.begin()+2);
  3. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

这里,根据容器的的类型,做了特化处理,实际调用 memmove 对overlap无惧

章节三:信息学竞赛 CPP 进阶篇 - 图52

如果换成: list

  1. list<int> a = {1,2,3,4,5,6,7,8};
  2. auto i2 = a.begin();
  3. i2++; i2++;
  4. auto i4 = i2;
  5. i4++; i4++;
  6. copy(a.begin(), i4, i2);
  7. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

注意,list 的迭代器并非可以随机访问,只好自己慢慢移动了。

章节三:信息学竞赛 CPP 进阶篇 - 图53

当然,如果我们自己调整了拷贝的顺序,还是可以:

  1. list<int> a = {1,2,3,4,5,6,7,8};
  2. auto i4 = a.begin();
  3. i4++; i4++; i4++; i4++;
  4. auto i6 = i4;
  5. i6++; i6++;
  6. copy_backward(a.begin(), i4, i6);
  7. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

如果,我们不希望自己开空间,可以用各种迭代器适配器。 ostream_iterator 便是。

注意包含

  1. vector<int> a = {1,2,3,4,5,6,7,8};
  2. vector<int> b;
  3. copy(a.begin(), a.begin()+4,
  4. back_insert_iterator<vector<int>>(b));
  5. copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));

此外,还有 c++11 引入的:

image-20220315124836333

image-20220315124707504

这些被实践证明的很方便有用的模板风格值得学习,也胜于自己写循环。

  1. set<int> a0 = {3,5,7};
  2. struct F{
  3. F(set<int> &t):s(t){};
  4. set<int> s;
  5. bool operator()(int x){
  6. return s.find(x) == s.end();
  7. }
  8. };
  9. vector<int> a = {1,2,3,4,5,6,7,8};
  10. vector<int> b;
  11. copy_if(a.begin(), a.end(),
  12. back_insert_iterator<vector<int>>(b), F(a0));
  13. copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));

实时效果反馈

1. STL的copy算法,会不会自动解决 overlap 问题 ?:__

  • A 不会,看重叠的位置的运气

  • B 会自动解决,根据重叠位置决定拷贝的顺序

  • C 对有些简单类型的情况进行了优化,多数类型不可

  • D 编译器不允许重叠拷贝

答案

1=>C

函数适配器

image-20220315202151564

有的时候,在逻辑上,我们已经有了判别函数(即所谓的谓词函数),

但参数太多,或其它方面的格式不对,没法传入到算法模板里。

这个时候,函数适配器就派上用场了。

大约的分类:

类型 实现的功能 函数
绑定适配器 对 n 元函数,绑定一个参数,使之成为 n-1 元函数 bind1st, bind2nd
组合适配器 谓词的结果取反 not1, not2
指针适配器 把函数(或者指针),变为functor,以便被其它适配器使用 ptr_fun
成员函数适配器 把成员函数变为对象,可以被其它适配器使用 mem_fun, mem_fun_ref

示例

  1. bool f(set<int>& t, int x)
  2. {
  3. return t.find(x) == t.end();
  4. }
  5. int main()
  6. {
  7. set<int> a0 = {3,5,7};
  8. vector<int> a = {1,2,3,4,5,6,7,8};
  9. vector<int> b;
  10. copy_if(a.begin(), a.end(),
  11. back_insert_iterator<vector<int>>(b),
  12. bind1st(ptr_fun(f), a0));
  13. copy(b.begin(), b.end(),
  14. ostream_iterator<int>(cout, " "));
  15. return 0;
  16. }

注意,需要 include

例2

找到第一个大于10的元素

可以用STL的内置的函数对象模板 greater

  1. vector<int> a = {1,2,31,45,5,16,7,28};
  2. auto i = find_if(a.begin(), a.end(),
  3. bind2nd(greater<int>(),10));
  4. cout << *i << endl;

适配也可以反复组合,达到所需要的效果。

比如,f(x,y,z) 三个参数,我们希望把 x z 定死,只要y,就可以:

bind2nd(bind1st(ptr_fun(f), x0), z0)

比如,统计不大于10的元素的个数

  1. vector<int> a = {1,2,31,45,5,16,7,28,10};
  2. int x = count_if(a.begin(), a.end(),
  3. not1(bind2nd(greater<int>(),10)));
  4. cout << x << endl;

实时效果反馈

1. 关于函数适配器, 说法正确的是:__

  • A 函数适配器是为了解决函数参数类型不符合算法要求的情况

  • B 函数适配器主要目的是解决函数规格不符合算法要求,主要是参数目数不对

  • C 函数适配器不能嵌套使用

  • D 函数适配器只能用于函数对象,不能用于普通函数

2. std::greater是:__

  • A 函数适配器

  • B 普通函数

  • C 单目的函数模板

  • D 双目的函数模板

答案

1=>B 2=>D

匿名函数

章节三:信息学竞赛 CPP 进阶篇 - 图57

c++11 提供了对匿名函数的支持。

匿名函数,也称为:lambda函数,或者:lambda表达式。

其格式:

[捕获变量列表](参数列表) -> 返回值类型 { 函数体 }

返回值类型能推断的时候,可以不写。

如果没有 return 语句,返回的是 void

如果没有参数,圆括号也可以省略。

匿名函数的用途

有的时候,在需要传入一个函数的时候,

定义全局的函数或者写一个类来生成函数对象都有点繁琐,有小题大做之意味。

函数是对用户定义动作的封装,最方便的写法是直接写在调用处,

类似立即数字面常量的说法,

可以类比称为:立即码字面常码。。。

很多的高级语言都提供了匿名函数,python, ruby, scala, go 。。。。

c++ 需要紧跟时代,c++11 引入了这个特性

千呼万唤始出来,仍然是为了效率和安全问题吵架。

闭包特性

可以捕获所在作用域的变量

可以按引用捕获,或按值捕获

  1. [] //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
  2. [x, &y] //x 按值捕获, y 按引用捕获.
  3. [&] //用到的任何外部变量都隐式按引用捕获
  4. [=] //用到的任何外部变量都隐式按值捕获
  5. [&, x] //x显式地按值捕获. 其它变量按引用捕获
  6. [=, &z] //z按引用捕获. 其它变量按值捕获

示例

  1. map<int,string> a;
  2. a[5] = "zhang";
  3. a[8] = "wang";
  4. a[99] = "newton";
  5. string s = "-->";
  6. for_each(a.begin(), a.end(),[s](pair<int,string> p){
  7. cout << p.first << s << p.second << endl;
  8. });

再看 count_if 的新版本写法:

  1. vector<int> a = {3,6,7,12,14,27,16,9,10};
  2. int x = count_if(a.begin(), a.end(), [](int t){
  3. return t <= 10;
  4. });
  5. cout << x << endl;

实时效果反馈

1. 关于匿名函数, 说法正确的是:__

  • A 匿名函数就是没有名字的函数指针

  • B 匿名函数就是没有名字的函数对象

  • C 匿名函数就是有闭包特性的临时函数

  • D 匿名函数是STL99的特性

2. 使用匿名函数时,如果没有写返回类型,则:__

  • A 视为写了 void

  • B 可以通过 return 类型来推断出来,没有return则为void

  • C 产生编译错误

  • D 视为返回 int

答案

1=>C 2=>B

移动语义

image-20220317105423643

c++11 引入的特性。为了解决遗留问题,提高效率。

包括std::move(移动语义) 和 std::forward(完美转发)

左值(lvalue)和右值(rvalue)

左值就是可以取地址的值,也可以理解为有名字的值

右值则恰是其反面,比如:字面常量,临时对象,匿名对象等

可以简单地认为:所有不是左值的就右值

问题在?

先看一段代码:

  1. string a = "abc";
  2. string b = "xyz123";
  3. string t = a;
  4. a = b;
  5. b = t;
  6. cout << a << "," << b << endl;

这个字符串交换有什么问题吗?

有个效率的问题,比如,a,b都是几百兆的大串。

a = b; 时到底发生了什么?

执行了 operator= 对象赋值操作,如何执行?

image-20220317091328374

仔细想想,不难发现,我们没必要这样大折腾,

仅仅让 a, b 交换它们的堆空间指针以及size不久好了吗?

也就是说,可不可以不要拷贝数据,只移交控制权

解决方案

c++11 支持了这个想法。

std::move(x) 可以把 x 从左值变量变为它的右值引用,这样,在赋值时,

y = std::move(x) 的时候,调用 y 的 operator=(string&& t)

而不是 operator=(const string& t)

  1. string a = "abc";
  2. string b = std::move(a);
  3. cout << a << "," << b << endl;

输出:

章节三:信息学竞赛 CPP 进阶篇 - 图60

string 类的 operator=(string&& t) 中,直接把自己的指针指向 t 的内部空间,

并且把 t 的内部指针置空。

这不会严重影响程序以前的行为,

类 A 如果不定义 operator=(A&& ),就调用它的 operator=(const A&)

如果这个也没定义,则执行默认的内存照相拷贝动作。

类似地,还有:拷贝构造与移动构造。

其定义类似:A(const A& t) , A(AA& t)

自定义类支持移动语义

  1. struct A{
  2. int* p;
  3. int n;
  4. A() { n=0; p=nullptr; }
  5. A(int n) {
  6. this->n = n;
  7. p = new int[n];
  8. }
  9. A(const A& t){
  10. n = t.n;
  11. p = new int [n];
  12. memcpy(p, t.p, n * sizeof(int));
  13. }
  14. A(A&& t) {
  15. n = t.n;
  16. p = t.p;
  17. t.n = 0;
  18. t.p = nullptr;
  19. }
  20. ~A() {
  21. if(p) delete [] p;
  22. }
  23. void operator=(const A& t) {
  24. if(p) delete [] p;
  25. n = t.n;
  26. p = new int [n];
  27. memcpy(p, t.p, n * sizeof(int));
  28. }
  29. void operator=(A&& t) {
  30. if(p) delete [] p;
  31. n = t.n;
  32. p = t.p;
  33. t.n = 0;
  34. t.p = nullptr;
  35. }
  36. };

对 A 的使用:

  1. A a(100);
  2. A b = std::move(a);
  3. cout << a.n << endl;

这个功能用在 vector中就很有用了。

  1. vector<A> a;
  2. for(int i=5; i<10; i++){
  3. A t(i);
  4. a.push_back(move(t));
  5. cout << t.n << endl;
  6. }

move 只是把左值伪右值,引导程序的move行为

如果已经是右值,则无需 move

对返回值,不同编译器有不同的优化处理,需要自己多实验,在必要时加move

实时效果反馈

1. c++11 引入 move 语义的目的是:__

  • A 服务于STL基础设施,取代拷贝构造

  • B 把左值变量右值化,指示编译器调用右值重载版本

  • C 指针赋值前,自动删除原来分配的内存

  • D 提供引用计数,必要时释放资源

2. 下列哪个不是右值变量?__

  • A 函数的返回值

  • B 临时变量

  • C 代码中的立即数

  • D 全局变量

答案

1=>B 2=>D

堆算法

image-20220318083924534

堆的概念再来温习一下。

堆是二叉树,父节点的数值不小于子节点,子节点间没有约束。

章节三:信息学竞赛 CPP 进阶篇 - 图62

堆是完全二叉树,每层都是满的,除了最后的叶子层。

这样的规定,是为了便于用数组顺序存储节点。

因为父节点和左右子节点间的索引号有明确的规律。

章节三:信息学竞赛 CPP 进阶篇 - 图63

对于已有的堆结构,增加新的节点,仍然保持堆性质,代价很小。

image-20220316081359726

STL堆算法与其它算法一样,不会强制底层的数据结构。

但显然,如果经常在堆顶、堆底右边(堆尾)操作,用 deque 结构比较合适。

堆结构有什么价值?

效率。

比如,从一百万个元素中输出前 10 个元素。

如果用排序算法,需要统一排序,然后输出前 10 个。

而用堆,100万个元素建立堆比排序要快很多。

然后输出堆顶,用堆尾元素补空位,再调整一下,再输出下一个堆顶。。。

主要函数

  • push_heap

    向堆尾添加一个元素,并调整为新堆。

  • pop_head

    从堆顶取走元素,用堆尾元素替换,并调整为新堆。

    而取走的元素不删除,而是放入原来堆尾元素的位置。

    注意,逻辑上,此时的新堆元素个数少了一个。

  • make_heap

    把给定区间的元素建成堆。

  • sort_heap

    给定区间,执行堆排序算法。

示例

不要排序,取出序列中最大的前三个元素。

算法默认是建立大顶堆,如果希望小顶堆,需要指定另一个参数—比较器

  1. deque<int> a = {3,8,2,9,11,4,6,13,7,1,10,12};
  2. make_heap(a.begin(), a.end());
  3. pop_heap(a.begin(), a.end());
  4. pop_heap(a.begin(), a.end()-1);
  5. pop_heap(a.begin(), a.end()-2);
  6. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

输出:

章节三:信息学竞赛 CPP 进阶篇 - 图65

比较器设为:greater( )

  1. deque<int> a = {3,8,2,9,11,4,6,13,7,1,10,12};
  2. make_heap(a.begin(), a.end(), greater<int>());
  3. pop_heap(a.begin(), a.end(), greater<int>());
  4. pop_heap(a.begin(), a.end()-1, greater<int>());
  5. pop_heap(a.begin(), a.end()-2, greater<int>());
  6. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

优先队列

priority_queue 是个适配器,不是底层容器,它封装了堆逻辑。

有的时候,用起来更方便。

同样的问题,用它来解决,是这样子的:

  1. priority_queue<int> a;
  2. int t[] = {3,8,2,9,11,4,6,13,7,1,10,12};
  3. for(int i=0; i<sizeof(t)/sizeof(int); i++)
  4. a.push(t[i]);
  5. cout << a.top() << endl; a.pop();
  6. cout << a.top() << endl; a.pop();
  7. cout << a.top() << endl;

注意,这个适配器需要 include

如果要定制为小顶堆:

  1. priority_queue<int, deque<int>, greater<int>> a;

实时效果反馈

1. 关于大顶堆的说法, 正确的是:__

  • A 建好的堆,堆顶元素一定是最大

  • B 最小元素一定是在最底层最右边

  • C 堆中元素个数必须为 2 的整数次幂 - 1

  • D 堆是二叉树的结构,必须用指针结构来存储

2. priorityqueue 适配器的底层容器是:_

  • A vector

  • B array

  • C deque

  • D map

答案

1=>A 2=>C

查找算法

image-20220318190534973

STL提供了大量的查找算法,较常见的:

算法 含义 对区间要求
find, find_if 查找值第一次出现的位置
count, count_if 查找匹配个数
adjacent_find 查找相邻的等值元素
binary_search 二分查找,存在否 有序区间
lower_bound, upper_bound 二分查找符合的边界 有序区间
equal_range 二分查找等值区间 有序区间
search,search_n 查找子序列
find_end 查找子序列最后出现位置
find_first_of 查找给定集合中任意元素

大体上可分为:

  • 单值匹配

    搜索符合条件的元素

    • 普通查找

      要求元素要支持相等性比较(operator==)

    • 二分查找

      要求区间必须已经有序

      要求元素必须支持大小比较(operator<)

      最好能支持随机访问,advance, distance 来处理指针大跳

  • 序列匹配

    并非查找单个元素,而是要求匹配一个序列,类似于在串中查找子串

算法举例

二分搜索十分重要,自己写很容易出纰漏。

  1. int a[] = {2,5,12,8,6,3,13,25,119};
  2. const int n = sizeof(a)/sizeof(int);
  3. sort(a, a+n);
  4. copy(a, a+n, ostream_iterator<int>(cout," "));
  5. cout << binary_search(a, a+n, 25) << endl;

搜索序列:

  1. vector<int> a = {1,2,3,4,1,2,3,4,5,6,7,8,4,5,6,7};
  2. vector<int> b = {4,5,6};
  3. auto i = search(a.begin(), a.end(), b.begin(), b.end());
  4. cout << (i!=a.end()) << endl;

序列搜索也适用于子串的查找

  1. char* a = "abcd123abc123xyz";
  2. string b = "123";
  3. char* p = find_end(a, a+strlen(a), b.begin(), b.end());
  4. cout << (p-a) << endl;

算法对char* 这样的原生类型操作时,一点也不牺牲效率。

除了匹配序列,还有匹配集合中任意元素的操作:

  1. char* a = "hernameis..";
  2. string b = "aeiou";
  3. char* p = find_first_of(a, a+strlen(a), b.begin(), b.end());
  4. cout << *p << endl;

实时效果反馈

1. 关于二分搜索, 说法正确的是:__

  • A 搜索序列中不能包含相等的元素

  • B 搜索序列必须支持随机访问

  • C 搜素序列必须已经有序

  • D 序列中的元素必须支持operator==

2. equalrange 对序列中的元素要求是:_

  • A 提供相等性比较 operator==

  • B 提供大小比较 operator<

  • C 提供拷贝构造函数

  • D 提供移动构造函数

答案

1=>C 2=>B

变序算法

image-20220320202503083

改变元素的顺序,并不删除或增加元素。

主要有:

算法 含义 备注
inplace_merge 对两个有序区间merge,结果就放在这两个区间中 需要输入有序
merge 合并两个序列,放到另外的地方 需要输入有序
nth_element 以第 n 个元素为标尺,使得小于它的在前,其它在后
partial_sort 部分排序,排序元素放在 [first, middle) 输入区间为:[first, middle, last)
partition 把元素分两组,使用输入谓词判定,false在前,true在后
random_shuffle 洗牌
reverse,reverse_copy 反序
rotate, rotate_copy 元素轮换位置,middle为开始元素 输入:[first, middle,last)
sort 升序,原地排序,可指定比较规则
stable_sort 与sort类似,保持相等元素的顺序关系

示例

inplace_merge 不需要额外空间,在原地实现归并算法

  1. vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
  2. sort(a.begin(), a.begin()+5);
  3. sort(a.begin()+5, a.end());
  4. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
  5. inplace_merge(a.begin(), a.begin()+5, a.end());
  6. cout << "\n";
  7. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

章节三:信息学竞赛 CPP 进阶篇 - 图68

partial_sort 可以实现只保证获得 前 n 个有序元素(但待排序区间是整个区间)

  1. vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
  2. partial_sort(a.begin(), a.begin()+5, a.end());
  3. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

运行结果:

章节三:信息学竞赛 CPP 进阶篇 - 图69

而,partition 则是根据谓词函数,把整个序列分成两个部分

  1. vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
  2. auto it = partition(a.begin(), a.end(), [](int x){
  3. return x % 2 == 0;
  4. });
  5. cout << (it-a.begin()) << endl;
  6. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

运行结果:

章节三:信息学竞赛 CPP 进阶篇 - 图70

此谓词函数位置,可以用函数,函数对象,或者匿名函数

stable_sort与sort 的区别是对等值元素保持原来的顺序

这对用户自定义类型很有价值。

  1. struct STU{
  2. string name;
  3. int age;
  4. bool operator> (const STU& t) const {
  5. return name > t.name;
  6. }
  7. };
  8. int main()
  9. {
  10. vector<STU> a = {
  11. STU{"zhang", 20},
  12. STU{"wang", 30},
  13. STU{"li", 25},
  14. STU{"wang", 20},
  15. STU{"zhao", 22},
  16. STU{"wang", 50},
  17. STU{"wang", 12},
  18. STU{"li", 15}
  19. };
  20. sort(a.begin(), a.end(), greater<STU>());
  21. for(int i=0; i<a.size(); i++){
  22. cout << a[i].name << ", " << a[i].age << endl;
  23. }
  24. return 0;
  25. }

比较器的控制,默认是 less,此处换成了 greater

注意,这样做,同时要修改STU中的比较函数: operator< 改为: operator>

章节三:信息学竞赛 CPP 进阶篇 - 图71

rotate 则实现了轮换的效果,就是把头上的元素删除,并添加到尾部。

用这个思路做一版约瑟夫环问题:

  1. vector<int> a = {1,2,3,4,5,6,7,8,9,10};
  2. while(a.size()>1){
  3. rotate(a.begin(), a.begin()+1, a.end()); // 模拟报数 1
  4. rotate(a.begin(), a.begin()+1, a.end()); // 报数 2
  5. rotate(a.begin(), a.begin()+1, a.end()); // 报数 3
  6. a.pop_back(); // 报 3 的人删去
  7. }
  8. cout << a[0] << endl;

实时效果反馈

1. 关于变序算法, 说法==错误==的是:__

  • A 不会增加新的元素,不会删除已经存在的元素

  • B 会改变已经存在的元素的存储位置

  • C 相等的元素也可能被移动位置

  • D 同样的数据和算法,两次运行的结果一定相同

2. sort排序算法,说法==错误==的是:__

  • A 对相等的元素,不能保证它们原来的位置关系

  • B 可以在数组结构上排序,也可以在list上,但效率不高

  • C 可以给定比较器,来决定元素顺序的定义

  • D 当元素数目 n 很大的时候,sort 的复杂度大约 ${O(n^2)}$

答案

1=>D 2=>D

删除与替换

image-20220321105505349

删除和替换的共性是:销毁了一些已经存在的信息,需要小心使用,

如果将来还需要这些信息,可以先暂存在其它地方。

算法 含义 备注
copy, copy_backward 复制序列,反向复制 不保证能正确处理重叠区(overlap)
iter_swap, swap 交换元素 支持移动语义
remove, remove_if 删除 不改变容器size,仅把后边元素向前覆盖
remove_copy,remove_copy_if 把处理结果拷贝到新位置 目标位置必须自己预留空间
replace, replace_if 旧值用新的值代替
replace_copy, replace_copy_if 替换后的结果放别处 自己留好空间
swap_ranges 指定范围的元素,与另一个序列交换
unique 清楚序列中重复元素 连续重复的才删除,否则需要先有序
unique_copy 除去重复,不过结果放它处

示例

删除算法并不会真的去删除元素,从而导致容器size的变化。

remove 只是把后边的元素向前拷贝,覆盖掉删除的元素。

下例,删除了所有负数:

  1. vector<int> a = {2,5,-3,0,19,-8,-6,2,-1,12,-7};
  2. auto i = remove_if(a.begin(), a.end(),
  3. bind2nd(less<int>(),0));
  4. cout << (i-a.begin()) << endl;
  5. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图73

这里的谓词函数用了函数式编程技巧。

下例,把所有的负数替换为0,并且写到新的位置。

  1. vector<int> a = {2,5,-3,0,19,-8,-6,2,-1,12,-7};
  2. vector<int> b;
  3. replace_copy_if(a.begin(), a.end(),
  4. back_insert_iterator<vector<int>>(b),
  5. [](int x){ return x < 0; },
  6. 0);
  7. copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));

章节三:信息学竞赛 CPP 进阶篇 - 图74

这里使用了后插入迭代器,而不是先开避足够的空间。

谓词函数的位置使用了匿名函数。

下一个例子,演示了如何去掉重复

  1. vector<int> a = {1,2,5,2,3,6,2,1,3,4,4};
  2. sort(a.begin(), a.end());
  3. auto i = unique(a.begin(), a.end());
  4. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
  5. a.resize(i-a.begin());
  6. cout << "\n";
  7. copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图75

实时效果反馈

1. 使用STL算法,而不是自己写循环, 好处是:__

  • A 达到最高的效率

  • B 代码更加简洁,对初学者更友好

  • C 健壮性与通用性,将来更易于维护

  • D 更酷炫,显档次

2. 关于STL删除(remove系列)算法,说法==错误==的是:__

  • A 不会真的删除,只是后边的元素向前移动

  • B 可能会使得容器size变小

  • C 不能对 map, set 操作

  • D 可以自定义谓词函数,决定删除哪些元素

答案

1=>C 2=>B

数值算法

image-20220321160221606

这里的数值运算是广义的。

它支持我们自己定义的任何运算。

注意:大部分需要包含 numeric 头文件

算法 含义 备注
min,max,minmax 求最大最小值 不能对容器操作
min_element, max_element 求区间元素的极值 对容器
accumulate 对区间元素求累加和 “和”是广义的
partial_sum 累积过程
inner_product 两个序列的内积 广义
adjacent_difference 相邻元素的差 相当于离散的微分

示例

求极值十分常用:

  1. int a=5, b=8, c=4;
  2. cout << max(a,b) << endl;
  3. cout << min({a,b,c}) << endl; //可以对多个变量求极值
  4. string x = "thisiacupofcoffeeplease";
  5. auto p = max_element(x.begin(), x.end());
  6. cout << *p << endl;

串也可当作容器来操作,它在很多功能上提供了两人API

下例,序列求和:

  1. int a[] = {1,2,3,4,5};
  2. cout << accumulate(a,a+5,0) << endl;
  3. int x = accumulate(a, a+5, 0,
  4. [](int a, int b){ return a*10+b;});
  5. cout << x << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图77

有的时候,我们需要累加的中间过程,可以如下:

  1. int a[] = {1,2,3,4,5};
  2. int b[5];
  3. partial_sum(a, a+5, b);
  4. copy(b,b+5,ostream_iterator<int>(cout," "));

章节三:信息学竞赛 CPP 进阶篇 - 图78

adjacent_difference 是求相邻项的差,

章节三:信息学竞赛 CPP 进阶篇 - 图79

比如,已知月产量,求环比最大项:

  1. vector<double> a = {1.5, 2.2, 3.5, 4.1, 4.6, 10.8, 11.9};
  2. vector<double> b(a.size());
  3. adjacent_difference(a.begin(), a.end(), b.begin(),
  4. [](double a, double b){
  5. return (a-b)/b*100; } );
  6. copy(b.begin(), b.end(), ostream_iterator<double>(cout, " "));
  7. cout << endl << *max_element(b.begin(), b.end());

章节三:信息学竞赛 CPP 进阶篇 - 图80

实时效果反馈

1. 当不能使用 minelement 的时候,如何求vector序列的最值较好:_

  • A 自己写循环,遍历vector

  • B 用 for_each 自己累积

  • C 用 accumulate 自定义“和”的行为: min(a,b)

  • D 用 inner_product

答案

1=>C

生成与变异

章节三:信息学竞赛 CPP 进阶篇 - 图81

通过运算,产生新的数据。

有的修改了原序列,有的不修改原来的序列。

算法 含义 备注
fill, fill_n 用给定的元素填充 改变容器中值
for_each 用指定的函数依次访问元素 不改变容器
generate, generate_n 用发生器函数来填充
transform 对输入序列中元素进行运算,产生新序列 可以改变或不改变容器

示例

for_each 对其Iterator范围内的元素,调用用户提供的函数fn

可以进行显示或其它复杂的,比如累积性的操作。

  1. vector<int> a = {1,2,3,4,5};
  2. int sum = 0;
  3. for_each(a.begin(), a.end(),
  4. [&sum](int x){ sum += x; });
  5. cout << sum << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图82

相比于 accumulate, for_each 更加灵活,可以支持不同于元素类型的累积结果。

  1. vector<int> a = {1,2,3,4,5};
  2. string sum;
  3. for_each(a.begin(), a.end(),
  4. [&sum](int x){ sum = sum + to_string(x) + ","; });
  5. cout << sum << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图83

这个匿名函数使用了引用的方式绑定了外部的变量。

generate 提供了比 fill 更丰富的填充方式,利用一个函数不断产生新的数据。

  1. int a[10];
  2. int i = 0;
  3. generate(a, a+10,
  4. [&i](){ ++i; return i*i; });
  5. copy(a,a+10,ostream_iterator<int>(cout," "));

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图84

transform 的功能很强,大体相当 于函数式语言中的 map

可以由用户指定的函数对序列中的元素进行变换。

  1. int a[] = {1,2,3,4,5};
  2. transform(a, a+5, a, [](int x){ return x*5; });
  3. copy(a,a+5,ostream_iterator<int>(cout," "));

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图85

我们也可以对两个序列进行运算,生成新的结果:

  1. int a[] = {1,2,3,4,5};
  2. int b[] = {5,4,3,2,1};
  3. int c[5];
  4. transform(a, a+5, b, c,
  5. [](int x, int y){ return x*y; });
  6. copy(c,c+5,ostream_iterator<int>(cout," "));

章节三:信息学竞赛 CPP 进阶篇 - 图86

实时效果反馈

1. 当STL算法中需要用户自定义行为时, 哪项==不可以==作为实参:__

  • A 自定义的函数

  • B 自定义类生成的函数对象

  • C 匿名函数

  • D 函数模板

2. 很多高级语言中有zip操作,可以把[1,2,3],[a,b,c] 变成:[(1,a),(2,b),(3,c)],

在c++STL中应该用哪个算法实现这个功能?__

  • A for_each

  • B transform

  • C accumulate

  • D 没有对应的实现

答案

1=>D 2=>B

关系算法

image-20220323084834389

并不改变参加运算的序列,只是对它们的关系进行某种判定。

主要算法:

算法 含义 备注
equal 两个序列是否等值
mismatch 两个序列首个不匹配的位置
includes 一个序列的所有元素是否包含在另一序列中 序列需要有序
lexicographical_compare 字典比较时,是否第一个序列小 可自定比较规则
all_of 是否所有元素都有某个性质
any_of 最否存在任一元素满足要求
none_of 是否没有任何元素满足要求

基本示例

equal 比较两个序列区间,“相等”的含义可以自己定义。

  1. int a[] = {1,2,3,4,5,11,12,13,14,15};
  2. bool x = equal(a,a+5,a+5,
  3. [](int a, int b){ return a%10==b%10;});
  4. cout << x << endl;

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图88

any_of 等是 c++11 引入的特性。

  1. vector<int> a = {2,5,1,6,-3,0,12,15,-4};
  2. if(any_of(a.begin(), a.end(), bind2nd(less<int>(),0))){
  3. cout << "find negative element" << endl;
  4. }

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图89

这里的函数部分传入的是:STL组合函数

虽然STL的算法很多,但最常使用的却有限。

5位数的黑圈问题

任意一个5位数,重新排列其各个十进制位,可得最大的数 a, 最小的 b(可以前导零)

求 a-b,如果不足5位,前补零。

不断重复这个过程,必然落入一个循环圈。

求这样循环圈一共有几个?

例如:1002 => (2100 - 0012=2088) => (8820 - 0288=8532) => ….

思路:x -> x’ -> x’’ 不断加入 vector, 若发现重复,则发现了一个解

收集所有的解,只要不重复,就可以了。

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <vector>
  4. #include <iterator>
  5. using namespace std;
  6. int next(int x)
  7. {
  8. string s = to_string(x);
  9. while(s.length()<5) s.push_back('0');
  10. sort(s.begin(), s.end());
  11. int a = stoi(s);
  12. reverse(s.begin(), s.end());
  13. int b = stoi(s);
  14. return b - a;
  15. }
  16. vector<int> f(int x)
  17. {
  18. vector<int> a;
  19. while(true){
  20. auto i = find(a.begin(), a.end(), x);
  21. if(i != a.end()){
  22. a.erase(a.begin(),i);
  23. return a;
  24. }
  25. a.push_back(x);
  26. x = next(x);
  27. }
  28. }
  29. int main()
  30. {
  31. vector<int> da;
  32. for(int i=10000; i<=99999; i++) {
  33. vector<int> t = f(i);
  34. if(find(da.begin(), da.end(), t[0]) == da.end()){
  35. copy(t.begin(), t.end(),
  36. back_insert_iterator<vector<int>>(da));
  37. da.push_back(-1);
  38. }
  39. }
  40. cout << count(da.begin(), da.end(), -1) << endl;
  41. copy(da.begin(), da.end(),
  42. ostream_iterator<int>(cout, " "));
  43. return 0;
  44. }

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图90

实时效果反馈

1. 使用STL函数模板,判断[2,1,5], 是否包含在[1,2,3,4,5]中,方法是:__

  • A 用 includes

  • B 用 sort 和 includes

  • C 用 find

  • D 用 sort 和 find

2. 怎样从vector中删除元素?__

  • A 用 vector 的 erase

  • B 用 vector 的 remove

  • C 用 STL 的 erase

  • D 用 STL 的 remove

答案

1=>B 2=>A

其它算法

image-20220323105321951

生成排列组合

next_permutation 当前排列的下一个排列

prev_permutation 当前排列的前一个排列

集合算法

set_union, set_intersection, set_difference, set_symmetric_difference

前边课程中讲过

堆算法

make_heap, pop_heap, push_heap, sort_heap 等,前边课程中讲过

示例

生成全排列:

  1. string s = "ABCD";
  2. do{
  3. cout << s << " ";
  4. }while(next_permutation(s.begin(), s.end()));

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图92

八皇后问题

前边的课程中讲过。

基本的条件是:每行放一个皇后,每列放一个皇后。

所以结果可以用一个数组表示,包含8个元素,第个数字表示该行的皇后放在哪一列。

显然,数组中的数字不能重复,否则就同列了,因而必然包含所有 0 ~ 7 的数字。

这个问题,就变成,求 01234567 的全排列,然后

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <array>
  4. using namespace std;
  5. bool ok(array<int,8> a)
  6. {
  7. for(auto i=a.begin(); i!=a.end(); ++i){
  8. for(auto j=i+1; j!=a.end(); ++j){
  9. if(j-i==abs(*i-*j)) return false;
  10. }
  11. }
  12. return true;
  13. }
  14. void show(array<int,8> a)
  15. {
  16. for_each(a.begin(), a.end(),
  17. [](int x){
  18. string s(8, '.');
  19. s[x] = '*';
  20. cout << s << endl;
  21. });
  22. }
  23. int main()
  24. {
  25. array<int,8> a = {0,1,2,3,4,5,6,7};
  26. do{
  27. if(ok(a)) {
  28. show(a);
  29. break;
  30. }
  31. }while(next_permutation(a.begin(),a.end()));
  32. return 0;
  33. }

实时效果反馈

1. 使用nextpermutation, 对[1,2,2,3]求全排,有多少项?_

  • A 24

  • B 36

  • C 12

  • D 10

2. 当使用array为函数参数时,说法正确是:__

  • A 按值传递,形参会拷贝实参的每个元素

  • B 按指针传递,与实参共享同一组元素

  • C 按值传递,但元素个数与实参不必相同

  • D 按指针传递,可以指定不同的元素个数

答案

1=>C 2=>A

智能指针

image-20220324143443568

在c++ 的开发中,我们很熟悉内存泄漏问题。

时刻要注意: new 与 delete 的成对儿出现。但这很难做到。

  1. MyA* p = new MyA();
  2. dosomething(p); // 如果这个函数中抛出了异常就。。。。
  3. delete p;

智能指针就能解决这个问题,防止我们忘记 delete

  1. #include <memory>
  2. ...
  3. auto_ptr<MyA> p(new MyA());
  4. dosomething(p.get());
  5. ...

p表面是个指针,可以像指针一样操作,但实际是对象,在生存期结束时会自动 delete

实践中, auto_ptr 有一大堆的坑,要处处注意。

比如:不能指向数组,不能共享所有权,不能做为容器的元素,不能作为参数传递。。。

指针赋值,reset(), release() 使用不当等。。

auto_ptr 重载了operator=,两个auto_ptr间赋值,会导致右值被置为空。

传参也有类似的效果。

c++11 中把 auto_ptr 标记为 [deprecated],其功能可以用unique_ptr 代替。

c++11中提供的智能指针有:

智能指针 含义 说明
unique_ptr 独占的(排它的) 只能一个指针有控制权,可以移交
shared_ptr 共享控制权(引用计数) 引用计数为0时,释放
weak_ptr 弱引用 常与shared_ptr配合,不参与计数

unique_ptr

unique_ptr 与 auto_ptr 类似,但更安全,更容易使用。

  1. struct MyA{
  2. ~MyA(){ cout << "MyA destroy..." << endl; }
  3. void f() { cout << "MyA.f()..." << endl; }
  4. };
  5. int main()
  6. {
  7. // MyA* p = new MyA();
  8. unique_ptr<MyA> p(new MyA);
  9. p->f();  // 对象冒充指针
  10. return 0;
  11. }

unique_ptr 是独占的,不能两个指针指向同一对象。但可以转移控制权。

  1. unique_ptr<MyA> p(new MyA);
  2. p->f();
  3. unique_ptr<MyA> q;
  4. q = p; // 编译出错

改为:q=move(p); 则可以。

因为 p 变成了空指针,q接管了控制权。

传入到其它函数中,也是同样道理。

传引用可以,直接传值不可以,但可以用 move 移动语义。

  1. #include <iostream>
  2. #include <memory>
  3. using namespace std;
  4. struct MyA{
  5. ~MyA(){ cout << "MyA destroy..." << endl; }
  6. void f() { cout << "MyA.f()..." << endl; }
  7. };
  8. void g(unique_ptr<MyA> r){ }
  9. int main()
  10. {
  11. unique_ptr<MyA> p(new MyA);
  12. p->f();
  13. g(move(p));
  14. return 0;
  15. }

注意,include

shared_ptr

shared_ptr 允许多个指针指向同一个对象,那么谁负责释放资源呢?

有一个引用计数被关联到对象,记录着指向对象的指针数目,为0,则产生释放。

  1. struct MyA{
  2. ~MyA(){ cout << "MyA destroy..." << endl; }
  3. void f() { cout << "MyA.f()..." << endl; }
  4. };
  5. void g(shared_ptr<MyA> r){ r->f(); }
  6. int main()
  7. {
  8. shared_ptr<MyA> p = make_shared<MyA>();
  9. shared_ptr<MyA> q = p;
  10. g(p);
  11. cout << p.use_count() << endl; // 有引用计数的概念
  12. return 0;
  13. }

shared_ptr有个著名的 循环引用问题:

  1. struct Node{
  2. int data;
  3. shared_ptr<Node> next;
  4. };
  5. int main()
  6. {
  7. shared_ptr<Node> p(new Node());
  8. p->next = p;
  9. cout << p.use_count() << endl;
  10. return 0;
  11. }

表面看起来,shared_ptr很完美,似乎可以解决动态内存问题,实际上远远不够。

章节三:信息学竞赛 CPP 进阶篇 - 图94

比如:如果出现了环状结构就惨了。

有些场景可以用weak_ptr 代替 shared_ptr,避开循环引用,

但不是万能,仅仅靠引用计数实现自动内存管理,尚无完美理论解法。

weak_ptr 不会引起引用计数变化,其它与shared_ptr相同,可以与shared_ptr 协同工作。

实时效果反馈

1. 关于autoptr, 说法正确的是:_

  • A c++11引入的智能指针

  • B 创建时必须初始化

  • C 一个auto_ptr赋值给另一个 auto_ptr 会编译错

  • D c++11 中已标为废弃

2. 对weakptr 说法==错误==的是:_

  • A 可以创建weak_ptr,不指向任何对象

  • B 可以从另一个 weak_ptr拷贝构造 weak_ptr

  • C 可以从另一个 shared_ptr拷贝构造 weak_ptr

  • D 一般与 unique_ptr 搭配使用

答案

1=>D 2=>D

散列表

image-20220324204225343

hash_map 在很多编译器中都在用,所谓既成事实。

但hash_map并非 c++ 标准。直到 c++11 才引入了标准的散列系列。

unordered_map, unordered_multimap

unordered_set, unordered_multiset

分别是 , 头文件中,可以用于需要散列的场景。

这里讲 unordered_map,余可类推。

散列原理

unordered_map在逻辑上,也是存储《键-值》对儿,但与map机制不同。

它从key 用一个散列公式计算,得出存储位置,计算量与规模无关。

精心设计 hash 公式,可以使得:这个位置在大概率上不会与其它key的计算结果相同。

如果相同,也非灾难,即所谓:碰撞

章节三:信息学竞赛 CPP 进阶篇 - 图96

有很多解决方案。比如:就存在一起好了。保存可能多个pair的这个东西叫:bucket

可以翻译为:

所以,unordered_map 对元素的要求与 map 不同。

  • key 不需要排序,因而不需要重载:operator<
  • key 需要相等性比较,因而需要重载: operator==

使用unordered_map 而不是map的理由:

当数据规模很大的时候,比如100万。

map 的查找复杂度大约 20 次,而unordered_map 是常数,与规模无关。

另外,元素不要求排序能力。

在小规模数据上,使用哪个差别甚微。

unordered_map 的成员方法

unordered_map 属于关联型容器,也属于无序型容器。

其含有的特殊方法:

方法 含义 备注
bucket_count() 当前的桶的数量 即可放键值对的位置有多少个
bucket(key) key为键的桶的编号
bucket_size(n) n 号桶中键值对的个数 一般都是 0或1, 除非发生hash碰撞
load_factor() 负载因子 元素个数 / 桶的个数
rehash(n) 重设桶的数量

示例

  1. typedef unordered_map<string, int> my_map;
  2. int main()
  3. {
  4. my_map a = {{"us",10},{"uk",20},{"fr",30},{"de",40}};
  5. for(int i=0; i<a.bucket_count(); ++i){
  6. cout << "bucket " << i << ": " << "has ";
  7. cout << a.bucket_size(i) << endl;
  8. }
  9. cout << a["de"] << endl;
  10. cout << a.at("cc") << endl;
  11. return 0;
  12. }

章节三:信息学竞赛 CPP 进阶篇 - 图97

实时效果反馈

1. 关于用散列表的map,比起用红黑树的map, 优点是:__

  • A 数据量很大时,查找效率更高

  • B 比红黑树更省空间

  • C 可以用string作为 key

  • D 可以修改 key

2. 设计散列表时,如何处理碰撞问题?下列哪项==最不可取==:__

  • A 顺序探测,存在下一个最近的空位。

  • B 使用再散列公式,重新得到一个位置

  • C 使用溢出链表存储碰撞的数据

  • D 在一块单独的区域顺序存储

答案

1=>A 2=>D

boost库

image-20220325082252892

boost 是c++的一个标准库,可以看作STL的延续和扩充。

它秉承的理念和思想也是模板泛型,与STL一脉相承。

STL集中在算法部分,而boost更加实用,提供了大量的工具类,可以应用到相当具体、多样的场景中去,因而,boost提供的类库十分丰富、庞大。

Boost社区发起人Dawes本人就是c++标准化委员会的成员之一,其内容经常成为下一代c++标准库的内容,对c++社区影响巨大,“准标准”的名头不折不扣。

Boost以源码的方式提供,商业与非商业的使用都是允许并且鼓励的。

Boost强调可移植性,强调最佳实践,强调c++标准。虽然有一些大胆、前卫、实验性质的东西,但大体上可以有工业级的质量,其稳定性、健壮性很不错。

综上,如果c++标准库无法满足要求,boost库是首选。

大致分类

  • 字符串与文本处理

    conversion(c++ 类型转换增强), format(实现类似printf的格式化对象)

    regex, spirit, string algo(字符串相关算法),tokenizer(把字符串拆成一组记号),

    wave(预处理器),xpressive(免编译正则库)

  • 容器库

    array, bitmap, circular buffer, disjoint sets, dynamic bitset, GIL(通用图像库),

    ICL(区间容器),intrusive(侵入式容器和算法), multi-array(多维数组), multi-index,

    pointer container(指针容器),property map, property tree, unordered(散列容器),variant(复杂类型联合体)

  • 迭代器类

    GIL,graph, iterators, operators(用少量操作符生成操作符重载), tokenizer

  • 算法库

    foreach, GIL, graph, min-max, range, string algo, untility

  • 函数对象与高阶函数

    bind,function, functional, functional/factory(工厂模式),functional/forward

    functional/hash, lambda(匿名函数),ref, signals(观察者模式), phoenix(函数式编程)

  • 泛型编程库

    call traits, concept check, enable if, function types, inplace factory, operators, property map, static assert(编译期诊断), type traits, TTI

  • 模板元编程

    fusion(编译时tuple容器与算法),MPL, proto(嵌入式),static assert type traits

  • 并发编程

    asio(异步IO),interprocess(进程间通信), MPI, thread(增强的线程处理能力),

    context(协程),atomic, corontine(协程支持),lockfree(无锁数据结构)

  • 数字与数学

    accumulators, integer, interval(区间概念),math, math common factr,

    math octonion(八元数),math quaternion(四元数),numeric conversion,

    random, rational(有理数),uBLAS(线性代数),geometry(几何库),ratio(编译期分数),

    multiprecision(高精度)

  • 数据结构

    any, bitmap, compressed pair(优化的pair), fusion, ICL, multi-index, pointer container, property container, tuple, uuid, variant, heap, type erasure(运行时多态)

  • 输入输出

  • 内存管理

    pool, smart(智能指针), utility

  • 杂类

    CRC, Date Time, flyweight 享元模式, …

安装boost

boost库以源码形式提供,大部分库就是个可包含的头文件。

少数库需要编译后的动态库的支持。boost提供了unix和windows上的编译配置。

如果没有特殊需要,只要解压安装包,稍加配置即可,不用安装编译过程。

  • 从官网下载某个版本的安装包

    章节三:信息学竞赛 CPP 进阶篇 - 图99

  • 解压在某个位置,内部目录大致:

    章节三:信息学竞赛 CPP 进阶篇 - 图100

    其中的 boost 是主要目录,内有很多头文件,直接包含了模板的定义

  • 在 qt 的工程配置文件中增加

    章节三:信息学竞赛 CPP 进阶篇 - 图101

    目录位置要写 boost 主目录的上一级目录的位置。

使用示例

c++11 才提供 lambda表达式的写法,而且语法比较保守。

如果不舒服,用 boost 提供的 lambda 表达式试试,有很前卫的感觉。

  1. #include <boost/lambda/lambda.hpp>
  2. #include <iostream>
  3. #include <algorithm>
  4. using namespace std;
  5. int main()
  6. {
  7. using namespace boost::lambda;
  8. int a[] = {1,2,3,4,5};
  9. transform(a, a+5, a, (_1 * 2));
  10. for(auto i: a){
  11. cout << i << endl;
  12. }
  13. }

这里同时也使用了 c++11 提供的 for range 循环的语法。

实时效果反馈

1. 关于boost, 说法==错误==的是:__

  • A boost的很多库已经成为了c++新标准的特性

  • B boost十分庞大,是一系列库的集合

  • C boost项目的初衷是为c++标准化工作提供可参考的实现

  • D boost主体在不同平台上有不同的二进制库

答案

1=>D

boost串处理

image-20220327091512792

同STL的其它类相比,string类已经算是比较臃肿的了,但还是有许多常见功能没有提供。

比如:java中的 trim, endswith, join, split 等等,甚至是大小写转换。

只要是“人有我无”的东西,就到 boost 海洋里翻吧,十有八九能找到。

首先,要包含头文件。

很多功能分散在各个文件中,为方便,有个大全型的 string.hpp

  1. #include <boost/algorithm/string.hpp>

一般为方便,还要引入名字空间:

using namespace boost::algorithm;

  • 大小写转换

    1. using namespace boost::algorithm;
    2. string s = "abcdAB";
    3. cout << to_upper_copy(s) << endl;
    4. to_lower(s);
    5. cout << s << endl;

    这里的STL惯例:

    有 _copy 的,表示不修改输入参数,而是生成新的串对象

  • 删除匹配的字符串

    1. string s = "abc,123;xyz,1111,123,ttt";
    2. cout << erase_first_copy(s,"123") << endl;
    3. cout << erase_nth_copy(s,",",2) << endl;
    4. cout << erase_last_copy(s,",") << endl;
    5. cout << erase_all_copy(s,",") << endl;
    6. cout << erase_head_copy(s,3) << endl;
    7. cout << erase_tail_copy(s,3) << endl;

    章节三:信息学竞赛 CPP 进阶篇 - 图103

    类似的还有:

    replace_first_copy, replace_all_copy 等

  • 串的连接

    1. string a[] = {"zhang", "wang", "li", "zhao"};
    2. cout << join(a, ",") << endl;

    章节三:信息学竞赛 CPP 进阶篇 - 图104

  • 去空白

    1. string s = " abc ";
    2. cout << "|" << trim_left_copy(s) << "|" << endl;
    3. cout << "|" << trim_right_copy(s) << "|" << endl;
    4. cout << "|" << trim_copy(s) << "|" << endl;

    还可以自定义什么叫空白

    1. string s = "abc ,,; .";
    2. cout << "|" <<
    3. trim_copy_if(s, is_any_of(",; .")) << "|" << endl;
  • 子串匹配

    1. string s = "abc1234xyz";
    2. cout << starts_with(s, "abc") << endl;
    3. cout << ends_with(s, "xyz") << endl;
    4. cout << contains(s, "123") << endl;
  • 串的拆分

    1. string s = "123,abc,xyz,,qqq";
    2. vector<string> v;
    3. split(v, s, [](char x){return x==',';});
    4. for(auto i: v) cout << i << " ";
  • 忽略大小写

    如果希望不要区分大小写字母,可以用以 i 开头的版本。

    比如: erase_all_copy 对应: ierase_all_copy

实时效果反馈

1. 关于boost中的串算法, 说法正确的是:__

  • A 所有算法都在 algorithm/string.hpp 这个源文件中。

  • B 带有 _copy 字样的名字,表示需要输出迭代器

  • C 带有_if 字样的名字,表示需要使用方提供一个谓词函数

  • D 如果希望不区分大小写,需要调用增加一个参数的重载版本

2. 如果想把”abc, ,,”尾巴上的逗号或空格全去掉,返回一个新串,应该用:__

  • A trim_all_copy

  • B trim_right_copy

  • C trim_right_copy_if

  • D trim_right_if

答案

1=>C 2=>C

boost格式化

image-20220328085832640

ostream 提供了与 printf 对应的精确输出控制,但总有人觉得冗长,不方便。

使用 cout 比直接使用 printf 的格式化串的好处是:类型安全。

boost 的format库提供了类似printf 格式控制符的方案,同时能够保证类型安全。

并且,format库引入了很多新特性,使得format工作更方便、更精细、更完美。

  • 头文件

    1. #include <boost/format.hpp>

boost::format 类实例化时,传入格式化串

之后,可以喂入被格式化的项,然后输出,或转为string。

其中的格式化串的写法,支持 3 大类风格:

  • 位置标记(最简单)

    1. boost::format fmt("%2% - %3% - %1%");
    2. fmt % 2008 % 8 % 31;
    3. cout << fmt << endl;
    4. // string s = fmt.str(); // 取出结果为string
    5. // 同一个位置符,可以出现多次。比如:"%1%-%2%-%1%"

    章节三:信息学竞赛 CPP 进阶篇 - 图106

    这种位置占位符的风格在现代高级语言中很流行,比如:python, go 等

  • 继承并强化了printf的格式化串

    %[N$] [flag] [width] [.precision] type-char

    N$ 是可选的,要么都省略,要么都有

    1. boost::format fmt("|%1$10.2f|%1$-10.3f|%1$=10.2f");
    2. fmt % 3.1415936;
    3. cout << fmt << endl;
    4. cout << boost::format("%b,%d,%04x")%true%254%254 << endl;

    结果:

    章节三:信息学竞赛 CPP 进阶篇 - 图107

  • 设置成打印风格,更直观点

    %|spec|

    其实就是 printf 风格的控制符

  • boost新的格式说明符

    %10t 保证前边有10个位置,为了下一行对齐

    %10Tx 同上,用 x 作为被位的填充符

    1. cout << boost::format("a%20tbcd") << endl;
    2. cout << boost::format("aa%20tbbbb") << endl;
    3. cout << boost::format("aaaa%20T*bbbbb") << endl;

    结果:

    章节三:信息学竞赛 CPP 进阶篇 - 图108

format 对象是可以反复使用的,这在大量输出同样格式数据时,很有用。

  1. int a = 5, b = 6, c = 128;
  2. boost::format fmt("%s=%d, %010p");
  3. cout << fmt % "a" % a % &a << endl;
  4. cout << fmt % "b" % b % &b << endl;
  5. cout << fmt % "c" % c % &c << endl;

章节三:信息学竞赛 CPP 进阶篇 - 图109

实时效果反馈

1. boost的format库,提供了类似printf的格式化, 下列格式化串中=的含义是:__

“%=12.3f”

  • A 精确输出

  • B 居中对齐

  • C 小数位不足补0

  • D 无论正数负数,都输出前导符号

2. boost的format库,在格式化串中,可以 %10t,其含义是?:__

  • A 提示下一项需要对齐输出,从位置11开始

  • B 强迫下一项从位置10开始输出

  • C 插入10个制表符

  • D 宽度为10,用户自定义类型的输出

答案

1=>B 2=>A

boost大整数

image-20220328095757704

python中直接支持大整数,java中有BigInteger,c++中要使用大整数,经常是自己动手。

这样做是重复造轮子,不安全,而且低效。

boost的数学库中提供了对高精度数值的支持,就包括任意精度的大整数。

image-20220325154626909

已经重载了常见运算符,使用起来与普通的整数没什么大区别。

示例-斐波那契数列

  1. #include <boost/multiprecision/cpp_int.hpp>
  2. #include <iostream>
  3. using namespace std;
  4. int main()
  5. {
  6. using namespace boost::multiprecision;
  7. cpp_int a = 0;
  8. cpp_int b = 1;
  9. int n = 100;
  10. for(int i=0; i<n; ++i){
  11. cpp_int c = a + b;
  12. a = b;
  13. b = c;
  14. }
  15. cout << a << endl;
  16. }

结果:

章节三:信息学竞赛 CPP 进阶篇 - 图112

与string间的转换也简单

  1. cpp_int a("123456789");
  2. a = a * cpp_int("987654321");
  3. string s = a.str();
  4. cout << s << endl;

实时效果反馈

1. boost的cppintbackend是高精度整数实质类型, 为了使用方便,在止基础上又定义了许多包装类型,其中==不==包括:__

  • A cpp_int

  • B cpp_rational

  • C int256_t

  • D big_int

2. checkedint128t与int128t的区别是:_

  • A 除 0 是否会抛异常

  • B 溢出是否抛异常

  • C 默认初始化是否为0

  • D 从string转换时是否抛异常

答案

1=>D 2=>B

正则表达式

image-20220328154950679

正则表达式对文本的处理很有用,但一直没有纳入到c++标准,很多人用boost库。

终于,c++11把正则标准加进来,其实现并非是boost, 性能、bug等都待时间检验。

如果能用c++11,当然最好不再依赖boost,毕竟标准是难以抗拒的。

先要嵌入头文件:

  1. regex re("\\d+");
  2. cout << regex_match("12345", re) << endl;
  3. cout << regex_match("12ab345", re) << endl;

这段代码判断一个串与 正则 模式是否匹配。

先要 regex 类实例化一个对象,其参数是一个串,即:正则表达式。

正则有自已的文法,与c++语法不相干,其复杂度堪比一门独立的语言。

历史上,存在不同版本的正则文法,其大部分内容是相同或相近的。

c++内置了对多种文法的支持,创建时可以给参数。

文法 说明 备注
ECMAScript javascript中即是 默认选项
basic 基本的 POSIX 正则语法
extended 扩展的POSIX 正则语法
awk awk 工具
grep grep 工具
egrep grep 工具

正则表达式中经常用转义符,写在串常量中,又要再次转义,很不方便。

这时候,要以使用 raw string 字面量

R”(正则表达式本身)”

“\d+” 可以写为: R”(\d+)”

有很多符号是正则表达式的特殊符号,有特殊含义,如果使用这些字符本身,需要转义。

字符 含义
. 匹配任意字符
[ 字符类开始
] 字符类结束
{ 重复量词开始
} 重复量词结束
( 子组开始
) 子组结束
\ 转义符
\\ 转义符自身
* 量词,0个或多个
+ 量词,1个或多个
量词,0个或1个
^ 行开始,或者是:否定
$ 行结束
\n 换行
\t 制表符
\xhh 用两位十六进制表示的 unicode字符
\xhhhh 用4位十六进制表示的 unicode 字符
  • 字符的分类
字符分类 同义表示法 说明
[[:alnum:]] [a-zA-Z0-9] 字母和数字
[[:alnum:]_] \w 字母数字,以及下划线
[^_[:alnum:]] \W 非字母数字以及下划线
[[:digit:]] \d [0-9] 数字
[^[:digit:]] \D 非数字
[[:space:]] \s 空白字符
[^[:space:]] \S 非空白字符
[[:lower:]] [a-z] 小写字母
[[:upper]] [A-Z] 大写字母
[[:alpha:]] [a-zA-Z] 字母
[[:blank:]] 非换行符的空白字符
[[:cntrl:]] 控制字符
[[:print:]] 可打印字符
[[:punct:]] 标点字符
[[:xdigt:]] 十六进制数字

常用的匹配举例

手机号

  1. regex re(R"(1\d{10})");
  2. cout << regex_match("13552335699", re) << endl;
  3. cout << regex_match("21152335699", re) << endl;
  4. cout << regex_match("135-3356999", re) << endl;

身份证

  1. regex re(R"(\d{6}(19|20)\d{2}[01]\d[0-3]\d\d{3}[0-9xX])");
  2. cout << regex_match("25122219600230332x", re) << endl;
  3. cout << regex_match("25122219602130332x", re) << endl;
  4. cout << regex_match("251222196002453320", re) << endl;

邮箱

  1. regex re(R"(([[:alnum:]_\.\-])+\@([[:alnum:]_\.\-])+\.[a-zA-Z]{2,4})");
  2. cout << regex_match("guo-ti@163ab.com", re) << endl;
  3. cout << regex_match("gyh@163abc", re) << endl;
  4. cout << regex_match("xx.yy.zhang16@111.com", re) << endl;

可以看出:

  • 或者的表示法 |

  • 量词的表示法 {min, max},或者 {N}

    特别地,

    {0,1} 相当 于 ?

    {1, } 相当于 +

    {0, } 相当于 *

  • 注意,正则用到的 + - * 括号等符号有特殊的含义,如用其本身,则要转义

    \- 在 [ ] 要放在最尾,否则会误判为范围表示符

迭代器

对于局部的多次匹配,用迭代器更方便,且高效率。

根据字符类型不同,有4种迭代器。

常用:

cregex_iterator 对应 const char*

sregex_iterator 对应 const string

  1. string s = "abc123xyz456 kkk 1399 dfs gag 026";
  2. regex re("\\d+");
  3. auto i = sregex_iterator(s.begin(), s.end(), re);
  4. auto end = sregex_iterator();
  5. while(i != end){
  6. cout << i->str() << endl;
  7. ++i;
  8. }
  • 使用regex_match 获得详情

实时效果反馈

1. 下列正则文法中, 想表达1个或多个数字,==错误==的是:__

  • A [[:digit:]]+

  • B \d+

  • C [0-9]{1,}

  • D [\d]{1,}

2. c++11正则:[[:alpha:]_]\.{0,3}[^0-9],与下列哪个串匹配?:__

  • A “bx=z8”

  • B “_abcde”

  • C “+abc51”

  • D “z3.1+”

答案

1=>D 2=>D

正则表达式(2)

image-20220328205937138

正则表达式匹配后,还可以获得更丰富的信息。

如果希望得到这些信息,需要一个匹配结果对象 match_results 模板类的一个对象。

其声明方式:

  1. match_results<string::const_iterator> result;
  2. // 等价于:
  3. smatch result;

或者:

  1. match_results<const char*> result;
  2. // 等价于:
  3. cmatch result;
  • 使用 regex_search 查找匹配串

    1. regex re("\\d+");
    2. smatch result;
    3. string s = "abc123xyz7890-ttt";
    4. auto it = s.cbegin();
    5. auto end = s.cend();
    6. while(regex_search(it, end, result, re)){
    7. cout << result[0] << endl;
    8. it = result[0].second;
    9. }

如果使用的是 char* , 则如下:

  1. regex re("\\d+");
  2. cmatch result;
  3. const char* s = "abc123xyz7890-ttt";
  4. auto it = s;
  5. auto end = s + strlen(s);
  6. while(regex_search(it, end, result, re)){
  7. cout << result[0] << endl;
  8. it = result[0].second;
  9. }

匹配子组

模式串中,可以加括号,把匹配分为若干子组,子组的编号以左括号开始顺序计算。

在匹配结果中,用数组方式引用子组。

  1. regex re("(\\d{4})-(\\d{2})-(\\d{2})");
  2. string s = "xyz 2030-05-14 thaisanewday";
  3. smatch result;
  4. if(regex_search(s,result,re)){
  5. cout << result[0] << endl;
  6. cout << result[1] << endl;
  7. cout << result[2] << endl;
  8. cout << result[3] << endl;
  9. }

替换

regex_replace 可实现对匹配项的替换。

  1. regex re("(\\d{4})-(\\d{2})-(\\d{2})");
  2. string s = "xyz 2030-05-14 thaisanewday";
  3. cout << regex_replace(s,re,"<***>") << endl;

章节三:信息学竞赛 CPP 进阶篇 - 图115

regex函数经常有多个重载版本,比如,有的版本可以实现把结果写到指定的位置。

  • 在替换文本中也可以使用子组的引用

    比如,想把 year - month - day 的格式,改为 month - day - year 格式:

    1. regex re("(\\d{4})-(\\d{2})-(\\d{2})");
    2. string s = "xyz 2030-05-14 thaisanewday";
    3. cout << regex_replace(s, re, "$2-$3-$1") << endl;

章节三:信息学竞赛 CPP 进阶篇 - 图116

实时效果反馈

1. 用正则模式”((.)(.).)”, 去匹配串“abc” , result中子组2是:__

  • A “a”

  • B “b”

  • C “c”

  • D “abc”

2. 关于regexreplace,说法正确的是:_

  • A 替换直接在原串上进行

  • B 多个版本都是返回替换完成的结果串

  • C 多个版本都是返回发生了多少次替换

  • D 替换串中可以使用$n 表示匹配的子组

答案

1=>A 2=>D