遍历

hash表是一种比较简单和直观的数据结构,在查找时也有很好的性能。但是hash表不能提供有序遍历,这个是其特性决定,所以不足为奇。但是,更为实际的一个问题是如果遍历整个hash表中的所有元素?

直观上讲,可以遍历一个hash的所有桶(bucket),但是这样明显效率偏低,特别是如果hash表为了提高性能,桶的数量很多,整个结构的有效负载率不高,这种遍历方法就更加低效了。

STL的实现

stl中的hash表主要数据结构如下图所示,可以看到,实现遍历的关键是_M_before_begin指针,这个指针指指向桶的第一个元素(而不是最后插入的元素)。因为这个链表中同一个桶中的所有元素必须在链表内连续分布,在这种情况下,如果在下图中的第三个bucket中插入新元素,就没有办法知道bucket3子链表在整个链表中的起始位置(当然,这种说法并不严谨:也可以从_M_before_begin开始遍历并计算hash值,直到找到一个和当前hash不同的元素就知道当前链表的结尾了,不过这种效率看起来不太高)。



下面是库代码在桶头插入新元素的代码流程

///libstdc++-v3\include\bits\hashtable.h
template<typename _Key, typename _Value,
typename _Alloc, typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
typename _Traits>
void
_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
_H1, _H2, _Hash, _RehashPolicy, _Traits>::
_M_insert_bucket_begin(size_type __bkt, __node_type* __node)
{
if (_M_buckets[__bkt])
{
// Bucket is not empty, we just need to insert the new node
// after the bucket before begin.
__node->_M_nxt = _M_buckets[__bkt]->_M_nxt;
_M_buckets[__bkt]->_M_nxt = __node;
}
else
{
// The bucket is empty, the new node is inserted at the
// beginning of the singly-linked list and the bucket will
// contain _M_before_begin pointer.
__node->_M_nxt = _M_before_begin._M_nxt;
_M_before_begin._M_nxt = __node;
if (__node->_M_nxt)
// We must update former begin bucket that is pointing to
// _M_before_begin.
_M_buckets[_M_bucket_index(__node->_M_next())] = __node;
_M_buckets[__bkt] = &_M_before_begin;
}
}

特定键值的查找

由于链表没有边界,所以是通过计算节点键值的键值,进而根据键值获得bucket下标是否和当前查找节点的下标相同(_M_bucket_index(__p->_M_next()) != __n)来判断的。

  // Find the node whose key compares equal to k in the bucket n.
// Return nullptr if no node is found.
template<typename _Key, typename _Value,
typename _Alloc, typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
typename _Traits>
auto
_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
_H1, _H2, _Hash, _RehashPolicy, _Traits>::
_M_find_before_node(size_type __n, const key_type& __k,
__hash_code __code) const
-> __node_base*
{
__node_base* __prev_p = _M_buckets[__n];
if (!__prev_p)
return nullptr; for (__node_type* __p = static_cast<__node_type*>(__prev_p->_M_nxt);;
__p = __p->_M_next())
{
if (this->_M_equals(__k, __code, __p))
return __prev_p; if (!__p->_M_nxt || _M_bucket_index(__p->_M_next()) != __n)
break;
__prev_p = __p;
}
return nullptr;
}

迭代器

迭代器就是不断的通过_M_nxt访问链表,由于这个本质上是一个单向链表,所以它只有前向迭代(forward_iterator)而不是随机迭代(random_access_iterator)。这里列出了标准的C++迭代器。

      __node_type*
_M_begin() const
{ return static_cast<__node_type*>(_M_before_begin._M_nxt); } /**
* struct _Hash_node_base
*
* Nodes, used to wrap elements stored in the hash table. A policy
* template parameter of class template _Hashtable controls whether
* nodes also store a hash code. In some cases (e.g. strings) this
* may be a performance win.
*/
struct _Hash_node_base
{
_Hash_node_base* _M_nxt; _Hash_node_base() noexcept : _M_nxt() { } _Hash_node_base(_Hash_node_base* __next) noexcept : _M_nxt(__next) { }
}; /**
* struct _Hash_node_value_base
*
* Node type with the value to store.
*/
template<typename _Value>
struct _Hash_node_value_base : _Hash_node_base
{
typedef _Value value_type; __gnu_cxx::__aligned_buffer<_Value> _M_storage; _Value*
_M_valptr() noexcept
{ return _M_storage._M_ptr(); } const _Value*
_M_valptr() const noexcept
{ return _M_storage._M_ptr(); } _Value&
_M_v() noexcept
{ return *_M_valptr(); } const _Value&
_M_v() const noexcept
{ return *_M_valptr(); }
}

PhysX的实现

由于hash表是一个基础的数据结构,所以在很多的大中型软件中都有实现,下图是PhysX引擎PsHashInternals.h中HashBase类的实现。其中的mEntriesNext和mEntries数组大小相同,相当于为每个节点配置了一个next指针,但是这个next指针的意义根据它在不同的链表有不同的意义:如果在free链表则表示是free节点,如果是在某个hash桶中则表示某个hash值的节点。

迭代器

这遍历其实比较直观,关键是记录了上次遍历的桶(mBucket)和桶内部的位置(mEntry),在迭代器中要注意自动跨过(skip)桶的边界。

	PX_INLINE void skip()
{
while(mEntry == mBase.EOL)
{
if(++mBucket == mBase.mHashSize)
break;
mEntry = mBase.mHash[mBucket];
}
}

压缩(compact)模式

有意思的是它还提供了一个compact模式,这个模式会尽量保证对象池中只是用前面的部分,也就是所有使用部分在前,所有空闲部分在后。这种实现的一个明显有点是可以减少内存足迹。

