一个迭代器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++找的是左子树的最右边,随随便便插入删除,既有可能影响迭代器的遍历。
在此记下这笔教训,告诫以后的自己:使用迭代器时,万不可修改容器本身。