当然标题里这个O(1)可以换成任何复杂度。

话说写程序的时候我们会用到各种数据结构,但十有八九不会由我们自己从头写起,都会直接拿来用。于是很多人就会记住,譬如HashMapDictionary的存取是O(1)的操作,二分查找什么的则是O(log(N))。不过,我们在实践中直接把这些类拿来用的时候,最好也留个心眼,知道这些类内部到底做了些什么,为什么它们能够达到O(1)之类的时间复杂度。

例如,我们都知道List<T>Add是O(1)的操作,但之所以它是O(1),是因为它的“扩容”操作被均摊了(amortized),但每次扩容时其实还是需要复制所有元素,次数越少越好,于是实践中在可行的情况下我们往往应该给它指定一个初始容量——用StringBuilder的时候也是一样。

我这里还可以再举一个更为复杂的例子,例如HashSetSortedSet,我们要向其中添加N个元素(如字符串),哪个会更快一些?从文档上可以知道,HashSetAdd方法是O(1)的操作,而SortedSet内部是用了红黑树,它的Add方法是O(log(N))的操作(但它能顺序输出元素)。显然,从时间复杂度上来讲,SortedSet的性能要落后于HashSet,不过我们能否设计一个用例,让HashSet慢于SortedSet呢?

当然可以,例如以前那个由哈希碰撞引起的DoS安全漏洞,其实就是设计了一些Hash Code相同,但具体内容不同的字符串,让Dictionary(原理与HashSet相同)Add/Remove操作的时间复杂度从O(1)退化为O(N),这显然低于O(log(N))。不过如今的BCL中的实现已经对碰撞次数设置了阈值,超过这个阈值则会对哈希函数进行随机化,因此这种做法已经很难生效了。所以这里我们可以设计出另一个案例,且看代码:

static string[] GetRandomStrings(int number, int length) {var random = new Random(DateTime.Now.Millisecond);var array = new string[number];var buffer = new char[length];for (var i = 0; i < array.Length; i++) {for (var j = 0; j < buffer.Length; j++) {buffer[j] = (char)(random.Next(Char.MinValue, Char.MaxValue) + 1);}array[i] = new string(buffer);}Console.WriteLine("Generated");return array;
}static void CollectGarbage() {for (var i = 0; i < 10; i++) {GC.Collect();GC.WaitForPendingFinalizers();}
}static void AddToSetTime(string name, ISet<string> set, string[] array) {CollectGarbage();var watch = new Stopwatch();watch.Start();foreach (var s in array) {set.Add(s);}Console.WriteLine(name + ": " + watch.ElapsedMilliseconds);
}static void Main() {var array = GetRandomStrings(5000, 50000);AddToSetTime("HashSet", new HashSet<string>(), array);AddToSetTime("SortedSet", new SortedSet<string>(), array);
}

GetRandomStrings方法用于生成一系列的随机字符串,我们会将这些字符串使用AddToSetTime方法放入一个集合类中,并输出耗时。这段程序在我的机器上输出:

Generated
HashSet: 165
SortedSet: 17

您可以自己尝试一下,具体数值可能不同,但HashSet显著慢于SortedSet则基本是确定的。为什么会这样?O(1)为什么完败于O(log(N))?难道HashSet的常数就那么大么?其实原因很简单,我们只需想想HashSetSortedSet分别是怎么实现的就行。

  • HashSet:调用元素的GetHashCode方法获得Hash Code,算出该元素放在哪个Bucket中,然后顺着链表使用Equals方法依次比较Hash Code的相同元素。由于Hash Code散列程度较高,相同Bucket中重复元素极少,因此时间复杂度近似为O(1)。
  • SortedSet:使用CompareTo方法比较新元素与红黑树里的元素,以此决定元素的树中的“走向”,需要时再进行“平衡”操作。由于一颗平衡二叉树的高度为log(N),因此添加一个元素要进行大约log(N)次比较(以及最多log(N)次O(1)的平衡操作),其时间复杂度大约为O(log(N))。

看起来很正常嘛,但是其中还隐含一些“假设”,那就是GetHashCodeEquals还有CompareTo方法都是O(1)的操作,但事实真是如此吗?针对以上代码生成的随机字符串来说,EqualsCompareTo方法都可以几乎瞬间返回(比较第一个字符即可)。不过GetHashCode便麻烦一些了,便顺手从BCL的代码里摘抄出来吧:

namespace System {public sealed class String {public override int GetHashCode() {#if FEATURE_RANDOMIZED_STRING_HASHINGif (HashHelpers.s_UseRandomizedStringHashing) {return InternalMarvin32HashString(this, this.Length, 0);}
#endif // FEATURE_RANDOMIZED_STRING_HASHINGunsafe {fixed (char* src = this) {Contract.Assert(src[Length] == '\0', "src[this.Length] == '\\0'");Contract.Assert(((int)src) % 4 == 0, "Managed string should start at 4 bytes boundary");#if WIN32int hash1 = (5381 << 16) + 5381;
#elseint hash1 = 5381;
#endifint hash2 = hash1;#if WIN32// 32 bit machines. int* pint = (int*)src;int len = Length;while (len > 2) {hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1];pint += 2;len -= 4;}if (len > 0) {hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];}
#elseint c;char* s = src;while ((c = s[0]) != 0) {hash1 = ((hash1 << 5) + hash1) ^ c;c = s[1];if (c == 0)break;hash2 = ((hash2 << 5) + hash2) ^ c;s += 2;}
#endif#if DEBUG// We want to ensure we can change our hash function daily.// This is perfectly fine as long as you don't persist the // value from GetHashCode to disk or count on String A // hashing before string B.  Those are bugs in your code.hash1 ^= ThisAssembly.DailyBuildNumber;
#endifreturn hash1 + (hash2 * 1566083941);}}}}
}

从代码里可以看出,撇开最先的InternalMarvin32HashString这个不谈,其他分支下的哈希算法都是与字符串的长度呈线性关系。至于Marvin32这个神秘的哈希算法,我只知道可用于避免哈希碰撞攻击,但搜索了半天都找不到它的具体信息,只有一个“疑似”的简化实现。目前,我们还是用简单的测试来验证字符串长度与GetHashCode方法耗时的关系:

static void GetHashCodeTime(string name, string str, int iteration) {CollectGarbage();str.GetHashCode(); // warm upvar watch = new Stopwatch();watch.Start();for (var i = 0; i < iteration; i++) {str.GetHashCode();}Console.WriteLine(name + ": " + watch.ElapsedMilliseconds);
}static void Main() {var shortStr = new string('a', 100);var longStr = new string('a', 10000);var iteration = 1000000;GetHashCodeTime("Short", shortStr, iteration);GetHashCodeTime("Long", longStr, iteration);
}

我们创建了长度相差百倍的字符串,并比较其GetHashCode方法的耗时。我们可以开启随机化的字符串哈希算法(即使用Marvin32哈希算法),例如打开之后在我的机器上执行结果是:

Short: 114
Long: 9142

耗时与长度基本呈线性关系。关闭“随机化哈希”之后执行速度略有提高,但耗时与长度的关系依然不变,其实从代码上也已经能够看出这点。

再回到最早针对HashSetSortedSet的实验,由于我故意生成了长度高达5w的字符串,因此HashSet时间复杂度为O(1)又如何?单次GetHashCode方法调用的耗时,就已经远远超过许多次CompareTo方法了。从中我们也可以看出,假如我们用字符串作为字典的键,其效率会较int或是普通未重载过GetHashCodeEquals方法的类型为低。话说回来,其实用一个最普通的类作为字典的键效率很高,因为它的GetHashCode可以直接返回它的初始地址,而Equals方法则直接比较两个对象的引用。

在实践中,我遇到各种需要以字符串作为键的场景,我都会思考下有没有简单的替代方法,例如使用int做键,甚至直接使用数组。例如,前段时间@左耳朵耗子提到对一个csv文件里的数据进行排序,我们可以使用一个字典来保存一行数据,其中键为字段名:

List<Dictionary<string, string>> data = ...;
var ordered = data.OrderBy(row => row["column0"]) // 先以column0排序.ThenBy(row => row["column1"]); // 再以column1排序

但更有效率(内存使用也更紧凑)的做法是以一个数组来保存一行数据。我们先找出需要排序的列的下标,然后再从数组中找出排序用的字段值:

string[] columns = ...;
var index0 = columns.IndexOf("column0");
var index1 = columns.IndexOf("column1");List<string[]> data = ...;
var ordered = data.OrderBy(row => row[index0]).ThenBy(row => row[index1]);

从理论上讲,两种做法的时间复杂度一致,但实际上后者比前者会有不少提高。我们在了解“理论”的同时也需要注意实践上的细节,例如,其实在实践中O(log(N))其实也是个不比O(1)大多少的时间复杂度,此时可能也需要考虑下“常数”会对性能造成多大影响。

from: http://blog.zhaojie.me/2013/01/think-in-detail-why-its-o-1.html

真是O(1)吗?想清楚了没?相关推荐

  1. 在河北当中学老师用不用考计算机,想当教师没编制?两类教师不用考,直接进编制...

    原标题:想当教师没编制?两类教师不用考,直接进编制 近年来,很多人都会去考个教师资格证,等以后再考个教师编,教师资格证考试难度不大,两个月的时间好好复习不是问题,但是教师编制相对来说比较难考.没有编制 ...

  2. duet连win10,duetdisplay这个软件在win10上用不了?安装vs2015的时候想取消安装没有点取消...

