C++primer(5th)——泛型算法

为什么会有泛型算法?因为在顺序容器中,只定义了很少的操作,这些完全不够我们对数据的处理。标准库定义了一组泛型算法,之所以称其为算法,是因为他们实现了一些经典算法的公共接口,比如搜索和排序。之所以称之为泛型,是因为他们可以用于不同类型元素和多种容器类型。

以一般的迭代器算法find为例

string val = "a value";
auto result = find(lst.cbegin(),lst.cend(),val);

实现了在string的list中查找一个给定值。

更具find算法的流程我们知道,除了比较迭代器中元素与要查找值之外,别的操作都可以用迭代器操作来实现。因此,我们知道,迭代器算法不依赖于容器,但是算法依赖于元素类型的操作。泛型算法本身不会执行容器的操作,他们只会运行于迭代器之上,执行迭代器的操作。从而得到一个和总要结论:算法永远不会改变底层容器的大小。算法肯呢个改变容器中保存的元素的值,也可能在容器总移动元素,但是永远不会直接添加或者删除元素。

只读算法。只会读取输入范围的元素,而不改变元素。比如find,count,accumulate。以accumulate为例

int sum = accumulate(vec.cbegin(), vec.cend(),0);

这里sum设置为和,和的初值为0。此处的0决定了函数中使用哪个加法运算符以及返回值的类型(int,double,long long)或者任何可以加到int上面的类型。对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()。equal也是另一种只读算法如下:

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

equal使用迭代器来完成操作,因此可以比较两个不同类型的容器中的元素。而且,元素类型也可以不一样。比如,roster1可以是vector<string>,二roster2可以是list<const char*>。

写容器元素的算法。将新的值赋予给序列中的元素,并且我们必须得保证,序列的原始大小必须要不小于我们即将写入的元素数目。(fill算法)

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

空间的规定确实比较麻烦,因此,有一种能够保证算法有足够空间来容纳输出数据的方法就是,使用插入迭代器(insert_itrator)。在文件中写入数据时我们用到的插入迭代器类型是back_inserter()。

另外一个向目标位置迭代器指向的输出序列中的元素写入数据的算法就是copy算法。

	int a1[] = { 0,1,2,3,4,5,6,7,8,9 };
	int a2[sizeof(a1)];

	auto ret = copy(begin(a1), end(a1), a2);

	for (int i = 0; i < 10; i++)
	{
		cout << a2[i] << endl;
	}

输出的a2与a1一致。copy返回的是目的位置迭代器的值。也就是ret只想的是a2的尾元素之后的位置。

当然,还有一些算法是会重新排序容器中的元素的,如sort()算法。sort算法和unique算法的一起使用时相当常见的,因为,unique算法需要对有序的序列中相邻的元素进行“去除”,这里的“去除”并不是真正意义上的删除,而是覆盖相邻的重复元素,使不重复的元素出现在前面。这里一定要注意的是:标准算法库对迭代器而不是容器进行操作。而想要真正的删除元素,可以使用erase。

我们也可以通过函数来限定出书序列的排列顺序,这就是使用到了谓词,谓词有一元谓词和二元谓词(分别代表接受一个参数和接受两个参数)。在函数外定义,在函数内调用。如,定义一个按长度排序(由短到长)的函数。

bool isShorter(const string& s1, const string& s2)
{
	return s1.size() < s2.size();
}
.
.
.	
//添加isSshorter可以按长度来排序单词
sort(words.begin(), words.end(),isShorter);
for (string& s : words)
	{
		cout << s;
	}
cout << words.size() << endl;

有时,我们希望对更多的操作数进行操作,比如三个或以上,lambda表达式可以做到。定义如下:

//capture list是一个lambda所在函数中定义的局部变量的列表(通常为空)
//剩下的参数与普通函数一样
[capture list](parameter list)-> return type{function body}

值得一提的是方法是尾置返回来指定返回类型。我们可以忽略他的参数列表和返回类型,但是必须包括捕捉列表和函数体。如下所示:

