C++标准库 -- 泛型算法 (Primer C++ 第五版 · 阅读笔记)

news2024/11/20 11:26:55

C++标准库 -- 泛型算法 (Primer C++ 第五版 · 阅读笔记)

  • 第10章 泛型算法------(持续更新)
    • 10.1、概述
    • 10.2、初识泛型算法
      • 10.2.1、只读算法
      • 10.2.2、写容器元素的算法
      • 10.2.3、重排容器元素的算法
    • 10.3、定制操作
    • 10.4、再探迭代器
    • 10.5、泛型算法结构
    • 10.6、特定容器算法

第10章 泛型算法------(持续更新)

顺序容器只定义了很少的操作:在多数情况下,我们可以添加和删除元素访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器。

我们可以想象用户可能还希望做其他很多有用的操作:查找特定元素、替换或删除一个特定值、重排元素顺序等。

标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法( generic algorithm):称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;称它们是“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如vectorlist,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。

关系图:
在这里插入图片描述

10.1、概述

大多数算法都定义在头文件algorithm中。标准库还在头文件 numeric 中定义了一组数值泛型算法。

一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。

例如,假定我们有一个intvector,希望知道vector中是否包含一-个特定值。回答这个问题最方便的方法是调用标准库算法find:

int val = 42; 	//我们将查找的值
//如果在vec中找到想要的元素,则返回结果指向它,否则返回结果为vec.cend()
auto result = find (vec.cbegin(), vec.cend(), val);
//报告结果
cout << "The value " << val <<(result == vec.cend() ? " is not present" : " is present") 
	 << endl;

例如,可以用find在一个stringlist中查找一个给定值:

string val = "a value" ; //我们要查找的值
//此调用在list中查找string 元素
auto result = find(lst.cbegin(), lst.cend(), val);

类似的,由于指针就像内置数组上的迭代器一样,我们可以用find在数组中查找值:

int ia[] = {27,210,12,47,109,83};
int val = 83;
int* result = find(begin(ia), end (ia), val);

算法如何工作

为了弄清这些算法如何用于不同类型的容器,让我们更近地观察一下findfind的工作是在一个未排序的元素序列中查找一个特定元素。概念上,find应执行如下步骤:

  1. 访问序列中的首元素。
  2. 比较此元素与我们要查找的值。
  3. 如果此元素与我们要查找的值匹配,find返回标识此元素的值。
  4. 否则,find前进到下一个元素,重复执行步骤2和3。
  5. 如果到达序列尾,find应停止。
  6. 如果find 到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。

这些步骤都不依赖于容器所保存的元素类型。因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。

迭代器令算法不依赖于容器,……

在上述find 函数流程中,除了第2步外,其他步骤都可以用迭代器操作来实现:利用迭代器解引用运算符可以实现元素访问;

  • 如果发现匹配元素,find可以返回指向该元素的迭代器;用迭代器递增运算符可以移动到下一个元素;
  • 尾后迭代器可以用来判断find是否到达给定序列的末尾;find可以返回尾后迭代器来表示未找到给定元素。

……,但算法依赖于元素类型的操作

虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个(或多个)元素类型上的操作。例如,

  • 在步骤2中,find用元素类型的 == 运算符完成每个元素与给定值的比较。
  • 其他算法可能要求元素类型支持 < 运算符。
  • 不过,我们将会看到,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。

关键概念:算法水远不会执行容器的操作
泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:
算法永远不会改变底层容器的大小
算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素

如我们将在10.4.1节所看到的,标准库定义了一类特殊的迭代器,称为插入器(inserter)。与普通迭代器只能遍历所绑定的容器相比,插入器能做更多的事情。当给这类迭代器赋值时,它们会在底层的容器上执行插入操作。因此,当一个算法操作一个这样的迭代器时,迭代器可以完成向容器添加元素的效果,但算法自身永远不会做这样的操作。

10.2、初识泛型算法

标准库提供了超过100个算法。幸运的是,与容器类似,这些算法有一致的结构。比起死记硬背全部100多个算法,理解此结构可以帮助我们更容易地学习和使用这些算法。

除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素尾元素之后位置的迭代器

虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本的方法就是了解它们是否读取元素改变元素或是重排元素顺序

10.2.1、只读算法

一些算法只会读取其输入范围内的元素,而从不改变元素。find就是这样一种算法,以及count函数也是如此。

另一个只读算法是accumulate,它定义在头文件numeric中。accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。假定vec是一个整数序列,则:

//对vec中的元素求和,和的初值是0
int sum = accumulate(vec.cbegin(), vec.cend(), 0);

accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。

