C++基础语法:STL之算法(二)

前言

      "打牢基础,万事不愁" .C++的基础语法的学习."学以致用,边学边用",编程是实践性很强的技术,在运用中理解,总结.

引入

        STL(标准模板库)的学习.以<C++ Prime Plus> 6th Edition(以下称"本书")内容理解 .算法是STL中的一个重要内容.C++基础语法:STL之算法(一)-CSDN博客的续篇

函数和容器方法 

        本书P716:有时可以选择使用STL方法STL函数。通常方法是更好的选择。 首先,它更适合于特定的容器;其次,作为成员函数,它可以使用模板类的内存管理工具,从而在需要时调整容器的长度。

        ----解读:

        容器方法优于算法.更重点的是概念的变化:用"方法"表示容器类对象方法,STL函数表示"算法".此前一律称函数---非对象函数,对象函数,静态函数等.这里把"方法"分出来了,和类有关的称"方法",类似Java.STL函数(算法)强调的是"通用"

         例如,假设有一个由数字组成的链表,并要删除链表中某个特定值 (例如4)的所有实例。如果la是一个list对象,则可以使用链表的 remove( )方法:         

//本书代码
la.remove(4);  //remove all 4s from the list

         调用该方法后,链表中所有值为4的元素都将被删除,同时链表的长度将被自动调整

        还有一个名为remove( )的STL算法(见附录G),它不是由对象调用,而是接受区间参数。因此,如果lb是一个list对象,则调用该函数的代码如下:

//本书代码
remove(lb.begin(),lb.end(),4);

         然而,由于该remove( )函数不是成员,因此不能调整链表的长度。它将没被删除的元素放在链表的开始位置,并返回一个指向新的超尾值的迭代器。这样,便可以用该迭代器来修改容器的长度。例如,可以使用链表的erase( )方法来删除一个区间,该区间描述了链表中不再需要的部分。程序清单16.18演示了这是如何进行的。

        ----解读:

        演示了容器方法比算法好的原因,容器方法可以自动调整链表长度.容器方法比算法更"纯粹"

        两种写法很好区分:容器方法是由对象调用:对象.方法名(参数列表)

                                      STL函数调用:函数名(迭代器对象参数,其他参数)

本书代码16.18listrmv.cpp,注释是自己加的

#include <iostream>
#include <list>
#include <algorithm>

void Show(int);
const int LIM = 10;
int main()
{
    using namespace std;
    int ar[LIM] = { 4, 5, 4, 2, 2, 3, 4, 8, 1, 4 };
    list<int> la(ar, ar + LIM);                         //生成list对象
    list<int> lb(la);                                   //复制对象

    cout << "Original list contents:\n\t";
    for_each(la.begin(), la.end(), Show);
    cout << endl;
    la.remove(4);                                       //容器删除
    cout << "After using the remove() method:\n";
    cout << "la:\t";
    for_each(la.begin(), la.end(), Show);               //打印
    cout << endl;
    list<int>::iterator last;                           //声明迭代器,接收算法删除后的迭代器
    last = remove(lb.begin(), lb.end(), 4);             //和上一句可以合在一起,算法删除
    cout << "After using the remove() function:\n";
    cout << "lb:\t";
    for_each(lb.begin(), lb.end(), Show);               //打印
    cout << endl;
    lb.erase(last, lb.end());                           //删除迭代器区间的结点     
    cout << "After using the erase() method:\n";
    cout << "lb:\t";
    for_each(lb.begin(), lb.end(), Show);
    cout << endl;
    // cin.get();    
    return 0;
}

void Show(int v)
{
    std::cout << v << ' ';
}

        从中可知,remove( )方法将链表la从10个元素减少到6个元素。但对链表lb应用remove( )后,它仍然包含10个元素。最后4个元素可任意处理,因为其中每个元素要么为4,要么与已经移到链表开头的值相同。

        尽管方法通常更适合,但非方法函数更通用。正如您看到的,可以将它们用于数组、string对象、STL容器,还可以用它们来处理混合的容器类型,例如,将矢量容器中的数据存储到链表或集合中。

输出结果:

Original list contents:    
        4 5 4 2 2 3 4 8 1 4            //原始数据
