在前面我们学习了顺序容器的基本操作。容器提供了添加、删除元素,访问首尾元素,获取迭代器等基础功能。然而,在实际编程中,我们经常需要执行更复杂的操作:在容器中查找特定元素、对元素进行排序、统计满足条件的元素数量、对元素进行变换等等。
如果为每种容器类型单独定义这些操作,不仅工作量巨大,而且会产生大量重复代码。C++标准库采用了一种更加优雅的解决方案——泛型算法。这些算法之所以被称为“泛型”,是因为它们能够适用于不同类型的容器,甚至包括内置数组类型;之所以被称为“算法”,是因为它们实现了经典的计算机算法,如查找、排序、搜索等。

标准库在algorithm头文件中定义了大部分泛型算法,同时在numeric头文件中提供了一组专门用于数值计算的算法。这些算法的设计遵循一个重要原则:它们不直接操作容器,而是通过迭代器来访问和操作元素。
让我们通过一个具体的例子来理解算法的工作方式。假设我们有一个存储学生分数的容器,需要查找是否有学生得了满分100分。使用标准库的find算法可以轻松完成这个任务:
|vector<int> scores = {85, 92, 100, 78, 95, 88}; int targetScore = 100; // 在scores中查找目标分数 auto result = find(scores.cbegin(), scores.cend(), targetScore); // 检查是否找到 if (result != scores.cend()) { cout << "找到满分学生,分数为:" << *result << endl; } else { cout << "没有学生获得满分" << endl; }
这个例子展示了算法的基本使用模式。find算法接受三个参数:前两个参数是迭代器,指定搜索范围的开始和结束位置;第三个参数是要查找的值。如果找到目标值,函数返回指向该元素的迭代器;如果没有找到,则返回表示"超出末尾"的迭代器。
为了深入理解算法的设计思想,让我们分析一下find算法的工作步骤。从概念上讲,find需要完成以下任务:
首先,它访问序列中的第一个元素。然后,将这个元素与要查找的值进行比较。如果匹配,就返回指向该元素的标识符;如果不匹配,就前进到下一个元素并重复比较过程。这个过程一直持续到找到匹配元素或到达序列末尾。如果到达序列末尾仍未找到匹配元素,就需要返回一个表示"未找到"的特殊值。
观察这个过程,我们会发现算法的设计非常巧妙。除了元素比较操作之外,其他所有步骤都可以通过迭代器操作来实现:访问元素值使用解引用操作符,移动到下一个元素使用递增操作符,检测是否到达末尾通过比较迭代器实现。这种设计使得算法完全独立于容器类型。
正因为如此,我们可以对不同类型的容器使用相同的find算法。无论是查找字符串列表中的特定字符串:
|list<string> studentNames = {"张三", "李四", "王五", "赵六"}; string target = "李四"; auto found = find(studentNames.cbegin(), studentNames.cend(), target);
还是在内置数组中进行查找:
|int grades[] = {89, 76, 95, 82, 90, 88}; int targetGrade = 95; int* result = find(begin(grades), end(grades), targetGrade);
都使用相同的find函数,这体现了泛型算法的强大之处。
算法与容器之间的这种独立性是通过迭代器实现的。迭代器为算法提供了一个统一的接口,屏蔽了不同容器的实现细节。无论底层是动态数组、链表还是其他数据结构,算法都能以相同的方式工作。
但是,算法确实依赖于元素类型的某些操作。比如find算法需要使用元素类型的==操作符来进行比较。其他算法可能需要<操作符来进行排序比较。这是算法设计中的一个重要考虑因素:算法提供了通用的处理逻辑,但元素类型必须支持算法所需的特定操作。
算法永远不会执行容器操作。它们完全通过迭代器及其操作来工作。这意味着算法本身不会改变底层容器的大小——它们不会直接添加或删除元素。这个特性对于理解算法的行为和正确使用算法至关重要。
C++的标准库提供了超过100个算法,这个数量看起来庞大。但幸运的是,这些算法具有一致的架构和命名模式,理解这些模式比记住所有算法要重要得多。
几乎所有算法都操作一个元素范围,我们称之为"输入范围"。接受输入范围的算法通常用前两个参数来表示这个范围,这两个参数是迭代器,分别指向要处理的第一个元素和最后一个元素之后的位置。
理解算法的最基本方法是了解它们如何使用输入范围中的元素。根据这个标准,我们可以将算法分为三大类:只读算法、写入算法和重排算法。
许多算法只读取输入范围中的元素,从不写入。find算法就是这样一个例子。另一个常用的只读算法是count,它用于统计特定值在序列中出现的次数:
|vector<int> examScores = {85, 92, 85, 78, 92, 85, 88}; int targetScore = 85; int occurrences = count(examScores.cbegin(), examScores.cend(), targetScore); cout << "分数" << targetScore << "出现了" << occurrences <<
在数值处理中,accumulate算法尤其有用。它定义在numeric头文件中,能够对序列中的元素进行累加操作。这个算法接受三个参数:前两个指定输入范围,第三个是累加的初始值:
|vector<double> monthlyExpenses = {1200.0, 1150.0, 1300.0, 980.0, 1100.0}; double totalExpense = accumulate(monthlyExpenses.cbegin(), monthlyExpenses.cend(), 0.0); cout << "总支出:" << totalExpense << " 元" << endl;
需要特别注意的是,accumulate的第三个参数不仅提供了累加的起始值,还决定了累加操作的类型和返回值的类型。这意味着容器中的元素类型必须能够与第三个参数的类型进行相应的运算。
一个有趣的应用是使用accumulate来连接字符串。由于string类型定义了+操作符,我们可以这样做:
|vector<string> courseNames = {"数学", "物理", "化学", "生物"}; string courselist = accumulate(courseNames.cbegin(), courseNames.cend(), string("课程列表:")); cout << courselist << endl; // 输出:课程列表:数学物理化学生物
这里必须明确创建一个string对象作为第三个参数。如果传递字符串字面值,会导致编译错误,因为const char*类型没有定义+操作符。
某些只读算法需要处理两个输入序列。equal算法就是这样一个例子,它用于判断两个序列是否包含相同的元素:
|vector<string> class1Students = {"张三", "李四", "王五"}; vector<string> class2Students = {"张三", "李四", "王五"}; // 检查两个班级的学生名单是否相同 bool isSame = equal(class1Students.cbegin(), class1Students.cbegin(), class2Students.cbegin());
使用equal算法时需要特别小心:它假设第二个序列至少与第一个序列一样长。算法会检查第一个序列中的每个元素,并假设第二个序列中存在对应的元素。如果第二个序列较短,就会发生访问越界错误。
接受单个迭代器表示第二个序列的算法都假设第二个序列至少与第一个序列一样长。确保这个条件成立是程序员的责任。
有些算法会向元素序列写入新值。在使用这类算法时,必须确保目标序列有足够的空间容纳要写入的元素数量。由于算法不能执行容器操作,它们无法自行改变容器的大小。
fill算法是一个典型的写入算法,它将指定范围内的所有元素设置为给定值:
|vector<int> scores(10); // 创建包含10个元素的vector fill(scores.begin(), scores.end(), 0); // 将所有元素设置为0 // 或者只设置前半部分 fill(scores.begin(), scores.begin() + scores.size()/2, 95);
fill算法是相对安全的,因为它只在给定的范围内写入,不会超出范围边界。
另一类写入算法接受一个目标迭代器作为参数。fill_n函数就是这样的例子,它从指定位置开始写入指定数量的元素:
|vector<int> testScores(20); fill_n(testScores.begin(), 10, 85); // 将前10个元素设置为85
使用这类算法时必须格外小心。如果目标容器没有足够的元素,程序会发生严重错误:
|vector<int> emptyScores; // 空vector // 危险!试图向空容器写入10个元素 fill_n(emptyScores.begin(), 10, 100); // 运行时错误
为了安全地使用写入算法,我们可以使用插入迭代器。插入迭代器是一种特殊的迭代器,当我们向它赋值时,它会调用容器的插入操作添加新元素。
back_inserter是最常用的插入迭代器,它定义在iterator头文件中。这个函数接受一个容器的引用,返回一个绑定到该容器的插入迭代器。通过这个迭代器进行的赋值操作会调用push_back在容器末尾添加元素:
|vector<int> studentIDs; // 空vector auto inserter = back_inserter(studentIDs); *inserter = 20210001; // 等价于studentIDs.push_back(20210001) *inserter = 20210002; // 等价于studentIDs.push_back(20210002)
结合fill_n使用back_inserter可以安全地向容器添加元素:
|vector<int> scores; // 空vector fill_n(back_inserter(scores), 15, 90); // 安全地添加15个值为90的元素
copy算法演示了算法的另一个重要特点。这个算法接受三个迭代器:前两个表示输入范围,第三个表示目标位置的起始点:
|int originalGrades[] = {88, 92, 76, 95, 83, 90, 87, 91, 79, 85}; int copyGrades[10]; // 确保目标数组有足够的空间 auto endIterator = copy(begin(originalGrades), end(originalGrades), copyGrades); // endIterator指向copyGrades中最后一个被复制元素的下一个位置
许多算法都提供"拷贝"版本,这些版本不会修改输入序列,而是将结果写入新的序列。例如,replace算法在原序列中替换指定值,而replace_copy算法将替换后的结果写入新序列:
|list<string> subjects = {"数学", "英语", "数学", "物理", "数学"}; vector<string> newSubjects; // 将所有"数学"替换为"高等数学",结果存入newSubjects replace_copy(subjects.cbegin(), subjects.cend(), back_inserter(newSubjects), "数学", "高等数学"); // subjects保持不变,newSubjects包含替换后的结果
第三类算法会重新排列容器中元素的顺序。最著名的例子是sort算法,它使用元素类型的<操作符对序列进行排序。
让我们通过一个文本处理的例子来了解重排算法的使用。假设我们要分析一段文本中使用的词汇,并生成一个无重复的词汇表:
|vector<string> textWords = {"编程", "学习", "C++", "编程", "算法", "学习", "数据结构", "C++", "编程"};
要创建无重复的词汇表,我们需要经过以下步骤:
首先,使用sort算法对词汇进行排序,这样相同的词会相邻出现:
|sort(textWords.begin(), textWords.end()); // textWords现在是:{"C++", "C++", "学习", "学习", "数据结构", "算法", "编程", "编程", "编程"}
然后,使用unique算法重排序列,将唯一的元素移动到序列前部:
|auto endUnique = unique(textWords.begin(), textWords.end()); // textWords现在是:{"C++", "学习", "数据结构", "算法", "编程", ???, ???, ???, ???} // ↑ // endUnique
unique算法并不真正删除重复元素,而是将唯一元素移动到序列前部,返回指向唯一序列末尾的迭代器。要真正删除多余的元素,我们需要使用容器的erase操作:
|textWords.erase(endUnique, textWords.end()); // 现在textWords只包含唯一的词汇:{"C++", "学习", "数据结构", "算法", "编程"}
我们可以将这个过程封装成一个函数:
|void eliminateDuplicates(vector<string> &words) { sort(words.begin(), words.end()); auto endUnique = unique(words.begin(), words.end()); words.erase(endUnique, words.end()); }
重要的是要理解算法和容器操作之间的分工:算法负责重排元素,容器操作负责实际的插入和删除。这种设计体现了C++中职责分离的原则。
到目前为止,我们使用的算法都依赖元素类型的默认操作,比如<或==操作符。但在实际应用中,我们经常需要定制算法的行为。例如,我们可能希望按照不同的标准对元素排序,或者使用自定义的比较条件查找元素。
标准库通过接受谓词参数的方式来支持这种定制。谓词是一个可调用的表达式,它返回一个可以用作条件的值。库算法使用一元谓词(接受单个参数)或二元谓词(接受两个参数)。
让我们继续前面的词汇分析例子。假设我们希望按照单词长度对词汇进行排序,而不是按字母顺序。我们可以编写一个比较函数:
|bool isShorterWord(const string &word1, const string &word2) { return word1.size() < word2.size(); }
然后将这个函数传递给sort算法:
|vector<string> vocabulary = {"编程", "C++", "学习", "数据结构", "算法"}; sort(vocabulary.begin(), vocabulary.end(), isShorterWord); // 结果按长度排序:{"C++", "学习", "算法", "编程", "数据结构"}
但是这样排序后,长度相同的单词可能失去原有的字母顺序。如果我们希望在保持长度排序的同时维持相同长度单词的字母顺序,可以使用stable_sort算法:
|eliminateDuplicates(vocabulary); // 去重并按字母排序 stable_sort(vocabulary.begin(), vocabulary.end(), isShorterWord); // 结果:先按长度排序,相同长度的单词保持字母顺序
使用函数作为谓词虽然有效,但有时会显得繁琐,特别是当我们需要更灵活的参数传递时。考虑这样一个需求:我们想要统计并打印所有长度不少于指定字符数的单词。
为了解决这个问题,C++11引入了lambda表达式。lambda表达式表示一个可调用的代码单元,可以看作是未命名的内联函数。与普通函数不同,lambda可以定义在函数内部。
lambda表达式的基本语法是:
|[捕获列表](参数列表) -> 返回类型 { 函数体 }
其中捕获列表和函数体是必需的,参数列表和返回类型可以省略。让我们通过例子来学习lambda的使用:
|auto simpleFunction = []{ return 42; }; cout << simpleFunction() << endl; // 输出:42
这个lambda接受零个参数,返回整数42。我们可以像调用普通函数一样调用它。
lambda可以接受参数,就像普通函数一样。参数类型必须匹配,且lambda不能有默认参数:
|auto lengthComparator = [](const string &a, const string &b) { return a.size() < b.size(); }; sort(vocabulary.begin(), vocabulary.end(), lengthComparator);
这个lambda实现了与前面isShorterWord函数相同的功能。
lambda的真正威力在于它的捕获列表,它允许lambda使用定义它的函数中的局部变量。让我们实现前面提到的功能:查找并打印长度不少于指定字符数的单词。
|void analyzeVocabulary(vector<string> &words, vector<string>::size_type minLength) { eliminateDuplicates(words); stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.
在这个例子中,第一个lambda的捕获列表为空,因为它只使用自己的参数。第二个lambda捕获了minLength变量,这样它就可以在函数体中使用这个局部变量。第三个lambda用于打印,它的捕获列表也是空的,因为它只使用标准输出流(这不是局部变量)。
当我们定义lambda时,编译器会生成一个新的未命名类类型。lambda的捕获列表中的变量会成为这个类的数据成员。这些数据成员在lambda对象创建时被初始化。
默认情况下,lambda通过值捕获变量。就像参数传递一样,被捕获变量的值在lambda创建时被复制,而不是在调用时:
|void demonstrateValueCapture() { size_t count = 42; auto counter = [count]{ return count; }; count = 0; // 修改局部变量 auto result = counter(); // result仍然是42 cout << "结果:" << result << endl; // 输出:结果:42 }
我们也可以通过引用捕获变量。在变量名前加上&表示引用捕获:
|void demonstrateReferenceCapture() { size_t count = 42; auto counter = [&count]{ return count; }; count = 0; // 修改局部变量 auto result = counter(); // result是0 cout << "结果:" << result << endl; // 输出:结果:0 }
引用捕获在某些情况下是必要的。例如,当我们需要捕获无法复制的对象(如输出流)时:
|void printToFile(vector<string> &words, ostream &outputStream, char separator = ' ') { for_each(words.begin(), words.end(), [&outputStream, separator](const string &word) { outputStream << word << separator; });
使用引用捕获时需要注意对象的生命周期。被引用的对象必须在lambda执行时仍然存在。
除了显式指定要捕获的变量,我们还可以让编译器推断捕获列表。使用=表示值捕获,使用&表示引用捕获:
|void demonstrateImplicitCapture(vector<string> &words, size_t minLength) { // 隐式值捕获 auto longWordFilter = [=](const string &word) { return word.size() >= minLength; // minLength被自动捕获 }; // 也可以混合使用 ostream &output = cout;
默认情况下,通过值捕获的变量在lambda内部是不能修改的。如果我们需要修改这些变量,必须使用mutable关键字:
|void demonstrateMutableLambda() { size_t counter = 0; auto incrementer = [counter]() mutable { return ++counter; // 修改捕获的副本 }; counter = 10; cout << incrementer() << endl; // 输出:1(修改的是副本) cout << counter << endl; // 输出:10(原变量未变)
当lambda函数体包含除return语句之外的其他语句时,编译器会假设返回类型为void。如果我们需要返回值,必须使用尾置返回类型:
|vector<int> numbers = {-5, -2, 0, 3, -1, 4, -3, 6}; // 错误:编译器推断返回类型为void // transform(numbers.begin(), numbers.end(), numbers.begin(), // [](int n) { if (n < 0) return -n; else return n; }); // 正确:明确指定返回类型 transform(numbers.begin(), numbers.end(), numbers.begin(), [](int
lambda表达式对于简单的操作非常有用,但如果一个操作很复杂或者需要在多个地方使用,定义函数通常更好。然而,将函数用作算法参数时有时会遇到困难。
考虑前面的单词长度检查例子。我们可以写一个函数来替代lambda:
|bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; }
但是这个函数不能直接用于find_if算法,因为find_if需要一个一元谓词,而我们的函数需要两个参数。
标准库的bind函数可以解决这个问题。bind定义在functional头文件中,可以看作是一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
bind的基本形式是:
|auto newCallable = bind(callable, arg_list);
其中arg_list中可以包含形如_n的占位符,表示新可调用对象的第n个参数。
让我们用bind来解决单词长度检查的问题:
|auto checkWordLength = bind(check_size, _1, 5); // checkWordLength接受一个string参数,相当于调用check_size(参数, 5) string testWord = "编程"; bool result = checkWordLength(testWord); // 等价于check_size(testWord, 5)
现在我们可以在算法中使用这个绑定的函数:
|auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
占位符_1, _2等定义在std::placeholders命名空间中。为了方便使用,我们通常会有这样的声明:
|using namespace std::placeholders;
bind的参数可以重排和重复:
|// 假设有一个比较函数 bool isLonger(const string &s1, const string &s2) { return s1.size() > s2.size(); } // 反转参数顺序 sort(words.begin(), words.end(), bind(isLonger, _2, _1));
默认情况下,bind复制它的参数。如果我们需要传递引用,必须使用ref函数:
|void printWord(ostream &os, const string &word, char separator) { os << word << separator; } // 错误:不能复制ostream // for_each(words.begin(), words.end(), bind(printWord, cout, _1, ' ')); // 正确:使用ref传递引用 for_each(words.begin(), words.end(), bind(printWord, ref(cout), _1, ' '));
ref函数返回一个包含给定引用的对象,这个对象本身是可复制的。类似地,cref函数生成包含const引用的对象。
除了容器定义的迭代器,标准库还在iterator头文件中定义了几种额外的迭代器类型:插入迭代器、流迭代器、反向迭代器和移动迭代器。这些特殊的迭代器大大扩展了算法的应用范围。
插入迭代器是一种迭代器适配器,它接受一个容器,产生一个向指定容器添加元素的迭代器。当我们对插入迭代器赋值时,该迭代器调用容器操作来插入一个元素。
标准库提供三种插入迭代器,它们的区别在于元素插入的位置:
back_inserter创建一个使用push_back的迭代器,只能用于支持push_back的容器。front_inserter创建一个使用push_front的迭代器,只能用于支持push_front的容器。inserter创建一个使用insert的迭代器,可以用于任何支持insert的容器。
让我们通过例子来理解它们的区别:
|list<int> originalList = {1, 2, 3, 4}; list<int> backList, frontList, insertList; // 使用back_inserter copy(originalList.cbegin(), originalList.cend(), back_inserter(backList)); // backList现在是:{1, 2, 3, 4} // 使用front_inserter copy(originalList.cbegin(), originalList.cend(), front_inserter(frontList)); // frontList现在是:{4, 3, 2, 1}(顺序相反) // 使用inserter
需要注意的是,front_inserter总是在容器开头插入,所以元素顺序会颠倒。而inserter在指定位置前插入,但插入位置会随着元素的插入而更新。
虽然iostream类型不是容器,但标准库提供了可以与IO对象一起使用的迭代器。istream_iterator用于读取输入流,ostream_iterator用于写入输出流。这些迭代器将流视为特定类型元素的序列。
创建istream_iterator时,我们必须指定迭代器要读写的对象类型。istream_iterator使用>>来读取流,因此元素类型必须定义了输入运算符:
|istream_iterator<int> intReader(cin); // 从cin读取int istream_iterator<int> eof; // 尾后迭代器 vector<int> numbers; while (intReader != eof) { numbers.push_back(*intReader++); }
更简洁的写法是直接用迭代器构造容器:
|istream_iterator<int> intReader(cin), eof; vector<int> numbers(intReader, eof); // 从cin读取直到EOF或错误
我们还可以将istream_iterator与算法结合使用:
|istream_iterator<int> input(cin), eof; cout << "数字总和:" << accumulate(input, eof, 0) << endl;
ostream_iterator可以为任何定义了输出运算符的类型创建。创建时可以提供可选的第二个参数,指定每个元素后输出的字符串:
|ostream_iterator<int> intWriter(cout, " "); // 每个int后跟一个空格 ostream_iterator<string> stringWriter(cout, "\n"); // 每个string后跟换行
使用ostream_iterator输出序列:
|vector<int> scores = {85, 92, 78, 96, 88}; copy(scores.begin(), scores.end(), intWriter); cout << endl;
我们甚至可以用流迭代器重写一些经典程序。例如,从输入读取、排序并输出:
|istream_iterator<string> wordInput(cin), eof; ostream_iterator<string> wordOutput(cout, " "); vector<string> words(wordInput, eof); // 读取所有单词 sort(words.begin(), words.end()); // 排序 copy(words.begin(), words.end(), wordOutput);
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增操作会移动到前一个元素,递减操作会移动到后一个元素。
除了forward_list之外,所有容器都支持反向迭代器。我们通过调用rbegin、rend、crbegin和crend成员函数来获取反向迭代器:
|vector<int> numbers = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // 反向输出所有元素 for (auto rit = numbers.crbegin(); rit != numbers.crend(); ++rit) { cout << *
反向迭代器的一个重要应用是反向排序:
|vector<string> words = {"apple", "banana", "cherry", "date"}; sort(words.rbegin(), words.rend()); // 降序排序 // words现在是:{"date", "cherry", "banana", "apple"}
反向迭代器和普通迭代器之间存在微妙但重要的关系。考虑这样一个场景:我们有一个逗号分隔的字符串,想要找到最后一个单词。
使用普通迭代器找第一个单词很简单:
|string text = "programming,algorithm,data,structure"; auto comma = find(text.cbegin(), text.cend(), ','); cout << string(text.cbegin(), comma) << endl; // 输出:programming
使用反向迭代器找最后一个单词:
|auto rcomma = find(text.crbegin(), text.crend(), ',');
但是现在我们不能直接使用rcomma来构造字符串,因为反向迭代器会产生相反的结果。我们需要将反向迭代器转换为普通迭代器:
|cout << string(rcomma.base(), text.cend()) << endl; // 输出:structure
base成员函数返回对应的普通迭代器。这种转换是必要的,因为反向迭代器和普通迭代器表示的是相邻的位置,以确保元素范围的一致性。
虽然泛型算法能够适用于多种容器类型,但某些容器为了获得更好的性能,提供了特定的算法实现。list和forward_list类型定义了几个成员函数版本的算法:sort、merge、remove、reverse和unique。
由于list和forward_list分别提供双向和前向迭代器,它们不能使用需要随机访问迭代器的泛型算法(如sort)。对于其他算法,虽然可以使用泛型版本,但成员函数版本通常具有更好的性能。
例如,泛型的remove算法需要重排序列来"删除"元素,而list的remove成员可以直接断开链接。类似地,泛型的merge算法需要将结果写入目标序列,而list的merge成员可以通过重新链接节点来合并列表。
|list<int> numbers1 = {1, 3, 5, 7, 9}; list<int> numbers2 = {2, 4, 6, 8, 10}; // 使用list特定的merge算法 numbers1.merge(numbers2); // numbers2变为空,所有元素移到numbers1中 // numbers1现在包含:{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // 删除所有偶数 numbers1.remove_if([](
需要注意的是,list特定的算法会改变底层容器。例如,remove成员函数会实际删除元素,而不是像泛型算法那样仅仅移动元素。
list和forward_list还提供了特有的splice操作,用于在链表之间移动元素:
|list<string> taskList1 = {"任务A", "任务B"}; list<string> taskList2 = {"任务C", "任务D", "任务E"}; // 将taskList2的所有元素移动到taskList1的开头 taskList1.splice(taskList1.begin(), taskList2); // taskList1: {"任务C", "任务D", "任务E", "任务A", "任务B"} // taskList2: {} (空) list<string> urgentTasks = {"紧急任务1"
这些操作对于需要频繁重排元素的应用非常有用,因为它们只需要修改指针,而不需要复制元素值。
通过学习泛型算法,我们获得了一套强大的工具来处理容器中的数据。这些算法体现了C++标准库的设计哲学:提供高效、通用、可组合的组件。掌握这些算法的使用方法和设计原理,将大大提高我们编写高质量C++程序的能力。
现在让我们通过一些简单的练习题来巩固本章学到的知识。每道题都涵盖了STL算法的核心概念,请先尝试独立完成,然后再查看答案。
编写一个程序,使用find算法在vector中查找指定元素,如果找到则输出其位置,否则输出"未找到"。
|#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v = {10, 20, 30, 40, 50}; int target = 30; // 使用find算法查找元素 auto it = find(v.
使用lambda表达式和count_if算法,统计vector中所有大于50的元素个数。
|#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> numbers = {30, 60, 45, 80, 25, 90, 55}; // 使用lambda表达式和count_if统计大于50的元素 int count = count_if(numbers.begin
给定一个包含重复元素的vector,编写代码删除重复元素并按降序排列。
|#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> scores = {85, 90, 75, 85, 90, 80, 75, 95}; // 先排序(升序) sort(scores.begin(), scores.
给定一个包含学生成绩的vector,统计及格(≥60分)的成绩数量,并输出所有及格成绩。
|#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> scores = {85, 45, 90, 55, 75, 30, 95, 60}; // 统计及格成绩数量 int passCount = count_if
编写一个程序,对vector中的元素进行以下操作:
|#include <iostream> #include <vector> #include <algorithm> #include <numeric> using namespace std; int main() { vector<int> numbers = {10, 20, 30, 40, 50}; // 找出最大值和最小值 auto maxIt = max_element(numbers.begin(), numbers.
find算法在指定范围内查找元素,返回指向找到元素的迭代器,如果没找到则返回end()迭代器。使用distance函数可以计算迭代器之间的距离,即元素的位置。
lambda表达式让我们可以在需要的地方直接定义函数,不需要单独定义函数。[](int n) { return n > 50; }是一个lambda表达式,[]是捕获列表(这里为空),(int n)是参数列表,{ return n > 50; }是函数体。count_if算法统计满足条件的元素个数。
sort算法用于排序,默认是升序。使用greater<int>()作为比较函数可以实现降序。unique算法将相邻的重复元素移到容器末尾,返回指向第一个重复元素的迭代器,然后使用erase删除这些重复元素。注意unique只能处理相邻的重复元素,所以需要先排序。
count_if用于统计满足条件的元素个数,for_each用于对每个元素执行操作。两者都可以配合lambda表达式使用,让代码更加简洁和灵活。
max_element和min_element返回指向最大/最小元素的迭代器。accumulate用于累加计算,第三个参数是初始值。transform算法对每个元素应用一个函数,并将结果写回原位置(或另一个容器)。这些算法配合lambda表达式可以高效地处理各种数据操作。