一个迭代器auto引发的血案 - 当我在迭代时我在做些什么

    写Leetcode 4Sum problem时,用了一个hashtable(PS: 在新的C++11中,这个类由一个名叫unordered_map的类所替代,据测试,unordered_map在存储上略高于原hashtable,但效率也高于hashtable)。在C++11中有一种很简便的迭代器写法:

for (auto it:obj)
{
    ...
    it.XXX      
    ...
}

  是的,不再只有MATLAB、python,C#等语言有这样简短的循环可写了!C++也有了!

  但是,auto曾经引发了一段“血案”。

  在4Sum这一简单的问题中,我先将一系列元素插入到map,然后遍历map,对每个ley为k的元素,在map中寻找key为target-k的元素:

for (auto item:m)
{
    int tmp = target - m.first;
    vector* v1 = &m.second;
    vector* v2 = &m[tmp];
    for (auto vi:*v1)
    {
        for (auto vi:*v1)
        {
            ......
        }
    }
}

    因为确信m[tmp]是不会报错也不会抛异常的,因此不管tmp在不在map里,直接m[tmp]。结果wrong answer。分析之后发现,在遍历m的过程中,item已经发生了变化。cplusplus中明确指出m[key]和m.at(key)的差别是:

    对于map<class K, class T> m,当key不存在时,m[key]将调用T的默认构造函数,强制将序对<key, T()>插入,并返回T()的引用;而后者将抛出exception。

    因此在迭代器遍历map的过程中,map已经发生了变化,这将导致程序输出不可预期的结果,是很危险的事情。那么上面的程序将有两种修改方法了:

   1,调用m[]前先做判断:

if(m.find(tmp) != m.end()) 
{
    ...
}
or

assert();

 

   2,既然at抛异常,那就用Java的习惯写法好了,try走起:

 

try{
    ...
    v2 = m.at(tmp);
    ...
}
catch(...){}

 

   为了偷懒直接catch(...),唉,写Java的时候留下的坏习惯。前一个要进哈希表查询两次,第一次find,第二次at;后一种实际上为了小小的try花费了昂贵的代价(More Effective C++指出,异常处理使得程序膨胀5%-10%)。

   总而言之各有利弊。接下来submit下面的代码,程序性能勉勉强强,但是得到了可爱的accepted。

 

class Solution {
public:
template <class T>
void distinct(vector<T> & v)
{
    int i = 0;
    map<T, bool> mp;
    for(auto e:v)
        mp[e] = 1;
    for(auto it:mp)
        v[i++] = it.first;
    v.resize(i);
}

vector<vector<int> > fourSum(vector<int> &num, int target)
{
    vector<vector<int>> ret;
    int const n = num.size(), m = 4;
    if(n<m) return ret;
    int i,j;
    unordered_map<int, vector<pair<int, int>>> mp;
    for(i=0; i<n; i++)
        for(j=i+1; j<n; j++)
            mp[num[i] + num[j]].push_back(make_pair(i, j));

    for(auto item:mp)
    {
        int res = target - item.first;
        if(mp.find(res)==mp.end())
            continue;
        vector<pair<int, int>> & v1 = item.second;
        vector<pair<int, int>> & v2 = mp[res];
        for(i=0; i<v1.size(); i++)
        {
            for(j=0; j<v2.size(); j++)
            {
                if(v1[i].first-v2[j].first &&  v1[i].second - v2[j].second
                    && v1[i].first-v2[j].second &&  v1[i].second - v2[j].first)
                {
                    vector<int> v(m);
                    v[1] = num[v1[i].second];
                    v[0] = num[v1[i].first];
                    v[2] = num[v2[j].first];
                    v[3] = num[v2[j].second];
                    sort(v.begin(), v.end());
                    ret.push_back(v);
                }
            }
        }
    }
    distinct(ret);
    return ret;
}

};

 

    迭代器模式的定义是:设计一种方法,使得依据某种顺序遍历容器时,容器内部的具体结构是不可见的。C++ STL中,迭代器的本质就是指针,但是这个指针在不同场合是有区别的:

    对vector::iterator而言,迭代器是指向vector某元素的指针,和T a*差不多;对于list和slist而言,迭代器指向链表节点;对map和set等基于二叉搜索树的容器而言,迭代器可以看成是指向treenode的指针。因此STL迭代器大致可分为两种,一种是连续地址空间的,一种是非连续地址空间的。然而不管是哪一种,在遍历过程中修改容器的内容绝对是大忌中的大忌!

   对vector而言,插入删除操作往往包含着malloc、realloc操作,一旦空间重塑了,原来的迭代器也将失效,如再理所应当地it++,你的it就不知道跑哪里去了。

   对list而言,虽然it++仅仅意味着p=p->next,但如果list的内容改变,此next还是不是彼next可就说不好了。

   对于map和set,it的遍历是遵循二叉搜索树中序遍历顺序的(即有序遍历),it++找的是右子树的最左边,reverse_it++找的是左子树的最右边,随随便便插入删除,既有可能影响迭代器的遍历。 

 

 

    在此记下这笔教训,告诫以后的自己:使用迭代器时,万不可修改容器本身。

 

posted @ 2014-11-15 12:38  chng  阅读(1446)  评论(0编辑  收藏  举报
BackToTop