面试经常被问的问题之一,便是map和hash_map的区别,以及什么时候用map什么时候用hash_map。另外也了解到还有C++11的unordered_map,所以这里一并介绍三个了。用法就不介绍了,主要介绍区别。
1. 三者的区别
map底层是用红黑树实现的,空间复杂度为O(n),是随着节点的增加才增加,而查找的时间时间复杂度则固定是O(log(n))了。因为红黑树本来就是一种二叉搜索树,所以从begin()遍历到end()得到的key值是有序的,这也是map的一个特点。
hash_map是SGI实现,而unordered_map是C++11标准中新增的。它们都是用hash表实现的,使用开链法解决冲突问题。但是由于用的是hash表,其特性已经决定使用的空间基本到会比实际需要的空间大。保存的数据不是有序的,不能像map那样子通过遍历得到一个有序的结果。查找的时间复杂度可以达到O(1),基本上都是比map快的。在unordered_map中,链表节点用的是称为bucket(桶)的数据结构。
虽然大多数情况下hash table实现的map都会比红黑树实现的map查找快,但不是绝对的,因为冲突过多的话,可能耗费时间比map还要多。所以对于前者基本上都会有个rehash的操作,而由于rehash的处理不一样,也导致了hash_map和unordered_map的性能有一些差别,这是下后面会讲到的。
2. 使用的场景
使用场景基本上就是用各自的特性决定了。
map一般就是用在数据量小于1000或者对内存使用要求比较高的情况下。因为hash table需要申请的空间比较大,而红黑树则是新增一个节点就申请一个节点的空间。
如果数据量大的话并且需要频繁查找的话,就可以使用hash table实现的map了。这个时候内存已经不是主要的问题所在,而是查找时间了,这种情况下hash table的查找速度是常数而红黑树是O(log(n)),对于后者1024个数据最坏情况下要10次比较,在频繁查找的情况下这种时间耗费是很大的。
总结下来就是三个权衡点:查找速度、数据量、内存使用。
3. hash_map与unordered_map的性能区别
hash_map和unordered_map底层都是hash table实现的,所以特意搜索了下二者的区别,但是没有找到相关的介绍,基本上都只说两者都是hash table实现以及unordered_map性能比hash_map要好很多,并且unordered_map已经成为标准,不建议再使用hash_map。
为了了解为什么unordered_map性能比hash_map好,简单的扒了下两者实现的部分源码,发现两者rehash的处理是不一样的。
对于unordered_map,主要源码是在“/usr/include/c++/4.8.2/tr1/”目录下。使用的是unordered_map.h文件,而实际底层使用的hash table则是在hashtable.h文件中。通过查看hashtable.sh中insert函数的实现,可以看到每次插入前都会调用如下代码:
size_type __n_elt = __detail::__distance_fw(__first, __last);
std::pair<bool, std::size_t> __do_rehash
= _M_rehash_policy._M_need_rehash(_M_bucket_count,
_M_element_count, __n_elt);
if (__do_rehash.first)
_M_rehash(__do_rehash.second);
即unordered_map在插入前都会调用_M_rehash_policy._M_need_rehash都会判断是否需要rehash,如果需要则会调用_M_rehash进行操作。_M_rehash_policy是在同目录下的hashtable_policy.h中实现的,_M_need_rehash代码如下:
// __n_bkt is current bucket count, __n_elt is current element count,
// and __n_ins is number of elements to be inserted. Do we need to
// increase bucket count? If so, return make_pair(true, n), where n
// is the new bucket count. If not, return make_pair(false, 0).
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);
}
_M_max_load_factor是整个hash table的负载因子,默认为1,_M_growth_factor是bucket数的增长因子,默认为2,而__prime_list则是素数列表。从上面的代码可以看出每次insert时,先判断最后需要的数据大小是否超过了预设的最大值_M_next_resize,是则判断需要的bucket数是否大于当前的bucket数,如果是则需要rehash操作了。
rehash的时候先将当前的bucket数乘以负载因子得到新的bucket数,在与__min_bkts对比得到最大的bucket数更新到__min_bkts中。接下来,再从__prime_list中选出比__min_bkts大的最小素数(素数的hash效果比较好),更新_M_max_load_factor后将该素数返回。
从_M_need_rehash中返回的结果是需要rehash了,则调用_M_rehash并将传回的素数传给该函数:
template<typename _Key, typename _Value,
typename _Allocator, typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
bool __chc, bool __cit, bool __uk>
void
_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal,
_H1, _H2, _Hash, _RehashPolicy, __chc, __cit, __uk>::
_M_rehash(size_type __n)
{
_Node** __new_array = _M_allocate_buckets(__n);
__try
{
for (size_type __i = 0; __i < _M_bucket_count; ++__i)
while (_Node* __p = _M_buckets[__i])
{
std::size_t __new_index = this->_M_bucket_index(__p, __n);
_M_buckets[__i] = __p->_M_next;
__p->_M_next = __new_array[__new_index];
__new_array[__new_index] = __p;
}
_M_deallocate_buckets(_M_buckets, _M_bucket_count);
_M_bucket_count = __n;
_M_buckets = __new_array;
}
__catch(...)
{
...
}
}
该函数就是重新申请__n个成员的bucket数组,并将旧bucket数组的节点重新hash到新bucket数组上,比较容易理解。
那,hash_map中是怎么实现rehash的呢。这里我看的源码主要是在“/usr/local/include/boost/asio/detail/”目录下的hash_map.hpp。insert函数内容如下:
// Insert a new entry into the map.
std::pair<iterator, bool> insert(const value_type& v)
{
if (size_ + 1 >= num_buckets_)
rehash(hash_size(size_ + 1));
size_t bucket = calculate_hash_value(v.first) % num_buckets_;
iterator it = buckets_[bucket].first;
...
}
可以看到插入的时候只要判断当前节点大小大于bucket数目,就会进行rehash的操作,而rehash函数内容如下:
// Re-initialise the hash from the values already contained in the list.
void rehash(std::size_t num_buckets)
{
if (num_buckets == num_buckets_)
return;
num_buckets_ = num_buckets;
BOOST_ASIO_ASSERT(num_buckets_ != 0);
iterator end_iter = values_.end();
// Update number of buckets and initialise all buckets to empty.
bucket_type* tmp = new bucket_type[num_buckets_];
delete[] buckets_;
buckets_ = tmp;
for (std::size_t i = 0; i < num_buckets_; ++i)
buckets_[i].first = buckets_[i].last = end_iter;
...
}
这里hash_map的rehash简单粗暴,直接就是更新bucket数目为需要的数目就行了,而不会像unordered_map那样子进行数量的优化。
此外,我也看了下boost里面的unordered_map的实现,在“/usr/local/include/boost/unordered”目录下的unordered_map.hpp,代码里面的rehash操作也是像上面的unordered_map一样会将新bucket数目转换成一个素数,但是似乎在插入新元素的时候并没有判断是否需要rehash,看起来是需要手动rehash操作的。
通过上面的对比,可以看到unordered_map在rehash的操作上比hash_map要很好多,这应该也就是unordered_map效率由于hash_map的原因之一吧。有什么说错的,也希望大家可以指点下。
下面有一些链接可以参考: