好像是去年CVTE在招聘的时候出了这样的一个笔试题:
题目的大意就是:本公司现在要给公司员工发波福利,在员工工作时间会提供大量的水果供员工补充营养。由于水果种类比较多,但是却又不知道哪种水果比较受欢迎,然后公司就让每个员工报告了自己最爱吃的三种水果,并且告知已经将所有员工喜欢吃的水果存储于一个数组中。然后让我们统计出所有水果出现的次数,并且求出排列前三的这三种水果。
拿到这个题,我们应该怎么做呢?
首先,统计所有水果出现的次数,很显而易见的我们最先想到的当然是,将数组遍历一遍,就能得到每种水果的出现次数。然后再对求出水果次数的数组进行排序,这样很容易就可以拿到最受欢迎三种水果。
可是,大家有没有想过,如果该公司有100w的员工,如此庞大的数量,我们对数字遍历并且排序是多么大的开销。而且其时间复杂度和空间复杂度都很高,如果按照这种思路我们肯定是会被拒之门外。
那么,这道题应该怎么做呢?
当然,首选的应该是红黑树了,并且需要的是K/V结构的红黑树,K来存储水果的名称,V来存储这个水果出现的次数。
可是,笔试时间那么短,红黑树相对来说比较复杂的结构,我们如何在短时间内拿到它呢?
不用担心,在c++STL库中提供了两个关联式容器:set和map.
那么我先大概的介绍一下这个set和map.
set和map这两个容器的底层都是用红黑树来实现的,并且他们都具有防冗余的特性(被插入的数据如果在容器中已经出现,那么插入失败),他两的唯一区别就是set是一个K结构,而map是一个K/V结构。
那么他们是怎么使用呢?
在这里,我主要讲解一下map,因为上边的这个题需要用到它。而set和map的用法基本上是一模一样的。
首先,来看一下这个map的原型:
template < class Key, // map::key_type class T, // map::mapped_type class Compare = less<Key>, // map::key_compare class Alloc = allocator<pair<const Key,T> > // map::allocator_type > class map;那么这个map的模板参数都是什么意思呢?
Key:这个就是我刚才说的K/V结构中的K
T:这个就是我刚才说的K/V结构中的V
Compare:这个是一个接受仿函数类型的参数,可以控制map是一个升序的还是降续的(不传这个参数时,默认是升序)
Alloc:这个是空间配置器,我们现在还用不到,先知道它是什么就好了。
那么,这个map都有哪些接口呢?(这里主要讲几个重要的接口)
1.insert
pair<iterator,bool> insert (const value_type& val);首先其返回值是一个pair,什么是pair:一个结构体,第一个参数是一个迭代器,第二个参数数一个bool值。意思就是:如果插入成功,就返回一个被插入元素的迭代器,并且第二个参数为true;反之返回容器中已经存在的这个和被插入元素相同的元素的迭代器和false.
其参数是value_type typedef pair<const Key,T> value_type;
2.Operator[]
这个运算符的重载非常巧妙,等会再来讲解。
那么现在,我们回归这道题:
首先,求解所有水果出现的次数,在这里有三种解法:
解法一:
定义一个map,然后将数组中所有的元素插入到map中,在插入前先使用find()来查找存在不存在,如果不存在则插入,如果存在,则利用find返回值来对找到的元素的V进行+1操作。(其中strs是水果数组,同下)
map<string, int> fruitCount;//创建map对象 for (int i = 0; i < sizeof(strs) / sizeof(strs[0]); ++i) { map<string, int>::iterator it = fruitCount.find(strs[i]);//创建map迭代器 if (it != fruitCount.end())//先查找看该字符数组内是否存在该字符串 { (*it).second++;//给该类对象的计数+1 //或 //it->second++; } else { fruitCount.insert(pair<string, int>(strs[i], 1)); } }解法二:
上边的解法一就是一种传统的思维方式,但是效率稍微显慢,英文如果map中没有要插入的元素,则需要遍历两次map。
那么,如何做到只遍历一次就能完成这个任务呢?
还记得我之前讲的那个insert的返回值pair吗?既然不管是否插入成功,它都能返回我们需要的这个元素的迭代器。那么,我们可以先插入,然后对其返回值进行保存,如果该返回值得第二个参数是true,表示插入成功,不进行其他操作,如果为flase,表示插入失败,那么其返回的第一个参数将会带回已经存在的这个被插入元素的迭代器,当然轻而易举就可以通过迭代器拿到这个元素的第二个参数V。
void CalculateFruitCount(map<string,int>& m, string s[], size_t size) { for (size_t i = 0; i < size; i++) { //m[s[i]]++; //map中有operator[]的重载,其内容等同于下边代码 pair<map<string, int>::iterator, bool> ret; ret = m.insert(make_pair(s[i], 1)); if (ret.second == false) ret.first->second++; } }解法三:
解法二中被注释掉的那一句。那么现在我就讲一下这个operator[].map中对[]这个运算符进行了重载,其格式为:
T& operator[] (const Key& k);
而其代码的实现就是解法二中的思想。现在大家对这个operator了解了吧。
好!到这里,这个问题已经解决了一半。那么我们继续往下走。
现在水果出现的次数已经统计出来,那么如何求出排列前3的水果呢?乃至前N呢?
这里有4中解法:
1.讲统计好的数据全部放入一个vector中,并且利用排序算法sort进行排序。而其默认为升序,最大的则位于数组后边,但是我们并不知道vector有多大。所以,我们采用降续,这样最大的永远在vector的前列.
void GetBeginOfThreeFruits(map<string, int>& m, vector<map<string, int>::iterator>& v) //按照水果出现的次数降续存储于v中{ map<string, int>::iterator it = m.begin(); while (it != m.end()) { v.push_back(it); it++; } struct Compare //仿函数(降续) { bool operator()(map<string, int>::iterator l, map<string, int>::iterator r) { return l->second > r->second; } }; sort(v.begin(), v.end(),Compare());}2.同1的思想,只不过这次我们不是放在vector中,而是放在一个set中,这样就可以省去我们自己给它排序的麻烦。(因为set底层是一个红黑树,而红黑树就是一个近似平衡的二叉排序树)代码比较简单,这里就不实现了。
3.大家还知道堆吗?既然我们需要找出最大的3个,那么我们只需要建立一个3个大小的堆,但是,我们应该建大堆还是小堆呢?当然是小堆,为什么呢?
因为,小堆的话大的数据就会往下沉,而对顶永远是最小的,当来了一个数据时,我们只需要和对顶的元素做一个判断,如果比对顶元素小,那么不用插入到堆,如果比对顶元素大,那么将对顶元素pop出去,然后将这个元素插入。当刚才求好水果出现次数的数组全部遍历一遍后,最后堆里面剩下的肯定就是出现次数最多的三个水果。
void GetBeginOfNFruits(map<string, int>& m, size_t n, vector<map<string,int>::iterator>& v){ map<string, int> ::iterator it = m.begin(); for (size_t i = 0; i < n; ++i) { v.push_back(it); it++; } struct Compare //堆算法默认是大堆,此处需要仿函数将其改为小堆 { bool operator()(map<string, int>::iterator l, map<string, int>::iterator r) { return l->second > r->second; //小堆 } }; make_heap(v.begin(), v.end(), Compare()); while (it != m.end()) { if (it->second > v.front()->second) { pop_heap(v.begin(), v.end(), Compare()); v.pop_back(); v.push_back(it); push_heap(v.begin(), v.end(), Compare()); } it++; }}4.利用优先级队列,优先级队列的底层其实就是一个堆,这里代码不予实现了,思想同3.
到这里,整个题目就解决完了。代码写出来其实不难,难的就是我们需要对STL库函数有一定的了解,已经要会使用。
新闻热点
疑难解答