auto f=[]{return 42;};

我们此时定义了一个f,它不接受参数,直接返回42。lambda的调用方式和普通函数调用方式一致,都是使用调用运算符。lambda通过将局部变量包含在其捕捉列表中来指出将会使用这些变量,并且这个变量必须是所在函数中的局部变量。

	stable_sort(words.begin(), words.end(), [sz](const string& a, const string& b)
		{return a.size() < sz; });

 用该方法调用find_if,可以达到查找第一个长度大于sz的元素。

	auto wc = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() >= sz; });

可以使用find_if返回的迭代器来计算从它开始到words的末尾一共有多少个元素。我们也可以使用for_each算法来打印words中长度大于sz的元素。

使用lambda时,编译器生成一个与lambda对应的新的类类型——当一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。对lambda表达式进行值捕获:

	//size_t是unsinged int 型
	size_t v1 = 42;
	//将v1拷贝到名为f的可调用对象
	auto f=[v1]{ return v1; };
	v1 = 0;
	auto j = f();//j为42,v1保存了我们创建它时v1的拷贝

 当然,也可以采用引用方式来捕获变量:

	size_t v1 = 42;
	//对象f包含v1的引用
	auto f = [&v1] {return v1; };
	v1 = 0;
	auto j = f();
	cout << j << endl;//j为0;f保存v1的引用,而非拷贝

当我们使用引用捕获的方式时,我们就必须确保被引用的对象在lambda执行的时候是存在的。因为在捕获变量时必须要保证我们所需要的值存在并且没有被改变,这是复杂的,所以需要尽量不去捕获引用。

除此之外,我们可以进行隐式捕获,也就是让编译器根据lambda中的代码推断我们需要使用哪种变量。特征就是在捕获列表中写一个&或=,&代表采用捕获引用方式,=表示使用值捕获方式。

void biggies(vector<string>& words, vector<string>::size_type sz, ostream& os = cout, char c = ' ')
{
	//os隐式捕获方式,c显示捕获,值捕获方式
	for_each(words.begin(), words.end(), [&, c](const string& s) {os << s << c; });
	//os显示捕获方式,引用捕获方式,c隐式捕获,值捕获方式
	for_each(words.begin(), words.end(), [=, &os](const string& s) {os << s << c; });

}

当我们混合使用隐式捕获方式和显式捕获方式时,捕获列表中第一个元素必须是&或=。它制定了默认捕获方式为引用或值。

默认情况下,对于一个被拷贝的变量,lambda不会改变其值,要是我们希望能改变的时候,需要加关键字mutable,它能够省略参数列表:

	size_t v1 = 42;
	//f可以改变它所捕获的变量的值
	auto f = [v1]()mutable {return ++v1; };
	v1 = 0;
	auto j = f();
	cout << j << endl;//j值为43

 指定lambda的返回类型:

transform(vi.begin(),vi.end(),vi.begin(),[](int i){return i<0?-i:i;});

tranform方法接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个表示目的位置。 上述代码表示将一个序列中的每个负数替换成其绝对值。

如果我们需要再很多地方使用相同的操作,通常使用lambda来构造一个函数,但是遇到像find_if这样的接受单一参数的函数,我么必须使用bind函数。bind函数可以看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。形式为:

auto newCallable=bind(callable,arg_list);

当我们调用newCallable时, newCallable回调用callable,并传递给它arg_list中的参数。arg_list中可能包含形如_n的名字,n是一个整数,这些参数是占位符,表示newCallable的参数,数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数...,因为_n的使用,我们在使用bind方法前需要提前加上新的命名空间using namespace placeholder;一般情况下,我们可以用bind绑定给定可调用对象中的参数或重新安排其顺序。例如:

//g是有两个参数的可调用对象
auto g=bind(f,a,b,_2,c,_1);

 g这个新的可调用对象,存在两个参数,_2,_1这两个,当我们调用它时,其中第一个参数_1会被作为f的最后一个参数,第二个参数_2作为f的第三个参数,比如g(x,y),会调用f(a,b,y,c,x)。当然,也可以用来重排参数顺序。

	vector<string>words{ "hello","shad","shy"};
	//按单词长度由短到长进行排序
	sort(words.begin(), words.end(), isSshorter);
	//按单词长度由长到短进行排序
	sort(words.begin(), words.end(), bind(isSshorter(_2, _1)));

通过bind方法,我们可以是isShorter()的参数顺序对调,达到相反的排序效果。当我们需要对bind中的参数以引用方式进行传递或是要绑定参数的类型无法拷贝。比如,为了替换一个引用方式捕获ostream的lambda:

//os是一个局部标量,引用一个输出流
//c是一个局部变量类型我char
for_each(words.begin(0,words,end(),[&os,c](const string &s){os<<s<<c;});

可以很容易的编写一个函数,来完成相同的工作:

ostream &print(ostream &os,const string &s,char c)
{
    return os<<s<<c;
}

 但是,我们不能直接用bind来代替对os的捕获,如果我们希望传递给bind一个对象而又不拷贝它,就必须适应标准库ref函数。

在标准库头文件iterator中还定义了额外集中迭代器:插入迭代器(insert iterator),流迭代器(stream iterator),反向迭代器(reverse iterator)移动迭代器(move iterator)。

插入迭代器被绑定到一个容器上,可用来向容器插入元素。接受一个容器,生成一个迭代器能实现给定容器添加元素,当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来给定容器的指定位置插入一个元素。插入器有三种类型:

back_inserter:创建一个使用push_back的迭代器

front_inserter:创建一个使用push_front的迭代器

inserter:创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

	//创建容器
	list<int>l1{ 1,2,3,4 };
	list<int>l2, l3, l4;//空容器
	//以front_inserter的形式进行拷贝
	copy(l1.begin(),l1.end(), front_inserter(l2));//4321,前向插入会改变元素顺序
	//以inserter的形式进行拷贝
	copy(l1.begin(), l1.end(), inserter(l3,l3.begin()));//1234,inserter不会改变元素顺序
	//以back_inserter的形式进行拷贝
	copy(l1.begin(), l1.end(), back_inserter(l4));//1234,back_inserter不会改变元素顺序

	//输出容器元素
	for (auto i : l2)
		cout << i ;
	cout << endl;
	for (auto i : l3)
		cout << i ;
	cout << endl;
	for (auto i : l4)
		cout << i;
	cout << endl;

还有一种unique_copy方法可以直接将元素进行非重复拷贝:

	vector<string>v1{ "a","aaa","a","shy","gj","gj"};
	list<string>l1;
	//字典序排序
	sort(v1.begin(), v1.end());
	//尾插
	unique_copy(v1.begin(), v1.end(),back_inserter(l1));//aaaashygj

	for (auto i : l1)
		cout << i;
	cout << endl;

iostream迭代器:istream_iterator读取输出流,ostream_iterator向一个输出流写数据,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator操作:当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。当创建一个istream_iterator时,我们可以将他绑定到一个流:

istream_iterator<int>int_it(cin);//从cin读取int
istream_iterator<int>int_eof;//尾后迭代器
ifstream in("afile");
istream_iterator<string>str_it(in);//从“afile”读取字符串
//从标准输入读取数据,存入vector
istream_iterator<int>in_iter(cin);//从cin读取int
istream_iterator<int>eof;//istream尾后迭代器
while(in_itr!=eof)//读取到最后一个字符
    //后置递增运算读取流,返回迭代器的旧值
    //解引用迭代器,获得从流读取的前一个值
    vec.push_back(*in_iter++);

对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。后置递增运算会从流中读取下一个值,向前推进,但返回的是迭代器的旧值,迭代器的旧值包含了从流中读取的前一个值,对迭代器进行解引用就能获得此值。也可以将程序改写为:

istream_iterator<int>in_itr(cin),eof;//从cin读取int
vector<int>vec(in_itr,eof);//从迭代器范围构造vec

采用一对表示元素范围的迭代器来构造vec,两个迭代器都是istream_iterator,意味着元素范围是通过从关联的流中读取数据获得的。

由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此我们至少可以使用某些算法来操作流迭代器:

	istream_iterator<int>in(cin), eof;
	//accumulate函数在numeric库中
	cout << accumulate(in, eof, 0) << endl;

stream_iterator操作:对任何具有输出运算符<<的类型定义ostream_iterator,创建一个ostream_iterator是,我们可以提供第二个参数,他是一个字符串,在输出每个元素之后都会打印此字符串,此字符串必须是一个C风格字符串(一个字符串字面常量或者一个指向以空字符串结尾的字符数组的指针),必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。可以用ostream_iterator来输出值的序列:

	vector<int>vec{ 1,2,3,4,5,6,7 };
	//out_iter将int类型的值写到输出流cout中,每个值后面都跟一个空格
	ostream_iterator<int>out_iter(cout, " ");
	for (auto e : vec)
		//赋值语句将e写到cout上
		*out_iter++ = e;
	cout << endl;

当我们向out_iter赋值时,可以忽略解引用和递增运算,重写为这样:

for(auto e:vec)
    out_iter=e;
cout<<endl;

运算符*和++实际上对ostream_iterator对象不做任何事情。但是,一般推荐第一种形式,方便与其他迭代器修改。

反向别带起就是在容器中从尾元素向首元素方向移动的迭代器。对于反向迭代器,递增操作的含义会颠倒过来,递增一个反向迭代器(++it)会移动到前一个元素:递减一个迭代器会移动到下一个元素:

	vector<int>v{ 1,2,3,4,5,6,7 };
	//crbegin()表示返回逆序迭代器,数据类型是const_iterator
	for (auto i = v.crbegin(); i != v.crend(); i++)
		cout << *i;

 假设有一个string,保存在一个逗号分隔的单词列表,要是想要打印其中的第一个单词,使用find函数可以很快完成:

	string v1{ "shy is good, gj is nice!" };
	auto comma=find(v1.cbegin(), v1.cend(), ',');
	cout << string(v1.cbegin(),comma) << endl;

这样打印出的结果就是,之前的内容,如果string中没有,那么将打印出全部内容。如果我们希望打印最后一个单词,可以改用反向迭代器:

	string v1{ "shy is good, gj is nice!" };
	auto comma=find(v1.cbegin(), v1.cend(), ',');
	auto rcomma = find(v1.crbegin(), v1.crend(), ',');
	cout << string(v1.cbegin(),comma) << endl;
	//必须使用base方法将反向迭代器转化回一个普通迭代器,否则,输出内容将会是倒着输出的。
	cout << string(rcomma.base(),v1.cend()) << endl;

泛型算法结构:包含5个迭代器类别:

 迭代器按他们所提供的操作来分类而这种分类形成了一种层次,除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。

输入迭代器:可以读取序列中的元素 ,一个输入迭代器必须支持:①用于比较两个迭代器的相等和不相等运算符(==、!=)②用于推进迭代器的前置和后置递增运算(++)③用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧④箭头运算符(->)等价于(it).member,即解引用迭代器,并提取对象的成员。

输出迭代器:可以看做输入迭代器的功能上的补集,只写而不读元素,输出迭代器必须支持:①用于推进迭代器的潜质和后置递增运算(++)②解引用运算符(*),只能出现在赋值运算符的左侧(向一个已经解引用的输出迭代器复制,就是将值写入它所指向的元素)。

前向迭代器:读写元素。只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。replace()

双向迭代器:可以正反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(--)。reverse()

随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器的所有功能(<,<=,.,>=)(+,+=,-,-=)(-)(iter[n]与*(iter[n])等价)sort(),其中array,deque,vector都是随机访问迭代器,用于访问内助数组的指针也是。

list上的迭代器属于双向迭代器,copy()前两个参数为输入迭代器,第三个为输出迭代器。

注意:向输出迭代器中写入的算法,都假定目标空间能够容纳写入的数据。 

算法的命名规则: 一些算法使用重载形式传递一个谓词。函数的一个版本使用元素类型的运算符来比较元素;另一个版本接受一个额外为此参数,来代替<或==:

unique(beg,end);//使用==运算符比较元素
unique(beg,end);//使用comp比较元素

 这两个调用都是重新整理给定序列,将相邻的重复元素删除。还有一种是_if版本的算法,这种算法接受一个谓词,接受谓词参数的算法都有附加的_if前缀:

find(beg,end,val);//查找输入范围中val第一次出现的位置
find_if(beg,end,pred);//查找第一个令pred为真的元素

find查找一个特定值,find_id 查找是的pred为返回值非零的元素。默认情况下,重拍元素的算法将重排后的元素写会给定的序列中,这些算法还提供另外一个版本,将元素写到一个指定的输出目的的位置。写到额外目的空间的算法都在名字后面附加一个_copy:

reverse(beg,end);//反转输入范围中元素的顺序
reverse_copy(beg,end,dest);//将元素按逆序拷贝到dest

一些算法同时提供_copy和_if两种版本。这些版本接受一个目的位置迭代器和一个谓词:

//从v1中删除奇数元素
remove_if(v1.begin(),v1.end(),[](int i){return i%2;});
//将偶数元素从v1拷贝到v2;v1不变
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),[](int i){return i%2;});