算法和元素类型

accumulate将第三个参数作为求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作必须是可行的。即,序列中元素的类型必须与第三个参数匹配.或者能够转换为第三个参数的类型。

在上例中, vec 中的元素可以是int,或者是doublelong long或任何其他可以加到int上的类型。

下面是另一个例子,由于string 定义了 + 运算符,所以我们可以通过调用 accumulate 来将vector 中所有 string 元素连接起来:

string sum = accumulate(v.cbegin(), v.cend(), string(""));

//错误:const char*上没有定义+运算符
string sum = accumulate(v.cbegin(), v.cend(), "");

注意

  • 我们通过第三个参数显式地创建了一个string
  • 将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误。原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是const char*。如前所述,此类型决定了使用哪个 + 运算符。由于const char* 并没有 + 运算符,此调用将产生编译错误。

对于只读取而不改变元素的算法,通常最好使用cbegin()cend()。但是,如果你计划使用算法返回的迭代器来改变元素的值,就需要使用begin()end()的结果作为参数。

操作两个序列的算法

另一个只读算法是equal,用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false

此算法接受三个迭代器:前两个(与以往一样)表示第一个序列中的元素范围,第三个表示第二个序列的首元素:

// roster2中的元素数目应该至少与roster1一样多
equal(rosterl.cbegin(), roster1.cend(), roster2.cbegin());

由于equal利用迭代器完成操作,因此我们可以通过调用equal 来比较两个不同类型的容器中的元素。而且,元素类型也不必一样,只要我们能用–来比较两个元素类型即可。

例如,在此例中,roster1可以是 vector<string>,而roster2list<const char*>

但是, equal 基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。
此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。

那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

10.2.2、写容器元素的算法

一些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。

一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。

例如,算法 fill 接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill 将给定的这个值赋予输入序列中的每个元素。

fill(vec.begin(), vec.end(), 0);//将每个元素重置为0
//将容器的一个子序列设置为10
fill(vec.begin(), vec.begin() + vec.size()/2, 10);

由于 fill 向给定输入序列中写入数据,因此,只要我们传递了一个有效的输入序列,写入操作就是安全的。

算法不检查写操作

一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。

例如,函数fill_n接受一个单代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。我们可以用fill_n将一个新值赋予vector中的元素:

vector<int> vec;// 空vector
//使用vec,赋予它不同值
fill_n(vec.begin(), vec.size(), 0);//将所有元素重置为0

//函数fill_n假定写入指定个元素是安全的。即,如下形式的调用
fill_n(dest, n, val);

fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素。

一个初学者非常容易犯的错误是在一个空容器上调用fill_n(或类似的写元素的算法):

vector<int> vec;//空向量
//灾难:修改vec中的10个(不存在)元素
fill_n(vec.begin(), 10, 0);

这个调用是一场灾难。我们指定了要写入10个元素,但 vec中并没有元素——它是空的。这条语句的结果是未定义的。

向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

介绍back_inserter

一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器insert iterator)。

  • 插入迭代器是一种向容器中添加元素的迭代器。
  • 通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

我们将在10.4.1节中详细介绍插入迭代器的内容。但是,为了展示如何用算法向容器写入数据,我们现在将使用back_inserter,它是定义在头文件iterator中的一个函数

back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int> vec;//空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it = 42;		// vec中现在有一个元素,值为42

我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:

vector<int> vec; //空向量
//正确: back_inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec), 10, 0);//添加10个元素到vec

在每步迭代中,fill_n 向给定序列的一个元素赋值。由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用push_back。最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素的值都是0.

拷贝算法

拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。

我们可以用copy实现内置数组的铂贝,如下面代码所示:

int a1[]= {0,1,2,3,4, 5,6,7,8,9};
int a2[sizeof(a1) / sizeof(*a1)];	//a2与al大小一样
// ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(al), end(al), a2); //把a1的内容拷贝给a2

copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2尾元素之后的位置。

多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。

例如,replace 算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。

  • 此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:
//将所有值为0的元素改为42
replace(ilst.begin(), ilst.end(), 0, 42);

此调用将序列中所有的0都替换为42。如果我们希望保留原序列不变,可以调用replace_copy。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:

//使用back_inserter按需要增长目标序列
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);

此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

10.2.3、重排容器元素的算法

某些算法会重排容器中元素的顺序,一个明显的例子是sort。调用sort会重排输入序列中的元素,使之有序,它是利用元素类型的 < 运算符来实现排序的。

例如,假定我们想分析一系列儿童故事中所用的词汇。假定已有一个vector,保存了多个故事的文本。我们希望化简这个vector,使得每个单词只出现一次,而不管单词在任意给定文档中到底出现了多少次。