但是,这个实现其实是通过执行析构/拷贝构造来移动之前已经存在对象的位置,从而保持在用空间的compact,对于存储了对象下标、或者构造/析构支持不是很完善的对象,这种方法并不适用。

	PX_INLINE void replaceWithLast(uint32_t index)
{
PX_PLACEMENT_NEW(mEntries + index, Entry)(mEntries[mEntriesCount]);
mEntries[mEntriesCount].~Entry();
mEntriesNext[index] = mEntriesNext[mEntriesCount]; uint32_t h = hash(GetKey()(mEntries[index]));
uint32_t* ptr;
for(ptr = mHash + h; *ptr != mEntriesCount; ptr = mEntriesNext + *ptr)
PX_ASSERT(*ptr != EOL);
*ptr = index;
}

总结

stl库通过将所有的使用中对象链接在同一个链表中,这样遍历所有对象的时候非常紧凑,是一种比较有特点的实现范式。

但是代价是在查找的时候不同通过一个特殊值表示所在bucket结束,也就是hash的计算可能会更频繁一些(在查找的时候)。

常见的hash数据结构的更多相关文章

  1. 【转】常见的hash算法及其原理

    Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值.这种转换是一种压缩映射,也就是 ...

  2. Redis之Hash数据结构

    0.前言 redis是KV型的内存数据库, 数据库存储的核心就是Hash表, 我们执行select命令选择一个存储的db之后, 所有的操作都是以hash表为基础的, 下面会分析下redis的hash数 ...

  3. Redis之hash数据结构实现

    参考 https://www.cnblogs.com/ourroad/p/4891648.html https://blog.csdn.net/hjkl950217/article/details/7 ...

  4. Redis hash数据结构

    1, 新增一个 hash 或者 新增数据 => hset key field value 2, 获取某个字段值 => hset key field 3, 获取所有字段值 => hge ...

  5. hash算法和常见的hash函数 [转]

       Hash,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值. 这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能 会散列成相同的输出,而不 ...

  6. leetcode常见算法与数据结构汇总

    leetcode刷题之后,很多问题老是记忆不深刻,因此特意开此帖: 一.对做过题目的总结: 二.对一些方法精妙未能领会透彻的代码汇总,进行时常学习: 三.总结面试笔试常见题目,并讨论最优解法及各种解法 ...

  7. Redis和nosql简介,api调用;Redis数据功能(String类型的数据处理);List数据结构(及Java调用处理);Hash数据结构;Set数据结构功能;sortedSet(有序集合)数

    1.Redis和nosql简介,api调用 14.1/ nosql介绍 NoSQL:一类新出现的数据库(not only sql),它的特点: 1.  不支持SQL语法 2.  存储结构跟传统关系型数 ...

  8. 常见算法和数据结构存在的坑(updating)

    数组: c++数组下标都+5会稳. 50005000的别开60006000. 二分: 实数二分可能因为神马精度问题出现了不满足二分序的情况,要小心. 注意二分完后,不能直接用当前数组里存的值,要pd( ...

  9. 常见hash算法的原理

    散列表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法.顾名思义,该数据结构可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在空隙. 散列表(Hash table,也叫 ...

  10. 常见hash原理

    原文出处:http://www.itmian4.com/forum.php?mod=viewthread&tid=4736&fromuid=1 散列表,它是基于快速存取的角度设计的,也 ...

随机推荐

  1. IT之软件公司组织架构

    总结一下软件企业的组织架构,软件公司大部分都很年轻,整个行业还在调整期,一般规模都在300人以内,现在国内大型的软件产品公司都不是靠软件起家的,国内软件三强:华为.中信.海尔都是从硬件甚至是家电做起的 ...

  2. postman连接mysql数据库

    转载:https://www.cnblogs.com/pengxiaojie/p/13495237.html 1.安装 2.启动服务 3.执行sql语句 安装: 想要postman连接mysql,需要 ...

  3. SpringBoot启动失败问题

    SpringBoot启动失败问题 一.现象 pom.xml没有显示报错,也能查看到各种需要的依赖jar包,但在启动时显示 二.原因 网络不好导致maven导包失败或者导包不完整 三.解决办法 进入项目 ...

  4. python中的上下文管理器以及python内建模块contextlib的contextmanager方法

    上下文管理器 上下文管理器是实现了上下文管理协议的对象,其特有的语法是"with -as".主要用于保存和恢复各种全局状态,关闭文件等,并为try-except-finally提供 ...

  5. Django基础(1)

    一.开发模式 MVC模式: model:数据库 view:前端展示 controller:逻辑控制 MTV模式(Django): model:数据库 view:逻辑控制 template:前端展示(模 ...

  6. python GIL解释器

    1.GIL是什么? GIL全称Global Interpreter Lock,即全局解释器锁. 作用就是,限制多线程同时执行,保证同一时间内只有一个线程在执行. GIL并不是Python的特性,它是在 ...

  7. cookie,session,token,drf-jwt

    1.cookie,session,token发展史 引入:我们都知道 http 协议本身是一种无状态的协议,一个普通的http请求简单分为三步:客户端发送请求request服务端收到请求并进行处理服务 ...

  8. 使用elemeng-plus控制台报错:$weight

    使用element-plus最开始按需使用,加入的组件少没有问题,但组件引入多了以后发现控制台会报下面的警告: Deprecation Warning: $weight: Passing a numb ...

  9. Linux系统下修改KVM虚拟机配置

    一. 安装虚拟机 1. 设备重启进入BIOS,打开SMMU.F10保存退出 2. 进入系统后安装线管组件 virt-install qemu-kvm qemu-img virt-manager lib ...

  10. C#中实现类型对foreach的支持

    代码实现: 首先创建用来遍历的类 class Car { public string name; public int age; } public class Cars: IEnumerable { ...