unordered_set 算法剖析

2015年3月12日 23:28

C转到C++时,被内部结构为红黑树的 set 的吸引,他的存在让我从手写二叉树的痛苦中解脱。

C++11 后我被 unordered_set  的执行效率深深的折服。于是写了这篇博客来解析下 unordered_set 的内部结构和实现细节。

本文使用 libstdc++-v3 的源代码进行分析,其他库或不同版本的库可能会略有不同。

unordered_set 内部结构

unordered_set 继承自 __unordered_set 继承自 _Hashtable_Hashtable 维护一个数组形式的哈希表(bucket),并且使用拉链法解决冲突。__unordered_set 构造函数中预设bucket的大小为10

接下来从hash值计算,hash表扩容与缩容,自定义类型使用 unordered_set 三个方面来剖析 unordered_set

hash 值的计算

key 哈希成一个 size_t 类型的变量,然后用它对 bucket 的大小取模算得hash值。

内建数据类型的 hash

unordered_set 为内建数据类型提供了默认的hash函数。那么这些hash函数是怎么实现的呢?

查阅libstdc++-v3源代码 /include/tr1/functional_hash.h 可知

std::hash 被定义成一个仿函数模版,其返回值为 size_t。

  1. 基本上所有整数数据类型的 hash 函数只是简单的把数值强转(static_cast)size_t 类型。这些类型包括:指针类型, `bool`, `char`, `unsigned char`, `wchar_t`, `short`, `int`, `long`, `long long`, `unsigned short`, `unsigned int`, `unsigned long`, `unsigned long long
  2. float, double, long double hash分别有对应的函数进行
    1. float: 如果值等于0.0f 则为0,否则使用下面函数计算hash
         template<>
          struct _Fnv_hash_base<4>
          {
            template<typename _Tp>
              static size_t
              hash(const _Tp* __ptr, size_t __clength)
              {
      	  size_t __result = static_cast<size_t>(2166136261UL);
      	  const char* __cptr = reinterpret_cast<const char*>(__ptr);
      	  for (; __clength; --__clength)
      	    {
      	      __result ^= static_cast<size_t>(*__cptr++);
      	      __result *= static_cast<size_t>(16777619UL);
      	    }
      	  return __result;
      	}
          };
    2. double: 如果值等于0.0 则为0,否则使用下面函数计算hash 
        template<>
          struct _Fnv_hash_base<8>
          {
            template<typename _Tp>
              static size_t
              hash(const _Tp* __ptr, size_t __clength)
              {
      	  size_t __result
      	    = static_cast<size_t>(14695981039346656037ULL);
      	  const char* __cptr = reinterpret_cast<const char*>(__ptr);
      	  for (; __clength; --__clength)
      	    {
      	      __result ^= static_cast<size_t>(*__cptr++);
      	      __result *= static_cast<size_t>(1099511628211ULL);
      	    }
      	  return __result;
      	}
          };
    3. long double: 这个类型使用的比较少,hash值计算也比较特殊,这里就不贴源代码了,如需查阅,请看 /src/hash-long-double-tr1-aux.cc
  3. string wstring 最终都是通过如下hash函数计算hash 
      template<size_t>
        struct _Fnv_hash_base
        {
          template<typename _Tp>
            static size_t
            hash(const _Tp* __ptr, size_t __clength)
            {
    	  size_t __result = 0;
    	  const char* __cptr = reinterpret_cast<const char*>(__ptr);
    	  for (; __clength; --__clength)
    	    __result = (__result * 131) + *__cptr++;
    	  return __result;
    	}
        };

用户自定义数据类型的hash函数

用户自定义类型设置 hash 有两种方法

1. 用户自定义类型重载函数类型转换接口`size_t operator()(_Tp __val) const;` 进行类型转换

2. 使用自定义仿函数进行哈希,形如 unordered_set<Point, point_hash_functional>  point_set;

hash 表扩容

查阅/include/tr1/unordered_set.h 可知unordered_set 使用__detail::_Prime_rehash_policy 进行扩容和缩容控制。_Prime_rehash_policy 包含一个 _M_max_load_factor(最大负载因子)和一个_M_growth_factor(扩容因子)。最大负载因子的默认值为1,扩容因子默认值为2.0

扩容

unordered_set 中添加k个元素时,如果插入这k歌元素后,负载因子大于最大负载因子时进行扩容。扩充后的容量为__prime_list(素数列表)中大于等于 max((当前大小+k)/负载因子), 当前大小 * _M_growth_factor) 的数。这个素数列表定义在 /src/shared/hashtable-aux.cc 中,一共有257个素数(sizeof(unsigned long)==4)

判断是否扩容的代码如下:

  inline std::pair<bool, std::size_t>
  _Prime_rehash_policy::
  _M_need_rehash(std::size_t __n_bkt, std::size_t __n_elt,
		 std::size_t __n_ins) const
  {
    if (__n_elt + __n_ins > _M_next_resize)
      {
	float __min_bkts = ((float(__n_ins) + float(__n_elt))
			    / _M_max_load_factor);
	if (__min_bkts > __n_bkt)
	  {
	    __min_bkts = std::max(__min_bkts, _M_growth_factor * __n_bkt);
	    const unsigned long* __p =
	      std::lower_bound(__prime_list, __prime_list + _S_n_primes,
			       __min_bkts);
	    _M_next_resize = static_cast<std::size_t>
	      (__builtin_ceil(*__p * _M_max_load_factor));
	    return std::make_pair(true, *__p);
	  }
	else 
	  {
	    _M_next_resize = static_cast<std::size_t>
	      (__builtin_ceil(__n_bkt * _M_max_load_factor));
	    return std::make_pair(false, 0);
	  }
      }
    else
      return std::make_pair(false, 0);
  }

素数表如下:

namespace __detail
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  extern const unsigned long __prime_list[] = // 256 + 1 or 256 + 48 + 1
  {
    2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,
    37ul, 41ul, 43ul, 47ul, 53ul, 59ul, 61ul, 67ul, 71ul, 73ul, 79ul,
    83ul, 89ul, 97ul, 103ul, 109ul, 113ul, 127ul, 137ul, 139ul, 149ul,
    157ul, 167ul, 179ul, 193ul, 199ul, 211ul, 227ul, 241ul, 257ul,
    277ul, 293ul, 313ul, 337ul, 359ul, 383ul, 409ul, 439ul, 467ul,
    503ul, 541ul, 577ul, 619ul, 661ul, 709ul, 761ul, 823ul, 887ul,
    953ul, 1031ul, 1109ul, 1193ul, 1289ul, 1381ul, 1493ul, 1613ul,
    1741ul, 1879ul, 2029ul, 2179ul, 2357ul, 2549ul, 2753ul, 2971ul,
    3209ul, 3469ul, 3739ul, 4027ul, 4349ul, 4703ul, 5087ul, 5503ul,
    5953ul, 6427ul, 6949ul, 7517ul, 8123ul, 8783ul, 9497ul, 10273ul,
    11113ul, 12011ul, 12983ul, 14033ul, 15173ul, 16411ul, 17749ul,
    19183ul, 20753ul, 22447ul, 24281ul, 26267ul, 28411ul, 30727ul,
    33223ul, 35933ul, 38873ul, 42043ul, 45481ul, 49201ul, 53201ul,
    57557ul, 62233ul, 67307ul, 72817ul, 78779ul, 85229ul, 92203ul,
    99733ul, 107897ul, 116731ul, 126271ul, 136607ul, 147793ul,
    159871ul, 172933ul, 187091ul, 202409ul, 218971ul, 236897ul,
    256279ul, 277261ul, 299951ul, 324503ul, 351061ul, 379787ul,
    410857ul, 444487ul, 480881ul, 520241ul, 562841ul, 608903ul,
    658753ul, 712697ul, 771049ul, 834181ul, 902483ul, 976369ul,
    1056323ul, 1142821ul, 1236397ul, 1337629ul, 1447153ul, 1565659ul,
    1693859ul, 1832561ul, 1982627ul, 2144977ul, 2320627ul, 2510653ul,
    2716249ul, 2938679ul, 3179303ul, 3439651ul, 3721303ul, 4026031ul,
    4355707ul, 4712381ul, 5098259ul, 5515729ul, 5967347ul, 6456007ul,
    6984629ul, 7556579ul, 8175383ul, 8844859ul, 9569143ul, 10352717ul,
    11200489ul, 12117689ul, 13109983ul, 14183539ul, 15345007ul,
    16601593ul, 17961079ul, 19431899ul, 21023161ul, 22744717ul,
    24607243ul, 26622317ul, 28802401ul, 31160981ul, 33712729ul,
    36473443ul, 39460231ul, 42691603ul, 46187573ul, 49969847ul,
    54061849ul, 58488943ul, 63278561ul, 68460391ul, 74066549ul,
    80131819ul, 86693767ul, 93793069ul, 101473717ul, 109783337ul,
    118773397ul, 128499677ul, 139022417ul, 150406843ul, 162723577ul,
    176048909ul, 190465427ul, 206062531ul, 222936881ul, 241193053ul,
    260944219ul, 282312799ul, 305431229ul, 330442829ul, 357502601ul,
    386778277ul, 418451333ul, 452718089ul, 489790921ul, 529899637ul,
    573292817ul, 620239453ul, 671030513ul, 725980837ul, 785430967ul,
    849749479ul, 919334987ul, 994618837ul, 1076067617ul, 1164186217ul,
    1259520799ul, 1362662261ul, 1474249943ul, 1594975441ul, 1725587117ul,
    1866894511ul, 2019773507ul, 2185171673ul, 2364114217ul, 2557710269ul,
    2767159799ul, 2993761039ul, 3238918481ul, 3504151727ul, 3791104843ul,
    4101556399ul, 4294967291ul,
    // Sentinel, so we don't have to test the result of lower_bound,
    // or, on 64-bit machines, rest of the table.
#if __SIZEOF_LONG__ != 8
    4294967291ul
#else
    6442450933ul, 8589934583ul, 12884901857ul, 17179869143ul,
    25769803693ul, 34359738337ul, 51539607367ul, 68719476731ul,
    103079215087ul, 137438953447ul, 206158430123ul, 274877906899ul,
    412316860387ul, 549755813881ul, 824633720731ul, 1099511627689ul,
    1649267441579ul, 2199023255531ul, 3298534883309ul, 4398046511093ul,
    6597069766607ul, 8796093022151ul, 13194139533241ul, 17592186044399ul,
    26388279066581ul, 35184372088777ul, 52776558133177ul, 70368744177643ul,
    105553116266399ul, 140737488355213ul, 211106232532861ul, 281474976710597ul,
    562949953421231ul, 1125899906842597ul, 2251799813685119ul,
    4503599627370449ul, 9007199254740881ul, 18014398509481951ul,
    36028797018963913ul, 72057594037927931ul, 144115188075855859ul,
    288230376151711717ul, 576460752303423433ul,
    1152921504606846883ul, 2305843009213693951ul,
    4611686018427387847ul, 9223372036854775783ul,
    18446744073709551557ul, 18446744073709551557ul
#endif
  };
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace __detail

缩容

unordered_set 可以自动扩容,但是无法自动缩容。用户可以使用  rehash(size_type n) 进行缩容,该函数将hash容量调整成在__prime_list 中,大于等于n并且使负载因子小于等于最大负载因子的数。当然该函数也可以用于扩容。

不管是扩容还是缩容,都需要对所有的数进行重新hash 所有扩容和缩容的时间复杂度至少是 O(n) 的。

 

自定义类型使用 unordered_set 

前面已经说过,自定义hash 函数的两种方法。除此之外,要使用 unordered_set 还需要自定义类型是可判断相等的,也就是重载了 operator == 或者是有全局的 operator==()运算。

技巧与注意事项

  1. 当自定义类型中有多个整数类型参与计算hash值时,可以通过位运算得到hash值,如 struct Point{ int x, y; } 可以用 hash = size_t(x) << (sizeof(size_t) * 4) | y;
  2. 当自定义类型中有一个或多个字符串类型,可以调用std::hash 函数计算单个字符串的hash值。并将所有部分的hash做异或操作设置为整体的hash值。如:struct City{ double lat, lon; string name; } 可以用 hash = std::hash<double>()(lon) ^ std::hash<double>()(lat) ^ std::hash<string>()(name);
  3. 对于 char * str类型,std::hash<char*>()(str) 只是更具指针进行hash,std::hash<string>()(str) 才是更具指向的内容hash

找工作总结、吐槽

2014年12月27日 03:31

接到G家的offer就准备写一点关于找工作的东西,不巧这几天有点事情拖到今天才动键盘。

仔细算算找工作尽然进行了半年之久,期间也是有喜有悲。笔过面过不少公司,就在这里吐槽吐槽。

works applications

wap 应该算是面的最早的一个公司,那个时候根本就还没有开始准备找工作,也没有参加任何的宣讲会。T同学听了他们家的宣讲会,拿回来了一份宣讲会发的纸质题目,“怂恿”我做做。一个算法题,一个数据结构的题目,做完了发到给定邮箱就有面试机会。不是很难,也就写了下。

6月10号去面的试,先是45分钟左右的时间做60左右个数学题、逻辑题,大概就是初高中的难度。然后就是1个小时左右的面试。和日本的面试官交流还是有点困难,问他能不能用STL,他尽然不晓得 STL 是啥(估计是我没有说清楚!)。不管三七二十一,直接上,然而悲剧的是尽然把next_permutation拼错了,调了半天。接下来一个题又把题目看错了,程序都快写完了才发现。于是果断悲剧了。面的好的直接发offer,我落了个“待定”,就是要参加他们的实习才能确定要不要。

他们家实习持续10天做一个项目。由于一些原因没请到假。其他人都是满打满算的10天。我就周末和检查点去一下,果断还是悲剧了的。不过日本公司还是很实在的,一天两百,就算你在那里待了半天甚至是一两个也都给。

阿里

wap 也就算是个前奏,真正找工作要从9月16日阿里的一面开始算。

先前有阿里合作的招聘人员联系过我,推荐过去参加实习生的面试。由于客观原因,我是不可能去实习的,当时被怂恿也抱着试一试,或许能为后面正式招聘省个流程的心态投了个算法工程师。连算法工程师是干什么的都不清楚,面试果断就扑街了。

正式招聘的时候就学乖了,报了个研发工程师。一面二面也还算顺利。

让我最想吐槽的就是后面的hr面。近1个小时的hr面让人非常的不爽,各种蛋疼的问题比相亲还麻烦。快结束的时候说了句话,大概的意思就是,看你的经历还是给你一个比较高的评价,但是通过这轮面试你的性格... 听说阿里的hr有很大的权利,果不其然最后勉强给了个offer,工资嘛,最低档。我这还算运气好的,周围不少人感觉面试问题答的挺好的,然后莫名其妙挂了。听说阿里前几站offer发多了,北京站就莫名其妙刷人。最后offer party的时候,hr还一副你爱来不来的姿势。这估计就是后面n多人拒阿里offer的原因。顺便说一句,阿里在国贸那边,超级远,去一趟来回路上两三个小时,过去一次真心不容易。这个也就算了,去了还要排队,一排就是一两个小时,你就不能安排好一点让人少等一会。这也就是算了,他面试还给你分开,一面去一次,二面再去一次,直接喷血了!

最后还给个招聘流程回访调查,果断给了个差评。

爱奇艺

爱奇艺也算是面的很早的公司,好像当时是走的推荐的形式,所有没有笔试。9月17号一口气面了3面,每一面持续大概45分钟到一个小时。面试比较看重基础知识,计算机专业基础课的问题问了不少,也有一些算法数据结构的题目。基础知识这一块答的不是很好,没时间复习,也只能靠本科时期微弱的记忆。算法数据结构还是凑合。

面完以后就没有消息了,给接待的hr打过几个电话都说她不负责通知结果,就是说帮你问问。

过了一个月,那个时候手头上都有满意的offer了,才收到短信说有offer,具体近期会有hr联系。然后又过了一两周才有电话通知offer,当场就拒了。

搜狐

搜狐好像是找T同学的学长内推的。9月17号面试,一口气面了三面。前面两个技术面感觉还可以。最后一面经理面,都开始介绍公司待遇了,他提了句,“年假嘛,7天,不过一般休不完”。我想都没多想,直接接了句“听说有些公司员工年假没有休完会影响经理的业绩?”。“我不会批的”经理严肃的答了句。卧槽,瞬间就觉得自己说错话了,立马补救到“道听途说”。面试就此结束。果不其然,人家也没有给我这种价值观不一致的人发offer。

百度

投简历的那段时间一心想着回南方,就投了个百度上海。笔试那也是people mountain people sea。不过也还算顺利。

面试回到了实习过的搜索框大楼,运气比较好,基本没有排队等候。值得一提的是,我一面的面试官还是很nice的。我觉得笔试试卷几个地方分都给低了,特别是有个题目有 N^2 的简单算法,我用了 N^logN的方法,15分的题目就得了5分。向一面面试官反映,主要是担心分数会影响后续的面试以及待遇的评估。他拿着我的试卷找了下另外一个人,应该是管事的hr,答复是不好改。面试官还是很nice的和我说,“我把这个情况给你写到面试记录里面去。”。

二面三面也基本上是连着,主要考察一些编程的基础知识。三面面试官还旁敲侧击的问你对加班的态度,对团队里面影响团队进度人的态度之类的问题。

最后也算比较顺利的拿到了百度的offer。hr通知的时候没有说待遇,只是问我心里的价位,那时候手上有阿里和网易的offer,也就说少于25w才考虑。对方要求提供其他公司的两方,才能申请sp。个人觉得这种事情不好,交涉了几次,没提供,后面也就不了了之。

腾讯

今年腾讯笔试也是有很大的bug,听说很多童孩由于填报的职位+城市不招人了,或者是直接没有这个职位,而没有获得面试的机会。好像后面补救的给了机会,具体就不清楚了。

腾讯的笔试嘛,也是分两部分,听说也是前面不达标就不看后面的。前面60个选择题,一堆基础知识,由于没有时间复习基础知识,果断扑街了。

雅虎北京全球研发中心

雅虎北研面试,简单说开端不错,最终坑货。

先是一轮笔试。感觉题目质量还是蛮高的。依然是前面基础题答的不好。后面中规中矩的大题答的还可以。一个数组除了一个数出现1次以外其他数都出现3次,找出出现1次的那个数。这个题目想了很久,后面没时间了,用了个 均摊复杂度为 O(n) 类似快速排序划分的算法解。后面回来想想,还是发现有更好更简洁的方法。

一面二面在一起,等了很久,一面面试官都没有给我问问题的机会就结束了,等到二面的时候,6点来钟的样子,很多面试官都已经下班了。

二面面试官是一个很nice的人,看模样气势,是个leader。先写一个小程序,然后问如何做测试。由于之前都没有测试的经历,面试官也是很有耐心的各种提示。最后在他的指引下,还是顺利的把题目做出来了。

三面是9月28日,面完出来稍微等了下,hr直接和我说面试结果还不错,有offer(是直接说有offer)还需要到美国审核一下。当时都满怀欣喜以为这个offer是肯定有的。然而等呀等,等呀等,等了一个多月都没有消息。听说雅虎的每一份offer都要经过ceo审核,这是梅姐的规定。我只想说,不好好把雅虎的业绩整整,真是闲的蛋疼。要不是阿里上市,雅虎的财报还能看吗?

等着等着有了网易的offer就没怎么关注他们家了。也不记得什么时候开始,陆陆续续的有人收到offer。大概到了12月份,打那边hr电话问了下,大概的意思就是把我当备胎了。也就呵呵了!

FreeWheel

之前也没有听说过FreeWheel。一次和本科W同学聊天他说那里还不错,也不知道他从哪里得到一个他也不认识的内部人员的qq。直接加了qq,求人内推。

FreeWheel 的面试流程和大部分公司一样,一场笔试,三场面试。

笔试的题目个人认为还是出的比较好的, 偏向Linux 系统,多线程,广告服务器方面的知识,应该说和他们公司做的事情紧密相连。笔试做的还可以,后面面试的时候看到成绩 71分,应该算是比较高的。

一二面依然是技术面,感觉他们要求特别严谨。

void fun(string str) {
   ...
   for (int i = 0; i < str.size(); ++i) {
      ...
   }
   ... 
}

类似上面的代码,面试官让我再check一下。恰好那段时间再做系统的去 warning,立即反应过来 i 应该是 size_t 类型的。

三面面试官是 VP ,应该是个台湾人,感觉聊的挺好的。甚至他有问那天后面一点面试的同学“你认不认识XXX”。恰巧那个同学认识我,她比我晚点面,不过我们没有遇到。

奇怪的是,等陆续听到一些人收到offer了,我依然还没有消息。大概又过了一周左右,我给那边的hr打电话问情况。反馈是他们正在哈尔滨(可能是其他城市,记不大清了)宣讲,我的情况还需要综合考虑,大概还需要一两周的时间。然后又过了一周不到的时间,那边hr打电话给了个中等的offer。不打算去,当场也就拒了。

IBM

前期广投,也不晓得什么时候投的IBM。进行了一次电面,后面人家要求做英语能力测试。考虑到当时已经有比较好的offer了。加上IBM待遇和发展前景都比较有限,就直接和hr说不参加后续的面试了

Facebook

我真正缓过神来开始找工作的时候,fb早就结束了。恰巧是西雅图那边没招满,又有T同学室友内推才挣到一次电面机会。然而因为时区原因,没有啥好的面试时间,只能一大早面试。当时状态也不是很好,还要用我颇足的英语给面试官介绍实验室的项目,基本等于直接扑街了。

由于受前面的影响,后面coding题,第一个看错题半天才反应过来,第二个题目就没有时间完成了。总的来说fb的题目很中规中矩,不难只是英语能力不足,与之无缘。

网易游戏

网易游戏是找师兄内推的,据说他们家刷简历刷的比较厉害。所有找人内推下还是很有必要的。

网易游戏的笔试是参加过笔试里面最难的一个。题目分为两部分,前面一部分是基础题,后一部分是大题。基础题什么都考,有简单的记忆题目,也有基本上没人能答出来的变态题。基础题只要过线,然后有没有面试机会就看后面的大题。大题一共是五个还是六个,主要是偏向数据结构算法,也有和游戏设计有关的内容。

我的面试是两面,一面感觉有点像压力面,一堆基础小题目一个接一个。最后来个个算法题目。

二面主要就是问项目,问系统设计的题目。我非常的喜欢二面的面试官也是给我签offer的人,很冷静,思维很敏捷,思路很清晰,话说也很有条理。感觉很像jony ive。

人人

人人是找帮主内推的,求内推联系的时候才知道他半年前就从人人离职了。

人人面试的头一天傍晚睡了会儿,导致晚上睡不着,加上室友打呼噜,直接折腾到4点才入睡。面试的时候各种犯困没状态。其中一面让我手写 kmp,混沌了很久写了个,也不晓得是不是对的。更甚的是,有个题目迷迷糊糊感觉想清楚了又没有想清楚,脑子一团糟。说说想想,自己的想法和面试官的正解不一样,解释了半天把面试官也说晕了,最后面试官说了句,嗯,感觉应该是对的。其实我也不知道是不是对的,后面也不记得当时自己说了啥。

人人最后能拿到一个不错的offer,全是托了帮主的福。每个面试官都会说一句,“哦,XX推荐过来的”。最后自己也是在人人、猿题库还有网易游戏中纠结了很久,后面没有打算在北京定居,也就没有接人人的offer。

猿题库

猿题库是因为M同学在那边实习,对那边比较了解,给内推的。没有笔试,直接过去面试。听前面的人说,三面,每一面在纸上写3个题。我的是一面手写3个算法题;二面手写2个题,聊了点项目;三面就写了1个题,然后聊点项目,聊点公司的业务以及一些自己的想法。每一面45分钟,时间控制的也还可以。面试的整体体验也是不错的。

面试结束后一周左右的时间收到的offer(一个恰到好处的时间,短了显得公司不够矜持,哈哈;长了显得办事拖沓)。

后面有和郭总做了些交流,感觉郭总本人以及题库都很靠谱。当时在人人、网易游戏和题库三个纠结的时候我个人最倾向题库,甚至都打算和F同学一起去题库奋斗了。然而父亲不希望我离家太远(杭州离我们家只有一个半小时的高铁),如果要选择北京,也希望我拿一个户口稳定一些。加上自己不是很喜欢北京的环境、气候。最后也只能选择放弃。

Hulu 

对hulu最简单的总结就是面试比较难。

10月16号hulu的电话面,那天很不巧,中午拉肚子状态不好。中途网络还不好,在线文档挂了,只能电话里说,总体感觉不好,要跪的节奏。

大概过了半个月,hr打电话过来,意思是还有面试的机会,还愿不愿意参加。于是果断让安排。

第一次过去是连续的三面技术面,其中两面还可以,有一面和面试官想的不一致,我把问题想复杂了,更注重细节的处理,而他更倾向于把问题简单处理。果然,那个面试官给了我一个负面的评价,导致在终面之间又加面了一面。

终面是google第二轮的前一天。面试官问到 gg/fb的情况,如实回答说如果能拿到google的offer,优先考虑去google(这种事情没太大必要说谎)。面试官(应该是vp或者经理)留了个联系方式,让我面完google联系他。

前段时间hr联系我,给了个相当有竞争力的offer。由于有google了,也就打算拒了。

Google 

最后来讲讲 Google 吧。

D 牛很早就让我把简历给他,帮我内推。阴差阳错搞的比较晚,10月20号才第一轮面试。第一轮两面,一面尚可,二面英语面,感觉比较糟。面完的时候都感觉要跪了。幸运的是gg给了个机会。第二轮被安排到11月中旬,这三面还是比较顺利。每一面严格的45分钟,少的也能做出一个题目,多的可以到两个。Google的面试很注重面试者的思维过程,以及代码质量。又过了一个来月正式接到gg的offer。做梦也没有想到,我一个英语这么渣渣的人竟然准备去美帝板砖。一份运气一份准备吧!

Tips

经历了不少的笔试面试,总结点经验给点小小 Tips:

基础知识

基础知识很重要,网络、数据库、操作系统、c/c++、设计模式都要复习到位,这些知识点往往会出现在面试环节,笔试就更是了。你说笔试都过不了谈何offer。我就是吃了这个亏,没时间复习腾讯笔试直接扑街,过了的笔试基础部分答的也不是很好

  • 网络:网络的协议层,常用的协议分别属于哪个层,常用的端口,TCP/UDP的一些细节要了解下
  • 数据库:数据库的一些基本概念,一些简单的sql还是要复习下
  • 操作系统:内存管理,进线程,文件系统可以了解点
  • c/c++:这个就比较广了,虚函数是基础,特别的智能指针要搞清楚,c++11多了解点没有坏处。推荐 effictive c++,不仅做工程用得到,面试也很有可能涉及
  • 设计模式:这个嘛一般不会问的很偏的设计模式,不过如何写好一个单例是必须的
  • 其他:暂时没有想到,多多益善

数据结构算法

这个是笔面试的重点需要长期的积累

  • 推荐一个大牛的博客 http://blog.csdn.net/v_july_v/article/details/6543438
  • 书籍方面:《编程之美》已经是很早之前的事情了。面试之前主要看了《Cracking the Coding Interview 5th Edition》我有英文电子版,有需要的 email 我
  • 代码能力: leetcode 是必做的,一般最少做两遍,一遍自己做,用电脑写,做完了可以看看别人精妙的代码。第二遍用纸或者文本编辑器写(推荐用纸),大部分面试要求在纸上写代码,google可以在googledoc上。平时也可以参加些在线的 OJ 比赛,比如 codeforces、topcoder 之类的
  • 系统设计:系统设计的题目也要做适当的练习,上面推荐大牛的blog中右详细学习资料

 

搜狗校招顺序随机整数编码

2014年8月24日 11:42

要准备找工作了,这几天在看july整理的面试题目,很全很强大。其中有个搜狗校招的笔试题目,觉得很有意思。思考了下,感觉相比给出的方法还有更好的方法。

题目

N个整数(数的大小为0-255)的序列,把它们加密为K个整数(数的大小为0-255).再将K个整数顺序随机打乱,使得可以从这乱序的K个整数中解码出原序列。设计加密解密算法,且要求K<=15*N. 如果是:

1. N<=16,要求K<=16*N.
2. N<=16,要求K<=10*N.
3. N<=64,要求K<=15*N

第1、2问

因为N条件相同,所以第1问和第2问其实是同一个问题,解决了第2问也就搞定了第1问。

简单方法

原理

应题目要求,加密后的K个整数打乱顺序后还要能恢复原文,那么就需要将原文整数的位置信息也编码进密文。

一种简单的方法就是把密文的8位数分成3段

第1部分编码位置,N<=16的情况需要4位来表示
第2部分编码整数的第几位,000表示第0位,111表示第7位
第3部分编码某一位上的数值,如:0110 101 1表示,第6个数的第5位为1

原文整数范围[0-255]有8位,因此编码1个原文数字需要8个密文整数,即 K<=8N。已经可以满足第1、2问的要求了。

栗子

假设原文第6个整数为77,二进制表示为 01001101,那么可以编码成

0110 111 0
0110 110 1
0110 101 0
0110 100 0
0110 011 1
0110 010 1
0110 001 0
0110 000 1

好一点的方法

原理

这里我想到了一种更好一点的方法,可以使 K<=4N

首先,我们将原文整数写成4进制:x = a3 * 64 + a2 * 16 + a1 * 4 + a0(也就是将原文整数分为4段,每段2位),记系数为ai。由于i属于{0, 1, 2, 3}并且ai也属于{0, 1, 2, 3},因此我们考虑用2位编码i,2位编码ai。于是密文的数字就被分为了这么三段

第1部分依然编码位置
第2部分编码i
第3部分编码ai

这样只需要4个密文整数就可以编码一个原文整数了,即 K<=4N

栗子

假设原文第6个数字为77,二进制表示为 01001101,那么可以编码成

0110 11 01
0110 10 00
0110 01 11
0110 00 01

第三问

好一点的方法

原理

由于N<=64这样就必须用6位来编码位置,剩下只有2位来编码数值了。

继承第1、2问好一点的方法,我们用剩下的2位编码i,用出现的次数编码 ai。也就是ai等于多少对应的密文整数就出现多少次。由于i有4个,ai<=3,故每一个原文整数最多需要4*3=12个密文整数来编码,也就是 K<=12N

栗子

假设原文第6个数字为77,二进制表示为 01001101,那么可以编码成

000110 11
000110 01
000110 01
000110 01
000110 00

更好一点的方法

原理

假设每1个原文整数最多能使用 n 个密文整数来编码。

对于1个密文数字依然需要6位来编码位置,剩下2位来编码数值信息。2位对应4个状态(00, 01, 10, 11)。n 个密文整数分给4个状态,状态出现的次数可以位0,并且n可以不用完。相当于5个状态分n个整数,最后一个状态表示没有用完的整数。则一共有C(n+4, 4)种组合方式。

我们用1种组合方式编码1个数值,[0-255]共有256个数值。那么要求满足 C(n+4, 4) >= 256。求的当n=7时,C(n+4,4)=330 > 256。故这种编码方式可以使得 K<=7N。状态对应的表格在这里下载

栗子

假设原文第6个数字为77,二进制表示为 01001101,那么可以编码成

000110 10
000110 10
000110 01
000110 01
000110 00
000110 00

使用Selenium自动玩2048

2014年8月20日 01:24

前几天看summer师弟玩Selenium感觉挺有意思。便拿着时间风靡一时的游戏“2048”来练手,写了个简单的 AI。甚是欢乐!

Selenium

啥是selenimu,简单的说——Selenium也是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE、Mozilla Firefox、Mozilla Suite等(来自 http://www.51testing.com/zhuanti/selenium.html)。

selenimu这里就不多介绍了,细节请看 selenium 的 python api 文档

2048策略

2048这个游戏地球人都知道就不介绍了。主要介绍下我的AI。我的AI对于每一个方向的评估有三个方面

  1. 移动导致合并的得分 score:这个就是游戏本身定义的得分,如 4和4合并得8分,128和128合并的256分
  2. 移动后每一行每一列的单调性 monotone:对于每一行(每一列)如果 line[i] <= line[i+1]则mon+= line[i]+line[i+1]否则,mon-=line[i]+line[i+1],monotone=sum(abs(mon))
  3. 移动后相邻块值相同的情况 adjoin:任意两个相邻块的值相同,如cells[i][j]=cells[i+1][j],则 adjoin+=cells[i][j]

最后的估值 estimation = score + monotone * 0.3 + adjoin。对于上下左右四个方向取estimation最大的方向操作

这种方法还可以,运气好的话,可以得到2048

程序结构

程序的源代码如下:


from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import os
import time

size = 4

class Estimator:
    def estimate(self, precells, postcells, action, score):
        for i in range(size):
            score += self.__estimate_line([postcells[i][j] for j in range(size)])
            score += self.__estimate_line([postcells[j][i] for j in range(size)])
        return score

    def __estimate_line(self, line):
        monotone, adjoin = 0, 0
        for i in range(size - 1):
            if line[i + 1] > line[i]:
                monotone += line[i + 1] + line[i]
            else:
                monotone -= line[i + 1] + line[i]
            if line[i + 1] == line[i]:
                adjoin += line[i]
        return abs(monotone) * .3 + adjoin

class Auto2048:
    def __init__(self, url, estimator):
        self.browser = webdriver.Firefox()
        self.browser.get(url)
        self.estimator = estimator

    def get_cells(self):
        tiles = self.browser.find_elements_by_class_name('tile')
        self.cells = [[0 for i in range(4)] for i in range(4)]

        for tile in tiles:
            attr = tile.get_attribute('class').split()
            value = int(attr[1].split('-')[1])
            x = int(attr[2].split('-')[3]) - 1
            y = int(attr[2].split('-')[2]) - 1
            self.cells[x][y] = value

    def AI(self):

        self.get_cells()
        self.Print(self.cells)

        action, actionname = '', ''
        moveable = False
        
        strategies = [    {'fun': self.try_up, 'action': Keys.UP, 'name': 'Up'}, 
                        {'fun': self.try_down, 'action': Keys.DOWN, 'name': 'Down'}, 
                        {'fun': self.try_left, 'action': Keys.LEFT, 'name': 'Left'}, 
                        {'fun': self.try_right, 'action': Keys.RIGHT, 'name': 'Right'}]
        for strategy in strategies:
            result = strategy['fun']()
            estimation = self.estimator.estimate(self.cells, result['cells'], strategy['name'], result['score'])
            if result['moveable'] and (moveable == False or max_estimation < estimation):
                action = strategy['action']
                max_estimation = estimation
                moveable = True
                actionname = strategy['name']

        if not moveable:
            return False

        self.browser.find_element_by_class_name('grid-container').send_keys(action)
        print 'Action: ', actionname
        return True

    def move_left(self, cells):
        moveable = False
        score = 0
        for x in range(size):
            pre = 0
            for y in range(size):
                if cells[x][y]:
                    cells[x][pre] = cells[x][y]
                    if y != pre:
                        moveable = True
                        cells[x][y] = 0
                    pre += 1
            for y in range(size - 1):
                if cells[x][y] and cells[x][y] == cells[x][y + 1]:
                    cells[x][y] += cells[x][y]
                    score += cells[x][y]
                    cells[x][y + 1] = 0
                    moveable = True
            pre = 0
            for y in range(size):
                if cells[x][y]:
                    cells[x][pre] = cells[x][y]
                    if y != pre:
                        moveable = True
                        cells[x][y] = 0
                    pre += 1
        return {'moveable': moveable, 'score': score, 'cells': cells}
    
    def try_left(self):
        cells = [[self.cells[i][j] for j in range(size)] for i in range(size)]
        return self.move_left(cells)

    def try_right(self):
        cells = [[self.cells[i][size - 1 - j] for j in range(size)] for i in range(size)]
        result = self.move_left(cells)
        result['cells'] = [[result['cells'][i][size - 1 - j] for j in range(size)] for i in range(size)]
        return result

    def try_up(self):
        cells = [[self.cells[j][i] for j in range(size)] for i in range(size)]
        result = self.move_left(cells)
        result['cells'] = [[result['cells'][j][i] for j in range(size)] for i in range(size)]
        return result
    
    def try_down(self):
        cells = [[self.cells[size - 1 - j][i] for j in range(size)] for i in range(size)]
        result = self.move_left(cells)
        result['cells'] = [[result['cells'][j][size - 1 - i] for j in range(size)] for i in range(size)]
        return result

    def __del__(self):
        self.browser.close()

    def Print(self, cells):
        print 
        for x in range(size):
            for y in range(size):
                print '%5d' % cells[x][y], 
            print 

if __name__ == '__main__':
    url = 'file://' + os.path.abspath('2048/index.html')
    # url = "http://gabrielecirulli.github.io/2048/"
    auto2048 = Auto2048(url, Estimator())
    while auto2048.AI():
        time.sleep(0.2)
    time.sleep(10)

源代码中有两个类

主逻辑类 Auto2048

使用 2048的url和估值类(AI逻辑)构造。测试过程中,用 wget 将 "http://gabrielecirulli.github.io/2048/" 的所有页面抓到本地分析(由于不懂js在网页中的工作原理,使用find_elements_by_class_name找当前cells的信息找了好久)

每一次操作调一次AI(),AI() 先获取当前页面的状态保存在 self.cells 中,然后对上下左右四个方向枚举,取估值最大的方向并使用send_keys进行操作。如果能操作则AI()返回True,否则返回False

估值类 Estimator

估值类只要实现 def estimate(self, precells, postcells, action, score): 估值方法即可。其中,precells为操作前状态,postcells为操作后状态,atcion为操作['Left', 'Right', 'Up', 'Down'],score为操作得分。返回值为估值estimation,值越到越好。

虽然我的估值方法运气好的话可以得到2048,但还是很粗糙的。你要有兴趣的话,可以写一个更好的Estimator得到更高的分。

源代码地址 https://github.com/xhSong/auto2048

the solutions of Leetcode and CtCI

2014年8月19日 22:15

很久以前,膜拜了下 Cracking the Coding Interview,用 c++ 写了部分代码,发个传送门 https://github.com/xhSong/CtCI

很久的很久以前,刷了下 leetcode,继续发个传送门 https://github.com/xhSong/leetcode