再比如,

replace_if(beg,end,pred,new_val);//表示在beg到end这个范围中,将满足谓词条件的值替换为new_val
replace_copy(beg,end,dest,old_val,new_val);//表示在范围中将old_val替换为new_val,不改变原始值,复制到dest中
replace_copy_if(beg,end,dest,pred,new_val);//范围中满足谓词条件的替换为new_val,不改变原始值,复制到dest中

特定容器算法:与其他容器不同,链表类型list和forward_list定义了几个成语函数形式的算法。他们定义了独有的sort、merge、remove、reverse和unique。 通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真的减缓他们的值来快速交换元素。因此,这些链表版本的算法性能比对应的通用版本好得多。

//list和forward_list成员函数版本的算法///
//这些操作都返回void
lst.merge(lst2); //将来自lst2的元素合并入lst.lst和lst2都必须是有序的
lst.merge(lst2.comp);//元素将从lst2中删除,在合并之后,list2变为空。第一个版本使用<运算符,第二个版本使用给定的比较操作
lst.remove(val)//lst.remove_if(pred)  调用erase删除掉与给定值相等或令一元谓词为真的每个元素
lst.reverse()//反转lst中的元素顺序
lst.sort()//lst.sort(comp)//使用<或给定比较操作排序元素
lst.unique()//lst.unique(pred)//调用erase删除同一个值的连续拷贝,第一个版本使用==第二个版本使用给定的二元谓词

链表类型还定义了splice算法,此算法是链表数据结构所特有的,因此不需要通用版本。

lst.splice(args)或flst.splice_after(args)
//p为list1中某个位置的迭代器
(p,lst2);//将lst2中元素剪切到lst的p位置
(p,lst2,iter);//将lst2中iter处的元素剪切到lst1中p处
(p,lst2,iter1,iter2);//将lst2中iter1到iter2的元素剪切到lst1中的p位置

立案料特有的操作会改变底层的容器。如,remove的链表版本会删除指定的元素,unique的链表版本会删除第二个和后续的重复元素。类似的merge和splice会销毁其参数。通用版本的merge将合并的序列写到一个给定的迭代器;两个序列是不变的。而链表版本的merge函数会销毁给定的链表——元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在merge之后,来自两个链表中的元素仍然会存在,但是他们都已经在同一个链表中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值