After using the remove() method:
la:     5 2 2 3 8 1                    //容器方法remove删除后的结果
After using the remove() function:
lb:     5 2 2 3 8 1 4 8 1 4            //算法删除remove后的结果
After using the erase() method:
lb:     5 2 2 3 8 1                    //容器方法erase删除结点后的结果

 ---解读:

        容器方法更适合,函数更通用.

        容器方法,一步达到效果,删除元素和结点.算法也可以,但是要达到同样的效果,需要多一步调用erase.因此操作元素首选容器方法,次选算法.在容器之间传递数据,多半只能靠算法(STL函数)--假设

使用STL

        本书:STL是一个库,其组成部分被设计成协同工作。STL组件是工具, 但也是创建其他工具的基本部件。我们用一个例子说明。假设要编写一个程序,让用户输入单词。希望最后得到一个按输入顺序排列的单词列表、一个按字母顺序排列的单词列表(忽略大小写),并记录每个单词被输入的次数。出于简化的目的,假设输入中不包含数字和标点符号。

        ----解读:原书附带的示例,学习别人的编程思路

        本书:输入和保存单词列表很简单。可以按程序清单16.8和程序清单16.9 那样创建一个vector对象,并用push_back( )将输入的单词添加到矢量中:

//本书代码
vector<string> words;
string input;
while(cin>>input&&input !="quit")
    words.push_back(input);

        ----解读:常规写法,建立个string集合,数据结构使用vector,添加输入的string对象

                        输入结束条件: input!="quit".当字符串输入quit即表示输入结束.

        本书:如何得到按字母顺序排列的单词列表呢?可以使用sort( ),然后使用unique( ),但这种方法将覆盖原始数据,因为sort( )是就地算法。有一 种更简单的方法,可以避免这种问题:创建一个set对象,然后将矢量中的单词复制(使用插入迭代器)到集合中。集合自动对其内容进行排序,因此无需调用sort( );集合只允许同一个键出现一次,因此无需调用unique( )。这里要求忽略大小写,处理这种情况的方法之一是使用transform( )而不是copy( ),将矢量中的数据复制到集合中。使用一个转换函数将字符串转换成小写形式。

        ----解读:

        1>首先提出了解决办法1:sort()+unique().因为sort()是就地算法,会覆盖原始数据,所以没用上.要想不覆盖原始数据,可以先做个备份,这样写:

vector<string> words_bak(words);            //复制一个string对象
//以下代码用了算法,可能有出入
sort(words_bak.begin(),words_bak.end());    //排序
unique(words_bak);                          //假设有个叫unique的算法去除多余元素,没有自己写

        但是他没有解决忽略大小写的问题,如果要这样写,应该先调用下面transform()的函数,因为书上不支持这种写法,所以不展开了,理解意思就行.

        2>因为需求的集合是排序而且要求不重复,set符合这个要求,即创建了一个set对象.

             copy()和transform():copy()复制一个新的集合,不能改变数据;transform()可以在复制时候更改数据,这里要求把大小写转换,所以选择了transform()

//本书代码
set<string> wordset;
transform(words.begin(),words.end(),
        insert_iterator<set<string> >(wordset,wordset.begin()),ToLower);

        解读函数transform():4个参数的transform(),表示某区间(前两个参数表示,类型为迭代器)里每个元素被函数符(第4个参数)处理,结果发给迭代器(第3个参数).

        函数规定了第4个参数的函数指针的输入值和返回值类型,和迭代器解引用后时类型一致(可以是引用类型,他们不构成函数重载).内在逻辑不管.transform()还规定了t值是遍历words后的值---也不算特别,因为函数指针做参数的函数里,其使用参数一般也在形参列表内.参考"函数指针"

//伪代码:T表示迭代器解引用后的类型
//函数的输入值类型和输出值类型已确定
T fun(T t);        //t值已确定,从*words.begin()开始往后取值,直到word.end()前一个值
T fun(T& t);       //形式2
T& fun(T& t);      //形式3

//几种形式取其一,因为两种形参类型都能接收t,返回值不管是T或者T&,在送到迭代器的时候都是T类型

        函数指针的优点:不管函数怎样编写,输入值类型和输出值类型已确定.可以起"逻辑占位"的作用,在不确定他代表的逻辑的情况下,继续程序的编写

        transform()中使用的函数指针ToLower正是使用了从顶向下的编程思路,暂时放下实现细节.对程序员而言,编程时可以用这种思路把整体和局部分开(难道是架构师与工程师的区别?)

================================内容分割线=================================

