LeetCode 500. Keyboard Row

c++ 标准库的各种容器(vector,deque,map,set,unordered_map,unordered_set,list)的性能考虑

 

一、vector

vector采用一段连续的内存来存储其元素,向vector添加元素的时候,如果容量不足,vector便会重新malloc一段更大的内存,然后把原内存中的数据memcpy到新的内存中,并free原内存块,然后将新元素加入。vector的元素插入性能跟以下几个要素关系重大:

1. 插入的位置

头部插入:将所有元素后移,然后将新元素插入

中间插入:将插入点后面的元素后移,然后插入新元素

尾部插入:将新元素直接插入尾部

尾部插入无疑是最快的,头部插入最慢,中间插入次之,慢的点在于插入前要移动内存。

删除元素也是同样的道理。

2. 保留空间大小

如果插入元素是,空间不足将导致重新malloc以及一系列的内存拷贝。如果使用者能对容量有预期,那么采用reserve()来预先分配内存,将大大的提高性能。

3. 内存扩展算法的库实现相关

在空间不足导致需要重新malloc的时候,不同的库实现有很大的不同,往往特定平台的实现会结合操作系统的平台特性以及malloc算法,提供相当优秀的内存扩展算法,在百万次vector<int>的插入操作中,不提供reserve()的情况向,性能表现非常优秀。接近于使用了reserve()的情况。

 

因为vector采用连续的内存存储其元素,因此其支持元素的下标法随机访问,且时间复杂度是常量0;

如果是查找元素,vector的find()成员函数对元素进行查找时是采用从头到尾扫描的方式,他时间复杂度是O(n),如果vector要应付查找的性能需求,那么应该采取排序的vector,利用算法库的getlowerbound()进行元素的有序插入,利用binary_search()对元素进行二分查找。这种情况下其查找性能不输于基于红黑树的set和map,更是令list望尘莫及。

 

综述,vector适用于尾部插入,但是此时无法兼顾查找的性能,因为二分查找的vector要求重新排序,或者要求vector在插入时就保持有序,这样就无法做到尾部插入。

但是vector作为动态数组的替代,已经足够优秀。

 

二、deque

deque采用多块内存串起来的方式提供其元素的存错,每一个内存块存储多个元素,每一块内存存储的元素个数相同,这是他不同于vector采用一块内存来存错所有的元素的方式。这样带来的好处是:

首先,头部插入和尾部插入/删除元素的成本是一样,弥补了vector再头部插入元素性能不佳的问题;

其次,对于vector的一个内存块的模式,当有巨大数量的元素,操作系统的大内存分配和赋值时很缓慢的,而且deque的方式就不会带来这个问题。

缺点是:

首先对元素的访问需要经过两个层次,第一次找到元素所在的内存块,第二次找到块中的元素。不过这个时间几乎是可以忽略,除非对性能要求极其苛刻。

其次,对其进行排序,以及排序后的查找会比较慢,想象知名的排序算法,都是针对一段连续内存进行下标访问,而deque是断续的内存块组成。同样排序后的折半查找也无法利用下标直接访问自然性能大打折扣。

 

三、list

list很简单,他就是个双向链表。每一个节点的内存都是独立的。理论上,其优点是任何位置的插入删除元素操作都是非常块的。缺点是不适合用于元素的查找,因为他只能是扫描的方式。根据实际的测试情况来看,我认为list不值得一用,因为节点的频繁新增与删除将导致大量重复的内存分配和释放操作。而实际上,操作系统以及运行库的内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点。

 

四、map/multi_map/set/multi_set

这四个数据结构是采用平衡树(红黑树)实现其元素在内存中的存储。理论上(与list一样是理论上)他们的性能是很高,而且在插入/删除与元素查找上是的平衡点掌握的相当好的,具体的算法复杂度可参考红黑树算法文献,但是同样基于“操作系统以及运行库的内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点”这一法则,他们在理论之外,实际的应用中,总是表现得不是太好。

 

五、散列容器unordered_map/unordered_set/unordered_multi_map/unordered_multi_set

c++11引入的散列容器,散列容器具有不稳定性:他依赖于实际所使用的散列算法。而针对不同的元素数量,不同的散列算法具有相当大的性能差异。

所以,理论上(同样是理论上)他们的算法时间复杂度是近乎“常量”(如果要处理冲突那就不是了)。但时间上对于一般用户使用起来可能会带来风险。除非你对所使用的散列算法和元素数量都有很好的预估。在我一些简单的测试(采用int元素类型,采用std的默认散列函数)中,散列容器的性能是排名垫底的。

 

六、“内存分配与释放频率和策略才是影响stl各大容器的性能最关键的点”

这点非常重要。不同的操作系统,不同的运行库,他的虚拟内存管理算法,以及运行库的malloc和free的内部实现都是有差异的。如何找出最有效率的使用标准库容器的方式,一定要结合这一点进行大量的测试才能得出。

有一点可以确认的是:各个标准库的实现,已经是在遵循c++标准的基础上,在性能和适用性上可以说做到了极致。因此很多人总想自己造重复的轮子是大可不必。理解好各种容器的实现原理,再结合实际应用的需求,选择合适的容器,以及容器的使用方式,才是上上之策。

class Solution {
public:
    vector<string> findWords(vector<string>& words) {
        unordered_set<char> row1 {'q', 'w', 'e', 'r', 't', 'y','u', 'i', 'o', 'p'};
        unordered_set<char> row2 {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'}; 
        unordered_set<char> row3 { 'z', 'x', 'c', 'v', 'b' ,'n', 'm'};
        vector<unordered_set<char>> rows {row1, row2, row3};
        
        
        vector<string> validWords;
        for(int i=0; i<words.size(); ++i){
            int row=0;
            
            for(int k=0; k<3; ++k){
                if(rows[k].count((char)tolower(words[i][0])) > 0) row = k;
            }
            
            validWords.push_back(words[i]);
            for(int j=1; j<words[i].size(); ++j){
                if(rows[row].count((char)tolower(words[i][j])) == 0){
                    validWords.pop_back();
                    break;
                }
            }
            
        }
        return validWords;
    }
};


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值