在上一讲中,我构造了一个排序函数的代码,该函数使用指针对数组中的数据进行排序。
//对begin和end之间的项进行排序。// end应该指向正在排序的值范围的末尾。void sort(int *begin,int *end){//使用插入排序对数组进行排序int *mover = begin;//移动对象指向要插入的元素。发+ +;While (mover != end) {int value = *mover;//被插入项的值//当前指针将扫描数组中出现在移动位置之前的部分//Int *current = mover;While (current != begin) {// previous指向当前之前的位置int *previous = current;前,;//只要我们一直看到大于//要插入的值的数字,我们就会继续把东西移到一边,为我们想要插入的值腾出空间。如果(*先前的>值){*当前= *先前;当前,;} else break;} //将值放入合适的位置,并继续插入下一个//项。*current = value;发+ +;}}
这段代码的一个限制是赢博体育的指针都被声明为指向int的指针。这意味着,如果我们想用这段代码对double类型的数组进行排序,我们就不走运了。为了使排序代码更加灵活,我们可以将此函数转换为模板函数。
//对begin和end之间的项进行排序。// end应该指向正在排序的值范围的末尾。模板<typename T> void sort(T *begin,T *end){//使用插入排序对数组进行排序T *mover = begin;//移动对象指向要插入的元素。发+ +;while(mover != end) {T value = *mover;//被插入项的值//当前指针将扫描数组中出现在移动位置之前的部分//T *电流=动器;while(current != begin) {// previous指向当前T之前的位置*previous = current;前,;//只要我们一直看到大于//要插入的值的数字,我们就会继续把东西移到一边,为我们想要插入的值腾出空间。如果(*先前的>值){*当前= *先前;当前,;} else break;} //将值放入合适的位置,并继续插入下一个//项。*current = value;发+ +;}}
《宣言》
模板<typename T
将此函数标记为模板函数。在模板函数中,用占位符替换一个或多个类型。在本例中,将int类型替换为占位符T。随后在程序中使用该函数时,编译器将检查传递给排序函数的指针的类型,以确定占位符T的类型。
例如,如果我们有一个已声明的数组B
double *B = new double[100];
我们呼唤
排序(B, B + 100);
编译器可以自动确定应该在模板化排序函数代码中出现T的任何地方将T替换为double。
模板机制使得在同一个程序中对几种不同类型的数组进行排序成为可能。例如,如果我们正在编写一个带有数组的程序
int *A = new int[100];double *B = new double[100];
以后这样做是完全合法的
排序(+ 100);排序(B, B + 100);
在第一种情况下,编译器将自动生成sort函数的一个版本来对整型数组进行排序。在第二种情况下,它将编译一个可以对双精度类型数组进行排序的版本。
在上一讲中,我们看到可以使用指针在数组上实现算法。为了实现这一点,指针必须支持一组键操作:使用++或——进行移动、比较和解引用。一旦我们认识到指针允许赢博体育这些操作,我们就可以将几乎任何使用传统索引表示法(如A[n])的算法重新定义为使用指针执行相同操作的算法。
使用指针处理数组的想法可以被扩展到处理非数组的数据结构。由于我们将在本课程中学习各种不同的数据结构,因此将指针概念扩展到其他非数组数据结构是值得的。这样做的主要障碍是,用于在数组元素中向前或向后移动指针的++和——操作符在其他数据结构中可能没有自然等效的操作符。特别是,如果我们正在处理一个非线性数据结构,其中的元素不是按线性序列排列的,那么++和——操作可能不再有意义。
为了将指针概念引入到更广泛的数据结构中,c++使用了指针概念的泛化,称为迭代器。迭代器通常是一个与特定数据结构密切相关的类,它是该数据结构的指针概念的泛化。在任何给定的时刻,迭代器“指向”底层数据结构的特定元素。一个有效的迭代器必须支持一组核心操作:
*
应该返回迭代器当前指向的元素。++
,并向后移动一项--
.==
和! =
。的==
操作符只有在两个迭代器都指向同一数据结构的相同元素时才返回true。下面是上面的排序算法的一个版本,它被设计成使用迭代器而不是指针。
template <typename iter> void sort(iter begin,iter end){//使用插入排序对数组进行排序iter mover = begin;//移动对象指向要插入的元素。发+ +;while(mover != end){//插入项的值auto value = *mover;//当前指针将扫描数组中出现在移动位置之前的部分。Iter电流=动器;While (current != begin) {// previous指向当前iter之前的位置previous = current;前,;//只要我们一直看到大于//要插入的值的数字,我们就会继续把东西移到一边,为我们想要插入的值腾出空间。如果(*先前的>值){*当前= *先前;当前,;} else break;} //将值放入合适的位置,并继续插入下一个//项。*current = value;发+ +;}}
在大多数情况下,这段代码看起来与原始代码非常相似。这是很自然的,因为迭代器是指针的泛化,我们可以用指针做的几乎赢博体育事情,我们也可以用迭代器做。
这两个版本之间只有一个显著的区别。在之前的版本中,在while循环的开始,我们看到了一条语句
T value = *mover;//插入项的值
在这个语句中,我们对指针移动对象解引用,以获得对指针所指向的项的访问。然后将该值存储在名为value的变量中以供以后使用。注意,变量的类型是T,也就是这里讨论的指针所指向的类型。(注意,begin、end和mover都被声明为类型T*,意思是“指向T的指针”。)
第二个版本中的等价语句是
Auto value = *mover;
我们再次对迭代器动器解引用,以便访问迭代器所指向的值。一旦获得该值,我们希望将其存储在一个名为value的变量中。这一次的复杂之处在于,我们不能立即访问value变量的正确类型。因为指针将被声明为T*,我们可以直接从指针的类型声明中看到指针指向T类型的项。对于迭代器,我们只知道我们有一个迭代器,但是我们没有直接的信息告诉我们迭代器指向什么类型的东西。幸运的是,我们可以让编译器为我们解决这个问题。从c++ 11开始,c++语言现在具有自动变量类型声明的特性。在自动类型声明中,你实际上是对编译器说:“我不知道这个东西的类型是什么,所以请为我推断它的类型。”
赢博体育现代c++编译器都支持c++ 11标准,应该能够成功地编译这些代码。如果您使用的编译器不支持此标准,则可以使用更复杂的机制来推断该类型,即iterator_traits类。给定一个迭代器类型,iterator_traits可以推断出关于该迭代器的有用信息,包括迭代器指向的值类型。下面是如何使用这种方法来声明value变量:
Typename std::iterator_traits<iter>::value_type value = *mover;
除了这一条语句之外,插入排序迭代器版本中的其余代码看起来与它所基于的指针版本完全相同。
由于迭代器是指针的泛化,因此测试为使用迭代器而编写的一段代码的第一个也是最简单的方法是将其与指针一起使用。下面是一个完整的程序,展示了插入排序的迭代器版本与数组和指向该数组的指针结合使用。
#include <iostream> #include <fstream> //对开始和结束之间的项进行排序。// end应该指向正在排序的值范围的末尾。template <typename iter> void sort(iter begin,iter end){//使用插入排序对数组进行排序iter mover = begin;//移动对象指向要插入的元素。发+ +;While (mover != end) {auto value = *mover;//被插入项的值//当前指针将扫描数组中出现在移动位置之前的部分//Iter电流=动器;While (current != begin) {// previous指向当前iter之前的位置previous = current;前,;//只要我们一直看到大于//要插入的值的数字,我们就会继续把东西移到一边,为我们想要插入的值腾出空间。如果(*先前的>值){*当前= *先前;当前,;} else break;} //将值放入合适的位置,并继续插入下一个//项。*current = value;发+ +;}} int main(int argc,const char* argv[]) {std::ifstream in;std:: ofstream;int N = 100;Int n = 0;int k;int* A = new int[N];in.open(“numbers.txt”);int x;while(in >> x){//根据需要调整A数组的大小。if(n == n) {int* B = new int[2* n];for(k = 0;k < n;k++) B[k] = A[k];删除();A = b;N *= 2;} A[n] = x;n + +;} in.close ();std::cout << “从文件中读取” << n << “整数。”< < std:: endl;排序(一+ n);out.open(“sorted.txt”);for(k = 0;k < n;k++) out << A[k] << std::endl;out.close ();删除();std::cout << “已完成排序和保存。”< < std:: endl;返回0;}
main中的关键行是
排序(一+ n);
在这个语句中,A和A+n都是指针。A指向所讨论的数组的开始,而A+n指向数组的末尾。由于这两个指针也是交互器,因此将它们作为参数传递给迭代器排序函数是完全合法的。
在Java中,我们使用ArrayList类。ArrayList类本质上是一个可以自动调整自身大小的奇特数组。与Java ArrayList等价的c++类是vector类。vector本质上是一个灵活的数组,它可以自动调整自己的大小以适应新的项。
下面是c++ vector类的一些关键特性。
# include <向量>
v
我们说std::向量v < int >;
std::向量v < int > (100);
std::向量< int > v (100 0);
V [0] = 12;
尺寸()
方法。为(n = 0; n < v.size (); n + +) std:: cout < < v [n] < < std:: endl;
push_back方法
方法。v.push_back (20);
开始()
方法。结束()
方法。下面是排序程序的一个版本,它使用向量来存储数据。
#include <iostream> #include <fstream> #include <vector> //对开始和结束之间的项进行排序// end应该指向正在排序的值范围的末尾。template <typename iter> void sort(iter begin,iter end){//使用插入排序对数组进行排序iter mover = begin;//移动对象指向要插入的元素。发+ +;While (mover != end) {auto value = *mover;//被插入项的值//当前指针将扫描数组中出现在移动位置之前的部分//Iter电流=动器;While (current != begin) {// previous指向当前iter之前的位置previous = current;前,;//只要我们一直看到大于//要插入的值的数字,我们就会继续把东西移到一边,为我们想要插入的值腾出空间。如果(*先前的>值){*当前= *先前;当前,;} else break;} //将值放入合适的位置,并继续插入下一个//项。*current = value;发+ +;}} int main(int argc,const char* argv[]) {std::ifstream in;std:: ofstream;std::向量v < int >;in.open(“numbers.txt”);int x;While (in >> x) {v.push_back(x);} in.close ();std::cout << “Read “ << v.size() << ”从文件中取出整数。”< < std:: endl;sort(逆序函数(),v.end ());out.open(“sorted.txt”);For (unsigned int k = 0;k < v.size();k++) out << v[k] << std::endl;out.close ();std::cout << “已完成排序和保存。”< < std:: endl;返回0;}
程序创建一个初始为空的int型vector,并使用push_back方法将从输入文件中读取的每个新int型push到v vector的后面。v向量将自动调整大小,以准确地容纳赢博体育数字所需的大小。
这里使用的排序函数与上一个程序完全相同。由于该sort函数被设计为与迭代器一起工作,因此它可以与vector类无缝连接:
sort(逆序函数(),v.end ());
该语句获取一对迭代器,一个指向vector的起点,另一个指向vector的末尾,并将这两个迭代器传递给sort函数,该函数期望接收一对迭代器。
c++ vector类是c++标准模板库(Standard Template Library, STL)的一部分。STL包含许多实现有用数据结构的类。随着课程的进展,我们将学习许多这些数据结构类。
除了数据结构之外,STL还提供了一组标准算法,这些算法被设计用于处理STL数据结构。例如,STL算法集合有一个排序函数,可用于对存储在STL数据结构中的数据进行排序。STL版本的排序函数与上面演示的排序函数完全相同。给定一对迭代器,一个指向范围的开始,另一个指向范围的末尾,STL排序算法可以快速正确地对数据项进行排序。
下面是我们重写的程序,将vector类与STL排序算法结合使用。
#include <iostream> #include <fstream> #include <vector> #include <algorithm> int main(int argc,const char* argv[]) {std::ifstream in;std:: ofstream;std::向量v < int >;in.open(“numbers.txt”);int x;While (in >> x) {v.push_back(x);} in.close ();std::cout << “Read “ << v.size() << ”从文件中取出整数。”< < std:: endl;std::类(逆序函数(),v.end ());out.open(“sorted.txt”);(汽车itr =逆序函数();itr ! = v.end (); itr + +)出< < * itr < < std:: endl;out.close ();std::cout << “已完成排序和保存。”< < std:: endl;返回0;}
使用std::sort的唯一额外要求是必须包含
Std::sort可以处理任何看起来像迭代器的对象。特别是,还可以使用std::sort对存储在传统数组中的数据进行排序。例如,如果A指向一个包含n个整型数的数组,我们可以这样对A排序
std::排序(一+ n);
使用std::sort的另一个好处是,std::sort使用的排序算法比我在示例程序的早期版本中使用的简单插入排序算法要复杂得多,速度也快得多。