C++泛型算法介绍导引

迄今为止,C++头文件<algorithm>已经提供了一百多种标准库泛型算法。之所以说这些算法是泛型的,是因为这些算法对底层容器本身是什么并没有要求。事实上,这些算法甚至不知道它所操作的底层容器到底是什么。让算法工作的核心是迭代器(iterator)。在这里我们把指针也看成是迭代器。虽然严格来讲指针并不是迭代器,但他们在概念上是很相似的,在这里看成一样不会造成什么错误。

举个例子,我们最熟悉的算法就是标准库的快速排序函数sort了。标准库下的sort有两个版本,原型是这样的:

namespace std
{
    template <typename RandomIter>
    void sort(RandomIter First, RandomIter Last);

    template <typename RandomIter, typename BinPred>
    void sort(RandomIter First, RandomIter Last, BinPred Pr);
}

其中第一个版本最简单,接受两个“随机访问迭代器”,第一个代表要排序的首元素,第二个代表要排序的尾元素的下一个位置,按照默认的假定能构成严格偏序的"<"(绝对不能是全序,如“≤”,否则若排序范围内存在具有“==”意义上的一对元素,程序会崩溃)对这个范围的元素本身进行排序。其中这一对迭代器必须支持以下两点:

  • 我们可以在有效排序范围内有限次递增First从而到达Last。
  • 必须是随机访问迭代器,也就是五类迭代器中最高级别的迭代器。

其中第一点不难理解,关键是第二点。何谓随机访问迭代器?随机访问迭代器就是可以用来读写、多遍扫描、支持全部迭代器的运算,其中的迭代器运算“+”和“-”尤为重要。对于随机访问迭代器iter,我们可以对其进行其它四类迭代器没有的操作: 

iter += 7;
iter -= 7;
auto anotheriter1 = iter + 7;
auto anotheriter2 = iter - 7;
auto diff = anotheriter1 - iter; //diff为7

这些操作全部能在O(1)的时间内完成,因为她们无非是将迭代器所保存的底层指针进行C语言概念上的算术运算。而STL中的list和forward_list就不能提供这样的操作。因为这两个容器的底层数据结构是链表,对于这种迭代器directionIter,我们没有任何魔法获得从directionIter数第n个元素的迭代器。虽然这可以通过定义在<iterator>头文件中的函数std::advance(directionIter,n)获取之,但这个模板函数是靠一组重载函数来完成工作的。简而言之,就是根据传来迭代器具有的“标签”(tag)进行最优的操作。比如对于随机访问迭代器,调用advance在效果上无异于语句“iter+=n;",而对于前向或双向迭代器,假设n为正数,其效果就类似于

while(n--)
    ++directionIter;

,这个操作显然是O(n)的。迭代器特征(“iterator_traits”)不是我们要讨论的重点,我们就停止深究了。在顶层方面,我们需要理解五类迭代器:

  1. 输入迭代器(input iterator)。可以读取元素,支持相等性运算(==,!=)、前置&后置推进运算(++)、提领运算(*,且只能在赋值运算符右侧)、成员访问运算符(->)。istream_iterator就是这样的迭代器。
  2. 输出迭代器(output iterator)。可以进行推进运算、提领运算。ostream_iterator属于这种迭代器。
  3. 前向迭代器(forward iterator)。可以读写元素,支持前两种迭代器的全部运算,还能多次读写同一个元素。forward_list<T>::iterator就是这样的迭代器类型。
  4. 双向迭代器(bidirectional iterator)。除了支持前向迭代器的操作之外,还接受前置&后置的后撤运算(--)。除了forward_list和1、2提到的两种迭代器之外,其它标准库迭代器都至少属于这一类迭代器。
  5. 随机访问迭代器(random-access iterator)。可以上天入地的迭代器。不仅是一种(is a kind of)双向迭代器,还能在O(1)提供之前提到的操作。

以后,我会使用InputIter、OutputIter、ForwardIter、BidIter、RandomIter作为typename参数来介绍标准库的算法。

另外提一点,sort也可以用于内置数组:

int a[1000]{/* ... */};

//...

sort(begin(a),end(a));
sort(a,a+1000); //与上一个sort等价

在这里,我们就把指针看成迭代器,传递给sort(虽然传递过后她们本身都仍然也只是指针而已)。 

其实对于所有容器类型,你都可以使用泛型的非成员begin和end,这样就统一了参数传递方法,对于泛型编程是有意义的。C++14中补全了非成员cbegin、cend、rbegin、rend、crbegin、crend,更正了原来的短视。 

接下来该谈一谈第二个版本的sort了。这个函数还接受第三个参数BinPred,我们称之为“谓词”(predicate)。sort接受的谓词是二元谓词(binary predicate)。除此之外还有一元谓词(unary predicate)。谓词有什么用?谓词是告诉这个算法工作的具体方法。想象一下,对于一个vector<int>类型的对象vi,如果我们要将之以从大到小排序(而不是第一个sort所提供的默认的从小到大排序),你当然也可以这么写啦:

sort(vi.rbegin(), vi.rend()); //逆向迭代器

前面提到过,sort在工作时只认迭代器,工作完毕后的结果就是从vi.rbegin()到vi.rend()是从小到大的顺序,自然从vi.begin()到vi.end()就是从大到小的顺序了。但是记住,我们需要的是一种不变(stable)的方法,也就是能够自己定义排序方法。如果问题是对于vector<string>类型的对象vs中每个元素的size()从小到大排序呢?这种方法是完全不可行的!

于是,我们需要传递的便是谓词了。对于vi从大到小排序,你有这三种办法传递谓词:

//方法1:C风格比较法

bool cmp_method(const int &lhs, const int &rhs)
{ return lhs > rhs; }
sort(vi.begin(), vi.end(), cmp_method);

//方法2:托管比较法(需要头文件<functional>)

sort(vi.begin(), vi.end(), greater<int>()); 
//PS:当不了解元素类型时,第三个参数可以写成
//greater<typename iterator_traits<decltype(vi.begin())>::value_type>()。。。


//方法3:lambda表达式比较法(需要编译器支持C++11标准)

sort(vi.begin(), vi.end(), [](const int &lhs, const int &rhs){ return lhs > rhs; } );
//PS:如果你的编译器也支持C++14标准,第三个参数的两个int可以写成auto

对于谓词,我们的要求就是对其可以使用调用运算符“()”,并能够将要比较的两个元素分别绑定到这个谓词的第一个和第二个参数上。关于具体的C++语法,请查阅其它资料,在这里不便展开,因为这会跟主题背道而驰。

以后,我会使用UnPred和BinPred来代表一元谓词和二元谓词的typename参数。

接下来,我会按照如上标准和《C++ Primer 5th》的顺序,分门别类地讲述标准库的各个算法,并附上大量实例来帮助理解(这一点特别重要)。

猜你喜欢

转载自blog.csdn.net/Starpast/article/details/81430497