为了便于说明问题,我们将使用下面简单的故事作为输入:

the quick red fox jumps over the slow red turtle

消除重复单词

为了消除重复单词,

  • 首先将vector排序,使得重复的单词都相邻出现。
  • 一旦vector排序完毕,我们就可以使用另一个称为unique 的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。
  • 由于算法不能执行容器的操作,我们将使用vectorerase成员来完成真正的删除操作:
void elimDups(vector<string> &words)
{
	//按字典序排序words,以便查找重复单词
	sort(words.begin(), words.end());
	// unique重排输入范围,使得每个单词只出现一次
	//排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
	auto end_unique = unique(words.begin(), words.end());
	//使用向量操作erase删除重复单词
	words.erase(end_unique, words.end());
}

unique并不真的删除任何元素,它只是覆盖相邻的重复元素,使得不重复元素出现在序列开始部分。unique返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么。

为了真正地删除无用元素,我们必须使用容器操作,本例中使用erase

标准库算法对迭代器而不是容器进行操作。因此,算法不能(直接)添加或删除元素。

10.3、定制操作

10.4、再探迭代器

10.5、泛型算法结构

10.6、特定容器算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/444422.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ubuntu】将硬盘挂载到指定目录并设置开机自动挂载

最近打算将数据盘开机自动挂载&#xff0c;省得每次都要手动挂载&#xff0c;总结步骤如下&#xff1a; 输入以下命令&#xff0c;将系统中所有的分区都列出来&#xff1a; sudo fdisk -l找到你要挂载的分区&#xff0c;可以通过容量大小分辨&#xff0c;当然&#xff0c;有可…

黑马Mysql从入门到高级

文章目录 1. 数据库基础1.1 基础概念1.2 SQL1.2.1 语法1.2.2 数据类型1.2.3 DDL&#xff08;definition&#xff09; 1.3 函数1.3.1 字符串函数1.3.2 日期函数1.3.3 数字函数1.3.4 流程函数 1.4 约束1.5 多表查询1.5.1 连接1.5.2 联合查询 2. 数据库进阶2.1 存储引擎2.1.1 Inno…

中国人民大学与加拿大女王大学金融硕士——学习的阶段让未来的人生更丰盈

初入职场的新人拥有同样的起跑线&#xff0c;经过时间的沉淀&#xff0c;每个人之间就会有差距。差距是怎样被拉开的呢&#xff1f;也可以说是行动导致的。毕竟想是问题&#xff0c;去做才是答案&#xff0c;有行动才会有结果。在职读研与其停留在想的阶段&#xff0c;不如去准…

我发现了PMP通关密码!这14页纸直接背!

一周就能背完的PMP考试技巧只有14页纸 共分成了4大模块 完全不用担心看不懂 01关键词篇 第1章引论 1.看到“驱动变革”--选项中找“将来状态” 2.看到“依赖关系”--选项中找“项目集管理” 3.看到“价值最大化”--选项中找“项目组合管理” 4.看到“可行性研究”--选项中…

「线性DP-步入」最长上升子序列(LIS)

题目描述 给定一个长度为 N 的数列&#xff0c;求数值严格单调递增的子序列的长度最长是多少。 输入格式 第一行包含整数 N。 第二行包含 N 个整数&#xff0c;表示完整序列。 输出格式 输出一个整数&#xff0c;表示最大长度。 数据范围 1 ≤ N ≤ 1000 1≤N≤1000 1≤…

【进阶C语言】有关动态内存管理的经典笔试题(详细图文讲解)

前言 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于C语言进阶系列&#xff0c;本专栏主要内容为数据的存储、指针的进阶、字符串和内存函数…

.class文件在线转成.java文件方法

使用背景&#xff1a; 工作中碰到老的项目就头疼&#xff0c;有些连源码都没有&#xff0c;解决bug只能从class文件反编译成java后&#xff0c;来读懂业务逻辑。 后来在网上找了一些工具来帮助class文件转码&#xff0c;有些遇到lamda表达式就转换错误&#xff0c;有些使用起…

正则表达式的字符串取反常用正则表达式

正则表达式的字符串取反操作 文件同步时&#xff0c;想要过滤掉扩展名为.tmp或者.TMP的临时文件&#xff0c;想要使用正则表达式对字符串进行取反操作。 注意&#xff1a;[^tmp]* 这种取反的表达式&#xff0c;只能表示匹配除了t、m、p以外的所有字符&#xff0c;是单字符匹配…

gRPC-Go源码解读三 服务端处理流程分析

