• 【读书笔记】【Effective STL】算法


    第 30 条:确保目标区间足够大。

    • 本条款讲解用的函数是 transform 函数。

    • transform 函数有两个重载函数,当前简介一个版本即可:transform(first, last, result, op)

    • transform 函数的具体实现如下:

      template <class InputIterator, class OutputIterator, class UnaryOperator>
        OutputIterator transform (InputIterator first1, InputIterator last1,
                                  OutputIterator result, UnaryOperator op)
      {
        while (first1 != last1) {
          *result = op(*first1);  // or: *result=binary_op(*first1,*first2++);
          ++result; ++first1;
        }
        return result;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • transform 函数的作用是,将某操作(op)应用于指定范围(first-last)的每个元素,移动到 result 来存放结果;

      • op 操作会被调用 last - first + 1 次,每次都会存放到 result + i 的位置上(i 指的是第 i 次被调用)。【返回的迭代器指向输出序列所保存的最后一个元素的下一个位置】
    • 本条款的代码如下:

      int transmogrify(int x) { return (x + 1); }
       
      int test_item_30()
      {
      	std::vector<int> values{ 1, 2, 3 };
      	std::vector<int> results;
      
      	// 第一种情况:back_inserter
      	results.reserve(results.size() + values.size()); // 可避免内存的重新分配
      		//std::transform(values.cbegin(), values.cend(), results.end(), transmogrify); // 错误,segmentation fault
      	std::transform(values.cbegin(), values.cend(), std::back_inserter(results), transmogrify); // 正确
      		// 在内部,std::back_inserter返回的迭代器将使得push_back被调用,所以back_inserter可适用于所有提供了push_back方法的容器
       
      	// 第二种情况:front_inserter
      	std::list<int> results2;
      	std::transform(values.begin(), values.end(), std::front_inserter(results2), transmogrify);
      			// 前面这个语句插入的结果可能不如预期,
      					// 例如,将values = [1,2,3]传到到results = [4,5,6]的前面
      					// 用前面的语句,结果会变成[3,2,1,4,5,6],所以需要将[1,2,3]反序插入
      	std::transform(values.cbegin(), values.cend(), std::front_inserter(results2), transmogrify);
      		// std::front_inserter在内部利用了push_front,所以front_inserter仅适用于那些提供了push_front成员函数的容器
       
      	// 第三种情况:inserter【插入任意位置】
      	std::transform(values.begin(), values.end(),inserter(results, results.begin() + results.size()/2),transmogrify);
      					// 	把它们的结果插入容器中的任意位置
      
      	// 第四种情况:用resize而并非reverse
      	if (results.size() < values.size()){		// 确保results至少和values一样大
      		results.resize(values.size()); }
      	transform(values.begin(), values.end(),	results.begin(), transmogrify);	// 覆盖values.size()个
      	
      	// 第五种情况:清空results然后用通常的方式使用插入迭代器
      	results.clear();// 销毁results中的所有元素
      	results.reserve(values.size());			// 保留足够空间
      	transform(values.begin(), values.end(), back_inserter(results), transmogrify);
      
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
    • 上面的代码有几点值得进行讨论:

      • 在前面用 transform 错误操作的时候,result 迭代器参数位置传入的是 results.end(),所以在调用 transform 的时候,就会给不存在的对象赋值。【第一种情况】
        • 而传入 back_inserter(results) 的时候,问题就能解决了;调用 back_inserter 来产生指定目标区间起点的迭代器,在内部,back_inserter 返回的迭代器会调用 push_back。【插入后面的同时push_back
      • 如果你想让一个算法在容器的前端插入东西,你可以使用 front_inserter;在内部,front_inserter 利用了 push_front,所以 front_insert 只和提供那个成员函数的容器配合(也就是 dequelist)。【插入前面的同时 push_front】【第二种情况】
      • 如果在 vectorstring 中,想要调用 transform,可以提前先调用 reverse 来分配足够内存。【第一种情况】
      • 如果不想用 reverse:可以考虑保障 results 至少有 values 的元素个数,即使用 resize【第四种情况】;又或者先清空 results 再用通常方式插入【第五种情况】。
    • 最后的总结如下:

      • 无论何时你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时可以增加大小。
      • 如果选择增加大小,就使用插入迭代器,比如 ostream_iterators 或从 back_inserterfront_inserterinserter 返回的迭代器。

    第 31 条:了解各种与排序有关的选择。

    • 本条款讨论的就是在哪种情况下用哪种排序算法。
    • stl 中 sort 相关函数及简介如下:
      1. std::sortstd::stable_sortstd::partial_sort
        • std::sort 对给定区间所有元素进行排序,采用类似快速排序算法来实现;【非稳定排序】
        • std::stable_sort 对给定区间所有元素进行稳定排序,采用类似归并算法实现;
        • std::partial_sort 对给定区间所有元素进行部分排序,采用类似堆排序算法实现。【非稳定排序】
      2. std::nth_element:【与 partial_sort 不同的点在于,partial_sort 的前半部分是完全排序的; nth_element 把前 n 个值放在前部分,但是整体无序】【其实就是快速选择】
        • std::nth_element 使得位置 n 上的元素正好是全排序情况下的第 n 个元素;前 n-1 个比第 n 个值小的值在 n 位置之前,n 位置之后比第 n 个值大的值在 n 位置之后。
      3. std::partitionstd::stable_partition:【与 nth_element 区别在于 partition 自定义判断条件】【只需要双向迭代器,不需要随机访问】
        • std::partition 会将区间 [first,last) 中的元素重新排列,满足判断条件 pred 的元素会被放在区间的前段,不满足 pred 的元素会被放在区间的后段。【非稳定】
        • std::stable_partition 保证初始相对位置。

    第 32 条:如果确实需要删除元素,则需要在 remove 这一类算法之后调用 erase。

    • 从容器中删除元素的唯一方法是调用该容器的成员函数,而 remove 并不知道它操作的元素所在的容器(remove 并不是成员函数,list 除外),所以 remove 不可能从容器中删除元素。

    • remove 并没有做到真正的删除元素,只是把该删除的元素移动到容器最后,然后把尾部迭代器前移 n 个(假设只删除 n 个元素);【可以把 remove 想象成一个压缩过程,被删除的值表演了在压缩中被填充的洞的角色】

      • remove 返回的一个迭代器指向最后一个“不用被删除”的元素之后的元素,这个返回值相当于该区间“新的逻辑结尾”。
    • std::listremove 成员函数是 STL 中唯一一个名为 remove 并且确实删除了容器中元素的函数。

    • remove() 函数并不是真正的删除,要想真正删除元素则可以使用 erase() 或者 resize() 函数。

      v.erase(std::remove(v.begin(), v.end(), 99), v.end()); // 真正删除所有值等于99的元素
      
      • 1
    • std::remove 并不是唯一一个适用于这种情形的算法,其它还有两个属于 remove 类的算法:remove_ifunique

      • remove_ifremove 的情况一致,只不过 remove_if 传入了判断式,满足判断式的才 remove
      • unique 的情况和 remove 一致,相当于把重复值移到后面。
      • 而和 list::remove 一样,list::unique 会真的删除元素;它旨在删除临近的重复元素(使元素唯一),而且比使用 erase + unique 更为高效。

    第 33 条:对包含指针的容器使用 remove 这一类算法时要特别小心。

    • 当容器中存放的是指向动态分配的对象的指针的时候,应该避免使用 remove 和类似的算法(remove_ifunique)。
      • 引出的问题其实很直白:当调用 remove 函数移走指针元素后(此时已经造成资源泄露),再接着调用 erase 函数真正删除元素,那么会造成资源泄露。
    • 消除资源泄露的做法:
      • 将那些被 remove 走的元素一一删除(delete)并置空。【先调用 for_each 把满足条件的指针元素 delete,最后再 erase + remove,过程中将指针置空】
      • 存放引用计数功能的智能指针,那么就可以直接使用 erase + remove 的习惯用法。【要知道的是,因为存放的是智能指针,要使 erase + remove 生效,需要保证智能指针能够隐式转换为智能指针内元素的指针】

    第 34 条:了解哪些算法要求使用排序的区间作为参数。

    • 并非所有的算法都可以应用于任何区间。
      • 举例一:remove 算法要求单向迭代器并且要求可以通过这些迭代器向容器中的对象赋值
        • remove 不能用于由输入迭代器指定的区间,也不适用于 mapmultimap,同样不适用于某些 setmultiset 的实现。
      • 举例二:很多排序算法要求随机访问迭代器,所以对于 list 的元素不可能调用这些算法。
    • 要求排序区间的 STL 算法:
      • 要求排序区间:binaray_searchlower_boundupper_boundequal_rangeset_unionset_intersectionset_differenceset_symmetric_differencemergeinplace_mergeincludes
      • 并不一定要求(同样有效率),但通常情况下会与排序区间一起使用:uniqueunique_copy
    • 本条款讨论的内容如下:
      • binaray_searchlower_boundupper_boundequal_range 用于查找,且它们用二分法(所以要求排序);当它们接受了随机访问迭代器,才保证对数时间的查找效率。
      • set_unionset_intersectionset_differenceset_symmetric_difference 用于 set 的各类操作,这四个函数保证了线性时间(所以要求排序)。
      • mergeinplace_merge 实际上实现了合并和排序的联合操作,它们读入两个排序的区间,然后合并成一个新的排序区间,其中包含了原来两个区间中的所有元素;同样的,因为要求区间排序,所以保证了线性时间的承诺。
      • includes 用来判断一个区间中的所有对象是否都在另一个区间中,它承诺线性时间的效率。
      • uniqueunique_copy 即使对于未排序的区间也有很好的行为,unique 使用了与 remove 类似的办法来删除区间中的元素,而并非真正意义上的删除。
      • 所有要求排序区间的算法(除 uniqueunique_copy 以外)都使用等价来判断两个对象是否相同,这是与关联容器相同的;uniqueunique_copy 在默认情况下使用相等来判断两个对象是否相同,当然你可以改变这种默认的行为,只需要给算法提供一个用于比较的谓词。
    • 最后一个讨论的内容其实很简单:
      • 假设 sort 函数和 binary_search 函数共同使用在同一个容器上,如果 sort 调用的时候使用了自定义的比较函数,那么 binary_search 就必须要使用同样的比较函数。

    第 35 条:通过 mismatch 或 lexicographical_compare 实现简单的忽略大小写的字符串比较。

    • 首先介绍条款中相关的几个函数:

      • mismatch 将标记出两个区间中第一个对应位置不同的位置;如果两个字符串长度不同,那么我们必须把短的字符串作为第一个区间传入。【类似 strcmp(返回一个负数、零或正数)】
      • lexicographical_comparestrcmp 的一个泛化版本,可以比较任何类型的值的区间;lexicographical_compare 可以接受一个判别式,由判别式来决定两个值是否满足一个用户自定义的准则。【类似 operator(返回 truefalse)】
      • strcmpstrcmpi 通常是被优化过的,它们在长字符串的处理上一般要比通用算法 mismatchlexicographical_compare 快。
    • 代码实现如下:

      // 实现一:【忽略大小写string比较的函数】
      	// 第一个函数,比较字符
      		// 忽略大小写地比较字符c1和c2,如果c1c2,返回1;如果c1==c2,返回0
      int ciCharCompare(char c1, char c2)
      {
      	int lc1 = std::tolower(static_cast<unsigned char>(c1));
      	int lc2 = std::tolower(static_cast<unsigned char>(c2));
       
      	if (lc1 < lc2) return -1;
      	if (lc1 > lc2) return 1;
      	return 0;
      }
      	// 第二个函数
      		// 用作边界检查,真正工作,即调用前面第一个函数
      int ciStringCompareImpl(const std::string& s1, const std::string& s2)
      {
      	typedef std::pair<std::string::const_iterator, std::string::const_iterator> PSCI;
      	PSCI p = std::mismatch(s1.begin(), s1.end(), s2.begin(), std::not2(std::ptr_fun(ciCharCompare)));
      	if (p.first == s1.end()) { // 如果为true,要么s1和s2相等,或者s1比s2短
      		if (p.second == s2.end()) return 0;
      		else return -1;
      	}
      	return ciCharCompare(*p.first, *p.second); // 字符串之间的关系和这两个不匹配的字符之间的关系相同
      }
      	// 第三个函数,即string比较函数
      int ciStringCompare(const std::string& s1, const std::string& s2)
      {
      	// 把短的字符串作为第一个区间传入
      	if (s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);
      	else return -ciStringCompareImpl(s2, s1);
      }
      
      // 实现二:
      	// 实现函数子类,方便后续lexicographical_compare调用
      	// 返回在忽略大小写的情况下,c1是否在c2之前
      bool ciCharLess(char c1, char c2)
      {
      	return std::tolower(static_cast<unsigned char>(c1)) <
      		std::tolower(static_cast<unsigned char>(c2));
      }
       
      bool ciStringCompare2(const std::string& s1, const std::string& s2)
      {
      	return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
      }
       
      // 实现三:
      bool ciStringCompare3(const std::string& s1, const std::string& s2)
      {
      	// 前提:不考虑国际化支持,也确定字符串中不会包含内嵌的空字符
      	return strcmp(s1.c_str(), s2.c_str());
      }
       
      int test_item_35()
      {
      	std::string str1{ "xxz" }, str2{ "xxx" };
      	fprintf(stdout, "str1:str2: %d\n", ciStringCompare(str1, str2));
      	fprintf(stdout, "str1:str2: %d\n", ciStringCompare2(str1, str2));
      	fprintf(stdout, "str1:str2: %d\n", ciStringCompare3(str1, str2));
       
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62

    第 36 条:理解 copy_if 算法的正确实现。

    • 本条款讨论的其实就是如何实现 copy_if() 算法,C++11 已经添加了该函数。
      • copy_if() 算法可以从源序列复制使谓词(判断式)返回 true 的元素,所以可以把它看作一个过滤器。
    • 条款中放出了 copy_if() 一个实现:
      template<typename InputIterator,				// 一个copy_if的正确实现
      			typename OutputIterator,
      			typename Predicate>
      OutputIterator copy_if(InputIterator begin,
      				InputIterator end,
      				OutputIterator destBegin,
      				Predicate p) {
      	while (begin != end) {// 逐字节拷贝
      		if (p(*begin))
      			*destBegin++ = *begin;
      		++begin;
      	}
      
      	return destBegin;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • copy_if() 函数的使用如下:
      int test_item_36()
      {
      	std::vector<int> v1{ 1, 2, 3, 4, 5 }, v2(v1.size());
       
      	auto it = std::copy_if(v1.begin(), v1.end(), v2.begin(), [](int i) { return (i % 2 == 1); });
      	v2.resize(std::distance(v2.begin(), it));
       
      	for (const auto& v : v2)
      		fprintf(stdout, "%d\n", v); // 1 3 5
       
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    第 37 条:使用 accumulate 或者 for_each 进行区间统计。

    • accumulate() 不存在于 ,它和其他三个数值算法(inner_product()adjacent_difference()partial_sum())都在 中。
      • accumulate() 只要求输入迭代器,所以你可以使用 std::istream_iteratorstd::istreambuf_iterator
      • accumulate() 有两种形式:
        • 第一种形式有两个迭代器和一个初始值,它返回该初始值加上由迭代器标识的区间中的值的总和;
        • 第二种形式带两个迭代器、一个初始值和一个任意的统计函数。【会将前面三个参数传入统计函数】
    • 有了 accumulate(),不仅可以计算累加,也可以计算累乘(乘积的时候初值需要设置为 1)。
      • 但是,如果有更复杂的情况,accumulate() 函数就很吃力,例如计算一个区间所有点的平均值;

      • 如果使用 accumulate() 函数,在此场景下仍能使用,但是它不符合 STL 标准,即传给 accumulate() 的函数不允许有副作用,而操作的过程中会带来副作用。

      • 代码如下:

        struct Point{
            Point(double initX, double initY): x(initX), y(initY) {}
            double x, y;
        };
        //定义传入accumulate的计算函数
        class PointAverage : public binary_function<Point, Point, Point> {//此处的继承参见条款40
        public:
        PointAverage() : xSum(0), ySum(0), numPoints(0){}
        const Point operator()(const Point& avgSoFar, const Point& p){
            ++numPoints;
            xSum += p.x;
            ySum += p.y;
            return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值
        }
        private:
        size_t numPoints;//记录当前读取元素的总数
        double xSum;//记录所有横坐标的总和
        double ySum;//记录所有纵坐标的总和
        };
        //调用函数
        list<Point> lp;
        Point avg = accumulate(lp.begin(), lp.end(), Point(0, 0), PointAverage());
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
    • for_each() 是另一个可被用来统计区间的算法,且它不受 accumulate() 的那些限制。
      • for_each() 也带两个参数:一个是区间(两个迭代器),另一个是函数(通常是函数对象)。
      • 对区间中的每个元素都要调用这个函数,但是,传给 for_each() 的这个函数只接收一个实参(即当前的区间元素)。【只会将第一个元素(即 first 迭代器)传入函数中】
      • std::for_each()std::accumulate() 在两个方面有所不同:
        1. accumulate() 暗示着这个算法将会计算出一个区间的统计信息,而 for_each() 听起来就好像是对一个区间的每个元素做一个操作。
        2. accumulate() 直接返回我们所要的统计结果,而 for_each() 却返回一个函数对象,我们必须从这个函数对象中提取出我们所要的统计信息(可以保存状态)。
      • 代码如下:
        struct Point{
            Point(double initX, double initY): x(initX), y(initY) {}
            double x, y;
        };
        //定义传入accumulate的计算函数
        class PointAverage : public unary_function<Point, void> {//此处继承的函数不同了
        public:
        PointAverage() : xSum(0), ySum(0), numPoints(0){}
        void operator()(const Point& p){
            ++numPoints;
            xSum += p.x;
            ySum += p.y;
            return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值
        }
        Point result() const{
            return Point(xSum/numPoints, ySum/numPoints);
        }
        private:
        size_t numPoints;//记录当前读取元素的总数
        double xSum;//记录所有横坐标的总和
        double ySum;//记录所有纵坐标的总和
        };
        //调用函数
        list<Point> lp;
        Point avg = for_each(lp.begin(), lp.end(), PointAverage()).result();// 取出返回值的状态
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
  • 相关阅读:
    电脑新加的硬盘如何分区?新加硬盘分区选MBR还是GPT
    46. 从零开始学springboot: spel结合aop实现动态传参
    【OpenCV】图像上绘制文字cv2.putText()函数用法,添加倾斜透明水印
    电力通信专业技术总结,智能电网通信技术总结
    1.屏幕分类和刘海屏的适配原则
    MYSQL的下载与配置 mysql远程操控
    计算机毕业设计java基于javaweb+ssm+vue婚纱摄影网站
    SwiftUI 5.0(iOS 17)TipKit 让用户更懂你的 App
    SpringCloud 08 Feign 负载均衡和服务熔断
    【Spring Boot】Spring Boot 的常用注解
  • 原文地址:https://blog.csdn.net/weixin_44705592/article/details/127409226