C++ STL 里为什么不维护一个 size 成员变量?
回答:
为什么 GCC 里要把 list::size() 的复杂度搞成 O(N)?
一通搜索后终于看到有这样的讨论:关于 list::splice() 函数。
list 是链表结构,它的优势就在于可以 O(1) 的时间复杂度任意插入删除甚至拼接 list 片段(删除时可能不是,因为要释放内存),list::splice() 是一个很强大的功能,它可在任意位置拼接两个 list,这正是 list 的优势。如果我们在类内部以一个变量储存 list 的长度,那么 splice() 之后新 list 的长度该如何确定?这是一个很严峻的问题,如果要在拼接操作时计算拼接部分的长度,那么将把 O(1) 的时间变成 O(N),这么一来 list 相对 vector 的优势就消失殆尽。
面对这个问题,GCC 和 VC 的 STL 库作者们做了不同的选择。GCC 选择舍弃在 list 内部保存元素数量,而在 size() 时直接从头数到尾,这便出现了开头看到的 O(N) 时间才算出 size();相反,VC 中有了变量 _Mysize ,无论在 insert() erase() splice() 或是 push() pop() 时都需要对其做相应修改。在上面的两个试验中已经看出同样是 10000 个 push_back() 操作,VC 花的时间比较长,不过也仅仅是一个 inc 指令,差别很小就是了。上面几种会改变 list 内容的操作中,大部分对元素数量的影响只是 +1 或 -1,只有 splice() 需要计算拼接部分元素个数,这个差别就大了
转自:http://dantvt.is-programmer.com/posts/8313.html
很奇怪的,或者说是一个不应成为问题的问题...
std::list 的 size() 方法时间复杂度是多少?第一感觉应该是 O(1) 没错吧,多一个变量用于储存链表长度应该是很轻易的事情。于是有了下面这段代码:
#include<list>
#include<ctime>
using namespace std;
int main(){
time_t start, finish;
int num = 0;
list<int> coll;
start = clock();
for(int i=0;i<10000;++i){
coll.push_back(i);
num += coll.size();
}
finish = clock();
cout<<finish - start<<" num:"<<num<<endl;
coll.clear();
start = clock();
for(int i=0;i<10000;++i){
coll.push_back(i);
}
finish = clock();
cout<<finish - start<<endl;
return 0;
}
对两个循环分别计时比较。前一个循环只比后一个多了一句 num += coll.size(); 为了使编译器确实生成 list::size() 的代码。
在 MinGW 5.1.4 中 (GCC 3.4.5) 编译结果运行如下:
10
可以看到,前一个循环居然比后一个多花了几乎 45 倍的时间...当我把循环次数从 10000 加到 100000 时程序半天没出结果...
由此有理由猜测 std::list 的 size() 方法难道是 O(N) 的?果然,在头文件中发现了这一段:
size() const
{ return std::distance(begin(), end()); }
直接调用 <algorithm> 算法库函数 distance() 计算元素个数……怪不得这么慢。然后又用 VS2008 (VC9.0)编译,结果如下:
60
奇怪的是前一个循环居然比后一个还快...不过至少知道 VS2008 (VC9.0)里的 size() 应该是 O(1) 的。同样查看了一下代码,如下:
{ // return length of sequence
return (_Mysize);
}
_Mysize 是一个 size_type 类型的变量。疑问解决。不过又有了新问题:
--------------- 咱 -- 是 -- 分 -- 隔 -- 线 ------------------
为什么 GCC 里要把 list::size() 的复杂度搞成 O(N)?
一通搜索后终于看到有这样的讨论:关于 list::splice() 函数。
list 是链表结构,它的优势就在于可以 O(1) 的时间复杂度任意插入删除甚至拼接 list 片段(删除时可能不是,因为要释放内存),list::splice() 是一个很强大的功能,它可在任意位置拼接两个 list,这正是 list 的优势。如果我们在类内部以一个变量储存 list 的长度,那么 splice() 之后新 list 的长度该如何确定?这是一个很严峻的问题,如果要在拼接操作时计算拼接部分的长度,那么将把 O(1) 的时间变成 O(N),这么一来 list 相对 vector 的优势就消失殆尽。
面对这个问题,GCC 和 VC 的 STL 库作者们做了不同的选择。GCC 选择舍弃在 list 内部保存元素数量,而在 size() 时直接从头数到尾,这便出现了开头看到的 O(N) 时间才算出 size();相反,VC 中有了变量 _Mysize ,无论在 insert() erase() splice() 或是 push() pop() 时都需要对其做相应修改。在上面的两个试验中已经看出同样是 10000 个 push_back() 操作,VC 花的时间比较长,不过也仅仅是一个 inc 指令,差别很小就是了。上面几种会改变 list 内容的操作中,大部分对元素数量的影响只是 +1 或 -1,只有 splice() 需要计算拼接部分元素个数,这个差别就大了,咱还是继续用实验证明吧:
#include<list>
#include<ctime>
using namespace std;
int main(){
time_t start,finish;
list<int> col;
col.push_back(1);
col.push_back(10000);
list<int> col2;
start = clock();
for(int i=2;i<10000;++i)
col2.push_back(i);
finish = clock();
cout<<finish - start<<endl;
int num = 0;
start = clock();
for(int i=0;i<10000;++i){
col.splice(++col.begin(),col2,++col2.begin(),--col2.end());
num += *(++col.begin());
col2.splice(++col2.begin(),col,++col.begin(),--col.end());
num += *(++col2.begin());
}
finish = clock();
cout<<finish - start<<" num:"<<num<<endl;
return 0;
}
首先是 MinGW (GCC 3.4.5) 的结果:
0 num:60000
可以看到 10000 次 push 是 10,相对的 20000 次 splice() 几乎没花时间 = =
然后是 VS2008 (VC9.0):
2714 num:60000
差别非常明显,花了2秒多才完成。当我把循环次数改成 100000 后 GCC 仍是眨眼间的事,VC 却长时间运行无结果……
怎么说呢,GCC 显然是追求效率至上,尽量体现出 list 的优势所在,不过我觉得这么一来倒不如干脆不提供 list 的 size() 方法,有需求的程序员可以自己维护一个变量记录长度,以免误认为 size() 是 O(1) 的而犯下严重错误。相对的 VC 强调功能性和整体效率,可能在实际中需要对链表一段内容做 splice() 操作的机会远远小于求 size() 的操作,所以舍弃前者而保留后者,不过要维护 _Mysize 其他相关函数中也增加了开销。一个见仁见智的问题,我觉得还是 GCC 的选择比较好,list 的优势应该保留,但能在 size() 函数处给个 warning 什么的就好了。
我想还有一个选择是这样:在 list 内部用一个 bool 变量指示当前内部 size 值是有效还是无效。在通常操作时 bool 保持 true,这样在 size() 时直接返回原值即可;在 splice() 后将此 bool 值置为 false 并不计算长度,直到最后又有需要 size() 时发现 bool 是 false 则从头再来一遍 distance() 并再将 bool 置为 true。暂时只想出这么一个算是折中的方法,基本上都能保持两边 O(1) 的效率,但相应其他各关于元素数量的函数内部都要多一个判断当前 size 值是有效还是无效并选择是否改变其值。反正总是不能非常完美
嘛...本来只是发现 size() 的效率问题,没想到却扯出这么一桩事出来...也算长知识了吧
C++ STL 里为什么不维护一个 size 成员变量?的更多相关文章
- 编写Java程序,创建Dota游戏中的兵营类,兵营类有一个类成员变量count、一个实例变量name和另一个实例变量selfCount。
返回本章节 返回作业目录 需求说明: 创建Dota游戏中的兵营类 兵营类有一个类成员变量count.一个实例变量name和另一个实例变量selfCount. count表示的是兵营已经创建士兵的总数: ...
- 编写Java程序,创建一个 Person 类,该类中有一个类成员变量 country、一个实例变量 name 和另一个实例变量 age。
返回本章节 返回作业目录 需求说明: 创建一个 Person 类,该类中有一个类成员变量 country.一个实例变量 name 和另一个实例变量 age. country 表示地区,name 表示姓 ...
- bzoj 1012 维护一个单调数列
Description 现在请求你维护一个数列,要求提供以下两种操作: 1. 查询操作.语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度. 2. ...
- UVA 1619/POJ2796 滑窗算法/维护一个单调栈
Feel Good Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 12409 Accepted: 3484 Case T ...
- 如何在一个div标签里显示出另一个网页? <iframe src=" http://www.baidu.com " width="800px" height="200px" scrolling="no" frameborder="0"> </iframe>
如何在一个div标签里显示出另一个网页? 用在div里用iframe,就像下面的代码 <iframe src=" http://www.baidu.com " width=& ...
- HDU 5945 维护一个单调队列 dp
Fxx and game Time Limit: 3000/1500 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others)Tot ...
- Javascript里,想把一个整数转换成字符串,字符串长度为2
Javascript里,想把一个整数转换成字符串,字符串长度为2. 想把一个整数转换成字符串,字符串长度为2,怎么弄?比如 1 => "01"11 => " ...
- 在C++里一个类成员函数多少行代码才是最好呢?
这个问题,很多同事以及学生都问我这个问题.其实这是一个比较实际的问题,因为设计一个类成员函数的好与坏,决定了一个类代码的质量. 为了回答这个问题,昨晚又重新看看斯坦福大学的编程视频,可以用下面这个截图 ...
- ORACLE里怎么能判断一个日期类型的字段是否为空,解决方法:is null
ORACLE里怎么能判断一个日期类型的字段是否为空,解决方法:is null,解决方法:判断什么null都可以用is null.
随机推荐
- IDEA下 Scala 安装 使用
Scala 安装 使用 1.安装Scala插件 Setting--->Plugins--->搜索Scala--->install--->安装完成 或者线上安装不成功/网速太慢, ...
- 如何建立一个足够安全的SSH连接?
1 概述 使用SSH连接服务器是一件很平常的事,但是,连接是否足够安全是一个令人担忧的问题.本文从如下几个方面介绍了如何建立一个足够安全的SSH连接: 端口 协议 用户 密码 密钥对 ssh-agen ...
- Fiddler使用 断点 模拟返回 AutoResponder Mock 模拟数据 相关学习记录
断点 测试中有时需要改变发出去的请求信息,需要用到打断点的方法.断点包含两种方式: before response:在request请求的时候,未到达服务器之前,一般用来修改请求参数 after re ...
- 19. slot插槽传递模板
插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示.以及怎样显示由父组件来决定. 插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制.但 ...
- 介绍一款能取代 Scrapy 的 Python 爬虫框架 - feapder
1. 前言 大家好,我是安果! 众所周知,Python 最流行的爬虫框架是 Scrapy,它主要用于爬取网站结构性数据 今天推荐一款更加简单.轻量级,且功能强大的爬虫框架:feapder 项目地址: ...
- 【多线程】Java线程池七个参数详解
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param coreP ...
- HTML5中meta属性的使用详解
meta属性在HTML中占据了很重要的位置.如:针对搜索引擎的SEO,文档的字符编码,设置刷新缓存等.虽然一些网页可能没有使用meta,但是作为正规军,我们还是有必要了解一些meta的属性,并且能够熟 ...
- Python 使用xlsxwriter绘制Excel表格
最近在统计资产,正好看到了xlsxwriter这个表格生成模块,借此机会,熟悉一下,写点有趣的小案例,一开始想使用C++ QT图形化开发一套自动化运维平台,但后来发现不仅消耗时间而且需要解决QT Qs ...
- hdu4847 水题
题意: 你看了上面很长很长的一片英语课文之后,发现根本不用看,直接看输入输出就行了,就是给你一坨字符串,然后问你里面有几个doge(不区分大小写). 思路: 没啥说的,直接写吧 ...
- PAT 乙级 -- 1006 -- 换个格式输出整数
题目简述 让我们用字母B来表示"百".字母S表示"十",用"12-n"来表示个位数字n(<10),换个格式来输出任一个不超过3位的正整 ...