    暑假的时候,电脑是win7,这个软件用的起来.前几天换了win10.然后重新下载了这个软件就用不起来了,求大神解答一下,谢谢. 嘿,谢谢你的文件.这个问题与我们的最新版本有关,并且与我们所经历的NVI ...

  3. 107+今日闲情:想吃我没那么容易.(16年分析解答)

    分析解释 今日闲情:想吃我没那么容易(免费分享-2宵)等待您的加入! 107期已解详请! 107期已解详请! →106淇 龙/鸡 开~鸡中 →105淇 蛇/马 开~马中 →104淇 鼠/龙 开~鼠中 ...

  4. 八零后高薪程序员感慨中年危机,月薪五万多,想要跳槽没地方!

    高薪也有高薪的烦恼,意味着跳槽的机会也变少了.就像金字塔的顶端一样,越往上走,机会也就越少了,这在程序员圈子比较普遍.月薪三万以下随便跳槽,能开得起这样薪资的公司很多,但如果薪资超过三万,机会就变得很 ...

  5. 没有什么能难倒伟大的电子工程师,办公室想点蚊香没打火机怎么办?安排!...

    作者:晓宇,排版:晓宇 微信公众号:芯片之家(ID:chiphome-dy) 1.电阻点火,办公室想点蚊香找不到打火机怎么办?这或许会难到其他人,但却难不倒聪明的电子工程师,由于使用的是不燃性电阻,短 ...

  6. 转。Nas配置。想找原版没找到,全是转载的,也没注出处,无语。

    随着家用宽带的不断提速和高清电影的普及外带单反的家庭占有率越来越搞,仅靠台式机里那几块硬盘越来越不够用了. 简单的计算了一下,家里的台式机上2T的容量(1T+640G+320G)已经接近于80%满,外 ...

  7. ajax请求怎么判断没有更多内容,怎么知道ajax 请求完了,想在数据没请求完时,页面有一个loading效果...

    参考下 var loading = (function(){ var $loading = $('body').append(' var show = function(){ $loading.fad ...

  8. 计算机与科学专硕考研院校排名,22考研|全国首次专硕院校评估排名,看看有你想报的没...

    随着专硕受认可程度的增加以及研究生考试招生录取人数的增多,报考专硕的同学也越来越多,一份官方的院校排名对要报考专硕的同学显得尤为重要.今年一月份,研招网公布了全国首次专业学位水平评估的结果,我们一起来 ...

  9. 还没开始学就想着接稿的事?想学好画画你需要这样做!

    最近有很多人问我,学习画画有什么秘诀?如何从一个绘画小白达到像我现在靠着副业收入比主业收入还要多的业余插画师,其实对于这些问题的回答,我主要想说的是,先不要紧盯着学好之后我能赚多少钱,而是我们应该考虑 ...

最新文章

  1. strust2自定义interceptor的基本方法及操作
  2. 报错解决:ninja: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by ninja)
  3. java视频流传输_java – 使用Xuggler流式传输视频
  4. 302状态码_HTTP协议详解(基础概念 方法 状态码 首部 连接 Cookie 新特性 安全)
  5. 信息学奥赛一本通(1088:分离整数的各个数)
  6. 托管元数据(2)——托管元数据和搜索中的精简面板
  7. 服务器2003系统黑屏怎么办,windows-server-2003 – Windows Server 2003 – 黑屏,光标在启动时...
  8. 判断某点在多边形内——方法二
  9. ModuleNotFoundError: No module named ‘sklearn‘ 解决办法
  10. 华为手机刷机后显示无服务器,华为手机刷机后,无法开机怎么办?
  11. R语言——相关系数图
  12. 安卓手机XPosed框架安装(详细版本)
  13. 澳洲PHP工作,怀爱伦澳洲行_在新西兰的工作
  14. BP反向传播算法原理及公式推导
  15. 文档服务器 件排名,全国服务器排名
  16. android 图片压缩总结1
  17. 一起捉妖 ios12.3更新了location不用了 怎么办
  18. IT行业男性出轨率最高!
  19. js写可以暂停的电子时钟
  20. js 根据百度地图提供经纬度计算两点距离

热门文章

  1. 求解LambdaMART的疑惑?
  2. font awesome java_Android使用Font Awesome显示小图标(一)
  3. java function获取参数_「Java容器」ArrayList源码,大厂面试必问
  4. 布道微服务_04服务的注册与发现
  5. JVM-白话聊一聊JVM类加载和双亲委派机制源码解析
  6. 白话Elasticsearch20-深度探秘搜索技术之使用rescoring机制优化近似匹配搜索的性能
  7. Spring Boot2.x-12 Spring Boot2.1.2中Filter和Interceptor 的使用
  8. 浅探C指针(一)--初识指针
  9. mysql 4 基础教程_MySQL基础教程(四):MySQL 管理
  10. mysql 使用不同引擎_mysql 不同引擎的比较