相较于Client端的复杂处理流程&#xff0c;Server端相对来说简单了很多&#xff0c;核心就是创建个TCP套接字并监听&#xff0c;收到客户端连接请求则起个go协程处理&#xff0c;子协程根据请求中的服务名和方法名调用对应的服务方法处理&#xff0c;处理完成之后则返回响应。整…

叫板IT部门和专业软件公司,低代码成为企业数字化的新选择

从2017年政府将“数字经济”写入工作报告&#xff0c;到今年两会将企业数字化转型列为重点议题&#xff0c;数字化的口号已喊了6年。政策对于数字化的支持越来越坚定&#xff0c;令人欣喜的是&#xff0c;越来越多具有远见卓识的企业已将数字化建设作为工作重心。 然而&#xf…

【LeetCode】剑指 Offer 60. n个骰子的点数 p294 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/nge-tou-zi-de-dian-shu-lcof/ 1. 题目介绍&#xff08;60. n个骰子的点数&#xff09; 把n个骰子扔在地上&#xff0c;所有骰子朝上一面的点数之和为 s。输入 n&#xff0c;打印出 s 的所有可能的值出现的概率。 你需要用一…

Vue.js条件渲染指令v-if及v-show

目录 一、v-if 二、v-show 三、v-if与v-show的选择 一、v-if v-if是Vue.js的条件语句&#xff0c;v-if指令用于条件性地渲染一块内容&#xff0c;这块内容只会在指令的表达式返回true的时候被渲染。需要特别注意的是&#xff0c;v-if所关联的是Vue.js的动态变量。 v-if的使用…

如何计算连续变量的熵

背景 做特征选择时&#xff0c;有时候会用到计算特征的信息熵&#xff0c;可是离散的好计算&#xff0c;但连续的呢&#xff1f;按照把连续变量离散的方法设置阈值点吗&#xff1f;好像比较麻烦&#xff0c;需要排序&#xff0c; 计算阈值。没有能自动的方法吗&#xff1f; 找…

动物养殖虚拟仿真之生猪屠宰VR教学系统

生猪屠宰是一个复杂而危险的工作&#xff0c;需要有严格的操作规程和丰富的经验。但是传统的生猪屠宰培训存在一些问题&#xff0c;例如成本高、难以模拟真实场景等。 为了解决这些问题&#xff0c;VR技术被应用到生猪屠宰培训中&#xff0c;广州华锐互动由此开发了生猪屠宰VR…

BOM(1)

BOM&#xff1a;浏览器对象模型&#xff0c;它提供了独立于内容而与浏览器窗口进行交换的对象&#xff0c;其核心对象是window&#xff0c;由一系列相关对象构成&#xff0c;并且每个对象都提供了很多方法和属性BOM的构成&#xff1a;BOM比DOM更大&#xff0c;它包含DOM。 …

Docker中安装redmine(亲自安装有效)

第一步&#xff1a; 官方的一键安装方式&#xff1a; curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun (我用的它) 国内 daocloud一键安装命令&#xff1a; curl -sSL https://get.daocloud.io/docker | sh ############################可能遇见问题##…

机器学习 day07(特征缩放)

特征缩放的作用 特征缩放可以让梯度下降算法运行的更快特征&#xff1a;X    对应的参数&#xff1a;W当一个特征的可能值范围很大时&#xff0c;一个好的模型会选择一个相对较小的对应参数值&#xff0c;因为W₁的一个非常小的变化会给估计价格产生非常大的影响&#xff0…

View系列

掌握View核心知识体系&#xff0c;两大方向&#xff1a;View事件分发&#xff0c;自定义View。 文章目录 一&#xff0c;基础知识1.1 页面创建1.2 页面管理 二&#xff0c;View事件分发2.1 基本概念2.2 分发流程2.3 面试题 三&#xff0c;View绘制3.1 measure&#xff08;测量&…

C#+asp.net基于web的大学社团管理信息系统

本系统的模块是分为用户模块和管理员模块&#xff0c;管理员负责学生管理模块、社团管理模块、公告管理模块、留言管理模块、加入社团管理模块、活动管理模块、管理员管理模块。社团管理员则负责预约管理模块、活动报名的审核等。。 系统具有三类系统用户分别是&#xff1a;系统…

linux网络

查看网络接口信息 ifconfig mtu 最大传输单元 mtu和mss区别 hostname命令 永久修改 hostnamectl set-hostname 切换shell环境生效 或者vi hostname 编辑完重启生效 查看路由表条目route route查看或设置主机中路由表信息 route -n将路由记录中的地址信息显示为数字形式 …