编程提供抽象的表达和抽象的实现,罗列一些程序中的抽象和具体

        变量是数据的抽象,值是具体的数据

        函数指针是逻辑的抽象.函数是具体的逻辑

        接口是需求的抽象,实现类是具体需求的满足.

抽象和具体的共通点:抽象=具体 这个等式恒成立(实现类转换成接口的引用).

当抽象被具体描述后,该部分代码完成.

================================内容分割线================================ 

         本书:ToLower( )函数很容易编写,只需使用transform( )将tolower( )函数应用于字符串中的各个元素,并将字符串用作源和目标。记住,string对象也可以使用STL函数。将字符串按引用传递和返回意味着算法不必复制字符串,而可以直接操作原始字符串。下面是函数ToLower( )的代码:

//本书代码
string& ToLower(string& st){
    transform(st.begin(),st.end(),st.begin(),tolower);
    return st;
}

        ----解读:

        string对象也可以使用STL函数.string对象实现了begin(),end(),operator*(),operator++()这些函数,所以string对象可以使用STL接口函数 .---string类型事实上属于"字符容器"类型.

        ToLower函数的内部逻辑:传入一个string对象,遍历每个字符,.调用函数tolower将其每个字符大写转换成小写,因为string类是字符容器,所以遍历和修改容器元素,再次使用STL函数transform()

        本书:一个可能出现的问题是:tolower( )函数被定义为int tolower(int),而一些编译器希望函数与元素类型(即char)匹配。一 种解决方法是,使用toLower代替tolower,并提供下面的定义:

//本书代码
char toLower(char ch){return tolower(ch);}

          ----解读:有的编译器tolower函数使用int类型做形参和返回值.所以这样做是为了保险.   

        这部分有一个小问题:ToLower函数修改了原值,所以不能重现最初输入(解决办法做备份).但在这个版本的要求中,没有重现的需求,而是改了原值再继续操作.   

         本书:要获得每个单词在输入中出现的次数,可以使用count( )函数。它将一个区间和一个值作为参数,并返回这个值在区间中出现的次数。可以使用vector对象来提供区间,并使用set对象来提供要计算其出现次数的单词列表。即对于集合中的每个词,都计算它在矢量中出现的次数。要 将单词与其出现的次数关联起来,可将单词和计数作为pair对象存储在map对象中。单词将作为键(只出现一次),计数作为值。这可以通过一个循环来完成:

//本书代码
map<string,int> wordmap;
set<string>::iterator si;
for(si=wordset.begin();si!=wordset.end();si++)
    wordmap.insert(pair<string,int>(*si,count(words.begin(),words.end(),*si)));

          ----解读:新的STL函数count(),提供区间和值作为参数,求得值在区间出现的次数.

            然后创建一个map对象,键类型string,值类型int,调用insert方法,存储string对象出现的次数.

        本书:map类有一个有趣的特征:可以用数组表示法(将键用作索引)来访问存储的值。例如,wordmap[“the”]表示与键“the”相关联的值,这里是字符串“the”出现的次数。因为wordset容器保存了wordmap使用的全部键,所以可以用下面的代码来存储结果,这是一种更具吸引力的方法

//本书代码
for(si=wordset.begin();si!=wordset.end();si++)
    wordmap[*si]=count(words.begin(),words.end(),*si);

         因为si指向wordset容器中的一个字符串,所以*si是一个字符串,可以用作wordmap的键。上述代码将键和值都放到wordmap映象中。

          ----解读:强烈推荐用这种方法来插入map类型元素:对象名[键]=值; 替代insert方法插入元素,写法更优雅,易懂.

         同样,也可以使用数组表示法来报告结果:

//本书代码
for(si=wordset.begin();si!=wordset.end();si++)
    cout<<*si<<":"<<wordmap[*si]<<endl;

        如果键无效,则对应的值将为0。 

        这里的寓意在于,使用STL时应尽可能减少要编写的代码。STL通用、灵活的设计将节省大量工作。另外,STL设计者就是非常关心效率的算法人员,算法是经过仔细选择的,并且是内联的。

          ----解读:尽量使用STL函数来解决问题.

小结

        STL函数(算法)的优点有:

        1>为访问和修改容器元素提供了统一的接口(一个transform()函数就能解决大部分问题),迭代器区间提取数据,函数符提供数据处理方法.

        2>在不同容器间传递数据很方便.

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jllws1

你的鼓励是我创作的动力,谢谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值